diff --git a/app/(my)/layout.tsx b/app/(my)/layout.tsx index 76a6fae5..c4c054fd 100644 --- a/app/(my)/layout.tsx +++ b/app/(my)/layout.tsx @@ -14,7 +14,9 @@ export default function MyLayout({ return (
-
{children}
+
+ {children} +
); } diff --git a/app/global.css b/app/global.css index bcbecc55..74119091 100644 --- a/app/global.css +++ b/app/global.css @@ -185,7 +185,7 @@ https://velog.io/@oneook/tailwindcss-4.0-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%E --color-background-alternative: var(--color-gray-50); --color-background-default: var(--color-gray-0); - --color-background-dimmer: rgba(24, 29, 39, 80); + --color-background-dimmer: rgba(24, 29, 39, 0.8); --color-background-disabled: var(--color-gray-100); --color-background-brand-subtle: var(--color-rose-300); diff --git a/app/not-found.tsx b/app/not-found.tsx index 805eb852..48151d31 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,10 +1,11 @@ +import Image from 'next/image'; import Link from 'next/link'; import Button from '@/shared/ui/button'; export default function NotFound() { return (
- + 404 에러
-
- - -
diff --git a/src/features/auth/ui/sign-up-image-selector.tsx b/src/features/auth/ui/sign-up-image-selector.tsx index 4e7fe757..8a8f9ce0 100644 --- a/src/features/auth/ui/sign-up-image-selector.tsx +++ b/src/features/auth/ui/sign-up-image-selector.tsx @@ -14,13 +14,13 @@ export default function SignupImageSelector({ handleImageChange: (event: React.ChangeEvent) => void; }) { const setDefaultImage = () => setImage('/profile-default.svg'); - const openFileDialog = () => fileInputRef.current?.click(); + const openFileFolder = () => fileInputRef.current?.click(); return (
프로필 - next최적화안쓴프로필 + next최적화안쓴프로필
@@ -44,7 +44,7 @@ export default function SignupImageSelector({ 앨범에서 선택 @@ -53,8 +53,9 @@ export default function SignupImageSelector({
diff --git a/src/features/auth/ui/sign-up-modal.tsx b/src/features/auth/ui/sign-up-modal.tsx index ebf64fae..e2b8e15b 100644 --- a/src/features/auth/ui/sign-up-modal.tsx +++ b/src/features/auth/ui/sign-up-modal.tsx @@ -1,3 +1,4 @@ +import { sendGTMEvent } from '@next/third-parties/google'; import { XIcon } from 'lucide-react'; import { useState, useRef } from 'react'; import { @@ -5,12 +6,11 @@ import { useUploadProfileImageMutation, } from '@/features/auth/model/use-auth-mutation'; import SignupImageSelector from '@/features/auth/ui/sign-up-image-selector'; -import SignupNameInput from '@/features/auth/ui/sign-up-name-input'; +import { hashValue } from '@/shared/lib/hash'; import { getCookie, setCookie } from '@/shared/tanstack-query/cookie'; import Button from '@/shared/ui/button'; +import { BaseInput } from '@/shared/ui/input'; import { Modal } from '@/shared/ui/modal'; -import { sendGTMEvent } from '@next/third-parties/google'; -import { hashValue } from '@/shared/lib/hash'; export default function SignupModal({ open, @@ -20,7 +20,7 @@ export default function SignupModal({ onClose: () => void; }) { const [name, setName] = useState(''); - const [error, setError] = useState(''); + const [checked, setChecked] = useState(false); const [image, setImage] = useState( getCookie('socialImageURL') || 'profile-default.svg', ); @@ -29,15 +29,7 @@ export default function SignupModal({ const signUp = useSignUpMutation(); const uploadProfileImage = useUploadProfileImageMutation(); - // 이름 유효성 검사 - const validateName = (value: string) => { - if (!/^[가-힣a-zA-Z]{2,10}$/.test(value)) { - setError('이름에는 숫자나 특수문자를 사용할 수 없습니다.'); - } else { - setError(''); - } - setName(value); - }; + const isValidName = /^[가-힣a-zA-Z]{2,10}$/.test(name); // 이미지 업로드 const handleImageChange = (e: React.ChangeEvent) => { @@ -60,12 +52,11 @@ export default function SignupModal({ { // 회원가입 성공 시 프로필 이미지 업로드 onSuccess: (data) => { - const memberId = data.content.generatedMemberId; if (data && memberId) { - setCookie('memberId', memberId) - + setCookie('memberId', memberId); + // 회원가입 GA 이벤트 전송 sendGTMEvent({ event: 'custom_member_join', @@ -85,7 +76,6 @@ export default function SignupModal({ file: formData, }); } - // 성공 후 홈페이지로 이동 window.location.href = '/'; @@ -124,17 +114,56 @@ export default function SignupModal({
서비스 이용을 위해 이름을 입력해주세요.
- +
+
+ { + const value = e.target.value; + const filteredValue = value + .replace(/[^a-zA-Zㄱ-힣]/g, '') + .slice(0, 10) + .trim(); + setName(filteredValue); + }} + placeholder="홍길동" + className={`w-full`} + color={ + isValidName || name.length === 0 ? 'default' : 'error' + } + maxLength={10} + /> +
+ {isValidName || name.length === 0 + ? '신뢰 있는 매칭을 위해 실명을 사용해주세요. (예: 홍길동 )' + : '이름에는 숫자나 특수문자를 사용할 수 없습니다. 두 글자 이상 입력해주세요.'} +
+
+
+ setChecked(e.target.checked)} + /> + +
+
diff --git a/src/features/auth/ui/sign-up-name-input.tsx b/src/features/auth/ui/sign-up-name-input.tsx deleted file mode 100644 index 96e1ae6d..00000000 --- a/src/features/auth/ui/sign-up-name-input.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { BaseInput } from '@/shared/ui/input'; - -export default function SignupNameInput({ - name, - setName, - error, -}: { - name: string; - setName: (name: string) => void; - error: string; -}) { - return ( -
- setName(e.target.value)} - placeholder="최예림" - className={`w-full`} - color={error ? 'error' : 'default'} - /> -
- {error - ? '이름에는 숫자나 특수문자를 사용할 수 없습니다. 두 글자 이상 입력해주세요.' - : '신뢰 있는 매칭을 위해 실명을 사용해주세요. (예: 홍길동 )'} -
-
- ); -} diff --git a/src/features/my-page/consts/my-page-const.ts b/src/features/my-page/consts/my-page-const.ts index 15b20a72..5f909266 100644 --- a/src/features/my-page/consts/my-page-const.ts +++ b/src/features/my-page/consts/my-page-const.ts @@ -29,3 +29,5 @@ export const MBTI_OPTIONS = [ { label: 'ENFJ', value: 'ENFJ' }, { label: 'ENTJ', value: 'ENTJ' }, ]; + +export const DEFAULT_PROFILE_IMAGE_URL = '/profile-default.svg'; diff --git a/public/icons/Cake.svg b/src/features/my-page/ui/icon/cake.svg similarity index 100% rename from public/icons/Cake.svg rename to src/features/my-page/ui/icon/cake.svg diff --git a/public/icons/GithubLogo.svg b/src/features/my-page/ui/icon/github-logo.svg similarity index 100% rename from public/icons/GithubLogo.svg rename to src/features/my-page/ui/icon/github-logo.svg diff --git a/public/icons/GlobeSimple.svg b/src/features/my-page/ui/icon/globe-simple.svg similarity index 100% rename from public/icons/GlobeSimple.svg rename to src/features/my-page/ui/icon/globe-simple.svg diff --git a/public/icons/Phone.svg b/src/features/my-page/ui/icon/phone.svg similarity index 100% rename from public/icons/Phone.svg rename to src/features/my-page/ui/icon/phone.svg diff --git a/src/features/my-page/ui/profile-edit-modal.tsx b/src/features/my-page/ui/profile-edit-modal.tsx index 02cdb8a0..dd6a4500 100644 --- a/src/features/my-page/ui/profile-edit-modal.tsx +++ b/src/features/my-page/ui/profile-edit-modal.tsx @@ -10,57 +10,84 @@ 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'; +import { + DEFAULT_OPTIONS, + DEFAULT_PROFILE_IMAGE_URL, + MBTI_OPTIONS, +} from '../consts/my-page-const'; +import { useUpdateUserProfileMutation } from '../model/use-update-user-profile-mutation'; interface Props { - onSubmit: (formData: UpdateUserProfileRequest) => void; memberProfile: MemberProfile; memberId: number; } export type MbtiValue = (typeof MBTI_OPTIONS)[number]['value']; -export default function ProfileEditModal({ - onSubmit, - memberProfile, - memberId, -}: Props) { - const [name, setName] = useState( - memberProfile.memberName ?? '', - ); - const [tel, setTel] = useState( - memberProfile.tel ?? '', - ); - const [githubLink, setGithubLink] = useState< - UpdateUserProfileRequest['githubLink'] - >(memberProfile.githubLink?.url ?? ''); - - const [blogOrSnsLink, setBlogOrSnsLink] = useState< - UpdateUserProfileRequest['blogOrSnsLink'] - >(memberProfile.blogOrSnsLink?.url ?? ''); +export default function ProfileEditModal({ memberProfile, memberId }: Props) { + const [isOpen, setIsOpen] = useState(false); - const [mbti, setMbti] = useState( - memberProfile.mbti ?? '', + return ( + + setIsOpen(true)} + className="rounded-100 bg-fill-brand-default-default font-designer-16b text-text-inverse w-full px-150 py-100" + > + 내 프로필 수정 + + + + + +
+ 내 프로필 수정 + + + +
+
+ setIsOpen(false)} + /> +
+
+
); +} - const [simpleIntroduction, setSimpleIntroduction] = useState< - UpdateUserProfileRequest['simpleIntroduction'] - >(memberProfile.simpleIntroduction ?? ''); - - const [interests, setInterests] = useState< - UpdateUserProfileRequest['interests'] - >(memberProfile.interests?.map((item) => item.name) ?? []); +function ProfileEditForm({ + memberProfile, + memberId, + onClose, +}: Props & { onClose: () => void }) { + const fileInputRef = useRef(null); + const [profileForm, setProfileForm] = useState({ + name: memberProfile.memberName ?? '', + tel: memberProfile.tel ?? '', + githubLink: memberProfile.githubLink?.url ?? '', + blogOrSnsLink: memberProfile.blogOrSnsLink?.url ?? '', + mbti: memberProfile.mbti ?? '', + simpleIntroduction: memberProfile.simpleIntroduction ?? '', + interests: memberProfile.interests?.map((item) => item.name) ?? [], + }); const [image, setImage] = useState( memberProfile.profileImage?.resizedImages?.[0]?.resizedImageUrl ?? - '/profile-default.svg', + DEFAULT_PROFILE_IMAGE_URL, ); - const [isOpen, setIsOpen] = useState(false); - const fileInputRef = useRef(null); - const uploadProfileImage = useUploadProfileImageMutation(); + // 이름 유효성 검사: 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 queryClient = useQueryClient(); + const { mutateAsync: updateProfile } = useUpdateUserProfileMutation(memberId); + const { mutateAsync: uploadProfileImage } = useUploadProfileImageMutation(); const handleImageChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { @@ -70,45 +97,50 @@ export default function ProfileEditModal({ }; const handleSubmit = async () => { - if (!name || !tel) { - alert('모든 필수 정보를 입력해주세요!'); - - return; - } - 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; + image === DEFAULT_PROFILE_IMAGE_URL ? 'jpg' : file?.name.split('.').pop(); const rawFormData: UpdateUserProfileRequest = { - name, - tel, - githubLink: githubLink.trim() || undefined, - blogOrSnsLink: blogOrSnsLink.trim() || undefined, - simpleIntroduction: simpleIntroduction.trim() || undefined, - mbti: mbti.trim() || undefined, - interests: interests.length > 0 ? interests : undefined, - profileImageExtension: hasImageFile ? profileImageExtension : undefined, + name: profileForm.name, + tel: profileForm.tel, + githubLink: profileForm.githubLink, + blogOrSnsLink: profileForm.blogOrSnsLink, + simpleIntroduction: profileForm.simpleIntroduction.trim() || undefined, + mbti: profileForm.mbti || undefined, + interests: + profileForm.interests.length > 0 ? profileForm.interests : undefined, + profileImageExtension: profileImageExtension, }; const formData = Object.fromEntries( Object.entries(rawFormData).filter(([_, v]) => v !== undefined), ) as UpdateUserProfileRequest; - const updated = await updateUserProfile(memberId, formData); + const updatedProfile = await updateProfile(formData); - if (fileInputRef.current?.files?.[0] && updated.profileImageUploadUrl) { + if (updatedProfile.profileImageUploadUrl) { const imageFormData = new FormData(); - imageFormData.append('file', fileInputRef.current.files[0]); - const filename = updated.profileImageUploadUrl.split('/').pop(); + if (file) imageFormData.append('file', file); + + // 기본 프로필 이미지를 선택할 경우, public 폴더의 profile-default.jpg로 서버 api에게 기본 프로필 이미지 전송 -> 서버 측에서 나중에 리팩토링 예정 + // profile-default.jpg를 추가한 이유는 서버에서 프로필 이미지 확장자에 svg 파일을 고려하지 못함 -> 서버 측에서 리팩토링 예정 + if (!file && image === DEFAULT_PROFILE_IMAGE_URL) { + const defaultProfileImage = 'profile-default.jpg'; + const response = await fetch(defaultProfileImage); + const blob = await response.blob(); + const defaultProfileFile = new File([blob], defaultProfileImage, { + type: 'image/jpg', + }); + imageFormData.append('file', defaultProfileFile); + } + + const filename = updatedProfile.profileImageUploadUrl.split('/').pop(); if (!filename) return; try { - await uploadProfileImage.mutateAsync({ + await uploadProfileImage({ memberId, filename, file: imageFormData, @@ -118,119 +150,132 @@ export default function ProfileEditModal({ alert('이미지 업로드에 실패했습니다.'); } } - await queryClient.invalidateQueries({ queryKey: ['memberInfo'] }); - onSubmit(formData); + await queryClient.invalidateQueries({ + queryKey: ['userProfile', memberId], + }); }; return ( - - setIsOpen(true)} - className="rounded-100 bg-fill-brand-default-default font-designer-16b text-text-inverse w-full px-150 py-100" - > - 내 프로필 수정 - - - - - -
- 내 프로필 수정 - - - -
-
- -
-
-
- 이미지 설정 -
- -
- - - - - - - -
-
- -
- - -
-
-
-
-
+ <> + +
+
+
이미지 설정
+ +
+ { + // 공백 입력하지 못하도록 제한 + setProfileForm({ + ...profileForm, + name: value.replace(/\s/g, ''), + }); + }} + required + /> + { + // 숫자와 하이픈(-)만 입력 허용 + const onlyNumberAndHyphen = value.replace(/[^\d-]/g, ''); + setProfileForm({ ...profileForm, tel: onlyNumberAndHyphen }); + }} + required + /> + + setProfileForm({ + ...profileForm, + githubLink: value.replace(/\s/g, ''), + }) + } + /> + + setProfileForm({ ...profileForm, mbti: value }) + } + options={MBTI_OPTIONS} + /> + + setProfileForm({ ...profileForm, interests: value }) + } + options={DEFAULT_OPTIONS} + /> + + setProfileForm({ ...profileForm, simpleIntroduction: value }) + } + /> + + setProfileForm({ + ...profileForm, + blogOrSnsLink: value.replace(/\s/g, ''), + }) + } + /> +
+
+ +
+ + +
+
+ ); } diff --git a/src/features/my-page/ui/profile-info-edit-modal.tsx b/src/features/my-page/ui/profile-info-edit-modal.tsx index 29b6247a..f1ed5a2b 100644 --- a/src/features/my-page/ui/profile-info-edit-modal.tsx +++ b/src/features/my-page/ui/profile-info-edit-modal.tsx @@ -5,7 +5,6 @@ import { XIcon } from 'lucide-react'; import { useState } from 'react'; import { MemberInfo } from '@/entities/user/api/types'; import { hashValue } from '@/shared/lib/hash'; -import { getCookie } from '@/shared/tanstack-query/cookie'; import Button from '@/shared/ui/button'; import { FormField } from '@/shared/ui/form/form-field'; import { Modal } from '@/shared/ui/modal'; @@ -14,50 +13,20 @@ import { useAvailableStudyTimesQuery, useStudySubjectsQuery, useTechStacksQuery, + useUpdateUserProfileInfoMutation, } from '../model/use-update-user-profile-mutation'; interface Props { + memberId: number; memberInfo: MemberInfo; - onSubmit: (formData: UpdateUserProfileInfoRequest) => void; } -export default function ProfileInfoEditModal({ memberInfo, onSubmit }: Props) { - const [selfIntroduction, setSelfIntroduction] = useState< - UpdateUserProfileInfoRequest['selfIntroduction'] - >(memberInfo.selfIntroduction ?? ''); - const [studyPlan, setStudyPlan] = useState< - UpdateUserProfileInfoRequest['studyPlan'] - >(memberInfo.studyPlan ?? ''); - const [preferredStudySubjectId, setPreferredStudySubjectId] = useState< - UpdateUserProfileInfoRequest['preferredStudySubjectId'] - >(memberInfo.preferredStudySubject?.studySubjectId ?? undefined); - const [availableTimeSlots, setAvailableTimeSlots] = useState< - UpdateUserProfileInfoRequest['availableStudyTimeIds'] - >((memberInfo.availableStudyTimes ?? []).map((time) => time?.id ?? 0)); - const [selectedSkills, setSelectedSkills] = useState< - UpdateUserProfileInfoRequest['techStackIds'] - >((memberInfo.techStacks ?? []).map((tech) => tech?.techStackId ?? 0)); - - const handleSubmit = () => { - const formData: UpdateUserProfileInfoRequest = { - selfIntroduction, - studyPlan, - preferredStudySubjectId: preferredStudySubjectId, - availableStudyTimeIds: availableTimeSlots - .filter((id) => id) - .map((id) => Number(id)), - techStackIds: selectedSkills.filter((id) => id).map((id) => Number(id)), - }; - onSubmit(formData); - }; - - const { data: availableStudyTimes } = useAvailableStudyTimesQuery(); - const { data: studySubjects } = useStudySubjectsQuery(); - const { data: techStacks } = useTechStacksQuery(); +export default function ProfileInfoEditModal({ memberId, memberInfo }: Props) { + const [isOpen, setIsOpen] = useState(false); return ( - - + + setIsOpen(true)}>
편집
@@ -73,106 +42,190 @@ export default function ProfileInfoEditModal({ memberInfo, onSubmit }: Props) { - -
- - - ({ - value: studySubjectId, - label: name, - })) ?? [] - } - /> - ({ - value: availableTimeId.toString(), - label: display, - })) ?? [] - } - onChange={(availableStudyTimeIds) => - setAvailableTimeSlots(availableStudyTimeIds.map(Number)) - } - required - /> - ({ - value: techStackId, - label: techStackName, - }), - )} - /> -
-
- - -
- - -
-
-
+ setIsOpen(false)} + />
); } + +function ProfileInfoEditForm({ + memberId, + memberInfo, + onClose, +}: { + memberId: number; + memberInfo: MemberInfo; + onClose: () => void; +}) { + const [infoForm, setInfoForm] = useState({ + selfIntroduction: memberInfo.selfIntroduction ?? '', + studyPlan: memberInfo.studyPlan ?? '', + preferredStudySubjectId: memberInfo.preferredStudySubject?.studySubjectId, + availableStudyTimeIds: (memberInfo.availableStudyTimes ?? []).map( + (time) => time?.id ?? 0, + ), + techStackIds: (memberInfo.techStacks ?? []).map( + (tech) => tech?.techStackId ?? 0, + ), + }); + + const { data: availableStudyTimes } = useAvailableStudyTimesQuery(); + const { data: studySubjects } = useStudySubjectsQuery(); + const { data: techStacks } = useTechStacksQuery(); + const { mutate: updateProfileInfo } = + useUpdateUserProfileInfoMutation(memberId); + + const handleSubmit = () => { + const formData: UpdateUserProfileInfoRequest = { + selfIntroduction: infoForm.selfIntroduction, + studyPlan: infoForm.studyPlan, + preferredStudySubjectId: infoForm.preferredStudySubjectId, + availableStudyTimeIds: infoForm.availableStudyTimeIds + .filter((id) => id) + .map((id) => Number(id)), + techStackIds: infoForm.techStackIds + .filter((id) => id) + .map((id) => Number(id)), + }; + + updateProfileInfo(formData, { + onSuccess: () => { + const selectedSkillNames = techStacks.filter((techStack) => + infoForm.techStackIds.includes(techStack.techStackId), + ); + + sendGTMEvent({ + event: 'custom_member_card', + dl_timestamp: new Date().toISOString(), + dl_member_id: hashValue(String(memberId)), + dl_tags: selectedSkillNames, + }); + + onClose(); + }, + }); + }; + + return ( + <> + +
+ + setInfoForm((prev) => ({ + ...prev, + selfIntroduction: value, + })) + } + direction="vertical" + maxLength={500} + /> + + setInfoForm((prev) => ({ + ...prev, + studyPlan: value, + })) + } + direction="vertical" + maxLength={500} + required + /> + + setInfoForm((prev) => ({ + ...prev, + preferredStudySubjectId: value, + })) + } + direction="vertical" + required + options={ + studySubjects?.map(({ studySubjectId, name }) => ({ + value: studySubjectId, + label: name, + })) ?? [] + } + /> + ({ + value: availableTimeId.toString(), + label: display, + })) ?? [] + } + onChange={(availableStudyTimeIds) => + setInfoForm((prev) => ({ + ...prev, + availableStudyTimeIds: availableStudyTimeIds.map(Number), + })) + } + required + /> + + setInfoForm((prev) => ({ + ...prev, + techStackIds: value, + })) + } + direction="vertical" + required + options={(techStacks ?? []).map( + ({ techStackId, techStackName }) => ({ + value: techStackId, + label: techStackName, + }), + )} + /> +
+
+ +
+ + + +
+
+ + ); +} diff --git a/src/features/my-page/ui/profile-info.tsx b/src/features/my-page/ui/profile-info.tsx index 4ba127ac..93e87afe 100644 --- a/src/features/my-page/ui/profile-info.tsx +++ b/src/features/my-page/ui/profile-info.tsx @@ -3,8 +3,6 @@ import { MemberInfo } from '@/entities/user/api/types'; import ProfileInfoEditModal from '@/features/my-page/ui/profile-info-edit-modal'; import ProfileInfoCard from '@/widgets/my-page/profileinfo-card'; -import { UpdateUserProfileInfoRequest } from '../api/types'; -import { useUpdateUserProfileInfoMutation } from '../model/use-update-user-profile-mutation'; interface ProfileInfoProps { memberId: number; @@ -15,21 +13,12 @@ export default function ProfileInfo({ memberId, memberInfo, }: ProfileInfoProps) { - const { mutate } = useUpdateUserProfileInfoMutation(memberId); - - const handleSubmit = (formData: UpdateUserProfileInfoRequest) => { - mutate(formData); - }; - return (
내 정보
- +
{ - mutate(formData); - }; - return (
-
- Profile +
+ {memberProfile.birthDate ?? '생일을 입력해주세요!'}
-
- Profile +
+ {memberProfile.tel ?? '번호를 입력해주세요!'}
-
- Profile - {memberProfile.githubLink?.url ?? '깃허브 링크를 입력해주세요!'} +
+ + {memberProfile.githubLink?.url || '깃허브 링크를 입력해주세요!'}
-
- Profile - {memberProfile.blogOrSnsLink?.url ?? +
+ + {memberProfile.blogOrSnsLink?.url || '블로그 링크를 입력해주세요!'}
- +
); diff --git a/src/features/my-page/ui/user-profile-modal.tsx b/src/features/my-page/ui/user-profile-modal.tsx index 9ecdb593..0cd0f824 100644 --- a/src/features/my-page/ui/user-profile-modal.tsx +++ b/src/features/my-page/ui/user-profile-modal.tsx @@ -1,8 +1,11 @@ 'use client'; import { XIcon } from 'lucide-react'; -import Image from 'next/image'; import { useUserProfileQuery } from '@/entities/user/model/use-user-profile-query'; +import CakeIcon from '@/features/my-page/ui/icon/cake.svg'; +import GithubIcon from '@/features/my-page/ui/icon/github-logo.svg'; +import GlobeIcon from '@/features/my-page/ui/icon/globe-simple.svg'; +import PhoneIcon from '@/features/my-page/ui/icon/phone.svg'; import UserAvatar from '@/shared/ui/avatar'; import Badge from '@/shared/ui/badge'; import { Modal } from '@/shared/ui/modal'; @@ -66,45 +69,25 @@ export default function UserProfileModal({
-
- Profile +
+ {profile.memberProfile.birthDate ?? '생일을 입력해주세요!'}
-
- Profile +
+ {profile.memberProfile.tel ?? '번호를 입력해주세요!'}
-
- Profile +
+ {profile.memberProfile.githubLink?.url ?? '깃허브 링크를 입력해주세요!'}
-
- Profile +
+ {profile.memberProfile.blogOrSnsLink?.url ?? '블로그 링크를 입력해주세요!'}
diff --git a/src/features/study/api/get-study-data.ts b/src/features/study/api/get-study-data.ts index 07cc4932..570e7b4e 100644 --- a/src/features/study/api/get-study-data.ts +++ b/src/features/study/api/get-study-data.ts @@ -1,27 +1,23 @@ import type { + CompleteStudyRequest, DailyStudyDetail, GetDailyStudiesParams, GetDailyStudiesResponse, - GetDailyStudyDetailParams, GetDailyStudyDetailParams2, GetMonthlyCalendarParams, JoinStudyRequest, MonthlyCalendarResponse, PostDailyRetrospectRequest, - PutRetrospectRequest, - PutStudyDailyRequest, - StudyProgressStatus, + PrepareStudyRequest, WeeklyParticipationResponse, } from '@/features/study/api/types'; import { axiosInstance } from '@/shared/tanstack-query/axios'; // 스터디 상세 조회 export const getDailyStudyDetail = async ( - params: GetDailyStudyDetailParams, + params: string, ): Promise => { - const { studyDate } = params; - - const res = await axiosInstance.get(`/study/daily/mine/${studyDate}`); + const res = await axiosInstance.get(`/study/daily/mine/${params}`); return res.data.content; }; @@ -50,39 +46,23 @@ export const postDailyRetrospect = async (body: PostDailyRetrospectRequest) => { return res.data; }; -// 피면접자 스터디 업데이트 +// 면접 준비 시작 export const putStudyDaily = async ( dailyId: number, - body: PutStudyDailyRequest, + body: PrepareStudyRequest, ) => { - const res = await axiosInstance.put(`/study/daily/${dailyId}`, body); + const res = await axiosInstance.put(`/study/daily/${dailyId}/prepare`, body); return res.data; }; -// 면접자 스터디 업데이트 [스터디 진행 상태] -export const patchStudyStatus = async ( +// 면접 완료 및 회고 작성 +export const completeStudy = async ( dailyStudyId: number, - progressStatus: StudyProgressStatus, -) => { - const res = await axiosInstance.patch( - `/study/daily/${dailyStudyId}/status`, - null, - { - params: { progressStatus }, - }, - ); - - return res.data; -}; - -// 면접자 스터디 업데이트 [피드백] -export const putRetrospect = async ( - retrospectId: number, - body: PutRetrospectRequest, + body: CompleteStudyRequest, ) => { - const res = await axiosInstance.put( - `/study/daily/retrospect/${retrospectId}`, + const res = await axiosInstance.post( + `/study/daily/${dailyStudyId}/complete`, body, ); diff --git a/src/features/study/api/types.ts b/src/features/study/api/types.ts index 978f3503..7dbf0b62 100644 --- a/src/features/study/api/types.ts +++ b/src/features/study/api/types.ts @@ -34,10 +34,6 @@ export interface DailyStudyDetail { feedback: string; } -export interface GetDailyStudyDetailParams { - studyDate: string; -} - export interface GetDailyStudyDetailParams2 { year: number; month: number; @@ -47,9 +43,7 @@ export interface GetDailyStudyDetailParams2 { export interface GetDailyStudiesParams { cursor?: number; pageSize?: number; - year?: number; - month?: number; - day?: number; + studyDate?: string; } export interface GetDailyStudiesResponse { @@ -80,9 +74,8 @@ export interface PostDailyRetrospectRequest { parentId: number; } -export interface PutStudyDailyRequest { +export interface PrepareStudyRequest { subject: string; - description: string; link: string; } @@ -103,6 +96,7 @@ export interface WeeklyParticipationResponse { isParticipate: boolean; } -export interface PutRetrospectRequest { - description: string; +export interface CompleteStudyRequest { + feedback: string; + progressStatus: StudyProgressStatus; } diff --git a/src/features/study/model/use-study-query.ts b/src/features/study/model/use-study-query.ts index 091764ca..72afc479 100644 --- a/src/features/study/model/use-study-query.ts +++ b/src/features/study/model/use-study-query.ts @@ -1,20 +1,24 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { + completeStudy, getDailyStudies, getDailyStudyDetail, getMonthlyStudyCalendar, getWeeklyParticipation, postJoinStudy, + putStudyDaily, } from '@/features/study/api/get-study-data'; import { + CompleteStudyRequest, GetDailyStudiesParams, - GetDailyStudyDetailParams, GetDailyStudyDetailParams2, GetMonthlyCalendarParams, JoinStudyRequest, MonthlyCalendarResponse, + PrepareStudyRequest, } from '../api/types'; +// 스터디 주간 참여 유무 확인 query export const useWeeklyParticipation = (params: GetDailyStudyDetailParams2) => { return useQuery({ queryKey: ['weeklyParticipation', params], @@ -23,18 +27,17 @@ export const useWeeklyParticipation = (params: GetDailyStudyDetailParams2) => { }); }; -export const useDailyStudyDetailQuery = ( - params: GetDailyStudyDetailParams, - enabled: boolean = true, -) => { +// 스터디 상세 조회 query +export const useDailyStudyDetailQuery = (params: string) => { return useQuery({ queryKey: ['dailyStudyDetail', params], queryFn: () => getDailyStudyDetail(params), staleTime: 60 * 1000, - enabled: enabled && !!params, + enabled: !!params, }); }; +// 스터디 전체 조회 query export const useDailyStudiesQuery = (params?: GetDailyStudiesParams) => { return useQuery({ queryKey: ['dailyStudies', params], @@ -43,6 +46,7 @@ export const useDailyStudiesQuery = (params?: GetDailyStudiesParams) => { }); }; +// 스터디 캘린더 조회 query export const useMonthlyStudyCalendarQuery = ( params: GetMonthlyCalendarParams, ) => { @@ -54,8 +58,42 @@ export const useMonthlyStudyCalendarQuery = ( }); }; +// 스터디 신청 mutation export const useJoinStudyMutation = () => { return useMutation({ mutationFn: (payload: JoinStudyRequest) => postJoinStudy(payload), }); }; + +// 스터디 상세 & 리스트 업데이트 +interface UpdateDailyStudyVariables { + dailyStudyId: number; + studyDate: string; + form: PrepareStudyRequest | CompleteStudyRequest; + requestType: 'prepare' | 'complete'; +} + +export const useUpdateDailyStudyMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ dailyStudyId, form, requestType }) => { + if (requestType === 'prepare') { + await putStudyDaily(dailyStudyId, form as PrepareStudyRequest); + } else { + await completeStudy(dailyStudyId, form as CompleteStudyRequest); + } + }, + onSuccess: async (_data, { studyDate }) => { + await queryClient.invalidateQueries({ + queryKey: ['dailyStudyDetail', studyDate], + exact: true, + }); + + await queryClient.invalidateQueries({ + queryKey: ['dailyStudies', { studyDate }], + exact: false, + }); + }, + }); +}; diff --git a/src/features/study/ui/start-study-modal.tsx b/src/features/study/ui/start-study-modal.tsx index bbf66317..e411b252 100644 --- a/src/features/study/ui/start-study-modal.tsx +++ b/src/features/study/ui/start-study-modal.tsx @@ -321,6 +321,7 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { }, { onSuccess: () => { + alert('스터디 신청이 완료되었습니다!'); router.refresh(); }, onError: () => { diff --git a/src/features/study/ui/status-badge-map.tsx b/src/features/study/ui/status-badge-map.tsx new file mode 100644 index 00000000..19cbaa63 --- /dev/null +++ b/src/features/study/ui/status-badge-map.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from 'react'; +import Badge from '@/shared/ui/badge'; +import { StudyProgressStatus } from '../api/types'; + +export function getStatusBadge(status: StudyProgressStatus): ReactNode { + switch (status) { + case 'PENDING': + return 시작 전; + case 'IN_PROGRESS': + return 진행중; + case 'COMPLETE': + return 완료; + case 'ABSENT': + return 불참; + default: + return null; + } +} diff --git a/src/features/study/ui/study-card.tsx b/src/features/study/ui/study-card.tsx index 4ce50232..6cb4faa4 100644 --- a/src/features/study/ui/study-card.tsx +++ b/src/features/study/ui/study-card.tsx @@ -5,10 +5,7 @@ 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, - useWeeklyParticipation, -} from '../model/use-study-query'; +import { useWeeklyParticipation } from '../model/use-study-query'; // 스터디 주차 구하는 함수 function getWeekly(date: Date): { month: number; week: number } { @@ -62,15 +59,14 @@ export default function StudyCard() { day: selectedDate.getDate(), }; - const studyParams = { studyDate: selectedDate.toISOString().split('T')[0] }; + const offset = selectedDate.getTimezoneOffset() * 60000; // ms단위라 60000곱해줌 + const dateOffset = new Date(selectedDate.getTime() - offset); + + const studyDate = dateOffset.toISOString().split('T')[0]; const { data: participationData } = useWeeklyParticipation(params); const isParticipate = participationData?.isParticipate ?? false; - const { data: todayStudyData, refetch } = useDailyStudyDetailQuery( - studyParams, - isParticipate, - ); const { month, week } = getWeekly(selectedDate); return ( @@ -80,10 +76,8 @@ export default function StudyCard() {
- {isParticipate && todayStudyData && ( - - )} - + {isParticipate && } +
); diff --git a/src/features/study/ui/study-done-modal.tsx b/src/features/study/ui/study-done-modal.tsx new file mode 100644 index 00000000..6224f2d3 --- /dev/null +++ b/src/features/study/ui/study-done-modal.tsx @@ -0,0 +1,163 @@ +'use client'; + +import { XIcon } from 'lucide-react'; +import { useState } from 'react'; +import Button from '@/shared/ui/button'; +import { SingleDropdown } from '@/shared/ui/dropdown'; +import { TextAreaInput } from '@/shared/ui/input'; +import { Modal } from '@/shared/ui/modal'; +import { + CompleteStudyRequest, + DailyStudyDetail, + StudyProgressStatus, +} from '../api/types'; +import { STUDY_PROGRESS_OPTIONS } from '../consts/study-const'; +import { useUpdateDailyStudyMutation } from '../model/use-study-query'; + +interface StudyDoneModalProps { + data: DailyStudyDetail; + studyDate: string; +} + +export default function StudyDoneModal({ + data, + studyDate, +}: StudyDoneModalProps) { + const [isOpen, setIsOpen] = useState(false); + + return ( + + + + + + + + 면접 완료하기 + + + + + + setIsOpen(false)} + /> + + + + ); +} + +interface StudyDoneFormProps { + data: DailyStudyDetail; + studyDate: string; + onClose: () => void; +} + +function StudyDoneForm({ data, studyDate, onClose }: StudyDoneFormProps) { + const [form, setForm] = useState({ + feedback: data.feedback ?? '', + progressStatus: data.progressStatus ?? 'PENDING', + }); + + const { mutate, isPending } = useUpdateDailyStudyMutation(); + const { feedback, progressStatus } = form; + + const handleChange = (key: keyof CompleteStudyRequest) => (value: string) => { + setForm((prev) => ({ ...prev, [key]: value })); + }; + + const handleSubmit = async () => { + if (!feedback.trim() || !progressStatus) return; + + mutate( + { + dailyStudyId: data.dailyStudyId, + studyDate, + form, + requestType: 'complete', + }, + { + onSuccess: onClose, + onError: (err) => { + console.error(err); + alert('요청 처리에 실패했습니다. 다시 시도해주세요.'); + }, + }, + ); + }; + + return ( + <> + +
+
+ + + 면접 완료 후 해당 지원자의 상태를 업데이트해 주세요. + +
+ + + handleChange('progressStatus')(value as StudyProgressStatus) + } + /> +
+ +
+
+ + + 면접 결과에 대한 간단한 피드백을 입력해 주세요. + +
+ + handleChange('feedback')(value)} + /> +
+
+ + +
+ + +
+
+ + ); +} diff --git a/src/features/study/ui/study-ready-modal.tsx b/src/features/study/ui/study-ready-modal.tsx new file mode 100644 index 00000000..2926eb22 --- /dev/null +++ b/src/features/study/ui/study-ready-modal.tsx @@ -0,0 +1,146 @@ +'use client'; + +import { XIcon } from 'lucide-react'; +import { useState } from 'react'; +import Button from '@/shared/ui/button'; +import { BaseInput } from '@/shared/ui/input'; +import { Modal } from '@/shared/ui/modal'; +import { DailyStudyDetail, PrepareStudyRequest } from '../api/types'; +import { useUpdateDailyStudyMutation } from '../model/use-study-query'; + +interface StudyReadyModalProps { + data: DailyStudyDetail; + studyDate: string; +} + +export default function StudyReadyModal({ + data, + studyDate, +}: StudyReadyModalProps) { + const [isOpen, setIsOpen] = useState(false); + + return ( + + + + + + + + + + 면접 준비하기 + + + + + + setIsOpen(false)} + /> + + + + ); +} + +interface StudyReadyFormProps { + data: DailyStudyDetail; + studyDate: string; + onClose: () => void; +} + +function StudyReadyForm({ data, studyDate, onClose }: StudyReadyFormProps) { + const [form, setForm] = useState({ + subject: data.subject ?? '', + link: data.link ?? '', + }); + + const { mutate, isPending } = useUpdateDailyStudyMutation(); + const { subject, link } = form; + + const handleChange = (key: keyof PrepareStudyRequest) => (value: string) => { + setForm((prev) => ({ ...prev, [key]: value })); + }; + + const handleSubmit = async () => { + if (!subject.trim()) return; + + mutate( + { + dailyStudyId: data.dailyStudyId, + studyDate, + form, + requestType: 'prepare', + }, + { + onSuccess: onClose, + onError: (err) => { + console.error(err); + alert('요청 처리에 실패했습니다. 다시 시도해주세요.'); + }, + }, + ); + }; + + return ( + <> + +
+
+ + + 이번 스터디에서 다룰 면접 주제를 입력하세요 + +
+ + handleChange('subject')(e.target.value)} + /> +
+ +
+
+ + + 참고할 링크나 자료가 있다면 입력해 주세요 + +
+ + handleChange('link')(e.target.value)} + /> +
+
+ + +
+ + +
+
+ + ); +} diff --git a/src/features/study/ui/today-study-card.tsx b/src/features/study/ui/today-study-card.tsx index 6ad64340..109f265e 100644 --- a/src/features/study/ui/today-study-card.tsx +++ b/src/features/study/ui/today-study-card.tsx @@ -1,51 +1,65 @@ 'use client'; +import { useEffect, useState } from 'react'; +import { getStatusBadge } from '@/features/study/ui/status-badge-map'; +import { getCookie } from '@/shared/tanstack-query/cookie'; import UserAvatar from '@/shared/ui/avatar'; -import Badge from '@/shared/ui/badge'; -import TodayStudyModal from './today-study-modal'; -import { DailyStudyDetail, StudyProgressStatus } from '../api/types'; +import StudyDoneModal from './study-done-modal'; +import StudyReadyModal from './study-ready-modal'; +import { useDailyStudyDetailQuery } from '../model/use-study-query'; -interface Props { - data: DailyStudyDetail; - refetch: () => void; -} +export default function TodayStudyCard({ studyDate }: { studyDate: string }) { + const [memberId, setMemberId] = useState(null); + + useEffect(() => { + const id = getCookie('memberId'); + setMemberId(id ? Number(id) : null); + }, []); + + const { data: todayStudyData } = useDailyStudyDetailQuery(studyDate); + + if (!todayStudyData) return null; -const statusBadgeMap: Partial> = { - PENDING: 시작 전, - IN_PROGRESS: 진행중, - COMPLETE: 완료, - ABSENT: 불참, -}; + const isInterviewee = memberId === todayStudyData.intervieweeId; -export default function TodayStudyCard({ data, refetch }: Props) { return (

오늘의 스터디

- + {memberId !== null && + (isInterviewee ? ( + + ) : ( + + ))}
- + - - {data.interviewerName} + + + {todayStudyData.interviewerName} +
} /> - +
피드백
-

{data.feedback ?? '-'}

+

{todayStudyData.feedback ?? '-'}

); @@ -64,8 +78,4 @@ export default function TodayStudyCard({ data, refetch }: Props) {
); } - - function getStatusBadge(status: StudyProgressStatus) { - return statusBadgeMap[status] ?? null; - } } diff --git a/src/features/study/ui/today-study-modal.tsx b/src/features/study/ui/today-study-modal.tsx deleted file mode 100644 index f21c3127..00000000 --- a/src/features/study/ui/today-study-modal.tsx +++ /dev/null @@ -1,258 +0,0 @@ -'use client'; - -import { XIcon } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { cn } from '@/shared/shadcn/lib/utils'; -import { getCookie } from '@/shared/tanstack-query/cookie'; -import Button from '@/shared/ui/button'; -import { SingleDropdown } from '@/shared/ui/dropdown'; -import { BaseInput, TextAreaInput } from '@/shared/ui/input'; -import { Modal } from '@/shared/ui/modal'; -import CreateIcon from 'public/icons/create.svg'; -import { - patchStudyStatus, - putRetrospect, - putStudyDaily, -} from '../api/get-study-data'; -import { - DailyStudyDetail, - PutRetrospectRequest, - PutStudyDailyRequest, - StudyProgressStatus, -} from '../api/types'; -import { STUDY_PROGRESS_OPTIONS } from '../consts/study-const'; - -interface TodayStudyModalProps { - data: DailyStudyDetail; - refetch: () => void; -} - -export default function TodayStudyModal({ - data, - refetch, -}: TodayStudyModalProps) { - const [memberId, setMemberId] = useState(null); - - useEffect(() => { - const id = getCookie('memberId'); - setMemberId(id ? Number(id) : null); - }, []); - - if (memberId === null) return null; - - const isInterviewee = memberId === data.intervieweeId; - - return ( - - -
- - 작성하기 -
-
- - - - - - {isInterviewee ? '면접 준비하기' : '면접 완료하기'} - - - - - - - - {isInterviewee ? ( - - ) : ( - - )} - - - -
- ); -} - -function ReadyForm({ - refetch, - data, -}: { - refetch: () => void; - data: DailyStudyDetail; -}) { - const [interviewTopic, setInterviewTopic] = useState< - PutStudyDailyRequest['subject'] - >(data.subject ?? ''); - - const [referenceLink, setReferenceLink] = useState< - PutStudyDailyRequest['link'] - >(data.link ?? ''); - - const handleSubmit = async (e: React.MouseEvent) => { - if (!interviewTopic) { - e.preventDefault(); - - return; - } - - try { - await putStudyDaily(data.dailyStudyId, { - subject: interviewTopic, - description: '', - link: referenceLink, - }); - await refetch(); - } catch (err) { - e.preventDefault(); - console.error(err); - alert('요청 처리에 실패했습니다. 다시 시도해주세요.'); - } - }; - - return ( - <> -
- - - 이번 스터디에서 다룰 면접 주제를 입력하세요 - - setInterviewTopic(e.target.value)} - /> -
- -
- - - 참고할 링크나 자료가 있다면 입력해 주세요 - - setReferenceLink(e.target.value)} - /> -
- - - - - - - - - - - ); -} - -function DoneForm({ - data, - refetch, -}: { - data: DailyStudyDetail; - refetch: () => void; -}) { - const [feedback, setFeedback] = useState( - data.description ?? '', - ); - - const [progressStatus, setProgressStatus] = useState( - data.progressStatus ?? 'PENDING', - ); - - const handleSubmit = async (e: React.MouseEvent) => { - if (!feedback || !progressStatus) { - e.preventDefault(); - - return; - } - - try { - await patchStudyStatus(data.dailyStudyId, progressStatus); - - await putRetrospect(data.dailyStudyId, { - description: feedback, - }); - - await refetch(); - } catch (err) { - e.preventDefault(); - console.error(err); - alert('요청 처리에 실패했습니다. 다시 시도해주세요.'); - } - }; - - return ( - <> -
- - - 면접 완료 후 해당 지원자의 상태를 업데이트해 주세요. - - setProgressStatus(e as StudyProgressStatus)} - /> -
- -
- - - 면접 결과에 대한 간단한 피드백을 입력해 주세요. - - setFeedback(e)} - /> -
- - - - - - - - - - - ); -} diff --git a/src/shared/lib/get-login-user.ts b/src/shared/lib/get-login-user.ts index 58110f4b..cdceb3a5 100644 --- a/src/shared/lib/get-login-user.ts +++ b/src/shared/lib/get-login-user.ts @@ -1,6 +1,6 @@ -import { getServerCookie, setServerCookie } from './server-cookie'; import axios from 'axios'; import { redirect } from 'next/navigation'; +import { getServerCookie, setServerCookie } from './server-cookie'; export async function getLoginUserId(): Promise { // memberId 쿠키 우선 확인 (기존 로직) @@ -17,7 +17,7 @@ export async function getLoginUserId(): Promise { // 1. accessToken 쿠키 확인 let accessToken = await getServerCookie('accessToken'); if (!accessToken) return null; - + // 2. accessToken으로 /auth/me 호출 try { // (SSR 이므로 useQuery 를 쓰지 않고 직접 API 호출) @@ -26,20 +26,19 @@ export async function getLoginUserId(): Promise { { headers: { Authorization: `Bearer ${accessToken}` }, withCredentials: true, - } + }, ); // 3. memberId 쿠키와 /auth/me의 memberId가 다르면 null 반환 if (Number(res.data.content) !== memberId) return null; return memberId; } catch (error: any) { - // 4. 401이면 만료된 토큰이므로 토큰 갱신 시도 if (error.response?.status === 401) { try { const refreshRes = await axios.get( `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/auth/access-token/refresh`, - { withCredentials: true } + { withCredentials: true }, ); accessToken = refreshRes.data.accessToken; // 5. SSR에서 최신화된 AccessToken을 쿠키등록 @@ -51,10 +50,10 @@ export async function getLoginUserId(): Promise { { headers: { Authorization: `Bearer ${accessToken}` }, withCredentials: true, - } + }, ); if (Number(res2.data.memberId) !== memberId) return null; - + return memberId; } catch (refreshError) { // 갱신 실패 → 로그인 페이지로 리다이렉트 diff --git a/src/shared/ui/badge/index.tsx b/src/shared/ui/badge/index.tsx index d8663c74..9dd0978e 100644 --- a/src/shared/ui/badge/index.tsx +++ b/src/shared/ui/badge/index.tsx @@ -6,14 +6,15 @@ import * as React from 'react'; import { cn } from '@/shared/shadcn/lib/utils'; const badgeVariants = cva( - 'inline-flex min-w-[24px] px-100 py-50 justify-center items-center gap-[2px] text-xs font-medium whitespace-nowrap', + 'inline-flex min-w-[24px] px-100 py-50 justify-center items-center border gap-[2px] text-xs font-medium whitespace-nowrap', { variants: { color: { - default: - 'bg-background-accent-blue-subtle text-text-subtlest border-border-default', - completed: 'bg-fill-success-subtle-default text-text-success', - incomplete: 'bg-fill-danger-subtle-default text-text-error', + default: 'text-text-subtlest border-border-default', + green: + 'bg-fill-success-subtle-default text-text-success border-fill-success-subtle-default', + red: 'bg-fill-danger-subtle-default text-text-error border-fill-danger-subtle-default', + blue: 'bg-background-accent-blue-subtle text-text-information border-background-accent-blue-subtle', orange: 'bg-background-accent-orange-subtle text-background-accent-orange-strong', gray: 'bg-background-accent-gray-subtle text-background-accent-gray-strong', diff --git a/src/shared/ui/form/form-field.tsx b/src/shared/ui/form/form-field.tsx index f617a5d6..737f3edf 100644 --- a/src/shared/ui/form/form-field.tsx +++ b/src/shared/ui/form/form-field.tsx @@ -21,10 +21,12 @@ interface FormFieldProps { value: T; direction?: 'horizontal' | 'vertical'; onChange: (value: T) => void; + error?: boolean; } export function FormField({ label, + error = false, description, type, required = false, @@ -42,10 +44,13 @@ export function FormField({ onChange(e.target.value as T)} /> {description && ( -
+
{description}
)} diff --git a/src/widgets/home/study-list-table.tsx b/src/widgets/home/study-list-table.tsx index 1ad06baa..12ec3d98 100644 --- a/src/widgets/home/study-list-table.tsx +++ b/src/widgets/home/study-list-table.tsx @@ -1,12 +1,9 @@ import { useDailyStudiesQuery } from '@/features/study/model/use-study-query'; +import { getStatusBadge } from '@/features/study/ui/status-badge-map'; import UserAvatar from '@/shared/ui/avatar'; -import Badge from '@/shared/ui/badge/index'; import TableList from '@/shared/ui/table'; import LinkIcon from 'public/icons/Link.svg'; -import { - DailyStudy, - StudyProgressStatus, -} from '../../features/study/api/types'; +import { DailyStudy } from '../../features/study/api/types'; const headers = [ '조', @@ -19,17 +16,6 @@ const headers = [ ] as const; type Header = (typeof headers)[number]; -interface Props { - date: Date; -} - -const statusBadgeMap: Partial> = { - PENDING: 시작 전, - IN_PROGRESS: 진행중, - COMPLETE: 완료, - ABSENT: 불참, -}; - function mapDailyStudyToDisplayData( row: DailyStudy, index: number, @@ -52,32 +38,24 @@ function mapDailyStudyToDisplayData(
), - '면접 주제': row.subject, - 피드백: ( -

- {row.feedback ?? '-'} -

- ), - '진행 상태': statusBadgeMap[row.progressStatus], + '면접 주제': row.subject || '-', + 피드백: row.feedback || '-', + '진행 상태': getStatusBadge(row.progressStatus), '참고 자료': row.link ? ( - + - ) : null, + ) : ( + + ), }; } -export default function StudyListSection({ date }: Props) { - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - +export default function StudyListSection({ studyDate }: { studyDate: string }) { const { data, isLoading, error } = useDailyStudiesQuery({ cursor: 0, pageSize: 10, - year, - month, - day, + studyDate, }); if (isLoading) return
로딩 중...
;