- 프론트엔드 개발자용 구현 가이드 (3개 메뉴) - 파일 구조, 타입 정의, API 호출 예시 포함 - 컴포넌트 활용 가이드, 배지 색상, 체크리스트 - vehicle-api.md 엔드포인트 수 17→20 업데이트
728 lines
23 KiB
Markdown
728 lines
23 KiB
Markdown
# 차량관리 React 구현 요청서
|
|
|
|
> **작성일**: 2026-03-12
|
|
> **요청자**: R&D 실장
|
|
> **대상**: 프론트엔드 개발자
|
|
> **우선순위**: 🟡 중요
|
|
> **API 상태**: 이관 진행중 (사진 API 구현 완료, CRUD API 구현 예정)
|
|
|
|
---
|
|
|
|
## 1. 개요
|
|
|
|
MNG에서 운영중인 차량관리 3개 메뉴를 React(Next.js)로 구현한다.
|
|
멀티테넌트 지원을 위한 이관 작업이며, API는 순차적으로 제공된다.
|
|
|
|
### 1.1 구현 대상
|
|
|
|
| 메뉴 | 설명 | 난이도 |
|
|
|------|------|:------:|
|
|
| **차량목록** | 법인/렌트/리스 차량 등록 관리 + 사진 | 중 |
|
|
| **차량일지** | 운행기록 CRUD + 월별 통계 | 중 |
|
|
| **정비이력** | 정비/주유/보험 비용 관리 + 카테고리별 집계 | 중 |
|
|
|
|
### 1.2 참고 문서
|
|
|
|
| 문서 | 경로 | 용도 |
|
|
|------|------|------|
|
|
| **API 명세** (필독) | `docs/frontend/api-specs/vehicle-api.md` | 전체 엔드포인트, 데이터 모델, UI 가이드 |
|
|
| 차량목록 기능 상세 | `docs/features/card-vehicle/corporate-vehicles.md` | MNG 기존 동작 |
|
|
| 차량일지 기능 상세 | `docs/features/card-vehicle/vehicle-logs.md` | MNG 기존 동작 |
|
|
| 정비이력 기능 상세 | `docs/features/card-vehicle/vehicle-maintenance.md` | MNG 기존 동작 |
|
|
|
|
---
|
|
|
|
## 2. 파일 구조 (제안)
|
|
|
|
```
|
|
src/
|
|
├── app/[locale]/(protected)/
|
|
│ ├── vehicles/ # 차량목록
|
|
│ │ ├── page.tsx # 목록 페이지
|
|
│ │ ├── create/page.tsx # 등록 페이지
|
|
│ │ └── [id]/
|
|
│ │ ├── page.tsx # 상세 페이지
|
|
│ │ └── edit/page.tsx # 수정 페이지
|
|
│ │
|
|
│ ├── vehicle-logs/ # 차량일지
|
|
│ │ └── page.tsx # 목록 + 등록/수정 모달
|
|
│ │
|
|
│ └── vehicle-maintenance/ # 정비이력
|
|
│ └── page.tsx # 목록 + 등록/수정 모달
|
|
│
|
|
├── components/vehicles/ # 차량 관련 컴포넌트
|
|
│ ├── VehicleListClient.tsx # 차량목록 클라이언트
|
|
│ ├── VehicleForm.tsx # 차량 등록/수정 폼
|
|
│ ├── VehiclePhotoSection.tsx # 사진 업로드/관리 영역
|
|
│ ├── VehicleDetailView.tsx # 차량 상세 뷰
|
|
│ ├── VehicleLogListClient.tsx # 차량일지 클라이언트
|
|
│ ├── VehicleLogModal.tsx # 차량일지 등록/수정 모달
|
|
│ ├── VehicleMaintenanceListClient.tsx # 정비이력 클라이언트
|
|
│ └── VehicleMaintenanceModal.tsx # 정비이력 등록/수정 모달
|
|
│
|
|
├── lib/api/
|
|
│ └── vehicle.ts # 차량 관련 Server Action
|
|
│
|
|
├── hooks/
|
|
│ ├── useVehicleList.ts # 차량목록 훅
|
|
│ ├── useVehicleLogs.ts # 차량일지 훅
|
|
│ └── useVehicleMaintenance.ts # 정비이력 훅
|
|
│
|
|
└── types/
|
|
└── vehicle.ts # 차량 관련 타입 정의
|
|
```
|
|
|
|
---
|
|
|
|
## 3. 메뉴별 구현 상세
|
|
|
|
### 3.1 차량목록
|
|
|
|
#### 화면 구성
|
|
|
|
```
|
|
┌─ PageHeader ──────────────────────────
|
|
│ 제목: "차량목록"
|
|
│ [검색] [Excel 다운로드] [+ 차량 등록]
|
|
│
|
|
├─ StatCards (4열) ─────────────────────
|
|
│ 총 차량 | 법인 취득가 | 월 렌트/리스비 | 총 주행거리
|
|
│
|
|
├─ 필터 바 ─────────────────────────────
|
|
│ 소유형태: [전체] [법인] [렌트] [리스] ← 탭 or 버튼 그룹
|
|
│ 상태: [전체] [운행중] [정비중] [처분]
|
|
│
|
|
├─ DataTable ───────────────────────────
|
|
│ 차량번호 | 차종/모델 | 소유형태(배지) | 운전자 | 주행거리 | 상태(배지)
|
|
│ └─ 행 클릭 → /vehicles/{id} 상세 페이지
|
|
│
|
|
└─ 빈 상태: EmptyState "등록된 차량이 없습니다."
|
|
```
|
|
|
|
#### API 호출
|
|
|
|
```typescript
|
|
// lib/api/vehicle.ts
|
|
export async function getVehicles(filters) {
|
|
return executePaginatedAction({
|
|
url: buildApiUrl('/api/v1/corporate-vehicles', filters),
|
|
transform: mapVehicleResponse,
|
|
errorMessage: '차량 목록 조회 실패',
|
|
});
|
|
}
|
|
```
|
|
|
|
#### 요약 카드 계산 (프론트엔드에서 처리)
|
|
|
|
| 카드 | 계산식 |
|
|
|------|--------|
|
|
| 총 차량 | 목록 전체 건수 |
|
|
| 법인 취득가 | `ownership_type === 'corporate'`인 차량의 `purchase_price` 합계 |
|
|
| 월 렌트/리스비 | `ownership_type in ('rent','lease')`인 차량의 `monthly_rent + monthly_rent_tax` 합계 |
|
|
| 총 주행거리 | 전체 차량의 `total_mileage` 합계 |
|
|
|
|
#### 등록/수정 폼 — 조건부 필드
|
|
|
|
`ownership_type` 값에 따라 폼 하단 필드가 변경된다:
|
|
|
|
| 소유형태 | 표시 필드 |
|
|
|---------|----------|
|
|
| `corporate` (법인) | 취득일자, 취득가액 |
|
|
| `rent` / `lease` | 계약일자, 렌트/리스사, 연락처, 계약기간, 약정주행거리, 차량가액, 잔존가치, 보증금, 월렌트료(공급가), 월렌트료(부가세), 보험사, 보험사 연락처 |
|
|
|
|
#### 사진 기능 (차량 상세/수정 페이지)
|
|
|
|
```
|
|
┌─ 사진 영역 ──────────────────────────
|
|
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
|
|
│ │ 📷 │ │ 📷 │ │ 📷 │ │ + │
|
|
│ │ 사진1 │ │ 사진2 │ │ 사진3 │ │ 추가 │
|
|
│ └──────┘ └──────┘ └──────┘ └──────┘
|
|
│ 3 / 10장
|
|
└──────────────────────────────────────
|
|
```
|
|
|
|
| 기능 | 설명 |
|
|
|------|------|
|
|
| 업로드 | `POST /{id}/photos`, `multipart/form-data`, `files[]` 다중 선택 |
|
|
| 삭제 | `DELETE /{id}/photos/{fileId}`, 각 사진에 X 버튼 |
|
|
| 제한 | 최대 10장, 10MB/파일, jpg/jpeg/png/gif/bmp/webp |
|
|
| 미리보기 | 업로드 전 FileReader API로 미리보기 |
|
|
| 확대 보기 | 사진 클릭 시 모달 확대 (선택 구현) |
|
|
| 카운터 | `{현재 수} / 10장` 표시, 10장 도달 시 추가 버튼 비활성화 |
|
|
|
|
#### 배지 색상
|
|
|
|
**소유형태 (`ownership_type`)**:
|
|
|
|
| 값 | 라벨 | 색상 |
|
|
|----|------|------|
|
|
| `corporate` | 법인 | purple |
|
|
| `rent` | 렌트 | blue |
|
|
| `lease` | 리스 | green |
|
|
|
|
**상태 (`status`)**:
|
|
|
|
| 값 | 라벨 | 색상 |
|
|
|----|------|------|
|
|
| `active` | 운행중 | green |
|
|
| `maintenance` | 정비중 | yellow |
|
|
| `disposed` | 처분 | red |
|
|
|
|
---
|
|
|
|
### 3.2 차량일지
|
|
|
|
#### 화면 구성
|
|
|
|
```
|
|
┌─ PageHeader ──────────────────────────
|
|
│ 차량: [드롭다운 ▼] (현재 주행거리: 17,350 km)
|
|
│ [2026년 ▼] [3월 ▼] [CSV 다운로드] [+ 기록 추가]
|
|
│
|
|
├─ StatCards (4열) ─────────────────────
|
|
│ 출근 (5건 / 125km) | 퇴근 (5건 / 130km) | 업무 (10건 / 280km) | 비업무 (2건 / 45km)
|
|
│
|
|
├─ DataTable ───────────────────────────
|
|
│ 날짜 | 차량 | 부서/이름 | 용도(배지) | 출발지 | 도착지 | 거리(km) | 비고 | 작업
|
|
│ └─ 작업: [복사] [수정] [삭제]
|
|
│
|
|
└─ 등록/수정 모달 (StandardDialog)
|
|
```
|
|
|
|
#### API 호출
|
|
|
|
```typescript
|
|
// 목록 조회
|
|
getVehicleLogs({ vehicle_id, year, month, trip_type, search })
|
|
|
|
// 통계 조회 (별도 API)
|
|
getVehicleLogSummary({ vehicle_id, year, month })
|
|
|
|
// 차량 드롭다운
|
|
getVehicleDropdown() // GET /api/v1/corporate-vehicles/dropdown
|
|
```
|
|
|
|
#### 통계 카드 (summary API 응답 사용)
|
|
|
|
API 응답 `data.byType`의 각 항목에서 `count`와 `distance`를 표시한다.
|
|
`data.total`로 전체 합계도 표시 가능하다.
|
|
|
|
#### 등록/수정 모달
|
|
|
|
```
|
|
┌─ 모달 ────────────────────────────────
|
|
│ 차량: [드롭다운 ▼]
|
|
│ 날짜: [2026-03-12] 부서: [영업부]
|
|
│ 운전자: [홍길동] 용도: [업무용 ▼]
|
|
│
|
|
│ ── 출발지 ──
|
|
│ 유형: [회사 ▼] 이름: [본사] 주소: [서울시 강남구...]
|
|
│
|
|
│ ── 도착지 ── [↕ 출발↔도착 교환]
|
|
│ 유형: [거래처 ▼] 이름: [거래처A] 주소: [경기도 성남시...]
|
|
│
|
|
│ 운행거리: [45] km
|
|
│
|
|
│ 비고: [거래처방문 ]
|
|
│ [거래처방문] [제조시설등] [회의참석] [판촉활동] [교육등] ← 프리셋 버튼
|
|
│
|
|
│ [삭제] [취소] [저장]
|
|
└───────────────────────────────────────
|
|
```
|
|
|
|
#### 특수 기능 (프론트엔드 로직)
|
|
|
|
| 기능 | 동작 |
|
|
|------|------|
|
|
| **기록 복사** | 기존 행의 데이터를 모달에 채워서 열기 (날짜만 오늘로 변경). API는 일반 POST |
|
|
| **출발↔도착 교환** | 출발지/도착지 필드 swap + `tripType` 자동 전환 (`commute_to` ↔ `commute_from`) |
|
|
| **비고 프리셋** | 5개 버튼 클릭 시 비고 input에 텍스트 삽입 |
|
|
| **왕복 용도** | `commute_round`, `business_round`, `personal_round` 선택 시 편도의 2배로 `distance_km` 자동 제안 (선택) |
|
|
|
|
#### 용도 배지 색상
|
|
|
|
| 값 | 라벨 | 색상 |
|
|
|----|------|------|
|
|
| `commute_to` | 출근용 | green |
|
|
| `commute_from` | 퇴근용 | blue |
|
|
| `business` | 업무용 | purple |
|
|
| `personal` | 비업무 | gray |
|
|
| `commute_round` | 출퇴근 왕복 | green |
|
|
| `business_round` | 업무용 왕복 | purple |
|
|
| `personal_round` | 비업무 왕복 | gray |
|
|
|
|
#### 위치 유형 드롭다운
|
|
|
|
| 값 | 라벨 |
|
|
|----|------|
|
|
| `home` | 자택 |
|
|
| `office` | 회사 |
|
|
| `client` | 거래처 |
|
|
| `other` | 기타 |
|
|
|
|
---
|
|
|
|
### 3.3 정비이력
|
|
|
|
#### 화면 구성
|
|
|
|
```
|
|
┌─ PageHeader ──────────────────────────
|
|
│ 제목: "정비이력"
|
|
│ [새로고침] [CSV 다운로드] [+ 정비 등록]
|
|
│
|
|
├─ StatCards (4열) ─────────────────────
|
|
│ 총 정비비용 | 주유비 | 정비비 | 기타비용
|
|
│
|
|
├─ 필터 영역 ───────────────────────────
|
|
│ 기간: [시작일] ~ [종료일] (기본: 최근 3개월)
|
|
│ 카테고리: [전체] [주유] [정비] [보험] [세차] [주차] [통행료] [검사] [기타]
|
|
│ 차량: [드롭다운 ▼]
|
|
│ 검색: [설명, 업체명 검색...]
|
|
│
|
|
├─ DataTable ───────────────────────────
|
|
│ 날짜 | 차량 | 카테고리(배지) | 설명 | 금액 | 주행거리 | 작업
|
|
│ └─ 작업: [수정] [삭제]
|
|
│
|
|
└─ 등록/수정 모달 (StandardDialog)
|
|
```
|
|
|
|
#### 요약 카드 계산 (조회된 목록 데이터 기준)
|
|
|
|
| 카드 | 계산식 |
|
|
|------|--------|
|
|
| 총 정비비용 | 전체 `amount` 합계 |
|
|
| 주유비 | `category === '주유'`인 `amount` 합계 |
|
|
| 정비비 | `category === '정비'`인 `amount` 합계 |
|
|
| 기타비용 | 총 정비비용 - 주유비 - 정비비 |
|
|
|
|
#### 등록/수정 모달
|
|
|
|
```
|
|
┌─ 모달 ────────────────────────────────
|
|
│ 차량: [드롭다운 ▼]
|
|
│ 날짜: [2026-03-10]
|
|
│ 카테고리: [주유 ▼]
|
|
│ 설명: [LPG 충전]
|
|
│ 금액: [85,000] 원 ← currency 포맷
|
|
│ 주행거리: [17,350] km
|
|
│ 업체명: [SK에너지 강남점]
|
|
│ 메모: [ ]
|
|
│
|
|
│ [삭제] [취소] [저장]
|
|
└───────────────────────────────────────
|
|
```
|
|
|
|
#### 카테고리 배지 색상
|
|
|
|
| 카테고리 | 색상 |
|
|
|---------|------|
|
|
| 주유 | amber |
|
|
| 정비 | blue |
|
|
| 보험 | emerald |
|
|
| 세차 | cyan |
|
|
| 주차 | purple |
|
|
| 통행료 | orange |
|
|
| 검사 | indigo |
|
|
| 기타 | gray |
|
|
|
|
#### 부수 효과 안내
|
|
|
|
정비 등록/수정 시 `mileage` 값이 있으면 해당 차량의 기준 주행거리가 자동 갱신된다.
|
|
프론트엔드에서는 별도 처리 불필요 (API가 자동 처리).
|
|
|
|
---
|
|
|
|
## 4. 타입 정의
|
|
|
|
```typescript
|
|
// types/vehicle.ts
|
|
|
|
// ─── 차량 ───
|
|
export interface CorporateVehicle {
|
|
id: number;
|
|
plateNumber: string;
|
|
model: string;
|
|
vehicleType: string;
|
|
ownershipType: 'corporate' | 'rent' | 'lease';
|
|
year?: number;
|
|
driver?: string;
|
|
status: 'active' | 'maintenance' | 'disposed';
|
|
mileage: number;
|
|
memo?: string;
|
|
// 법인 전용
|
|
purchaseDate?: string;
|
|
purchasePrice?: number;
|
|
// 렌트/리스 전용
|
|
contractDate?: string;
|
|
rentCompany?: string;
|
|
rentCompanyTel?: string;
|
|
rentPeriod?: string;
|
|
agreedMileage?: string;
|
|
vehiclePrice?: number;
|
|
residualValue?: number;
|
|
deposit?: number;
|
|
monthlyRent?: number;
|
|
monthlyRentTax?: number;
|
|
insuranceCompany?: string;
|
|
insuranceCompanyTel?: string;
|
|
// 계산 필드
|
|
logDistance: number;
|
|
totalMileage: number;
|
|
}
|
|
|
|
export interface VehicleDropdownItem {
|
|
id: number;
|
|
plateNumber: string;
|
|
model: string;
|
|
}
|
|
|
|
// ─── 차량 사진 ───
|
|
export interface VehiclePhoto {
|
|
id: number;
|
|
fileName: string;
|
|
filePath: string;
|
|
fileUrl: string;
|
|
fileSize: number;
|
|
mimeType: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
// ─── 운행기록 ───
|
|
export interface VehicleLog {
|
|
id: number;
|
|
logDate: string;
|
|
vehicleId: number;
|
|
plateNumber: string;
|
|
model: string;
|
|
department?: string;
|
|
driverName: string;
|
|
tripType: TripType;
|
|
departureType?: LocationType;
|
|
departureName?: string;
|
|
departureAddress?: string;
|
|
arrivalType?: LocationType;
|
|
arrivalName?: string;
|
|
arrivalAddress?: string;
|
|
distanceKm: number;
|
|
note?: string;
|
|
}
|
|
|
|
export type TripType =
|
|
| 'commute_to' | 'commute_from'
|
|
| 'business' | 'personal'
|
|
| 'commute_round' | 'business_round' | 'personal_round';
|
|
|
|
export type LocationType = 'home' | 'office' | 'client' | 'other';
|
|
|
|
export interface VehicleLogSummary {
|
|
byType: Record<string, { label: string; count: number; distance: number }>;
|
|
total: { count: number; distance: number };
|
|
}
|
|
|
|
// ─── 정비이력 ───
|
|
export interface VehicleMaintenance {
|
|
id: number;
|
|
date: string;
|
|
vehicleId: number;
|
|
plateNumber: string;
|
|
model: string;
|
|
category: MaintenanceCategory;
|
|
description?: string;
|
|
amount: number;
|
|
mileage?: number;
|
|
vendor?: string;
|
|
memo?: string;
|
|
}
|
|
|
|
export type MaintenanceCategory =
|
|
| '주유' | '정비' | '보험' | '세차'
|
|
| '주차' | '통행료' | '검사' | '기타';
|
|
```
|
|
|
|
---
|
|
|
|
## 5. API 호출 예시
|
|
|
|
```typescript
|
|
// lib/api/vehicle.ts
|
|
'use server';
|
|
|
|
import { buildApiUrl } from '@/lib/api/query-params';
|
|
import { executePaginatedAction, executeServerAction } from '@/lib/api/execute-server-action';
|
|
|
|
// ─── 차량 목록 ───
|
|
export async function getVehicles(params: {
|
|
ownership_type?: string;
|
|
vehicle_type?: string;
|
|
status?: string;
|
|
search?: string;
|
|
}) {
|
|
return executePaginatedAction({
|
|
url: buildApiUrl('/api/v1/corporate-vehicles', params),
|
|
transform: mapVehicleResponse,
|
|
errorMessage: '차량 목록 조회 실패',
|
|
});
|
|
}
|
|
|
|
// ─── 차량 드롭다운 ───
|
|
export async function getVehicleDropdown() {
|
|
return executeServerAction({
|
|
url: buildApiUrl('/api/v1/corporate-vehicles/dropdown'),
|
|
errorMessage: '차량 목록 조회 실패',
|
|
});
|
|
}
|
|
|
|
// ─── 차량 등록 ───
|
|
export async function createVehicle(data: Record<string, unknown>) {
|
|
return executeServerAction({
|
|
url: buildApiUrl('/api/v1/corporate-vehicles'),
|
|
options: { method: 'POST', body: JSON.stringify(data) },
|
|
errorMessage: '차량 등록 실패',
|
|
});
|
|
}
|
|
|
|
// ─── 차량 사진 ───
|
|
export async function getVehiclePhotos(vehicleId: number) {
|
|
return executeServerAction({
|
|
url: buildApiUrl(`/api/v1/corporate-vehicles/${vehicleId}/photos`),
|
|
errorMessage: '사진 조회 실패',
|
|
});
|
|
}
|
|
|
|
// ─── 운행기록 목록 ───
|
|
export async function getVehicleLogs(params: {
|
|
vehicle_id?: number;
|
|
year?: number;
|
|
month?: number;
|
|
trip_type?: string;
|
|
search?: string;
|
|
}) {
|
|
return executePaginatedAction({
|
|
url: buildApiUrl('/api/v1/vehicle-logs', params),
|
|
transform: (item) => item, // camelCase 그대로
|
|
errorMessage: '운행기록 조회 실패',
|
|
});
|
|
}
|
|
|
|
// ─── 운행기록 통계 ───
|
|
export async function getVehicleLogSummary(params: {
|
|
vehicle_id?: number;
|
|
year?: number;
|
|
month?: number;
|
|
}) {
|
|
return executeServerAction({
|
|
url: buildApiUrl('/api/v1/vehicle-logs/summary', params),
|
|
errorMessage: '운행 통계 조회 실패',
|
|
});
|
|
}
|
|
|
|
// ─── 정비이력 목록 ───
|
|
export async function getMaintenances(params: {
|
|
vehicle_id?: number;
|
|
category?: string;
|
|
start_date?: string;
|
|
end_date?: string;
|
|
search?: string;
|
|
}) {
|
|
return executePaginatedAction({
|
|
url: buildApiUrl('/api/v1/vehicle-maintenances', params),
|
|
transform: (item) => item,
|
|
errorMessage: '정비이력 조회 실패',
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. 공통 컴포넌트 활용 가이드
|
|
|
|
기존 프로젝트에 구현된 공통 컴포넌트를 **반드시 재사용**한다.
|
|
|
|
| 컴포넌트 | 위치 | 용도 |
|
|
|---------|------|------|
|
|
| `PageLayout` | `components/organisms/` | 페이지 기본 레이아웃 |
|
|
| `PageHeader` | `components/organisms/` | 페이지 헤더 (제목 + 액션 버튼) |
|
|
| `StatCards` | `components/organisms/` | 요약 통계 카드 |
|
|
| `DataTable` | `components/organisms/` | 데이터 테이블 |
|
|
| `EmptyState` | `components/organisms/` | 빈 상태 표시 |
|
|
| `StandardDialog` | `components/molecules/` | 모달 다이얼로그 |
|
|
| `FormField` | `components/molecules/` | 통합 폼 필드 (text, number, date, select, currency 등) |
|
|
| `StatusBadge` | `components/molecules/` | 상태 배지 |
|
|
| `DateRangeSelector` | `components/molecules/` | 기간 선택 |
|
|
| `SearchableSelectionModal` | `components/organisms/` | 검색형 선택 모달 |
|
|
|
|
### 사용 예시
|
|
|
|
```tsx
|
|
// FormField 사용 (신규 폼 필수)
|
|
<FormField
|
|
label="차량번호"
|
|
type="text"
|
|
value={plateNumber}
|
|
onChange={setPlateNumber}
|
|
required
|
|
error={errors.plateNumber}
|
|
/>
|
|
|
|
<FormField
|
|
label="취득가액"
|
|
type="currency"
|
|
value={purchasePrice}
|
|
onChange={setPurchasePrice}
|
|
unit="원"
|
|
/>
|
|
|
|
// StatusBadge 사용
|
|
<StatusBadge status={vehicle.status} config={vehicleStatusConfig} />
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 숫자/금액 포맷
|
|
|
|
| 항목 | 포맷 | 예시 |
|
|
|------|------|------|
|
|
| 금액 | 천단위 콤마 + "원" | `85,000원` |
|
|
| 거리 | 천단위 콤마 + "km" | `17,350 km` |
|
|
| 취득가 | 천단위 콤마 + "원" | `85,000,000원` |
|
|
| 월렌트료 | 천단위 콤마 + "원" | `1,200,000원` |
|
|
|
|
기존 `lib/utils/amount.ts`의 포맷 함수를 사용한다.
|
|
|
|
---
|
|
|
|
## 8. 사진 업로드 컴포넌트 구현 가이드
|
|
|
|
사진 업로드는 별도 컴포넌트(`VehiclePhotoSection`)로 분리한다.
|
|
|
|
### 핵심 로직
|
|
|
|
```typescript
|
|
// 사진 업로드 (multipart/form-data)
|
|
async function uploadPhotos(vehicleId: number, files: File[]) {
|
|
const formData = new FormData();
|
|
files.forEach(file => formData.append('files[]', file));
|
|
|
|
// Next.js 프록시를 통해 전송
|
|
const res = await fetch(`/api/proxy/corporate-vehicles/${vehicleId}/photos`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
// Content-Type 헤더를 직접 설정하지 않음 (브라우저가 boundary 자동 설정)
|
|
});
|
|
return res.json();
|
|
}
|
|
```
|
|
|
|
### 주의사항
|
|
|
|
```
|
|
❌ Content-Type: multipart/form-data 직접 설정 금지 (boundary 누락됨)
|
|
❌ JSON.stringify(formData) 금지
|
|
✅ FormData 객체를 body에 그대로 전달
|
|
✅ 파일 선택 시 남은 수량(10 - 현재) 체크 후 초과분 차단
|
|
```
|
|
|
|
---
|
|
|
|
## 9. 라우팅 및 메뉴 등록
|
|
|
|
### 페이지 라우트
|
|
|
|
| 경로 | 페이지 |
|
|
|------|--------|
|
|
| `/vehicles` | 차량 목록 |
|
|
| `/vehicles/create` | 차량 등록 |
|
|
| `/vehicles/{id}` | 차량 상세 |
|
|
| `/vehicles/{id}/edit` | 차량 수정 |
|
|
| `/vehicle-logs` | 차량일지 (모달 CRUD) |
|
|
| `/vehicle-maintenance` | 정비이력 (모달 CRUD) |
|
|
|
|
### 메뉴 등록
|
|
|
|
메뉴 DB 등록은 별도 요청 예정. 개발 단계에서는 URL 직접 접근으로 테스트한다.
|
|
|
|
---
|
|
|
|
## 10. 데이터 관계 요약
|
|
|
|
```
|
|
corporate_vehicles (차량 마스터)
|
|
│
|
|
├── vehicle_logs (1:N)
|
|
│ 운행기록 → distance_km 합산 → 차량 total_mileage 계산
|
|
│
|
|
├── vehicle_maintenances (1:N)
|
|
│ 정비기록 → mileage 입력 시 차량 기준 주행거리 자동 갱신
|
|
│
|
|
└── files (1:N, polymorphic)
|
|
사진 → 최대 10장, R2 스토리지
|
|
|
|
총 주행거리 = mileage (정비 갱신 기준값) + SUM(vehicle_logs.distance_km)
|
|
```
|
|
|
|
---
|
|
|
|
## 11. 작업 순서 (권장)
|
|
|
|
| 순서 | 작업 | 의존성 |
|
|
|:----:|------|--------|
|
|
| 1 | `types/vehicle.ts` 타입 정의 | 없음 |
|
|
| 2 | `lib/api/vehicle.ts` Server Action | 타입 정의 |
|
|
| 3 | **차량목록** 페이지 (가장 기본) | Server Action |
|
|
| 4 | 차량 사진 컴포넌트 | 차량목록 |
|
|
| 5 | **차량일지** 페이지 (드롭다운이 차량목록 API 사용) | 차량목록 API |
|
|
| 6 | **정비이력** 페이지 (드롭다운이 차량목록 API 사용) | 차량목록 API |
|
|
|
|
---
|
|
|
|
## 12. 체크리스트
|
|
|
|
### 공통
|
|
- [ ] 모든 페이지 `'use client'` 선언
|
|
- [ ] `FormField` 컴포넌트 사용 (Label+Input 수동 조합 금지)
|
|
- [ ] `buildApiUrl` 사용 (new URLSearchParams 금지)
|
|
- [ ] 금액은 `currency` 포맷, 거리는 천단위 콤마 + km
|
|
- [ ] 빈 상태 표시 (`EmptyState` 컴포넌트)
|
|
- [ ] 에러 처리 (토스트 알림)
|
|
|
|
### 차량목록
|
|
- [ ] 소유형태별 조건부 폼 필드 (법인 vs 렌트/리스)
|
|
- [ ] 요약 카드 4개 (총 차량, 법인 취득가, 월 렌트/리스비, 총 주행거리)
|
|
- [ ] 소유형태/상태 배지 색상
|
|
- [ ] 사진 업로드/삭제 (최대 10장, 카운터 표시)
|
|
|
|
### 차량일지
|
|
- [ ] 차량 드롭다운 (dropdown API)
|
|
- [ ] 연/월 선택 필터
|
|
- [ ] 용도별 통계 카드 (summary API)
|
|
- [ ] 기록 복사 기능 (날짜만 오늘로)
|
|
- [ ] 출발↔도착 교환 기능 (tripType 자동 전환)
|
|
- [ ] 비고 프리셋 버튼 5개
|
|
|
|
### 정비이력
|
|
- [ ] 기간 필터 (기본: 최근 3개월)
|
|
- [ ] 카테고리 필터 (8종)
|
|
- [ ] 요약 카드 4개 (총 정비비용, 주유비, 정비비, 기타비용)
|
|
- [ ] 카테고리별 배지 색상 (8종)
|
|
- [ ] 금액 currency 포맷
|
|
|
|
---
|
|
|
|
## 13. 질문/논의 사항
|
|
|
|
API 구현은 순차적으로 진행된다.
|
|
현재 **차량 사진 API**(#18~20)만 구현 완료 상태이며,
|
|
차량 CRUD(#1~6), 운행기록(#7~12), 정비이력(#13~17) API는 이관 진행 예정이다.
|
|
|
|
API 완성 전에 타입 정의, 컴포넌트 구조, UI 레이아웃을 먼저 작업하고,
|
|
API 연동은 엔드포인트 완성 후 순차적으로 연결하면 된다.
|
|
|
|
궁금한 사항은 `docs/frontend/api-specs/vehicle-api.md` 문서를 먼저 확인하고,
|
|
추가 질문은 Slack 또는 대면으로 요청한다.
|
|
|
|
---
|
|
|
|
**최종 업데이트**: 2026-03-12
|