refactor(WEB): DataTable 개선 및 회계 상세 컴포넌트 리팩토링

- DataTable 컴포넌트 기능 확장 및 코드 개선
- 회계 상세 컴포넌트(Bill/Deposit/Purchase/Sales/Withdrawal) 리팩토링
- 엑셀 다운로드 유틸리티 개선
- 대시보드 및 각종 리스트 페이지 업데이트
- dashboard_type2 페이지 추가
- 프론트엔드 개선 로드맵 문서 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-11 11:03:19 +09:00
parent 0db6302652
commit e14335b635
33 changed files with 1354 additions and 217 deletions

View File

@@ -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]);
// ===== 저장 핸들러 =====

View File

@@ -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]);
// ===== 저장 핸들러 =====

View File

@@ -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]);
// ===== 합계 계산 =====

View File

@@ -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]);
// ===== 선택된 거래처 정보 =====

View File

@@ -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]);
// ===== 저장 핸들러 =====