Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a9fafe7
style: 면접 준비하기, 완료하기 모달 스타일 변경
Mimiminz Jun 30, 2025
d85647e
fix: 오늘의 스터디 상세 param name 변경
Mimiminz Jun 30, 2025
1dd6b13
refactor: const 선언 상수 파일 분리
Mimiminz Jul 1, 2025
1a980de
fix: api 호출부 이동
Mimiminz Jul 1, 2025
f6a7a3e
fix: 선택한 값 중복 선택 방지 추가
Mimiminz Jul 1, 2025
bc5b17d
refactor: 예외를 렌더링 컴포넌트에서 처리로 변경
Mimiminz Jul 1, 2025
8bdca21
style: 띄어쓰기 변경
Mimiminz Jul 1, 2025
d768dd4
style: 다중 선택 UI 변경
Mimiminz Jul 1, 2025
01d557f
feat: 스크롤 숨기기 utility 추가
Mimiminz Jul 1, 2025
4d130d9
feat: 유저 프로필 모달 추가
Mimiminz Jul 3, 2025
43d6cdc
feat: 프로필 클릭시 모달
Mimiminz Jul 3, 2025
1c4cb00
fix: 스터디 미 참여 시 임시 값으로 처리
Mimiminz Jul 3, 2025
dc077e3
style: UI 수정
Mimiminz Jul 3, 2025
54d887b
Merge pull request #80 from code-zero-to-one/QNRR-348-박경도-조민주-메인화면에서-…
Mimiminz Jul 4, 2025
af61439
fix : 프로필이미지가 존재하지 않을경우 null 값에 대한 옵셔널체이닝 처리
Jul 4, 2025
15e653d
feat: 404 페이지 설정
aken-you Jul 4, 2025
1e98a99
feat: 참여 여부에 따라 오늘의 스터디 UI 렌더
Mimiminz Jul 7, 2025
2be2d9c
style: 리스트 빈 값 처리
Mimiminz Jul 7, 2025
5e66084
fix: 이미지 업데이트 수정
Mimiminz Jul 7, 2025
b1f342d
style: error 메세지 추가
Mimiminz Jul 7, 2025
ab03b95
Merge pull request #81 from code-zero-to-one/QNRR-359-유수아-404-페이지
aken-you Jul 8, 2025
2a112c6
Merge pull request #82 from code-zero-to-one/QNRR-293-조민주-박경도-프로필-조회-…
Mimiminz Jul 9, 2025
c571c45
feat: 스터디 신청 유무에 따라 UI 변경
Mimiminz Jul 9, 2025
36888d8
feat: 마이스터디 api 추가
Mimiminz Jul 9, 2025
75229c4
Merge branch 'develop' of https://github.com/code-zero-to-one/study-p…
Mimiminz Jul 9, 2025
1f4f9f6
Merge pull request #84 from code-zero-to-one/QNRR-293-조민주-박경도-프로필-조회-…
Mimiminz Jul 10, 2025
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);

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
9 changes: 9 additions & 0 deletions app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,15 @@ https://velog.io/@oneook/tailwindcss-4.0-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%E
letter-spacing: 0;
}

@layer utilities {
.scrollbar-hide {
&::-webkit-scrollbar {
display: none;
}
scrollbar-width: none;
}
}

/* shadcn 색상 테마 */

:root {
Expand Down
15 changes: 15 additions & 0 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Link from 'next/link';
import Button from '@/shared/ui/button';

export default function NotFound() {
return (
<div className="flex h-[calc(100vh-45px)] w-full flex-col items-center justify-center gap-5">
<img src="/images/404.png" width={256} height={221} />
<Link href="/">
<Button size="large" type="button" color="secondary">
홈으로 이동
</Button>
</Link>
</div>
);
}
Binary file added public/images/404.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions src/entities/user/api/get-user-profile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
GetUserProfileResponse,
PatchAutoMatchingParams,
StudyDashboardResponse,
} from '@/entities/user/api/types';
import { axiosInstance } from '@/shared/tanstack-query/axios';

Expand All @@ -20,3 +21,12 @@ export const patchAutoMatching = async ({
params: { 'auto-matching': autoMatching },
});
};

export const getStudyDashboard = async (
memberId: number,
): Promise<StudyDashboardResponse> => {
const res = await axiosInstance.get(`/study/dashboard/${memberId}`);
console.log('errrrpr' + res.data.content);

return res.data.content;
};
14 changes: 14 additions & 0 deletions src/entities/user/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface MemberProfile {
export interface GetUserProfileResponse {
memberId: number;
autoMatching: boolean;
studyApplied: boolean;
memberInfo: MemberInfo;
memberProfile: MemberProfile;
}
Expand All @@ -83,3 +84,16 @@ export interface PatchAutoMatchingParams {
memberId: number;
autoMatching: boolean;
}

export interface StudyDashboardResponse {
tier: string;
experience: number;
monthAttendance: number;
weekAttendance: number;
techStack: Record<string, number>;
mainStackCount: {
FE: number;
BE: number;
CS: number;
};
}
10 changes: 10 additions & 0 deletions src/entities/user/model/use-user-profile-query.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sendGTMEvent } from '@next/third-parties/google';
import { useMutation, useQuery } from '@tanstack/react-query';
import {
getStudyDashboard,
getUserProfile,
patchAutoMatching,
} from '@/entities/user/api/get-user-profile';
Expand Down Expand Up @@ -39,3 +40,12 @@ export const usePatchAutoMatchingMutation = () => {
},
});
};

export const useStudyDashboardQuery = (memberId: number) => {
return useQuery({
queryKey: ['studyDashboard', memberId],
queryFn: () => getStudyDashboard(memberId),
enabled: !!memberId,
staleTime: 60 * 1000,
});
};
31 changes: 31 additions & 0 deletions src/features/my-page/consts/my-page-const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export const DEFAULT_OPTIONS = [
{ label: '운동/헬스', value: '운동/헬스' },
{ label: '여행', value: '여행' },
{ label: '음악', value: '음악' },
{ label: '영화/드라마', value: '영화/드라마' },
{ label: '독서', value: '독서' },
{ label: '게임', value: '게임' },
{ label: '요리/맛집 탐방', value: '요리/맛집 탐방' },
{ label: '패션/뷰티', value: '패션/뷰티' },
{ label: '사진/영상', value: '사진/영상' },
{ label: '자기계발', value: '자기계발' },
];

export const MBTI_OPTIONS = [
{ label: 'ISTJ', value: 'ISTJ' },
{ label: 'ISFJ', value: 'ISFJ' },
{ label: 'INFJ', value: 'INFJ' },
{ label: 'INTJ', value: 'INTJ' },
{ label: 'ISTP', value: 'ISTP' },
{ label: 'ISFP', value: 'ISFP' },
{ label: 'INFP', value: 'INFP' },
{ label: 'INTP', value: 'INTP' },
{ label: 'ESTP', value: 'ESTP' },
{ label: 'ESFP', value: 'ESFP' },
{ label: 'ENFP', value: 'ENFP' },
{ label: 'ENTP', value: 'ENTP' },
{ label: 'ESTJ', value: 'ESTJ' },
{ label: 'ESFJ', value: 'ESFJ' },
{ label: 'ENFJ', value: 'ENFJ' },
{ label: 'ENTJ', value: 'ENTJ' },
];
109 changes: 43 additions & 66 deletions src/features/my-page/ui/profile-edit-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,15 @@ 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 {
onSubmit: (formData: UpdateUserProfileRequest) => void;
memberProfile: MemberProfile;
memberId: number;
}

const DEFAULT_OPTIONS = [
{ label: '운동/헬스', value: '운동/헬스' },
{ label: '여행', value: '여행' },
{ label: '음악', value: '음악' },
{ label: '영화/드라마', value: '영화/드라마' },
{ label: '독서', value: '독서' },
{ label: '게임', value: '게임' },
{ label: '요리/맛집 탐방', value: '요리/맛집 탐방' },
{ label: '패션/뷰티', value: '패션/뷰티' },
{ label: '사진/영상', value: '사진/영상' },
{ label: '자기계발', value: '자기계발' },
];

export const MBTI_OPTIONS = [
{ label: 'ISTJ', value: 'ISTJ' },
{ label: 'ISFJ', value: 'ISFJ' },
{ label: 'INFJ', value: 'INFJ' },
{ label: 'INTJ', value: 'INTJ' },
{ label: 'ISTP', value: 'ISTP' },
{ label: 'ISFP', value: 'ISFP' },
{ label: 'INFP', value: 'INFP' },
{ label: 'INTP', value: 'INTP' },
{ label: 'ESTP', value: 'ESTP' },
{ label: 'ESFP', value: 'ESFP' },
{ label: 'ENFP', value: 'ENFP' },
{ label: 'ENTP', value: 'ENTP' },
{ label: 'ESTJ', value: 'ESTJ' },
{ label: 'ESFJ', value: 'ESFJ' },
{ label: 'ENFJ', value: 'ENFJ' },
{ label: 'ENTJ', value: 'ENTJ' },
];

export type MbtiValue = (typeof MBTI_OPTIONS)[number]['value'];

export default function ProfileEditModal({
Expand Down Expand Up @@ -82,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 @@ -98,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 @@ -116,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 @@ -126,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 @@ -238,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
2 changes: 1 addition & 1 deletion src/features/my-page/ui/profile-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function ProfileInfo({
<div className="border-border-subtle flex flex-col items-start gap-[40px] border-t pt-200">
<div className="flex w-full flex-col gap-200">
<div className="flex w-full items-center gap-150">
<div className="font-designer-20b">내정보</div>
<div className="font-designer-20b">내 정보</div>
<ProfileInfoEditModal
memberInfo={memberInfo}
onSubmit={handleSubmit}
Expand Down
Loading