diff --git a/CLAUDE.md b/CLAUDE.md index bfc3edcf..d963b583 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -326,16 +326,19 @@ const [data, setData] = useState(() => { --- -## Backend API Analysis Policy +## Backend API Policy **Priority**: ๐ŸŸก -- Backend API ์ฝ”๋“œ๋Š” **๋ถ„์„๋งŒ**, ์ง์ ‘ ์ˆ˜์ • ์•ˆ ํ•จ -- ์ˆ˜์ • ํ•„์š” ์‹œ ๋ฐฑ์—”๋“œ ์š”์ฒญ ๋ฌธ์„œ๋กœ ์ •๋ฆฌ: +- **์‹ ๊ทœ API ์ƒ์„ฑ ๊ธˆ์ง€**: ์ƒˆ๋กœ์šด ์—”๋“œํฌ์ธํŠธ/์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ์€ ์ง์ ‘ ํ•˜์ง€ ์•Š์Œ โ†’ ์š”์ฒญ ๋ฌธ์„œ๋กœ ์ •๋ฆฌ +- **๊ธฐ์กด API ์ˆ˜์ •/์ถ”๊ฐ€ ๊ฐ€๋Šฅ**: ์ด๋ฏธ ์กด์žฌํ•˜๋Š” API์˜ ์ˆ˜์ •, ํ•„๋“œ ์ถ”๊ฐ€, ๋กœ์ง ๋ณ€๊ฒฝ์€ ์ง์ ‘ ์ˆ˜ํ–‰ ๊ฐ€๋Šฅ +- ๋ฐฑ์—”๋“œ ๊ฒฝ๋กœ: `sam_project/sam-api/sam-api` (PHP Laravel) +- ์ˆ˜์ • ์‹œ ๊ธฐ์กด ์ฝ”๋“œ ํŒจํ„ด(Service-First, ๊ธฐ์กด ์‘๋‹ต ๊ตฌ์กฐ) ์ค€์ˆ˜ +- ์‹ ๊ทœ API๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์š”์ฒญ ๋ฌธ์„œ๋กœ ์ •๋ฆฌ: ```markdown -## ๋ฐฑ์—”๋“œ API ์ˆ˜์ • ์š”์ฒญ -### ํŒŒ์ผ ์œ„์น˜: `/path/to/file.php` - ๋ฉ”์„œ๋“œ๋ช… (Line XX-XX) -### ํ˜„์žฌ ๋ฌธ์ œ: [์„ค๋ช…] -### ์ˆ˜์ • ์š”์ฒญ: [๋‚ด์šฉ] +## ๋ฐฑ์—”๋“œ API ์‹ ๊ทœ ์š”์ฒญ +### ์—”๋“œํฌ์ธํŠธ: [HTTP METHOD /api/v1/path] +### ๋ชฉ์ : [์„ค๋ช…] +### ์š”์ฒญ/์‘๋‹ต ๊ตฌ์กฐ: [๋‚ด์šฉ] ``` --- diff --git a/src/components/accounting/BankTransactionInquiry/index.tsx b/src/components/accounting/BankTransactionInquiry/index.tsx index d58d9c7d..10eddf10 100644 --- a/src/components/accounting/BankTransactionInquiry/index.tsx +++ b/src/components/accounting/BankTransactionInquiry/index.tsx @@ -51,12 +51,27 @@ import { getBankAccountOptions, getFinancialInstitutions, batchSaveTransactions, - exportBankTransactionsExcel, type BankTransactionSummaryData, } from './actions'; import { TransactionFormModal } from './TransactionFormModal'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { formatNumber } from '@/lib/utils/amount'; +import { downloadExcel, type ExcelColumn } from '@/lib/utils/excel-download'; + +// ===== ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์ปฌ๋Ÿผ ===== +const excelColumns: ExcelColumn>[] = [ + { header: '๊ฑฐ๋ž˜์ผ์‹œ', key: 'transactionDate', width: 12 }, + { header: '๊ตฌ๋ถ„', key: 'type', width: 8, + transform: (v) => v === 'deposit' ? '์ž…๊ธˆ' : '์ถœ๊ธˆ' }, + { header: '์€ํ–‰๋ช…', key: 'bankName', width: 12 }, + { header: '๊ณ„์ขŒ๋ช…', key: 'accountName', width: 15 }, + { header: '์ ์š”/๋‚ด์šฉ', key: 'note', width: 20 }, + { header: '์ž…๊ธˆ', key: 'depositAmount', width: 14 }, + { header: '์ถœ๊ธˆ', key: 'withdrawalAmount', width: 14 }, + { header: '์ž”์•ก', key: 'balance', width: 14 }, + { header: '์ทจ๊ธ‰์ ', key: 'branch', width: 12 }, + { header: '์ƒ๋Œ€๊ณ„์ขŒ์˜ˆ๊ธˆ์ฃผ๋ช…', key: 'depositorName', width: 18 }, +]; // ===== ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •์˜ (์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ 10๊ฐœ) ===== const tableColumns = [ @@ -226,22 +241,45 @@ export function BankTransactionInquiry() { } }, [localChanges, loadData]); - // ์—‘์…€ ๋‹ค์šด๋กœ๋“œ + // ์—‘์…€ ๋‹ค์šด๋กœ๋“œ (ํ”„๋ก ํŠธ xlsx ์ƒ์„ฑ) const handleExcelDownload = useCallback(async () => { try { - const result = await exportBankTransactionsExcel({ - startDate, - endDate, - accountCategory: accountCategoryFilter, - financialInstitution: financialInstitutionFilter, - }); - if (result.success && result.data) { - window.open(result.data.downloadUrl, '_blank'); + toast.info('์—‘์…€ ํŒŒ์ผ ์ƒ์„ฑ ์ค‘...'); + const allData: BankTransaction[] = []; + let page = 1; + let lastPage = 1; + + do { + const result = await getBankTransactionList({ + startDate, + endDate, + accountCategory: accountCategoryFilter, + financialInstitution: financialInstitutionFilter, + perPage: 100, + page, + }); + if (result.success && result.data.length > 0) { + allData.push(...result.data); + lastPage = result.pagination?.lastPage ?? 1; + } else { + break; + } + page++; + } while (page <= lastPage); + + if (allData.length > 0) { + await downloadExcel({ + data: allData as (BankTransaction & Record)[], + columns: excelColumns, + filename: '๊ณ„์ขŒ์ž…์ถœ๊ธˆ๋‚ด์—ญ', + sheetName: '์ž…์ถœ๊ธˆ๋‚ด์—ญ', + }); + toast.success('์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ'); } else { - toast.error(result.error || '์—‘์…€ ๋‹ค์šด๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + toast.warning('๋‹ค์šด๋กœ๋“œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'); } } catch { - toast.error('์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + toast.error('์—‘์…€ ๋‹ค์šด๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); } }, [startDate, endDate, accountCategoryFilter, financialInstitutionFilter]); diff --git a/src/components/accounting/CardTransactionInquiry/index.tsx b/src/components/accounting/CardTransactionInquiry/index.tsx index 84e4e146..6357ef6d 100644 --- a/src/components/accounting/CardTransactionInquiry/index.tsx +++ b/src/components/accounting/CardTransactionInquiry/index.tsx @@ -55,6 +55,29 @@ import { JournalEntryModal } from './JournalEntryModal'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { formatNumber } from '@/lib/utils/amount'; import { filterByEnum } from '@/lib/utils/search'; +import { downloadExcel, type ExcelColumn } from '@/lib/utils/excel-download'; + +// ===== ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์ปฌ๋Ÿผ ===== +const excelColumns: ExcelColumn[] = [ + { header: '์‚ฌ์šฉ์ผ์‹œ', key: 'usedAt', width: 18 }, + { header: '์นด๋“œ์‚ฌ', key: 'cardCompany', width: 10 }, + { header: '์นด๋“œ๋ฒˆํ˜ธ', key: 'card', width: 12 }, + { header: '์นด๋“œ๋ช…', key: 'cardName', width: 12 }, + { header: '๊ณต์ œ', key: 'deductionType', width: 10, + transform: (v) => v === 'deductible' ? '๊ณต์ œ' : '๋ถˆ๊ณต์ œ' }, + { header: '์‚ฌ์—…์ž๋ฒˆํ˜ธ', key: 'businessNumber', width: 15 }, + { header: '๊ฐ€๋งน์ ๋ช…', key: 'merchantName', width: 15 }, + { header: '์ฆ๋น™/ํŒ๋งค์ž์ƒํ˜ธ', key: 'vendorName', width: 18 }, + { header: '๋‚ด์—ญ', key: 'description', width: 15 }, + { header: 'ํ•ฉ๊ณ„๊ธˆ์•ก', key: 'totalAmount', width: 12 }, + { header: '๊ณต๊ธ‰๊ฐ€์•ก', key: 'supplyAmount', width: 12 }, + { header: '์„ธ์•ก', key: 'taxAmount', width: 10 }, + { header: '๊ณ„์ •๊ณผ๋ชฉ', key: 'accountSubject', width: 12, + transform: (v) => { + const found = ACCOUNT_SUBJECT_OPTIONS.find(o => o.value === v); + return found?.label || String(v || ''); + }}, +]; // ===== ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •์˜ (์ฒดํฌ๋ฐ•์Šค/No. ์ œ์™ธ 15๊ฐœ) ===== const tableColumns = [ @@ -269,9 +292,45 @@ export function CardTransactionInquiry() { setShowJournalEntry(true); }, []); - const handleExcelDownload = useCallback(() => { - toast.info('์—‘์…€ ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ์€ ๋ฐฑ์—”๋“œ ์—ฐ๋™ ํ›„ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.'); - }, []); + const handleExcelDownload = useCallback(async () => { + try { + toast.info('์—‘์…€ ํŒŒ์ผ ์ƒ์„ฑ ์ค‘...'); + const allData: CardTransaction[] = []; + let page = 1; + let lastPage = 1; + + do { + const result = await getCardTransactionList({ + startDate, + endDate, + search: searchQuery || undefined, + perPage: 100, + page, + }); + if (result.success && result.data.length > 0) { + allData.push(...result.data); + lastPage = result.pagination?.lastPage ?? 1; + } else { + break; + } + page++; + } while (page <= lastPage); + + if (allData.length > 0) { + await downloadExcel>({ + data: allData as (CardTransaction & Record)[], + columns: excelColumns as ExcelColumn>[], + filename: '์นด๋“œ์‚ฌ์šฉ๋‚ด์—ญ', + sheetName: '์นด๋“œ์‚ฌ์šฉ๋‚ด์—ญ', + }); + toast.success('์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ'); + } else { + toast.warning('๋‹ค์šด๋กœ๋“œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'); + } + } catch { + toast.error('์—‘์…€ ๋‹ค์šด๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + }, [startDate, endDate, searchQuery]); // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( diff --git a/src/components/accounting/ReceivablesStatus/index.tsx b/src/components/accounting/ReceivablesStatus/index.tsx index 347a2529..a79736cb 100644 --- a/src/components/accounting/ReceivablesStatus/index.tsx +++ b/src/components/accounting/ReceivablesStatus/index.tsx @@ -32,9 +32,10 @@ import { CATEGORY_LABELS, SORT_OPTIONS, } from './types'; -import { getReceivablesList, getReceivablesSummary, updateOverdueStatus, updateMemos, exportReceivablesExcel } from './actions'; +import { getReceivablesList, getReceivablesSummary, updateOverdueStatus, updateMemos } from './actions'; import { toast } from 'sonner'; import { filterByText } from '@/lib/utils/search'; +import { downloadExcel, type ExcelColumn } from '@/lib/utils/excel-download'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { usePermission } from '@/hooks/usePermission'; @@ -213,27 +214,45 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma }); }, []); - // ===== ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ํ•ธ๋“ค๋Ÿฌ ===== + // ===== ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ํ•ธ๋“ค๋Ÿฌ (ํ”„๋ก ํŠธ xlsx ์ƒ์„ฑ) ===== const handleExcelDownload = useCallback(async () => { - const result = await exportReceivablesExcel({ - year: selectedYear, - search: searchQuery || undefined, - }); - - if (result.success && result.data) { - const url = URL.createObjectURL(result.data); - const a = document.createElement('a'); - a.href = url; - a.download = result.filename || '์ฑ„๊ถŒํ˜„ํ™ฉ.xlsx'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - toast.success('์—‘์…€ ํŒŒ์ผ์ด ๋‹ค์šด๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); - } else { - toast.error(result.error || '์—‘์…€ ๋‹ค์šด๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + try { + toast.info('์—‘์…€ ํŒŒ์ผ ์ƒ์„ฑ ์ค‘...'); + // ๋ฐ์ดํ„ฐ๊ฐ€ ์ด๋ฏธ ๋กœ๋“œ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ sortedData ์‚ฌ์šฉ + if (sortedData.length === 0) { + toast.warning('๋‹ค์šด๋กœ๋“œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'); + return; + } + // ๋™์  ์›” ์ปฌ๋Ÿผ ํฌํ•จ ์—‘์…€ ์ปฌ๋Ÿผ ์ƒ์„ฑ + const columns: ExcelColumn>[] = [ + { header: '๊ฑฐ๋ž˜์ฒ˜', key: 'vendorName', width: 20 }, + { header: '์—ฐ์ฒด', key: 'isOverdue', width: 8 }, + ...monthLabels.map((label, idx) => ({ + header: label, key: `month_${idx}`, width: 12, + })), + { header: 'ํ•ฉ๊ณ„', key: 'total', width: 14 }, + { header: '๋ฉ”๋ชจ', key: 'memo', width: 20 }, + ]; + // ๋ฏธ์ˆ˜๊ธˆ ์นดํ…Œ๊ณ ๋ฆฌ ๊ธฐ์ค€์œผ๋กœ ํ”Œ๋žซ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ + const exportData = sortedData.map(vendor => { + const receivable = vendor.categories.find(c => c.category === 'receivable'); + const row: Record = { + vendorName: vendor.vendorName, + isOverdue: vendor.isOverdue ? '์—ฐ์ฒด' : '', + }; + monthLabels.forEach((_, idx) => { + row[`month_${idx}`] = receivable?.amounts.values[idx] || 0; + }); + row.total = receivable?.amounts.total || 0; + row.memo = vendor.memo || ''; + return row; + }); + await downloadExcel({ data: exportData, columns, filename: '๋ฏธ์ˆ˜๊ธˆํ˜„ํ™ฉ', sheetName: '๋ฏธ์ˆ˜๊ธˆํ˜„ํ™ฉ' }); + toast.success('์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ'); + } catch { + toast.error('์—‘์…€ ๋‹ค์šด๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); } - }, [selectedYear, searchQuery]); + }, [sortedData, monthLabels]); // ===== ๋ณ€๊ฒฝ๋œ ์—ฐ์ฒด ํ•ญ๋ชฉ ํ™•์ธ ===== const changedOverdueItems = useMemo(() => { diff --git a/src/components/accounting/TaxInvoiceManagement/index.tsx b/src/components/accounting/TaxInvoiceManagement/index.tsx index ec534423..f64a2ca3 100644 --- a/src/components/accounting/TaxInvoiceManagement/index.tsx +++ b/src/components/accounting/TaxInvoiceManagement/index.tsx @@ -45,8 +45,8 @@ import { MobileCard } from '@/components/organisms/MobileCard'; import { getTaxInvoices, getTaxInvoiceSummary, - downloadTaxInvoiceExcel, } from './actions'; +import { downloadExcel, type ExcelColumn } from '@/lib/utils/excel-download'; const ManualEntryModal = dynamic( () => import('./ManualEntryModal').then(mod => ({ default: mod.ManualEntryModal })), @@ -58,6 +58,10 @@ import type { TaxInvoiceMgmtRecord, InvoiceTab, TaxInvoiceSummary, + TaxType, + ReceiptType, + InvoiceStatus, + InvoiceSource, } from './types'; import { TAB_OPTIONS, @@ -77,6 +81,26 @@ const QUARTER_BUTTONS = [ { value: 'Q4', label: '4๋ถ„๊ธฐ', startMonth: 10, endMonth: 12 }, ]; +// ===== ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์ปฌ๋Ÿผ ===== +const excelColumns: ExcelColumn>[] = [ + { header: '์ž‘์„ฑ์ผ์ž', key: 'writeDate', width: 12 }, + { header: '๋ฐœ๊ธ‰์ผ์ž', key: 'issueDate', width: 12 }, + { header: '๊ฑฐ๋ž˜์ฒ˜', key: 'vendorName', width: 20 }, + { header: '์‚ฌ์—…์ž๋ฒˆํ˜ธ', key: 'vendorBusinessNumber', width: 15 }, + { header: '๊ณผ์„ธํ˜•ํƒœ', key: 'taxType', width: 10, + transform: (v) => TAX_TYPE_LABELS[v as TaxType] || String(v || '') }, + { header: 'ํ’ˆ๋ชฉ', key: 'itemName', width: 15 }, + { header: '๊ณต๊ธ‰๊ฐ€์•ก', key: 'supplyAmount', width: 14 }, + { header: '์„ธ์•ก', key: 'taxAmount', width: 14 }, + { header: 'ํ•ฉ๊ณ„', key: 'totalAmount', width: 14 }, + { header: '์˜์ˆ˜์ฒญ๊ตฌ', key: 'receiptType', width: 10, + transform: (v) => RECEIPT_TYPE_LABELS[v as ReceiptType] || String(v || '') }, + { header: '์ƒํƒœ', key: 'status', width: 10, + transform: (v) => INVOICE_STATUS_MAP[v as InvoiceStatus]?.label || String(v || '') }, + { header: '๋ฐœ๊ธ‰ํ˜•ํƒœ', key: 'source', width: 10, + transform: (v) => INVOICE_SOURCE_LABELS[v as InvoiceSource] || String(v || '') }, +]; + // ===== ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ===== const tableColumns = [ { key: 'writeDate', label: '์ž‘์„ฑ์ผ์ž', className: 'text-center', sortable: true }, @@ -224,19 +248,46 @@ export function TaxInvoiceManagement() { loadData(); }, [loadData]); - // ===== ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ===== + // ===== ์—‘์…€ ๋‹ค์šด๋กœ๋“œ (ํ”„๋ก ํŠธ xlsx ์ƒ์„ฑ) ===== const handleExcelDownload = useCallback(async () => { - const result = await downloadTaxInvoiceExcel({ - division: activeTab, - dateType, - startDate, - endDate, - vendorSearch, - }); - if (result.success && result.data) { - window.open(result.data.url, '_blank'); - } else { - toast.error(result.error || '์—‘์…€ ๋‹ค์šด๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + try { + toast.info('์—‘์…€ ํŒŒ์ผ ์ƒ์„ฑ ์ค‘...'); + const allData: TaxInvoiceMgmtRecord[] = []; + let page = 1; + let lastPage = 1; + + do { + const result = await getTaxInvoices({ + division: activeTab, + dateType, + startDate, + endDate, + vendorSearch, + page, + perPage: 100, + }); + if (result.success && result.data.length > 0) { + allData.push(...result.data); + lastPage = result.pagination?.lastPage ?? 1; + } else { + break; + } + page++; + } while (page <= lastPage); + + if (allData.length > 0) { + await downloadExcel({ + data: allData as (TaxInvoiceMgmtRecord & Record)[], + columns: excelColumns, + filename: `์„ธ๊ธˆ๊ณ„์‚ฐ์„œ_${activeTab === 'sales' ? '๋งค์ถœ' : '๋งค์ž…'}`, + sheetName: activeTab === 'sales' ? '๋งค์ถœ' : '๋งค์ž…', + }); + toast.success('์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ'); + } else { + toast.warning('๋‹ค์šด๋กœ๋“œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'); + } + } catch { + toast.error('์—‘์…€ ๋‹ค์šด๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); } }, [activeTab, dateType, startDate, endDate, vendorSearch]); diff --git a/src/components/accounting/VendorLedger/index.tsx b/src/components/accounting/VendorLedger/index.tsx index 629c986e..fcf1ba23 100644 --- a/src/components/accounting/VendorLedger/index.tsx +++ b/src/components/accounting/VendorLedger/index.tsx @@ -26,8 +26,9 @@ import { type StatCard, } from '@/components/templates/UniversalListPage'; import type { VendorLedgerItem, VendorLedgerSummary } from './types'; -import { getVendorLedgerList, getVendorLedgerSummary, exportVendorLedgerExcel } from './actions'; +import { getVendorLedgerList, getVendorLedgerSummary } from './actions'; import { formatNumber } from '@/lib/utils/amount'; +import { downloadExcel, type ExcelColumn } from '@/lib/utils/excel-download'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { usePermission } from '@/hooks/usePermission'; @@ -43,6 +44,16 @@ const tableColumns = [ { key: 'paymentDate', label: '๊ฒฐ์ œ์ผ', className: 'text-center w-[100px]', sortable: true }, ]; +// ===== ์—‘์…€ ์ปฌ๋Ÿผ ์ •์˜ ===== +const excelColumns: ExcelColumn>[] = [ + { header: '๊ฑฐ๋ž˜์ฒ˜๋ช…', key: 'vendorName', width: 20 }, + { header: '์ด์›”์ž”์•ก', key: 'carryoverBalance', width: 14 }, + { header: '๋งค์ถœ', key: 'sales', width: 14 }, + { header: '์ˆ˜๊ธˆ', key: 'collection', width: 14 }, + { header: '์ž”์•ก', key: 'balance', width: 14 }, + { header: '๊ฒฐ์ œ์ผ', key: 'paymentDate', width: 12 }, +]; + // ===== Props ===== interface VendorLedgerProps { initialData?: VendorLedgerItem[]; @@ -144,24 +155,42 @@ export function VendorLedger({ ); const handleExcelDownload = useCallback(async () => { - const result = await exportVendorLedgerExcel({ - startDate, - endDate, - search: searchQuery || undefined, - }); + try { + toast.info('์—‘์…€ ํŒŒ์ผ ์ƒ์„ฑ ์ค‘...'); + const allData: VendorLedgerItem[] = []; + let page = 1; + let lastPage = 1; - if (result.success && result.data) { - const url = URL.createObjectURL(result.data); - const a = document.createElement('a'); - a.href = url; - a.download = result.filename || '๊ฑฐ๋ž˜์ฒ˜์›์žฅ.xlsx'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - toast.success('์—‘์…€ ํŒŒ์ผ์ด ๋‹ค์šด๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); - } else { - toast.error(result.error || '์—‘์…€ ๋‹ค์šด๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + do { + const result = await getVendorLedgerList({ + startDate, + endDate, + search: searchQuery || undefined, + perPage: 100, + page, + }); + if (result.success && result.data.length > 0) { + allData.push(...result.data); + lastPage = result.pagination?.lastPage ?? 1; + } else { + break; + } + page++; + } while (page <= lastPage); + + if (allData.length > 0) { + await downloadExcel>({ + data: allData as (VendorLedgerItem & Record)[], + columns: excelColumns, + filename: '๊ฑฐ๋ž˜์ฒ˜์›์žฅ', + sheetName: '๊ฑฐ๋ž˜์ฒ˜์›์žฅ', + }); + toast.success('์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ'); + } else { + toast.warning('๋‹ค์šด๋กœ๋“œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'); + } + } catch { + toast.error('์—‘์…€ ๋‹ค์šด๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); } }, [startDate, endDate, searchQuery]); diff --git a/src/components/approval/ApprovalBox/index.tsx b/src/components/approval/ApprovalBox/index.tsx index 02d8f50c..1ff38345 100644 --- a/src/components/approval/ApprovalBox/index.tsx +++ b/src/components/approval/ApprovalBox/index.tsx @@ -546,7 +546,7 @@ export function ApprovalBox() { dateRangeSelector: { enabled: true, - showPresets: false, + showPresets: true, startDate, endDate, onStartDateChange: setStartDate, diff --git a/src/components/business/CEODashboard/CEODashboard.tsx b/src/components/business/CEODashboard/CEODashboard.tsx index f83320b1..4f8da43c 100644 --- a/src/components/business/CEODashboard/CEODashboard.tsx +++ b/src/components/business/CEODashboard/CEODashboard.tsx @@ -34,15 +34,17 @@ import { ScheduleDetailModal, DetailModal } from './modals'; import { DashboardSettingsDialog } from './dialogs/DashboardSettingsDialog'; import { LazySection } from './LazySection'; import { EmptySection } from './components'; -import { useCEODashboard, useTodayIssue, useCalendar, useVat, useEntertainment, useWelfare, useWelfareDetail, useMonthlyExpenseDetail, type MonthlyExpenseCardId } from '@/hooks/useCEODashboard'; +import { useCEODashboard, useTodayIssue, useCalendar, useVat, useEntertainment, useEntertainmentDetail, useWelfare, useWelfareDetail, useVatDetail, useMonthlyExpenseDetail, type MonthlyExpenseCardId } from '@/hooks/useCEODashboard'; import { useCardManagementModals } from '@/hooks/useCardManagementModals'; import { getMonthlyExpenseModalConfig, getCardManagementModalConfig, + getCardManagementModalConfigWithData, getEntertainmentModalConfig, getWelfareModalConfig, getVatModalConfig, } from './modalConfigs'; +import { transformEntertainmentDetailResponse, transformWelfareDetailResponse, transformVatDetailResponse } from '@/lib/api/dashboard/transformers'; export function CEODashboard() { const router = useRouter(); @@ -138,11 +140,17 @@ export function CEODashboard() { const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); const [dashboardSettings, setDashboardSettings] = useState(DEFAULT_DASHBOARD_SETTINGS); + // EntertainmentDetail Hook (๋ชจ๋‹ฌ์šฉ ์ƒ์„ธ API) - dashboardSettings ์ดํ›„์— ์„ ์–ธ + const entertainmentDetailData = useEntertainmentDetail(); + // WelfareDetail Hook (๋ชจ๋‹ฌ์šฉ ์ƒ์„ธ API) - dashboardSettings ์ดํ›„์— ์„ ์–ธ const welfareDetailData = useWelfareDetail({ calculationType: dashboardSettings.welfare.calculationType, }); + // VatDetail Hook (๋ถ€๊ฐ€์„ธ ์ƒ์„ธ ๋ชจ๋‹ฌ์šฉ API) + const vatDetailData = useVatDetail(); + // MonthlyExpenseDetail Hook (๋‹น์›” ์˜ˆ์ƒ ์ง€์ถœ ๋ชจ๋‹ฌ์šฉ ์ƒ์„ธ API) const monthlyExpenseDetailData = useMonthlyExpenseDetail(); @@ -231,9 +239,66 @@ export function CEODashboard() { } }, [monthlyExpenseDetailData]); - // ๋‹น์›” ์˜ˆ์ƒ ์ง€์ถœ ๋ชจ๋‹ฌ ๋‚ ์งœ/๊ฒ€์ƒ‰ ํ•„ํ„ฐ ๋ณ€๊ฒฝ โ†’ ์žฌ์กฐํšŒ + // ๋ชจ๋‹ฌ ๋‚ ์งœ/๊ฒ€์ƒ‰ ํ•„ํ„ฐ ๋ณ€๊ฒฝ โ†’ ์žฌ์กฐํšŒ (๋‹น์›” ์˜ˆ์ƒ ์ง€์ถœ + ๊ฐ€์ง€๊ธ‰๊ธˆ + ์ ‘๋Œ€๋น„ ์ƒ์„ธ) const handleDateFilterChange = useCallback(async (params: { startDate: string; endDate: string; search: string }) => { if (!currentModalCardId) return; + + // cm2: ๊ฐ€์ง€๊ธ‰๊ธˆ ์ƒ์„ธ ๋ชจ๋‹ฌ ๋‚ ์งœ ํ•„ํ„ฐ + if (currentModalCardId === 'cm2') { + try { + const modalData = await cardManagementModals.fetchModalData('cm2', { + start_date: params.startDate, + end_date: params.endDate, + }); + const config = getCardManagementModalConfigWithData('cm2', modalData); + if (config) { + setDetailModalConfig(config); + } + } catch { + // ์‹คํŒจ ์‹œ ๊ธฐ์กด config ์œ ์ง€ + } + return; + } + + // ๋ณต๋ฆฌํ›„์ƒ๋น„ ์ƒ์„ธ ๋ชจ๋‹ฌ ๋‚ ์งœ ํ•„ํ„ฐ + if (currentModalCardId === 'welfare_detail') { + try { + const response = await fetch( + `/api/proxy/welfare/detail?calculation_type=${dashboardSettings.welfare.calculationType}&start_date=${params.startDate}&end_date=${params.endDate}`, + ); + if (response.ok) { + const result = await response.json(); + if (result.success) { + const config = transformWelfareDetailResponse(result.data); + setDetailModalConfig(config); + } + } + } catch { + // ์‹คํŒจ ์‹œ ๊ธฐ์กด config ์œ ์ง€ + } + return; + } + + // ์ ‘๋Œ€๋น„ ์ƒ์„ธ ๋ชจ๋‹ฌ ๋‚ ์งœ ํ•„ํ„ฐ + if (currentModalCardId === 'entertainment_detail') { + try { + const response = await fetch( + `/api/proxy/entertainment/detail?company_type=${dashboardSettings.entertainment.companyType}&start_date=${params.startDate}&end_date=${params.endDate}`, + ); + if (response.ok) { + const result = await response.json(); + if (result.success) { + const config = transformEntertainmentDetailResponse(result.data); + setDetailModalConfig(config); + } + } + } catch { + // ์‹คํŒจ ์‹œ ๊ธฐ์กด config ์œ ์ง€ + } + return; + } + + // ๋‹น์›” ์˜ˆ์ƒ ์ง€์ถœ ๋ชจ๋‹ฌ ๋‚ ์งœ ํ•„ํ„ฐ const config = await monthlyExpenseDetailData.fetchData( currentModalCardId as MonthlyExpenseCardId, params, @@ -241,7 +306,7 @@ export function CEODashboard() { if (config) { setDetailModalConfig(config); } - }, [currentModalCardId, monthlyExpenseDetailData]); + }, [currentModalCardId, monthlyExpenseDetailData, cardManagementModals, dashboardSettings.entertainment, dashboardSettings.welfare]); // ๋‹น์›” ์˜ˆ์ƒ ์ง€์ถœ ํด๋ฆญ (deprecated - ๊ฐœ๋ณ„ ์นด๋“œ ํด๋ฆญ์œผ๋กœ ๋Œ€์ฒด) const handleMonthlyExpenseClick = useCallback(() => { @@ -249,41 +314,96 @@ export function CEODashboard() { // ์นด๋“œ/๊ฐ€์ง€๊ธ‰๊ธˆ ๊ด€๋ฆฌ ์นด๋“œ ํด๋ฆญ โ†’ ๋ชจ๋‘ ๊ฐ€์ง€๊ธ‰๊ธˆ ์ƒ์„ธ(cm2) ๋ชจ๋‹ฌ // ๊ธฐํš์„œ P52: ์นด๋“œ, ๊ฒฝ์กฐ์‚ฌ, ์ƒํ’ˆ๊ถŒ, ์ ‘๋Œ€๋น„, ์ดํ•ฉ๊ณ„ ๋ชจ๋‘ ๋™์ผํ•œ ๊ฐ€์ง€๊ธ‰๊ธˆ ์ƒ์„ธ ๋ชจ๋‹ฌ - const handleCardManagementCardClick = useCallback((cardId: string) => { - const config = getCardManagementModalConfig('cm2'); - if (config) { - setDetailModalConfig(config); - setIsDetailModalOpen(true); + const handleCardManagementCardClick = useCallback(async (cardId: string) => { + try { + const modalData = await cardManagementModals.fetchModalData('cm2'); + const config = getCardManagementModalConfigWithData('cm2', modalData); + if (config) { + setCurrentModalCardId('cm2'); + setDetailModalConfig(config); + setIsDetailModalOpen(true); + } + } catch { + // API ์‹คํŒจ ์‹œ fallback mock ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ + const config = getCardManagementModalConfig('cm2'); + if (config) { + setCurrentModalCardId('cm2'); + setDetailModalConfig(config); + setIsDetailModalOpen(true); + } } - }, []); + }, [cardManagementModals]); - // ์ ‘๋Œ€๋น„ ํ˜„ํ™ฉ ์นด๋“œ ํด๋ฆญ (๊ฐœ๋ณ„ ์นด๋“œ ํด๋ฆญ ์‹œ ์ƒ์„ธ ๋ชจ๋‹ฌ) - const handleEntertainmentCardClick = useCallback((cardId: string) => { - const config = getEntertainmentModalConfig(cardId); + // ์ ‘๋Œ€๋น„ ํ˜„ํ™ฉ ์นด๋“œ ํด๋ฆญ - API ๋ฐ์ดํ„ฐ๋กœ ๋ชจ๋‹ฌ ์—ด๊ธฐ (fallback: ์ •์  config) + const handleEntertainmentCardClick = useCallback(async (cardId: string) => { + // et_sales ์นด๋“œ๋Š” ๋ณ„๋„ ์ •์  config ์‚ฌ์šฉ (๋งค์ถœ ์ƒ์„ธ) + if (cardId === 'et_sales') { + const config = getEntertainmentModalConfig(cardId); + if (config) { + setDetailModalConfig(config); + setIsDetailModalOpen(true); + } + return; + } + + // ๋ฆฌ์Šคํฌ ์นด๋“œ โ†’ API์—์„œ ์ƒ์„ธ ๋ฐ์ดํ„ฐ fetch, ๋ฐ˜ํ™˜๊ฐ’ ์ง์ ‘ ์‚ฌ์šฉ + setCurrentModalCardId('entertainment_detail'); + const apiConfig = await entertainmentDetailData.refetch(); + const config = apiConfig ?? getEntertainmentModalConfig(cardId); if (config) { setDetailModalConfig(config); setIsDetailModalOpen(true); } - }, []); + }, [entertainmentDetailData]); // ๋ณต๋ฆฌํ›„์ƒ๋น„ ํ˜„ํ™ฉ ์นด๋“œ ํด๋ฆญ (๋ชจ๋“  ์นด๋“œ๊ฐ€ ๋™์ผํ•œ ์ƒ์„ธ ๋ชจ๋‹ฌ) // ๋ณต๋ฆฌํ›„์ƒ๋น„ ํด๋ฆญ - API ๋ฐ์ดํ„ฐ๋กœ ๋ชจ๋‹ฌ ์—ด๊ธฐ (fallback: ์ •์  config) const handleWelfareCardClick = useCallback(async () => { - // 1. ๋จผ์ € API์—์„œ ๋ฐ์ดํ„ฐ fetch ์‹œ๋„ - await welfareDetailData.refetch(); - - // 2. API ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ, ์—†์œผ๋ฉด fallback config ์‚ฌ์šฉ - const config = welfareDetailData.modalConfig ?? getWelfareModalConfig(dashboardSettings.welfare.calculationType); + const apiConfig = await welfareDetailData.refetch(); + const config = apiConfig ?? getWelfareModalConfig(dashboardSettings.welfare.calculationType); setDetailModalConfig(config); + setCurrentModalCardId('welfare_detail'); setIsDetailModalOpen(true); }, [welfareDetailData, dashboardSettings.welfare.calculationType]); - // ๋ถ€๊ฐ€์„ธ ํด๋ฆญ (๋ชจ๋“  ์นด๋“œ๊ฐ€ ๋™์ผํ•œ ์ƒ์„ธ ๋ชจ๋‹ฌ) - const handleVatClick = useCallback(() => { - const config = getVatModalConfig(); + // ์‹ ๊ณ ๊ธฐ๊ฐ„ ๋ณ€๊ฒฝ ์‹œ API ์žฌํ˜ธ์ถœ + const handlePeriodChange = useCallback(async (periodValue: string) => { + // periodValue: "2026-quarter-1" โ†’ parse + const parts = periodValue.split('-'); + if (parts.length < 3) return; + const [year, periodType, period] = parts; + try { + const response = await fetch( + `/api/proxy/vat/detail?period_type=${periodType}&year=${year}&period=${period}`, + ); + if (response.ok) { + const result = await response.json(); + if (result.success) { + const config = transformVatDetailResponse(result.data); + // ์ƒˆ config์—๋„ onPeriodChange ์ฝœ๋ฐฑ ์ฃผ์ž… + if (config.periodSelect) { + config.periodSelect.onPeriodChange = handlePeriodChange; + } + setDetailModalConfig(config); + } + } + } catch { + // ์‹คํŒจ ์‹œ ๊ธฐ์กด config ์œ ์ง€ + } + }, []); + + // ๋ถ€๊ฐ€์„ธ ํด๋ฆญ (๋ชจ๋“  ์นด๋“œ๊ฐ€ ๋™์ผํ•œ ์ƒ์„ธ ๋ชจ๋‹ฌ) - API ๋ฐ์ดํ„ฐ๋กœ ์—ด๊ธฐ (fallback: ์ •์  config) + const handleVatClick = useCallback(async () => { + setCurrentModalCardId('vat_detail'); + const apiConfig = await vatDetailData.refetch(); + const config = apiConfig ?? getVatModalConfig(); + // onPeriodChange ์ฝœ๋ฐฑ ์ฃผ์ž… + if (config.periodSelect) { + config.periodSelect.onPeriodChange = handlePeriodChange; + } setDetailModalConfig(config); setIsDetailModalOpen(true); - }, []); + }, [vatDetailData, handlePeriodChange]); // ์บ˜๋ฆฐ๋” ์ผ์ • ํด๋ฆญ (๊ธฐ์กด ์ผ์ • ์ˆ˜์ •) const handleScheduleClick = useCallback((schedule: CalendarScheduleItem) => { @@ -303,8 +423,8 @@ export function CEODashboard() { setSelectedSchedule(null); }, []); - // ์ผ์ • ์ €์žฅ - const handleScheduleSave = useCallback((formData: { + // ์ผ์ • ์ €์žฅ (optimistic update โ€” refetch ์—†์ด ๋กœ์ปฌ ์ƒํƒœ๋งŒ ๊ฐฑ์‹ ) + const handleScheduleSave = useCallback(async (formData: { title: string; department: string; startDate: string; @@ -315,17 +435,114 @@ export function CEODashboard() { color: string; content: string; }) => { - // TODO: API ํ˜ธ์ถœํ•˜์—ฌ ์ผ์ • ์ €์žฅ - setIsScheduleModalOpen(false); - setSelectedSchedule(null); - }, []); + try { + // schedule_ ์ ‘๋‘์‚ฌ์—์„œ ์‹ค์ œ ID ์ถ”์ถœ + const rawId = selectedSchedule?.id; + const numericId = rawId?.startsWith('schedule_') ? rawId.replace('schedule_', '') : null; - // ์ผ์ • ์‚ญ์ œ - const handleScheduleDelete = useCallback((id: string) => { - // TODO: API ํ˜ธ์ถœํ•˜์—ฌ ์ผ์ • ์‚ญ์ œ - setIsScheduleModalOpen(false); - setSelectedSchedule(null); - }, []); + const body = { + title: formData.title, + description: formData.content, + start_date: formData.startDate, + end_date: formData.endDate, + start_time: formData.isAllDay ? null : (formData.startTime || null), + end_time: formData.isAllDay ? null : (formData.endTime || null), + is_all_day: formData.isAllDay, + color: formData.color || null, + }; + + const url = numericId + ? `/api/proxy/calendar/schedules/${numericId}` + : '/api/proxy/calendar/schedules'; + const method = numericId ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!response.ok) throw new Error('Failed to save schedule'); + + // API ์‘๋‹ต์—์„œ ์‹ค์ œ ID ์ถ”์ถœ (์—†์œผ๋ฉด ์ž„์‹œ ID) + let savedId = numericId; + try { + const result = await response.json(); + savedId = result.data?.id?.toString() || numericId || `temp_${Date.now()}`; + } catch { + savedId = numericId || `temp_${Date.now()}`; + } + + const updatedSchedule: CalendarScheduleItem = { + id: `schedule_${savedId}`, + title: formData.title, + startDate: formData.startDate, + endDate: formData.endDate, + startTime: formData.isAllDay ? undefined : formData.startTime, + endTime: formData.isAllDay ? undefined : formData.endTime, + isAllDay: formData.isAllDay, + type: 'schedule', + department: formData.department !== 'all' ? formData.department : undefined, + color: formData.color, + }; + + // Optimistic update: loading ๋ณ€ํ™” ์—†์ด ๋ฐ์ดํ„ฐ๋งŒ ๊ฐฑ์‹  โ†’ ์บ˜๋ฆฐ๋”๋งŒ ๋ฆฌ๋ Œ๋” + calendarData.setData((prev) => { + if (!prev) return { items: [updatedSchedule], totalCount: 1 }; + if (numericId) { + // ์ˆ˜์ •: ๊ธฐ์กด ํ•ญ๋ชฉ ๊ต์ฒด + return { + ...prev, + items: prev.items.map((item) => + item.id === rawId ? updatedSchedule : item + ), + }; + } + // ์‹ ๊ทœ: ์ถ”๊ฐ€ + return { + ...prev, + items: [...prev.items, updatedSchedule], + totalCount: prev.totalCount + 1, + }; + }); + } catch { + // ์—๋Ÿฌ ์‹œ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋กœ ๋™๊ธฐํ™” + calendarData.refetch(); + } finally { + setIsScheduleModalOpen(false); + setSelectedSchedule(null); + } + }, [selectedSchedule, calendarData]); + + // ์ผ์ • ์‚ญ์ œ (optimistic update) + const handleScheduleDelete = useCallback(async (id: string) => { + try { + // schedule_ ์ ‘๋‘์‚ฌ์—์„œ ์‹ค์ œ ID ์ถ”์ถœ + const numericId = id.startsWith('schedule_') ? id.replace('schedule_', '') : id; + + const response = await fetch(`/api/proxy/calendar/schedules/${numericId}`, { + method: 'DELETE', + }); + + if (!response.ok) throw new Error('Failed to delete schedule'); + + // Optimistic update: ์‚ญ์ œ๋œ ํ•ญ๋ชฉ๋งŒ ์ œ๊ฑฐ โ†’ ์บ˜๋ฆฐ๋”๋งŒ ๋ฆฌ๋ Œ๋” + calendarData.setData((prev) => { + if (!prev) return prev; + return { + ...prev, + items: prev.items.filter((item) => item.id !== id), + totalCount: Math.max(0, prev.totalCount - 1), + }; + }); + } catch { + // ์—๋Ÿฌ ์‹œ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋กœ ๋™๊ธฐํ™” + calendarData.refetch(); + } finally { + setIsScheduleModalOpen(false); + setSelectedSchedule(null); + } + }, [calendarData]); // ์„น์…˜ ์ˆœ์„œ const sectionOrder = dashboardSettings.sectionOrder ?? DEFAULT_SECTION_ORDER; @@ -548,13 +765,14 @@ export function CEODashboard() { {sectionOrder.map(renderDashboardSection)} - {/* ์ผ์ • ์ƒ์„ธ ๋ชจ๋‹ฌ */} + {/* ์ผ์ • ์ƒ์„ธ ๋ชจ๋‹ฌ โ€” schedule_ ์ ‘๋‘์‚ฌ๋งŒ ์ˆ˜์ •/์‚ญ์ œ ๊ฐ€๋Šฅ */} {/* ํ•ญ๋ชฉ ์„ค์ • ๋ชจ๋‹ฌ */} diff --git a/src/components/business/CEODashboard/components.tsx b/src/components/business/CEODashboard/components.tsx index ae61ef70..ff59e89c 100644 --- a/src/components/business/CEODashboard/components.tsx +++ b/src/components/business/CEODashboard/components.tsx @@ -319,8 +319,8 @@ export const AmountCardItem = ({
{card.subItems.map((item, idx) => (
- {item.label} - {typeof item.value === 'number' ? formatKoreanAmount(item.value) : item.value} + {item.label} + {typeof item.value === 'number' ? formatKoreanAmount(item.value) : item.value}
))}
diff --git a/src/components/business/CEODashboard/modalConfigs/cardManagementConfigTransformers.ts b/src/components/business/CEODashboard/modalConfigs/cardManagementConfigTransformers.ts index 6c7e688c..4ddae7cb 100644 --- a/src/components/business/CEODashboard/modalConfigs/cardManagementConfigTransformers.ts +++ b/src/components/business/CEODashboard/modalConfigs/cardManagementConfigTransformers.ts @@ -175,21 +175,39 @@ export function transformCm1ModalConfig( // cm2: ๊ฐ€์ง€๊ธ‰๊ธˆ ์ƒ์„ธ ๋ชจ๋‹ฌ ๋ณ€ํ™˜๊ธฐ // ============================================ +/** ์นดํ…Œ๊ณ ๋ฆฌ ํ‚ค โ†’ ํ•œ๊ธ€ ๋ผ๋ฒจ ๋งคํ•‘ + * - category_breakdown ํ‚ค: ์˜๋ฌธ (card, congratulatory, ...) + * - loans[].category: ํ•œ๊ธ€ (์นด๋“œ, ๊ฒฝ์กฐ์‚ฌ, ...) โ€” ๋ฐฑ์—”๋“œ category_label accessor + * ์–‘์ชฝ ๋ชจ๋‘ ๋Œ€์‘ + */ +const CATEGORY_LABELS: Record = { + // ์˜๋ฌธ ํ‚ค (category_breakdown์šฉ) + card: '์นด๋“œ', + congratulatory: '๊ฒฝ์กฐ์‚ฌ', + gift_certificate: '์ƒํ’ˆ๊ถŒ', + entertainment: '์ ‘๋Œ€๋น„', + // ํ•œ๊ธ€ ๊ฐ’ (loans[].category๊ฐ€ ์ด๋ฏธ ํ•œ๊ธ€์ธ ๊ฒฝ์šฐ โ€” ๊ทธ๋Œ€๋กœ ํ†ต๊ณผ) + '์นด๋“œ': '์นด๋“œ', + '๊ฒฝ์กฐ์‚ฌ': '๊ฒฝ์กฐ์‚ฌ', + '์ƒํ’ˆ๊ถŒ': '์ƒํ’ˆ๊ถŒ', + '์ ‘๋Œ€๋น„': '์ ‘๋Œ€๋น„', +}; + /** * ๊ฐ€์ง€๊ธ‰๊ธˆ ๋Œ€์‹œ๋ณด๋“œ API ์‘๋‹ต์„ cm2 ๋ชจ๋‹ฌ ์„ค์ •์œผ๋กœ ๋ณ€ํ™˜ */ export function transformCm2ModalConfig( data: LoanDashboardApiResponse ): DetailModalConfig { - const { summary, items = [] } = data; + const { summary, category_breakdown, loans = [] } = data; - // ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋งคํ•‘ - const tableData = (items || []).map((item) => ({ + // ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋งคํ•‘ (๋ฐฑ์—”๋“œ ํ•„๋“œ๋ช… ๊ธฐ์ค€, ์˜๋ฌธ ํ‚ค โ†’ ํ•œ๊ธ€ ๋ณ€ํ™˜) + const tableData = (loans || []).map((item) => ({ date: item.loan_date, - classification: item.status_label || '์นด๋“œ', - category: '-', + classification: CATEGORY_LABELS[item.category] || item.category || '์นด๋“œ', + category: item.status_label || '-', amount: item.amount, - content: item.description, + response: item.content, })); // ๋ถ„๋ฅ˜ ํ•„ํ„ฐ ์˜ต์…˜ ๋™์  ์ƒ์„ฑ @@ -202,22 +220,42 @@ export function transformCm2ModalConfig( })), ]; + // reviewCards: category_breakdown์—์„œ 4๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ ์นด๋“œ ์ƒ์„ฑ + const reviewCards = category_breakdown + ? { + title: '๊ฐ€์ง€๊ธ‰๊ธˆ ๊ฒ€ํ†  ํ•„์š”', + cards: Object.entries(category_breakdown).map(([key, breakdown]) => ({ + label: CATEGORY_LABELS[key] || key, + amount: breakdown.outstanding_amount, + subLabel: breakdown.unverified_count > 0 + ? `๋ฏธ์ฆ๋น™ ${breakdown.unverified_count}๊ฑด` + : `${breakdown.total_count}๊ฑด`, + })), + } + : undefined; + return { title: '๊ฐ€์ง€๊ธ‰๊ธˆ ์ƒ์„ธ', + dateFilter: { + enabled: true, + defaultPreset: '๋‹น์›”', + showSearch: true, + }, summaryCards: [ { label: '๊ฐ€์ง€๊ธ‰๊ธˆ ํ•ฉ๊ณ„', value: formatKoreanCurrency(summary.total_outstanding) }, { label: '์ธ์ •๋น„์œจ 4.6%', value: summary.recognized_interest, unit: '์›' }, - { label: '๋ฏธ์ •๋ฆฌ/๋ฏธ๋ถ„๋ฅ˜', value: `${summary.pending_count ?? 0}๊ฑด` }, + { label: '๊ฑด์ˆ˜', value: `${summary.outstanding_count ?? 0}๊ฑด` }, ], + reviewCards, table: { - title: '๊ฐ€์ง€๊ธ‰๊ธˆ ๊ด€๋ จ ๋‚ด์—ญ', + title: '๊ฐ€์ง€๊ธ‰๊ธˆ ๋‚ด์—ญ', columns: [ { key: 'no', label: 'No.', align: 'center' }, { key: 'date', label: '๋ฐœ์ƒ์ผ', align: 'center' }, { key: 'classification', label: '๋ถ„๋ฅ˜', align: 'center' }, { key: 'category', label: '๊ตฌ๋ถ„', align: 'center' }, { key: 'amount', label: '๊ธˆ์•ก', align: 'right', format: 'currency' }, - { key: 'content', label: '๋‚ด์šฉ', align: 'left' }, + { key: 'response', label: '๋Œ€์‘', align: 'left' }, ], data: tableData, filters: [ @@ -227,11 +265,12 @@ export function transformCm2ModalConfig( defaultValue: 'all', }, { - key: 'category', + key: 'sortOrder', options: [ - { value: 'all', label: '์ „์ฒด' }, - { value: '์นด๋“œ๋ช…', label: '์นด๋“œ๋ช…' }, - { value: '๊ณ„์ขŒ๋ช…', label: '๊ณ„์ขŒ๋ช…' }, + { value: 'all', label: '์ •๋ ฌ' }, + { value: 'amountDesc', label: '๊ธˆ์•ก ๋†’์€์ˆœ' }, + { value: 'amountAsc', label: '๊ธˆ์•ก ๋‚ฎ์€์ˆœ' }, + { value: 'latest', label: '์ตœ์‹ ์ˆœ' }, ], defaultValue: 'all', }, diff --git a/src/components/business/CEODashboard/modalConfigs/entertainmentConfigs.ts b/src/components/business/CEODashboard/modalConfigs/entertainmentConfigs.ts index 39164a5b..dfb1bfb4 100644 --- a/src/components/business/CEODashboard/modalConfigs/entertainmentConfigs.ts +++ b/src/components/business/CEODashboard/modalConfigs/entertainmentConfigs.ts @@ -224,11 +224,15 @@ export function getEntertainmentModalConfig(cardId: string): DetailModalConfig | totalColumnKey: 'amount', }, }, - // et_limit, et_remaining, et_used๋Š” ๋ชจ๋‘ ๋™์ผํ•œ ์ ‘๋Œ€๋น„ ์ƒ์„ธ ๋ชจ๋‹ฌ + // D1.7 ๋ฆฌ์Šคํฌ๊ฐ์ง€ํ˜• ์นด๋“œ ID โ†’ ์ ‘๋Œ€๋น„ ์ƒ์„ธ ๋ชจ๋‹ฌ + et_weekend: entertainmentDetailConfig, + et_prohibited: entertainmentDetailConfig, + et_high_amount: entertainmentDetailConfig, + et_no_receipt: entertainmentDetailConfig, + // ๋ ˆ๊ฑฐ์‹œ ์นด๋“œ ID (ํ•˜์œ„ ํ˜ธํ™˜) et_limit: entertainmentDetailConfig, et_remaining: entertainmentDetailConfig, et_used: entertainmentDetailConfig, - // ๋Œ€์‹œ๋ณด๋“œ ์นด๋“œ ID (et1~et4) โ†’ ์ ‘๋Œ€๋น„ ์ƒ์„ธ ๋ชจ๋‹ฌ et1: entertainmentDetailConfig, et2: entertainmentDetailConfig, et3: entertainmentDetailConfig, diff --git a/src/components/business/CEODashboard/modals/DetailModalSections.tsx b/src/components/business/CEODashboard/modals/DetailModalSections.tsx index 3015dd6c..d4c3a227 100644 --- a/src/components/business/CEODashboard/modals/DetailModalSections.tsx +++ b/src/components/business/CEODashboard/modals/DetailModalSections.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import { Search as SearchIcon } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { @@ -58,6 +58,16 @@ export const DateFilterSection = ({ config, onFilterChange }: { config: DateFilt return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }); const [searchText, setSearchText] = useState(''); + const isInitialMount = useRef(true); + + // ๋‚ ์งœ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ์กฐํšŒ (๋‹ค๋ฅธ ํŽ˜์ด์ง€์™€ ๋™์ผํ•œ UX) + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + return; + } + onFilterChange?.({ startDate, endDate, search: searchText }); + }, [startDate, endDate]); const handleSearch = useCallback(() => { onFilterChange?.({ startDate, endDate, search: searchText }); @@ -88,11 +98,6 @@ export const DateFilterSection = ({ config, onFilterChange }: { config: DateFilt /> )} - {onFilterChange && ( - - )} } /> @@ -103,10 +108,15 @@ export const DateFilterSection = ({ config, onFilterChange }: { config: DateFilt export const PeriodSelectSection = ({ config }: { config: PeriodSelectConfig }) => { const [selected, setSelected] = useState(config.defaultValue || config.options[0]?.value || ''); + const handleChange = useCallback((value: string) => { + setSelected(value); + config.onPeriodChange?.(value); + }, [config]); + return (
์‹ ๊ณ ๊ธฐ๊ฐ„ - diff --git a/src/components/business/CEODashboard/modals/ScheduleDetailModal.tsx b/src/components/business/CEODashboard/modals/ScheduleDetailModal.tsx index 976d385c..7131661f 100644 --- a/src/components/business/CEODashboard/modals/ScheduleDetailModal.tsx +++ b/src/components/business/CEODashboard/modals/ScheduleDetailModal.tsx @@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Checkbox } from '@/components/ui/checkbox'; import { TimePicker } from '@/components/ui/time-picker'; -import { DatePicker } from '@/components/ui/date-picker'; +import { DateRangePicker } from '@/components/ui/date-range-picker'; import { Dialog, DialogContent, @@ -21,6 +21,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Badge } from '@/components/ui/badge'; import type { CalendarScheduleItem } from '../types'; // ์ƒ‰์ƒ ์˜ต์…˜ @@ -59,6 +60,7 @@ interface ScheduleDetailModalProps { schedule: CalendarScheduleItem | null; onSave: (data: ScheduleFormData) => void; onDelete?: (id: string) => void; + isEditable?: boolean; } export function ScheduleDetailModal({ @@ -67,6 +69,7 @@ export function ScheduleDetailModal({ schedule, onSave, onDelete, + isEditable = true, }: ScheduleDetailModalProps) { const isEditMode = schedule && schedule.id !== ''; @@ -128,7 +131,14 @@ export function ScheduleDetailModal({ !open && handleCancel()}> - ์ผ์ • ์ƒ์„ธ + + ์ผ์ • ์ƒ์„ธ + {!isEditable && ( + + ์ฝ๊ธฐ์ „์šฉ + + )} +
@@ -139,6 +149,7 @@ export function ScheduleDetailModal({ value={formData.title} onChange={(e) => handleFieldChange('title', e.target.value)} placeholder="์ œ๋ชฉ" + disabled={!isEditable} />
@@ -148,6 +159,7 @@ export function ScheduleDetailModal({