Files
sam-docs/plans/vehicle-react-implementation.md
김보곤 93ddc74352 docs: [vehicle] 차량관리 React 구현 요청서 작성
- 프론트엔드 개발자용 구현 가이드 (3개 메뉴)
- 파일 구조, 타입 정의, API 호출 예시 포함
- 컴포넌트 활용 가이드, 배지 색상, 체크리스트
- vehicle-api.md 엔드포인트 수 17→20 업데이트
2026-03-12 22:38:13 +09:00

23 KiB

차량관리 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 호출

// 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 호출

// 목록 조회
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의 각 항목에서 countdistance를 표시한다. data.total로 전체 합계도 표시 가능하다.

등록/수정 모달

┌─ 모달 ────────────────────────────────
│  차량: [드롭다운 ▼]
│  날짜: [2026-03-12]    부서: [영업부]
│  운전자: [홍길동]      용도: [업무용 ▼]
│
│  ── 출발지 ──
│  유형: [회사 ▼]  이름: [본사]  주소: [서울시 강남구...]
│
│  ── 도착지 ──            [↕ 출발↔도착 교환]
│  유형: [거래처 ▼]  이름: [거래처A]  주소: [경기도 성남시...]
│
│  운행거리: [45] km
│
│  비고: [거래처방문              ]
│  [거래처방문] [제조시설등] [회의참석] [판촉활동] [교육등]  ← 프리셋 버튼
│
│           [삭제]         [취소] [저장]
└───────────────────────────────────────

특수 기능 (프론트엔드 로직)

기능 동작
기록 복사 기존 행의 데이터를 모달에 채워서 열기 (날짜만 오늘로 변경). API는 일반 POST
출발↔도착 교환 출발지/도착지 필드 swap + tripType 자동 전환 (commute_tocommute_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. 타입 정의

// 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 호출 예시

// 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/ 검색형 선택 모달

사용 예시

// 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)로 분리한다.

핵심 로직

// 사진 업로드 (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(#1820)만 구현 완료 상태이며, 차량 CRUD(#16), 운행기록(#712), 정비이력(#1317) API는 이관 진행 예정이다.

API 완성 전에 타입 정의, 컴포넌트 구조, UI 레이아웃을 먼저 작업하고, API 연동은 엔드포인트 완성 후 순차적으로 연결하면 된다.

궁금한 사항은 docs/frontend/api-specs/vehicle-api.md 문서를 먼저 확인하고, 추가 질문은 Slack 또는 대면으로 요청한다.


최종 업데이트: 2026-03-12