# 차량관리 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; 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) { 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 사용 (신규 폼 필수) // StatusBadge 사용 ``` --- ## 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