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 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/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, - }); -}; 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최적화안쓴프로필
diff --git a/src/features/my-page/api/types.ts b/src/features/my-page/api/types.ts index 2c4c9b87..63315e17 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; @@ -62,3 +63,20 @@ export interface TechStackResponse { parentId: number | undefined; level: number; } + +export interface StudyActivity { + totalParticipationDays: number; + sequenceParticipationWeeks: number; + maxSequenceParticipationWeeks: number; + completedStudyCount: number; + failureStudyCount: number; +} + +export interface GrowthMetric { + studyCompleteness: number; +} + +export interface StudyDashboardResponse { + studyActivity: StudyActivity; + growthMetric: GrowthMetric; +} 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, + }); +}; diff --git a/src/features/my-page/ui/profile-edit-modal.tsx b/src/features/my-page/ui/profile-edit-modal.tsx index dd6a4500..4f4365c6 100644 --- a/src/features/my-page/ui/profile-edit-modal.tsx +++ b/src/features/my-page/ui/profile-edit-modal.tsx @@ -24,6 +24,30 @@ interface Props { export type MbtiValue = (typeof MBTI_OPTIONS)[number]['value']; +const formValidations = { + regex: { + // 이름 유효성 검사: 2~10자, 한글 또는 영문만 허용 + username: /^[가-힣a-zA-Z]{2,10}$/, + // 연락처 유효성 검사: "(2~3자리 지역번호)-(3~4자리 번호)-(4자리 번호)" 형식 + telephone: /^\d{2,3}-\d{3,4}-\d{4}$/, + // 생년월일 텍스트 패턴 유효성 검사 : ^(4자리 년도).(2자리 월).(2자리 일) + birthDate: /^\d{4}.([0][1-9]|[1][0-2]).([0][1-9]|[1-2][0-9]|[3][0-1])/, + }, + checker: { + // year 는 최소 1900년 이상 현재 년도 이하이어야 한다. + // month 와 date 의 유효성은 생성자로 검사됨 + birthDate: (value: string) => { + const birthDate = new Date(value); + const now = new Date(); + if (birthDate.toString() === 'Invalid Date') return false; + const year = birthDate.getFullYear(); + if (year < 1900 || year > now.getFullYear()) return false; + + return true; + }, + }, +}; + export default function ProfileEditModal({ memberProfile, memberId }: Props) { const [isOpen, setIsOpen] = useState(false); @@ -66,6 +90,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,12 +103,14 @@ function ProfileEditForm({ DEFAULT_PROFILE_IMAGE_URL, ); - // 이름 유효성 검사: 2~10자, 한글 또는 영문만 허용 - const isNameValid = /^[가-힣a-zA-Z]{2,10}$/.test(profileForm.name); - // 연락처 유효성 검사: "(2~3자리 지역번호)-(3~4자리 번호)-(4자리 번호)" 형식 - const isTelValid = - profileForm.tel.length === 0 || - /^\d{2,3}-\d{3,4}-\d{4}$/.test(profileForm.tel); + const isNameValid = formValidations.regex.username.test(profileForm.name); + const isTelValid = formValidations.regex.telephone.test(profileForm.tel); + const isBirthDateValid = + profileForm.birthDate.length === 0 || + (formValidations.regex.birthDate.test(profileForm.birthDate) && + formValidations.checker.birthDate(profileForm.birthDate)); + + const isReadyToSubmit = isNameValid && isTelValid && isBirthDateValid; const queryClient = useQueryClient(); const { mutateAsync: updateProfile } = useUpdateUserProfileMutation(memberId); @@ -104,6 +131,7 @@ function ProfileEditForm({ const rawFormData: UpdateUserProfileRequest = { name: profileForm.name, tel: profileForm.tel, + birthDate: profileForm.birthDate.replace(/\./g, '-'), githubLink: profileForm.githubLink, blogOrSnsLink: profileForm.blogOrSnsLink, simpleIntroduction: profileForm.simpleIntroduction.trim() || undefined, @@ -191,12 +219,13 @@ function ProfileEditForm({ { // 숫자와 하이픈(-)만 입력 허용 @@ -205,10 +234,29 @@ function ProfileEditForm({ }} required /> + { + setProfileForm({ + ...profileForm, + birthDate: value, + }); + }} + /> setProfileForm({ @@ -270,7 +318,7 @@ function ProfileEditForm({ await handleSubmit(); // 여기서 이미지 업로드 포함 onClose(); }} - disabled={!isNameValid || !isTelValid} + disabled={!isReadyToSubmit} > 수정 완료 diff --git a/src/features/study/ui/my-profile-card.tsx b/src/features/study/ui/my-profile-card.tsx index be84c7ea..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'; @@ -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( @@ -52,14 +56,12 @@ export default function MyProfileCard({
- - - - } - /> + + +
{name?.trim() || '비회원'}님
@@ -69,7 +71,7 @@ export default function MyProfileCard({ size="md" checked={enabled} onCheckedChange={handleToggleChange} - disabled={isPending} + disabled={isPending || !studyApplied} />
diff --git a/src/features/study/ui/start-study-modal.tsx b/src/features/study/ui/start-study-modal.tsx index e411b252..4c3a190e 100644 --- a/src/features/study/ui/start-study-modal.tsx +++ b/src/features/study/ui/start-study-modal.tsx @@ -78,56 +78,14 @@ 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 { data: availableStudyTimes } = useAvailableStudyTimesQuery(); - const { data: studySubjects } = useStudySubjectsQuery(); - const { data: techStacks } = useTechStacksQuery(); - const router = useRouter(); - - const { mutate: joinStudy } = useJoinStudyMutation(); - - const toggleTimeSlot = (id: number) => { - setAvailableTimeSlots((prev) => - prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id], - ); - }; - - const isFormValid = - introduce.trim() !== '' && - studyPlan.trim() !== '' && - phoneNumber.trim() !== '' && - preferredSubject !== undefined && - availableTimeSlots.length > 0 && - selectedSkills.length > 0; - - const getMissingFields = () => { - const missing: string[] = []; - - if (introduce.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('연락처'); - - return missing; - }; +type JoinStudyFormError = { + [K in keyof Omit< + JoinStudyRequest, + 'memberId' | 'githubLink' | 'blogOrSnsLink' + >]: boolean; +}; +export default function StartStudyModal({ memberId }: StartStudyModalProps) { return ( @@ -152,193 +110,299 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { - -
CS 스터디 진행 방법
- {studySteps.map((step, idx) => ( - - ))} + + + +
+ ); +} + +function StartStudyForm({ memberId }: StartStudyModalProps) { + const [form, setForm] = useState>({ + selfIntroduction: '', + studyPlan: '', + tel: '', + githubLink: '', + blogOrSnsLink: '', + preferredStudySubjectId: undefined, + availableStudyTimeIds: [], + techStackIds: [], + }); -
+ const [error, setError] = useState({ + selfIntroduction: false, + studyPlan: false, + tel: false, + preferredStudySubjectId: false, + availableStudyTimeIds: false, + techStackIds: false, + }); -
- - setIntroduce(e.target.value)} - /> - + const { + selfIntroduction, + studyPlan, + tel, + githubLink, + blogOrSnsLink, + preferredStudySubjectId, + availableStudyTimeIds, + } = form; - - setStudyPlan(e.target.value)} - /> - + const { data: availableStudyTimes } = useAvailableStudyTimesQuery(); + const { data: studySubjects } = useStudySubjectsQuery(); + const { data: techStacks } = useTechStacksQuery(); + const router = useRouter(); + + const { mutate: joinStudy } = useJoinStudyMutation(); - - ({ - value: studySubjectId, - label: name, - }), - )} - placeholder="선택하세요" - onChange={(value) => setPreferredSubject(value.toString())} - /> - + const toggleStudyTime = (id: number) => { + setForm((prev) => + prev.availableStudyTimeIds.includes(id) + ? { + ...prev, + availableStudyTimeIds: prev.availableStudyTimeIds.filter( + (item) => item !== id, + ), + } + : { + ...prev, + availableStudyTimeIds: [...prev.availableStudyTimeIds, id], + }, + ); + }; + + const handleSubmit = () => { + 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, + }; - -
- {(availableStudyTimes ?? []).map( - ({ availableTimeId, display }) => ( - toggleTimeSlot(availableTimeId)} - > - {display} - - ), - )} -
-
+ if (Object.values(newError).some(Boolean)) { + setError(newError); - - ({ - value: techStackId, - label: techStackName, - }), - )} - onChange={(newSelected) => - setSelectedSkills(newSelected as number[]) - } - placeholder="기술을 선택해주세요" - /> - + return; + } - - setPhoneNumber(e.target.value)} - /> - + joinStudy( + { + ...form, + memberId, + githubLink: githubLink.trim() || undefined, + blogOrSnsLink: blogOrSnsLink.trim() || undefined, + }, + { + onSuccess: () => { + alert('스터디 신청이 완료되었습니다!'); + router.refresh(); + }, + onError: () => { + alert('스터디 신청 중 오류가 발생했습니다. 다시 시도해 주세요.'); + }, + }, + ); + }; - - setGithub(e.target.value)} - /> - + return ( + <> + +
CS 스터디 진행 방법
+ {studySteps.map((step, idx) => ( + + ))} - - setBlog(e.target.value)} - /> - +
+ +
+ + { + setForm((prev) => ({ + ...prev, + selfIntroduction: e.target.value, + })); + setError((prev) => ({ + ...prev, + selfIntroduction: e.target.value.trim() === '', + })); + }} + /> + + + + { + setForm((prev) => ({ + ...prev, + studyPlan: e.target.value, + })); + setError((prev) => ({ + ...prev, + studyPlan: e.target.value.trim() === '', + })); + }} + /> + + + + ({ + value: studySubjectId, + label: name, + }), + )} + placeholder="선택하세요" + onChange={(value) => { + setForm((prev) => ({ + ...prev, + preferredStudySubjectId: value.toString(), + })); + setError((prev) => ({ + ...prev, + preferredStudySubjectId: value === undefined, + })); + }} + /> + + + +
+ {(availableStudyTimes ?? []).map( + ({ availableTimeId, display }) => ( + toggleStudyTime(availableTimeId)} + > + {display} + + ), + )}
- +
- - - - - - - - - - - + + + setForm((prev) => ({ ...prev, githubLink: e.target.value })) + } + /> + + + + + setForm((prev) => ({ + ...prev, + blogOrSnsLink: e.target.value, + })) + } + /> + +
+ + + + + + + + + ); } diff --git a/src/features/study/ui/today-study-card.tsx b/src/features/study/ui/today-study-card.tsx index 109f265e..ffc87692 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'; @@ -22,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 (
@@ -35,26 +50,26 @@ export default function TodayStudyCard({ studyDate }: { studyDate: string }) {
- - - - - {todayStudyData.interviewerName} - -
- } - /> - - + + {`${todayStudyData.studySpaceId} 조`} + + + + + + {matchedUsername} + +
+ } + /> + + {todayStudyData.subject} + + {getStatusBadge(todayStudyData.progressStatus ?? 'PENDING')} +
@@ -66,15 +81,15 @@ export default function TodayStudyCard({ studyDate }: { studyDate: string }) { 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 59648e3c..3f8817bc 100644 --- a/src/shared/ui/avatar/index.tsx +++ b/src/shared/ui/avatar/index.tsx @@ -10,11 +10,13 @@ interface UserAvatarProps { export default function UserAvatar({ image, - alt = 'user image', + alt = 'user profile', size = 32, -}: UserAvatarProps) { + ref, + ...props +}: React.RefAttributes & UserAvatarProps) { return ( - + {image ? ( ) : ( 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/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 && (
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 ( ); 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} ); } 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: { 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 ? (