refactor(WEB): 회계/견적/설정/생산 등 전반적 코드 개선 및 공통화 2차

- 회계 모듈 전면 개선: 청구/입금/출금/매입/매출/세금계산서/일반전표/거래처원장 등
- 견적 모듈 금액 포맷/할인/수식/미리보기 등 코드 정리
- 설정 모듈: 계정관리/직급/직책/권한 상세 간소화
- 생산 모듈: 작업지시서/작업자화면/검수 문서 코드 정리
- UniversalListPage 엑셀 다운로드 및 필터 기능 확장
- 대시보드/게시판/수주 등 날짜 유틸 공통화 적용
- claudedocs 문서 인덱스 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-20 10:45:47 +09:00
parent 71352923c8
commit f344dc7d00
123 changed files with 877 additions and 789 deletions

View File

@@ -12,6 +12,7 @@
import { useState, useEffect, useCallback } from "react";
import { Percent } from "lucide-react";
import { formatNumber } from "@/lib/utils/amount";
import {
Dialog,
@@ -159,7 +160,7 @@ export function DiscountModal({
<div className="space-y-2">
<Label></Label>
<Input
value={supplyAmount.toLocaleString()}
value={formatNumber(supplyAmount)}
disabled
className="bg-gray-50 text-right font-medium"
/>
@@ -189,7 +190,7 @@ export function DiscountModal({
<Input
type="text"
placeholder="0"
value={discountAmount ? parseInt(discountAmount).toLocaleString() : ""}
value={discountAmount ? formatNumber(parseInt(discountAmount)) : ""}
onChange={(e) => handleDiscountAmountChange(e.target.value.replace(/,/g, ""))}
className="pr-8 text-right"
/>
@@ -203,7 +204,7 @@ export function DiscountModal({
<div className="space-y-2">
<Label> </Label>
<Input
value={discountedAmount.toLocaleString()}
value={formatNumber(discountedAmount)}
disabled
className="bg-gray-50 text-right font-bold text-blue-600"
/>

View File

@@ -8,6 +8,7 @@
import { Calculator, ChevronDown, ChevronRight } from "lucide-react";
import { useState } from "react";
import { formatNumber } from "@/lib/utils/amount";
import {
Dialog,
DialogContent,
@@ -131,11 +132,11 @@ function LocationDetail({ location }: { location: LocationItem }) {
</div>
<div className="text-right">
<span className="text-sm text-gray-600">1 </span>
<span className="font-bold text-blue-600">{bom.grand_total.toLocaleString()}</span>
<span className="font-bold text-blue-600">{formatNumber(bom.grand_total)}</span>
<span className="mx-2">×</span>
<span className="text-sm">{location.quantity} =</span>
<span className="font-bold text-green-600 text-lg ml-2">
{(bom.grand_total * location.quantity).toLocaleString()}
{formatNumber(bom.grand_total * location.quantity)}
</span>
</div>
</div>
@@ -212,7 +213,7 @@ function FormulaTable({ formulas, stepName }: { formulas: FormulaItem[]; stepNam
<tr key={i} className="border-b hover:bg-gray-50">
<td className="p-2 border font-mono font-bold text-blue-600">{f.var}</td>
<td className="p-2 border text-gray-600">{f.desc}</td>
<td className="p-2 border text-right font-mono">{typeof f.value === 'number' ? f.value.toLocaleString() : f.value}</td>
<td className="p-2 border text-right font-mono">{typeof f.value === 'number' ? formatNumber(f.value) : f.value}</td>
<td className="p-2 border text-gray-500">{f.unit}</td>
</tr>
))}
@@ -243,7 +244,7 @@ function FormulaTable({ formulas, stepName }: { formulas: FormulaItem[]; stepNam
<td className="p-2 border font-mono text-purple-600">{f.formula}</td>
<td className="p-2 border font-mono text-gray-600">{f.calculation}</td>
<td className="p-2 border text-right">
<span className="font-mono font-bold">{typeof f.result === 'number' ? f.result.toLocaleString() : f.result}</span>
<span className="font-mono font-bold">{typeof f.result === 'number' ? formatNumber(f.result) : f.result}</span>
<span className="text-gray-500 text-xs ml-1">{f.unit}</span>
</td>
</tr>
@@ -273,9 +274,9 @@ function FormulaTable({ formulas, stepName }: { formulas: FormulaItem[]; stepNam
<td className="p-2 border font-medium">{f.item}</td>
<td className="p-2 border font-mono text-purple-600 text-xs">{f.qty_formula}</td>
<td className="p-2 border text-right font-mono">{f.qty_result}</td>
<td className="p-2 border text-right font-mono">{f.unit_price?.toLocaleString()}</td>
<td className="p-2 border text-right font-mono">{formatNumber(f.unit_price)}</td>
<td className="p-2 border font-mono text-gray-600 text-xs">{f.price_calc}</td>
<td className="p-2 border text-right font-mono font-bold">{f.total?.toLocaleString()}</td>
<td className="p-2 border text-right font-mono font-bold">{formatNumber(f.total)}</td>
</tr>
))}
</tbody>
@@ -299,7 +300,7 @@ function FormulaTable({ formulas, stepName }: { formulas: FormulaItem[]; stepNam
<tr key={i} className="border-b hover:bg-gray-50">
<td className="p-2 border font-medium">{f.category}</td>
<td className="p-2 border text-gray-600 text-xs">{f.formula}</td>
<td className="p-2 border text-right font-mono font-bold">{f.result?.toLocaleString()}</td>
<td className="p-2 border text-right font-mono font-bold">{typeof f.result === 'number' ? formatNumber(f.result) : f.result}</td>
</tr>
))}
</tbody>

View File

@@ -8,6 +8,7 @@
import { QuoteFormData } from "./types";
import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types";
import { DocumentHeader, LotApprovalTable } from "@/components/document-system";
import { formatNumber } from '@/lib/utils/amount';
interface PurchaseOrderDocumentProps {
quote: QuoteFormData;
@@ -232,7 +233,7 @@ export function PurchaseOrderDocument({ quote, companyInfo }: PurchaseOrderDocum
<td style={{ textAlign: 'center' }}>{item.no}</td>
<td>{item.name}</td>
<td>{item.spec}</td>
<td style={{ textAlign: 'right' }}>{item.length > 0 ? item.length.toLocaleString() : ''}</td>
<td style={{ textAlign: 'right' }}>{item.length > 0 ? formatNumber(item.length) : ''}</td>
<td style={{ textAlign: 'center', fontWeight: '600' }}>{item.quantity}</td>
<td>{item.note}</td>
</tr>

View File

@@ -9,6 +9,7 @@
import { QuoteFormData } from "./types";
import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types";
import { DocumentHeader, SignatureSection } from "@/components/document-system";
import { formatNumber } from "@/lib/utils/amount";
interface QuoteDocumentProps {
quote: QuoteFormData;
@@ -18,7 +19,7 @@ interface QuoteDocumentProps {
export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) {
const formatAmount = (amount: number | undefined) => {
if (amount === undefined || amount === null) return '0';
return amount.toLocaleString('ko-KR');
return formatNumber(amount);
};
const formatDate = (dateStr: string) => {

View File

@@ -11,6 +11,7 @@
import { Save, Check, ArrowLeft, Loader2, FileText, Pencil, ClipboardList, Percent, Calculator } from "lucide-react";
import { Button } from "../ui/button";
import { formatNumber } from "@/lib/utils/amount";
// =============================================================================
// Props
@@ -92,7 +93,7 @@ export function QuoteFooterBar({
<div>
<p className="text-xs md:text-sm text-gray-600"> </p>
<p className="text-lg md:text-3xl font-bold text-blue-600">
{totalAmount.toLocaleString()}
{formatNumber(totalAmount)}
<span className="text-sm md:text-lg font-normal text-gray-500 ml-1"></span>
</p>
</div>

View File

@@ -226,15 +226,15 @@ export function QuoteManagementClient({
// 테이블 컬럼
columns: [
{ key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' },
{ key: 'quoteNumber', label: '견적번호', className: 'min-w-[120px]' },
{ key: 'registrationDate', label: '접수일', className: 'w-[100px]' },
{ key: 'status', label: '상태', className: 'w-[80px]' },
{ key: 'productCategory', label: '제품분류', className: 'w-[100px]' },
{ key: 'quantity', label: '수량', className: 'w-[60px] text-center' },
{ key: 'amount', label: '금액', className: 'w-[120px] text-right' },
{ key: 'client', label: '발주처', className: 'min-w-[100px]' },
{ key: 'site', label: '현장명', className: 'min-w-[120px]' },
{ key: 'manager', label: '담당자', className: 'w-[80px]' },
{ key: 'quoteNumber', label: '견적번호', className: 'min-w-[120px]', sortable: true },
{ key: 'registrationDate', label: '접수일', className: 'w-[100px]', sortable: true },
{ key: 'status', label: '상태', className: 'w-[80px]', sortable: true },
{ key: 'productCategory', label: '제품분류', className: 'w-[100px]', sortable: true },
{ key: 'quantity', label: '수량', className: 'w-[60px] text-center', sortable: true },
{ key: 'amount', label: '금액', className: 'w-[120px] text-right', sortable: true },
{ key: 'client', label: '발주처', className: 'min-w-[100px]', sortable: true },
{ key: 'site', label: '현장명', className: 'min-w-[120px]', sortable: true },
{ key: 'manager', label: '담당자', className: 'w-[80px]', sortable: true },
{ key: 'remarks', label: '비고', className: 'min-w-[150px]' },
{ key: 'actions', label: '작업', className: 'w-[100px]' },
],

View File

@@ -10,6 +10,7 @@
import React from 'react';
import type { QuoteFormDataV2 } from './QuoteRegistration';
import { formatNumber } from '@/lib/utils/amount';
import type { BomCalculationResultItem } from './types';
// 양식 타입
@@ -212,10 +213,10 @@ export function QuotePreviewContent({
<td className="border-r border-gray-300 px-2 py-1 text-center">{loc.quantity}</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">SET</td>
<td className="border-r border-gray-300 px-2 py-1 text-right">
{(loc.unitPrice || 0).toLocaleString()}
{formatNumber(loc.unitPrice || 0)}
</td>
<td className="px-2 py-1 text-right">
{(loc.totalPrice || 0).toLocaleString()}
{formatNumber(loc.totalPrice || 0)}
</td>
</tr>
))}
@@ -231,7 +232,7 @@ export function QuotePreviewContent({
</td>
<td className="border-r border-gray-300 px-2 py-1"></td>
<td className="border-r border-gray-300 px-2 py-1"></td>
<td className="px-2 py-1 text-right">{subtotal.toLocaleString()}</td>
<td className="px-2 py-1 text-right">{formatNumber(subtotal)}</td>
</tr>
{/* 할인율 */}
@@ -250,7 +251,7 @@ export function QuotePreviewContent({
</td>
<td className="px-2 py-1 text-right">
{hasDiscount ? `-${discountAmount.toLocaleString()}` : '0'}
{hasDiscount ? `-${formatNumber(discountAmount)}` : '0'}
</td>
</tr>
@@ -259,7 +260,7 @@ export function QuotePreviewContent({
<td colSpan={9} className="border-r border-gray-300 px-2 py-1 text-center bg-gray-50">
</td>
<td className="px-2 py-1 text-right">{afterDiscount.toLocaleString()}</td>
<td className="px-2 py-1 text-right">{formatNumber(afterDiscount)}</td>
</tr>
{/* 부가세 포함일 때 추가 행들 */}
@@ -269,13 +270,13 @@ export function QuotePreviewContent({
<td colSpan={9} className="border-r border-gray-300 px-2 py-1 text-center bg-gray-50">
</td>
<td className="px-2 py-1 text-right">{vat.toLocaleString()}</td>
<td className="px-2 py-1 text-right">{formatNumber(vat)}</td>
</tr>
<tr>
<td colSpan={9} className="border-r border-gray-300 px-2 py-1 text-center bg-gray-50 font-semibold">
</td>
<td className="px-2 py-1 text-right font-semibold">{grandTotal.toLocaleString()}</td>
<td className="px-2 py-1 text-right font-semibold">{formatNumber(grandTotal)}</td>
</tr>
</>
)}
@@ -289,7 +290,7 @@ export function QuotePreviewContent({
({vatIncluded ? '부가세 포함' : '부가세 별도'})
</span>
<span className="text-xl font-bold">
{grandTotal.toLocaleString()}
{formatNumber(grandTotal)}
</span>
</div>
@@ -346,10 +347,10 @@ export function QuotePreviewContent({
{item.unit || 'EA'}
</td>
<td className="border-r border-gray-300 px-2 py-1 text-right">
{item.unit_price.toLocaleString()}
{formatNumber(item.unit_price)}
</td>
<td className="px-2 py-1 text-right">
{item.total_price.toLocaleString()}
{formatNumber(item.total_price)}
</td>
</tr>
))
@@ -372,10 +373,10 @@ export function QuotePreviewContent({
</td>
<td className="border-r border-gray-300 px-2 py-1"></td>
<td className="border-r border-gray-300 px-2 py-1 text-right">
{(loc.bomResult?.grand_total || 0).toLocaleString()}
{formatNumber(loc.bomResult?.grand_total || 0)}
</td>
<td className="px-2 py-1 text-right font-semibold">
{(locationSubtotal * loc.quantity).toLocaleString()}
{formatNumber(locationSubtotal * loc.quantity)}
</td>
</tr>
</React.Fragment>
@@ -388,7 +389,7 @@ export function QuotePreviewContent({
</td>
<td className="px-2 py-2 text-right font-bold text-lg">
{subtotal.toLocaleString()}
{formatNumber(subtotal)}
</td>
</tr>
</tbody>

View File

@@ -53,6 +53,7 @@ import { useDevFill } from "@/components/dev/useDevFill";
import type { Vendor } from "../accounting/VendorManagement";
import type { BomMaterial, CalculationResults, BomCalculationResultItem } from "./types";
import { getLocalDateString, getDateAfterDays } from "@/lib/utils/date";
import { formatNumber } from "@/lib/utils/amount";
// =============================================================================
// 타입 정의
@@ -322,7 +323,7 @@ export function QuoteRegistration({
// 할인 적용 핸들러
const handleApplyDiscount = useCallback((rate: number, amount: number) => {
setFormData(prev => ({ ...prev, discountRate: rate, discountAmount: amount }));
toast.success(`할인이 적용되었습니다. (${rate.toFixed(1)}%, ${amount.toLocaleString()}원)`);
toast.success(`할인이 적용되었습니다. (${rate.toFixed(1)}%, ${formatNumber(amount)}원)`);
}, []);
// 개소별 합계

View File

@@ -12,6 +12,7 @@ import { useMemo } from "react";
import { Coins } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { formatNumber } from "@/lib/utils/amount";
import type { LocationItem } from "./QuoteRegistration";
@@ -175,11 +176,11 @@ export function QuoteSummaryPanel({
<div className="text-right">
<p className="text-xs text-gray-500"></p>
<p className="font-bold text-blue-600">
{loc.totalPrice.toLocaleString()}
{formatNumber(loc.totalPrice)}
</p>
{loc.unitPrice > 0 && (
<p className="text-xs text-gray-400">
: {(loc.unitPrice * loc.quantity).toLocaleString()}
: {formatNumber(loc.unitPrice * loc.quantity)}
</p>
)}
</div>
@@ -221,7 +222,7 @@ export function QuoteSummaryPanel({
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">({category.count})</span>
<span className="font-bold text-blue-600">
{category.amount.toLocaleString()}
{formatNumber(category.amount)}
</span>
</div>
</div>
@@ -232,11 +233,11 @@ export function QuoteSummaryPanel({
<div>
<span className="text-sm text-gray-700">{item.name}</span>
<p className="text-xs text-gray-400">
: {item.quantity} × : {item.unitPrice.toLocaleString()}
: {item.quantity} × : {formatNumber(item.unitPrice)}
</p>
</div>
<span className="text-sm font-medium text-blue-600">
{item.totalPrice.toLocaleString()}
{formatNumber(item.totalPrice)}
</span>
</div>
))}
@@ -258,7 +259,7 @@ export function QuoteSummaryPanel({
<div>
<p className="text-sm text-gray-400"> </p>
<p className="text-4xl font-bold text-blue-400">
{totalAmount.toLocaleString()}
{formatNumber(totalAmount)}
<span className="text-xl ml-1"></span>
</p>
</div>

View File

@@ -13,6 +13,7 @@
import { DocumentViewer } from '@/components/document-system';
import { getTodayString } from '@/lib/utils/date';
import { formatNumber } from '@/lib/utils/amount';
import type { QuoteFormDataV2 } from './QuoteRegistration';
interface QuoteTransactionModalProps {
@@ -222,10 +223,10 @@ export function QuoteTransactionModal({
<td className="border-r border-gray-300 px-2 py-1 text-center">{loc.quantity || 1}</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">SET</td>
<td className="border-r border-gray-300 px-2 py-1 text-right">
{(loc.unitPrice || 0).toLocaleString()}
{formatNumber(loc.unitPrice || 0)}
</td>
<td className="px-2 py-1 text-right">
{(loc.totalPrice || 0).toLocaleString()}
{formatNumber(loc.totalPrice || 0)}
</td>
</tr>
))
@@ -248,7 +249,7 @@ export function QuoteTransactionModal({
</td>
<td className="border-r border-gray-300 px-2 py-1"></td>
<td className="border-r border-gray-300 px-2 py-1"></td>
<td className="px-2 py-1 text-right">{subtotal.toLocaleString()}</td>
<td className="px-2 py-1 text-right">{formatNumber(subtotal)}</td>
</tr>
{/* 할인율 */}
@@ -267,7 +268,7 @@ export function QuoteTransactionModal({
</td>
<td className="px-2 py-1 text-right">
{hasDiscount ? discountAmount.toLocaleString() : '0'}
{hasDiscount ? formatNumber(discountAmount) : '0'}
</td>
</tr>
@@ -276,7 +277,7 @@ export function QuoteTransactionModal({
<td colSpan={9} className="border-r border-gray-300 px-2 py-1 text-center bg-gray-50">
</td>
<td className="px-2 py-1 text-right">{afterDiscount.toLocaleString()}</td>
<td className="px-2 py-1 text-right">{formatNumber(afterDiscount)}</td>
</tr>
{/* 부가세 포함일 때 추가 행들 */}
@@ -286,13 +287,13 @@ export function QuoteTransactionModal({
<td colSpan={9} className="border-r border-gray-300 px-2 py-1 text-center bg-gray-50">
</td>
<td className="px-2 py-1 text-right">{vat.toLocaleString()}</td>
<td className="px-2 py-1 text-right">{formatNumber(vat)}</td>
</tr>
<tr>
<td colSpan={9} className="border-r border-gray-300 px-2 py-1 text-center bg-gray-50 font-semibold">
</td>
<td className="px-2 py-1 text-right font-semibold">{grandTotal.toLocaleString()}</td>
<td className="px-2 py-1 text-right font-semibold">{formatNumber(grandTotal)}</td>
</tr>
</>
)}
@@ -306,7 +307,7 @@ export function QuoteTransactionModal({
({vatIncluded ? '부가세 포함' : '부가세 별도'})
</span>
<span className="text-xl font-bold">
{grandTotal.toLocaleString()}
{formatNumber(grandTotal)}
</span>
</div>