Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 18 additions & 24 deletions src/components/card/mission-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import dayjs from 'dayjs';
import { Lock } from 'lucide-react';
import dynamic from 'next/dynamic';
import { ComponentProps, SyntheticEvent } from 'react';
import type { ComponentProps } from 'react';

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

const stopActionAreaPropagation = (event: SyntheticEvent) => {
event.stopPropagation();
};

// 비가입자 2주차+ 잠금 카드
if (isLocked) {
return (
Expand Down Expand Up @@ -158,24 +154,22 @@ export default function MissionCard({
if (isLeader && mission.status === 'NOT_STARTED') {
return (
<li className="border-border-default rounded-100 border bg-[#fff]">
<button
type="button"
className="flex w-full cursor-pointer items-center justify-between p-300"
onClick={handleSelectMission}
>
<MissionCardContent
title={mission.title}
weekNum={mission.weekNum}
statusConfig={statusConfig}
startDate={mission.startDate}
endDate={mission.endDate}
deadlineInfo={undefined}
/>
<div
className="flex flex-col gap-100"
role="none"
onClick={stopActionAreaPropagation}
<div className="flex w-full items-center justify-between p-300">
<button
type="button"
className="flex flex-1 cursor-pointer items-center"
onClick={handleSelectMission}
>
<MissionCardContent
title={mission.title}
weekNum={mission.weekNum}
statusConfig={statusConfig}
startDate={mission.startDate}
endDate={mission.endDate}
deadlineInfo={undefined}
/>
</button>
<div className="flex flex-col gap-100">
<EditMissionModal
missionId={mission.missionId}
groupStudyId={groupStudyId}
Expand All @@ -185,7 +179,7 @@ export default function MissionCard({
groupStudyId={groupStudyId}
/>
</div>
</button>
</div>
</li>
);
}
Expand Down Expand Up @@ -282,7 +276,7 @@ function MissionCardContent({
return (
<div className="flex flex-col gap-100">
{deadlineInfo && (
<span className={cn('font-designer-12b', 'text-text-brand')}>
<span className={cn('font-designer-12b', 'text-text-brand text-left')}>
{deadlineInfo.text}
</span>
)}
Expand Down
14 changes: 10 additions & 4 deletions src/components/card/my-homework-status-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ export default function MyHomeworkStatusCard({
};

const isSubmissionOpen = mission?.status === 'IN_PROGRESS';
const isSubmissionNotOpened = mission?.status === 'NOT_STARTED';
const isSubmissionClosed =
mission?.status === 'ENDED' || mission?.status === 'EVALUATION_COMPLETED';

// 미제출 상태
if (!myHomework || myHomework.homeworkStatus === 'NOT_SUBMITTED') {
return (
<div className="flex flex-col gap-300">
Expand All @@ -44,9 +46,13 @@ export default function MyHomeworkStatusCard({
</span>
<div className="border-border-default rounded-100 flex flex-col items-center justify-center gap-200 border py-400">
<span className="text-text-subtlest font-designer-14r">
{isSubmissionOpen
? '아직 과제를 제출하지 않았습니다.'
: '제출 기간이 종료되었습니다.'}
{isSubmissionOpen && '아직 과제를 제출하지 않았습니다.'}
</span>
<span className="text-text-subtlest font-designer-14r">
{isSubmissionNotOpened && '아직 제출 기간이 아닙니다.'}
</span>
<span className="text-text-subtlest font-designer-14r">
{isSubmissionClosed && '제출 기간이 종료되었습니다.'}
</span>
{isSubmissionOpen && <SubmitHomeworkModal missionId={missionId} />}
</div>
Expand Down
167 changes: 167 additions & 0 deletions src/components/common/modals/create-evaluation-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
'use client';

import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { z } from 'zod';
import Button from '@/components/common/ui/button';
import FormField from '@/components/common/ui/form/form-field';
import { TextAreaInput } from '@/components/common/ui/input';
import { Modal } from '@/components/common/ui/modal';
import { GroupItems } from '@/components/common/ui/toggle';
import {
useCreateEvaluation,
useGetMissionEvaluationGrades,
} from '@/hooks/queries/evaluation-api';
import { useToastStore } from '@/stores/use-toast-store';

const CreateEvaluationFormSchema = z.object({
gradeCode: z.string().min(1, '평가 등급을 선택해주세요.'),
comment: z.string().min(1, '정성 코멘트를 입력해주세요.').max(1000),
});

type CreateEvaluationFormValues = z.infer<typeof CreateEvaluationFormSchema>;

interface CreateEvaluationModalProps {
homeworkId: number;
}

export default function CreateEvaluationModal({
homeworkId,
}: CreateEvaluationModalProps) {
const [open, setOpen] = useState<boolean>(false);

return (
<Modal.Root open={open} onOpenChange={setOpen}>
<Modal.Trigger asChild>
<Button size="medium" className="font-designer-16r w-fit">
과제 평가하기
</Button>
</Modal.Trigger>

<Modal.Portal>
<Modal.Overlay />
<Modal.Content className="w-[840px]">
<Modal.Header variant="form">
<Modal.Title className="font-designer-20b text-text-strong">
평가하기
</Modal.Title>
<Modal.CloseButton onClick={() => setOpen(false)} />
</Modal.Header>

<CreateEvaluationForm
homeworkId={homeworkId}
onClose={() => setOpen(false)}
/>
</Modal.Content>
</Modal.Portal>
</Modal.Root>
);
}

interface CreateEvaluationFormProps {
homeworkId: number;
onClose: () => void;
}

function CreateEvaluationForm({
homeworkId,
onClose,
}: CreateEvaluationFormProps) {
const methods = useForm<CreateEvaluationFormValues>({
resolver: zodResolver(CreateEvaluationFormSchema),
mode: 'onChange',
defaultValues: {
gradeCode: undefined,
comment: '',
},
});

const { handleSubmit, formState } = methods;

const { data: grades } = useGetMissionEvaluationGrades();
const { mutate: createEvaluation } = useCreateEvaluation();
const showToast = useToastStore((state) => state.showToast);

const onValidSubmit = (values: CreateEvaluationFormValues) => {
createEvaluation(
{
homeworkId,
request: values,
},
{
onSuccess: () => {
showToast('평가가 성공적으로 제출되었습니다!');
onClose();
},
onError: () => {
showToast('평가 제출에 실패했습니다. 다시 시도해주세요.', 'error');
},
},
);
};

const gradeOptions = (grades ?? [])
.sort((a, b) => (a.orderNum ?? 0) - (b.orderNum ?? 0))
.map((grade) => ({
value: grade.code,
label: `${grade.label} (${grade.score === 0 ? '0' : (grade.score?.toFixed(1) ?? '-')})`,
}));

return (
<FormProvider {...methods}>
<Modal.Body variant="form">
<form
id="create-evaluation"
className="flex flex-col gap-300"
onSubmit={handleSubmit(onValidSubmit)}
>
<FormField<CreateEvaluationFormValues, 'gradeCode'>
name="gradeCode"
label="평가 점수 선택"
direction="vertical"
required
>
<GroupItems
variant="square"
options={gradeOptions}
multiple={false}
allowDeselect={false}
/>
</FormField>

<FormField<CreateEvaluationFormValues, 'comment'>
name="comment"
label="정성 코멘트"
direction="vertical"
required
>
<TextAreaInput
id="comment"
placeholder="정성 코멘트를 입력해 주세요."
className="min-h-[230px]"
maxLength={1000}
/>
</FormField>
</form>
</Modal.Body>

<Modal.Footer variant="form">
<Modal.Close asChild>
<Button color="secondary" size="large" onClick={onClose}>
취소
</Button>
</Modal.Close>
<Button
color="primary"
size="large"
type="submit"
form="create-evaluation"
disabled={!formState.isValid || formState.isSubmitting}
>
평가 완료
</Button>
</Modal.Footer>
</FormProvider>
);
}
66 changes: 64 additions & 2 deletions src/components/contents/homework-detail-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import dynamic from 'next/dynamic';
import { useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react';

import type { PeerReviewResponse } from '@/api/openapi/models';
import type {
EvaluationResponse,
PeerReviewResponse,
} from '@/api/openapi/models';
import Avatar from '@/components/common/ui/avatar';
import Button from '@/components/common/ui/button';
import MoreMenu from '@/components/common/ui/dropdown/more-menu';
Expand All @@ -15,7 +18,6 @@ import {
useDeletePeerReview,
useUpdatePeerReview,
} from '@/hooks/queries/peer-review-api';

import { useUserStore } from '@/stores/useUserStore';
import { formatExternalLink } from '@/utils/format';
import MarkdownContent from '../common/ui/editor/markdown-content';
Expand All @@ -35,14 +37,21 @@ const EditHomeworkModal = dynamic(
{ ssr: false },
);

const CreateEvaluationModal = dynamic(
() => import('@/components/common/modals/create-evaluation-modal'),
{ ssr: false },
);

interface HomeworkDetailContentProps {
missionId: number;
homeworkId: number;
showLeaderEvaluation?: boolean;
}

export default function HomeworkDetailContent({
homeworkId,
missionId,
showLeaderEvaluation,
}: HomeworkDetailContentProps) {
const router = useRouter();
const searchParams = useSearchParams();
Expand Down Expand Up @@ -148,6 +157,13 @@ export default function HomeworkDetailContent({
)}
</div>

{showLeaderEvaluation && mission.status === 'ENDED' && (
<LeaderEvaluationSection
evaluation={homework.evaluation}
homeworkId={homeworkId}
/>
)}

{/* 피어 리뷰 */}
<PeerReviewSection
homeworkId={homeworkId}
Expand Down Expand Up @@ -417,3 +433,49 @@ function PeerReviewInput({
</div>
);
}

interface LeaderEvaluationSectionProps {
evaluation?: EvaluationResponse;
homeworkId: number;
}

function LeaderEvaluationSection({
evaluation,
homeworkId,
}: LeaderEvaluationSectionProps) {
return (
<div className="flex flex-col gap-200">
<span className="font-designer-18b text-text-default">리더 평가</span>

<div className="border-border-default rounded-100 flex flex-col items-center justify-center gap-200 border p-400">
{evaluation ? (
<div className="flex w-full flex-col gap-200">
<div className="flex items-center gap-200">
<span className="font-designer-14b text-text-default">
평가 등급
</span>
<span className="text-text-brand font-designer-16b">
{evaluation.grade?.gradeLabel ?? '-'}
</span>
</div>
<div className="flex flex-col gap-100">
<span className="font-designer-14b text-text-default">
평가 코멘트
</span>
<p className="text-text-default font-designer-14r wrap-anywhere whitespace-pre-wrap">
{evaluation.comment?.trim() ? evaluation.comment : '-'}
</p>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
</div>
) : (
<>
<span className="text-text-subtlest font-designer-14r">
아직 평가하지 않은 과제입니다.
</span>
<CreateEvaluationModal homeworkId={homeworkId} />
</>
)}
</div>
</div>
);
}
6 changes: 3 additions & 3 deletions src/components/contents/mission-detail-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ export default function MissionDetailContent({
return null;
}

const maxCount = mission.maxHomeworkSubmissionCount || 1;

const progressValue =
((mission.currentHomeworkSubmissionCount ?? 0) /
(mission.maxHomeworkSubmissionCount ?? 1)) *
100;
((mission.currentHomeworkSubmissionCount ?? 0) / maxCount) * 100;

return (
<div className="flex flex-col gap-400">
Expand Down
Loading
Loading