ERP-node/frontend/components/admin/CodeFormModal.tsx

327 lines
12 KiB
TypeScript

"use client";
import { useEffect, 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 { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { ValidationMessage } from "@/components/common/ValidationMessage";
import { useCreateCode, useUpdateCode } from "@/hooks/queries/useCodes";
import { useCheckCodeDuplicate } from "@/hooks/queries/useValidation";
import { createCodeSchema, updateCodeSchema, type CreateCodeData, type UpdateCodeData } from "@/lib/schemas/commonCode";
import type { CodeInfo } from "@/types/commonCode";
import type { FieldError } from "react-hook-form";
interface CodeFormModalProps {
isOpen: boolean;
onClose: () => void;
categoryCode: string;
editingCode?: CodeInfo | null;
codes: CodeInfo[];
}
// 에러 메시지를 안전하게 문자열로 변환하는 헬퍼 함수
const getErrorMessage = (error: FieldError | undefined): string => {
if (!error) return "";
if (typeof error === "string") return error;
return error.message || "";
};
export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, codes }: CodeFormModalProps) {
const createCodeMutation = useCreateCode();
const updateCodeMutation = useUpdateCode();
const isEditing = !!editingCode;
// 검증 상태 관리
const [validationStates, setValidationStates] = useState({
codeValue: { enabled: false, value: "" },
codeName: { enabled: false, value: "" },
codeNameEng: { enabled: false, value: "" },
});
// 중복 검사 훅들
const codeValueCheck = useCheckCodeDuplicate(
categoryCode,
"codeValue",
validationStates.codeValue.value,
isEditing ? editingCode?.codeValue || editingCode?.code_value : undefined,
validationStates.codeValue.enabled,
);
const codeNameCheck = useCheckCodeDuplicate(
categoryCode,
"codeName",
validationStates.codeName.value,
isEditing ? editingCode?.codeValue || editingCode?.code_value : undefined,
validationStates.codeName.enabled,
);
const codeNameEngCheck = useCheckCodeDuplicate(
categoryCode,
"codeNameEng",
validationStates.codeNameEng.value,
isEditing ? editingCode?.codeValue || editingCode?.code_value : undefined,
validationStates.codeNameEng.enabled,
);
// 중복 검사 결과 확인
const hasDuplicateErrors =
(codeValueCheck.data?.isDuplicate && validationStates.codeValue.enabled) ||
(codeNameCheck.data?.isDuplicate && validationStates.codeName.enabled) ||
(codeNameEngCheck.data?.isDuplicate && validationStates.codeNameEng.enabled);
// 중복 검사 로딩 중인지 확인
const isDuplicateChecking = codeValueCheck.isLoading || codeNameCheck.isLoading || codeNameEngCheck.isLoading;
// 폼 스키마 선택 (생성/수정에 따라)
const schema = isEditing ? updateCodeSchema : createCodeSchema;
const form = useForm({
resolver: zodResolver(schema),
mode: "onChange", // 실시간 검증 활성화
defaultValues: {
codeValue: "",
codeName: "",
codeNameEng: "",
description: "",
sortOrder: 1,
...(isEditing && { isActive: "Y" as const }),
},
});
// 편집 모드일 때 기존 데이터 로드
useEffect(() => {
if (isOpen) {
if (isEditing && editingCode) {
// 수정 모드: 기존 데이터 로드 (codeValue는 표시용으로만 설정)
form.reset({
codeName: editingCode.codeName || editingCode.code_name,
codeNameEng: editingCode.codeNameEng || editingCode.code_name_eng || "",
description: editingCode.description || "",
sortOrder: editingCode.sortOrder || editingCode.sort_order,
isActive: (editingCode.isActive || editingCode.is_active) as "Y" | "N", // 타입 캐스팅
});
// codeValue는 별도로 설정 (표시용)
form.setValue("codeValue" as any, editingCode.codeValue || editingCode.code_value);
} else {
// 새 코드 모드: 자동 순서 계산
const maxSortOrder = codes.length > 0 ? Math.max(...codes.map((c) => c.sortOrder || c.sort_order)) : 0;
form.reset({
codeValue: "",
codeName: "",
codeNameEng: "",
description: "",
sortOrder: maxSortOrder + 1,
});
}
}
}, [isOpen, isEditing, editingCode, codes]);
const handleSubmit = form.handleSubmit(async (data) => {
try {
if (isEditing && editingCode) {
// 수정
await updateCodeMutation.mutateAsync({
categoryCode,
codeValue: editingCode.codeValue || editingCode.code_value,
data: data as UpdateCodeData,
});
} else {
// 생성
await createCodeMutation.mutateAsync({
categoryCode,
data: data as CreateCodeData,
});
}
onClose();
form.reset();
} catch (error) {
console.error("코드 저장 실패:", error);
}
});
const isLoading = createCodeMutation.isPending || updateCodeMutation.isPending;
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{isEditing ? "코드 수정" : "새 코드"}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* 코드값 */}
<div className="space-y-2">
<Label htmlFor="codeValue"> *</Label>
<Input
id="codeValue"
{...form.register("codeValue")}
disabled={isLoading || isEditing} // 수정 시에는 비활성화
placeholder="코드값을 입력하세요"
className={(form.formState.errors as any)?.codeValue ? "border-red-500" : ""}
onBlur={(e) => {
const value = e.target.value.trim();
if (value && !isEditing) {
setValidationStates((prev) => ({
...prev,
codeValue: { enabled: true, value },
}));
}
}}
/>
{(form.formState.errors as any)?.codeValue && (
<p className="text-sm text-red-600">{getErrorMessage((form.formState.errors as any)?.codeValue)}</p>
)}
{!isEditing && !(form.formState.errors as any)?.codeValue && (
<ValidationMessage
message={codeValueCheck.data?.message}
isValid={!codeValueCheck.data?.isDuplicate}
isLoading={codeValueCheck.isLoading}
/>
)}
</div>
{/* 코드명 */}
<div className="space-y-2">
<Label htmlFor="codeName"> *</Label>
<Input
id="codeName"
{...form.register("codeName")}
disabled={isLoading}
placeholder="코드명을 입력하세요"
className={form.formState.errors.codeName ? "border-red-500" : ""}
onBlur={(e) => {
const value = e.target.value.trim();
if (value) {
setValidationStates((prev) => ({
...prev,
codeName: { enabled: true, value },
}));
}
}}
/>
{form.formState.errors.codeName && (
<p className="text-sm text-red-600">{getErrorMessage(form.formState.errors.codeName)}</p>
)}
{!form.formState.errors.codeName && (
<ValidationMessage
message={codeNameCheck.data?.message}
isValid={!codeNameCheck.data?.isDuplicate}
isLoading={codeNameCheck.isLoading}
/>
)}
</div>
{/* 영문명 */}
<div className="space-y-2">
<Label htmlFor="codeNameEng"> *</Label>
<Input
id="codeNameEng"
{...form.register("codeNameEng")}
disabled={isLoading}
placeholder="코드 영문명을 입력하세요"
className={form.formState.errors.codeNameEng ? "border-red-500" : ""}
onBlur={(e) => {
const value = e.target.value.trim();
if (value) {
setValidationStates((prev) => ({
...prev,
codeNameEng: { enabled: true, value },
}));
}
}}
/>
{form.formState.errors.codeNameEng && (
<p className="text-sm text-red-600">{getErrorMessage(form.formState.errors.codeNameEng)}</p>
)}
{!form.formState.errors.codeNameEng && (
<ValidationMessage
message={codeNameEngCheck.data?.message}
isValid={!codeNameEngCheck.data?.isDuplicate}
isLoading={codeNameEngCheck.isLoading}
/>
)}
</div>
{/* 설명 */}
<div className="space-y-2">
<Label htmlFor="description"> *</Label>
<Textarea
id="description"
{...form.register("description")}
disabled={isLoading}
placeholder="설명을 입력하세요"
rows={3}
className={form.formState.errors.description ? "border-red-500" : ""}
/>
{form.formState.errors.description && (
<p className="text-sm text-red-600">{getErrorMessage(form.formState.errors.description)}</p>
)}
</div>
{/* 정렬 순서 */}
<div className="space-y-2">
<Label htmlFor="sortOrder"> </Label>
<Input
id="sortOrder"
type="number"
{...form.register("sortOrder", { valueAsNumber: true })}
disabled={isLoading}
min={1}
className={form.formState.errors.sortOrder ? "border-red-500" : ""}
/>
{form.formState.errors.sortOrder && (
<p className="text-sm text-red-600">{getErrorMessage(form.formState.errors.sortOrder)}</p>
)}
</div>
{/* 활성 상태 (수정 시에만) */}
{isEditing && (
<div className="flex items-center space-x-2">
<Switch
id="isActive"
checked={form.watch("isActive") === "Y"}
onCheckedChange={(checked) => form.setValue("isActive", checked ? "Y" : "N")}
disabled={isLoading}
/>
<Label htmlFor="isActive">{form.watch("isActive") === "Y" ? "활성" : "비활성"}</Label>
</div>
)}
{/* 버튼 */}
<div className="flex justify-end space-x-2 pt-4">
<Button type="button" variant="outline" onClick={onClose} disabled={isLoading}>
</Button>
<Button
type="submit"
disabled={isLoading || !form.formState.isValid || hasDuplicateErrors || isDuplicateChecking}
>
{isLoading ? (
<>
<LoadingSpinner size="sm" className="mr-2" />
{isEditing ? "수정 중..." : "저장 중..."}
</>
) : isEditing ? (
"코드 수정"
) : (
"코드 저장"
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}