Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -68,44 +68,45 @@ export function BillDetail({ billId, mode }: BillDetailProps) {
|
||||
const [note, setNote] = useState('');
|
||||
const [installments, setInstallments] = useState<InstallmentRecord[]>([]);
|
||||
|
||||
// ===== 거래처 목록 로드 =====
|
||||
// ===== 초기 데이터 로드 (거래처 + 어음 상세 병렬) =====
|
||||
useEffect(() => {
|
||||
async function loadClients() {
|
||||
const result = await getClients();
|
||||
if (result.success && result.data) {
|
||||
setClients(result.data.map(c => ({ id: String(c.id), name: c.name })));
|
||||
async function loadInitialData() {
|
||||
const isEditMode = billId && billId !== 'new';
|
||||
setIsLoading(!!isEditMode);
|
||||
|
||||
const [clientsResult, billResult] = await Promise.all([
|
||||
getClients(),
|
||||
isEditMode ? getBill(billId) : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
// 거래처 목록
|
||||
if (clientsResult.success && clientsResult.data) {
|
||||
setClients(clientsResult.data.map(c => ({ id: String(c.id), name: c.name })));
|
||||
}
|
||||
}
|
||||
loadClients();
|
||||
}, []);
|
||||
|
||||
// ===== 데이터 로드 =====
|
||||
useEffect(() => {
|
||||
async function loadBill() {
|
||||
if (!billId || billId === 'new') return;
|
||||
// 어음 상세
|
||||
if (billResult) {
|
||||
if (billResult.success && billResult.data) {
|
||||
const data = billResult.data;
|
||||
setBillNumber(data.billNumber);
|
||||
setBillType(data.billType);
|
||||
setVendorId(data.vendorId);
|
||||
setAmount(data.amount);
|
||||
setIssueDate(data.issueDate);
|
||||
setMaturityDate(data.maturityDate);
|
||||
setStatus(data.status);
|
||||
setNote(data.note);
|
||||
setInstallments(data.installments);
|
||||
} else {
|
||||
toast.error(billResult.error || '어음 정보를 불러올 수 없습니다.');
|
||||
router.push('/ko/accounting/bills');
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const result = await getBill(billId);
|
||||
setIsLoading(false);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const data = result.data;
|
||||
setBillNumber(data.billNumber);
|
||||
setBillType(data.billType);
|
||||
setVendorId(data.vendorId);
|
||||
setAmount(data.amount);
|
||||
setIssueDate(data.issueDate);
|
||||
setMaturityDate(data.maturityDate);
|
||||
setStatus(data.status);
|
||||
setNote(data.note);
|
||||
setInstallments(data.installments);
|
||||
} else {
|
||||
toast.error(result.error || '어음 정보를 불러올 수 없습니다.');
|
||||
router.push('/ko/accounting/bills');
|
||||
}
|
||||
}
|
||||
|
||||
loadBill();
|
||||
loadInitialData();
|
||||
}, [billId, router]);
|
||||
|
||||
// ===== 저장 핸들러 =====
|
||||
|
||||
@@ -55,38 +55,39 @@ export function DepositDetail({ depositId, mode }: DepositDetailProps) {
|
||||
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();
|
||||
}, []);
|
||||
const loadInitialData = async () => {
|
||||
const isEditMode = depositId && !isNewMode;
|
||||
if (isEditMode) setIsLoading(true);
|
||||
|
||||
// ===== 데이터 로드 =====
|
||||
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);
|
||||
const [vendorsResult, depositResult] = await Promise.all([
|
||||
getVendors(),
|
||||
isEditMode ? getDepositById(depositId) : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
// 거래처 목록
|
||||
if (vendorsResult.success) {
|
||||
setVendors(vendorsResult.data);
|
||||
}
|
||||
|
||||
// 입금 상세
|
||||
if (depositResult) {
|
||||
if (depositResult.success && depositResult.data) {
|
||||
setDepositDate(depositResult.data.depositDate);
|
||||
setAccountName(depositResult.data.accountName);
|
||||
setDepositorName(depositResult.data.depositorName);
|
||||
setDepositAmount(depositResult.data.depositAmount);
|
||||
setNote(depositResult.data.note);
|
||||
setVendorId(depositResult.data.vendorId);
|
||||
setDepositType(depositResult.data.depositType);
|
||||
} else {
|
||||
toast.error(result.error || '입금 내역을 불러오는데 실패했습니다.');
|
||||
toast.error(depositResult.error || '입금 내역을 불러오는데 실패했습니다.');
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
loadDeposit();
|
||||
loadInitialData();
|
||||
}, [depositId, isNewMode]);
|
||||
|
||||
// ===== 저장 핸들러 =====
|
||||
|
||||
@@ -93,28 +93,29 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
// ===== 다이얼로그 상태 =====
|
||||
const [documentModalOpen, setDocumentModalOpen] = useState(false);
|
||||
|
||||
// ===== 거래처 목록 로드 =====
|
||||
// ===== 초기 데이터 로드 (거래처 + 매입 상세 병렬) =====
|
||||
useEffect(() => {
|
||||
async function loadClients() {
|
||||
const result = await getClients({ size: 1000, only_active: true });
|
||||
if (result.success) {
|
||||
setClients(result.data.map(v => ({
|
||||
async function loadInitialData() {
|
||||
const isEditMode = purchaseId && mode !== 'new';
|
||||
setIsLoading(true);
|
||||
|
||||
const [clientsResult, purchaseResult] = await Promise.all([
|
||||
getClients({ size: 1000, only_active: true }),
|
||||
isEditMode ? getPurchaseById(purchaseId) : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
// 거래처 목록
|
||||
if (clientsResult.success) {
|
||||
setClients(clientsResult.data.map(v => ({
|
||||
id: v.id,
|
||||
name: v.vendorName,
|
||||
})));
|
||||
}
|
||||
}
|
||||
loadClients();
|
||||
}, []);
|
||||
|
||||
// ===== 매입 상세 데이터 로드 =====
|
||||
useEffect(() => {
|
||||
async function loadPurchaseDetail() {
|
||||
if (purchaseId && mode !== 'new') {
|
||||
setIsLoading(true);
|
||||
const result = await getPurchaseById(purchaseId);
|
||||
if (result.success && result.data) {
|
||||
const data = result.data;
|
||||
// 매입 상세
|
||||
if (purchaseResult) {
|
||||
if (purchaseResult.success && purchaseResult.data) {
|
||||
const data = purchaseResult.data;
|
||||
setPurchaseNo(data.purchaseNo);
|
||||
setPurchaseDate(data.purchaseDate);
|
||||
setVendorId(data.vendorId);
|
||||
@@ -126,16 +127,13 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
setWithdrawalAccount(data.withdrawalAccount);
|
||||
setCreatedAt(data.createdAt);
|
||||
}
|
||||
setIsLoading(false);
|
||||
} else if (isNewMode) {
|
||||
// 신규: 매입번호는 서버에서 자동 생성
|
||||
setPurchaseNo('(자동생성)');
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
loadPurchaseDetail();
|
||||
loadInitialData();
|
||||
}, [purchaseId, mode, isNewMode]);
|
||||
|
||||
// ===== 합계 계산 =====
|
||||
|
||||
@@ -99,29 +99,30 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
||||
const [showEmailAlert, setShowEmailAlert] = useState(false);
|
||||
const [emailAlertMessage, setEmailAlertMessage] = useState('');
|
||||
|
||||
// ===== 거래처 목록 로드 =====
|
||||
// ===== 초기 데이터 로드 (거래처 + 매출 상세 병렬) =====
|
||||
useEffect(() => {
|
||||
async function loadClients() {
|
||||
const result = await getClients({ size: 1000, only_active: true });
|
||||
if (result.success) {
|
||||
setClients(result.data.map(v => ({
|
||||
async function loadInitialData() {
|
||||
const isEditMode = salesId && mode !== 'new';
|
||||
setIsLoading(true);
|
||||
|
||||
const [clientsResult, saleResult] = await Promise.all([
|
||||
getClients({ size: 1000, only_active: true }),
|
||||
isEditMode ? getSaleById(salesId) : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
// 거래처 목록
|
||||
if (clientsResult.success) {
|
||||
setClients(clientsResult.data.map(v => ({
|
||||
id: v.id,
|
||||
name: v.vendorName,
|
||||
email: v.email,
|
||||
})));
|
||||
}
|
||||
}
|
||||
loadClients();
|
||||
}, []);
|
||||
|
||||
// ===== 매출 상세 데이터 로드 =====
|
||||
useEffect(() => {
|
||||
async function loadSaleDetail() {
|
||||
if (salesId && mode !== 'new') {
|
||||
setIsLoading(true);
|
||||
const result = await getSaleById(salesId);
|
||||
if (result.success && result.data) {
|
||||
const data = result.data;
|
||||
// 매출 상세
|
||||
if (saleResult) {
|
||||
if (saleResult.success && saleResult.data) {
|
||||
const data = saleResult.data;
|
||||
setSalesNo(data.salesNo);
|
||||
setSalesDate(data.salesDate);
|
||||
setVendorId(data.vendorId);
|
||||
@@ -132,16 +133,13 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
||||
setTransactionStatementIssued(data.transactionStatementIssued);
|
||||
setNote(data.note || '');
|
||||
}
|
||||
setIsLoading(false);
|
||||
} else if (isNewMode) {
|
||||
// 신규: 매출번호는 서버에서 자동 생성
|
||||
setSalesNo('(자동생성)');
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
loadSaleDetail();
|
||||
loadInitialData();
|
||||
}, [salesId, mode, isNewMode]);
|
||||
|
||||
// ===== 선택된 거래처 정보 =====
|
||||
|
||||
@@ -55,38 +55,39 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps)
|
||||
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();
|
||||
}, []);
|
||||
const loadInitialData = async () => {
|
||||
const isEditMode = withdrawalId && !isNewMode;
|
||||
if (isEditMode) setIsLoading(true);
|
||||
|
||||
// ===== 데이터 로드 =====
|
||||
useEffect(() => {
|
||||
const loadWithdrawal = async () => {
|
||||
if (withdrawalId && !isNewMode) {
|
||||
setIsLoading(true);
|
||||
const result = await getWithdrawalById(withdrawalId);
|
||||
if (result.success && result.data) {
|
||||
setWithdrawalDate(result.data.withdrawalDate);
|
||||
setAccountName(result.data.accountName);
|
||||
setRecipientName(result.data.recipientName);
|
||||
setWithdrawalAmount(result.data.withdrawalAmount);
|
||||
setNote(result.data.note);
|
||||
setVendorId(result.data.vendorId);
|
||||
setWithdrawalType(result.data.withdrawalType);
|
||||
const [vendorsResult, withdrawalResult] = await Promise.all([
|
||||
getVendors(),
|
||||
isEditMode ? getWithdrawalById(withdrawalId) : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
// 거래처 목록
|
||||
if (vendorsResult.success) {
|
||||
setVendors(vendorsResult.data);
|
||||
}
|
||||
|
||||
// 출금 상세
|
||||
if (withdrawalResult) {
|
||||
if (withdrawalResult.success && withdrawalResult.data) {
|
||||
setWithdrawalDate(withdrawalResult.data.withdrawalDate);
|
||||
setAccountName(withdrawalResult.data.accountName);
|
||||
setRecipientName(withdrawalResult.data.recipientName);
|
||||
setWithdrawalAmount(withdrawalResult.data.withdrawalAmount);
|
||||
setNote(withdrawalResult.data.note);
|
||||
setVendorId(withdrawalResult.data.vendorId);
|
||||
setWithdrawalType(withdrawalResult.data.withdrawalType);
|
||||
} else {
|
||||
toast.error(result.error || '출금 내역을 불러오는데 실패했습니다.');
|
||||
toast.error(withdrawalResult.error || '출금 내역을 불러오는데 실패했습니다.');
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
loadWithdrawal();
|
||||
loadInitialData();
|
||||
}, [withdrawalId, isNewMode]);
|
||||
|
||||
// ===== 저장 핸들러 =====
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from "react";
|
||||
import { CEODashboard } from "./CEODashboard";
|
||||
import dynamic from 'next/dynamic';
|
||||
import { DetailPageSkeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const CEODashboard = dynamic(
|
||||
() => import('./CEODashboard').then(mod => ({ default: mod.CEODashboard })),
|
||||
{ loading: () => <DetailPageSkeleton /> }
|
||||
);
|
||||
|
||||
/**
|
||||
* Dashboard - 대표님 전용 대시보드
|
||||
*
|
||||
@@ -24,9 +28,5 @@ import { DetailPageSkeleton } from "@/components/ui/skeleton";
|
||||
*/
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<Suspense fallback={<DetailPageSkeleton />}>
|
||||
<CEODashboard />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
return <CEODashboard />;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from "react";
|
||||
import { ConstructionMainDashboard } from "./ConstructionMainDashboard";
|
||||
import dynamic from 'next/dynamic';
|
||||
import { DetailPageSkeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const ConstructionMainDashboard = dynamic(
|
||||
() => import('./ConstructionMainDashboard').then(mod => ({ default: mod.ConstructionMainDashboard })),
|
||||
{ loading: () => <DetailPageSkeleton /> }
|
||||
);
|
||||
|
||||
/**
|
||||
* ConstructionDashboard - 주일기업 전용 대시보드
|
||||
*
|
||||
*
|
||||
* 건설/공사 프로젝트 중심의 메트릭과 현황을 보여줍니다.
|
||||
*/
|
||||
export function ConstructionDashboard() {
|
||||
return (
|
||||
<Suspense fallback={<DetailPageSkeleton />}>
|
||||
<ConstructionMainDashboard />
|
||||
</Suspense>
|
||||
);
|
||||
return <ConstructionMainDashboard />;
|
||||
}
|
||||
|
||||
@@ -288,8 +288,8 @@ export default function ItemListClient() {
|
||||
];
|
||||
|
||||
// 양식 다운로드
|
||||
const handleTemplateDownload = () => {
|
||||
downloadExcelTemplate({
|
||||
const handleTemplateDownload = async () => {
|
||||
await downloadExcelTemplate({
|
||||
columns: templateColumns,
|
||||
filename: '품목등록_양식',
|
||||
sheetName: '품목등록',
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* - 테이블 푸터 (요약 정보)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Package,
|
||||
@@ -230,7 +230,7 @@ export function StockStatusList() {
|
||||
];
|
||||
|
||||
// ===== 테이블 컬럼 =====
|
||||
const tableColumns = [
|
||||
const tableColumns = useMemo(() => [
|
||||
{ key: 'no', label: 'No.', className: 'w-[60px] text-center' },
|
||||
{ key: 'itemCode', label: '품목코드', className: 'min-w-[100px]' },
|
||||
{ key: 'itemType', label: '품목유형', className: 'w-[80px]' },
|
||||
@@ -241,7 +241,7 @@ export function StockStatusList() {
|
||||
{ key: 'safetyStock', label: '안전재고', className: 'w-[80px] text-center' },
|
||||
{ key: 'wipQty', label: '재공품', className: 'w-[80px] text-center' },
|
||||
{ key: 'useStatus', label: '상태', className: 'w-[80px] text-center' },
|
||||
];
|
||||
], []);
|
||||
|
||||
// ===== 테이블 행 렌더링 =====
|
||||
const renderTableRow = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode, memo, useCallback } from "react";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -204,6 +204,62 @@ function getAlignClass<T>(column: Column<T>): string {
|
||||
}
|
||||
}
|
||||
|
||||
// 메모이즈드 행 컴포넌트 — 행 데이터가 변경되지 않으면 리렌더링 스킵
|
||||
interface DataTableRowProps<T extends object> {
|
||||
row: T;
|
||||
rowIndex: number;
|
||||
columns: Column<T>[];
|
||||
onRowClick?: (row: T) => void;
|
||||
hoverable: boolean;
|
||||
striped: boolean;
|
||||
compact: boolean;
|
||||
rowKey: string;
|
||||
}
|
||||
|
||||
function DataTableRowInner<T extends object>({
|
||||
row,
|
||||
rowIndex,
|
||||
columns,
|
||||
onRowClick,
|
||||
hoverable,
|
||||
striped,
|
||||
compact,
|
||||
}: DataTableRowProps<T>) {
|
||||
const handleClick = useCallback(() => {
|
||||
onRowClick?.(row);
|
||||
}, [onRowClick, row]);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
onClick={onRowClick ? handleClick : undefined}
|
||||
className={cn(
|
||||
onRowClick && "cursor-pointer",
|
||||
hoverable && "hover:bg-muted/50",
|
||||
striped && rowIndex % 2 === 1 && "bg-muted/20",
|
||||
compact && "h-10"
|
||||
)}
|
||||
>
|
||||
{columns.map((column) => {
|
||||
const value = column.key in row ? row[column.key as keyof T] : null;
|
||||
return (
|
||||
<TableCell
|
||||
key={String(column.key)}
|
||||
className={cn(
|
||||
getAlignClass(column),
|
||||
column.className,
|
||||
compact && "py-2"
|
||||
)}
|
||||
>
|
||||
{renderCell(column, value, row, rowIndex)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedDataTableRow = memo(DataTableRowInner) as typeof DataTableRowInner;
|
||||
|
||||
export function DataTable<T extends object>({
|
||||
columns,
|
||||
data,
|
||||
@@ -216,6 +272,11 @@ export function DataTable<T extends object>({
|
||||
hoverable = true,
|
||||
compact = false
|
||||
}: DataTableProps<T>) {
|
||||
const stableOnRowClick = useCallback(
|
||||
(row: T) => onRowClick?.(row),
|
||||
[onRowClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="hidden md:block">
|
||||
<CardContent className="p-0">
|
||||
@@ -252,32 +313,17 @@ export function DataTable<T extends object>({
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((row, rowIndex) => (
|
||||
<TableRow
|
||||
<MemoizedDataTableRow<T>
|
||||
key={row[keyField] ? String(row[keyField]) : `row-${rowIndex}`}
|
||||
onClick={() => onRowClick?.(row)}
|
||||
className={cn(
|
||||
onRowClick && "cursor-pointer",
|
||||
hoverable && "hover:bg-muted/50",
|
||||
striped && rowIndex % 2 === 1 && "bg-muted/20",
|
||||
compact && "h-10"
|
||||
)}
|
||||
>
|
||||
{columns.map((column) => {
|
||||
const value = column.key in row ? row[column.key as keyof T] : null;
|
||||
return (
|
||||
<TableCell
|
||||
key={String(column.key)}
|
||||
className={cn(
|
||||
getAlignClass(column),
|
||||
column.className,
|
||||
compact && "py-2"
|
||||
)}
|
||||
>
|
||||
{renderCell(column, value, row, rowIndex)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
row={row}
|
||||
rowIndex={rowIndex}
|
||||
columns={columns}
|
||||
onRowClick={onRowClick ? stableOnRowClick : undefined}
|
||||
hoverable={hoverable}
|
||||
striped={striped}
|
||||
compact={compact}
|
||||
rowKey={row[keyField] ? String(row[keyField]) : `row-${rowIndex}`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FileText, FilePlus } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -147,14 +147,14 @@ export function PriceDistributionList() {
|
||||
};
|
||||
|
||||
// 테이블 컬럼
|
||||
const tableColumns: TableColumn[] = [
|
||||
const tableColumns: TableColumn[] = useMemo(() => [
|
||||
{ key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' },
|
||||
{ key: 'distributionNo', label: '단가배포번호', className: 'min-w-[120px]' },
|
||||
{ key: 'distributionName', label: '단가배포명', className: 'min-w-[150px]' },
|
||||
{ key: 'status', label: '상태', className: 'min-w-[100px]' },
|
||||
{ key: 'author', label: '작성자', className: 'min-w-[100px]' },
|
||||
{ key: 'createdAt', label: '등록일', className: 'min-w-[120px]' },
|
||||
];
|
||||
], []);
|
||||
|
||||
// 테이블 행 렌더링
|
||||
const renderTableRow = (
|
||||
|
||||
@@ -196,7 +196,7 @@ export function PricingListClient({
|
||||
];
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns: TableColumn[] = [
|
||||
const tableColumns: TableColumn[] = useMemo(() => [
|
||||
{ key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' },
|
||||
{ key: 'itemType', label: '품목유형', className: 'min-w-[100px]' },
|
||||
{ key: 'itemCode', label: '품목코드', className: 'min-w-[120px]' },
|
||||
@@ -209,7 +209,7 @@ export function PricingListClient({
|
||||
{ key: 'marginRate', label: '마진율', className: 'min-w-[80px] text-right', hideOnMobile: true },
|
||||
{ key: 'effectiveDate', label: '적용일', className: 'min-w-[100px]', hideOnMobile: true },
|
||||
{ key: 'status', label: '상태', className: 'min-w-[80px]' },
|
||||
];
|
||||
], []);
|
||||
|
||||
// 테이블 행 렌더링
|
||||
const renderTableRow = (
|
||||
|
||||
@@ -35,7 +35,7 @@ import { DeleteConfirmDialog } from "../ui/confirm-dialog";
|
||||
|
||||
import type { LocationItem } from "./QuoteRegistrationV2";
|
||||
import type { FinishedGoods } from "./actions";
|
||||
import * as XLSX from "xlsx";
|
||||
// xlsx는 동적 로드 (번들 크기 최적화)
|
||||
|
||||
// =============================================================================
|
||||
// 상수
|
||||
@@ -181,7 +181,8 @@ export function LocationListPanel({
|
||||
}, [formData, finishedGoods, onAddLocation]);
|
||||
|
||||
// 엑셀 양식 다운로드
|
||||
const handleDownloadTemplate = useCallback(() => {
|
||||
const handleDownloadTemplate = useCallback(async () => {
|
||||
const XLSX = await import("xlsx");
|
||||
const templateData = [
|
||||
{
|
||||
층: "1층",
|
||||
@@ -219,10 +220,11 @@ export function LocationListPanel({
|
||||
|
||||
// 엑셀 업로드
|
||||
const handleFileUpload = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const XLSX = await import("xlsx");
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
|
||||
@@ -628,7 +628,7 @@ export function UniversalListPage<T>({
|
||||
return;
|
||||
}
|
||||
|
||||
downloadExcel({
|
||||
await downloadExcel({
|
||||
data: dataToDownload as Record<string, unknown>[],
|
||||
columns: columns as ExcelColumn<Record<string, unknown>>[],
|
||||
filename,
|
||||
@@ -645,7 +645,7 @@ export function UniversalListPage<T>({
|
||||
}, [config.excelDownload, config.clientSideFiltering, filteredData, rawData, activeTab, filters, debouncedSearchValue]);
|
||||
|
||||
// 선택 항목 엑셀 다운로드
|
||||
const handleSelectedExcelDownload = useCallback(() => {
|
||||
const handleSelectedExcelDownload = useCallback(async () => {
|
||||
if (!config.excelDownload) return;
|
||||
|
||||
const { columns, filename = 'export', sheetName = 'Sheet1' } = config.excelDownload;
|
||||
@@ -659,7 +659,7 @@ export function UniversalListPage<T>({
|
||||
// 현재 데이터에서 선택된 항목 필터링
|
||||
const selectedData = rawData.filter((item) => selectedIds.includes(getItemId(item)));
|
||||
|
||||
downloadSelectedExcel({
|
||||
await downloadSelectedExcel({
|
||||
data: selectedData as Record<string, unknown>[],
|
||||
columns: columns as ExcelColumn<Record<string, unknown>>[],
|
||||
selectedIds,
|
||||
|
||||
Reference in New Issue
Block a user