feat(WEB): 견적 시스템 개선, 엑셀 다운로드, PDF 생성 기능 추가

견적 시스템:
- QuoteRegistrationV2: 할인 모달, 거래명세서 모달, vatType 필드 추가
- DiscountModal: 할인율/할인금액 상호 계산 모달
- QuoteTransactionModal: 거래명세서 미리보기 모달
- LocationDetailPanel, LocationListPanel 개선

템플릿 기능:
- UniversalListPage: 엑셀 다운로드 기능 추가 (전체/선택 다운로드)
- DocumentViewer: PDF 생성 기능 개선

신규 API:
- /api/pdf/generate: Puppeteer 기반 PDF 생성 엔드포인트

UI 개선:
- 입력 컴포넌트 placeholder 스타일 개선 (opacity 50%)
- 각종 리스트 컴포넌트 정렬/필터링 개선

패키지 추가:
- html2canvas, jspdf, puppeteer, dom-to-image-more

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-27 19:49:03 +09:00
parent c4644489e7
commit afd7bda269
35 changed files with 3493 additions and 946 deletions

View File

@@ -17,9 +17,9 @@ import {
CheckCircle2,
Clock,
AlertCircle,
Download,
Eye,
} from 'lucide-react';
import type { ExcelColumn } from '@/lib/utils/excel-download';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { TableCell, TableRow } from '@/components/ui/table';
@@ -37,7 +37,7 @@ import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
import { getStocks, getStockStats, getStockStatsByType } from './actions';
import { ITEM_TYPE_LABELS, ITEM_TYPE_STYLES, STOCK_STATUS_LABELS } from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import type { StockItem, StockStats, ItemType } from './types';
import type { StockItem, StockStats, ItemType, StockStatusType } from './types';
// 페이지당 항목 수
const ITEMS_PER_PAGE = 20;
@@ -80,10 +80,42 @@ export function StockStatusList() {
[router]
);
// ===== 엑셀 다운로드 =====
const handleExcelDownload = useCallback(() => {
console.log('엑셀 다운로드');
// TODO: 엑셀 다운로드 기능 구현
// ===== 엑셀 컬럼 정의 =====
const excelColumns: ExcelColumn<StockItem>[] = useMemo(() => [
{ header: '품목코드', key: 'itemCode' },
{ header: '품목명', key: 'itemName' },
{ header: '품목유형', key: 'itemType', transform: (value) => ITEM_TYPE_LABELS[value as ItemType] || String(value) },
{ header: '단위', key: 'unit' },
{ header: '재고량', key: 'stockQty' },
{ header: '안전재고', key: 'safetyStock' },
{ header: 'LOT수', key: 'lotCount' },
{ header: 'LOT경과일', key: 'lotDaysElapsed' },
{ header: '상태', key: 'status', transform: (value) => value ? STOCK_STATUS_LABELS[value as StockStatusType] : '-' },
{ header: '위치', key: 'location' },
], []);
// ===== API 응답 매핑 함수 =====
const mapStockResponse = useCallback((result: unknown): StockItem[] => {
const data = result as { data?: { data?: Record<string, unknown>[] } };
const rawItems = data.data?.data ?? [];
return rawItems.map((item: Record<string, unknown>) => {
const stock = item.stock as Record<string, unknown> | null;
const hasStock = !!stock;
return {
id: String(item.id ?? ''),
itemCode: (item.code ?? '') as string,
itemName: (item.name ?? '') as string,
itemType: (item.item_type ?? 'RM') as ItemType,
unit: (item.unit ?? 'EA') as string,
stockQty: hasStock ? (parseFloat(String(stock?.stock_qty)) || 0) : 0,
safetyStock: hasStock ? (parseFloat(String(stock?.safety_stock)) || 0) : 0,
lotCount: hasStock ? (Number(stock?.lot_count) || 0) : 0,
lotDaysElapsed: hasStock ? (Number(stock?.days_elapsed) || 0) : 0,
status: hasStock ? (stock?.status as StockStatusType | null) : null,
location: hasStock ? ((stock?.location as string) || '-') : '-',
hasStock,
};
});
}, []);
// ===== 통계 카드 =====
@@ -236,13 +268,24 @@ export function StockStatusList() {
// 테이블 푸터
tableFooter,
// 헤더 액션 (엑셀 다운로드)
headerActions: () => (
<Button variant="outline" onClick={handleExcelDownload}>
<Download className="w-4 h-4 mr-1.5" />
</Button>
),
// 엑셀 다운로드 설정
excelDownload: {
columns: excelColumns,
filename: '재고현황',
sheetName: '재고',
fetchAllUrl: '/api/proxy/stocks',
fetchAllParams: ({ activeTab, searchValue }) => {
const params: Record<string, string> = {};
if (activeTab && activeTab !== 'all') {
params.item_type = activeTab;
}
if (searchValue) {
params.search = searchValue;
}
return params;
},
mapResponse: mapStockResponse,
},
// 테이블 행 렌더링
renderTableRow: (
@@ -363,7 +406,7 @@ export function StockStatusList() {
);
},
}),
[tabs, stats, tableFooter, handleRowClick, handleExcelDownload]
[tabs, stats, tableFooter, handleRowClick, excelColumns, mapStockResponse]
);
return <UniversalListPage config={config} />;