Skip to content

Commit 957f8a8

Browse files
Merge pull request #541 from code-zero-to-one/fix/mission
멘토스터디 / 그룹스터디 미션 평가 노출 로직 수정 및 JS 로드 크기 개선
2 parents 83a75ea + 79a1414 commit 957f8a8

9 files changed

Lines changed: 318 additions & 76 deletions

src/components/card/mission-card.tsx

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import dayjs from 'dayjs';
44
import { Lock } from 'lucide-react';
55
import dynamic from 'next/dynamic';
6-
import { ComponentProps, SyntheticEvent } from 'react';
6+
import type { ComponentProps } from 'react';
77

88
import type { MissionListResponse } from '@/api/openapi/models';
99
import Badge from '@/components/common/ui/badge';
@@ -121,10 +121,6 @@ export default function MissionCard({
121121
? getDeadlineInfo(mission.endDate)
122122
: undefined;
123123

124-
const stopActionAreaPropagation = (event: SyntheticEvent) => {
125-
event.stopPropagation();
126-
};
127-
128124
// 비가입자 2주차+ 잠금 카드
129125
if (isLocked) {
130126
return (
@@ -158,24 +154,22 @@ export default function MissionCard({
158154
if (isLeader && mission.status === 'NOT_STARTED') {
159155
return (
160156
<li className="border-border-default rounded-100 border bg-[#fff]">
161-
<button
162-
type="button"
163-
className="flex w-full cursor-pointer items-center justify-between p-300"
164-
onClick={handleSelectMission}
165-
>
166-
<MissionCardContent
167-
title={mission.title}
168-
weekNum={mission.weekNum}
169-
statusConfig={statusConfig}
170-
startDate={mission.startDate}
171-
endDate={mission.endDate}
172-
deadlineInfo={undefined}
173-
/>
174-
<div
175-
className="flex flex-col gap-100"
176-
role="none"
177-
onClick={stopActionAreaPropagation}
157+
<div className="flex w-full items-center justify-between p-300">
158+
<button
159+
type="button"
160+
className="flex flex-1 cursor-pointer items-center"
161+
onClick={handleSelectMission}
178162
>
163+
<MissionCardContent
164+
title={mission.title}
165+
weekNum={mission.weekNum}
166+
statusConfig={statusConfig}
167+
startDate={mission.startDate}
168+
endDate={mission.endDate}
169+
deadlineInfo={undefined}
170+
/>
171+
</button>
172+
<div className="flex flex-col gap-100">
179173
<EditMissionModal
180174
missionId={mission.missionId}
181175
groupStudyId={groupStudyId}
@@ -185,7 +179,7 @@ export default function MissionCard({
185179
groupStudyId={groupStudyId}
186180
/>
187181
</div>
188-
</button>
182+
</div>
189183
</li>
190184
);
191185
}
@@ -282,7 +276,7 @@ function MissionCardContent({
282276
return (
283277
<div className="flex flex-col gap-100">
284278
{deadlineInfo && (
285-
<span className={cn('font-designer-12b', 'text-text-brand')}>
279+
<span className={cn('font-designer-12b', 'text-text-brand text-left')}>
286280
{deadlineInfo.text}
287281
</span>
288282
)}

src/components/card/my-homework-status-card.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ export default function MyHomeworkStatusCard({
3434
};
3535

3636
const isSubmissionOpen = mission?.status === 'IN_PROGRESS';
37+
const isSubmissionNotOpened = mission?.status === 'NOT_STARTED';
38+
const isSubmissionClosed =
39+
mission?.status === 'ENDED' || mission?.status === 'EVALUATION_COMPLETED';
3740

38-
// 미제출 상태
3941
if (!myHomework || myHomework.homeworkStatus === 'NOT_SUBMITTED') {
4042
return (
4143
<div className="flex flex-col gap-300">
@@ -44,9 +46,13 @@ export default function MyHomeworkStatusCard({
4446
</span>
4547
<div className="border-border-default rounded-100 flex flex-col items-center justify-center gap-200 border py-400">
4648
<span className="text-text-subtlest font-designer-14r">
47-
{isSubmissionOpen
48-
? '아직 과제를 제출하지 않았습니다.'
49-
: '제출 기간이 종료되었습니다.'}
49+
{isSubmissionOpen && '아직 과제를 제출하지 않았습니다.'}
50+
</span>
51+
<span className="text-text-subtlest font-designer-14r">
52+
{isSubmissionNotOpened && '아직 제출 기간이 아닙니다.'}
53+
</span>
54+
<span className="text-text-subtlest font-designer-14r">
55+
{isSubmissionClosed && '제출 기간이 종료되었습니다.'}
5056
</span>
5157
{isSubmissionOpen && <SubmitHomeworkModal missionId={missionId} />}
5258
</div>
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
'use client';
2+
3+
import { zodResolver } from '@hookform/resolvers/zod';
4+
import { useState } from 'react';
5+
import { FormProvider, useForm } from 'react-hook-form';
6+
import { z } from 'zod';
7+
import Button from '@/components/common/ui/button';
8+
import FormField from '@/components/common/ui/form/form-field';
9+
import { TextAreaInput } from '@/components/common/ui/input';
10+
import { Modal } from '@/components/common/ui/modal';
11+
import { GroupItems } from '@/components/common/ui/toggle';
12+
import {
13+
useCreateEvaluation,
14+
useGetMissionEvaluationGrades,
15+
} from '@/hooks/queries/evaluation-api';
16+
import { useToastStore } from '@/stores/use-toast-store';
17+
18+
const CreateEvaluationFormSchema = z.object({
19+
gradeCode: z.string().min(1, '평가 등급을 선택해주세요.'),
20+
comment: z.string().min(1, '정성 코멘트를 입력해주세요.').max(1000),
21+
});
22+
23+
type CreateEvaluationFormValues = z.infer<typeof CreateEvaluationFormSchema>;
24+
25+
interface CreateEvaluationModalProps {
26+
homeworkId: number;
27+
}
28+
29+
export default function CreateEvaluationModal({
30+
homeworkId,
31+
}: CreateEvaluationModalProps) {
32+
const [open, setOpen] = useState<boolean>(false);
33+
34+
return (
35+
<Modal.Root open={open} onOpenChange={setOpen}>
36+
<Modal.Trigger asChild>
37+
<Button size="medium" className="font-designer-16r w-fit">
38+
과제 평가하기
39+
</Button>
40+
</Modal.Trigger>
41+
42+
<Modal.Portal>
43+
<Modal.Overlay />
44+
<Modal.Content className="w-[840px]">
45+
<Modal.Header variant="form">
46+
<Modal.Title className="font-designer-20b text-text-strong">
47+
평가하기
48+
</Modal.Title>
49+
<Modal.CloseButton onClick={() => setOpen(false)} />
50+
</Modal.Header>
51+
52+
<CreateEvaluationForm
53+
homeworkId={homeworkId}
54+
onClose={() => setOpen(false)}
55+
/>
56+
</Modal.Content>
57+
</Modal.Portal>
58+
</Modal.Root>
59+
);
60+
}
61+
62+
interface CreateEvaluationFormProps {
63+
homeworkId: number;
64+
onClose: () => void;
65+
}
66+
67+
function CreateEvaluationForm({
68+
homeworkId,
69+
onClose,
70+
}: CreateEvaluationFormProps) {
71+
const methods = useForm<CreateEvaluationFormValues>({
72+
resolver: zodResolver(CreateEvaluationFormSchema),
73+
mode: 'onChange',
74+
defaultValues: {
75+
gradeCode: undefined,
76+
comment: '',
77+
},
78+
});
79+
80+
const { handleSubmit, formState } = methods;
81+
82+
const { data: grades } = useGetMissionEvaluationGrades();
83+
const { mutate: createEvaluation } = useCreateEvaluation();
84+
const showToast = useToastStore((state) => state.showToast);
85+
86+
const onValidSubmit = (values: CreateEvaluationFormValues) => {
87+
createEvaluation(
88+
{
89+
homeworkId,
90+
request: values,
91+
},
92+
{
93+
onSuccess: () => {
94+
showToast('평가가 성공적으로 제출되었습니다!');
95+
onClose();
96+
},
97+
onError: () => {
98+
showToast('평가 제출에 실패했습니다. 다시 시도해주세요.', 'error');
99+
},
100+
},
101+
);
102+
};
103+
104+
const gradeOptions = (grades ?? [])
105+
.sort((a, b) => (a.orderNum ?? 0) - (b.orderNum ?? 0))
106+
.map((grade) => ({
107+
value: grade.code,
108+
label: `${grade.label} (${grade.score === 0 ? '0' : (grade.score?.toFixed(1) ?? '-')})`,
109+
}));
110+
111+
return (
112+
<FormProvider {...methods}>
113+
<Modal.Body variant="form">
114+
<form
115+
id="create-evaluation"
116+
className="flex flex-col gap-300"
117+
onSubmit={handleSubmit(onValidSubmit)}
118+
>
119+
<FormField<CreateEvaluationFormValues, 'gradeCode'>
120+
name="gradeCode"
121+
label="평가 점수 선택"
122+
direction="vertical"
123+
required
124+
>
125+
<GroupItems
126+
variant="square"
127+
options={gradeOptions}
128+
multiple={false}
129+
allowDeselect={false}
130+
/>
131+
</FormField>
132+
133+
<FormField<CreateEvaluationFormValues, 'comment'>
134+
name="comment"
135+
label="정성 코멘트"
136+
direction="vertical"
137+
required
138+
>
139+
<TextAreaInput
140+
id="comment"
141+
placeholder="정성 코멘트를 입력해 주세요."
142+
className="min-h-[230px]"
143+
maxLength={1000}
144+
/>
145+
</FormField>
146+
</form>
147+
</Modal.Body>
148+
149+
<Modal.Footer variant="form">
150+
<Modal.Close asChild>
151+
<Button color="secondary" size="large" onClick={onClose}>
152+
취소
153+
</Button>
154+
</Modal.Close>
155+
<Button
156+
color="primary"
157+
size="large"
158+
type="submit"
159+
form="create-evaluation"
160+
disabled={!formState.isValid || formState.isSubmitting}
161+
>
162+
평가 완료
163+
</Button>
164+
</Modal.Footer>
165+
</FormProvider>
166+
);
167+
}

src/components/contents/homework-detail-content.tsx

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import dynamic from 'next/dynamic';
44
import { useRouter, useSearchParams } from 'next/navigation';
55
import { useState } from 'react';
66

7-
import type { PeerReviewResponse } from '@/api/openapi/models';
7+
import type {
8+
EvaluationResponse,
9+
PeerReviewResponse,
10+
} from '@/api/openapi/models';
811
import Avatar from '@/components/common/ui/avatar';
912
import Button from '@/components/common/ui/button';
1013
import MoreMenu from '@/components/common/ui/dropdown/more-menu';
@@ -15,7 +18,6 @@ import {
1518
useDeletePeerReview,
1619
useUpdatePeerReview,
1720
} from '@/hooks/queries/peer-review-api';
18-
1921
import { useUserStore } from '@/stores/useUserStore';
2022
import { formatExternalLink } from '@/utils/format';
2123
import MarkdownContent from '../common/ui/editor/markdown-content';
@@ -35,14 +37,21 @@ const EditHomeworkModal = dynamic(
3537
{ ssr: false },
3638
);
3739

40+
const CreateEvaluationModal = dynamic(
41+
() => import('@/components/common/modals/create-evaluation-modal'),
42+
{ ssr: false },
43+
);
44+
3845
interface HomeworkDetailContentProps {
3946
missionId: number;
4047
homeworkId: number;
48+
showLeaderEvaluation?: boolean;
4149
}
4250

4351
export default function HomeworkDetailContent({
4452
homeworkId,
4553
missionId,
54+
showLeaderEvaluation,
4655
}: HomeworkDetailContentProps) {
4756
const router = useRouter();
4857
const searchParams = useSearchParams();
@@ -148,6 +157,13 @@ export default function HomeworkDetailContent({
148157
)}
149158
</div>
150159

160+
{showLeaderEvaluation && mission.status === 'ENDED' && (
161+
<LeaderEvaluationSection
162+
evaluation={homework.evaluation}
163+
homeworkId={homeworkId}
164+
/>
165+
)}
166+
151167
{/* 피어 리뷰 */}
152168
<PeerReviewSection
153169
homeworkId={homeworkId}
@@ -417,3 +433,49 @@ function PeerReviewInput({
417433
</div>
418434
);
419435
}
436+
437+
interface LeaderEvaluationSectionProps {
438+
evaluation?: EvaluationResponse;
439+
homeworkId: number;
440+
}
441+
442+
function LeaderEvaluationSection({
443+
evaluation,
444+
homeworkId,
445+
}: LeaderEvaluationSectionProps) {
446+
return (
447+
<div className="flex flex-col gap-200">
448+
<span className="font-designer-18b text-text-default">리더 평가</span>
449+
450+
<div className="border-border-default rounded-100 flex flex-col items-center justify-center gap-200 border p-400">
451+
{evaluation ? (
452+
<div className="flex w-full flex-col gap-200">
453+
<div className="flex items-center gap-200">
454+
<span className="font-designer-14b text-text-default">
455+
평가 등급
456+
</span>
457+
<span className="text-text-brand font-designer-16b">
458+
{evaluation.grade?.gradeLabel ?? '-'}
459+
</span>
460+
</div>
461+
<div className="flex flex-col gap-100">
462+
<span className="font-designer-14b text-text-default">
463+
평가 코멘트
464+
</span>
465+
<p className="text-text-default font-designer-14r wrap-anywhere whitespace-pre-wrap">
466+
{evaluation.comment?.trim() ? evaluation.comment : '-'}
467+
</p>
468+
</div>
469+
</div>
470+
) : (
471+
<>
472+
<span className="text-text-subtlest font-designer-14r">
473+
아직 평가하지 않은 과제입니다.
474+
</span>
475+
<CreateEvaluationModal homeworkId={homeworkId} />
476+
</>
477+
)}
478+
</div>
479+
</div>
480+
);
481+
}

src/components/contents/mission-detail-content.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ export default function MissionDetailContent({
4040
return null;
4141
}
4242

43+
const maxCount = mission.maxHomeworkSubmissionCount || 1;
44+
4345
const progressValue =
44-
((mission.currentHomeworkSubmissionCount ?? 0) /
45-
(mission.maxHomeworkSubmissionCount ?? 1)) *
46-
100;
46+
((mission.currentHomeworkSubmissionCount ?? 0) / maxCount) * 100;
4747

4848
return (
4949
<div className="flex flex-col gap-400">

0 commit comments

Comments
 (0)