refactor(WEB): CEO 대시보드 대규모 개선 및 문서/권한/스토어 리팩토링
- CEO 대시보드: 섹션별 API 연동 강화 (매출/매입/생산 실데이터 표시) - DashboardSettingsDialog 드래그 정렬 및 설정 UX 개선 - dashboard transformers 모듈 분리 (파일 분할) - DocumentTable/DocumentWrapper 공통 문서 컴포넌트 추출 - LineItemsTable organisms 컴포넌트 추가 - PurchaseOrderDocument/InspectionRequestDocument 문서 컴포넌트 리팩토링 - PermissionContext → permissionStore(Zustand) 전환 - useUIStore, stores/utils/userStorage 추가 - favoritesStore/useTableColumnStore 사용자별 저장 지원 - DepositDetail/WithdrawalDetail 삭제 (통합) - PurchaseDetail/SalesDetail 간소화 - amount.ts/formatters.ts 유틸 확장 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,320 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Banknote,
|
||||
List,
|
||||
} from 'lucide-react';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { useDeleteDialog } from '@/hooks/useDeleteDialog';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { toast } from 'sonner';
|
||||
import type { WithdrawalRecord, WithdrawalType } from './types';
|
||||
import { WITHDRAWAL_TYPE_SELECTOR_OPTIONS } from './types';
|
||||
import {
|
||||
getWithdrawalById,
|
||||
createWithdrawal,
|
||||
updateWithdrawal,
|
||||
deleteWithdrawal,
|
||||
getVendors,
|
||||
} from './actions';
|
||||
|
||||
// ===== Props =====
|
||||
interface WithdrawalDetailProps {
|
||||
withdrawalId: string;
|
||||
mode: 'view' | 'edit' | 'new';
|
||||
}
|
||||
|
||||
export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps) {
|
||||
const router = useRouter();
|
||||
const isViewMode = mode === 'view';
|
||||
const isNewMode = mode === 'new';
|
||||
|
||||
// ===== 폼 상태 =====
|
||||
const [withdrawalDate, setWithdrawalDate] = useState('');
|
||||
const [accountName, setAccountName] = useState('');
|
||||
const [recipientName, setRecipientName] = useState('');
|
||||
const [withdrawalAmount, setWithdrawalAmount] = useState(0);
|
||||
const [note, setNote] = useState('');
|
||||
const [vendorId, setVendorId] = useState('');
|
||||
const [withdrawalType, setWithdrawalType] = useState<WithdrawalType>('unset');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [vendors, setVendors] = useState<{ id: string; name: string }[]>([]);
|
||||
|
||||
// ===== 초기 데이터 로드 (거래처 + 출금 상세 병렬) =====
|
||||
useEffect(() => {
|
||||
const loadInitialData = async () => {
|
||||
const isEditMode = withdrawalId && !isNewMode;
|
||||
if (isEditMode) setIsLoading(true);
|
||||
|
||||
const [vendorsResult, withdrawalResult] = await Promise.all([
|
||||
getVendors(),
|
||||
isEditMode ? getWithdrawalById(withdrawalId) : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
// 거래처 목록
|
||||
if (vendorsResult.success) {
|
||||
setVendors(vendorsResult.data);
|
||||
}
|
||||
|
||||
// 출금 상세
|
||||
if (withdrawalResult) {
|
||||
if (withdrawalResult.success && withdrawalResult.data) {
|
||||
setWithdrawalDate(withdrawalResult.data.withdrawalDate);
|
||||
setAccountName(withdrawalResult.data.accountName);
|
||||
setRecipientName(withdrawalResult.data.recipientName);
|
||||
setWithdrawalAmount(withdrawalResult.data.withdrawalAmount);
|
||||
setNote(withdrawalResult.data.note);
|
||||
setVendorId(withdrawalResult.data.vendorId);
|
||||
setWithdrawalType(withdrawalResult.data.withdrawalType);
|
||||
} else {
|
||||
toast.error(withdrawalResult.error || '출금 내역을 불러오는데 실패했습니다.');
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
loadInitialData();
|
||||
}, [withdrawalId, isNewMode]);
|
||||
|
||||
// ===== 저장 핸들러 =====
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!vendorId) {
|
||||
toast.error('거래처를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
if (withdrawalType === 'unset') {
|
||||
toast.error('출금 유형을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const formData: Partial<WithdrawalRecord> = {
|
||||
withdrawalDate,
|
||||
accountName,
|
||||
recipientName,
|
||||
withdrawalAmount,
|
||||
note,
|
||||
vendorId,
|
||||
vendorName: vendors.find(v => v.id === vendorId)?.name || '',
|
||||
withdrawalType,
|
||||
};
|
||||
|
||||
const result = isNewMode
|
||||
? await createWithdrawal(formData)
|
||||
: await updateWithdrawal(withdrawalId, formData);
|
||||
|
||||
if (result.success) {
|
||||
toast.success(isNewMode ? '출금 내역이 등록되었습니다.' : '출금 내역이 수정되었습니다.');
|
||||
router.push('/ko/accounting/withdrawals');
|
||||
} else {
|
||||
toast.error(result.error || '저장에 실패했습니다.');
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, [withdrawalId, withdrawalDate, accountName, recipientName, withdrawalAmount, note, vendorId, vendors, withdrawalType, router, isNewMode]);
|
||||
|
||||
// ===== 취소 핸들러 =====
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isNewMode) {
|
||||
router.push('/ko/accounting/withdrawals');
|
||||
} else {
|
||||
router.push(`/ko/accounting/withdrawals/${withdrawalId}?mode=view`);
|
||||
}
|
||||
}, [router, withdrawalId, isNewMode]);
|
||||
|
||||
// ===== 목록으로 이동 =====
|
||||
const handleBack = useCallback(() => {
|
||||
router.push('/ko/accounting/withdrawals');
|
||||
}, [router]);
|
||||
|
||||
// ===== 수정 모드로 이동 =====
|
||||
const handleEdit = useCallback(() => {
|
||||
router.push(`/ko/accounting/withdrawals/${withdrawalId}?mode=edit`);
|
||||
}, [router, withdrawalId]);
|
||||
|
||||
// ===== 삭제 다이얼로그 =====
|
||||
const deleteDialog = useDeleteDialog({
|
||||
onDelete: async (id) => deleteWithdrawal(id),
|
||||
onSuccess: () => router.push('/ko/accounting/withdrawals'),
|
||||
entityName: '출금',
|
||||
});
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* 페이지 헤더 */}
|
||||
<PageHeader
|
||||
title={isNewMode ? '출금 등록' : isViewMode ? '출금 상세' : '출금 수정'}
|
||||
description="출금 상세 내역을 등록합니다"
|
||||
icon={Banknote}
|
||||
/>
|
||||
|
||||
{/* 헤더 액션 버튼 */}
|
||||
<div className="flex items-center justify-end gap-2 mb-6">
|
||||
{/* view 모드: [목록] [삭제] [수정] */}
|
||||
{isViewMode ? (
|
||||
<>
|
||||
<Button variant="outline" onClick={handleBack}>
|
||||
<List className="h-4 w-4 mr-2" />
|
||||
목록
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-red-500 border-red-200 hover:bg-red-50"
|
||||
onClick={() => deleteDialog.single.open(withdrawalId)}
|
||||
disabled={deleteDialog.isPending}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
<Button onClick={handleEdit} className="bg-blue-500 hover:bg-blue-600">
|
||||
수정
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
/* edit/new 모드: [취소] [저장/등록] */
|
||||
<>
|
||||
<Button variant="outline" onClick={handleCancel} disabled={isLoading}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-blue-500 hover:bg-blue-600"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? '처리중...' : isNewMode ? '등록' : '저장'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 기본 정보 섹션 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">기본 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* 출금일 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="withdrawalDate">출금일</Label>
|
||||
<DatePicker
|
||||
value={withdrawalDate}
|
||||
disabled
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 출금계좌 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="accountName">출금계좌</Label>
|
||||
<Input
|
||||
id="accountName"
|
||||
value={accountName}
|
||||
readOnly
|
||||
disabled
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 수취인명 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="recipientName">수취인명</Label>
|
||||
<Input
|
||||
id="recipientName"
|
||||
value={recipientName}
|
||||
readOnly
|
||||
disabled
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 출금금액 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="withdrawalAmount">출금금액</Label>
|
||||
<Input
|
||||
id="withdrawalAmount"
|
||||
value={formatNumber(withdrawalAmount)}
|
||||
readOnly
|
||||
disabled
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 적요 */}
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="note">적요</Label>
|
||||
<Input
|
||||
id="note"
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder="적요를 입력해주세요"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 거래처 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="vendorId">
|
||||
거래처 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select value={vendorId} onValueChange={setVendorId} disabled={isViewMode}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{vendors.map((vendor) => (
|
||||
<SelectItem key={vendor.id} value={vendor.id}>
|
||||
{vendor.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 출금 유형 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="withdrawalType">
|
||||
출금 유형 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select value={withdrawalType} onValueChange={(v) => setWithdrawalType(v as WithdrawalType)} disabled={isViewMode}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{WITHDRAWAL_TYPE_SELECTOR_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ===== 삭제 확인 다이얼로그 ===== */}
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialog.single.isOpen}
|
||||
onOpenChange={deleteDialog.single.onOpenChange}
|
||||
onConfirm={deleteDialog.single.confirm}
|
||||
title="출금 삭제"
|
||||
description="이 출금 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다."
|
||||
loading={deleteDialog.isPending}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user