diff --git a/claudedocs/[REF-2026-02-19] code-dedup-commonization-checklist.md b/claudedocs/[REF-2026-02-19] code-dedup-commonization-checklist.md index c69efde2..12e741e1 100644 --- a/claudedocs/[REF-2026-02-19] code-dedup-commonization-checklist.md +++ b/claudedocs/[REF-2026-02-19] code-dedup-commonization-checklist.md @@ -297,6 +297,77 @@ Phase 3 (Phase 2 완료 후): | 2 | WP-4 + WP-5 | WP-4는 hooks/ + page.tsx, WP-5는 components/accounting/shared/ + 컴포넌트 내부. 파일 겹침 없음 | | 3 | WP-6 단독 | WP-4의 useDateRange 훅을 일부 파일에서 활용해야 하므로 Phase 2 이후 | +--- + +## WP-7: .toLocaleString() → formatNumber() 마이그레이션 ✅ 완료 (2026-02-19) + +**심각도**: 🟢 MEDIUM (코드 일관성) +**난이도**: 낮음 | **파일 수**: 52파일, 183건 | **방법**: Node.js 자동 마이그레이션 스크립트 + +### 수정 완료 +- [x] 52개 파일에서 183건 `.toLocaleString()` → `formatNumber()` 자동 치환 +- [x] `formatNumber` import 자동 추가 +- [x] 5개 파일 수동 import 수정 (double-quote import 패턴) + +### 검증 +- [x] `npx tsc --noEmit` 통과 +- [x] `src/` 내 잔여 `.toLocaleString()` = 0건 (amount.ts 내부 제외) + +--- + +## WP-8: 인라인 formatDate → date.ts import 마이그레이션 ✅ 완료 (2026-02-19) + +**심각도**: 🟢 MEDIUM (코드 일관성) +**난이도**: 낮음 | **파일 수**: 21파일, 32건 | **방법**: Node.js 자동 마이그레이션 스크립트 + +### 수정 완료 +- [x] `.split('T')[0]` → `formatDate()` 변환 (21파일) +- [x] `new Date().toISOString().slice(0, 10)` → `getTodayString()` 변환 +- [x] `dateVar.toISOString().slice(0, 10)` → `getLocalDateString(dateVar)` 변환 +- [x] 6개 파일 수동 import 수정 (JSDoc 삽입 버그) + +### 스킵 항목 +- `?.split('T')[0] || ''` — formatDate는 null일 때 '-' 반환 (falsy 의미 다름) +- `.toLocaleDateString()` — 42건, 로케일 표시 형식으로 canonical과 비호환 +- `.toISOString().slice(0,10).replace(/-/g,'')` — YYYYMMDD 형식 (파일명용) + +### 검증 +- [x] `npx tsc --noEmit` 통과 + +--- + +## WP-9: useDeleteDialog 훅 채택 확대 ✅ 완료 (2026-02-20) + +**심각도**: 🟢 MEDIUM (코드 일관성) +**난이도**: 중간 | **전체 후보**: ~56파일 | **기존 채택자**: 7파일 + +### 완료된 작업 (9파일) +- [x] `src/hooks/index.ts`에 useDeleteDialog export 추가 +- [x] `settings/RankManagement/index.tsx` 마이그레이션 (단건 삭제, number ID) +- [x] `settings/TitleManagement/index.tsx` 마이그레이션 (단건 삭제, number ID) +- [x] `settings/AccountManagement/AccountDetail.tsx` 마이그레이션 (상세→삭제→목록, number ID) +- [x] `settings/PermissionManagement/PermissionDetailClient.tsx` 마이그레이션 (상세→삭제→목록, number ID) +- [x] `accounting/DepositManagement/DepositDetail.tsx` 마이그레이션 (상세→삭제→목록) +- [x] `accounting/WithdrawalManagement/WithdrawalDetail.tsx` 마이그레이션 (상세→삭제→목록) +- [x] `hr/CardManagement/CardDetail.tsx` 마이그레이션 (상세→삭제→목록) +- [x] `board/BoardDetail/index.tsx` 마이그레이션 (deletePost 클로저 캡처, 2-arg delete) +- [x] `board/BoardManagement/BoardDetailClientV2.tsx` 마이그레이션 (deleteBoard + forceRefreshMenus) + +### 의도적 스킵 (마이그레이션 비적합) +- `settings/AccountManagement/AccountDetailForm.tsx` — prop 기반 삭제 + isSaving 공유 +- `settings/PermissionManagement/PermissionDetail.tsx` — prop 기반 + IntegratedDetailTemplate 연동 +- `settings/PermissionManagement/index.tsx` — bulk+single 혼합 다이얼로그 +- `board/CommentSection/CommentItem.tsx` — void onDelete prop, 행동 변경됨 +- `hr/EmployeeManagement/index.tsx` — 780줄, 복잡한 useMemo, 위험도 높음 +- `hr/DepartmentManagement/index.tsx` — bulk+single 혼합 다이얼로그 +- `settings/PopupManagement/PopupDetailClientV2.tsx` — IntegratedDetailTemplate 내부 처리 + +### 최종 채택 현황: 7(기존) + 9(신규) = 16파일 +### 검증 +- [x] `npx tsc --noEmit` 통과 (모든 수정 파일 tsc 에러 0건) + +--- + ### Phase 간 의존성 상세 | 의존 관계 | 이유 | diff --git a/claudedocs/_index.md b/claudedocs/_index.md index da8480ed..5b5b183d 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -1,6 +1,6 @@ # claudedocs 문서 맵 -> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-02-12) +> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-02-19) ## 빠른 참조 @@ -214,6 +214,62 @@ export const remove = service.remove; - `toPaginationMeta` 자동 활용 (직접 import 불필요) - URL 빌딩 패턴 완전 일관화 (undefined/null/'' 자동 필터링, boolean/number 자동 변환) +### KST 안전 날짜 유틸리티 — `toISOString` 사용 금지 (2026-02-19) + +**현황**: `new Date().toISOString().split('T')[0]` — 15개 파일 26곳에서 사용 중이었음 + +**문제**: `toISOString()`은 **UTC 기준**으로 변환. 한국(KST, UTC+9)에서 오전 9시 이전에 실행하면 **전날 날짜** 반환 +``` +// 2026-02-19 08:30 KST → UTC는 2026-02-18 23:30 +new Date().toISOString().split('T')[0] // "2026-02-18" ← 잘못됨 +``` + +**결정**: KST 안전 유틸리티 함수로 전량 교체, 직접 `toISOString` 사용 금지 + +**유틸리티** (`src/lib/utils/date.ts`): +| 함수 | 용도 | 대체 대상 | +|------|------|-----------| +| `getTodayString()` | 오늘 날짜 문자열 | `new Date().toISOString().split('T')[0]` | +| `getLocalDateString(date)` | 임의 Date 객체 문자열 | `someDate.toISOString().split('T')[0]` | + +**사용 규칙**: +```typescript +// ✅ 올바른 패턴 +import { getTodayString, getLocalDateString } from '@/lib/utils/date'; +const today = getTodayString(); // "2026-02-19" +const thirtyDaysAgo = getLocalDateString(pastDate); // "2026-01-20" + +// ❌ 금지 패턴 +const today = new Date().toISOString().split('T')[0]; +``` + +**현재 상태**: `src/` 내 `toISOString().split` 사용 0건 (date.ts 내 구현부 제외) + +### `useDateRange` 훅 — 날짜 필터 보일러플레이트 제거 (2026-02-19) + +**현황**: 20+ 리스트 페이지에서 `useState('2025-01-01')` / `useState('2025-12-31')` 하드코딩 + +**문제**: 연도가 바뀌면 수동으로 모든 파일 수정 필요 (2025→2026 전환 시 데이터 미표시 버그 발생) + +**결정**: `useDateRange` 훅으로 동적 날짜 범위 자동 계산 + +**훅** (`src/hooks/useDateRange.ts`): +```typescript +import { useDateRange } from '@/hooks'; + +// 프리셋 +const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear'); // 2026-01-01 ~ 2026-12-31 +const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentMonth'); // 2026-02-01 ~ 2026-02-28 +const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today'); // 2026-02-19 ~ 2026-02-19 +``` + +**적용 규칙**: +- 리스트 페이지 날짜 필터 → `useDateRange` 필수 사용 +- 연간 조회 → `'currentYear'`, 월간 조회 → `'currentMonth'` +- `useState('YYYY-MM-DD')` 하드코딩 금지 + +**현재 상태**: `useState('2025` 패턴 0건 (전량 `useDateRange`로 전환 완료) + ### Zod 스키마 검증 — 신규 폼 적용 규칙 (2026-02-11) **결정**: 기존 폼은 건드리지 않음. **신규 폼에만 Zod + zodResolver 적용** diff --git a/src/app/[locale]/(protected)/dashboard_type2/_components/charts/ExpenseDonutChart.tsx b/src/app/[locale]/(protected)/dashboard_type2/_components/charts/ExpenseDonutChart.tsx index 8451e859..6527ffe7 100644 --- a/src/app/[locale]/(protected)/dashboard_type2/_components/charts/ExpenseDonutChart.tsx +++ b/src/app/[locale]/(protected)/dashboard_type2/_components/charts/ExpenseDonutChart.tsx @@ -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[] }) { diff --git a/src/app/[locale]/(protected)/dashboard_type2/_components/charts/OverviewSummaryChart.tsx b/src/app/[locale]/(protected)/dashboard_type2/_components/charts/OverviewSummaryChart.tsx index 70e7cdbf..8fb96bd3 100644 --- a/src/app/[locale]/(protected)/dashboard_type2/_components/charts/OverviewSummaryChart.tsx +++ b/src/app/[locale]/(protected)/dashboard_type2/_components/charts/OverviewSummaryChart.tsx @@ -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[] }) { diff --git a/src/app/[locale]/(protected)/dashboard_type2/_components/charts/ReceivableBarChart.tsx b/src/app/[locale]/(protected)/dashboard_type2/_components/charts/ReceivableBarChart.tsx index 457f10ba..c318671b 100644 --- a/src/app/[locale]/(protected)/dashboard_type2/_components/charts/ReceivableBarChart.tsx +++ b/src/app/[locale]/(protected)/dashboard_type2/_components/charts/ReceivableBarChart.tsx @@ -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[] }) { diff --git a/src/app/[locale]/(protected)/dashboard_type2/_components/hooks/transformers.ts b/src/app/[locale]/(protected)/dashboard_type2/_components/hooks/transformers.ts index 4acade83..836409bf 100644 --- a/src/app/[locale]/(protected)/dashboard_type2/_components/hooks/transformers.ts +++ b/src/app/[locale]/(protected)/dashboard_type2/_components/hooks/transformers.ts @@ -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); } // ============================================ diff --git a/src/app/[locale]/(protected)/dashboard_type3/_components/DashboardType3.tsx b/src/app/[locale]/(protected)/dashboard_type3/_components/DashboardType3.tsx index 36ccae5d..7d34a450 100644 --- a/src/app/[locale]/(protected)/dashboard_type3/_components/DashboardType3.tsx +++ b/src/app/[locale]/(protected)/dashboard_type3/_components/DashboardType3.tsx @@ -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() { `${v}만`} width={50} /> - `${Number(v ?? 0).toLocaleString()}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} /> + `${formatNumber(Number(v ?? 0))}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} /> @@ -119,7 +120,7 @@ function ExpenseWidget() { {chartData.map((entry, i) => )} - `${Number(v ?? 0).toLocaleString()}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} /> + `${formatNumber(Number(v ?? 0))}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
@@ -129,7 +130,7 @@ function ExpenseWidget() {
{item.name}
- {item.value.toLocaleString()}만 + {formatNumber(item.value)}만
))} diff --git a/src/app/[locale]/(protected)/dashboard_type4/_components/DashboardType4.tsx b/src/app/[locale]/(protected)/dashboard_type4/_components/DashboardType4.tsx index 3509f9f9..6090a717 100644 --- a/src/app/[locale]/(protected)/dashboard_type4/_components/DashboardType4.tsx +++ b/src/app/[locale]/(protected)/dashboard_type4/_components/DashboardType4.tsx @@ -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 - `${Number(v ?? 0).toLocaleString()}원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} /> + `${formatNumber(Number(v ?? 0))}원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} /> diff --git a/src/app/[locale]/(protected)/dev/editable-table/page.tsx b/src/app/[locale]/(protected)/dev/editable-table/page.tsx index ae2bb57e..e445b695 100644 --- a/src/app/[locale]/(protected)/dev/editable-table/page.tsx +++ b/src/app/[locale]/(protected)/dev/editable-table/page.tsx @@ -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() {
총 합계 - {totalAmount.toLocaleString()}원 + {formatNumber(totalAmount)}원
diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/BendingInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/BendingInspectionDocument.tsx index db5d7b17..969cfc1a 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/BendingInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/BendingInspectionDocument.tsx @@ -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 ☐ 양호 ☐ 불량 - {item.length > 0 ? item.length.toLocaleString() : ''} + {item.length > 0 ? formatNumber(item.length) : ''} {item.conductance1} {item.measured1} {item.point} diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/ScreenInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/ScreenInspectionDocument.tsx index 3e99eaa7..78526b92 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/ScreenInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/ScreenInspectionDocument.tsx @@ -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 ☐ 양호 ☐ 불량 - {item.height.standard.toLocaleString()} - {item.width.standard.toLocaleString()} + {formatNumber(item.height.standard)} + {formatNumber(item.width.standard)} {item.checkCount} ☐ OK ☐ NG diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/SlatInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/SlatInspectionDocument.tsx index b7798274..8b5b37e1 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/SlatInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/SlatInspectionDocument.tsx @@ -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 {item.height1.standard} ± 1 {item.height1.measured} {item.bandLength.conductance} - {item.bandLength.measured > 0 ? item.bandLength.measured.toLocaleString() : ''} + {item.bandLength.measured > 0 ? formatNumber(item.bandLength.measured) : ''} ☐ 적합 ☐ 부
적합 diff --git a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx index 5646adc2..c39022d6 100644 --- a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx +++ b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx @@ -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 }, ], []); // 핸들러 - 페이지 기반 네비게이션 diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx index 518d836d..e5fd0b19 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx @@ -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 제거 diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx index ad4c1f57..8bc53653 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx @@ -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 제거 diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx index 9d2a8a05..2927336e 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx @@ -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" }, ], []); diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx index 6f7fb13f..046bd585 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx @@ -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() { - {item.requiredQty > 0 ? item.requiredQty.toLocaleString() : "-"} + {item.requiredQty > 0 ? formatNumber(item.requiredQty) : "-"} {item.qty} diff --git a/src/components/accounting/BadDebtCollection/index.tsx b/src/components/accounting/BadDebtCollection/index.tsx index 820a8f17..df0017f7 100644 --- a/src/components/accounting/BadDebtCollection/index.tsx +++ b/src/components/accounting/BadDebtCollection/index.tsx @@ -42,6 +42,7 @@ import { STATUS_BADGE_STYLES, SORT_OPTIONS, } from './types'; +import { formatNumber } from '@/lib/utils/amount'; import { deleteBadDebt, toggleBadDebt } from './actions'; // ===== 테이블 컬럼 정의 ===== @@ -324,25 +325,25 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec computeStats: (): StatCard[] => [ { label: '총 악성채권', - value: `${statsData.totalAmount.toLocaleString()}원`, + value: `${formatNumber(statsData.totalAmount)}원`, icon: AlertTriangle, iconColor: 'text-red-500', }, { label: '추심중', - value: `${statsData.collectingAmount.toLocaleString()}원`, + value: `${formatNumber(statsData.collectingAmount)}원`, icon: AlertTriangle, iconColor: 'text-orange-500', }, { label: '법적조치', - value: `${statsData.legalActionAmount.toLocaleString()}원`, + value: `${formatNumber(statsData.legalActionAmount)}원`, icon: AlertTriangle, iconColor: 'text-red-600', }, { label: '회수완료', - value: `${statsData.recoveredAmount.toLocaleString()}원`, + value: `${formatNumber(statsData.recoveredAmount)}원`, icon: AlertTriangle, iconColor: 'text-green-500', }, @@ -375,7 +376,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec {item.vendorName} {/* 채권금액 */} - {item.debtAmount.toLocaleString()}원 + {formatNumber(item.debtAmount)}원 {/* 발생일 */} {item.occurrenceDate} @@ -410,7 +411,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec { const raw = e.target.value.replace(/[^0-9]/g, ''); handleChange('amount', raw ? parseInt(raw, 10) : 0); @@ -344,7 +345,7 @@ export function TransactionFormModal({
diff --git a/src/components/accounting/BankTransactionInquiry/index.tsx b/src/components/accounting/BankTransactionInquiry/index.tsx index 8e811171..9ef563ad 100644 --- a/src/components/accounting/BankTransactionInquiry/index.tsx +++ b/src/components/accounting/BankTransactionInquiry/index.tsx @@ -56,6 +56,7 @@ import { } from './actions'; import { TransactionFormModal } from './TransactionFormModal'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { formatNumber } from '@/lib/utils/amount'; // ===== 테이블 컬럼 정의 (체크박스 제외 10개) ===== const tableColumns = [ @@ -410,10 +411,10 @@ export function BankTransactionInquiry() { 합계 - {tableTotals.totalDeposit.toLocaleString()} + {formatNumber(tableTotals.totalDeposit)} - {tableTotals.totalWithdrawal.toLocaleString()} + {formatNumber(tableTotals.totalWithdrawal)} @@ -440,19 +441,19 @@ export function BankTransactionInquiry() { computeStats: (): StatCard[] => [ { label: '입금', - value: `${summary.totalDeposit.toLocaleString()}원`, + value: `${formatNumber(summary.totalDeposit)}원`, icon: Building2, iconColor: 'text-blue-500', }, { label: '출금', - value: `${summary.totalWithdrawal.toLocaleString()}원`, + value: `${formatNumber(summary.totalWithdrawal)}원`, icon: Building2, iconColor: 'text-red-500', }, { label: '잔고', - value: `${summary.totalBalance.toLocaleString()}원`, + value: `${formatNumber(summary.totalBalance)}원`, icon: Building2, iconColor: 'text-green-500', }, @@ -542,15 +543,15 @@ export function BankTransactionInquiry() { {/* 입금 */} - {item.depositAmount > 0 ? item.depositAmount.toLocaleString() : '-'} + {item.depositAmount > 0 ? formatNumber(item.depositAmount) : '-'} {/* 출금 */} - {item.withdrawalAmount > 0 ? item.withdrawalAmount.toLocaleString() : '-'} + {item.withdrawalAmount > 0 ? formatNumber(item.withdrawalAmount) : '-'} {/* 잔액 */} - {item.balance.toLocaleString()} + {formatNumber(item.balance)} {/* 취급점 */} @@ -590,13 +591,13 @@ export function BankTransactionInquiry() { { label: '적요', value: item.note || '-' }, { label: '입금', - value: item.depositAmount > 0 ? `${item.depositAmount.toLocaleString()}원` : '-', + value: item.depositAmount > 0 ? `${formatNumber(item.depositAmount)}원` : '-', }, { label: '출금', - value: item.withdrawalAmount > 0 ? `${item.withdrawalAmount.toLocaleString()}원` : '-', + value: item.withdrawalAmount > 0 ? `${formatNumber(item.withdrawalAmount)}원` : '-', }, - { label: '잔액', value: `${item.balance.toLocaleString()}원` }, + { label: '잔액', value: `${formatNumber(item.balance)}원` }, { label: '취급점', value: item.branch || '-' }, { label: '예금주', value: item.depositorName || '-' }, ]} diff --git a/src/components/accounting/BillManagement/BillDetail.tsx b/src/components/accounting/BillManagement/BillDetail.tsx index 4dc17028..6332a995 100644 --- a/src/components/accounting/BillManagement/BillDetail.tsx +++ b/src/components/accounting/BillManagement/BillDetail.tsx @@ -35,7 +35,7 @@ import { import { getBill, createBill, updateBill, deleteBill, getClients } from './actions'; // ===== 새 훅 import ===== -import { useDetailData, useCRUDHandlers } from '@/hooks'; +import { useDetailData } from '@/hooks'; // ===== Props ===== interface BillDetailProps { @@ -178,68 +178,44 @@ export function BillDetail({ billId, mode }: BillDetailProps) { return { valid: true }; }, [formData]); - // ===== 타입 래퍼: 훅은 string | number를 받지만 actions는 string만 받음 ===== - const updateBillWrapper = useCallback( - (id: string | number, data: Partial) => updateBill(String(id), data), - [] - ); + // ===== 제출 상태 ===== + const [isSubmitting, setIsSubmitting] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); - const deleteBillWrapper = useCallback( - (id: string | number) => deleteBill(String(id)), - [] - ); - - // ===== 새 훅: useCRUDHandlers로 CRUD 처리 ===== - const { - handleCreate, - handleUpdate, - handleDelete: crudDelete, - isSubmitting, - isDeleting, - } = useCRUDHandlers, Partial>({ - onCreate: createBill, - onUpdate: updateBillWrapper, - onDelete: deleteBillWrapper, - successRedirect: '/ko/accounting/bills', - successMessages: { - create: '어음이 등록되었습니다.', - update: '어음이 수정되었습니다.', - delete: '어음이 삭제되었습니다.', - }, - // 수정 성공 시 view 모드로 이동 - disableRedirect: !isNewMode, - onSuccess: (action) => { - if (action === 'update') { - router.push(`/ko/accounting/bills/${billId}?mode=view`); - } - }, - }); - - // ===== 저장 핸들러 (유효성 검사 + CRUD 훅 사용) ===== + // ===== 저장 핸들러 (결과만 반환, 토스트/리다이렉트는 IntegratedDetailTemplate에 위임) ===== const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { - // 유효성 검사 const validation = validateForm(); if (!validation.valid) { toast.error(validation.error!); return { success: false, error: validation.error }; } - const billData: Partial = { - ...formData, - vendorName: clients.find(c => c.id === formData.vendorId)?.name || '', - }; + setIsSubmitting(true); + try { + const billData: Partial = { + ...formData, + vendorName: clients.find(c => c.id === formData.vendorId)?.name || '', + }; - if (isNewMode) { - return handleCreate(billData); - } else { - return handleUpdate(billId, billData); + if (isNewMode) { + return await createBill(billData); + } else { + return await updateBill(String(billId), billData); + } + } finally { + setIsSubmitting(false); } - }, [formData, clients, isNewMode, billId, handleCreate, handleUpdate, validateForm]); + }, [formData, clients, isNewMode, billId, validateForm]); - // ===== 삭제 핸들러 ===== + // ===== 삭제 핸들러 (결과만 반환, 토스트/리다이렉트는 IntegratedDetailTemplate에 위임) ===== const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => { - return crudDelete(billId); - }, [billId, crudDelete]); + setIsDeleting(true); + try { + return await deleteBill(String(billId)); + } finally { + setIsDeleting(false); + } + }, [billId]); // ===== 차수 관리 핸들러 ===== const handleAddInstallment = useCallback(() => { diff --git a/src/components/accounting/BillManagement/BillManagementClient.tsx b/src/components/accounting/BillManagement/BillManagementClient.tsx index 1bb84af9..7b28fbc3 100644 --- a/src/components/accounting/BillManagement/BillManagementClient.tsx +++ b/src/components/accounting/BillManagement/BillManagementClient.tsx @@ -12,6 +12,7 @@ import { useState, useMemo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; +import { formatNumber } from '@/lib/utils/amount'; import { useDateRange } from '@/hooks'; import { FileText, @@ -95,7 +96,8 @@ export function BillManagementClient({ onDelete: async (id) => { const result = await deleteBill(id); if (result.success) { - setData(prev => prev.filter(item => item.id !== id)); + // 서버에서 재조회 (로컬 필터링 대신 - 페이지네이션 정합성 보장) + await loadData(currentPage); setSelectedItems(prev => { const newSet = new Set(prev); newSet.delete(id); @@ -174,14 +176,14 @@ export function BillManagementClient({ // ===== 테이블 컬럼 ===== const tableColumns: TableColumn[] = useMemo(() => [ { key: 'no', label: '번호', className: 'text-center w-[60px]' }, - { key: 'billNumber', label: '어음번호' }, - { key: 'billType', label: '구분', className: 'text-center' }, - { key: 'vendorName', label: '거래처' }, - { key: 'amount', label: '금액', className: 'text-right' }, - { key: 'issueDate', label: '발행일' }, - { key: 'maturityDate', label: '만기일' }, - { key: 'installmentCount', label: '차수', className: 'text-center' }, - { key: 'status', label: '상태', className: 'text-center' }, + { key: 'billNumber', label: '어음번호', sortable: true }, + { key: 'billType', label: '구분', className: 'text-center', sortable: true }, + { key: 'vendorName', label: '거래처', sortable: true }, + { key: 'amount', label: '금액', className: 'text-right', sortable: true }, + { key: 'issueDate', label: '발행일', sortable: true }, + { key: 'maturityDate', label: '만기일', sortable: true }, + { key: 'installmentCount', label: '차수', className: 'text-center', sortable: true }, + { key: 'status', label: '상태', className: 'text-center', sortable: true }, ], []); // ===== 테이블 행 렌더링 ===== @@ -208,7 +210,7 @@ export function BillManagementClient({ {item.vendorName} - {item.amount.toLocaleString()} + {formatNumber(item.amount)} {item.issueDate} {item.maturityDate} {item.installmentCount || '-'} @@ -247,7 +249,7 @@ export function BillManagementClient({ infoGrid={
- +
diff --git a/src/components/accounting/BillManagement/index.tsx b/src/components/accounting/BillManagement/index.tsx index 98f5419b..11c9ad62 100644 --- a/src/components/accounting/BillManagement/index.tsx +++ b/src/components/accounting/BillManagement/index.tsx @@ -14,6 +14,7 @@ import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; +import { formatNumber } from '@/lib/utils/amount'; import { getBills, deleteBill, updateBillStatus } from './actions'; import { useDateRange } from '@/hooks'; import { createDeleteItemHandler, extractUniqueOptions } from '../shared'; @@ -397,7 +398,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem {/* 거래처 */} {item.vendorName} {/* 금액 */} - {item.amount.toLocaleString()} + {formatNumber(item.amount)} {/* 발행일 */} {item.issueDate} {/* 만기일 */} @@ -453,7 +454,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem onToggle={handlers.onToggle} onClick={() => handleRowClick(item)} details={[ - { label: '금액', value: `${item.amount.toLocaleString()}원` }, + { label: '금액', value: `${formatNumber(item.amount)}원` }, { label: '발행일', value: item.issueDate }, { label: '만기일', value: item.maturityDate }, { label: '상태', value: getBillStatusLabel(item.billType, item.status) }, diff --git a/src/components/accounting/CardTransactionInquiry/JournalEntryModal.tsx b/src/components/accounting/CardTransactionInquiry/JournalEntryModal.tsx index 639e903c..c287fdbf 100644 --- a/src/components/accounting/CardTransactionInquiry/JournalEntryModal.tsx +++ b/src/components/accounting/CardTransactionInquiry/JournalEntryModal.tsx @@ -3,6 +3,7 @@ import { useState, useCallback } from 'react'; import { toast } from 'sonner'; import { Loader2, Minus, Plus } from 'lucide-react'; +import { formatNumber } from '@/lib/utils/amount'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -134,15 +135,15 @@ export function JournalEntryModal({ open, onOpenChange, transaction, onSuccess }
-

{transaction.supplyAmount.toLocaleString()}

+

{formatNumber(transaction.supplyAmount)}

-

{transaction.taxAmount.toLocaleString()}

+

{formatNumber(transaction.taxAmount)}

-

{transaction.totalAmount.toLocaleString()}

+

{formatNumber(transaction.totalAmount)}

@@ -269,7 +270,7 @@ export function JournalEntryModal({ open, onOpenChange, transaction, onSuccess } {/* 분개 합계 */}
분개 합계 - {journalTotal.toLocaleString()}원 + {formatNumber(journalTotal)}원
diff --git a/src/components/accounting/CardTransactionInquiry/ManualInputModal.tsx b/src/components/accounting/CardTransactionInquiry/ManualInputModal.tsx index 26b87a73..9ee00fe2 100644 --- a/src/components/accounting/CardTransactionInquiry/ManualInputModal.tsx +++ b/src/components/accounting/CardTransactionInquiry/ManualInputModal.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { toast } from 'sonner'; +import { formatNumber } from '@/lib/utils/amount'; import { Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; @@ -26,6 +27,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import type { ManualInputFormData } from './types'; import { DEDUCTION_OPTIONS, ACCOUNT_SUBJECT_OPTIONS } from './types'; import { getCardList, createCardTransaction } from './actions'; +import { getTodayString } from '@/lib/utils/date'; interface ManualInputModalProps { open: boolean; @@ -35,7 +37,7 @@ interface ManualInputModalProps { const initialFormData: ManualInputFormData = { cardId: '', - usedDate: new Date().toISOString().slice(0, 10), + usedDate: getTodayString(), usedTime: '', approvalNumber: '', approvalType: 'approved', @@ -297,7 +299,7 @@ export function ManualInputModal({ open, onOpenChange, onSuccess }: ManualInputM {/* 합계 금액 */}
합계 금액 (공급가액 + 세액) - {totalAmount.toLocaleString()}원 + {formatNumber(totalAmount)}원
diff --git a/src/components/accounting/CardTransactionInquiry/actions.ts b/src/components/accounting/CardTransactionInquiry/actions.ts index 92bf1ee9..6dca9d71 100644 --- a/src/components/accounting/CardTransactionInquiry/actions.ts +++ b/src/components/accounting/CardTransactionInquiry/actions.ts @@ -5,6 +5,7 @@ import { executeServerAction, type ActionResult } from '@/lib/api/execute-server import { executePaginatedAction } from '@/lib/api/execute-paginated-action'; import { buildApiUrl } from '@/lib/api/query-params'; import type { CardTransaction, ManualInputFormData, InlineEditData, JournalEntryItem } from './types'; +import { getLocalDateString } from '@/lib/utils/date'; // ===== API 응답 타입 ===== interface CardTransactionApiItem { @@ -117,7 +118,7 @@ function generateMockData(): CardTransaction[] { const tax = Math.round(supply * 0.1); const d = new Date(now); d.setDate(d.getDate() - i); - const dateStr = d.toISOString().slice(0, 10); + const dateStr = getLocalDateString(d); const timeStr = `${String(9 + (i % 10)).padStart(2, '0')}:${String((i * 17) % 60).padStart(2, '0')}`; return { diff --git a/src/components/accounting/CardTransactionInquiry/index.tsx b/src/components/accounting/CardTransactionInquiry/index.tsx index 3cdb36ad..c1abe716 100644 --- a/src/components/accounting/CardTransactionInquiry/index.tsx +++ b/src/components/accounting/CardTransactionInquiry/index.tsx @@ -53,6 +53,7 @@ import { import { ManualInputModal } from './ManualInputModal'; import { JournalEntryModal } from './JournalEntryModal'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { formatNumber } from '@/lib/utils/amount'; // ===== 테이블 컬럼 정의 (체크박스/No. 제외 15개) ===== const tableColumns = [ @@ -425,13 +426,13 @@ export function CardTransactionInquiry() { computeStats: (): StatCard[] => [ { label: '전월', - value: `${summary.previousMonthTotal.toLocaleString()}원`, + value: `${formatNumber(summary.previousMonthTotal)}원`, icon: CreditCard, iconColor: 'text-gray-500', }, { label: '당월', - value: `${summary.currentMonthTotal.toLocaleString()}원`, + value: `${formatNumber(summary.currentMonthTotal)}원`, icon: CreditCard, iconColor: 'text-blue-500', }, @@ -521,7 +522,7 @@ export function CardTransactionInquiry() { /> {/* 합계금액 */} - {item.totalAmount.toLocaleString()} + {formatNumber(item.totalAmount)} {/* 공급가액 (인라인 숫자 Input) */} e.stopPropagation()}> ); @@ -662,7 +663,7 @@ export function CardTransactionInquiry() { {item.cardName} {item.businessNumber} {item.merchantName} - {item.totalAmount.toLocaleString()} + {formatNumber(item.totalAmount)} {item.hiddenAt || '-'} @@ -252,7 +246,7 @@ export function DepositDetail({ depositId, mode }: DepositDetailProps) { ); diff --git a/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx b/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx index f76e0e53..f134ca6c 100644 --- a/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx +++ b/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx @@ -66,7 +66,7 @@ export default function DepositDetailClientV2({ loadDeposit(); }, [depositId, initialMode]); - // ===== 저장/등록 핸들러 ===== + // ===== 저장/등록 핸들러 (결과만 반환, 토스트/리다이렉트는 IntegratedDetailTemplate에 위임) ===== const handleSubmit = useCallback( async (formData: Record): Promise<{ success: boolean; error?: string }> => { const submitData = depositDetailConfig.transformSubmitData?.(formData) || formData; @@ -81,32 +81,22 @@ export default function DepositDetailClientV2({ ? await createDeposit(submitData as Partial) : await updateDeposit(depositId!, submitData as Partial); - if (result.success) { - toast.success(mode === 'create' ? '입금 내역이 등록되었습니다.' : '입금 내역이 수정되었습니다.'); - router.push('/ko/accounting/deposits'); - return { success: true }; - } else { - toast.error(result.error || '저장에 실패했습니다.'); - return { success: false, error: result.error }; - } + return result.success + ? { success: true } + : { success: false, error: result.error }; }, - [mode, depositId, router] + [mode, depositId] ); - // ===== 삭제 핸들러 ===== + // ===== 삭제 핸들러 (결과만 반환, 토스트/리다이렉트는 IntegratedDetailTemplate에 위임) ===== const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => { if (!depositId) return { success: false, error: 'ID가 없습니다.' }; const result = await deleteDeposit(depositId); - if (result.success) { - toast.success('입금 내역이 삭제되었습니다.'); - router.push('/ko/accounting/deposits'); - return { success: true }; - } else { - toast.error(result.error || '삭제에 실패했습니다.'); - return { success: false, error: result.error }; - } - }, [depositId, router]); + return result.success + ? { success: true } + : { success: false, error: result.error }; + }, [depositId]); // ===== 모드 변경 핸들러 ===== const handleModeChange = useCallback( diff --git a/src/components/accounting/DepositManagement/index.tsx b/src/components/accounting/DepositManagement/index.tsx index 3f584695..6f904233 100644 --- a/src/components/accounting/DepositManagement/index.tsx +++ b/src/components/accounting/DepositManagement/index.tsx @@ -70,10 +70,10 @@ import { ACCOUNT_SUBJECT_OPTIONS, } from './types'; import { deleteDeposit, updateDepositTypes, getDeposits } from './actions'; +import { formatNumber } from '@/lib/utils/amount'; import { toast } from 'sonner'; import { useDateRange } from '@/hooks'; import { - createDeleteItemHandler, extractUniqueOptions, createDateAmountSortFn, computeMonthlyTotal, @@ -82,13 +82,13 @@ import { // ===== 테이블 컬럼 정의 ===== const tableColumns = [ - { key: 'depositDate', label: '입금일' }, - { key: 'accountName', label: '입금계좌' }, - { key: 'depositorName', label: '입금자명' }, - { key: 'depositAmount', label: '입금금액', className: 'text-right' }, - { key: 'vendorName', label: '거래처' }, - { key: 'note', label: '적요' }, - { key: 'depositType', label: '입금유형', className: 'text-center' }, + { key: 'depositDate', label: '입금일', sortable: true }, + { key: 'accountName', label: '입금계좌', sortable: true }, + { key: 'depositorName', label: '입금자명', sortable: true }, + { key: 'depositAmount', label: '입금금액', className: 'text-right', sortable: true }, + { key: 'vendorName', label: '거래처', sortable: true }, + { key: 'note', label: '적요', sortable: true }, + { key: 'depositType', label: '입금유형', className: 'text-center', sortable: true }, ]; // ===== 컴포넌트 Props ===== @@ -221,7 +221,15 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan totalCount: initialData.length, }; }, - deleteItem: createDeleteItemHandler(deleteDeposit, setDepositData, '입금 내역이 삭제되었습니다.'), + deleteItem: async (id: string) => { + const result = await deleteDeposit(id); + if (result.success) { + toast.success('입금 내역이 삭제되었습니다.'); + // 서버에서 재조회 (로컬 필터링 대신 - 페이지네이션 정합성 보장) + await handleRefresh(); + } + return { success: result.success, error: result.error }; + }, }, // 테이블 컬럼 @@ -354,8 +362,8 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan // Stats 카드 computeStats: (): StatCard[] => [ - { label: '총 입금', value: `${stats.totalDeposit.toLocaleString()}원`, icon: Banknote, iconColor: 'text-blue-500' }, - { label: '당월 입금', value: `${stats.monthlyDeposit.toLocaleString()}원`, icon: Banknote, iconColor: 'text-green-500' }, + { label: '총 입금', value: `${formatNumber(stats.totalDeposit)}원`, icon: Banknote, iconColor: 'text-blue-500' }, + { label: '당월 입금', value: `${formatNumber(stats.monthlyDeposit)}원`, icon: Banknote, iconColor: 'text-green-500' }, { label: '거래처 미설정', value: `${stats.vendorUnsetCount}건`, icon: Banknote, iconColor: 'text-orange-500' }, { label: '입금유형 미설정', value: `${stats.depositTypeUnsetCount}건`, icon: Banknote, iconColor: 'text-red-500' }, ], @@ -414,7 +422,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan 합계 - {tableTotals.totalAmount.toLocaleString()} + {formatNumber(tableTotals.totalAmount)} @@ -448,7 +456,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan {item.depositDate} {item.accountName} {item.depositorName} - {(item.depositAmount ?? 0).toLocaleString()} + {formatNumber(item.depositAmount ?? 0)} {item.vendorName || '미설정'} @@ -483,7 +491,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan onClick={() => handleRowClick(item)} details={[ { label: '입금일', value: item.depositDate }, - { label: '입금액', value: `${(item.depositAmount ?? 0).toLocaleString()}원` }, + { label: '입금액', value: `${formatNumber(item.depositAmount ?? 0)}원` }, { label: '거래처', value: item.vendorName || '-' }, ]} /> diff --git a/src/components/accounting/ExpectedExpenseManagement/index.tsx b/src/components/accounting/ExpectedExpenseManagement/index.tsx index 075f28f7..71bed70d 100644 --- a/src/components/accounting/ExpectedExpenseManagement/index.tsx +++ b/src/components/accounting/ExpectedExpenseManagement/index.tsx @@ -91,6 +91,7 @@ import { ACCOUNT_SUBJECT_OPTIONS, } from './types'; import { extractUniqueOptions } from '../shared'; +import { formatNumber } from '@/lib/utils/amount'; // ===== 테이블 행 타입 (데이터 + 그룹 헤더 + 소계) ===== type RowType = 'data' | 'monthHeader' | 'monthSubtotal' | 'totalExpense' | 'expectedBalance' | 'finalBalance'; @@ -610,7 +611,7 @@ export function ExpectedExpenseManagement({ {item.monthLabel} 소계 - {item.subtotalAmount?.toLocaleString()} + {formatNumber(item.subtotalAmount)} @@ -626,7 +627,7 @@ export function ExpectedExpenseManagement({ 지출 합계 - {item.subtotalAmount?.toLocaleString()} + {formatNumber(item.subtotalAmount)} @@ -642,7 +643,7 @@ export function ExpectedExpenseManagement({ 예상 잔액 - {item.subtotalAmount?.toLocaleString()} + {formatNumber(item.subtotalAmount)} @@ -658,7 +659,7 @@ export function ExpectedExpenseManagement({ 최종 잔액 - {item.subtotalAmount?.toLocaleString()}원 + {formatNumber(item.subtotalAmount)}원 @@ -684,7 +685,7 @@ export function ExpectedExpenseManagement({ {item.expectedPaymentDate} {item.accountSubject} - {item.amount.toLocaleString()} + {formatNumber(item.amount)} {item.vendorName} {item.bankAccount} @@ -717,7 +718,7 @@ export function ExpectedExpenseManagement({ return (
{item.monthLabel} 소계 - {item.subtotalAmount?.toLocaleString()}원 + {formatNumber(item.subtotalAmount)}원
); } @@ -725,7 +726,7 @@ export function ExpectedExpenseManagement({ return (
지출 합계 - {item.subtotalAmount?.toLocaleString()}원 + {formatNumber(item.subtotalAmount)}원
); } @@ -733,7 +734,7 @@ export function ExpectedExpenseManagement({ return (
예상 잔액 - {item.subtotalAmount?.toLocaleString()}원 + {formatNumber(item.subtotalAmount)}원
); } @@ -741,7 +742,7 @@ export function ExpectedExpenseManagement({ return (
최종 잔액 - {item.subtotalAmount?.toLocaleString()}원 + {formatNumber(item.subtotalAmount)}원
); } @@ -771,7 +772,7 @@ export function ExpectedExpenseManagement({ infoGrid={
- +
@@ -961,8 +962,8 @@ export function ExpectedExpenseManagement({ const expectedBalance = 10000000; return [ - { label: '지출 합계', value: `${totalExpense.toLocaleString()}원`, icon: Receipt, iconColor: 'text-red-500' }, - { label: '예상 잔액', value: `${expectedBalance.toLocaleString()}원`, icon: Receipt, iconColor: 'text-blue-500' }, + { label: '지출 합계', value: `${formatNumber(totalExpense)}원`, icon: Receipt, iconColor: 'text-red-500' }, + { label: '예상 잔액', value: `${formatNumber(expectedBalance)}원`, icon: Receipt, iconColor: 'text-blue-500' }, ]; }, @@ -1041,7 +1042,7 @@ export function ExpectedExpenseManagement({ {item.vendorName} {item.accountSubject} • {item.expectedPaymentDate} - {item.amount.toLocaleString()}원 + {formatNumber(item.amount)}원 ))} @@ -1050,7 +1051,7 @@ export function ExpectedExpenseManagement({ {/* 합계 */}
합계 - {selectedItemsSummary.totalAmount.toLocaleString()}원 + {formatNumber(selectedItemsSummary.totalAmount)}원
{/* 예상 지급일 선택 */} @@ -1272,13 +1273,13 @@ export function ExpectedExpenseManagement({ {item.vendorName} {item.accountSubject} • {item.expectedPaymentDate} - {item.amount.toLocaleString()}원 + {formatNumber(item.amount)}원 ))}
합계 - {selectedItemsSummary.totalAmount.toLocaleString()}원 + {formatNumber(selectedItemsSummary.totalAmount)}원
취소 diff --git a/src/components/accounting/GeneralJournalEntry/JournalEditModal.tsx b/src/components/accounting/GeneralJournalEntry/JournalEditModal.tsx index b3a9fa5a..fb9b6755 100644 --- a/src/components/accounting/GeneralJournalEntry/JournalEditModal.tsx +++ b/src/components/accounting/GeneralJournalEntry/JournalEditModal.tsx @@ -13,6 +13,7 @@ import { useState, useCallback, useEffect, useMemo } from 'react'; import { toast } from 'sonner'; import { Plus, Trash2 } from 'lucide-react'; +import { formatNumber } from '@/lib/utils/amount'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -290,7 +291,7 @@ export function JournalEditModal({
-
{record.amount.toLocaleString()}원
+
{formatNumber(record.amount)}원
@@ -451,17 +452,17 @@ export function JournalEditModal({ 합계 - {totals.debitTotal.toLocaleString()} + {formatNumber(totals.debitTotal)} - {totals.creditTotal.toLocaleString()} + {formatNumber(totals.creditTotal)} {totals.isBalanced ? ( 대차 균형 ) : ( - 차이: {totals.difference.toLocaleString()}원 + 차이: {formatNumber(totals.difference)}원 )} diff --git a/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx b/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx index 92375363..af3cfa46 100644 --- a/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx +++ b/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx @@ -12,6 +12,7 @@ import { useState, useCallback, useEffect, useMemo } from 'react'; import { toast } from 'sonner'; import { Plus, Trash2, Loader2 } from 'lucide-react'; +import { formatNumber } from '@/lib/utils/amount'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -44,6 +45,7 @@ import { import { createManualJournal, getAccountSubjects, getVendorList } from './actions'; import type { JournalEntryRow, JournalSide, AccountSubject, VendorOption } from './types'; import { JOURNAL_SIDE_OPTIONS } from './types'; +import { getTodayString } from '@/lib/utils/date'; interface ManualJournalEntryModalProps { open: boolean; @@ -71,7 +73,7 @@ export function ManualJournalEntryModal({ onSuccess, }: ManualJournalEntryModalProps) { // 거래 정보 - const [journalDate, setJournalDate] = useState(() => new Date().toISOString().slice(0, 10)); + const [journalDate, setJournalDate] = useState(() => getTodayString()); const [journalNumber, setJournalNumber] = useState('자동생성'); const [description, setDescription] = useState(''); @@ -87,7 +89,7 @@ export function ManualJournalEntryModal({ useEffect(() => { if (!open) return; // 초기화 - setJournalDate(new Date().toISOString().slice(0, 10)); + setJournalDate(getTodayString()); setJournalNumber('자동생성'); setDescription(''); setRows([createEmptyRow()]); @@ -361,10 +363,10 @@ export function ManualJournalEntryModal({ 합계 - {totals.debitTotal.toLocaleString()} + {formatNumber(totals.debitTotal)} - {totals.creditTotal.toLocaleString()} + {formatNumber(totals.creditTotal)} @@ -375,8 +377,8 @@ export function ManualJournalEntryModal({ {/* 차대변 불일치 경고 */} {totals.debitTotal !== totals.creditTotal && totals.debitTotal > 0 && (

- 차변 합계({totals.debitTotal.toLocaleString()})와 대변 합계( - {totals.creditTotal.toLocaleString()})가 일치하지 않습니다. + 차변 합계({formatNumber(totals.debitTotal)})와 대변 합계( + {formatNumber(totals.creditTotal)})가 일치하지 않습니다.

)} diff --git a/src/components/accounting/GeneralJournalEntry/index.tsx b/src/components/accounting/GeneralJournalEntry/index.tsx index 671e587f..074e55f6 100644 --- a/src/components/accounting/GeneralJournalEntry/index.tsx +++ b/src/components/accounting/GeneralJournalEntry/index.tsx @@ -37,6 +37,7 @@ import { JOURNAL_DIVISION_LABELS, getPeriodDates, } from './types'; +import { formatNumber } from '@/lib/utils/amount'; // ===== 테이블 컬럼 (기획서 기준 10개) ===== const tableColumns = [ @@ -232,8 +233,8 @@ export function GeneralJournalEntry() { // ===== 통계 카드 5개 ===== computeStats: (): StatCard[] => [ { label: '전체', value: `${summary.totalCount}건`, icon: FileText, iconColor: 'text-gray-500' }, - { label: '입금', value: `${summary.depositAmount.toLocaleString()}원`, icon: FileText, iconColor: 'text-blue-500' }, - { label: '출금', value: `${summary.withdrawalAmount.toLocaleString()}원`, icon: FileText, iconColor: 'text-red-500' }, + { label: '입금', value: `${formatNumber(summary.depositAmount)}원`, icon: FileText, iconColor: 'text-blue-500' }, + { label: '출금', value: `${formatNumber(summary.withdrawalAmount)}원`, icon: FileText, iconColor: 'text-red-500' }, { label: '분개완료', value: `${summary.journalCompleteCount}건`, icon: FileText, iconColor: 'text-green-500' }, { label: '미분개', value: `${summary.journalIncompleteCount}건`, icon: FileText, iconColor: 'text-orange-500' }, ], @@ -268,12 +269,12 @@ export function GeneralJournalEntry() {
- {item.depositAmount ? item.depositAmount.toLocaleString() : '-'} + {item.depositAmount ? formatNumber(item.depositAmount) : '-'} - {item.withdrawalAmount ? item.withdrawalAmount.toLocaleString() : '-'} + {item.withdrawalAmount ? formatNumber(item.withdrawalAmount) : '-'} - {item.balance.toLocaleString()} + {formatNumber(item.balance)} {JOURNAL_DIVISION_LABELS[item.division] || item.division} @@ -281,10 +282,10 @@ export function GeneralJournalEntry() { {item.journalDescription || '-'} - {item.debitAmount ? item.debitAmount.toLocaleString() : '-'} + {item.debitAmount ? formatNumber(item.debitAmount) : '-'} - {item.creditAmount ? item.creditAmount.toLocaleString() : '-'} + {item.creditAmount ? formatNumber(item.creditAmount) : '-'}
-
{invoice.supplyAmount.toLocaleString()}원
+
{formatNumber(invoice.supplyAmount)}원
-
{invoice.taxAmount.toLocaleString()}원
+
{formatNumber(invoice.taxAmount)}원
@@ -348,10 +349,10 @@ export function JournalEntryModal({ 합계 - {totals.debitTotal.toLocaleString()} + {formatNumber(totals.debitTotal)} - {totals.creditTotal.toLocaleString()} + {formatNumber(totals.creditTotal)} @@ -363,8 +364,8 @@ export function JournalEntryModal({ {/* 차대변 불일치 경고 */} {totals.debitTotal !== totals.creditTotal && (

- 차변 합계({totals.debitTotal.toLocaleString()})와 대변 합계( - {totals.creditTotal.toLocaleString()})가 일치하지 않습니다. + 차변 합계({formatNumber(totals.debitTotal)})와 대변 합계( + {formatNumber(totals.creditTotal)})가 일치하지 않습니다.

)} diff --git a/src/components/accounting/TaxInvoiceManagement/index.tsx b/src/components/accounting/TaxInvoiceManagement/index.tsx index c6cf53fd..1bdfe2ca 100644 --- a/src/components/accounting/TaxInvoiceManagement/index.tsx +++ b/src/components/accounting/TaxInvoiceManagement/index.tsx @@ -60,6 +60,7 @@ import { INVOICE_STATUS_MAP, INVOICE_SOURCE_LABELS, } from './types'; +import { formatNumber } from '@/lib/utils/amount'; // ===== 분기 옵션 ===== const QUARTER_BUTTONS = [ @@ -338,11 +339,11 @@ export function TaxInvoiceManagement() { {/* 요약 카드 5개 */} @@ -397,9 +398,9 @@ export function TaxInvoiceManagement() { {item.itemName || '-'} - {item.supplyAmount.toLocaleString()} - {item.taxAmount.toLocaleString()} - {item.totalAmount.toLocaleString()} + {formatNumber(item.supplyAmount)} + {formatNumber(item.taxAmount)} + {formatNumber(item.totalAmount)} {RECEIPT_TYPE_LABELS[item.receiptType]} @@ -444,9 +445,9 @@ export function TaxInvoiceManagement() { onClick={() => setJournalTarget(item)} details={[ { label: '작성일자', value: item.writeDate }, - { label: '공급가액', value: `${item.supplyAmount.toLocaleString()}원` }, - { label: '세액', value: `${item.taxAmount.toLocaleString()}원` }, - { label: '합계', value: `${item.totalAmount.toLocaleString()}원` }, + { label: '공급가액', value: `${formatNumber(item.supplyAmount)}원` }, + { label: '세액', value: `${formatNumber(item.taxAmount)}원` }, + { label: '합계', value: `${formatNumber(item.totalAmount)}원` }, { label: '과세여부', value: TAX_TYPE_LABELS[item.taxType] }, { label: '소스', value: INVOICE_SOURCE_LABELS[item.source] }, ]} @@ -479,18 +480,18 @@ export function TaxInvoiceManagement() {
매출 합계 (공급가액 + 세액)
-
{summary.salesTotalAmount.toLocaleString()}원
+
{formatNumber(summary.salesTotalAmount)}원
매입 합계 (공급가액 + 세액)
-
{summary.purchaseTotalAmount.toLocaleString()}원
+
{formatNumber(summary.purchaseTotalAmount)}원
=
예상 부가세
= 0 ? 'text-blue-700' : 'text-red-700'}`}> - {periodDifference.toLocaleString()}원 + {formatNumber(periodDifference)}원
diff --git a/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx b/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx index 2f160951..a3a32045 100644 --- a/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx +++ b/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx @@ -25,6 +25,7 @@ import { vendorLedgerConfig } from './vendorLedgerConfig'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import type { VendorLedgerDetail as VendorLedgerDetailType, TransactionEntry, VendorLedgerSummary } from './types'; import { getVendorLedgerDetail, exportVendorLedgerDetailPdf } from './actions'; +import { formatNumber } from '@/lib/utils/amount'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; @@ -119,7 +120,7 @@ export function VendorLedgerDetail({ // ===== 금액 포맷 ===== const formatAmount = (amount: number, isParenthesis?: boolean) => { if (amount === 0) return ''; - const formatted = amount.toLocaleString(); + const formatted = formatNumber(amount); return isParenthesis ? `(${formatted})` : formatted; }; @@ -247,20 +248,20 @@ export function VendorLedgerDetail({
이월잔액
-
{summary.carryoverBalance.toLocaleString()}원
+
{formatNumber(summary.carryoverBalance)}원
매출
-
{summary.totalSales.toLocaleString()}원
+
{formatNumber(summary.totalSales)}원
수금
-
{summary.totalCollection.toLocaleString()}원
+
{formatNumber(summary.totalCollection)}원
잔액
- {summary.balance.toLocaleString()}원 + {formatNumber(summary.balance)}원
diff --git a/src/components/accounting/VendorLedger/index.tsx b/src/components/accounting/VendorLedger/index.tsx index a69edc44..069b1d04 100644 --- a/src/components/accounting/VendorLedger/index.tsx +++ b/src/components/accounting/VendorLedger/index.tsx @@ -27,6 +27,7 @@ import { } from '@/components/templates/UniversalListPage'; import type { VendorLedgerItem, VendorLedgerSummary } from './types'; import { getVendorLedgerList, getVendorLedgerSummary, exportVendorLedgerExcel } from './actions'; +import { formatNumber } from '@/lib/utils/amount'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { usePermission } from '@/hooks/usePermission'; @@ -167,7 +168,7 @@ export function VendorLedger({ // ===== 금액 포맷 ===== const formatAmount = (amount: number) => { if (amount === 0) return ''; - return amount.toLocaleString(); + return formatNumber(amount); }; // ===== UniversalListPage Config ===== @@ -249,25 +250,25 @@ export function VendorLedger({ computeStats: (): StatCard[] => [ { label: '전기 이월', - value: `${summary.carryoverBalance.toLocaleString()}원`, + value: `${formatNumber(summary.carryoverBalance)}원`, icon: FileText, iconColor: 'text-blue-500', }, { label: '매출', - value: `${summary.totalSales.toLocaleString()}원`, + value: `${formatNumber(summary.totalSales)}원`, icon: FileText, iconColor: 'text-green-500', }, { label: '수금', - value: `${summary.totalCollection.toLocaleString()}원`, + value: `${formatNumber(summary.totalCollection)}원`, icon: FileText, iconColor: 'text-orange-500', }, { label: '잔액', - value: `${summary.balance.toLocaleString()}원`, + value: `${formatNumber(summary.balance)}원`, icon: FileText, iconColor: 'text-red-500', }, diff --git a/src/components/accounting/VendorManagement/VendorDetail.tsx b/src/components/accounting/VendorManagement/VendorDetail.tsx index c064e9a0..b840d6e0 100644 --- a/src/components/accounting/VendorManagement/VendorDetail.tsx +++ b/src/components/accounting/VendorManagement/VendorDetail.tsx @@ -47,6 +47,7 @@ import { PAYMENT_DAY_OPTIONS, BANK_OPTIONS, } from './types'; +import { getLocalDateString } from '@/lib/utils/date'; interface VendorDetailProps { mode: 'view' | 'edit' | 'new'; @@ -185,7 +186,7 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) { const handleAddMemo = useCallback(() => { if (!newMemo.trim()) return; const now = new Date(); - const dateStr = now.toISOString().slice(0, 10); + const dateStr = getLocalDateString(now); const timeStr = now.toTimeString().slice(0, 5); const memo: VendorMemo = { id: String(Date.now()), diff --git a/src/components/accounting/VendorManagement/VendorDetailClient.tsx b/src/components/accounting/VendorManagement/VendorDetailClient.tsx index 325c20ec..9a05facd 100644 --- a/src/components/accounting/VendorManagement/VendorDetailClient.tsx +++ b/src/components/accounting/VendorManagement/VendorDetailClient.tsx @@ -2,6 +2,7 @@ import { useState, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import { formatNumber } from '@/lib/utils/amount'; import { Plus, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -29,6 +30,7 @@ import { PAYMENT_DAY_OPTIONS, BANK_OPTIONS, } from './types'; +import { getLocalDateString } from '@/lib/utils/date'; interface VendorDetailClientProps { mode: 'view' | 'edit' | 'new'; @@ -136,7 +138,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail const handleAddMemo = useCallback(() => { if (!newMemo.trim()) return; const now = new Date(); - const dateStr = now.toISOString().slice(0, 10); + const dateStr = getLocalDateString(now); const timeStr = now.toTimeString().slice(0, 5); const memo: VendorMemo = { id: String(Date.now()), @@ -465,7 +467,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail @@ -475,7 +477,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail diff --git a/src/components/accounting/VendorManagement/VendorManagementClient.tsx b/src/components/accounting/VendorManagement/VendorManagementClient.tsx index 295be279..5d529522 100644 --- a/src/components/accounting/VendorManagement/VendorManagementClient.tsx +++ b/src/components/accounting/VendorManagement/VendorManagementClient.tsx @@ -11,6 +11,7 @@ import { useState, useMemo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; +import { formatNumber } from '@/lib/utils/amount'; import { Building2, Pencil, @@ -208,14 +209,14 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana // ===== 테이블 컬럼 ===== const tableColumns: TableColumn[] = useMemo(() => [ { key: 'no', label: '번호', className: 'text-center w-[60px]' }, - { key: 'category', label: '구분', className: 'text-center w-[100px]' }, - { key: 'vendorName', label: '거래처명' }, - { key: 'purchasePaymentDay', label: '매입 결제일', className: 'text-center w-[100px]' }, - { key: 'salesPaymentDay', label: '매출 결제일', className: 'text-center w-[100px]' }, - { key: 'creditRating', label: '신용등급', className: 'text-center w-[90px]' }, - { key: 'transactionGrade', label: '거래등급', className: 'text-center w-[100px]' }, - { key: 'outstandingAmount', label: '미수금', className: 'text-right w-[120px]' }, - { key: 'badDebtStatus', label: '악성채권', className: 'text-center w-[90px]' }, + { key: 'category', label: '구분', className: 'text-center w-[100px]', sortable: true }, + { key: 'vendorName', label: '거래처명', sortable: true }, + { key: 'purchasePaymentDay', label: '매입 결제일', className: 'text-center w-[100px]', sortable: true }, + { key: 'salesPaymentDay', label: '매출 결제일', className: 'text-center w-[100px]', sortable: true }, + { key: 'creditRating', label: '신용등급', className: 'text-center w-[90px]', sortable: true }, + { key: 'transactionGrade', label: '거래등급', className: 'text-center w-[100px]', sortable: true }, + { key: 'outstandingAmount', label: '미수금', className: 'text-right w-[120px]', sortable: true }, + { key: 'badDebtStatus', label: '악성채권', className: 'text-center w-[90px]', sortable: true }, { key: 'actions', label: '작업', className: 'text-center w-[150px]' }, ], []); @@ -264,7 +265,7 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana {/* 미수금 */} {item.outstandingAmount > 0 ? ( - {item.outstandingAmount.toLocaleString()}원 + {formatNumber(item.outstandingAmount)}원 ) : ( - )} @@ -335,7 +336,7 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana 0 ? `${item.outstandingAmount.toLocaleString()}원` : '-'} + value={item.outstandingAmount > 0 ? `${formatNumber(item.outstandingAmount)}원` : '-'} className={item.outstandingAmount > 0 ? 'text-red-600' : ''} /> diff --git a/src/components/accounting/VendorManagement/index.tsx b/src/components/accounting/VendorManagement/index.tsx index 55bcae31..0d409733 100644 --- a/src/components/accounting/VendorManagement/index.tsx +++ b/src/components/accounting/VendorManagement/index.tsx @@ -12,6 +12,7 @@ import { useState, useMemo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; +import { formatNumber } from '@/lib/utils/amount'; import { Building2, } from 'lucide-react'; @@ -336,7 +337,7 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement {/* 미수금 */} {vendor.outstandingAmount > 0 ? ( - {vendor.outstandingAmount.toLocaleString()}원 + {formatNumber(vendor.outstandingAmount)}원 ) : ( - )} @@ -374,7 +375,7 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement { label: '거래등급', value: TRANSACTION_GRADE_LABELS[vendor.transactionGrade] }, { label: '미수금', - value: vendor.outstandingAmount > 0 ? `${vendor.outstandingAmount.toLocaleString()}원` : '-', + value: vendor.outstandingAmount > 0 ? `${formatNumber(vendor.outstandingAmount)}원` : '-', }, { label: '결제일', value: `매입 ${vendor.purchasePaymentDay}일 / 매출 ${vendor.salesPaymentDay}일` }, ]} diff --git a/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx b/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx index 5ed9801c..18870f44 100644 --- a/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx +++ b/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx @@ -6,6 +6,7 @@ import { Banknote, List, } from 'lucide-react'; +import { formatNumber } from '@/lib/utils/amount'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; @@ -19,6 +20,7 @@ import { SelectValue, } from '@/components/ui/select'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; +import { useDeleteDialog } from '@/hooks/useDeleteDialog'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { toast } from 'sonner'; @@ -51,7 +53,6 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps) const [note, setNote] = useState(''); const [vendorId, setVendorId] = useState(''); const [withdrawalType, setWithdrawalType] = useState('unset'); - const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [isLoading, setIsLoading] = useState(false); const [vendors, setVendors] = useState<{ id: string; name: string }[]>([]); @@ -145,19 +146,12 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps) router.push(`/ko/accounting/withdrawals/${withdrawalId}?mode=edit`); }, [router, withdrawalId]); - // ===== 삭제 핸들러 ===== - const handleDelete = useCallback(async () => { - setIsLoading(true); - const result = await deleteWithdrawal(withdrawalId); - if (result.success) { - toast.success('출금 내역이 삭제되었습니다.'); - setShowDeleteDialog(false); - router.push('/ko/accounting/withdrawals'); - } else { - toast.error(result.error || '삭제에 실패했습니다.'); - } - setIsLoading(false); - }, [withdrawalId, router]); + // ===== 삭제 다이얼로그 ===== + const deleteDialog = useDeleteDialog({ + onDelete: async (id) => deleteWithdrawal(id), + onSuccess: () => router.push('/ko/accounting/withdrawals'), + entityName: '출금', + }); return ( @@ -180,8 +174,8 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps) @@ -252,7 +246,7 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps) ); diff --git a/src/components/accounting/WithdrawalManagement/index.tsx b/src/components/accounting/WithdrawalManagement/index.tsx index 90f87ec4..61effab7 100644 --- a/src/components/accounting/WithdrawalManagement/index.tsx +++ b/src/components/accounting/WithdrawalManagement/index.tsx @@ -69,6 +69,7 @@ import { ACCOUNT_SUBJECT_OPTIONS, } from './types'; import { deleteWithdrawal, updateWithdrawalTypes, getWithdrawals } from './actions'; +import { formatNumber } from '@/lib/utils/amount'; import { toast } from 'sonner'; import { useDateRange } from '@/hooks'; import { @@ -432,7 +433,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra 합계 - {tableTotals.totalAmount.toLocaleString()} + {formatNumber(tableTotals.totalAmount)} @@ -441,8 +442,8 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra // Stats 카드 computeStats: (): StatCard[] => [ - { label: '총 출금', value: `${stats.totalWithdrawal.toLocaleString()}원`, icon: Banknote, iconColor: 'text-blue-500' }, - { label: '당월 출금', value: `${stats.monthlyWithdrawal.toLocaleString()}원`, icon: Banknote, iconColor: 'text-green-500' }, + { label: '총 출금', value: `${formatNumber(stats.totalWithdrawal)}원`, icon: Banknote, iconColor: 'text-blue-500' }, + { label: '당월 출금', value: `${formatNumber(stats.monthlyWithdrawal)}원`, icon: Banknote, iconColor: 'text-green-500' }, { label: '거래처 미설정', value: `${stats.vendorUnsetCount}건`, icon: Banknote, iconColor: 'text-orange-500' }, { label: '출금유형 미설정', value: `${stats.withdrawalTypeUnsetCount}건`, icon: Banknote, iconColor: 'text-red-500' }, ], @@ -479,7 +480,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra {/* 수취인명 */} {item.recipientName} {/* 출금금액 */} - {(item.withdrawalAmount ?? 0).toLocaleString()} + {formatNumber(item.withdrawalAmount ?? 0)} {/* 거래처 */} {item.vendorName || '미설정'} @@ -517,7 +518,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra onClick={() => handleRowClick(item)} details={[ { label: '출금일', value: item.withdrawalDate || '-' }, - { label: '출금액', value: `${(item.withdrawalAmount ?? 0).toLocaleString()}원` }, + { label: '출금액', value: `${formatNumber(item.withdrawalAmount ?? 0)}원` }, { label: '거래처', value: item.vendorName || '-' }, { label: '적요', value: item.note || '-' }, ]} diff --git a/src/components/accounting/WithdrawalManagement/withdrawalDetailConfig.ts b/src/components/accounting/WithdrawalManagement/withdrawalDetailConfig.ts index f7077833..42a89b5b 100644 --- a/src/components/accounting/WithdrawalManagement/withdrawalDetailConfig.ts +++ b/src/components/accounting/WithdrawalManagement/withdrawalDetailConfig.ts @@ -1,4 +1,5 @@ import { Banknote } from 'lucide-react'; +import { formatNumber } from '@/lib/utils/amount'; import type { DetailConfig, FieldDefinition } from '@/components/templates/IntegratedDetailTemplate/types'; import type { WithdrawalRecord } from './types'; import { WITHDRAWAL_TYPE_SELECTOR_OPTIONS } from './types'; @@ -117,7 +118,7 @@ export const withdrawalDetailConfig: DetailConfig = { withdrawalDate: record.withdrawalDate || '', bankAccountId: record.bankAccountId || '', recipientName: record.recipientName || '', - withdrawalAmount: record.withdrawalAmount ? record.withdrawalAmount.toLocaleString() : '0', + withdrawalAmount: record.withdrawalAmount ? formatNumber(record.withdrawalAmount) : '0', note: record.note || '', vendorId: record.vendorId || '', withdrawalType: record.withdrawalType || 'unset', diff --git a/src/components/approval/DraftBox/actions.ts b/src/components/approval/DraftBox/actions.ts index fd6193ef..1a5db2b9 100644 --- a/src/components/approval/DraftBox/actions.ts +++ b/src/components/approval/DraftBox/actions.ts @@ -17,6 +17,7 @@ import { executeServerAction, type ActionResult } from '@/lib/api/execute-server import { buildApiUrl } from '@/lib/api/query-params'; import type { PaginatedApiResponse } from '@/lib/api/types'; import type { DraftRecord, DocumentStatus, Approver } from './types'; +import { formatDate } from '@/lib/utils/date'; // ============================================ // API 응답 타입 정의 @@ -141,7 +142,7 @@ function transformApiToFrontend(data: ApprovalApiData): DraftRecord { documentType: data.form?.name || '', documentTypeCode: data.form?.code || 'proposal', title: data.title, - draftDate: data.created_at.split('T')[0], + draftDate: formatDate(data.created_at), drafter: data.drafter?.name || '', drafterPosition: getPositionLabel(drafterProfile?.position_key), drafterDepartment: drafterProfile?.department?.name || '', diff --git a/src/components/board/BoardDetail/index.tsx b/src/components/board/BoardDetail/index.tsx index eba637f2..70e93a57 100644 --- a/src/components/board/BoardDetail/index.tsx +++ b/src/components/board/BoardDetail/index.tsx @@ -29,7 +29,7 @@ import { CardHeader, } from '@/components/ui/card'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; -import { toast } from 'sonner'; +import { useDeleteDialog } from '@/hooks/useDeleteDialog'; import { CommentSection } from '../CommentSection'; import { deletePost } from '../actions'; import type { Post, Comment } from '../types'; @@ -46,8 +46,6 @@ interface BoardDetailProps { export function BoardDetail({ post, comments: initialComments, currentUserId }: BoardDetailProps) { const router = useRouter(); const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed); - const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); const [comments, setComments] = useState(initialComments); const isMyPost = post.authorId === currentUserId; @@ -61,25 +59,11 @@ export function BoardDetail({ post, comments: initialComments, currentUserId }: router.push(`/ko/board/${post.boardCode}/${post.id}?mode=edit`); }, [router, post.boardCode, post.id]); - const handleConfirmDelete = useCallback(async () => { - setIsDeleting(true); - try { - const result = await deletePost(post.boardCode, post.id); - if (result.success) { - toast.success('게시글이 삭제되었습니다.'); - router.push('/ko/board'); - } else { - toast.error(result.error || '게시글 삭제에 실패했습니다.'); - } - } catch (error) { - if (isNextRedirectError(error)) throw error; - console.error('게시글 삭제 오류:', error); - toast.error('게시글 삭제에 실패했습니다.'); - } finally { - setIsDeleting(false); - setShowDeleteDialog(false); - } - }, [post.boardCode, post.id, router]); + const deleteDialog = useDeleteDialog({ + onDelete: async () => deletePost(post.boardCode, post.id), + onSuccess: () => router.push('/ko/board'), + entityName: '게시글', + }); // ===== 댓글 핸들러 ===== // TODO: 댓글 API 연동 (별도 작업) @@ -211,7 +195,7 @@ export function BoardDetail({ post, comments: initialComments, currentUserId }:
@@ -364,8 +356,8 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
카드를 정말 삭제하시겠습니까? @@ -375,7 +367,8 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro } - onConfirm={handleConfirmDelete} + onConfirm={deleteDialog.single.confirm} + loading={deleteDialog.isPending} /> ); diff --git a/src/components/hr/CardManagement/index.tsx b/src/components/hr/CardManagement/index.tsx index 9e662746..273e21a0 100644 --- a/src/components/hr/CardManagement/index.tsx +++ b/src/components/hr/CardManagement/index.tsx @@ -41,12 +41,9 @@ import { CARD_STATUS_COLORS, getCardCompanyLabel, } from './types'; +import { formatAmountWon as formatCurrency } from '@/lib/utils/amount'; import { getCards, getCardStats } from './actions'; -function formatCurrency(value: number): string { - return value.toLocaleString('ko-KR') + '원'; -} - export function CardManagement() { const router = useRouter(); const itemsPerPage = 20; diff --git a/src/components/hr/EmployeeManagement/EmployeeDetail.tsx b/src/components/hr/EmployeeManagement/EmployeeDetail.tsx index 0837b61b..22d2da23 100644 --- a/src/components/hr/EmployeeManagement/EmployeeDetail.tsx +++ b/src/components/hr/EmployeeManagement/EmployeeDetail.tsx @@ -23,6 +23,7 @@ import { USER_ROLE_LABELS, USER_ACCOUNT_STATUS_LABELS, } from './types'; +import { formatNumber } from '@/lib/utils/amount'; interface EmployeeDetailProps { employee: Employee; @@ -87,7 +88,7 @@ export function EmployeeDetail({ employee, onEdit, onDelete }: EmployeeDetailPro {employee.salary && (
연봉
-
{employee.salary.toLocaleString()}원
+
{formatNumber(employee.salary)}원
)} {employee.bankAccount && ( diff --git a/src/components/hr/EmployeeManagement/index.tsx b/src/components/hr/EmployeeManagement/index.tsx index fdce2064..bd621516 100644 --- a/src/components/hr/EmployeeManagement/index.tsx +++ b/src/components/hr/EmployeeManagement/index.tsx @@ -255,17 +255,17 @@ export function EmployeeManagement() { // 테이블 컬럼 정의 const tableColumns = useMemo(() => [ { key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' }, - { key: 'employeeCode', label: '사원코드', className: 'min-w-[100px]' }, - { key: 'department', label: '부서', className: 'min-w-[100px]' }, - { key: 'position', label: '직책', className: 'min-w-[100px]' }, - { key: 'name', label: '이름', className: 'min-w-[80px]' }, - { key: 'rank', label: '직급', className: 'min-w-[80px]' }, - { key: 'phone', label: '휴대폰', className: 'min-w-[120px]' }, - { key: 'email', label: '이메일', className: 'min-w-[150px]' }, - { key: 'hireDate', label: '입사일', className: 'min-w-[100px]' }, - { key: 'status', label: '상태', className: 'min-w-[80px]' }, - { key: 'userId', label: '사용자아이디', className: 'min-w-[100px]' }, - { key: 'userRole', label: '권한', className: 'min-w-[80px]' }, + { key: 'employeeCode', label: '사원코드', className: 'min-w-[100px]', sortable: true }, + { key: 'department', label: '부서', className: 'min-w-[100px]', sortable: true }, + { key: 'position', label: '직책', className: 'min-w-[100px]', sortable: true }, + { key: 'name', label: '이름', className: 'min-w-[80px]', sortable: true }, + { key: 'rank', label: '직급', className: 'min-w-[80px]', sortable: true }, + { key: 'phone', label: '휴대폰', className: 'min-w-[120px]', sortable: true }, + { key: 'email', label: '이메일', className: 'min-w-[150px]', sortable: true }, + { key: 'hireDate', label: '입사일', className: 'min-w-[100px]', sortable: true }, + { key: 'status', label: '상태', className: 'min-w-[80px]', sortable: true }, + { key: 'userId', label: '사용자아이디', className: 'min-w-[100px]', sortable: true }, + { key: 'userRole', label: '권한', className: 'min-w-[80px]', sortable: true }, { key: 'actions', label: '작업', className: 'w-[100px] text-right' }, ], []); diff --git a/src/components/hr/VacationManagement/index.tsx b/src/components/hr/VacationManagement/index.tsx index 4e569ea9..1cb27f2e 100644 --- a/src/components/hr/VacationManagement/index.tsx +++ b/src/components/hr/VacationManagement/index.tsx @@ -62,6 +62,7 @@ import { } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { toast } from 'sonner'; +import { formatDate } from '@/lib/utils/date'; // ===== Mock 데이터 생성 (request 탭용 - 신청 현황은 leaves API 사용 예정) ===== @@ -188,7 +189,7 @@ export function VacationManagement() { position: item.jobTitle || '-', // job_title_label → 직책 rank: item.rank || '-', // json_extra.rank → 직급 vacationType: item.grantType as VacationType, - grantDate: item.grantDate.split('T')[0], + grantDate: formatDate(item.grantDate), grantDays: item.grantDays, reason: item.reason || undefined, createdAt: item.createdAt, @@ -235,7 +236,7 @@ export function VacationManagement() { endDate: item.endDate, vacationDays: item.days, status: item.status as RequestStatus, - requestDate: item.createdAt.split('T')[0], + requestDate: formatDate(item.createdAt), createdAt: item.createdAt, updatedAt: item.updatedAt, }; diff --git a/src/components/material/ReceivingManagement/actions.ts b/src/components/material/ReceivingManagement/actions.ts index 5bd1b57e..a995e713 100644 --- a/src/components/material/ReceivingManagement/actions.ts +++ b/src/components/material/ReceivingManagement/actions.ts @@ -22,7 +22,7 @@ import { buildApiUrl } from '@/lib/api/query-params'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { executeServerAction } from '@/lib/api/execute-server-action'; import { executePaginatedAction } from '@/lib/api/execute-paginated-action'; -import { getTodayString } from '@/lib/utils/date'; +import { getTodayString, formatDate } from '@/lib/utils/date'; import type { ReceivingItem, @@ -414,7 +414,7 @@ function transformApiToListItem(data: ReceivingApiData): ReceivingItem { // 수량 (입고수량) receivingQty: data.receiving_qty ? parseFloat(String(data.receiving_qty)) : undefined, // 입고변경일: updated_at 매핑 - receivingDate: data.updated_at ? data.updated_at.split('T')[0] : data.receiving_date, + receivingDate: data.updated_at ? formatDate(data.updated_at) : data.receiving_date, // 작성자 createdBy: data.creator?.name, // 상태 diff --git a/src/components/orders/OrderSalesDetailEdit.tsx b/src/components/orders/OrderSalesDetailEdit.tsx index 2e55b7b7..13bc2100 100644 --- a/src/components/orders/OrderSalesDetailEdit.tsx +++ b/src/components/orders/OrderSalesDetailEdit.tsx @@ -39,7 +39,7 @@ import { toast } from "sonner"; import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate"; import { orderSalesConfig } from "./orderSalesConfig"; import { BadgeSm } from "@/components/atoms/BadgeSm"; -import { formatAmount } from "@/lib/utils/amount"; +import { formatAmount, formatNumber } from "@/lib/utils/amount"; import { OrderItem, getOrderById, @@ -196,7 +196,7 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) { const upperUnit = (unit || "").toUpperCase(); if (countableUnits.includes(upperUnit)) { - return Math.round(quantity).toLocaleString(); + return formatNumber(Math.round(quantity)); } const rounded = Math.round(quantity * 10000) / 10000; diff --git a/src/components/orders/actions.ts b/src/components/orders/actions.ts index 671e6259..1ac3d8a7 100644 --- a/src/components/orders/actions.ts +++ b/src/components/orders/actions.ts @@ -3,6 +3,7 @@ import { executeServerAction } from '@/lib/api/execute-server-action'; import { buildApiUrl } from '@/lib/api/query-params'; import type { PaginatedApiResponse } from '@/lib/api/types'; +import { formatDate } from '@/lib/utils/date'; // ============================================================================ // API 타입 정의 @@ -517,7 +518,7 @@ function transformApiToFrontend(apiData: ApiOrder): Order { lotNumber: apiData.order_no, quoteNumber: apiData.quote?.quote_number || '', quoteId: apiData.quote_id ?? undefined, - orderDate: apiData.received_at || apiData.created_at.split('T')[0], + orderDate: apiData.received_at || formatDate(apiData.created_at), client: apiData.client_name || apiData.client?.name || '', clientId: apiData.client_id ?? undefined, siteName: apiData.site_name || '', diff --git a/src/components/orders/documents/ContractDocument.tsx b/src/components/orders/documents/ContractDocument.tsx index 4ac2670d..a2647b3d 100644 --- a/src/components/orders/documents/ContractDocument.tsx +++ b/src/components/orders/documents/ContractDocument.tsx @@ -7,7 +7,7 @@ * - DocumentHeader: simple 레이아웃 (결재란 없음) */ -import { formatAmount } from "@/lib/utils/amount"; +import { formatAmount, formatNumber } from "@/lib/utils/amount"; import { OrderItem } from "../actions"; import { DocumentHeader } from "@/components/document-system"; @@ -15,7 +15,7 @@ import { DocumentHeader } from "@/components/document-system"; * 수량 포맷 함수 (정수로 표시) */ function formatQuantity(quantity: number): string { - return Math.round(quantity).toLocaleString(); + return formatNumber(Math.round(quantity)); } // 제품 정보 타입 diff --git a/src/components/orders/documents/PurchaseOrderDocument.tsx b/src/components/orders/documents/PurchaseOrderDocument.tsx index 56b5288a..765c2cb8 100644 --- a/src/components/orders/documents/PurchaseOrderDocument.tsx +++ b/src/components/orders/documents/PurchaseOrderDocument.tsx @@ -7,6 +7,7 @@ import { getTodayString } from "@/lib/utils/date"; import { OrderItem } from "../actions"; +import { formatNumber } from '@/lib/utils/amount'; /** * 수량 포맷 함수 @@ -18,7 +19,7 @@ function formatQuantity(quantity: number, unit?: string): string { const upperUnit = (unit || "").toUpperCase(); if (countableUnits.includes(upperUnit)) { - return Math.round(quantity).toLocaleString(); + return formatNumber(Math.round(quantity)); } const rounded = Math.round(quantity * 10000) / 10000; diff --git a/src/components/orders/documents/SalesOrderDocument.tsx b/src/components/orders/documents/SalesOrderDocument.tsx index 317ab590..b1d70b10 100644 --- a/src/components/orders/documents/SalesOrderDocument.tsx +++ b/src/components/orders/documents/SalesOrderDocument.tsx @@ -12,6 +12,7 @@ import { getTodayString } from "@/lib/utils/date"; import { OrderItem } from "../actions"; import { ProductInfo } from "./OrderDocumentModal"; import { ConstructionApprovalTable } from "@/components/document-system"; +import { formatNumber } from '@/lib/utils/amount'; interface SalesOrderDocumentProps { documentNumber?: string; @@ -270,10 +271,10 @@ export function SalesOrderDocument({ {row.no} {row.type} {row.code} - {row.openW.toLocaleString()} - {row.openH.toLocaleString()} - {row.madeW.toLocaleString()} - {row.madeH.toLocaleString()} + {formatNumber(row.openW)} + {formatNumber(row.openH)} + {formatNumber(row.madeW)} + {formatNumber(row.madeH)} {row.guideRail} {row.shaft} {row.caseInch} @@ -319,10 +320,10 @@ export function SalesOrderDocument({ {row.no} {row.code} - {row.openW.toLocaleString()} - {row.openH.toLocaleString()} - {row.madeW.toLocaleString()} - {row.madeH.toLocaleString()} + {formatNumber(row.openW)} + {formatNumber(row.openH)} + {formatNumber(row.madeW)} + {formatNumber(row.madeH)} {row.guideRail} {row.shaft} {row.jointBar} diff --git a/src/components/orders/documents/TransactionDocument.tsx b/src/components/orders/documents/TransactionDocument.tsx index e717b899..f10c9392 100644 --- a/src/components/orders/documents/TransactionDocument.tsx +++ b/src/components/orders/documents/TransactionDocument.tsx @@ -7,7 +7,7 @@ * - DocumentHeader: simple 레이아웃 (결재란 없음) */ -import { formatAmount } from "@/lib/utils/amount"; +import { formatAmount, formatNumber } from "@/lib/utils/amount"; import { OrderItem } from "../actions"; import { DocumentHeader } from "@/components/document-system"; @@ -21,7 +21,7 @@ function formatQuantity(quantity: number, unit?: string): string { const upperUnit = (unit || "").toUpperCase(); if (countableUnits.includes(upperUnit)) { - return Math.round(quantity).toLocaleString(); + return formatNumber(Math.round(quantity)); } const rounded = Math.round(quantity * 10000) / 10000; diff --git a/src/components/organisms/DataTable.tsx b/src/components/organisms/DataTable.tsx index 08bad834..30b7033b 100644 --- a/src/components/organisms/DataTable.tsx +++ b/src/components/organisms/DataTable.tsx @@ -10,6 +10,7 @@ import { IconWithBadge } from "@/components/molecules/IconWithBadge"; import { TableActions, TableAction } from "@/components/molecules/TableActions"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; +import { formatNumber } from '@/lib/utils/amount'; // 셀 타입 정의 export type CellType = @@ -97,7 +98,7 @@ function renderCell(column: Column, value: any, row: T, index?: number): R // 타입별 렌더링 switch (column.type) { case "number": - return {formattedValue?.toLocaleString()}; + return {formatNumber(formattedValue)}; case "currency": const locale = column.currencyConfig?.locale || "ko-KR"; diff --git a/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx b/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx index 763c571b..8d9c9161 100644 --- a/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx +++ b/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx @@ -10,6 +10,7 @@ import { useState } from 'react'; import type { ShipmentDetail } from '../types'; import { DELIVERY_METHOD_LABELS } from '../types'; import { ConstructionApprovalTable } from '@/components/document-system'; +import { formatNumber } from '@/lib/utils/amount'; interface ShipmentOrderDocumentProps { title: string; @@ -271,10 +272,10 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s {row.no} {row.type} {row.code} - {row.openW.toLocaleString()} - {row.openH.toLocaleString()} - {row.madeW.toLocaleString()} - {row.madeH.toLocaleString()} + {formatNumber(row.openW)} + {formatNumber(row.openH)} + {formatNumber(row.madeW)} + {formatNumber(row.madeH)} {row.guideRail} {row.shaft} {row.caseInch} @@ -320,10 +321,10 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s {row.no} {row.code} - {row.openW.toLocaleString()} - {row.openH.toLocaleString()} - {row.madeW.toLocaleString()} - {row.madeH.toLocaleString()} + {formatNumber(row.openW)} + {formatNumber(row.openH)} + {formatNumber(row.madeW)} + {formatNumber(row.madeH)} {row.guideRail} {row.shaft} {row.jointBar} diff --git a/src/components/pricing-distribution/PriceDistributionDetail.tsx b/src/components/pricing-distribution/PriceDistributionDetail.tsx index 525ed13f..bed7ed4a 100644 --- a/src/components/pricing-distribution/PriceDistributionDetail.tsx +++ b/src/components/pricing-distribution/PriceDistributionDetail.tsx @@ -19,6 +19,7 @@ import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { getPresetStyle } from '@/lib/utils/status-config'; +import { formatNumber } from '@/lib/utils/amount'; import { Checkbox } from '@/components/ui/checkbox'; import { Select, @@ -212,8 +213,7 @@ export function PriceDistributionDetail({ id, mode: propMode }: Props) { // 금액 포맷 const formatPrice = (price?: number) => { - if (price === undefined || price === null) return '-'; - return price.toLocaleString(); + return formatNumber(price); }; if (isLoading) { diff --git a/src/components/pricing-distribution/PriceDistributionDocumentModal.tsx b/src/components/pricing-distribution/PriceDistributionDocumentModal.tsx index bd2d0c2f..6d8a68bc 100644 --- a/src/components/pricing-distribution/PriceDistributionDocumentModal.tsx +++ b/src/components/pricing-distribution/PriceDistributionDocumentModal.tsx @@ -11,6 +11,7 @@ import { DocumentViewer } from '@/components/document-system/viewer/DocumentViewer'; import { DocumentHeader } from '@/components/document-system/components/DocumentHeader'; import { ConstructionApprovalTable } from '@/components/document-system/components/ConstructionApprovalTable'; +import { formatNumber } from '@/lib/utils/amount'; import type { PriceDistributionDetail } from './types'; interface Props { @@ -132,7 +133,7 @@ export function PriceDistributionDocumentModal({ open, onOpenChange, detail }: P - {item.unit} - {item.salesPrice.toLocaleString()} + {formatNumber(item.salesPrice)} ))} diff --git a/src/components/pricing/PricingFinalizeDialog.tsx b/src/components/pricing/PricingFinalizeDialog.tsx index dcbbd7df..967444ff 100644 --- a/src/components/pricing/PricingFinalizeDialog.tsx +++ b/src/components/pricing/PricingFinalizeDialog.tsx @@ -14,6 +14,7 @@ import { } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Lock, CheckCircle2 } from 'lucide-react'; +import { formatNumber } from '@/lib/utils/amount'; interface PricingFinalizeDialogProps { open: boolean; @@ -56,13 +57,13 @@ export function PricingFinalizeDialog({
매입단가: - {purchasePrice?.toLocaleString() || '-'}원 + {formatNumber(purchasePrice)}원
판매단가: - {salesPrice?.toLocaleString() || '-'}원 + {formatNumber(salesPrice)}원
diff --git a/src/components/pricing/PricingFormClient.tsx b/src/components/pricing/PricingFormClient.tsx index 394da55c..3f420176 100644 --- a/src/components/pricing/PricingFormClient.tsx +++ b/src/components/pricing/PricingFormClient.tsx @@ -14,6 +14,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { getTodayString } from '@/lib/utils/date'; +import { formatNumber } from '@/lib/utils/amount'; import { DollarSign, Package, @@ -547,29 +548,29 @@ export function PricingFormClient({
입고가: - {(purchasePrice || 0).toLocaleString()}원 + {formatNumber(purchasePrice || 0)}원
가공비: - {(processingCost || 0).toLocaleString()}원 + {formatNumber(processingCost || 0)}원
소계: - {((purchasePrice || 0) + (processingCost || 0)).toLocaleString()}원 + {formatNumber((purchasePrice || 0) + (processingCost || 0))}원
{loss > 0 && (
LOSS ({loss}%): - +{(((purchasePrice || 0) + (processingCost || 0)) * (loss / 100)).toLocaleString()}원 + +{formatNumber(((purchasePrice || 0) + (processingCost || 0)) * (loss / 100))}원
)}
LOSS 적용 원가: - {costWithLoss.toLocaleString()}원 + {formatNumber(costWithLoss)}원
@@ -679,17 +680,17 @@ export function PricingFormClient({
LOSS 적용 원가: - {costWithLoss.toLocaleString()}원 + {formatNumber(costWithLoss)}원
판매단가: - {salesPrice.toLocaleString()}원 + {formatNumber(salesPrice)}원
마진: - {marginAmount.toLocaleString()}원 ({marginRate.toFixed(1)}%) + {formatNumber(marginAmount)}원 ({marginRate.toFixed(1)}%)
diff --git a/src/components/pricing/PricingHistoryDialog.tsx b/src/components/pricing/PricingHistoryDialog.tsx index a11ce919..56493bd7 100644 --- a/src/components/pricing/PricingHistoryDialog.tsx +++ b/src/components/pricing/PricingHistoryDialog.tsx @@ -15,6 +15,7 @@ import { Badge } from '@/components/ui/badge'; import { getPresetStyle } from '@/lib/utils/status-config'; import { Separator } from '@/components/ui/separator'; import { History } from 'lucide-react'; +import { formatNumber } from '@/lib/utils/amount'; import type { PricingData } from './types'; interface PricingHistoryDialogProps { @@ -66,19 +67,19 @@ export function PricingHistoryDialog({
매입단가:
- {pricingData.purchasePrice?.toLocaleString() || '-'}원 + {formatNumber(pricingData.purchasePrice)}원
가공비:
- {pricingData.processingCost?.toLocaleString() || '-'}원 + {formatNumber(pricingData.processingCost)}원
판매단가:
- {pricingData.salesPrice?.toLocaleString() || '-'}원 + {formatNumber(pricingData.salesPrice)}원
@@ -118,19 +119,19 @@ export function PricingHistoryDialog({
매입단가:
- {revision.previousData?.purchasePrice?.toLocaleString() || '-'}원 + {formatNumber(revision.previousData?.purchasePrice)}원
가공비:
- {revision.previousData?.processingCost?.toLocaleString() || '-'}원 + {formatNumber(revision.previousData?.processingCost)}원
판매단가:
- {revision.previousData?.salesPrice?.toLocaleString() || '-'}원 + {formatNumber(revision.previousData?.salesPrice)}원
diff --git a/src/components/pricing/PricingListClient.tsx b/src/components/pricing/PricingListClient.tsx index 13070d2e..cc1de3cf 100644 --- a/src/components/pricing/PricingListClient.tsx +++ b/src/components/pricing/PricingListClient.tsx @@ -31,6 +31,7 @@ import { import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import type { PricingListItem, ItemType } from './types'; import { ITEM_TYPE_LABELS, ITEM_TYPE_COLORS } from './types'; +import { formatNumber } from '@/lib/utils/amount'; interface PricingListClientProps { initialData: PricingListItem[]; @@ -100,8 +101,8 @@ export function PricingListClient({ // 금액 포맷팅 const formatPrice = (price?: number) => { - if (price === undefined || price === null) return '-'; - return `${price.toLocaleString()}원`; + if (price == null) return '-'; + return `${formatNumber(price)}원`; }; // 품목 유형 Badge 렌더링 diff --git a/src/components/production/WorkOrders/documents/ScreenInspectionContent.tsx b/src/components/production/WorkOrders/documents/ScreenInspectionContent.tsx index 1b2525f3..d3d08fce 100644 --- a/src/components/production/WorkOrders/documents/ScreenInspectionContent.tsx +++ b/src/components/production/WorkOrders/documents/ScreenInspectionContent.tsx @@ -31,6 +31,7 @@ import { JudgmentCell, calculateOverallResult, } from './inspection-shared'; +import { formatNumber } from '@/lib/utils/amount'; export type { InspectionContentRef }; @@ -108,7 +109,7 @@ export const ScreenInspectionContent = forwardRef { const num = value.replace(/[^\d]/g, ''); if (!num) return ''; - return Number(num).toLocaleString(); + return formatNumber(Number(num)); }; const handleInputChange = useCallback((rowId: number, field: 'lengthMeasured' | 'widthMeasured', value: string) => { diff --git a/src/components/production/WorkOrders/documents/ScreenWorkLogContent.tsx b/src/components/production/WorkOrders/documents/ScreenWorkLogContent.tsx index 7ec098ac..4dba5a84 100644 --- a/src/components/production/WorkOrders/documents/ScreenWorkLogContent.tsx +++ b/src/components/production/WorkOrders/documents/ScreenWorkLogContent.tsx @@ -23,6 +23,7 @@ import type { WorkOrder, WorkOrderItem } from '../types'; import { SectionHeader, ConstructionApprovalTable } from '@/components/document-system'; +import { formatNumber } from '@/lib/utils/amount'; // ===== 절단 계산 로직 (기존 시스템 calculateCutSize 이식) ===== @@ -122,7 +123,7 @@ export function ScreenWorkLogContent({ data: order, materialLots = [] }: ScreenW const items = order.items || []; // 숫자 천단위 콤마 포맷 - const fmt = (v?: number) => v != null ? v.toLocaleString() : '-'; + const fmt = (v?: number) => v != null ? formatNumber(v) : '-'; // floorCode에서 부호 추출: "1층/FSS-01" → "FSS-01" const getSymbolCode = (floorCode?: string) => { diff --git a/src/components/production/WorkOrders/documents/SlatWorkLogContent.tsx b/src/components/production/WorkOrders/documents/SlatWorkLogContent.tsx index 9b2833fd..b2ff7a5d 100644 --- a/src/components/production/WorkOrders/documents/SlatWorkLogContent.tsx +++ b/src/components/production/WorkOrders/documents/SlatWorkLogContent.tsx @@ -14,6 +14,7 @@ import type { WorkOrder } from '../types'; import { SectionHeader } from '@/components/document-system'; +import { formatNumber } from '@/lib/utils/amount'; interface MaterialInputLot { lot_no: string; @@ -47,7 +48,7 @@ export function SlatWorkLogContent({ data: order, materialLots = [] }: SlatWorkL const items = order.items || []; // 숫자 천단위 콤마 포맷 - const fmt = (v?: number) => v != null ? v.toLocaleString() : '-'; + const fmt = (v?: number) => v != null ? formatNumber(v) : '-'; // floorCode에서 부호 추출: "1층/FSS-01" → "FSS-01" const getSymbolCode = (floorCode?: string) => { diff --git a/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx index 26135c70..e3c98ae6 100644 --- a/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx +++ b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx @@ -34,6 +34,7 @@ import { getOrderInfo, INPUT_CLASS, } from './inspection-shared'; +import { formatNumber } from '@/lib/utils/amount'; export type { InspectionContentRef }; @@ -85,7 +86,7 @@ function resolveReferenceValue( function formatStandard(item: InspectionTemplateSectionItem, workItem?: WorkItemData): string { const refVal = resolveReferenceValue(item, workItem); - if (refVal !== null) return refVal.toLocaleString(); + if (refVal !== null) return formatNumber(refVal); const sc = item.standard_criteria; if (!sc) return item.standard || '-'; if (typeof sc === 'object') { diff --git a/src/components/production/WorkOrders/types.ts b/src/components/production/WorkOrders/types.ts index 270422a3..e5ac5ef5 100644 --- a/src/components/production/WorkOrders/types.ts +++ b/src/components/production/WorkOrders/types.ts @@ -2,6 +2,8 @@ * 작업지시 관리 타입 정의 */ +import { formatDate } from '@/lib/utils/date'; + // 공정 정보 (API 관계) export interface ProcessInfo { id: number; @@ -439,14 +441,14 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder { dueDate: api.scheduled_date || '-', assignee: assigneeName, assignees: assignees.length > 0 ? assignees : undefined, - orderDate: api.created_at.split('T')[0], + orderDate: formatDate(api.created_at), scheduledDate: api.scheduled_date || '', shipmentDate: api.scheduled_date || '-', isAssigned: api.assignee_id !== null || assignees.length > 0, isStarted: ['in_progress', 'completed', 'shipped'].includes(api.status), priority: priorityValue, priorityLabel: getPriorityLabel(priorityValue), - salesOrderDate: api.sales_order?.received_at?.split('T')[0] || api.sales_order?.created_at?.split('T')[0] || api.created_at.split('T')[0], + salesOrderDate: api.sales_order?.received_at?.split('T')[0] || api.sales_order?.created_at?.split('T')[0] || formatDate(api.created_at), salesOrderWriter: api.sales_order?.writer?.name || '-', clientContact: api.sales_order?.client_contact || '-', shutterCount: api.sales_order?.root_nodes_count || null, @@ -472,7 +474,7 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder { status: issue.is_resolved ? 'resolved' : 'pending', type: issue.priority === 'high' ? '긴급' : '일반', description: issue.title + (issue.description ? ` - ${issue.description}` : ''), - createdAt: issue.created_at.split('T')[0], + createdAt: formatDate(issue.created_at), })), note: api.memo || undefined, }; diff --git a/src/components/production/WorkResults/WorkResultList.tsx b/src/components/production/WorkResults/WorkResultList.tsx index 460620e5..7a743391 100644 --- a/src/components/production/WorkResults/WorkResultList.tsx +++ b/src/components/production/WorkResults/WorkResultList.tsx @@ -36,6 +36,7 @@ import { toast } from 'sonner'; import { getWorkResults, getWorkResultStats } from './actions'; import type { WorkResult, WorkResultStats } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { formatNumber } from '@/lib/utils/amount'; // 페이지당 항목 수 const ITEMS_PER_PAGE = 20; @@ -129,19 +130,19 @@ export function WorkResultList() { () => [ { label: '총 생산수량', - value: `${statsData.totalProduction.toLocaleString()}개`, + value: `${formatNumber(statsData.totalProduction)}개`, icon: Package, iconColor: 'text-gray-600', }, { label: '양품수량', - value: `${statsData.totalGood.toLocaleString()}개`, + value: `${formatNumber(statsData.totalGood)}개`, icon: CheckCircle2, iconColor: 'text-green-600', }, { label: '불량수량', - value: `${statsData.totalDefect.toLocaleString()}개`, + value: `${formatNumber(statsData.totalDefect)}개`, icon: XCircle, iconColor: 'text-red-600', }, diff --git a/src/components/production/WorkerScreen/InspectionInputModal.tsx b/src/components/production/WorkerScreen/InspectionInputModal.tsx index a9c93570..1e6ac898 100644 --- a/src/components/production/WorkerScreen/InspectionInputModal.tsx +++ b/src/components/production/WorkerScreen/InspectionInputModal.tsx @@ -23,6 +23,7 @@ import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; import type { InspectionTemplateData, InspectionTemplateSectionItem } from './types'; +import { formatNumber } from '@/lib/utils/amount'; // 중간검사 공정 타입 export type InspectionProcessType = @@ -278,7 +279,7 @@ function resolveRefValue( function formatDimension(val: number | undefined): string { if (val === undefined || val === null) return '-'; - return val.toLocaleString(); + return formatNumber(val); } // ===== 항목별 입력 유형 판별 ===== @@ -337,7 +338,7 @@ function DynamicInspectionForm({ if (isNumericItem(item)) { const numValue = formValues[fieldKey] as number | null | undefined; - const designLabel = designValue !== undefined ? designValue.toLocaleString() : ''; + const designLabel = designValue !== undefined ? formatNumber(designValue) : ''; const toleranceLabel = item.tolerance ? ` (${designLabel ? designLabel + ' ' : ''}${formatToleranceLabel(item.tolerance)})` : designLabel ? ` (${designLabel})` : ''; @@ -381,8 +382,8 @@ function DynamicInspectionForm({ const hasStandard = designValue !== undefined || item.standard; const standardDisplay = designValue !== undefined ? (item.tolerance - ? `${designValue.toLocaleString()} ${formatToleranceLabel(item.tolerance)}` - : String(designValue.toLocaleString())) + ? `${formatNumber(designValue)} ${formatToleranceLabel(item.tolerance)}` + : String(formatNumber(designValue))) : item.standard; return ( diff --git a/src/components/production/WorkerScreen/MaterialInputModal.tsx b/src/components/production/WorkerScreen/MaterialInputModal.tsx index 5163a080..7757e592 100644 --- a/src/components/production/WorkerScreen/MaterialInputModal.tsx +++ b/src/components/production/WorkerScreen/MaterialInputModal.tsx @@ -31,6 +31,7 @@ import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { getMaterialsForWorkOrder, registerMaterialInput, getMaterialsForItem, registerMaterialInputForItem, type MaterialForInput, type MaterialForItemInput } from './actions'; import type { WorkOrder } from '../ProductionDashboard/types'; import type { MaterialInput } from './types'; +import { formatNumber } from '@/lib/utils/amount'; interface MaterialInputModalProps { open: boolean; @@ -55,7 +56,7 @@ interface MaterialGroup { lots: MaterialForInput[]; } -const fmtQty = (v: number) => parseFloat(String(v)).toLocaleString(); +const fmtQty = (v: number) => formatNumber(parseFloat(String(v))); export function MaterialInputModal({ open, diff --git a/src/components/production/WorkerScreen/WorkItemCard.tsx b/src/components/production/WorkerScreen/WorkItemCard.tsx index 7019af1d..02a3c8be 100644 --- a/src/components/production/WorkerScreen/WorkItemCard.tsx +++ b/src/components/production/WorkerScreen/WorkItemCard.tsx @@ -32,6 +32,7 @@ import type { WorkStepData, MaterialListItem, } from './types'; +import { formatNumber } from '@/lib/utils/amount'; interface WorkItemCardProps { item: WorkItemData; @@ -91,7 +92,7 @@ export const WorkItemCard = memo(function WorkItemCard({
제작 사이즈 - {item.width.toLocaleString()} X {item.height.toLocaleString()} mm + {formatNumber(item.width)} X {formatNumber(item.height)} mm {item.quantity}개
@@ -205,7 +206,7 @@ export const WorkItemCard = memo(function WorkItemCard({ {mat.lotNo} {mat.itemName} - {parseFloat(String(mat.quantity)).toLocaleString()} + {formatNumber(parseFloat(String(mat.quantity)))} {mat.unit}
@@ -246,7 +247,7 @@ function ScreenCuttingInfo({ width, sheets }: { width: number; sheets: number })

절단정보

- 폭 {width.toLocaleString()}mm X {sheets}장 + 폭 {formatNumber(width)}mm X {sheets}장

); @@ -265,7 +266,7 @@ function SlatExtraInfo({ return (
- 길이 {length.toLocaleString()}mm + 길이 {formatNumber(length)}mm 슬랫 매수 {slatCount}장 @@ -317,7 +318,7 @@ function BendingExtraInfo({ info }: { info: BendingInfo }) {
{i === 0 ? '길이별 수량' : ''} - {lq.length.toLocaleString()}mm X {lq.quantity}개 + {formatNumber(lq.length)}mm X {lq.quantity}개
))} diff --git a/src/components/quality/InspectionManagement/documents/InspectionReportModal.tsx b/src/components/quality/InspectionManagement/documents/InspectionReportModal.tsx index b154290d..e7e90cb3 100644 --- a/src/components/quality/InspectionManagement/documents/InspectionReportModal.tsx +++ b/src/components/quality/InspectionManagement/documents/InspectionReportModal.tsx @@ -26,6 +26,7 @@ import type { } from '../types'; import type { FqcDocument, FqcTemplate } from '../fqcActions'; import { buildReportDocumentDataForItem } from '../mockData'; +import { formatDate } from '@/lib/utils/date'; interface InspectionReportModalProps { open: boolean; @@ -215,7 +216,7 @@ export function InspectionReportModal({ // PDF 메타 정보 const pdfMeta = useFqcMode && fqcDocument - ? { documentNumber: fqcDocument.documentNo, createdDate: fqcDocument.createdAt.split('T')[0] } + ? { documentNumber: fqcDocument.documentNo, createdDate: formatDate(fqcDocument.createdAt) } : legacyCurrentData ? { documentNumber: legacyCurrentData.documentNumber, createdDate: legacyCurrentData.createdDate } : undefined; @@ -246,7 +247,7 @@ export function InspectionReportModal({ template={fqcTemplate} documentData={fqcDocument.data} documentNo={fqcDocument.documentNo} - createdDate={fqcDocument.createdAt.split('T')[0]} + createdDate={formatDate(fqcDocument.createdAt)} readonly={true} /> ) : ( diff --git a/src/components/quotes/DiscountModal.tsx b/src/components/quotes/DiscountModal.tsx index 4b618025..caae6405 100644 --- a/src/components/quotes/DiscountModal.tsx +++ b/src/components/quotes/DiscountModal.tsx @@ -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({
@@ -189,7 +190,7 @@ export function DiscountModal({ handleDiscountAmountChange(e.target.value.replace(/,/g, ""))} className="pr-8 text-right" /> @@ -203,7 +204,7 @@ export function DiscountModal({
diff --git a/src/components/quotes/FormulaViewModal.tsx b/src/components/quotes/FormulaViewModal.tsx index f589cae2..3ed3feb4 100644 --- a/src/components/quotes/FormulaViewModal.tsx +++ b/src/components/quotes/FormulaViewModal.tsx @@ -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 }) {
1개당 - {bom.grand_total.toLocaleString()}원 + {formatNumber(bom.grand_total)}원 × {location.quantity}개 = - {(bom.grand_total * location.quantity).toLocaleString()}원 + {formatNumber(bom.grand_total * location.quantity)}원
@@ -212,7 +213,7 @@ function FormulaTable({ formulas, stepName }: { formulas: FormulaItem[]; stepNam {f.var} {f.desc} - {typeof f.value === 'number' ? f.value.toLocaleString() : f.value} + {typeof f.value === 'number' ? formatNumber(f.value) : f.value} {f.unit} ))} @@ -243,7 +244,7 @@ function FormulaTable({ formulas, stepName }: { formulas: FormulaItem[]; stepNam {f.formula} {f.calculation} - {typeof f.result === 'number' ? f.result.toLocaleString() : f.result} + {typeof f.result === 'number' ? formatNumber(f.result) : f.result} {f.unit} @@ -273,9 +274,9 @@ function FormulaTable({ formulas, stepName }: { formulas: FormulaItem[]; stepNam {f.item} {f.qty_formula} {f.qty_result} - {f.unit_price?.toLocaleString()} + {formatNumber(f.unit_price)} {f.price_calc} - {f.total?.toLocaleString()} + {formatNumber(f.total)} ))} @@ -299,7 +300,7 @@ function FormulaTable({ formulas, stepName }: { formulas: FormulaItem[]; stepNam {f.category} {f.formula} - {f.result?.toLocaleString()}원 + {typeof f.result === 'number' ? formatNumber(f.result) : f.result}원 ))} diff --git a/src/components/quotes/PurchaseOrderDocument.tsx b/src/components/quotes/PurchaseOrderDocument.tsx index 30f63f7b..a670356d 100644 --- a/src/components/quotes/PurchaseOrderDocument.tsx +++ b/src/components/quotes/PurchaseOrderDocument.tsx @@ -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 {item.no} {item.name} {item.spec} - {item.length > 0 ? item.length.toLocaleString() : ''} + {item.length > 0 ? formatNumber(item.length) : ''} {item.quantity} {item.note} diff --git a/src/components/quotes/QuoteDocument.tsx b/src/components/quotes/QuoteDocument.tsx index e46e01a8..312b24bb 100644 --- a/src/components/quotes/QuoteDocument.tsx +++ b/src/components/quotes/QuoteDocument.tsx @@ -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) => { diff --git a/src/components/quotes/QuoteFooterBar.tsx b/src/components/quotes/QuoteFooterBar.tsx index f7c1f1f9..1a97e410 100644 --- a/src/components/quotes/QuoteFooterBar.tsx +++ b/src/components/quotes/QuoteFooterBar.tsx @@ -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({

예상 전체 견적금액

- {totalAmount.toLocaleString()} + {formatNumber(totalAmount)}

diff --git a/src/components/quotes/QuoteManagementClient.tsx b/src/components/quotes/QuoteManagementClient.tsx index e48ad60c..49ec1b4a 100644 --- a/src/components/quotes/QuoteManagementClient.tsx +++ b/src/components/quotes/QuoteManagementClient.tsx @@ -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]' }, ], diff --git a/src/components/quotes/QuotePreviewContent.tsx b/src/components/quotes/QuotePreviewContent.tsx index 3d399121..b9ae7c20 100644 --- a/src/components/quotes/QuotePreviewContent.tsx +++ b/src/components/quotes/QuotePreviewContent.tsx @@ -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({ {loc.quantity} SET - {(loc.unitPrice || 0).toLocaleString()} + {formatNumber(loc.unitPrice || 0)} - {(loc.totalPrice || 0).toLocaleString()} + {formatNumber(loc.totalPrice || 0)} ))} @@ -231,7 +232,7 @@ export function QuotePreviewContent({ - {subtotal.toLocaleString()} + {formatNumber(subtotal)} {/* 할인율 */} @@ -250,7 +251,7 @@ export function QuotePreviewContent({ 할인금액 - {hasDiscount ? `-${discountAmount.toLocaleString()}` : '0'} + {hasDiscount ? `-${formatNumber(discountAmount)}` : '0'} @@ -259,7 +260,7 @@ export function QuotePreviewContent({ 할인 후 금액 - {afterDiscount.toLocaleString()} + {formatNumber(afterDiscount)} {/* 부가세 포함일 때 추가 행들 */} @@ -269,13 +270,13 @@ export function QuotePreviewContent({ 부가가치세 합계 - {vat.toLocaleString()} + {formatNumber(vat)} 총 견적금액 - {grandTotal.toLocaleString()} + {formatNumber(grandTotal)} )} @@ -289,7 +290,7 @@ export function QuotePreviewContent({ 합계금액 ({vatIncluded ? '부가세 포함' : '부가세 별도'}) - ₩ {grandTotal.toLocaleString()} + ₩ {formatNumber(grandTotal)}
@@ -346,10 +347,10 @@ export function QuotePreviewContent({ {item.unit || 'EA'} - {item.unit_price.toLocaleString()} + {formatNumber(item.unit_price)} - {item.total_price.toLocaleString()} + {formatNumber(item.total_price)} )) @@ -372,10 +373,10 @@ export function QuotePreviewContent({ - {(loc.bomResult?.grand_total || 0).toLocaleString()} + {formatNumber(loc.bomResult?.grand_total || 0)} - {(locationSubtotal * loc.quantity).toLocaleString()} + {formatNumber(locationSubtotal * loc.quantity)} @@ -388,7 +389,7 @@ export function QuotePreviewContent({ 총 합계 - {subtotal.toLocaleString()} + {formatNumber(subtotal)} diff --git a/src/components/quotes/QuoteRegistration.tsx b/src/components/quotes/QuoteRegistration.tsx index 84d19d0b..701f3d53 100644 --- a/src/components/quotes/QuoteRegistration.tsx +++ b/src/components/quotes/QuoteRegistration.tsx @@ -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)}원)`); }, []); // 개소별 합계 diff --git a/src/components/quotes/QuoteSummaryPanel.tsx b/src/components/quotes/QuoteSummaryPanel.tsx index 22271695..9cb36844 100644 --- a/src/components/quotes/QuoteSummaryPanel.tsx +++ b/src/components/quotes/QuoteSummaryPanel.tsx @@ -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({

상세소계

- {loc.totalPrice.toLocaleString()} + {formatNumber(loc.totalPrice)}

{loc.unitPrice > 0 && (

- 수량 적용: {(loc.unitPrice * loc.quantity).toLocaleString()} + 수량 적용: {formatNumber(loc.unitPrice * loc.quantity)}

)}
@@ -221,7 +222,7 @@ export function QuoteSummaryPanel({
({category.count}개) - {category.amount.toLocaleString()} + {formatNumber(category.amount)}
@@ -232,11 +233,11 @@ export function QuoteSummaryPanel({
{item.name}

- 수량: {item.quantity} × 단가: {item.unitPrice.toLocaleString()} + 수량: {item.quantity} × 단가: {formatNumber(item.unitPrice)}

- {item.totalPrice.toLocaleString()} + {formatNumber(item.totalPrice)}
))} @@ -258,7 +259,7 @@ export function QuoteSummaryPanel({

예상 견적금액

- {totalAmount.toLocaleString()} + {formatNumber(totalAmount)}

diff --git a/src/components/quotes/QuoteTransactionModal.tsx b/src/components/quotes/QuoteTransactionModal.tsx index 6ab8bd6c..c3640d87 100644 --- a/src/components/quotes/QuoteTransactionModal.tsx +++ b/src/components/quotes/QuoteTransactionModal.tsx @@ -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({ {loc.quantity || 1} SET - {(loc.unitPrice || 0).toLocaleString()} + {formatNumber(loc.unitPrice || 0)} - {(loc.totalPrice || 0).toLocaleString()} + {formatNumber(loc.totalPrice || 0)} )) @@ -248,7 +249,7 @@ export function QuoteTransactionModal({ - {subtotal.toLocaleString()} + {formatNumber(subtotal)} {/* 할인율 */} @@ -267,7 +268,7 @@ export function QuoteTransactionModal({ 할인금액 - {hasDiscount ? discountAmount.toLocaleString() : '0'} + {hasDiscount ? formatNumber(discountAmount) : '0'} @@ -276,7 +277,7 @@ export function QuoteTransactionModal({ 할인 후 금액 - {afterDiscount.toLocaleString()} + {formatNumber(afterDiscount)} {/* 부가세 포함일 때 추가 행들 */} @@ -286,13 +287,13 @@ export function QuoteTransactionModal({ 부가가치세 합계 - {vat.toLocaleString()} + {formatNumber(vat)} 총 금액 - {grandTotal.toLocaleString()} + {formatNumber(grandTotal)} )} @@ -306,7 +307,7 @@ export function QuoteTransactionModal({ 합계금액 ({vatIncluded ? '부가세 포함' : '부가세 별도'}) - ₩ {grandTotal.toLocaleString()} + ₩ {formatNumber(grandTotal)}
diff --git a/src/components/settings/AccountManagement/AccountDetail.tsx b/src/components/settings/AccountManagement/AccountDetail.tsx index 8ef47595..78757d54 100644 --- a/src/components/settings/AccountManagement/AccountDetail.tsx +++ b/src/components/settings/AccountManagement/AccountDetail.tsx @@ -17,6 +17,7 @@ import { SelectValue, } from '@/components/ui/select'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; +import { useDeleteDialog } from '@/hooks/useDeleteDialog'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { toast } from 'sonner'; @@ -39,7 +40,11 @@ export function AccountDetail({ account, mode: initialMode }: AccountDetailProps const router = useRouter(); const searchParams = useSearchParams(); const [mode, setMode] = useState(initialMode); - const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const deleteDialog = useDeleteDialog({ + onDelete: async (id) => deleteBankAccount(Number(id)), + onSuccess: () => router.push('/ko/settings/accounts'), + entityName: '계좌', + }); // URL에서 mode 파라미터 확인 useEffect(() => { @@ -99,21 +104,6 @@ export function AccountDetail({ account, mode: initialMode }: AccountDetailProps } }; - const handleDelete = () => { - setShowDeleteDialog(true); - }; - - const handleConfirmDelete = async () => { - if (!account?.id) return; - const result = await deleteBankAccount(account.id); - if (result.success) { - toast.success('계좌가 삭제되었습니다.'); - router.push('/ko/settings/accounts'); - } else { - toast.error(result.error || '계좌 삭제에 실패했습니다.'); - } - }; - const handleCancel = () => { if (isCreateMode) { router.push('/ko/settings/accounts'); @@ -202,7 +192,7 @@ export function AccountDetail({ account, mode: initialMode }: AccountDetailProps 목록으로
- @@ -216,8 +206,8 @@ export function AccountDetail({ account, mode: initialMode }: AccountDetailProps {/* 삭제 확인 다이얼로그 */} 계좌를 정말 삭제하시겠습니까? @@ -227,7 +217,8 @@ export function AccountDetail({ account, mode: initialMode }: AccountDetailProps } - onConfirm={handleConfirmDelete} + onConfirm={deleteDialog.single.confirm} + loading={deleteDialog.isPending} /> ); diff --git a/src/components/settings/PaymentHistoryManagement/PaymentHistoryClient.tsx b/src/components/settings/PaymentHistoryManagement/PaymentHistoryClient.tsx index b4ca9b93..b86c14af 100644 --- a/src/components/settings/PaymentHistoryManagement/PaymentHistoryClient.tsx +++ b/src/components/settings/PaymentHistoryManagement/PaymentHistoryClient.tsx @@ -29,6 +29,7 @@ import { } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { getPayments } from './actions'; +import { formatNumber } from '@/lib/utils/amount'; import type { PaymentHistory, SortOption } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; @@ -147,7 +148,7 @@ export function PaymentHistoryClient({ {/* 금액 */} - {item.amount.toLocaleString()}원 + {formatNumber(item.amount)}원 {/* 거래명세서 */} @@ -190,7 +191,7 @@ export function PaymentHistoryClient({ - +
{/* 금액 */} - {item.amount.toLocaleString()} + {formatNumber(item.amount)} {/* 거래명세서 */} @@ -159,7 +160,7 @@ export function PaymentHistoryManagement({ - +
setDeleteDialogOpen(true); - - const confirmDelete = async () => { - if (!role) return; - - setIsDeleting(true); - try { - const result = await deleteRole(role.id); - if (result.success) { - toast.success('역할이 삭제되었습니다.'); - handleBack(); - } else { - toast.error(result.error || '역할 삭제 실패'); - } - } catch (error) { - if (isNextRedirectError(error)) throw error; - toast.error(error instanceof Error ? error.message : '삭제 중 오류 발생'); - } finally { - setIsDeleting(false); - setDeleteDialogOpen(false); - } - }; + const deleteDialog = useDeleteDialog({ + onDelete: async (id) => deleteRole(Number(id)), + onSuccess: handleBack, + entityName: '역할', + }); // 메뉴의 권한 상태 가져오기 const getMenuPermission = (menuId: number, permType: string): boolean => { @@ -664,7 +646,7 @@ export function PermissionDetailClient({ permissionId, isNew = false, mode = 'vi
- {apiCallsUsed.toLocaleString()} / {apiCallsLimit.toLocaleString()} + {formatNumber(apiCallsUsed)} / {formatNumber(apiCallsLimit)}
diff --git a/src/components/settings/TitleManagement/index.tsx b/src/components/settings/TitleManagement/index.tsx index a83e8f3a..973bdc26 100644 --- a/src/components/settings/TitleManagement/index.tsx +++ b/src/components/settings/TitleManagement/index.tsx @@ -10,6 +10,7 @@ import { Input } from '@/components/ui/input'; import { Card, CardContent } from '@/components/ui/card'; import { TitleDialog } from './TitleDialog'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; +import { useDeleteDialog } from '@/hooks/useDeleteDialog'; import { toast } from 'sonner'; import type { Title } from './types'; import { @@ -36,8 +37,17 @@ export function TitleManagement() { const [selectedTitle, setSelectedTitle] = useState(); // 삭제 확인 다이얼로그 - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [titleToDelete, setTitleToDelete] = useState<Title | null>(null); + const deleteDialog = useDeleteDialog({ + onDelete: async (id) => { + const numId = Number(id); + const result = await deleteTitle(numId); + if (result.success) { + setTitles(prev => prev.filter(t => t.id !== numId)); + } + return result; + }, + entityName: '직책', + }); // 드래그 상태 const [draggedItem, setDraggedItem] = useState<number | null>(null); @@ -96,36 +106,6 @@ export function TitleManagement() { setDialogOpen(true); }; - // 직책 삭제 확인 - const handleDelete = (title: Title) => { - setTitleToDelete(title); - setDeleteDialogOpen(true); - }; - - // 삭제 실행 - const confirmDelete = async () => { - if (!titleToDelete || isSubmitting) return; - - try { - setIsSubmitting(true); - const result = await deleteTitle(titleToDelete.id); - if (result.success) { - setTitles(prev => prev.filter(t => t.id !== titleToDelete.id)); - toast.success('직책이 삭제되었습니다.'); - } else { - toast.error(result.error || '직책 삭제에 실패했습니다.'); - } - } catch (error) { - if (isNextRedirectError(error)) throw error; - console.error('직책 삭제 실패:', error); - toast.error('직책 삭제에 실패했습니다.'); - } finally { - setIsSubmitting(false); - setDeleteDialogOpen(false); - setTitleToDelete(null); - } - }; - // 다이얼로그 제출 const handleDialogSubmit = async (name: string) => { if (dialogMode === 'edit' && selectedTitle) { @@ -292,9 +272,9 @@ export function TitleManagement() { <Button variant="ghost" size="sm" - onClick={() => handleDelete(title)} + onClick={() => deleteDialog.single.open(String(title.id))} className="h-8 w-8 p-0 text-destructive hover:text-destructive" - disabled={isSubmitting} + disabled={deleteDialog.isPending} > <Trash2 className="h-4 w-4" /> <span className="sr-only">삭제</span> @@ -331,20 +311,20 @@ export function TitleManagement() { {/* 삭제 확인 다이얼로그 */} <DeleteConfirmDialog - open={deleteDialogOpen} - onOpenChange={setDeleteDialogOpen} - onConfirm={confirmDelete} + open={deleteDialog.single.isOpen} + onOpenChange={deleteDialog.single.onOpenChange} + onConfirm={deleteDialog.single.confirm} title="직책 삭제" description={ <> - "{titleToDelete?.name}" 직책을 삭제하시겠습니까? + "{titles.find(t => String(t.id) === deleteDialog.single.targetId)?.name}" 직책을 삭제하시겠습니까? <br /> <span className="text-destructive"> 이 직책을 사용 중인 사원이 있으면 해당 사원의 직책이 초기화됩니다. </span> </> } - loading={isSubmitting} + loading={deleteDialog.isPending} /> </PageLayout> ); diff --git a/src/components/templates/IntegratedListTemplateV2.tsx b/src/components/templates/IntegratedListTemplateV2.tsx index 3020c702..e5406062 100644 --- a/src/components/templates/IntegratedListTemplateV2.tsx +++ b/src/components/templates/IntegratedListTemplateV2.tsx @@ -26,6 +26,7 @@ import { ScreenVersionHistory } from "@/components/organisms/ScreenVersionHistor import { TabChip } from "@/components/atoms/TabChip"; import { MultiSelectCombobox } from "@/components/ui/multi-select-combobox"; import { MobileFilter, FilterFieldConfig, FilterValues } from "@/components/molecules/MobileFilter"; +import { formatNumber } from '@/lib/utils/amount'; /** * 기본 통합 목록_버젼2 @@ -904,13 +905,13 @@ export function IntegratedListTemplateV2<T = any>({ </Button> )} <span className="text-xs text-muted-foreground"> - {loadedCount.toLocaleString()} / {totalDataCount.toLocaleString()} + {formatNumber(loadedCount)} / {formatNumber(totalDataCount)} </span> </div> </> ) : ( <div className="text-center py-4 text-sm text-muted-foreground"> - 모든 항목을 불러왔습니다 ({totalDataCount.toLocaleString()}개) + 모든 항목을 불러왔습니다 ({formatNumber(totalDataCount)}개) </div> )} </div> diff --git a/src/components/templates/UniversalListPage/index.tsx b/src/components/templates/UniversalListPage/index.tsx index 5574bbb8..d1d08265 100644 --- a/src/components/templates/UniversalListPage/index.tsx +++ b/src/components/templates/UniversalListPage/index.tsx @@ -120,13 +120,35 @@ export function UniversalListPage<T>({ const filteredData = useMemo(() => { if (!config.clientSideFiltering) { - // 서버 사이드 모드: searchFilter가 정의되어 있으면 클라이언트 사이드 검색 적용 (백엔드 검색 미지원 대비) + // 서버 사이드 모드 + let serverData = rawData; + + // searchFilter가 정의되어 있으면 클라이언트 사이드 검색 적용 (백엔드 검색 미지원 대비) if (debouncedSearchValue && config.searchFilter) { - return rawData.filter((item) => + serverData = rawData.filter((item) => config.searchFilter!(item, debouncedSearchValue) ); } - return rawData; + + // 서버 사이드에서도 컬럼 정렬 지원 (onSortChange 미정의 시 클라이언트 사이드 정렬) + if (sortBy && !config.onSortChange) { + serverData = [...serverData].sort((a, b) => { + const aValue = (a as Record<string, unknown>)[sortBy]; + const bValue = (b as Record<string, unknown>)[sortBy]; + if (aValue == null && bValue == null) return 0; + if (aValue == null) return sortOrder === 'asc' ? 1 : -1; + if (bValue == null) return sortOrder === 'asc' ? -1 : 1; + if (typeof aValue === 'number' && typeof bValue === 'number') { + return sortOrder === 'asc' ? aValue - bValue : bValue - aValue; + } + const aStr = String(aValue); + const bStr = String(bValue); + const comparison = aStr.localeCompare(bStr, 'ko'); + return sortOrder === 'asc' ? comparison : -comparison; + }); + } + + return serverData; } let filtered = [...rawData]; @@ -194,7 +216,7 @@ export function UniversalListPage<T>({ } return filtered; - }, [rawData, activeTab, debouncedSearchValue, filters, sortBy, sortOrder, config.clientSideFiltering, config.tabFilter, config.searchFilter, config.customFilterFn, config.customSortFn, config.dateRangeSelector]); + }, [rawData, activeTab, debouncedSearchValue, filters, sortBy, sortOrder, config.clientSideFiltering, config.onSortChange, config.tabFilter, config.searchFilter, config.customFilterFn, config.customSortFn, config.dateRangeSelector]); // 페이지네이션 (클라이언트 사이드 + 서버 사이드 검색 시) const paginatedData = useMemo(() => { @@ -221,7 +243,8 @@ export function UniversalListPage<T>({ : (isServerSearchFiltered ? (Math.ceil(filteredData.length / itemsPerPage) || 1) : serverTotalPages); // 표시할 데이터 - const displayData = (config.clientSideFiltering || isServerSearchFiltered) ? paginatedData : rawData; + // 서버 사이드 모드에서도 filteredData 사용 (클라이언트 사이드 정렬 반영) + const displayData = (config.clientSideFiltering || isServerSearchFiltered) ? paginatedData : filteredData; // ===== 탭 카운트 계산 (클라이언트 사이드) ===== const computedTabs = useMemo(() => { @@ -720,21 +743,33 @@ export function UniversalListPage<T>({ // ===== 정렬 핸들러 ===== const handleSort = useCallback((key: string) => { + let newSortBy: string | undefined; + let newSortOrder: 'asc' | 'desc' = 'asc'; + if (sortBy === key) { // 같은 컬럼 클릭: asc → desc → 정렬 해제 if (sortOrder === 'asc') { - setSortOrder('desc'); + newSortBy = key; + newSortOrder = 'desc'; } else { - setSortBy(undefined); - setSortOrder('asc'); + newSortBy = undefined; + newSortOrder = 'asc'; } } else { // 다른 컬럼 클릭: 해당 컬럼으로 asc 정렬 - setSortBy(key); - setSortOrder('asc'); + newSortBy = key; + newSortOrder = 'asc'; } + + setSortBy(newSortBy); + setSortOrder(newSortOrder); setCurrentPage(1); - }, [sortBy, sortOrder]); + + // 서버 사이드 정렬: 부모 컴포넌트에 콜백 전달 + if (!config.clientSideFiltering && config.onSortChange) { + config.onSortChange(newSortBy, newSortOrder); + } + }, [sortBy, sortOrder, config.clientSideFiltering, config.onSortChange]); // ===== 탭 핸들러 ===== const handleTabChange = useCallback((value: string) => { @@ -922,10 +957,10 @@ export function UniversalListPage<T>({ } // 테이블 컬럼 (탭별 다른 컬럼 지원) tableColumns={effectiveColumns} - // 정렬 설정 (클라이언트 사이드 필터링 시에만 활성화) - sortBy={config.clientSideFiltering ? sortBy : undefined} - sortOrder={config.clientSideFiltering ? sortOrder : undefined} - onSort={config.clientSideFiltering ? handleSort : undefined} + // 정렬 설정 (모든 페이지에서 활성화) + sortBy={sortBy} + sortOrder={sortOrder} + onSort={handleSort} // 커스텀 테이블 헤더 (동적 컬럼용) renderCustomTableHeader={ config.renderCustomTableHeader diff --git a/src/components/templates/UniversalListPage/types.ts b/src/components/templates/UniversalListPage/types.ts index 64374705..d4b1c709 100644 --- a/src/components/templates/UniversalListPage/types.ts +++ b/src/components/templates/UniversalListPage/types.ts @@ -375,6 +375,8 @@ export interface UniversalListConfig<T> { customFilterFn?: (items: T[], filterValues: Record<string, string | string[]>) => T[]; /** 커스텀 정렬 함수 */ customSortFn?: (items: T[], filterValues: Record<string, string | string[]>) => T[]; + /** 서버 사이드 정렬 콜백 (clientSideFiltering: false일 때 컬럼 헤더 클릭 시 호출) */ + onSortChange?: (sortBy: string | undefined, sortOrder: 'asc' | 'desc') => void; // ===== 테이블 헤더 액션 ===== /** 테이블 헤더 우측 추가 액션 (총건, 필터 해제 버튼, 일괄 액션 등) diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 89d920b8..af8c00f5 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -58,3 +58,4 @@ export { useFCM } from './useFCM'; export { useMenuPolling } from './useMenuPolling'; export { useCEODashboard } from './useCEODashboard'; export { useCardManagementModals } from './useCardManagementModals'; +export { useDeleteDialog } from './useDeleteDialog'; diff --git a/src/hooks/useItemList.ts b/src/hooks/useItemList.ts index 80a07fe9..b0173385 100644 --- a/src/hooks/useItemList.ts +++ b/src/hooks/useItemList.ts @@ -268,14 +268,13 @@ export function useItemList(): UseItemListResult { setTabColumns(initData.tabColumns); } - // 2. 품목 목록 API 호출 + 전체 통계 병렬 조회 - const [result, stats] = await Promise.all([ - fetchItems(currentFilters.current), - fetchTotalStats(), - ]); + // 2. 품목 목록 먼저 로드 (테이블 렌더링 우선) + const result = await fetchItems(currentFilters.current); setItems(result.items); setPagination(result.pagination); - setTotalStats(stats); + + // 3. 전체 통계는 백그라운드로 로드 (테이블 렌더링을 블로킹하지 않음) + fetchTotalStats().then(stats => setTotalStats(stats)); } catch (err) { console.error('품목 목록 로드 실패:', err); diff --git a/src/lib/api/dashboard/transformers.ts b/src/lib/api/dashboard/transformers.ts index 24f418d1..7d8ecfd9 100644 --- a/src/lib/api/dashboard/transformers.ts +++ b/src/lib/api/dashboard/transformers.ts @@ -43,6 +43,7 @@ import type { WelfareData, HighlightColor, } from '@/components/business/CEODashboard/types'; +import { formatNumber } from '@/lib/utils/amount'; // ============================================ // 헬퍼 함수 @@ -90,9 +91,9 @@ function formatAmount(amount: number): string { if (absAmount >= 100000000) { return `${(amount / 100000000).toFixed(1)}억원`; } else if (absAmount >= 10000) { - return `${Math.round(amount / 10000).toLocaleString()}만원`; + return `${formatNumber(Math.round(amount / 10000))}만원`; } - return `${amount.toLocaleString()}원`; + return `${formatNumber(amount)}원`; } /** @@ -974,7 +975,7 @@ export function transformWelfareDetailResponse(api: WelfareDetailApiResponse): D const calculationCards = calculation.type === 'fixed' ? { title: '복리후생비 계산', - subtitle: `직원당 정액 금액/월 ${(calculation.fixed_amount_per_month ?? 200000).toLocaleString()}원`, + subtitle: `직원당 정액 금액/월 ${formatNumber(calculation.fixed_amount_per_month ?? 200000)}원`, cards: [ { label: '직원 수', value: calculation.employee_count, unit: '명' }, { label: '연간 직원당 월급 금액', value: calculation.annual_amount_per_employee ?? 0, unit: '원', operator: '×' as const }, diff --git a/src/lib/utils/excel-download.ts b/src/lib/utils/excel-download.ts index ec569d65..6bb58966 100644 --- a/src/lib/utils/excel-download.ts +++ b/src/lib/utils/excel-download.ts @@ -22,6 +22,8 @@ * ``` */ +import { getTodayString } from '@/lib/utils/date'; + // xlsx는 ~400KB로 무거워서, 실제 사용 시점에 동적 로드 async function loadXLSX() { const XLSX = await import('xlsx'); @@ -304,7 +306,7 @@ export async function downloadExcelTemplate({ return col.options[0]; } if (col.type === 'date') { - return new Date().toISOString().slice(0, 10); + return getTodayString(); } if (col.type === 'number') { return 0;