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
84fa909
feat: FormField props에 placeholder 추가
aken-you Jul 24, 2025
b1a754e
refactor: 연락처 validation 수정
aken-you Jul 24, 2025
fb5bde7
refactor: github input placeholder 설정
aken-you Jul 24, 2025
bf01261
fix : 도커파일에 next.config.ts 추가
Jul 26, 2025
37ebacf
fix : 테스트용 img 태그 제거 후 next Image 컴포넌트만 남김
Jul 28, 2025
614e92a
feat: 토글 비활성화 스타일 추가
aken-you Jul 28, 2025
c522e2f
refactor: 스터디 신청하지 않을 경우, 매칭 토글 비활성화
aken-you Jul 28, 2025
81b1d5f
Merge pull request #104 from code-zero-to-one/QNRR-327-박경도-조성진-회원가입-직…
aken-you Jul 28, 2025
f4c3723
Merge pull request #106 from code-zero-to-one/QNRR-415-유수아-스터디-신청하지-않…
aken-you Jul 28, 2025
317ef99
Merge pull request #100 from code-zero-to-one/QNRR-396-유수아-프로필-수정-모달-…
aken-you Jul 28, 2025
75c4768
feat: 설정 아이콘을 클릭하면, "프로필" 페이지로 이동
aken-you Jul 28, 2025
f5b06df
Merge pull request #107 from code-zero-to-one/QNRR-415-유수아-스터디-신청하지-않…
aken-you Jul 28, 2025
6335cfd
refactor: 스터디 신청 폼을 한 상태에서 관리
aken-you Jul 28, 2025
643686c
feat: 오늘의 스터디 카드에서 면접관/면접자 프로필 클릭 시 유저 프로필 모달 열림 기능 추가
yeonhwan Jul 29, 2025
ddab246
fix: today-study-card 컴포넌트, user-avatar 컴포넌트, modal 컴포넌트 리팩토
yeonhwan Jul 29, 2025
f8a3e61
chore: 개발관련 더미 텍스트 및 옵셔널체이닝 제거
yeonhwan Jul 29, 2025
9bff1c1
refactor: 스터디 신청 폼 로직을 컴포넌트로 분리
aken-you Jul 29, 2025
eee7bfb
Merge pull request #108 from code-zero-to-one/연환-fix-modal-opens-for-…
yeonhwan Jul 29, 2025
8464c3e
feat: update-user-profile birthDate filed 추가
yeonhwan Jul 30, 2025
bcde698
feat: 스터디 신청 폼 에러 처리
aken-you Jul 30, 2025
205aa09
chore: update-user-profile 사용하지 않는 코드 제거
yeonhwan Jul 30, 2025
784e1d1
fix: 파일 위치 이동
Mimiminz Jul 30, 2025
6f1dfe4
feat: 마이스터디 api 연결
Mimiminz Jul 30, 2025
3e825a2
Merge pull request #109 from code-zero-to-one/연환-fix-add-birthday-to-…
yeonhwan Jul 30, 2025
a817424
Merge pull request #110 from code-zero-to-one/QNRR-418-유수아-스터디-신청-모달-…
aken-you Jul 31, 2025
ce2ddc7
Merge pull request #111 from code-zero-to-one/QNRR-357-조민주-마이스터디-ui-및…
Mimiminz Aug 1, 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
1 change: 1 addition & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/yarn.lock ./yarn.lock
COPY --from=builder /app/next.config.ts ./next.config.ts

# 런타임시점에도 .env 파일 복사
COPY --from=builder /app/.env .env
Expand Down
1 change: 1 addition & 0 deletions Dockerfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/yarn.lock ./yarn.lock
COPY --from=builder /app/next.config.ts ./next.config.ts

# 런타임시점에도 .env 파일 복사
COPY --from=builder /app/.env .env
Expand Down
56 changes: 38 additions & 18 deletions app/(my)/my-study/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
'use client';

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 { useStudyDashboardQuery } from '@/features/my-page/model/use-update-user-profile-mutation';
import MyStudyCard from '@/widgets/my-study/my-study-card';

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

if (!memberId) {
redirect('/login');
}
export default function MyStudy() {
const { data: dashboard, isLoading, isError } = useStudyDashboardQuery();

//const dashboard = await getStudyDashboard(memberId);
if (isLoading) return <div>로딩 중...</div>;
if (isError || !dashboard) return <div>에러가 발생했습니다.</div>;

return (
<div className="flex flex-col gap-400">
Expand All @@ -22,14 +19,33 @@ export default async function MyStudy() {
</div>

<div className="flex gap-200">
<MyStudyCard title="총 참여" value="데이터 없음" />
<MyStudyCard title="연속 참여" value="8" unit="주" />
<MyStudyCard title="완료" value="23" unit="회" />
<MyStudyCard
title="총 참여"
value={dashboard.studyActivity.totalParticipationDays}
/>
<MyStudyCard
title="연속 참여"
value={dashboard.studyActivity.sequenceParticipationWeeks}
unit="주"
/>
<MyStudyCard
title="완료"
value={dashboard.studyActivity.completedStudyCount}
unit="회"
/>
</div>

<div className="flex gap-200">
<MyStudyCard title="최대 연속 참여" value="8" unit="주" />
<MyStudyCard title="실패" value="23" unit="회" />
<MyStudyCard
title="최대 연속 참여"
value={dashboard.studyActivity.maxSequenceParticipationWeeks}
unit="주"
/>
<MyStudyCard
title="실패"
value={dashboard.studyActivity.failureStudyCount}
unit="회"
/>
</div>
</div>

Expand All @@ -42,11 +58,15 @@ export default async function MyStudy() {
<div className="flex gap-200">
<MyStudyCard
title="스터디 완료율"
value="173"
changeIndicator={{ type: 'increase', value: 8.2 }}
value={dashboard.growthMetric.studyCompleteness}
unit="%"
/>
<MyStudyCard title="등급" value="S" unit="등급" />
<MyStudyCard title="최대 연속 참여" value="8" unit="주" />
<MyStudyCard
title="최대 연속 참여"
value={dashboard.studyActivity.maxSequenceParticipationWeeks}
unit="주"
/>
</div>
</div>
</div>
Expand Down
10 changes: 0 additions & 10 deletions src/entities/user/api/get-user-profile.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
GetUserProfileResponse,
PatchAutoMatchingParams,
StudyDashboardResponse,
} from '@/entities/user/api/types';
import { axiosInstance } from '@/shared/tanstack-query/axios';

Expand All @@ -21,12 +20,3 @@ 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;
};
13 changes: 0 additions & 13 deletions src/entities/user/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,3 @@ 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: 0 additions & 10 deletions src/entities/user/model/use-user-profile-query.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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 @@ -40,12 +39,3 @@ export const usePatchAutoMatchingMutation = () => {
},
});
};

export const useStudyDashboardQuery = (memberId: number) => {
return useQuery({
queryKey: ['studyDashboard', memberId],
queryFn: () => getStudyDashboard(memberId),
enabled: !!memberId,
staleTime: 60 * 1000,
});
};
1 change: 0 additions & 1 deletion src/features/auth/ui/sign-up-image-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export default function SignupImageSelector({
<div className="relative">
<div className="relative h-[112px] w-[112px] overflow-hidden rounded-full">
<Image src={image} alt="프로필" fill className="object-cover" />
<img src={image} alt="next최적화안쓴프로필" className="object-covoer" />
</div>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
Expand Down
18 changes: 18 additions & 0 deletions src/features/my-page/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface UpdateUserProfileRequest {
name: string;
tel: string;
birthDate?: string;
githubLink?: string;
blogOrSnsLink?: string;
simpleIntroduction?: string;
Expand Down Expand Up @@ -62,3 +63,20 @@ export interface TechStackResponse {
parentId: number | undefined;
level: number;
}

export interface StudyActivity {
totalParticipationDays: number;
sequenceParticipationWeeks: number;
maxSequenceParticipationWeeks: number;
completedStudyCount: number;
failureStudyCount: number;
}

export interface GrowthMetric {
studyCompleteness: number;
}

export interface StudyDashboardResponse {
studyActivity: StudyActivity;
growthMetric: GrowthMetric;
}
7 changes: 7 additions & 0 deletions src/features/my-page/api/update-user-profile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
AvailableStudyTimeResponse,
StudyDashboardResponse,
StudySubjectResponse,
TechStackResponse,
UpdateUserProfileInfoRequest,
Expand Down Expand Up @@ -49,3 +50,9 @@ export const getTechStacks = async (): Promise<TechStackResponse[]> => {

return res.data.content;
};

export const getStudyDashboard = async (): Promise<StudyDashboardResponse> => {
const res = await axiosInstance.get('/study/dashboard');

return res.data.content;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '../api/types';
import {
getAvailableStudyTimes,
getStudyDashboard,
getStudySubjects,
getTechStacks,
updateUserProfile,
Expand Down Expand Up @@ -59,3 +60,11 @@ export const useTechStacksQuery = () => {
queryFn: getTechStacks,
});
};

export const useStudyDashboardQuery = () => {
return useQuery({
queryKey: ['studyDashboard'],
queryFn: () => getStudyDashboard(),
staleTime: 60 * 1000,
});
};
66 changes: 57 additions & 9 deletions src/features/my-page/ui/profile-edit-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ interface Props {

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

const formValidations = {
regex: {
// 이름 유효성 검사: 2~10자, 한글 또는 영문만 허용
username: /^[가-힣a-zA-Z]{2,10}$/,
// 연락처 유효성 검사: "(2~3자리 지역번호)-(3~4자리 번호)-(4자리 번호)" 형식
telephone: /^\d{2,3}-\d{3,4}-\d{4}$/,
// 생년월일 텍스트 패턴 유효성 검사 : ^(4자리 년도).(2자리 월).(2자리 일)
birthDate: /^\d{4}.([0][1-9]|[1][0-2]).([0][1-9]|[1-2][0-9]|[3][0-1])/,
},
checker: {
// year 는 최소 1900년 이상 현재 년도 이하이어야 한다.
// month 와 date 의 유효성은 생성자로 검사됨
birthDate: (value: string) => {
const birthDate = new Date(value);
const now = new Date();
if (birthDate.toString() === 'Invalid Date') return false;
const year = birthDate.getFullYear();
if (year < 1900 || year > now.getFullYear()) return false;

return true;
},
},
};

export default function ProfileEditModal({ memberProfile, memberId }: Props) {
const [isOpen, setIsOpen] = useState(false);

Expand Down Expand Up @@ -66,6 +90,7 @@ function ProfileEditForm({
const [profileForm, setProfileForm] = useState<UpdateUserProfileRequest>({
name: memberProfile.memberName ?? '',
tel: memberProfile.tel ?? '',
birthDate: memberProfile.birthDate ?? '',
githubLink: memberProfile.githubLink?.url ?? '',
blogOrSnsLink: memberProfile.blogOrSnsLink?.url ?? '',
mbti: memberProfile.mbti ?? '',
Expand All @@ -78,12 +103,14 @@ function ProfileEditForm({
DEFAULT_PROFILE_IMAGE_URL,
);

// 이름 유효성 검사: 2~10자, 한글 또는 영문만 허용
const isNameValid = /^[가-힣a-zA-Z]{2,10}$/.test(profileForm.name);
// 연락처 유효성 검사: "(2~3자리 지역번호)-(3~4자리 번호)-(4자리 번호)" 형식
const isTelValid =
profileForm.tel.length === 0 ||
/^\d{2,3}-\d{3,4}-\d{4}$/.test(profileForm.tel);
const isNameValid = formValidations.regex.username.test(profileForm.name);
const isTelValid = formValidations.regex.telephone.test(profileForm.tel);
const isBirthDateValid =
profileForm.birthDate.length === 0 ||
(formValidations.regex.birthDate.test(profileForm.birthDate) &&
formValidations.checker.birthDate(profileForm.birthDate));

const isReadyToSubmit = isNameValid && isTelValid && isBirthDateValid;

const queryClient = useQueryClient();
const { mutateAsync: updateProfile } = useUpdateUserProfileMutation(memberId);
Expand All @@ -104,6 +131,7 @@ function ProfileEditForm({
const rawFormData: UpdateUserProfileRequest = {
name: profileForm.name,
tel: profileForm.tel,
birthDate: profileForm.birthDate.replace(/\./g, '-'),
githubLink: profileForm.githubLink,
blogOrSnsLink: profileForm.blogOrSnsLink,
simpleIntroduction: profileForm.simpleIntroduction.trim() || undefined,
Expand Down Expand Up @@ -191,12 +219,13 @@ function ProfileEditForm({
<FormField
label="연락처"
type="text"
error={!isTelValid}
error={!(isTelValid || profileForm.tel === '')}
description={
isTelValid
isTelValid || profileForm.tel === ''
? '스터디 진행을 위한 연락 가능한 정보를 입력해 주세요.'
: '연락처는 숫자와 하이픈(-)을 포함한 형식으로 입력해주세요.'
}
placeholder="010-1234-5678"
value={profileForm.tel}
onChange={(value) => {
// 숫자와 하이픈(-)만 입력 허용
Expand All @@ -205,10 +234,29 @@ function ProfileEditForm({
}}
required
/>
<FormField
label="생년월일"
type="text"
description={
!isBirthDateValid
? '잘못된 형식입니다.'
: '생년월일을 입력해 주세요.'
}
error={!isBirthDateValid}
placeholder="2000.00.00"
value={profileForm.birthDate}
onChange={(value) => {
setProfileForm({
...profileForm,
birthDate: value,
});
}}
/>
<FormField
label="Github"
type="text"
description="본인의 활동을 확인할 수 있는 GitHub 링크를 입력해 주세요."
placeholder="https://github.com/username"
value={profileForm.githubLink}
onChange={(value) =>
setProfileForm({
Expand Down Expand Up @@ -270,7 +318,7 @@ function ProfileEditForm({
await handleSubmit(); // 여기서 이미지 업로드 포함
onClose();
}}
disabled={!isNameValid || !isTelValid}
disabled={!isReadyToSubmit}
>
수정 완료
</Button>
Expand Down
Loading