From 84fa9090e0f0b5892b174ec924ad13ff71246506 Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 25 Jul 2025 00:34:11 +0900 Subject: [PATCH 01/18] =?UTF-8?q?feat:=20FormField=20props=EC=97=90=20plac?= =?UTF-8?q?eholder=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/form/form-field.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/shared/ui/form/form-field.tsx b/src/shared/ui/form/form-field.tsx index 737f3edf..943cb7d4 100644 --- a/src/shared/ui/form/form-field.tsx +++ b/src/shared/ui/form/form-field.tsx @@ -14,6 +14,7 @@ type InputType = interface FormFieldProps { label: string; description?: string; + placeholder?: string; type: InputType; maxLength?: number; required?: boolean; @@ -28,6 +29,7 @@ export function FormField({ label, error = false, description, + placeholder, type, required = false, maxLength = 30, @@ -42,7 +44,7 @@ export function FormField({ return ( <> onChange(e.target.value as T)} @@ -59,7 +61,7 @@ export function FormField({ case 'textarea': return ( ({ onChange(v as T)} /> {description && ( @@ -89,7 +91,7 @@ export function FormField({ options={options} defaultValue={value as string[]} onChange={(v) => onChange(v as T)} - placeholder="선택해주세요" + placeholder={placeholder || '선택해주세요.'} /> {description && (
From b1a754e5117e7c90b26ce91d27d1e2245e05696d Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 25 Jul 2025 00:36:28 +0900 Subject: [PATCH 02/18] =?UTF-8?q?refactor:=20=EC=97=B0=EB=9D=BD=EC=B2=98?= =?UTF-8?q?=20validation=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/my-page/ui/profile-edit-modal.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/features/my-page/ui/profile-edit-modal.tsx b/src/features/my-page/ui/profile-edit-modal.tsx index dd6a4500..8a0369e3 100644 --- a/src/features/my-page/ui/profile-edit-modal.tsx +++ b/src/features/my-page/ui/profile-edit-modal.tsx @@ -81,9 +81,7 @@ function ProfileEditForm({ // 이름 유효성 검사: 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 isTelValid = /^\d{2,3}-\d{3,4}-\d{4}$/.test(profileForm.tel); const queryClient = useQueryClient(); const { mutateAsync: updateProfile } = useUpdateUserProfileMutation(memberId); @@ -191,12 +189,13 @@ function ProfileEditForm({ { // 숫자와 하이픈(-)만 입력 허용 From fb5bde7e7929b59a5cbcf8cc909634997d01940d Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 25 Jul 2025 00:40:17 +0900 Subject: [PATCH 03/18] =?UTF-8?q?refactor:=20github=20input=20placeholder?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/my-page/ui/profile-edit-modal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/my-page/ui/profile-edit-modal.tsx b/src/features/my-page/ui/profile-edit-modal.tsx index 8a0369e3..7f2e66fe 100644 --- a/src/features/my-page/ui/profile-edit-modal.tsx +++ b/src/features/my-page/ui/profile-edit-modal.tsx @@ -208,6 +208,7 @@ function ProfileEditForm({ label="Github" type="text" description="본인의 활동을 확인할 수 있는 GitHub 링크를 입력해 주세요." + placeholder="https://github.com/username" value={profileForm.githubLink} onChange={(value) => setProfileForm({ From bf01261682eca86e81e6cf253d4d8efe2e8eac89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=EC=A7=84?= Date: Sat, 26 Jul 2025 22:11:03 +0900 Subject: [PATCH 04/18] =?UTF-8?q?fix=20:=20=EB=8F=84=EC=BB=A4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=97=90=20=20next.config.ts=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile.dev | 1 + Dockerfile.prod | 1 + 2 files changed, 2 insertions(+) diff --git a/Dockerfile.dev b/Dockerfile.dev index 512be29b..d8e6b9b8 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -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 diff --git a/Dockerfile.prod b/Dockerfile.prod index 512be29b..d8e6b9b8 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -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 From 37ebacf0a3654b1efe3643d7f74585ec9f78938a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=EC=A7=84?= Date: Mon, 28 Jul 2025 18:34:52 +0900 Subject: [PATCH 05/18] =?UTF-8?q?fix=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9=20img=20=ED=83=9C=EA=B7=B8=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=ED=9B=84=20next=20Image=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=EB=A7=8C=20=EB=82=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/auth/ui/sign-up-image-selector.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/auth/ui/sign-up-image-selector.tsx b/src/features/auth/ui/sign-up-image-selector.tsx index 8a8f9ce0..8bd21034 100644 --- a/src/features/auth/ui/sign-up-image-selector.tsx +++ b/src/features/auth/ui/sign-up-image-selector.tsx @@ -20,7 +20,6 @@ export default function SignupImageSelector({
프로필 - next최적화안쓴프로필
From 614e92aa691c749357a54f196d498c8e2268e3cf Mon Sep 17 00:00:00 2001 From: aken-you Date: Mon, 28 Jul 2025 22:51:42 +0900 Subject: [PATCH 06/18] =?UTF-8?q?feat:=20=ED=86=A0=EA=B8=80=20=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/toggle/switch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/ui/toggle/switch.tsx b/src/shared/ui/toggle/switch.tsx index b2adea29..91b7c322 100644 --- a/src/shared/ui/toggle/switch.tsx +++ b/src/shared/ui/toggle/switch.tsx @@ -5,7 +5,7 @@ import { cva } from 'class-variance-authority'; import { cn } from '@/shared/shadcn/lib/utils'; const toggleRootVariants = cva( - 'inline-flex items-center rounded-full transition-colors cursor-pointer', + 'inline-flex items-center rounded-full transition-colors cursor-pointer disabled:cursor-not-allowed', { variants: { color: { From c522e2f53640c5beab206772b29f5104f1ba92b9 Mon Sep 17 00:00:00 2001 From: aken-you Date: Mon, 28 Jul 2025 22:54:57 +0900 Subject: [PATCH 07/18] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=20=EC=8B=A0=EC=B2=AD=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=84=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0,=20=EB=A7=A4=EC=B9=AD=20=ED=86=A0=EA=B8=80?= =?UTF-8?q?=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/ui/my-profile-card.tsx | 6 +++++- src/widgets/home/sidebar.tsx | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/features/study/ui/my-profile-card.tsx b/src/features/study/ui/my-profile-card.tsx index be84c7ea..df1b0178 100644 --- a/src/features/study/ui/my-profile-card.tsx +++ b/src/features/study/ui/my-profile-card.tsx @@ -18,6 +18,7 @@ interface MyProfileCardProps { subject?: string; time?: string; techStacks?: string; + studyApplied?: boolean; } export default function MyProfileCard({ @@ -28,6 +29,7 @@ export default function MyProfileCard({ subject, time, techStacks, + studyApplied, }: MyProfileCardProps) { const [enabled, setEnabled] = useState(matching); @@ -35,6 +37,8 @@ export default function MyProfileCard({ usePatchAutoMatchingMutation(); const handleToggleChange = (checked: boolean) => { + if (!studyApplied) return; + setEnabled(checked); patchAutoMatching( @@ -69,7 +73,7 @@ export default function MyProfileCard({ size="md" checked={enabled} onCheckedChange={handleToggleChange} - disabled={isPending} + disabled={isPending || !studyApplied} />
diff --git a/src/widgets/home/sidebar.tsx b/src/widgets/home/sidebar.tsx index cd342461..9709d142 100644 --- a/src/widgets/home/sidebar.tsx +++ b/src/widgets/home/sidebar.tsx @@ -27,6 +27,7 @@ export default async function Sidebar() { techStacks={userProfile?.memberInfo.techStacks ?.map((t) => t.techStackName) .join(', ')} + studyApplied={userProfile?.studyApplied ?? false} /> {userProfile.studyApplied ? ( From 75c4768fe9b0d85d5ee4d9cfb5cbe3af72b00cac Mon Sep 17 00:00:00 2001 From: aken-you Date: Mon, 28 Jul 2025 23:45:18 +0900 Subject: [PATCH 08/18] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=EC=9D=84=20=ED=81=B4=EB=A6=AD=ED=95=98?= =?UTF-8?q?=EB=A9=B4,=20"=ED=94=84=EB=A1=9C=ED=95=84"=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/ui/my-profile-card.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/features/study/ui/my-profile-card.tsx b/src/features/study/ui/my-profile-card.tsx index df1b0178..32ab753a 100644 --- a/src/features/study/ui/my-profile-card.tsx +++ b/src/features/study/ui/my-profile-card.tsx @@ -1,8 +1,8 @@ 'use client'; +import Link from 'next/link'; import React, { useState } from 'react'; import { usePatchAutoMatchingMutation } from '@/entities/user/model/use-user-profile-query'; -import UserProfileModal from '@/features/my-page/ui/user-profile-modal'; import UserAvatar from '@/shared/ui/avatar'; import { ToggleSwitch } from '@/shared/ui/toggle'; import AccessTimeIcon from 'public/icons/access_time.svg'; @@ -56,14 +56,12 @@ export default function MyProfileCard({
- - - - } - /> + + +
{name?.trim() || '비회원'}님
From 6335cfd0f54ab737d1413fd8980fc3c8f21f817d Mon Sep 17 00:00:00 2001 From: aken-you Date: Tue, 29 Jul 2025 00:57:35 +0900 Subject: [PATCH 09/18] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=20=EC=8B=A0=EC=B2=AD=20=ED=8F=BC=EC=9D=84=20=ED=95=9C=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=97=90=EC=84=9C=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/ui/start-study-modal.tsx | 141 +++++++++++++------- 1 file changed, 95 insertions(+), 46 deletions(-) diff --git a/src/features/study/ui/start-study-modal.tsx b/src/features/study/ui/start-study-modal.tsx index e411b252..e0538154 100644 --- a/src/features/study/ui/start-study-modal.tsx +++ b/src/features/study/ui/start-study-modal.tsx @@ -79,20 +79,26 @@ export function LabeledField({ } export default function StartStudyModal({ memberId }: StartStudyModalProps) { - const [introduce, setIntroduce] = - useState(''); - const [studyPlan, setStudyPlan] = useState(''); - const [phoneNumber, setPhoneNumber] = useState(''); - const [github, setGithub] = useState(''); - const [blog, setBlog] = useState(''); - const [preferredSubject, setPreferredSubject] = - useState(undefined); - const [availableTimeSlots, setAvailableTimeSlots] = useState< - JoinStudyRequest['availableStudyTimeIds'] - >([]); - const [selectedSkills, setSelectedSkills] = useState< - JoinStudyRequest['techStackIds'] - >([]); + const [form, setForm] = useState>({ + selfIntroduction: '', + studyPlan: '', + tel: '', + githubLink: '', + blogOrSnsLink: '', + preferredStudySubjectId: undefined, + availableStudyTimeIds: [], + techStackIds: [], + }); + const { + selfIntroduction, + studyPlan, + tel, + githubLink, + blogOrSnsLink, + preferredStudySubjectId, + availableStudyTimeIds, + techStackIds, + } = form; const { data: availableStudyTimes } = useAvailableStudyTimesQuery(); const { data: studySubjects } = useStudySubjectsQuery(); @@ -102,28 +108,39 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { const { mutate: joinStudy } = useJoinStudyMutation(); const toggleTimeSlot = (id: number) => { - setAvailableTimeSlots((prev) => - prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id], + setForm((prev) => + prev.availableStudyTimeIds.includes(id) + ? { + ...prev, + availableStudyTimeIds: prev.availableStudyTimeIds.filter( + (item) => item !== id, + ), + } + : { + ...prev, + availableStudyTimeIds: [...prev.availableStudyTimeIds, id], + }, ); }; const isFormValid = - introduce.trim() !== '' && + selfIntroduction.trim() !== '' && studyPlan.trim() !== '' && - phoneNumber.trim() !== '' && - preferredSubject !== undefined && - availableTimeSlots.length > 0 && - selectedSkills.length > 0; + tel.trim() !== '' && + preferredStudySubjectId !== undefined && + availableStudyTimeIds.length > 0 && + techStackIds.length > 0; const getMissingFields = () => { const missing: string[] = []; - if (introduce.trim() === '') missing.push('자기소개'); + if (selfIntroduction.trim() === '') missing.push('자기소개'); if (studyPlan.trim() === '') missing.push('공부 주제 및 계획'); - if (preferredSubject === undefined) missing.push('선호하는 스터디 주제'); - if (availableTimeSlots.length === 0) missing.push('가능 시간대'); - if (selectedSkills.length === 0) missing.push('사용 가능한 기술 스택'); - if (phoneNumber.trim() === '') missing.push('연락처'); + if (preferredStudySubjectId === undefined) + missing.push('선호하는 스터디 주제'); + if (availableStudyTimeIds.length === 0) missing.push('가능 시간대'); + if (techStackIds.length === 0) missing.push('사용 가능한 기술 스택'); + if (tel.trim() === '') missing.push('연락처'); return missing; }; @@ -173,8 +190,13 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { setIntroduce(e.target.value)} + value={selfIntroduction} + onChange={(e) => + setForm((prev) => ({ + ...prev, + selfIntroduction: e.target.value, + })) + } /> @@ -187,7 +209,12 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { className="border-border-default rounded-100 border p-150" placeholder="CS 기본기를 탄탄하게 다지는 것이 목표입니다. 각자 맡은 주제를 정리하고 공유하는 방식으로 진행하고 싶어요." value={studyPlan} - onChange={(e) => setStudyPlan(e.target.value)} + onChange={(e) => + setForm((prev) => ({ + ...prev, + studyPlan: e.target.value, + })) + } /> @@ -197,7 +224,7 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { description="관심 있는 스터디 유형을 선택해 주세요." > ({ value: studySubjectId, @@ -205,7 +232,12 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { }), )} placeholder="선택하세요" - onChange={(value) => setPreferredSubject(value.toString())} + onChange={(value) => + setForm((prev) => ({ + ...prev, + preferredStudySubjectId: value.toString(), + })) + } /> @@ -219,7 +251,9 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { ({ availableTimeId, display }) => ( toggleTimeSlot(availableTimeId)} > {display} @@ -242,7 +276,10 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { }), )} onChange={(newSelected) => - setSelectedSkills(newSelected as number[]) + setForm((prev) => ({ + ...prev, + techStackIds: newSelected as number[], + })) } placeholder="기술을 선택해주세요" /> @@ -256,8 +293,13 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { setPhoneNumber(e.target.value)} + value={tel} + onChange={(e) => + setForm((prev) => ({ + ...prev, + tel: e.target.value, + })) + } /> @@ -268,8 +310,10 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { setGithub(e.target.value)} + value={githubLink} + onChange={(e) => + setForm((prev) => ({ ...prev, githubLink: e.target.value })) + } /> @@ -280,8 +324,13 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { setBlog(e.target.value)} + value={blogOrSnsLink} + onChange={(e) => + setForm((prev) => ({ + ...prev, + blogOrSnsLink: e.target.value, + })) + } />
@@ -310,14 +359,14 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { joinStudy( { memberId, - selfIntroduction: introduce, + selfIntroduction, studyPlan, - preferredStudySubjectId: preferredSubject?.toString(), - availableStudyTimeIds: availableTimeSlots, - techStackIds: selectedSkills, - tel: phoneNumber, - githubLink: github.trim() || undefined, - blogOrSnsLink: blog.trim() || undefined, + preferredStudySubjectId, + availableStudyTimeIds, + techStackIds, + tel, + githubLink: githubLink.trim() || undefined, + blogOrSnsLink: blogOrSnsLink.trim() || undefined, }, { onSuccess: () => { From 643686c8f06c8775db1cdd87ba2ee7e357eb8dda Mon Sep 17 00:00:00 2001 From: yeonhwan Date: Tue, 29 Jul 2025 15:23:16 +0900 Subject: [PATCH 10/18] =?UTF-8?q?feat:=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=EC=8A=A4=ED=84=B0=EB=94=94=20=EC=B9=B4=EB=93=9C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=A9=B4=EC=A0=91=EA=B4=80/=EB=A9=B4=EC=A0=91?= =?UTF-8?q?=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EC=8B=9C=20=EC=9C=A0=EC=A0=80=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=97=B4=EB=A6=BC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/ui/today-study-card.tsx | 49 +++++++++++++++++----- src/shared/ui/avatar/index.tsx | 40 +++++++++++------- 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/src/features/study/ui/today-study-card.tsx b/src/features/study/ui/today-study-card.tsx index 109f265e..8fb3cbaa 100644 --- a/src/features/study/ui/today-study-card.tsx +++ b/src/features/study/ui/today-study-card.tsx @@ -1,8 +1,11 @@ 'use client'; import { useEffect, useState } from 'react'; +import UserProfileModal from '@/features/my-page/ui/user-profile-modal'; import { getStatusBadge } from '@/features/study/ui/status-badge-map'; import { getCookie } from '@/shared/tanstack-query/cookie'; +// TODO: FSD 의 import 바운더리를 넘어서 import 해야하는데, +// 해당 UI를 shared 등으로 빼던지 수정 필요 import UserAvatar from '@/shared/ui/avatar'; import StudyDoneModal from './study-done-modal'; import StudyReadyModal from './study-ready-modal'; @@ -39,17 +42,41 @@ export default function TodayStudyCard({ studyDate }: { studyDate: string }) { label="스터디 조" value={`${todayStudyData.studySpaceId} 조`} /> - - - - {todayStudyData.interviewerName} - -
- } - /> + {isInterviewee ? ( + + + } + /> + + {todayStudyData.interviewerName} + + + } + /> + ) : ( + + + } + /> + + {todayStudyData.interviewerName} + + + } + /> + )} - {image ? ( - - ) : ( - - )} - - ); -} +const UserAvatar = forwardRef( + ( + { image, alt = 'user profile', size = 32, ...props }: UserAvatarProps, + forwardedRef, + ) => { + return ( + + {image ? ( + + ) : ( + + )} + + ); + }, +); + +UserAvatar.displayName = 'UserAvatar'; + +export default UserAvatar; From ddab246c2cc68fc242ae558afb44190246b7a193 Mon Sep 17 00:00:00 2001 From: yeonhwan Date: Tue, 29 Jul 2025 21:38:06 +0900 Subject: [PATCH 11/18] =?UTF-8?q?fix:=20today-study-card=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8,=20user-avatar=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8,=20modal=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - today-study-card body 렌더링 부분 리팩토링 - user-avatar forwardRef 삭제 - modal 컴포넌트 description 부재에 관련한 접근성 워닝 제거 --- src/features/study/ui/today-study-card.tsx | 76 ++++++++++------------ src/shared/ui/avatar/index.tsx | 42 +++++------- src/shared/ui/modal/index.tsx | 4 ++ 3 files changed, 54 insertions(+), 68 deletions(-) diff --git a/src/features/study/ui/today-study-card.tsx b/src/features/study/ui/today-study-card.tsx index 8fb3cbaa..0e6e05ad 100644 --- a/src/features/study/ui/today-study-card.tsx +++ b/src/features/study/ui/today-study-card.tsx @@ -25,6 +25,18 @@ export default function TodayStudyCard({ studyDate }: { studyDate: string }) { const isInterviewee = memberId === todayStudyData.intervieweeId; + const matchedUserId = isInterviewee + ? todayStudyData?.interviewerId + : todayStudyData.intervieweeId; + + const matchedUserImage = isInterviewee + ? todayStudyData?.interviewerImage + : todayStudyData.intervieweeImage; + + const matchedUsername = isInterviewee + ? todayStudyData?.interviewerName + : todayStudyData.intervieweeName; + return (
@@ -38,70 +50,48 @@ export default function TodayStudyCard({ studyDate }: { studyDate: string }) {
- - {isInterviewee ? ( - - - } - /> - - {todayStudyData.interviewerName} - -
- } - /> - ) : ( - + {`${todayStudyData?.studySpaceId} 조`} + + + - - } - /> - - {todayStudyData.interviewerName} + + + {matchedUsername} } /> - )} - - + + + {todayStudyData?.subject ?? 'asdf'} + + + {getStatusBadge(todayStudyData?.progressStatus ?? 'PENDING')} +
피드백
-

{todayStudyData.feedback ?? '-'}

+

{todayStudyData?.feedback ?? '-'}

); function InfoBox({ label, - value, + children, }: { label: string; - value: React.ReactNode; + children: React.ReactNode; }) { return (
{label} - {value} + {children}
); } diff --git a/src/shared/ui/avatar/index.tsx b/src/shared/ui/avatar/index.tsx index f12715a5..3f8817bc 100644 --- a/src/shared/ui/avatar/index.tsx +++ b/src/shared/ui/avatar/index.tsx @@ -1,6 +1,5 @@ 'use client'; -import { forwardRef } from 'react'; import { Avatar, AvatarImage } from '@/shared/shadcn/ui/avatar'; interface UserAvatarProps { @@ -9,27 +8,20 @@ interface UserAvatarProps { size?: number; } -const UserAvatar = forwardRef( - ( - { image, alt = 'user profile', size = 32, ...props }: UserAvatarProps, - forwardedRef, - ) => { - return ( - - {image ? ( - - ) : ( - - )} - - ); - }, -); - -UserAvatar.displayName = 'UserAvatar'; - -export default UserAvatar; +export default function UserAvatar({ + image, + alt = 'user profile', + size = 32, + ref, + ...props +}: React.RefAttributes & UserAvatarProps) { + return ( + + {image ? ( + + ) : ( + + )} + + ); +} diff --git a/src/shared/ui/modal/index.tsx b/src/shared/ui/modal/index.tsx index 262a2504..c579eff8 100644 --- a/src/shared/ui/modal/index.tsx +++ b/src/shared/ui/modal/index.tsx @@ -11,6 +11,7 @@ import { DialogOverlay, DialogPortal, DialogTitle, + DialogDescription, } from '@/shared/shadcn/ui/dialog'; function ModalRoot({ @@ -71,9 +72,11 @@ function ModalOverlay({ function ModalContent({ className, children, + description = '', ...props }: React.ComponentProps & { size?: 'small' | 'medium' | 'large'; + description?: string; }) { const { size = 'small', ...rest } = props; @@ -104,6 +107,7 @@ function ModalContent({ {...rest} >
{children}
+ {description} ); } From f8a3e610d6c344cb5c79b55fe3199c9da42f702c Mon Sep 17 00:00:00 2001 From: yeonhwan Date: Tue, 29 Jul 2025 21:49:18 +0900 Subject: [PATCH 12/18] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=8D=94=EB=AF=B8=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EC=98=B5=EC=85=94=EB=84=90=EC=B2=B4=EC=9D=B4?= =?UTF-8?q?=EB=8B=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/ui/today-study-card.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/features/study/ui/today-study-card.tsx b/src/features/study/ui/today-study-card.tsx index 0e6e05ad..ffc87692 100644 --- a/src/features/study/ui/today-study-card.tsx +++ b/src/features/study/ui/today-study-card.tsx @@ -26,15 +26,15 @@ export default function TodayStudyCard({ studyDate }: { studyDate: string }) { const isInterviewee = memberId === todayStudyData.intervieweeId; const matchedUserId = isInterviewee - ? todayStudyData?.interviewerId + ? todayStudyData.interviewerId : todayStudyData.intervieweeId; const matchedUserImage = isInterviewee - ? todayStudyData?.interviewerImage + ? todayStudyData.interviewerImage : todayStudyData.intervieweeImage; const matchedUsername = isInterviewee - ? todayStudyData?.interviewerName + ? todayStudyData.interviewerName : todayStudyData.intervieweeName; return ( @@ -51,7 +51,7 @@ export default function TodayStudyCard({ studyDate }: { studyDate: string }) {
- {`${todayStudyData?.studySpaceId} 조`} + {`${todayStudyData.studySpaceId} 조`} - - {todayStudyData?.subject ?? 'asdf'} - + {todayStudyData.subject} - {getStatusBadge(todayStudyData?.progressStatus ?? 'PENDING')} + {getStatusBadge(todayStudyData.progressStatus ?? 'PENDING')}
피드백
-

{todayStudyData?.feedback ?? '-'}

+

{todayStudyData.feedback ?? '-'}

); From 9bff1c1646286de6ed06e9c17b18b1ec840eff15 Mon Sep 17 00:00:00 2001 From: aken-you Date: Tue, 29 Jul 2025 23:46:41 +0900 Subject: [PATCH 13/18] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=20=EC=8B=A0=EC=B2=AD=20=ED=8F=BC=20=EB=A1=9C=EC=A7=81=EC=9D=84?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/ui/start-study-modal.tsx | 518 ++++++++++---------- 1 file changed, 266 insertions(+), 252 deletions(-) diff --git a/src/features/study/ui/start-study-modal.tsx b/src/features/study/ui/start-study-modal.tsx index e0538154..3efcdc48 100644 --- a/src/features/study/ui/start-study-modal.tsx +++ b/src/features/study/ui/start-study-modal.tsx @@ -3,7 +3,7 @@ import { XIcon } from 'lucide-react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { useAvailableStudyTimesQuery, useStudySubjectsQuery, @@ -78,7 +78,46 @@ export function LabeledField({ ); } +type JoinStudyFormError = { + [K in keyof Omit< + JoinStudyRequest, + 'memberId' | 'githubLink' | 'blogOrSnsLink' + >]: boolean; +}; + export default function StartStudyModal({ memberId }: StartStudyModalProps) { + return ( + + + 스터디 시작 버튼 + + + + + + + CS 스터디 신청하기 + + + + + + + + + + + ); +} + +function StartStudyForm({ memberId }: StartStudyModalProps) { const [form, setForm] = useState>({ selfIntroduction: '', studyPlan: '', @@ -89,6 +128,16 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { availableStudyTimeIds: [], techStackIds: [], }); + + const [error, setError] = useState({ + selfIntroduction: false, + studyPlan: false, + tel: false, + preferredStudySubjectId: false, + availableStudyTimeIds: false, + techStackIds: false, + }); + const { selfIntroduction, studyPlan, @@ -107,7 +156,7 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { const { mutate: joinStudy } = useJoinStudyMutation(); - const toggleTimeSlot = (id: number) => { + const toggleStudyTime = (id: number) => { setForm((prev) => prev.availableStudyTimeIds.includes(id) ? { @@ -123,271 +172,236 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { ); }; - const isFormValid = - selfIntroduction.trim() !== '' && - studyPlan.trim() !== '' && - tel.trim() !== '' && - preferredStudySubjectId !== undefined && - availableStudyTimeIds.length > 0 && - techStackIds.length > 0; + const handleSubmit = () => { + const isValidSelfIntroduction = selfIntroduction.trim() !== ''; + const isValidStudyPlan = studyPlan.trim() !== ''; + const isValidTel = tel.trim() !== ''; + const isValidPreferredStudySubjectId = + preferredStudySubjectId !== undefined; + const isValidAvailableStudyTimeIds = availableStudyTimeIds.length > 0; + const isValidTechStackIds = techStackIds.length > 0; - const getMissingFields = () => { - const missing: string[] = []; + if ( + !isValidSelfIntroduction || + !isValidStudyPlan || + !isValidTel || + !isValidPreferredStudySubjectId || + !isValidAvailableStudyTimeIds || + !isValidTechStackIds + ) { + setError({ + selfIntroduction: !isValidSelfIntroduction, + studyPlan: !isValidStudyPlan, + tel: !isValidTel, + preferredStudySubjectId: !isValidPreferredStudySubjectId, + availableStudyTimeIds: !isValidAvailableStudyTimeIds, + techStackIds: !isValidTechStackIds, + }); - if (selfIntroduction.trim() === '') missing.push('자기소개'); - if (studyPlan.trim() === '') missing.push('공부 주제 및 계획'); - if (preferredStudySubjectId === undefined) - missing.push('선호하는 스터디 주제'); - if (availableStudyTimeIds.length === 0) missing.push('가능 시간대'); - if (techStackIds.length === 0) missing.push('사용 가능한 기술 스택'); - if (tel.trim() === '') missing.push('연락처'); + return; + } - return missing; + joinStudy( + { + ...form, + memberId, + githubLink: githubLink.trim() || undefined, + blogOrSnsLink: blogOrSnsLink.trim() || undefined, + }, + { + onSuccess: () => { + alert('스터디 신청이 완료되었습니다!'); + router.refresh(); + }, + onError: () => { + alert('스터디 신청 중 오류가 발생했습니다. 다시 시도해 주세요.'); + }, + }, + ); }; return ( - - - 스터디 시작 버튼 - - - - - - - CS 스터디 신청하기 - - - - - - - -
CS 스터디 진행 방법
- {studySteps.map((step, idx) => ( - - ))} - -
+ <> + +
CS 스터디 진행 방법
+ {studySteps.map((step, idx) => ( + + ))} -
- - - setForm((prev) => ({ - ...prev, - selfIntroduction: e.target.value, - })) - } - /> - +
- - - setForm((prev) => ({ - ...prev, - studyPlan: e.target.value, - })) - } - /> - +
+ + + setForm((prev) => ({ + ...prev, + selfIntroduction: e.target.value, + })) + } + /> + - - ({ - value: studySubjectId, - label: name, - }), - )} - placeholder="선택하세요" - onChange={(value) => - setForm((prev) => ({ - ...prev, - preferredStudySubjectId: value.toString(), - })) - } - /> - + + + setForm((prev) => ({ + ...prev, + studyPlan: e.target.value, + })) + } + /> + - -
- {(availableStudyTimes ?? []).map( - ({ availableTimeId, display }) => ( - toggleTimeSlot(availableTimeId)} - > - {display} - - ), - )} -
-
+ + ({ + value: studySubjectId, + label: name, + }), + )} + placeholder="선택하세요" + onChange={(value) => + setForm((prev) => ({ + ...prev, + preferredStudySubjectId: value.toString(), + })) + } + /> + - - ({ - value: techStackId, - label: techStackName, - }), - )} - onChange={(newSelected) => - setForm((prev) => ({ - ...prev, - techStackIds: newSelected as number[], - })) - } - placeholder="기술을 선택해주세요" - /> - - - - - setForm((prev) => ({ - ...prev, - tel: e.target.value, - })) - } - /> - + +
+ {(availableStudyTimes ?? []).map( + ({ availableTimeId, display }) => ( + toggleStudyTime(availableTimeId)} + > + {display} + + ), + )} +
+
- - - setForm((prev) => ({ ...prev, githubLink: e.target.value })) - } - /> - + + ({ + value: techStackId, + label: techStackName, + }), + )} + onChange={(newSelected) => { + setForm((prev) => ({ + ...prev, + techStackIds: newSelected as number[], + })); + }} + placeholder="기술을 선택해주세요" + /> + - - - setForm((prev) => ({ - ...prev, - blogOrSnsLink: e.target.value, - })) - } - /> - -
- + + { + setForm((prev) => ({ + ...prev, + tel: e.target.value, + })); + }} + /> + - - - - - -
+ - joinStudy( - { - memberId, - selfIntroduction, - studyPlan, - preferredStudySubjectId, - availableStudyTimeIds, - techStackIds, - tel, - githubLink: githubLink.trim() || undefined, - blogOrSnsLink: blogOrSnsLink.trim() || undefined, - }, - { - onSuccess: () => { - alert('스터디 신청이 완료되었습니다!'); - router.refresh(); - }, - onError: () => { - alert( - '스터디 신청 중 오류가 발생했습니다. 다시 시도해 주세요.', - ); - }, - }, - ); - }} - > - 신청 완료 - - - - - - + + + + + + + ); } From 8464c3e1175b335d3d4396b4747483fe3f61df81 Mon Sep 17 00:00:00 2001 From: yeonhwan Date: Wed, 30 Jul 2025 22:48:20 +0900 Subject: [PATCH 14/18] =?UTF-8?q?feat:=20update-user-profile=20birthDate?= =?UTF-8?q?=20filed=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/my-page/api/types.ts | 1 + .../my-page/ui/profile-edit-modal.tsx | 61 +++++++++++++++++-- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/features/my-page/api/types.ts b/src/features/my-page/api/types.ts index 2c4c9b87..04a25e5b 100644 --- a/src/features/my-page/api/types.ts +++ b/src/features/my-page/api/types.ts @@ -1,6 +1,7 @@ export interface UpdateUserProfileRequest { name: string; tel: string; + birthDate?: string; githubLink?: string; blogOrSnsLink?: string; simpleIntroduction?: string; diff --git a/src/features/my-page/ui/profile-edit-modal.tsx b/src/features/my-page/ui/profile-edit-modal.tsx index 7f2e66fe..486ee5a2 100644 --- a/src/features/my-page/ui/profile-edit-modal.tsx +++ b/src/features/my-page/ui/profile-edit-modal.tsx @@ -24,6 +24,33 @@ 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: { + isOptional: (value: string) => { + return value.length === 0; + }, + // 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); @@ -66,6 +93,7 @@ function ProfileEditForm({ const [profileForm, setProfileForm] = useState({ name: memberProfile.memberName ?? '', tel: memberProfile.tel ?? '', + birthDate: memberProfile.birthDate ?? '', githubLink: memberProfile.githubLink?.url ?? '', blogOrSnsLink: memberProfile.blogOrSnsLink?.url ?? '', mbti: memberProfile.mbti ?? '', @@ -78,10 +106,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 = /^\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); @@ -102,6 +134,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, @@ -204,6 +237,24 @@ function ProfileEditForm({ }} required /> + { + setProfileForm({ + ...profileForm, + birthDate: value, + }); + }} + /> 수정 완료 From bcde698a774d9e671c6a6e74d6b7f5fcad3b724d Mon Sep 17 00:00:00 2001 From: aken-you Date: Wed, 30 Jul 2025 23:20:37 +0900 Subject: [PATCH 15/18] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=ED=8F=BC=20=EC=97=90=EB=9F=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/ui/start-study-modal.tsx | 91 +++++++++++---------- src/shared/ui/dropdown/multi.tsx | 4 +- src/shared/ui/dropdown/single.tsx | 6 +- src/shared/ui/input/base.tsx | 17 +++- 4 files changed, 68 insertions(+), 50 deletions(-) diff --git a/src/features/study/ui/start-study-modal.tsx b/src/features/study/ui/start-study-modal.tsx index 3efcdc48..4c3a190e 100644 --- a/src/features/study/ui/start-study-modal.tsx +++ b/src/features/study/ui/start-study-modal.tsx @@ -3,7 +3,7 @@ import { XIcon } from 'lucide-react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; -import { useRef, useState } from 'react'; +import { useState } from 'react'; import { useAvailableStudyTimesQuery, useStudySubjectsQuery, @@ -146,7 +146,6 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { blogOrSnsLink, preferredStudySubjectId, availableStudyTimeIds, - techStackIds, } = form; const { data: availableStudyTimes } = useAvailableStudyTimesQuery(); @@ -173,30 +172,17 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { }; const handleSubmit = () => { - const isValidSelfIntroduction = selfIntroduction.trim() !== ''; - const isValidStudyPlan = studyPlan.trim() !== ''; - const isValidTel = tel.trim() !== ''; - const isValidPreferredStudySubjectId = - preferredStudySubjectId !== undefined; - const isValidAvailableStudyTimeIds = availableStudyTimeIds.length > 0; - const isValidTechStackIds = techStackIds.length > 0; + const newError: JoinStudyFormError = { + selfIntroduction: selfIntroduction.trim() === '', + studyPlan: studyPlan.trim() === '', + tel: !/^\d{2,3}-\d{3,4}-\d{4}$/.test(tel), + preferredStudySubjectId: preferredStudySubjectId === undefined, + availableStudyTimeIds: availableStudyTimeIds.length === 0, + techStackIds: form.techStackIds.length === 0, + }; - if ( - !isValidSelfIntroduction || - !isValidStudyPlan || - !isValidTel || - !isValidPreferredStudySubjectId || - !isValidAvailableStudyTimeIds || - !isValidTechStackIds - ) { - setError({ - selfIntroduction: !isValidSelfIntroduction, - studyPlan: !isValidStudyPlan, - tel: !isValidTel, - preferredStudySubjectId: !isValidPreferredStudySubjectId, - availableStudyTimeIds: !isValidAvailableStudyTimeIds, - techStackIds: !isValidTechStackIds, - }); + if (Object.values(newError).some(Boolean)) { + setError(newError); return; } @@ -241,15 +227,19 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { description="간단한 자기소개를 입력해 주세요." > + color={error.selfIntroduction ? 'error' : 'default'} + onChange={(e) => { setForm((prev) => ({ ...prev, selfIntroduction: e.target.value, - })) - } + })); + setError((prev) => ({ + ...prev, + selfIntroduction: e.target.value.trim() === '', + })); + }} /> @@ -259,15 +249,19 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { description="스터디에서 다루고 싶은 주제와 학습 목표를 알려주세요." > + onChange={(e) => { setForm((prev) => ({ ...prev, studyPlan: e.target.value, - })) - } + })); + setError((prev) => ({ + ...prev, + studyPlan: e.target.value.trim() === '', + })); + }} /> @@ -277,6 +271,7 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { description="관심 있는 스터디 유형을 선택해 주세요." > ({ @@ -285,12 +280,16 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { }), )} placeholder="선택하세요" - onChange={(value) => + onChange={(value) => { setForm((prev) => ({ ...prev, preferredStudySubjectId: value.toString(), - })) - } + })); + setError((prev) => ({ + ...prev, + preferredStudySubjectId: value === undefined, + })); + }} /> @@ -320,6 +319,7 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { description="현재 본인이 사용할 수 있는 기술 스택을 모두 선택해 주세요." > ({ value: techStackId, @@ -331,6 +331,10 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { ...prev, techStackIds: newSelected as number[], })); + setError((prev) => ({ + ...prev, + techStackIds: newSelected.length === 0, + })); }} placeholder="기술을 선택해주세요" /> @@ -342,14 +346,18 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { description="스터디 진행을 위해 연락 가능한 정보를 입력해 주세요. 입력하신 정보는 매칭된 스터디원에게만 제공되며, 외부에는 노출되지 않습니다." > { setForm((prev) => ({ ...prev, tel: e.target.value, })); + setError((prev) => ({ + ...prev, + tel: !/^\d{2,3}-\d{3,4}-\d{4}$/.test(e.target.value), + })); }} /> @@ -359,7 +367,6 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { description="본인의 활동을 확인할 수 있는 GitHub 링크를 입력해 주세요." > @@ -373,7 +380,6 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { description="본인의 활동을 확인할 수 있는 외부 링크가 있다면 입력해 주세요." > @@ -393,12 +399,7 @@ function StartStudyForm({ memberId }: StartStudyModalProps) { 취소 - diff --git a/src/shared/ui/dropdown/multi.tsx b/src/shared/ui/dropdown/multi.tsx index ee855d49..7b228088 100644 --- a/src/shared/ui/dropdown/multi.tsx +++ b/src/shared/ui/dropdown/multi.tsx @@ -17,6 +17,7 @@ interface Option { interface MultiDropdownProps { options: Option[]; defaultValue?: (string | number)[]; + error?: boolean; onChange?: (selected: (string | number)[]) => void; placeholder?: string; } @@ -24,6 +25,7 @@ interface MultiDropdownProps { function MultiDropdown({ options, defaultValue = [], + error = false, onChange, placeholder = '선택해주세요', }: MultiDropdownProps) { @@ -63,7 +65,7 @@ function MultiDropdown({
diff --git a/src/shared/ui/dropdown/single.tsx b/src/shared/ui/dropdown/single.tsx index d7513bed..49c81df8 100644 --- a/src/shared/ui/dropdown/single.tsx +++ b/src/shared/ui/dropdown/single.tsx @@ -16,6 +16,7 @@ interface Props { options: Option[]; defaultValue?: string | number; placeholder?: string; + error?: boolean; onChange: (value: string | number) => void; } @@ -23,6 +24,7 @@ function SingleDropdown({ placeholder, options, defaultValue, + error = false, onChange, }: Props) { const [selectedValue, setSelectedValue] = useState< @@ -37,7 +39,9 @@ function SingleDropdown({ return ( -
+
diff --git a/src/shared/ui/input/base.tsx b/src/shared/ui/input/base.tsx index 13d7d67d..23ce5314 100644 --- a/src/shared/ui/input/base.tsx +++ b/src/shared/ui/input/base.tsx @@ -2,8 +2,10 @@ import { cva } from 'class-variance-authority'; import { cn } from '@/shared/shadcn/lib/utils'; import { Input as ShadcnInput } from '@/shared/shadcn/ui/input'; -interface BaseInputProps extends React.ComponentProps { +interface BaseInputProps + extends Omit, 'size'> { color?: 'default' | 'error' | 'success'; + size?: 'm' | 'l'; } const inputVariants = cva( @@ -15,6 +17,10 @@ const inputVariants = cva( error: 'border-border-error text-text-default', success: 'border-border-success text-text-default', }, + size: { + m: 'py-100', + l: 'py-150', + }, }, defaultVariants: { color: 'default', @@ -22,10 +28,15 @@ const inputVariants = cva( }, ); -function BaseInput({ className, color = 'default', ...props }: BaseInputProps) { +function BaseInput({ + className, + color = 'default', + size = 'l', + ...props +}: BaseInputProps) { return ( ); From 205aa09a5e70866c5d6b63cc69b84aabb95a84a4 Mon Sep 17 00:00:00 2001 From: yeonhwan Date: Thu, 31 Jul 2025 00:02:05 +0900 Subject: [PATCH 16/18] =?UTF-8?q?chore:=20update-user-profile=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/my-page/ui/profile-edit-modal.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/features/my-page/ui/profile-edit-modal.tsx b/src/features/my-page/ui/profile-edit-modal.tsx index 486ee5a2..4f4365c6 100644 --- a/src/features/my-page/ui/profile-edit-modal.tsx +++ b/src/features/my-page/ui/profile-edit-modal.tsx @@ -34,9 +34,6 @@ const formValidations = { birthDate: /^\d{4}.([0][1-9]|[1][0-2]).([0][1-9]|[1-2][0-9]|[3][0-1])/, }, checker: { - isOptional: (value: string) => { - return value.length === 0; - }, // year 는 최소 1900년 이상 현재 년도 이하이어야 한다. // month 와 date 의 유효성은 생성자로 검사됨 birthDate: (value: string) => { From 784e1d1d6198c9b9b856d253d8c13f19ef568e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EB=AF=BC=EC=A3=BC?= Date: Thu, 31 Jul 2025 00:25:31 +0900 Subject: [PATCH 17/18] =?UTF-8?q?fix:=20=ED=8C=8C=EC=9D=BC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/user/api/get-user-profile.ts | 10 ---------- src/entities/user/api/types.ts | 13 ------------- src/entities/user/model/use-user-profile-query.ts | 10 ---------- 3 files changed, 33 deletions(-) diff --git a/src/entities/user/api/get-user-profile.ts b/src/entities/user/api/get-user-profile.ts index df1284b4..67b9f322 100644 --- a/src/entities/user/api/get-user-profile.ts +++ b/src/entities/user/api/get-user-profile.ts @@ -1,7 +1,6 @@ import type { GetUserProfileResponse, PatchAutoMatchingParams, - StudyDashboardResponse, } from '@/entities/user/api/types'; import { axiosInstance } from '@/shared/tanstack-query/axios'; @@ -21,12 +20,3 @@ export const patchAutoMatching = async ({ params: { 'auto-matching': autoMatching }, }); }; - -export const getStudyDashboard = async ( - memberId: number, -): Promise => { - const res = await axiosInstance.get(`/study/dashboard/${memberId}`); - console.log('errrrpr' + res.data.content); - - return res.data.content; -}; diff --git a/src/entities/user/api/types.ts b/src/entities/user/api/types.ts index c9569b4d..fbd0d905 100644 --- a/src/entities/user/api/types.ts +++ b/src/entities/user/api/types.ts @@ -84,16 +84,3 @@ export interface PatchAutoMatchingParams { memberId: number; autoMatching: boolean; } - -export interface StudyDashboardResponse { - tier: string; - experience: number; - monthAttendance: number; - weekAttendance: number; - techStack: Record; - mainStackCount: { - FE: number; - BE: number; - CS: number; - }; -} diff --git a/src/entities/user/model/use-user-profile-query.ts b/src/entities/user/model/use-user-profile-query.ts index 03b5a7c7..374fbcf9 100644 --- a/src/entities/user/model/use-user-profile-query.ts +++ b/src/entities/user/model/use-user-profile-query.ts @@ -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'; @@ -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, - }); -}; From 6f1dfe49249521a77d5d585656ea226ca535ba41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EB=AF=BC=EC=A3=BC?= Date: Thu, 31 Jul 2025 00:25:44 +0900 Subject: [PATCH 18/18] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(my)/my-study/page.tsx | 56 +++++++++++++------ src/features/my-page/api/types.ts | 17 ++++++ .../my-page/api/update-user-profile.ts | 7 +++ .../model/use-update-user-profile-mutation.ts | 9 +++ 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/app/(my)/my-study/page.tsx b/app/(my)/my-study/page.tsx index 2e021670..328642cb 100644 --- a/app/(my)/my-study/page.tsx +++ b/app/(my)/my-study/page.tsx @@ -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
로딩 중...
; + if (isError || !dashboard) return
에러가 발생했습니다.
; return (
@@ -22,14 +19,33 @@ export default async function MyStudy() {
- - - + + +
- - + +
@@ -42,11 +58,15 @@ export default async function MyStudy() {
- +
diff --git a/src/features/my-page/api/types.ts b/src/features/my-page/api/types.ts index 2c4c9b87..2b9ea97a 100644 --- a/src/features/my-page/api/types.ts +++ b/src/features/my-page/api/types.ts @@ -62,3 +62,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; +} diff --git a/src/features/my-page/api/update-user-profile.ts b/src/features/my-page/api/update-user-profile.ts index dc9968be..2d5b7877 100644 --- a/src/features/my-page/api/update-user-profile.ts +++ b/src/features/my-page/api/update-user-profile.ts @@ -1,5 +1,6 @@ import type { AvailableStudyTimeResponse, + StudyDashboardResponse, StudySubjectResponse, TechStackResponse, UpdateUserProfileInfoRequest, @@ -49,3 +50,9 @@ export const getTechStacks = async (): Promise => { return res.data.content; }; + +export const getStudyDashboard = async (): Promise => { + const res = await axiosInstance.get('/study/dashboard'); + + return res.data.content; +}; diff --git a/src/features/my-page/model/use-update-user-profile-mutation.ts b/src/features/my-page/model/use-update-user-profile-mutation.ts index 62cbedbd..d49cdaf7 100644 --- a/src/features/my-page/model/use-update-user-profile-mutation.ts +++ b/src/features/my-page/model/use-update-user-profile-mutation.ts @@ -6,6 +6,7 @@ import { } from '../api/types'; import { getAvailableStudyTimes, + getStudyDashboard, getStudySubjects, getTechStacks, updateUserProfile, @@ -59,3 +60,11 @@ export const useTechStacksQuery = () => { queryFn: getTechStacks, }); }; + +export const useStudyDashboardQuery = () => { + return useQuery({ + queryKey: ['studyDashboard'], + queryFn: () => getStudyDashboard(), + staleTime: 60 * 1000, + }); +};