diff --git a/apps/admin/src/components/problem/ChildProblemSection.tsx b/apps/admin/src/components/problem/ChildProblemSection.tsx index f9ef5b89..73418da9 100644 --- a/apps/admin/src/components/problem/ChildProblemSection.tsx +++ b/apps/admin/src/components/problem/ChildProblemSection.tsx @@ -108,7 +108,7 @@ export const ChildProblemSection = ({ }; return { - // id: blockData.id || 0, + // id: blockData.id ?? 0, rank: index, type: blockData.type, data: blockData.data || blockData.content, @@ -233,9 +233,9 @@ export const ChildProblemSection = ({

새끼 문제 등록

-

+ {/*

새끼 문제은 저장 후 항목 추가가 불가능해요 -

+

*/}
@@ -291,6 +291,7 @@ export const ChildProblemSection = ({ diff --git a/apps/admin/src/components/problem/MainProblemSection.tsx b/apps/admin/src/components/problem/MainProblemSection.tsx index 6b21403a..3eef89a3 100644 --- a/apps/admin/src/components/problem/MainProblemSection.tsx +++ b/apps/admin/src/components/problem/MainProblemSection.tsx @@ -1,5 +1,14 @@ import { useState, ChangeEvent } from 'react'; -import { AnswerInput, ComponentWithLabel, Input, SectionCard, Tag, Button } from '@components'; +import { + AnswerInput, + ComponentWithLabel, + Input, + SectionCard, + Tag, + Button, + Modal, + TagSelectModal, +} from '@components'; import { Controller, Control, @@ -10,8 +19,10 @@ import { } from 'react-hook-form'; import { components } from '@schema'; import EditorModal from '@repo/pointer-editor/EditorModal'; +import { IcDelete, IcPlus } from '@svg'; +import { useModal } from '@hooks'; -import { ImageUpload, LevelSelect, TextArea } from '@/components/problem'; +import { LevelSelect, TextArea } from '@/components/problem'; import { ProblemAnswerType } from '@/types/component'; type ProblemUpdateRequest = components['schemas']['ProblemUpdateRequest']; @@ -46,17 +57,26 @@ export const MainProblemSection = ({ const [isEditorModalOpen, setIsEditorModalOpen] = useState(false); const [isPointingQuestionModalOpen, setIsPointingQuestionModalOpen] = useState(false); const [isPointingCommentModalOpen, setIsPointingCommentModalOpen] = useState(false); + const [currentPointingIndex, setCurrentPointingIndex] = useState(null); // 임시로 수정된 블록들을 저장하는 상태 const [tempMainProblemBlocks, setTempMainProblemBlocks] = useState( fetchedProblemData?.problemContent?.blocks || null ); - const [tempPointingQuestionBlocks, setTempPointingQuestionBlocks] = useState( - fetchedProblemData?.pointings?.[0]?.questionContent?.blocks || null - ); - const [tempPointingCommentBlocks, setTempPointingCommentBlocks] = useState( - fetchedProblemData?.pointings?.[0]?.commentContent?.blocks || null - ); + const [tempPointingQuestionBlocks, setTempPointingQuestionBlocks] = useState< + Record + >({}); + const [tempPointingCommentBlocks, setTempPointingCommentBlocks] = useState< + Record + >({}); + + // Tag modal for main pointings + const { + isOpen: isMainPointingTagModalOpen, + openModal: openMainPointingTagModal, + closeModal: closeMainPointingTagModal, + } = useModal(); + const [currentMainPointingTagList, setCurrentMainPointingTagList] = useState([]); const handleOpenEditorModal = () => { setIsEditorModalOpen(true); @@ -66,7 +86,8 @@ export const MainProblemSection = ({ setIsEditorModalOpen(false); }; - const handleOpenPointingQuestionModal = () => { + const handleOpenPointingQuestionModal = (index: number) => { + setCurrentPointingIndex(index); setIsPointingQuestionModalOpen(true); }; @@ -74,7 +95,8 @@ export const MainProblemSection = ({ setIsPointingQuestionModalOpen(false); }; - const handleOpenPointingCommentModal = () => { + const handleOpenPointingCommentModal = (index: number) => { + setCurrentPointingIndex(index); setIsPointingCommentModalOpen(true); }; @@ -101,6 +123,18 @@ export const MainProblemSection = ({ const minutes = Math.floor((watchedRecommendedTimeSec || 0) / 60); const seconds = Math.max(0, (watchedRecommendedTimeSec || 0) % 60); + type MainPointing = components['schemas']['PointingUpdateRequest']; + const watchedPointings = useWatch({ + control, + name: 'pointings', + }) as MainPointing[] | undefined; + + const pointingList = ( + Array.isArray(watchedPointings) + ? watchedPointings + : (fetchedProblemData?.pointings as unknown as MainPointing[] | undefined) || [] + ) as MainPointing[]; + const handleChangeMinutes = (e: ChangeEvent) => { const raw = e.target.value.trim(); const nextMinutes = raw === '' ? 0 : Math.max(0, Number(raw)); @@ -120,14 +154,14 @@ export const MainProblemSection = ({ const formatBlocks = (blocks: unknown[]): ContentBlockUpdateRequest[] => { return blocks.map((block, index) => { const blockData = block as { - id?: number; + // id?: number; type?: 'TEXT' | 'IMAGE'; data?: string; content?: string; }; return { - id: blockData.id ?? 0, + // id: blockData.id ?? 0, rank: index, type: blockData.type, data: blockData.data || blockData.content, @@ -144,83 +178,128 @@ export const MainProblemSection = ({ }; const handleSavePointingQuestion = (blocks: unknown[]) => { + if (currentPointingIndex === null) return; const formattedBlocks = formatBlocks(blocks); - setValue('pointings.0.questionContent.blocks', formattedBlocks); - setTempPointingQuestionBlocks(blocks); // 임시 상태에 원본 블록 저장 + setValue(`pointings.${currentPointingIndex}.questionContent.blocks`, formattedBlocks); + setTempPointingQuestionBlocks((prev) => ({ ...prev, [currentPointingIndex!]: blocks })); console.log('Updated pointing question blocks:', formattedBlocks); setIsPointingQuestionModalOpen(false); }; const handleSavePointingComment = (blocks: unknown[]) => { + if (currentPointingIndex === null) return; const formattedBlocks = formatBlocks(blocks); - setValue('pointings.0.commentContent.blocks', formattedBlocks); - setTempPointingCommentBlocks(blocks); // 임시 상태에 원본 블록 저장 + setValue(`pointings.${currentPointingIndex}.commentContent.blocks`, formattedBlocks); + setTempPointingCommentBlocks((prev) => ({ ...prev, [currentPointingIndex!]: blocks })); console.log('Updated pointing comment blocks:', formattedBlocks); setIsPointingCommentModalOpen(false); }; + + const handleAddPointing = () => { + const current = (pointingList || []).filter(Boolean) as MainPointing[]; + const newPointing = { + // id: undefined, + no: current.length + 1, + questionContent: { blocks: [] }, + commentContent: { blocks: [] }, + concepts: [], + }; + setValue('pointings', [...current, newPointing], { shouldDirty: true, shouldValidate: true }); + }; + + const handleDeletePointing = (index: number) => { + const current = (pointingList || []).filter(Boolean) as MainPointing[]; + const updated = current.filter((_, i) => i !== index).map((p, i) => ({ ...p, no: i + 1 })); + setValue('pointings', updated, { shouldDirty: true, shouldValidate: true }); + }; + + const hasPointingBlocks = (type: 'question' | 'comment', index: number): boolean => { + if (type === 'question') { + const temp = tempPointingQuestionBlocks[index]; + const fetched = fetchedProblemData?.pointings?.[index]?.questionContent?.blocks; + return Boolean((temp && temp.length > 0) || (fetched && fetched.length > 0)); + } + const temp = tempPointingCommentBlocks[index]; + const fetched = fetchedProblemData?.pointings?.[index]?.commentContent?.blocks; + return Boolean((temp && temp.length > 0) || (fetched && fetched.length > 0)); + }; + + const handleOpenMainPointingTagModal = (index: number, concepts: number[]) => { + setCurrentPointingIndex(index); + setCurrentMainPointingTagList(concepts || []); + openMainPointingTagModal(); + }; + + const handleChangeMainPointingTagList = (tagList: number[]) => { + if (currentPointingIndex === null) return; + setValue(`pointings.${currentPointingIndex}.concepts`, [...tagList], { + shouldDirty: true, + shouldValidate: true, + }); + }; return ( - -

메인 문제 등록

-
- - - {errors?.title && ( -

- {(errors.title as { message?: string })?.message || '필수 입력 항목입니다.'} -

- )} -
- -
- {concepts && - concepts?.length > 0 && - concepts.map((tag) => ( - onRemoveTag?.(tag)} +
+

메인 문제 입력

+ +
+ + + {errors?.title && ( +

+ {(errors.title as { message?: string })?.message || '필수 입력 항목입니다.'} +

+ )} +
+ +
+ {concepts && + concepts?.length > 0 && + concepts.map((tag) => ( + onRemoveTag?.(tag)} + /> + ))} + + {concepts && concepts?.length === 0 && ( +

태그를 추가해주세요.

+ )} +
+
+ + +
+ + { + answerTypeRegister.onChange(e); + }} /> - ))} - - {concepts && concepts?.length === 0 && ( -

태그를 추가해주세요.

+ +
+
+ {(errors?.answerType || errors?.answer) && ( +

필수 입력 항목입니다.

)} -
- - - -
- - { - answerTypeRegister.onChange(e); - }} - /> - - -
- {(errors?.answerType || errors?.answer) && ( -

필수 입력 항목입니다.

- )} -
+ -
- +
)} -
- -
-
- - - - - - (v !== undefined && v !== null && !Number.isNaN(v)) || - '필수 입력 항목입니다.', - })} - /> -
+ +
+
+ + + + + + (v !== undefined && v !== null && !Number.isNaN(v)) || + '필수 입력 항목입니다.', + })} + />
- {errors?.recommendedTimeSec && ( -

- {(errors.recommendedTimeSec as { message?: string })?.message || - '필수 입력 항목입니다.'} -

- )} - -
-
- - - - -
-
+
+ {errors?.recommendedTimeSec && ( +

+ {(errors.recommendedTimeSec as { message?: string })?.message || + '필수 입력 항목입니다.'} +

+ )} + + + + + + + +