"use client"; import { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { LoadingSpinner } from "@/components/common/LoadingSpinner"; import { ValidationMessage } from "@/components/common/ValidationMessage"; import { useCreateCodeDetail, useUpdateCodeDetail } from "@/hooks/queries/useCodeDetail"; import { useCheckCodeDetailDuplicate } from "@/hooks/queries/useValidation"; import { createCodeDetailSchema, updateCodeDetailSchema, type CreateCodeDetailData, type UpdateCodeDetailData, } from "@/lib/schemas/commonCode"; import type { CodeDetail } from "@/types/commonCode"; interface CodeFormModalProps { isOpen: boolean; onClose: () => void; codeInfo: string; editingRow?: CodeDetail | null; allRows: CodeDetail[]; defaultParentDetailId?: number | null; } const PARENT_NONE = "__NONE__"; /** code_value 자동 생성 (timestamp+random) */ const generateCodeValue = (): string => { const timestamp = Date.now().toString(36).toUpperCase(); const random = Math.random().toString(36).substring(2, 6).toUpperCase(); return `${timestamp}${random}`; }; export function CodeFormModal({ isOpen, onClose, codeInfo, editingRow, allRows, defaultParentDetailId, }: CodeFormModalProps) { const createMutation = useCreateCodeDetail(); const updateMutation = useUpdateCodeDetail(); const isEditing = !!editingRow; const schema = isEditing ? updateCodeDetailSchema : createCodeDetailSchema; const form = useForm({ resolver: zodResolver(schema), mode: "onChange", defaultValues: { code_info: codeInfo, parent_detail_id: null as number | null, code_value: "", code_name: "", code_name_eng: "", description: "", sort_order: 10, ...(isEditing && { is_active: "Y" as const }), }, }); // 부모 후보: 자기 자신 + 자기 자손은 제외 const parentOptions = useMemo(() => { if (!isEditing) return allRows; if (!editingRow) return allRows; const selfId = String(editingRow.code_detail_id); const descendants = new Set([selfId]); let changed = true; while (changed) { changed = false; for (const row of allRows) { const parentKey = row.parent_detail_id == null ? "" : String(row.parent_detail_id); if ( descendants.has(parentKey) && !descendants.has(String(row.code_detail_id)) ) { descendants.add(String(row.code_detail_id)); changed = true; } } } return allRows.filter((r) => !descendants.has(String(r.code_detail_id))); }, [allRows, editingRow, isEditing]); // 중복 검사 (code_name) const [duplicateState, setDuplicateState] = useState<{ enabled: boolean; value: string }>({ enabled: false, value: "", }); const nameCheck = useCheckCodeDetailDuplicate( codeInfo, "code_name", duplicateState.value, isEditing ? editingRow?.code_detail_id : undefined, duplicateState.enabled, ); const hasDuplicateErrors = !!nameCheck.data?.isDuplicate && duplicateState.enabled; const isDuplicateChecking = nameCheck.isLoading; useEffect(() => { if (!isOpen) return; if (isEditing && editingRow) { form.reset({ code_info: editingRow.code_info || codeInfo, parent_detail_id: editingRow.parent_detail_id ?? null, code_value: editingRow.code_value || "", code_name: editingRow.code_name || "", code_name_eng: editingRow.code_name_eng || "", description: editingRow.description || "", sort_order: editingRow.sort_order ?? 10, is_active: ((editingRow.is_active as "Y" | "N") || "Y") as any, } as any); } else { // 형제 중 최대 sort_order + 10 const siblings = allRows.filter((r) => { const pid = r.parent_detail_id ?? null; return pid === (defaultParentDetailId ?? null); }); const maxSort = siblings.length > 0 ? Math.max(...siblings.map((r) => r.sort_order ?? 0)) : 0; form.reset({ code_info: codeInfo, parent_detail_id: defaultParentDetailId ?? null, code_value: generateCodeValue(), code_name: "", code_name_eng: "", description: "", sort_order: maxSort + 10, }); } setDuplicateState({ enabled: false, value: "" }); }, [isOpen, isEditing, editingRow, codeInfo, defaultParentDetailId]); // eslint-disable-line react-hooks/exhaustive-deps const handleSubmit = form.handleSubmit(async (data) => { try { if (isEditing && editingRow) { await updateMutation.mutateAsync({ codeDetailId: editingRow.code_detail_id, data: data as UpdateCodeDetailData, }); } else { await createMutation.mutateAsync(data as CreateCodeDetailData); } onClose(); form.reset(); } catch (error) { console.error("디테일 저장 실패:", error); } }); const isLoading = createMutation.isPending || updateMutation.isPending; const watchedParent = form.watch("parent_detail_id"); const parentSelectValue = watchedParent == null || watchedParent === undefined ? PARENT_NONE : String(watchedParent); return ( {isEditing ? "디테일 수정" : defaultParentDetailId != null ? "하위 코드 추가" : "새 코드"}
{/* code_value */}
{isEditing && (

코드값은 변경할 수 없습니다

)} {form.formState.errors.code_value && (

{form.formState.errors.code_value.message as string}

)}
{/* 부모 선택 */}

비워두면 그룹 직속(2 레벨)으로 등록됩니다

{/* 코드명 */}
{ const value = e.target.value.trim(); if (value) setDuplicateState({ enabled: true, value }); }} /> {form.formState.errors.code_name && (

{form.formState.errors.code_name.message as string}

)} {!form.formState.errors.code_name && ( )}
{/* 영문명 */}
{/* 설명 */}