diff --git a/src/app/(admin)/layout.tsx b/src/app/(admin)/layout.tsx index 29dd1c52..cddddc45 100644 --- a/src/app/(admin)/layout.tsx +++ b/src/app/(admin)/layout.tsx @@ -37,7 +37,7 @@ export default async function AdminLayout({ const initialAccessToken = await getServerCookie('accessToken'); return ( - + {GTM_ID && } diff --git a/src/app/(landing)/layout.tsx b/src/app/(landing)/layout.tsx index 296f2063..5aa0f364 100644 --- a/src/app/(landing)/layout.tsx +++ b/src/app/(landing)/layout.tsx @@ -92,7 +92,7 @@ export default async function LandingPageLayout({ -
+
{/** 1400 + 48*2 패딩 양옆 48로 임의적용 */}
diff --git a/src/app/(service)/insights/ui/blog-detail-page.tsx b/src/app/(service)/insights/ui/blog-detail-page.tsx index 3d0434f2..754f0542 100644 --- a/src/app/(service)/insights/ui/blog-detail-page.tsx +++ b/src/app/(service)/insights/ui/blog-detail-page.tsx @@ -445,7 +445,7 @@ export default function BlogDetailPage({ article }: BlogDetailPageProps) {
{/* 커버 이미지 (GNB 바로 아래, 전체 너비) */} {coverUrl && ( -
+
{article.title} + {GTM_ID && } -
+
{children}
diff --git a/src/components/card/study-card.tsx b/src/components/card/study-card.tsx index 8ca11cb8..6c8ff6af 100644 --- a/src/components/card/study-card.tsx +++ b/src/components/card/study-card.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Clock5, Users } from 'lucide-react'; +import { Clock5, Eye, Users } from 'lucide-react'; import Image from 'next/image'; import Link from 'next/link'; import { GroupStudyListItemDto } from '@/api/openapi'; @@ -45,12 +45,25 @@ interface StudyCardProps { study: GroupStudyListItemDto; href: string; onClick?: () => void; + viewCount?: number; } -export default function StudyCard({ study, href, onClick }: StudyCardProps) { +function formatCount(n: number): string { + if (n >= 1000) return `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k`; + + return String(n); +} + +export default function StudyCard({ + study, + href, + onClick, + viewCount, +}: StudyCardProps) { const studyType = study.basicInfo?.type as StudyType; const badgeColor = studyType ? STUDY_TYPE_BADGE_COLORS[studyType] : 'default'; const price = study.basicInfo?.price ?? 0; + const isCompleted = study.basicInfo?.status === 'COMPLETED'; return ( {study.simpleDetailInfo?.thumbnail?.resizedImages?.[0] ?.resizedImageUrl ? ( - {study.simpleDetailInfo?.title + <> + {study.simpleDetailInfo?.title + {isCompleted && ( +
+ )} + ) : (
ZERO ONE IT @@ -112,8 +131,9 @@ export default function StudyCard({ study, href, onClick }: StudyCardProps) { {study.simpleDetailInfo?.summary}

- {/* 활성 배지 (RECRUITING 일 때만) */} - {study.basicInfo?.status === 'RECRUITING' && + {/* 활성 배지 (RECRUITING / ENDING_SOON 일 때만) */} + {(study.basicInfo?.status === 'RECRUITING' || + study.basicInfo?.status === 'ENDING_SOON') && (() => { const remaining = (study.basicInfo?.maxMembersCount ?? 0) - @@ -196,15 +216,25 @@ export default function StudyCard({ study, href, onClick }: StudyCardProps) {
- {/* 가격 (0원이면 숨김) */} - {price > 0 && ( - - {price.toLocaleString()} - - 원 + {/* 가격 & 조회수 */} +
+ {price > 0 && ( + + {price.toLocaleString()} + + 원 + - - )} + )} + {viewCount !== undefined && viewCount > 0 && ( +
+ + + {formatCount(viewCount)} + +
+ )} +
diff --git a/src/components/filtering/study-filter.tsx b/src/components/filtering/study-filter.tsx index a3c0fd7c..bf745750 100644 --- a/src/components/filtering/study-filter.tsx +++ b/src/components/filtering/study-filter.tsx @@ -9,6 +9,10 @@ import { DropdownMenuTrigger, } from '@/components/ui/(shadcn)/ui/dropdown-menu'; import ToggleButton from '@/components/ui/toggle/button'; +import { + ROLE_OPTIONS_UI, + STUDY_METHOD_LABELS, +} from '@/features/study/group/const/group-study-const'; // 필터 옵션 타입 export interface StudyFilterValues { @@ -33,20 +37,10 @@ const STUDY_TYPE_OPTIONS = [ { value: 'LECTURE_STUDY', label: '강의스터디' }, ] as const; -// 포지션 옵션 -const POSITION_OPTIONS = [ - { value: 'BACKEND', label: '백엔드' }, - { value: 'FRONTEND', label: '프론트엔드' }, - { value: 'PLANNER', label: '기획자' }, - { value: 'DESIGNER', label: '디자이너' }, -] as const; - // 진행 방식 옵션 -const METHOD_OPTIONS = [ - { value: 'ONLINE', label: '온라인' }, - { value: 'OFFLINE', label: '오프라인' }, - { value: 'HYBRID', label: '병행' }, -] as const; +const METHOD_OPTIONS = Object.entries(STUDY_METHOD_LABELS).map( + ([value, label]) => ({ value, label }), +); // 경험 레벨 옵션 const EXPERIENCE_LEVEL_OPTIONS = [ @@ -214,7 +208,7 @@ export default function StudyFilter({ values, onChange }: StudyFilterProps) { diff --git a/src/components/modals/create-mission-modal.tsx b/src/components/modals/create-mission-modal.tsx index a4975506..6deab294 100644 --- a/src/components/modals/create-mission-modal.tsx +++ b/src/components/modals/create-mission-modal.tsx @@ -1,7 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import dayjs from 'dayjs'; import { Plus } from 'lucide-react'; -import { ChangeEvent, useState } from 'react'; +import { FormEvent, useState } from 'react'; import { Controller, FormProvider, useForm } from 'react-hook-form'; import { z } from 'zod'; import Button from '@/components/ui/button'; @@ -158,7 +158,7 @@ function CreateMissionForm({ ); }; - const handlePreventInvalidValue = (event: ChangeEvent) => { + const handlePreventInvalidValue = (event: FormEvent) => { event.currentTarget.value = event.currentTarget.value.replace( /[^0-9]/g, '', @@ -217,7 +217,7 @@ function CreateMissionForm({ label="수행 가이드" direction="vertical" required - counterMax={5000} + maxCharCount={5000} showCounterRight={false} > (false); + const [open, setOpen] = useState(false); + const [hasSubmitted, setHasSubmitted] = useState(false); + + const { mutate, isPending } = useUpdateMemberDiscretion(); return ( @@ -35,6 +41,7 @@ export default function DiscretionaryEvaluationModal({ size="small" color="outlined" className="font-designer-14r w-fit" + disabled={isPending || hasSubmitted} > 재량평가 추가 @@ -49,7 +56,8 @@ export default function DiscretionaryEvaluationModal({ 재량 평가

- 재량 평가는 최대 3회까지 가능하며, 각 평가별 5점씩 부여됩니다. + 재량 평가는 최대 {EVALUATION_COUNT}회까지 가능하며, 각 평가별 + 5점씩 부여됩니다.

setOpen(false)} /> @@ -59,6 +67,9 @@ export default function DiscretionaryEvaluationModal({ groupStudyId={groupStudyId} memberId={memberId} onClose={() => setOpen(false)} + onSubmitSuccess={() => setHasSubmitted(true)} + mutate={mutate} + isPending={isPending} /> @@ -70,12 +81,18 @@ interface DiscretionaryEvaluationFormProps { groupStudyId: number; memberId: number; onClose: () => void; + onSubmitSuccess: () => void; + mutate: ReturnType['mutate']; + isPending: boolean; } function DiscretionaryEvaluationForm({ groupStudyId, memberId, onClose, + onSubmitSuccess, + mutate, + isPending, }: DiscretionaryEvaluationFormProps) { const methods = useForm({ resolver: zodResolver(DiscretionaryEvaluationFormSchema), @@ -87,11 +104,10 @@ function DiscretionaryEvaluationForm({ const { handleSubmit, formState } = methods; - const { mutate: updateMemberDiscretion } = useUpdateMemberDiscretion(); const showToast = useToastStore((state) => state.showToast); const onValidSubmit = (values: DiscretionaryEvaluationFormValues) => { - updateMemberDiscretion( + mutate( { id: groupStudyId, request: { @@ -102,6 +118,7 @@ function DiscretionaryEvaluationForm({ { onSuccess: () => { showToast('재량 평가가 성공적으로 제출되었습니다!'); + onSubmitSuccess(); onClose(); }, onError: () => { @@ -126,7 +143,7 @@ function DiscretionaryEvaluationForm({ name="content" label="평가 내역" direction="vertical" - counterMax={500} + maxCharCount={EVALUATION_CONTENT_MAX_LENGTH} showCounterRight={false} required > @@ -134,7 +151,7 @@ function DiscretionaryEvaluationForm({ id="content" placeholder="평가 내역을 입력해 주세요." className="min-h-[300px]" - maxLength={5000} + maxLength={EVALUATION_CONTENT_MAX_LENGTH} /> @@ -151,7 +168,7 @@ function DiscretionaryEvaluationForm({ size="large" type="submit" form="discretionary-evaluation" - disabled={!formState.isValid || formState.isSubmitting} + disabled={!formState.isValid || isPending} > 평가완료 diff --git a/src/components/modals/edit-homework-modal.tsx b/src/components/modals/edit-homework-modal.tsx index 5dc1c398..49b4561a 100644 --- a/src/components/modals/edit-homework-modal.tsx +++ b/src/components/modals/edit-homework-modal.tsx @@ -127,7 +127,7 @@ function EditHomeworkForm({ label="과제 상세 내용" direction="vertical" required - counterMax={5000} + maxCharCount={5000} showCounterRight={false} > { if (file) { + if (imagePreview) URL.revokeObjectURL(imagePreview); setImageFile(file); setImagePreview(URL.createObjectURL(file)); } else { @@ -77,15 +79,13 @@ export default function QuestionModal({ }; const uploadImage = async (uploadUrl: string, file: File) => { - const res = await fetch(uploadUrl, { - method: 'PUT', - body: file, - headers: { 'Content-Type': file.type }, - }); - - if (!res.ok) { - throw new Error(`이미지 업로드 실패 (status: ${res.status})`); - } + const formData = new FormData(); + formData.append('file', file); + + const url = new URL(uploadUrl); + const relativePath = url.pathname.replace(/^\/api\/v1\//, '') + url.search; + + await axiosInstanceForMultipart.put(relativePath, formData); }; const resetImageState = () => { diff --git a/src/components/modals/submit-homework-modal.tsx b/src/components/modals/submit-homework-modal.tsx index 22473d3b..d84f4f05 100644 --- a/src/components/modals/submit-homework-modal.tsx +++ b/src/components/modals/submit-homework-modal.tsx @@ -127,7 +127,7 @@ function SubmitHomeworkForm({ label="과제 상세 내용" direction="vertical" required - counterMax={1000} + maxCharCount={1000} showCounterRight={false} > setShowStudyFormModal(!showStudyFormModal)} /> -
+ {/* 플로팅 정보 바 */} +
+ +
+
{studyDetail?.detailInfo.title} diff --git a/src/components/pages/premium-study-detail-page.tsx b/src/components/pages/premium-study-detail-page.tsx index 9da74ae4..993439d2 100644 --- a/src/components/pages/premium-study-detail-page.tsx +++ b/src/components/pages/premium-study-detail-page.tsx @@ -4,6 +4,7 @@ import { sendGTMEvent } from '@next/third-parties/google'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useState } from 'react'; import MoreMenu from '@/components/ui/dropdown/more-menu'; +import StudyActiveTicker from '@/components/ui/study-active-ticker'; import Tabs from '@/components/ui/tabs'; import { STUDY_DETAIL_TABS, StudyTabValue } from '@/config/constants'; import { @@ -162,7 +163,15 @@ export default function PremiumStudyDetailPage({ onOpenChange={() => setShowStudyFormModal(!showStudyFormModal)} /> -
+ {/* 플로팅 정보 바 */} +
+ +
+

{studyDetail?.detailInfo.title} diff --git a/src/components/section/group-study-info-section.tsx b/src/components/section/group-study-info-section.tsx index 62b2ab24..a7598468 100644 --- a/src/components/section/group-study-info-section.tsx +++ b/src/components/section/group-study-info-section.tsx @@ -9,7 +9,6 @@ import UserAvatar from '@/components/ui/avatar'; import AvatarStack from '@/components/ui/avatar-stack'; import type { AvatarStackMember } from '@/components/ui/avatar-stack'; import Button from '@/components/ui/button'; -import StudyActiveTicker from '@/components/ui/study-active-ticker'; import UserProfileModal from '@/entities/user/ui/user-profile-modal'; import { CurriculumSummaryItem } from '@/features/study/group/api/group-study-types'; import { useApplicantsByStatusQuery } from '@/features/study/group/application/model/use-applicant-qeury'; @@ -147,12 +146,7 @@ export default function StudyInfoSection({

-
- +
setIsModalOpen(true)} /> ) : ( void; onSelectQuestion: (id: number) => void; - onOpenModal: () => void; } function ListView({ @@ -82,7 +80,6 @@ function ListView({ page, onPageChange, onSelectQuestion, - onOpenModal, }: ListViewProps) { const { data, isLoading } = useGetQuestions({ groupStudyId, diff --git a/src/components/section/my-participating-studies-section.tsx b/src/components/section/my-participating-studies-section.tsx index 3bc4e815..2a3eb52b 100644 --- a/src/components/section/my-participating-studies-section.tsx +++ b/src/components/section/my-participating-studies-section.tsx @@ -64,7 +64,9 @@ export default function MyParticipatingStudiesSection({ const filtered = myStudiesData.notCompleted.content.filter( (study) => - study.status === 'IN_PROGRESS' || study.status === 'RECRUITING', + study.status === 'IN_PROGRESS' || + study.status === 'RECRUITING' || + study.status === 'ENDING_SOON', ); // GROUP_STUDY인 경우 type 필드로 추가 필터링 diff --git a/src/components/ui/form/form-field.tsx b/src/components/ui/form/form-field.tsx index c81d86f8..fed4114f 100644 --- a/src/components/ui/form/form-field.tsx +++ b/src/components/ui/form/form-field.tsx @@ -47,7 +47,7 @@ export interface FormFieldProps< id?: string; showCounterRight?: boolean; - counterMax?: number; + maxCharCount?: number; /** true로 설정하면 data-scroll-field 속성을 wrapper div에 추가 (자동 스크롤 위치 마킹) */ scrollable?: boolean; @@ -75,7 +75,7 @@ export default function FormField< id, children, showCounterRight = false, - counterMax, + maxCharCount, scrollable, onAfterChange, onAfterBlurFilled, @@ -148,9 +148,9 @@ export default function FormField< {errorMsg ? errorMsg : description}
- {showCounterRight && typeof counterMax === 'number' && ( + {showCounterRight && typeof maxCharCount === 'number' && (
- {currentLen}/{counterMax} + {currentLen}/{maxCharCount}
)}
diff --git a/src/components/ui/image-upload-input.tsx b/src/components/ui/image-upload-input.tsx index 59dda120..5d3f9155 100644 --- a/src/components/ui/image-upload-input.tsx +++ b/src/components/ui/image-upload-input.tsx @@ -1,17 +1,29 @@ 'use client'; +import { cva } from 'class-variance-authority'; import Image from 'next/image'; import { useState, DragEvent, ChangeEvent, useRef } from 'react'; +import { cn } from '@/components/ui/(shadcn)/lib/utils'; import Button from '@/components/ui/button'; +import CameraIcon from 'public/icons/camera.svg'; // 클라이언트에서 선제 차단하여 불필요한 413 에러 및 대용량 업로드 요청을 방지. const DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5MB -const inputStyles = { - base: 'rounded-100 flex w-full flex-col items-center justify-center border-2 p-500', - dragging: 'border-border-brand bg-fill-brand-subtle-hover', - notDragging: 'border-gray-300 border-dashed', -}; +const dropzoneVariants = cva( + 'rounded-100 flex w-full flex-col items-center justify-center border-2 p-500', + { + variants: { + dragging: { + true: 'border-border-brand bg-fill-brand-subtle-hover', + false: 'border-gray-300 border-dashed', + }, + }, + defaultVariants: { + dragging: false, + }, + }, +); export default function ImageUploadInput({ image, @@ -93,7 +105,7 @@ export default function ImageUploadInput({ onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} onDragOver={handleDragOver} - className={`${inputStyles.base} ${isDragging ? inputStyles.dragging : inputStyles.notDragging}`} + className={cn(dropzoneVariants({ dragging: isDragging }))} > {!image ? (
diff --git a/src/components/ui/study-active-ticker.tsx b/src/components/ui/study-active-ticker.tsx index 608c5059..0f16b149 100644 --- a/src/components/ui/study-active-ticker.tsx +++ b/src/components/ui/study-active-ticker.tsx @@ -1,96 +1,80 @@ 'use client'; -import dayjs from 'dayjs'; +import { Eye, Flame } from 'lucide-react'; import { useEffect, useState } from 'react'; -import { useNow } from '@/hooks/use-now'; -import { getCountdownState } from '@/lib/countdown'; - interface StudyActiveTickerProps { approvedCount: number; maxMembersCount: number; - startDate: string; // YYYY-MM-DD + startDate: string; viewCount?: number; + className?: string; } export default function StudyActiveTicker({ approvedCount, maxMembersCount, - startDate, viewCount = 0, + className = '', }: StudyActiveTickerProps) { const remaining = Math.max(0, maxMembersCount - approvedCount); + const [currentIndex, setCurrentIndex] = useState(0); const messages = [ - remaining > 3 - ? `🔥 마감까지 ${remaining}석` - : remaining > 0 - ? `🔥 마지막 ${remaining}자리` - : '🔥 모집 마감', - `지금 ${viewCount}명이 이 스터디를 보고 있어요.`, - `${approvedCount}명이 가입했고 현재 ${remaining}자리 남았어요.`, + { + icon: , + content: ( +

+ 지금{' '} + + {viewCount}명 + + 이 이 스터디를 보고 있어요. +

+ ), + }, + { + icon: , + content: ( +

+ + {approvedCount}명 + + 이 가입했고 현재{' '} + + {remaining}자리 + {' '} + 남았어요. +

+ ), + }, ]; - const [currentIndex, setCurrentIndex] = useState(0); - const [visible, setVisible] = useState(true); - useEffect(() => { const interval = setInterval(() => { - setVisible(false); - setTimeout(() => { - setCurrentIndex((prev) => (prev + 1) % messages.length); - setVisible(true); - }, 300); + setCurrentIndex((prev) => (prev + 1) % messages.length); }, 3000); return () => clearInterval(interval); }, [messages.length]); - const now = useNow(); - - const start = dayjs(startDate); - const diffMs = start.diff(now); - const state = getCountdownState(diffMs); - - const countdown = - state?.urgent === true - ? { text: state.label, color: state.textColorClass, pulse: state.pulse } - : null; - - const isPulse = remaining > 0 && remaining <= 3; - return ( -
- {/* 전광판 섹션 */} -
- - 전광판 - - - {messages[currentIndex]} - -
- - {/* 마감 카운트다운 섹션 */} - {countdown && ( - <> -
-
- - 모집 마감까지 - - - {countdown.text} - +
+
+ {messages.map((message, index) => ( +
+ {message.icon} + {message.content}
- - )} + ))} +
); } diff --git a/src/components/ui/study-card-countdown-badge.tsx b/src/components/ui/study-card-countdown-badge.tsx index da69bbf3..c9682b10 100644 --- a/src/components/ui/study-card-countdown-badge.tsx +++ b/src/components/ui/study-card-countdown-badge.tsx @@ -18,7 +18,26 @@ export default function StudyCardCountdownBadge({ }: Props) { const now = useNow(); - if (status !== 'RECRUITING') return null; + // 종료 배지 (gray) + if (status === 'COMPLETED') { + return ( + + 종료 + + ); + } + + // 진행 중 배지 (purple) + if (status === 'IN_PROGRESS') { + return ( + + 진행 중 + + ); + } + + const isRecruiting = status === 'RECRUITING' || status === 'ENDING_SOON'; + if (!isRecruiting) return null; if (remaining !== undefined && remaining <= 0) { return ( @@ -35,6 +54,14 @@ export default function StudyCardCountdownBadge({ const state = getCountdownState(diffMs); if (!state || !state.urgent) { + if (status === 'ENDING_SOON') { + return ( + + 마감 임박 + + ); + } + return ( 모집 중 @@ -44,7 +71,7 @@ export default function StudyCardCountdownBadge({ return ( 마감까지 {state.label} diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index 719aecc5..9af44b08 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -1,6 +1,6 @@ 'use client'; -import { CheckCircle2, XCircle } from 'lucide-react'; +import { CheckCircle2, Info, XCircle } from 'lucide-react'; import React, { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { cn } from '@/components/ui/(shadcn)/lib/utils'; @@ -72,8 +72,10 @@ export default function Toast({ > {isSuccess ? ( - ) : ( + ) : isError ? ( + ) : ( + )} {message}
diff --git a/src/components/voting/voting-create-modal.tsx b/src/components/voting/voting-create-modal.tsx index 90448bdd..2f0cb7f3 100644 --- a/src/components/voting/voting-create-modal.tsx +++ b/src/components/voting/voting-create-modal.tsx @@ -159,7 +159,7 @@ function VotingCreateForm({ onClose, onSubmit }: VotingCreateFormProps) { required direction="vertical" showCounterRight - counterMax={200} + maxCharCount={200} > - {status === 'RECRUITING' && ( + {(status === 'RECRUITING' || status === 'ENDING_SOON') && (
시작 전
@@ -79,7 +79,9 @@ export default function MyStudyInfoCard({
- {(status === 'RECRUITING' || status === 'IN_PROGRESS') && + {(status === 'RECRUITING' || + status === 'ENDING_SOON' || + status === 'IN_PROGRESS') && participantsCount < maxMembersCount && studyRole === 'LEADER' && ( diff --git a/src/features/my-page/ui/profile-edit-modal.tsx b/src/features/my-page/ui/profile-edit-modal.tsx index ce13acf5..475a94b8 100644 --- a/src/features/my-page/ui/profile-edit-modal.tsx +++ b/src/features/my-page/ui/profile-edit-modal.tsx @@ -210,7 +210,7 @@ function ProfileEditForm({ label="한마디 소개" description="본인을 간단히 소개하는 한마디를 입력해 주세요." showCounterRight - counterMax={200} + maxCharCount={200} > { if (arr.length > 10) { ctx.addIssue({ - code: "custom", + code: 'custom', message: '질문은 최대 10개까지만 입력할 수 있습니다.', }); } arr.forEach((item, idx) => { if (!item || item.trim() === '') { ctx.addIssue({ - code: "custom", + code: 'custom', message: '질문을 입력해주세요.', path: [idx], }); @@ -141,7 +142,7 @@ export const GroupStudyFormSchema = z if (data.classification === 'PREMIUM_STUDY') { if (!data.price || priceNum < 10000) { ctx.addIssue({ - code: "custom", + code: 'custom', message: '참가비는 10,000원 이상이어야 합니다.', path: ['price'], }); @@ -150,7 +151,7 @@ export const GroupStudyFormSchema = z // 그룹 스터디: 0원만 허용 else if (data.price && priceNum !== 0) { ctx.addIssue({ - code: "custom", + code: 'custom', message: '그룹 스터디는 무료만 가능합니다.', path: ['price'], }); @@ -174,7 +175,7 @@ export const GroupStudyFormSchema = z if (data.startDate < tomorrowYmd) { ctx.addIssue({ - code: "custom", + code: 'custom', message: '스터디 시작일은 내일부터 설정할 수 있습니다.', path: ['startDate'], }); @@ -190,7 +191,7 @@ export const GroupStudyFormSchema = z const end = parseDate(data.endDate); if (end < start) { ctx.addIssue({ - code: "custom", + code: 'custom', message: '종료일은 시작일과 같거나 이후여야 합니다.', path: ['endDate'], }); diff --git a/src/features/study/group/ui/delete-group-study-member.tsx b/src/features/study/group/ui/delete-group-study-member.tsx index 24d2b6d3..455da9d8 100644 --- a/src/features/study/group/ui/delete-group-study-member.tsx +++ b/src/features/study/group/ui/delete-group-study-member.tsx @@ -94,7 +94,7 @@ function DeleteGroupStudyMemberForm({ label="스터디원을 내보내는 사유를 작성해 주세요." direction="vertical" required - counterMax={300} + maxCharCount={300} > ({ + resolver: zodResolver(GroupStudyFormSchema), + mode: 'onChange', + defaultValues: buildOpenGroupDefaultValues(classification), + }); + const handleVerificationComplete = (phoneNumber: string) => { setVerified(phoneNumber); setIsVerificationModalOpen(false); @@ -193,6 +202,7 @@ export default function GroupStudyFormModal({ 'error', ); } finally { + createMethods.reset(buildOpenGroupDefaultValues(classification)); setOpen(false); } }; @@ -266,7 +276,7 @@ export default function GroupStudyFormModal({ {mode === 'create' && ( )} diff --git a/src/features/study/group/ui/group-study-form.tsx b/src/features/study/group/ui/group-study-form.tsx index 982fa97b..c1085669 100644 --- a/src/features/study/group/ui/group-study-form.tsx +++ b/src/features/study/group/ui/group-study-form.tsx @@ -1,6 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { createContext, useContext, useMemo, useState } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; +import { FormProvider, UseFormReturn, useForm } from 'react-hook-form'; import { cn } from '@/components/ui/(shadcn)/lib/utils'; import Button from '@/components/ui/button'; import { Modal } from '@/components/ui/modal'; @@ -17,7 +17,8 @@ const ClassificationContext = createContext('GROUP_STUDY'); export const useClassification = () => useContext(ClassificationContext); interface GroupStudyFormProps { - defaultValues: GroupStudyFormValues; + defaultValues?: GroupStudyFormValues; + methods?: UseFormReturn; onSubmit: (values: GroupStudyFormValues) => void; } @@ -41,14 +42,17 @@ const STEP_FIELDS: Record<1 | 2 | 3, (keyof GroupStudyFormValues)[]> = { export default function GroupStudyForm({ defaultValues, + methods: externalMethods, onSubmit, }: GroupStudyFormProps) { - const methods = useForm({ + const internalMethods = useForm({ resolver: zodResolver(GroupStudyFormSchema), mode: 'onChange', defaultValues: defaultValues, }); + const methods = externalMethods ?? internalMethods; + const { handleSubmit, trigger, formState, watch } = methods; const classification = watch('classification'); diff --git a/src/features/study/group/ui/group-study-member-item.tsx b/src/features/study/group/ui/group-study-member-item.tsx index 836f6832..1cbaf0b5 100644 --- a/src/features/study/group/ui/group-study-member-item.tsx +++ b/src/features/study/group/ui/group-study-member-item.tsx @@ -3,7 +3,9 @@ import { useState } from 'react'; import DiscretionGradeHistoryList from '@/components/lists/discretion-grade-history-list'; import MissionProgressHistoryList from '@/components/lists/mission-progress-history-list'; -import DiscretionaryEvaluationModal from '@/components/modals/discretionary-evaluation-modal'; +import DiscretionaryEvaluationModal, { + EVALUATION_COUNT, +} from '@/components/modals/discretionary-evaluation-modal'; import EndGroupStudyModal from '@/components/modals/end-group-study'; import UserAvatar from '@/components/ui/avatar'; @@ -84,12 +86,19 @@ export default function GroupStudyMemberItem({
가입 인사 - {isLeader && member.id !== myId && discretionCount < 3 && ( - - )} + {isLeader && + member.id !== myId && + (discretionCount < EVALUATION_COUNT ? ( + + ) : ( + + 재량 평가 {EVALUATION_COUNT}회 모두 완료 + + ))}
(); + const { setValue, getValues, formState } = + useFormContext(); const initQuestions = getValues('interviewPost'); @@ -69,6 +70,11 @@ export default function Step3OpenGroupStudy() { )}
))} + {formState.errors.interviewPost?.root?.message && ( +

+ {formState.errors.interviewPost.root.message} +

+ )} diff --git a/src/features/study/interview/ui/study-done-modal.tsx b/src/features/study/interview/ui/study-done-modal.tsx index 5f2090e4..4b6c0f0b 100644 --- a/src/features/study/interview/ui/study-done-modal.tsx +++ b/src/features/study/interview/ui/study-done-modal.tsx @@ -213,7 +213,7 @@ function StudyDoneForm({ required direction="vertical" showCounterRight - counterMax={100} + maxCharCount={100} >