diff --git a/claudedocs/dev/[REF] all-pages-test-urls.md b/claudedocs/dev/[REF] all-pages-test-urls.md index 71bde98c..eaeb7a3a 100644 --- a/claudedocs/dev/[REF] all-pages-test-urls.md +++ b/claudedocs/dev/[REF] all-pages-test-urls.md @@ -1,6 +1,6 @@ # 전체 페이지 테스트 URL 목록 -> 백엔드 메뉴 연동 전 테스트용 직접 접근 URL (Last Updated: 2026-01-21) +> 백엔드 메뉴 연동 전 테스트용 직접 접근 URL (Last Updated: 2026-01-28) ## 🚀 클릭 가능한 웹 페이지 @@ -136,6 +136,24 @@ http://localhost:3000/ko/quality/inspections # 🆕 검사관리 --- +## 🚗 차량/지게차 (Vehicle Management) + +| 페이지 | URL | 상태 | +|--------|-----|------| +| **차량관리** | `/ko/vehicle-management/vehicle` | 🆕 NEW | +| **차량일지/월간사진기록** | `/ko/vehicle-management/vehicle-log` | 🆕 NEW | +| **지게차 관리** | `/ko/vehicle-management/forklift` | 🆕 NEW | + +``` +http://localhost:3000/ko/vehicle-management/vehicle # 🆕 차량관리 +http://localhost:3000/ko/vehicle-management/vehicle-log # 🆕 차량일지/월간사진기록 +http://localhost:3000/ko/vehicle-management/forklift # 🆕 지게차 관리 +``` + +> ℹ️ **참고**: 각 페이지에서 등록/상세/수정 페이지로 이동 가능 (별도 URL 등록 불필요) + +--- + ## 📤 출고관리 (Outbound) | 페이지 | URL | 상태 | @@ -357,6 +375,13 @@ http://localhost:3000/ko/quality/inspections # 🆕 검사관리 http://localhost:3000/ko/outbound/shipments # 🆕 출하관리 ``` +### Vehicle Management (차량/지게차) +``` +http://localhost:3000/ko/vehicle-management/vehicle # 🆕 차량관리 +http://localhost:3000/ko/vehicle-management/vehicle-log # 🆕 차량일지/월간사진기록 +http://localhost:3000/ko/vehicle-management/forklift # 🆕 지게차 관리 +``` + ### Settings ``` http://localhost:3000/ko/settings/leave-policy @@ -464,6 +489,11 @@ http://localhost:3000/ko/dev/editable-table # Editable Table 테스트 // Outbound (출고관리) '/outbound/shipments' // 출하관리 (🆕 NEW) +// Vehicle Management (차량/지게차) +'/vehicle-management/vehicle' // 차량관리 (🆕 NEW) +'/vehicle-management/vehicle-log' // 차량일지/월간사진기록 (🆕 NEW) +'/vehicle-management/forklift' // 지게차 관리 (🆕 NEW) + // Settings '/settings/leave-policy' '/settings/permissions' @@ -521,4 +551,4 @@ http://localhost:3000/ko/dev/editable-table # Editable Table 테스트 ## 작성일 - 최초 작성: 2025-12-06 -- 최종 업데이트: 2026-01-21 (account-info 경로 수정) +- 최종 업데이트: 2026-01-28 (차량/지게차 메뉴 추가) diff --git a/claudedocs/vehicle/[PLAN-2025-01-28] vehicle-forklift-menu-implementation.md b/claudedocs/vehicle/[PLAN-2025-01-28] vehicle-forklift-menu-implementation.md new file mode 100644 index 00000000..274b173d --- /dev/null +++ b/claudedocs/vehicle/[PLAN-2025-01-28] vehicle-forklift-menu-implementation.md @@ -0,0 +1,322 @@ +# 차량/지게차 메뉴 구현 계획 + +## 1. 개요 + +### 1.1 목적 +5130 사이트(레거시)의 차량/지게차 메뉴를 SAM 프로젝트로 마이그레이션 + +### 1.2 메뉴 구조 + +``` +📁 차량/지게차 +├── 📄 차량관리 (vehicle) +├── 📄 차량일지/월간사진기록 (vehicle-log) +└── 📄 지게차 관리 (forklift) +``` + +### 1.3 페이지 구성 방식 + +| 페이지 | 리스트 | 등록 | 상세 | 수정 | +|--------|--------|------|------|------| +| 차량관리 | ✅ 페이지 | ✅ 페이지 | ✅ 페이지 | ✅ 페이지 | +| 차량일지 | ✅ 페이지 | ✅ 페이지 | ✅ 페이지 | ✅ 페이지 | +| 지게차 관리 | ✅ 페이지 | ✅ 페이지 | ✅ 페이지 | ✅ 페이지 | + +### 1.4 사용할 공통 컴포넌트 + +| 페이지 타입 | 사용 템플릿 | 비고 | +|------------|------------|------| +| 리스트 | **UniversalListPage** | config 기반 | +| 등록/상세/수정 | **IntegratedDetailTemplate** | config 기반 | + +**구현 패턴**: +``` +config.ts → 컬럼/필드 정의 +actions.ts → API 호출 (Server Actions) +index.tsx → 템플릿에 config 전달 +``` + +--- + +## 2. 페이지별 상세 분석 (5130 사이트 기준) + +### 2.1 차량관리 (vehicle) + +| 항목 | 내용 | +|------|------| +| **페이지 타입** | 표준 CRUD (리스트 + 등록/상세/수정 페이지) | +| **레거시 URL** | `/car/list.php` | +| **전체 건수** | 6건 (예시) | + +**리스트 테이블 컬럼** (12개): +| 순서 | 컬럼명 | 타입 | 설명 | +|------|--------|------|------| +| 1 | 번호 | number | 행 번호 (1부터) | +| 2 | 차량번호 | string | 차량 식별번호 | +| 3 | 담당자 | string | 담당자명 | +| 4 | 보험사 | string | 보험 회사 | +| 5 | 보험만료일 | date | 보험 만료일 | +| 6 | 차량명 | string | 차량 이름 | +| 7 | 구입금액 | number | 구입 금액 | +| 8 | 구입일 | date | 구입 날짜 | +| 9 | 작성자 | string | 작성자명 | +| 10 | 정비정보 | string | 정비 관련 정보 | +| 11 | 등록일 | date | 등록 날짜 | +| 12 | 관리 | action | 상세/수정/삭제 버튼 | + +--- + +### 2.2 차량일지/월간사진기록 (vehicle-log) + +| 항목 | 내용 | +|------|------| +| **페이지 타입** | 표준 CRUD (리스트 + 등록/상세/수정 페이지) | +| **레거시 URL** | `/carrecord/list.php` | +| **전체 건수** | 41건 (예시) | + +**리스트 테이블 컬럼** (5개): +| 순서 | 컬럼명 | 타입 | 설명 | +|------|--------|------|------| +| 1 | 번호 | number | 행 번호 (1부터) | +| 2 | 작성일 | date | 작성 날짜 | +| 3 | 차량종류 | string | 차량 종류/타입 | +| 4 | 작성자 | string | 작성자명 | +| 5 | 글제목 | string | 일지 제목 | + +--- + +### 2.3 지게차 관리 (forklift) + +| 항목 | 내용 | +|------|------| +| **페이지 타입** | 표준 CRUD (리스트 + 등록/상세/수정 페이지) | +| **레거시 URL** | `/lift/list.php` | +| **전체 건수** | 7건 (예시) | + +**리스트 테이블 컬럼** (13개, 차량관리 + 용량): +| 순서 | 컬럼명 | 타입 | 설명 | +|------|--------|------|------| +| 1 | 번호 | number | 행 번호 (1부터) | +| 2 | 차량번호 | string | 지게차 식별번호 | +| 3 | 담당자 | string | 담당자명 | +| 4 | 보험사 | string | 보험 회사 | +| 5 | 보험만료일 | date | 보험 만료일 | +| 6 | 차량명 | string | 지게차 이름 | +| 7 | 구입금액 | number | 구입 금액 | +| 8 | 구입일 | date | 구입 날짜 | +| 9 | 작성자 | string | 작성자명 | +| 10 | 정비정보 | string | 정비 관련 정보 | +| 11 | 등록일 | date | 등록 날짜 | +| 12 | 용량 | string | 지게차 용량 ⭐ | +| 13 | 관리 | action | 상세/수정/삭제 버튼 | + +--- + +## 3. 파일 구조 + +### 3.1 페이지 구조 + +``` +src/app/[locale]/(protected)/ +└── vehicle-management/ # 차량/지게차 + ├── vehicle/ # 차량관리 + │ ├── page.tsx # 리스트 + │ ├── new/ + │ │ └── page.tsx # 등록 + │ └── [id]/ + │ ├── page.tsx # 상세 + │ └── edit/ + │ └── page.tsx # 수정 + │ + ├── vehicle-log/ # 차량일지/월간사진기록 + │ ├── page.tsx # 리스트 + │ ├── new/ + │ │ └── page.tsx # 등록 + │ └── [id]/ + │ ├── page.tsx # 상세 + │ └── edit/ + │ └── page.tsx # 수정 + │ + └── forklift/ # 지게차 관리 + ├── page.tsx # 리스트 + ├── new/ + │ └── page.tsx # 등록 + └── [id]/ + ├── page.tsx # 상세 + └── edit/ + └── page.tsx # 수정 +``` + +### 3.2 컴포넌트 구조 + +``` +src/components/vehicle-management/ +├── VehicleList/ +│ ├── index.tsx # UniversalListPage 사용 +│ ├── actions.ts # Server Actions +│ └── config.ts # 컬럼/필터 설정 +├── VehicleDetail/ +│ ├── index.tsx # IntegratedDetailTemplate 사용 +│ └── config.ts # 필드 설정 +│ +├── VehicleLogList/ +│ ├── index.tsx +│ ├── actions.ts +│ └── config.ts +├── VehicleLogDetail/ +│ ├── index.tsx +│ └── config.ts +│ +├── ForkliftList/ +│ ├── index.tsx +│ ├── actions.ts +│ └── config.ts +└── ForkliftDetail/ + ├── index.tsx + └── config.ts +``` + +--- + +## 4. API 엔드포인트 (예상) + +| 기능 | Method | Endpoint | +|------|--------|----------| +| 차량 목록 | GET | `/api/vehicle/list` | +| 차량 상세 | GET | `/api/vehicle/{id}` | +| 차량 등록 | POST | `/api/vehicle` | +| 차량 수정 | PUT | `/api/vehicle/{id}` | +| 차량 삭제 | DELETE | `/api/vehicle/{id}` | +| 차량일지 목록 | GET | `/api/vehicle-log/list` | +| 차량일지 상세 | GET | `/api/vehicle-log/{id}` | +| 차량일지 등록 | POST | `/api/vehicle-log` | +| 차량일지 수정 | PUT | `/api/vehicle-log/{id}` | +| 차량일지 삭제 | DELETE | `/api/vehicle-log/{id}` | +| 지게차 목록 | GET | `/api/forklift/list` | +| 지게차 상세 | GET | `/api/forklift/{id}` | +| 지게차 등록 | POST | `/api/forklift` | +| 지게차 수정 | PUT | `/api/forklift/{id}` | +| 지게차 삭제 | DELETE | `/api/forklift/{id}` | + +--- + +## 5. 구현 체크리스트 + +### Phase 1: 리스트 페이지 & 컴포넌트 + +**차량관리** +- [x] VehicleList/actions.ts ✅ +- [x] VehicleList/index.tsx ✅ +- [x] vehicle/page.tsx ✅ + +**차량일지** +- [x] VehicleLogList/actions.ts ✅ +- [x] VehicleLogList/index.tsx ✅ +- [x] vehicle-log/page.tsx ✅ + +**지게차 관리** +- [x] ForkliftList/actions.ts ✅ +- [x] ForkliftList/index.tsx ✅ +- [x] forklift/page.tsx ✅ + +### Phase 2: 상세/등록/수정 페이지 & 컴포넌트 + +**차량관리** +- [x] VehicleDetail/config.ts ✅ +- [x] VehicleDetail/index.tsx ✅ +- [x] vehicle/new/page.tsx (등록) ✅ +- [x] vehicle/[id]/page.tsx (상세) ✅ +- [x] vehicle/[id]/edit/page.tsx (수정) ✅ + +**차량일지** +- [x] VehicleLogDetail/config.ts ✅ +- [x] VehicleLogDetail/index.tsx ✅ +- [x] vehicle-log/new/page.tsx (등록) ✅ +- [x] vehicle-log/[id]/page.tsx (상세) ✅ +- [x] vehicle-log/[id]/edit/page.tsx (수정) ✅ + +**지게차 관리** +- [x] ForkliftDetail/config.ts ✅ +- [x] ForkliftDetail/index.tsx ✅ +- [x] forklift/new/page.tsx (등록) ✅ +- [x] forklift/[id]/page.tsx (상세) ✅ +- [x] forklift/[id]/edit/page.tsx (수정) ✅ + +### Phase 3: 메뉴 및 테스트 URL 등록 + +- [ ] 사이드바 메뉴 추가 (차량/지게차) - 백엔드 메뉴 연동 필요 +- [x] /dev/test-urls에 등록 ✅ + +### 공통 파일 +- [x] types.ts (Vehicle, VehicleLog, Forklift 타입) ✅ + +--- + +## 6. 참고 스크린샷 (Desktop) + +| 파일명 | 내용 | +|--------|------| +| 5130_car_list.png | 차량관리 목록 | +| 5130_carrecord_list.png | 차량일지 목록 | +| 5130_lift_list.png | 지게차 관리 목록 | + +--- + +## 7. 특이사항 + +1. **차량 vs 지게차 핵심 차이점** (2025-01-28 확인): + - **차량**: 보험사, 보험사 연락처, 엔진오일 교환 주기/일 사용 + - **지게차**: 구입업체, 구입업체 연락처, 부속품 교환 주기/일 사용 + - 차종 필드에 용량 포함 (예: "3톤 디젤") + +2. **API 미확정**: 백엔드 API가 아직 없을 수 있으므로 Mock 데이터로 우선 구현 + +3. **번호 컬럼**: 모든 테이블에서 1번부터 시작 (globalIndex 사용) + +4. **정비정보/월간사진**: 상세 페이지에서 이미지 업로드 기능 필요할 수 있음 + +--- + +## 8. 컬럼 검증 결과 (2025-01-28) + +### 8.1 차량관리 (vehicle) - ✅ 검증 완료 +| 순서 | 레거시 컬럼명 | SAM 구현 | 상태 | +|------|-------------|----------|------| +| 1 | 번호 | globalIndex | ✅ | +| 2 | 차량번호 | vehicleNumber | ✅ | +| 3 | 담당자 | manager | ✅ | +| 4 | 보험사 | insuranceCompany | ✅ | +| 5 | 보험사 연락처 | insuranceContact | ✅ | +| 6 | 최초등록일 | firstRegistrationDate | ✅ | +| 7 | 구매일자 | purchaseDate | ✅ | +| 8 | 구매 유형 | purchaseType | ✅ | +| 9 | 엔진오일 교환 주기 | oilChangeCycle | ✅ | +| 10 | 엔진오일교환일 | oilChangeDate | ✅ | +| 11 | 정비 정보 | maintenanceInfo | ✅ | +| 12 | 비고 | remarks | ✅ | + +### 8.2 차량일지 (vehicle-log) - ✅ 검증 완료 +| 순서 | 레거시 컬럼명 | SAM 구현 | 상태 | +|------|-------------|----------|------| +| 1 | 번호 | globalIndex | ✅ | +| 2 | 작성일 | writeDate | ✅ | +| 3 | 차량종류 | vehicleType | ✅ | +| 4 | 작성자 | writer | ✅ | +| 5 | 글제목 | title | ✅ | + +### 8.3 지게차 관리 (forklift) - ✅ 검증 완료 +| 순서 | 레거시 컬럼명 | SAM 구현 | 상태 | +|------|-------------|----------|------| +| 1 | 번호 | globalIndex | ✅ | +| 2 | 차량번호 | vehicleNumber | ✅ | +| 3 | 차종 | vehicleType | ✅ | +| 4 | 담당자 | manager | ✅ | +| 5 | 구입업체 | purchaseCompany | ✅ | +| 6 | 구입업체 연락처 | purchaseCompanyContact | ✅ | +| 7 | 최초등록일 | firstRegistrationDate | ✅ | +| 8 | 구매일자 | purchaseDate | ✅ | +| 9 | 구매 유형 | purchaseType | ✅ | +| 10 | 부속품 교환 주기 | partsChangeCycle | ✅ | +| 11 | 부속품 교환일 | partsChangeDate | ✅ | +| 12 | 정비 정보 | maintenanceInfo | ✅ | +| 13 | 비고 | remarks | ✅ | diff --git a/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/edit/page.tsx b/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/edit/page.tsx new file mode 100644 index 00000000..ef2f2c89 --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/edit/page.tsx @@ -0,0 +1,52 @@ +'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'; + +export default function ForkliftEditPage() { + const params = useParams(); + const id = params.id as string; + + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) return; + + getForkliftById(id) + .then((result) => { + if (result.success && result.data) { + setData(result.data); + } else { + setError(result.error || '데이터를 불러올 수 없습니다.'); + } + }) + .finally(() => setIsLoading(false)); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + if (error || !data) { + return ( +
+
{error || '지게차를 찾을 수 없습니다.'}
+
+ ); + } + + return ; +} diff --git a/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/page.tsx b/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/page.tsx new file mode 100644 index 00000000..80af038f --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/page.tsx @@ -0,0 +1,52 @@ +'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'; + +export default function ForkliftDetailPage() { + const params = useParams(); + const id = params.id as string; + + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) return; + + getForkliftById(id) + .then((result) => { + if (result.success && result.data) { + setData(result.data); + } else { + setError(result.error || '데이터를 불러올 수 없습니다.'); + } + }) + .finally(() => setIsLoading(false)); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + if (error || !data) { + return ( +
+
{error || '지게차를 찾을 수 없습니다.'}
+
+ ); + } + + return ; +} diff --git a/src/app/[locale]/(protected)/vehicle-management/forklift/new/page.tsx b/src/app/[locale]/(protected)/vehicle-management/forklift/new/page.tsx new file mode 100644 index 00000000..fe2c46eb --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/forklift/new/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +/** + * 지게차 등록 페이지 + */ + +import { ForkliftDetail } from '@/components/vehicle-management/ForkliftDetail'; + +export default function ForkliftNewPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/vehicle-management/forklift/page.tsx b/src/app/[locale]/(protected)/vehicle-management/forklift/page.tsx new file mode 100644 index 00000000..c247f8ad --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/forklift/page.tsx @@ -0,0 +1,35 @@ +'use client'; + +/** + * 지게차 관리 리스트 페이지 + */ + +import { useEffect, useState } from 'react'; +import { ForkliftList } from '@/components/vehicle-management/ForkliftList'; +import { getForklifts } from '@/components/vehicle-management/ForkliftList/actions'; +import type { Forklift } from '@/components/vehicle-management/types'; + +export default function ForkliftPage() { + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + getForklifts() + .then((result) => { + if (result.success) { + setData(result.data); + } + }) + .finally(() => setIsLoading(false)); + }, []); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + return ; +} diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/edit/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/edit/page.tsx new file mode 100644 index 00000000..1fb26894 --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/edit/page.tsx @@ -0,0 +1,52 @@ +'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'; + +export default function VehicleLogEditPage() { + const params = useParams(); + const id = params.id as string; + + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) return; + + getVehicleLogById(id) + .then((result) => { + if (result.success && result.data) { + setData(result.data); + } else { + setError(result.error || '데이터를 불러올 수 없습니다.'); + } + }) + .finally(() => setIsLoading(false)); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + if (error || !data) { + return ( +
+
{error || '차량일지를 찾을 수 없습니다.'}
+
+ ); + } + + return ; +} diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/page.tsx new file mode 100644 index 00000000..227c68e7 --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/page.tsx @@ -0,0 +1,52 @@ +'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'; + +export default function VehicleLogDetailPage() { + const params = useParams(); + const id = params.id as string; + + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) return; + + getVehicleLogById(id) + .then((result) => { + if (result.success && result.data) { + setData(result.data); + } else { + setError(result.error || '데이터를 불러올 수 없습니다.'); + } + }) + .finally(() => setIsLoading(false)); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + if (error || !data) { + return ( +
+
{error || '차량일지를 찾을 수 없습니다.'}
+
+ ); + } + + return ; +} diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle-log/new/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/new/page.tsx new file mode 100644 index 00000000..7995041e --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/new/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +/** + * 차량일지 등록 페이지 + */ + +import { VehicleLogDetail } from '@/components/vehicle-management/VehicleLogDetail'; + +export default function VehicleLogNewPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle-log/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/page.tsx new file mode 100644 index 00000000..5f820bd6 --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/page.tsx @@ -0,0 +1,35 @@ +'use client'; + +/** + * 차량일지/월간사진기록 리스트 페이지 + */ + +import { useEffect, useState } from 'react'; +import { VehicleLogList } from '@/components/vehicle-management/VehicleLogList'; +import { getVehicleLogs } from '@/components/vehicle-management/VehicleLogList/actions'; +import type { VehicleLog } from '@/components/vehicle-management/types'; + +export default function VehicleLogPage() { + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + getVehicleLogs() + .then((result) => { + if (result.success) { + setData(result.data); + } + }) + .finally(() => setIsLoading(false)); + }, []); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + return ; +} diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/edit/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/edit/page.tsx new file mode 100644 index 00000000..7c447e9c --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/edit/page.tsx @@ -0,0 +1,52 @@ +'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'; + +export default function VehicleEditPage() { + const params = useParams(); + const id = params.id as string; + + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) return; + + getVehicleById(id) + .then((result) => { + if (result.success && result.data) { + setData(result.data); + } else { + setError(result.error || '데이터를 불러올 수 없습니다.'); + } + }) + .finally(() => setIsLoading(false)); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + if (error || !data) { + return ( +
+
{error || '차량을 찾을 수 없습니다.'}
+
+ ); + } + + return ; +} diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/page.tsx new file mode 100644 index 00000000..5c8bef17 --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/page.tsx @@ -0,0 +1,52 @@ +'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'; + +export default function VehicleDetailPage() { + const params = useParams(); + const id = params.id as string; + + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) return; + + getVehicleById(id) + .then((result) => { + if (result.success && result.data) { + setData(result.data); + } else { + setError(result.error || '데이터를 불러올 수 없습니다.'); + } + }) + .finally(() => setIsLoading(false)); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + if (error || !data) { + return ( +
+
{error || '차량을 찾을 수 없습니다.'}
+
+ ); + } + + return ; +} diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle/new/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle/new/page.tsx new file mode 100644 index 00000000..dad5568a --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle/new/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +/** + * 차량 등록 페이지 + */ + +import { VehicleDetail } from '@/components/vehicle-management/VehicleDetail'; + +export default function VehicleNewPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle/page.tsx new file mode 100644 index 00000000..265d238f --- /dev/null +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle/page.tsx @@ -0,0 +1,35 @@ +'use client'; + +/** + * 차량 관리 리스트 페이지 + */ + +import { useEffect, useState } from 'react'; +import { VehicleList } from '@/components/vehicle-management/VehicleList'; +import { getVehicles } from '@/components/vehicle-management/VehicleList/actions'; +import type { Vehicle } from '@/components/vehicle-management/types'; + +export default function VehiclePage() { + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + getVehicles() + .then((result) => { + if (result.success) { + setData(result.data); + } + }) + .finally(() => setIsLoading(false)); + }, []); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + return ; +} diff --git a/src/components/business/CEODashboard/CEODashboard.tsx b/src/components/business/CEODashboard/CEODashboard.tsx index 41f1cab7..267d3542 100644 --- a/src/components/business/CEODashboard/CEODashboard.tsx +++ b/src/components/business/CEODashboard/CEODashboard.tsx @@ -9,9 +9,9 @@ import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { TodayIssueSection, - StatusBoardSection, - DailyReportSection, - MonthlyExpenseSection, + EnhancedStatusBoardSection, + EnhancedDailyReportSection, + EnhancedMonthlyExpenseSection, CardManagementSection, EntertainmentSection, WelfareSection, @@ -302,25 +302,25 @@ export function CEODashboard() { )} - {/* 일일 일보 */} + {/* 일일 일보 (Enhanced) */} {dashboardSettings.dailyReport && ( - )} - {/* 현황판 (구 오늘의 이슈 - 카드 형태) */} + {/* 현황판 (Enhanced - 아이콘 + 컬러 테마) */} {(dashboardSettings.statusBoard?.enabled ?? dashboardSettings.todayIssue.enabled) && ( - )} - {/* 당월 예상 지출 내역 */} + {/* 당월 예상 지출 내역 (Enhanced) */} {dashboardSettings.monthlyExpense && ( - diff --git a/src/components/business/CEODashboard/components.tsx b/src/components/business/CEODashboard/components.tsx index 03fdf332..52387cdf 100644 --- a/src/components/business/CEODashboard/components.tsx +++ b/src/components/business/CEODashboard/components.tsx @@ -1,11 +1,36 @@ 'use client'; -import { Check, AlertTriangle, Info, AlertCircle } from 'lucide-react'; +import { + Check, + AlertTriangle, + Info, + AlertCircle, + TrendingUp, + TrendingDown, + type LucideIcon, +} from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import type { CheckPoint, CheckPointType, AmountCard, HighlightColor } from './types'; +// 섹션별 컬러 테마 타입 +export type SectionColorTheme = 'blue' | 'purple' | 'orange' | 'green' | 'red' | 'amber' | 'cyan' | 'pink' | 'emerald' | 'indigo'; + +// 컬러 테마별 스타일 +export const SECTION_THEME_STYLES: Record = { + blue: { bg: '#eff6ff', border: '#bfdbfe', iconBg: '#3b82f6', labelColor: '#1d4ed8', accentColor: '#3b82f6' }, + purple: { bg: '#faf5ff', border: '#e9d5ff', iconBg: '#a855f7', labelColor: '#7c3aed', accentColor: '#a855f7' }, + orange: { bg: '#fff7ed', border: '#fed7aa', iconBg: '#f97316', labelColor: '#ea580c', accentColor: '#f97316' }, + green: { bg: '#f0fdf4', border: '#bbf7d0', iconBg: '#22c55e', labelColor: '#16a34a', accentColor: '#22c55e' }, + red: { bg: '#fef2f2', border: '#fecaca', iconBg: '#ef4444', labelColor: '#dc2626', accentColor: '#ef4444' }, + amber: { bg: '#fffbeb', border: '#fde68a', iconBg: '#f59e0b', labelColor: '#d97706', accentColor: '#f59e0b' }, + cyan: { bg: '#ecfeff', border: '#a5f3fc', iconBg: '#06b6d4', labelColor: '#0891b2', accentColor: '#06b6d4' }, + pink: { bg: '#fdf2f8', border: '#fbcfe8', iconBg: '#ec4899', labelColor: '#db2777', accentColor: '#ec4899' }, + emerald: { bg: '#ecfdf5', border: '#a7f3d0', iconBg: '#10b981', labelColor: '#059669', accentColor: '#10b981' }, + indigo: { bg: '#eef2ff', border: '#c7d2fe', iconBg: '#6366f1', labelColor: '#4f46e5', accentColor: '#6366f1' }, +}; + /** * 금액 포맷 함수 */ @@ -133,27 +158,42 @@ export const CheckPointItem = ({ checkpoint }: { checkpoint: CheckPoint }) => { }; /** - * 섹션 타이틀 컴포넌트 + * 섹션 타이틀 컴포넌트 (아이콘 지원 버전) */ export const SectionTitle = ({ title, badge, actionButton, + icon: Icon, + colorTheme, }: { title: string; badge?: 'warning' | 'success' | 'info' | 'error'; actionButton?: { label: string; onClick: () => void }; + icon?: LucideIcon; + colorTheme?: SectionColorTheme; }) => { + const themeStyle = colorTheme ? SECTION_THEME_STYLES[colorTheme] : null; + return (
-
-
-

{title}

+
+ {Icon && themeStyle ? ( +
+ +
+ ) : ( +
+ )} +

{title}

{actionButton && ( + ), + + onBulkDelete: handleBulkDelete, + + renderTableRow: ( + forklift: Forklift, + index: number, + globalIndex: number, + handlers: SelectionHandlers & RowClickHandlers + ) => { + // 부속품 교환일 포맷 + const partsChangeText = forklift.partsChangeRecords?.length > 0 + ? forklift.partsChangeRecords.map(r => + `${r.date}, 주행거리 : ${r.mileage || ''} km` + ).join('\n') + : '정보 없음'; + + // 정비내역 포맷 + const maintenanceText = forklift.maintenanceRecords?.length > 0 + ? forklift.maintenanceRecords.map(r => + `${r.date}: ${r.description}` + ).join('\n') + : '정보 없음'; + + // 담당자 (정/부 합침) + const managerText = [forklift.managerMain, forklift.managerSub] + .filter(Boolean) + .join(' / ') || '-'; + + return ( + handleView(forklift)} + > + e.stopPropagation()} className="text-center"> + + + {globalIndex} + {forklift.vehicleNumber} + {forklift.vehicleType} + {managerText} + {forklift.purchaseCompany || '-'} + {forklift.purchaseCompanyContact || '-'} + {forklift.firstRegistrationDate || '-'} + {forklift.purchaseDate || '-'} + {forklift.purchaseType || '-'} + {forklift.partsChangeCycle || '정보 없음'} + {partsChangeText} + {maintenanceText} + {forklift.remarks || '-'} + + ); + }, + + renderMobileCard: ( + forklift: Forklift, + index: number, + globalIndex: number, + handlers: SelectionHandlers & RowClickHandlers + ) => { + // 담당자 (정/부 합침) + const managerText = [forklift.managerMain, forklift.managerSub] + .filter(Boolean) + .join(' / ') || '-'; + + // 부속품 교환일 포맷 + const partsChangeText = forklift.partsChangeRecords?.length > 0 + ? forklift.partsChangeRecords.map(r => `${r.date}`).join(', ') + : '정보 없음'; + + // 정비내역 포맷 + const maintenanceText = forklift.maintenanceRecords?.length > 0 + ? forklift.maintenanceRecords.map(r => `${r.date}: ${r.description}`).join('\n') + : '정보 없음'; + + return ( + handleView(forklift)} + title={forklift.vehicleNumber} + subtitle={`${forklift.vehicleType} (${forklift.purchaseType || '-'})`} + infoGrid={ +
+ + + + + + + + + + + {forklift.remarks && ( + + )} +
+ } + actions={ + handlers.isSelected ? ( +
+ + +
+ ) : undefined + } + /> + ); + }, + }), + [router, handleView, handleEdit, handleDeleteClick, handleBulkDelete, isPending] + ); + + return ( + <> + + + + {deleteTargetId + ? `차량번호: ${allData.find((f) => f.id === deleteTargetId)?.vehicleNumber || deleteTargetId}` + : ''} +
+ 이 지게차를 삭제하시겠습니까? + + } + loading={isPending} + onConfirm={handleConfirmDelete} + /> + + + 선택한 {bulkDeleteIds.length}개의 지게차를 삭제하시겠습니까? + + } + loading={isPending} + onConfirm={handleConfirmBulkDelete} + /> + + ); +} + +export default ForkliftList; diff --git a/src/components/vehicle-management/VehicleDetail/config.tsx b/src/components/vehicle-management/VehicleDetail/config.tsx new file mode 100644 index 00000000..500b3cbb --- /dev/null +++ b/src/components/vehicle-management/VehicleDetail/config.tsx @@ -0,0 +1,478 @@ +'use client'; + +/** + * 차량 관리 상세 페이지 설정 + * 레거시 5130 사이트 등록 폼 기준 (2025-01-28 스크린샷 검증) + */ + +import { Car } from 'lucide-react'; +import type { + DetailConfig, + FieldDefinition, + SectionDefinition, +} from '@/components/templates/IntegratedDetailTemplate/types'; +import type { Vehicle, VehicleFormData, OilChangeRecord, MaintenanceRecord } from '../types'; +import { EditableTable, EditableColumn } from '@/components/common/EditableTable'; + +// ===== 엔진오일 교환 기록 테이블 컬럼 ===== +const oilChangeColumns: EditableColumn[] = [ + { + key: 'date', + header: '교환일', + type: 'text', + placeholder: 'YYYY-MM-DD', + width: '140px', + }, + { + key: 'mileage', + header: '주행거리(km)', + type: 'text', + placeholder: '예: 92000', + width: '140px', + align: 'right', + }, + { + key: 'cost', + header: '비용', + type: 'text', + placeholder: '예: 50,000', + width: '120px', + align: 'right', + }, +]; + +// ===== 정비내역 테이블 컬럼 ===== +const maintenanceColumns: EditableColumn[] = [ + { + key: 'date', + header: '정비일', + type: 'text', + placeholder: 'YYYY-MM-DD', + width: '140px', + }, + { + key: 'description', + header: '정비내역', + type: 'text', + placeholder: '정비 내용 입력', + }, + { + key: 'cost', + header: '비용', + type: 'text', + placeholder: '예: 150,000', + width: '120px', + align: 'right', + }, +]; + +// ===== 새 행 생성 함수 ===== +let oilRecordIdCounter = 0; +const createNewOilRecord = (): OilChangeRecord => ({ + id: `oil-${Date.now()}-${++oilRecordIdCounter}`, + date: '', + mileage: '', + cost: '', +}); + +let maintenanceRecordIdCounter = 0; +const createNewMaintenanceRecord = (): MaintenanceRecord => ({ + id: `maint-${Date.now()}-${++maintenanceRecordIdCounter}`, + date: '', + description: '', + cost: '', +}); + +// ===== 필드 정의 (스크린샷 순서대로) ===== +export const vehicleFields: FieldDefinition[] = [ + // ===== Row 1: 차량번호, 차종, 구매유형, 최초등록/계약일 ===== + { + key: 'vehicleNumber', + label: '차량번호', + type: 'text', + required: true, + placeholder: '차량번호 입력', + validation: [ + { type: 'required', message: '차량번호를 입력해주세요.' }, + ], + }, + { + key: 'vehicleType', + label: '차종', + type: 'text', + required: true, + placeholder: '차종 입력', + validation: [ + { type: 'required', message: '차종을 입력해주세요.' }, + ], + }, + { + key: 'purchaseType', + label: '구매유형', + type: 'select', + placeholder: '구매유형 선택', + options: [ + { label: '회사 소유', value: '회사 소유' }, + { label: '렌트', value: '렌트' }, + { label: '리스', value: '리스' }, + ], + }, + { + key: 'firstRegistrationDate', + label: '최초등록/계약일', + type: 'date', + placeholder: '연도.월.일.', + }, + + // ===== Row 2: 담당자(정), 담당자(부), 구매일자, 리스/렌트 종료일 ===== + { + key: 'managerMain', + label: '담당자(정)', + type: 'text', + required: true, + placeholder: '담당자(정) 입력', + validation: [ + { type: 'required', message: '담당자(정)를 입력해주세요.' }, + ], + }, + { + key: 'managerSub', + label: '담당자(부)', + type: 'text', + placeholder: '담당자(부) 입력', + }, + { + key: 'purchaseDate', + label: '구매일자', + type: 'date', + placeholder: '연도.월.일.', + }, + { + key: 'leaseEndDate', + label: '리스/렌트 종료일', + type: 'date', + placeholder: '연도.월.일.', + }, + + // ===== Row 3: 총 주행거리, 기록일, 검사주기 ===== + { + key: 'totalMileage', + label: '총 주행거리 (km)', + type: 'text', + placeholder: 'km', + }, + { + key: 'mileageRecordDate', + label: '기록일', + type: 'date', + placeholder: '연도.월.일.', + }, + { + key: 'inspectionStartDate', + label: '검사주기 시작', + type: 'date', + placeholder: '연도.월.일.', + }, + { + key: 'inspectionEndDate', + label: '검사주기 종료', + type: 'date', + placeholder: '연도.월.일.', + }, + + // ===== 보험사 가입정보 ===== + { + key: 'insuranceJoinDate', + label: '가입일', + type: 'date', + placeholder: '연도.월.일.', + }, + { + key: 'insuranceCompany', + label: '보험회사, 증서번호', + type: 'text', + placeholder: '보험사 및 증권번호', + }, + { + key: 'insuranceContact', + label: '연락처', + type: 'text', + placeholder: '연락처 입력', + }, + { + key: 'insuranceFee', + label: '보험료', + type: 'text', + placeholder: '금액', + }, + { + key: 'insuranceAmount', + label: '가입금액', + type: 'text', + placeholder: '금액', + }, + + // ===== 엔진오일 교환 ===== + { + key: 'oilChangeCycle', + label: '엔진오일 교환주기(Km)', + type: 'text', + placeholder: 'Km', + }, + { + key: 'oilChangeRecords', + label: '엔진오일 교환 기록', + type: 'custom', + gridSpan: 2, + formatValue: (value: unknown) => { + const records = (value as OilChangeRecord[]) || []; + if (records.length === 0) return '기록 없음'; + return records.map((record, index) => + `${index + 1}. ${record.date} / 주행거리: ${record.mileage}km / 비용: ${record.cost}원` + ).join('\n'); + }, + renderField: ({ value, onChange, mode }) => { + const records = (value as OilChangeRecord[]) || []; + const isViewMode = mode === 'view'; + + // view 모드에서는 formatValue가 처리 + if (isViewMode) { + if (records.length === 0) { + return
기록 없음
; + } + return ( +
+ {records.map((record, index) => ( +
+ {index + 1}. {record.date} / 주행거리: {record.mileage}km / 비용: {record.cost}원 +
+ ))} +
+ ); + } + + // create/edit 모드에서는 EditableTable + return ( + onChange(newData)} + createNewRow={createNewOilRecord} + addButtonLabel="교환 기록 추가" + emptyMessage="교환 기록이 없습니다. 기록을 추가해주세요." + showRowNumber={true} + compact={true} + /> + ); + }, + }, + + // ===== 정비내역 ===== + { + key: 'maintenanceRecords', + label: '정비 기록', + type: 'custom', + gridSpan: 2, + formatValue: (value: unknown) => { + const records = (value as MaintenanceRecord[]) || []; + if (records.length === 0) return '기록 없음'; + return records.map((record, index) => + `${index + 1}. ${record.date}: ${record.description} (비용: ${record.cost}원)` + ).join('\n'); + }, + renderField: ({ value, onChange, mode }) => { + const records = (value as MaintenanceRecord[]) || []; + const isViewMode = mode === 'view'; + + // view 모드에서는 formatValue가 처리 + if (isViewMode) { + if (records.length === 0) { + return
기록 없음
; + } + return ( +
+ {records.map((record, index) => ( +
+ {index + 1}. {record.date}: {record.description} (비용: {record.cost}원) +
+ ))} +
+ ); + } + + // create/edit 모드에서는 EditableTable + return ( + onChange(newData)} + createNewRow={createNewMaintenanceRecord} + addButtonLabel="정비 기록 추가" + emptyMessage="정비 기록이 없습니다. 기록을 추가해주세요." + showRowNumber={true} + compact={true} + /> + ); + }, + }, + + // ===== 비고 ===== + { + key: 'remarks', + label: '비고', + type: 'textarea', + placeholder: '비고 입력', + gridSpan: 2, + }, +]; + +// ===== 섹션 정의 (스크린샷 순서대로) ===== +export const vehicleSections: SectionDefinition[] = [ + { + id: 'basicInfo', + title: '기본 정보', + description: '차량의 기본 정보를 입력하세요', + fields: ['vehicleNumber', 'vehicleType', 'purchaseType', 'firstRegistrationDate'], + }, + { + id: 'managerInfo', + title: '담당자 및 구매 정보', + description: '담당자 및 구매 관련 정보를 입력하세요', + fields: ['managerMain', 'managerSub', 'purchaseDate', 'leaseEndDate'], + }, + { + id: 'mileageInfo', + title: '주행거리 및 검사주기', + description: '주행거리 및 검사 관련 정보를 입력하세요', + fields: ['totalMileage', 'mileageRecordDate', 'inspectionStartDate', 'inspectionEndDate'], + }, + { + id: 'insuranceInfo', + title: '보험사 가입정보', + description: '보험 관련 정보를 입력하세요', + fields: ['insuranceJoinDate', 'insuranceCompany', 'insuranceContact', 'insuranceFee', 'insuranceAmount'], + }, + { + id: 'oilChangeInfo', + title: '엔진오일 교환', + description: '엔진오일 교환 관련 정보를 입력하세요', + fields: ['oilChangeCycle', 'oilChangeRecords'], + }, + { + id: 'maintenanceSection', + title: '정비내역', + description: '정비 관련 정보를 입력하세요', + fields: ['maintenanceRecords'], + }, + { + id: 'remarksSection', + title: '비고', + description: '기타 정보를 입력하세요', + fields: ['remarks'], + }, +]; + +// ===== 설정 ===== +export const vehicleDetailConfig: DetailConfig = { + title: '차량', + description: '차량 정보를 관리합니다', + icon: Car, + basePath: '/ko/vehicle-management/vehicle', + fields: vehicleFields, + sections: vehicleSections, + gridColumns: 2, + actions: { + submitLabel: '저장', + cancelLabel: '취소', + showDelete: true, + deleteLabel: '삭제', + showEdit: true, + editLabel: '수정', + showBack: true, + backLabel: '목록', + deleteConfirmMessage: { + title: '차량 삭제', + description: '이 차량을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.', + }, + }, + transformInitialData: (data: Vehicle) => ({ + vehicleNumber: data.vehicleNumber || '', + vehicleType: data.vehicleType || '', + purchaseType: data.purchaseType || '', + firstRegistrationDate: data.firstRegistrationDate || '', + managerMain: data.managerMain || '', + managerSub: data.managerSub || '', + purchaseDate: data.purchaseDate || '', + leaseEndDate: data.leaseEndDate || '', + totalMileage: data.totalMileage || '', + mileageRecordDate: data.mileageRecordDate || '', + inspectionStartDate: data.inspectionStartDate || '', + inspectionEndDate: data.inspectionEndDate || '', + insuranceJoinDate: data.insuranceJoinDate || '', + insuranceCompany: data.insuranceCompany || '', + insuranceContact: data.insuranceContact || '', + insuranceFee: data.insuranceFee || '', + insuranceAmount: data.insuranceAmount || '', + oilChangeCycle: data.oilChangeCycle || '', + oilChangeRecords: data.oilChangeRecords || [], + maintenanceRecords: data.maintenanceRecords || [], + remarks: data.remarks || '', + }), + transformSubmitData: (formData): Partial => ({ + vehicleNumber: formData.vehicleNumber as string, + vehicleType: formData.vehicleType as string, + purchaseType: formData.purchaseType as string, + firstRegistrationDate: formData.firstRegistrationDate as string, + managerMain: formData.managerMain as string, + managerSub: formData.managerSub as string, + purchaseDate: formData.purchaseDate as string, + leaseEndDate: formData.leaseEndDate as string, + totalMileage: formData.totalMileage as string, + mileageRecordDate: formData.mileageRecordDate as string, + inspectionStartDate: formData.inspectionStartDate as string, + inspectionEndDate: formData.inspectionEndDate as string, + insuranceJoinDate: formData.insuranceJoinDate as string, + insuranceCompany: formData.insuranceCompany as string, + insuranceContact: formData.insuranceContact as string, + insuranceFee: formData.insuranceFee as string, + insuranceAmount: formData.insuranceAmount as string, + oilChangeCycle: formData.oilChangeCycle as string, + oilChangeRecords: formData.oilChangeRecords as OilChangeRecord[], + maintenanceRecords: formData.maintenanceRecords as MaintenanceRecord[], + remarks: formData.remarks as string, + }), +}; + +// ===== 등록 페이지 Config ===== +export const vehicleCreateConfig: DetailConfig = { + title: '차량', + description: '차량 정보를 입력하세요', + icon: Car, + basePath: '/vehicle-management/vehicle', + fields: vehicleFields, + sections: vehicleSections, + gridColumns: 2, + actions: { + showBack: true, + showSave: true, + submitLabel: '저장', + backLabel: '닫기', + }, +}; + +// ===== 수정 페이지 Config ===== +export const vehicleEditConfig: DetailConfig = { + title: '차량', + description: '차량 정보를 수정합니다', + icon: Car, + basePath: '/vehicle-management/vehicle', + fields: vehicleFields, + sections: vehicleSections, + gridColumns: 2, + actions: { + showBack: true, + showSave: true, + submitLabel: '저장', + backLabel: '닫기', + }, +}; diff --git a/src/components/vehicle-management/VehicleDetail/index.tsx b/src/components/vehicle-management/VehicleDetail/index.tsx new file mode 100644 index 00000000..f380d5ed --- /dev/null +++ b/src/components/vehicle-management/VehicleDetail/index.tsx @@ -0,0 +1,125 @@ +'use client'; + +/** + * 차량 관리 상세/등록/수정 컴포넌트 + * IntegratedDetailTemplate 기반 + */ + +import { useRouter } from 'next/navigation'; +import { useTransition } from 'react'; +import { toast } from 'sonner'; +import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; +import type { Vehicle, VehicleFormData } from '../types'; +import { vehicleDetailConfig, vehicleCreateConfig, vehicleEditConfig } from './config'; +import { + getVehicleById, + createVehicle, + updateVehicle, + deleteVehicle, +} from '../VehicleList/actions'; + +// ===== Props 타입 ===== +interface VehicleDetailProps { + mode: 'create' | 'view' | 'edit'; + initialData?: Vehicle; + id?: string; +} + +export function VehicleDetail({ mode, initialData, id }: VehicleDetailProps) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + // ===== 설정 선택 ===== + const getConfig = () => { + switch (mode) { + case 'create': + return vehicleCreateConfig; + case 'edit': + return vehicleEditConfig; + default: + return vehicleDetailConfig; + } + }; + + // ===== 저장 핸들러 ===== + const handleSave = async (formData: Record) => { + const vehicleData = vehicleDetailConfig.transformSubmitData?.(formData) as Partial; + + startTransition(async () => { + try { + if (mode === 'create') { + const result = await createVehicle(vehicleData); + if (result.success) { + toast.success('차량이 등록되었습니다.'); + router.push('/vehicle-management/vehicle'); + } else { + toast.error(result.error || '등록에 실패했습니다.'); + } + } else if (mode === 'edit' && id) { + const result = await updateVehicle(id, vehicleData); + if (result.success) { + toast.success('차량 정보가 수정되었습니다.'); + router.push(`/vehicle-management/vehicle/${id}`); + } else { + toast.error(result.error || '수정에 실패했습니다.'); + } + } + } catch (error) { + console.error('Save error:', error); + toast.error('저장 중 오류가 발생했습니다.'); + } + }); + }; + + // ===== 삭제 핸들러 ===== + const handleDelete = async () => { + if (!id) return; + + startTransition(async () => { + try { + const result = await deleteVehicle(id); + if (result.success) { + toast.success('차량이 삭제되었습니다.'); + router.push('/vehicle-management/vehicle'); + } else { + toast.error(result.error || '삭제에 실패했습니다.'); + } + } catch (error) { + console.error('Delete error:', error); + toast.error('삭제 중 오류가 발생했습니다.'); + } + }); + }; + + // ===== 수정 모드 전환 ===== + const handleEdit = () => { + if (id) { + router.push(`/vehicle-management/vehicle/${id}/edit`); + } + }; + + // ===== 목록으로 이동 ===== + const handleBack = () => { + router.push('/vehicle-management/vehicle'); + }; + + // ===== 초기 데이터 변환 ===== + const transformedData = initialData && vehicleDetailConfig.transformInitialData + ? vehicleDetailConfig.transformInitialData(initialData) + : undefined; + + return ( + + ); +} + +export default VehicleDetail; \ No newline at end of file diff --git a/src/components/vehicle-management/VehicleList/actions.ts b/src/components/vehicle-management/VehicleList/actions.ts new file mode 100644 index 00000000..993f03ae --- /dev/null +++ b/src/components/vehicle-management/VehicleList/actions.ts @@ -0,0 +1,315 @@ +'use server'; + +/** + * 차량 관리 서버 액션 + * 레거시 5130 사이트 컬럼 구조 기반 (2025-01-28 스크린샷 검증) + */ + +import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import type { Vehicle, VehicleFormData, ActionResponse, ListResponse, OilChangeRecord, MaintenanceRecord } from '../types'; + +// ===== Mock 데이터 (API 연동 전까지 사용) ===== +const mockVehicles: Vehicle[] = [ + { + id: '1', + vehicleNumber: '83버5277', + vehicleType: '포터', + purchaseType: '회사 소유', + firstRegistrationDate: '2022-01-15', + managerMain: '이재만', + managerSub: '', + purchaseDate: '2023-01-15', + leaseEndDate: '', + totalMileage: '92000', + mileageRecordDate: '2025-01-15', + inspectionStartDate: '2025-01-01', + inspectionEndDate: '2025-12-31', + insuranceJoinDate: '2023-01-01', + insuranceCompany: '한화손해보험, 12345678', + insuranceContact: '1566-8000', + insuranceFee: '500000', + insuranceAmount: '50000000', + oilChangeCycle: '10000', + oilChangeRecords: [ + { id: 'oil-1-1', date: '2025-01-15', mileage: '92000', cost: '50000' }, + ], + maintenanceRecords: [ + { id: 'maint-1-1', date: '2025-10-01', description: '오일교환, 브레이크라이닝2개교체', cost: '150000' }, + ], + remarks: '', + }, + { + id: '2', + vehicleNumber: '81러8178', + vehicleType: '봉고III 1.2톤', + purchaseType: '회사 소유', + firstRegistrationDate: '2016-06-16', + managerMain: '김진호선임', + managerSub: '', + purchaseDate: '2016-06-16', + leaseEndDate: '', + totalMileage: '92000', + mileageRecordDate: '2024-12-12', + inspectionStartDate: '2025-06-01', + inspectionEndDate: '2025-06-16', + insuranceJoinDate: '2016-06-16', + insuranceCompany: '한화손해보험', + insuranceContact: '1566-8000', + insuranceFee: '', + insuranceAmount: '', + oilChangeCycle: '10000', + oilChangeRecords: [ + { id: 'oil-2-1', date: '2024-12-12', mileage: '92000', cost: '' }, + ], + maintenanceRecords: [ + { id: 'maint-2-1', date: '2024-12-19', description: '차량검사 완료', cost: '' }, + { id: 'maint-2-2', date: '2025-05-22', description: '디스크 삼발이 교체', cost: '' }, + { id: 'maint-2-3', date: '2025-06-17', description: '후방 소음기 파손 교체', cost: '' }, + ], + remarks: '', + }, + { + id: '3', + vehicleNumber: '81러8197', + vehicleType: '그랜드 스타렉스', + purchaseType: '회사 소유', + firstRegistrationDate: '2016-06-22', + managerMain: '황규선', + managerSub: '', + purchaseDate: '2016-06-22', + leaseEndDate: '', + totalMileage: '', + mileageRecordDate: '', + inspectionStartDate: '', + inspectionEndDate: '', + insuranceJoinDate: '2016-06-22', + insuranceCompany: '한화손해보험', + insuranceContact: '1566-8000', + insuranceFee: '', + insuranceAmount: '', + oilChangeCycle: '', + oilChangeRecords: [], + maintenanceRecords: [ + { id: 'maint-3-1', date: '2025-08-18', description: '앞바퀴 타이어 교체', cost: '' }, + { id: 'maint-3-2', date: '2025-10-01', description: '오일교환 및 에어컨필터교환', cost: '' }, + { id: 'maint-3-3', date: '2025-12-04', description: '오일교환', cost: '' }, + ], + remarks: '', + }, + { + id: '4', + vehicleNumber: '80소0595', + vehicleType: '코란도스포츠', + purchaseType: '회사 소유', + firstRegistrationDate: '2017-08-14', + managerMain: '이세희차장', + managerSub: '', + purchaseDate: '2017-08-14', + leaseEndDate: '', + totalMileage: '16400', + mileageRecordDate: '2025-01-15', + inspectionStartDate: '', + inspectionEndDate: '', + insuranceJoinDate: '2017-08-14', + insuranceCompany: '한화손해보험', + insuranceContact: '1566-8000', + insuranceFee: '', + insuranceAmount: '', + oilChangeCycle: '', + oilChangeRecords: [ + { id: 'oil-4-1', date: '2025-01-15', mileage: '16400', cost: '' }, + ], + maintenanceRecords: [ + { id: 'maint-4-1', date: '2025-01-15', description: '베터리 교체(솔라이트90r)', cost: '' }, + ], + remarks: '2025.8.8차량매각(주식회사 차들)', + }, + { + id: '5', + vehicleNumber: '224호9739', + vehicleType: '레이', + purchaseType: '렌트', + firstRegistrationDate: '2024-10-25', + managerMain: '공장', + managerSub: '', + purchaseDate: '2024-10-25', + leaseEndDate: '', + totalMileage: '', + mileageRecordDate: '', + inspectionStartDate: '', + inspectionEndDate: '', + insuranceJoinDate: '2024-10-25', + insuranceCompany: '메리츠캐피탈', + insuranceContact: '1588-9666', + insuranceFee: '', + insuranceAmount: '', + oilChangeCycle: '7000', + oilChangeRecords: [], + maintenanceRecords: [ + { id: 'maint-5-1', date: '2025-06-25', description: '배터리 및 메인휴즈교체', cost: '' }, + ], + remarks: '긴급출동 연락처:1577-0565', + }, +]; + +// ===== 차량 목록 조회 ===== +export async function getVehicles(params?: { + page?: number; + perPage?: number; + search?: string; +}): Promise> { + try { + let filteredData = [...mockVehicles]; + + if (params?.search) { + const search = params.search.toLowerCase(); + filteredData = filteredData.filter( + (v) => + v.vehicleNumber.toLowerCase().includes(search) || + v.vehicleType.toLowerCase().includes(search) || + v.managerMain.toLowerCase().includes(search) || + v.insuranceCompany.toLowerCase().includes(search) + ); + } + + return { + success: true, + data: filteredData, + totalCount: filteredData.length, + totalPages: 1, + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleActions] getVehicles error:', error); + return { + success: false, + data: [], + totalCount: 0, + totalPages: 0, + error: '차량 목록 조회 중 오류가 발생했습니다.', + }; + } +} + +// ===== 차량 단건 조회 ===== +export async function getVehicleById(id: string): Promise> { + try { + const vehicle = mockVehicles.find((v) => v.id === id); + if (!vehicle) { + return { success: false, error: '차량을 찾을 수 없습니다.' }; + } + return { success: true, data: vehicle }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleActions] getVehicleById error:', error); + return { + success: false, + error: '차량 조회 중 오류가 발생했습니다.', + }; + } +} + +// ===== 차량 생성 ===== +export async function createVehicle( + formData: Partial +): Promise> { + try { + const newVehicle: Vehicle = { + id: String(Date.now()), + vehicleNumber: formData.vehicleNumber || '', + vehicleType: formData.vehicleType || '', + purchaseType: formData.purchaseType || '', + firstRegistrationDate: formData.firstRegistrationDate || '', + managerMain: formData.managerMain || '', + managerSub: formData.managerSub || '', + purchaseDate: formData.purchaseDate || '', + leaseEndDate: formData.leaseEndDate || '', + totalMileage: formData.totalMileage || '', + mileageRecordDate: formData.mileageRecordDate || '', + inspectionStartDate: formData.inspectionStartDate || '', + inspectionEndDate: formData.inspectionEndDate || '', + insuranceJoinDate: formData.insuranceJoinDate || '', + insuranceCompany: formData.insuranceCompany || '', + insuranceContact: formData.insuranceContact || '', + insuranceFee: formData.insuranceFee || '', + insuranceAmount: formData.insuranceAmount || '', + oilChangeCycle: formData.oilChangeCycle || '', + oilChangeRecords: formData.oilChangeRecords || [], + maintenanceRecords: formData.maintenanceRecords || [], + remarks: formData.remarks || '', + }; + mockVehicles.push(newVehicle); + return { success: true, data: newVehicle }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleActions] createVehicle error:', error); + return { + success: false, + error: '차량 등록 중 오류가 발생했습니다.', + }; + } +} + +// ===== 차량 수정 ===== +export async function updateVehicle( + id: string, + formData: Partial +): Promise> { + try { + const index = mockVehicles.findIndex((v) => v.id === id); + if (index === -1) { + return { success: false, error: '차량을 찾을 수 없습니다.' }; + } + mockVehicles[index] = { + ...mockVehicles[index], + ...formData, + }; + return { success: true, data: mockVehicles[index] }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleActions] updateVehicle error:', error); + return { + success: false, + error: '차량 수정 중 오류가 발생했습니다.', + }; + } +} + +// ===== 차량 삭제 ===== +export async function deleteVehicle(id: string): Promise { + try { + const index = mockVehicles.findIndex((v) => v.id === id); + if (index === -1) { + return { success: false, error: '차량을 찾을 수 없습니다.' }; + } + mockVehicles.splice(index, 1); + return { success: true }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleActions] deleteVehicle error:', error); + return { + success: false, + error: '차량 삭제 중 오류가 발생했습니다.', + }; + } +} + +// ===== 차량 일괄 삭제 ===== +export async function bulkDeleteVehicles(ids: string[]): Promise { + try { + ids.forEach((id) => { + const index = mockVehicles.findIndex((v) => v.id === id); + if (index !== -1) { + mockVehicles.splice(index, 1); + } + }); + return { success: true }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleActions] bulkDeleteVehicles error:', error); + return { + success: false, + error: '차량 일괄 삭제 중 오류가 발생했습니다.', + }; + } +} diff --git a/src/components/vehicle-management/VehicleList/index.tsx b/src/components/vehicle-management/VehicleList/index.tsx new file mode 100644 index 00000000..9a3e672f --- /dev/null +++ b/src/components/vehicle-management/VehicleList/index.tsx @@ -0,0 +1,341 @@ +'use client'; + +/** + * 차량 관리 리스트 - UniversalListPage 기반 + * 레거시 5130 사이트 컬럼 구조 기반 (2025-01-28 스크린샷 검증) + */ + +import { useState, useMemo, useCallback, useTransition } from 'react'; +import { useRouter } from 'next/navigation'; +import { Car, Edit, Trash2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { TableRow, TableCell } from '@/components/ui/table'; +import { + UniversalListPage, + type UniversalListConfig, + type SelectionHandlers, + type RowClickHandlers, + type ListParams, +} from '@/components/templates/UniversalListPage'; +import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; +import { toast } from 'sonner'; +import type { Vehicle } from '../types'; +import { getVehicles, deleteVehicle, bulkDeleteVehicles } from './actions'; + +interface VehicleListProps { + initialData: Vehicle[]; +} + +export function VehicleList({ initialData }: VehicleListProps) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [deleteTargetId, setDeleteTargetId] = useState(null); + const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); + const [bulkDeleteIds, setBulkDeleteIds] = useState([]); + const [allData, setAllData] = useState(initialData); + + const handleView = useCallback( + (vehicle: Vehicle) => { + router.push(`/vehicle-management/vehicle/${vehicle.id}`); + }, + [router] + ); + + const handleEdit = useCallback( + (vehicle: Vehicle) => { + router.push(`/vehicle-management/vehicle/${vehicle.id}/edit`); + }, + [router] + ); + + const handleDeleteClick = useCallback((id: string) => { + setDeleteTargetId(id); + setIsDeleteDialogOpen(true); + }, []); + + const handleConfirmDelete = useCallback(async () => { + if (!deleteTargetId) return; + + startTransition(async () => { + const result = await deleteVehicle(deleteTargetId); + + if (result.success) { + const vehicle = allData.find((v) => v.id === deleteTargetId); + setAllData(allData.filter((v) => v.id !== deleteTargetId)); + toast.success(`차량이 삭제되었습니다${vehicle ? `: ${vehicle.vehicleNumber}` : ''}`); + window.location.reload(); + } else { + toast.error(result.error || '삭제에 실패했습니다.'); + } + + setIsDeleteDialogOpen(false); + setDeleteTargetId(null); + }); + }, [deleteTargetId, allData]); + + const handleBulkDelete = useCallback((selectedIds: string[]) => { + if (selectedIds.length === 0) { + toast.error('삭제할 항목을 선택해주세요'); + return; + } + setBulkDeleteIds(selectedIds); + setIsBulkDeleteDialogOpen(true); + }, []); + + const handleConfirmBulkDelete = useCallback(async () => { + startTransition(async () => { + const result = await bulkDeleteVehicles(bulkDeleteIds); + + if (result.success) { + setAllData(allData.filter((v) => !bulkDeleteIds.includes(v.id))); + toast.success(`${bulkDeleteIds.length}개의 차량이 삭제되었습니다`); + window.location.reload(); + } else { + toast.error(result.error || '일괄 삭제에 실패했습니다.'); + } + + setIsBulkDeleteDialogOpen(false); + setBulkDeleteIds([]); + }); + }, [bulkDeleteIds, allData]); + + const config: UniversalListConfig = useMemo( + () => ({ + title: '차량 관리', + description: '차량 정보를 관리합니다', + icon: Car, + basePath: '/vehicle-management/vehicle', + + idField: 'id', + getItemId: (item: Vehicle) => item.id, + + actions: { + getList: async (params?: ListParams) => { + try { + const result = await getVehicles({ + page: params?.page || 1, + perPage: params?.perPage || 20, + search: params?.search || undefined, + }); + + if (result.success) { + setAllData(result.data); + return { + success: true, + data: result.data, + totalCount: result.totalCount, + totalPages: result.totalPages, + }; + } + return { success: false, error: result.error || '데이터 조회에 실패했습니다.' }; + } catch { + return { success: false, error: '서버 오류가 발생했습니다.' }; + } + }, + deleteItem: async (id: string) => { + const result = await deleteVehicle(id); + return { success: result.success, error: result.error }; + }, + deleteBulk: async (ids: string[]) => { + const result = await bulkDeleteVehicles(ids); + return { success: result.success, error: result.error }; + }, + }, + + // 레거시 컬럼 구조 (5130 기준) - 12개 컬럼 + columns: [ + { key: 'rowNumber', label: '번호', className: 'w-[50px] text-center' }, + { key: 'vehicleNumber', label: '차량번호', className: 'min-w-[120px]' }, + { key: 'managerMain', label: '담당자', className: 'w-[80px]' }, + { key: 'insuranceCompany', label: '보험사', className: 'w-[100px]' }, + { key: 'insuranceContact', label: '보험사 연락처', className: 'w-[110px]' }, + { key: 'firstRegistrationDate', label: '최초등록일', className: 'w-[90px]' }, + { key: 'purchaseDate', label: '구매일자', className: 'w-[90px]' }, + { key: 'purchaseType', label: '구매 유형', className: 'w-[80px]' }, + { key: 'oilChangeCycle', label: '엔진오일 교환 주기', className: 'w-[110px]' }, + { key: 'oilChangeRecords', label: '엔진오일교환일', className: 'min-w-[150px]' }, + { key: 'maintenanceRecords', label: '정비 정보', className: 'min-w-[180px]' }, + { key: 'remarks', label: '비고', className: 'min-w-[120px]' }, + ], + + clientSideFiltering: true, + itemsPerPage: 20, + + searchFilter: (item: Vehicle, searchValue: string) => { + const search = searchValue.toLowerCase(); + return ( + item.vehicleNumber.toLowerCase().includes(search) || + item.vehicleType.toLowerCase().includes(search) || + item.managerMain.toLowerCase().includes(search) || + item.insuranceCompany.toLowerCase().includes(search) + ); + }, + + tabs: [], + searchPlaceholder: '차량번호, 차종, 담당자, 보험사 검색...', + + headerActions: () => ( + + ), + + onBulkDelete: handleBulkDelete, + + renderTableRow: ( + vehicle: Vehicle, + index: number, + globalIndex: number, + handlers: SelectionHandlers & RowClickHandlers + ) => { + // 엔진오일 교환일 포맷 (5130 형식: "날짜, 주행거리 : XX km") + const oilChangeText = vehicle.oilChangeRecords && vehicle.oilChangeRecords.length > 0 + ? vehicle.oilChangeRecords.map(r => + `${r.date}, 주행거리 : ${r.mileage || ''} km` + ).join('\n') + : '정보 없음'; + + // 정비 정보 포맷 (5130 형식: "날짜: 내용") + const maintenanceText = vehicle.maintenanceRecords && vehicle.maintenanceRecords.length > 0 + ? vehicle.maintenanceRecords.map(r => + `${r.date}: ${r.description}` + ).join('\n') + : '정보 없음'; + + return ( + handleView(vehicle)} + > + e.stopPropagation()} className="text-center"> + + + {globalIndex} + {vehicle.vehicleNumber} + {vehicle.managerMain || '-'} + {vehicle.insuranceCompany || '-'} + {vehicle.insuranceContact || '-'} + {vehicle.firstRegistrationDate || '-'} + {vehicle.purchaseDate || '-'} + {vehicle.purchaseType || '-'} + {vehicle.oilChangeCycle || '정보 없음'} + {oilChangeText} + {maintenanceText} + {vehicle.remarks || '-'} + + ); + }, + + renderMobileCard: ( + vehicle: Vehicle, + index: number, + globalIndex: number, + handlers: SelectionHandlers & RowClickHandlers + ) => { + return ( + handleView(vehicle)} + title={`${vehicle.vehicleNumber} ${vehicle.vehicleType}`} + subtitle={vehicle.purchaseType} + infoGrid={ +
+ + + + + + +
+ } + actions={ + handlers.isSelected ? ( +
+ + +
+ ) : undefined + } + /> + ); + }, + }), + [router, handleView, handleEdit, handleDeleteClick, handleBulkDelete, isPending] + ); + + return ( + <> + + + + {deleteTargetId + ? `차량번호: ${allData.find((v) => v.id === deleteTargetId)?.vehicleNumber || deleteTargetId}` + : ''} +
+ 이 차량을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다. + + } + loading={isPending} + onConfirm={handleConfirmDelete} + /> + + + 선택한 {bulkDeleteIds.length}개의 차량을 삭제하시겠습니까? +
+ 삭제된 데이터는 복구할 수 없습니다. + + } + loading={isPending} + onConfirm={handleConfirmBulkDelete} + /> + + ); +} + +export default VehicleList; diff --git a/src/components/vehicle-management/VehicleLogDetail/config.tsx b/src/components/vehicle-management/VehicleLogDetail/config.tsx new file mode 100644 index 00000000..76f1e69f --- /dev/null +++ b/src/components/vehicle-management/VehicleLogDetail/config.tsx @@ -0,0 +1,265 @@ +'use client'; + +/** + * 차량일지/월간사진기록 상세 페이지 설정 + * 레거시 5130 사이트 등록 폼 기준 (2025-01-28 스크린샷 검증) + */ + +import { FileText, Upload, X, Image as ImageIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import type { + DetailConfig, + FieldDefinition, + SectionDefinition, +} from '@/components/templates/IntegratedDetailTemplate/types'; +import type { VehicleLog, VehicleLogFormData } from '../types'; + +// ===== 차량 정보 옵션 (5130 레거시와 동일) ===== +const vehicleOptions = [ + { label: '그랜드 스타렉스 - 81러8197', value: '그랜드 스타렉스 - 81러8197' }, + { label: '레이 - 224호9739', value: '레이 - 224호9739' }, + { label: '봉고III - 81러8178', value: '봉고III - 81러8178' }, + { label: '봉고III1.2톤 - 81러8178', value: '봉고III1.2톤 - 81러8178' }, + { label: '코란도스포츠 - 80소0595', value: '코란도스포츠 - 80소0595' }, + { label: '포터 - 83버5277', value: '포터 - 83버5277' }, +]; + +// ===== 필드 정의 (5130 등록 폼 순서대로) ===== +export const vehicleLogFields: FieldDefinition[] = [ + // Row 1: 작성일, 차량정보 + { + key: 'writeDate', + label: '작성일', + type: 'date', + required: true, + validation: [ + { type: 'required', message: '작성일을 선택해주세요.' }, + ], + }, + { + key: 'vehicleInfo', + label: '차량정보', + type: 'select', + required: true, + placeholder: '(차량 정보 선택)', + options: vehicleOptions, + validation: [ + { type: 'required', message: '차량정보를 선택해주세요.' }, + ], + }, + // Row 2: 작성자, 제목 + { + key: 'writer', + label: '작성자', + type: 'text', + required: true, + placeholder: '작성자 입력', + validation: [ + { type: 'required', message: '작성자를 입력해주세요.' }, + ], + }, + { + key: 'title', + label: '제목', + type: 'text', + required: true, + placeholder: '월별 차량사진 대지', + validation: [ + { type: 'required', message: '제목을 입력해주세요.' }, + ], + }, + // 사진 첨부 (custom) + { + key: 'images', + label: '사진 첨부', + type: 'custom', + gridSpan: 2, + formatValue: (value: unknown) => { + const images = (value as string[]) || []; + if (images.length === 0) return '첨부된 사진 없음'; + return `${images.length}개 사진 첨부됨`; + }, + renderField: ({ value, onChange, mode }) => { + const images = (value as string[]) || []; + const isViewMode = mode === 'view'; + + // view 모드 + if (isViewMode) { + if (images.length === 0) { + return
첨부된 사진 없음
; + } + return ( +
+ {images.map((img, index) => ( +
+ {`첨부 +
+ ))} +
+ ); + } + + // create/edit 모드 + const handleFileSelect = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files) return; + + // 파일을 URL로 변환 (실제 구현에서는 서버 업로드 필요) + const newImages: string[] = [...images]; + Array.from(files).forEach((file) => { + const url = URL.createObjectURL(file); + newImages.push(url); + }); + onChange(newImages); + }; + + const handleRemove = (index: number) => { + const newImages = images.filter((_, i) => i !== index); + onChange(newImages); + }; + + return ( +
+ {/* 업로드 영역 */} + + + {/* 업로드된 이미지 미리보기 */} + {images.length > 0 && ( +
+ {images.map((img, index) => ( +
+ {`첨부 + +
+ ))} +
+ )} + + {images.length === 0 && ( +

+ + 아직 첨부된 사진이 없습니다 +

+ )} +
+ ); + }, + }, +]; + +// ===== 섹션 정의 ===== +export const vehicleLogSections: SectionDefinition[] = [ + { + id: 'basicInfo', + title: '기본 정보', + description: '차량일지 기본 정보를 입력하세요', + fields: ['writeDate', 'vehicleInfo', 'writer', 'title'], + }, + { + id: 'imageSection', + title: '사진 첨부', + description: '차량 사진을 첨부하세요', + fields: ['images'], + }, +]; + +// ===== 설정 ===== +export const vehicleLogDetailConfig: DetailConfig = { + title: '차량일지', + description: '차량일지 정보를 관리합니다', + icon: FileText, + basePath: '/ko/vehicle-management/vehicle-log', + fields: vehicleLogFields, + sections: vehicleLogSections, + gridColumns: 2, + actions: { + submitLabel: '저장', + cancelLabel: '취소', + showDelete: true, + deleteLabel: '삭제', + showEdit: true, + editLabel: '수정', + showBack: true, + backLabel: '목록', + deleteConfirmMessage: { + title: '차량일지 삭제', + description: '이 차량일지를 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.', + }, + }, + transformInitialData: (data: VehicleLog) => ({ + writeDate: data.writeDate || '', + vehicleInfo: data.vehicleInfo || '', + writer: data.writer || '', + title: data.title || '', + images: data.images || [], + }), + transformSubmitData: (formData): Partial => ({ + writeDate: formData.writeDate as string, + vehicleInfo: formData.vehicleInfo as string, + writer: formData.writer as string, + title: formData.title as string, + images: formData.images as string[], + }), +}; + +export const vehicleLogCreateConfig: DetailConfig = { + title: '차량일지', + description: '차량일지를 등록합니다', + icon: FileText, + basePath: '/vehicle-management/vehicle-log', + fields: vehicleLogFields, + sections: vehicleLogSections, + gridColumns: 2, + actions: { + showBack: true, + showSave: true, + submitLabel: '저장', + backLabel: '닫기', + }, +}; + +export const vehicleLogEditConfig: DetailConfig = { + title: '차량일지', + description: '차량일지를 수정합니다', + icon: FileText, + basePath: '/vehicle-management/vehicle-log', + fields: vehicleLogFields, + sections: vehicleLogSections, + gridColumns: 2, + actions: { + showBack: true, + showSave: true, + submitLabel: '저장', + backLabel: '닫기', + }, +}; diff --git a/src/components/vehicle-management/VehicleLogDetail/index.tsx b/src/components/vehicle-management/VehicleLogDetail/index.tsx new file mode 100644 index 00000000..d2102468 --- /dev/null +++ b/src/components/vehicle-management/VehicleLogDetail/index.tsx @@ -0,0 +1,117 @@ +'use client'; + +/** + * 차량일지/월간사진기록 상세/등록/수정 컴포넌트 + */ + +import { useRouter } from 'next/navigation'; +import { useTransition } from 'react'; +import { toast } from 'sonner'; +import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; +import type { VehicleLog, VehicleLogFormData } from '../types'; +import { vehicleLogDetailConfig, vehicleLogCreateConfig, vehicleLogEditConfig } from './config'; +import { + getVehicleLogById, + createVehicleLog, + updateVehicleLog, + deleteVehicleLog, +} from '../VehicleLogList/actions'; + +interface VehicleLogDetailProps { + mode: 'create' | 'view' | 'edit'; + initialData?: VehicleLog; + id?: string; +} + +export function VehicleLogDetail({ mode, initialData, id }: VehicleLogDetailProps) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + const getConfig = () => { + switch (mode) { + case 'create': + return vehicleLogCreateConfig; + case 'edit': + return vehicleLogEditConfig; + default: + return vehicleLogDetailConfig; + } + }; + + const handleSave = async (formData: Record) => { + const logData = vehicleLogDetailConfig.transformSubmitData?.(formData) as Partial; + + startTransition(async () => { + try { + if (mode === 'create') { + const result = await createVehicleLog(logData); + if (result.success) { + toast.success('차량일지가 등록되었습니다.'); + router.push('/vehicle-management/vehicle-log'); + } else { + toast.error(result.error || '등록에 실패했습니다.'); + } + } else if (mode === 'edit' && id) { + const result = await updateVehicleLog(id, logData); + if (result.success) { + toast.success('차량일지가 수정되었습니다.'); + router.push(`/vehicle-management/vehicle-log/${id}`); + } else { + toast.error(result.error || '수정에 실패했습니다.'); + } + } + } catch (error) { + console.error('Save error:', error); + toast.error('저장 중 오류가 발생했습니다.'); + } + }); + }; + + const handleDelete = async () => { + if (!id) return; + + startTransition(async () => { + try { + const result = await deleteVehicleLog(id); + if (result.success) { + toast.success('차량일지가 삭제되었습니다.'); + router.push('/vehicle-management/vehicle-log'); + } else { + toast.error(result.error || '삭제에 실패했습니다.'); + } + } catch (error) { + console.error('Delete error:', error); + toast.error('삭제 중 오류가 발생했습니다.'); + } + }); + }; + + const handleEdit = () => { + if (id) { + router.push(`/vehicle-management/vehicle-log/${id}/edit`); + } + }; + + const handleBack = () => { + router.push('/vehicle-management/vehicle-log'); + }; + + const transformedData = initialData && vehicleLogDetailConfig.transformInitialData + ? vehicleLogDetailConfig.transformInitialData(initialData) + : undefined; + + return ( + + ); +} + +export default VehicleLogDetail; diff --git a/src/components/vehicle-management/VehicleLogList/actions.ts b/src/components/vehicle-management/VehicleLogList/actions.ts new file mode 100644 index 00000000..fe76340b --- /dev/null +++ b/src/components/vehicle-management/VehicleLogList/actions.ts @@ -0,0 +1,207 @@ +'use server'; + +/** + * 차량일지/월간사진기록 서버 액션 + */ + +import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import type { VehicleLog, VehicleLogFormData, ActionResponse, ListResponse } from '../types'; + +// ===== Mock 데이터 (5130 레거시 기준) ===== +const mockVehicleLogs: VehicleLog[] = [ + { + id: '1', + writeDate: '2025-12-01', + vehicleInfo: '레이 - 224호9739', + writer: '이세희', + title: '25년11월 차량사진 대지', + images: [ + 'https://picsum.photos/400/300?random=1', + 'https://picsum.photos/400/300?random=2', + 'https://picsum.photos/400/300?random=3', + ], + createdAt: '2025-12-01', + }, + { + id: '2', + writeDate: '2025-12-01', + vehicleInfo: '봉고III1.2톤 - 81러8178', + writer: '김진호', + title: '25년11월 차량사진 대지', + images: [], + createdAt: '2025-12-01', + }, + { + id: '3', + writeDate: '2025-12-01', + vehicleInfo: '그랜드 스타렉스 - 81러8197', + writer: '황규선', + title: '25년11월 차량사진 대지', + images: [], + createdAt: '2025-12-01', + }, + { + id: '4', + writeDate: '2025-12-01', + vehicleInfo: '포터 - 83버5277', + writer: '이재만', + title: '25년11월 차량사진 대지', + images: [], + createdAt: '2025-12-01', + }, + { + id: '5', + writeDate: '2025-11-05', + vehicleInfo: '레이 - 224호9739', + writer: '이세희', + title: '25년 10월 차량사진 대지', + images: [], + createdAt: '2025-11-05', + }, +]; + +// ===== 차량일지 목록 조회 ===== +export async function getVehicleLogs(params?: { + page?: number; + perPage?: number; + search?: string; +}): Promise> { + try { + let filteredData = [...mockVehicleLogs]; + + if (params?.search) { + const search = params.search.toLowerCase(); + filteredData = filteredData.filter( + (v) => + v.title.toLowerCase().includes(search) || + v.vehicleInfo.toLowerCase().includes(search) || + v.writer.toLowerCase().includes(search) + ); + } + + return { + success: true, + data: filteredData, + totalCount: filteredData.length, + totalPages: 1, + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleLogActions] getVehicleLogs error:', error); + return { + success: false, + data: [], + totalCount: 0, + totalPages: 0, + error: '차량일지 목록 조회 중 오류가 발생했습니다.', + }; + } +} + +// ===== 차량일지 단건 조회 ===== +export async function getVehicleLogById(id: string): Promise> { + try { + const log = mockVehicleLogs.find((v) => v.id === id); + if (!log) { + return { success: false, error: '차량일지를 찾을 수 없습니다.' }; + } + return { success: true, data: log }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleLogActions] getVehicleLogById error:', error); + return { + success: false, + error: '차량일지 조회 중 오류가 발생했습니다.', + }; + } +} + +// ===== 차량일지 생성 ===== +export async function createVehicleLog( + formData: Partial +): Promise> { + try { + const newLog: VehicleLog = { + id: String(Date.now()), + writeDate: formData.writeDate || new Date().toISOString().split('T')[0], + vehicleInfo: formData.vehicleInfo || '', + writer: formData.writer || '', + title: formData.title || '', + images: formData.images || [], + createdAt: new Date().toISOString().split('T')[0], + }; + mockVehicleLogs.push(newLog); + return { success: true, data: newLog }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleLogActions] createVehicleLog error:', error); + return { + success: false, + error: '차량일지 등록 중 오류가 발생했습니다.', + }; + } +} + +// ===== 차량일지 수정 ===== +export async function updateVehicleLog( + id: string, + formData: Partial +): Promise> { + try { + const index = mockVehicleLogs.findIndex((v) => v.id === id); + if (index === -1) { + return { success: false, error: '차량일지를 찾을 수 없습니다.' }; + } + mockVehicleLogs[index] = { + ...mockVehicleLogs[index], + ...formData, + }; + return { success: true, data: mockVehicleLogs[index] }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleLogActions] updateVehicleLog error:', error); + return { + success: false, + error: '차량일지 수정 중 오류가 발생했습니다.', + }; + } +} + +// ===== 차량일지 삭제 ===== +export async function deleteVehicleLog(id: string): Promise { + try { + const index = mockVehicleLogs.findIndex((v) => v.id === id); + if (index === -1) { + return { success: false, error: '차량일지를 찾을 수 없습니다.' }; + } + mockVehicleLogs.splice(index, 1); + return { success: true }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleLogActions] deleteVehicleLog error:', error); + return { + success: false, + error: '차량일지 삭제 중 오류가 발생했습니다.', + }; + } +} + +// ===== 차량일지 일괄 삭제 ===== +export async function bulkDeleteVehicleLogs(ids: string[]): Promise { + try { + ids.forEach((id) => { + const index = mockVehicleLogs.findIndex((v) => v.id === id); + if (index !== -1) { + mockVehicleLogs.splice(index, 1); + } + }); + return { success: true }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[VehicleLogActions] bulkDeleteVehicleLogs error:', error); + return { + success: false, + error: '차량일지 일괄 삭제 중 오류가 발생했습니다.', + }; + } +} diff --git a/src/components/vehicle-management/VehicleLogList/index.tsx b/src/components/vehicle-management/VehicleLogList/index.tsx new file mode 100644 index 00000000..ebecd883 --- /dev/null +++ b/src/components/vehicle-management/VehicleLogList/index.tsx @@ -0,0 +1,312 @@ +'use client'; + +/** + * 차량일지/월간사진기록 리스트 - UniversalListPage 기반 + */ + +import { useState, useMemo, useCallback, useTransition } from 'react'; +import { useRouter } from 'next/navigation'; +import { FileText, Edit, Trash2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { TableRow, TableCell } from '@/components/ui/table'; +import { + UniversalListPage, + type UniversalListConfig, + type SelectionHandlers, + type RowClickHandlers, + type ListParams, +} from '@/components/templates/UniversalListPage'; +import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; +import { toast } from 'sonner'; +import type { VehicleLog } from '../types'; +import { getVehicleLogs, deleteVehicleLog, bulkDeleteVehicleLogs } from './actions'; + +// ===== Props 타입 ===== +interface VehicleLogListProps { + initialData: VehicleLog[]; +} + +export function VehicleLogList({ initialData }: VehicleLogListProps) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + // ===== 삭제 다이얼로그 상태 ===== + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [deleteTargetId, setDeleteTargetId] = useState(null); + const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); + const [bulkDeleteIds, setBulkDeleteIds] = useState([]); + + // ===== 전체 데이터 상태 ===== + const [allData, setAllData] = useState(initialData); + + // ===== 핸들러 ===== + const handleView = useCallback( + (log: VehicleLog) => { + router.push(`/vehicle-management/vehicle-log/${log.id}`); + }, + [router] + ); + + const handleEdit = useCallback( + (log: VehicleLog) => { + router.push(`/vehicle-management/vehicle-log/${log.id}/edit`); + }, + [router] + ); + + const handleDeleteClick = useCallback((id: string) => { + setDeleteTargetId(id); + setIsDeleteDialogOpen(true); + }, []); + + const handleConfirmDelete = useCallback(async () => { + if (!deleteTargetId) return; + + startTransition(async () => { + const result = await deleteVehicleLog(deleteTargetId); + + if (result.success) { + const log = allData.find((v) => v.id === deleteTargetId); + setAllData(allData.filter((v) => v.id !== deleteTargetId)); + toast.success(`차량일지가 삭제되었습니다${log ? `: ${log.title}` : ''}`); + window.location.reload(); + } else { + toast.error(result.error || '삭제에 실패했습니다.'); + } + + setIsDeleteDialogOpen(false); + setDeleteTargetId(null); + }); + }, [deleteTargetId, allData]); + + const handleBulkDelete = useCallback((selectedIds: string[]) => { + if (selectedIds.length === 0) { + toast.error('삭제할 항목을 선택해주세요'); + return; + } + setBulkDeleteIds(selectedIds); + setIsBulkDeleteDialogOpen(true); + }, []); + + const handleConfirmBulkDelete = useCallback(async () => { + startTransition(async () => { + const result = await bulkDeleteVehicleLogs(bulkDeleteIds); + + if (result.success) { + setAllData(allData.filter((v) => !bulkDeleteIds.includes(v.id))); + toast.success(`${bulkDeleteIds.length}개의 차량일지가 삭제되었습니다`); + window.location.reload(); + } else { + toast.error(result.error || '일괄 삭제에 실패했습니다.'); + } + + setIsBulkDeleteDialogOpen(false); + setBulkDeleteIds([]); + }); + }, [bulkDeleteIds, allData]); + + // ===== UniversalListPage Config ===== + const config: UniversalListConfig = useMemo( + () => ({ + title: '차량일지/월간사진기록', + description: '차량 운행 및 정비 기록을 관리합니다', + icon: FileText, + basePath: '/vehicle-management/vehicle-log', + + idField: 'id', + getItemId: (item: VehicleLog) => item.id, + + actions: { + getList: async (params?: ListParams) => { + try { + const result = await getVehicleLogs({ + page: params?.page || 1, + perPage: params?.perPage || 20, + search: params?.search || undefined, + }); + + if (result.success) { + setAllData(result.data); + return { + success: true, + data: result.data, + totalCount: result.totalCount, + totalPages: result.totalPages, + }; + } + return { success: false, error: result.error || '데이터 조회에 실패했습니다.' }; + } catch { + return { success: false, error: '서버 오류가 발생했습니다.' }; + } + }, + deleteItem: async (id: string) => { + const result = await deleteVehicleLog(id); + return { success: result.success, error: result.error }; + }, + deleteBulk: async (ids: string[]) => { + const result = await bulkDeleteVehicleLogs(ids); + return { success: result.success, error: result.error }; + }, + }, + + // 레거시 컬럼 구조 (5130 기준) - 5개 컬럼 + columns: [ + { key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' }, + { key: 'writeDate', label: '작성일', className: 'w-[100px]' }, + { key: 'vehicleInfo', label: '차량종류', className: 'min-w-[180px]' }, + { key: 'writer', label: '작성자', className: 'w-[80px]' }, + { key: 'title', label: '글제목', className: 'min-w-[250px]' }, + ], + + clientSideFiltering: true, + itemsPerPage: 20, + + searchFilter: (item: VehicleLog, searchValue: string) => { + const search = searchValue.toLowerCase(); + return ( + item.title.toLowerCase().includes(search) || + item.vehicleInfo.toLowerCase().includes(search) || + item.writer.toLowerCase().includes(search) + ); + }, + + tabs: [], + + searchPlaceholder: '제목, 차량종류, 작성자 검색...', + + headerActions: () => ( + + ), + + onBulkDelete: handleBulkDelete, + + renderTableRow: ( + log: VehicleLog, + index: number, + globalIndex: number, + handlers: SelectionHandlers & RowClickHandlers + ) => { + return ( + handleView(log)} + > + e.stopPropagation()} className="text-center"> + + + {globalIndex} + {log.writeDate} + {log.vehicleInfo} + {log.writer} + {log.title} + + ); + }, + + renderMobileCard: ( + log: VehicleLog, + index: number, + globalIndex: number, + handlers: SelectionHandlers & RowClickHandlers + ) => { + return ( + handleView(log)} + title={log.title} + subtitle={log.vehicleInfo} + infoGrid={ +
+ + +
+ } + actions={ + handlers.isSelected ? ( +
+ + +
+ ) : undefined + } + /> + ); + }, + }), + [router, handleView, handleEdit, handleDeleteClick, handleBulkDelete, isPending] + ); + + return ( + <> + + + + {deleteTargetId + ? `제목: ${allData.find((v) => v.id === deleteTargetId)?.title || deleteTargetId}` + : ''} +
+ 이 차량일지를 삭제하시겠습니까? + + } + loading={isPending} + onConfirm={handleConfirmDelete} + /> + + + 선택한 {bulkDeleteIds.length}개의 차량일지를 삭제하시겠습니까? + + } + loading={isPending} + onConfirm={handleConfirmBulkDelete} + /> + + ); +} + +export default VehicleLogList; diff --git a/src/components/vehicle-management/types.ts b/src/components/vehicle-management/types.ts new file mode 100644 index 00000000..4a46722b --- /dev/null +++ b/src/components/vehicle-management/types.ts @@ -0,0 +1,174 @@ +/** + * 차량/지게차 관리 타입 정의 + * 레거시 5130 사이트 컬럼 구조 기반 (2025-01-28 스크린샷 검증) + */ + +// ===== 엔진오일 교환 기록 ===== +export interface OilChangeRecord { + id: string; + date: string; // 교환일 + mileage: string; // 주행거리 + cost: string; // 비용 +} + +// ===== 정비 기록 ===== +export interface MaintenanceRecord { + id: string; + date: string; // 정비일 + description: string; // 정비내역 + cost: string; // 비용 +} + +// ===== 차량 관리 ===== +export interface Vehicle { + id: string; + // 기본 정보 + vehicleNumber: string; // 차량번호 + vehicleType: string; // 차종 + purchaseType: string; // 구매유형 (회사 소유, 렌트, 리스) + firstRegistrationDate: string; // 최초등록/계약일 + + // 담당자 정보 + managerMain: string; // 담당자(정) + managerSub: string; // 담당자(부) + + // 구매/리스 정보 + purchaseDate: string; // 구매일자 + leaseEndDate: string; // 리스/렌트 종료일 + + // 주행/검사 정보 + totalMileage: string; // 총 주행거리 (km) + mileageRecordDate: string; // 기록일 + inspectionStartDate: string; // 검사주기 시작일 + inspectionEndDate: string; // 검사주기 종료일 + + // 보험사 가입정보 + insuranceJoinDate: string; // 가입일 + insuranceCompany: string; // 보험회사, 증서번호 + insuranceContact: string; // 연락처 + insuranceFee: string; // 보험료 + insuranceAmount: string; // 가입금액 + + // 엔진오일 교환 + oilChangeCycle: string; // 엔진오일 교환주기 (Km) + oilChangeRecords: OilChangeRecord[]; // 엔진오일 교환 기록 (테이블) + + // 정비내역 + maintenanceRecords: MaintenanceRecord[]; // 정비 기록 (테이블) + + // 비고 + remarks: string; // 비고 +} + +export interface VehicleFormData { + vehicleNumber: string; + vehicleType: string; + purchaseType: string; + firstRegistrationDate: string; + managerMain: string; + managerSub: string; + purchaseDate: string; + leaseEndDate: string; + totalMileage: string; + mileageRecordDate: string; + inspectionStartDate: string; + inspectionEndDate: string; + insuranceJoinDate: string; + insuranceCompany: string; + insuranceContact: string; + insuranceFee: string; + insuranceAmount: string; + oilChangeCycle: string; + oilChangeRecords: OilChangeRecord[]; + maintenanceRecords: MaintenanceRecord[]; + remarks: string; +} + +// ===== 차량일지/월간사진기록 ===== +export interface VehicleLog { + id: string; + writeDate: string; // 작성일 + vehicleInfo: string; // 차량정보 (차종 - 차량번호) + writer: string; // 작성자 + title: string; // 제목 + images?: string[]; // 첨부 이미지 + createdAt: string; // 등록일 +} + +export interface VehicleLogFormData { + writeDate: string; + vehicleInfo: string; // 차량정보 (차종 - 차량번호) + writer: string; // 작성자 + title: string; // 제목 + images?: string[]; // 첨부 이미지 +} + +// ===== 지게차 부속품 교환 기록 ===== +export interface PartsChangeRecord { + id: string; + date: string; // 교환일 + mileage: string; // 주행거리 + cost: string; // 비용 +} + +// ===== 지게차 정비 기록 ===== +export interface ForkliftMaintenanceRecord { + id: string; + date: string; // 정비일자 + description: string; // 정비내역 기록 + cost: string; // 비용 +} + +// ===== 지게차 관리 ===== +export interface Forklift { + id: string; + vehicleNumber: string; // 지게차번호 + vehicleType: string; // 차종 + purchaseType: string; // 구매유형 + managerMain: string; // 담당자(정) + managerSub: string; // 담당자(부) + purchaseCompany: string; // 구입업체 + purchaseCompanyContact: string; // 구입업체 연락처 + totalMileage: string; // 총 주행거리 (km) + mileageRecordDate: string; // 기록일 + firstRegistrationDate: string; // 최초등록일 + purchaseDate: string; // 구매일자 + partsChangeCycle: string; // 부속품 교환주기(Km) + partsChangeRecords: PartsChangeRecord[]; // 부속품 교환 기록 (테이블) + maintenanceRecords: ForkliftMaintenanceRecord[]; // 정비내역 (테이블) + remarks: string; // 비고 +} + +export interface ForkliftFormData { + vehicleNumber: string; + vehicleType: string; + purchaseType: string; + managerMain: string; + managerSub: string; + purchaseCompany: string; + purchaseCompanyContact: string; + totalMileage: string; + mileageRecordDate: string; + firstRegistrationDate: string; + purchaseDate: string; + partsChangeCycle: string; + partsChangeRecords: PartsChangeRecord[]; + maintenanceRecords: ForkliftMaintenanceRecord[]; + remarks: string; +} + +// ===== API 응답 타입 ===== +export interface ActionResponse { + success: boolean; + data?: T; + error?: string; + __authError?: boolean; +} + +export interface ListResponse { + success: boolean; + data: T[]; + totalCount: number; + totalPages: number; + error?: string; +} diff --git a/src/layouts/AuthenticatedLayout.tsx b/src/layouts/AuthenticatedLayout.tsx index 6322554d..822623aa 100644 --- a/src/layouts/AuthenticatedLayout.tsx +++ b/src/layouts/AuthenticatedLayout.tsx @@ -903,7 +903,7 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro return (
{/* 헤더 - 전체 너비 상단 고정 */} -
+
{/* SAM 로고 섹션 - 클릭 시 대시보드로 이동 */} @@ -1161,7 +1161,7 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
{/* 메인 콘텐츠 */} -
+
{children}