feat(WEB): 리스트 페이지 UI 레이아웃 표준화
- 공통 레이아웃 패턴 적용: [달력] → [프리셋] → [검색창] → [버튼들] - beforeTableContent → headerActions + createButton 마이그레이션 - DateRangeSelector extraActions prop 활용하여 검색창 통합 - PricingListClient 테이블 행 클릭 → 상세 이동 기능 추가 - 회계 관련 페이지 (입금/출금/매입/매출/어음/카드/예상지출 등) 정리 - 건설 관련 페이지 검색 영역 정리 - 부모 메뉴 리다이렉트 컴포넌트 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,8 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { Search, Plus, Edit, Trash2, Package } from 'lucide-react';
|
||||
import { Search, Plus, Edit, Trash2, Package, Download, FileDown, Upload } from 'lucide-react';
|
||||
import { downloadExcel, downloadSelectedExcel, downloadExcelTemplate, parseExcelFile, type ExcelColumn, type TemplateColumn } from '@/lib/utils/excel-download';
|
||||
import { useItemList } from '@/hooks/useItemList';
|
||||
import { handleApiError } from '@/lib/api/error-handler';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
@@ -237,6 +238,120 @@ export default function ItemListClient() {
|
||||
}
|
||||
};
|
||||
|
||||
// 엑셀 다운로드용 컬럼 정의
|
||||
const excelColumns: ExcelColumn<ItemMaster>[] = [
|
||||
{ header: '품목코드', key: 'itemCode', width: 15 },
|
||||
{ header: '품목유형', key: 'itemType', width: 10, transform: (v) => ITEM_TYPE_LABELS[v as keyof typeof ITEM_TYPE_LABELS] || String(v) },
|
||||
{ header: '품목명', key: 'itemName', width: 30 },
|
||||
{ header: '규격', key: 'specification', width: 20 },
|
||||
{ header: '단위', key: 'unit', width: 8 },
|
||||
{ header: '대분류', key: 'category1', width: 12 },
|
||||
{ header: '중분류', key: 'category2', width: 12 },
|
||||
{ header: '소분류', key: 'category3', width: 12 },
|
||||
{ header: '구매단가', key: 'purchasePrice', width: 12 },
|
||||
{ header: '판매단가', key: 'salesPrice', width: 12 },
|
||||
{ header: '활성상태', key: 'isActive', width: 10, transform: (v) => v ? '활성' : '비활성' },
|
||||
];
|
||||
|
||||
// 전체 엑셀 다운로드
|
||||
const handleExcelDownload = () => {
|
||||
if (items.length === 0) {
|
||||
alert('다운로드할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
downloadExcel({
|
||||
data: items,
|
||||
columns: excelColumns,
|
||||
filename: '품목목록',
|
||||
sheetName: '품목',
|
||||
});
|
||||
};
|
||||
|
||||
// 선택 항목 엑셀 다운로드
|
||||
const handleSelectedExcelDownload = (selectedIds: string[]) => {
|
||||
if (selectedIds.length === 0) {
|
||||
alert('선택된 항목이 없습니다.');
|
||||
return;
|
||||
}
|
||||
downloadSelectedExcel({
|
||||
data: items,
|
||||
columns: excelColumns,
|
||||
selectedIds,
|
||||
idField: 'id',
|
||||
filename: '품목목록_선택',
|
||||
sheetName: '품목',
|
||||
});
|
||||
};
|
||||
|
||||
// 업로드용 템플릿 컬럼 정의
|
||||
const templateColumns: TemplateColumn[] = [
|
||||
{ header: '품목코드', key: 'itemCode', required: true, type: 'text', sampleValue: 'KD-FG-001', description: '고유 코드', width: 15 },
|
||||
{ header: '품목유형', key: 'itemType', required: true, type: 'select', options: ['FG', 'PT', 'SM', 'RM', 'CS'], sampleValue: 'FG', description: 'FG:제품/PT:부품/SM:부자재/RM:원자재/CS:소모품', width: 12 },
|
||||
{ header: '품목명', key: 'itemName', required: true, type: 'text', sampleValue: '스크린도어 본체', width: 25 },
|
||||
{ header: '규격', key: 'specification', type: 'text', sampleValue: '1800x2100', width: 15 },
|
||||
{ header: '단위', key: 'unit', required: true, type: 'select', options: ['EA', 'SET', 'KG', 'M', 'M2', 'BOX'], sampleValue: 'EA', width: 10 },
|
||||
{ header: '대분류', key: 'category1', type: 'text', sampleValue: '스크린도어', width: 12 },
|
||||
{ header: '중분류', key: 'category2', type: 'text', sampleValue: '본체류', width: 12 },
|
||||
{ header: '소분류', key: 'category3', type: 'text', sampleValue: '프레임', width: 12 },
|
||||
{ header: '구매단가', key: 'purchasePrice', type: 'number', sampleValue: 150000, width: 12 },
|
||||
{ header: '판매단가', key: 'salesPrice', type: 'number', sampleValue: 200000, width: 12 },
|
||||
{ header: '활성상태', key: 'isActive', type: 'boolean', sampleValue: 'Y', description: 'Y:활성/N:비활성', width: 10 },
|
||||
];
|
||||
|
||||
// 양식 다운로드
|
||||
const handleTemplateDownload = () => {
|
||||
downloadExcelTemplate({
|
||||
columns: templateColumns,
|
||||
filename: '품목등록_양식',
|
||||
sheetName: '품목등록',
|
||||
includeSampleRow: true,
|
||||
includeGuideRow: true,
|
||||
});
|
||||
};
|
||||
|
||||
// 파일 업로드 input ref
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 양식 업로드 핸들러
|
||||
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const result = await parseExcelFile(file, {
|
||||
columns: templateColumns,
|
||||
skipRows: 2, // 헤더 + 안내 행 스킵
|
||||
});
|
||||
|
||||
if (!result.success || result.errors.length > 0) {
|
||||
const errorMessages = result.errors.slice(0, 5).map(
|
||||
(err) => `${err.row}행: ${err.message}`
|
||||
).join('\n');
|
||||
alert(`업로드 오류:\n${errorMessages}${result.errors.length > 5 ? `\n... 외 ${result.errors.length - 5}건` : ''}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.data.length === 0) {
|
||||
alert('업로드할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 실제 API 호출로 데이터 저장
|
||||
// 지금은 파싱 결과만 확인
|
||||
console.log('[Excel Upload] 파싱 결과:', result.data);
|
||||
alert(`${result.data.length}건의 데이터가 파싱되었습니다.\n(실제 등록 기능은 추후 구현 예정)`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Excel Upload] 오류:', error);
|
||||
alert('파일 업로드에 실패했습니다.');
|
||||
} finally {
|
||||
// input 초기화 (같은 파일 재선택 가능하도록)
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 탭 옵션 (품목 유형별)
|
||||
const tabs: TabOption[] = [
|
||||
{ value: 'all', label: '전체', count: totalStats.totalAll, color: 'gray' },
|
||||
@@ -301,6 +416,56 @@ export default function ItemListClient() {
|
||||
icon: Plus,
|
||||
},
|
||||
|
||||
// 헤더 액션 (엑셀 다운로드)
|
||||
headerActions: ({ selectedItems }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 양식 다운로드 버튼 - 추후 활성화
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleTemplateDownload}
|
||||
className="gap-2"
|
||||
>
|
||||
<FileDown className="h-4 w-4" />
|
||||
양식 다운로드
|
||||
</Button>
|
||||
*/}
|
||||
{/* 양식 업로드 버튼 - 추후 활성화
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="gap-2"
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
양식 업로드
|
||||
</Button>
|
||||
*/}
|
||||
{/* 엑셀 데이터 다운로드 버튼 */}
|
||||
{selectedItems.size > 0 ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleSelectedExcelDownload(Array.from(selectedItems))}
|
||||
className="gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
선택 다운로드 ({selectedItems.size})
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleExcelDownload}
|
||||
className="gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
|
||||
// API 액션 (일괄 삭제 포함)
|
||||
actions: {
|
||||
getList: async () => ({ success: true, data: items }),
|
||||
@@ -488,6 +653,15 @@ export default function ItemListClient() {
|
||||
externalIsLoading={isLoading}
|
||||
/>
|
||||
|
||||
{/* 숨겨진 파일 업로드 input */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* 개별 삭제 확인 다이얼로그 */}
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
|
||||
Reference in New Issue
Block a user