refactor(WEB): 회계/견적/설정/생산 등 전반적 코드 개선 및 공통화 2차
- 회계 모듈 전면 개선: 청구/입금/출금/매입/매출/세금계산서/일반전표/거래처원장 등 - 견적 모듈 금액 포맷/할인/수식/미리보기 등 코드 정리 - 설정 모듈: 계정관리/직급/직책/권한 상세 간소화 - 생산 모듈: 작업지시서/작업자화면/검수 문서 코드 정리 - UniversalListPage 엑셀 다운로드 및 필터 기능 확장 - 대시보드/게시판/수주 등 날짜 유틸 공통화 적용 - claudedocs 문서 인덱스 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 간 의존성 상세
|
||||
|
||||
| 의존 관계 | 이유 |
|
||||
|
||||
@@ -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 적용**
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import type { ExpenseChartItem } from '../hooks/transformers';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
function formatTooltipValue(value: number): string {
|
||||
if (value >= 100000000) return `${(value / 100000000).toFixed(1)}억원`;
|
||||
if (value >= 10000) return `${Math.round(value / 10000).toLocaleString()}만원`;
|
||||
return `${value.toLocaleString()}원`;
|
||||
if (value >= 10000) return `${formatNumber(Math.round(value / 10000))}만원`;
|
||||
return `${formatNumber(value)}원`;
|
||||
}
|
||||
|
||||
export function ExpenseDonutChart({ data }: { data: ExpenseChartItem[] }) {
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip, Cell } from 'recharts';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import type { OverviewChartItem } from '../hooks/transformers';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
function formatTooltipValue(value: number): string {
|
||||
if (value >= 100000000) return `${(value / 100000000).toFixed(1)}억원`;
|
||||
if (value >= 10000) return `${Math.round(value / 10000).toLocaleString()}만원`;
|
||||
return `${value.toLocaleString()}원`;
|
||||
if (value >= 10000) return `${formatNumber(Math.round(value / 10000))}만원`;
|
||||
return `${formatNumber(value)}원`;
|
||||
}
|
||||
|
||||
export function OverviewSummaryChart({ data }: { data: OverviewChartItem[] }) {
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip, Legend } from 'recharts';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import type { ReceivableChartItem } from '../hooks/transformers';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
function formatTooltipValue(value: number): string {
|
||||
if (value >= 100000000) return `${(value / 100000000).toFixed(1)}억원`;
|
||||
if (value >= 10000) return `${Math.round(value / 10000).toLocaleString()}만원`;
|
||||
return `${value.toLocaleString()}원`;
|
||||
if (value >= 10000) return `${formatNumber(Math.round(value / 10000))}만원`;
|
||||
return `${formatNumber(value)}원`;
|
||||
}
|
||||
|
||||
export function ReceivableBarChart({ data }: { data: ReceivableChartItem[] }) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
WelfareData,
|
||||
} from '@/components/business/CEODashboard/types';
|
||||
import type { TodayIssueData } from '@/hooks/useCEODashboard';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
// ============================================
|
||||
// 금액 포맷 헬퍼
|
||||
@@ -27,9 +28,9 @@ function formatAmount(amount: number): string {
|
||||
const value = (absAmount / 100000000).toFixed(1);
|
||||
return `${sign}${value}억`;
|
||||
} else if (absAmount >= 10000) {
|
||||
return `${sign}${Math.round(absAmount / 10000).toLocaleString()}만`;
|
||||
return `${sign}${formatNumber(Math.round(absAmount / 10000))}만`;
|
||||
}
|
||||
return `${sign}${absAmount.toLocaleString()}원`;
|
||||
return `${sign}${formatNumber(absAmount)}원`;
|
||||
}
|
||||
|
||||
function formatAmountWon(amount: number): string {
|
||||
@@ -39,13 +40,13 @@ function formatAmountWon(amount: number): string {
|
||||
const value = (absAmount / 100000000).toFixed(1);
|
||||
return `${sign}${value}억원`;
|
||||
} else if (absAmount >= 10000) {
|
||||
return `${sign}${Math.round(absAmount / 10000).toLocaleString()}만원`;
|
||||
return `${sign}${formatNumber(Math.round(absAmount / 10000))}만원`;
|
||||
}
|
||||
return `${sign}${absAmount.toLocaleString()}원`;
|
||||
return `${sign}${formatNumber(absAmount)}원`;
|
||||
}
|
||||
|
||||
function formatCurrency(amount: number): string {
|
||||
return amount.toLocaleString();
|
||||
return formatNumber(amount);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { DashboardSwitcher } from '@/components/business/DashboardSwitcher';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
// ============================================
|
||||
// Mock 데이터
|
||||
@@ -102,7 +103,7 @@ function CashflowWidget() {
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
|
||||
<YAxis tick={{ fontSize: 11 }} tickFormatter={(v: number) => `${v}만`} width={50} />
|
||||
<Tooltip formatter={(v: number | string | undefined) => `${Number(v ?? 0).toLocaleString()}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
|
||||
<Tooltip formatter={(v: number | string | undefined) => `${formatNumber(Number(v ?? 0))}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
|
||||
<Bar dataKey="입금" fill="#3b82f6" radius={[3, 3, 0, 0]} maxBarSize={20} />
|
||||
<Bar dataKey="출금" fill="#f97316" radius={[3, 3, 0, 0]} maxBarSize={20} />
|
||||
</BarChart>
|
||||
@@ -119,7 +120,7 @@ function ExpenseWidget() {
|
||||
<Pie data={chartData} cx="50%" cy="50%" innerRadius={40} outerRadius={65} dataKey="value" nameKey="name">
|
||||
{chartData.map((entry, i) => <Cell key={i} fill={entry.color} />)}
|
||||
</Pie>
|
||||
<Tooltip formatter={(v: number | string | undefined) => `${Number(v ?? 0).toLocaleString()}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
|
||||
<Tooltip formatter={(v: number | string | undefined) => `${formatNumber(Number(v ?? 0))}만원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="flex-1 space-y-2">
|
||||
@@ -129,7 +130,7 @@ function ExpenseWidget() {
|
||||
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: item.color }} />
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
<span className="font-medium">{item.value.toLocaleString()}만</span>
|
||||
<span className="font-medium">{formatNumber(item.value)}만</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { DashboardSwitcher } from '@/components/business/DashboardSwitcher';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip, Cell } from 'recharts';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
// ============================================
|
||||
// Mock 데이터
|
||||
@@ -211,7 +212,7 @@ function Level2({ kpi, items, onSelect, onBack }: { kpi: KpiItem; items: DetailI
|
||||
<CartesianGrid strokeDasharray="3 3" horizontal={false} />
|
||||
<XAxis type="number" tick={{ fontSize: 11 }} />
|
||||
<YAxis dataKey="name" type="category" tick={{ fontSize: 11 }} width={100} />
|
||||
<Tooltip formatter={(v: number | string | undefined) => `${Number(v ?? 0).toLocaleString()}원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
|
||||
<Tooltip formatter={(v: number | string | undefined) => `${formatNumber(Number(v ?? 0))}원`} contentStyle={{ fontSize: '12px', borderRadius: '8px' }} />
|
||||
<Bar dataKey="value" fill={kpi.color} radius={[0, 4, 4, 0]} maxBarSize={22} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
// 샘플 데이터 타입
|
||||
interface ProductItem {
|
||||
@@ -227,7 +228,7 @@ export default function EditableTableSamplePage() {
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium">총 합계</span>
|
||||
<span className="text-xl font-bold text-primary">
|
||||
{totalAmount.toLocaleString()}원
|
||||
{formatNumber(totalAmount)}원
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { DocumentHeader, QualityApprovalTable } from '@/components/document-system';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
// 절곡품 중간검사 성적서 데이터 타입
|
||||
export interface BendingInspectionData {
|
||||
@@ -300,7 +301,7 @@ export const BendingInspectionDocument = ({ data = MOCK_BENDING_INSPECTION }: Be
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">
|
||||
☐ 양호 ☐ 불량
|
||||
</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">{item.length > 0 ? item.length.toLocaleString() : ''}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">{item.length > 0 ? formatNumber(item.length) : ''}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">{item.conductance1}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">{item.measured1}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">{item.point}</td>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { DocumentHeader, QualityApprovalTable } from '@/components/document-system';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
// 스크린 중간검사 성적서 데이터 타입
|
||||
export interface ScreenInspectionData {
|
||||
@@ -259,8 +260,8 @@ export const ScreenInspectionDocument = ({ data = MOCK_SCREEN_INSPECTION }: Scre
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">
|
||||
☐ 양호 ☐ 불량
|
||||
</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center font-medium">{item.height.standard.toLocaleString()}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center font-medium">{item.width.standard.toLocaleString()}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center font-medium">{formatNumber(item.height.standard)}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center font-medium">{formatNumber(item.width.standard)}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">{item.checkCount}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">
|
||||
☐ OK ☐ NG
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { DocumentHeader, QualityApprovalTable } from '@/components/document-system';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
// 슬랫 중간검사 성적서 데이터 타입
|
||||
export interface SlatInspectionData {
|
||||
@@ -241,7 +242,7 @@ export const SlatInspectionDocument = ({ data = MOCK_SLAT_INSPECTION }: SlatInsp
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">{item.height1.standard} ± 1</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">{item.height1.measured}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">{item.bandLength.conductance}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">{item.bandLength.measured > 0 ? item.bandLength.measured.toLocaleString() : ''}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">{item.bandLength.measured > 0 ? formatNumber(item.bandLength.measured) : ''}</td>
|
||||
<td className="border border-gray-400 px-1 py-1 text-center">
|
||||
☐ 적합 ☐ 부<br/>적합
|
||||
</td>
|
||||
|
||||
@@ -262,12 +262,12 @@ export default function CustomerAccountManagementPage() {
|
||||
// 테이블 컬럼 정의 (Hooks 순서 보장을 위해 조건부 return 전에 정의)
|
||||
const tableColumns: TableColumn[] = useMemo(() => [
|
||||
{ key: "rowNumber", label: "번호", className: "px-4" },
|
||||
{ key: "code", label: "코드", className: "px-4" },
|
||||
{ key: "clientType", label: "구분", className: "px-4" },
|
||||
{ key: "name", label: "거래처명", className: "px-4" },
|
||||
{ key: "representative", label: "대표자", className: "px-4" },
|
||||
{ key: "manager", label: "담당자", className: "px-4" },
|
||||
{ key: "phone", label: "전화번호", className: "px-4" },
|
||||
{ key: "code", label: "코드", className: "px-4", sortable: true },
|
||||
{ key: "clientType", label: "구분", className: "px-4", sortable: true },
|
||||
{ key: "name", label: "거래처명", className: "px-4", sortable: true },
|
||||
{ key: "representative", label: "대표자", className: "px-4", sortable: true },
|
||||
{ key: "manager", label: "담당자", className: "px-4", sortable: true },
|
||||
{ key: "phone", label: "전화번호", className: "px-4", sortable: true },
|
||||
], []);
|
||||
|
||||
// 핸들러 - 페이지 기반 네비게이션
|
||||
|
||||
@@ -38,7 +38,7 @@ import { toast } from "sonner";
|
||||
import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate";
|
||||
import { orderSalesConfig } from "@/components/orders/orderSalesConfig";
|
||||
import { BadgeSm } from "@/components/atoms/BadgeSm";
|
||||
import { formatAmount } from "@/lib/utils/amount";
|
||||
import { formatAmount, formatNumber } from "@/lib/utils/amount";
|
||||
import {
|
||||
OrderItem,
|
||||
getOrderById,
|
||||
@@ -57,7 +57,7 @@ function formatQuantity(quantity: number, unit?: string): string {
|
||||
|
||||
if (countableUnits.includes(upperUnit)) {
|
||||
// 개수 단위는 정수로 반올림
|
||||
return Math.round(quantity).toLocaleString();
|
||||
return formatNumber(Math.round(quantity));
|
||||
}
|
||||
|
||||
// 측정 단위는 소수점 4자리까지 반올림 후 불필요한 0 제거
|
||||
|
||||
@@ -44,7 +44,7 @@ import { toast } from "sonner";
|
||||
import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate";
|
||||
import { orderSalesConfig } from "@/components/orders/orderSalesConfig";
|
||||
import { BadgeSm } from "@/components/atoms/BadgeSm";
|
||||
import { formatAmount } from "@/lib/utils/amount";
|
||||
import { formatAmount, formatNumber } from "@/lib/utils/amount";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -87,7 +87,7 @@ function formatQuantity(quantity: number, unit?: string): string {
|
||||
|
||||
if (countableUnits.includes(upperUnit)) {
|
||||
// 개수 단위는 정수로 반올림
|
||||
return Math.round(quantity).toLocaleString();
|
||||
return formatNumber(Math.round(quantity));
|
||||
}
|
||||
|
||||
// 측정 단위는 소수점 4자리까지 반올림 후 불필요한 0 제거
|
||||
|
||||
@@ -475,19 +475,19 @@ function OrderListContent() {
|
||||
// 테이블 컬럼 정의 (16개: 체크박스, 번호, 로트번호, 현장명, 출고예정일, 접수일, 수주처, 제품명, 수신자, 수신주소, 수신처, 배송, 담당자, 틀수, 상태, 비고)
|
||||
const tableColumns: TableColumn[] = useMemo(() => [
|
||||
{ key: "rowNumber", label: "번호", className: "px-2 text-center" },
|
||||
{ key: "lotNumber", label: "로트번호", className: "px-2" },
|
||||
{ key: "siteName", label: "현장명", className: "px-2" },
|
||||
{ key: "expectedShipDate", label: "출고예정일", className: "px-2" },
|
||||
{ key: "orderDate", label: "수주일", className: "px-2" },
|
||||
{ key: "client", label: "수주처", className: "px-2" },
|
||||
{ key: "productName", label: "제품명", className: "px-2" },
|
||||
{ key: "receiver", label: "수신자", className: "px-2" },
|
||||
{ key: "receiverAddress", label: "수신주소", className: "px-2" },
|
||||
{ key: "receiverPlace", label: "수신처", className: "px-2" },
|
||||
{ key: "deliveryMethod", label: "배송", className: "px-2" },
|
||||
{ key: "manager", label: "담당자", className: "px-2" },
|
||||
{ key: "frameCount", label: "틀수", className: "px-2 text-center" },
|
||||
{ key: "status", label: "상태", className: "px-2" },
|
||||
{ key: "lotNumber", label: "로트번호", className: "px-2", sortable: true },
|
||||
{ key: "siteName", label: "현장명", className: "px-2", sortable: true },
|
||||
{ key: "expectedShipDate", label: "출고예정일", className: "px-2", sortable: true },
|
||||
{ key: "orderDate", label: "수주일", className: "px-2", sortable: true },
|
||||
{ key: "client", label: "수주처", className: "px-2", sortable: true },
|
||||
{ key: "productName", label: "제품명", className: "px-2", sortable: true },
|
||||
{ key: "receiver", label: "수신자", className: "px-2", sortable: true },
|
||||
{ key: "receiverAddress", label: "수신주소", className: "px-2", sortable: true },
|
||||
{ key: "receiverPlace", label: "수신처", className: "px-2", sortable: true },
|
||||
{ key: "deliveryMethod", label: "배송", className: "px-2", sortable: true },
|
||||
{ key: "manager", label: "담당자", className: "px-2", sortable: true },
|
||||
{ key: "frameCount", label: "틀수", className: "px-2 text-center", sortable: true },
|
||||
{ key: "status", label: "상태", className: "px-2", sortable: true },
|
||||
{ key: "remarks", label: "비고", className: "px-2" },
|
||||
], []);
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||
import { toast } from "sonner";
|
||||
import { ServerErrorPage } from "@/components/common/ServerErrorPage";
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
// 생산지시 상태 타입
|
||||
type ProductionOrderStatus = "waiting" | "in_progress" | "completed";
|
||||
@@ -590,7 +591,7 @@ export default function ProductionOrderDetailPage() {
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{item.requiredQty > 0 ? item.requiredQty.toLocaleString() : "-"}
|
||||
{item.requiredQty > 0 ? formatNumber(item.requiredQty) : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{item.qty}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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 || '-' },
|
||||
]}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 || '-' },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}원` },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
// ===== 합계 계산 (동적 월) =====
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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}일`} />
|
||||
|
||||
@@ -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}일` },
|
||||
]}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 || '-' },
|
||||
]}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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)}원`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 값 포맷
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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' },
|
||||
], []);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
// 상태
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
// 제품 정보 타입
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 렌더링
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user