Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 29 additions & 18 deletions app/(my)/my-study/page.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,52 @@
import Image from 'next/image';
import { redirect } from 'next/navigation';
import { getStudyDashboard } from '@/entities/user/api/get-user-profile';
import { getLoginUserId } from '@/shared/lib/get-login-user';
import MyStudyCard from '@/widgets/my-study/my-study-card';

export default function MyStudy() {
export default async function MyStudy() {
const memberId = await getLoginUserId();

if (!memberId) {
redirect('/login');
}

//const dashboard = await getStudyDashboard(memberId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 주석처리한 이유가 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 부분은 아직 마이스터디 api 자체가 오류가 나는 바람에 주석처리 해 두었습니다!
백엔드 측에서 예외처리를 완료하면 해당 주석을 풀고 진행할 예정입니다


return (
<div className="flex flex-col gap-[32px]">
<div className="flex flex-col gap-[16px]">
<div className="flex items-center gap-[var(--spacing-75)]">
<div className="flex flex-col gap-400">
<div className="flex flex-col gap-200">
<div className="flex items-center gap-75">
<Image src="icons/file_icon.svg" alt="file" width={40} height={40} />
<div className="text-[20px] leading-9 font-[var(--font-weight-bold)]">
내 활동
</div>
<div className="font-designer-20b text-text-default">내 활동</div>
</div>

<div className="flex gap-[var(--spacing-200)]">
<MyStudyCard title="총 참여" value="173" />
<div className="flex gap-200">
<MyStudyCard title="총 참여" value="데이터 없음" />
<MyStudyCard title="연속 참여" value="8" unit="주" />
<MyStudyCard title="완료" value="23" unit="회" />
</div>

<div className="flex gap-[var(--spacing-200)]">
<div className="flex gap-200">
<MyStudyCard title="최대 연속 참여" value="8" unit="주" />
<MyStudyCard title="실패" value="23" unit="회" />
</div>
</div>

<div className="flex flex-col gap-[16px]">
<div className="flex items-center gap-[var(--spacing-75)]">
<div className="flex flex-col gap-200">
<div className="flex items-center gap-75">
<Image src="icons/graph_icon.svg" alt="file" width={32} height={24} />
<div className="text-[20px] leading-9 font-[var(--font-weight-bold)]">
성장 지표
</div>
<div className="font-designer-20b text-text-default">성장 지표</div>
</div>

<div className="flex gap-[var(--spacing-200)]">
<MyStudyCard title="총 참여" value="173" />
<div className="flex gap-200">
<MyStudyCard
title="스터디 완료율"
value="173"
changeIndicator={{ type: 'increase', value: 8.2 }}
/>
<MyStudyCard title="등급" value="S" unit="등급" />
<MyStudyCard title="완료" value="23회" />
<MyStudyCard title="최대 연속 참여" value="8" unit="주" />
</div>
</div>
</div>
Expand Down
76 changes: 42 additions & 34 deletions src/features/my-page/ui/profile-edit-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Button from '@/shared/ui/button';
import { Modal } from '@/shared/ui/modal';
import { FormField } from '../../../shared/ui/form/form-field';
import { UpdateUserProfileRequest } from '../api/types';
import { updateUserProfile } from '../api/update-user-profile';
import { DEFAULT_OPTIONS, MBTI_OPTIONS } from '../consts/my-page-const';

interface Props {
Expand Down Expand Up @@ -51,14 +52,12 @@ export default function ProfileEditModal({
UpdateUserProfileRequest['interests']
>(memberProfile.interests?.map((item) => item.name) ?? []);

const [profileImageExtension, setProfileImageExtension] =
useState<UpdateUserProfileRequest['profileImageExtension']>(undefined);

const [image, setImage] = useState(
memberProfile.profileImage?.resizedImages?.[0]?.resizedImageUrl ??
'/profile-default.svg',
);

const [isOpen, setIsOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const uploadProfileImage = useUploadProfileImageMutation();
const queryClient = useQueryClient();
Expand All @@ -67,14 +66,6 @@ export default function ProfileEditModal({
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
setImage(URL.createObjectURL(file));

const ext = file.name.split('.').pop()?.toUpperCase();
if (ext && ['JPG', 'PNG', 'GIF', 'WEBP'].includes(ext)) {
setProfileImageExtension(ext as 'JPG' | 'PNG' | 'GIF' | 'WEBP');
} else {
alert('지원하지 않는 이미지 형식입니다.');
setProfileImageExtension(undefined);
}
}
};

Expand All @@ -85,7 +76,12 @@ export default function ProfileEditModal({
return;
}

const isDefaultImage = image === '/profile-default.svg';
const file = fileInputRef.current?.files?.[0];

const hasImageFile = !!file;
const ext = file?.name.split('.').pop()?.toUpperCase();
const profileImageExtension =
ext && ['JPG', 'PNG', 'GIF', 'WEBP'].includes(ext) ? ext : undefined;

const rawFormData: UpdateUserProfileRequest = {
name,
Expand All @@ -95,37 +91,44 @@ export default function ProfileEditModal({
simpleIntroduction: simpleIntroduction.trim() || undefined,
mbti: mbti.trim() || undefined,
interests: interests.length > 0 ? interests : undefined,
profileImageExtension: isDefaultImage ? undefined : profileImageExtension,
profileImageExtension: hasImageFile ? profileImageExtension : undefined,
};

const formData = Object.fromEntries(
Object.entries(rawFormData).filter(([_, v]) => v !== undefined),
) as UpdateUserProfileRequest;

onSubmit(formData);
const updated = await updateUserProfile(memberId, formData);

if (fileInputRef.current?.files?.[0]) {
if (fileInputRef.current?.files?.[0] && updated.profileImageUploadUrl) {
const imageFormData = new FormData();
imageFormData.append('file', fileInputRef.current.files[0]);

const filename = updated.profileImageUploadUrl.split('/').pop();
if (!filename) return;

try {
await uploadProfileImage.mutateAsync({
memberId: memberId,
filename: `profile-formdata-${memberId}`,
memberId,
filename,
file: imageFormData,
});

await queryClient.invalidateQueries({ queryKey: ['memberInfo'] });
} catch (error) {
console.error('이미지 업로드 실패:', error);
alert('이미지 업로드에 실패했습니다.');
}
}
await queryClient.invalidateQueries({ queryKey: ['memberInfo'] });

onSubmit(formData);
};

return (
<Modal.Root>
<Modal.Trigger className="rounded-100 bg-fill-brand-default-default font-designer-16b text-text-inverse w-full px-150 py-100">
<Modal.Root open={isOpen} onOpenChange={setIsOpen}>
<Modal.Trigger
onClick={() => setIsOpen(true)}
className="rounded-100 bg-fill-brand-default-default font-designer-16b text-text-inverse w-full px-150 py-100"
>
내 프로필 수정
</Modal.Trigger>
<Modal.Portal>
Expand Down Expand Up @@ -207,19 +210,24 @@ export default function ProfileEditModal({
</div>
</Modal.Body>
<Modal.Footer>
<Modal.Close asChild>
<div className="flex w-full justify-center gap-[8px]">
<Button color="secondary" className="w-[140px] cursor-pointer">
취소
</Button>
<Button
className="w-[140px] cursor-pointer"
onClick={handleSubmit}
>
수정 완료
</Button>
</div>
</Modal.Close>
<div className="flex w-full justify-center gap-[8px]">
<Button
color="secondary"
className="w-[140px] cursor-pointer"
onClick={() => setIsOpen(false)}
>
취소
</Button>
<Button
className="w-[140px] cursor-pointer"
onClick={async () => {
await handleSubmit(); // 여기서 이미지 업로드 포함
setIsOpen(false); // 모달 닫기
}}
>
수정 완료
</Button>
</div>
</Modal.Footer>
</Modal.Content>
</Modal.Portal>
Expand Down
15 changes: 13 additions & 2 deletions src/features/study/api/get-study-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
MonthlyCalendarResponse,
PostDailyRetrospectRequest,
PostStudyDailyRequest,
WeeklyParticipationResponse,
} from '@/features/study/api/types';
import { axiosInstance } from '@/shared/tanstack-query/axios';

Expand Down Expand Up @@ -47,7 +48,7 @@ export const postStudyDaily = async (body: PostStudyDailyRequest) => {
return res.data;
};

export async function postJoinStudy(payload: JoinStudyRequest) {
export const postJoinStudy = async (payload: JoinStudyRequest) => {
const cleanPayload = Object.fromEntries(
Object.entries(payload).filter(
([_, value]) =>
Expand All @@ -60,4 +61,14 @@ export async function postJoinStudy(payload: JoinStudyRequest) {
const res = await axiosInstance.post('/matching/apply', cleanPayload);

return res.data;
}
};

export const getWeeklyParticipation = async (
params: GetDailyStudyDetailParams,
): Promise<WeeklyParticipationResponse> => {
const res = await axiosInstance.get('/study/week/participation', {
params,
});

return res.data.content;
};
5 changes: 5 additions & 0 deletions src/features/study/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,8 @@ export interface JoinStudyRequest {
githubLink?: string;
blogOrSnsLink?: string;
}

export interface WeeklyParticipationResponse {
memberId: number;
isParticipate: boolean;
}
17 changes: 13 additions & 4 deletions src/features/study/model/use-study-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getDailyStudies,
getDailyStudyDetail,
getMonthlyStudyCalendar,
getWeeklyParticipation,
postJoinStudy,
} from '@/features/study/api/get-study-data';
import {
Expand All @@ -13,15 +14,23 @@ import {
MonthlyCalendarResponse,
} from '../api/types';

export const useDailyStudyDetailQuery = (params: GetDailyStudyDetailParams) => {
// 나중에 변수 추가 시 tanstack-query를 이용해 받을 예정입니다.
const hasParticipated = false;
export const useWeeklyParticipation = (params: GetDailyStudyDetailParams) => {
return useQuery({
queryKey: ['weeklyParticipation', params],
queryFn: () => getWeeklyParticipation(params),
staleTime: 60 * 1000,
});
};

export const useDailyStudyDetailQuery = (
params: GetDailyStudyDetailParams,
enabled: boolean = true,
) => {
return useQuery({
queryKey: ['dailyStudyDetail', params],
queryFn: () => getDailyStudyDetail(params),
staleTime: 60 * 1000,
enabled: hasParticipated && !!params,
enabled: enabled && !!params,
});
};

Expand Down
14 changes: 11 additions & 3 deletions src/features/study/ui/study-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { useState } from 'react';
import DateSelector from './data-selector';
import TodayStudyCard from './today-study-card';
import StudyListSection from '../../../widgets/home/study-list-table';
import { useDailyStudyDetailQuery } from '../model/use-study-query';
import {
useDailyStudyDetailQuery,
useWeeklyParticipation,
} from '../model/use-study-query';

const FRIDAY = 5;
const FRIDAY_OFFSET = 4;
Expand Down Expand Up @@ -76,8 +79,11 @@ export default function StudyCard() {
month: selectedDate.getMonth() + 1,
day: selectedDate.getDate(),
};
const { data, refetch } = useDailyStudyDetailQuery(params);

const { data: participationData } = useWeeklyParticipation(params);
const isParticipate = participationData?.isParticipate ?? false;

const { data, refetch } = useDailyStudyDetailQuery(params, isParticipate);
const { month, week } = getWeekly(selectedDate);

return (
Expand All @@ -87,7 +93,9 @@ export default function StudyCard() {
<DateSelector value={selectedDate} onChange={setSelectedDate} />
</div>
<div className="border-border-default rounded-200 flex flex-col gap-500 border p-400">
{data && <TodayStudyCard data={data} refetch={refetch} />}
{isParticipate && data && (
<TodayStudyCard data={data} refetch={refetch} />
)}
<StudyListSection date={selectedDate} />
</div>
</>
Expand Down
18 changes: 13 additions & 5 deletions src/shared/ui/form/multi-item-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import { BaseInput } from '../input';

interface Props {
value: string[];
maxSelectable?: number;
onChange: (updated: string[]) => void;
options?: string[];
}

export default function SelectableTagsInput({
value,
onChange,
maxSelectable = 4,
options = [],
}: Props) {
const [customInput, setCustomInput] = useState('');
Expand Down Expand Up @@ -90,20 +92,26 @@ export default function SelectableTagsInput({
name="custom"
type="text"
placeholder={
value.length >= 4
? '최대 4개까지 선택 가능합니다'
value.length >= maxSelectable
? `최대 ${maxSelectable}개까지 선택 가능합니다`
: 'IT, Back-end, AI'
}
color={value.length >= 4 ? 'error' : 'default'}
disabled={value.length >= 4}
color={value.length >= maxSelectable ? 'error' : 'default'}
disabled={value.length >= maxSelectable}
value={customInput}
onChange={(e) => setCustomInput(e.target.value)}
/>
<Button type="submit" disabled={value.length >= 4}>
<Button type="submit" disabled={value.length >= maxSelectable}>
추가
</Button>
</form>
)}

{value.length >= maxSelectable && (
<p className="text-text-brand font-designer-13r mt-50">
최대 {maxSelectable}개까지 선택 가능합니다.
</p>
)}
</div>
);
}
Loading
Loading