From 32d6e3bbbd490a57d187df4468a92dad1d4904ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Thu, 5 Feb 2026 17:38:38 +0900 Subject: [PATCH] =?UTF-8?q?feat(WEB):=20=EA=B3=B5=EC=82=AC=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=ED=99=94=20=EB=B0=8F=20=EC=BA=98=EB=A6=B0=EB=8D=94/=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=84=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공사관리 리스트 공통화: - 입찰/계약/견적/인수인계/이슈/품목/노무/현장/파트너/단가/기성/현장브리핑/구조검토/유틸리티/작업자현황 리스트 공통 포맷터 적용 - 중복 포맷팅 로직 제거 (-530줄) 캘린더 기능 개선: - CEODashboard CalendarSection 기능 확장 - ScheduleCalendar DayCell/MonthView/WeekView 개선 - ui/calendar 컴포넌트 기능 추가 유틸리티 개선: - date.ts 날짜 유틸 함수 추가 - formatAmount.ts 금액 포맷 함수 추가 신규 추가: - useListHandlers 훅 추가 - src/constants/ 디렉토리 추가 - 포맷터 공통화 계획 문서 추가 - SAM ERP/MES 정체성 분석 문서 추가 Co-Authored-By: Claude Opus 4.5 --- ...SIS-2026-02-05] SAM-ERP-MES-정체성-분석.md | 256 ++++++++++++++++++ ...026-02-05] formatter-commonization-plan.md | 88 ++++++ .../CEODashboard/sections/CalendarSection.tsx | 61 ++++- .../bidding/BiddingDetailForm.tsx | 84 +++--- .../bidding/BiddingListClient.tsx | 47 +--- .../contract/ContractDetailForm.tsx | 8 +- .../contract/ContractListClient.tsx | 57 +--- .../estimates/EstimateListClient.tsx | 32 +-- .../modals/EstimateDocumentContent.tsx | 40 ++- .../estimates/utils/formatters.ts | 6 +- .../HandoverReportDetailForm.tsx | 10 +- .../HandoverReportListClient.tsx | 59 +--- .../modals/HandoverReportDocumentModal.tsx | 21 +- .../IssueManagementListClient.tsx | 30 +- .../item-management/ItemManagementClient.tsx | 13 +- .../LaborManagementClient.tsx | 23 +- .../management/ConstructionDetailClient.tsx | 9 +- .../ConstructionManagementListClient.tsx | 22 +- .../management/ProjectListClient.tsx | 20 +- .../OrderManagementListClient.tsx | 23 +- .../partners/PartnerListClient.tsx | 23 +- .../pricing-management/PricingListClient.tsx | 23 +- .../ProgressBillingManagementListClient.tsx | 24 +- .../site-briefings/SiteBriefingListClient.tsx | 23 +- .../SiteManagementListClient.tsx | 22 +- .../StructureReviewListClient.tsx | 30 +- .../UtilityManagementListClient.tsx | 17 +- .../worker-status/WorkerStatusListClient.tsx | 26 +- .../common/ScheduleCalendar/DayCell.tsx | 25 +- .../common/ScheduleCalendar/MonthView.tsx | 28 +- .../common/ScheduleCalendar/WeekView.tsx | 16 +- src/components/ui/calendar.tsx | 39 +++ src/constants/calendarEvents.ts | 102 +++++++ src/hooks/useListHandlers.ts | 40 +++ src/utils/date.ts | 25 ++ src/utils/formatAmount.ts | 10 + 36 files changed, 852 insertions(+), 530 deletions(-) create mode 100644 claudedocs/[ANALYSIS-2026-02-05] SAM-ERP-MES-정체성-분석.md create mode 100644 claudedocs/[IMPL-2026-02-05] formatter-commonization-plan.md create mode 100644 src/constants/calendarEvents.ts create mode 100644 src/hooks/useListHandlers.ts diff --git a/claudedocs/[ANALYSIS-2026-02-05] SAM-ERP-MES-정체성-분석.md b/claudedocs/[ANALYSIS-2026-02-05] SAM-ERP-MES-정체성-분석.md new file mode 100644 index 00000000..840d9e02 --- /dev/null +++ b/claudedocs/[ANALYSIS-2026-02-05] SAM-ERP-MES-정체성-분석.md @@ -0,0 +1,256 @@ +# SAM 프로젝트 정체성 및 현장 효용성 분석 + +> 작성일: 2026-02-05 +> 목적: ERP/MES 관점에서 SAM 시스템의 포지션, 강점/약점, 현장 효용성 분석 + +--- + +## 1. SAM의 정체성: "제조+설치 통합형 ERP/MES" + +### 포지셔닝 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ SAM 시스템 포지션 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Pure MES ◄────────── SAM ──────────► Pure ERP │ +│ (공장 실행) │ (경영 관리) │ +│ │ │ +│ ┌────────┴────────┐ │ +│ │ 70% ERP │ │ +│ │ 30% MES │ │ +│ │ + 건설 프로젝트 │ │ +│ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +SAM은 **순수 MES도 아니고 순수 ERP도 아닌**, 제조업체가 실제로 필요로 하는 기능들을 통합한 시스템이다. + +### 타겟 산업: 블라인드/셔터 제조 + 설치 + +| 특징 | SAM의 대응 | +|------|-----------| +| 주문생산(Make-to-Order) | 수주 → 생산지시 → 작업실적 흐름 | +| 다품종 소량생산 | 동적 품목 마스터 (빌더 시스템) | +| 설치 서비스 병행 | 건설/시공 프로젝트 모듈 | +| 품질 인증 필요 | QMS 검사성적서 시스템 | +| 중소기업 규모 | SaaS 멀티테넌트 구조 | + +--- + +## 2. ERP 관점 분석 + +### 커버리지 + +| ERP 영역 | SAM 구현 수준 | 비고 | +|----------|-------------|------| +| **재무회계** | ⭐⭐⭐⭐ (80%) | 매입/매출/입출금/어음/카드/채권 | +| **영업관리** | ⭐⭐⭐⭐ (85%) | 견적→수주→생산지시 연동 | +| **구매관리** | ⭐⭐⭐ (70%) | 입고 중심, 발주 모듈 약함 | +| **재고관리** | ⭐⭐⭐ (65%) | 재고현황 중심, 창고이동 미흡 | +| **인사관리** | ⭐⭐⭐⭐ (80%) | 근태/급여/휴가/문서 | +| **전자결재** | ⭐⭐⭐ (70%) | 기안/결재/참조 기본 구조 | +| **프로젝트** | ⭐⭐⭐⭐⭐ (90%) | 건설 모듈이 매우 정교함 | + +### ERP로서의 강점 + +1. **영업-생산 연동**: 수주가 바로 생산지시로 연결되는 구조 +2. **프로젝트 관리**: 입찰→계약→시공→정산까지 풀 사이클 +3. **회계 통합**: 매출/매입이 거래처원장과 연동 +4. **멀티테넌트**: 신규 고객사 온보딩이 빠름 + +### ERP로서의 약점 + +1. **구매/발주**: 입고 위주, 구매요청→발주→입고 흐름 미흡 +2. **원가계산**: 제조원가 계산 로직이 명시적이지 않음 +3. **창고관리**: 다창고, 로케이션 관리 부재 +4. **BI/분석**: 대시보드는 있으나 심층 분석 약함 + +--- + +## 3. MES 관점 분석 + +### 커버리지 + +| MES 영역 | SAM 구현 수준 | 비고 | +|----------|-------------|------| +| **작업지시** | ⭐⭐⭐⭐ (80%) | 생산지시 생성/관리 | +| **작업실적** | ⭐⭐⭐⭐ (80%) | 실적 입력/조회 | +| **품질관리** | ⭐⭐⭐⭐⭐ (90%) | 다양한 검사성적서 | +| **설비관리** | ⭐ (20%) | 거의 없음 | +| **실시간 모니터링** | ⭐⭐⭐ (60%) | 대시보드 있음, PLC 연동 없음 | +| **작업자 화면** | ⭐⭐⭐⭐ (75%) | 현장 터치 인터페이스 | +| **추적성(Traceability)** | ⭐⭐⭐ (65%) | 로트 추적 기본 구조 | + +### MES로서의 강점 + +1. **품질 시스템**: QMS가 상당히 정교함 (6종 검사성적서) +2. **작업자 친화적**: 현장용 작업자 화면 별도 존재 +3. **생산-영업 연결**: 수주 기반 생산이라 주문 추적 용이 + +### MES로서의 약점 + +1. **설비 연동 없음**: PLC, 바코드 스캐너 등 현장 장비 연동 부재 +2. **실시간성 부족**: 폴링 기반, 실시간 푸시 아님 +3. **공정 스케줄링**: 단순 작업지시, APS(고급계획) 없음 +4. **설비 모니터링**: OEE, 설비 가동률 등 없음 + +--- + +## 4. 현장 효용성 평가 + +### 실제로 잘 맞는 업종 + +``` +✅ 주문생산 제조업 (블라인드, 가구, 인테리어 자재) +✅ 설치 서비스 병행 업체 +✅ 다품종 소량생산 +✅ 품질 인증 필요 업종 (ISO, KS 등) +✅ 직원 50명 이하 중소기업 +✅ IT 인력이 부족한 회사 (SaaS로 운영부담 최소화) +``` + +### 맞지 않는 업종 + +``` +❌ 대량생산 (자동차, 반도체) - MES 깊이 부족 +❌ 연속공정 (화학, 식품) - 배치/레시피 관리 없음 +❌ 설비 집약 산업 - 설비 연동/모니터링 없음 +❌ 복잡한 원가계산 필요 업종 - 원가 모듈 약함 +❌ 대기업 (100명+) - 워크플로우 복잡도 한계 +``` + +### 현장에서의 실제 가치 + +| 관점 | 효용 | +|------|------| +| **경영진** | 수주~매출까지 한눈에, 프로젝트별 손익 파악 | +| **영업팀** | 견적→수주→생산현황 실시간 확인 | +| **생산팀** | 작업지시 받고 실적 입력, 품질 기록 | +| **품질팀** | 검사성적서 발행, 인증심사 대응 | +| **경리팀** | 매입/매출/입출금 통합 관리 | +| **현장 작업자** | 터치 화면으로 작업 확인/실적 입력 | + +--- + +## 5. 경쟁 포지션 + +### vs 범용 ERP (더존, 영림원) + +| 항목 | SAM | 범용 ERP | +|------|-----|---------| +| 제조 특화 | ⭐⭐⭐⭐ | ⭐⭐ | +| 건설/시공 | ⭐⭐⭐⭐⭐ | ⭐ | +| 회계 깊이 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| 커스터마이징 | ⭐⭐⭐⭐ (빌더) | ⭐⭐ | +| 도입 비용 | 낮음 (SaaS) | 높음 | + +### vs 전문 MES (포스코ICT, 미라콤) + +| 항목 | SAM | 전문 MES | +|------|-----|---------| +| 설비 연동 | ❌ | ⭐⭐⭐⭐⭐ | +| 실시간성 | ⭐⭐ | ⭐⭐⭐⭐⭐ | +| 품질 관리 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | +| ERP 통합 | ⭐⭐⭐⭐⭐ | ⭐⭐ (별도 연동) | +| 도입 기간 | 짧음 | 길음 | + +### SAM의 틈새 (Niche) + +``` +"전문 MES는 과하고, 범용 ERP는 제조 기능이 부족한" +중소 제조+설치 업체를 위한 통합 솔루션 +``` + +--- + +## 6. 빌더 확장 시 기대효과 + +현재 품목기준관리 빌더를 다른 영역으로 확장하면: + +| 확장 영역 | 예상 효과 | +|----------|----------| +| **폼 빌더** (등록/수정) | 신규 업종 대응 시 개발 50% 절감 | +| **리스트 빌더** (조회) | 화면 추가/변경 무코딩 가능 | +| **문서 빌더** (성적서) | 업종별 양식 빠른 대응 | +| **워크플로우 빌더** | 결재/승인 프로세스 설정화 | + +**신규 업체 온보딩 시나리오**: +``` +현재: 요구분석 → 개발 → 테스트 → 배포 (4-8주) +목표: 요구분석 → 빌더 설정 → 배포 (1-2주) +``` + +--- + +## 7. 종합 평가 + +### SAM의 정체성 한 문장 + +> **"주문생산 중소 제조업을 위한 ERP+MES 통합 SaaS로, 생산-품질-영업-회계를 하나로 연결하고, 설치 프로젝트까지 관리하는 올인원 솔루션"** + +### 핵심 차별점 + +1. **제조+설치 통합** - 대부분의 시스템이 둘 중 하나만 함 +2. **품질 시스템 내장** - QMS가 기본 탑재 +3. **빌더 기반 확장성** - 업종별 커스터마이징 용이 +4. **SaaS 멀티테넌트** - 도입 부담 최소화 + +### 발전 방향 제안 + +| 단기 | 중기 | 장기 | +|------|------|------| +| 빌더 → 리스트까지 확장 | 바코드/QR 스캐닝 | 설비 연동 (IoT) | +| 발주 모듈 보강 | 모바일 앱 강화 | AI 수요 예측 | +| 원가계산 기본 기능 | 실시간 알림 (WebSocket) | APS 스케줄링 | + +--- + +## 8. 시스템 규모 현황 + +### 프로젝트 스케일 + +- **24개** 주요 기능 모듈 +- **250+** 페이지 +- **900+** 컴포넌트 파일 +- 멀티테넌트 아키텍처 +- 다국어 지원 (한국어, 영어, 일본어) + +### 모듈별 복잡도 + +| 모듈 | 복잡도 | 페이지 수 | 핵심 기능 | +|------|--------|----------|----------| +| Construction | ⭐⭐⭐⭐⭐ | 57 | 프로젝트 풀 라이프사이클 | +| Accounting | ⭐⭐⭐⭐ | 31 | 재무 관리 전체 | +| Production | ⭐⭐⭐⭐ | 12 | 실시간 MES 코어 | +| Quality | ⭐⭐⭐⭐ | 24 | 다중 검사 QMS | +| Master Data | ⭐⭐⭐⭐⭐ | 12 | 동적 폼 템플릿 | +| Sales | ⭐⭐⭐ | 20 | 견적→수주 흐름 | +| HR | ⭐⭐⭐ | 17 | 직원 라이프사이클 | +| Material | ⭐⭐ | 6 | 재고 & 입고 | +| Outbound | ⭐⭐ | 7 | 출고 & 배차 | + +--- + +## 부록: 기술 스택 + +**Frontend:** +- Next.js 15 (App Router) +- React 18 +- TypeScript +- Tailwind CSS +- Radix UI +- Zustand + +**Backend:** +- PHP Laravel API (별도 코드베이스) +- MySQL/MariaDB +- JWT 인증 +- 멀티테넌트 아키텍처 + +**인프라:** +- HttpOnly 쿠키 보안 +- 멀티테넌트 데이터 격리 +- RESTful API 설계 diff --git a/claudedocs/[IMPL-2026-02-05] formatter-commonization-plan.md b/claudedocs/[IMPL-2026-02-05] formatter-commonization-plan.md new file mode 100644 index 00000000..5cb6e846 --- /dev/null +++ b/claudedocs/[IMPL-2026-02-05] formatter-commonization-plan.md @@ -0,0 +1,88 @@ +# 금액/날짜 포맷터 공통화 계획 + +> 작성일: 2026-02-05 +> 상태: ✅ 완료 +> 목적: 중복 정의된 formatAmount, formatDate 함수를 공통 유틸로 통합 + +--- + +## 📊 현황 분석 + +### 이미 존재하는 유틸 + +| 파일 | 함수 | 설명 | +|------|------|------| +| `src/utils/formatAmount.ts` | `formatAmount()` | 자동 만원 변환 (1만 이상 → "N만원") | +| | `formatAmountWon()` | 항상 원 단위 ("N원") | +| | `formatAmountManwon()` | 항상 만원 단위 ("N만원") | +| | `formatKoreanAmount()` | 억/만 축약 ("1억 5,000만") | +| | `formatNumber()` | **신규** 단순 천단위 콤마 | +| `src/utils/date.ts` | `getLocalDateString()` | YYYY-MM-DD 반환 | +| | `getTodayString()` | 오늘 날짜 YYYY-MM-DD | +| | `formatDateForInput()` | input용 날짜 변환 | +| | `formatDate()` | **신규** YYYY-MM-DD 표시용 | +| | `formatDateRange()` | **신규** "시작 ~ 종료" 형식 | + +--- + +## 📊 결과 요약 + +### 마이그레이션 완료 파일 + +#### formatAmount → formatNumber (12개 파일) ✅ +| 파일 | 상태 | +|------|------| +| `construction/contract/ContractListClient.tsx` | ✅ 완료 | +| `construction/contract/ContractDetailForm.tsx` | ✅ 완료 | +| `construction/bidding/BiddingListClient.tsx` | ✅ 완료 | +| `construction/bidding/BiddingDetailForm.tsx` | ✅ 완료 | +| `construction/estimates/EstimateListClient.tsx` | ✅ 완료 | +| `construction/estimates/modals/EstimateDocumentContent.tsx` | ✅ 완료 | +| `construction/handover-report/HandoverReportListClient.tsx` | ✅ 완료 | +| `construction/handover-report/HandoverReportDetailForm.tsx` | ✅ 완료 | +| `construction/handover-report/modals/HandoverReportDocumentModal.tsx` | ✅ 완료 | +| `construction/utility-management/UtilityManagementListClient.tsx` | ✅ 완료 | +| `construction/estimates/utils/formatters.ts` | ✅ re-export로 변경 | + +#### formatDate 공통화 (7개 파일) ✅ +| 파일 | 상태 | +|------|------| +| `construction/contract/ContractListClient.tsx` | ✅ 완료 (formatDateRange) | +| `construction/bidding/BiddingListClient.tsx` | ✅ 완료 | +| `construction/handover-report/HandoverReportListClient.tsx` | ✅ 완료 (formatDateRange) | +| `construction/utility-management/UtilityManagementListClient.tsx` | ✅ 완료 | +| `construction/issue-management/IssueManagementListClient.tsx` | ✅ 완료 | +| `construction/structure-review/StructureReviewListClient.tsx` | ✅ 완료 | +| `construction/management/ConstructionDetailClient.tsx` | ✅ 완료 | + +#### 마이그레이션 제외 (한글 형식 유지) +| 파일 | 사유 | +|------|------| +| `handover-report/modals/HandoverReportDocumentModal.tsx` | 한글 형식 ("년 월 일") | +| `order-management/modals/OrderDocumentModal.tsx` | 한글 형식 ("년 월 일") | + +--- + +## 📋 효과 + +| 항목 | Before | After | +|------|--------|-------| +| formatAmount 정의 | 12개 파일 | 1개 파일 (`formatNumber`) | +| formatDate 정의 | 8개 파일 | 1개 파일 | +| 중복 코드 라인 | ~150줄 | 0줄 | +| 포맷 변경 시 수정 | 20개 파일 | 1개 파일 | + +--- + +## ⚠️ 주의사항 + +1. **기존 formatAmount()와 formatNumber() 차이** + - 기존 `formatAmount()`: 자동 만원 변환 (유지됨) + - 신규 `formatNumber()`: 단순 천단위 콤마만 + +2. **한글 날짜 형식은 별도 유지** + - 문서 모달에서 사용하는 "년 월 일" 형식은 로컬 유지 + - 공통 `formatDate()`는 YYYY-MM-DD 형식만 처리 + +3. **backward compatibility** + - `estimates/utils/formatters.ts`는 `formatNumber`를 `formatAmount`로 re-export diff --git a/src/components/business/CEODashboard/sections/CalendarSection.tsx b/src/components/business/CEODashboard/sections/CalendarSection.tsx index adbb510d..e215855f 100644 --- a/src/components/business/CEODashboard/sections/CalendarSection.tsx +++ b/src/components/business/CEODashboard/sections/CalendarSection.tsx @@ -15,6 +15,7 @@ import { import { Plus, ExternalLink } from 'lucide-react'; import { ScheduleCalendar } from '@/components/common/ScheduleCalendar'; import type { ScheduleEvent } from '@/components/common/ScheduleCalendar/types'; +import { CALENDAR_EVENTS, type CalendarEvent } from '@/constants/calendarEvents'; import type { CalendarScheduleItem, CalendarViewType, @@ -38,6 +39,8 @@ const SCHEDULE_TYPE_COLORS: Record = { construction: 'purple', issue: 'red', other: 'gray', + holiday: 'red', + tax: 'orange', }; // 이슈 뱃지별 색상 @@ -118,7 +121,22 @@ export function CalendarSection({ return issuesWithDate; }, [issuesWithDate, taskFilter]); - // ScheduleCalendar용 이벤트 변환 (스케줄 + 이슈 통합) + // 현재 연도의 공휴일/세금일정 + const staticEvents: ScheduleEvent[] = useMemo(() => { + const year = currentDate.getFullYear(); + const events = CALENDAR_EVENTS[year] || []; + + return events.map((event: CalendarEvent) => ({ + id: `${event.type}-${event.date}`, + title: event.type === 'holiday' ? `🔴 ${event.name}` : `🟠 ${event.name}`, + startDate: event.date, + endDate: event.date, + color: SCHEDULE_TYPE_COLORS[event.type] || 'gray', + data: { ...event, _type: event.type as 'holiday' | 'tax' }, + })); + }, [currentDate]); + + // ScheduleCalendar용 이벤트 변환 (스케줄 + 이슈 + 공휴일/세금 통합) const calendarEvents: ScheduleEvent[] = useMemo(() => { const scheduleEvents = filteredSchedules.map((schedule) => ({ id: schedule.id, @@ -139,12 +157,12 @@ export function CalendarSection({ data: { ...issue, _type: 'issue' as const }, })); - return [...scheduleEvents, ...issueEvents]; - }, [filteredSchedules, filteredIssues]); + return [...staticEvents, ...scheduleEvents, ...issueEvents]; + }, [staticEvents, filteredSchedules, filteredIssues]); - // 선택된 날짜의 일정 + 이슈 목록 + // 선택된 날짜의 일정 + 이슈 + 공휴일/세금 목록 const selectedDateItems = useMemo(() => { - if (!selectedDate) return { schedules: [], issues: [] }; + if (!selectedDate) return { schedules: [], issues: [], staticEvents: [] }; // 로컬 타임존 기준으로 날짜 문자열 생성 (UTC 변환 방지) const year = selectedDate.getFullYear(); const month = String(selectedDate.getMonth() + 1).padStart(2, '0'); @@ -157,11 +175,14 @@ export function CalendarSection({ const dateIssues = filteredIssues.filter((issue) => issue.date === dateStr); - return { schedules: dateSchedules, issues: dateIssues }; - }, [selectedDate, filteredSchedules, filteredIssues]); + // 공휴일/세금일정 + const dateStaticEvents = staticEvents.filter((event) => event.startDate === dateStr); + + return { schedules: dateSchedules, issues: dateIssues, staticEvents: dateStaticEvents }; + }, [selectedDate, filteredSchedules, filteredIssues, staticEvents]); // 총 건수 계산 - const totalItemCount = selectedDateItems.schedules.length + selectedDateItems.issues.length; + const totalItemCount = selectedDateItems.schedules.length + selectedDateItems.issues.length + selectedDateItems.staticEvents.length; // 날짜 포맷 (기획서: "1월 6일 화요일") const formatSelectedDate = (date: Date) => { @@ -313,6 +334,30 @@ export function CalendarSection({ ) : (
+ {/* 공휴일/세금일정 목록 */} + {selectedDateItems.staticEvents.map((event) => { + const eventData = event.data as CalendarEvent & { _type: string }; + const isHoliday = eventData.type === 'holiday'; + return ( +
+
+ {isHoliday ? '🔴' : '🟠'} + {eventData.name} +
+
+ {isHoliday ? '공휴일' : '세금 신고 마감일'} +
+
+ ); + })} + {/* 일정 목록 */} {selectedDateItems.schedules.map((schedule) => (
@@ -392,14 +388,14 @@ export default function BiddingDetailForm({ {item.name} - {formatAmount(item.amount)}원 + {formatNumber(item.amount)}원 ))} 합계 - {formatAmount(expenseTotal)}원 + {formatNumber(expenseTotal)}원 @@ -465,26 +461,26 @@ export default function BiddingDetailForm({ {item.height?.toFixed(2) || '0.00'} {item.weight?.toFixed(2) || '0.00'} {item.area?.toFixed(2) || '0.00'} - {formatAmount(item.steelScreen || 0)} - {formatAmount(item.caulking || 0)} - {formatAmount(item.rail || 0)} - {formatAmount(item.bottom || 0)} - {formatAmount(item.boxReinforce || 0)} - {formatAmount(item.shaft || 0)} - {formatAmount(item.painting || 0)} - {formatAmount(item.motor || 0)} - {formatAmount(item.controller || 0)} - {formatAmount(item.widthConstruction || 0)} - {formatAmount(item.heightConstruction || 0)} - {formatAmount(item.unitPrice || 0)} + {formatNumber(item.steelScreen || 0)} + {formatNumber(item.caulking || 0)} + {formatNumber(item.rail || 0)} + {formatNumber(item.bottom || 0)} + {formatNumber(item.boxReinforce || 0)} + {formatNumber(item.shaft || 0)} + {formatNumber(item.painting || 0)} + {formatNumber(item.motor || 0)} + {formatNumber(item.controller || 0)} + {formatNumber(item.widthConstruction || 0)} + {formatNumber(item.heightConstruction || 0)} + {formatNumber(item.unitPrice || 0)} {item.expenseRate || 0} - {formatAmount(item.expense || 0)} + {formatNumber(item.expense || 0)} {item.quantity || 0} - {formatAmount(item.cost || 0)} - {formatAmount(item.costExecution || 0)} - {formatAmount(item.marginCost || 0)} - {formatAmount(item.marginCostExecution || 0)} - {formatAmount(item.expenseExecution || 0)} + {formatNumber(item.cost || 0)} + {formatNumber(item.costExecution || 0)} + {formatNumber(item.marginCost || 0)} + {formatNumber(item.marginCostExecution || 0)} + {formatNumber(item.expenseExecution || 0)} ))} {/* 합계 행 */} @@ -492,26 +488,26 @@ export default function BiddingDetailForm({ 합계 {estimateDetailTotals.weight.toFixed(2)} {estimateDetailTotals.area.toFixed(2)} - {formatAmount(estimateDetailTotals.steelScreen)} - {formatAmount(estimateDetailTotals.caulking)} - {formatAmount(estimateDetailTotals.rail)} - {formatAmount(estimateDetailTotals.bottom)} - {formatAmount(estimateDetailTotals.boxReinforce)} - {formatAmount(estimateDetailTotals.shaft)} - {formatAmount(estimateDetailTotals.painting)} - {formatAmount(estimateDetailTotals.motor)} - {formatAmount(estimateDetailTotals.controller)} - {formatAmount(estimateDetailTotals.widthConstruction)} - {formatAmount(estimateDetailTotals.heightConstruction)} - {formatAmount(estimateDetailTotals.unitPrice)} + {formatNumber(estimateDetailTotals.steelScreen)} + {formatNumber(estimateDetailTotals.caulking)} + {formatNumber(estimateDetailTotals.rail)} + {formatNumber(estimateDetailTotals.bottom)} + {formatNumber(estimateDetailTotals.boxReinforce)} + {formatNumber(estimateDetailTotals.shaft)} + {formatNumber(estimateDetailTotals.painting)} + {formatNumber(estimateDetailTotals.motor)} + {formatNumber(estimateDetailTotals.controller)} + {formatNumber(estimateDetailTotals.widthConstruction)} + {formatNumber(estimateDetailTotals.heightConstruction)} + {formatNumber(estimateDetailTotals.unitPrice)} - - {formatAmount(estimateDetailTotals.expense)} + {formatNumber(estimateDetailTotals.expense)} {estimateDetailTotals.quantity} - {formatAmount(estimateDetailTotals.cost)} - {formatAmount(estimateDetailTotals.costExecution)} - {formatAmount(estimateDetailTotals.marginCost)} - {formatAmount(estimateDetailTotals.marginCostExecution)} - {formatAmount(estimateDetailTotals.expenseExecution)} + {formatNumber(estimateDetailTotals.cost)} + {formatNumber(estimateDetailTotals.costExecution)} + {formatNumber(estimateDetailTotals.marginCost)} + {formatNumber(estimateDetailTotals.marginCostExecution)} + {formatNumber(estimateDetailTotals.expenseExecution)} )} diff --git a/src/components/business/construction/bidding/BiddingListClient.tsx b/src/components/business/construction/bidding/BiddingListClient.tsx index 2ad4ca39..9caa5393 100644 --- a/src/components/business/construction/bidding/BiddingListClient.tsx +++ b/src/components/business/construction/bidding/BiddingListClient.tsx @@ -11,9 +11,9 @@ * - 등록 버튼 없음 (견적완료 시 자동 등록) */ -import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; +import { useState, useMemo, useEffect } from 'react'; import { FileText, Clock, Trophy, Pencil } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -33,6 +33,8 @@ import { BIDDING_STATUS_LABELS, } from './types'; import { getBiddingList, getBiddingStats, deleteBidding, deleteBiddings } from './actions'; +import { formatNumber } from '@/utils/formatAmount'; +import { formatDate } from '@/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ @@ -64,32 +66,14 @@ const MOCK_BIDDERS = [ { value: 'lee', label: '이영희' }, ]; -// 금액 포맷팅 -function formatAmount(amount: number): string { - return new Intl.NumberFormat('ko-KR').format(amount); -} - -// 날짜 포맷팅 -function formatDate(dateStr: string | null): string { - if (!dateStr) return '-'; - const date = new Date(dateStr); - return date - .toLocaleDateString('ko-KR', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }) - .replace(/\. /g, '-') - .replace('.', ''); -} - interface BiddingListClientProps { initialData?: Bidding[]; initialStats?: BiddingStats; } export default function BiddingListClient({ initialData = [], initialStats }: BiddingListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit } = useListHandlers('construction/project/bidding'); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== // Stats 카드 클릭 필터용 @@ -113,21 +97,6 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi } }, [initialStats]); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (item: Bidding) => { - router.push(`/ko/construction/project/bidding/${item.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (item: Bidding) => { - router.push(`/ko/construction/project/bidding/${item.id}?mode=edit`); - }, - [router] - ); - // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ @@ -363,7 +332,7 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi {item.projectName} {item.bidderName} {item.totalCount} - {formatAmount(item.biddingAmount)} + {formatNumber(item.biddingAmount)} {formatDate(item.bidDate)} {formatDate(item.submissionDate)} {formatDate(item.confirmDate)} @@ -411,7 +380,7 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi onClick={() => handleRowClick(item)} details={[ { label: '거래처', value: item.partnerName }, - { label: '입찰금액', value: `${formatAmount(item.biddingAmount)}원` }, + { label: '입찰금액', value: `${formatNumber(item.biddingAmount)}원` }, { label: '입찰일자', value: formatDate(item.biddingDate) }, { label: '총 개소', value: `${item.totalCount}` }, ]} diff --git a/src/components/business/construction/contract/ContractDetailForm.tsx b/src/components/business/construction/contract/ContractDetailForm.tsx index 60e86866..762f3547 100644 --- a/src/components/business/construction/contract/ContractDetailForm.tsx +++ b/src/components/business/construction/contract/ContractDetailForm.tsx @@ -40,11 +40,7 @@ import { getEmptyElectronicApproval, } from '../common'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; - -// 금액 포맷팅 -function formatAmount(amount: number): string { - return new Intl.NumberFormat('ko-KR').format(amount); -} +import { formatNumber } from '@/utils/formatAmount'; interface ContractDetailFormProps { mode: 'view' | 'edit' | 'create'; @@ -365,7 +361,7 @@ export default function ContractDetailForm({ { const value = e.target.value.replace(/[^0-9]/g, ''); handleFieldChange('contractAmount', parseInt(value) || 0); diff --git a/src/components/business/construction/contract/ContractListClient.tsx b/src/components/business/construction/contract/ContractListClient.tsx index 0e02d547..55bb2bf2 100644 --- a/src/components/business/construction/contract/ContractListClient.tsx +++ b/src/components/business/construction/contract/ContractListClient.tsx @@ -10,9 +10,9 @@ * - filterConfig (multi: 거래처, 계약담당자, 공사PM / single: 상태, 정렬) */ -import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; +import { useState, useMemo, useEffect } from 'react'; import { FileText, Clock, CheckCircle, Pencil, Trash2 } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -32,6 +32,8 @@ import { CONTRACT_STATUS_LABELS, } from './types'; import { getContractList, getContractStats, deleteContract, deleteContracts } from './actions'; +import { formatNumber } from '@/utils/formatAmount'; +import { formatDate, formatDateRange } from '@/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ @@ -67,40 +69,14 @@ const MOCK_CONSTRUCTION_PMS = [ { value: 'park', label: '박PM' }, ]; -// 금액 포맷팅 -function formatAmount(amount: number): string { - return new Intl.NumberFormat('ko-KR').format(amount); -} - -// 날짜 포맷팅 -function formatDate(dateStr: string | null): string { - if (!dateStr) return '-'; - const date = new Date(dateStr); - return date - .toLocaleDateString('ko-KR', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }) - .replace(/\. /g, '-') - .replace('.', ''); -} - -// 계약기간 포맷팅 -function formatPeriod(startDate: string | null, endDate: string | null): string { - const start = formatDate(startDate); - const end = formatDate(endDate); - if (start === '-' && end === '-') return '-'; - return `${start} ~ ${end}`; -} - interface ContractListClientProps { initialData?: Contract[]; initialStats?: ContractStats; } export default function ContractListClient({ initialData = [], initialStats }: ContractListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit } = useListHandlers('construction/project/contract'); // ===== 외부 상태 ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all'); @@ -120,21 +96,6 @@ export default function ContractListClient({ initialData = [], initialStats }: C } }, [initialStats]); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (item: Contract) => { - router.push(`/ko/construction/project/contract/${item.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (item: Contract) => { - router.push(`/ko/construction/project/contract/${item.id}?mode=edit`); - }, - [router] - ); - // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ @@ -376,9 +337,9 @@ export default function ContractListClient({ initialData = [], initialStats }: C {item.contractManagerName} {item.constructionPMName || '-'} {item.totalLocations} - {formatAmount(item.contractAmount)}원 + {formatNumber(item.contractAmount)}원 - {formatPeriod(item.contractStartDate, item.contractEndDate)} + {formatDateRange(item.contractStartDate, item.contractEndDate)} {CONTRACT_STATUS_LABELS[item.status]} @@ -433,7 +394,7 @@ export default function ContractListClient({ initialData = [], initialStats }: C details={[ { label: '거래처', value: item.partnerName }, { label: '총 개소', value: `${item.totalLocations}개` }, - { label: '계약금액', value: `${formatAmount(item.contractAmount)}원` }, + { label: '계약금액', value: `${formatNumber(item.contractAmount)}원` }, { label: '계약담당자', value: item.contractManagerName }, { label: '공사PM', value: item.constructionPMName || '-' }, ]} diff --git a/src/components/business/construction/estimates/EstimateListClient.tsx b/src/components/business/construction/estimates/EstimateListClient.tsx index 32a72f87..f58db1fc 100644 --- a/src/components/business/construction/estimates/EstimateListClient.tsx +++ b/src/components/business/construction/estimates/EstimateListClient.tsx @@ -10,9 +10,9 @@ * - filterConfig (multi: 거래처, 견적자 / single: 상태, 정렬) */ -import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; +import { useState, useMemo, useEffect } from 'react'; import { FileText, FileTextIcon, Clock, FileCheck, Pencil } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -33,6 +33,7 @@ import { } from './types'; import { getEstimateList, getEstimateStats, deleteEstimate, deleteEstimates, getClientOptions, getUserOptions } from './actions'; import type { ClientOption, UserOption } from './actions'; +import { formatNumber } from '@/utils/formatAmount'; // 테이블 컬럼 정의 const tableColumns = [ @@ -49,18 +50,14 @@ const tableColumns = [ { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; -// 금액 포맷팅 -function formatAmount(amount: number): string { - return new Intl.NumberFormat('ko-KR').format(amount); -} - interface EstimateListClientProps { initialData?: Estimate[]; initialStats?: EstimateStats; } export default function EstimateListClient({ initialData = [], initialStats }: EstimateListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit } = useListHandlers('construction/project/bidding/estimates'); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== // Stats 카드 클릭 필터용 @@ -103,21 +100,6 @@ export default function EstimateListClient({ initialData = [], initialStats }: E }); }, []); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (item: Estimate) => { - router.push(`/ko/construction/project/bidding/estimates/${item.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (item: Estimate) => { - router.push(`/ko/construction/project/bidding/estimates/${item.id}?mode=edit`); - }, - [router] - ); - // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ @@ -346,7 +328,7 @@ export default function EstimateListClient({ initialData = [], initialStats }: E {item.projectName} {item.estimatorName} {item.itemCount} - {formatAmount(item.estimateAmount)} + {formatNumber(item.estimateAmount)} {item.completedDate || '-'} {item.bidDate || '-'} @@ -391,7 +373,7 @@ export default function EstimateListClient({ initialData = [], initialStats }: E details={[ { label: '거래처', value: item.partnerName }, { label: '견적자', value: item.estimatorName }, - { label: '견적금액', value: `${formatAmount(item.estimateAmount)}원` }, + { label: '견적금액', value: `${formatNumber(item.estimateAmount)}원` }, { label: '입찰일', value: item.bidDate || '-' }, ]} /> diff --git a/src/components/business/construction/estimates/modals/EstimateDocumentContent.tsx b/src/components/business/construction/estimates/modals/EstimateDocumentContent.tsx index 408a7534..882810ef 100644 --- a/src/components/business/construction/estimates/modals/EstimateDocumentContent.tsx +++ b/src/components/business/construction/estimates/modals/EstimateDocumentContent.tsx @@ -12,11 +12,7 @@ import type { EstimateDetailFormData } from '../types'; import type { CompanyInfo } from '../actions'; import { DocumentHeader } from '@/components/document-system'; - -// 금액 포맷팅 -function formatAmount(amount: number): string { - return new Intl.NumberFormat('ko-KR').format(amount); -} +import { formatNumber } from '@/utils/formatAmount'; // 금액을 한글로 변환 function amountToKorean(amount: number): string { @@ -169,9 +165,9 @@ export function EstimateDocumentContent({ data }: EstimateDocumentContentProps) {item.name} {item.quantity} {item.unit} - {formatAmount(item.materialCost)} - {formatAmount(item.laborCost)} - {formatAmount(item.totalCost)} + {formatNumber(item.materialCost)} + {formatNumber(item.laborCost)} + {formatNumber(item.totalCost)} {item.remarks} )) @@ -180,13 +176,13 @@ export function EstimateDocumentContent({ data }: EstimateDocumentContentProps) 합 계 - {formatAmount(formData.summaryItems.reduce((sum, item) => sum + item.materialCost, 0))} + {formatNumber(formData.summaryItems.reduce((sum, item) => sum + item.materialCost, 0))} - {formatAmount(formData.summaryItems.reduce((sum, item) => sum + item.laborCost, 0))} + {formatNumber(formData.summaryItems.reduce((sum, item) => sum + item.laborCost, 0))} - ₩{formatAmount(formData.summaryItems.reduce((sum, item) => sum + item.totalCost, 0))} + ₩{formatNumber(formData.summaryItems.reduce((sum, item) => sum + item.totalCost, 0))} @@ -242,16 +238,16 @@ export function EstimateDocumentContent({ data }: EstimateDocumentContentProps) {index + 1} {item.name} {item.material} - {formatAmount(item.width)} - {formatAmount(item.height)} + {formatNumber(item.width)} + {formatNumber(item.height)} {item.quantity} SET - {formatAmount(item.unitPrice)} - {formatAmount(item.materialCost)} - {formatAmount(item.laborCost)} - {formatAmount(item.laborCost * item.quantity)} - {formatAmount(item.totalPrice)} - {formatAmount(item.totalCost)} + {formatNumber(item.unitPrice)} + {formatNumber(item.materialCost)} + {formatNumber(item.laborCost)} + {formatNumber(item.laborCost * item.quantity)} + {formatNumber(item.totalPrice)} + {formatNumber(item.totalCost)} )) )} @@ -264,15 +260,15 @@ export function EstimateDocumentContent({ data }: EstimateDocumentContentProps) SET - {formatAmount(formData.detailItems.reduce((sum, item) => sum + item.materialCost, 0))} + {formatNumber(formData.detailItems.reduce((sum, item) => sum + item.materialCost, 0))} - {formatAmount(formData.detailItems.reduce((sum, item) => sum + item.laborCost * item.quantity, 0))} + {formatNumber(formData.detailItems.reduce((sum, item) => sum + item.laborCost * item.quantity, 0))} - {formatAmount(formData.detailItems.reduce((sum, item) => sum + item.totalCost, 0))} + {formatNumber(formData.detailItems.reduce((sum, item) => sum + item.totalCost, 0))} {/* 비고 행 */} diff --git a/src/components/business/construction/estimates/utils/formatters.ts b/src/components/business/construction/estimates/utils/formatters.ts index a8e35453..104c8173 100644 --- a/src/components/business/construction/estimates/utils/formatters.ts +++ b/src/components/business/construction/estimates/utils/formatters.ts @@ -1,4 +1,2 @@ -// 금액 포맷팅 -export function formatAmount(amount: number): string { - return new Intl.NumberFormat('ko-KR').format(amount); -} \ No newline at end of file +// 공통 유틸 re-export (backward compatibility) +export { formatNumber as formatAmount } from '@/utils/formatAmount'; diff --git a/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx b/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx index 740e08e1..fcd9026b 100644 --- a/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx +++ b/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx @@ -49,11 +49,7 @@ import { type ElectronicApproval, getEmptyElectronicApproval, } from '../common'; - -// 금액 포맷팅 -function formatAmount(amount: number): string { - return new Intl.NumberFormat('ko-KR').format(amount); -} +import { formatNumber } from '@/utils/formatAmount'; interface HandoverReportDetailFormProps { mode: 'view' | 'edit'; @@ -315,7 +311,7 @@ export default function HandoverReportDetailForm({ { const value = e.target.value.replace(/[^0-9]/g, ''); handleFieldChange('contractAmount', parseInt(value) || 0); @@ -493,7 +489,7 @@ export default function HandoverReportDetailForm({ {item.no} {item.name} {item.product} - {formatAmount(item.quantity)} + {formatNumber(item.quantity)} {isEditMode ? ( ( + 'construction/project/contract/handover-report' + ); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all'); @@ -127,21 +105,6 @@ export default function HandoverReportListClient({ } }, [initialStats]); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (report: HandoverReport) => { - router.push(`/ko/construction/project/contract/handover-report/${report.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (report: HandoverReport) => { - router.push(`/ko/construction/project/contract/handover-report/${report.id}?mode=edit`); - }, - [router] - ); - // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ @@ -363,9 +326,9 @@ export default function HandoverReportListClient({ {item.contractManagerName} {item.constructionPMName || '-'} {item.totalSites} - {formatAmount(item.contractAmount)} + {formatNumber(item.contractAmount)} - {formatPeriod(item.contractStartDate, item.contractEndDate)} + {formatDateRange(item.contractStartDate, item.contractEndDate)} @@ -410,7 +373,7 @@ export default function HandoverReportListClient({ onClick={() => handleRowClick(item)} details={[ { label: '거래처', value: item.partnerName }, - { label: '계약금액', value: `${formatAmount(item.contractAmount)}원` }, + { label: '계약금액', value: `${formatNumber(item.contractAmount)}원` }, { label: '계약담당자', value: item.contractManagerName }, { label: '총 개소', value: `${item.totalSites}개소` }, ]} diff --git a/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx b/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx index 2551116c..578c016c 100644 --- a/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx +++ b/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx @@ -8,12 +8,7 @@ import { import { toast } from 'sonner'; import { useRouter } from 'next/navigation'; import type { HandoverReportDetail } from '../types'; - -// 금액 포맷팅 -function formatAmount(amount: number | undefined | null): string { - if (amount === undefined || amount === null) return '0'; - return new Intl.NumberFormat('ko-KR').format(amount); -} +import { formatNumber } from '@/utils/formatAmount'; // 날짜 포맷팅 (년월) function formatYearMonth(dateStr: string | null): string { @@ -110,7 +105,7 @@ export function HandoverReportDocumentModal({ {/* 계약금액 (공급가액) */} 계약금액 (공급가액) - ₩ {formatAmount(report.contractAmount)} + ₩ {formatNumber(report.contractAmount)} {/* 계약 ITEM - 기획서: 구분, 수량, 비고 3컬럼 */} @@ -137,7 +132,7 @@ export function HandoverReportDocumentModal({ {item.name || '-'} 0 ? 'border-t' : ''}`}> - {formatAmount(item.quantity)} + {formatNumber(item.quantity)} 0 ? 'border-t' : ''}`}> {item.remark || '-'} @@ -160,7 +155,7 @@ export function HandoverReportDocumentModal({ 2차 배관 유무 {report.hasSecondaryPiping - ? `포함 (${formatAmount(report.secondaryPipingAmount)})` + ? `포함 (${formatNumber(report.secondaryPipingAmount)})` : '미포함'} @@ -168,7 +163,7 @@ export function HandoverReportDocumentModal({ 도장 & 코킹 유무 {report.hasCoating - ? `포함 (${formatAmount(report.coatingAmount)})` + ? `포함 (${formatNumber(report.coatingAmount)})` : '미포함'} @@ -182,9 +177,9 @@ export function HandoverReportDocumentModal({ 장비 외 실행금액
-
운반비 : {formatAmount(report.externalEquipmentCost?.shippingCost)}
-
양중장비 : {formatAmount(report.externalEquipmentCost?.highAltitudeWork)}
-
공과금 : {formatAmount(report.externalEquipmentCost?.publicExpense)}
+
운반비 : {formatNumber(report.externalEquipmentCost?.shippingCost)}
+
양중장비 : {formatNumber(report.externalEquipmentCost?.highAltitudeWork)}
+
공과금 : {formatNumber(report.externalEquipmentCost?.publicExpense)}
diff --git a/src/components/business/construction/issue-management/IssueManagementListClient.tsx b/src/components/business/construction/issue-management/IssueManagementListClient.tsx index 34615385..01b77ef4 100644 --- a/src/components/business/construction/issue-management/IssueManagementListClient.tsx +++ b/src/components/business/construction/issue-management/IssueManagementListClient.tsx @@ -12,8 +12,8 @@ */ import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { AlertTriangle, Pencil, Inbox, Clock, CheckCircle, XCircle, Undo2 } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -48,6 +48,7 @@ import { getIssueStats, withdrawIssues, } from './actions'; +import { formatDate } from '@/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ @@ -67,12 +68,6 @@ const tableColumns = [ { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; -// 날짜 포맷 -function formatDate(dateStr: string | null): string { - if (!dateStr) return '-'; - return dateStr.split('T')[0]; -} - interface IssueManagementListClientProps { initialData?: Issue[]; initialStats?: IssueStats; @@ -82,7 +77,10 @@ export default function IssueManagementListClient({ initialData = [], initialStats, }: IssueManagementListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit, router } = useListHandlers( + 'construction/project/issue-management' + ); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'received' | 'in_progress' | 'resolved' | 'unresolved'>('all'); @@ -105,21 +103,7 @@ export default function IssueManagementListClient({ } }, [initialStats]); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (item: Issue) => { - router.push(`/ko/construction/project/issue-management/${item.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (item: Issue) => { - router.push(`/ko/construction/project/issue-management/${item.id}?mode=edit`); - }, - [router] - ); - + // ===== 추가 핸들러 ===== const handleCreate = useCallback(() => { router.push('/ko/construction/project/issue-management?mode=new'); }, [router]); diff --git a/src/components/business/construction/item-management/ItemManagementClient.tsx b/src/components/business/construction/item-management/ItemManagementClient.tsx index 70772bd0..13e0e5d1 100644 --- a/src/components/business/construction/item-management/ItemManagementClient.tsx +++ b/src/components/business/construction/item-management/ItemManagementClient.tsx @@ -1,8 +1,8 @@ 'use client'; import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { format, startOfYear, endOfYear } from 'date-fns'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Package, Plus, Pencil, Trash2, PackageCheck } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; @@ -46,7 +46,8 @@ export default function ItemManagementClient({ initialData = [], initialStats, }: ItemManagementClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, router } = useListHandlers('construction/order/base-info/items'); const today = new Date(); // 날짜 상태 (당해년도 기본값) @@ -208,13 +209,7 @@ export default function ItemManagementClient({ } }, [selectedItems.size, paginatedData]); - const handleRowClick = useCallback( - (item: Item) => { - router.push(`/ko/construction/order/base-info/items/${item.id}?mode=view`); - }, - [router] - ); - + // ===== 추가 핸들러 (handleRowClick은 Hook에서 제공) ===== const handleCreate = useCallback(() => { router.push('/ko/construction/order/base-info/items?mode=new'); }, [router]); diff --git a/src/components/business/construction/labor-management/LaborManagementClient.tsx b/src/components/business/construction/labor-management/LaborManagementClient.tsx index ff7b17a5..412aad8d 100644 --- a/src/components/business/construction/labor-management/LaborManagementClient.tsx +++ b/src/components/business/construction/labor-management/LaborManagementClient.tsx @@ -12,8 +12,8 @@ */ import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { format, startOfYear, endOfYear } from 'date-fns'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Hammer, Pencil, Trash2, HardHat } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; @@ -52,7 +52,10 @@ export default function LaborManagementClient({ initialData = [], initialStats, }: LaborManagementClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit, router } = useListHandlers( + 'construction/order/base-info/labor' + ); const today = new Date(); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== @@ -96,21 +99,7 @@ export default function LaborManagementClient({ return value.toFixed(2); }, []); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (labor: Labor) => { - router.push(`/ko/construction/order/base-info/labor/${labor.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (labor: Labor) => { - router.push(`/ko/construction/order/base-info/labor/${labor.id}?mode=edit`); - }, - [router] - ); - + // ===== 추가 핸들러 ===== const handleCreate = useCallback(() => { router.push('/ko/construction/order/base-info/labor?mode=new'); }, [router]); diff --git a/src/components/business/construction/management/ConstructionDetailClient.tsx b/src/components/business/construction/management/ConstructionDetailClient.tsx index 0316668b..f6f9886b 100644 --- a/src/components/business/construction/management/ConstructionDetailClient.tsx +++ b/src/components/business/construction/management/ConstructionDetailClient.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; -import { getTodayString } from '@/utils/date'; +import { getTodayString, formatDate } from '@/utils/date'; import { Plus, Trash2, FileText, Upload, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -47,13 +47,6 @@ interface ConstructionDetailClientProps { mode: 'view' | 'edit'; } -// 날짜 포맷팅 -function formatDate(dateStr: string | null): string { - if (!dateStr) return '-'; - const date = new Date(dateStr); - return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; -} - export default function ConstructionDetailClient({ id, mode }: ConstructionDetailClientProps) { const router = useRouter(); diff --git a/src/components/business/construction/management/ConstructionManagementListClient.tsx b/src/components/business/construction/management/ConstructionManagementListClient.tsx index b8e4e42f..7d64c03e 100644 --- a/src/components/business/construction/management/ConstructionManagementListClient.tsx +++ b/src/components/business/construction/management/ConstructionManagementListClient.tsx @@ -15,8 +15,8 @@ */ import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { HardHat, Pencil, Clock, CheckCircle } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -75,7 +75,10 @@ export default function ConstructionManagementListClient({ initialData = [], initialStats, }: ConstructionManagementListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit, router } = useListHandlers( + 'construction/project/construction-management' + ); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'in_progress' | 'completed'>('all'); @@ -159,21 +162,6 @@ export default function ConstructionManagementListClient({ return dateStr.split('T')[0]; }, []); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (item: ConstructionManagement) => { - router.push(`/ko/construction/project/construction-management/${item.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (item: ConstructionManagement) => { - router.push(`/ko/construction/project/construction-management/${item.id}?mode=edit`); - }, - [router] - ); - // 달력 이벤트 핸들러 const handleCalendarDateClick = useCallback((date: Date) => { if (selectedCalendarDate && isSameDay(selectedCalendarDate, date)) { diff --git a/src/components/business/construction/management/ProjectListClient.tsx b/src/components/business/construction/management/ProjectListClient.tsx index f43e923f..ed4e1e09 100644 --- a/src/components/business/construction/management/ProjectListClient.tsx +++ b/src/components/business/construction/management/ProjectListClient.tsx @@ -1,8 +1,8 @@ 'use client'; import { useState, useMemo, useCallback, useEffect, Fragment } from 'react'; -import { useRouter } from 'next/navigation'; import { FolderKanban, ClipboardList, PlayCircle, CheckCircle2 } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -108,7 +108,8 @@ interface ProjectListClientProps { } export default function ProjectListClient({ initialData = [], initialStats }: ProjectListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, router } = useListHandlers('construction/project/execution-management'); // 상태 const [projects, setProjects] = useState(initialData); @@ -238,19 +239,6 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr } }, [selectedItems.size, paginatedData]); - const handleRowClick = useCallback( - (project: Project) => { - router.push(`/ko/construction/project/execution-management/${project.id}?mode=view`); - }, - [router] - ); - - const handleGanttProjectClick = useCallback( - (project: Project) => { - router.push(`/ko/construction/project/execution-management/${project.id}?mode=view`); - }, - [router] - ); // 금액 포맷 const formatAmount = (amount: number) => { @@ -400,7 +388,7 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr projects={chartProjects} viewMode={chartViewMode} currentDate={chartDate} - onProjectClick={handleGanttProjectClick} + onProjectClick={handleRowClick} onDateChange={setChartDate} />
diff --git a/src/components/business/construction/order-management/OrderManagementListClient.tsx b/src/components/business/construction/order-management/OrderManagementListClient.tsx index 06d56acd..d4e3c493 100644 --- a/src/components/business/construction/order-management/OrderManagementListClient.tsx +++ b/src/components/business/construction/order-management/OrderManagementListClient.tsx @@ -14,8 +14,8 @@ */ import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { Package, Pencil, Trash2 } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -84,7 +84,10 @@ export default function OrderManagementListClient({ initialData = [], initialStats, }: OrderManagementListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit, router } = useListHandlers( + 'construction/order/order-management' + ); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [startDate, setStartDate] = useState(''); @@ -157,21 +160,7 @@ export default function OrderManagementListClient({ return dateStr.split('T')[0]; }, []); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (order: Order) => { - router.push(`/ko/construction/order/order-management/${order.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (order: Order) => { - router.push(`/ko/construction/order/order-management/${order.id}?mode=edit`); - }, - [router] - ); - + // ===== 추가 핸들러 ===== const handleCreate = useCallback(() => { router.push('/ko/construction/order/order-management?mode=new'); }, [router]); diff --git a/src/components/business/construction/partners/PartnerListClient.tsx b/src/components/business/construction/partners/PartnerListClient.tsx index cce614f4..b8222c08 100644 --- a/src/components/business/construction/partners/PartnerListClient.tsx +++ b/src/components/business/construction/partners/PartnerListClient.tsx @@ -12,8 +12,8 @@ */ import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { Building2, AlertTriangle, Pencil, Trash2 } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -49,7 +49,10 @@ interface PartnerListClientProps { } export default function PartnerListClient({ initialData = [], initialStats }: PartnerListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit, router } = useListHandlers( + 'construction/project/bidding/partners' + ); // ===== 외부 상태 ===== const [stats, setStats] = useState( @@ -67,25 +70,11 @@ export default function PartnerListClient({ initialData = [], initialStats }: Pa } }, [initialStats]); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (item: Partner) => { - router.push(`/ko/construction/project/bidding/partners/${item.id}?mode=view`); - }, - [router] - ); - + // ===== 추가 핸들러 ===== const handleCreate = useCallback(() => { router.push('/ko/construction/project/bidding/partners?mode=new'); }, [router]); - const handleEdit = useCallback( - (item: Partner) => { - router.push(`/ko/construction/project/bidding/partners/${item.id}?mode=edit`); - }, - [router] - ); - // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ diff --git a/src/components/business/construction/pricing-management/PricingListClient.tsx b/src/components/business/construction/pricing-management/PricingListClient.tsx index fddc22b5..408c2010 100644 --- a/src/components/business/construction/pricing-management/PricingListClient.tsx +++ b/src/components/business/construction/pricing-management/PricingListClient.tsx @@ -13,8 +13,8 @@ */ import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { DollarSign, Package, CheckCircle, AlertCircle, Pencil, Trash2 } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow, TableHead } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -54,7 +54,10 @@ export default function PricingListClient({ initialData = [], initialStats, }: PricingListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit, router } = useListHandlers( + 'construction/order/base-info/pricing' + ); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'in_use' | 'not_registered'>('all'); @@ -96,21 +99,7 @@ export default function PricingListClient({ return num.toLocaleString('ko-KR'); }, []); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (pricing: Pricing) => { - router.push(`/ko/construction/order/base-info/pricing/${pricing.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (pricing: Pricing) => { - router.push(`/ko/construction/order/base-info/pricing/${pricing.id}?mode=edit`); - }, - [router] - ); - + // ===== 추가 핸들러 ===== const handleCreate = useCallback(() => { router.push('/ko/construction/order/base-info/pricing?mode=new'); }, [router]); diff --git a/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx b/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx index 54407a8a..f0f02292 100644 --- a/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx +++ b/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx @@ -11,9 +11,9 @@ * - 삭제 기능 없음 (조회/수정 전용) */ -import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; +import { useState, useMemo, useEffect } from 'react'; import { FileText, Pencil } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -68,7 +68,10 @@ export default function ProgressBillingManagementListClient({ initialData = [], initialStats, }: ProgressBillingManagementListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit } = useListHandlers( + 'construction/billing/progress-billing-management' + ); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'contractWaiting' | 'contractComplete'>('all'); @@ -88,21 +91,6 @@ export default function ProgressBillingManagementListClient({ } }, [initialStats]); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (item: ProgressBilling) => { - router.push(`/ko/construction/billing/progress-billing-management/${item.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (item: ProgressBilling) => { - router.push(`/ko/construction/billing/progress-billing-management/${item.id}?mode=edit`); - }, - [router] - ); - // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ diff --git a/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx b/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx index 89f681e9..7e02be93 100644 --- a/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx +++ b/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx @@ -10,8 +10,8 @@ */ import { useState, useMemo, useCallback } from 'react'; -import { useRouter } from 'next/navigation'; import { Calendar, CalendarDays, CalendarCheck, CalendarClock, Pencil, Trash2 } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -90,7 +90,10 @@ interface SiteBriefingListClientProps { } export default function SiteBriefingListClient({ initialData = [] }: SiteBriefingListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit, router } = useListHandlers( + 'construction/project/bidding/site-briefings' + ); // Stats 탭 상태 const [activeStatTab, setActiveStatTab] = useState<'all' | 'scheduled' | 'attended'>('all'); @@ -99,21 +102,7 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin const [endDate, setEndDate] = useState(''); const [searchQuery, setSearchQuery] = useState(''); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (item: SiteBriefing) => { - router.push(`/ko/construction/project/bidding/site-briefings/${item.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (item: SiteBriefing) => { - router.push(`/ko/construction/project/bidding/site-briefings/${item.id}?mode=edit`); - }, - [router] - ); - + // ===== 추가 핸들러 ===== const handleCreate = useCallback(() => { router.push('/ko/construction/project/bidding/site-briefings?mode=new'); }, [router]); diff --git a/src/components/business/construction/site-management/SiteManagementListClient.tsx b/src/components/business/construction/site-management/SiteManagementListClient.tsx index cb30a8b4..dc63e16f 100644 --- a/src/components/business/construction/site-management/SiteManagementListClient.tsx +++ b/src/components/business/construction/site-management/SiteManagementListClient.tsx @@ -11,9 +11,9 @@ * - 삭제 기능 (deleteConfirmMessage로 AlertDialog 대체) */ -import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; +import { useState, useMemo, useEffect } from 'react'; import { Building2, HardHat, AlertCircle, Pencil } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -61,7 +61,8 @@ export default function SiteManagementListClient({ initialData = [], initialStats, }: SiteManagementListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit } = useListHandlers('construction/order/site-management'); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'construction' | 'unregistered'>('all'); @@ -81,21 +82,6 @@ export default function SiteManagementListClient({ } }, [initialStats]); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (site: Site) => { - router.push(`/ko/construction/order/site-management/${site.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (site: Site) => { - router.push(`/ko/construction/order/site-management/${site.id}?mode=edit`); - }, - [router] - ); - // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ diff --git a/src/components/business/construction/structure-review/StructureReviewListClient.tsx b/src/components/business/construction/structure-review/StructureReviewListClient.tsx index a393187a..2a94bb5e 100644 --- a/src/components/business/construction/structure-review/StructureReviewListClient.tsx +++ b/src/components/business/construction/structure-review/StructureReviewListClient.tsx @@ -12,8 +12,8 @@ */ import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { ClipboardCheck, Clock, CheckCircle, Pencil, Trash2 } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -38,6 +38,7 @@ import { deleteStructureReview, deleteStructureReviews, } from './actions'; +import { formatDate } from '@/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ @@ -60,12 +61,6 @@ const MOCK_PARTNERS = [ { value: '3', label: '회사명C' }, ]; -// 날짜 포맷 -function formatDate(dateStr: string | null): string { - if (!dateStr) return '-'; - return dateStr.split('T')[0]; -} - interface StructureReviewListClientProps { initialData?: StructureReview[]; initialStats?: StructureReviewStats; @@ -75,7 +70,10 @@ export default function StructureReviewListClient({ initialData = [], initialStats, }: StructureReviewListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick, handleEdit, router } = useListHandlers( + 'construction/order/structure-review' + ); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all'); @@ -95,21 +93,7 @@ export default function StructureReviewListClient({ } }, [initialStats]); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (item: StructureReview) => { - router.push(`/ko/construction/order/structure-review/${item.id}?mode=view`); - }, - [router] - ); - - const handleEdit = useCallback( - (item: StructureReview) => { - router.push(`/ko/construction/order/structure-review/${item.id}?mode=edit`); - }, - [router] - ); - + // ===== 추가 핸들러 ===== const handleCreate = useCallback(() => { router.push('/ko/construction/order/structure-review?mode=new'); }, [router]); diff --git a/src/components/business/construction/utility-management/UtilityManagementListClient.tsx b/src/components/business/construction/utility-management/UtilityManagementListClient.tsx index f2ff2637..86de4cad 100644 --- a/src/components/business/construction/utility-management/UtilityManagementListClient.tsx +++ b/src/components/business/construction/utility-management/UtilityManagementListClient.tsx @@ -42,6 +42,8 @@ import { deleteUtility, deleteUtilities, } from './actions'; +import { formatNumber } from '@/utils/formatAmount'; +import { formatDate } from '@/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ @@ -59,17 +61,6 @@ const tableColumns = [ { key: 'actions', label: '작업', className: 'w-[100px] text-center' }, ]; -// 날짜 포맷 -function formatDate(dateStr: string | null): string { - if (!dateStr) return '-'; - return dateStr.split('T')[0]; -} - -// 금액 포맷 -function formatAmount(amount: number): string { - return amount.toLocaleString('ko-KR') + '원'; -} - interface UtilityManagementListClientProps { initialData?: Utility[]; initialStats?: UtilityStats; @@ -350,7 +341,7 @@ export default function UtilityManagementListClient({ {item.constructionPM} {item.utilityType} {formatDate(item.scheduledDate)} - {formatAmount(item.amount)} + {formatNumber(item.amount)}원 {item.workTeamLeader} {formatDate(item.constructionStartDate)} @@ -391,7 +382,7 @@ export default function UtilityManagementListClient({ details={[ { label: '거래처', value: item.partnerName }, { label: '공사PM', value: item.constructionPM }, - { label: '금액', value: formatAmount(item.amount) }, + { label: '금액', value: `${formatNumber(item.amount)}원` }, ]} /> ), diff --git a/src/components/business/construction/worker-status/WorkerStatusListClient.tsx b/src/components/business/construction/worker-status/WorkerStatusListClient.tsx index 56d1dd14..a638b99e 100644 --- a/src/components/business/construction/worker-status/WorkerStatusListClient.tsx +++ b/src/components/business/construction/worker-status/WorkerStatusListClient.tsx @@ -11,9 +11,9 @@ * - 등록/삭제 버튼 없음 (조회 전용) */ -import { useState, useMemo, useCallback, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; +import { useState, useMemo, useEffect } from 'react'; import { Users, Eye, FileText, Clock, CheckCircle } from 'lucide-react'; +import { useListHandlers } from '@/hooks/useListHandlers'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -77,7 +77,8 @@ export default function WorkerStatusListClient({ initialData = [], initialStats, }: WorkerStatusListClientProps) { - const router = useRouter(); + // ===== 공통 핸들러 Hook ===== + const { handleRowClick } = useListHandlers('construction/project/worker-status'); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'all_contract' | 'pending' | 'completed'>('all'); @@ -97,21 +98,6 @@ export default function WorkerStatusListClient({ } }, [initialStats]); - // ===== 핸들러 ===== - const handleRowClick = useCallback( - (item: WorkerStatus) => { - router.push(`/ko/construction/project/worker-status/${item.id}?mode=view`); - }, - [router] - ); - - const handleViewDetail = useCallback( - (item: WorkerStatus) => { - router.push(`/ko/construction/project/worker-status/${item.id}?mode=view`); - }, - [router] - ); - // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ @@ -387,7 +373,7 @@ export default function WorkerStatusListClient({ className="h-8 w-8" onClick={(e) => { e.stopPropagation(); - handleViewDetail(item); + handleRowClick(item); }} > @@ -422,7 +408,7 @@ export default function WorkerStatusListClient({ /> ), }), - [startDate, endDate, activeStatTab, stats, handleRowClick, handleViewDetail, searchQuery] + [startDate, endDate, activeStatTab, stats, handleRowClick, searchQuery] ); return ; diff --git a/src/components/common/ScheduleCalendar/DayCell.tsx b/src/components/common/ScheduleCalendar/DayCell.tsx index 8ab779b9..e5380bb0 100644 --- a/src/components/common/ScheduleCalendar/DayCell.tsx +++ b/src/components/common/ScheduleCalendar/DayCell.tsx @@ -10,7 +10,10 @@ interface DayCellProps { isCurrentMonth: boolean; isToday: boolean; isSelected: boolean; - isWeekend: boolean; + isSaturday: boolean; + isSunday: boolean; + isHoliday?: boolean; + holidayName?: string | null; isPast: boolean; badge?: DayBadge; onClick: (date: Date) => void; @@ -28,18 +31,24 @@ export function DayCell({ isCurrentMonth, isToday, isSelected, - isWeekend, + isSaturday, + isSunday, + isHoliday = false, + holidayName, isPast, badge, onClick, }: DayCellProps) { const dayNumber = format(date, 'd'); const badgeColor = badge?.color || 'red'; + const isRedDay = isSunday || isHoliday; // 일요일 또는 공휴일 → 빨간색 + const isBlueDay = isSaturday && !isHoliday; // 토요일(공휴일 아닌 경우) → 파란색 return (