Skip to content

Commit 94ff76f

Browse files
Merge pull request #631 from code-zero-to-one/fix/lesson-type
LessonReviewForm 리팩토링 및 3-type 버그 수정
2 parents cfab283 + 69980c1 commit 94ff76f

3 files changed

Lines changed: 151 additions & 152 deletions

File tree

src/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-review-form.tsx

Lines changed: 123 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
import { Image as ImageIcon, Link as LinkIcon, X } from 'lucide-react';
44
import Image from 'next/image';
5+
import { useEffect, useState } from 'react';
56
import { cn } from '@/components/common/ui/(shadcn)/lib/utils';
67
import MarkdownEditor from '@/components/common/ui/editor/markdown-editor';
78
import { uploadCommunityMarkdownImage } from '@/features/community/model/community-markdown-image-upload';
9+
import type { LessonRetrospectivePurpose } from '@/types/api/course.types';
10+
import { LessonLinkModal } from './lesson-link-modal';
811
import { RatingBox } from './lesson-rating-box';
12+
import { LessonScreenshotModal } from './lesson-screenshot-modal';
913

1014
export const POSITIVE_CHIPS = [
1115
'설명이 이해하기 쉬웠어요',
@@ -18,29 +22,22 @@ export const NEGATIVE_CHIPS = [
1822
'뭘 하는 건지 모르겠어요',
1923
];
2024

21-
interface Props {
25+
export interface LessonFormData {
2226
starRating: number;
23-
onStarRatingChange: (v: number) => void;
24-
highlightAnswer: string;
25-
unexpectedAnswer: string;
26-
selectedChips: Set<string>;
27+
expectationAnswer: string;
28+
surpriseAnswer: string;
29+
artifactImageUrl: string | null;
30+
artifactLink: string | null;
31+
checklistFlags: boolean[];
2732
feedbackText: string;
28-
submitDisabled: boolean;
29-
submitting: boolean;
30-
alreadySubmitted: boolean;
31-
showArtifact: boolean;
33+
}
34+
35+
interface LessonFormProps {
36+
retrospectivePurpose: LessonRetrospectivePurpose;
3237
retrospectivePrompt?: string;
33-
onHighlightAnswerChange: (v: string) => void;
34-
onUnexpectedAnswerChange: (v: string) => void;
35-
onToggleChip: (chip: string) => void;
36-
onFeedbackChange: (v: string) => void;
37-
artifactImagePreviewUrl?: string | null;
38-
artifactLink?: string | null;
39-
onAttachScreenshot: () => void;
40-
onAttachLink: () => void;
41-
onRemoveArtifactImage?: () => void;
42-
onRemoveArtifactLink?: () => void;
43-
onSubmit: () => void;
38+
alreadySubmitted: boolean;
39+
submitting: boolean;
40+
onSubmit: (data: LessonFormData) => void;
4441
}
4542

4643
function QuestionBlock({
@@ -97,29 +94,69 @@ function SectionTitle({ bold, suffix }: { bold: string; suffix: string }) {
9794
}
9895

9996
export function LessonReviewForm({
100-
starRating,
101-
onStarRatingChange,
102-
highlightAnswer,
103-
unexpectedAnswer,
104-
selectedChips,
105-
feedbackText,
106-
submitDisabled,
107-
submitting,
108-
alreadySubmitted,
109-
showArtifact,
97+
retrospectivePurpose,
11098
retrospectivePrompt,
111-
onHighlightAnswerChange,
112-
onUnexpectedAnswerChange,
113-
onToggleChip,
114-
onFeedbackChange,
115-
artifactImagePreviewUrl,
116-
artifactLink,
117-
onAttachScreenshot,
118-
onAttachLink,
119-
onRemoveArtifactImage,
120-
onRemoveArtifactLink,
99+
alreadySubmitted,
100+
submitting,
121101
onSubmit,
122-
}: Props) {
102+
}: LessonFormProps) {
103+
const isQuiz = retrospectivePurpose === 'SUBJECTIVE_QUIZ';
104+
105+
const [starRating, setStarRating] = useState(0);
106+
const [expectationAnswer, setExpectationAnswer] = useState('');
107+
const [surpriseAnswer, setSurpriseAnswer] = useState('');
108+
const [selectedChips, setSelectedChips] = useState<Set<string>>(new Set());
109+
const [feedbackText, setFeedbackText] = useState('');
110+
const [artifactImageUrl, setArtifactImageUrl] = useState<string | null>(null);
111+
const [artifactPreviewUrl, setArtifactPreviewUrl] = useState<string | null>(
112+
null,
113+
);
114+
const [artifactLink, setArtifactLink] = useState<string | null>(null);
115+
const [screenshotOpen, setScreenshotOpen] = useState(false);
116+
const [linkOpen, setLinkOpen] = useState(false);
117+
118+
useEffect(() => {
119+
return () => {
120+
if (artifactPreviewUrl) URL.revokeObjectURL(artifactPreviewUrl);
121+
};
122+
}, [artifactPreviewUrl]);
123+
124+
// Backend requires both answers non-blank for all types; artifact required for non-quiz
125+
const isFormValid =
126+
starRating > 0 &&
127+
expectationAnswer.trim().length > 0 &&
128+
surpriseAnswer.trim().length > 0 &&
129+
(isQuiz || !!artifactImageUrl) &&
130+
selectedChips.size >= 2;
131+
132+
const submitDisabled = !isFormValid || submitting || alreadySubmitted;
133+
134+
function toggleChip(chip: string) {
135+
setSelectedChips((prev) => {
136+
const next = new Set(prev);
137+
if (next.has(chip)) next.delete(chip);
138+
else next.add(chip);
139+
return next;
140+
});
141+
}
142+
143+
function handleSubmit() {
144+
if (submitDisabled) return;
145+
const chips = [...selectedChips];
146+
const checklistFlags = [...POSITIVE_CHIPS, ...NEGATIVE_CHIPS].map((c) =>
147+
chips.includes(c),
148+
);
149+
onSubmit({
150+
starRating,
151+
expectationAnswer,
152+
surpriseAnswer,
153+
artifactImageUrl,
154+
artifactLink,
155+
checklistFlags,
156+
feedbackText,
157+
});
158+
}
159+
123160
return (
124161
<div className="flex w-full flex-col gap-700">
125162
{/* Title */}
@@ -143,46 +180,50 @@ export function LessonReviewForm({
143180
별점을 선택해 오늘 레슨 내용 이해도를 알려주세요.
144181
</p>
145182
</div>
146-
<RatingBox rating={starRating} onChange={onStarRatingChange} />
183+
<RatingBox rating={starRating} onChange={setStarRating} />
147184
</div>
148185

149-
{/* Q1 — highlight */}
186+
{/* Q1 — always required by backend (highlightAnswer) */}
150187
<div className="flex flex-col gap-350">
151188
<QuestionBlock
152-
question="오늘 가장 신기했던 코드 하나만 적어볼까요?"
189+
question="이번 레슨, 어떤 기대로 시작했나요?"
153190
helper="어려웠던 점이나 뿌듯했던 순간을 기록해 보세요. 이 기록들은 모여서 당신만의 멋진 포트폴리오가 됩니다."
154-
value={highlightAnswer}
191+
value={expectationAnswer}
155192
placeholder="예 : Cursor에서 Cmd+K를 누르면 Claude가 바로 나타나는 게 신기했다."
156-
onChange={onHighlightAnswerChange}
157-
tall
193+
onChange={setExpectationAnswer}
194+
tall={!isQuiz}
158195
/>
159196
</div>
160197

161-
{/* Q2 — unexpected */}
198+
{/* Q2 — always required by backend (unexpectedAnswer); retrospectivePrompt overrides text for SUBJECTIVE_QUIZ */}
162199
<div className="flex flex-col gap-350">
163200
<QuestionBlock
164201
question={
165202
retrospectivePrompt ?? '직접 해보니 생각과 달랐던 의외의 순간은?'
166203
}
167204
helper="예상과 다르게 잘 됐거나, 막혔던 순간을 솔직하게 적어주세요."
168-
value={unexpectedAnswer}
169-
placeholder="예 : 코드 한 줄만 바꿨는데 전체 디자인이 바뀌어서 놀랐다."
170-
onChange={onUnexpectedAnswerChange}
205+
value={surpriseAnswer}
206+
placeholder={
207+
isQuiz
208+
? '내용을 작성해 주세요. 정답은 없습니다.'
209+
: '예 : 코드 한 줄만 바꿨는데 전체 디자인이 바뀌어서 놀랐다.'
210+
}
211+
onChange={setSurpriseAnswer}
171212
/>
172213
</div>
173214

174-
{/* Project completiononly for 실습 type (artifactSubmissionRequired) */}
175-
{showArtifact && (
215+
{/* Artifact sectionPRACTICE_PROOF and ARTIFACT_SHARE (backend requires artifact for !isQuiz) */}
216+
{!isQuiz && (
176217
<div className="flex flex-col gap-350">
177218
<SectionTitle
178219
bold="오늘의 프로젝트 완성 알리기"
179220
suffix="이미지는 필수로 등록해주세요(링크는 선택)"
180221
/>
181222
<div className="flex flex-col gap-200">
182-
{artifactImagePreviewUrl ? (
223+
{artifactPreviewUrl ? (
183224
<div className="relative">
184225
<Image
185-
src={artifactImagePreviewUrl}
226+
src={artifactPreviewUrl}
186227
alt="첨부 스크린샷"
187228
width={400}
188229
height={202}
@@ -192,7 +233,12 @@ export function LessonReviewForm({
192233
<button
193234
type="button"
194235
aria-label="스크린샷 삭제"
195-
onClick={onRemoveArtifactImage}
236+
onClick={() => {
237+
if (artifactPreviewUrl)
238+
URL.revokeObjectURL(artifactPreviewUrl);
239+
setArtifactImageUrl(null);
240+
setArtifactPreviewUrl(null);
241+
}}
196242
className="absolute -right-75 -top-75 flex h-250 w-250 items-center justify-center rounded-full bg-gray-800 text-background-default"
197243
>
198244
<X className="h-150 w-150" />
@@ -201,7 +247,7 @@ export function LessonReviewForm({
201247
) : (
202248
<button
203249
type="button"
204-
onClick={onAttachScreenshot}
250+
onClick={() => setScreenshotOpen(true)}
205251
className="flex h-800 w-full items-center justify-center gap-75 rounded-100 border border-gray-400 bg-background-default font-designer-18b text-gray-800"
206252
>
207253
<ImageIcon className="h-300 w-300" />
@@ -216,7 +262,7 @@ export function LessonReviewForm({
216262
<button
217263
type="button"
218264
aria-label="링크 삭제"
219-
onClick={onRemoveArtifactLink}
265+
onClick={() => setArtifactLink(null)}
220266
className="ml-200 shrink-0 text-gray-400 hover:text-gray-800"
221267
>
222268
<X className="h-250 w-250" />
@@ -225,7 +271,7 @@ export function LessonReviewForm({
225271
) : (
226272
<button
227273
type="button"
228-
onClick={onAttachLink}
274+
onClick={() => setLinkOpen(true)}
229275
className="flex h-800 w-full items-center justify-center gap-75 rounded-100 border border-gray-400 bg-background-default font-designer-18b text-gray-800"
230276
>
231277
<LinkIcon className="h-300 w-300" />
@@ -247,7 +293,7 @@ export function LessonReviewForm({
247293
chip={chip}
248294
selected={selectedChips.has(chip)}
249295
positive
250-
onClick={() => onToggleChip(chip)}
296+
onClick={() => toggleChip(chip)}
251297
/>
252298
))}
253299
</div>
@@ -257,14 +303,14 @@ export function LessonReviewForm({
257303
key={chip}
258304
chip={chip}
259305
selected={selectedChips.has(chip)}
260-
onClick={() => onToggleChip(chip)}
306+
onClick={() => toggleChip(chip)}
261307
/>
262308
))}
263309
</div>
264310
</div>
265311
<textarea
266312
value={feedbackText}
267-
onChange={(e) => onFeedbackChange(e.target.value)}
313+
onChange={(e) => setFeedbackText(e.target.value)}
268314
placeholder="어떠한 피드백도 좋아요! 간략히 적어주세요! (선택사항)"
269315
className="h-1625 w-full resize-none rounded-200 border border-gray-300 bg-background-default px-300 py-250 font-designer-16m text-gray-800 outline-none placeholder:text-gray-400 focus:border-border-brand"
270316
/>
@@ -274,7 +320,7 @@ export function LessonReviewForm({
274320
<button
275321
type="button"
276322
disabled={submitDisabled}
277-
onClick={onSubmit}
323+
onClick={handleSubmit}
278324
className={cn(
279325
'flex h-1000 w-full items-center justify-center gap-150 rounded-100 font-designer-24b text-text-inverse transition-colors',
280326
submitDisabled
@@ -297,6 +343,20 @@ export function LessonReviewForm({
297343
? '제출 중...'
298344
: '제출하고 다음 Lesson 하러 가기'}
299345
</button>
346+
347+
<LessonScreenshotModal
348+
open={screenshotOpen}
349+
onClose={() => setScreenshotOpen(false)}
350+
onConfirm={(imageUrl, previewUrl) => {
351+
setArtifactImageUrl(imageUrl);
352+
setArtifactPreviewUrl(previewUrl);
353+
}}
354+
/>
355+
<LessonLinkModal
356+
open={linkOpen}
357+
onClose={() => setLinkOpen(false)}
358+
onConfirm={(url) => setArtifactLink(url)}
359+
/>
300360
</div>
301361
);
302362
}

0 commit comments

Comments
 (0)