feat(WEB): Phase 2-3 V2 마이그레이션 완료 및 ServerErrorPage 적용
Phase 2 완료 (4개): - 노무관리, 단가관리(건설), 입금, 출금 Phase 3 라우팅 구조 변경 완료 (22개): - 거래처(영업), 팝업관리, 공정관리, 게시판관리, 대손추심, Q&A - 현장관리, 실행내역, 견적관리, 견적(테스트) - 입찰관리, 이슈관리, 현장설명회, 견적서(건설) - 협력업체, 시공관리, 기성관리, 품목관리(건설) - 회계 도메인: 거래처, 매출, 세금계산서, 매입 신규 컴포넌트: - ErrorCard: 에러 페이지 UI 통일 - ServerErrorPage: V2 페이지 에러 처리 필수 - V2 Client 컴포넌트 및 Config 파일들 총 47개 상세 페이지 중 28개 완료, 19개 제외/불필요 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
229
src/components/clients/ClientDetailClientV2.tsx
Normal file
229
src/components/clients/ClientDetailClientV2.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 거래처(영업) 상세 클라이언트 컴포넌트 V2
|
||||
* IntegratedDetailTemplate 기반 마이그레이션
|
||||
*
|
||||
* 클라이언트 사이드 데이터 페칭 (useClientList 훅 활용)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import type { DetailMode } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
import type { Client, ClientFormData } from '@/hooks/useClientList';
|
||||
import { useClientList, transformClientToApiCreate, transformClientToApiUpdate } from '@/hooks/useClientList';
|
||||
import { clientDetailConfig } from './clientDetailConfig';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface ClientDetailClientV2Props {
|
||||
clientId?: string;
|
||||
initialMode?: DetailMode;
|
||||
}
|
||||
|
||||
// 8자리 영문+숫자 코드 생성
|
||||
function generateCode(): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function ClientDetailClientV2({ clientId, initialMode }: ClientDetailClientV2Props) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { fetchClient, createClient, updateClient, deleteClient } = useClientList();
|
||||
|
||||
// URL 쿼리에서 모드 결정
|
||||
const modeFromQuery = searchParams.get('mode') as DetailMode | null;
|
||||
const isNewMode = !clientId || clientId === 'new';
|
||||
|
||||
const [mode, setMode] = useState<DetailMode>(() => {
|
||||
if (isNewMode) return 'create';
|
||||
if (initialMode) return initialMode;
|
||||
if (modeFromQuery === 'edit') return 'edit';
|
||||
return 'view';
|
||||
});
|
||||
|
||||
const [clientData, setClientData] = useState<Client | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(!isNewMode);
|
||||
const [generatedCode, setGeneratedCode] = useState<string>('');
|
||||
|
||||
// 데이터 로드
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
if (isNewMode) {
|
||||
// 신규 등록 시 코드 생성
|
||||
const code = generateCode();
|
||||
setGeneratedCode(code);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await fetchClient(clientId!);
|
||||
if (data) {
|
||||
setClientData(data);
|
||||
} else {
|
||||
toast.error('거래처를 불러오는데 실패했습니다.');
|
||||
router.push(clientDetailConfig.basePath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('거래처 조회 실패:', error);
|
||||
toast.error('거래처를 불러오는데 실패했습니다.');
|
||||
router.push(clientDetailConfig.basePath);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [clientId, isNewMode, router, fetchClient]);
|
||||
|
||||
// URL 쿼리 변경 감지
|
||||
useEffect(() => {
|
||||
if (!isNewMode && modeFromQuery === 'edit') {
|
||||
setMode('edit');
|
||||
} else if (!isNewMode && !modeFromQuery) {
|
||||
setMode('view');
|
||||
}
|
||||
}, [modeFromQuery, isNewMode]);
|
||||
|
||||
// 모드 변경 핸들러
|
||||
const handleModeChange = useCallback(
|
||||
(newMode: DetailMode) => {
|
||||
setMode(newMode);
|
||||
if (newMode === 'edit' && clientId) {
|
||||
router.push(`${clientDetailConfig.basePath}/${clientId}?mode=edit`);
|
||||
} else if (newMode === 'view' && clientId) {
|
||||
router.push(`${clientDetailConfig.basePath}/${clientId}`);
|
||||
}
|
||||
},
|
||||
[router, clientId]
|
||||
);
|
||||
|
||||
// 저장 핸들러
|
||||
const handleSubmit = useCallback(
|
||||
async (formData: Record<string, unknown>) => {
|
||||
try {
|
||||
// formData를 ClientFormData로 변환
|
||||
const clientFormData: ClientFormData = {
|
||||
clientCode: (formData.clientCode as string) || generatedCode,
|
||||
name: formData.name as string,
|
||||
businessNo: formData.businessNo as string,
|
||||
representative: formData.representative as string,
|
||||
phone: formData.phone as string,
|
||||
address: formData.address as string,
|
||||
email: formData.email as string,
|
||||
businessType: formData.businessType as string,
|
||||
businessItem: formData.businessItem as string,
|
||||
isActive: formData.isActive === 'true',
|
||||
clientType: (formData.clientType as ClientFormData['clientType']) || '매입',
|
||||
mobile: formData.mobile as string,
|
||||
fax: formData.fax as string,
|
||||
managerName: formData.managerName as string,
|
||||
managerTel: formData.managerTel as string,
|
||||
systemManager: formData.systemManager as string,
|
||||
accountId: formData.accountId as string || '',
|
||||
accountPassword: formData.accountPassword as string || '',
|
||||
purchasePaymentDay: '말일',
|
||||
salesPaymentDay: '말일',
|
||||
taxAgreement: false,
|
||||
taxAmount: '',
|
||||
taxStartDate: '',
|
||||
taxEndDate: '',
|
||||
badDebt: false,
|
||||
badDebtAmount: '',
|
||||
badDebtReceiveDate: '',
|
||||
badDebtEndDate: '',
|
||||
badDebtProgress: '',
|
||||
memo: formData.memo as string || '',
|
||||
};
|
||||
|
||||
if (isNewMode) {
|
||||
const result = await createClient(clientFormData);
|
||||
if (result) {
|
||||
toast.success('거래처가 등록되었습니다.');
|
||||
router.push(clientDetailConfig.basePath);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: '거래처 등록에 실패했습니다.' };
|
||||
} else {
|
||||
const result = await updateClient(clientId!, clientFormData);
|
||||
if (result) {
|
||||
toast.success('거래처가 수정되었습니다.');
|
||||
router.push(`${clientDetailConfig.basePath}/${clientId}`);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: '거래처 수정에 실패했습니다.' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('저장 실패:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '저장 중 오류가 발생했습니다.' };
|
||||
}
|
||||
},
|
||||
[isNewMode, clientId, generatedCode, router, createClient, updateClient]
|
||||
);
|
||||
|
||||
// 삭제 핸들러
|
||||
const handleDelete = useCallback(
|
||||
async (id: string | number) => {
|
||||
try {
|
||||
const result = await deleteClient(String(id));
|
||||
if (result) {
|
||||
toast.success('거래처가 삭제되었습니다.');
|
||||
router.push(clientDetailConfig.basePath);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: '거래처 삭제에 실패했습니다.' };
|
||||
} catch (error) {
|
||||
console.error('삭제 실패:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '삭제 중 오류가 발생했습니다.' };
|
||||
}
|
||||
},
|
||||
[router, deleteClient]
|
||||
);
|
||||
|
||||
// 취소 핸들러
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isNewMode) {
|
||||
router.push(clientDetailConfig.basePath);
|
||||
} else {
|
||||
setMode('view');
|
||||
router.push(`${clientDetailConfig.basePath}/${clientId}`);
|
||||
}
|
||||
}, [router, clientId, isNewMode]);
|
||||
|
||||
// 초기 데이터 (신규 등록 시 코드 포함)
|
||||
const initialData = isNewMode
|
||||
? ({ code: generatedCode } as Client)
|
||||
: clientData || undefined;
|
||||
|
||||
// 타이틀 동적 설정
|
||||
const dynamicConfig = {
|
||||
...clientDetailConfig,
|
||||
title:
|
||||
mode === 'create'
|
||||
? '거래처'
|
||||
: mode === 'edit'
|
||||
? clientData?.name || '거래처'
|
||||
: clientData?.name || '거래처 상세',
|
||||
};
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={dynamicConfig}
|
||||
mode={mode}
|
||||
initialData={initialData}
|
||||
itemId={clientId}
|
||||
isLoading={isLoading}
|
||||
onSubmit={handleSubmit}
|
||||
onDelete={handleDelete}
|
||||
onCancel={handleCancel}
|
||||
onModeChange={handleModeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
257
src/components/clients/actions.ts
Normal file
257
src/components/clients/actions.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
'use server';
|
||||
|
||||
/**
|
||||
* 거래처(영업) 서버 액션
|
||||
*
|
||||
* API Endpoints:
|
||||
* - GET /api/v1/clients/{id} - 상세 조회
|
||||
* - POST /api/v1/clients - 등록
|
||||
* - PUT /api/v1/clients/{id} - 수정
|
||||
* - DELETE /api/v1/clients/{id} - 삭제
|
||||
*
|
||||
* 🚨 401 에러 시 __authError: true 반환 → 클라이언트에서 로그인 페이지로 리다이렉트
|
||||
*/
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { Client, ClientFormData, ClientApiResponse } from '@/hooks/useClientList';
|
||||
import {
|
||||
transformClientFromApi,
|
||||
transformClientToApiCreate,
|
||||
transformClientToApiUpdate,
|
||||
} from '@/hooks/useClientList';
|
||||
|
||||
// ===== 응답 타입 =====
|
||||
interface ActionResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// ===== 거래처 단건 조회 =====
|
||||
export async function getClientById(id: string): Promise<ActionResponse<Client>> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
|
||||
console.log('[ClientActions] GET client URL:', url);
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
console.error('[ClientActions] Auth error:', error);
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
console.error('[ClientActions] No response, error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '서버 응답이 없습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
// 응답 텍스트 먼저 읽기
|
||||
const responseText = await response.text();
|
||||
console.log('[ClientActions] Response status:', response.status);
|
||||
console.log('[ClientActions] Response text:', responseText);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[ClientActions] GET client error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
// JSON 파싱
|
||||
let result: ApiResponse<ClientApiResponse>;
|
||||
try {
|
||||
result = JSON.parse(responseText);
|
||||
} catch {
|
||||
console.error('[ClientActions] JSON parse error');
|
||||
return {
|
||||
success: false,
|
||||
error: 'JSON 파싱 오류',
|
||||
};
|
||||
}
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
console.error('[ClientActions] API returned error:', result);
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '거래처를 찾을 수 없습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformClientFromApi(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ClientActions] getClientById error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '거래처 조회 중 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 거래처 생성 =====
|
||||
export async function createClient(
|
||||
formData: Partial<ClientFormData>
|
||||
): Promise<ActionResponse<Client>> {
|
||||
try {
|
||||
const apiData = transformClientToApiCreate(formData as ClientFormData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients`;
|
||||
|
||||
console.log('[ClientActions] POST client request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result: ApiResponse<ClientApiResponse> = await response.json();
|
||||
console.log('[ClientActions] POST client response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '거래처 생성에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformClientFromApi(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ClientActions] createClient error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '거래처 생성 중 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 거래처 수정 =====
|
||||
export async function updateClient(
|
||||
id: string,
|
||||
formData: Partial<ClientFormData>
|
||||
): Promise<ActionResponse<Client>> {
|
||||
try {
|
||||
const apiData = transformClientToApiUpdate(formData as ClientFormData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
|
||||
|
||||
console.log('[ClientActions] PUT client request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result: ApiResponse<ClientApiResponse> = await response.json();
|
||||
console.log('[ClientActions] PUT client response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '거래처 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformClientFromApi(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ClientActions] updateClient error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '거래처 수정 중 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 거래처 삭제 =====
|
||||
export async function deleteClient(id: string): Promise<ActionResponse> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'DELETE' });
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[ClientActions] DELETE client response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '거래처 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ClientActions] deleteClient error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '거래처 삭제 중 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 거래처 코드 생성 (8자리 영문+숫자) =====
|
||||
export async function generateClientCode(): Promise<string> {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
276
src/components/clients/clientDetailConfig.ts
Normal file
276
src/components/clients/clientDetailConfig.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* 거래처(영업) 상세 페이지 설정
|
||||
* IntegratedDetailTemplate V2 마이그레이션
|
||||
*/
|
||||
|
||||
import { Building2 } from 'lucide-react';
|
||||
import type { DetailConfig, FieldDefinition, SectionDefinition } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
import type { ClientFormData, Client } from '@/hooks/useClientList';
|
||||
|
||||
// ===== 거래처 유형 옵션 =====
|
||||
const CLIENT_TYPE_OPTIONS = [
|
||||
{ value: '매입', label: '매입' },
|
||||
{ value: '매출', label: '매출' },
|
||||
{ value: '매입매출', label: '매입매출' },
|
||||
];
|
||||
|
||||
// ===== 상태 옵션 =====
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'true', label: '활성' },
|
||||
{ value: 'false', label: '비활성' },
|
||||
];
|
||||
|
||||
// ===== 필드 정의 =====
|
||||
export const clientFields: FieldDefinition[] = [
|
||||
// 기본 정보
|
||||
{
|
||||
key: 'businessNo',
|
||||
label: '사업자등록번호',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '10자리 숫자 (예: 123-45-67890)',
|
||||
validation: [
|
||||
{ type: 'required', message: '사업자등록번호를 입력해주세요.' },
|
||||
{
|
||||
type: 'custom',
|
||||
message: '사업자등록번호는 10자리 숫자여야 합니다.',
|
||||
validate: (value) => {
|
||||
const digits = String(value || '').replace(/-/g, '').trim();
|
||||
return /^\d{10}$/.test(digits);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'clientCode',
|
||||
label: '거래처 코드',
|
||||
type: 'text',
|
||||
disabled: true,
|
||||
helpText: '자동 생성됩니다',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: '거래처명',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '거래처명 입력',
|
||||
validation: [
|
||||
{ type: 'required', message: '거래처명을 입력해주세요.' },
|
||||
{ type: 'minLength', value: 2, message: '거래처명은 2자 이상 입력해주세요.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'representative',
|
||||
label: '대표자명',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '대표자명 입력',
|
||||
validation: [
|
||||
{ type: 'required', message: '대표자명을 입력해주세요.' },
|
||||
{ type: 'minLength', value: 2, message: '대표자명은 2자 이상 입력해주세요.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'clientType',
|
||||
label: '거래처 유형',
|
||||
type: 'radio',
|
||||
required: true,
|
||||
options: CLIENT_TYPE_OPTIONS,
|
||||
},
|
||||
{
|
||||
key: 'businessType',
|
||||
label: '업태',
|
||||
type: 'text',
|
||||
placeholder: '제조업, 도소매업 등',
|
||||
},
|
||||
{
|
||||
key: 'businessItem',
|
||||
label: '종목',
|
||||
type: 'text',
|
||||
placeholder: '철강, 건설 등',
|
||||
},
|
||||
|
||||
// 연락처 정보
|
||||
{
|
||||
key: 'address',
|
||||
label: '주소',
|
||||
type: 'text',
|
||||
placeholder: '주소 입력',
|
||||
gridSpan: 2,
|
||||
},
|
||||
{
|
||||
key: 'phone',
|
||||
label: '전화번호',
|
||||
type: 'tel',
|
||||
placeholder: '02-1234-5678',
|
||||
},
|
||||
{
|
||||
key: 'mobile',
|
||||
label: '모바일',
|
||||
type: 'tel',
|
||||
placeholder: '010-1234-5678',
|
||||
},
|
||||
{
|
||||
key: 'fax',
|
||||
label: '팩스',
|
||||
type: 'tel',
|
||||
placeholder: '02-1234-5678',
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: '이메일',
|
||||
type: 'email',
|
||||
placeholder: 'example@company.com',
|
||||
validation: [
|
||||
{
|
||||
type: 'custom',
|
||||
message: '올바른 이메일 형식이 아닙니다.',
|
||||
validate: (value) => {
|
||||
if (!value) return true; // 선택 필드
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value));
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// 담당자 정보
|
||||
{
|
||||
key: 'managerName',
|
||||
label: '담당자명',
|
||||
type: 'text',
|
||||
placeholder: '담당자명 입력',
|
||||
},
|
||||
{
|
||||
key: 'managerTel',
|
||||
label: '담당자 전화',
|
||||
type: 'tel',
|
||||
placeholder: '010-1234-5678',
|
||||
},
|
||||
{
|
||||
key: 'systemManager',
|
||||
label: '시스템 관리자',
|
||||
type: 'text',
|
||||
placeholder: '시스템 관리자명',
|
||||
},
|
||||
|
||||
// 기타 정보
|
||||
{
|
||||
key: 'memo',
|
||||
label: '메모',
|
||||
type: 'textarea',
|
||||
placeholder: '메모 입력',
|
||||
gridSpan: 2,
|
||||
},
|
||||
{
|
||||
key: 'isActive',
|
||||
label: '상태',
|
||||
type: 'radio',
|
||||
options: STATUS_OPTIONS,
|
||||
defaultValue: 'true',
|
||||
},
|
||||
];
|
||||
|
||||
// ===== 섹션 정의 =====
|
||||
export const clientSections: SectionDefinition[] = [
|
||||
{
|
||||
id: 'basicInfo',
|
||||
title: '기본 정보',
|
||||
description: '거래처의 기본 정보를 입력하세요',
|
||||
fields: ['businessNo', 'clientCode', 'name', 'representative', 'clientType', 'businessType', 'businessItem'],
|
||||
},
|
||||
{
|
||||
id: 'contactInfo',
|
||||
title: '연락처 정보',
|
||||
description: '거래처의 연락처 정보를 입력하세요',
|
||||
fields: ['address', 'phone', 'mobile', 'fax', 'email'],
|
||||
},
|
||||
{
|
||||
id: 'managerInfo',
|
||||
title: '담당자 정보',
|
||||
description: '거래처 담당자 정보를 입력하세요',
|
||||
fields: ['managerName', 'managerTel', 'systemManager'],
|
||||
},
|
||||
{
|
||||
id: 'otherInfo',
|
||||
title: '기타 정보',
|
||||
description: '추가 정보를 입력하세요',
|
||||
fields: ['memo', 'isActive'],
|
||||
},
|
||||
];
|
||||
|
||||
// ===== 설정 =====
|
||||
export const clientDetailConfig: DetailConfig<Client> = {
|
||||
title: '거래처',
|
||||
description: '거래처 정보를 관리합니다',
|
||||
icon: Building2,
|
||||
basePath: '/ko/sales/client-management-sales-admin',
|
||||
fields: clientFields,
|
||||
sections: clientSections,
|
||||
gridColumns: 2,
|
||||
actions: {
|
||||
submitLabel: '저장',
|
||||
cancelLabel: '취소',
|
||||
showDelete: true,
|
||||
deleteLabel: '삭제',
|
||||
showEdit: true,
|
||||
editLabel: '수정',
|
||||
showBack: true,
|
||||
backLabel: '목록',
|
||||
deleteConfirmMessage: {
|
||||
title: '거래처 삭제',
|
||||
description: '이 거래처를 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.',
|
||||
},
|
||||
},
|
||||
transformInitialData: (data: Client) => ({
|
||||
businessNo: data.businessNo || '',
|
||||
clientCode: data.code || '',
|
||||
name: data.name || '',
|
||||
representative: data.representative || '',
|
||||
clientType: data.clientType || '매입',
|
||||
businessType: data.businessType || '',
|
||||
businessItem: data.businessItem || '',
|
||||
address: data.address || '',
|
||||
phone: data.phone || '',
|
||||
mobile: data.mobile || '',
|
||||
fax: data.fax || '',
|
||||
email: data.email || '',
|
||||
managerName: data.managerName || '',
|
||||
managerTel: data.managerTel || '',
|
||||
systemManager: data.systemManager || '',
|
||||
memo: data.memo || '',
|
||||
isActive: data.status === '활성' ? 'true' : 'false',
|
||||
}),
|
||||
transformSubmitData: (formData): Partial<ClientFormData> => ({
|
||||
clientCode: formData.clientCode as string,
|
||||
name: formData.name as string,
|
||||
businessNo: formData.businessNo as string,
|
||||
representative: formData.representative as string,
|
||||
clientType: formData.clientType as ClientFormData['clientType'],
|
||||
businessType: formData.businessType as string,
|
||||
businessItem: formData.businessItem as string,
|
||||
address: formData.address as string,
|
||||
phone: formData.phone as string,
|
||||
mobile: formData.mobile as string,
|
||||
fax: formData.fax as string,
|
||||
email: formData.email as string,
|
||||
managerName: formData.managerName as string,
|
||||
managerTel: formData.managerTel as string,
|
||||
systemManager: formData.systemManager as string,
|
||||
memo: formData.memo as string,
|
||||
isActive: formData.isActive === 'true',
|
||||
// 기본값 설정
|
||||
purchasePaymentDay: '말일',
|
||||
salesPaymentDay: '말일',
|
||||
taxAgreement: false,
|
||||
taxAmount: '',
|
||||
taxStartDate: '',
|
||||
taxEndDate: '',
|
||||
badDebt: false,
|
||||
badDebtAmount: '',
|
||||
badDebtReceiveDate: '',
|
||||
badDebtEndDate: '',
|
||||
badDebtProgress: '',
|
||||
accountId: '',
|
||||
accountPassword: '',
|
||||
}),
|
||||
};
|
||||
Reference in New Issue
Block a user