refactor(WEB): 회계/차량/결재 등 코드 중복 제거 및 공통 훅 추출
- useAccountingListPage, useDateRange 공통 훅 추출 - accounting/shared/ 공통 컴포넌트 분리 - 회계 모듈(입금/출금/매출/매입/청구 등) 중복 로직 통합 - 차량관리 page.tsx 패턴 간소화 - 건설/결재/자재/출하/단가 등 날짜 관련 코드 공통화 - 코드 중복 제거 체크리스트 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
# 코드 중복 제거 및 공통화 체크리스트
|
||||
|
||||
> 작성일: 2026-02-19
|
||||
> 기반: 4개 에이전트 병렬 분석 결과 (회계 page.tsx / 회계 컴포넌트 / 유틸 중복 / 비회계 페이지)
|
||||
> 구조: 6개 작업 패키지 (WP) — 의존성 기반 병렬화 설계
|
||||
|
||||
---
|
||||
|
||||
## 작업 패키지 의존성 맵
|
||||
|
||||
```
|
||||
Phase 1 (독립 — 전부 병렬 가능):
|
||||
[x] WP-1: 날짜 하드코딩 긴급 수정 (10파일) ✅
|
||||
[x] WP-2: formatAmount/formatDate import 통일 (35+파일) ✅
|
||||
[x] WP-3: 차량관리 edit 페이지 리다이렉트 전환 (6파일) ✅
|
||||
|
||||
Phase 2 (Phase 1 완료 후 — 서로 간 병렬 가능):
|
||||
[x] WP-4: 공통 훅 추출 (useDateRange, useAccountingListPage 등) ✅
|
||||
[x] WP-5: 회계 컴포넌트 공통 패턴 추출 (팩토리 함수, 공통 컴포넌트) ✅
|
||||
|
||||
Phase 3 (Phase 2 완료 후):
|
||||
[x] WP-6: .toISOString().split('T')[0] → getTodayString() 전환 (15파일 26건) ✅
|
||||
```
|
||||
|
||||
### 병렬 실행 판단 기준
|
||||
|
||||
| 판단 | 조건 |
|
||||
|------|------|
|
||||
| **병렬 가능** | 수정 파일이 겹치지 않고, 새 유틸/훅에 의존하지 않음 |
|
||||
| **순차 필요** | 새로 만드는 훅/유틸을 다른 WP에서 import해야 하는 경우 |
|
||||
|
||||
---
|
||||
|
||||
## WP-1: 날짜 필터 하드코딩 긴급 수정 ✅ 완료 (2026-02-19)
|
||||
|
||||
**심각도**: 🔴 CRITICAL (데이터 누락 버그)
|
||||
**난이도**: 낮음 | **파일 수**: 10 | **예상 변경량**: 각 2줄
|
||||
**병렬**: Phase 1 — 독립 실행 가능
|
||||
|
||||
### 현황
|
||||
매출관리와 동일한 버그. `2025-xx-xx` 하드코딩으로 잘못된 기간 데이터 표시.
|
||||
|
||||
### 수정 완료
|
||||
|
||||
**WP-4에서 useDateRange('currentYear') 적용으로 이미 수정된 파일:**
|
||||
- [x] `accounting/DepositManagement/index.tsx`
|
||||
- [x] `accounting/WithdrawalManagement/index.tsx`
|
||||
- [x] `accounting/BillManagement/index.tsx`
|
||||
|
||||
**이번 세션에서 추가 발견 및 수정한 파일 (동일 버그):**
|
||||
- [x] `accounting/BillManagement/BillManagementClient.tsx` — 실제 page에서 사용되는 컴포넌트 (`'2025-09-01'`~`'2025-09-03'` → `useDateRange('currentYear')`)
|
||||
- [x] `accounting/PurchaseManagement/index.tsx` — (`'2025-01-01'`~`'2025-12-31'` → `useDateRange('currentYear')`)
|
||||
- [x] `approval/ApprovalBox/index.tsx` — (`'2025-09-01'`~`'2025-09-03'` → `useDateRange('currentYear')`)
|
||||
- [x] `approval/ReferenceBox/index.tsx` — (`'2025-09-01'`~`'2025-09-03'` → `useDateRange('currentYear')`)
|
||||
- [x] `approval/DraftBox/index.tsx` — (`'2025-01-01'`~`'2025-12-31'` → `useDateRange('currentYear')`)
|
||||
- [x] `process-management/ProcessListClient.tsx` — (`'2025-01-01'`~`'2025-12-31'` → `useDateRange('currentYear')`)
|
||||
- [x] `hr/VacationManagement/index.tsx` — (`'2025-12-01'`~`'2025-12-31'` → `useDateRange('currentMonth')`)
|
||||
- [x] `hr/SalaryManagement/index.tsx` — (`'2025-12-01'`~`'2025-12-31'` → `useDateRange('currentMonth')`)
|
||||
- [x] `pricing-table-management/PricingTableListClient.tsx` — (`'2025-01-01'`~`'2025-12-31'` → `useDateRange('currentYear')`)
|
||||
- [x] `checklist-management/ChecklistListClient.tsx` — (`'2025-01-01'`~`'2025-12-31'` → `useDateRange('currentYear')`)
|
||||
|
||||
### 검증
|
||||
- [x] `npx tsc --noEmit` 통과
|
||||
- [x] `useState('2025` 잔존 0건 확인
|
||||
|
||||
---
|
||||
|
||||
## WP-2: formatAmount / formatDate import 통일 ✅ 완료 (2026-02-19)
|
||||
|
||||
**심각도**: 🟡 HIGH (코드 품질)
|
||||
**난이도**: 낮음 | **파일 수**: 24파일 수정 / 10파일 스킵 | **예상 변경량**: 각 파일 2-5줄 (로컬 함수 삭제 + import 추가)
|
||||
**병렬**: Phase 1 — 독립 실행 가능
|
||||
|
||||
### 2-A: formatAmount/formatNumber/formatCurrency 통일 — 21파일 완료
|
||||
|
||||
**canonical**: `src/lib/utils/amount.ts` → `formatNumber()`, `formatAmountWon()`
|
||||
|
||||
- [x] VehicleDispatch 3파일 → `import { formatNumber as formatAmount } from '@/lib/utils/amount'`
|
||||
- [x] 결재 문서 5파일 → `import { formatNumber as formatCurrency } from '@/lib/utils/amount'`
|
||||
- [x] SalesDetail, PurchaseDetail → `import { formatNumber as formatAmount } from '@/lib/utils/amount'`
|
||||
- [x] GiftCertificateManagement, DailyReport → `import { formatNumber as formatAmount } from '@/lib/utils/amount'`
|
||||
- [x] StageCard, ProjectCard → `import { formatNumber as formatAmount } from '@/lib/utils/amount'`
|
||||
- [x] ProjectListClient → `import { formatAmountWon as formatAmount } from '@/lib/utils/amount'`
|
||||
- [x] PricingTableForm, PricingTableListClient → `import { formatNumber } from '@/lib/utils/amount'`
|
||||
- [x] DirectConstructionContent, IndirectConstructionContent → `import { formatNumber } from '@/lib/utils/amount'`
|
||||
- [x] SubscriptionClient, SubscriptionManagement → `import { formatAmountWon as formatCurrency } from '@/lib/utils/amount'`
|
||||
|
||||
**스킵 (다른 동작):**
|
||||
- ReceivablesStatus (0일 때 '' 반환), VendorLedger (0일 때 '' 반환), VendorLedgerDetail (괄호 옵션)
|
||||
- CEODashboard/components.tsx (showUnit 옵션)
|
||||
|
||||
### 2-B: formatDate 통일 — 3파일 완료
|
||||
|
||||
**canonical**: `src/lib/utils/date.ts` → `formatDate()`
|
||||
|
||||
- [x] OrderManagementListClient, OrderManagementUnified, ConstructionManagementListClient → `import { formatDate } from '@/lib/utils/date'`
|
||||
|
||||
**스킵 (다른 출력 형식 — canonical formatDate와 비호환):**
|
||||
- BoardDetail, BoardForm (formatDateTime: `YYYY-MM-DD HH:MM`)
|
||||
- SubscriptionClient, SubscriptionManagement (한국어: `YYYY년 M월 D일`)
|
||||
- PermissionManagement (date-fns format 사용)
|
||||
- ProductionDashboard (`M/D` 형식)
|
||||
- HandoverReportDocumentModal (한국어 long date)
|
||||
|
||||
### 검증
|
||||
- [x] TypeScript 빌드 에러 없음 확인
|
||||
- [x] 각 수정 파일에서 포맷 결과가 기존과 동일한지 확인 (동일 로직 import)
|
||||
|
||||
---
|
||||
|
||||
## WP-3: 차량관리 edit 페이지 리다이렉트 전환 ✅ 이전 세션에서 완료
|
||||
|
||||
**심각도**: 🟡 HIGH (코드 중복)
|
||||
**난이도**: 낮음 | **파일 수**: 6 (3쌍)
|
||||
**병렬**: Phase 1 — 독립 실행 가능
|
||||
|
||||
### 현황
|
||||
이전 세션에서 이미 완료됨. 3개 edit 페이지 모두 리다이렉트 스텁으로 교체되어 있고,
|
||||
3개 `[id]/page.tsx` 모두 `useSearchParams`로 mode를 읽고 있음.
|
||||
|
||||
### 수정 완료
|
||||
|
||||
- [x] `vehicle-management/vehicle/[id]/edit/page.tsx` → `router.replace(...?mode=edit)` 리다이렉트 스텁
|
||||
- [x] `vehicle-management/vehicle/[id]/page.tsx` → `useSearchParams` + mode 처리
|
||||
- [x] `vehicle-management/forklift/[id]/edit/page.tsx` → 리다이렉트 스텁
|
||||
- [x] `vehicle-management/forklift/[id]/page.tsx` → `useSearchParams` + mode 처리
|
||||
- [x] `vehicle-management/vehicle-log/[id]/edit/page.tsx` → 리다이렉트 스텁
|
||||
- [x] `vehicle-management/vehicle-log/[id]/page.tsx` → `useSearchParams` + mode 처리
|
||||
|
||||
---
|
||||
|
||||
## WP-4: 공통 훅 추출 ✅ 완료 (2026-02-19)
|
||||
|
||||
**심각도**: 🟡 HIGH (아키텍처 개선)
|
||||
**난이도**: 중간 | **신규 파일**: 2개 | **적용 파일**: 18개
|
||||
**병렬**: Phase 2 — WP-4와 WP-5는 서로 병렬 가능.
|
||||
|
||||
### 4-A: `useDateRange` 훅 — 15파일 적용 완료
|
||||
|
||||
**위치**: `src/hooks/useDateRange.ts`
|
||||
**프리셋**: `'currentYear'` | `'currentMonth'` | `'today'` | `'none'`
|
||||
|
||||
- [x] `src/hooks/useDateRange.ts` 생성
|
||||
- [x] 회계 5개 컴포넌트 적용:
|
||||
- SalesManagement → `useDateRange('currentYear')`
|
||||
- DepositManagement → `useDateRange('currentYear')`
|
||||
- WithdrawalManagement → `useDateRange('currentYear')`
|
||||
- BillManagement → `useDateRange('currentYear')`
|
||||
- GiftCertificateManagement → `useDateRange('currentMonth')` (date-fns import 제거)
|
||||
- [x] 건설 10개 ListClient 적용 (모두 `useDateRange('today')` — UTC 버그도 동시 수정):
|
||||
- ProgressBillingManagementListClient, OrderManagementListClient
|
||||
- ConstructionManagementListClient, IssueManagementListClient
|
||||
- WorkerStatusListClient, PricingListClient
|
||||
- UtilityManagementListClient, SiteManagementListClient
|
||||
- StructureReviewListClient, HandoverReportListClient
|
||||
|
||||
**스킵 (비호환):**
|
||||
- ExpectedExpenseManagement (현재월 ~ 3개월 후 커스텀 범위)
|
||||
- TaxInvoiceIssuance (filters 객체 패턴, 별도 날짜 관리)
|
||||
|
||||
### 4-B: `useAccountingListPage` 훅 — 3개 page.tsx 적용 완료
|
||||
|
||||
**위치**: `src/hooks/useAccountingListPage.ts`
|
||||
**효과**: 50줄 → 20줄 (DEFAULT_PAGINATION + useSearchParams + useEffect + state 통합)
|
||||
|
||||
- [x] `src/hooks/useAccountingListPage.ts` 생성 (DEFAULT_PAGINATION 포함)
|
||||
- [x] `src/hooks/index.ts`에 양쪽 훅 export 추가
|
||||
- [x] 회계 3개 page.tsx 적용:
|
||||
- `accounting/sales/page.tsx` (56줄 → 22줄)
|
||||
- `accounting/deposits/page.tsx` (53줄 → 20줄)
|
||||
- `accounting/withdrawals/page.tsx` (53줄 → 20줄)
|
||||
|
||||
**미적용 (패턴 비호환):**
|
||||
- bills: 추가 searchParams (billType, page, vendorId)
|
||||
- expected-expenses: mode 없음, 커스텀 fetch params
|
||||
- gift-certificates: 리스트 자체 로딩, edit 모드 별도
|
||||
- bad-debt-collection: Promise.all + summary
|
||||
- vendors: total 사용 (pagination 미사용)
|
||||
- tax-invoice-issuance: 복합 mode (edit+id), Promise.all
|
||||
|
||||
### 검증
|
||||
- [x] TypeScript 빌드 에러 없음 확인
|
||||
- [x] 15파일 useDateRange 적용 + 3파일 useAccountingListPage 적용
|
||||
|
||||
---
|
||||
|
||||
## WP-5: 회계 컴포넌트 공통 패턴 추출 ✅ 완료
|
||||
|
||||
**심각도**: 🟢 MEDIUM (코드 품질)
|
||||
**난이도**: 중간 | **신규 파일**: 1개 | **영향 파일**: 5개 컴포넌트
|
||||
**병렬**: Phase 2 — WP-4와 서로 병렬 가능 (겹치는 파일 없음)
|
||||
|
||||
### 5-A: 팩토리 함수 추출 ✅
|
||||
|
||||
**위치**: `src/components/accounting/shared/index.ts` (신규 생성)
|
||||
|
||||
4개 유틸리티 함수 생성 및 적용:
|
||||
|
||||
- [x] `createDeleteItemHandler(deleteFn, setData, msg)` — 4개 컴포넌트 적용 (Sales, Deposit, Withdrawal, Bill)
|
||||
- 7줄 inline handler → 1줄 팩토리 호출 (컴포넌트당 -6줄)
|
||||
- [x] `extractUniqueOptions(data, fieldKey, opts?)` — 5개 컴포넌트 적용 (Sales, Deposit, Withdrawal, Bill, ExpectedExpense)
|
||||
- 4-6줄 useMemo body → 1줄 호출 (컴포넌트당 -3~5줄)
|
||||
- [x] `createDateAmountSortFn(dateField, amountField, direction)` — 3개 컴포넌트 적용 (Sales, Deposit, Withdrawal)
|
||||
- 15줄 switch문 → 1줄 팩토리 호출 (컴포넌트당 -14줄)
|
||||
- Bill은 'maturityDate' 추가 정렬이 있어 제외, ExpectedExpense는 createdAt 정렬이라 제외
|
||||
- [x] `computeMonthlyTotal(data, dateField, amountField)` — 3개 컴포넌트 적용 (Sales, Deposit, Withdrawal)
|
||||
- 6줄 (currentMonth/currentYear + filter + reduce) → 1줄 (컴포넌트당 -5줄)
|
||||
|
||||
### 5-B: 스코프 조정 (계획 대비)
|
||||
|
||||
**적용하지 않은 항목과 사유:**
|
||||
- `createFilterFn` — 각 컴포넌트의 customFilterFn이 너무 상이 (필드명, 조건문 구조 다름). 추상화 비용 > 이점
|
||||
- `AccountSubjectSavePanel` — UI+로직이 각 컴포넌트마다 미묘하게 다름 (Withdrawal은 배열, Deposit은 Set). 현재 수준으로 충분
|
||||
- `useRefreshableData` — Deposit/Withdrawal 2곳만 사용. 훅 생성 오버헤드가 이점을 초과
|
||||
|
||||
### 검증
|
||||
- [x] TypeScript 빌드 에러 없음 (`npx tsc --noEmit` 통과)
|
||||
- [x] 삭제/정렬/필터 로직이 기존과 동일한 동작 보장 (동일한 switch/filter 구현을 shared로 이동)
|
||||
|
||||
### 정량 결과
|
||||
- **삭제된 중복 코드**: ~120줄 (5파일 × 평균 24줄/파일)
|
||||
- **신규 공통 코드**: ~130줄 (shared/index.ts)
|
||||
- **순 효과**: 중복 제거 + 단일 소스 오브 트루스 확보
|
||||
|
||||
---
|
||||
|
||||
## WP-6: toISOString UTC 버그 전환 ✅ 완료 (2026-02-19)
|
||||
|
||||
**심각도**: 🟢 MEDIUM (잠재적 타임존 버그)
|
||||
**난이도**: 중간 | **파일 수**: 15파일 26건 | **예상 변경량**: 각 1-2줄
|
||||
**병렬**: Phase 3 — WP-4의 useDateRange가 먼저 필요 (일부 파일 중복)
|
||||
|
||||
### 현황
|
||||
`new Date().toISOString().split('T')[0]`은 UTC 기준이라 KST 00:00~08:59에 전날 날짜 반환.
|
||||
`src/lib/utils/date.ts`에 `getTodayString()`/`getLocalDateString()` 이미 존재.
|
||||
|
||||
### 수정 완료
|
||||
|
||||
**건설관리 10파일 — WP-4에서 useDateRange('today')로 이미 수정됨 (Phase 2에서 완료)**
|
||||
|
||||
**getTodayString() 적용 (7파일, 11건):**
|
||||
- [x] `quotes/QuoteTransactionModal.tsx` — const today 초기화 (1건)
|
||||
- [x] `vehicle-management/VehicleLogList/actions.ts` — writeDate + createdAt 기본값 (2건)
|
||||
- [x] `pricing-table-management/actions.ts` — create/update changedDate (2건)
|
||||
- [x] `pricing-table-management/PricingTableForm.tsx` — 변경일 Input value (1건)
|
||||
- [x] `material/ReceivingManagement/actions.ts` — reportDate ×2 + today 변수 (3건)
|
||||
- [x] `material/ReceivingManagement/ReceivingDetail.tsx` — adjustmentDate (1건)
|
||||
- [x] `accounting/TaxInvoiceManagement/ManualEntryModal.tsx` — writeDate 기본값 (1건)
|
||||
|
||||
**getLocalDateString() 적용 (7파일, 13건):**
|
||||
- [x] `pricing-distribution/PriceDistributionList.tsx` — startDate + endDate (2건)
|
||||
- [x] `pricing-distribution/actions.ts` — createdAt + effectiveDate (2건)
|
||||
- [x] `material/StockStatus/StockStatusList.tsx` — startDate + endDate (2건)
|
||||
- [x] `quality/InspectionManagement/InspectionList.tsx` — startDate + endDate (2건)
|
||||
- [x] `material/ReceivingManagement/ReceivingList.tsx` — startDate + endDate (2건)
|
||||
- [x] `outbound/ShipmentManagement/ShipmentList.tsx` — startDate + endDate (2건)
|
||||
- [x] `outbound/VehicleDispatchManagement/VehicleDispatchList.tsx` — startDate + endDate (2건)
|
||||
|
||||
**both 적용 (1파일, 2건):**
|
||||
- [x] `accounting/TaxInvoiceManagement/CardHistoryModal.tsx` — today (getTodayString) + monthAgo (getLocalDateString)
|
||||
|
||||
### 검증
|
||||
- [x] `npx tsc --noEmit` 통과 — TypeScript 에러 0건
|
||||
- [x] `src/` 내 잔여 `.toISOString().split('T')[0]` = 0건 (date.ts 주석 1건 제외)
|
||||
|
||||
---
|
||||
|
||||
## 병렬 실행 계획 요약
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Phase 1 (독립 — 동시 3개 병렬) │
|
||||
│ │
|
||||
│ WP-1 날짜 하드코딩 WP-2 format 통일 WP-3 edit 리다이렉트 │
|
||||
│ (3파일, ~10분) (35파일, ~40분) (6파일, ~20분) │
|
||||
│ ↓ ↓ ↓ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Phase 2 (Phase 1 완료 후 — 동시 2개 병렬) │
|
||||
│ │
|
||||
│ WP-4 공통 훅 추출 WP-5 컴포넌트 공통화 │
|
||||
│ (신규 2-3파일 + 70파일 적용) (신규 3-5파일 + 7파일 적용) │
|
||||
│ ↓ ↓ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Phase 3 (Phase 2 완료 후) │
|
||||
│ │
|
||||
│ WP-6 toISOString UTC 버그 전환 │
|
||||
│ (60+파일, 가장 큰 작업) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Phase별 병렬 가능 근거
|
||||
|
||||
| Phase | WP 조합 | 병렬 가능 이유 |
|
||||
|-------|---------|---------------|
|
||||
| 1 | WP-1 + WP-2 + WP-3 | 수정 파일이 완전히 겹치지 않음. 기존 유틸만 사용, 신규 생성 없음 |
|
||||
| 2 | WP-4 + WP-5 | WP-4는 hooks/ + page.tsx, WP-5는 components/accounting/shared/ + 컴포넌트 내부. 파일 겹침 없음 |
|
||||
| 3 | WP-6 단독 | WP-4의 useDateRange 훅을 일부 파일에서 활용해야 하므로 Phase 2 이후 |
|
||||
|
||||
### Phase 간 의존성 상세
|
||||
|
||||
| 의존 관계 | 이유 |
|
||||
|-----------|------|
|
||||
| WP-4 → WP-1 | WP-1에서 수정한 날짜 초기값 패턴을 useDateRange 훅 설계에 반영 |
|
||||
| WP-5 → WP-2 | WP-2에서 통일된 formatAmount를 공통 컴포넌트에서 import |
|
||||
| WP-6 → WP-4 | useDateRange가 getTodayString()을 내부 사용하므로 훅 완성 후 적용 |
|
||||
@@ -571,6 +571,7 @@ claudedocs/
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[IMPL-2026-02-09] phase1-common-hooks-checklist.md` | Phase 1 공통 훅 추출 체크리스트 (완료) + Phase 3 프로토타입 기록 |
|
||||
| `[REF-2026-02-19] code-dedup-commonization-checklist.md` | 코드 중복 제거 및 공통화 체크리스트 (6 WP, 3 Phase 병렬 실행 계획) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,49 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { DepositManagement } from '@/components/accounting/DepositManagement';
|
||||
import DepositDetailClientV2 from '@/components/accounting/DepositManagement/DepositDetailClientV2';
|
||||
import { getDeposits } from '@/components/accounting/DepositManagement/actions';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
perPage: 100,
|
||||
total: 0,
|
||||
};
|
||||
import { useAccountingListPage } from '@/hooks';
|
||||
|
||||
export default function DepositsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getDeposits>>['data']>([]);
|
||||
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// mode=new일 때는 데이터 로드 불필요
|
||||
if (mode === 'new') {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
getDeposits({ perPage: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [mode]);
|
||||
const { data, pagination, isLoading, mode } = useAccountingListPage(
|
||||
() => getDeposits({ perPage: 100 })
|
||||
);
|
||||
|
||||
if (mode === 'new') return <DepositDetailClientV2 initialMode="create" />;
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
|
||||
// mode=new일 때 등록 화면 표시
|
||||
if (mode === 'new') {
|
||||
return <DepositDetailClientV2 initialMode="create" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DepositManagement
|
||||
initialData={data}
|
||||
|
||||
@@ -1,50 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { SalesManagement } from '@/components/accounting/SalesManagement';
|
||||
import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail';
|
||||
import { getSales } from '@/components/accounting/SalesManagement/actions';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
perPage: 100,
|
||||
total: 0,
|
||||
};
|
||||
import { useAccountingListPage } from '@/hooks';
|
||||
|
||||
export default function SalesPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getSales>>['data']>([]);
|
||||
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// mode=new일 때는 데이터 로드 불필요
|
||||
if (mode === 'new') {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
getSales({ perPage: 100 })
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [mode]);
|
||||
|
||||
// mode=new일 때 등록 화면 표시
|
||||
if (mode === 'new') {
|
||||
return <SalesDetail mode="new" />;
|
||||
}
|
||||
const { data, pagination, isLoading, mode } = useAccountingListPage(
|
||||
() => getSales({ perPage: 100 })
|
||||
);
|
||||
|
||||
if (mode === 'new') return <SalesDetail mode="new" />;
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,49 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { WithdrawalManagement } from '@/components/accounting/WithdrawalManagement';
|
||||
import WithdrawalDetailClientV2 from '@/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2';
|
||||
import { getWithdrawals } from '@/components/accounting/WithdrawalManagement/actions';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
perPage: 100,
|
||||
total: 0,
|
||||
};
|
||||
import { useAccountingListPage } from '@/hooks';
|
||||
|
||||
export default function WithdrawalsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getWithdrawals>>['data']>([]);
|
||||
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// mode=new일 때는 데이터 로드 불필요
|
||||
if (mode === 'new') {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
getWithdrawals({ perPage: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [mode]);
|
||||
const { data, pagination, isLoading, mode } = useAccountingListPage(
|
||||
() => getWithdrawals({ perPage: 100 })
|
||||
);
|
||||
|
||||
if (mode === 'new') return <WithdrawalDetailClientV2 initialMode="create" />;
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
|
||||
// mode=new일 때 등록 화면 표시
|
||||
if (mode === 'new') {
|
||||
return <WithdrawalDetailClientV2 initialMode="create" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<WithdrawalManagement
|
||||
initialData={data}
|
||||
|
||||
@@ -1,47 +1,24 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 지게차 수정 페이지
|
||||
* 지게차 수정 페이지 → 상세 페이지로 리다이렉트
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ForkliftDetail } from '@/components/vehicle-management/ForkliftDetail';
|
||||
import { getForkliftById } from '@/components/vehicle-management/ForkliftList/actions';
|
||||
import type { Forklift } from '@/components/vehicle-management/types';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
|
||||
export default function ForkliftEditPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
const [data, setData] = useState<Forklift | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
router.replace(`/vehicle-management/forklift/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
getForkliftById(id)
|
||||
.then((result) => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError(result.error || '데이터를 불러올 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-red-500">{error || '지게차를 찾을 수 없습니다.'}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <ForkliftDetail mode="edit" initialData={data} id={id} />;
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 지게차 상세 페이지
|
||||
* 지게차 상세/수정 페이지
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { ForkliftDetail } from '@/components/vehicle-management/ForkliftDetail';
|
||||
import { getForkliftById } from '@/components/vehicle-management/ForkliftList/actions';
|
||||
import type { Forklift } from '@/components/vehicle-management/types';
|
||||
@@ -14,6 +14,8 @@ import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
export default function ForkliftDetailPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
const [data, setData] = useState<Forklift | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -43,5 +45,5 @@ export default function ForkliftDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
return <ForkliftDetail mode="view" initialData={data} id={id} />;
|
||||
return <ForkliftDetail mode={mode} initialData={data} id={id} />;
|
||||
}
|
||||
|
||||
@@ -1,47 +1,24 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 차량일지 수정 페이지
|
||||
* 차량일지 수정 페이지 → 상세 페이지로 리다이렉트
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { VehicleLogDetail } from '@/components/vehicle-management/VehicleLogDetail';
|
||||
import { getVehicleLogById } from '@/components/vehicle-management/VehicleLogList/actions';
|
||||
import type { VehicleLog } from '@/components/vehicle-management/types';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
|
||||
export default function VehicleLogEditPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
const [data, setData] = useState<VehicleLog | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
router.replace(`/vehicle-management/vehicle-log/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
getVehicleLogById(id)
|
||||
.then((result) => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError(result.error || '데이터를 불러올 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-red-500">{error || '차량일지를 찾을 수 없습니다.'}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <VehicleLogDetail mode="edit" initialData={data} id={id} />;
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 차량일지 상세 페이지
|
||||
* 차량일지 상세/수정 페이지
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { VehicleLogDetail } from '@/components/vehicle-management/VehicleLogDetail';
|
||||
import { getVehicleLogById } from '@/components/vehicle-management/VehicleLogList/actions';
|
||||
import type { VehicleLog } from '@/components/vehicle-management/types';
|
||||
@@ -14,6 +14,8 @@ import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
export default function VehicleLogDetailPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
const [data, setData] = useState<VehicleLog | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -43,5 +45,5 @@ export default function VehicleLogDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
return <VehicleLogDetail mode="view" initialData={data} id={id} />;
|
||||
return <VehicleLogDetail mode={mode} initialData={data} id={id} />;
|
||||
}
|
||||
|
||||
@@ -1,47 +1,24 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 차량 수정 페이지
|
||||
* 차량 수정 페이지 → 상세 페이지로 리다이렉트
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { VehicleDetail } from '@/components/vehicle-management/VehicleDetail';
|
||||
import { getVehicleById } from '@/components/vehicle-management/VehicleList/actions';
|
||||
import type { Vehicle } from '@/components/vehicle-management/types';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
|
||||
export default function VehicleEditPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
const [data, setData] = useState<Vehicle | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
router.replace(`/vehicle-management/vehicle/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
getVehicleById(id)
|
||||
.then((result) => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError(result.error || '데이터를 불러올 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-red-500">{error || '차량을 찾을 수 없습니다.'}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <VehicleDetail mode="edit" initialData={data} id={id} />;
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 차량 상세 페이지
|
||||
* 차량 상세/수정 페이지
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { VehicleDetail } from '@/components/vehicle-management/VehicleDetail';
|
||||
import { getVehicleById } from '@/components/vehicle-management/VehicleList/actions';
|
||||
import type { Vehicle } from '@/components/vehicle-management/types';
|
||||
@@ -14,6 +14,8 @@ import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
export default function VehicleDetailPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
const [data, setData] = useState<Vehicle | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -43,5 +45,5 @@ export default function VehicleDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
return <VehicleDetail mode="view" initialData={data} id={id} />;
|
||||
return <VehicleDetail mode={mode} initialData={data} id={id} />;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import {
|
||||
FileText,
|
||||
Plus,
|
||||
@@ -107,8 +108,7 @@ export function BillManagementClient({
|
||||
});
|
||||
|
||||
// 날짜 범위 상태
|
||||
const [startDate, setStartDate] = useState('2025-09-01');
|
||||
const [endDate, setEndDate] = useState('2025-09-03');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
|
||||
// ===== API 데이터 로드 =====
|
||||
const loadData = useCallback(async (page: number = 1) => {
|
||||
|
||||
@@ -15,6 +15,8 @@ import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
import { getBills, deleteBill, updateBillStatus } from './actions';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import { createDeleteItemHandler, extractUniqueOptions } from '../shared';
|
||||
import {
|
||||
FileText,
|
||||
Plus,
|
||||
@@ -84,8 +86,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// 날짜 범위
|
||||
const [startDate, setStartDate] = useState('2025-09-01');
|
||||
const [endDate, setEndDate] = useState('2025-09-03');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
|
||||
// 인라인 필터 상태
|
||||
const [billTypeFilter, setBillTypeFilter] = useState<string>(initialBillType || 'received');
|
||||
@@ -163,13 +164,10 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem
|
||||
}, [billData, sortOption]);
|
||||
|
||||
// 거래처 목록 (필터용)
|
||||
const vendorOptions = useMemo(() => {
|
||||
const uniqueVendors = [...new Set(billData.map(d => d.vendorName).filter(v => v))];
|
||||
return [
|
||||
{ value: 'all', label: '전체' },
|
||||
...uniqueVendors.map(v => ({ value: v, label: v }))
|
||||
];
|
||||
}, [billData]);
|
||||
const vendorOptions = useMemo(
|
||||
() => extractUniqueOptions(billData, 'vendorName'),
|
||||
[billData]
|
||||
);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback((item: BillRecord) => {
|
||||
@@ -245,14 +243,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem
|
||||
totalCount: pagination.total,
|
||||
};
|
||||
},
|
||||
deleteItem: async (id: string) => {
|
||||
const result = await deleteBill(id);
|
||||
if (result.success) {
|
||||
toast.success('어음이 삭제되었습니다.');
|
||||
setBillData(prev => prev.filter(item => item.id !== id));
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
},
|
||||
deleteItem: createDeleteItemHandler(deleteBill, setBillData, '어음이 삭제되었습니다.'),
|
||||
},
|
||||
|
||||
// 테이블 컬럼
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { formatNumber as formatAmount } from '@/lib/utils/amount';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { NoteReceivableItem, DailyAccountItem } from './types';
|
||||
import { MATCH_STATUS_LABELS, MATCH_STATUS_COLORS } from './types';
|
||||
@@ -95,12 +96,6 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
}
|
||||
}, [selectedDate, loadData]);
|
||||
|
||||
// ===== 금액 포맷 =====
|
||||
const formatAmount = useCallback((amount: number) => {
|
||||
if (amount === 0) return '0';
|
||||
return amount.toLocaleString();
|
||||
}, []);
|
||||
|
||||
// ===== 어음 합계 (API 요약 사용) =====
|
||||
const noteReceivableTotal = useMemo(() => {
|
||||
return summary?.noteReceivableTotal ?? noteReceivables.reduce((sum, item) => sum + item.currentBalance, 0);
|
||||
|
||||
@@ -71,6 +71,14 @@ import {
|
||||
} from './types';
|
||||
import { deleteDeposit, updateDepositTypes, getDeposits } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import {
|
||||
createDeleteItemHandler,
|
||||
extractUniqueOptions,
|
||||
createDateAmountSortFn,
|
||||
computeMonthlyTotal,
|
||||
type SortDirection,
|
||||
} from '../shared';
|
||||
|
||||
// ===== 테이블 컬럼 정의 =====
|
||||
const tableColumns = [
|
||||
@@ -98,8 +106,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
const router = useRouter();
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [startDate, setStartDate] = useState('2025-09-01');
|
||||
const [endDate, setEndDate] = useState('2025-09-03');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
const [depositData, setDepositData] = useState<DepositRecord[]>(initialData);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -120,27 +127,17 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
// ===== 통계 계산 =====
|
||||
const stats = useMemo(() => {
|
||||
const totalDeposit = depositData.reduce((sum, d) => sum + (d.depositAmount ?? 0), 0);
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentYear = new Date().getFullYear();
|
||||
const monthlyDeposit = depositData
|
||||
.filter(d => {
|
||||
const date = new Date(d.depositDate);
|
||||
return date.getMonth() === currentMonth && date.getFullYear() === currentYear;
|
||||
})
|
||||
.reduce((sum, d) => sum + (d.depositAmount ?? 0), 0);
|
||||
const monthlyDeposit = computeMonthlyTotal(depositData, 'depositDate', 'depositAmount');
|
||||
const vendorUnsetCount = depositData.filter(d => !d.vendorName).length;
|
||||
const depositTypeUnsetCount = depositData.filter(d => d.depositType === 'unset').length;
|
||||
return { totalDeposit, monthlyDeposit, vendorUnsetCount, depositTypeUnsetCount };
|
||||
}, [depositData]);
|
||||
|
||||
// ===== 거래처 목록 (필터용) =====
|
||||
const vendorOptions = useMemo(() => {
|
||||
const uniqueVendors = [...new Set(depositData.map(d => d.vendorName).filter(v => v))];
|
||||
return [
|
||||
{ value: 'all', label: '전체' },
|
||||
...uniqueVendors.map(v => ({ value: v, label: v }))
|
||||
];
|
||||
}, [depositData]);
|
||||
const vendorOptions = useMemo(
|
||||
() => extractUniqueOptions(depositData, 'vendorName'),
|
||||
[depositData]
|
||||
);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback((item: DepositRecord) => {
|
||||
@@ -224,14 +221,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
totalCount: initialData.length,
|
||||
};
|
||||
},
|
||||
deleteItem: async (id: string) => {
|
||||
const result = await deleteDeposit(id);
|
||||
if (result.success) {
|
||||
setDepositData(prev => prev.filter(item => item.id !== id));
|
||||
toast.success('입금 내역이 삭제되었습니다.');
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
},
|
||||
deleteItem: createDeleteItemHandler(deleteDeposit, setDepositData, '입금 내역이 삭제되었습니다.'),
|
||||
},
|
||||
|
||||
// 테이블 컬럼
|
||||
@@ -284,24 +274,8 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
},
|
||||
|
||||
// 커스텀 정렬 함수
|
||||
customSortFn: (items, filterValues) => {
|
||||
const sorted = [...items];
|
||||
switch (sortOption) {
|
||||
case 'oldest':
|
||||
sorted.sort((a, b) => new Date(a.depositDate).getTime() - new Date(b.depositDate).getTime());
|
||||
break;
|
||||
case 'amountHigh':
|
||||
sorted.sort((a, b) => (b.depositAmount ?? 0) - (a.depositAmount ?? 0));
|
||||
break;
|
||||
case 'amountLow':
|
||||
sorted.sort((a, b) => (a.depositAmount ?? 0) - (b.depositAmount ?? 0));
|
||||
break;
|
||||
default: // latest
|
||||
sorted.sort((a, b) => new Date(b.depositDate).getTime() - new Date(a.depositDate).getTime());
|
||||
break;
|
||||
}
|
||||
return sorted;
|
||||
},
|
||||
customSortFn: (items) =>
|
||||
createDateAmountSortFn<DepositRecord>('depositDate', 'depositAmount', sortOption as SortDirection)(items),
|
||||
|
||||
// 검색창 (공통 컴포넌트에서 자동 생성)
|
||||
hideSearch: true,
|
||||
|
||||
@@ -90,6 +90,7 @@ import {
|
||||
PAYMENT_STATUS_FILTER_OPTIONS,
|
||||
ACCOUNT_SUBJECT_OPTIONS,
|
||||
} from './types';
|
||||
import { extractUniqueOptions } from '../shared';
|
||||
|
||||
// ===== 테이블 행 타입 (데이터 + 그룹 헤더 + 소계) =====
|
||||
type RowType = 'data' | 'monthHeader' | 'monthSubtotal' | 'totalExpense' | 'expectedBalance' | 'finalBalance';
|
||||
@@ -296,13 +297,10 @@ export function ExpectedExpenseManagement({
|
||||
}, []);
|
||||
|
||||
// ===== 거래처 필터 옵션 (데이터에서 동적 추출) =====
|
||||
const vendorFilterOptions = useMemo(() => {
|
||||
const vendors = [...new Set(data.map(item => item.vendorName).filter(Boolean))];
|
||||
return [
|
||||
{ value: 'all', label: '전체' },
|
||||
...vendors.map(vendor => ({ value: vendor, label: vendor }))
|
||||
];
|
||||
}, [data]);
|
||||
const vendorFilterOptions = useMemo(
|
||||
() => extractUniqueOptions(data, 'vendorName'),
|
||||
[data]
|
||||
);
|
||||
|
||||
// ===== 필터링된 원본 데이터 =====
|
||||
const filteredRawData = useMemo(() => {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format, startOfMonth, endOfMonth } from 'date-fns';
|
||||
import {
|
||||
Gift, PlusCircle,
|
||||
} from 'lucide-react';
|
||||
@@ -47,6 +46,8 @@ import {
|
||||
getGiftCertificateSummary,
|
||||
} from './actions';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { formatNumber as formatAmount } from '@/lib/utils/amount';
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
// ===== 테이블 컬럼 정의 (체크박스/No. 제외) =====
|
||||
const tableColumns = [
|
||||
@@ -86,10 +87,7 @@ export function GiftCertificateManagement() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// 날짜 범위
|
||||
const [startDate, setStartDate] = useState(() => format(startOfMonth(new Date()), 'yyyy-MM-dd'));
|
||||
const [endDate, setEndDate] = useState(() => format(endOfMonth(new Date()), 'yyyy-MM-dd'));
|
||||
|
||||
const formatAmount = (amount: number) => amount.toLocaleString('ko-KR');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentMonth');
|
||||
|
||||
// ===== 데이터 로드 =====
|
||||
const loadData = useCallback(async () => {
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
} from './actions';
|
||||
import { getClients } from '../VendorManagement/actions';
|
||||
import { toast } from 'sonner';
|
||||
import { formatNumber as formatAmount } from '@/lib/utils/amount';
|
||||
|
||||
interface PurchaseDetailProps {
|
||||
purchaseId: string;
|
||||
@@ -147,11 +148,6 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
// ===== 금액 포맷 =====
|
||||
const formatAmount = (amount: number): string => {
|
||||
return amount.toLocaleString();
|
||||
};
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleVendorChange = useCallback((clientId: string) => {
|
||||
const client = clients.find(c => c.id === clientId);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Receipt,
|
||||
@@ -81,8 +82,7 @@ export function PurchaseManagement() {
|
||||
const router = useRouter();
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [startDate, setStartDate] = useState('2025-01-01');
|
||||
const [endDate, setEndDate] = useState('2025-12-31');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
const [purchaseData, setPurchaseData] = useState<PurchaseRecord[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const isInitialLoadDone = useRef(false);
|
||||
|
||||
@@ -48,6 +48,7 @@ import { SALES_TYPE_OPTIONS } from './types';
|
||||
import { getSaleById, createSale, updateSale, deleteSale } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import { getClients } from '../VendorManagement/actions';
|
||||
import { formatNumber as formatAmount } from '@/lib/utils/amount';
|
||||
|
||||
// ===== Props =====
|
||||
interface SalesDetailProps {
|
||||
@@ -265,11 +266,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
||||
}
|
||||
}, [selectedVendor]);
|
||||
|
||||
// ===== 금액 포맷 =====
|
||||
const formatAmount = (amount: number): string => {
|
||||
return amount.toLocaleString();
|
||||
};
|
||||
|
||||
// ===== 폼 내용 렌더링 =====
|
||||
const renderFormContent = () => (
|
||||
<>
|
||||
|
||||
@@ -63,6 +63,14 @@ import {
|
||||
ACCOUNT_SUBJECT_SELECTOR_OPTIONS,
|
||||
} from './types';
|
||||
import { getSales, deleteSale, toggleSaleIssuance } from './actions';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import {
|
||||
createDeleteItemHandler,
|
||||
extractUniqueOptions,
|
||||
createDateAmountSortFn,
|
||||
computeMonthlyTotal,
|
||||
type SortDirection,
|
||||
} from '../shared';
|
||||
|
||||
// ===== 테이블 컬럼 정의 =====
|
||||
const tableColumns = [
|
||||
@@ -93,9 +101,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
const router = useRouter();
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [startDate, setStartDate] = useState(`${currentYear}-01-01`);
|
||||
const [endDate, setEndDate] = useState(`${currentYear}-12-31`);
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
const [salesData, setSalesData] = useState<SalesRecord[]>(initialData || []);
|
||||
const [pagination, setPagination] = useState(initialPagination);
|
||||
const [currentPage, setCurrentPage] = useState(initialPagination.currentPage);
|
||||
@@ -118,24 +124,17 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
// ===== 통계 계산 =====
|
||||
const stats = useMemo(() => {
|
||||
const totalSalesAmount = salesData.reduce((sum, d) => sum + d.totalAmount, 0);
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentYear = new Date().getFullYear();
|
||||
const monthlyAmount = salesData
|
||||
.filter(d => {
|
||||
const date = new Date(d.salesDate);
|
||||
return date.getMonth() === currentMonth && date.getFullYear() === currentYear;
|
||||
})
|
||||
.reduce((sum, d) => sum + d.totalAmount, 0);
|
||||
const monthlyAmount = computeMonthlyTotal(salesData, 'salesDate', 'totalAmount');
|
||||
const taxInvoicePendingCount = salesData.filter(d => !d.taxInvoiceIssued).length;
|
||||
const transactionStatementPendingCount = salesData.filter(d => !d.transactionStatementIssued).length;
|
||||
return { totalSalesAmount, monthlyAmount, taxInvoicePendingCount, transactionStatementPendingCount };
|
||||
}, [salesData]);
|
||||
|
||||
// ===== 거래처 목록 (필터용) =====
|
||||
const vendorOptions = useMemo(() => {
|
||||
const uniqueVendors = [...new Set(salesData.map(d => d.vendorName))].filter(v => v && v.trim() !== '');
|
||||
return uniqueVendors.map(v => ({ value: v, label: v }));
|
||||
}, [salesData]);
|
||||
const vendorOptions = useMemo(
|
||||
() => extractUniqueOptions(salesData, 'vendorName', { includeAll: false }),
|
||||
[salesData]
|
||||
);
|
||||
|
||||
// ===== filterConfig 정의 =====
|
||||
const filterConfig: FilterFieldConfig[] = useMemo(() => [
|
||||
@@ -305,14 +304,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
}
|
||||
return { success: true, data: salesData, totalCount: salesData.length };
|
||||
},
|
||||
deleteItem: async (id: string) => {
|
||||
const result = await deleteSale(id);
|
||||
if (result.success) {
|
||||
setSalesData(prev => prev.filter(item => item.id !== id));
|
||||
toast.success('매출이 삭제되었습니다.');
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
},
|
||||
deleteItem: createDeleteItemHandler(deleteSale, setSalesData, '매출이 삭제되었습니다.'),
|
||||
},
|
||||
|
||||
// 테이블 컬럼
|
||||
@@ -374,25 +366,8 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
},
|
||||
|
||||
// 커스텀 정렬 함수
|
||||
customSortFn: (items, fv) => {
|
||||
const sorted = [...items];
|
||||
const sortVal = fv.sort as string;
|
||||
switch (sortVal) {
|
||||
case 'oldest':
|
||||
sorted.sort((a, b) => new Date(a.salesDate).getTime() - new Date(b.salesDate).getTime());
|
||||
break;
|
||||
case 'amountHigh':
|
||||
sorted.sort((a, b) => b.totalAmount - a.totalAmount);
|
||||
break;
|
||||
case 'amountLow':
|
||||
sorted.sort((a, b) => a.totalAmount - b.totalAmount);
|
||||
break;
|
||||
default: // latest
|
||||
sorted.sort((a, b) => new Date(b.salesDate).getTime() - new Date(a.salesDate).getTime());
|
||||
break;
|
||||
}
|
||||
return sorted;
|
||||
},
|
||||
customSortFn: (items, fv) =>
|
||||
createDateAmountSortFn<SalesRecord>('salesDate', 'totalAmount', (fv.sort as SortDirection) ?? 'latest')(items),
|
||||
|
||||
// 검색창 (공통 컴포넌트에서 자동 생성)
|
||||
hideSearch: true,
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { getTodayString, getLocalDateString } from '@/lib/utils/date';
|
||||
import { getCardHistory } from './actions';
|
||||
import type { CardHistoryRecord } from './types';
|
||||
|
||||
@@ -44,10 +45,8 @@ export function CardHistoryModal({
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
}: CardHistoryModalProps) {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
||||
.toISOString()
|
||||
.split('T')[0];
|
||||
const today = getTodayString();
|
||||
const monthAgo = getLocalDateString(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000));
|
||||
|
||||
const [startDate, setStartDate] = useState(monthAgo);
|
||||
const [endDate, setEndDate] = useState(today);
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { getTodayString } from '@/lib/utils/date';
|
||||
import { createTaxInvoice } from './actions';
|
||||
import { CardHistoryModal } from './CardHistoryModal';
|
||||
import type { InvoiceTab, TaxType, ManualEntryFormData, CardHistoryRecord } from './types';
|
||||
@@ -46,7 +47,7 @@ interface ManualEntryModalProps {
|
||||
|
||||
const initialFormData: ManualEntryFormData = {
|
||||
division: 'sales',
|
||||
writeDate: new Date().toISOString().split('T')[0],
|
||||
writeDate: getTodayString(),
|
||||
vendorName: '',
|
||||
vendorBusinessNumber: '',
|
||||
supplyAmount: 0,
|
||||
|
||||
@@ -70,6 +70,14 @@ import {
|
||||
} from './types';
|
||||
import { deleteWithdrawal, updateWithdrawalTypes, getWithdrawals } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import {
|
||||
createDeleteItemHandler,
|
||||
extractUniqueOptions,
|
||||
createDateAmountSortFn,
|
||||
computeMonthlyTotal,
|
||||
type SortDirection,
|
||||
} from '../shared';
|
||||
|
||||
// ===== 테이블 컬럼 정의 =====
|
||||
const tableColumns = [
|
||||
@@ -100,8 +108,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
const [withdrawalData, setWithdrawalData] = useState<WithdrawalRecord[]>(initialData);
|
||||
|
||||
// 날짜 범위
|
||||
const [startDate, setStartDate] = useState('2025-09-01');
|
||||
const [endDate, setEndDate] = useState('2025-09-03');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
|
||||
// 인라인 필터 상태
|
||||
const [vendorFilter, setVendorFilter] = useState<string>('all');
|
||||
@@ -125,16 +132,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
// ===== Stats 계산 =====
|
||||
const stats = useMemo(() => {
|
||||
const totalWithdrawal = withdrawalData.reduce((sum, d) => sum + (d.withdrawalAmount ?? 0), 0);
|
||||
|
||||
// 당월 출금
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentYear = new Date().getFullYear();
|
||||
const monthlyWithdrawal = withdrawalData
|
||||
.filter(d => {
|
||||
const date = new Date(d.withdrawalDate);
|
||||
return date.getMonth() === currentMonth && date.getFullYear() === currentYear;
|
||||
})
|
||||
.reduce((sum, d) => sum + (d.withdrawalAmount ?? 0), 0);
|
||||
const monthlyWithdrawal = computeMonthlyTotal(withdrawalData, 'withdrawalDate', 'withdrawalAmount');
|
||||
|
||||
// 거래처 미설정 건수
|
||||
const vendorUnsetCount = withdrawalData.filter(d => !d.vendorName).length;
|
||||
@@ -146,13 +144,10 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
}, [withdrawalData]);
|
||||
|
||||
// 거래처 목록 (필터용)
|
||||
const vendorOptions = useMemo(() => {
|
||||
const uniqueVendors = [...new Set(withdrawalData.map(d => d.vendorName).filter(v => v))];
|
||||
return [
|
||||
{ value: 'all', label: '전체' },
|
||||
...uniqueVendors.map(v => ({ value: v, label: v }))
|
||||
];
|
||||
}, [withdrawalData]);
|
||||
const vendorOptions = useMemo(
|
||||
() => extractUniqueOptions(withdrawalData, 'vendorName'),
|
||||
[withdrawalData]
|
||||
);
|
||||
|
||||
// ===== 테이블 합계 계산 =====
|
||||
const tableTotals = useMemo(() => {
|
||||
@@ -240,14 +235,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
totalCount: initialData.length,
|
||||
};
|
||||
},
|
||||
deleteItem: async (id: string) => {
|
||||
const result = await deleteWithdrawal(id);
|
||||
if (result.success) {
|
||||
toast.success('출금 내역이 삭제되었습니다.');
|
||||
setWithdrawalData(prev => prev.filter(item => item.id !== id));
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
},
|
||||
deleteItem: createDeleteItemHandler(deleteWithdrawal, setWithdrawalData, '출금 내역이 삭제되었습니다.'),
|
||||
},
|
||||
|
||||
// 테이블 컬럼
|
||||
@@ -331,24 +319,8 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
},
|
||||
|
||||
// 커스텀 정렬 함수
|
||||
customSortFn: (items) => {
|
||||
const sorted = [...items];
|
||||
switch (sortOption) {
|
||||
case 'oldest':
|
||||
sorted.sort((a, b) => new Date(a.withdrawalDate).getTime() - new Date(b.withdrawalDate).getTime());
|
||||
break;
|
||||
case 'amountHigh':
|
||||
sorted.sort((a, b) => b.withdrawalAmount - a.withdrawalAmount);
|
||||
break;
|
||||
case 'amountLow':
|
||||
sorted.sort((a, b) => a.withdrawalAmount - b.withdrawalAmount);
|
||||
break;
|
||||
default: // latest
|
||||
sorted.sort((a, b) => new Date(b.withdrawalDate).getTime() - new Date(a.withdrawalDate).getTime());
|
||||
break;
|
||||
}
|
||||
return sorted;
|
||||
},
|
||||
customSortFn: (items) =>
|
||||
createDateAmountSortFn<WithdrawalRecord>('withdrawalDate', 'withdrawalAmount', sortOption as SortDirection)(items),
|
||||
|
||||
// 검색창 (공통 컴포넌트에서 자동 생성)
|
||||
searchValue: searchQuery,
|
||||
|
||||
131
src/components/accounting/shared/index.ts
Normal file
131
src/components/accounting/shared/index.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 회계 컴포넌트 공통 패턴
|
||||
*
|
||||
* SalesManagement, DepositManagement, WithdrawalManagement, BillManagement 등
|
||||
* 회계 목록 컴포넌트에서 반복되는 로직을 추출한 유틸리티 모음
|
||||
*/
|
||||
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// ===== 1. 삭제 핸들러 팩토리 (4파일 동일) =====
|
||||
|
||||
/**
|
||||
* UniversalListPage의 deleteItem 핸들러를 생성합니다.
|
||||
*
|
||||
* @example
|
||||
* deleteItem: createDeleteItemHandler(deleteSale, setSalesData, '매출이 삭제되었습니다.'),
|
||||
*/
|
||||
export function createDeleteItemHandler<T extends { id: string }>(
|
||||
deleteAction: (id: string) => Promise<{ success: boolean; error?: string }>,
|
||||
setData: React.Dispatch<React.SetStateAction<T[]>>,
|
||||
successMessage: string
|
||||
) {
|
||||
return async (id: string) => {
|
||||
const result = await deleteAction(id);
|
||||
if (result.success) {
|
||||
setData(prev => prev.filter(item => item.id !== id));
|
||||
toast.success(successMessage);
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 2. 거래처 옵션 추출 (5파일 동일) =====
|
||||
|
||||
/**
|
||||
* 데이터 배열에서 고유한 값을 추출하여 Select 옵션으로 변환합니다.
|
||||
*
|
||||
* @example
|
||||
* const vendorOptions = useMemo(() => extractUniqueOptions(data, 'vendorName'), [data]);
|
||||
*/
|
||||
export function extractUniqueOptions<T>(
|
||||
data: T[],
|
||||
fieldKey: keyof T,
|
||||
options?: { includeAll?: boolean; allLabel?: string }
|
||||
): { value: string; label: string }[] {
|
||||
const { includeAll = true, allLabel = '전체' } = options ?? {};
|
||||
|
||||
const uniqueValues = [...new Set(
|
||||
data
|
||||
.map(item => {
|
||||
const val = item[fieldKey];
|
||||
return typeof val === 'string' ? val.trim() : '';
|
||||
})
|
||||
.filter(v => v !== '')
|
||||
)];
|
||||
|
||||
const items = uniqueValues.map(v => ({ value: v, label: v }));
|
||||
|
||||
if (includeAll) {
|
||||
return [{ value: 'all', label: allLabel }, ...items];
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// ===== 3. 날짜+금액 정렬 팩토리 (5파일 유사) =====
|
||||
|
||||
export type SortDirection = 'latest' | 'oldest' | 'amountHigh' | 'amountLow';
|
||||
|
||||
/**
|
||||
* 날짜+금액 기준 정렬 함수를 생성합니다.
|
||||
*
|
||||
* @example
|
||||
* customSortFn: (items) => createDateAmountSortFn('salesDate', 'totalAmount', sortOption)(items),
|
||||
*/
|
||||
export function createDateAmountSortFn<T>(
|
||||
dateField: keyof T,
|
||||
amountField: keyof T,
|
||||
direction: SortDirection
|
||||
): (items: T[]) => T[] {
|
||||
return (items: T[]) => {
|
||||
const sorted = [...items];
|
||||
switch (direction) {
|
||||
case 'oldest':
|
||||
sorted.sort((a, b) =>
|
||||
new Date(String(a[dateField])).getTime() - new Date(String(b[dateField])).getTime()
|
||||
);
|
||||
break;
|
||||
case 'amountHigh':
|
||||
sorted.sort((a, b) =>
|
||||
((b[amountField] as number) ?? 0) - ((a[amountField] as number) ?? 0)
|
||||
);
|
||||
break;
|
||||
case 'amountLow':
|
||||
sorted.sort((a, b) =>
|
||||
((a[amountField] as number) ?? 0) - ((b[amountField] as number) ?? 0)
|
||||
);
|
||||
break;
|
||||
default: // latest
|
||||
sorted.sort((a, b) =>
|
||||
new Date(String(b[dateField])).getTime() - new Date(String(a[dateField])).getTime()
|
||||
);
|
||||
break;
|
||||
}
|
||||
return sorted;
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 4. 당월 합계 계산 (3파일 동일) =====
|
||||
|
||||
/**
|
||||
* 데이터 배열에서 현재 월에 해당하는 금액 합계를 계산합니다.
|
||||
*
|
||||
* @example
|
||||
* const monthlyAmount = computeMonthlyTotal(data, 'salesDate', 'totalAmount');
|
||||
*/
|
||||
export function computeMonthlyTotal<T>(
|
||||
data: T[],
|
||||
dateField: keyof T,
|
||||
amountField: keyof T
|
||||
): number {
|
||||
const now = new Date();
|
||||
const currentMonth = now.getMonth();
|
||||
const currentYear = now.getFullYear();
|
||||
|
||||
return data
|
||||
.filter(item => {
|
||||
const date = new Date(String(item[dateField]));
|
||||
return date.getMonth() === currentMonth && date.getFullYear() === currentYear;
|
||||
})
|
||||
.reduce((sum, item) => sum + (((item[amountField]) as number) ?? 0), 0);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useTransition, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import {
|
||||
FileCheck,
|
||||
Check,
|
||||
@@ -98,8 +99,7 @@ export function ApprovalBox() {
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// 날짜 범위 상태
|
||||
const [startDate, setStartDate] = useState('2025-09-01');
|
||||
const [endDate, setEndDate] = useState('2025-09-03');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [approveDialogOpen, setApproveDialogOpen] = useState(false);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { ExpenseEstimateData, ExpenseEstimateItem } from './types';
|
||||
import { formatNumber as formatCurrency } from '@/lib/utils/amount';
|
||||
|
||||
interface ExpenseEstimateFormProps {
|
||||
data: ExpenseEstimateData;
|
||||
@@ -22,10 +23,6 @@ interface ExpenseEstimateFormProps {
|
||||
export function ExpenseEstimateForm({ data, onChange, isLoading }: ExpenseEstimateFormProps) {
|
||||
const items = data.items;
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
};
|
||||
|
||||
const handleCheckChange = (id: string, checked: boolean) => {
|
||||
const newItems = items.map((item) =>
|
||||
item.id === id ? { ...item, checked } : item
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '@/components/ui/table';
|
||||
import type { ExpenseReportData, ExpenseReportItem } from './types';
|
||||
import { CARD_OPTIONS } from './types';
|
||||
import { formatNumber as formatCurrency } from '@/lib/utils/amount';
|
||||
|
||||
interface ExpenseReportFormProps {
|
||||
data: ExpenseReportData;
|
||||
@@ -71,10 +72,6 @@ export function ExpenseReportForm({ data, onChange }: ExpenseReportFormProps) {
|
||||
onChange({ ...data, attachments: updatedAttachments });
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 지출 정보 */}
|
||||
|
||||
@@ -11,15 +11,13 @@ import { Fragment } from 'react';
|
||||
import { ApprovalLineBox } from './ApprovalLineBox';
|
||||
import type { ExpenseEstimateDocumentData } from './types';
|
||||
import { DocumentHeader } from '@/components/document-system';
|
||||
import { formatNumber as formatCurrency } from '@/lib/utils/amount';
|
||||
|
||||
interface ExpenseEstimateDocumentProps {
|
||||
data: ExpenseEstimateDocumentData;
|
||||
}
|
||||
|
||||
export function ExpenseEstimateDocument({ data }: ExpenseEstimateDocumentProps) {
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
};
|
||||
|
||||
// 월별 그룹핑
|
||||
const groupedByMonth = data.items.reduce((acc, item) => {
|
||||
|
||||
@@ -10,15 +10,13 @@
|
||||
import { ApprovalLineBox } from './ApprovalLineBox';
|
||||
import type { ExpenseReportDocumentData } from './types';
|
||||
import { DocumentHeader } from '@/components/document-system';
|
||||
import { formatNumber as formatCurrency } from '@/lib/utils/amount';
|
||||
|
||||
interface ExpenseReportDocumentProps {
|
||||
data: ExpenseReportDocumentData;
|
||||
}
|
||||
|
||||
export function ExpenseReportDocument({ data }: ExpenseReportDocumentProps) {
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 min-h-full">
|
||||
|
||||
@@ -10,15 +10,13 @@
|
||||
import { ApprovalLineBox } from './ApprovalLineBox';
|
||||
import type { ProposalDocumentData } from './types';
|
||||
import { DocumentHeader } from '@/components/document-system';
|
||||
import { formatNumber as formatCurrency } from '@/lib/utils/amount';
|
||||
|
||||
interface ProposalDocumentProps {
|
||||
data: ProposalDocumentData;
|
||||
}
|
||||
|
||||
export function ProposalDocument({ data }: ProposalDocumentProps) {
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 min-h-full">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useTransition, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import {
|
||||
FileText,
|
||||
Send,
|
||||
@@ -81,8 +82,7 @@ export function DraftBox() {
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// 날짜 범위 상태
|
||||
const [startDate, setStartDate] = useState('2025-01-01');
|
||||
const [endDate, setEndDate] = useState('2025-12-31');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
|
||||
// API 데이터
|
||||
const [data, setData] = useState<DraftRecord[]>([]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useTransition, useRef } from 'react';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import {
|
||||
Files,
|
||||
Eye,
|
||||
@@ -74,8 +75,7 @@ export function ReferenceBox() {
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// 날짜 범위 상태
|
||||
const [startDate, setStartDate] = useState('2025-09-01');
|
||||
const [endDate, setEndDate] = useState('2025-09-03');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [markReadDialogOpen, setMarkReadDialogOpen] = useState(false);
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { getHandoverReportList, getHandoverReportStats } from './actions';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { formatDateRange } from '@/lib/utils/date';
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -89,8 +90,7 @@ export default function HandoverReportListClient({
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all');
|
||||
const [startDate, setStartDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [stats, setStats] = useState<HandoverReportStats | null>(initialStats || null);
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
withdrawIssues,
|
||||
} from './actions';
|
||||
import { formatDate } from '@/lib/utils/date';
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -84,8 +85,7 @@ export default function IssueManagementListClient({
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'received' | 'in_progress' | 'resolved' | 'unresolved'>('all');
|
||||
const [startDate, setStartDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today');
|
||||
const [stats, setStats] = useState<IssueStats | null>(initialStats || null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [withdrawDialogOpen, setWithdrawDialogOpen] = useState(false);
|
||||
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
getConstructionManagementList,
|
||||
getConstructionManagementStats,
|
||||
} from './actions';
|
||||
import { formatDate } from '@/lib/utils/date';
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -83,8 +85,7 @@ export default function ConstructionManagementListClient({
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'in_progress' | 'completed'>('all');
|
||||
const [startDate, setStartDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today');
|
||||
const { data: stats } = useStatsLoader(getConstructionManagementStats, initialStats);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
@@ -156,12 +157,6 @@ export default function ConstructionManagementListClient({
|
||||
// 달력 뱃지 (사용 안 함)
|
||||
const calendarBadges: DayBadge[] = [];
|
||||
|
||||
// 날짜 포맷
|
||||
const formatDate = useCallback((dateStr: string | null) => {
|
||||
if (!dateStr) return '-';
|
||||
return dateStr.split('T')[0];
|
||||
}, []);
|
||||
|
||||
// 달력 이벤트 핸들러
|
||||
const handleCalendarDateClick = useCallback((date: Date) => {
|
||||
if (selectedCalendarDate && isSameDay(selectedCalendarDate, date)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { getPresetStyle } from '@/lib/utils/status-config';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ProjectDetail, ProjectStatus } from './types';
|
||||
import { formatNumber as formatAmount } from '@/lib/utils/amount';
|
||||
|
||||
interface ProjectCardProps {
|
||||
project: ProjectDetail;
|
||||
@@ -28,11 +29,6 @@ const ProjectCard = memo(function ProjectCard({ project, isSelected, onClick }:
|
||||
}
|
||||
};
|
||||
|
||||
// 금액 포맷
|
||||
const formatAmount = (amount: number) => {
|
||||
return amount.toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { MobileCard } from '@/components/organisms/MobileCard';
|
||||
import { formatAmountWon as formatAmount } from '@/lib/utils/amount';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Project, ProjectStats, ChartViewMode, SelectOption } from './types';
|
||||
@@ -240,11 +241,6 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr
|
||||
}, [selectedItems.size, paginatedData]);
|
||||
|
||||
|
||||
// 금액 포맷
|
||||
const formatAmount = (amount: number) => {
|
||||
return amount.toLocaleString() + '원';
|
||||
};
|
||||
|
||||
// 날짜 포맷
|
||||
const formatDate = (dateStr: string) => {
|
||||
return dateStr.replace(/-/g, '.');
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getPresetStyle } from '@/lib/utils/status-config';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Stage, StageCardStatus } from './types';
|
||||
import { STAGE_LABELS, STAGE_CARD_STATUS_LABELS } from './types';
|
||||
import { formatNumber as formatAmount } from '@/lib/utils/amount';
|
||||
|
||||
interface StageCardProps {
|
||||
stage: Stage;
|
||||
@@ -28,11 +29,6 @@ const StageCard = memo(function StageCard({ stage, isSelected, onClick }: StageC
|
||||
}
|
||||
};
|
||||
|
||||
// 금액 포맷
|
||||
const formatAmount = (amount: number) => {
|
||||
return amount.toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -45,12 +45,14 @@ import {
|
||||
MOCK_WORK_TEAM_LEADERS,
|
||||
getScheduleColorByManager,
|
||||
} from './types';
|
||||
import { formatDate } from '@/lib/utils/date';
|
||||
import {
|
||||
getOrderList,
|
||||
getOrderStats,
|
||||
deleteOrder,
|
||||
deleteOrders,
|
||||
} from './actions';
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -90,8 +92,7 @@ export default function OrderManagementListClient({
|
||||
);
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [startDate, setStartDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// 달력 관련 상태
|
||||
@@ -164,12 +165,6 @@ export default function OrderManagementListClient({
|
||||
// 달력 뱃지 (사용 안 함)
|
||||
const calendarBadges: DayBadge[] = [];
|
||||
|
||||
// 날짜 포맷
|
||||
const formatDate = useCallback((dateStr: string | null) => {
|
||||
if (!dateStr) return '-';
|
||||
return dateStr.split('T')[0];
|
||||
}, []);
|
||||
|
||||
// ===== 추가 핸들러 =====
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/order/order-management?mode=new');
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
deleteOrder,
|
||||
deleteOrders,
|
||||
} from './actions';
|
||||
import { formatDate } from '@/lib/utils/date';
|
||||
|
||||
interface OrderManagementUnifiedProps {
|
||||
initialData?: Order[];
|
||||
@@ -163,12 +164,6 @@ export function OrderManagementUnified({ initialData }: OrderManagementUnifiedPr
|
||||
setCalendarDate(date);
|
||||
}, []);
|
||||
|
||||
// 날짜 포맷
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return '-';
|
||||
return dateStr.split('T')[0];
|
||||
};
|
||||
|
||||
// 달력 필터 슬롯
|
||||
const calendarFilterSlot = (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
deletePricing,
|
||||
deletePricings,
|
||||
} from './actions';
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
interface PricingListClientProps {
|
||||
initialData?: Pricing[];
|
||||
@@ -61,8 +62,7 @@ export default function PricingListClient({
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'in_use' | 'not_registered'>('all');
|
||||
const [startDate, setStartDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today');
|
||||
const [stats, setStats] = useState<PricingStats | null>(initialStats || null);
|
||||
const [pricingData, setPricingData] = useState<Pricing[]>(initialData);
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
getProgressBillingList,
|
||||
getProgressBillingStats,
|
||||
} from './actions';
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -76,8 +77,7 @@ export default function ProgressBillingManagementListClient({
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'contractWaiting' | 'contractComplete'>('all');
|
||||
const [startDate, setStartDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today');
|
||||
const { data: stats } = useStatsLoader(getProgressBillingStats, initialStats);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
|
||||
@@ -9,11 +9,7 @@
|
||||
|
||||
import type { ProgressBillingDetailFormData } from '../types';
|
||||
import { DocumentHeader } from '@/components/document-system';
|
||||
|
||||
function formatNumber(num: number | undefined): string {
|
||||
if (num === undefined || num === null) return '-';
|
||||
return num.toLocaleString('ko-KR');
|
||||
}
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
interface DirectConstructionItem {
|
||||
id: string;
|
||||
|
||||
@@ -9,12 +9,7 @@
|
||||
|
||||
import type { ProgressBillingDetailFormData } from '../types';
|
||||
import { DocumentHeader } from '@/components/document-system';
|
||||
|
||||
// 숫자 포맷팅 (천단위 콤마)
|
||||
function formatNumber(num: number | undefined): string {
|
||||
if (num === undefined || num === null) return '-';
|
||||
return num.toLocaleString('ko-KR');
|
||||
}
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
// 간접 공사 내역 아이템 타입
|
||||
interface IndirectConstructionItem {
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
SITE_STATUS_LABELS,
|
||||
} from './types';
|
||||
import { getSiteList, getSiteStats, deleteSite, deleteSites } from './actions';
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -66,8 +67,7 @@ export default function SiteManagementListClient({
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'construction' | 'unregistered'>('all');
|
||||
const [startDate, setStartDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today');
|
||||
const [stats, setStats] = useState<SiteStats | null>(initialStats || null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
deleteStructureReviews,
|
||||
} from './actions';
|
||||
import { formatDate } from '@/lib/utils/date';
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -77,8 +78,7 @@ export default function StructureReviewListClient({
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all');
|
||||
const [startDate, setStartDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today');
|
||||
const [stats, setStats] = useState<StructureReviewStats | null>(initialStats || null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
} from './actions';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { formatDate } from '@/lib/utils/date';
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -72,8 +73,7 @@ export default function UtilityManagementListClient({
|
||||
}: UtilityManagementListClientProps) {
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'waiting' | 'complete'>('all');
|
||||
const [startDate, setStartDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today');
|
||||
const [stats, setStats] = useState<UtilityStats | null>(initialStats || null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
MOCK_WORKER_NAMES,
|
||||
} from './types';
|
||||
import { getWorkerStatusList, getWorkerStatusStats } from './actions';
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -82,8 +83,7 @@ export default function WorkerStatusListClient({
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'all_contract' | 'pending' | 'completed'>('all');
|
||||
const [startDate, setStartDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(() => new Date().toISOString().split('T')[0]);
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today');
|
||||
const [stats, setStats] = useState<WorkerStatusStats | null>(initialStats || null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import { ClipboardList, Plus, GripVertical } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow, TableHead } from '@/components/ui/table';
|
||||
@@ -47,8 +48,7 @@ export default function ChecklistListClient() {
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
|
||||
// 날짜 범위 상태
|
||||
const [startDate, setStartDate] = useState('2025-01-01');
|
||||
const [endDate, setEndDate] = useState('2025-12-31');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
|
||||
// 검색어 상태
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import {
|
||||
DollarSign,
|
||||
Check,
|
||||
Clock,
|
||||
Pencil,
|
||||
Banknote,
|
||||
Briefcase,
|
||||
Timer,
|
||||
@@ -61,8 +61,7 @@ export function SalaryManagement() {
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// 날짜 범위 상태
|
||||
const [startDate, setStartDate] = useState('2025-12-01');
|
||||
const [endDate, setEndDate] = useState('2025-12-31');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentMonth');
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [detailDialogOpen, setDetailDialogOpen] = useState(false);
|
||||
@@ -317,7 +316,6 @@ export function SalaryManagement() {
|
||||
{ key: 'netPayment', label: '실지급액', className: 'text-right' },
|
||||
{ key: 'paymentDate', label: '일자', className: 'text-center' },
|
||||
{ key: 'status', label: '상태', className: 'text-center' },
|
||||
{ key: 'action', label: '작업', className: 'text-center w-[80px]' },
|
||||
], []);
|
||||
|
||||
// ===== filterConfig 기반 통합 필터 시스템 =====
|
||||
@@ -453,8 +451,12 @@ export function SalaryManagement() {
|
||||
const { isSelected, onToggle } = handlers;
|
||||
|
||||
return (
|
||||
<TableRow key={item.id} className="hover:bg-muted/50">
|
||||
<TableCell className="text-center">
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => handleViewDetail(item)}
|
||||
>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox checked={isSelected} onCheckedChange={onToggle} />
|
||||
</TableCell>
|
||||
<TableCell>{item.department}</TableCell>
|
||||
@@ -473,17 +475,6 @@ export function SalaryManagement() {
|
||||
{PAYMENT_STATUS_LABELS[item.status]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleViewDetail(item)}
|
||||
title="수정"
|
||||
disabled={isActionLoading}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -502,6 +493,7 @@ export function SalaryManagement() {
|
||||
}
|
||||
isSelected={isSelected}
|
||||
onToggleSelection={onToggle}
|
||||
onClick={() => handleViewDetail(item)}
|
||||
infoGrid={
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<InfoField label="부서" value={item.department} />
|
||||
@@ -515,18 +507,6 @@ export function SalaryManagement() {
|
||||
<InfoField label="지급일" value={item.paymentDate} />
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => handleViewDetail(item)}
|
||||
disabled={isActionLoading}
|
||||
>
|
||||
<Pencil className="h-4 w-4 mr-2" />
|
||||
수정
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import {
|
||||
Plus,
|
||||
Calendar,
|
||||
@@ -103,8 +104,7 @@ export function VacationManagement() {
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// 날짜 범위 상태 (input type="date" 용)
|
||||
const [startDate, setStartDate] = useState('2025-12-01');
|
||||
const [endDate, setEndDate] = useState('2025-12-31');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentMonth');
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [grantDialogOpen, setGrantDialogOpen] = useState(false);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Upload, FileText, Search, X, Plus, ClipboardCheck } from 'lucide-react';
|
||||
import { getTodayString } from '@/lib/utils/date';
|
||||
import { FileDropzone } from '@/components/ui/file-dropzone';
|
||||
import { ItemSearchModal } from '@/components/quotes/ItemSearchModal';
|
||||
import { InspectionModal } from '@/app/[locale]/(protected)/quality/qms/components/InspectionModal';
|
||||
@@ -311,7 +312,7 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
|
||||
const handleAddAdjustment = () => {
|
||||
const newRecord: InventoryAdjustmentRecord = {
|
||||
id: `adj-${Date.now()}`,
|
||||
adjustmentDate: new Date().toISOString().split('T')[0],
|
||||
adjustmentDate: getTodayString(),
|
||||
quantity: 0,
|
||||
inspector: getLoggedInUserName() || '홍길동',
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getLocalDateString } from '@/lib/utils/date';
|
||||
import {
|
||||
Package,
|
||||
CheckCircle2,
|
||||
@@ -89,8 +90,8 @@ export function ReceivingList() {
|
||||
// ===== 날짜 범위 상태 (최근 30일) =====
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
const [startDate, setStartDate] = useState<string>(thirtyDaysAgo.toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(today.toISOString().split('T')[0]);
|
||||
const [startDate, setStartDate] = useState<string>(getLocalDateString(thirtyDaysAgo));
|
||||
const [endDate, setEndDate] = useState<string>(getLocalDateString(today));
|
||||
|
||||
// ===== 필터 상태 =====
|
||||
const [filterValues, setFilterValues] = useState<Record<string, string | string[]>>({
|
||||
|
||||
@@ -22,6 +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 type {
|
||||
ReceivingItem,
|
||||
@@ -1074,7 +1075,7 @@ export async function getInspectionTemplate(params: {
|
||||
lotNo: params.lotNo || '250715-02',
|
||||
inspectionDate: new Date().toLocaleDateString('ko-KR', { month: '2-digit', day: '2-digit' }).replace('. ', '/').replace('.', ''),
|
||||
inspector: inspectorName,
|
||||
reportDate: new Date().toISOString().split('T')[0],
|
||||
reportDate: getTodayString(),
|
||||
approvers: {
|
||||
writer: inspectorName,
|
||||
reviewer: '',
|
||||
@@ -1653,7 +1654,7 @@ function transformResolveToTemplate(
|
||||
lotNo: params.lotNo || '',
|
||||
inspectionDate: new Date().toLocaleDateString('ko-KR', { month: '2-digit', day: '2-digit' }).replace('. ', '/').replace('.', ''),
|
||||
inspector: params.inspector || '',
|
||||
reportDate: new Date().toISOString().split('T')[0],
|
||||
reportDate: getTodayString(),
|
||||
approvers: {
|
||||
// 작성자는 현재 검사자 (로그인 사용자)로 설정
|
||||
writer: params.inspector || writerLine?.role || '',
|
||||
@@ -1888,7 +1889,7 @@ export async function saveInspectionData(params: {
|
||||
if (!docResult.success) return { success: false, error: docResult.error };
|
||||
|
||||
// Step 2: PUT /v1/receivings/{id} - 검사 완료 후 입고대기로 상태 변경 (비필수)
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = getTodayString();
|
||||
const inspectionStatus = params.inspectionResult === 'pass' ? '적' : params.inspectionResult === 'fail' ? '부적' : '-';
|
||||
const inspectionResultLabel = params.inspectionResult === 'pass' ? '합격' : params.inspectionResult === 'fail' ? '불합격' : null;
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
|
||||
import { getStocks, getStockStats } from './actions';
|
||||
import { USE_STATUS_LABELS, ITEM_TYPE_LABELS } from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { getLocalDateString } from '@/lib/utils/date';
|
||||
import type { StockItem, StockStats, ItemType, StockStatusType } from './types';
|
||||
|
||||
// 페이지당 항목 수
|
||||
@@ -49,8 +50,8 @@ export function StockStatusList() {
|
||||
// ===== 날짜 범위 상태 =====
|
||||
const today = new Date();
|
||||
const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const [startDate, setStartDate] = useState<string>(firstDayOfMonth.toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(today.toISOString().split('T')[0]);
|
||||
const [startDate, setStartDate] = useState<string>(getLocalDateString(firstDayOfMonth));
|
||||
const [endDate, setEndDate] = useState<string>(getLocalDateString(today));
|
||||
|
||||
// ===== 데이터 상태 (수주관리 패턴) =====
|
||||
const [stocks, setStocks] = useState<StockItem[]>([]);
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
} from './types';
|
||||
import type { ShipmentItem, ShipmentStatus, ShipmentStats } from './types';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { getLocalDateString } from '@/lib/utils/date';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
// 페이지당 항목 수
|
||||
@@ -61,11 +62,11 @@ export function ShipmentList() {
|
||||
const today = new Date();
|
||||
const [startDate, setStartDate] = useState(() => {
|
||||
const d = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
return d.toISOString().split('T')[0];
|
||||
return getLocalDateString(d);
|
||||
});
|
||||
const [endDate, setEndDate] = useState(() => {
|
||||
const d = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||||
return d.toISOString().split('T')[0];
|
||||
return getLocalDateString(d);
|
||||
});
|
||||
|
||||
// ===== 캘린더 상태 =====
|
||||
|
||||
@@ -27,16 +27,12 @@ import {
|
||||
FREIGHT_COST_STYLES,
|
||||
} from './types';
|
||||
import type { VehicleDispatchDetail as VehicleDispatchDetailType } from './types';
|
||||
import { formatNumber as formatAmount } from '@/lib/utils/amount';
|
||||
|
||||
interface VehicleDispatchDetailProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// 금액 포맷
|
||||
function formatAmount(amount: number): string {
|
||||
return amount.toLocaleString('ko-KR');
|
||||
}
|
||||
|
||||
export function VehicleDispatchDetail({ id }: VehicleDispatchDetailProps) {
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -34,16 +34,13 @@ import type {
|
||||
FreightCostType,
|
||||
} from './types';
|
||||
|
||||
import { formatNumber as formatAmount } from '@/lib/utils/amount';
|
||||
|
||||
// 운임비용 옵션
|
||||
const freightCostOptions: { value: FreightCostType; label: string }[] = Object.entries(
|
||||
FREIGHT_COST_LABELS
|
||||
).map(([value, label]) => ({ value: value as FreightCostType, label }));
|
||||
|
||||
// 금액 포맷
|
||||
function formatAmount(amount: number): string {
|
||||
return amount.toLocaleString('ko-KR');
|
||||
}
|
||||
|
||||
interface VehicleDispatchEditProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
@@ -39,14 +39,11 @@ import {
|
||||
FREIGHT_COST_STYLES,
|
||||
} from './types';
|
||||
import type { VehicleDispatchItem, VehicleDispatchStats } from './types';
|
||||
import { formatNumber as formatAmount } from '@/lib/utils/amount';
|
||||
import { getLocalDateString } from '@/lib/utils/date';
|
||||
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
|
||||
// 금액 포맷
|
||||
function formatAmount(amount: number): string {
|
||||
return amount.toLocaleString('ko-KR');
|
||||
}
|
||||
|
||||
export function VehicleDispatchList() {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -57,11 +54,11 @@ export function VehicleDispatchList() {
|
||||
const today = new Date();
|
||||
const [startDate, setStartDate] = useState(() => {
|
||||
const d = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
return d.toISOString().split('T')[0];
|
||||
return getLocalDateString(d);
|
||||
});
|
||||
const [endDate, setEndDate] = useState(() => {
|
||||
const d = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||||
return d.toISOString().split('T')[0];
|
||||
return getLocalDateString(d);
|
||||
});
|
||||
|
||||
// 초기 통계 로드
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
DISTRIBUTION_STATUS_LABELS,
|
||||
DISTRIBUTION_STATUS_STYLES,
|
||||
} from './types';
|
||||
import { getLocalDateString } from '@/lib/utils/date';
|
||||
import {
|
||||
getPriceDistributionList,
|
||||
createPriceDistribution,
|
||||
@@ -57,8 +58,8 @@ export function PriceDistributionList() {
|
||||
// 날짜 범위 상태 (최근 30일)
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
const [startDate, setStartDate] = useState<string>(thirtyDaysAgo.toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState<string>(today.toISOString().split('T')[0]);
|
||||
const [startDate, setStartDate] = useState<string>(getLocalDateString(thirtyDaysAgo));
|
||||
const [endDate, setEndDate] = useState<string>(getLocalDateString(today));
|
||||
|
||||
// 데이터 로드
|
||||
const loadData = useCallback(async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import { getLocalDateString } from '@/lib/utils/date';
|
||||
import type {
|
||||
PriceDistributionListItem,
|
||||
PriceDistributionDetail,
|
||||
@@ -362,9 +363,9 @@ export async function createPriceDistribution(): Promise<{
|
||||
distributionNo: String(Math.floor(100000 + Math.random() * 900000)),
|
||||
distributionName: name,
|
||||
status: 'initial',
|
||||
createdAt: now.toISOString().split('T')[0],
|
||||
createdAt: getLocalDateString(now),
|
||||
documentNo: '',
|
||||
effectiveDate: now.toISOString().split('T')[0],
|
||||
effectiveDate: getLocalDateString(now),
|
||||
officePhone: '',
|
||||
orderPhone: '',
|
||||
author: '현재사용자',
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, Save, Trash2, X } from 'lucide-react';
|
||||
import { getTodayString } from '@/lib/utils/date';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -31,6 +32,7 @@ import { toast } from 'sonner';
|
||||
import { createPricingTable, updatePricingTable, deletePricingTable } from './actions';
|
||||
import { calculateSellingPrice } from './types';
|
||||
import type { PricingTable, PricingTableFormData, GradePricing, TradeGrade, PricingTableStatus } from './types';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
interface PricingTableFormProps {
|
||||
mode: 'create' | 'edit';
|
||||
@@ -205,8 +207,6 @@ export function PricingTableForm({ mode, initialData }: PricingTableFormProps) {
|
||||
router.push('/ko/master-data/pricing-table-management');
|
||||
};
|
||||
|
||||
const formatNumber = (num: number) => num.toLocaleString('ko-KR');
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
@@ -309,7 +309,7 @@ export function PricingTableForm({ mode, initialData }: PricingTableFormProps) {
|
||||
<div className="space-y-2">
|
||||
<Label>변경일</Label>
|
||||
<Input
|
||||
value={isEdit ? initialData?.changedDate ?? '' : new Date().toISOString().split('T')[0]}
|
||||
value={isEdit ? initialData?.changedDate ?? '' : getTodayString()}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import { DollarSign, Plus } from 'lucide-react';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
deletePricingTable,
|
||||
deletePricingTables,
|
||||
} from './actions';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
export default function PricingTableListClient() {
|
||||
const router = useRouter();
|
||||
@@ -34,8 +36,7 @@ export default function PricingTableListClient() {
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
|
||||
// 날짜 범위
|
||||
const [startDate, setStartDate] = useState('2025-01-01');
|
||||
const [endDate, setEndDate] = useState('2025-12-31');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
|
||||
// 검색어
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -135,9 +136,6 @@ export default function PricingTableListClient() {
|
||||
[selectedGrade]
|
||||
);
|
||||
|
||||
// 숫자 포맷
|
||||
const formatNumber = (num: number) => num.toLocaleString('ko-KR');
|
||||
|
||||
// ===== Config =====
|
||||
const config: UniversalListConfig<PricingTable> = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import { getTodayString } from '@/lib/utils/date';
|
||||
import type { PricingTable, PricingTableFormData, TradeGrade } from './types';
|
||||
|
||||
// ============================================================================
|
||||
@@ -238,7 +239,7 @@ export async function createPricingTable(data: PricingTableFormData): Promise<{
|
||||
processingCost: data.processingCost,
|
||||
status: data.status,
|
||||
author: '현재사용자',
|
||||
changedDate: new Date().toISOString().split('T')[0],
|
||||
changedDate: getTodayString(),
|
||||
gradePricings: data.gradePricings,
|
||||
};
|
||||
return { success: true, data: newItem };
|
||||
@@ -262,7 +263,7 @@ export async function updatePricingTable(
|
||||
const updated: PricingTable = {
|
||||
...existing,
|
||||
...data,
|
||||
changedDate: new Date().toISOString().split('T')[0],
|
||||
changedDate: getTodayString(),
|
||||
};
|
||||
return { success: true, data: updated };
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import { Wrench, Plus, GripVertical } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow, TableHead } from '@/components/ui/table';
|
||||
@@ -45,8 +46,7 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
|
||||
// 날짜 범위 상태
|
||||
const [startDate, setStartDate] = useState('2025-01-01');
|
||||
const [endDate, setEndDate] = useState('2025-12-31');
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
|
||||
// 검색어 상태
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
@@ -47,6 +47,7 @@ import { getInspections, getInspectionStats, getInspectionCalendar } from './act
|
||||
import { statusColorMap } from './mockData';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { getLocalDateString } from '@/lib/utils/date';
|
||||
import type { ProductInspection, InspectionStats, InspectionStatus } from './types';
|
||||
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
@@ -61,11 +62,11 @@ export function InspectionList() {
|
||||
const today = new Date();
|
||||
const [startDate, setStartDate] = useState(() => {
|
||||
const d = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
return d.toISOString().split('T')[0];
|
||||
return getLocalDateString(d);
|
||||
});
|
||||
const [endDate, setEndDate] = useState(() => {
|
||||
const d = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||||
return d.toISOString().split('T')[0];
|
||||
return getLocalDateString(d);
|
||||
});
|
||||
|
||||
// ===== 캘린더 상태 =====
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import { DocumentViewer } from '@/components/document-system';
|
||||
import { getTodayString } from '@/lib/utils/date';
|
||||
import type { QuoteFormDataV2 } from './QuoteRegistration';
|
||||
|
||||
interface QuoteTransactionModalProps {
|
||||
@@ -55,7 +56,7 @@ export function QuoteTransactionModal({
|
||||
const hasDiscount = discountAmount > 0;
|
||||
|
||||
// 오늘 날짜
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = getTodayString();
|
||||
|
||||
return (
|
||||
<DocumentViewer
|
||||
|
||||
@@ -14,6 +14,7 @@ import { usePermission } from '@/hooks/usePermission';
|
||||
import { cancelSubscription, requestDataExport } from './actions';
|
||||
import type { SubscriptionInfo } from './types';
|
||||
import { PLAN_LABELS, SUBSCRIPTION_STATUS_LABELS } from './types';
|
||||
import { formatAmountWon as formatCurrency } from '@/lib/utils/amount';
|
||||
|
||||
// ===== Props 타입 =====
|
||||
interface SubscriptionClientProps {
|
||||
@@ -31,11 +32,6 @@ const formatDate = (dateStr: string): string => {
|
||||
return `${year}년 ${month}월 ${day}일`;
|
||||
};
|
||||
|
||||
// ===== 금액 포맷 함수 =====
|
||||
const formatCurrency = (amount: number): string => {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount) + '원';
|
||||
};
|
||||
|
||||
export function SubscriptionClient({ initialData }: SubscriptionClientProps) {
|
||||
const { canExport } = usePermission();
|
||||
const [subscription, setSubscription] = useState<SubscriptionInfo>(initialData);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import type { SubscriptionInfo } from './types';
|
||||
import { PLAN_LABELS } from './types';
|
||||
import { requestDataExport, cancelSubscription } from './actions';
|
||||
import { formatAmountWon as formatCurrency } from '@/lib/utils/amount';
|
||||
|
||||
// ===== 기본 저장공간 (10GB) =====
|
||||
const DEFAULT_STORAGE_LIMIT = 10 * 1024 * 1024 * 1024; // 10GB in bytes
|
||||
@@ -42,11 +43,6 @@ const formatDate = (dateStr: string): string => {
|
||||
return `${year}년 ${month}월 ${day}일`;
|
||||
};
|
||||
|
||||
// ===== 금액 포맷 함수 =====
|
||||
const formatCurrency = (amount: number): string => {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount) + '원';
|
||||
};
|
||||
|
||||
interface SubscriptionManagementProps {
|
||||
initialData: SubscriptionInfo | null;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export function ForkliftDetail({ mode, initialData, id }: ForkliftDetailProps) {
|
||||
|
||||
const handleEdit = () => {
|
||||
if (id) {
|
||||
router.push(`/vehicle-management/forklift/${id}/edit`);
|
||||
router.push(`/vehicle-management/forklift/${id}?mode=edit`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export function ForkliftList({ initialData }: ForkliftListProps) {
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(forklift: Forklift) => {
|
||||
router.push(`/vehicle-management/forklift/${forklift.id}/edit`);
|
||||
router.push(`/vehicle-management/forklift/${forklift.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
@@ -94,7 +94,7 @@ export function VehicleDetail({ mode, initialData, id }: VehicleDetailProps) {
|
||||
// ===== 수정 모드 전환 =====
|
||||
const handleEdit = () => {
|
||||
if (id) {
|
||||
router.push(`/vehicle-management/vehicle/${id}/edit`);
|
||||
router.push(`/vehicle-management/vehicle/${id}?mode=edit`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export function VehicleList({ initialData }: VehicleListProps) {
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(vehicle: Vehicle) => {
|
||||
router.push(`/vehicle-management/vehicle/${vehicle.id}/edit`);
|
||||
router.push(`/vehicle-management/vehicle/${vehicle.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
@@ -88,7 +88,7 @@ export function VehicleLogDetail({ mode, initialData, id }: VehicleLogDetailProp
|
||||
|
||||
const handleEdit = () => {
|
||||
if (id) {
|
||||
router.push(`/vehicle-management/vehicle-log/${id}/edit`);
|
||||
router.push(`/vehicle-management/vehicle-log/${id}?mode=edit`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { getTodayString } from '@/lib/utils/date';
|
||||
import type { VehicleLog, VehicleLogFormData, ActionResponse, ListResponse } from '../types';
|
||||
|
||||
// ===== Mock 데이터 (5130 레거시 기준) =====
|
||||
@@ -123,12 +124,12 @@ export async function createVehicleLog(
|
||||
try {
|
||||
const newLog: VehicleLog = {
|
||||
id: String(Date.now()),
|
||||
writeDate: formData.writeDate || new Date().toISOString().split('T')[0],
|
||||
writeDate: formData.writeDate || getTodayString(),
|
||||
vehicleInfo: formData.vehicleInfo || '',
|
||||
writer: formData.writer || '',
|
||||
title: formData.title || '',
|
||||
images: formData.images || [],
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
createdAt: getTodayString(),
|
||||
};
|
||||
mockVehicleLogs.push(newLog);
|
||||
return { success: true, data: newLog };
|
||||
|
||||
@@ -50,7 +50,7 @@ export function VehicleLogList({ initialData }: VehicleLogListProps) {
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(log: VehicleLog) => {
|
||||
router.push(`/vehicle-management/vehicle-log/${log.id}/edit`);
|
||||
router.push(`/vehicle-management/vehicle-log/${log.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
@@ -37,6 +37,13 @@ export type {
|
||||
UseDetailPermissionsReturn,
|
||||
} from './useDetailPermissions';
|
||||
|
||||
// ===== 날짜 범위 / 목록 페이지 =====
|
||||
export { useDateRange } from './useDateRange';
|
||||
export type { DateRangePreset, UseDateRangeReturn } from './useDateRange';
|
||||
|
||||
export { useAccountingListPage, DEFAULT_PAGINATION } from './useAccountingListPage';
|
||||
export type { PaginationMeta, UseAccountingListPageReturn } from './useAccountingListPage';
|
||||
|
||||
// ===== 기존 훅 =====
|
||||
export { usePermission } from './usePermission';
|
||||
export { useAuthGuard } from './useAuthGuard';
|
||||
|
||||
79
src/hooks/useAccountingListPage.ts
Normal file
79
src/hooks/useAccountingListPage.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
// ===== 공통 페이지네이션 기본값 =====
|
||||
export const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
perPage: 100,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
export type PaginationMeta = {
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
// ===== 반환 타입 =====
|
||||
export interface UseAccountingListPageReturn<TData> {
|
||||
data: TData[];
|
||||
pagination: PaginationMeta;
|
||||
isLoading: boolean;
|
||||
mode: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회계 목록 페이지 공통 훅
|
||||
*
|
||||
* searchParams의 mode를 읽어서 skipModes가 아닌 경우 fetchFn을 호출합니다.
|
||||
* data, pagination, isLoading, mode를 반환합니다.
|
||||
*
|
||||
* @param fetchFn - 데이터를 가져오는 함수 (클로저로 params 전달)
|
||||
* @param options.skipModes - 데이터 로드를 건너뛸 mode 값 (기본: ['new'])
|
||||
*
|
||||
* @example
|
||||
* const { data, pagination, isLoading, mode } = useAccountingListPage(
|
||||
* () => getSales({ perPage: 100 })
|
||||
* );
|
||||
*/
|
||||
export function useAccountingListPage<TData>(
|
||||
fetchFn: () => Promise<{ success?: boolean; data: TData[]; pagination?: PaginationMeta }>,
|
||||
options?: { skipModes?: string[] }
|
||||
): UseAccountingListPageReturn<TData> {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
const skipModes = options?.skipModes ?? ['new'];
|
||||
|
||||
const [data, setData] = useState<TData[]>([]);
|
||||
const [pagination, setPagination] = useState<PaginationMeta>(DEFAULT_PAGINATION);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// fetchFn을 ref로 보관하여 stale closure 방지
|
||||
const fetchFnRef = useRef(fetchFn);
|
||||
fetchFnRef.current = fetchFn;
|
||||
|
||||
useEffect(() => {
|
||||
if (mode && skipModes.includes(mode)) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
fetchFnRef.current()
|
||||
.then(result => {
|
||||
setData(result.data ?? []);
|
||||
if (result.pagination) {
|
||||
setPagination(result.pagination);
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
// mode 변경 시에만 재실행
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mode]);
|
||||
|
||||
return { data, pagination, isLoading, mode };
|
||||
}
|
||||
60
src/hooks/useDateRange.ts
Normal file
60
src/hooks/useDateRange.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { getLocalDateString } from '@/lib/utils/date';
|
||||
|
||||
// ===== 프리셋 타입 =====
|
||||
export type DateRangePreset = 'currentYear' | 'currentMonth' | 'today' | 'none';
|
||||
|
||||
// ===== 반환 타입 =====
|
||||
export interface UseDateRangeReturn {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
setStartDate: React.Dispatch<React.SetStateAction<string>>;
|
||||
setEndDate: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
// ===== 프리셋별 초기값 계산 =====
|
||||
function getInitialDates(preset: DateRangePreset): { start: string; end: string } {
|
||||
const now = new Date();
|
||||
switch (preset) {
|
||||
case 'currentYear': {
|
||||
const year = now.getFullYear();
|
||||
return { start: `${year}-01-01`, end: `${year}-12-31` };
|
||||
}
|
||||
case 'currentMonth': {
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const lastDay = new Date(year, now.getMonth() + 1, 0).getDate();
|
||||
return {
|
||||
start: `${year}-${month}-01`,
|
||||
end: `${year}-${month}-${String(lastDay).padStart(2, '0')}`,
|
||||
};
|
||||
}
|
||||
case 'today': {
|
||||
const today = getLocalDateString(now);
|
||||
return { start: today, end: today };
|
||||
}
|
||||
case 'none':
|
||||
return { start: '', end: '' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 범위 상태 관리 훅
|
||||
*
|
||||
* @param preset - 초기 날짜 범위 프리셋
|
||||
* - 'currentYear': 올해 1/1 ~ 12/31 (회계 관리 기본값)
|
||||
* - 'currentMonth': 이번 달 1일 ~ 말일
|
||||
* - 'today': 오늘 ~ 오늘 (로컬 시간대 기준)
|
||||
* - 'none': 빈 문자열
|
||||
*
|
||||
* @example
|
||||
* const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
*/
|
||||
export function useDateRange(preset: DateRangePreset = 'currentYear'): UseDateRangeReturn {
|
||||
const [startDate, setStartDate] = useState(() => getInitialDates(preset).start);
|
||||
const [endDate, setEndDate] = useState(() => getInitialDates(preset).end);
|
||||
|
||||
return { startDate, endDate, setStartDate, setEndDate };
|
||||
}
|
||||
Reference in New Issue
Block a user