22
33import { Image as ImageIcon , Link as LinkIcon , X } from 'lucide-react' ;
44import Image from 'next/image' ;
5+ import { useEffect , useState } from 'react' ;
56import { cn } from '@/components/common/ui/(shadcn)/lib/utils' ;
67import MarkdownEditor from '@/components/common/ui/editor/markdown-editor' ;
78import { 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' ;
811import { RatingBox } from './lesson-rating-box' ;
12+ import { LessonScreenshotModal } from './lesson-screenshot-modal' ;
913
1014export 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
4643function QuestionBlock ( {
@@ -97,29 +94,69 @@ function SectionTitle({ bold, suffix }: { bold: string; suffix: string }) {
9794}
9895
9996export 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 completion — only for 실습 type (artifactSubmissionRequired ) */ }
175- { showArtifact && (
215+ { /* Artifact section — PRACTICE_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