
먼저 현황을 파악하기 위해 웹 검색을 진행하겠습니다.좋은 자료 감사합니다. 현재 화물 운송 플랫폼의 핵심 문제점을 파악했습니다. 제시하신 기획안을 분석하고 개선된 기획안을 작성하겠습니다.
📦 기획안: "트럭커-프리(Trucker-Free)"
투명한 화물 거래 생태계 플랫폼
🎯 1. 핵심 문제점 분석
현재 플랫폼의 주요 문제:
- 깜깜이 수수료 (10~40% 불투명한 중간 마진)
- Pay-to-Work 구조 (월 27,500원 고정 앱 사용료)
- 불량 업체 방치 (신고해도 플랫폼이 방치)
- 운임 미수 사고 (월 2~3건 발생)
- 법적 책임 회피 (정보통신사업자로만 등록)
- 화주-차주 직접 소통 불가
🚀 2. 개선된 핵심 기능
2.1 완전 투명한 수수료 시스템
✅ 수정 사항:
- 수수료 실시간 공개: 모든 거래에서 플랫폼이 가져가는 수수료를 거래 전에 명시
- 고정 수수료율: 화주 측 3%, 차주 측 2% (총 5%로 고정)
- 수수료 분석 대시보드: 월별 수수료 내역 자동 제공
- 중개업체 금지: 직거래만 가능하도록 시스템 설계
💡 추가 기능:
[거래 화면 예시]
━━━━━━━━━━━━━━━━━━━━━
운임 총액: 500,000원
━━━━━━━━━━━━━━━━━━━━━
차주 수령액: 490,000원 (98%)
플랫폼 수수료: 10,000원 (2%)
━━━━━━━━━━━━━━━━━━━━━
화주 지불액: 515,000원
플랫폼 수수료: 15,000원 (3%)
━━━━━━━━━━━━━━━━━━━━━
2.2 개선된 입찰 시스템
✅ 기존 기획안 보완:
- 역경매 + 즉시 매칭 하이브리드
- 긴급 화물: 즉시 매칭 (AI 추천)
- 일반 화물: 역경매 (24시간 입찰 기간)
- 정기 화물: 장기 계약 (월 단위 고정가)
- 스마트 입찰 도우미
- 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개월):
- 역경매 시스템
- 투명 수수료
- 에스크로 결제
- 별점 시스템
Phase 2 (6개월):
- AI 비용 계산기
- 위치 추적
- 커뮤니티
- 보증금 시스템
Phase 3 (9개월):
- 금융 서비스
- 데이터 분석
- API 개방
- 제휴 네트워크
트럭커-프리(Trucker-Free) 데이터베이스 설계 문서
📋 목차
🎯 개요
기술 스택
- 데이터베이스: MySQL 8.0+
- ORM: Prisma
- 백엔드: NestJS
- 프론트엔드: Nuxt.js
데이터베이스 구조
총 30개 테이블로 구성되며, 크게 10개 영역으로 분류됩니다:
- 사용자 관리 (4개 테이블)
- 주문 관리 (4개 테이블)
- 결제 및 정산 (4개 테이블)
- 평가 시스템 (1개 테이블)
- 블랙리스트 (1개 테이블)
- 채팅 및 알림 (3개 테이블)
- 시스템 관리 (6개 테이블)
- 통계 분석 (1개 테이블)
- 인증 관리 (2개 테이블)
- 프리미엄 서비스 (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 데이터베이스 생성
mysql -u root -p
CREATE DATABASE truckersfree CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE truckersfree;
SOURCE truckersfree_db_schema.sql;
2단계: NestJS 프로젝트 설정
# 프로젝트 루트에 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 프론트엔드 개발
# API 엔드포인트 연결
- 관리자: /admin/*
- 화주: /shipper/*
- 차주: /driver/*
📱 화면 구성 (3개)
🔧 관리자 화면
- 전체 주문 현황 모니터링
- 화주/차주 인증 승인
- 블랙리스트 관리
- 통계 대시보드
- 문의/신고 처리
📦 화주 화면
- 화물 주문 등록
- 입찰 현황 확인
- 차주 선택 (평점 기반)
- 실시간 배송 추적
- 결제 및 리뷰 작성
🚛 차주 화면 (모바일 최적화)
- 배차 가능 주문 조회
- 입찰 참여
- 운송 진행 상태 업데이트
- GPS 위치 전송
- 정산 내역 확인
💡 다음 단계 추천
- 백엔드 API 개발 (NestJS)
- 인증 모듈 (JWT + SMS)
- 주문 관리 모듈
- 입찰 시스템 모듈
- 결제 연동 (PG사)
- 프론트엔드 개발 (Nuxt.js)
- 관리자 대시보드
- 화주 웹 화면
- 차주 모바일 앱
- 추가 기능 개발
- 실시간 채팅 (Socket.io)
- 푸시 알림 (FCM)
- 지도 API 연동 (카카오맵/네이버맵)
- 자동 요금 계산 API
-- ================================================
-- 트럭커-프리(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를 사용하기 위한 공통 모듈을 설정합니다.
// 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에 저장하는 로직입니다.
// 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)**이 함께 노출되는 구조입니다.
// 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 트리거가 평점을 계산하지만, 백엔드에서 블랙리스트 기반 접근 제어를 수행합니다.
// 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;
}
}
🚀 다음 개발 단계 가이드
이제 백엔드의 핵심 로직이 준비되었습니다. 이어서 진행할 작업은 다음과 같습니다.
- Auth 모듈 완성: sms_verifications 테이블을 활용한 휴대폰 본인인증 및 JWT 발급 로직 구현.
- 결제(Escrow) 연동: 가상계좌 발급 시 escrow_accounts에 입금 확인 후 주문을 dispatched로 변경하는 Webhook 연동.
- 실시간 위치(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)
회원가입 및 로그인 시 휴대폰 번호로 인증번호를 발송하고 확인하는 핵심 로직입니다.
// 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 테이블에서 관리합니다.
// 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)에 따라 추가 정보를 생성합니다.
// 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입니다.
// 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
}
}
💡 보안 강화 팁
- 비밀번호 암호화: bcrypt를 사용하여 password_hash를 저장하세요.
- SMS 어뷰징 방지: 동일 번호에 대해 1분 이내 재요청 시 차단하는 로직을 sms_verifications 테이블의 createdAt을 조회해 추가하는 것이 좋습니다.
- 트랜잭션: 회원가입 시 User 테이블과 Shipper/Driver 테이블 생성은 반드시 한 트랜잭션(this.prisma.$transaction)으로 묶어야 데이터 무결성이 보장됩니다.
이 시스템은 차주의 모바일 앱에서 GPS 좌표를 주기적으로 전송하면, 서버가 이를 DB에 기록하고 해당 화물을 기다리는 화주에게 실시간으로 브로드캐스팅하는 구조입니다.
1. 위치 추적 게이트웨이 (Socket.io Gateway)
차주로부터 위치 데이터를 받고, 이를 특정 화주(Order Room)에게 전달하는 핵심 클래스입니다.
// 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)**을 계산할 때 사용합니다.
// 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분마다 서버로 좌표를 쏩니다.
// React Native / Flutter 예시 로직
setInterval(() => {
socket.emit('updateLocation', {
driverId: myId,
orderId: currentOrderId,
lat: currentGPS.lat,
lng: currentGPS.lng
});
}, 60000); // 1분 간격
📦 화주 웹 화면 (Map Integration)
화주는 지도를 통해 트럭의 아이콘이 움직이는 것을 실시간으로 봅니다.
// Nuxt.js / Vue.js 예시 로직
socket.emit('joinOrder', orderId);
socket.on('locationChanged', (data) => {
// 네이버/카카오 지도 API 마커 위치 업데이트
myMapMarker.setPosition(new kakao.maps.LatLng(data.lat, data.lng));
});
💡 실시간 위치 추적 최적화 포인트
- 배터리 소모 고려: 차주 앱이 포그라운드(켜진 상태)일 때는 30초, 백그라운드일 때는 2~5분 단위로 전송 주기를 조절하는 로직이 필요합니다.
- 데이터 무결성: orders 테이블에 pickup_at(상차시간)과 delivery_at(하차시간)이 기록될 때의 최종 위치를 스냅샷으로 저장하여 향후 분쟁 시 증빙 자료로 활용하세요.
- Redis 연동: 사용자가 많아질 경우, 모든 위치 업데이트를 매번 MySQL DB에 저장하면 부하가 큽니다. 현재 위치는 Redis에 임시 저장하고, 일정 주기마다 DB에 배치 업데이트(Batch Update)하는 방식을 추천합니다.
이제 핵심 기능들이 거의 완성되었습니다. 마지막으로 '에스크로 결제 및 정산 시스템'을 마무리해 드릴까요? 아니면 관리자 대시보드 통계 기능을 보시겠어요?
이 시스템의 핵심은 화주가 입금한 돈을 플랫폼이 안전하게 예치(Escrow)하고, 운송이 완료된 후 **플랫폼 수수료(화주 3%, 차주 2%)**를 제외한 금액을 차주에게 자동으로 정산하는 것입니다.
1. 에스크로 결제 및 예치 로직 (Escrow & Payment)
화주가 가상계좌로 입금하거나 카드로 결제했을 때, 해당 주문을 '배차 확정' 상태로 변경하고 대금을 예치합니다.
// 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)에 따라 수수료를 계산하고 차주에게 정산 예정 금액을 확정합니다.
// 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)
차주가 자신의 앱에서 이번 달 벌어들인 순수익과 공제된 수수료, 톨게이트비 등을 확인할 수 있는 엔드포인트입니다.
// 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
};
}
}
🚀 시스템의 완성도 및 향후 과제
이로써 트럭커-프리의 핵심 백엔드 로직인 주문-입찰-인증-위치추적-정산이 모두 연결되었습니다.
- 세금계산서 자동화: 정산 완료 시 settlements 데이터를 기반으로 국세청 연동 API(예: 팝빌)를 통해 전자세금계산서를 자동 발행하는 기능을 추가할 수 있습니다.
- 분쟁 처리 시스템: 차주가 운송 완료를 눌렀으나 화주가 승인을 안 할 경우, GPS 데이터(drivers.last_location)를 대조하여 24시간 후 자동 정산되는 로직을 settlement_status_histories와 함께 구현하세요.
- 관리자 화면: admin_logs 테이블을 활용하여 정산 승인권자가 최종 이체 버튼을 누르는 관리 대시보드를 Nuxt.js로 연결하면 됩니다.
플랫폼의 운영 효율을 극대화할 관리자 통계 기능과 차주에게 최적의 일감을 매칭해주는 배차 알고리즘을 구현하겠습니다.
이 기능들은 축적된 데이터를 분석하여 화주에게는 빠른 배차를, 차주에게는 수익 극대화를 제공하는 플랫폼의 '두뇌' 역할을 합니다.
1. 관리자 대시보드 통계 (Statistics)
제공해주신 daily_statistics 테이블과 각 주문/결제 데이터를 활용하여 실시간 지표를 산출합니다.
// 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 테이블을 활용하여 유료 회원을 상단에 노출하는 로직도 포함합니다.
// 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 프론트엔드와 연결하시면 실무급 화물 플랫폼 서비스가 완성됩니다.
'IT 개발,관리,연동,자동화' 카테고리의 다른 글
| 서랍 속 안드로이드 폰의 화려한 변신: 공인 IP 없이도 가능한 나만의 웹 서버 구축하기 (3) | 2026.03.18 |
|---|---|
| Windsurf: 혁신적인 AI 코드 에디터의 주요 특징과 장점 (1) | 2026.03.12 |
| 편리함이라는 이름의 함정: 화물 운송 플랫폼이 감춘 '잔인한' 진실 (0) | 2026.02.27 |
| 신재생 에너지 발전소의 효율적인 통합 관리앱 만들기 (0) | 2026.02.10 |
| 키오스크 앞에 줄 서는 시대는 끝났다: 내 스마트폰이 '주문 매니저'가 되는 세상 (0) | 2026.02.08 |