diff --git a/frontend/app/(main)/admin/commonCode/page.tsx b/frontend/app/(main)/admin/commonCode/page.tsx index b1941bb2..be946e05 100644 --- a/frontend/app/(main)/admin/commonCode/page.tsx +++ b/frontend/app/(main)/admin/commonCode/page.tsx @@ -1,14 +1,14 @@ "use client"; -import { useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { CodeCategoryPanel } from "@/components/admin/CodeCategoryPanel"; import { CodeDetailPanel } from "@/components/admin/CodeDetailPanel"; +import { useSelectedCategory } from "@/hooks/useSelectedCategory"; // import { useMultiLang } from "@/hooks/useMultiLang"; // 무한 루프 방지를 위해 임시 제거 export default function CommonCodeManagementPage() { // const { getText } = useMultiLang(); // 무한 루프 방지를 위해 임시 제거 - const [selectedCategoryCode, setSelectedCategoryCode] = useState(""); + const { selectedCategoryCode, selectCategory } = useSelectedCategory(); return (
@@ -30,10 +30,7 @@ export default function CommonCodeManagementPage() { 📂 코드 카테고리 - +
diff --git a/frontend/components/admin/CodeCategoryFormModal.tsx b/frontend/components/admin/CodeCategoryFormModal.tsx index 3c61be1a..465b84df 100644 --- a/frontend/components/admin/CodeCategoryFormModal.tsx +++ b/frontend/components/admin/CodeCategoryFormModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; @@ -13,13 +13,13 @@ import { LoadingSpinner } from "@/components/common/LoadingSpinner"; import { ValidationMessage } from "@/components/common/ValidationMessage"; import { useCategories, useCreateCategory, useUpdateCategory } from "@/hooks/queries/useCategories"; import { useCheckCategoryDuplicate } from "@/hooks/queries/useValidation"; +import { useFormValidation } from "@/hooks/useFormValidation"; import { createCategorySchema, updateCategorySchema, type CreateCategoryData, type UpdateCategoryData, } from "@/lib/schemas/commonCode"; -import type { CodeCategory } from "@/types/commonCode"; interface CodeCategoryFormModalProps { isOpen: boolean; @@ -36,87 +36,88 @@ export function CodeCategoryFormModal({ isOpen, onClose, editingCategoryCode }: const editingCategory = categories.find((c) => c.category_code === editingCategoryCode); // 검증 상태 관리 - const [validationStates, setValidationStates] = useState({ - categoryCode: { enabled: false, value: "" }, - categoryName: { enabled: false, value: "" }, - categoryNameEng: { enabled: false, value: "" }, - description: { enabled: false, value: "" }, // 설명 필드 추가 + const formValidation = useFormValidation({ + fields: ["categoryCode", "categoryName", "categoryNameEng", "description"], }); // 중복 검사 훅들 const categoryCodeCheck = useCheckCategoryDuplicate( "categoryCode", - validationStates.categoryCode.value, + formValidation.getFieldValue("categoryCode"), isEditing ? editingCategoryCode : undefined, - validationStates.categoryCode.enabled, + formValidation.isFieldValidated("categoryCode"), ); const categoryNameCheck = useCheckCategoryDuplicate( "categoryName", - validationStates.categoryName.value, + formValidation.getFieldValue("categoryName"), isEditing ? editingCategoryCode : undefined, - validationStates.categoryName.enabled, + formValidation.isFieldValidated("categoryName"), ); const categoryNameEngCheck = useCheckCategoryDuplicate( "categoryNameEng", - validationStates.categoryNameEng.value, + formValidation.getFieldValue("categoryNameEng"), isEditing ? editingCategoryCode : undefined, - validationStates.categoryNameEng.enabled, + formValidation.isFieldValidated("categoryNameEng"), ); // 중복 검사 결과 확인 (수정 시에는 카테고리 코드 검사 제외) const hasDuplicateErrors = - (!isEditing && categoryCodeCheck.data?.isDuplicate && validationStates.categoryCode.enabled) || - (categoryNameCheck.data?.isDuplicate && validationStates.categoryName.enabled) || - (categoryNameEngCheck.data?.isDuplicate && validationStates.categoryNameEng.enabled); + (!isEditing && categoryCodeCheck.data?.isDuplicate && formValidation.isFieldValidated("categoryCode")) || + (categoryNameCheck.data?.isDuplicate && formValidation.isFieldValidated("categoryName")) || + (categoryNameEngCheck.data?.isDuplicate && formValidation.isFieldValidated("categoryNameEng")); // 중복 검사 로딩 중인지 확인 (수정 시에는 카테고리 코드 검사 제외) const isDuplicateChecking = (!isEditing && categoryCodeCheck.isLoading) || categoryNameCheck.isLoading || categoryNameEngCheck.isLoading; // 필수 필드들이 모두 검증되었는지 확인 (생성 시에만 적용) - const requiredFieldsValidated = - isEditing || - (validationStates.categoryCode.enabled && - validationStates.categoryName.enabled && - validationStates.categoryNameEng.enabled && - validationStates.description.enabled); - // 폼 스키마 선택 (생성/수정에 따라) - const schema = isEditing ? updateCategorySchema : createCategorySchema; - - const form = useForm({ - resolver: zodResolver(schema), - mode: "onChange", // 실시간 검증 활성화 + // 생성과 수정을 위한 별도 폼 설정 + const createForm = useForm({ + resolver: zodResolver(createCategorySchema), + mode: "onChange", defaultValues: { categoryCode: "", categoryName: "", categoryNameEng: "", description: "", sortOrder: 1, - ...(isEditing && { isActive: true }), }, }); + const updateForm = useForm({ + resolver: zodResolver(updateCategorySchema), + mode: "onChange", + defaultValues: { + categoryName: "", + categoryNameEng: "", + description: "", + sortOrder: 1, + isActive: "Y", + }, + }); + + // 폼은 조건부로 직접 사용 + // 편집 모드일 때 기존 데이터 로드 useEffect(() => { if (isOpen) { if (isEditing && editingCategory) { // 수정 모드: 기존 데이터 로드 - form.reset({ - categoryCode: editingCategory.category_code, // 카테고리 코드도 표시 + updateForm.reset({ categoryName: editingCategory.category_name, categoryNameEng: editingCategory.category_name_eng || "", description: editingCategory.description || "", sortOrder: editingCategory.sort_order, - isActive: editingCategory.is_active, // 🔧 "Y"/"N" 문자열 그대로 사용 + isActive: editingCategory.is_active as "Y" | "N", // 타입 안전한 캐스팅 }); } else { // 새 카테고리 모드: 자동 순서 계산 const maxSortOrder = categories.length > 0 ? Math.max(...categories.map((c) => c.sort_order)) : 0; - form.reset({ + createForm.reset({ categoryCode: "", categoryName: "", categoryNameEng: "", @@ -125,27 +126,30 @@ export function CodeCategoryFormModal({ isOpen, onClose, editingCategoryCode }: }); } } - }, [isOpen, isEditing, editingCategory, categories, form]); + }, [isOpen, isEditing, editingCategory, categories, createForm, updateForm]); - const handleSubmit = form.handleSubmit(async (data) => { - try { - if (isEditing && editingCategoryCode) { - // 수정 - await updateCategoryMutation.mutateAsync({ - categoryCode: editingCategoryCode, - data: data as UpdateCategoryData, - }); - } else { - // 생성 - await createCategoryMutation.mutateAsync(data as CreateCategoryData); - } - - onClose(); - form.reset(); - } catch (error) { - console.error("카테고리 저장 실패:", error); - } - }); + const handleSubmit = isEditing + ? updateForm.handleSubmit(async (data) => { + try { + await updateCategoryMutation.mutateAsync({ + categoryCode: editingCategoryCode!, + data: data as UpdateCategoryData, + }); + onClose(); + updateForm.reset(); + } catch (error) { + console.error("카테고리 수정 실패:", error); + } + }) + : createForm.handleSubmit(async (data) => { + try { + await createCategoryMutation.mutateAsync(data as CreateCategoryData); + onClose(); + createForm.reset(); + } catch (error) { + console.error("카테고리 생성 실패:", error); + } + }); const isLoading = createCategoryMutation.isPending || updateCategoryMutation.isPending; @@ -158,29 +162,21 @@ export function CodeCategoryFormModal({ isOpen, onClose, editingCategoryCode }:
{/* 카테고리 코드 */} - { + {!isEditing && (
{ - const value = e.target.value.trim(); - if (value) { - setValidationStates((prev) => ({ - ...prev, - categoryCode: { enabled: true, value }, - })); - } - }} + className={createForm.formState.errors.categoryCode ? "border-red-500" : ""} + onBlur={formValidation.createBlurHandler("categoryCode")} /> - {form.formState.errors.categoryCode && ( -

{form.formState.errors.categoryCode.message}

+ {createForm.formState.errors.categoryCode && ( +

{createForm.formState.errors.categoryCode.message}

)} - {!isEditing && !form.formState.errors.categoryCode && ( + {!createForm.formState.errors.categoryCode && ( )}
- } + )} + + {/* 카테고리 코드 표시 (수정 시) */} + {isEditing && editingCategory && ( +
+ + +

카테고리 코드는 수정할 수 없습니다.

+
+ )} {/* 카테고리명 */}
{ - const value = e.target.value.trim(); - if (value) { - setValidationStates((prev) => ({ - ...prev, - categoryName: { enabled: true, value }, - })); - } - }} + className={ + isEditing + ? updateForm.formState.errors.categoryName + ? "border-red-500" + : "" + : createForm.formState.errors.categoryName + ? "border-red-500" + : "" + } + onBlur={formValidation.createBlurHandler("categoryName")} /> - {form.formState.errors.categoryName && ( -

{form.formState.errors.categoryName.message}

- )} - {!form.formState.errors.categoryName && ( + {isEditing + ? updateForm.formState.errors.categoryName && ( +

{updateForm.formState.errors.categoryName.message}

+ ) + : createForm.formState.errors.categoryName && ( +

{createForm.formState.errors.categoryName.message}

+ )} + {!(isEditing ? updateForm.formState.errors.categoryName : createForm.formState.errors.categoryName) && ( 카테고리 영문명 * { - const value = e.target.value.trim(); - if (value) { - setValidationStates((prev) => ({ - ...prev, - categoryNameEng: { enabled: true, value }, - })); - } - }} + className={ + isEditing + ? updateForm.formState.errors.categoryNameEng + ? "border-red-500" + : "" + : createForm.formState.errors.categoryNameEng + ? "border-red-500" + : "" + } + onBlur={formValidation.createBlurHandler("categoryNameEng")} /> - {form.formState.errors.categoryNameEng && ( -

{form.formState.errors.categoryNameEng.message}

- )} - {!form.formState.errors.categoryNameEng && ( + {isEditing + ? updateForm.formState.errors.categoryNameEng && ( +

{updateForm.formState.errors.categoryNameEng.message}

+ ) + : createForm.formState.errors.categoryNameEng && ( +

{createForm.formState.errors.categoryNameEng.message}

+ )} + {!(isEditing + ? updateForm.formState.errors.categoryNameEng + : createForm.formState.errors.categoryNameEng) && ( 설명 *