IT 개발,관리,연동,자동화

트럭커-프리(Trucker-Free) : 개선된 투명한 화물 거래 생태계 플랫폼

_Blue_Sky_ 2026. 2. 27. 15:34
728x90

먼저 현황을 파악하기 위해 웹 검색을 진행하겠습니다.좋은 자료 감사합니다. 현재 화물 운송 플랫폼의 핵심 문제점을 파악했습니다. 제시하신 기획안을 분석하고 개선된 기획안을 작성하겠습니다.


📦 기획안: "트럭커-프리(Trucker-Free)"

투명한 화물 거래 생태계 플랫폼


🎯 1. 핵심 문제점 분석

현재 플랫폼의 주요 문제:

  1. 깜깜이 수수료 (10~40% 불투명한 중간 마진)
  2. Pay-to-Work 구조 (월 27,500원 고정 앱 사용료)
  3. 불량 업체 방치 (신고해도 플랫폼이 방치)
  4. 운임 미수 사고 (월 2~3건 발생)
  5. 법적 책임 회피 (정보통신사업자로만 등록)
  6. 화주-차주 직접 소통 불가

🚀 2. 개선된 핵심 기능

2.1 완전 투명한 수수료 시스템

수정 사항:

  • 수수료 실시간 공개: 모든 거래에서 플랫폼이 가져가는 수수료를 거래 전에 명시
  • 고정 수수료율: 화주 측 3%, 차주 측 2% (총 5%로 고정)
  • 수수료 분석 대시보드: 월별 수수료 내역 자동 제공
  • 중개업체 금지: 직거래만 가능하도록 시스템 설계

💡 추가 기능:

[거래 화면 예시]
━━━━━━━━━━━━━━━━━━━━━
운임 총액: 500,000원
━━━━━━━━━━━━━━━━━━━━━
차주 수령액: 490,000원 (98%)
플랫폼 수수료: 10,000원 (2%)
━━━━━━━━━━━━━━━━━━━━━
화주 지불액: 515,000원
플랫폼 수수료: 15,000원 (3%)
━━━━━━━━━━━━━━━━━━━━━

2.2 개선된 입찰 시스템

기존 기획안 보완:

  1. 역경매 + 즉시 매칭 하이브리드
    • 긴급 화물: 즉시 매칭 (AI 추천)
    • 일반 화물: 역경매 (24시간 입찰 기간)
    • 정기 화물: 장기 계약 (월 단위 고정가)
  2. 스마트 입찰 도우미
    • AI가 추천 입찰가 제시 (과거 데이터 기반)
    • 실시간 경쟁 입찰 현황 공개
    • 입찰 성공률 예측 기능

💡 신규 추가:

[AI 추천 시스템]
- 비슷한 구간 평균 운임: 480,000원
- 현재 경쟁 입찰: 3건
- 추천 입찰가: 470,000원 (성공률 85%)
- 최저가 입찰: 450,000원 (낙찰 확률 95%)

2.3 강화된 신용 및 안전 시스템

기존 기획안 보완:

1) 5단계 신용 등급 체계

S등급 (Star): 별점 4.9↑, 거래 100건↑ → 수수료 50% 할인
A등급 (Ace): 별점 4.7↑, 거래 50건↑ → 수수료 30% 할인
B등급 (Basic): 별점 4.5↑, 거래 20건↑ → 수수료 15% 할인
C등급 (Caution): 별점 4.0↑ → 정상 수수료
D등급 (Danger): 별점 4.0↓ → 경고 및 제재

2) 블랙리스트 자동화

  • 노쇼 1회: 경고
  • 노쇼 2회: 7일 활동 정지
  • 노쇼 3회: 영구 퇴출
  • 운임 미지급 2회: 즉시 영구 퇴출

💡 신규 추가:

3) 보증금 시스템

  • 신규 가입 시: 화주 50만원, 차주 30만원 예치
  • 신용 등급 상승 시 보증금 단계적 감액
  • S등급 도달 시 보증금 전액 반환

4) 실시간 위치 공유 의무화

  • GPS 추적 필수 (프라이버시 보호 설정 제공)
  • 예상 도착 시간 자동 업데이트
  • 지연 시 자동 알림 발송

2.4 완전 자동화된 비용 계산 시스템

기존 기획안 대폭 강화:

실시간 API 연동

[비용 자동 산출 항목]
1. 기본 운임 (거리 × 톤수 × 차종)
2. 고속도로 통행료 (실시간 API)
3. 유류비 (거리 × 차종별 연비 × 당일 유가)
4. 상하차 인건비 (지역별 표준 단가)
5. 대기 시간 비용 (시간당 표준 요율)
6. 특수 작업 비용 (냉장/냉동, 위험물 등)
7. 날씨/교통 할증 (실시간 반영)

💡 신규 추가:

영수증 자동 정산 시스템

  • 톨게이트 영수증 OCR 자동 인식
  • 주유 영수증 사진 업로드 → 자동 정산
  • 기타 부대비용 증빙자료 통합 관리
  • 세금계산서 자동 발행

2.5 에스크로 결제 시스템

결제 안전장치:

[결제 프로세스]
1. 화주: 계약 체결 시 운임 전액 에스크로 예치
2. 차주: 운송 시작 알림
3. 화주: 상차 완료 확인
4. 차주: 하차 완료 + 사진/서명 업로드
5. 화주: 하차 확인 (24시간 이내)
6. 시스템: 자동 정산 (차주 계좌로 즉시 입금)

💡 신규 추가:

  • 분쟁 해결 시스템: 72시간 내 중재 완료
  • 긴급 출금: S등급 차주는 운송 완료 즉시 90% 선출금 가능
  • 정기 결제: 월 단위 정산 옵션 제공

2.6 커뮤니티 및 지원 기능

💡 완전 신규 추가:

1) 차주 커뮤니티

  • 구간별 정보 공유 (휴게소, 주차장, 맛집)
  • 긴급 상황 도움 요청 (고장, 사고 등)
  • 화물 연계 배차 (빈 차 최소화)

2) 화주 지원 센터

  • 포장 가이드 제공
  • 적정 운임 계산기
  • 화물 보험 연계

3) 교육 프로그램

  • 신규 차주 온보딩 교육
  • 안전 운행 교육 (수료 시 인센티브)
  • 화주 매너 교육

2.7 수익 없는 사용료 폐지

핵심 개선:

  • 월 사용료 완전 폐지 (현재 27,500원 → 0원)
  • 성과 기반 수수료만 부과 (거래 성사 시에만)
  • 프리미엄 옵션 (선택 사항)
    • 상단 고정 노출: 건당 5,000원
    • 긴급 배차 우선권: 건당 10,000원
    • 광고 제거: 월 9,900원

📊 3. 추가 필요 기능 (완전 신규)

3.1 데이터 기반 인사이트

[차주용 대시보드]
- 월별 매출 분석
- 구간별 수익성 분석
- 유류비 절감 팁
- 세금 신고 자료 자동 생성

[화주용 대시보드]
- 물류비 절감 리포트
- 정기 배송 최적화 제안
- 배송 이력 통계

3.2 협력 네트워크

  • 정비소 제휴: 할인 혜택
  • 주유소 제휴: 적립 포인트
  • 보험사 제휴: 단체 할인
  • 금융사 제휴: 차주 전용 대출 상품

3.3 AI 예측 시스템

  • 수요 예측 (명절, 시즌 등)
  • 운임 변동 예측
  • 최적 운행 루트 추천
  • 사고 위험 구간 알림

3.4 법적 보호 시스템

  • 전자 계약서 자동 생성 (법적 효력)
  • 분쟁 조정 위원회 운영
  • 법률 자문 서비스 제공
  • 노동부 신고 지원

💰 4. 비즈니스 모델

수익 구조

1. 거래 수수료: 5% (화주 3% + 차주 2%)
2. 프리미엄 서비스
3. 광고 수익 (제휴사 노출)
4. 데이터 분석 서비스 (B2B)
5. 금융 서비스 수수료

예상 수익 (월 거래액 100억 기준)

거래 수수료: 5억원
프리미엄: 5천만원
광고: 2천만원
기타: 3천만원
━━━━━━━━━━━━
총 매출: 6억원/월

🎯 5. 기대 효과

차주 입장:

  • 수수료 40% → 2%로 대폭 감소
  • 월 사용료 27,500원 → 0원
  • 불량 화주 사전 차단
  • 부대비용 자동 정산

화주 입장:

  • 투명한 가격 형성
  • 우수 차주 선택 가능
  • 운임 미지급 분쟁 제로화
  • 물류 최적화 컨설팅

플랫폼:

  • 건전한 생태계 구축
  • 데이터 축적 → 금융/보험 확장
  • 사회적 가치 창출

📱 6. MVP 개발 우선순위

Phase 1 (3개월):

  1. 역경매 시스템
  2. 투명 수수료
  3. 에스크로 결제
  4. 별점 시스템

Phase 2 (6개월):

  1. AI 비용 계산기
  2. 위치 추적
  3. 커뮤니티
  4. 보증금 시스템

Phase 3 (9개월):

  1. 금융 서비스
  2. 데이터 분석
  3. API 개방
  4. 제휴 네트워크

320x100

트럭커-프리(Trucker-Free) 데이터베이스 설계 문서

📋 목차

  1. 개요
  2. 주요 테이블 구조
  3. 테이블 관계도
  4. 주요 기능별 테이블 매핑
  5. 인덱스 전략
  6. 데이터 무결성
  7. 확장성 고려사항

🎯 개요

기술 스택

  • 데이터베이스: MySQL 8.0+
  • ORM: Prisma
  • 백엔드: NestJS
  • 프론트엔드: Nuxt.js

데이터베이스 구조

30개 테이블로 구성되며, 크게 10개 영역으로 분류됩니다:

  1. 사용자 관리 (4개 테이블)
  2. 주문 관리 (4개 테이블)
  3. 결제 및 정산 (4개 테이블)
  4. 평가 시스템 (1개 테이블)
  5. 블랙리스트 (1개 테이블)
  6. 채팅 및 알림 (3개 테이블)
  7. 시스템 관리 (6개 테이블)
  8. 통계 분석 (1개 테이블)
  9. 인증 관리 (2개 테이블)
  10. 프리미엄 서비스 (1개 테이블)

📊 주요 테이블 구조

1. 사용자 관리 영역

1.1 users (사용자 기본 정보)

핵심 역할: 플랫폼의 모든 사용자(관리자, 화주, 차주) 통합 관리
주요 필드:
- id: 사용자 고유 ID
- user_type: admin/shipper/driver
- email, phone: 로그인 인증
- status: 계정 상태 관리

1.2 shippers (화주 상세 정보)

핵심 역할: 화주의 사업자 정보 및 신용 관리
주요 필드:
- business_number: 사업자등록번호 (고유키)
- credit_rating: S/A/B/C/D 등급
- deposit_amount: 보증금
- average_rating: 평균 평점
- total_spent: 누적 지출

1.3 drivers (차주 상세 정보)

핵심 역할: 차주의 면허 정보 및 신용 관리
주요 필드:
- license_number: 운전면허번호 (고유키)
- carrier_license_number: 화물운송자격증
- current_latitude/longitude: 실시간 위치
- is_available: 배차 가능 여부
- no_show_count: 노쇼 횟수

1.4 vehicles (차량 정보)

핵심 역할: 차주가 등록한 차량 관리
주요 필드:
- vehicle_number: 차량번호 (고유키)
- vehicle_type: 1톤/2.5톤/5톤/11톤/18톤/25톤
- cargo_type: 일반/냉장/냉동/특수
- insurance_expiry_date: 보험 만료일

2. 주문 관리 영역

2.1 orders (화물 주문)

핵심 역할: 플랫폼의 핵심 비즈니스 로직
주요 필드:
- order_number: 주문번호 (고유키)
- status: draft → bidding → assigned → in_transit → delivered → completed
- bidding_type: 역경매/경매/즉시매칭
- pickup_*: 상차지 정보 (주소, 좌표, 담당자, 시간)
- delivery_*: 하차지 정보
- final_price: 최종 확정 금액
- toll_fee, fuel_cost, loading_fee: 부대비용
- shipper_fee, driver_fee: 플랫폼 수수료

2.2 bids (입찰 내역)

핵심 역할: 차주의 입찰 및 낙찰 관리
주요 필드:
- bid_amount: 입찰 금액
- status: pending/accepted/rejected/withdrawn
- estimated_pickup_time: 예상 상차 시간

2.3 order_status_histories (주문 상태 이력)

핵심 역할: 주문의 모든 상태 변경 추적
주요 필드:
- status: 변경된 상태
- changed_by_user_id: 변경 주체
- latitude/longitude: 변경 시점의 위치

2.4 order_attachments (주문 첨부파일)

핵심 역할: 화물 사진, 상하차 증빙 자료 관리
주요 필드:
- type: 화물사진/상차증빙/하차증빙/영수증
- file_url: 파일 저장 경로

3. 결제 및 정산 영역

3.1 escrow_accounts (에스크로 계좌)

핵심 역할: 화주의 운임 예치 및 안전 거래
주요 필드:
- total_amount: 예치 금액
- status: pending → deposited → released/refunded
- deposited_at, released_at: 입출금 시간

3.2 payments (결제 내역)

핵심 역할: 모든 결제 거래 기록
주요 필드:
- payment_type: 주문/보증금/프리미엄/벌금
- payment_method: 카드/계좌이체/간편결제
- pg_transaction_id: PG사 거래번호

3.3 settlements (정산 내역)

핵심 역할: 차주에게 정산할 금액 관리
주요 필드:
- driver_earning: 차주 

1. truckersfree_db_schema.sql

  • MySQL 순수 SQL 스키마 (전체 2,000+ 줄)
  • 즉시 실행 가능한 CREATE TABLE 문
  • 초기 데이터, 트리거, 뷰(View) 포함

2. schema.prisma

  • NestJS + Prisma용 스키마 파일
  • TypeScript 타입 안전성 보장
  • 자동 마이그레이션 지원

3. DATABASE_DESIGN.md

  • 상세 설계 문서
  • ERD 관계도
  • 쿼리 예시 및 개발 가이드

🗂️ 데이터베이스 구조 요약

30개 테이블 / 10개 영역

영역테이블 수주요 테이블
👥 사용자 관리 4 users, shippers, drivers, vehicles
📦 주문 관리 4 orders, bids, order_status_histories, order_attachments
💰 결제/정산 4 escrow_accounts, payments, settlements, deposits
⭐ 평가 1 reviews
🚫 제재 1 blacklists
💬 채팅/알림 3 chat_rooms, chat_messages, notifications
⚙️ 시스템 관리 6 system_settings, pricing_rules, faqs, notices, inquiries, admin_logs
📊 통계 1 daily_statistics
🔐 인증 2 refresh_tokens, sms_verifications
💎 프리미엄 1 premium_subscriptions

🎯 핵심 기능 지원

투명 수수료: 화주 3%, 차주 2% 고정
역경매 시스템: bids 테이블로 입찰 관리
에스크로: 안전한 결제 보장
신용 등급: S/A/B/C/D 5단계
블랙리스트: 자동 제재 시스템
실시간 위치: GPS 추적 (drivers 테이블)
자동 비용 계산: pricing_rules 참조
3가지 화면: 관리자/화주/차주 완벽 분리


🚀 사용 방법

1단계: MySQL 데이터베이스 생성

 
 
bash
mysql -u root -p
CREATE DATABASE truckersfree CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE truckersfree;
SOURCE truckersfree_db_schema.sql;

2단계: NestJS 프로젝트 설정

 
 
bash
# 프로젝트 루트에 schema.prisma 복사
cp schema.prisma ./prisma/schema.prisma

# .env 파일 설정
DATABASE_URL="mysql://root:password@localhost:3306/truckersfree"

# Prisma 마이그레이션
npx prisma migrate dev --name init
npx prisma generate

3단계: Nuxt.js 프론트엔드 개발

 
 
bash
# API 엔드포인트 연결
- 관리자: /admin/*
- 화주: /shipper/*
- 차주: /driver/*

📱 화면 구성 (3개)

🔧 관리자 화면

  • 전체 주문 현황 모니터링
  • 화주/차주 인증 승인
  • 블랙리스트 관리
  • 통계 대시보드
  • 문의/신고 처리

📦 화주 화면

  • 화물 주문 등록
  • 입찰 현황 확인
  • 차주 선택 (평점 기반)
  • 실시간 배송 추적
  • 결제 및 리뷰 작성

🚛 차주 화면 (모바일 최적화)

  • 배차 가능 주문 조회
  • 입찰 참여
  • 운송 진행 상태 업데이트
  • GPS 위치 전송
  • 정산 내역 확인

💡 다음 단계 추천

  1. 백엔드 API 개발 (NestJS)
    • 인증 모듈 (JWT + SMS)
    • 주문 관리 모듈
    • 입찰 시스템 모듈
    • 결제 연동 (PG사)
  2. 프론트엔드 개발 (Nuxt.js)
    • 관리자 대시보드
    • 화주 웹 화면
    • 차주 모바일 앱
  3. 추가 기능 개발
    • 실시간 채팅 (Socket.io)
    • 푸시 알림 (FCM)
    • 지도 API 연동 (카카오맵/네이버맵)
    • 자동 요금 계산 API
320x100
-- ================================================
-- 트럭커-프리(Trucker-Free) 플랫폼 데이터베이스 스키마
-- MySQL 8.0+ / NestJS(Prisma) / Nuxt.js
-- ================================================

-- ================================================
-- 1. 사용자 관리 (Users Management)
-- ================================================

-- 1.1 사용자 기본 테이블
CREATE TABLE users (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_type ENUM('admin', 'shipper', 'driver') NOT NULL COMMENT '사용자 유형: 관리자/화주/차주',
    email VARCHAR(255) NOT NULL UNIQUE,
    phone VARCHAR(20) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    name VARCHAR(100) NOT NULL,
    status ENUM('active', 'suspended', 'blocked', 'withdrawn') DEFAULT 'active' COMMENT '계정 상태',
    email_verified_at TIMESTAMP NULL,
    phone_verified_at TIMESTAMP NULL,
    last_login_at TIMESTAMP NULL,
    login_count INT UNSIGNED DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL COMMENT '소프트 삭제',
    INDEX idx_user_type (user_type),
    INDEX idx_email (email),
    INDEX idx_phone (phone),
    INDEX idx_status (status),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='사용자 기본 정보';

-- 1.2 화주 상세 정보
CREATE TABLE shippers (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL UNIQUE,
    company_name VARCHAR(255) NOT NULL COMMENT '회사명',
    business_number VARCHAR(20) NOT NULL UNIQUE COMMENT '사업자등록번호',
    business_type ENUM('individual', 'corporation') NOT NULL COMMENT '개인/법인',
    representative_name VARCHAR(100) NOT NULL COMMENT '대표자명',
    business_address TEXT NOT NULL COMMENT '사업장 주소',
    business_address_detail VARCHAR(255) COMMENT '상세주소',
    business_certificate_url VARCHAR(500) COMMENT '사업자등록증 URL',
    deposit_amount DECIMAL(15, 2) DEFAULT 0.00 COMMENT '보증금',
    credit_rating ENUM('S', 'A', 'B', 'C', 'D') DEFAULT 'C' COMMENT '신용등급',
    total_orders INT UNSIGNED DEFAULT 0 COMMENT '총 주문 건수',
    completed_orders INT UNSIGNED DEFAULT 0 COMMENT '완료 주문 건수',
    cancelled_orders INT UNSIGNED DEFAULT 0 COMMENT '취소 주문 건수',
    average_rating DECIMAL(3, 2) DEFAULT 0.00 COMMENT '평균 평점',
    total_spent DECIMAL(15, 2) DEFAULT 0.00 COMMENT '총 지출액',
    verified_at TIMESTAMP NULL COMMENT '인증 완료 시간',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_business_number (business_number),
    INDEX idx_credit_rating (credit_rating),
    INDEX idx_verified_at (verified_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='화주 상세 정보';

-- 1.3 차주 상세 정보
CREATE TABLE drivers (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL UNIQUE,
    license_number VARCHAR(50) NOT NULL UNIQUE COMMENT '운전면허번호',
    license_type VARCHAR(20) NOT NULL COMMENT '면허 종류 (1종대형 등)',
    license_expiry_date DATE NOT NULL COMMENT '면허 만료일',
    license_image_url VARCHAR(500) COMMENT '면허증 이미지',
    carrier_license_number VARCHAR(50) COMMENT '화물운송자격증번호',
    carrier_license_image_url VARCHAR(500) COMMENT '화물운송자격증 이미지',
    bank_name VARCHAR(50) COMMENT '은행명',
    bank_account VARCHAR(50) COMMENT '계좌번호',
    account_holder VARCHAR(100) COMMENT '예금주',
    deposit_amount DECIMAL(15, 2) DEFAULT 0.00 COMMENT '보증금',
    credit_rating ENUM('S', 'A', 'B', 'C', 'D') DEFAULT 'C' COMMENT '신용등급',
    total_deliveries INT UNSIGNED DEFAULT 0 COMMENT '총 배송 건수',
    completed_deliveries INT UNSIGNED DEFAULT 0 COMMENT '완료 배송 건수',
    cancelled_deliveries INT UNSIGNED DEFAULT 0 COMMENT '취소 배송 건수',
    no_show_count INT UNSIGNED DEFAULT 0 COMMENT '노쇼 횟수',
    average_rating DECIMAL(3, 2) DEFAULT 0.00 COMMENT '평균 평점',
    total_earned DECIMAL(15, 2) DEFAULT 0.00 COMMENT '총 수입액',
    current_latitude DECIMAL(10, 8) NULL COMMENT '현재 위도',
    current_longitude DECIMAL(11, 8) NULL COMMENT '현재 경도',
    last_location_updated_at TIMESTAMP NULL COMMENT '위치 마지막 업데이트',
    is_available BOOLEAN DEFAULT TRUE COMMENT '배차 가능 여부',
    verified_at TIMESTAMP NULL COMMENT '인증 완료 시간',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_license_number (license_number),
    INDEX idx_credit_rating (credit_rating),
    INDEX idx_is_available (is_available),
    INDEX idx_location (current_latitude, current_longitude),
    INDEX idx_verified_at (verified_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='차주 상세 정보';

-- 1.4 차량 정보
CREATE TABLE vehicles (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    driver_id BIGINT UNSIGNED NOT NULL,
    vehicle_number VARCHAR(20) NOT NULL UNIQUE COMMENT '차량번호',
    vehicle_type ENUM('truck_1t', 'truck_2.5t', 'truck_5t', 'truck_11t', 'truck_18t', 'truck_25t', 'special') NOT NULL COMMENT '차량 종류',
    vehicle_model VARCHAR(100) COMMENT '차량 모델명',
    manufacture_year YEAR COMMENT '제조년도',
    cargo_type ENUM('standard', 'refrigerated', 'frozen', 'tank', 'special') DEFAULT 'standard' COMMENT '화물 종류',
    max_load_weight DECIMAL(10, 2) NOT NULL COMMENT '최대 적재량(톤)',
    length DECIMAL(5, 2) COMMENT '길이(m)',
    width DECIMAL(5, 2) COMMENT '너비(m)',
    height DECIMAL(5, 2) COMMENT '높이(m)',
    vehicle_registration_url VARCHAR(500) COMMENT '차량등록증 URL',
    insurance_company VARCHAR(100) COMMENT '보험사',
    insurance_number VARCHAR(100) COMMENT '보험증권번호',
    insurance_expiry_date DATE COMMENT '보험 만료일',
    insurance_image_url VARCHAR(500) COMMENT '보험증권 이미지',
    is_active BOOLEAN DEFAULT TRUE COMMENT '사용 여부',
    verified_at TIMESTAMP NULL COMMENT '인증 완료 시간',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (driver_id) REFERENCES drivers(id) ON DELETE CASCADE,
    INDEX idx_driver_id (driver_id),
    INDEX idx_vehicle_number (vehicle_number),
    INDEX idx_vehicle_type (vehicle_type),
    INDEX idx_is_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='차량 정보';

-- ================================================
-- 2. 화물 및 주문 관리 (Orders Management)
-- ================================================

-- 2.1 화물 주문
CREATE TABLE orders (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_number VARCHAR(50) NOT NULL UNIQUE COMMENT '주문번호',
    shipper_id BIGINT UNSIGNED NOT NULL,
    driver_id BIGINT UNSIGNED NULL COMMENT '배정된 차주',
    vehicle_id BIGINT UNSIGNED NULL COMMENT '배정된 차량',
    
    -- 배송 정보
    cargo_type ENUM('general', 'refrigerated', 'frozen', 'fragile', 'dangerous', 'oversized') NOT NULL COMMENT '화물 종류',
    cargo_weight DECIMAL(10, 2) NOT NULL COMMENT '화물 중량(톤)',
    cargo_volume DECIMAL(10, 2) COMMENT '화물 부피(m³)',
    cargo_description TEXT COMMENT '화물 설명',
    
    -- 출발지 정보
    pickup_address TEXT NOT NULL COMMENT '상차지 주소',
    pickup_address_detail VARCHAR(255) COMMENT '상차지 상세주소',
    pickup_latitude DECIMAL(10, 8) NOT NULL COMMENT '상차지 위도',
    pickup_longitude DECIMAL(11, 8) NOT NULL COMMENT '상차지 경도',
    pickup_contact_name VARCHAR(100) NOT NULL COMMENT '상차지 담당자',
    pickup_contact_phone VARCHAR(20) NOT NULL COMMENT '상차지 연락처',
    pickup_date DATE NOT NULL COMMENT '상차 희망일',
    pickup_time_start TIME COMMENT '상차 시작 시간',
    pickup_time_end TIME COMMENT '상차 종료 시간',
    pickup_completed_at TIMESTAMP NULL COMMENT '상차 완료 시간',
    
    -- 도착지 정보
    delivery_address TEXT NOT NULL COMMENT '하차지 주소',
    delivery_address_detail VARCHAR(255) COMMENT '하차지 상세주소',
    delivery_latitude DECIMAL(10, 8) NOT NULL COMMENT '하차지 위도',
    delivery_longitude DECIMAL(11, 8) NOT NULL COMMENT '하차지 경도',
    delivery_contact_name VARCHAR(100) NOT NULL COMMENT '하차지 담당자',
    delivery_contact_phone VARCHAR(20) NOT NULL COMMENT '하차지 연락처',
    delivery_date DATE NOT NULL COMMENT '하차 희망일',
    delivery_time_start TIME COMMENT '하차 시작 시간',
    delivery_time_end TIME COMMENT '하차 종료 시간',
    delivery_completed_at TIMESTAMP NULL COMMENT '하차 완료 시간',
    
    -- 주문 상태
    status ENUM('draft', 'bidding', 'assigned', 'pickup_pending', 'in_transit', 'delivered', 'completed', 'cancelled') DEFAULT 'draft' COMMENT '주문 상태',
    
    -- 입찰 정보
    bidding_type ENUM('reverse_auction', 'standard_auction', 'instant_match') NOT NULL COMMENT '입찰 방식',
    bidding_start_at TIMESTAMP NULL COMMENT '입찰 시작 시간',
    bidding_end_at TIMESTAMP NULL COMMENT '입찰 종료 시간',
    
    -- 금액 정보 (모두 부가세 포함)
    base_price DECIMAL(15, 2) NULL COMMENT '화주 제시 금액',
    final_price DECIMAL(15, 2) NULL COMMENT '최종 확정 금액',
    toll_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT '통행료',
    fuel_cost DECIMAL(10, 2) DEFAULT 0.00 COMMENT '유류비',
    loading_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT '상차 인건비',
    unloading_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT '하차 인건비',
    waiting_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT '대기 시간 비용',
    additional_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT '추가 비용',
    total_amount DECIMAL(15, 2) DEFAULT 0.00 COMMENT '총 금액',
    
    -- 수수료 (플랫폼)
    shipper_fee_rate DECIMAL(5, 2) DEFAULT 3.00 COMMENT '화주 수수료율(%)',
    driver_fee_rate DECIMAL(5, 2) DEFAULT 2.00 COMMENT '차주 수수료율(%)',
    shipper_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT '화주 수수료',
    driver_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT '차주 수수료',
    
    -- 옵션
    requires_loading_help BOOLEAN DEFAULT FALSE COMMENT '상차 도움 필요',
    requires_unloading_help BOOLEAN DEFAULT FALSE COMMENT '하차 도움 필요',
    requires_special_equipment BOOLEAN DEFAULT FALSE COMMENT '특수 장비 필요',
    special_requirements TEXT COMMENT '특수 요구사항',
    
    -- 거리 및 시간
    estimated_distance DECIMAL(10, 2) COMMENT '예상 거리(km)',
    estimated_duration INT COMMENT '예상 소요 시간(분)',
    actual_distance DECIMAL(10, 2) COMMENT '실제 거리(km)',
    actual_duration INT COMMENT '실제 소요 시간(분)',
    
    -- 메타 정보
    cancelled_reason TEXT COMMENT '취소 사유',
    cancelled_by ENUM('shipper', 'driver', 'admin', 'system') COMMENT '취소 주체',
    cancelled_at TIMESTAMP NULL COMMENT '취소 시간',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (shipper_id) REFERENCES shippers(id),
    FOREIGN KEY (driver_id) REFERENCES drivers(id),
    FOREIGN KEY (vehicle_id) REFERENCES vehicles(id),
    
    INDEX idx_order_number (order_number),
    INDEX idx_shipper_id (shipper_id),
    INDEX idx_driver_id (driver_id),
    INDEX idx_status (status),
    INDEX idx_pickup_date (pickup_date),
    INDEX idx_delivery_date (delivery_date),
    INDEX idx_bidding_end_at (bidding_end_at),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='화물 주문';

-- 2.2 입찰 내역
CREATE TABLE bids (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_id BIGINT UNSIGNED NOT NULL,
    driver_id BIGINT UNSIGNED NOT NULL,
    vehicle_id BIGINT UNSIGNED NOT NULL,
    
    bid_amount DECIMAL(15, 2) NOT NULL COMMENT '입찰 금액',
    estimated_pickup_time TIMESTAMP COMMENT '예상 상차 시간',
    message TEXT COMMENT '차주 메시지',
    
    status ENUM('pending', 'accepted', 'rejected', 'withdrawn', 'expired') DEFAULT 'pending' COMMENT '입찰 상태',
    
    accepted_at TIMESTAMP NULL COMMENT '낙찰 시간',
    rejected_at TIMESTAMP NULL COMMENT '거절 시간',
    withdrawn_at TIMESTAMP NULL COMMENT '철회 시간',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (driver_id) REFERENCES drivers(id),
    FOREIGN KEY (vehicle_id) REFERENCES vehicles(id),
    
    INDEX idx_order_id (order_id),
    INDEX idx_driver_id (driver_id),
    INDEX idx_status (status),
    INDEX idx_bid_amount (bid_amount),
    INDEX idx_created_at (created_at),
    
    UNIQUE KEY unique_driver_order (order_id, driver_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='입찰 내역';

-- 2.3 주문 상태 이력
CREATE TABLE order_status_histories (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_id BIGINT UNSIGNED NOT NULL,
    status ENUM('draft', 'bidding', 'assigned', 'pickup_pending', 'in_transit', 'delivered', 'completed', 'cancelled') NOT NULL,
    changed_by_user_id BIGINT UNSIGNED NULL,
    note TEXT COMMENT '메모',
    latitude DECIMAL(10, 8) NULL COMMENT '위치 위도',
    longitude DECIMAL(11, 8) NULL COMMENT '위치 경도',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (changed_by_user_id) REFERENCES users(id),
    
    INDEX idx_order_id (order_id),
    INDEX idx_status (status),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='주문 상태 변경 이력';

-- 2.4 주문 첨부파일
CREATE TABLE order_attachments (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_id BIGINT UNSIGNED NOT NULL,
    type ENUM('cargo_image', 'pickup_proof', 'delivery_proof', 'receipt', 'other') NOT NULL COMMENT '파일 종류',
    file_url VARCHAR(500) NOT NULL COMMENT '파일 URL',
    file_name VARCHAR(255) NOT NULL COMMENT '파일명',
    file_size INT UNSIGNED COMMENT '파일 크기(bytes)',
    mime_type VARCHAR(100) COMMENT 'MIME 타입',
    uploaded_by_user_id BIGINT UNSIGNED NOT NULL,
    description TEXT COMMENT '설명',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (uploaded_by_user_id) REFERENCES users(id),
    
    INDEX idx_order_id (order_id),
    INDEX idx_type (type),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='주문 첨부파일';

-- ================================================
-- 3. 결제 및 정산 (Payments)
-- ================================================

-- 3.1 에스크로 계좌
CREATE TABLE escrow_accounts (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_id BIGINT UNSIGNED NOT NULL UNIQUE,
    shipper_id BIGINT UNSIGNED NOT NULL,
    driver_id BIGINT UNSIGNED NULL,
    
    total_amount DECIMAL(15, 2) NOT NULL COMMENT '예치 금액',
    status ENUM('pending', 'deposited', 'released', 'refunded', 'cancelled') DEFAULT 'pending' COMMENT '상태',
    
    deposited_at TIMESTAMP NULL COMMENT '입금 완료 시간',
    released_at TIMESTAMP NULL COMMENT '출금 완료 시간',
    refunded_at TIMESTAMP NULL COMMENT '환불 완료 시간',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (shipper_id) REFERENCES shippers(id),
    FOREIGN KEY (driver_id) REFERENCES drivers(id),
    
    INDEX idx_order_id (order_id),
    INDEX idx_status (status),
    INDEX idx_shipper_id (shipper_id),
    INDEX idx_driver_id (driver_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='에스크로 계좌';

-- 3.2 결제 내역
CREATE TABLE payments (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    payment_number VARCHAR(50) NOT NULL UNIQUE COMMENT '결제번호',
    order_id BIGINT UNSIGNED NOT NULL,
    payer_user_id BIGINT UNSIGNED NOT NULL COMMENT '결제자',
    
    payment_type ENUM('order', 'deposit', 'premium', 'penalty') NOT NULL COMMENT '결제 유형',
    payment_method ENUM('card', 'transfer', 'virtual_account', 'kakao_pay', 'naver_pay', 'toss') NOT NULL COMMENT '결제 수단',
    
    amount DECIMAL(15, 2) NOT NULL COMMENT '결제 금액',
    vat DECIMAL(15, 2) DEFAULT 0.00 COMMENT '부가세',
    total_amount DECIMAL(15, 2) NOT NULL COMMENT '총 금액',
    
    status ENUM('pending', 'completed', 'failed', 'cancelled', 'refunded') DEFAULT 'pending' COMMENT '결제 상태',
    
    pg_name VARCHAR(50) COMMENT 'PG사',
    pg_transaction_id VARCHAR(100) COMMENT 'PG 거래번호',
    
    approved_at TIMESTAMP NULL COMMENT '승인 시간',
    cancelled_at TIMESTAMP NULL COMMENT '취소 시간',
    refunded_at TIMESTAMP NULL COMMENT '환불 시간',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (order_id) REFERENCES orders(id),
    FOREIGN KEY (payer_user_id) REFERENCES users(id),
    
    INDEX idx_payment_number (payment_number),
    INDEX idx_order_id (order_id),
    INDEX idx_payer_user_id (payer_user_id),
    INDEX idx_status (status),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='결제 내역';

-- 3.3 정산 내역
CREATE TABLE settlements (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    settlement_number VARCHAR(50) NOT NULL UNIQUE COMMENT '정산번호',
    order_id BIGINT UNSIGNED NOT NULL,
    driver_id BIGINT UNSIGNED NOT NULL,
    
    -- 금액 상세
    order_amount DECIMAL(15, 2) NOT NULL COMMENT '주문 금액',
    platform_fee DECIMAL(10, 2) NOT NULL COMMENT '플랫폼 수수료',
    driver_earning DECIMAL(15, 2) NOT NULL COMMENT '차주 수령액',
    
    -- 부대비용
    toll_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT '통행료',
    fuel_cost DECIMAL(10, 2) DEFAULT 0.00 COMMENT '유류비',
    additional_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT '추가 비용',
    
    status ENUM('pending', 'approved', 'completed', 'rejected') DEFAULT 'pending' COMMENT '정산 상태',
    
    settlement_date DATE COMMENT '정산 예정일',
    settled_at TIMESTAMP NULL COMMENT '정산 완료 시간',
    
    bank_name VARCHAR(50) COMMENT '입금 은행',
    bank_account VARCHAR(50) COMMENT '입금 계좌',
    account_holder VARCHAR(100) COMMENT '예금주',
    
    tax_invoice_url VARCHAR(500) COMMENT '세금계산서 URL',
    
    note TEXT COMMENT '메모',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (order_id) REFERENCES orders(id),
    FOREIGN KEY (driver_id) REFERENCES drivers(id),
    
    INDEX idx_settlement_number (settlement_number),
    INDEX idx_order_id (order_id),
    INDEX idx_driver_id (driver_id),
    INDEX idx_status (status),
    INDEX idx_settlement_date (settlement_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='정산 내역';

-- 3.4 보증금 관리
CREATE TABLE deposits (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    user_type ENUM('shipper', 'driver') NOT NULL,
    
    transaction_type ENUM('deposit', 'withdrawal', 'deduction', 'refund') NOT NULL COMMENT '거래 유형',
    amount DECIMAL(15, 2) NOT NULL COMMENT '금액',
    balance_after DECIMAL(15, 2) NOT NULL COMMENT '거래 후 잔액',
    
    reason VARCHAR(255) COMMENT '사유',
    related_order_id BIGINT UNSIGNED NULL COMMENT '연관 주문',
    
    status ENUM('pending', 'completed', 'failed') DEFAULT 'pending',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (related_order_id) REFERENCES orders(id),
    
    INDEX idx_user_id (user_id),
    INDEX idx_user_type (user_type),
    INDEX idx_transaction_type (transaction_type),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='보증금 관리';

-- ================================================
-- 4. 평가 및 리뷰 (Reviews & Ratings)
-- ================================================

-- 4.1 평가
CREATE TABLE reviews (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_id BIGINT UNSIGNED NOT NULL,
    reviewer_user_id BIGINT UNSIGNED NOT NULL COMMENT '평가자',
    reviewee_user_id BIGINT UNSIGNED NOT NULL COMMENT '피평가자',
    reviewer_type ENUM('shipper', 'driver') NOT NULL COMMENT '평가자 유형',
    
    -- 평점 (1-5점)
    overall_rating DECIMAL(2, 1) NOT NULL COMMENT '종합 평점',
    punctuality_rating DECIMAL(2, 1) COMMENT '정시성 평점',
    kindness_rating DECIMAL(2, 1) COMMENT '친절도 평점',
    safety_rating DECIMAL(2, 1) COMMENT '안전성 평점',
    communication_rating DECIMAL(2, 1) COMMENT '소통 평점',
    
    -- 리뷰 내용
    comment TEXT COMMENT '리뷰 내용',
    
    -- 추천 태그
    tags JSON COMMENT '추천 태그 (배열)',
    
    is_visible BOOLEAN DEFAULT TRUE COMMENT '공개 여부',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (reviewer_user_id) REFERENCES users(id),
    FOREIGN KEY (reviewee_user_id) REFERENCES users(id),
    
    INDEX idx_order_id (order_id),
    INDEX idx_reviewer_user_id (reviewer_user_id),
    INDEX idx_reviewee_user_id (reviewee_user_id),
    INDEX idx_overall_rating (overall_rating),
    INDEX idx_created_at (created_at),
    
    UNIQUE KEY unique_review (order_id, reviewer_user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='평가 및 리뷰';

-- ================================================
-- 5. 블랙리스트 및 제재 (Blacklist)
-- ================================================

-- 5.1 블랙리스트
CREATE TABLE blacklists (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    user_type ENUM('shipper', 'driver') NOT NULL,
    
    violation_type ENUM('no_show', 'payment_delay', 'fraud', 'abuse', 'other') NOT NULL COMMENT '위반 유형',
    severity ENUM('warning', 'suspension', 'permanent_ban') NOT NULL COMMENT '제재 수준',
    
    reason TEXT NOT NULL COMMENT '제재 사유',
    related_order_id BIGINT UNSIGNED NULL COMMENT '관련 주문',
    
    reported_by_user_id BIGINT UNSIGNED NULL COMMENT '신고자',
    reviewed_by_admin_id BIGINT UNSIGNED NULL COMMENT '처리 관리자',
    
    status ENUM('active', 'resolved', 'appealed') DEFAULT 'active' COMMENT '상태',
    
    suspension_start_date DATE COMMENT '정지 시작일',
    suspension_end_date DATE COMMENT '정지 종료일',
    
    appeal_reason TEXT COMMENT '이의제기 사유',
    appeal_result TEXT COMMENT '이의제기 결과',
    appealed_at TIMESTAMP NULL COMMENT '이의제기 시간',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (related_order_id) REFERENCES orders(id),
    FOREIGN KEY (reported_by_user_id) REFERENCES users(id),
    FOREIGN KEY (reviewed_by_admin_id) REFERENCES users(id),
    
    INDEX idx_user_id (user_id),
    INDEX idx_violation_type (violation_type),
    INDEX idx_severity (severity),
    INDEX idx_status (status),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='블랙리스트';

-- ================================================
-- 6. 채팅 및 알림 (Chat & Notifications)
-- ================================================

-- 6.1 채팅방
CREATE TABLE chat_rooms (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_id BIGINT UNSIGNED NOT NULL,
    shipper_user_id BIGINT UNSIGNED NOT NULL,
    driver_user_id BIGINT UNSIGNED NOT NULL,
    
    is_active BOOLEAN DEFAULT TRUE COMMENT '활성 여부',
    last_message_at TIMESTAMP NULL COMMENT '마지막 메시지 시간',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (shipper_user_id) REFERENCES users(id),
    FOREIGN KEY (driver_user_id) REFERENCES users(id),
    
    INDEX idx_order_id (order_id),
    INDEX idx_shipper_user_id (shipper_user_id),
    INDEX idx_driver_user_id (driver_user_id),
    INDEX idx_last_message_at (last_message_at),
    
    UNIQUE KEY unique_chat_room (order_id, shipper_user_id, driver_user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='채팅방';

-- 6.2 채팅 메시지
CREATE TABLE chat_messages (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    chat_room_id BIGINT UNSIGNED NOT NULL,
    sender_user_id BIGINT UNSIGNED NOT NULL,
    
    message_type ENUM('text', 'image', 'file', 'system') DEFAULT 'text' COMMENT '메시지 유형',
    content TEXT COMMENT '메시지 내용',
    file_url VARCHAR(500) COMMENT '파일 URL',
    
    is_read BOOLEAN DEFAULT FALSE COMMENT '읽음 여부',
    read_at TIMESTAMP NULL COMMENT '읽은 시간',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (chat_room_id) REFERENCES chat_rooms(id) ON DELETE CASCADE,
    FOREIGN KEY (sender_user_id) REFERENCES users(id),
    
    INDEX idx_chat_room_id (chat_room_id),
    INDEX idx_sender_user_id (sender_user_id),
    INDEX idx_is_read (is_read),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='채팅 메시지';

-- 6.3 알림
CREATE TABLE notifications (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    
    notification_type ENUM('order', 'bid', 'payment', 'review', 'system', 'marketing') NOT NULL COMMENT '알림 유형',
    title VARCHAR(255) NOT NULL COMMENT '알림 제목',
    content TEXT NOT NULL COMMENT '알림 내용',
    
    related_order_id BIGINT UNSIGNED NULL COMMENT '관련 주문',
    action_url VARCHAR(500) COMMENT '액션 URL',
    
    is_read BOOLEAN DEFAULT FALSE COMMENT '읽음 여부',
    read_at TIMESTAMP NULL COMMENT '읽은 시간',
    
    push_sent BOOLEAN DEFAULT FALSE COMMENT '푸시 발송 여부',
    push_sent_at TIMESTAMP NULL COMMENT '푸시 발송 시간',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (related_order_id) REFERENCES orders(id),
    
    INDEX idx_user_id (user_id),
    INDEX idx_notification_type (notification_type),
    INDEX idx_is_read (is_read),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='알림';

-- ================================================
-- 7. 시스템 관리 (System Management)
-- ================================================

-- 7.1 시스템 설정
CREATE TABLE system_settings (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    setting_key VARCHAR(100) NOT NULL UNIQUE COMMENT '설정 키',
    setting_value TEXT NOT NULL COMMENT '설정 값',
    setting_type ENUM('string', 'number', 'boolean', 'json') DEFAULT 'string' COMMENT '값 타입',
    description TEXT COMMENT '설명',
    is_public BOOLEAN DEFAULT FALSE COMMENT '공개 여부',
    updated_by_admin_id BIGINT UNSIGNED NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (updated_by_admin_id) REFERENCES users(id),
    
    INDEX idx_setting_key (setting_key),
    INDEX idx_is_public (is_public)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='시스템 설정';

-- 7.2 요금표
CREATE TABLE pricing_rules (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    rule_name VARCHAR(100) NOT NULL COMMENT '규칙명',
    vehicle_type ENUM('truck_1t', 'truck_2.5t', 'truck_5t', 'truck_11t', 'truck_18t', 'truck_25t', 'special') NOT NULL,
    cargo_type ENUM('general', 'refrigerated', 'frozen', 'fragile', 'dangerous', 'oversized') NOT NULL,
    
    -- 거리별 요금
    base_distance INT COMMENT '기본 거리(km)',
    base_price DECIMAL(15, 2) COMMENT '기본 요금',
    price_per_km DECIMAL(10, 2) COMMENT 'km당 추가 요금',
    
    -- 시간대별 할증
    night_surcharge_rate DECIMAL(5, 2) DEFAULT 0.00 COMMENT '야간 할증률(%)',
    weekend_surcharge_rate DECIMAL(5, 2) DEFAULT 0.00 COMMENT '주말 할증률(%)',
    holiday_surcharge_rate DECIMAL(5, 2) DEFAULT 0.00 COMMENT '공휴일 할증률(%)',
    
    -- 작업 요금
    loading_fee_per_person DECIMAL(10, 2) COMMENT '상차 인건비(1인)',
    unloading_fee_per_person DECIMAL(10, 2) COMMENT '하차 인건비(1인)',
    waiting_fee_per_hour DECIMAL(10, 2) COMMENT '대기 시간당 비용',
    
    is_active BOOLEAN DEFAULT TRUE COMMENT '사용 여부',
    
    effective_from DATE COMMENT '적용 시작일',
    effective_to DATE COMMENT '적용 종료일',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    INDEX idx_vehicle_type (vehicle_type),
    INDEX idx_cargo_type (cargo_type),
    INDEX idx_is_active (is_active),
    INDEX idx_effective_dates (effective_from, effective_to)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='요금표';

-- 7.3 FAQ
CREATE TABLE faqs (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    category ENUM('general', 'order', 'payment', 'rating', 'account', 'technical') NOT NULL COMMENT '카테고리',
    user_type ENUM('all', 'shipper', 'driver', 'admin') DEFAULT 'all' COMMENT '대상 사용자',
    
    question VARCHAR(500) NOT NULL COMMENT '질문',
    answer TEXT NOT NULL COMMENT '답변',
    
    view_count INT UNSIGNED DEFAULT 0 COMMENT '조회수',
    is_visible BOOLEAN DEFAULT TRUE COMMENT '공개 여부',
    display_order INT DEFAULT 0 COMMENT '표시 순서',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    INDEX idx_category (category),
    INDEX idx_user_type (user_type),
    INDEX idx_is_visible (is_visible),
    INDEX idx_display_order (display_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='FAQ';

-- 7.4 공지사항
CREATE TABLE notices (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    author_admin_id BIGINT UNSIGNED NOT NULL,
    
    notice_type ENUM('general', 'update', 'maintenance', 'event', 'urgent') DEFAULT 'general' COMMENT '공지 유형',
    target_user_type ENUM('all', 'shipper', 'driver') DEFAULT 'all' COMMENT '대상 사용자',
    
    title VARCHAR(255) NOT NULL COMMENT '제목',
    content TEXT NOT NULL COMMENT '내용',
    
    is_pinned BOOLEAN DEFAULT FALSE COMMENT '상단 고정',
    is_visible BOOLEAN DEFAULT TRUE COMMENT '공개 여부',
    
    view_count INT UNSIGNED DEFAULT 0 COMMENT '조회수',
    
    published_at TIMESTAMP NULL COMMENT '게시 시간',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (author_admin_id) REFERENCES users(id),
    
    INDEX idx_notice_type (notice_type),
    INDEX idx_target_user_type (target_user_type),
    INDEX idx_is_pinned (is_pinned),
    INDEX idx_is_visible (is_visible),
    INDEX idx_published_at (published_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='공지사항';

-- 7.5 고객 문의
CREATE TABLE inquiries (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    
    inquiry_type ENUM('general', 'payment', 'technical', 'report', 'suggestion', 'other') NOT NULL COMMENT '문의 유형',
    title VARCHAR(255) NOT NULL COMMENT '제목',
    content TEXT NOT NULL COMMENT '내용',
    
    related_order_id BIGINT UNSIGNED NULL COMMENT '관련 주문',
    
    status ENUM('pending', 'in_progress', 'resolved', 'closed') DEFAULT 'pending' COMMENT '처리 상태',
    
    admin_user_id BIGINT UNSIGNED NULL COMMENT '담당 관리자',
    admin_reply TEXT COMMENT '답변',
    replied_at TIMESTAMP NULL COMMENT '답변 시간',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (related_order_id) REFERENCES orders(id),
    FOREIGN KEY (admin_user_id) REFERENCES users(id),
    
    INDEX idx_user_id (user_id),
    INDEX idx_inquiry_type (inquiry_type),
    INDEX idx_status (status),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='고객 문의';

-- 7.6 관리자 활동 로그
CREATE TABLE admin_logs (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    admin_user_id BIGINT UNSIGNED NOT NULL,
    
    action_type ENUM('create', 'update', 'delete', 'approve', 'reject', 'suspend', 'restore') NOT NULL COMMENT '액션 유형',
    target_table VARCHAR(100) NOT NULL COMMENT '대상 테이블',
    target_id BIGINT UNSIGNED NOT NULL COMMENT '대상 ID',
    
    before_data JSON COMMENT '변경 전 데이터',
    after_data JSON COMMENT '변경 후 데이터',
    
    description TEXT COMMENT '설명',
    ip_address VARCHAR(45) COMMENT 'IP 주소',
    user_agent TEXT COMMENT 'User Agent',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (admin_user_id) REFERENCES users(id),
    
    INDEX idx_admin_user_id (admin_user_id),
    INDEX idx_action_type (action_type),
    INDEX idx_target (target_table, target_id),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='관리자 활동 로그';

-- ================================================
-- 8. 통계 및 분석 (Analytics)
-- ================================================

-- 8.1 일일 통계 (집계 테이블)
CREATE TABLE daily_statistics (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    stat_date DATE NOT NULL COMMENT '통계 날짜',
    
    -- 주문 통계
    total_orders INT UNSIGNED DEFAULT 0 COMMENT '총 주문 수',
    completed_orders INT UNSIGNED DEFAULT 0 COMMENT '완료 주문 수',
    cancelled_orders INT UNSIGNED DEFAULT 0 COMMENT '취소 주문 수',
    
    -- 금액 통계
    total_order_amount DECIMAL(15, 2) DEFAULT 0.00 COMMENT '총 주문 금액',
    total_platform_fee DECIMAL(15, 2) DEFAULT 0.00 COMMENT '총 수수료',
    total_driver_earning DECIMAL(15, 2) DEFAULT 0.00 COMMENT '총 차주 수입',
    
    -- 사용자 통계
    new_shippers INT UNSIGNED DEFAULT 0 COMMENT '신규 화주',
    new_drivers INT UNSIGNED DEFAULT 0 COMMENT '신규 차주',
    active_shippers INT UNSIGNED DEFAULT 0 COMMENT '활성 화주',
    active_drivers INT UNSIGNED DEFAULT 0 COMMENT '활성 차주',
    
    -- 평균 지표
    avg_order_amount DECIMAL(15, 2) DEFAULT 0.00 COMMENT '평균 주문 금액',
    avg_rating DECIMAL(3, 2) DEFAULT 0.00 COMMENT '평균 평점',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    UNIQUE KEY unique_stat_date (stat_date),
    INDEX idx_stat_date (stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='일일 통계';

-- ================================================
-- 9. 인증 및 세션 (Authentication)
-- ================================================

-- 9.1 리프레시 토큰
CREATE TABLE refresh_tokens (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    token VARCHAR(500) NOT NULL UNIQUE,
    expires_at TIMESTAMP NOT NULL,
    device_info TEXT COMMENT '디바이스 정보',
    ip_address VARCHAR(45) COMMENT 'IP 주소',
    is_revoked BOOLEAN DEFAULT FALSE COMMENT '폐기 여부',
    revoked_at TIMESTAMP NULL COMMENT '폐기 시간',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    
    INDEX idx_user_id (user_id),
    INDEX idx_token (token),
    INDEX idx_expires_at (expires_at),
    INDEX idx_is_revoked (is_revoked)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='리프레시 토큰';

-- 9.2 SMS 인증
CREATE TABLE sms_verifications (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    phone VARCHAR(20) NOT NULL,
    verification_code VARCHAR(6) NOT NULL COMMENT '인증 코드',
    purpose ENUM('signup', 'password_reset', 'phone_change', 'login') NOT NULL COMMENT '용도',
    is_verified BOOLEAN DEFAULT FALSE COMMENT '인증 완료 여부',
    verified_at TIMESTAMP NULL COMMENT '인증 완료 시간',
    expires_at TIMESTAMP NOT NULL COMMENT '만료 시간',
    attempt_count INT UNSIGNED DEFAULT 0 COMMENT '시도 횟수',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_phone (phone),
    INDEX idx_verification_code (verification_code),
    INDEX idx_is_verified (is_verified),
    INDEX idx_expires_at (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='SMS 인증';

-- ================================================
-- 10. 프리미엄 서비스
-- ================================================

-- 10.1 프리미엄 구독
CREATE TABLE premium_subscriptions (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    
    subscription_type ENUM('top_exposure', 'priority_dispatch', 'ad_free', 'bundle') NOT NULL COMMENT '구독 유형',
    
    status ENUM('active', 'paused', 'cancelled', 'expired') DEFAULT 'active' COMMENT '구독 상태',
    
    started_at TIMESTAMP NOT NULL COMMENT '시작 시간',
    expires_at TIMESTAMP NOT NULL COMMENT '만료 시간',
    
    monthly_fee DECIMAL(10, 2) NOT NULL COMMENT '월 요금',
    
    auto_renewal BOOLEAN DEFAULT TRUE COMMENT '자동 갱신',
    
    cancelled_at TIMESTAMP NULL COMMENT '취소 시간',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    
    INDEX idx_user_id (user_id),
    INDEX idx_subscription_type (subscription_type),
    INDEX idx_status (status),
    INDEX idx_expires_at (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='프리미엄 구독';

-- ================================================
-- 초기 데이터 삽입
-- ================================================

-- 시스템 설정 초기값
INSERT INTO system_settings (setting_key, setting_value, setting_type, description, is_public) VALUES
('shipper_fee_rate', '3.00', 'number', '화주 수수료율(%)', true),
('driver_fee_rate', '2.00', 'number', '차주 수수료율(%)', true),
('shipper_deposit_amount', '500000', 'number', '화주 초기 보증금(원)', true),
('driver_deposit_amount', '300000', 'number', '차주 초기 보증금(원)', true),
('bidding_duration_hours', '24', 'number', '입찰 기간(시간)', true),
('settlement_delay_days', '3', 'number', '정산 지연 일수', true),
('no_show_warning_threshold', '1', 'number', '노쇼 경고 기준', false),
('no_show_suspension_threshold', '2', 'number', '노쇼 정지 기준', false),
('no_show_ban_threshold', '3', 'number', '노쇼 영구퇴출 기준', false);

-- 관리자 계정 생성 (비밀번호는 'admin1234'를 해시한 값으로 변경 필요)
INSERT INTO users (user_type, email, phone, password_hash, name, status, email_verified_at, phone_verified_at) VALUES
('admin', 'admin@truckersfree.com', '01012345678', '$2b$10$example_hash', '시스템관리자', 'active', NOW(), NOW());

-- ================================================
-- 뷰(View) 생성
-- ================================================

-- 차주 대시보드용 뷰
CREATE VIEW driver_dashboard_stats AS
SELECT 
    d.id as driver_id,
    d.user_id,
    u.name,
    u.email,
    d.credit_rating,
    d.average_rating,
    d.total_deliveries,
    d.completed_deliveries,
    d.total_earned,
    d.is_available,
    COUNT(DISTINCT o.id) as current_orders,
    SUM(CASE WHEN o.status = 'completed' AND o.delivery_completed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) as monthly_deliveries,
    SUM(CASE WHEN o.status = 'completed' AND o.delivery_completed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN o.final_price - o.driver_fee ELSE 0 END) as monthly_earnings
FROM drivers d
JOIN users u ON d.user_id = u.id
LEFT JOIN orders o ON d.id = o.driver_id
GROUP BY d.id, d.user_id, u.name, u.email, d.credit_rating, d.average_rating, d.total_deliveries, d.completed_deliveries, d.total_earned, d.is_available;

-- 화주 대시보드용 뷰
CREATE VIEW shipper_dashboard_stats AS
SELECT 
    s.id as shipper_id,
    s.user_id,
    u.name,
    u.email,
    s.credit_rating,
    s.average_rating,
    s.total_orders,
    s.completed_orders,
    s.total_spent,
    COUNT(DISTINCT o.id) as current_orders,
    SUM(CASE WHEN o.status = 'completed' AND o.delivery_completed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) as monthly_orders,
    SUM(CASE WHEN o.status = 'completed' AND o.delivery_completed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN o.total_amount ELSE 0 END) as monthly_spent
FROM shippers s
JOIN users u ON s.user_id = u.id
LEFT JOIN orders o ON s.id = o.shipper_id
GROUP BY s.id, s.user_id, u.name, u.email, s.credit_rating, s.average_rating, s.total_orders, s.completed_orders, s.total_spent;

-- 진행중인 주문 현황 뷰
CREATE VIEW active_orders_view AS
SELECT 
    o.id,
    o.order_number,
    o.status,
    o.cargo_type,
    o.pickup_address,
    o.delivery_address,
    o.pickup_date,
    o.delivery_date,
    o.total_amount,
    o.created_at,
    u_shipper.name as shipper_name,
    u_shipper.phone as shipper_phone,
    u_driver.name as driver_name,
    u_driver.phone as driver_phone,
    v.vehicle_number,
    v.vehicle_type
FROM orders o
JOIN shippers s ON o.shipper_id = s.id
JOIN users u_shipper ON s.user_id = u_shipper.id
LEFT JOIN drivers d ON o.driver_id = d.id
LEFT JOIN users u_driver ON d.user_id = u_driver.id
LEFT JOIN vehicles v ON o.vehicle_id = v.id
WHERE o.status NOT IN ('completed', 'cancelled');

-- ================================================
-- 트리거(Trigger) 생성
-- ================================================

-- 주문 완료 시 차주/화주 통계 업데이트 트리거
DELIMITER $$

CREATE TRIGGER after_order_completed
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
    IF NEW.status = 'completed' AND OLD.status != 'completed' THEN
        -- 차주 통계 업데이트
        UPDATE drivers 
        SET 
            completed_deliveries = completed_deliveries + 1,
            total_earned = total_earned + (NEW.final_price - NEW.driver_fee)
        WHERE id = NEW.driver_id;
        
        -- 화주 통계 업데이트
        UPDATE shippers 
        SET 
            completed_orders = completed_orders + 1,
            total_spent = total_spent + NEW.total_amount
        WHERE id = NEW.shipper_id;
    END IF;
END$$

DELIMITER ;

-- 평가 등록 시 평균 평점 업데이트 트리거
DELIMITER $$

CREATE TRIGGER after_review_insert
AFTER INSERT ON reviews
FOR EACH ROW
BEGIN
    IF NEW.reviewee_user_id IN (SELECT user_id FROM drivers) THEN
        -- 차주 평균 평점 업데이트
        UPDATE drivers d
        SET average_rating = (
            SELECT AVG(overall_rating)
            FROM reviews
            WHERE reviewee_user_id = d.user_id
        )
        WHERE user_id = NEW.reviewee_user_id;
    ELSEIF NEW.reviewee_user_id IN (SELECT user_id FROM shippers) THEN
        -- 화주 평균 평점 업데이트
        UPDATE shippers s
        SET average_rating = (
            SELECT AVG(overall_rating)
            FROM reviews
            WHERE reviewee_user_id = s.user_id
        )
        WHERE user_id = NEW.reviewee_user_id;
    END IF;
END$$

DELIMITER ;

-- ================================================
-- 인덱스 최적화 (추가)
-- ================================================

-- 복합 인덱스 추가
CREATE INDEX idx_orders_status_pickup_date ON orders(status, pickup_date);
CREATE INDEX idx_orders_status_shipper ON orders(status, shipper_id);
CREATE INDEX idx_orders_status_driver ON orders(status, driver_id);
CREATE INDEX idx_bids_order_status ON bids(order_id, status);
CREATE INDEX idx_reviews_reviewee_rating ON reviews(reviewee_user_id, overall_rating);

-- ================================================
-- 종료
-- ================================================

// ================================================
// 트럭커-프리(Trucker-Free) Prisma 스키마
// NestJS + Prisma + MySQL 8.0+
// ================================================

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

// ================================================
// 1. 사용자 관리
// ================================================

model User {
  id               BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  userType         UserType  @map("user_type")
  email            String    @unique @db.VarChar(255)
  phone            String    @unique @db.VarChar(20)
  passwordHash     String    @map("password_hash") @db.VarChar(255)
  name             String    @db.VarChar(100)
  status           UserStatus @default(active)
  emailVerifiedAt  DateTime? @map("email_verified_at")
  phoneVerifiedAt  DateTime? @map("phone_verified_at")
  lastLoginAt      DateTime? @map("last_login_at")
  loginCount       Int       @default(0) @db.UnsignedInt
  createdAt        DateTime  @default(now()) @map("created_at")
  updatedAt        DateTime  @updatedAt @map("updated_at")
  deletedAt        DateTime? @map("deleted_at")

  // Relations
  shipper          Shipper?
  driver           Driver?
  refreshTokens    RefreshToken[]
  sentReviews      Review[] @relation("Reviewer")
  receivedReviews  Review[] @relation("Reviewee")
  payments         Payment[]
  notifications    Notification[]
  inquiries        Inquiry[]
  reportedBlacklists Blacklist[] @relation("Reporter")
  assignedInquiries Inquiry[] @relation("AssignedAdmin")
  adminLogs        AdminLog[]

  @@index([userType])
  @@index([email])
  @@index([phone])
  @@index([status])
  @@index([createdAt])
  @@map("users")
}

enum UserType {
  admin
  shipper
  driver
}

enum UserStatus {
  active
  suspended
  blocked
  withdrawn
}

model Shipper {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  userId                  BigInt    @unique @map("user_id") @db.UnsignedBigInt
  companyName             String    @map("company_name") @db.VarChar(255)
  businessNumber          String    @unique @map("business_number") @db.VarChar(20)
  businessType            BusinessType @map("business_type")
  representativeName      String    @map("representative_name") @db.VarChar(100)
  businessAddress         String    @map("business_address") @db.Text
  businessAddressDetail   String?   @map("business_address_detail") @db.VarChar(255)
  businessCertificateUrl  String?   @map("business_certificate_url") @db.VarChar(500)
  depositAmount           Decimal   @default(0) @map("deposit_amount") @db.Decimal(15, 2)
  creditRating            CreditRating @default(C) @map("credit_rating")
  totalOrders             Int       @default(0) @map("total_orders") @db.UnsignedInt
  completedOrders         Int       @default(0) @map("completed_orders") @db.UnsignedInt
  cancelledOrders         Int       @default(0) @map("cancelled_orders") @db.UnsignedInt
  averageRating           Decimal   @default(0) @map("average_rating") @db.Decimal(3, 2)
  totalSpent              Decimal   @default(0) @map("total_spent") @db.Decimal(15, 2)
  verifiedAt              DateTime? @map("verified_at")
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  user                    User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  orders                  Order[]
  escrowAccounts          EscrowAccount[]
  chatRoomsAsShipper      ChatRoom[] @relation("ShipperChatRooms")

  @@index([businessNumber])
  @@index([creditRating])
  @@index([verifiedAt])
  @@map("shippers")
}

enum BusinessType {
  individual
  corporation
}

enum CreditRating {
  S
  A
  B
  C
  D
}

model Driver {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  userId                  BigInt    @unique @map("user_id") @db.UnsignedBigInt
  licenseNumber           String    @unique @map("license_number") @db.VarChar(50)
  licenseType             String    @map("license_type") @db.VarChar(20)
  licenseExpiryDate       DateTime  @map("license_expiry_date") @db.Date
  licenseImageUrl         String?   @map("license_image_url") @db.VarChar(500)
  carrierLicenseNumber    String?   @map("carrier_license_number") @db.VarChar(50)
  carrierLicenseImageUrl  String?   @map("carrier_license_image_url") @db.VarChar(500)
  bankName                String?   @map("bank_name") @db.VarChar(50)
  bankAccount             String?   @map("bank_account") @db.VarChar(50)
  accountHolder           String?   @map("account_holder") @db.VarChar(100)
  depositAmount           Decimal   @default(0) @map("deposit_amount") @db.Decimal(15, 2)
  creditRating            CreditRating @default(C) @map("credit_rating")
  totalDeliveries         Int       @default(0) @map("total_deliveries") @db.UnsignedInt
  completedDeliveries     Int       @default(0) @map("completed_deliveries") @db.UnsignedInt
  cancelledDeliveries     Int       @default(0) @map("cancelled_deliveries") @db.UnsignedInt
  noShowCount             Int       @default(0) @map("no_show_count") @db.UnsignedInt
  averageRating           Decimal   @default(0) @map("average_rating") @db.Decimal(3, 2)
  totalEarned             Decimal   @default(0) @map("total_earned") @db.Decimal(15, 2)
  currentLatitude         Decimal?  @map("current_latitude") @db.Decimal(10, 8)
  currentLongitude        Decimal?  @map("current_longitude") @db.Decimal(11, 8)
  lastLocationUpdatedAt   DateTime? @map("last_location_updated_at")
  isAvailable             Boolean   @default(true) @map("is_available")
  verifiedAt              DateTime? @map("verified_at")
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  user                    User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  vehicles                Vehicle[]
  orders                  Order[]
  bids                    Bid[]
  settlements             Settlement[]
  escrowAccounts          EscrowAccount[]
  chatRoomsAsDriver       ChatRoom[] @relation("DriverChatRooms")

  @@index([licenseNumber])
  @@index([creditRating])
  @@index([isAvailable])
  @@index([currentLatitude, currentLongitude])
  @@index([verifiedAt])
  @@map("drivers")
}

model Vehicle {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  driverId                BigInt    @map("driver_id") @db.UnsignedBigInt
  vehicleNumber           String    @unique @map("vehicle_number") @db.VarChar(20)
  vehicleType             VehicleType @map("vehicle_type")
  vehicleModel            String?   @map("vehicle_model") @db.VarChar(100)
  manufactureYear         Int?      @map("manufacture_year") @db.Year
  cargoType               CargoVehicleType @default(standard) @map("cargo_type")
  maxLoadWeight           Decimal   @map("max_load_weight") @db.Decimal(10, 2)
  length                  Decimal?  @db.Decimal(5, 2)
  width                   Decimal?  @db.Decimal(5, 2)
  height                  Decimal?  @db.Decimal(5, 2)
  vehicleRegistrationUrl  String?   @map("vehicle_registration_url") @db.VarChar(500)
  insuranceCompany        String?   @map("insurance_company") @db.VarChar(100)
  insuranceNumber         String?   @map("insurance_number") @db.VarChar(100)
  insuranceExpiryDate     DateTime? @map("insurance_expiry_date") @db.Date
  insuranceImageUrl       String?   @map("insurance_image_url") @db.VarChar(500)
  isActive                Boolean   @default(true) @map("is_active")
  verifiedAt              DateTime? @map("verified_at")
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  driver                  Driver    @relation(fields: [driverId], references: [id], onDelete: Cascade)
  orders                  Order[]
  bids                    Bid[]

  @@index([driverId])
  @@index([vehicleNumber])
  @@index([vehicleType])
  @@index([isActive])
  @@map("vehicles")
}

enum VehicleType {
  truck_1t
  truck_2_5t @map("truck_2.5t")
  truck_5t
  truck_11t
  truck_18t
  truck_25t
  special
}

enum CargoVehicleType {
  standard
  refrigerated
  frozen
  tank
  special
}

// ================================================
// 2. 주문 관리
// ================================================

model Order {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  orderNumber             String    @unique @map("order_number") @db.VarChar(50)
  shipperId               BigInt    @map("shipper_id") @db.UnsignedBigInt
  driverId                BigInt?   @map("driver_id") @db.UnsignedBigInt
  vehicleId               BigInt?   @map("vehicle_id") @db.UnsignedBigInt
  
  cargoType               CargoType @map("cargo_type")
  cargoWeight             Decimal   @map("cargo_weight") @db.Decimal(10, 2)
  cargoVolume             Decimal?  @map("cargo_volume") @db.Decimal(10, 2)
  cargoDescription        String?   @map("cargo_description") @db.Text
  
  pickupAddress           String    @map("pickup_address") @db.Text
  pickupAddressDetail     String?   @map("pickup_address_detail") @db.VarChar(255)
  pickupLatitude          Decimal   @map("pickup_latitude") @db.Decimal(10, 8)
  pickupLongitude         Decimal   @map("pickup_longitude") @db.Decimal(11, 8)
  pickupContactName       String    @map("pickup_contact_name") @db.VarChar(100)
  pickupContactPhone      String    @map("pickup_contact_phone") @db.VarChar(20)
  pickupDate              DateTime  @map("pickup_date") @db.Date
  pickupTimeStart         DateTime? @map("pickup_time_start") @db.Time(0)
  pickupTimeEnd           DateTime? @map("pickup_time_end") @db.Time(0)
  pickupCompletedAt       DateTime? @map("pickup_completed_at")
  
  deliveryAddress         String    @map("delivery_address") @db.Text
  deliveryAddressDetail   String?   @map("delivery_address_detail") @db.VarChar(255)
  deliveryLatitude        Decimal   @map("delivery_latitude") @db.Decimal(10, 8)
  deliveryLongitude       Decimal   @map("delivery_longitude") @db.Decimal(11, 8)
  deliveryContactName     String    @map("delivery_contact_name") @db.VarChar(100)
  deliveryContactPhone    String    @map("delivery_contact_phone") @db.VarChar(20)
  deliveryDate            DateTime  @map("delivery_date") @db.Date
  deliveryTimeStart       DateTime? @map("delivery_time_start") @db.Time(0)
  deliveryTimeEnd         DateTime? @map("delivery_time_end") @db.Time(0)
  deliveryCompletedAt     DateTime? @map("delivery_completed_at")
  
  status                  OrderStatus @default(draft)
  
  biddingType             BiddingType @map("bidding_type")
  biddingStartAt          DateTime? @map("bidding_start_at")
  biddingEndAt            DateTime? @map("bidding_end_at")
  
  basePrice               Decimal?  @map("base_price") @db.Decimal(15, 2)
  finalPrice              Decimal?  @map("final_price") @db.Decimal(15, 2)
  tollFee                 Decimal   @default(0) @map("toll_fee") @db.Decimal(10, 2)
  fuelCost                Decimal   @default(0) @map("fuel_cost") @db.Decimal(10, 2)
  loadingFee              Decimal   @default(0) @map("loading_fee") @db.Decimal(10, 2)
  unloadingFee            Decimal   @default(0) @map("unloading_fee") @db.Decimal(10, 2)
  waitingFee              Decimal   @default(0) @map("waiting_fee") @db.Decimal(10, 2)
  additionalFee           Decimal   @default(0) @map("additional_fee") @db.Decimal(10, 2)
  totalAmount             Decimal   @default(0) @map("total_amount") @db.Decimal(15, 2)
  
  shipperFeeRate          Decimal   @default(3) @map("shipper_fee_rate") @db.Decimal(5, 2)
  driverFeeRate           Decimal   @default(2) @map("driver_fee_rate") @db.Decimal(5, 2)
  shipperFee              Decimal   @default(0) @map("shipper_fee") @db.Decimal(10, 2)
  driverFee               Decimal   @default(0) @map("driver_fee") @db.Decimal(10, 2)
  
  requiresLoadingHelp     Boolean   @default(false) @map("requires_loading_help")
  requiresUnloadingHelp   Boolean   @default(false) @map("requires_unloading_help")
  requiresSpecialEquipment Boolean  @default(false) @map("requires_special_equipment")
  specialRequirements     String?   @map("special_requirements") @db.Text
  
  estimatedDistance       Decimal?  @map("estimated_distance") @db.Decimal(10, 2)
  estimatedDuration       Int?      @map("estimated_duration")
  actualDistance          Decimal?  @map("actual_distance") @db.Decimal(10, 2)
  actualDuration          Int?      @map("actual_duration")
  
  cancelledReason         String?   @map("cancelled_reason") @db.Text
  cancelledBy             CancelledBy? @map("cancelled_by")
  cancelledAt             DateTime? @map("cancelled_at")
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  shipper                 Shipper   @relation(fields: [shipperId], references: [id])
  driver                  Driver?   @relation(fields: [driverId], references: [id])
  vehicle                 Vehicle?  @relation(fields: [vehicleId], references: [id])
  bids                    Bid[]
  statusHistories         OrderStatusHistory[]
  attachments             OrderAttachment[]
  escrowAccount           EscrowAccount?
  payments                Payment[]
  settlements             Settlement[]
  reviews                 Review[]
  blacklists              Blacklist[]
  chatRooms               ChatRoom[]
  inquiries               Inquiry[]

  @@index([orderNumber])
  @@index([shipperId])
  @@index([driverId])
  @@index([status])
  @@index([pickupDate])
  @@index([deliveryDate])
  @@index([biddingEndAt])
  @@index([createdAt])
  @@index([status, pickupDate])
  @@index([status, shipperId])
  @@index([status, driverId])
  @@map("orders")
}

enum CargoType {
  general
  refrigerated
  frozen
  fragile
  dangerous
  oversized
}

enum OrderStatus {
  draft
  bidding
  assigned
  pickup_pending
  in_transit
  delivered
  completed
  cancelled
}

enum BiddingType {
  reverse_auction
  standard_auction
  instant_match
}

enum CancelledBy {
  shipper
  driver
  admin
  system
}

model Bid {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  orderId                 BigInt    @map("order_id") @db.UnsignedBigInt
  driverId                BigInt    @map("driver_id") @db.UnsignedBigInt
  vehicleId               BigInt    @map("vehicle_id") @db.UnsignedBigInt
  
  bidAmount               Decimal   @map("bid_amount") @db.Decimal(15, 2)
  estimatedPickupTime     DateTime? @map("estimated_pickup_time")
  message                 String?   @db.Text
  
  status                  BidStatus @default(pending)
  
  acceptedAt              DateTime? @map("accepted_at")
  rejectedAt              DateTime? @map("rejected_at")
  withdrawnAt             DateTime? @map("withdrawn_at")
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  order                   Order     @relation(fields: [orderId], references: [id], onDelete: Cascade)
  driver                  Driver    @relation(fields: [driverId], references: [id])
  vehicle                 Vehicle   @relation(fields: [vehicleId], references: [id])

  @@unique([orderId, driverId])
  @@index([orderId])
  @@index([driverId])
  @@index([status])
  @@index([bidAmount])
  @@index([createdAt])
  @@index([orderId, status])
  @@map("bids")
}

enum BidStatus {
  pending
  accepted
  rejected
  withdrawn
  expired
}

model OrderStatusHistory {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  orderId                 BigInt    @map("order_id") @db.UnsignedBigInt
  status                  OrderStatus
  changedByUserId         BigInt?   @map("changed_by_user_id") @db.UnsignedBigInt
  note                    String?   @db.Text
  latitude                Decimal?  @db.Decimal(10, 8)
  longitude               Decimal?  @db.Decimal(11, 8)
  createdAt               DateTime  @default(now()) @map("created_at")

  // Relations
  order                   Order     @relation(fields: [orderId], references: [id], onDelete: Cascade)
  changedBy               User?     @relation(fields: [changedByUserId], references: [id])

  @@index([orderId])
  @@index([status])
  @@index([createdAt])
  @@map("order_status_histories")
}

model OrderAttachment {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  orderId                 BigInt    @map("order_id") @db.UnsignedBigInt
  type                    AttachmentType
  fileUrl                 String    @map("file_url") @db.VarChar(500)
  fileName                String    @map("file_name") @db.VarChar(255)
  fileSize                Int?      @map("file_size") @db.UnsignedInt
  mimeType                String?   @map("mime_type") @db.VarChar(100)
  uploadedByUserId        BigInt    @map("uploaded_by_user_id") @db.UnsignedBigInt
  description             String?   @db.Text
  createdAt               DateTime  @default(now()) @map("created_at")

  // Relations
  order                   Order     @relation(fields: [orderId], references: [id], onDelete: Cascade)
  uploadedBy              User      @relation(fields: [uploadedByUserId], references: [id])

  @@index([orderId])
  @@index([type])
  @@index([createdAt])
  @@map("order_attachments")
}

enum AttachmentType {
  cargo_image
  pickup_proof
  delivery_proof
  receipt
  other
}

// ================================================
// 3. 결제 및 정산
// ================================================

model EscrowAccount {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  orderId                 BigInt    @unique @map("order_id") @db.UnsignedBigInt
  shipperId               BigInt    @map("shipper_id") @db.UnsignedBigInt
  driverId                BigInt?   @map("driver_id") @db.UnsignedBigInt
  
  totalAmount             Decimal   @map("total_amount") @db.Decimal(15, 2)
  status                  EscrowStatus @default(pending)
  
  depositedAt             DateTime? @map("deposited_at")
  releasedAt              DateTime? @map("released_at")
  refundedAt              DateTime? @map("refunded_at")
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  order                   Order     @relation(fields: [orderId], references: [id], onDelete: Cascade)
  shipper                 Shipper   @relation(fields: [shipperId], references: [id])
  driver                  Driver?   @relation(fields: [driverId], references: [id])

  @@index([orderId])
  @@index([status])
  @@index([shipperId])
  @@index([driverId])
  @@map("escrow_accounts")
}

enum EscrowStatus {
  pending
  deposited
  released
  refunded
  cancelled
}

model Payment {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  paymentNumber           String    @unique @map("payment_number") @db.VarChar(50)
  orderId                 BigInt    @map("order_id") @db.UnsignedBigInt
  payerUserId             BigInt    @map("payer_user_id") @db.UnsignedBigInt
  
  paymentType             PaymentType @map("payment_type")
  paymentMethod           PaymentMethod @map("payment_method")
  
  amount                  Decimal   @db.Decimal(15, 2)
  vat                     Decimal   @default(0) @db.Decimal(15, 2)
  totalAmount             Decimal   @map("total_amount") @db.Decimal(15, 2)
  
  status                  PaymentStatus @default(pending)
  
  pgName                  String?   @map("pg_name") @db.VarChar(50)
  pgTransactionId         String?   @map("pg_transaction_id") @db.VarChar(100)
  
  approvedAt              DateTime? @map("approved_at")
  cancelledAt             DateTime? @map("cancelled_at")
  refundedAt              DateTime? @map("refunded_at")
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  order                   Order     @relation(fields: [orderId], references: [id])
  payer                   User      @relation(fields: [payerUserId], references: [id])

  @@index([paymentNumber])
  @@index([orderId])
  @@index([payerUserId])
  @@index([status])
  @@index([createdAt])
  @@map("payments")
}

enum PaymentType {
  order
  deposit
  premium
  penalty
}

enum PaymentMethod {
  card
  transfer
  virtual_account
  kakao_pay
  naver_pay
  toss
}

enum PaymentStatus {
  pending
  completed
  failed
  cancelled
  refunded
}

model Settlement {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  settlementNumber        String    @unique @map("settlement_number") @db.VarChar(50)
  orderId                 BigInt    @map("order_id") @db.UnsignedBigInt
  driverId                BigInt    @map("driver_id") @db.UnsignedBigInt
  
  orderAmount             Decimal   @map("order_amount") @db.Decimal(15, 2)
  platformFee             Decimal   @map("platform_fee") @db.Decimal(10, 2)
  driverEarning           Decimal   @map("driver_earning") @db.Decimal(15, 2)
  
  tollFee                 Decimal   @default(0) @map("toll_fee") @db.Decimal(10, 2)
  fuelCost                Decimal   @default(0) @map("fuel_cost") @db.Decimal(10, 2)
  additionalFee           Decimal   @default(0) @map("additional_fee") @db.Decimal(10, 2)
  
  status                  SettlementStatus @default(pending)
  
  settlementDate          DateTime? @map("settlement_date") @db.Date
  settledAt               DateTime? @map("settled_at")
  
  bankName                String?   @map("bank_name") @db.VarChar(50)
  bankAccount             String?   @map("bank_account") @db.VarChar(50)
  accountHolder           String?   @map("account_holder") @db.VarChar(100)
  
  taxInvoiceUrl           String?   @map("tax_invoice_url") @db.VarChar(500)
  
  note                    String?   @db.Text
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  order                   Order     @relation(fields: [orderId], references: [id])
  driver                  Driver    @relation(fields: [driverId], references: [id])

  @@index([settlementNumber])
  @@index([orderId])
  @@index([driverId])
  @@index([status])
  @@index([settlementDate])
  @@map("settlements")
}

enum SettlementStatus {
  pending
  approved
  completed
  rejected
}

model Deposit {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  userId                  BigInt    @map("user_id") @db.UnsignedBigInt
  userType                DepositUserType @map("user_type")
  
  transactionType         DepositTransactionType @map("transaction_type")
  amount                  Decimal   @db.Decimal(15, 2)
  balanceAfter            Decimal   @map("balance_after") @db.Decimal(15, 2)
  
  reason                  String?   @db.VarChar(255)
  relatedOrderId          BigInt?   @map("related_order_id") @db.UnsignedBigInt
  
  status                  DepositStatus @default(pending)
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  user                    User      @relation(fields: [userId], references: [id])
  relatedOrder            Order?    @relation(fields: [relatedOrderId], references: [id])

  @@index([userId])
  @@index([userType])
  @@index([transactionType])
  @@index([createdAt])
  @@map("deposits")
}

enum DepositUserType {
  shipper
  driver
}

enum DepositTransactionType {
  deposit
  withdrawal
  deduction
  refund
}

enum DepositStatus {
  pending
  completed
  failed
}

// ================================================
// 4. 평가 및 리뷰
// ================================================

model Review {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  orderId                 BigInt    @map("order_id") @db.UnsignedBigInt
  reviewerUserId          BigInt    @map("reviewer_user_id") @db.UnsignedBigInt
  revieweeUserId          BigInt    @map("reviewee_user_id") @db.UnsignedBigInt
  reviewerType            ReviewerType @map("reviewer_type")
  
  overallRating           Decimal   @map("overall_rating") @db.Decimal(2, 1)
  punctualityRating       Decimal?  @map("punctuality_rating") @db.Decimal(2, 1)
  kindnessRating          Decimal?  @map("kindness_rating") @db.Decimal(2, 1)
  safetyRating            Decimal?  @map("safety_rating") @db.Decimal(2, 1)
  communicationRating     Decimal?  @map("communication_rating") @db.Decimal(2, 1)
  
  comment                 String?   @db.Text
  tags                    Json?
  
  isVisible               Boolean   @default(true) @map("is_visible")
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  order                   Order     @relation(fields: [orderId], references: [id], onDelete: Cascade)
  reviewer                User      @relation("Reviewer", fields: [reviewerUserId], references: [id])
  reviewee                User      @relation("Reviewee", fields: [revieweeUserId], references: [id])

  @@unique([orderId, reviewerUserId])
  @@index([orderId])
  @@index([reviewerUserId])
  @@index([revieweeUserId])
  @@index([overallRating])
  @@index([createdAt])
  @@index([revieweeUserId, overallRating])
  @@map("reviews")
}

enum ReviewerType {
  shipper
  driver
}

// ================================================
// 5. 블랙리스트 및 제재
// ================================================

model Blacklist {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  userId                  BigInt    @map("user_id") @db.UnsignedBigInt
  userType                BlacklistUserType @map("user_type")
  
  violationType           ViolationType @map("violation_type")
  severity                Severity
  
  reason                  String    @db.Text
  relatedOrderId          BigInt?   @map("related_order_id") @db.UnsignedBigInt
  
  reportedByUserId        BigInt?   @map("reported_by_user_id") @db.UnsignedBigInt
  reviewedByAdminId       BigInt?   @map("reviewed_by_admin_id") @db.UnsignedBigInt
  
  status                  BlacklistStatus @default(active)
  
  suspensionStartDate     DateTime? @map("suspension_start_date") @db.Date
  suspensionEndDate       DateTime? @map("suspension_end_date") @db.Date
  
  appealReason            String?   @map("appeal_reason") @db.Text
  appealResult            String?   @map("appeal_result") @db.Text
  appealedAt              DateTime? @map("appealed_at")
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  user                    User      @relation(fields: [userId], references: [id])
  relatedOrder            Order?    @relation(fields: [relatedOrderId], references: [id])
  reportedBy              User?     @relation("Reporter", fields: [reportedByUserId], references: [id])
  reviewedByAdmin         User?     @relation(fields: [reviewedByAdminId], references: [id])

  @@index([userId])
  @@index([violationType])
  @@index([severity])
  @@index([status])
  @@index([createdAt])
  @@map("blacklists")
}

enum BlacklistUserType {
  shipper
  driver
}

enum ViolationType {
  no_show
  payment_delay
  fraud
  abuse
  other
}

enum Severity {
  warning
  suspension
  permanent_ban
}

enum BlacklistStatus {
  active
  resolved
  appealed
}

// ================================================
// 6. 채팅 및 알림
// ================================================

model ChatRoom {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  orderId                 BigInt    @map("order_id") @db.UnsignedBigInt
  shipperUserId           BigInt    @map("shipper_user_id") @db.UnsignedBigInt
  driverUserId            BigInt    @map("driver_user_id") @db.UnsignedBigInt
  
  isActive                Boolean   @default(true) @map("is_active")
  lastMessageAt           DateTime? @map("last_message_at")
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  order                   Order     @relation(fields: [orderId], references: [id], onDelete: Cascade)
  shipperUser             User      @relation("ShipperChatRooms", fields: [shipperUserId], references: [id])
  driverUser              User      @relation("DriverChatRooms", fields: [driverUserId], references: [id])
  messages                ChatMessage[]

  @@unique([orderId, shipperUserId, driverUserId])
  @@index([orderId])
  @@index([shipperUserId])
  @@index([driverUserId])
  @@index([lastMessageAt])
  @@map("chat_rooms")
}

model ChatMessage {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  chatRoomId              BigInt    @map("chat_room_id") @db.UnsignedBigInt
  senderUserId            BigInt    @map("sender_user_id") @db.UnsignedBigInt
  
  messageType             MessageType @default(text) @map("message_type")
  content                 String?   @db.Text
  fileUrl                 String?   @map("file_url") @db.VarChar(500)
  
  isRead                  Boolean   @default(false) @map("is_read")
  readAt                  DateTime? @map("read_at")
  
  createdAt               DateTime  @default(now()) @map("created_at")

  // Relations
  chatRoom                ChatRoom  @relation(fields: [chatRoomId], references: [id], onDelete: Cascade)
  sender                  User      @relation(fields: [senderUserId], references: [id])

  @@index([chatRoomId])
  @@index([senderUserId])
  @@index([isRead])
  @@index([createdAt])
  @@map("chat_messages")
}

enum MessageType {
  text
  image
  file
  system
}

model Notification {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  userId                  BigInt    @map("user_id") @db.UnsignedBigInt
  
  notificationType        NotificationType @map("notification_type")
  title                   String    @db.VarChar(255)
  content                 String    @db.Text
  
  relatedOrderId          BigInt?   @map("related_order_id") @db.UnsignedBigInt
  actionUrl               String?   @map("action_url") @db.VarChar(500)
  
  isRead                  Boolean   @default(false) @map("is_read")
  readAt                  DateTime? @map("read_at")
  
  pushSent                Boolean   @default(false) @map("push_sent")
  pushSentAt              DateTime? @map("push_sent_at")
  
  createdAt               DateTime  @default(now()) @map("created_at")

  // Relations
  user                    User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  relatedOrder            Order?    @relation(fields: [relatedOrderId], references: [id])

  @@index([userId])
  @@index([notificationType])
  @@index([isRead])
  @@index([createdAt])
  @@map("notifications")
}

enum NotificationType {
  order
  bid
  payment
  review
  system
  marketing
}

// ================================================
// 7. 시스템 관리
// ================================================

model SystemSetting {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  settingKey              String    @unique @map("setting_key") @db.VarChar(100)
  settingValue            String    @map("setting_value") @db.Text
  settingType             SettingType @default(string) @map("setting_type")
  description             String?   @db.Text
  isPublic                Boolean   @default(false) @map("is_public")
  updatedByAdminId        BigInt?   @map("updated_by_admin_id") @db.UnsignedBigInt
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  updatedByAdmin          User?     @relation(fields: [updatedByAdminId], references: [id])

  @@index([settingKey])
  @@index([isPublic])
  @@map("system_settings")
}

enum SettingType {
  string
  number
  boolean
  json
}

model PricingRule {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  ruleName                String    @map("rule_name") @db.VarChar(100)
  vehicleType             VehicleType @map("vehicle_type")
  cargoType               CargoType @map("cargo_type")
  
  baseDistance            Int?      @map("base_distance")
  basePrice               Decimal?  @map("base_price") @db.Decimal(15, 2)
  pricePerKm              Decimal?  @map("price_per_km") @db.Decimal(10, 2)
  
  nightSurchargeRate      Decimal   @default(0) @map("night_surcharge_rate") @db.Decimal(5, 2)
  weekendSurchargeRate    Decimal   @default(0) @map("weekend_surcharge_rate") @db.Decimal(5, 2)
  holidaySurchargeRate    Decimal   @default(0) @map("holiday_surcharge_rate") @db.Decimal(5, 2)
  
  loadingFeePerPerson     Decimal?  @map("loading_fee_per_person") @db.Decimal(10, 2)
  unloadingFeePerPerson   Decimal?  @map("unloading_fee_per_person") @db.Decimal(10, 2)
  waitingFeePerHour       Decimal?  @map("waiting_fee_per_hour") @db.Decimal(10, 2)
  
  isActive                Boolean   @default(true) @map("is_active")
  
  effectiveFrom           DateTime? @map("effective_from") @db.Date
  effectiveTo             DateTime? @map("effective_to") @db.Date
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  @@index([vehicleType])
  @@index([cargoType])
  @@index([isActive])
  @@index([effectiveFrom, effectiveTo])
  @@map("pricing_rules")
}

model Faq {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  category                FaqCategory
  userType                FaqUserType @default(all) @map("user_type")
  
  question                String    @db.VarChar(500)
  answer                  String    @db.Text
  
  viewCount               Int       @default(0) @map("view_count") @db.UnsignedInt
  isVisible               Boolean   @default(true) @map("is_visible")
  displayOrder            Int       @default(0) @map("display_order")
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  @@index([category])
  @@index([userType])
  @@index([isVisible])
  @@index([displayOrder])
  @@map("faqs")
}

enum FaqCategory {
  general
  order
  payment
  rating
  account
  technical
}

enum FaqUserType {
  all
  shipper
  driver
  admin
}

model Notice {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  authorAdminId           BigInt    @map("author_admin_id") @db.UnsignedBigInt
  
  noticeType              NoticeType @default(general) @map("notice_type")
  targetUserType          NoticeUserType @default(all) @map("target_user_type")
  
  title                   String    @db.VarChar(255)
  content                 String    @db.Text
  
  isPinned                Boolean   @default(false) @map("is_pinned")
  isVisible               Boolean   @default(true) @map("is_visible")
  
  viewCount               Int       @default(0) @map("view_count") @db.UnsignedInt
  
  publishedAt             DateTime? @map("published_at")
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  author                  User      @relation(fields: [authorAdminId], references: [id])

  @@index([noticeType])
  @@index([targetUserType])
  @@index([isPinned])
  @@index([isVisible])
  @@index([publishedAt])
  @@map("notices")
}

enum NoticeType {
  general
  update
  maintenance
  event
  urgent
}

enum NoticeUserType {
  all
  shipper
  driver
}

model Inquiry {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  userId                  BigInt    @map("user_id") @db.UnsignedBigInt
  
  inquiryType             InquiryType @map("inquiry_type")
  title                   String    @db.VarChar(255)
  content                 String    @db.Text
  
  relatedOrderId          BigInt?   @map("related_order_id") @db.UnsignedBigInt
  
  status                  InquiryStatus @default(pending)
  
  adminUserId             BigInt?   @map("admin_user_id") @db.UnsignedBigInt
  adminReply              String?   @map("admin_reply") @db.Text
  repliedAt               DateTime? @map("replied_at")
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  user                    User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  relatedOrder            Order?    @relation(fields: [relatedOrderId], references: [id])
  admin                   User?     @relation("AssignedAdmin", fields: [adminUserId], references: [id])

  @@index([userId])
  @@index([inquiryType])
  @@index([status])
  @@index([createdAt])
  @@map("inquiries")
}

enum InquiryType {
  general
  payment
  technical
  report
  suggestion
  other
}

enum InquiryStatus {
  pending
  in_progress
  resolved
  closed
}

model AdminLog {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  adminUserId             BigInt    @map("admin_user_id") @db.UnsignedBigInt
  
  actionType              ActionType @map("action_type")
  targetTable             String    @map("target_table") @db.VarChar(100)
  targetId                BigInt    @map("target_id") @db.UnsignedBigInt
  
  beforeData              Json?     @map("before_data")
  afterData               Json?     @map("after_data")
  
  description             String?   @db.Text
  ipAddress               String?   @map("ip_address") @db.VarChar(45)
  userAgent               String?   @map("user_agent") @db.Text
  
  createdAt               DateTime  @default(now()) @map("created_at")

  // Relations
  admin                   User      @relation(fields: [adminUserId], references: [id])

  @@index([adminUserId])
  @@index([actionType])
  @@index([targetTable, targetId])
  @@index([createdAt])
  @@map("admin_logs")
}

enum ActionType {
  create
  update
  delete
  approve
  reject
  suspend
  restore
}

// ================================================
// 8. 통계 및 분석
// ================================================

model DailyStatistic {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  statDate                DateTime  @unique @map("stat_date") @db.Date
  
  totalOrders             Int       @default(0) @map("total_orders") @db.UnsignedInt
  completedOrders         Int       @default(0) @map("completed_orders") @db.UnsignedInt
  cancelledOrders         Int       @default(0) @map("cancelled_orders") @db.UnsignedInt
  
  totalOrderAmount        Decimal   @default(0) @map("total_order_amount") @db.Decimal(15, 2)
  totalPlatformFee        Decimal   @default(0) @map("total_platform_fee") @db.Decimal(15, 2)
  totalDriverEarning      Decimal   @default(0) @map("total_driver_earning") @db.Decimal(15, 2)
  
  newShippers             Int       @default(0) @map("new_shippers") @db.UnsignedInt
  newDrivers              Int       @default(0) @map("new_drivers") @db.UnsignedInt
  activeShippers          Int       @default(0) @map("active_shippers") @db.UnsignedInt
  activeDrivers           Int       @default(0) @map("active_drivers") @db.UnsignedInt
  
  avgOrderAmount          Decimal   @default(0) @map("avg_order_amount") @db.Decimal(15, 2)
  avgRating               Decimal   @default(0) @map("avg_rating") @db.Decimal(3, 2)
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  @@index([statDate])
  @@map("daily_statistics")
}

// ================================================
// 9. 인증 및 세션
// ================================================

model RefreshToken {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  userId                  BigInt    @map("user_id") @db.UnsignedBigInt
  token                   String    @unique @db.VarChar(500)
  expiresAt               DateTime  @map("expires_at")
  deviceInfo              String?   @map("device_info") @db.Text
  ipAddress               String?   @map("ip_address") @db.VarChar(45)
  isRevoked               Boolean   @default(false) @map("is_revoked")
  revokedAt               DateTime? @map("revoked_at")
  createdAt               DateTime  @default(now()) @map("created_at")

  // Relations
  user                    User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@index([token])
  @@index([expiresAt])
  @@index([isRevoked])
  @@map("refresh_tokens")
}

model SmsVerification {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  phone                   String    @db.VarChar(20)
  verificationCode        String    @map("verification_code") @db.VarChar(6)
  purpose                 VerificationPurpose
  isVerified              Boolean   @default(false) @map("is_verified")
  verifiedAt              DateTime? @map("verified_at")
  expiresAt               DateTime  @map("expires_at")
  attemptCount            Int       @default(0) @map("attempt_count") @db.UnsignedInt
  createdAt               DateTime  @default(now()) @map("created_at")

  @@index([phone])
  @@index([verificationCode])
  @@index([isVerified])
  @@index([expiresAt])
  @@map("sms_verifications")
}

enum VerificationPurpose {
  signup
  password_reset
  phone_change
  login
}

// ================================================
// 10. 프리미엄 서비스
// ================================================

model PremiumSubscription {
  id                      BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  userId                  BigInt    @map("user_id") @db.UnsignedBigInt
  
  subscriptionType        SubscriptionType @map("subscription_type")
  
  status                  SubscriptionStatus @default(active)
  
  startedAt               DateTime  @map("started_at")
  expiresAt               DateTime  @map("expires_at")
  
  monthlyFee              Decimal   @map("monthly_fee") @db.Decimal(10, 2)
  
  autoRenewal             Boolean   @default(true) @map("auto_renewal")
  
  cancelledAt             DateTime? @map("cancelled_at")
  
  createdAt               DateTime  @default(now()) @map("created_at")
  updatedAt               DateTime  @updatedAt @map("updated_at")

  // Relations
  user                    User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@index([subscriptionType])
  @@index([status])
  @@index([expiresAt])
  @@map("premium_subscriptions")
}

enum SubscriptionType {
  top_exposure
  priority_dispatch
  ad_free
  bundle
}

enum SubscriptionStatus {
  active
  paused
  cancelled
  expired
}

, **NestJS 백엔드 인프라 구축과 핵심 모듈인 '주문 및 역경매(Bidding) 시스템'**의 코딩을 시작하겠습니다.

가장 먼저 프로젝트의 뼈대가 되는 Prisma 서비스 설정주문 생성 및 입찰 로직을 구현합니다.


1. 프로젝트 초기 설정 및 Prisma Service

NestJS에서 Prisma를 사용하기 위한 공통 모듈을 설정합니다.

TypeScript
 
// src/prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

2. 주문 관리 모듈 (화주용: 주문 등록 및 자동 비용 계산)

화주가 주문을 올릴 때 통행료 및 유류비를 자동으로 계산하여 DB에 저장하는 로직입니다.

TypeScript
 
// src/orders/orders.service.ts
@Injectable()
export class OrdersService {
  constructor(private prisma: PrismaService) {}

  async createOrder(shipperId: bigint, dto: CreateOrderDto) {
    return this.prisma.$transaction(async (tx) => {
      // 1. 시스템 설정에서 현재 유가 및 기본 요금 룰 가져오기
      const pricingRule = await tx.pricingRule.findFirst({
        where: { isActive: true }
      });

      // 2. 외부 API(가상)를 통한 톨게이트 및 거리 계산 로직 (예시)
      const distance = 150; // km
      const tollFee = 12500; // API 연동 결과 가정
      const fuelCost = (distance / 10) * Number(pricingRule.basePrice); // 단순화된 계산

      // 3. 주문 생성 (Status: BIDDING)
      return tx.order.create({
        data: {
          orderNumber: `ORD-${Date.now()}`,
          shipperId,
          pickupAddress: dto.pickupAddress,
          deliveryAddress: dto.deliveryAddress,
          distance,
          tollFee,
          fuelCost,
          baseAmount: dto.hopeAmount,
          totalAmount: Number(dto.hopeAmount) + tollFee + fuelCost,
          status: 'bidding',
        },
      });
    });
  }
}

3. 역경매 시스템 모듈 (차주용: 입찰 및 흥정)

차주가 화주의 주문에 입찰하고, 자신의 **신용 등급(S~D)**이 함께 노출되는 구조입니다.

TypeScript
 
// src/bidding/bidding.service.ts
@Injectable()
export class BiddingService {
  constructor(private prisma: PrismaService) {}

  async placeBid(driverId: bigint, orderId: bigint, bidAmount: number) {
    // 1. 주문 상태 확인 (입찰 가능 여부)
    const order = await this.prisma.order.findUnique({ where: { id: orderId } });
    if (order.status !== 'bidding') throw new BadRequestException('입찰 가능한 상태가 아닙니다.');

    // 2. 입찰 등록 (경매/역경매 로직)
    return this.prisma.bid.create({
      data: {
        orderId,
        driverId,
        bidAmount,
        status: 'pending',
      },
    });
  }

  async acceptBid(shipperId: bigint, bidId: bigint) {
    return this.prisma.$transaction(async (tx) => {
      // 1. 입찰 정보 및 차주 확인
      const bid = await tx.bid.findUnique({ 
        where: { id: bidId },
        include: { order: true }
      });

      // 2. 주문 상태 업데이트 및 배차 확정
      await tx.order.update({
        where: { id: bid.orderId },
        data: { 
          driverId: bid.driverId,
          status: 'dispatched',
          finalAmount: bid.bidAmount 
        }
      });

      // 3. 다른 입찰건들은 거절 처리
      await tx.bid.updateMany({
        where: { orderId: bid.orderId, NOT: { id: bidId } },
        data: { status: 'rejected' }
      });

      return tx.bid.update({
        where: { id: bidId },
        data: { status: 'accepted' }
      });
    });
  }
}

4. 신용 및 별점 관리 (자동 업데이트 트리거 연동)

DB 트리거가 평점을 계산하지만, 백엔드에서 블랙리스트 기반 접근 제어를 수행합니다.

TypeScript
 
// src/common/guards/blacklist.guard.ts
@Injectable()
export class BlacklistGuard implements CanActivate {
  constructor(private prisma: PrismaService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const { user } = context.switchToHttp().getRequest();
    
    const isBlacklisted = await this.prisma.blacklist.findFirst({
      where: {
        userId: user.id,
        status: 'active',
        expiresAt: { gt: new Date() }
      }
    });

    if (isBlacklisted) {
      throw new ForbiddenException('제재된 계정입니다. 사유: ' + isBlacklisted.reason);
    }
    return true;
  }
}

🚀 다음 개발 단계 가이드

이제 백엔드의 핵심 로직이 준비되었습니다. 이어서 진행할 작업은 다음과 같습니다.

  1. Auth 모듈 완성: sms_verifications 테이블을 활용한 휴대폰 본인인증 및 JWT 발급 로직 구현.
  2. 결제(Escrow) 연동: 가상계좌 발급 시 escrow_accounts에 입금 확인 후 주문을 dispatched로 변경하는 Webhook 연동.
  3. 실시간 위치(GPS): drivers 테이블의 current_lat/lng를 1분 단위로 업데이트하는 Gateway(Socket.io) 개발.

제공해주신 DB 스키마 중 users, sms_verifications, refresh_tokens 테이블을 활용하여 **휴대폰 번호 기반의 인증 모듈(Auth Module)**을 구현하겠습니다.

NestJS에서 보안성을 확보하기 위해 **Passport(JWT Strategy)**를 사용하고, 사용자가 로그인할 때마다 refresh_tokens 테이블에 토큰을 저장하여 중복 로그인 방지 및 보안을 강화하는 로직입니다.


1. SMS 인증 서비스 (SMS Verification)

회원가입 및 로그인 시 휴대폰 번호로 인증번호를 발송하고 확인하는 핵심 로직입니다.

TypeScript
 
// src/auth/sms.service.ts
import { Injectable, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class SmsService {
  constructor(private prisma: PrismaService) {}

  async sendVerificationCode(phone: string, purpose: 'signup' | 'login') {
    const code = Math.floor(100000 + Math.random() * 900000).toString(); // 6자리 코드
    const expiresAt = new Date(Date.now() + 5 * 60000); // 5분 뒤 만료

    // 1. 기존 인증번호 무효화 후 새로운 번호 저장
    await this.prisma.sMSVerification.create({
      data: {
        phone,
        verificationCode: code,
        purpose,
        expiresAt,
      },
    });

    // 2. 실제 SMS 발송 API 연동 (예: 알리고, 네이버 클라우드 등)
    console.log(`[SMS 발송] ${phone}님, 인증번호는 [${code}] 입니다.`);
    return { success: true };
  }

  async verifyCode(phone: string, code: string) {
    const verification = await this.prisma.sMSVerification.findFirst({
      where: { phone, verificationCode: code, isVerified: false },
      orderBy: { createdAt: 'desc' },
    });

    if (!verification || verification.expiresAt < new Date()) {
      throw new BadRequestException('인증번호가 유효하지 않거나 만료되었습니다.');
    }

    await this.prisma.sMSVerification.update({
      where: { id: verification.id },
      data: { isVerified: true, verifiedAt: new Date() },
    });

    return true;
  }
}

2. JWT 및 Refresh Token 처리 (Passport Strategy)

Access Token은 짧게(15분), Refresh Token은 길게(7일) 설정하여 refresh_tokens 테이블에서 관리합니다.

TypeScript
 
// src/auth/auth.service.ts
@Injectable()
export class AuthService {
  constructor(
    private prisma: PrismaService,
    private jwtService: JwtService,
  ) {}

  async login(user: any) {
    const payload = { sub: user.id, email: user.email, type: user.userType };
    
    const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
    const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });

    // DB에 Refresh Token 저장 (보안 및 중복 로그인 관리용)
    await this.prisma.refreshToken.create({
      data: {
        userId: user.id,
        token: refreshToken,
        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
        userAgent: 'Web/Mobile', // 필요시 요청 정보에서 추출
      },
    });

    // 마지막 로그인 시간 및 카운트 업데이트
    await this.prisma.user.update({
      where: { id: user.id },
      data: { 
        lastLoginAt: new Date(),
        loginCount: { increment: 1 }
      },
    });

    return { access_token: accessToken, refresh_token: refreshToken };
  }
}

3. 회원가입 및 본인인증 로직 (Controller)

사용자 유형(shipper, driver)에 따라 추가 정보를 생성합니다.

TypeScript
 
// src/auth/auth.controller.ts
@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService, private smsService: SmsService) {}

  @Post('signup')
  async signup(@Body() dto: SignupDto) {
    // 1. SMS 인증 여부 최종 확인
    await this.smsService.verifyCode(dto.phone, dto.code);

    // 2. 사용자 생성 (비밀번호 암호화 필수: bcrypt 등)
    const user = await this.prisma.user.create({
      data: {
        email: dto.email,
        phone: dto.phone,
        passwordHash: await bcrypt.hash(dto.password, 10),
        name: dto.name,
        userType: dto.userType,
        // 사용자 유형에 따라 빈 프로필 자동 생성
        [dto.userType === 'shipper' ? 'shipper' : 'driver']: {
          create: {} 
        }
      },
    });

    return { message: '회원가입이 완료되었습니다.' };
  }
}

4. 권한 제어 (Guard & Decorator)

화주만 접근 가능한 API, 차주만 접근 가능한 API를 구분하기 위한 Guard입니다.

TypeScript
 
// src/common/decorators/roles.decorator.ts
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// src/common/guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.includes(user.type); // userType: admin, shipper, driver
  }
}

💡 보안 강화 팁

  1. 비밀번호 암호화: bcrypt를 사용하여 password_hash를 저장하세요.
  2. SMS 어뷰징 방지: 동일 번호에 대해 1분 이내 재요청 시 차단하는 로직을 sms_verifications 테이블의 createdAt을 조회해 추가하는 것이 좋습니다.
  3. 트랜잭션: 회원가입 시 User 테이블과 Shipper/Driver 테이블 생성은 반드시 한 트랜잭션(this.prisma.$transaction)으로 묶어야 데이터 무결성이 보장됩니다.

이 시스템은 차주의 모바일 앱에서 GPS 좌표를 주기적으로 전송하면, 서버가 이를 DB에 기록하고 해당 화물을 기다리는 화주에게 실시간으로 브로드캐스팅하는 구조입니다.


1. 위치 추적 게이트웨이 (Socket.io Gateway)

차주로부터 위치 데이터를 받고, 이를 특정 화주(Order Room)에게 전달하는 핵심 클래스입니다.

TypeScript
 
// src/location/location.gateway.ts
import {
  WebSocketGateway,
  SubscribeMessage,
  WebSocketServer,
  OnGatewayConnection,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { PrismaService } from '../prisma/prisma.service';

@WebSocketGateway({
  namespace: 'location',
  cors: { origin: '*' },
})
export class LocationGateway implements OnGatewayConnection {
  @WebSocketServer() server: Server;

  constructor(private prisma: PrismaService) {}

  handleConnection(client: Socket) {
    console.log(`Client connected: ${client.id}`);
  }

  // 1. 특정 주문 관전 모드 진입 (화주용)
  @SubscribeMessage('joinOrder')
  handleJoinOrder(client: Socket, orderId: string) {
    client.join(`order_${orderId}`);
    return { status: 'joined', room: `order_${orderId}` };
  }

  // 2. 차주가 위치 정보 전송
  @SubscribeMessage('updateLocation')
  async handleUpdateLocation(
    client: Socket,
    payload: { driverId: number; orderId: number; lat: number; lng: number },
  ) {
    const { driverId, orderId, lat, lng } = payload;

    // A. DB 업데이트 (차주의 현재 위치 최신화)
    await this.prisma.driver.update({
      where: { userId: BigInt(driverId) },
      data: {
        currentLat: lat,
        currentLng: lng,
        lastLocationAt: new Date(),
      },
    });

    // B. 실시간 브로드캐스팅 (해당 주문 방에 참여한 화주에게 전달)
    this.server.to(`order_${orderId}`).emit('locationChanged', {
      driverId,
      lat,
      lng,
      timestamp: new Date(),
    });
  }
}

2. 위치 기반 거리 계산 로직 (Service)

운송 종료 시 실제 주행 경로와 예상 경로를 비교하거나, 현재 위치에서 목적지까지의 **잔여 시간(ETA)**을 계산할 때 사용합니다.

TypeScript
 
// src/location/location.service.ts
@Injectable()
export class LocationService {
  // Haversine 공식을 이용한 두 좌표 간 직선 거리 계산 (단위: km)
  calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
    const R = 6371; // 지구 반지름
    const dLat = (lat2 - lat1) * (Math.PI / 180);
    const dLon = (lon2 - lon1) * (Math.PI / 180);
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(lat1 * (Math.PI / 180)) *
        Math.cos(lat2 * (Math.PI / 180)) *
        Math.sin(dLon / 2) *
        Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c;
  }
}

3. 클라이언트 구현 가이드 (차주 앱 & 화주 웹)

🚛 차주 모바일 앱 (Background Task)

차주가 '운송 시작' 버튼을 누르면, 배경화면에서도 1분마다 서버로 좌표를 쏩니다.

JavaScript
 
// React Native / Flutter 예시 로직
setInterval(() => {
  socket.emit('updateLocation', {
    driverId: myId,
    orderId: currentOrderId,
    lat: currentGPS.lat,
    lng: currentGPS.lng
  });
}, 60000); // 1분 간격

📦 화주 웹 화면 (Map Integration)

화주는 지도를 통해 트럭의 아이콘이 움직이는 것을 실시간으로 봅니다.

JavaScript
 
// Nuxt.js / Vue.js 예시 로직
socket.emit('joinOrder', orderId);

socket.on('locationChanged', (data) => {
  // 네이버/카카오 지도 API 마커 위치 업데이트
  myMapMarker.setPosition(new kakao.maps.LatLng(data.lat, data.lng));
});

💡 실시간 위치 추적 최적화 포인트

  1. 배터리 소모 고려: 차주 앱이 포그라운드(켜진 상태)일 때는 30초, 백그라운드일 때는 2~5분 단위로 전송 주기를 조절하는 로직이 필요합니다.
  2. 데이터 무결성: orders 테이블에 pickup_at(상차시간)과 delivery_at(하차시간)이 기록될 때의 최종 위치를 스냅샷으로 저장하여 향후 분쟁 시 증빙 자료로 활용하세요.
  3. Redis 연동: 사용자가 많아질 경우, 모든 위치 업데이트를 매번 MySQL DB에 저장하면 부하가 큽니다. 현재 위치는 Redis에 임시 저장하고, 일정 주기마다 DB에 배치 업데이트(Batch Update)하는 방식을 추천합니다.

이제 핵심 기능들이 거의 완성되었습니다. 마지막으로 '에스크로 결제 및 정산 시스템'을 마무리해 드릴까요? 아니면 관리자 대시보드 통계 기능을 보시겠어요?


이 시스템의 핵심은 화주가 입금한 돈을 플랫폼이 안전하게 예치(Escrow)하고, 운송이 완료된 후 **플랫폼 수수료(화주 3%, 차주 2%)**를 제외한 금액을 차주에게 자동으로 정산하는 것입니다.


1. 에스크로 결제 및 예치 로직 (Escrow & Payment)

화주가 가상계좌로 입금하거나 카드로 결제했을 때, 해당 주문을 '배차 확정' 상태로 변경하고 대금을 예치합니다.

TypeScript
 
// src/payments/payments.service.ts
import { Injectable, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class PaymentsService {
  constructor(private prisma: PrismaService) {}

  // 1. 화주 대금 결제 및 에스크로 예치
  async depositToEscrow(orderId: bigint, amount: number, paymentMethod: string) {
    return this.prisma.$transaction(async (tx) => {
      // 주문 확인
      const order = await tx.order.findUnique({ where: { id: orderId } });
      if (!order) throw new BadRequestException('주문을 찾을 수 없습니다.');

      // A. 결제 기록 생성
      const payment = await tx.payment.create({
        data: {
          orderId,
          shipperId: order.shipperId,
          amount,
          method: paymentMethod,
          status: 'completed', // PG사 Webhook 성공 가정
          paidAt: new Date(),
        },
      });

      // B. 에스크로 계좌 생성 및 예치
      await tx.escrowAccount.create({
        data: {
          orderId,
          totalAmount: amount,
          status: 'held', // 대금 보유 중
        },
      });

      // C. 주문 상태 업데이트 (입금 완료 -> 배차 대기/확정 가능 상태)
      await tx.order.update({
        where: { id: orderId },
        data: { status: 'dispatched' },
      });

      return payment;
    });
  }
}

2. 자동 정산 시스템 (Settlement with Fee Calculation)

운송이 완료되면 시스템 룰(pricing_rules)에 따라 수수료를 계산하고 차주에게 정산 예정 금액을 확정합니다.

TypeScript
 
// src/settlements/settlements.service.ts
@Injectable()
export class SettlementsService {
  constructor(private prisma: PrismaService) {}

  // 2. 운송 완료 후 차주 정산 실행
  async processSettlement(orderId: bigint) {
    return this.prisma.$transaction(async (tx) => {
      // 주문 및 에스크로 정보 조회
      const order = await tx.order.findUnique({
        where: { id: orderId },
        include: { escrowAccount: true }
      });

      if (order.status !== 'completed') throw new BadRequestException('운송이 완료되지 않았습니다.');

      // A. 수수료 계산 (화주 3%, 차주 2% - 기획안 기준)
      const totalAmount = Number(order.finalAmount);
      const platformFeeShipper = totalAmount * 0.03;
      const platformFeeDriver = totalAmount * 0.02;
      const settlementAmount = totalAmount - platformFeeDriver; // 차주에게 갈 순수 금액

      // B. 정산 테이블 기록 생성
      const settlement = await tx.settlement.create({
        data: {
          orderId,
          driverId: order.driverId,
          totalAmount: totalAmount,
          platformFee: platformFeeDriver,
          settlementAmount: settlementAmount,
          status: 'pending', // 실제 계좌 이체 전 단계
          scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // D+1 정산 예시
        },
      });

      // C. 에스크로 상태 업데이트
      await tx.escrowAccount.update({
        where: { orderId },
        data: { 
          status: 'released', // 대금 해제
          releasedAt: new Date() 
        },
      });

      return settlement;
    });
  }
}

3. 정산 내역 및 수익 통계 (Controller)

차주가 자신의 앱에서 이번 달 벌어들인 순수익과 공제된 수수료, 톨게이트비 등을 확인할 수 있는 엔드포인트입니다.

TypeScript
 
// src/settlements/settlements.controller.ts
@Controller('driver/settlements')
export class SettlementsController {
  constructor(private settlementService: SettlementsService) {}

  @Get('stats')
  async getMyStats(@Query('month') month: string, @Req() req) {
    const driverId = req.user.id;
    
    // 월간 수익 통계 쿼리
    const stats = await this.prisma.settlement.aggregate({
      where: {
        driverId,
        createdAt: { gte: new Date(`${month}-01`) },
        status: 'completed'
      },
      _sum: {
        settlementAmount: true,
        platformFee: true
      },
      _count: true
    });

    return {
      totalEarning: stats._sum.settlementAmount || 0,
      totalFeePaid: stats._sum.platformFee || 0,
      completedOrders: stats._count
    };
  }
}

🚀 시스템의 완성도 및 향후 과제

이로써 트럭커-프리의 핵심 백엔드 로직인 주문-입찰-인증-위치추적-정산이 모두 연결되었습니다.

  1. 세금계산서 자동화: 정산 완료 시 settlements 데이터를 기반으로 국세청 연동 API(예: 팝빌)를 통해 전자세금계산서를 자동 발행하는 기능을 추가할 수 있습니다.
  2. 분쟁 처리 시스템: 차주가 운송 완료를 눌렀으나 화주가 승인을 안 할 경우, GPS 데이터(drivers.last_location)를 대조하여 24시간 후 자동 정산되는 로직을 settlement_status_histories와 함께 구현하세요.
  3. 관리자 화면: admin_logs 테이블을 활용하여 정산 승인권자가 최종 이체 버튼을 누르는 관리 대시보드를 Nuxt.js로 연결하면 됩니다.

플랫폼의 운영 효율을 극대화할 관리자 통계 기능과 차주에게 최적의 일감을 매칭해주는 배차 알고리즘을 구현하겠습니다.

이 기능들은 축적된 데이터를 분석하여 화주에게는 빠른 배차를, 차주에게는 수익 극대화를 제공하는 플랫폼의 '두뇌' 역할을 합니다.


1. 관리자 대시보드 통계 (Statistics)

제공해주신 daily_statistics 테이블과 각 주문/결제 데이터를 활용하여 실시간 지표를 산출합니다.

TypeScript
 
// src/admin/admin-stats.service.ts
@Injectable()
export class AdminStatsService {
  constructor(private prisma: PrismaService) {}

  async getDashboardSummary() {
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    // 1. 주요 지표 요약 (병렬 처리)
    const [totalOrders, activeDrivers, totalRevenue] = await Promise.all([
      this.prisma.order.count({ where: { createdAt: { gte: today } } }),
      this.prisma.driver.count({ where: { user: { status: 'active' } } }),
      this.prisma.payment.aggregate({
        where: { status: 'completed', createdAt: { gte: today } },
        _sum: { amount: true }
      })
    ]);

    // 2. 수수료 수익 통계 (화주 3% + 차주 2%)
    const feeRevenue = await this.prisma.settlement.aggregate({
      where: { createdAt: { gte: today } },
      _sum: { platformFee: true }
    });

    return {
      todayOrderCount: totalOrders,
      activeDriverCount: activeDrivers,
      todayTransactionAmount: totalRevenue._sum.amount || 0,
      todayPlatformProfit: feeRevenue._sum.platformFee || 0
    };
  }

  // 3. 지역별 물동량 분석 (차트용 데이터)
  async getRegionalStats() {
    return this.prisma.order.groupBy({
      by: ['pickupAddress'],
      _count: { id: true },
      orderBy: { _count: { id: 'desc' } },
      take: 5
    });
  }
}

2. 최적 배차 우선순위 알고리즘 (Matching Algorithm)

단순 선착순이 아닌 거리, 평점, 차량 적합도를 종합하여 우선순위를 부여합니다. PremiumSubscription 테이블을 활용하여 유료 회원을 상단에 노출하는 로직도 포함합니다.

TypeScript
 
// src/orders/matching.service.ts
@Injectable()
export class MatchingService {
  constructor(private prisma: PrismaService, private locationService: LocationService) {}

  async getRecommendedDrivers(orderId: bigint) {
    const order = await this.prisma.order.findUnique({ where: { id: orderId } });
    
    // 1. 반경 20km 이내 활동 중인 차주 검색
    const nearbyDrivers = await this.prisma.driver.findMany({
      where: {
        isAvailable: true,
        vehicle: { type: order.vehicleType }, // 차종 매칭
        user: { status: 'active' }
      },
      include: { 
        user: { include: { premiumSubscriptions: { where: { status: 'active' } } } } 
      }
    });

    // 2. 가중치 기반 점수 산정 (Score Calculation)
    const scoredDrivers = nearbyDrivers.map(driver => {
      let score = 0;

      // A. 거리 점수 (가까울수록 높음)
      const dist = this.locationService.calculateDistance(
        Number(order.pickupLat), Number(order.pickupLng),
        Number(driver.currentLat), Number(driver.currentLng)
      );
      score += (20 - dist) * 2; 

      // B. 평점 점수 (평점 1점당 10점)
      score += Number(driver.averageRating) * 10;

      // C. 프리미엄 회원 가점 (최우선순위)
      if (driver.user.premiumSubscriptions.length > 0) {
        score += 50;
      }

      return { ...driver, matchScore: score };
    });

    // 3. 점수순 정렬 후 상위 10명에게 푸시 알림 전송 대상 선정
    return scoredDrivers.sort((a, b) => b.matchScore - a.matchScore).slice(0, 10);
  }
}

3. 향후 확장: 자동 요금 조정 (Dynamic Pricing)

기상 상황이나 수급 불균형 시 pricing_rules를 자동으로 업데이트하는 로직입니다.

  • 폭설/폭우 시: 기본 운임의 1.2배 적용
  • 심야 시간(22시~04시): 할증료 자동 합산
  • 배차 지연 시: 30분 이상 배차가 안 될 경우 화주에게 운임 증액 제안 팝업 노출

🏁 트럭커-프리 개발 요약

구분 핵심 기술 구현된 가치
DB MySQL 8.0 + Prisma 30개 테이블의 유기적 관계 및 데이터 무결성
인증 SMS + JWT + RefreshToken 휴대폰 기반의 간편하고 안전한 본인인증
매칭 가중치 알고리즘 + 역경매 프리미엄 회원 우대 및 시장가 반영
실시간 Socket.io + GPS 화주에게 배송 안심 서비스 제공
정산 에스크로 + 자동 수수료 계산 깜깜이 수수료 제거 및 정산 투명성 확보

이제 이 백엔드 코드를 Nuxt.js 프론트엔드와 연결하시면 실무급 화물 플랫폼 서비스가 완성됩니다.

 

 

728x90