refactor(WEB): 회계/견적/설정/생산 등 전반적 코드 개선 및 공통화 2차
- 회계 모듈 전면 개선: 청구/입금/출금/매입/매출/세금계산서/일반전표/거래처원장 등 - 견적 모듈 금액 포맷/할인/수식/미리보기 등 코드 정리 - 설정 모듈: 계정관리/직급/직책/권한 상세 간소화 - 생산 모듈: 작업지시서/작업자화면/검수 문서 코드 정리 - UniversalListPage 엑셀 다운로드 및 필터 기능 확장 - 대시보드/게시판/수주 등 날짜 유틸 공통화 적용 - claudedocs 문서 인덱스 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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[] }) {
|
||||
|
||||
@@ -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[] }) {
|
||||
|
||||
@@ -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[] }) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
], []);
|
||||
|
||||
// 핸들러 - 페이지 기반 네비게이션
|
||||
|
||||
@@ -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 제거
|
||||
|
||||
@@ -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 제거
|
||||
|
||||
@@ -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" },
|
||||
], []);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user