"use client"; import * as React from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Textarea } from "@/components/ui/textarea"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Edit, Plus, Trash2 } from "lucide-react"; import { updateComplianceQuestion, getComplianceQuestionOptions, createComplianceQuestionOption, deleteComplianceQuestionOption, getSelectableParentQuestions, } from "@/lib/compliance/services"; import { QUESTION_TYPES } from "@/db/schema/compliance"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; import { complianceQuestions } from "@/db/schema/compliance"; type OptionItem = { optionValue: string; optionText: string; allowsOtherInput: boolean; displayOrder: number }; const RED_FLAG_OPTIONS: OptionItem[] = [ { optionValue: "YES", optionText: "YES", allowsOtherInput: false, displayOrder: 1 }, { optionValue: "NO", optionText: "NO", allowsOtherInput: false, displayOrder: 2 }, ]; const questionSchema = z.object({ questionNumber: z.string().min(1, "질문 번호를 입력하세요"), questionText: z.string().min(1, "질문 내용을 입력하세요"), questionType: z.string().min(1, "질문 유형을 선택하세요"), isRequired: z.boolean(), isRedFlag: z.boolean(), hasDetailText: z.boolean(), hasFileUpload: z.boolean(), isConditional: z.boolean(), parentQuestionId: z.number().optional(), conditionalValue: z.string().optional(), }); type QuestionFormData = z.infer; interface ComplianceQuestionEditDialogProps { question: typeof complianceQuestions.$inferSelect; onSuccess?: () => void; } export function ComplianceQuestionEditSheet({ question, onSuccess }: ComplianceQuestionEditDialogProps) { const [open, setOpen] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false); const router = useRouter(); const [options, setOptions] = React.useState>([]); const [newOptionValue, setNewOptionValue] = React.useState(""); const [newOptionText, setNewOptionText] = React.useState(""); const [newOptionOther, setNewOptionOther] = React.useState(false); const [parentOptions, setParentOptions] = React.useState>([]); const [selectableParents, setSelectableParents] = React.useState>([]); const [parentQuestionId, setParentQuestionId] = React.useState(question.parentQuestionId || null); const [showOptionForm, setShowOptionForm] = React.useState(false); const [showOptionsDeleteDialog, setShowOptionsDeleteDialog] = React.useState(false); const [pendingQuestionTypeChange, setPendingQuestionTypeChange] = React.useState(null); const previousQuestionIdRef = React.useRef(null); const form = useForm({ resolver: zodResolver(questionSchema), defaultValues: { questionNumber: question.questionNumber, questionText: question.questionText, questionType: question.questionType, isRequired: question.isRequired, isRedFlag: question.isRedFlag || false, hasDetailText: question.hasDetailText, hasFileUpload: question.hasFileUpload, isConditional: !!question.parentQuestionId, parentQuestionId: question.parentQuestionId || undefined, conditionalValue: question.conditionalValue || "", }, }); const isRedFlag = form.watch("isRedFlag"); const questionTypeValue = form.watch("questionType"); const isSelectionType = React.useMemo(() => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return [QUESTION_TYPES.RADIO, QUESTION_TYPES.CHECKBOX, QUESTION_TYPES.DROPDOWN].includes((questionTypeValue || "").toUpperCase() as any); }, [questionTypeValue]); const loadOptions = React.useCallback(async () => { if (!isSelectionType) return; try { const data = await getComplianceQuestionOptions(question.id); setOptions(data); } catch (e) { console.error("loadOptions error", e); } }, [isSelectionType, question.id]); // 레드플래그 선택 시 질문 유형을 RADIO로 자동 설정 React.useEffect(() => { if (isRedFlag && open) { form.setValue("questionType", QUESTION_TYPES.RADIO); } }, [form, isRedFlag, open]); // 레드플래그 선택 시 옵션을 YES/NO로 고정 React.useEffect(() => { if (isRedFlag && open && isSelectionType) { // 레드플래그가 켜지면 옵션을 YES/NO로 설정 const redFlagOptionsList = RED_FLAG_OPTIONS.map((opt) => ({ id: 0, // 임시 ID optionValue: opt.optionValue, optionText: opt.optionText, allowsOtherInput: opt.allowsOtherInput, displayOrder: opt.displayOrder, })); setOptions(redFlagOptionsList); } else if (!isRedFlag && open && isSelectionType) { // 레드플래그가 꺼지면 기존 옵션을 다시 로드 void loadOptions(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isRedFlag, open, isSelectionType]); // 시트가 열릴 때마다 또는 question.id가 변경될 때만 폼을 원본 데이터로 리셋 React.useEffect(() => { // question.id가 변경되거나 시트가 새로 열릴 때만 reset const shouldReset = open && (previousQuestionIdRef.current !== question.id || previousQuestionIdRef.current === null); if (shouldReset) { previousQuestionIdRef.current = question.id; const isRedFlagValue = question.isRedFlag || false; // 레드플래그가 활성화되어 있으면 질문 유형을 RADIO로 설정 const questionTypeValue = isRedFlagValue ? QUESTION_TYPES.RADIO : (question.questionType as string); form.reset({ questionNumber: question.questionNumber, questionText: question.questionText, questionType: questionTypeValue, isRequired: question.isRequired, isRedFlag: isRedFlagValue, hasDetailText: question.hasDetailText, hasFileUpload: question.hasFileUpload, isConditional: !!question.parentQuestionId, parentQuestionId: question.parentQuestionId || undefined, conditionalValue: question.conditionalValue || "", }); setParentQuestionId(question.parentQuestionId || null); // 레드플래그가 활성화되어 있으면 YES/NO 옵션 설정, 아니면 기존 옵션 로드 if (isRedFlagValue) { const redFlagOptionsList = RED_FLAG_OPTIONS.map((opt) => ({ id: 0, // 임시 ID optionValue: opt.optionValue, optionText: opt.optionText, allowsOtherInput: opt.allowsOtherInput, displayOrder: opt.displayOrder, })); setOptions(redFlagOptionsList); } else { // loadOptions는 별도로 호출 if (isSelectionType) { void loadOptions(); } } } else if (!open) { // 시트가 닫히면 previousQuestionIdRef를 초기화 previousQuestionIdRef.current = null; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, question.id, form, isSelectionType]); // 선택 가능한 부모 질문들 로드 (조건부 질문용) React.useEffect(() => { const loadSelectableParents = async () => { if (!open) return; try { // 현재 질문과 같은 템플릿의 선택형 질문들만 가져오기 const data = await getSelectableParentQuestions(question.templateId, question.id); setSelectableParents(data); } catch (e) { console.error("loadSelectableParents error", e); setSelectableParents([]); } }; loadSelectableParents(); }, [open, question.templateId, question.id]); // 부모 질문의 옵션 로드 (조건부 질문용) React.useEffect(() => { const loadParentOptions = async () => { if (!open) return; if (!parentQuestionId) { setParentOptions([]); return; } try { const data = await getComplianceQuestionOptions(parentQuestionId); // eslint-disable-next-line @typescript-eslint/no-explicit-any setParentOptions(data.map((o: any) => ({ id: o.id, optionValue: o.optionValue, optionText: o.optionText }))); } catch (e) { console.error("loadParentOptions error", e); setParentOptions([]); } }; loadParentOptions(); }, [open, parentQuestionId]); const onSubmit = async (data: QuestionFormData) => { try { setIsLoading(true); // 조건부 질문 관련 데이터 처리 const updateData = { ...data, parentQuestionId: data.isConditional ? (parentQuestionId ?? undefined) : undefined, conditionalValue: data.isConditional ? (data.conditionalValue ?? undefined) : undefined, }; // isConditional은 제거 (스키마에 없음) // eslint-disable-next-line @typescript-eslint/no-explicit-any delete (updateData as any).isConditional; console.log("Final updateData:", updateData); await updateComplianceQuestion(question.id, updateData); // 레드플래그가 켜져 있고 질문 유형이 RADIO이면 옵션을 YES/NO로 교체 if (data.isRedFlag && data.questionType === QUESTION_TYPES.RADIO) { // 기존 옵션 가져오기 const existingOptions = await getComplianceQuestionOptions(question.id); // 기존 옵션 삭제 for (const option of existingOptions) { await deleteComplianceQuestionOption(option.id); } // YES/NO 옵션 추가 for (const redFlagOption of RED_FLAG_OPTIONS) { await createComplianceQuestionOption({ questionId: question.id, optionValue: redFlagOption.optionValue, optionText: redFlagOption.optionText, allowsOtherInput: redFlagOption.allowsOtherInput, displayOrder: redFlagOption.displayOrder, }); } } toast.success("질문이 성공적으로 수정되었습니다."); setOpen(false); // 페이지 새로고침 router.refresh(); if (onSuccess) { onSuccess(); } } catch (error) { console.error("Error updating question:", error); // 중복 질문번호 오류 처리 if (error instanceof Error && error.message === "DUPLICATE_QUESTION_NUMBER") { form.setError("questionNumber", { type: "manual", message: "이미 사용 중인 질문번호입니다." }); toast.error("이미 사용 중인 질문번호입니다."); } else { toast.error("질문 수정 중 오류가 발생했습니다."); } } finally { setIsLoading(false); } }; return ( 질문 수정 질문 내용을 수정합니다.
( 질문 번호 )} /> {/* 필수 질문과 조건부 질문 체크박스 */}
( { if (checked) { // 필수 질문을 선택하면 조건부 질문 해제 form.setValue("isConditional", false); } field.onChange(checked); }} />
필수 질문 응답자가 반드시 답변해야 하는 질문
)} /> ( { if (checked) { // 조건부 질문을 선택하면 필수 질문 해제 form.setValue("isRequired", false); } field.onChange(checked); }} />
조건부 질문 특정 조건에 따라 표시되는 질문
)} />
{/* 레드플래그 체크박스 */} ( { // 이벤트 전파 방지 field.onChange(checked); }} onClick={(e) => { // 클릭 이벤트 전파 방지 e.stopPropagation(); }} />
레드플래그 질문 질문 유형 - RADIO || 옵션 - YES/NO
)} /> ( 질문 내용