From cfa72fe19b541147d81f7c53c6ab6449de089694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Tue, 20 Jan 2026 20:41:45 +0900 Subject: [PATCH] =?UTF-8?q?refactor(WEB):=20=ED=8F=BC=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=ED=86=B5=ED=95=A9=20=EB=B0=8F=20=EB=AF=B8=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ResponsiveFormTemplate → IntegratedDetailTemplate 마이그레이션 - ClientRegistration, OrderRegistration, QuoteRegistration 완료 - QuoteRegistrationV2 미사용 import 정리 - 미사용 컴포넌트 삭제 - ListPageTemplate.tsx - ResponsiveFormTemplate.tsx - common/DataTable 폴더 전체 (SearchFilter 누락 export 에러 해결) - Config 파일 추가 - clientConfig.ts, orderConfig.ts - quoteConfig.ts에 edit 모드 config 추가 Co-Authored-By: Claude --- src/components/clients/ClientRegistration.tsx | 655 +++++---- src/components/clients/clientConfig.ts | 38 + src/components/common/DataTable/DataTable.tsx | 548 ------- .../common/DataTable/Pagination.tsx | 143 -- .../common/DataTable/SearchFilter.tsx | 58 - src/components/common/DataTable/StatCards.tsx | 46 - src/components/common/DataTable/TabFilter.tsx | 31 - src/components/common/DataTable/index.ts | 16 - src/components/common/DataTable/types.ts | 248 ---- src/components/orders/OrderRegistration.tsx | 108 +- src/components/orders/orderConfig.ts | 38 + src/components/quotes/QuoteRegistration.tsx | 1257 +++++++++-------- src/components/quotes/QuoteRegistrationV2.tsx | 7 +- src/components/quotes/quoteConfig.ts | 51 + src/components/templates/ListPageTemplate.tsx | 144 -- .../templates/ResponsiveFormTemplate.tsx | 95 -- src/components/templates/index.ts | 1 - 17 files changed, 1172 insertions(+), 2312 deletions(-) create mode 100644 src/components/clients/clientConfig.ts delete mode 100644 src/components/common/DataTable/DataTable.tsx delete mode 100644 src/components/common/DataTable/Pagination.tsx delete mode 100644 src/components/common/DataTable/SearchFilter.tsx delete mode 100644 src/components/common/DataTable/StatCards.tsx delete mode 100644 src/components/common/DataTable/TabFilter.tsx delete mode 100644 src/components/common/DataTable/index.ts delete mode 100644 src/components/common/DataTable/types.ts create mode 100644 src/components/orders/orderConfig.ts delete mode 100644 src/components/templates/ListPageTemplate.tsx delete mode 100644 src/components/templates/ResponsiveFormTemplate.tsx diff --git a/src/components/clients/ClientRegistration.tsx b/src/components/clients/ClientRegistration.tsx index 3ad4d479..9ca5bfab 100644 --- a/src/components/clients/ClientRegistration.tsx +++ b/src/components/clients/ClientRegistration.tsx @@ -1,47 +1,41 @@ /** * 거래처 등록/수정 컴포넌트 * - * ResponsiveFormTemplate 적용 + * IntegratedDetailTemplate 마이그레이션 (2026-01-20) * - 데스크톱/태블릿/모바일 통합 폼 레이아웃 * - 섹션 기반 정보 입력 * - 유효성 검사 및 에러 표시 */ -"use client"; +'use client'; -import { useState, useEffect } from "react"; -import { Input } from "../ui/input"; -import { Textarea } from "../ui/textarea"; -import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; -import { Label } from "../ui/label"; -import { - Building2, - UserCircle, - Phone, - FileText, -} from "lucide-react"; -import { toast } from "sonner"; -import { Alert, AlertDescription } from "../ui/alert"; - -// 필드명 매핑 -const FIELD_NAME_MAP: Record = { - name: "거래처명", - businessNo: "사업자등록번호", - representative: "대표자명", - phone: "전화번호", - email: "이메일", -}; -import { - ResponsiveFormTemplate, - FormSection, - FormFieldGrid, -} from "../templates/ResponsiveFormTemplate"; -import { FormField } from "../molecules/FormField"; +import { useState, useEffect, useCallback } from 'react'; +import { Input } from '../ui/input'; +import { Textarea } from '../ui/textarea'; +import { RadioGroup, RadioGroupItem } from '../ui/radio-group'; +import { Label } from '../ui/label'; +import { Building2, UserCircle, Phone, FileText } from 'lucide-react'; +import { toast } from 'sonner'; +import { Alert, AlertDescription } from '../ui/alert'; +import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; +import { clientCreateConfig, clientEditConfig } from './clientConfig'; +import { FormSection } from '../organisms/FormSection'; +import { FormFieldGrid } from '../organisms/FormFieldGrid'; +import { FormField } from '../molecules/FormField'; import { ClientFormData, INITIAL_CLIENT_FORM, ClientType, -} from "../../hooks/useClientList"; +} from '../../hooks/useClientList'; + +// 필드명 매핑 +const FIELD_NAME_MAP: Record = { + name: '거래처명', + businessNo: '사업자등록번호', + representative: '대표자명', + phone: '전화번호', + email: '이메일', +}; interface ClientRegistrationProps { onBack: () => void; @@ -52,9 +46,9 @@ interface ClientRegistrationProps { // 4자리 영문+숫자 조합 코드 생성 (중복 방지를 위해 타임스탬프 기반) const generateClientCode = (): string => { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const timestamp = Date.now().toString(36).toUpperCase().slice(-2); // 타임스탬프 2자리 - let random = ""; + let random = ''; for (let i = 0; i < 2; i++) { random += chars.charAt(Math.floor(Math.random() * chars.length)); } @@ -80,6 +74,8 @@ export function ClientRegistration({ const [errors, setErrors] = useState>({}); const [isSaving, setIsSaving] = useState(false); + const isEditMode = !!editingClient; + // editingClient가 변경되면 formData 업데이트 useEffect(() => { if (editingClient) { @@ -92,40 +88,34 @@ export function ClientRegistration({ const newErrors: Record = {}; if (!formData.name || formData.name.length < 2) { - newErrors.name = "거래처명은 2자 이상 입력해주세요"; + newErrors.name = '거래처명은 2자 이상 입력해주세요'; } // 하이픈 제거 후 10자리 숫자 검증 (123-45-67890 또는 1234567890 허용) - const businessNoDigits = formData.businessNo.replace(/-/g, "").trim(); - console.log("[ClientRegistration] businessNo 검증:", { - 원본: formData.businessNo, - 하이픈제거: businessNoDigits, - 길이: businessNoDigits.length, - 숫자만: /^\d{10}$/.test(businessNoDigits), - }); + const businessNoDigits = formData.businessNo.replace(/-/g, '').trim(); if (!formData.businessNo || !/^\d{10}$/.test(businessNoDigits)) { - newErrors.businessNo = "사업자등록번호는 10자리 숫자여야 합니다"; + newErrors.businessNo = '사업자등록번호는 10자리 숫자여야 합니다'; } if (!formData.representative || formData.representative.length < 2) { - newErrors.representative = "대표자명은 2자 이상 입력해주세요"; + newErrors.representative = '대표자명은 2자 이상 입력해주세요'; } // 전화번호 형식 검사 (선택적) const phonePattern = /^[0-9-]+$/; if (formData.phone && !phonePattern.test(formData.phone)) { - newErrors.phone = "올바른 전화번호 형식이 아닙니다"; + newErrors.phone = '올바른 전화번호 형식이 아닙니다'; } if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { - newErrors.email = "올바른 이메일 형식이 아닙니다"; + newErrors.email = '올바른 이메일 형식이 아닙니다'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; - const handleSubmit = async () => { + const handleSubmit = useCallback(async () => { if (!validateForm()) { // 페이지 상단으로 스크롤 window.scrollTo({ top: 0, behavior: 'smooth' }); @@ -138,21 +128,20 @@ export function ClientRegistration({ try { await onSave(formData); toast.success( - editingClient ? "거래처가 수정되었습니다." : "거래처가 등록되었습니다." + editingClient ? '거래처가 수정되었습니다.' : '거래처가 등록되었습니다.' ); onBack(); } catch (error) { - toast.error("저장 중 오류가 발생했습니다."); + toast.error('저장 중 오류가 발생했습니다.'); } finally { setIsSaving(false); } - }; + }, [formData, editingClient, onSave, onBack]); const handleFieldChange = ( field: keyof ClientFormData, value: string | boolean ) => { - console.log("[ClientRegistration] handleFieldChange:", field, value); setFormData({ ...formData, [field]: value }); if (errors[field]) { setErrors((prev) => { @@ -163,304 +152,314 @@ export function ClientRegistration({ } }; - return ( - - {/* Validation 에러 표시 */} - {Object.keys(errors).length > 0 && ( - - -
- ⚠️ -
- - 입력 내용을 확인해주세요 ({Object.keys(errors).length}개 오류) - -
    - {Object.entries(errors).map(([field, message]) => { - const fieldName = FIELD_NAME_MAP[field] || field; - return ( -
  • - - - {fieldName}: {message} - -
  • - ); - })} -
+ // Config 선택 + const config = isEditMode ? clientEditConfig : clientCreateConfig; + + // 폼 콘텐츠 렌더링 + const renderFormContent = useCallback( + () => ( +
+ {/* Validation 에러 표시 */} + {Object.keys(errors).length > 0 && ( + + +
+ ⚠️ +
+ + 입력 내용을 확인해주세요 ({Object.keys(errors).length}개 오류) + +
    + {Object.entries(errors).map(([field, message]) => { + const fieldName = FIELD_NAME_MAP[field] || field; + return ( +
  • + + + {fieldName}: {message} + +
  • + ); + })} +
+
-
- - - )} + + + )} - {/* 1. 기본 정보 */} - - - + {/* 1. 기본 정보 */} + + + + handleFieldChange('businessNo', e.target.value)} + /> + + + + + + + + + + handleFieldChange('name', e.target.value)} + /> + + + + + handleFieldChange('representative', e.target.value) + } + /> + + + + + + handleFieldChange('clientType', value as ClientType) + } + className="flex gap-4" + > +
+ + +
+
+ + +
+
+ + +
+
+
+ + + + + handleFieldChange('businessType', e.target.value) + } + /> + + + + + handleFieldChange('businessItem', e.target.value) + } + /> + + +
+ + {/* 2. 연락처 정보 */} + + handleFieldChange("businessNo", e.target.value)} + id="address" + placeholder="주소 입력" + value={formData.address} + onChange={(e) => handleFieldChange('address', e.target.value)} /> - - - -
+ + + handleFieldChange('phone', e.target.value)} + /> + - - - handleFieldChange("name", e.target.value)} - /> - + + handleFieldChange('mobile', e.target.value)} + /> + + + + handleFieldChange('fax', e.target.value)} + /> + + handleFieldChange('email', e.target.value)} + /> + +
+ + {/* 3. 담당자 정보 */} + + + + handleFieldChange('managerName', e.target.value)} + /> + + + + handleFieldChange('managerTel', e.target.value)} + /> + + + + + - handleFieldChange("representative", e.target.value) + handleFieldChange('systemManager', e.target.value) } /> - + - - - handleFieldChange("clientType", value as ClientType) - } - className="flex gap-4" - > -
- - -
-
- - -
-
- - -
-
-
+ {/* 4. 기타 */} + + +