공통코드 계층구조 구현
This commit is contained in:
@@ -24,6 +24,7 @@ interface CodeFormModalProps {
|
||||
categoryCode: string;
|
||||
editingCode?: CodeInfo | null;
|
||||
codes: CodeInfo[];
|
||||
defaultParentCode?: string; // 하위 코드 추가 시 기본 부모 코드
|
||||
}
|
||||
|
||||
// 에러 메시지를 안전하게 문자열로 변환하는 헬퍼 함수
|
||||
@@ -33,28 +34,32 @@ const getErrorMessage = (error: FieldError | undefined): string => {
|
||||
return error.message || "";
|
||||
};
|
||||
|
||||
export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, codes }: CodeFormModalProps) {
|
||||
// 코드값 자동 생성 함수 (UUID 기반 짧은 코드)
|
||||
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,
|
||||
categoryCode,
|
||||
editingCode,
|
||||
codes,
|
||||
defaultParentCode,
|
||||
}: 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",
|
||||
@@ -63,22 +68,11 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
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 hasDuplicateErrors = codeNameCheck.data?.isDuplicate && validationStates.codeName.enabled;
|
||||
|
||||
// 중복 검사 로딩 중인지 확인
|
||||
const isDuplicateChecking = codeValueCheck.isLoading || codeNameCheck.isLoading || codeNameEngCheck.isLoading;
|
||||
const isDuplicateChecking = codeNameCheck.isLoading;
|
||||
|
||||
// 폼 스키마 선택 (생성/수정에 따라)
|
||||
const schema = isEditing ? updateCodeSchema : createCodeSchema;
|
||||
@@ -92,6 +86,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
codeNameEng: "",
|
||||
description: "",
|
||||
sortOrder: 1,
|
||||
parentCodeValue: "" as string | undefined,
|
||||
...(isEditing && { isActive: "Y" as const }),
|
||||
},
|
||||
});
|
||||
@@ -101,30 +96,40 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
if (isOpen) {
|
||||
if (isEditing && editingCode) {
|
||||
// 수정 모드: 기존 데이터 로드 (codeValue는 표시용으로만 설정)
|
||||
const parentValue = editingCode.parentCodeValue || editingCode.parent_code_value || "";
|
||||
|
||||
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", // 타입 캐스팅
|
||||
isActive: (editingCode.isActive || editingCode.is_active) as "Y" | "N",
|
||||
parentCodeValue: parentValue,
|
||||
});
|
||||
|
||||
// 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;
|
||||
const maxSortOrder = codes.length > 0 ? Math.max(...codes.map((c) => c.sortOrder || c.sort_order || 0)) : 0;
|
||||
|
||||
// 기본 부모 코드가 있으면 설정 (하위 코드 추가 시)
|
||||
const parentValue = defaultParentCode || "";
|
||||
|
||||
// 코드값 자동 생성
|
||||
const autoCodeValue = generateCodeValue();
|
||||
|
||||
form.reset({
|
||||
codeValue: "",
|
||||
codeValue: autoCodeValue,
|
||||
codeName: "",
|
||||
codeNameEng: "",
|
||||
description: "",
|
||||
sortOrder: maxSortOrder + 1,
|
||||
parentCodeValue: parentValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isOpen, isEditing, editingCode, codes]);
|
||||
}, [isOpen, isEditing, editingCode, codes, defaultParentCode]);
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
try {
|
||||
@@ -132,7 +137,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
// 수정
|
||||
await updateCodeMutation.mutateAsync({
|
||||
categoryCode,
|
||||
codeValue: editingCode.codeValue || editingCode.code_value,
|
||||
codeValue: editingCode.codeValue || editingCode.code_value || "",
|
||||
data: data as UpdateCodeData,
|
||||
});
|
||||
} else {
|
||||
@@ -156,50 +161,38 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">{isEditing ? "코드 수정" : "새 코드"}</DialogTitle>
|
||||
<DialogTitle className="text-base sm:text-lg">
|
||||
{isEditing ? "코드 수정" : defaultParentCode ? "하위 코드 추가" : "새 코드"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-3 sm:space-y-4">
|
||||
{/* 코드값 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="codeValue" className="text-xs sm:text-sm">코드값 *</Label>
|
||||
<Input
|
||||
id="codeValue"
|
||||
{...form.register("codeValue")}
|
||||
disabled={isLoading || isEditing}
|
||||
placeholder="코드값을 입력하세요"
|
||||
className={(form.formState.errors as any)?.codeValue ? "h-8 text-xs sm:h-10 sm:text-sm border-destructive" : "h-8 text-xs sm:h-10 sm:text-sm"}
|
||||
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-[10px] sm:text-xs text-destructive">{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>
|
||||
{/* 코드값 (자동 생성, 수정 시에만 표시) */}
|
||||
{isEditing && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs sm:text-sm">코드값</Label>
|
||||
<div className="bg-muted h-8 rounded-md border px-3 py-1.5 text-xs sm:h-10 sm:py-2 sm:text-sm">
|
||||
{form.watch("codeValue")}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-[10px] sm:text-xs">코드값은 변경할 수 없습니다</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 코드명 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="codeName" className="text-xs sm:text-sm">코드명 *</Label>
|
||||
<Label htmlFor="codeName" className="text-xs sm:text-sm">
|
||||
코드명 *
|
||||
</Label>
|
||||
<Input
|
||||
id="codeName"
|
||||
{...form.register("codeName")}
|
||||
disabled={isLoading}
|
||||
placeholder="코드명을 입력하세요"
|
||||
className={form.formState.errors.codeName ? "h-8 text-xs sm:h-10 sm:text-sm border-destructive" : "h-8 text-xs sm:h-10 sm:text-sm"}
|
||||
className={
|
||||
form.formState.errors.codeName
|
||||
? "border-destructive h-8 text-xs sm:h-10 sm:text-sm"
|
||||
: "h-8 text-xs sm:h-10 sm:text-sm"
|
||||
}
|
||||
onBlur={(e) => {
|
||||
const value = e.target.value.trim();
|
||||
if (value) {
|
||||
@@ -211,7 +204,9 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
}}
|
||||
/>
|
||||
{form.formState.errors.codeName && (
|
||||
<p className="text-[10px] sm:text-xs text-destructive">{getErrorMessage(form.formState.errors.codeName)}</p>
|
||||
<p className="text-destructive text-[10px] sm:text-xs">
|
||||
{getErrorMessage(form.formState.errors.codeName)}
|
||||
</p>
|
||||
)}
|
||||
{!form.formState.errors.codeName && (
|
||||
<ValidationMessage
|
||||
@@ -222,66 +217,72 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 영문명 */}
|
||||
{/* 영문명 (선택) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="codeNameEng" className="text-xs sm:text-sm">코드 영문명 *</Label>
|
||||
<Label htmlFor="codeNameEng" className="text-xs sm:text-sm">
|
||||
코드 영문명
|
||||
</Label>
|
||||
<Input
|
||||
id="codeNameEng"
|
||||
{...form.register("codeNameEng")}
|
||||
disabled={isLoading}
|
||||
placeholder="코드 영문명을 입력하세요"
|
||||
className={form.formState.errors.codeNameEng ? "h-8 text-xs sm:h-10 sm:text-sm border-destructive" : "h-8 text-xs sm:h-10 sm:text-sm"}
|
||||
onBlur={(e) => {
|
||||
const value = e.target.value.trim();
|
||||
if (value) {
|
||||
setValidationStates((prev) => ({
|
||||
...prev,
|
||||
codeNameEng: { enabled: true, value },
|
||||
}));
|
||||
}
|
||||
}}
|
||||
placeholder="코드 영문명을 입력하세요 (선택사항)"
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
{form.formState.errors.codeNameEng && (
|
||||
<p className="text-[10px] sm:text-xs text-destructive">{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" className="text-xs sm:text-sm">설명 *</Label>
|
||||
<Label htmlFor="description" className="text-xs sm:text-sm">
|
||||
설명
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
{...form.register("description")}
|
||||
disabled={isLoading}
|
||||
placeholder="설명을 입력하세요"
|
||||
rows={3}
|
||||
className={form.formState.errors.description ? "text-xs sm:text-sm border-destructive" : "text-xs sm:text-sm"}
|
||||
placeholder="설명을 입력하세요 (선택사항)"
|
||||
rows={2}
|
||||
className="text-xs sm:text-sm"
|
||||
/>
|
||||
{form.formState.errors.description && (
|
||||
<p className="text-[10px] sm:text-xs text-destructive">{getErrorMessage(form.formState.errors.description)}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 부모 코드 표시 (하위 코드 추가 시에만 표시, 읽기 전용) */}
|
||||
{defaultParentCode && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs sm:text-sm">상위 코드</Label>
|
||||
<div className="bg-muted h-8 rounded-md border px-3 py-1.5 text-xs sm:h-10 sm:py-2 sm:text-sm">
|
||||
{(() => {
|
||||
const parentCode = codes.find((c) => (c.codeValue || c.code_value) === defaultParentCode);
|
||||
return parentCode
|
||||
? `${parentCode.codeName || parentCode.code_name} (${defaultParentCode})`
|
||||
: defaultParentCode;
|
||||
})()}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-[10px] sm:text-xs">이 코드는 위 코드의 하위로 추가됩니다</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 정렬 순서 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sortOrder" className="text-xs sm:text-sm">정렬 순서</Label>
|
||||
<Label htmlFor="sortOrder" className="text-xs sm:text-sm">
|
||||
정렬 순서
|
||||
</Label>
|
||||
<Input
|
||||
id="sortOrder"
|
||||
type="number"
|
||||
{...form.register("sortOrder", { valueAsNumber: true })}
|
||||
disabled={isLoading}
|
||||
min={1}
|
||||
className={form.formState.errors.sortOrder ? "h-8 text-xs sm:h-10 sm:text-sm border-destructive" : "h-8 text-xs sm:h-10 sm:text-sm"}
|
||||
className={
|
||||
form.formState.errors.sortOrder
|
||||
? "border-destructive h-8 text-xs sm:h-10 sm:text-sm"
|
||||
: "h-8 text-xs sm:h-10 sm:text-sm"
|
||||
}
|
||||
/>
|
||||
{form.formState.errors.sortOrder && (
|
||||
<p className="text-[10px] sm:text-xs text-destructive">{getErrorMessage(form.formState.errors.sortOrder)}</p>
|
||||
<p className="text-destructive text-[10px] sm:text-xs">
|
||||
{getErrorMessage(form.formState.errors.sortOrder)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -295,16 +296,18 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
disabled={isLoading}
|
||||
aria-label="활성 상태"
|
||||
/>
|
||||
<Label htmlFor="isActive" className="text-xs sm:text-sm">{form.watch("isActive") === "Y" ? "활성" : "비활성"}</Label>
|
||||
<Label htmlFor="isActive" className="text-xs sm:text-sm">
|
||||
{form.watch("isActive") === "Y" ? "활성" : "비활성"}
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 버튼 */}
|
||||
<div className="flex gap-2 pt-4 sm:justify-end sm:gap-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user