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

@@ -3,11 +3,12 @@
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type { ExpenseChartItem } from '../hooks/transformers';
import { formatNumber } from '@/lib/utils/amount';
function formatTooltipValue(value: number): string {
if (value >= 100000000) return `${(value / 100000000).toFixed(1)}억원`;
if (value >= 10000) return `${Math.round(value / 10000).toLocaleString()}만원`;
return `${value.toLocaleString()}`;
if (value >= 10000) return `${formatNumber(Math.round(value / 10000))}만원`;
return `${formatNumber(value)}`;
}
export function ExpenseDonutChart({ data }: { data: ExpenseChartItem[] }) {

View File

@@ -3,11 +3,12 @@
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip, Cell } from 'recharts';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type { OverviewChartItem } from '../hooks/transformers';
import { formatNumber } from '@/lib/utils/amount';
function formatTooltipValue(value: number): string {
if (value >= 100000000) return `${(value / 100000000).toFixed(1)}억원`;
if (value >= 10000) return `${Math.round(value / 10000).toLocaleString()}만원`;
return `${value.toLocaleString()}`;
if (value >= 10000) return `${formatNumber(Math.round(value / 10000))}만원`;
return `${formatNumber(value)}`;
}
export function OverviewSummaryChart({ data }: { data: OverviewChartItem[] }) {

View File

@@ -3,11 +3,12 @@
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip, Legend } from 'recharts';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type { ReceivableChartItem } from '../hooks/transformers';
import { formatNumber } from '@/lib/utils/amount';
function formatTooltipValue(value: number): string {
if (value >= 100000000) return `${(value / 100000000).toFixed(1)}억원`;
if (value >= 10000) return `${Math.round(value / 10000).toLocaleString()}만원`;
return `${value.toLocaleString()}`;
if (value >= 10000) return `${formatNumber(Math.round(value / 10000))}만원`;
return `${formatNumber(value)}`;
}
export function ReceivableBarChart({ data }: { data: ReceivableChartItem[] }) {

View File

@@ -15,6 +15,7 @@ import type {
WelfareData,
} from '@/components/business/CEODashboard/types';
import type { TodayIssueData } from '@/hooks/useCEODashboard';
import { formatNumber } from '@/lib/utils/amount';
// ============================================
// 금액 포맷 헬퍼
@@ -27,9 +28,9 @@ function formatAmount(amount: number): string {
const value = (absAmount / 100000000).toFixed(1);
return `${sign}${value}`;
} else if (absAmount >= 10000) {
return `${sign}${Math.round(absAmount / 10000).toLocaleString()}`;
return `${sign}${formatNumber(Math.round(absAmount / 10000))}`;
}
return `${sign}${absAmount.toLocaleString()}`;
return `${sign}${formatNumber(absAmount)}`;
}
function formatAmountWon(amount: number): string {
@@ -39,13 +40,13 @@ function formatAmountWon(amount: number): string {
const value = (absAmount / 100000000).toFixed(1);
return `${sign}${value}억원`;
} else if (absAmount >= 10000) {
return `${sign}${Math.round(absAmount / 10000).toLocaleString()}만원`;
return `${sign}${formatNumber(Math.round(absAmount / 10000))}만원`;
}
return `${sign}${absAmount.toLocaleString()}`;
return `${sign}${formatNumber(absAmount)}`;
}
function formatCurrency(amount: number): string {
return amount.toLocaleString();
return formatNumber(amount);
}
// ============================================

View File

@@ -7,6 +7,7 @@ import { PageHeader } from '@/components/organisms/PageHeader';
import { DashboardSwitcher } from '@/components/business/DashboardSwitcher';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from 'recharts';
import { formatNumber } from '@/lib/utils/amount';
// ============================================
// Mock 데이터
@@ -102,7 +103,7 @@ function CashflowWidget() {
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 11 }} tickFormatter={(v: number) => `${v}`} width={50} />
<Tooltip formatter={(v: number | string | undefined) => `${Number(v ?? 0).toLocaleString()}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
<Tooltip formatter={(v: number | string | undefined) => `${formatNumber(Number(v ?? 0))}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
<Bar dataKey="입금" fill="#3b82f6" radius={[3, 3, 0, 0]} maxBarSize={20} />
<Bar dataKey="출금" fill="#f97316" radius={[3, 3, 0, 0]} maxBarSize={20} />
</BarChart>
@@ -119,7 +120,7 @@ function ExpenseWidget() {
<Pie data={chartData} cx="50%" cy="50%" innerRadius={40} outerRadius={65} dataKey="value" nameKey="name">
{chartData.map((entry, i) => <Cell key={i} fill={entry.color} />)}
</Pie>
<Tooltip formatter={(v: number | string | undefined) => `${Number(v ?? 0).toLocaleString()}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
<Tooltip formatter={(v: number | string | undefined) => `${formatNumber(Number(v ?? 0))}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
</PieChart>
</ResponsiveContainer>
<div className="flex-1 space-y-2">
@@ -129,7 +130,7 @@ function ExpenseWidget() {
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: item.color }} />
<span>{item.name}</span>
</div>
<span className="font-medium">{item.value.toLocaleString()}</span>
<span className="font-medium">{formatNumber(item.value)}</span>
</div>
))}
</div>

View File

@@ -7,6 +7,7 @@ import { PageHeader } from '@/components/organisms/PageHeader';
import { DashboardSwitcher } from '@/components/business/DashboardSwitcher';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip, Cell } from 'recharts';
import { formatNumber } from '@/lib/utils/amount';
// ============================================
// Mock 데이터
@@ -211,7 +212,7 @@ function Level2({ kpi, items, onSelect, onBack }: { kpi: KpiItem; items: DetailI
<CartesianGrid strokeDasharray="3 3" horizontal={false} />
<XAxis type="number" tick={{ fontSize: 11 }} />
<YAxis dataKey="name" type="category" tick={{ fontSize: 11 }} width={100} />
<Tooltip formatter={(v: number | string | undefined) => `${Number(v ?? 0).toLocaleString()}`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
<Tooltip formatter={(v: number | string | undefined) => `${formatNumber(Number(v ?? 0))}`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
<Bar dataKey="value" fill={kpi.color} radius={[0, 4, 4, 0]} maxBarSize={22} />
</BarChart>
</ResponsiveContainer>

View File

@@ -15,6 +15,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import { formatNumber } from '@/lib/utils/amount';
// 샘플 데이터 타입
interface ProductItem {
@@ -227,7 +228,7 @@ export default function EditableTableSamplePage() {
<div className="flex justify-between items-center">
<span className="font-medium"> </span>
<span className="text-xl font-bold text-primary">
{totalAmount.toLocaleString()}
{formatNumber(totalAmount)}
</span>
</div>
</CardContent>

View File

@@ -9,6 +9,7 @@
import React from 'react';
import { DocumentHeader, QualityApprovalTable } from '@/components/document-system';
import { formatNumber } from '@/lib/utils/amount';
// 절곡품 중간검사 성적서 데이터 타입
export interface BendingInspectionData {
@@ -300,7 +301,7 @@ export const BendingInspectionDocument = ({ data = MOCK_BENDING_INSPECTION }: Be
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.length > 0 ? item.length.toLocaleString() : ''}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.length > 0 ? formatNumber(item.length) : ''}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.conductance1}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.measured1}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.point}</td>

View File

@@ -9,6 +9,7 @@
import React from 'react';
import { DocumentHeader, QualityApprovalTable } from '@/components/document-system';
import { formatNumber } from '@/lib/utils/amount';
// 스크린 중간검사 성적서 데이터 타입
export interface ScreenInspectionData {
@@ -259,8 +260,8 @@ export const ScreenInspectionDocument = ({ data = MOCK_SCREEN_INSPECTION }: Scre
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
<td className="border border-gray-400 px-1 py-1 text-center font-medium">{item.height.standard.toLocaleString()}</td>
<td className="border border-gray-400 px-1 py-1 text-center font-medium">{item.width.standard.toLocaleString()}</td>
<td className="border border-gray-400 px-1 py-1 text-center font-medium">{formatNumber(item.height.standard)}</td>
<td className="border border-gray-400 px-1 py-1 text-center font-medium">{formatNumber(item.width.standard)}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.checkCount}</td>
<td className="border border-gray-400 px-1 py-1 text-center">
OK NG

View File

@@ -9,6 +9,7 @@
import React from 'react';
import { DocumentHeader, QualityApprovalTable } from '@/components/document-system';
import { formatNumber } from '@/lib/utils/amount';
// 슬랫 중간검사 성적서 데이터 타입
export interface SlatInspectionData {
@@ -241,7 +242,7 @@ export const SlatInspectionDocument = ({ data = MOCK_SLAT_INSPECTION }: SlatInsp
<td className="border border-gray-400 px-1 py-1 text-center">{item.height1.standard} ± 1</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.height1.measured}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.bandLength.conductance}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.bandLength.measured > 0 ? item.bandLength.measured.toLocaleString() : ''}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.bandLength.measured > 0 ? formatNumber(item.bandLength.measured) : ''}</td>
<td className="border border-gray-400 px-1 py-1 text-center">
<br/>
</td>

View File

@@ -262,12 +262,12 @@ export default function CustomerAccountManagementPage() {
// 테이블 컬럼 정의 (Hooks 순서 보장을 위해 조건부 return 전에 정의)
const tableColumns: TableColumn[] = useMemo(() => [
{ key: "rowNumber", label: "번호", className: "px-4" },
{ key: "code", label: "코드", className: "px-4" },
{ key: "clientType", label: "구분", className: "px-4" },
{ key: "name", label: "거래처명", className: "px-4" },
{ key: "representative", label: "대표자", className: "px-4" },
{ key: "manager", label: "담당자", className: "px-4" },
{ key: "phone", label: "전화번호", className: "px-4" },
{ key: "code", label: "코드", className: "px-4", sortable: true },
{ key: "clientType", label: "구분", className: "px-4", sortable: true },
{ key: "name", label: "거래처명", className: "px-4", sortable: true },
{ key: "representative", label: "대표자", className: "px-4", sortable: true },
{ key: "manager", label: "담당자", className: "px-4", sortable: true },
{ key: "phone", label: "전화번호", className: "px-4", sortable: true },
], []);
// 핸들러 - 페이지 기반 네비게이션

View File

@@ -38,7 +38,7 @@ import { toast } from "sonner";
import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate";
import { orderSalesConfig } from "@/components/orders/orderSalesConfig";
import { BadgeSm } from "@/components/atoms/BadgeSm";
import { formatAmount } from "@/lib/utils/amount";
import { formatAmount, formatNumber } from "@/lib/utils/amount";
import {
OrderItem,
getOrderById,
@@ -57,7 +57,7 @@ function formatQuantity(quantity: number, unit?: string): string {
if (countableUnits.includes(upperUnit)) {
// 개수 단위는 정수로 반올림
return Math.round(quantity).toLocaleString();
return formatNumber(Math.round(quantity));
}
// 측정 단위는 소수점 4자리까지 반올림 후 불필요한 0 제거

View File

@@ -44,7 +44,7 @@ import { toast } from "sonner";
import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate";
import { orderSalesConfig } from "@/components/orders/orderSalesConfig";
import { BadgeSm } from "@/components/atoms/BadgeSm";
import { formatAmount } from "@/lib/utils/amount";
import { formatAmount, formatNumber } from "@/lib/utils/amount";
import {
Dialog,
DialogContent,
@@ -87,7 +87,7 @@ function formatQuantity(quantity: number, unit?: string): string {
if (countableUnits.includes(upperUnit)) {
// 개수 단위는 정수로 반올림
return Math.round(quantity).toLocaleString();
return formatNumber(Math.round(quantity));
}
// 측정 단위는 소수점 4자리까지 반올림 후 불필요한 0 제거

View File

@@ -475,19 +475,19 @@ function OrderListContent() {
// 테이블 컬럼 정의 (16개: 체크박스, 번호, 로트번호, 현장명, 출고예정일, 접수일, 수주처, 제품명, 수신자, 수신주소, 수신처, 배송, 담당자, 틀수, 상태, 비고)
const tableColumns: TableColumn[] = useMemo(() => [
{ key: "rowNumber", label: "번호", className: "px-2 text-center" },
{ key: "lotNumber", label: "로트번호", className: "px-2" },
{ key: "siteName", label: "현장명", className: "px-2" },
{ key: "expectedShipDate", label: "출고예정일", className: "px-2" },
{ key: "orderDate", label: "수주일", className: "px-2" },
{ key: "client", label: "수주처", className: "px-2" },
{ key: "productName", label: "제품명", className: "px-2" },
{ key: "receiver", label: "수신자", className: "px-2" },
{ key: "receiverAddress", label: "수신주소", className: "px-2" },
{ key: "receiverPlace", label: "수신처", className: "px-2" },
{ key: "deliveryMethod", label: "배송", className: "px-2" },
{ key: "manager", label: "담당자", className: "px-2" },
{ key: "frameCount", label: "틀수", className: "px-2 text-center" },
{ key: "status", label: "상태", className: "px-2" },
{ key: "lotNumber", label: "로트번호", className: "px-2", sortable: true },
{ key: "siteName", label: "현장명", className: "px-2", sortable: true },
{ key: "expectedShipDate", label: "출고예정일", className: "px-2", sortable: true },
{ key: "orderDate", label: "수주일", className: "px-2", sortable: true },
{ key: "client", label: "수주처", className: "px-2", sortable: true },
{ key: "productName", label: "제품명", className: "px-2", sortable: true },
{ key: "receiver", label: "수신자", className: "px-2", sortable: true },
{ key: "receiverAddress", label: "수신주소", className: "px-2", sortable: true },
{ key: "receiverPlace", label: "수신처", className: "px-2", sortable: true },
{ key: "deliveryMethod", label: "배송", className: "px-2", sortable: true },
{ key: "manager", label: "담당자", className: "px-2", sortable: true },
{ key: "frameCount", label: "틀수", className: "px-2 text-center", sortable: true },
{ key: "status", label: "상태", className: "px-2", sortable: true },
{ key: "remarks", label: "비고", className: "px-2" },
], []);

View File

@@ -46,6 +46,7 @@ import {
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { toast } from "sonner";
import { ServerErrorPage } from "@/components/common/ServerErrorPage";
import { formatNumber } from '@/lib/utils/amount';
// 생산지시 상태 타입
type ProductionOrderStatus = "waiting" | "in_progress" | "completed";
@@ -590,7 +591,7 @@ export default function ProductionOrderDetailPage() {
</code>
</TableCell>
<TableCell className="text-right">
{item.requiredQty > 0 ? item.requiredQty.toLocaleString() : "-"}
{item.requiredQty > 0 ? formatNumber(item.requiredQty) : "-"}
</TableCell>
<TableCell className="text-center">{item.qty}</TableCell>
</TableRow>