DatePicker 공통화: - date-picker.tsx 공통 컴포넌트 신규 추가 - 전체 폼 컴포넌트 DatePicker 통일 적용 (50+ 파일) - DateRangeSelector 개선 공정관리: - RuleModal 대폭 리팩토링 (-592줄 → 간소화) - ProcessForm, StepForm 개선 - ProcessDetail 수정, actions 확장 작업자화면: - WorkerScreen 기능 대폭 확장 (+543줄) - WorkItemCard 개선 - types 확장 회계/인사/영업/품질: - BadDebtDetail, BillDetail, DepositDetail, SalesDetail 등 DatePicker 적용 - EmployeeForm, VacationDialog 등 DatePicker 적용 - OrderRegistration, QuoteRegistration DatePicker 적용 - InspectionCreate, InspectionDetail DatePicker 적용 공사관리/CEO대시보드: - BiddingDetail, ContractDetail, HandoverReport 등 DatePicker 적용 - ScheduleDetailModal, TodayIssueSection 개선 기타: - WorkOrderCreate/Edit/Detail/List 개선 - ShipmentCreate/Edit, ReceivingDetail 개선 - calendar, calendarEvents 수정 - datepicker 마이그레이션 체크리스트 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
326 lines
10 KiB
TypeScript
326 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import {
|
|
Banknote,
|
|
List,
|
|
} from 'lucide-react';
|
|
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 { PageLayout } from '@/components/organisms/PageLayout';
|
|
import { PageHeader } from '@/components/organisms/PageHeader';
|
|
import { toast } from 'sonner';
|
|
import type { DepositRecord, DepositType } from './types';
|
|
import { DEPOSIT_TYPE_SELECTOR_OPTIONS } from './types';
|
|
import {
|
|
getDepositById,
|
|
createDeposit,
|
|
updateDeposit,
|
|
deleteDeposit,
|
|
getVendors,
|
|
} from './actions';
|
|
|
|
// ===== Props =====
|
|
interface DepositDetailProps {
|
|
depositId: string;
|
|
mode: 'view' | 'edit' | 'new';
|
|
}
|
|
|
|
export function DepositDetail({ depositId, mode }: DepositDetailProps) {
|
|
const router = useRouter();
|
|
const isViewMode = mode === 'view';
|
|
const isNewMode = mode === 'new';
|
|
|
|
// ===== 폼 상태 =====
|
|
const [depositDate, setDepositDate] = useState('');
|
|
const [accountName, setAccountName] = useState('');
|
|
const [depositorName, setDepositorName] = useState('');
|
|
const [depositAmount, setDepositAmount] = useState(0);
|
|
const [note, setNote] = useState('');
|
|
const [vendorId, setVendorId] = useState('');
|
|
const [depositType, setDepositType] = useState<DepositType>('unset');
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [vendors, setVendors] = useState<{ id: string; name: string }[]>([]);
|
|
|
|
// ===== 거래처 목록 로드 =====
|
|
useEffect(() => {
|
|
const loadVendors = async () => {
|
|
const result = await getVendors();
|
|
if (result.success) {
|
|
setVendors(result.data);
|
|
}
|
|
};
|
|
loadVendors();
|
|
}, []);
|
|
|
|
// ===== 데이터 로드 =====
|
|
useEffect(() => {
|
|
const loadDeposit = async () => {
|
|
if (depositId && !isNewMode) {
|
|
setIsLoading(true);
|
|
const result = await getDepositById(depositId);
|
|
if (result.success && result.data) {
|
|
setDepositDate(result.data.depositDate);
|
|
setAccountName(result.data.accountName);
|
|
setDepositorName(result.data.depositorName);
|
|
setDepositAmount(result.data.depositAmount);
|
|
setNote(result.data.note);
|
|
setVendorId(result.data.vendorId);
|
|
setDepositType(result.data.depositType);
|
|
} else {
|
|
toast.error(result.error || '입금 내역을 불러오는데 실패했습니다.');
|
|
}
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
loadDeposit();
|
|
}, [depositId, isNewMode]);
|
|
|
|
// ===== 저장 핸들러 =====
|
|
const handleSave = useCallback(async () => {
|
|
if (!vendorId) {
|
|
toast.error('거래처를 선택해주세요.');
|
|
return;
|
|
}
|
|
if (depositType === 'unset') {
|
|
toast.error('입금 유형을 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
const formData: Partial<DepositRecord> = {
|
|
depositDate,
|
|
accountName,
|
|
depositorName,
|
|
depositAmount,
|
|
note,
|
|
vendorId,
|
|
vendorName: vendors.find(v => v.id === vendorId)?.name || '',
|
|
depositType,
|
|
};
|
|
|
|
const result = isNewMode
|
|
? await createDeposit(formData)
|
|
: await updateDeposit(depositId, formData);
|
|
|
|
if (result.success) {
|
|
toast.success(isNewMode ? '입금 내역이 등록되었습니다.' : '입금 내역이 수정되었습니다.');
|
|
router.push('/ko/accounting/deposits');
|
|
} else {
|
|
toast.error(result.error || '저장에 실패했습니다.');
|
|
}
|
|
setIsLoading(false);
|
|
}, [depositId, depositDate, accountName, depositorName, depositAmount, note, vendorId, vendors, depositType, router, isNewMode]);
|
|
|
|
// ===== 취소 핸들러 =====
|
|
const handleCancel = useCallback(() => {
|
|
if (isNewMode) {
|
|
router.push('/ko/accounting/deposits');
|
|
} else {
|
|
router.push(`/ko/accounting/deposits/${depositId}?mode=view`);
|
|
}
|
|
}, [router, depositId, isNewMode]);
|
|
|
|
// ===== 목록으로 이동 =====
|
|
const handleBack = useCallback(() => {
|
|
router.push('/ko/accounting/deposits');
|
|
}, [router]);
|
|
|
|
// ===== 수정 모드로 이동 =====
|
|
const handleEdit = useCallback(() => {
|
|
router.push(`/ko/accounting/deposits/${depositId}?mode=edit`);
|
|
}, [router, depositId]);
|
|
|
|
// ===== 삭제 핸들러 =====
|
|
const handleDelete = useCallback(async () => {
|
|
setIsLoading(true);
|
|
const result = await deleteDeposit(depositId);
|
|
if (result.success) {
|
|
toast.success('입금 내역이 삭제되었습니다.');
|
|
setShowDeleteDialog(false);
|
|
router.push('/ko/accounting/deposits');
|
|
} else {
|
|
toast.error(result.error || '삭제에 실패했습니다.');
|
|
}
|
|
setIsLoading(false);
|
|
}, [depositId, router]);
|
|
|
|
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={() => setShowDeleteDialog(true)}
|
|
disabled={isLoading}
|
|
>
|
|
삭제
|
|
</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="depositDate">입금일</Label>
|
|
<DatePicker
|
|
value={depositDate}
|
|
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="depositorName">입금자명</Label>
|
|
<Input
|
|
id="depositorName"
|
|
value={depositorName}
|
|
readOnly
|
|
disabled
|
|
className="bg-gray-50"
|
|
/>
|
|
</div>
|
|
|
|
{/* 입금금액 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="depositAmount">입금금액</Label>
|
|
<Input
|
|
id="depositAmount"
|
|
value={depositAmount.toLocaleString()}
|
|
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="depositType">
|
|
입금 유형 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Select value={depositType} onValueChange={(v) => setDepositType(v as DepositType)} disabled={isViewMode}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{DEPOSIT_TYPE_SELECTOR_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* ===== 삭제 확인 다이얼로그 ===== */}
|
|
<DeleteConfirmDialog
|
|
open={showDeleteDialog}
|
|
onOpenChange={setShowDeleteDialog}
|
|
onConfirm={handleDelete}
|
|
title="입금 삭제"
|
|
description="이 입금 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다."
|
|
loading={isLoading}
|
|
/>
|
|
</PageLayout>
|
|
);
|
|
}
|