From 1691337f7df290501435f6fcc0a07577fcfa5c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Sun, 1 Mar 2026 12:20:21 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=ED=9A=8C=EA=B3=84]=20=EB=A7=A4?= =?UTF-8?q?=EC=B6=9C/=EB=A7=A4=EC=9E=85/=EB=B6=80=EC=8B=A4=EC=B1=84?= =?UTF-8?q?=EA=B6=8C/=EC=9D=BC=EC=9D=BC=EB=B3=B4=EA=B3=A0=20UI=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 부실채권 상세/목록/타입 개선 - 매출관리 SalesDetail 개선 - 매입관리 PurchaseDetail 개선 - 일일보고 UI 리팩토링 Co-Authored-By: Claude Opus 4.6 --- .../accounting/bad-debt-collection/page.tsx | 2 +- .../BadDebtCollection/BadDebtDetail.tsx | 60 ++- .../accounting/BadDebtCollection/actions.ts | 8 +- .../accounting/BadDebtCollection/index.tsx | 38 +- .../accounting/BadDebtCollection/types.ts | 11 +- .../accounting/DailyReport/index.tsx | 384 ++++++++++++------ .../PurchaseManagement/PurchaseDetail.tsx | 251 ++++-------- .../accounting/PurchaseManagement/index.tsx | 74 +--- .../accounting/PurchaseManagement/types.ts | 11 +- .../SalesManagement/SalesDetail.tsx | 81 ++-- .../accounting/SalesManagement/index.tsx | 47 +-- .../accounting/SalesManagement/types.ts | 19 +- 12 files changed, 531 insertions(+), 455 deletions(-) diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx index a59937c4..897a5b4e 100644 --- a/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx @@ -55,7 +55,7 @@ export default function BadDebtCollectionPage() { return ( ); } diff --git a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx index 0428751c..c519a1c3 100644 --- a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx +++ b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx @@ -39,8 +39,10 @@ import type { } from './types'; import { STATUS_SELECT_OPTIONS, + COLLECTION_END_REASON_OPTIONS, VENDOR_TYPE_LABELS, } from './types'; +import type { CollectionEndReason } from './types'; import { createBadDebt, updateBadDebt, deleteBadDebt, addBadDebtMemo, deleteBadDebtMemo } from './actions'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; @@ -87,6 +89,7 @@ const getEmptyRecord = (): Omit assignedManagerId: null, assignedManager: null, settingToggle: true, + collectionEndReason: undefined, badDebtCount: 0, badDebts: [], files: [], @@ -778,22 +781,47 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp {/* 상태 */}
- +
+ + {formData.status === 'collectionEnd' && ( + + )} +
{/* 연체일수 */}
diff --git a/src/components/accounting/BadDebtCollection/actions.ts b/src/components/accounting/BadDebtCollection/actions.ts index c90cb9c0..3dd07fe6 100644 --- a/src/components/accounting/BadDebtCollection/actions.ts +++ b/src/components/accounting/BadDebtCollection/actions.ts @@ -72,8 +72,9 @@ function mapApiStatusToFrontend(apiStatus: string): CollectionStatus { switch (apiStatus) { case 'collecting': return 'collecting'; case 'legal_action': return 'legalAction'; - case 'recovered': return 'recovered'; - case 'bad_debt': return 'badDebt'; + case 'recovered': + case 'bad_debt': + case 'collection_end': return 'collectionEnd'; default: return 'collecting'; } } @@ -82,8 +83,7 @@ function mapFrontendStatusToApi(status: CollectionStatus): string { switch (status) { case 'collecting': return 'collecting'; case 'legalAction': return 'legal_action'; - case 'recovered': return 'recovered'; - case 'badDebt': return 'bad_debt'; + case 'collectionEnd': return 'collection_end'; default: return 'collecting'; } } diff --git a/src/components/accounting/BadDebtCollection/index.tsx b/src/components/accounting/BadDebtCollection/index.tsx index 0e599edb..95d5e297 100644 --- a/src/components/accounting/BadDebtCollection/index.tsx +++ b/src/components/accounting/BadDebtCollection/index.tsx @@ -15,7 +15,8 @@ export { BadDebtDetailClientV2 } from './BadDebtDetailClientV2'; import { useState, useMemo, useCallback, useTransition } from 'react'; import { useRouter } from 'next/navigation'; -import { AlertTriangle } from 'lucide-react'; +import { AlertTriangle, Pencil, Trash2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; @@ -56,6 +57,7 @@ const tableColumns = [ { key: 'managerName', label: '담당자', className: 'w-[100px]', sortable: true }, { key: 'status', label: '상태', className: 'text-center w-[100px]', sortable: true }, { key: 'setting', label: '설정', className: 'text-center w-[80px]' }, + { key: 'actions', label: '작업', className: 'text-center w-[80px]' }, ]; // ===== Props 타입 정의 ===== @@ -65,8 +67,7 @@ interface BadDebtCollectionProps { total_amount: number; collecting_amount: number; legal_action_amount: number; - recovered_amount: number; - bad_debt_amount: number; + collection_end_amount: number; } | null; } @@ -132,7 +133,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec totalAmount: initialSummary.total_amount, collectingAmount: initialSummary.collecting_amount, legalActionAmount: initialSummary.legal_action_amount, - recoveredAmount: initialSummary.recovered_amount, + collectionEndAmount: initialSummary.collection_end_amount, }; } @@ -144,11 +145,11 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec const legalActionAmount = data .filter((d) => d.status === 'legalAction') .reduce((sum, d) => sum + d.debtAmount, 0); - const recoveredAmount = data - .filter((d) => d.status === 'recovered') + const collectionEndAmount = data + .filter((d) => d.status === 'collectionEnd') .reduce((sum, d) => sum + d.debtAmount, 0); - return { totalAmount, collectingAmount, legalActionAmount, recoveredAmount }; + return { totalAmount, collectingAmount, legalActionAmount, collectionEndAmount }; }, [data, initialSummary]); // ===== UniversalListPage Config ===== @@ -335,7 +336,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec }, { label: '회수완료', - value: `${formatNumber(statsData.recoveredAmount)}원`, + value: `${formatNumber(statsData.collectionEndAmount)}원`, icon: AlertTriangle, iconColor: 'text-green-500', }, @@ -390,6 +391,27 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec disabled={isPending} /> + {/* 작업 */} + e.stopPropagation()}> +
+ + +
+
), diff --git a/src/components/accounting/BadDebtCollection/types.ts b/src/components/accounting/BadDebtCollection/types.ts index 3d4c83bb..d46b81e1 100644 --- a/src/components/accounting/BadDebtCollection/types.ts +++ b/src/components/accounting/BadDebtCollection/types.ts @@ -1,7 +1,15 @@ // ===== 악성채권 추심관리 타입 정의 ===== // 추심 상태 -export type CollectionStatus = 'collecting' | 'legalAction' | 'recovered' | 'badDebt'; +export type CollectionStatus = 'collecting' | 'legalAction' | 'collectionEnd'; + +// 추심종료 사유 +export type CollectionEndReason = 'recovered' | 'badDebt'; + +export const COLLECTION_END_REASON_OPTIONS: { value: CollectionEndReason; label: string }[] = [ + { value: 'recovered', label: '회수완료' }, + { value: 'badDebt', label: '대손처리' }, +]; // 정렬 옵션 export type SortOption = 'latest' | 'oldest'; @@ -70,6 +78,7 @@ export interface BadDebtRecord { debtAmount: number; // 총 미수금액 badDebtCount: number; // 악성채권 건수 status: CollectionStatus; // 대표 상태 (가장 최근) + collectionEndReason?: CollectionEndReason; // 추심종료 사유 (status === 'collectionEnd'일 때) overdueDays: number; // 최대 연체일수 overdueToggle: boolean; occurrenceDate: string; diff --git a/src/components/accounting/DailyReport/index.tsx b/src/components/accounting/DailyReport/index.tsx index b1bd44cb..555889e0 100644 --- a/src/components/accounting/DailyReport/index.tsx +++ b/src/components/accounting/DailyReport/index.tsx @@ -1,9 +1,9 @@ 'use client'; import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; -import { format, parseISO } from 'date-fns'; +import { format, parseISO, subMonths, startOfMonth, endOfMonth } from 'date-fns'; import { ko } from 'date-fns/locale'; -import { Download, FileText, Loader2, RefreshCw, Calendar } from 'lucide-react'; +import { Download, FileText, Loader2, Printer, Search } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { @@ -15,18 +15,27 @@ import { TableRow, TableFooter, } from '@/components/ui/table'; -import { DatePicker } from '@/components/ui/date-picker'; +import { DateRangePicker } from '@/components/ui/date-range-picker'; +import { Input } from '@/components/ui/input'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { formatNumber as formatAmount } from '@/lib/utils/amount'; -import { Badge } from '@/components/ui/badge'; import type { NoteReceivableItem, DailyAccountItem } from './types'; -import { MATCH_STATUS_LABELS, MATCH_STATUS_COLORS } from './types'; import { getNoteReceivables, getDailyAccounts, getDailyReportSummary } from './actions'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { usePermission } from '@/hooks/usePermission'; +// ===== 빠른 월 선택 버튼 정의 ===== +const QUICK_MONTH_BUTTONS = [ + { label: '이번달', months: 0 }, + { label: '지난달', months: 1 }, + { label: 'D-2월', months: 2 }, + { label: 'D-3월', months: 3 }, + { label: 'D-4월', months: 4 }, + { label: 'D-5월', months: 5 }, +] as const; + // ===== Props 인터페이스 ===== interface DailyReportProps { initialNoteReceivables?: NoteReceivableItem[]; @@ -36,7 +45,9 @@ interface DailyReportProps { export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts = [] }: DailyReportProps) { const { canExport } = usePermission(); // ===== 상태 관리 ===== - const [selectedDate, setSelectedDate] = useState(() => format(new Date(), 'yyyy-MM-dd')); + const [startDate, setStartDate] = useState(() => format(startOfMonth(new Date()), 'yyyy-MM-dd')); + const [endDate, setEndDate] = useState(() => format(new Date(), 'yyyy-MM-dd')); + const [searchTerm, setSearchTerm] = useState(''); const [noteReceivables, setNoteReceivables] = useState(initialNoteReceivables); const [dailyAccounts, setDailyAccounts] = useState(initialDailyAccounts); const [summary, setSummary] = useState<{ @@ -53,9 +64,9 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts setIsLoading(true); try { const [noteResult, accountResult, summaryResult] = await Promise.all([ - getNoteReceivables({ date: selectedDate }), - getDailyAccounts({ date: selectedDate }), - getDailyReportSummary({ date: selectedDate }), + getNoteReceivables({ date: startDate }), + getDailyAccounts({ date: startDate }), + getDailyReportSummary({ date: startDate }), ]); if (noteResult.success) { @@ -81,20 +92,20 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts } finally { setIsLoading(false); } - }, [selectedDate]); + }, [startDate]); // ===== 초기 로드 및 날짜 변경시 재로드 ===== const isInitialMount = useRef(true); - const prevDateRef = useRef(selectedDate); + const prevDateRef = useRef(startDate); useEffect(() => { // 초기 마운트 또는 날짜가 실제로 변경된 경우에만 로드 - if (isInitialMount.current || prevDateRef.current !== selectedDate) { + if (isInitialMount.current || prevDateRef.current !== startDate) { isInitialMount.current = false; - prevDateRef.current = selectedDate; + prevDateRef.current = startDate; loadData(); } - }, [selectedDate, loadData]); + }, [startDate, loadData]); // ===== 어음 합계 (API 요약 사용) ===== const noteReceivableTotal = useMemo(() => { @@ -144,9 +155,9 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts }, [accountTotals]); // ===== 선택된 날짜 정보 ===== - const selectedDateInfo = useMemo(() => { + const startDateInfo = useMemo(() => { try { - const date = parseISO(selectedDate); + const date = parseISO(startDate); return { formatted: format(date, 'yyyy년 M월 d일', { locale: ko }), dayOfWeek: format(date, 'EEEE', { locale: ko }), @@ -154,12 +165,12 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts } catch { return { formatted: '', dayOfWeek: '' }; } - }, [selectedDate]); + }, [startDate]); // ===== 엑셀 다운로드 (프록시 API 직접 호출) ===== const handleExcelDownload = useCallback(async () => { try { - const url = `/api/proxy/daily-report/export?date=${selectedDate}`; + const url = `/api/proxy/daily-report/export?date=${startDate}`; const response = await fetch(url); if (!response.ok) { @@ -169,7 +180,7 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts const blob = await response.blob(); const contentDisposition = response.headers.get('Content-Disposition'); - const filename = contentDisposition?.match(/filename="?(.+)"?/)?.[1] || `일일일보_${selectedDate}.xlsx`; + const filename = contentDisposition?.match(/filename="?(.+)"?/)?.[1] || `일일일보_${startDate}.xlsx`; const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -183,7 +194,36 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts } catch { toast.error('엑셀 다운로드 중 오류가 발생했습니다.'); } - }, [selectedDate]); + }, [startDate]); + + // ===== 빠른 월 선택 ===== + const handleQuickMonth = useCallback((monthsAgo: number) => { + const target = monthsAgo === 0 ? new Date() : subMonths(new Date(), monthsAgo); + setStartDate(format(startOfMonth(target), 'yyyy-MM-dd')); + setEndDate(format(endOfMonth(target), 'yyyy-MM-dd')); + }, []); + + // ===== 인쇄 ===== + const handlePrint = useCallback(() => { + window.print(); + }, []); + + // ===== 검색 필터링 ===== + const filteredNoteReceivables = useMemo(() => { + if (!searchTerm) return noteReceivables; + const term = searchTerm.toLowerCase(); + return noteReceivables.filter(item => + item.content.toLowerCase().includes(term) + ); + }, [noteReceivables, searchTerm]); + + const filteredDailyAccounts = useMemo(() => { + if (!searchTerm) return dailyAccounts; + const term = searchTerm.toLowerCase(); + return dailyAccounts.filter(item => + item.category.toLowerCase().includes(term) + ); + }, [dailyAccounts, searchTerm]); return ( @@ -194,42 +234,57 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts icon={FileText} /> - {/* 헤더 액션 (날짜 선택, 버튼 등) */} + {/* 헤더 액션 (날짜 선택, 빠른 월 선택, 검색, 인쇄) */} - -
-
- - 조회 일자 - -
-
- - {canExport && ( - - )} + ))} +
+ {/* 검색 + 인쇄/엑셀 - 모바일: 세로 배치 */} +
+
+ + setSearchTerm(e.target.value)} + placeholder="검색..." + className="pl-8 h-8 text-sm" + /> +
+
+ + {canExport && ( + + )} +
@@ -237,19 +292,19 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts {/* 어음 및 외상매출채권현황 */} - -
-

어음 및 외상매출채권현황

+ +
+

어음 및 외상매출채권현황

-
-
+
+
- + - 내용 - 현재 잔액 - 발행일 - 만기일 + 내용 + 현재 잔액 + 발행일 + 만기일 @@ -258,32 +313,32 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
- 데이터를 불러오는 중... + 데이터를 불러오는 중...
- ) : noteReceivables.length === 0 ? ( + ) : filteredNoteReceivables.length === 0 ? ( - + 데이터가 없습니다. ) : ( - noteReceivables.map((item) => ( + filteredNoteReceivables.map((item) => ( - {item.content} - {formatAmount(item.currentBalance)} - {item.issueDate} - {item.dueDate} + {item.content} + {formatAmount(item.currentBalance)} + {item.issueDate} + {item.dueDate} )) )}
- {noteReceivables.length > 0 && ( - + {filteredNoteReceivables.length > 0 && ( + - 합계 - {formatAmount(noteReceivableTotal)} + 합계 + {formatAmount(noteReceivableTotal)} @@ -297,82 +352,63 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts {/* 일자별 상세 */} - -
-

- 일자: {selectedDateInfo.formatted} {selectedDateInfo.dayOfWeek} + +
+

+ 일자: {startDateInfo.formatted} {startDateInfo.dayOfWeek}

-
+

- 구분 - 상태 - 전월 이월 - 수입 - 지출 - 잔액 + 구분 + 입금 + 출금 + 잔액 {isLoading ? ( - +
- 데이터를 불러오는 중... + 데이터를 불러오는 중...
- ) : dailyAccounts.length === 0 ? ( + ) : filteredDailyAccounts.length === 0 ? ( - + 데이터가 없습니다. ) : ( <> {/* KRW 계좌들 */} - {dailyAccounts + {filteredDailyAccounts .filter(item => item.currency === 'KRW') .map((item) => ( - {item.category} - - - {MATCH_STATUS_LABELS[item.matchStatus]} - - - {formatAmount(item.carryover)} - {formatAmount(item.income)} - {formatAmount(item.expense)} - {formatAmount(item.balance)} + {item.category} + {formatAmount(item.income)} + {formatAmount(item.expense)} + {formatAmount(item.balance)} ))} )}
- {dailyAccounts.length > 0 && ( + {filteredDailyAccounts.length > 0 && ( - {/* 외화원 (USD) 합계 */} - - 외화원 (USD) 합계 - - ${formatAmount(accountTotals.usd.carryover)} - ${formatAmount(accountTotals.usd.income)} - ${formatAmount(accountTotals.usd.expense)} - ${formatAmount(accountTotals.usd.balance)} - - {/* 현금성 자산 합계 */} + {/* 합계 */} - 현금성 자산 합계 - - {formatAmount(cashAssetTotal.carryover)} - {formatAmount(cashAssetTotal.income)} - {formatAmount(cashAssetTotal.expense)} - {formatAmount(cashAssetTotal.balance)} + 합계 + {formatAmount(cashAssetTotal.income)} + {formatAmount(cashAssetTotal.expense)} + {formatAmount(cashAssetTotal.balance)} )} @@ -381,6 +417,114 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts + + {/* 예금 입출금 내역 */} + + +
+

예금 입출금 내역

+
+
+ {/* 입금 */} +
+
+ 입금 +
+
+
+ + + 입금처/적요 + 금액 + + + + {isLoading ? ( + + + + + + ) : filteredDailyAccounts.filter(item => item.currency === 'KRW' && item.income > 0).length === 0 ? ( + + + 입금 내역이 없습니다. + + + ) : ( + filteredDailyAccounts + .filter(item => item.currency === 'KRW' && item.income > 0) + .map((item) => ( + + +
{item.category}
+
+ {formatAmount(item.income)} +
+ )) + )} +
+ + + 입금 합계 + {formatAmount(cashAssetTotal.income)} + + +
+
+
+ + {/* 출금 */} +
+
+ 출금 +
+
+ + + + 출금처/적요 + 금액 + + + + {isLoading ? ( + + + + + + ) : filteredDailyAccounts.filter(item => item.currency === 'KRW' && item.expense > 0).length === 0 ? ( + + + 출금 내역이 없습니다. + + + ) : ( + filteredDailyAccounts + .filter(item => item.currency === 'KRW' && item.expense > 0) + .map((item) => ( + + +
{item.category}
+
+ {formatAmount(item.expense)} +
+ )) + )} +
+ + + 출금 합계 + {formatAmount(cashAssetTotal.expense)} + + +
+
+
+
+ + ); -} \ No newline at end of file +} diff --git a/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx b/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx index 7966b22e..1c4f8c20 100644 --- a/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx +++ b/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx @@ -24,8 +24,7 @@ import { purchaseConfig } from './purchaseConfig'; import { DocumentDetailModalV2 as DocumentDetailModal } from '@/components/approval/DocumentDetail'; import type { ProposalDocumentData, ExpenseReportDocumentData, ExpenseEstimateDocumentData } from '@/components/approval/DocumentDetail/types'; import { getApprovalById } from '@/components/approval/DocumentCreate/actions'; -import type { PurchaseRecord, PurchaseItem, PurchaseType } from './types'; -import { PURCHASE_TYPE_LABELS } from './types'; +import type { PurchaseRecord, PurchaseItem } from './types'; import { getPurchaseById, createPurchase, @@ -74,7 +73,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { const [purchaseDate, setPurchaseDate] = useState(format(new Date(), 'yyyy-MM-dd')); const [vendorId, setVendorId] = useState(''); const [vendorName, setVendorName] = useState(''); - const [purchaseType, setPurchaseType] = useState('unset'); + // purchaseType 삭제됨 (기획서 P.109) const [items, setItems] = useState([createEmptyItem()]); const [taxInvoiceReceived, setTaxInvoiceReceived] = useState(false); @@ -126,7 +125,6 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { setPurchaseDate(data.purchaseDate); setVendorId(data.vendorId); setVendorName(data.vendorName); - setPurchaseType(data.purchaseType); setItems(data.items.length > 0 ? data.items : [createEmptyItem()]); setTaxInvoiceReceived(data.taxInvoiceReceived); setSourceDocument(data.sourceDocument); @@ -250,7 +248,6 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { supplyAmount: totals.supplyAmount, vat: totals.vat, totalAmount: totals.total, - purchaseType, taxInvoiceReceived, }; @@ -275,7 +272,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { } finally { setIsSaving(false); } - }, [purchaseDate, vendorId, totals, purchaseType, taxInvoiceReceived, isNewMode, purchaseId]); + }, [purchaseDate, vendorId, totals, taxInvoiceReceived, isNewMode, purchaseId]); // ===== 삭제 (IntegratedDetailTemplate 호환) ===== const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => { @@ -301,179 +298,101 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { const renderFormContent = () => ( <>
- {/* ===== 기본 정보 섹션 ===== */} + {/* ===== 기본 정보 섹션 (품의서/지출결의서 + 예상비용) ===== */} 기본 정보 - - {/* 품의서/지출결의서인 경우 전용 레이아웃 */} - {sourceDocument ? ( - <> - {/* 문서 타입 및 열람 버튼 */} -
-
- - {sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'} - - 연결된 문서가 있습니다 -
+ +
+ {/* 품의서/지출결의서 */} +
+ +
+
- - {/* 품의서/지출결의서용 필드 */} -
- {/* 품의서/지출결의서 제목 */} -
- - -
- - {/* 예상비용 */} -
- - -
- - {/* 매입번호 */} -
- - -
- - {/* 거래처명 */} -
- - -
- - {/* 매입 유형 */} -
- - -
-
- - ) : ( - /* 일반 매입 (품의서/지출결의서 없는 경우) */ -
- {/* 매입번호 */} -
- - -
- - {/* 매입일 */} -
- - -
- - {/* 거래처명 */} -
- - -
- - {/* 매입 유형 */} -
- - -
- )} + + {/* 예상비용 */} +
+ + +
+
+ + + + {/* ===== 매입 정보 섹션 ===== */} + + + 매입 정보 + + +
+ {/* 매입번호 */} +
+ + +
+ + {/* 매입일 */} +
+ + +
+ + {/* 거래처명 */} +
+ + +
+
diff --git a/src/components/accounting/PurchaseManagement/index.tsx b/src/components/accounting/PurchaseManagement/index.tsx index c2665743..e47c54c4 100644 --- a/src/components/accounting/PurchaseManagement/index.tsx +++ b/src/components/accounting/PurchaseManagement/index.tsx @@ -26,8 +26,7 @@ import { import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; -import { Badge } from '@/components/ui/badge'; -import { getPresetStyle } from '@/lib/utils/status-config'; +// Badge, getPresetStyle removed (매입유형/연결문서 컬럼 삭제) import { Switch } from '@/components/ui/switch'; import { Dialog, @@ -57,9 +56,7 @@ import { MobileCard } from '@/components/organisms/MobileCard'; import type { PurchaseRecord } from './types'; import { SORT_OPTIONS, - PURCHASE_TYPE_LABELS, - PURCHASE_TYPE_FILTER_OPTIONS, - ISSUANCE_FILTER_OPTIONS, + TAX_INVOICE_RECEIVED_FILTER_OPTIONS, ACCOUNT_SUBJECT_SELECTOR_OPTIONS, } from './types'; import { getPurchases, togglePurchaseTaxInvoice, deletePurchase } from './actions'; @@ -71,11 +68,9 @@ const tableColumns = [ { key: 'purchaseNo', label: '매입번호', sortable: true }, { key: 'purchaseDate', label: '매입일', sortable: true }, { key: 'vendorName', label: '거래처', sortable: true }, - { key: 'sourceDocument', label: '연결문서', className: 'text-center', sortable: true }, { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true }, { key: 'vat', label: '부가세', className: 'text-right', sortable: true }, { key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true }, - { key: 'purchaseType', label: '매입유형', className: 'text-center', sortable: true }, { key: 'taxInvoice', label: '세금계산서 수취 확인', className: 'text-center' }, ]; @@ -92,8 +87,7 @@ export function PurchaseManagement() { // 통합 필터 상태 (filterConfig 기반) const [filterValues, setFilterValues] = useState>({ vendor: 'all', - purchaseType: 'all', - issuance: 'all', + taxInvoiceReceived: 'all', sort: 'latest', }); @@ -142,9 +136,8 @@ export function PurchaseManagement() { return date.getMonth() === currentMonth && date.getFullYear() === currentYear; }) .reduce((sum, d) => sum + d.totalAmount, 0); - const unsetTypeCount = purchaseData.filter(d => d.purchaseType === 'unset').length; const taxInvoicePendingCount = purchaseData.filter(d => !d.taxInvoiceReceived).length; - return { totalPurchaseAmount, monthlyAmount, unsetTypeCount, taxInvoicePendingCount }; + return { totalPurchaseAmount, monthlyAmount, taxInvoicePendingCount }; }, [purchaseData]); // ===== 거래처 목록 (필터용) ===== @@ -163,17 +156,10 @@ export function PurchaseManagement() { allOptionLabel: '거래처 전체', }, { - key: 'purchaseType', - label: '매입유형', + key: 'taxInvoiceReceived', + label: '세금계산서 수취여부', type: 'single', - options: PURCHASE_TYPE_FILTER_OPTIONS.filter(o => o.value !== 'all'), - allOptionLabel: '전체', - }, - { - key: 'issuance', - label: '발행여부', - type: 'single', - options: ISSUANCE_FILTER_OPTIONS.filter(o => o.value !== 'all'), + options: TAX_INVOICE_RECEIVED_FILTER_OPTIONS.filter(o => o.value !== 'all'), allOptionLabel: '전체', }, { @@ -194,8 +180,7 @@ export function PurchaseManagement() { const handleFilterReset = useCallback(() => { setFilterValues({ vendor: 'all', - purchaseType: 'all', - issuance: 'all', + taxInvoiceReceived: 'all', sort: 'latest', }); }, []); @@ -309,18 +294,16 @@ export function PurchaseManagement() { } const vendorVal = fv.vendor as string; - const purchaseTypeVal = fv.purchaseType as string; - const issuanceVal = fv.issuance as string; + const taxInvoiceReceivedVal = fv.taxInvoiceReceived as string; // 거래처 필터 if (vendorVal !== 'all' && item.vendorName !== vendorVal) { return false; } - // 매입유형 필터 - if (purchaseTypeVal !== 'all' && item.purchaseType !== purchaseTypeVal) { + // 세금계산서 수취여부 필터 + if (taxInvoiceReceivedVal === 'received' && !item.taxInvoiceReceived) { return false; } - // 발행여부 필터 - if (issuanceVal === 'taxInvoicePending' && item.taxInvoiceReceived) { + if (taxInvoiceReceivedVal === 'notReceived' && item.taxInvoiceReceived) { return false; } return true; @@ -393,9 +376,8 @@ export function PurchaseManagement() { // Stats 카드 computeStats: (): StatCard[] => [ - { label: '총 매입', value: `${formatNumber(stats.totalPurchaseAmount)}원`, icon: Receipt, iconColor: 'text-blue-500' }, + { label: '총매입', value: `${formatNumber(stats.totalPurchaseAmount)}원`, icon: Receipt, iconColor: 'text-blue-500' }, { label: '당월 매입', value: `${formatNumber(stats.monthlyAmount)}원`, icon: Receipt, iconColor: 'text-green-500' }, - { label: '매입유형 미설정', value: `${stats.unsetTypeCount}건`, icon: Receipt, iconColor: 'text-orange-500' }, { label: '세금계산서 수취 미확인', value: `${stats.taxInvoicePendingCount}건`, icon: Receipt, iconColor: 'text-red-500' }, ], @@ -406,13 +388,10 @@ export function PurchaseManagement() { 합계 - - {formatNumber(tableTotals.totalSupplyAmount)} {formatNumber(tableTotals.totalVat)} {formatNumber(tableTotals.totalAmount)} - ), @@ -428,9 +407,7 @@ export function PurchaseManagement() { index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers - ) => { - const isUnsetType = item.purchaseType === 'unset'; - return ( + ) => ( {item.purchaseNo} {item.purchaseDate} {item.vendorName} - - {item.sourceDocument ? ( - - {item.sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'} - - ) : ( - - - )} - {formatNumber(item.supplyAmount)} {formatNumber(item.vat)} {formatNumber(item.totalAmount)} - - - {PURCHASE_TYPE_LABELS[item.purchaseType]} - - e.stopPropagation()}>
- ); - }, + ), // 모바일 카드 렌더링 renderMobileCard: ( @@ -488,14 +447,11 @@ export function PurchaseManagement() { key={item.id} title={item.vendorName} subtitle={item.purchaseNo} - badge={PURCHASE_TYPE_LABELS[item.purchaseType]} - badgeVariant="outline" isSelected={handlers.isSelected} onToggle={handlers.onToggle} onClick={() => handleRowClick(item)} details={[ { label: '매입일', value: item.purchaseDate }, - { label: '연결문서', value: item.sourceDocument ? (item.sourceDocument.type === 'proposal' ? '품의서' : '지출결의서') : '-' }, { label: '공급가액', value: `${formatNumber(item.supplyAmount)}원` }, { label: '합계금액', value: `${formatNumber(item.totalAmount)}원` }, ]} diff --git a/src/components/accounting/PurchaseManagement/types.ts b/src/components/accounting/PurchaseManagement/types.ts index 88f2cab8..55e0a6ff 100644 --- a/src/components/accounting/PurchaseManagement/types.ts +++ b/src/components/accounting/PurchaseManagement/types.ts @@ -81,8 +81,8 @@ export interface PurchaseRecord { // 정렬 옵션 export type SortOption = 'latest' | 'oldest' | 'amountHigh' | 'amountLow'; -// 발행여부 필터 -export type IssuanceFilter = 'all' | 'taxInvoicePending'; +// 세금계산서 수취여부 필터 +export type TaxInvoiceReceivedFilter = 'all' | 'received' | 'notReceived'; // ===== 상수 정의 ===== @@ -154,10 +154,11 @@ export const PURCHASE_TYPE_FILTER_OPTIONS: { value: string; label: string }[] = { value: 'unset', label: '미설정' }, ]; -// 발행여부 필터 옵션 -export const ISSUANCE_FILTER_OPTIONS: { value: IssuanceFilter; label: string }[] = [ +// 세금계산서 수취여부 필터 옵션 +export const TAX_INVOICE_RECEIVED_FILTER_OPTIONS: { value: TaxInvoiceReceivedFilter; label: string }[] = [ { value: 'all', label: '전체' }, - { value: 'taxInvoicePending', label: '세금계산서 미수취' }, + { value: 'received', label: '수취 확인' }, + { value: 'notReceived', label: '수취 미확인' }, ]; // 계정과목명 셀렉터 옵션 (상단 일괄 변경용) diff --git a/src/components/accounting/SalesManagement/SalesDetail.tsx b/src/components/accounting/SalesManagement/SalesDetail.tsx index f4374527..c5fc902a 100644 --- a/src/components/accounting/SalesManagement/SalesDetail.tsx +++ b/src/components/accounting/SalesManagement/SalesDetail.tsx @@ -32,8 +32,7 @@ import { import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { LineItemsTable, useLineItems } from '@/components/organisms/LineItemsTable'; import { salesConfig } from './salesConfig'; -import type { SalesRecord, SalesItem, SalesType } from './types'; -import { SALES_TYPE_OPTIONS } from './types'; +import type { SalesRecord, SalesItem } from './types'; import { getSaleById, createSale, updateSale, deleteSale } from './actions'; import { toast } from 'sonner'; import { getClients } from '../VendorManagement/actions'; @@ -78,7 +77,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) { const [salesDate, setSalesDate] = useState(format(new Date(), 'yyyy-MM-dd')); const [vendorId, setVendorId] = useState(''); const [vendorName, setVendorName] = useState(''); - const [salesType, setSalesType] = useState('product'); const [items, setItems] = useState([createEmptyItem()]); const [taxInvoiceIssued, setTaxInvoiceIssued] = useState(false); const [transactionStatementIssued, setTransactionStatementIssued] = useState(false); @@ -126,7 +124,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) { setSalesDate(data.salesDate); setVendorId(data.vendorId); setVendorName(data.vendorName); - setSalesType(data.salesType); setItems(data.items.length > 0 ? data.items : [createEmptyItem()]); setTaxInvoiceIssued(data.taxInvoiceIssued); setTransactionStatementIssued(data.transactionStatementIssued); @@ -158,7 +155,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) { const saleData: Partial = { salesDate, vendorId, - salesType, items, totalSupplyAmount: totals.supplyAmount, totalVat: totals.vat, @@ -189,7 +185,7 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) { } finally { setIsSaving(false); } - }, [salesDate, vendorId, salesType, items, totals, taxInvoiceIssued, transactionStatementIssued, note, isNewMode, salesId]); + }, [salesDate, vendorId, items, totals, taxInvoiceIssued, transactionStatementIssued, note, isNewMode, salesId]); // ===== 삭제 (IntegratedDetailTemplate 호환) ===== const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => { @@ -268,23 +264,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
- - {/* 매출 유형 */} -
- - -
@@ -318,28 +297,42 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) { 세금계산서 -
-
- - +
+
+
+ + +
+
+ {taxInvoiceIssued ? ( + + + 발행완료 + + ) : ( + + + 미발행 + + )} +
-
- {taxInvoiceIssued ? ( - - - 발행완료 - - ) : ( - - - 미발행 - - )} +
+
diff --git a/src/components/accounting/SalesManagement/index.tsx b/src/components/accounting/SalesManagement/index.tsx index 8b0b90bf..3dd10606 100644 --- a/src/components/accounting/SalesManagement/index.tsx +++ b/src/components/accounting/SalesManagement/index.tsx @@ -25,7 +25,6 @@ import { import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; -import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Dialog, @@ -55,11 +54,8 @@ import { MobileCard } from '@/components/organisms/MobileCard'; import type { SalesRecord } from './types'; import { SORT_OPTIONS, - SALES_STATUS_LABELS, - SALES_STATUS_COLORS, - SALES_TYPE_LABELS, - SALES_TYPE_FILTER_OPTIONS, - ISSUANCE_FILTER_OPTIONS, + TAX_INVOICE_FILTER_OPTIONS, + TRANSACTION_STATEMENT_FILTER_OPTIONS, ACCOUNT_SUBJECT_SELECTOR_OPTIONS, } from './types'; import { getSales, deleteSale, toggleSaleIssuance } from './actions'; @@ -83,7 +79,6 @@ const tableColumns = [ { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true }, { key: 'vat', label: '부가세', className: 'text-right', sortable: true }, { key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true }, - { key: 'salesType', label: '매출유형', className: 'text-center', sortable: true }, { key: 'taxInvoice', label: '세금계산서 발행완료', className: 'text-center' }, { key: 'transactionStatement', label: '거래명세서 발행완료', className: 'text-center' }, ]; @@ -113,8 +108,8 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem // 필터 초기값 (filterConfig 기반 - ULP가 내부 state로 관리) const initialFilterValues: Record = { vendor: 'all', - salesType: 'all', - issuance: 'all', + taxInvoice: 'all', + transactionStatement: 'all', sort: 'latest', }; @@ -148,17 +143,17 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem allOptionLabel: '거래처 전체', }, { - key: 'salesType', - label: '매출유형', + key: 'taxInvoice', + label: '세금계산서 발행여부', type: 'single', - options: SALES_TYPE_FILTER_OPTIONS.filter(o => o.value !== 'all'), + options: TAX_INVOICE_FILTER_OPTIONS.filter(o => o.value !== 'all'), allOptionLabel: '전체', }, { - key: 'issuance', - label: '발행여부', + key: 'transactionStatement', + label: '거래명세서 발행여부', type: 'single', - options: ISSUANCE_FILTER_OPTIONS.filter(o => o.value !== 'all'), + options: TRANSACTION_STATEMENT_FILTER_OPTIONS.filter(o => o.value !== 'all'), allOptionLabel: '전체', }, { @@ -322,18 +317,24 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem // 검색은 searchFilter에서 처리하므로 여기서는 필터만 처리 customFilterFn: (items, fv) => { if (!items || items.length === 0) return items; - const issuanceVal = fv.issuance as string; + const taxInvoiceVal = fv.taxInvoice as string; + const transactionStatementVal = fv.transactionStatement as string; let result = applyFilters(items, [ enumFilter('vendorName', fv.vendor as string), - enumFilter('salesType', fv.salesType as string), ]); - // 발행여부 필터 (특수 로직 - enumFilter로 대체 불가) - if (issuanceVal === 'taxInvoicePending') { + // 세금계산서 발행여부 필터 + if (taxInvoiceVal === 'issued') { + result = result.filter(item => item.taxInvoiceIssued); + } else if (taxInvoiceVal === 'notIssued') { result = result.filter(item => !item.taxInvoiceIssued); } - if (issuanceVal === 'transactionStatementPending') { + + // 거래명세서 발행여부 필터 + if (transactionStatementVal === 'issued') { + result = result.filter(item => item.transactionStatementIssued); + } else if (transactionStatementVal === 'notIssued') { result = result.filter(item => !item.transactionStatementIssued); } @@ -411,7 +412,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem {formatNumber(tableTotals.totalAmount)} - ), @@ -443,9 +443,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem {formatNumber(item.totalSupplyAmount)} {formatNumber(item.totalVat)} {formatNumber(item.totalAmount)} - - {SALES_TYPE_LABELS[item.salesType]} - e.stopPropagation()}>
handleRowClick(item)} diff --git a/src/components/accounting/SalesManagement/types.ts b/src/components/accounting/SalesManagement/types.ts index 89109a39..77c1fa41 100644 --- a/src/components/accounting/SalesManagement/types.ts +++ b/src/components/accounting/SalesManagement/types.ts @@ -133,13 +133,22 @@ export const ACCOUNT_SUBJECT_SELECTOR_OPTIONS = [ { value: 'other', label: '기타매출' }, ]; -// ===== 발행여부 필터 ===== -export type IssuanceFilter = 'all' | 'taxInvoicePending' | 'transactionStatementPending'; +// ===== 세금계산서 발행여부 필터 ===== +export type TaxInvoiceFilter = 'all' | 'issued' | 'notIssued'; -export const ISSUANCE_FILTER_OPTIONS: { value: IssuanceFilter; label: string }[] = [ +export const TAX_INVOICE_FILTER_OPTIONS: { value: TaxInvoiceFilter; label: string }[] = [ { value: 'all', label: '전체' }, - { value: 'taxInvoicePending', label: '세금계산서 미발행' }, - { value: 'transactionStatementPending', label: '거래명세서 미발행' }, + { value: 'issued', label: '발행완료' }, + { value: 'notIssued', label: '미발행' }, +]; + +// ===== 거래명세서 발행여부 필터 ===== +export type TransactionStatementFilter = 'all' | 'issued' | 'notIssued'; + +export const TRANSACTION_STATEMENT_FILTER_OPTIONS: { value: TransactionStatementFilter; label: string }[] = [ + { value: 'all', label: '전체' }, + { value: 'issued', label: '발행완료' }, + { value: 'notIssued', label: '미발행' }, ]; // ===== 매출유형 필터 옵션 (스크린샷 기준) =====