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

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

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

View File

@@ -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 간 의존성 상세
| 의존 관계 | 이유 |

View File

@@ -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 적용**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
<TableCell className="font-medium">{item.vendorName}</TableCell>
{/* 채권금액 */}
<TableCell className="text-right font-medium text-red-600">
{item.debtAmount.toLocaleString()}
{formatNumber(item.debtAmount)}
</TableCell>
{/* 발생일 */}
<TableCell className="text-center">{item.occurrenceDate}</TableCell>
@@ -410,7 +411,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
<MobileCard
key={item.id}
title={item.vendorName}
subtitle={`채권금액: ${item.debtAmount.toLocaleString()}`}
subtitle={`채권금액: ${formatNumber(item.debtAmount)}`}
badge={COLLECTION_STATUS_LABELS[item.status]}
badgeVariant="outline"
badgeClassName={STATUS_BADGE_STYLES[item.status]}

View File

@@ -2,6 +2,7 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { format } from 'date-fns';
import { formatNumber } from '@/lib/utils/amount';
import { Loader2, RotateCcw } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
@@ -334,7 +335,7 @@ export function TransactionFormModal({
<Input
type="text"
inputMode="numeric"
value={formData.amount ? formData.amount.toLocaleString() : ''}
value={formData.amount ? formatNumber(formData.amount) : ''}
onChange={(e) => {
const raw = e.target.value.replace(/[^0-9]/g, '');
handleChange('amount', raw ? parseInt(raw, 10) : 0);
@@ -344,7 +345,7 @@ export function TransactionFormModal({
<div className="space-y-2">
<Label className="font-medium text-gray-500"> ()</Label>
<Input
value={calculatedBalance.toLocaleString()}
value={formatNumber(calculatedBalance)}
disabled
className="bg-gray-50"
/>

View File

@@ -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() {
<TableCell />
<TableCell colSpan={5} className="text-right font-bold"></TableCell>
<TableCell className="text-right font-bold text-blue-600">
{tableTotals.totalDeposit.toLocaleString()}
{formatNumber(tableTotals.totalDeposit)}
</TableCell>
<TableCell className="text-right font-bold text-red-600">
{tableTotals.totalWithdrawal.toLocaleString()}
{formatNumber(tableTotals.totalWithdrawal)}
</TableCell>
<TableCell />
<TableCell />
@@ -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() {
</TableCell>
{/* 입금 */}
<TableCell className={`text-right font-medium text-blue-600 ${isCellModified(item, 'deposit_amount') ? 'bg-green-100 rounded' : ''}`}>
{item.depositAmount > 0 ? item.depositAmount.toLocaleString() : '-'}
{item.depositAmount > 0 ? formatNumber(item.depositAmount) : '-'}
</TableCell>
{/* 출금 */}
<TableCell className={`text-right font-medium text-red-600 ${isCellModified(item, 'withdrawal_amount') ? 'bg-green-100 rounded' : ''}`}>
{item.withdrawalAmount > 0 ? item.withdrawalAmount.toLocaleString() : '-'}
{item.withdrawalAmount > 0 ? formatNumber(item.withdrawalAmount) : '-'}
</TableCell>
{/* 잔액 */}
<TableCell className="text-right font-medium">
{item.balance.toLocaleString()}
{formatNumber(item.balance)}
</TableCell>
{/* 취급점 */}
<TableCell className="text-center text-sm text-muted-foreground">
@@ -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 || '-' },
]}

View File

@@ -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<BillRecord>) => 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<BillRecord>, Partial<BillRecord>>({
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<BillRecord> = {
...formData,
vendorName: clients.find(c => c.id === formData.vendorId)?.name || '',
};
setIsSubmitting(true);
try {
const billData: Partial<BillRecord> = {
...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(() => {

View File

@@ -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({
</Badge>
</TableCell>
<TableCell>{item.vendorName}</TableCell>
<TableCell className="text-right font-medium">{item.amount.toLocaleString()}</TableCell>
<TableCell className="text-right font-medium">{formatNumber(item.amount)}</TableCell>
<TableCell>{item.issueDate}</TableCell>
<TableCell>{item.maturityDate}</TableCell>
<TableCell className="text-center">{item.installmentCount || '-'}</TableCell>
@@ -247,7 +249,7 @@ export function BillManagementClient({
infoGrid={
<div className="grid grid-cols-2 gap-3">
<InfoField label="거래처" value={item.vendorName} />
<InfoField label="금액" value={`${item.amount.toLocaleString()}`} />
<InfoField label="금액" value={`${formatNumber(item.amount)}`} />
<InfoField label="발행일" value={item.issueDate} />
<InfoField label="만기일" value={item.maturityDate} />
</div>

View File

@@ -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
{/* 거래처 */}
<TableCell>{item.vendorName}</TableCell>
{/* 금액 */}
<TableCell className="text-right font-medium">{item.amount.toLocaleString()}</TableCell>
<TableCell className="text-right font-medium">{formatNumber(item.amount)}</TableCell>
{/* 발행일 */}
<TableCell>{item.issueDate}</TableCell>
{/* 만기일 */}
@@ -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) },

View File

@@ -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 }
<div className="grid grid-cols-3 gap-3">
<div>
<Label className="text-xs text-muted-foreground"></Label>
<p className="text-sm font-medium mt-0.5">{transaction.supplyAmount.toLocaleString()}</p>
<p className="text-sm font-medium mt-0.5">{formatNumber(transaction.supplyAmount)}</p>
</div>
<div>
<Label className="text-xs text-muted-foreground"></Label>
<p className="text-sm font-medium mt-0.5">{transaction.taxAmount.toLocaleString()}</p>
<p className="text-sm font-medium mt-0.5">{formatNumber(transaction.taxAmount)}</p>
</div>
<div>
<Label className="text-xs text-muted-foreground"></Label>
<p className="text-sm font-medium mt-0.5">{transaction.totalAmount.toLocaleString()}</p>
<p className="text-sm font-medium mt-0.5">{formatNumber(transaction.totalAmount)}</p>
</div>
</div>
</div>
@@ -269,7 +270,7 @@ export function JournalEntryModal({ open, onOpenChange, transaction, onSuccess }
{/* 분개 합계 */}
<div className="bg-muted/50 rounded-lg p-3 flex items-center justify-between">
<span className="text-sm font-medium"> </span>
<span className="text-lg font-bold">{journalTotal.toLocaleString()}</span>
<span className="text-lg font-bold">{formatNumber(journalTotal)}</span>
</div>
</div>

View File

@@ -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
{/* 합계 금액 */}
<div className="bg-muted/50 rounded-lg p-3 flex items-center justify-between">
<span className="text-sm font-medium"> ( + )</span>
<span className="text-lg font-bold">{totalAmount.toLocaleString()}</span>
<span className="text-lg font-bold">{formatNumber(totalAmount)}</span>
</div>
</div>

View File

@@ -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 {

View File

@@ -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() {
/>
</TableCell>
{/* 합계금액 */}
<TableCell className="text-sm text-right font-medium">{item.totalAmount.toLocaleString()}</TableCell>
<TableCell className="text-sm text-right font-medium">{formatNumber(item.totalAmount)}</TableCell>
{/* 공급가액 (인라인 숫자 Input) */}
<TableCell onClick={(e) => e.stopPropagation()}>
<Input
@@ -609,9 +610,9 @@ export function CardTransactionInquiry() {
{ label: '카드명', value: item.cardName },
{ label: '가맹점', value: item.merchantName },
{ label: '내역', value: item.description || '-' },
{ label: '합계금액', value: `${item.totalAmount.toLocaleString()}` },
{ label: '공급가액', value: `${item.supplyAmount.toLocaleString()}` },
{ label: '세액', value: `${item.taxAmount.toLocaleString()}` },
{ label: '합계금액', value: `${formatNumber(item.totalAmount)}` },
{ label: '공급가액', value: `${formatNumber(item.supplyAmount)}` },
{ label: '세액', value: `${formatNumber(item.taxAmount)}` },
]}
/>
);
@@ -662,7 +663,7 @@ export function CardTransactionInquiry() {
<TableCell className="text-sm">{item.cardName}</TableCell>
<TableCell className="text-sm">{item.businessNumber}</TableCell>
<TableCell className="text-sm">{item.merchantName}</TableCell>
<TableCell className="text-sm text-right font-medium">{item.totalAmount.toLocaleString()}</TableCell>
<TableCell className="text-sm text-right font-medium">{formatNumber(item.totalAmount)}</TableCell>
<TableCell className="text-sm text-muted-foreground">{item.hiddenAt || '-'}</TableCell>
<TableCell className="text-center">
<Button

View File

@@ -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 DepositDetail({ depositId, mode }: DepositDetailProps) {
const [note, setNote] = useState('');
const [vendorId, setVendorId] = useState('');
const [depositType, setDepositType] = useState<DepositType>('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 DepositDetail({ depositId, mode }: DepositDetailProps) {
router.push(`/ko/accounting/deposits/${depositId}?mode=edit`);
}, [router, depositId]);
// ===== 삭제 핸들러 =====
const handleDelete = useCallback(async () => {
setIsLoading(true);
const result = await deleteDeposit(depositId);
if (result.success) {
toast.success('입금 내역이 삭제되었습니다.');
setShowDeleteDialog(false);
router.push('/ko/accounting/deposits');
} else {
toast.error(result.error || '삭제에 실패했습니다.');
}
setIsLoading(false);
}, [depositId, router]);
// ===== 삭제 다이얼로그 =====
const deleteDialog = useDeleteDialog({
onDelete: async (id) => deleteDeposit(id),
onSuccess: () => router.push('/ko/accounting/deposits'),
entityName: '입금',
});
return (
<PageLayout>
@@ -180,8 +174,8 @@ export function DepositDetail({ depositId, mode }: DepositDetailProps) {
<Button
variant="outline"
className="text-red-500 border-red-200 hover:bg-red-50"
onClick={() => setShowDeleteDialog(true)}
disabled={isLoading}
onClick={() => deleteDialog.single.open(depositId)}
disabled={deleteDialog.isPending}
>
</Button>
@@ -252,7 +246,7 @@ export function DepositDetail({ depositId, mode }: DepositDetailProps) {
<Label htmlFor="depositAmount"></Label>
<Input
id="depositAmount"
value={depositAmount.toLocaleString()}
value={formatNumber(depositAmount)}
readOnly
disabled
className="bg-gray-50"
@@ -314,12 +308,12 @@ export function DepositDetail({ depositId, mode }: DepositDetailProps) {
{/* ===== 삭제 확인 다이얼로그 ===== */}
<DeleteConfirmDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
onConfirm={handleDelete}
open={deleteDialog.single.isOpen}
onOpenChange={deleteDialog.single.onOpenChange}
onConfirm={deleteDialog.single.confirm}
title="입금 삭제"
description="이 입금 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다."
loading={isLoading}
loading={deleteDialog.isPending}
/>
</PageLayout>
);

View File

@@ -66,7 +66,7 @@ export default function DepositDetailClientV2({
loadDeposit();
}, [depositId, initialMode]);
// ===== 저장/등록 핸들러 =====
// ===== 저장/등록 핸들러 (결과만 반환, 토스트/리다이렉트는 IntegratedDetailTemplate에 위임) =====
const handleSubmit = useCallback(
async (formData: Record<string, unknown>): Promise<{ success: boolean; error?: string }> => {
const submitData = depositDetailConfig.transformSubmitData?.(formData) || formData;
@@ -81,32 +81,22 @@ export default function DepositDetailClientV2({
? await createDeposit(submitData as Partial<DepositRecord>)
: await updateDeposit(depositId!, submitData as Partial<DepositRecord>);
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(

View File

@@ -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
<TableCell className="font-bold"></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell className="text-right font-bold">{tableTotals.totalAmount.toLocaleString()}</TableCell>
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalAmount)}</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
@@ -448,7 +456,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
<TableCell>{item.depositDate}</TableCell>
<TableCell>{item.accountName}</TableCell>
<TableCell>{item.depositorName}</TableCell>
<TableCell className="text-right font-medium">{(item.depositAmount ?? 0).toLocaleString()}</TableCell>
<TableCell className="text-right font-medium">{formatNumber(item.depositAmount ?? 0)}</TableCell>
<TableCell className={isVendorUnset ? 'text-red-500 font-medium' : ''}>
{item.vendorName || '미설정'}
</TableCell>
@@ -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 || '-' },
]}
/>

View File

@@ -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}
</TableCell>
<TableCell className="text-right font-bold text-blue-700">
{item.subtotalAmount?.toLocaleString()}
{formatNumber(item.subtotalAmount)}
</TableCell>
<TableCell colSpan={3}></TableCell>
</TableRow>
@@ -626,7 +627,7 @@ export function ExpectedExpenseManagement({
</TableCell>
<TableCell className="text-right font-bold text-red-600">
{item.subtotalAmount?.toLocaleString()}
{formatNumber(item.subtotalAmount)}
</TableCell>
<TableCell colSpan={3}></TableCell>
</TableRow>
@@ -642,7 +643,7 @@ export function ExpectedExpenseManagement({
</TableCell>
<TableCell className="text-right font-bold">
{item.subtotalAmount?.toLocaleString()}
{formatNumber(item.subtotalAmount)}
</TableCell>
<TableCell colSpan={3}></TableCell>
</TableRow>
@@ -658,7 +659,7 @@ export function ExpectedExpenseManagement({
</TableCell>
<TableCell className="text-right font-bold text-orange-600">
{item.subtotalAmount?.toLocaleString()}
{formatNumber(item.subtotalAmount)}
</TableCell>
<TableCell colSpan={3}></TableCell>
</TableRow>
@@ -684,7 +685,7 @@ export function ExpectedExpenseManagement({
<TableCell>{item.expectedPaymentDate}</TableCell>
<TableCell>{item.accountSubject}</TableCell>
<TableCell className="text-right font-medium text-red-600">
{item.amount.toLocaleString()}
{formatNumber(item.amount)}
</TableCell>
<TableCell>{item.vendorName}</TableCell>
<TableCell>{item.bankAccount}</TableCell>
@@ -717,7 +718,7 @@ export function ExpectedExpenseManagement({
return (
<div className="bg-blue-50 p-3 rounded-lg flex justify-between">
<span className="font-semibold text-blue-700">{item.monthLabel} </span>
<span className="font-bold text-blue-700">{item.subtotalAmount?.toLocaleString()}</span>
<span className="font-bold text-blue-700">{formatNumber(item.subtotalAmount)}</span>
</div>
);
}
@@ -725,7 +726,7 @@ export function ExpectedExpenseManagement({
return (
<div className="bg-gray-100 p-3 rounded-lg flex justify-between">
<span className="font-bold"> </span>
<span className="font-bold text-red-600">{item.subtotalAmount?.toLocaleString()}</span>
<span className="font-bold text-red-600">{formatNumber(item.subtotalAmount)}</span>
</div>
);
}
@@ -733,7 +734,7 @@ export function ExpectedExpenseManagement({
return (
<div className="bg-gray-100 p-3 rounded-lg flex justify-between">
<span className="font-bold"> </span>
<span className="font-bold">{item.subtotalAmount?.toLocaleString()}</span>
<span className="font-bold">{formatNumber(item.subtotalAmount)}</span>
</div>
);
}
@@ -741,7 +742,7 @@ export function ExpectedExpenseManagement({
return (
<div className="bg-orange-50 p-3 rounded-lg flex justify-between">
<span className="font-bold"> </span>
<span className="font-bold text-orange-600">{item.subtotalAmount?.toLocaleString()}</span>
<span className="font-bold text-orange-600">{formatNumber(item.subtotalAmount)}</span>
</div>
);
}
@@ -771,7 +772,7 @@ export function ExpectedExpenseManagement({
infoGrid={
<div className="grid grid-cols-2 gap-3">
<InfoField label="예상 지급일" value={item.expectedPaymentDate} />
<InfoField label="지출금액" value={`${item.amount.toLocaleString()}`} />
<InfoField label="지출금액" value={`${formatNumber(item.amount)}`} />
<InfoField label="거래처" value={item.vendorName} />
<InfoField label="계좌" value={item.bankAccount} />
</div>
@@ -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({
<span className="text-sm font-medium">{item.vendorName}</span>
<span className="text-xs text-gray-500">{item.accountSubject} {item.expectedPaymentDate}</span>
</div>
<span className="font-semibold text-red-600">{item.amount.toLocaleString()}</span>
<span className="font-semibold text-red-600">{formatNumber(item.amount)}</span>
</div>
))}
</div>
@@ -1050,7 +1051,7 @@ export function ExpectedExpenseManagement({
{/* 합계 */}
<div className="flex items-center justify-between p-3 bg-gray-100 rounded-lg">
<span className="text-sm font-medium text-gray-700"></span>
<span className="font-bold text-lg">{selectedItemsSummary.totalAmount.toLocaleString()}</span>
<span className="font-bold text-lg">{formatNumber(selectedItemsSummary.totalAmount)}</span>
</div>
{/* 예상 지급일 선택 */}
@@ -1272,13 +1273,13 @@ export function ExpectedExpenseManagement({
<span className="text-sm font-medium">{item.vendorName}</span>
<span className="text-xs text-gray-500">{item.accountSubject} {item.expectedPaymentDate}</span>
</div>
<span className="font-semibold text-red-600">{item.amount.toLocaleString()}</span>
<span className="font-semibold text-red-600">{formatNumber(item.amount)}</span>
</div>
))}
</div>
<div className="flex items-center justify-between p-3 bg-gray-100 rounded-lg">
<span className="text-sm font-medium text-gray-700"></span>
<span className="font-bold text-lg text-red-600">{selectedItemsSummary.totalAmount.toLocaleString()}</span>
<span className="font-bold text-lg text-red-600">{formatNumber(selectedItemsSummary.totalAmount)}</span>
</div>
<AlertDialogFooter className="mt-4">
<AlertDialogCancel></AlertDialogCancel>

View File

@@ -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({
</div>
<div>
<Label className="text-xs text-muted-foreground"></Label>
<div className="font-medium">{record.amount.toLocaleString()}</div>
<div className="font-medium">{formatNumber(record.amount)}</div>
</div>
<div>
<Label className="text-xs text-muted-foreground"></Label>
@@ -451,17 +452,17 @@ export function JournalEditModal({
</TableCell>
<TableCell className="text-right text-sm font-bold">
{totals.debitTotal.toLocaleString()}
{formatNumber(totals.debitTotal)}
</TableCell>
<TableCell className="text-right text-sm font-bold">
{totals.creditTotal.toLocaleString()}
{formatNumber(totals.creditTotal)}
</TableCell>
<TableCell colSpan={2} className="text-center text-sm">
{totals.isBalanced ? (
<span className="text-green-600 font-medium"> </span>
) : (
<span className="text-red-500 font-medium">
: {totals.difference.toLocaleString()}
: {formatNumber(totals.difference)}
</span>
)}
</TableCell>

View File

@@ -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({
</TableCell>
<TableCell className="text-right text-sm font-bold">
{totals.debitTotal.toLocaleString()}
{formatNumber(totals.debitTotal)}
</TableCell>
<TableCell className="text-right text-sm font-bold">
{totals.creditTotal.toLocaleString()}
{formatNumber(totals.creditTotal)}
</TableCell>
<TableCell colSpan={2} />
</TableRow>
@@ -375,8 +377,8 @@ export function ManualJournalEntryModal({
{/* 차대변 불일치 경고 */}
{totals.debitTotal !== totals.creditTotal && totals.debitTotal > 0 && (
<p className="text-xs text-red-500">
({totals.debitTotal.toLocaleString()}) (
{totals.creditTotal.toLocaleString()}) .
({formatNumber(totals.debitTotal)}) (
{formatNumber(totals.creditTotal)}) .
</p>
)}

View File

@@ -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() {
</span>
</TableCell>
<TableCell className="text-right text-sm text-blue-600 w-[100px]">
{item.depositAmount ? item.depositAmount.toLocaleString() : '-'}
{item.depositAmount ? formatNumber(item.depositAmount) : '-'}
</TableCell>
<TableCell className="text-right text-sm text-red-600 w-[100px]">
{item.withdrawalAmount ? item.withdrawalAmount.toLocaleString() : '-'}
{item.withdrawalAmount ? formatNumber(item.withdrawalAmount) : '-'}
</TableCell>
<TableCell className="text-right text-sm w-[100px]">{item.balance.toLocaleString()}</TableCell>
<TableCell className="text-right text-sm w-[100px]">{formatNumber(item.balance)}</TableCell>
<TableCell className="text-center text-sm w-[70px]">
<Badge variant="outline" className="text-xs">
{JOURNAL_DIVISION_LABELS[item.division] || item.division}
@@ -281,10 +282,10 @@ export function GeneralJournalEntry() {
</TableCell>
<TableCell className="text-sm w-[100px]">{item.journalDescription || '-'}</TableCell>
<TableCell className="text-right text-sm w-[90px]">
{item.debitAmount ? item.debitAmount.toLocaleString() : '-'}
{item.debitAmount ? formatNumber(item.debitAmount) : '-'}
</TableCell>
<TableCell className="text-right text-sm w-[90px]">
{item.creditAmount ? item.creditAmount.toLocaleString() : '-'}
{item.creditAmount ? formatNumber(item.creditAmount) : '-'}
</TableCell>
<TableCell className="text-center w-[70px]">
<Button
@@ -319,11 +320,11 @@ export function GeneralJournalEntry() {
onToggle={() => {}}
onClick={() => setJournalEditTarget(item)}
details={[
{ label: '입금', value: `${(item.depositAmount || 0).toLocaleString()}` },
{ label: '출금', value: `${(item.withdrawalAmount || 0).toLocaleString()}` },
{ label: '잔액', value: `${item.balance.toLocaleString()}` },
{ label: '차변', value: `${(item.debitAmount || 0).toLocaleString()}` },
{ label: '대변', value: `${(item.creditAmount || 0).toLocaleString()}` },
{ label: '입금', value: `${formatNumber(item.depositAmount || 0)}` },
{ label: '출금', value: `${formatNumber(item.withdrawalAmount || 0)}` },
{ label: '잔액', value: `${formatNumber(item.balance)}` },
{ label: '차변', value: `${formatNumber(item.debitAmount || 0)}` },
{ label: '대변', value: `${formatNumber(item.creditAmount || 0)}` },
]}
/>
),
@@ -334,19 +335,19 @@ export function GeneralJournalEntry() {
<TableRow className="bg-muted/50 font-medium">
<TableCell colSpan={2} className="text-right font-bold"></TableCell>
<TableCell className="text-right font-bold text-blue-600">
{tableTotals.depositTotal.toLocaleString()}
{formatNumber(tableTotals.depositTotal)}
</TableCell>
<TableCell className="text-right font-bold text-red-600">
{tableTotals.withdrawalTotal.toLocaleString()}
{formatNumber(tableTotals.withdrawalTotal)}
</TableCell>
<TableCell />
<TableCell />
<TableCell />
<TableCell className="text-right font-bold">
{tableTotals.debitTotal.toLocaleString()}
{formatNumber(tableTotals.debitTotal)}
</TableCell>
<TableCell className="text-right font-bold">
{tableTotals.creditTotal.toLocaleString()}
{formatNumber(tableTotals.creditTotal)}
</TableCell>
<TableCell />
</TableRow>

View File

@@ -2,6 +2,8 @@
* 일반전표입력 - 타입 및 상수 정의
*/
import { getLocalDateString } from '@/lib/utils/date';
// ===== 전표 구분 =====
export type JournalDivision = 'deposit' | 'withdrawal' | 'transfer' | 'manual';
@@ -228,7 +230,7 @@ export function transformAccountSubjectApi(apiData: AccountSubjectApiData): Acco
// ===== 기간 버튼 → 날짜 변환 =====
export function getPeriodDates(period: PeriodButtonValue): { start: string; end: string } {
const today = new Date();
const formatDate = (d: Date) => d.toISOString().slice(0, 10);
const formatDate = (d: Date) => getLocalDateString(d);
switch (period) {
case 'this_month': {

View File

@@ -43,6 +43,7 @@ import {
import { getClients } from '../VendorManagement/actions';
import { toast } from 'sonner';
import { formatNumber as formatAmount } from '@/lib/utils/amount';
import { formatDate } from '@/lib/utils/date';
interface PurchaseDetailProps {
purchaseId: string;
@@ -297,7 +298,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
<div className="space-y-2">
<Label></Label>
<Input
value={`${sourceDocument.expectedCost.toLocaleString()}`}
value={`${formatAmount(sourceDocument.expectedCost)}`}
readOnly
disabled
className="bg-gray-50"
@@ -576,7 +577,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
sourceDocument.type === 'proposal'
? {
documentNo: sourceDocument.documentNo,
createdAt: createdAt.split('T')[0],
createdAt: formatDate(createdAt),
vendor: vendorName,
vendorPaymentDate: purchaseDate,
title: sourceDocument.title,
@@ -591,7 +592,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
position: '팀장',
department: '경영지원팀',
status: 'approved',
approvedAt: createdAt.split('T')[0],
approvedAt: formatDate(createdAt),
},
{
id: 'approver-2',
@@ -599,7 +600,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
position: '부장',
department: '경영지원팀',
status: 'approved',
approvedAt: createdAt.split('T')[0],
approvedAt: formatDate(createdAt),
},
],
drafter: {
@@ -612,7 +613,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
} as ProposalDocumentData
: {
documentNo: sourceDocument.documentNo,
createdAt: createdAt.split('T')[0],
createdAt: formatDate(createdAt),
requestDate: purchaseDate,
paymentDate: purchaseDate,
items: items.map((item, idx) => ({
@@ -634,7 +635,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
position: '팀장',
department: '경영지원팀',
status: 'approved',
approvedAt: createdAt.split('T')[0],
approvedAt: formatDate(createdAt),
},
{
id: 'approver-2',
@@ -642,7 +643,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
position: '부장',
department: '경영지원팀',
status: 'approved',
approvedAt: createdAt.split('T')[0],
approvedAt: formatDate(createdAt),
},
],
drafter: {

View File

@@ -32,6 +32,7 @@ import {
TableRow,
} from '@/components/ui/table';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { formatNumber } from '@/lib/utils/amount';
import { FileText, Plus, X, ExternalLink } from 'lucide-react';
import type { PurchaseRecord, PurchaseItem, PurchaseType } from './types';
import { PURCHASE_TYPE_LABELS } from './types';
@@ -187,7 +188,7 @@ export function PurchaseDetailModal({
</Badge>
<span className="font-medium">{data.sourceDocument.documentNo}</span>
<span className="text-muted-foreground">
: {data.sourceDocument.expectedCost.toLocaleString()}
: {formatNumber(data.sourceDocument.expectedCost)}
</span>
<Button variant="ghost" size="sm" className="ml-auto">
<ExternalLink className="h-4 w-4 mr-1" />
@@ -322,10 +323,10 @@ export function PurchaseDetailModal({
/>
</TableCell>
<TableCell className="text-right font-medium">
{item.supplyPrice.toLocaleString()}
{formatNumber(item.supplyPrice)}
</TableCell>
<TableCell className="text-right">
{item.vat.toLocaleString()}
{formatNumber(item.vat)}
</TableCell>
<TableCell>
<Input
@@ -350,10 +351,10 @@ export function PurchaseDetailModal({
{/* 합계 행 */}
<TableRow className="bg-muted/50 font-medium">
<TableCell colSpan={4} className="text-right"></TableCell>
<TableCell className="text-right">{totalSupplyPrice.toLocaleString()}</TableCell>
<TableCell className="text-right">{totalVat.toLocaleString()}</TableCell>
<TableCell className="text-right">{formatNumber(totalSupplyPrice)}</TableCell>
<TableCell className="text-right">{formatNumber(totalVat)}</TableCell>
<TableCell colSpan={2} className="text-right font-bold">
: {totalAmount.toLocaleString()}
: {formatNumber(totalAmount)}
</TableCell>
</TableRow>
</TableBody>

View File

@@ -63,6 +63,7 @@ import {
ACCOUNT_SUBJECT_SELECTOR_OPTIONS,
} from './types';
import { getPurchases, togglePurchaseTaxInvoice, deletePurchase } from './actions';
import { formatNumber } from '@/lib/utils/amount';
// ===== 테이블 컬럼 정의 =====
const tableColumns = [
@@ -392,8 +393,8 @@ export function PurchaseManagement() {
// Stats 카드
computeStats: (): StatCard[] => [
{ label: '총 매입', value: `${stats.totalPurchaseAmount.toLocaleString()}`, icon: Receipt, iconColor: 'text-blue-500' },
{ label: '당월 매입', value: `${stats.monthlyAmount.toLocaleString()}`, icon: Receipt, iconColor: 'text-green-500' },
{ label: '총 매입', value: `${formatNumber(stats.totalPurchaseAmount)}`, icon: Receipt, iconColor: 'text-blue-500' },
{ label: '당월 매입', value: `${formatNumber(stats.monthlyAmount)}`, icon: Receipt, iconColor: 'text-green-500' },
{ label: '매입유형 미설정', value: `${stats.unsetTypeCount}`, icon: Receipt, iconColor: 'text-orange-500' },
{ label: '세금계산서 수취 미확인', value: `${stats.taxInvoicePendingCount}`, icon: Receipt, iconColor: 'text-red-500' },
],
@@ -407,9 +408,9 @@ export function PurchaseManagement() {
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell className="text-right font-bold">{tableTotals.totalSupplyAmount.toLocaleString()}</TableCell>
<TableCell className="text-right font-bold">{tableTotals.totalVat.toLocaleString()}</TableCell>
<TableCell className="text-right font-bold">{tableTotals.totalAmount.toLocaleString()}</TableCell>
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalSupplyAmount)}</TableCell>
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalVat)}</TableCell>
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalAmount)}</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
@@ -451,9 +452,9 @@ export function PurchaseManagement() {
<span className="text-gray-400 text-xs">-</span>
)}
</TableCell>
<TableCell className="text-right">{item.supplyAmount.toLocaleString()}</TableCell>
<TableCell className="text-right">{item.vat.toLocaleString()}</TableCell>
<TableCell className="text-right font-medium">{item.totalAmount.toLocaleString()}</TableCell>
<TableCell className="text-right">{formatNumber(item.supplyAmount)}</TableCell>
<TableCell className="text-right">{formatNumber(item.vat)}</TableCell>
<TableCell className="text-right font-medium">{formatNumber(item.totalAmount)}</TableCell>
<TableCell className="text-center">
<Badge
variant="outline"
@@ -495,8 +496,8 @@ export function PurchaseManagement() {
details={[
{ label: '매입일', value: item.purchaseDate },
{ label: '연결문서', value: item.sourceDocument ? (item.sourceDocument.type === 'proposal' ? '품의서' : '지출결의서') : '-' },
{ label: '공급가액', value: `${item.supplyAmount.toLocaleString()}` },
{ label: '합계금액', value: `${item.totalAmount.toLocaleString()}` },
{ label: '공급가액', value: `${formatNumber(item.supplyAmount)}` },
{ label: '합계금액', value: `${formatNumber(item.totalAmount)}` },
]}
/>
),

View File

@@ -2,6 +2,7 @@
import { useState, useMemo, useCallback, useEffect, useRef, useTransition } from 'react';
import { Download, FileText, Save, Loader2, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react';
import { formatNumber } from '@/lib/utils/amount';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/input';
@@ -307,7 +308,7 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma
// ===== 금액 포맷 =====
const formatAmount = (amount: number) => {
if (amount === 0) return '';
return amount.toLocaleString();
return formatNumber(amount);
};
// ===== 합계 계산 (동적 월) =====

View File

@@ -71,6 +71,7 @@ import {
computeMonthlyTotal,
type SortDirection,
} from '../shared';
import { formatNumber } from '@/lib/utils/amount';
// ===== 테이블 컬럼 정의 =====
const tableColumns = [
@@ -108,13 +109,13 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
const [isLoading, setIsLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
// 통합 필터 상태 (filterConfig 사용)
const [filterValues, setFilterValues] = useState<Record<string, string | string[]>>({
// 필터 초기값 (filterConfig 기반 - ULP가 내부 state로 관리)
const initialFilterValues: Record<string, string | string[]> = {
vendor: 'all',
salesType: 'all',
issuance: 'all',
sort: 'latest',
});
};
// 계정과목명 저장 다이얼로그
const [selectedAccountSubject, setSelectedAccountSubject] = useState<string>('unset');
@@ -167,19 +168,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
},
], [vendorOptions]);
// ===== 필터 핸들러 =====
const handleFilterChange = useCallback((key: string, value: string | string[]) => {
setFilterValues(prev => ({ ...prev, [key]: value }));
}, []);
const handleFilterReset = useCallback(() => {
setFilterValues({
vendor: 'all',
salesType: 'all',
issuance: 'all',
sort: 'latest',
});
}, []);
// 참고: 필터 변경/리셋은 UniversalListPage가 filterConfig 기반으로 내부 처리
// ===== API 데이터 로드 =====
const loadData = useCallback(async (page: number = 1) => {
@@ -328,30 +317,21 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
);
},
// 커스텀 필터 함수 (filterConfig 사용)
// 커스텀 필터 함수 (filterConfig 기반 - ULP의 filters state에서 값 전달)
// 검색은 searchFilter에서 처리하므로 여기서는 필터만 처리
customFilterFn: (items, fv) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// 검색어 필터
if (searchQuery) {
const search = searchQuery.toLowerCase();
const matchesSearch =
item.salesNo.toLowerCase().includes(search) ||
item.vendorName.toLowerCase().includes(search) ||
item.note.toLowerCase().includes(search);
if (!matchesSearch) return false;
}
const vendorVal = fv.vendor as string;
const salesTypeVal = fv.salesType as string;
const issuanceVal = fv.issuance as string;
// 거래처 필터
if (vendorVal !== 'all' && item.vendorName !== vendorVal) {
if (vendorVal && vendorVal !== 'all' && item.vendorName !== vendorVal) {
return false;
}
// 매출유형 필터
if (salesTypeVal !== 'all' && item.salesType !== salesTypeVal) {
if (salesTypeVal && salesTypeVal !== 'all' && item.salesType !== salesTypeVal) {
return false;
}
// 발행여부 필터
@@ -412,15 +392,15 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
// Stats 카드
computeStats: (): StatCard[] => [
{ label: '총 매출', value: `${stats.totalSalesAmount.toLocaleString()}`, icon: Receipt, iconColor: 'text-blue-500' },
{ label: '당월 매출', value: `${stats.monthlyAmount.toLocaleString()}`, icon: Receipt, iconColor: 'text-green-500' },
{ label: '총 매출', value: `${formatNumber(stats.totalSalesAmount)}`, icon: Receipt, iconColor: 'text-blue-500' },
{ label: '당월 매출', value: `${formatNumber(stats.monthlyAmount)}`, icon: Receipt, iconColor: 'text-green-500' },
{ label: '세금계산서 발행대기', value: `${stats.taxInvoicePendingCount}`, icon: Receipt, iconColor: 'text-orange-500' },
{ label: '거래명세서 발행대기', value: `${stats.transactionStatementPendingCount}`, icon: Receipt, iconColor: 'text-orange-500' },
],
// 필터 설정 (filterConfig 사용 - PC 인라인, 모바일 바텀시트)
filterConfig,
initialFilters: filterValues,
initialFilters: initialFilterValues,
filterTitle: '매출 필터',
// 테이블 하단 합계 행
@@ -431,9 +411,9 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
<TableCell className="font-bold"></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell className="text-right font-bold">{tableTotals.totalSupplyAmount.toLocaleString()}</TableCell>
<TableCell className="text-right font-bold">{tableTotals.totalVat.toLocaleString()}</TableCell>
<TableCell className="text-right font-bold">{tableTotals.totalAmount.toLocaleString()}</TableCell>
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalSupplyAmount)}</TableCell>
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalVat)}</TableCell>
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalAmount)}</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
@@ -465,9 +445,9 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
<TableCell className="text-sm font-medium">{item.salesNo}</TableCell>
<TableCell>{item.salesDate}</TableCell>
<TableCell>{item.vendorName}</TableCell>
<TableCell className="text-right">{item.totalSupplyAmount.toLocaleString()}</TableCell>
<TableCell className="text-right">{item.totalVat.toLocaleString()}</TableCell>
<TableCell className="text-right font-medium">{item.totalAmount.toLocaleString()}</TableCell>
<TableCell className="text-right">{formatNumber(item.totalSupplyAmount)}</TableCell>
<TableCell className="text-right">{formatNumber(item.totalVat)}</TableCell>
<TableCell className="text-right font-medium">{formatNumber(item.totalAmount)}</TableCell>
<TableCell className="text-center">
<Badge variant="outline">{SALES_TYPE_LABELS[item.salesType]}</Badge>
</TableCell>
@@ -512,8 +492,8 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
onClick={() => handleRowClick(item)}
details={[
{ label: '매출일', value: item.salesDate },
{ label: '매출금액', value: `${item.totalAmount.toLocaleString()}` },
{ label: '미수금액', value: item.outstandingAmount > 0 ? `${item.outstandingAmount.toLocaleString()}` : '-' },
{ label: '매출금액', value: `${formatNumber(item.totalAmount)}` },
{ label: '미수금액', value: item.outstandingAmount > 0 ? `${formatNumber(item.outstandingAmount)}` : '-' },
]}
/>
),
@@ -525,10 +505,8 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
endDate,
stats,
filterConfig,
filterValues,
selectedAccountSubject,
tableTotals,
searchQuery,
handleRowClick,
handleCreate,
handleTaxInvoiceToggle,

View File

@@ -1,6 +1,7 @@
'use client';
import { useCallback } from 'react';
import { formatNumber } from '@/lib/utils/amount';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
@@ -65,9 +66,7 @@ export function TaxInvoiceItemTable({ items, onItemsChange }: TaxInvoiceItemTabl
[items, onItemsChange]
);
const formatNumber = (num: number) => {
return num.toLocaleString('ko-KR');
};
// formatNumber imported from @/lib/utils/amount
// 합계 계산
const totals = items.reduce(

View File

@@ -12,6 +12,7 @@
import { useState, useCallback } from 'react';
import { toast } from 'sonner';
import { formatNumber } from '@/lib/utils/amount';
import { Search } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -150,7 +151,7 @@ export function CardHistoryModal({
</TableCell>
<TableCell className="text-sm">{item.merchantName}</TableCell>
<TableCell className="text-right text-sm font-medium">
{item.amount.toLocaleString()}
{formatNumber(item.amount)}
</TableCell>
<TableCell className="text-center text-sm text-muted-foreground">
{item.approvalNumber}

View File

@@ -11,6 +11,7 @@
import { useState, useCallback, useEffect, useMemo } from 'react';
import { toast } from 'sonner';
import { formatNumber } from '@/lib/utils/amount';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -238,11 +239,11 @@ export function JournalEntryModal({
</div>
<div>
<Label className="text-xs text-muted-foreground"></Label>
<div className="font-medium">{invoice.supplyAmount.toLocaleString()}</div>
<div className="font-medium">{formatNumber(invoice.supplyAmount)}</div>
</div>
<div>
<Label className="text-xs text-muted-foreground"></Label>
<div className="font-medium">{invoice.taxAmount.toLocaleString()}</div>
<div className="font-medium">{formatNumber(invoice.taxAmount)}</div>
</div>
</div>
</div>
@@ -348,10 +349,10 @@ export function JournalEntryModal({
</TableCell>
<TableCell className="text-right text-sm font-bold">
{totals.debitTotal.toLocaleString()}
{formatNumber(totals.debitTotal)}
</TableCell>
<TableCell className="text-right text-sm font-bold">
{totals.creditTotal.toLocaleString()}
{formatNumber(totals.creditTotal)}
</TableCell>
</TableRow>
</TableFooter>
@@ -363,8 +364,8 @@ export function JournalEntryModal({
{/* 차대변 불일치 경고 */}
{totals.debitTotal !== totals.creditTotal && (
<p className="text-xs text-red-500">
({totals.debitTotal.toLocaleString()}) (
{totals.creditTotal.toLocaleString()}) .
({formatNumber(totals.debitTotal)}) (
{formatNumber(totals.creditTotal)}) .
</p>
)}

View File

@@ -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개 */}
<StatCards
stats={[
{ label: '매출 공급가액', value: `${summary.salesSupplyAmount.toLocaleString()}` },
{ label: '매출 세액', value: `${summary.salesTaxAmount.toLocaleString()}` },
{ label: '매입 과세 공급가액', value: `${summary.purchaseSupplyAmount.toLocaleString()}` },
{ label: '매출 공급가액', value: `${formatNumber(summary.salesSupplyAmount)}` },
{ label: '매출 세액', value: `${formatNumber(summary.salesTaxAmount)}` },
{ label: '매입 과세 공급가액', value: `${formatNumber(summary.purchaseSupplyAmount)}` },
{ label: '매입 면세 공급가액', value: '0원' },
{ label: '매입 세액', value: `${summary.purchaseTaxAmount.toLocaleString()}` },
{ label: '매입 세액', value: `${formatNumber(summary.purchaseTaxAmount)}` },
]}
/>
@@ -397,9 +398,9 @@ export function TaxInvoiceManagement() {
</Badge>
</TableCell>
<TableCell className="text-sm">{item.itemName || '-'}</TableCell>
<TableCell className="text-right text-sm">{item.supplyAmount.toLocaleString()}</TableCell>
<TableCell className="text-right text-sm">{item.taxAmount.toLocaleString()}</TableCell>
<TableCell className="text-right text-sm font-medium">{item.totalAmount.toLocaleString()}</TableCell>
<TableCell className="text-right text-sm">{formatNumber(item.supplyAmount)}</TableCell>
<TableCell className="text-right text-sm">{formatNumber(item.taxAmount)}</TableCell>
<TableCell className="text-right text-sm font-medium">{formatNumber(item.totalAmount)}</TableCell>
<TableCell className="text-center text-sm">
{RECEIPT_TYPE_LABELS[item.receiptType]}
</TableCell>
@@ -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() {
<div className="flex items-center justify-center gap-4 text-center">
<div>
<div className="text-xs text-muted-foreground mb-1"> ( + )</div>
<div className="text-xl font-bold">{summary.salesTotalAmount.toLocaleString()}</div>
<div className="text-xl font-bold">{formatNumber(summary.salesTotalAmount)}</div>
</div>
<div className="text-2xl font-bold text-muted-foreground"></div>
<div>
<div className="text-xs text-muted-foreground mb-1"> ( + )</div>
<div className="text-xl font-bold">{summary.purchaseTotalAmount.toLocaleString()}</div>
<div className="text-xl font-bold">{formatNumber(summary.purchaseTotalAmount)}</div>
</div>
<div className="text-2xl font-bold text-muted-foreground">=</div>
<div>
<div className="text-xs text-muted-foreground mb-1"> </div>
<div className={`text-xl font-bold ${periodDifference >= 0 ? 'text-blue-700' : 'text-red-700'}`}>
{periodDifference.toLocaleString()}
{formatNumber(periodDifference)}
</div>
</div>
</div>

View File

@@ -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({
<div className="grid grid-cols-4 gap-4 text-center">
<div>
<div className="text-sm text-gray-500"></div>
<div className="font-medium">{summary.carryoverBalance.toLocaleString()}</div>
<div className="font-medium">{formatNumber(summary.carryoverBalance)}</div>
</div>
<div>
<div className="text-sm text-gray-500"></div>
<div className="font-medium text-green-600">{summary.totalSales.toLocaleString()}</div>
<div className="font-medium text-green-600">{formatNumber(summary.totalSales)}</div>
</div>
<div>
<div className="text-sm text-gray-500"></div>
<div className="font-medium text-blue-600">{summary.totalCollection.toLocaleString()}</div>
<div className="font-medium text-blue-600">{formatNumber(summary.totalCollection)}</div>
</div>
<div>
<div className="text-sm text-gray-500"></div>
<div className={`font-medium ${summary.balance < 0 ? 'text-red-600' : ''}`}>
{summary.balance.toLocaleString()}
{formatNumber(summary.balance)}
</div>
</div>
</div>

View File

@@ -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',
},

View File

@@ -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()),

View File

@@ -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
<Label className="text-sm font-medium text-gray-700"></Label>
<Input
type="text"
value={formData.outstandingAmount?.toLocaleString() + '원'}
value={formatNumber(formData.outstandingAmount) + '원'}
disabled
className="bg-gray-50"
/>
@@ -475,7 +477,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
<Label className="text-sm font-medium text-gray-700"> </Label>
<Input
type="text"
value={formData.badDebtAmount ? formData.badDebtAmount.toLocaleString() + '원' : '-'}
value={formData.badDebtAmount ? formatNumber(formData.badDebtAmount) + '원' : '-'}
disabled
className="bg-gray-50"
/>

View File

@@ -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
{/* 미수금 */}
<TableCell className="text-right">
{item.outstandingAmount > 0 ? (
<span className="text-red-600 font-medium">{item.outstandingAmount.toLocaleString()}</span>
<span className="text-red-600 font-medium">{formatNumber(item.outstandingAmount)}</span>
) : (
<span className="text-gray-500">-</span>
)}
@@ -335,7 +336,7 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana
<InfoField label="거래등급" value={TRANSACTION_GRADE_LABELS[item.transactionGrade]} />
<InfoField
label="미수금"
value={item.outstandingAmount > 0 ? `${item.outstandingAmount.toLocaleString()}` : '-'}
value={item.outstandingAmount > 0 ? `${formatNumber(item.outstandingAmount)}` : '-'}
className={item.outstandingAmount > 0 ? 'text-red-600' : ''}
/>
<InfoField label="결제일" value={`매입 ${item.purchasePaymentDay}일 / 매출 ${item.salesPaymentDay}`} />

View File

@@ -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
{/* 미수금 */}
<TableCell className="text-right">
{vendor.outstandingAmount > 0 ? (
<span className="text-red-600 font-medium">{vendor.outstandingAmount.toLocaleString()}</span>
<span className="text-red-600 font-medium">{formatNumber(vendor.outstandingAmount)}</span>
) : (
<span className="text-gray-500">-</span>
)}
@@ -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}` },
]}

View File

@@ -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<WithdrawalType>('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 (
<PageLayout>
@@ -180,8 +174,8 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps)
<Button
variant="outline"
className="text-red-500 border-red-200 hover:bg-red-50"
onClick={() => setShowDeleteDialog(true)}
disabled={isLoading}
onClick={() => deleteDialog.single.open(withdrawalId)}
disabled={deleteDialog.isPending}
>
</Button>
@@ -252,7 +246,7 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps)
<Label htmlFor="withdrawalAmount"></Label>
<Input
id="withdrawalAmount"
value={withdrawalAmount.toLocaleString()}
value={formatNumber(withdrawalAmount)}
readOnly
disabled
className="bg-gray-50"
@@ -314,12 +308,12 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps)
{/* ===== 삭제 확인 다이얼로그 ===== */}
<DeleteConfirmDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
onConfirm={handleDelete}
open={deleteDialog.single.isOpen}
onOpenChange={deleteDialog.single.onOpenChange}
onConfirm={deleteDialog.single.confirm}
title="출금 삭제"
description="이 출금 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다."
loading={isLoading}
loading={deleteDialog.isPending}
/>
</PageLayout>
);

View File

@@ -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
<TableCell className="font-bold"></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell className="text-right font-bold">{tableTotals.totalAmount.toLocaleString()}</TableCell>
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalAmount)}</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
@@ -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
{/* 수취인명 */}
<TableCell>{item.recipientName}</TableCell>
{/* 출금금액 */}
<TableCell className="text-right font-medium">{(item.withdrawalAmount ?? 0).toLocaleString()}</TableCell>
<TableCell className="text-right font-medium">{formatNumber(item.withdrawalAmount ?? 0)}</TableCell>
{/* 거래처 */}
<TableCell className={isVendorUnset ? 'text-red-500 font-medium' : ''}>
{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 || '-' },
]}

View File

@@ -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',

View File

@@ -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 || '',

View File

@@ -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<Comment[]>(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 }:
<div className="flex items-center gap-1 md:gap-2">
<Button
variant="outline"
onClick={() => setShowDeleteDialog(true)}
onClick={() => deleteDialog.single.open(post.id)}
size="sm"
className="text-destructive hover:bg-destructive hover:text-destructive-foreground md:size-default"
>
@@ -228,12 +212,12 @@ export function BoardDetail({ post, comments: initialComments, currentUserId }:
{/* 삭제 확인 다이얼로그 */}
<DeleteConfirmDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
onConfirm={handleConfirmDelete}
open={deleteDialog.single.isOpen}
onOpenChange={deleteDialog.single.onOpenChange}
onConfirm={deleteDialog.single.confirm}
title="게시글 삭제"
description="정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
loading={isDeleting}
loading={deleteDialog.isPending}
/>
</PageLayout>
);

View File

@@ -224,10 +224,10 @@ export function BoardList() {
columns: [
{ key: 'no', label: 'No.', className: 'w-[60px] text-center' },
{ key: 'title', label: '제목', className: 'min-w-[300px]' },
{ key: 'author', label: '작성자', className: 'w-[120px]' },
{ key: 'createdAt', label: '등록일', className: 'w-[120px]' },
{ key: 'viewCount', label: '조회수', className: 'w-[80px] text-center' },
{ key: 'title', label: '제목', className: 'min-w-[300px]', sortable: true },
{ key: 'author', label: '작성자', className: 'w-[120px]', sortable: true },
{ key: 'createdAt', label: '등록일', className: 'w-[120px]', sortable: true },
{ key: 'viewCount', label: '조회수', className: 'w-[80px] text-center', sortable: true },
{ key: 'actions', label: '작업', className: 'w-[100px] text-center' },
],

View File

@@ -19,6 +19,7 @@ import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { ErrorCard } from '@/components/ui/error-card';
import { Button } from '@/components/ui/button';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import { useDeleteDialog } from '@/hooks/useDeleteDialog';
import { toast } from 'sonner';
import { usePermission } from '@/hooks/usePermission';
@@ -58,8 +59,6 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV
const [isLoading, setIsLoading] = useState(!isNewMode);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
// 데이터 로드
useEffect(() => {
@@ -161,36 +160,17 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV
};
// 삭제 핸들러
const handleDelete = () => {
setDeleteDialogOpen(true);
};
const confirmDelete = async () => {
if (!boardData) return;
setIsDeleting(true);
try {
const result = await deleteBoard(boardData.id);
const deleteDialog = useDeleteDialog({
onDelete: async (id) => {
const result = await deleteBoard(id);
if (result.success) {
await forceRefreshMenus();
toast.success('게시판이 삭제되었습니다.');
router.push(BASE_PATH);
} else {
setError(result.error || '삭제에 실패했습니다.');
toast.error(result.error || '삭제에 실패했습니다.');
setDeleteDialogOpen(false);
}
} catch (err) {
console.error('게시판 삭제 실패:', err);
setError('게시판 삭제 중 오류가 발생했습니다.');
toast.error('게시판 삭제 중 오류가 발생했습니다.');
setDeleteDialogOpen(false);
} finally {
setIsDeleting(false);
}
};
return result;
},
onSuccess: () => router.push(BASE_PATH),
entityName: '게시판',
});
// 수정 모드 전환
const handleEdit = () => {
@@ -271,13 +251,13 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV
<BoardDetail
board={boardData}
onEdit={canUpdate ? handleEdit : undefined}
onDelete={canDelete ? handleDelete : undefined}
onDelete={canDelete ? () => deleteDialog.single.open(boardData!.id) : undefined}
/>
<DeleteConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
onConfirm={confirmDelete}
open={deleteDialog.single.isOpen}
onOpenChange={deleteDialog.single.onOpenChange}
onConfirm={deleteDialog.single.confirm}
title="게시판 삭제"
description={
<>
@@ -288,7 +268,7 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV
</span>
</>
}
loading={isDeleting}
loading={deleteDialog.isPending}
/>
</>
);

View File

@@ -17,6 +17,7 @@ import type {
LoanDashboardApiResponse,
TaxSimulationApiResponse,
} from '@/lib/api/dashboard/types';
import { formatNumber } from '@/lib/utils/amount';
// ============================================
// 유틸리티 함수
@@ -34,7 +35,7 @@ function formatKoreanCurrency(value: number): string {
const thousands = value / 10000;
return `${thousands.toFixed(0)}만원`;
}
return `${value.toLocaleString()}`;
return `${formatNumber(value)}`;
}
/**

View File

@@ -47,6 +47,7 @@ import {
FileCheck
} from "lucide-react";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, Area, AreaChart } from "recharts";
import { formatNumber } from '@/lib/utils/amount';
/**
* MainDashboard - 통합 대시보드
@@ -961,7 +962,7 @@ export function MainDashboard() {
</CardHeader>
<CardContent className="space-y-3">
<div className="text-2xl font-bold text-foreground">
{ceoData.dailySales.today.toLocaleString()}
{formatNumber(ceoData.dailySales.today)}
</div>
<div className="space-y-1">
<div className="flex items-center space-x-2 text-sm">
@@ -1428,7 +1429,7 @@ export function MainDashboard() {
<p className="text-sm text-muted-foreground">{receivable.days} </p>
</div>
<div className="text-right">
<p className="font-bold text-red-600 dark:text-red-400">{receivable.amount.toLocaleString()}</p>
<p className="font-bold text-red-600 dark:text-red-400">{formatNumber(receivable.amount)}</p>
<Badge className={`${receivable.days > 30 ? 'bg-red-500' : 'bg-yellow-500'} text-white text-xs`}>
{receivable.days > 30 ? '위험' : '주의'}
</Badge>
@@ -1693,11 +1694,11 @@ export function MainDashboard() {
<div className="flex justify-between items-center">
<div className="text-sm">
<span className="text-muted-foreground">: </span>
<span className="font-bold text-blue-600">{material.thisMonth.toLocaleString()}</span>
<span className="font-bold text-blue-600">{formatNumber(material.thisMonth)}</span>
</div>
<div className="text-sm">
<span className="text-muted-foreground">: </span>
<span className="font-bold">{material.lastMonth.toLocaleString()}</span>
<span className="font-bold">{formatNumber(material.lastMonth)}</span>
</div>
<div className={`text-sm font-bold ${
material.thisMonth > material.lastMonth ? 'text-red-600' : 'text-green-600'
@@ -1735,13 +1736,13 @@ export function MainDashboard() {
<div className="p-3 bg-muted/50 dark:bg-muted/20 rounded-lg">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground"> </span>
<span className="font-bold text-foreground">{ceoData.materialEfficiency.productionOutput.toLocaleString()}ea</span>
<span className="font-bold text-foreground">{formatNumber(ceoData.materialEfficiency.productionOutput)}ea</span>
</div>
</div>
<div className="p-3 bg-muted/50 dark:bg-muted/20 rounded-lg">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground"> </span>
<span className="font-bold text-foreground">{ceoData.materialEfficiency.materialConsumption.toLocaleString()}kg</span>
<span className="font-bold text-foreground">{formatNumber(ceoData.materialEfficiency.materialConsumption)}kg</span>
</div>
</div>
</div>
@@ -2626,7 +2627,7 @@ export function MainDashboard() {
</div>
<div>
<p className="font-medium text-foreground">{customer.name}</p>
<p className="text-sm text-muted-foreground">{customer.amount.toLocaleString()}</p>
<p className="text-sm text-muted-foreground">{formatNumber(customer.amount)}</p>
</div>
</div>
<div className="text-right">

View File

@@ -17,6 +17,7 @@ import type {
} from './types';
import { getEmptyPriceAdjustmentData } from './types';
import { apiClient } from '@/lib/api';
import { formatDate } from '@/lib/utils/date';
/**
* 건설 프로젝트 - 견적관리 Server Actions
@@ -252,7 +253,7 @@ function transformQuoteToEstimate(apiData: ApiQuote): Estimate {
// 완료 상태인 경우 updated_at을 완료일로 사용
// (상태가 완료로 변경될 때 updated_at이 갱신되므로)
const completedDate = status === 'completed' && apiData.updated_at
? apiData.updated_at.split('T')[0] // ISO 형식에서 날짜만 추출
? formatDate(apiData.updated_at) // ISO 형식에서 날짜만 추출
: null;
return {

View File

@@ -30,6 +30,7 @@ import {
import type { Labor, LaborStats, LaborCategory, LaborStatus } from './types';
import { CATEGORY_OPTIONS, STATUS_OPTIONS, SORT_OPTIONS, DEFAULT_PAGE_SIZE } from './constants';
import { getLaborList, deleteLabor, deleteLaborBulk, getLaborStats } from './actions';
import { formatNumber } from '@/lib/utils/amount';
// 테이블 컬럼 정의
const tableColumns = [
@@ -90,7 +91,7 @@ export default function LaborManagementClient({
// 가격 포맷
const formatPrice = useCallback((price: number | null) => {
if (price === null || price === 0) return '-';
return price.toLocaleString();
return formatNumber(price);
}, []);
// M 값 포맷

View File

@@ -4,6 +4,7 @@ import type { Order, OrderStats, OrderDetail, OrderDetailFormData, OrderStatus,
import { apiClient } from '@/lib/api';
import type { CommonCode } from '@/lib/api/common-codes';
import { toCommonCodeOptions } from '@/lib/api/common-codes';
import { formatDate } from '@/lib/utils/date';
// ========================================
// 타입 변환 함수
@@ -141,8 +142,8 @@ function transformOrder(apiOrder: ApiOrder): Order {
// 달력 표시용 기간: received_at ~ delivery_date
// received_at이 없으면 delivery_date를 시작일로 사용 (단일 날짜 이벤트)
// delivery_date도 없으면 created_at을 사용
periodStart: apiOrder.received_at || apiOrder.delivery_date || apiOrder.created_at.split('T')[0],
periodEnd: apiOrder.delivery_date || apiOrder.received_at || apiOrder.created_at.split('T')[0],
periodStart: apiOrder.received_at || apiOrder.delivery_date || formatDate(apiOrder.created_at),
periodEnd: apiOrder.delivery_date || apiOrder.received_at || formatDate(apiOrder.created_at),
createdAt: apiOrder.created_at,
updatedAt: apiOrder.updated_at,
};

View File

@@ -35,6 +35,8 @@ import {
partnerToFormData,
} from './types';
import { createPartner, updatePartner, deletePartner } from './actions';
import { formatNumber } from '@/lib/utils/amount';
import { getLocalDateString } from '@/lib/utils/date';
// 목업 문서 목록 (상세 모드에서 다운로드 버튼 테스트용)
const MOCK_DOCUMENTS: PartnerDocument[] = [
@@ -163,7 +165,7 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
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: PartnerMemo = {
id: String(Date.now()),
@@ -498,7 +500,7 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
<Label className="text-sm font-medium text-gray-700"></Label>
<Input
type="text"
value={formData.outstandingAmount?.toLocaleString() + '원'}
value={formatNumber(formData.outstandingAmount) + '원'}
disabled
className="bg-gray-50"
/>

View File

@@ -23,6 +23,7 @@ import {
} from '@/components/ui/table';
import type { ProgressBillingItem } from '../types';
import { MOCK_BILLING_NAMES } from '../types';
import { formatNumber } from '@/lib/utils/amount';
interface ProgressBillingItemTableProps {
items: ProgressBillingItem[];
@@ -142,7 +143,7 @@ export function ProgressBillingItemTable({
className="min-w-[50px]"
/>
) : (
item.width.toLocaleString()
formatNumber(item.width)
)}
</TableCell>
<TableCell>
@@ -153,7 +154,7 @@ export function ProgressBillingItemTable({
className="min-w-[50px]"
/>
) : (
item.height.toLocaleString()
formatNumber(item.height)
)}
</TableCell>
<TableCell>{item.workTeamLeader}</TableCell>
@@ -169,10 +170,10 @@ export function ProgressBillingItemTable({
allowDecimal
/>
) : (
item.quantity.toLocaleString()
formatNumber(item.quantity)
)}
</TableCell>
<TableCell>{item.currentBilling.toLocaleString()}</TableCell>
<TableCell>{formatNumber(item.currentBilling)}</TableCell>
<TableCell>{item.status}</TableCell>
</TableRow>
))}

View File

@@ -28,6 +28,7 @@ import { Client } from "../../hooks/useClientList";
import { PageLayout } from "../organisms/PageLayout";
import { PageHeader } from "../organisms/PageHeader";
import { useMenuStore } from "@/stores/menuStore";
import { formatNumber } from "@/lib/utils/amount";
interface ClientDetailProps {
client: Client;
@@ -70,8 +71,7 @@ export function ClientDetail({
// 금액 포맷
const formatCurrency = (amount: string) => {
if (!amount) return "-";
const num = Number(amount);
return `${num.toLocaleString()}`;
return `${formatNumber(Number(amount))}`;
};
return (

View File

@@ -2,6 +2,8 @@
* 이벤트 타입 정의
*/
import { formatDate } from '@/lib/utils/date';
// API 타입 re-export
export type { PostApiData, PostPaginationResponse } from '../shared/types';
@@ -14,7 +16,7 @@ export function transformPostToEvent(post: import('../shared/types').PostApiData
const endDateField = post.custom_field_values?.find(f => f.field_key === 'end_date');
// 기본값으로 created_at 사용
const createdDate = post.created_at.split('T')[0];
const createdDate = formatDate(post.created_at);
return {
id: String(post.id),

View File

@@ -2,6 +2,8 @@
* 공지사항 타입 정의
*/
import { formatDate } from '@/lib/utils/date';
// API 타입 re-export
export type { PostApiData, PostPaginationResponse } from '../shared/types';
@@ -32,7 +34,7 @@ export function transformPostToNotice(post: import('../shared/types').PostApiDat
title: post.title,
content: post.content,
author: post.author?.name || '관리자',
createdAt: post.created_at.split('T')[0],
createdAt: formatDate(post.created_at),
viewCount: post.views,
attachments: post.files?.map(file => ({
id: String(file.id),

View File

@@ -20,6 +20,7 @@ import {
DocumentFeatures,
PdfMeta,
} from '../types';
import { getTodayString } from '@/lib/utils/date';
/**
* 문서 뷰어 (Shell)
@@ -263,7 +264,7 @@ export function DocumentViewer({
title,
orientation: 'portrait',
documentNumber: pdfMeta?.documentNumber || '',
createdDate: pdfMeta?.createdDate || new Date().toISOString().slice(0, 10),
createdDate: pdfMeta?.createdDate || getTodayString(),
showHeaderFooter: pdfMeta?.showHeaderFooter !== false,
}),
});

View File

@@ -19,10 +19,12 @@ 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 { ContentSkeleton } from '@/components/ui/skeleton';
import { toast } from 'sonner';
import { formatAmountWon as formatCurrency } from '@/lib/utils/amount';
import type { Card as CardType, CardFormData, CardStatus } from './types';
import {
CARD_COMPANIES,
@@ -40,10 +42,6 @@ import {
getApprovalFormUrl,
} from './actions';
function formatCurrency(value: number): string {
return value.toLocaleString('ko-KR') + '원';
}
function formatExpiryDate(value: string): string {
if (value && value.length === 4) {
return `${value.slice(0, 2)}/${value.slice(2)}`;
@@ -71,7 +69,6 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
const router = useRouter();
const searchParams = useSearchParams();
const [mode, setMode] = useState(initialMode);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isLoadingApproval, setIsLoadingApproval] = useState(false);
const [employees, setEmployees] = useState<Array<{ id: string; label: string }>>([]);
@@ -154,16 +151,11 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
}
};
const handleConfirmDelete = async () => {
if (!card?.id) return;
const result = await deleteCard(card.id);
if (result.success) {
toast.success('카드가 삭제되었습니다.');
router.push('/ko/hr/card-management');
} else {
toast.error(result.error || '카드 삭제에 실패했습니다.');
}
};
const deleteDialog = useDeleteDialog({
onDelete: async (id) => deleteCard(id),
onSuccess: () => router.push('/ko/hr/card-management'),
entityName: '카드',
});
const handleCancel = () => {
if (isCreateMode) {
@@ -352,7 +344,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
{/* 하단 버튼 */}
<div className="flex items-center justify-end gap-2">
<Button variant="outline" onClick={() => setShowDeleteDialog(true)} className="text-destructive hover:bg-destructive hover:text-destructive-foreground">
<Button variant="outline" onClick={() => deleteDialog.single.open(card!.id)} className="text-destructive hover:bg-destructive hover:text-destructive-foreground">
<Trash2 className="w-4 h-4 mr-2" />
</Button>
@@ -364,8 +356,8 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
</div>
<DeleteConfirmDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
open={deleteDialog.single.isOpen}
onOpenChange={deleteDialog.single.onOpenChange}
description={
<>
?
@@ -375,7 +367,8 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
</span>
</>
}
onConfirm={handleConfirmDelete}
onConfirm={deleteDialog.single.confirm}
loading={deleteDialog.isPending}
/>
</PageLayout>
);

View File

@@ -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;

View File

@@ -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 && (
<div>
<dt className="text-sm font-medium text-muted-foreground"></dt>
<dd className="text-sm mt-1">{employee.salary.toLocaleString()}</dd>
<dd className="text-sm mt-1">{formatNumber(employee.salary)}</dd>
</div>
)}
{employee.bankAccount && (

View File

@@ -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' },
], []);

View File

@@ -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,
};

View File

@@ -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,
// 상태

View File

@@ -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;

View File

@@ -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 || '',

View File

@@ -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));
}
// 제품 정보 타입

View File

@@ -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;

View File

@@ -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({
<td className={tdCenter}>{row.no}</td>
<td className={tdCenter}>{row.type}</td>
<td className={tdCenter}>{row.code}</td>
<td className={tdCenter}>{row.openW.toLocaleString()}</td>
<td className={tdCenter}>{row.openH.toLocaleString()}</td>
<td className={tdCenter}>{row.madeW.toLocaleString()}</td>
<td className={tdCenter}>{row.madeH.toLocaleString()}</td>
<td className={tdCenter}>{formatNumber(row.openW)}</td>
<td className={tdCenter}>{formatNumber(row.openH)}</td>
<td className={tdCenter}>{formatNumber(row.madeW)}</td>
<td className={tdCenter}>{formatNumber(row.madeH)}</td>
<td className={tdCenter}>{row.guideRail}</td>
<td className={tdCenter}>{row.shaft}</td>
<td className={tdCenter}>{row.caseInch}</td>
@@ -319,10 +320,10 @@ export function SalesOrderDocument({
<tr key={row.no} className="border-b border-gray-300">
<td className={tdCenter}>{row.no}</td>
<td className={tdCenter}>{row.code}</td>
<td className={tdCenter}>{row.openW.toLocaleString()}</td>
<td className={tdCenter}>{row.openH.toLocaleString()}</td>
<td className={tdCenter}>{row.madeW.toLocaleString()}</td>
<td className={tdCenter}>{row.madeH.toLocaleString()}</td>
<td className={tdCenter}>{formatNumber(row.openW)}</td>
<td className={tdCenter}>{formatNumber(row.openH)}</td>
<td className={tdCenter}>{formatNumber(row.madeW)}</td>
<td className={tdCenter}>{formatNumber(row.madeH)}</td>
<td className={tdCenter}>{row.guideRail}</td>
<td className={tdCenter}>{row.shaft}</td>
<td className={tdCenter}>{row.jointBar}</td>

View File

@@ -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;

View File

@@ -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<T>(column: Column<T>, value: any, row: T, index?: number): R
// 타입별 렌더링
switch (column.type) {
case "number":
return <span className="font-mono">{formattedValue?.toLocaleString()}</span>;
return <span className="font-mono">{formatNumber(formattedValue)}</span>;
case "currency":
const locale = column.currencyConfig?.locale || "ko-KR";

View File

@@ -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
<td className={tdCenter}>{row.no}</td>
<td className={tdCenter}>{row.type}</td>
<td className={tdCenter}>{row.code}</td>
<td className={tdCenter}>{row.openW.toLocaleString()}</td>
<td className={tdCenter}>{row.openH.toLocaleString()}</td>
<td className={tdCenter}>{row.madeW.toLocaleString()}</td>
<td className={tdCenter}>{row.madeH.toLocaleString()}</td>
<td className={tdCenter}>{formatNumber(row.openW)}</td>
<td className={tdCenter}>{formatNumber(row.openH)}</td>
<td className={tdCenter}>{formatNumber(row.madeW)}</td>
<td className={tdCenter}>{formatNumber(row.madeH)}</td>
<td className={tdCenter}>{row.guideRail}</td>
<td className={tdCenter}>{row.shaft}</td>
<td className={tdCenter}>{row.caseInch}</td>
@@ -320,10 +321,10 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s
<tr key={row.no} className="border-b border-gray-300">
<td className={tdCenter}>{row.no}</td>
<td className={tdCenter}>{row.code}</td>
<td className={tdCenter}>{row.openW.toLocaleString()}</td>
<td className={tdCenter}>{row.openH.toLocaleString()}</td>
<td className={tdCenter}>{row.madeW.toLocaleString()}</td>
<td className={tdCenter}>{row.madeH.toLocaleString()}</td>
<td className={tdCenter}>{formatNumber(row.openW)}</td>
<td className={tdCenter}>{formatNumber(row.openH)}</td>
<td className={tdCenter}>{formatNumber(row.madeW)}</td>
<td className={tdCenter}>{formatNumber(row.madeH)}</td>
<td className={tdCenter}>{row.guideRail}</td>
<td className={tdCenter}>{row.shaft}</td>
<td className={tdCenter}>{row.jointBar}</td>

View File

@@ -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) {

View File

@@ -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
<td className="border border-gray-300 px-3 py-2 text-center">-</td>
<td className="border border-gray-300 px-3 py-2 text-center">{item.unit}</td>
<td className="border border-gray-300 px-3 py-2 text-right font-mono">
{item.salesPrice.toLocaleString()}
{formatNumber(item.salesPrice)}
</td>
</tr>
))}

View File

@@ -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({
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-semibold">
{purchasePrice?.toLocaleString() || '-'}
{formatNumber(purchasePrice)}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-semibold">
{salesPrice?.toLocaleString() || '-'}
{formatNumber(salesPrice)}
</span>
</div>
<div className="flex justify-between">

View File

@@ -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({
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span>{(purchasePrice || 0).toLocaleString()}</span>
<span>{formatNumber(purchasePrice || 0)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span>{(processingCost || 0).toLocaleString()}</span>
<span>{formatNumber(processingCost || 0)}</span>
</div>
<Separator className="my-2" />
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span>{((purchasePrice || 0) + (processingCost || 0)).toLocaleString()}</span>
<span>{formatNumber((purchasePrice || 0) + (processingCost || 0))}</span>
</div>
{loss > 0 && (
<div className="flex justify-between text-orange-600">
<span>LOSS ({loss}%):</span>
<span>
+{(((purchasePrice || 0) + (processingCost || 0)) * (loss / 100)).toLocaleString()}
+{formatNumber(((purchasePrice || 0) + (processingCost || 0)) * (loss / 100))}
</span>
</div>
)}
<Separator className="my-2" />
<div className="flex justify-between font-semibold text-base">
<span className="text-blue-900">LOSS :</span>
<span className="text-blue-600">{costWithLoss.toLocaleString()}</span>
<span className="text-blue-600">{formatNumber(costWithLoss)}</span>
</div>
</div>
</div>
@@ -679,17 +680,17 @@ export function PricingFormClient({
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">LOSS :</span>
<span>{costWithLoss.toLocaleString()}</span>
<span>{formatNumber(costWithLoss)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span>{salesPrice.toLocaleString()}</span>
<span>{formatNumber(salesPrice)}</span>
</div>
<Separator className="my-2" />
<div className="flex justify-between font-semibold text-base">
<span className="text-green-900">:</span>
<span className="text-green-600">
{marginAmount.toLocaleString()} ({marginRate.toFixed(1)}%)
{formatNumber(marginAmount)} ({marginRate.toFixed(1)}%)
</span>
</div>
</div>

View File

@@ -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({
<div>
<span className="text-muted-foreground">:</span>
<div className="font-semibold">
{pricingData.purchasePrice?.toLocaleString() || '-'}
{formatNumber(pricingData.purchasePrice)}
</div>
</div>
<div>
<span className="text-muted-foreground">:</span>
<div className="font-semibold">
{pricingData.processingCost?.toLocaleString() || '-'}
{formatNumber(pricingData.processingCost)}
</div>
</div>
<div>
<span className="text-muted-foreground">:</span>
<div className="font-semibold">
{pricingData.salesPrice?.toLocaleString() || '-'}
{formatNumber(pricingData.salesPrice)}
</div>
</div>
<div>
@@ -118,19 +119,19 @@ export function PricingHistoryDialog({
<div>
<span className="text-muted-foreground">:</span>
<div>
{revision.previousData?.purchasePrice?.toLocaleString() || '-'}
{formatNumber(revision.previousData?.purchasePrice)}
</div>
</div>
<div>
<span className="text-muted-foreground">:</span>
<div>
{revision.previousData?.processingCost?.toLocaleString() || '-'}
{formatNumber(revision.previousData?.processingCost)}
</div>
</div>
<div>
<span className="text-muted-foreground">:</span>
<div>
{revision.previousData?.salesPrice?.toLocaleString() || '-'}
{formatNumber(revision.previousData?.salesPrice)}
</div>
</div>
<div>

View File

@@ -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 렌더링

View File

@@ -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<InspectionContentRef, ScreenIn
const formatNumberWithComma = (value: string): string => {
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) => {

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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') {

View File

@@ -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,
};

View File

@@ -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',
},

View File

@@ -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 (

View File

@@ -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,

View File

@@ -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({
<div className="flex items-center gap-1.5 text-sm text-gray-700">
<span className="text-gray-500"> </span>
<span className="font-medium">
{item.width.toLocaleString()} X {item.height.toLocaleString()} mm
{formatNumber(item.width)} X {formatNumber(item.height)} mm
</span>
<span className="font-medium text-gray-900">{item.quantity}</span>
</div>
@@ -205,7 +206,7 @@ export const WorkItemCard = memo(function WorkItemCard({
<TableRow key={mat.id}>
<TableCell className="text-center text-xs">{mat.lotNo}</TableCell>
<TableCell className="text-center text-xs">{mat.itemName}</TableCell>
<TableCell className="text-center text-xs">{parseFloat(String(mat.quantity)).toLocaleString()}</TableCell>
<TableCell className="text-center text-xs">{formatNumber(parseFloat(String(mat.quantity)))}</TableCell>
<TableCell className="text-center text-xs">{mat.unit}</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
@@ -246,7 +247,7 @@ function ScreenCuttingInfo({ width, sheets }: { width: number; sheets: number })
<div className="p-4 bg-gray-50 rounded-lg border border-gray-100">
<p className="text-xs text-gray-400 mb-1.5"></p>
<p className="text-base font-semibold text-gray-900">
{width.toLocaleString()}mm X {sheets}
{formatNumber(width)}mm X {sheets}
</p>
</div>
);
@@ -265,7 +266,7 @@ function SlatExtraInfo({
return (
<div className="flex gap-2">
<Badge variant="outline" className="text-xs px-2.5 py-1 border-gray-300">
{length.toLocaleString()}mm
{formatNumber(length)}mm
</Badge>
<Badge variant="outline" className="text-xs px-2.5 py-1 border-gray-300">
{slatCount}
@@ -317,7 +318,7 @@ function BendingExtraInfo({ info }: { info: BendingInfo }) {
<div key={i} className="flex gap-2 text-xs">
<span className="text-gray-500 w-14">{i === 0 ? '길이별 수량' : ''}</span>
<span className="text-gray-900 font-medium">
{lq.length.toLocaleString()}mm X {lq.quantity}
{formatNumber(lq.length)}mm X {lq.quantity}
</span>
</div>
))}

View File

@@ -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}
/>
) : (

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More