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:
@@ -288,7 +288,7 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps)
|
||||
</Label>
|
||||
<Select value={vendorId} onValueChange={setVendorId} disabled={isViewMode}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택 ▼" />
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{vendors.map((vendor) => (
|
||||
@@ -307,7 +307,7 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps)
|
||||
</Label>
|
||||
<Select value={withdrawalType} onValueChange={(v) => setWithdrawalType(v as WithdrawalType)} disabled={isViewMode}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택 ▼" />
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{WITHDRAWAL_TYPE_SELECTOR_OPTIONS.map((option) => (
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import type { DetailMode } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
import { withdrawalDetailConfig } from './withdrawalDetailConfig';
|
||||
import type { WithdrawalRecord } from './types';
|
||||
import {
|
||||
getWithdrawalById,
|
||||
createWithdrawal,
|
||||
updateWithdrawal,
|
||||
deleteWithdrawal,
|
||||
} from './actions';
|
||||
|
||||
// ===== Props =====
|
||||
interface WithdrawalDetailClientV2Props {
|
||||
withdrawalId?: string;
|
||||
initialMode?: DetailMode;
|
||||
}
|
||||
|
||||
export default function WithdrawalDetailClientV2({
|
||||
withdrawalId,
|
||||
initialMode = 'view',
|
||||
}: WithdrawalDetailClientV2Props) {
|
||||
const router = useRouter();
|
||||
const [mode, setMode] = useState<DetailMode>(initialMode);
|
||||
const [withdrawal, setWithdrawal] = useState<WithdrawalRecord | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(initialMode !== 'create');
|
||||
|
||||
// ===== 데이터 로드 =====
|
||||
useEffect(() => {
|
||||
const loadWithdrawal = async () => {
|
||||
if (withdrawalId && initialMode !== 'create') {
|
||||
setIsLoading(true);
|
||||
const result = await getWithdrawalById(withdrawalId);
|
||||
if (result.success && result.data) {
|
||||
setWithdrawal(result.data);
|
||||
} else {
|
||||
toast.error(result.error || '출금 내역을 불러오는데 실패했습니다.');
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
loadWithdrawal();
|
||||
}, [withdrawalId, initialMode]);
|
||||
|
||||
// ===== 저장/등록 핸들러 =====
|
||||
const handleSubmit = useCallback(
|
||||
async (formData: Record<string, unknown>): Promise<{ success: boolean; error?: string }> => {
|
||||
const submitData = withdrawalDetailConfig.transformSubmitData?.(formData) || formData;
|
||||
|
||||
if (!submitData.vendorId) {
|
||||
toast.error('거래처를 선택해주세요.');
|
||||
return { success: false, error: '거래처를 선택해주세요.' };
|
||||
}
|
||||
if (submitData.withdrawalType === 'unset') {
|
||||
toast.error('출금 유형을 선택해주세요.');
|
||||
return { success: false, error: '출금 유형을 선택해주세요.' };
|
||||
}
|
||||
|
||||
const result =
|
||||
mode === 'create'
|
||||
? await createWithdrawal(submitData as Partial<WithdrawalRecord>)
|
||||
: await updateWithdrawal(withdrawalId!, submitData as Partial<WithdrawalRecord>);
|
||||
|
||||
if (result.success) {
|
||||
toast.success(mode === 'create' ? '출금 내역이 등록되었습니다.' : '출금 내역이 수정되었습니다.');
|
||||
router.push('/ko/accounting/withdrawals');
|
||||
return { success: true };
|
||||
} else {
|
||||
toast.error(result.error || '저장에 실패했습니다.');
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
},
|
||||
[mode, withdrawalId, router]
|
||||
);
|
||||
|
||||
// ===== 삭제 핸들러 =====
|
||||
const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||
if (!withdrawalId) return { success: false, error: 'ID가 없습니다.' };
|
||||
|
||||
const result = await deleteWithdrawal(withdrawalId);
|
||||
if (result.success) {
|
||||
toast.success('출금 내역이 삭제되었습니다.');
|
||||
router.push('/ko/accounting/withdrawals');
|
||||
return { success: true };
|
||||
} else {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
}, [withdrawalId, router]);
|
||||
|
||||
// ===== 모드 변경 핸들러 =====
|
||||
const handleModeChange = useCallback(
|
||||
(newMode: DetailMode) => {
|
||||
if (newMode === 'edit' && withdrawalId) {
|
||||
router.push(`/ko/accounting/withdrawals/${withdrawalId}/edit`);
|
||||
} else {
|
||||
setMode(newMode);
|
||||
}
|
||||
},
|
||||
[withdrawalId, router]
|
||||
);
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={withdrawalDetailConfig as Parameters<typeof IntegratedDetailTemplate>[0]['config']}
|
||||
mode={mode}
|
||||
initialData={withdrawal as unknown as Record<string, unknown> | undefined}
|
||||
itemId={withdrawalId}
|
||||
isLoading={isLoading}
|
||||
onSubmit={handleSubmit}
|
||||
onDelete={handleDelete}
|
||||
onModeChange={handleModeChange}
|
||||
buttonPosition="top"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Banknote } from 'lucide-react';
|
||||
import type { DetailConfig, FieldDefinition } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
import type { WithdrawalRecord } from './types';
|
||||
import { WITHDRAWAL_TYPE_SELECTOR_OPTIONS } from './types';
|
||||
import { getVendors } from './actions';
|
||||
|
||||
// ===== 필드 정의 =====
|
||||
const fields: FieldDefinition[] = [
|
||||
// 출금일 (readonly)
|
||||
{
|
||||
key: 'withdrawalDate',
|
||||
label: '출금일',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
placeholder: '-',
|
||||
},
|
||||
// 출금계좌 (readonly)
|
||||
{
|
||||
key: 'accountName',
|
||||
label: '출금계좌',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
placeholder: '-',
|
||||
},
|
||||
// 수취인명 (readonly)
|
||||
{
|
||||
key: 'recipientName',
|
||||
label: '수취인명',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
placeholder: '-',
|
||||
},
|
||||
// 출금금액 (readonly)
|
||||
{
|
||||
key: 'withdrawalAmount',
|
||||
label: '출금금액',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
placeholder: '-',
|
||||
},
|
||||
// 적요 (editable)
|
||||
{
|
||||
key: 'note',
|
||||
label: '적요',
|
||||
type: 'text',
|
||||
placeholder: '적요를 입력해주세요',
|
||||
gridSpan: 2,
|
||||
disabled: (mode) => mode === 'view',
|
||||
},
|
||||
// 거래처 (editable, required)
|
||||
{
|
||||
key: 'vendorId',
|
||||
label: '거래처',
|
||||
type: 'select',
|
||||
required: true,
|
||||
placeholder: '선택',
|
||||
fetchOptions: async () => {
|
||||
const result = await getVendors();
|
||||
if (result.success) {
|
||||
return result.data.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
disabled: (mode) => mode === 'view',
|
||||
},
|
||||
// 출금 유형 (editable, required)
|
||||
{
|
||||
key: 'withdrawalType',
|
||||
label: '출금 유형',
|
||||
type: 'select',
|
||||
required: true,
|
||||
placeholder: '선택',
|
||||
options: WITHDRAWAL_TYPE_SELECTOR_OPTIONS.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
})),
|
||||
disabled: (mode) => mode === 'view',
|
||||
},
|
||||
];
|
||||
|
||||
// ===== Config 정의 =====
|
||||
export const withdrawalDetailConfig: DetailConfig = {
|
||||
title: '출금',
|
||||
description: '출금 상세 내역을 등록합니다',
|
||||
icon: Banknote,
|
||||
basePath: '/accounting/withdrawals',
|
||||
fields,
|
||||
gridColumns: 2,
|
||||
actions: {
|
||||
showBack: true,
|
||||
showDelete: true,
|
||||
showEdit: true,
|
||||
backLabel: '목록',
|
||||
deleteLabel: '삭제',
|
||||
editLabel: '수정',
|
||||
deleteConfirmMessage: {
|
||||
title: '출금 삭제',
|
||||
description: '이 출금 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.',
|
||||
},
|
||||
},
|
||||
transformInitialData: (data: Record<string, unknown>): Record<string, unknown> => {
|
||||
const record = data as unknown as WithdrawalRecord;
|
||||
return {
|
||||
withdrawalDate: record.withdrawalDate || '',
|
||||
accountName: record.accountName || '',
|
||||
recipientName: record.recipientName || '',
|
||||
withdrawalAmount: record.withdrawalAmount ? record.withdrawalAmount.toLocaleString() : '0',
|
||||
note: record.note || '',
|
||||
vendorId: record.vendorId || '',
|
||||
withdrawalType: record.withdrawalType || 'unset',
|
||||
};
|
||||
},
|
||||
transformSubmitData: (formData: Record<string, unknown>): Partial<WithdrawalRecord> => {
|
||||
return {
|
||||
note: formData.note as string,
|
||||
vendorId: formData.vendorId as string,
|
||||
withdrawalType: formData.withdrawalType as WithdrawalRecord['withdrawalType'],
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user