2차 프로젝트 테이블 설계 - ORDERS (주문) 테이블 설명
구조 자체는 크게 문제 없지만, 운영 상황을 고려하면 몇 가지 개선 포인트가 존재합니다.
지금 설계는 깔끔하지만 실서비스 쇼핑몰 기준으로 빠지거나 위험한 부분이 있어요.
✅ 좋았던 부분
| 항목 | 평가 |
|---|---|
orders.id 를 payments.order_id 와 동일하게 유지 → 참조성 OK |
👍 좋은 선택 |
status ENUM 으로 주문 단계 관리 |
👍 기본 설계 OK |
payments 과 1:N 관계 정의 |
👍 실제 결제 재시도 케이스 대응됨 |
❗ 개선이 필요한 부분
1. orders.total_amount 값은 "재계산이 아닌 고정 Snapshot" 이 되어야 함
지금 설명은
"상품 가격 합계 = 결제 금액과 동일해야 한다"
라고 되어 있는데,
⚠️ 운영 현실에서는 결제 금액과 주문 상품 subtotal 은 항상 같지 않습니다.
예시:
| 항목 | 값 |
|---|---|
| 상품 가격 총합 | ₩100,000 |
| 쿠폰 할인 | -₩10,000 |
| 프로모션(첫 구매 할인) | -₩5,000 |
| 배송비 | +₩3,000 |
| 최종 결제 금액 | ₩88,000 |
👉 이 경우:
-
orders.total_amount= ₩88,000 -
orders.order_items.subtotal_sum= ₩100,000 -
할인, 배송비, 포인트 내역은 별도 컬럼 또는 테이블로 저장됨
따라서 "소계(subtotal)" 와 "최종 결제 금액(total_amount)" 는 반드시 분리 저장해야 함.
💡 (추천)
total_amount → 사용자가 최종 결제한 금액
subtotal_amount → 상품 원가 기준 총합
discount_amount → 총 할인금액
shipping_fee → 배송비
2. status ENUM은 운영 중 스케일에 불리함
🚫 ENUM은 상태가 늘어날 때 ALTER TABLE 필요 → 운영 중 문제.
✔ 추천: status는 ENUM 대신 별도 테이블 + VARCHAR(20) 추천.
3. 주문 ID를 VARCHAR(64) 로 하는 것은 가능하지만 Best Practice는 아님
보통:
| 필드 | 용도 |
|---|---|
| auto_increment BIGINT | 내부 PK, 조인, 인덱스 최적화 |
| order_number VARCHAR(30~64) | 결제사·사용자 UX용 주문번호 (UUID 가능) |
→ 인덱싱 비용이 덜하고 join 성능이 훨씬 좋아짐.
📌 개선된 추천 스키마 예시
CREATE TABLE orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_number VARCHAR(64) NOT NULL UNIQUE COMMENT '사용자/결제사 주문번호',
user_id BIGINT UNSIGNED NOT NULL,
subtotal_amount DECIMAL(10,2) NOT NULL COMMENT '상품 원가 합계',
discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '총 할인 금액',
shipping_fee DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '배송비',
total_amount DECIMAL(10,2) NOT NULL COMMENT '실제 결제 금액',
status VARCHAR(20) NOT NULL DEFAULT 'CREATED',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY (order_number),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
📌 결론
| 항목 | 현재 설계 | 개선 필요 여부 |
|---|---|---|
| 주문 - 결제 관계 (1:N) | ✔ 완벽 | 유지 |
| 주문 총금액 저장 | ⚠ 결제 금액 ≠ 상품 총액 고려 부족 | 수정 필요 |
| ENUM 상태값 | ⚠ 운영 확장성 문제 | VARCHAR + state table 추천 |
| 주문 ID 설계 | ⚠ 조인 성능 개선 여지 | 내부 PK + 외부 order_number 분리 추천 |
다음 질문 예상 방향
-
order_items설계? -
취소/환불 로직?
-
부분환불 / 재승인 / 재배송 고려?
-
inventory (stock)스냅샷 처리 방식?
원하면 다음 단계로
📍 order_items + stock snapshot + refund 테이블 구조 설계까지 이어서 설계해줄게.
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_number VARCHAR(64) NOT NULL UNIQUE COMMENT '사용자/결제사 주문번호', 이게 두개인이유는? id로 하나로 결제사로보내는 주문번호로 하면 안됨?
좋은 질문이고 대부분 쇼핑몰 개발자가 한 번씩 헷갈리는 지점이야.
결론부터 말하면:
id는 내부 시스템 식별자이고,order_number는 외부(사용자, 관리자, PG, 송장 등)에 노출되는 식별자라서 역할이 다르기 때문에 둘 다 필요해.
📌 왜 id 하나만 쓰면 안 되나요?
이유 1️⃣: id는 단순히 DB용 숫자이며 변경하면 안 되는 값
-
자동 증가 (AUTO_INCREMENT)
-
보안성 없음 (규칙성 때문에 추측 가능)
-
외부 시스템(PG, 고객, CS 등)에 노출되면 위험
예:
https://shop.com/order?id=10001
→ 다른 사용자 주문 번호 바꿔 넣으면 상대 주문 정보 보일 가능성 (취약점: IDOR)
이유 2️⃣: order_number는 외부 규격에 맞춘 포맷이 필요함
PG사, 배송 시스템, ERP 등 외부 규격은 대부분 숫자만 있는 ID를 허용하지 않거나 제한됨.
예시 구조:
| 서비스 | 주문번호 예시 |
|---|---|
| 네이버 스마트스토어 | 2025-11-00001234 |
| 쿠팡 | O20251201-293948 |
| Shopify | #1008 |
| Amazon | 112-8549248-9393023 |
➡ 이런 번호들은:
-
날짜가 포함
-
보안적 난수 포함
-
규칙적이지 않음
-
사용자에게 읽기 쉬움
이유 3️⃣: 주문번호는 비즈니스적으로 유의미한 정보 포함 가능
예:
ORDER-20251201-000129
이 포맷을 쓰면:
| 의미 | 포함 여부 |
|---|---|
구분 prefix (ORDER-) |
YES |
| 연도/월/일 | YES |
| 내부 시퀀스 | YES |
➡ 나중에 통계/백오피스에서 조회 & 인덱싱 쉬움
이유 4️⃣: 시스템 마이그레이션/샤딩 가능성
id는 연속 증가라:
데이터베이스 sharding 또는 region 분리 시 충돌 발생 100%
그러나 order_number 는 예를 들어:
| 샤드 | 포맷 |
|---|---|
| KR | KR-20251201-38191 |
| JP | JP-20251201-23302 |
➡ 글로벌 커머스로 확장될 때 필수.
🎯 최종 정리
| 항목 | id |
order_number |
|---|---|---|
| 목적 | 내부 DB 식별자 | 사용자/PG/배송/ERP 외부 공개용 |
| 노출 여부 | ❌ 외부 노출 금지 | ⭕ 외부 노출 |
| 유저가 조회할 수 있음? | X | O |
| 형식 | 단순 정수 증가 | 규칙/랜덤/문자 포함 가능 |
| 유지 성격 | 기술적 | 비즈니스적 |
👉 그래서 두 개가 있는 게 업계 표준이고, 네가 만들고 있는 구조도 정확히 그 방향이 맞아.
원하면 다음으로:
-
order_number생성 규칙 추천 3가지 + UUID 비교 -
샤딩 고려한 주문번호 패턴
-
글로벌 PG(Stripe/PayPal/NicePay/Toss) 요구 규격 비교
까지 정리해줄게.
진짜 이 정도 고민이면 백엔드 커머스 설계 제대로 하고 있는 거 맞음. 👍
추가 설명
카카오페이 토스페이 api에서 오는게 주문번호랑 트랜잭션 아이디인가?
- 주문번호 (partner_order_id / orderId): 이 값은 가맹점(쇼핑몰 등)에서 직접 생성하여 결제 요청 시 API로 보내는 고유한 값입니다. 각 주문을 식별하는 데 사용됩니다.
- 트랜잭션 아이디:
- 카카오페이에서는
tid(결제 고유번호)라는 파라미터로 응답받습니다. - 토스페이먼츠에서는 결제 승인 시
paymentKey라는 파라미터로 응답받으며, 이 값이 결제를 식별하는 중요한 고유 키 역할을 합니다. (과거 토스페이 연동 가이드에서는payToken또는transactionId를 사용하기도 했습니다).
- 카카오페이에서는
맞아. 정확하게 이해한 거야.
order_number는 **카카오페이, 토스페이, 네이버페이, 페이코 등 PG사에 전달해야 하는 “가맹점 주문번호”**야.
즉,
PG사에 결제 요청을 보낼 때 JSON 바디 안에 포함되는 우리의 주문 식별자(merchant_uid, orderId, partner_order_id 같은 역할)
🔍 PG사 기준 비교
| PG / 결제사 | 필드명 | 의미 | 요구사항 |
|---|---|---|---|
| 카카오페이 | partner_order_id |
가맹점 주문번호 | 🔸고유해야 함 |
| 토스페이 | orderId |
주문 식별자 | 🔸중복 불가 |
| 페이코 | orderNo |
외부 주문번호 | 🔸고유해야 함 |
| 네이버페이 | merchantPayKey |
상점 주문번호 | 🔸중복 불가 |
| 아임포트 (중간 운영) | merchant_uid |
가맹점 주문번호 | 🔸고유필수 |
➡ 즉 모든 PG는 공통적으로 가맹점 생성 주문번호를 앞단에서 요구해.
🚩 왜 직접 생성해야 할까?
이유는 세 가지:
1️⃣ 결제 중단/재시도 고려
PG는 이런 상황이 생길 수 있음:
-
결제 요청 → 인증 페이지로 이동 → 유저 취소
-
첫 요청 실패 후 재시도
이때 PG에서 orderId 생성하면, 결제 실패→DB 매칭 불가 문제 발생함.
그래서 결제 시도 전에 주문번호는 이미 존재해야 함 → 즉 우리가 만들어야 함.
2️⃣ PG사에서 결제 완료 후 콜백(webhook) 들어올 때 매핑키로 사용
{
"orderId": "ORDER-20251201-91823",
"status": "DONE",
"transactionId": "tx_19293"
}
이걸 받고 DB에서 주문을 찾으려면?
WHERE order_number = 'ORDER-20251201-91823'
→ 이렇게 찾아야 함.
(만약 id만 외부에 노출하면 보안 문제 + 번호 추측 가능)
3️⃣ 추후 플랫폼 확장 고려 (배송사·CS·ERP·재결제 등)
많은 SaaS/OMS 시스템은 주문 번호를 외부 통일된 기준으로 삼음.
예:
-
운송장 생성 API → 주문번호 필요
-
CS센터 상담 기록 → 주문번호 기준 조회
-
ERP(Odoo, SAP, 더존) → 주문번호 매핑
➡ order_number는 비즈니스 키(Business Key) 역할
🔧 주문번호 생성 포맷 추천
✅ 실무 표준 포맷 예시
{PREFIX}-{YYYYMMDD}-{RANDOM6}
ORDER-20251201-928301
또는
NPAY-20251201-000129 (네이버페이 요청 시)
TOSS-20251201-451920 (토스페이 요청 시)
혹은 PG 무관 공통:
ORD-2025-12-01-000123
👍 결론
✔ order_number는 DB 내부키가 아니라
➡ 외부 결제사·고객·CS·배송·ERP용의 공개 주문번호
✔ 따라서 생성 시점은:
주문 생성 → 결제 요청 전 → PG로 전달 → 나중에 callback matching
✔ 그리고 DB에 UNIQUE 저장해야 하는 게 맞아.
원하면 다음 단계로:
-
order_number생성 함수 (Node.js, Spring, NestJS 버전) -
결제 실패/재시도 정책
-
UUID vs Sequence vs Hybrid 비교
오더 테이블 개선
오더 테이블 개선
근대 오더테이블에 fk몰아넣는 설계는 안좋아? 왜 테이블마다 오더테이블 id를 fk로넣어?
데이터베이스 설계에서 각 테이블이 자신의 역할에 충실하도록 설계하는 것이 일반적인 관행이며, 이것이 바로 payments와 deliveries 테이블에 order_id를 외래 키(FK)로 두는 이유입니다. [1]orders 테이블에 모든 외래 키를 몰아넣는 설계는 권장되지 않습니다.왜 order_id를 각 테이블에 FK로 넣는가?각 테이블에 order_id를 외래 키로 설정하면, 각 테이블은 자신의 정보(결제 정보, 배송 정보)가 어떤 주문에 속하는지 명확하게 알 수 있습니다. 이는 데이터의 응집성을 높이고 독립적인 관리를 가능하게 합니다.payments 테이블: 이 테이블은 오직 결제 기록만 관리합니다. order_id를 통해 "이 결제는 A 주문 건이다"라고 참조합니다.deliveries 테이블: 이 테이블은 오직 배송 정보만 관리합니다. order_id를 통해 "이 배송은 A 주문 건이다"라고 참조합니다.왜 orders 테이블에 FK를 몰아넣는 설계가 안 좋은가?만약 orders 테이블에 payment_id나 delivery_id 같은 외래 키를 넣게 되면, 다음과 같은 문제점이 발생할 수 있습니다.1. 1:N 관계 처리의 어려움 (가장 큰 이유)주문과 결제는 항상 1:1 관계가 아닙니다.주문 1 : N 결제 시도: 한 주문에 대해 결제가 여러 번 시도될 수 있습니다 (실패 후 재시도). payments 테이블은 이 모든 시도(여러 개의 레코드)를 저장해야 합니다.orders 테이블에 단 하나의 payment_id FK 필드만 있다면, 성공한 최종 결제 건만 참조할 수 있고, 실패한 결제 내역들은 orders 테이블과 연결할 방법이 사라집니다.2. 데이터 관리의 복잡성 및 비효율성결제/배송 전 정보: 주문이 생성될 때는 결제 정보나 배송 정보가 존재하지 않습니다. 이 경우 orders 테이블의 FK 필드는 NULL이 됩니다.결제/배송 후 업데이트: 결제나 배송이 완료된 후에야 orders 테이블로 돌아와서 FK 필드를 업데이트해야 합니다. 이 과정은 데이터 흐름을 복잡하게 만듭니다.조회의 비효율성: 특정 주문의 결제 내역을 조회할 때, payments 테이블에서 order_id를 기준으로 찾으면 되지만, FK를 몰아넣으면 특정 결제 ID를 들고 다시 payments 테이블로 가야 합니다.3. 테이블 간의 독립성 저해 (강한 결합)orders 테이블이 payments와 deliveries 테이블의 구조에 종속됩니다. 만약 배송 시스템이 바뀌어 배송 ID가 여러 개가 된다면 orders 테이블의 스키마를 수정해야 합니다.현재 설계에서는 각 테이블이 독립적으로 존재하며, 필요할 때만 order_id를 통해 서로 조인(JOIN)하여 데이터를 가져옵니다.결론
각 테이블에 order_id를 외래 키로 두는 것은 테이블 간의 결합도를 낮추고(느슨한 결합), 데이터의 유연성을 높이는 표준적이고 좋은 설계 방식입니다. 각 테이블이 자신의 도메인 지식에 충실할 수 있도록 합니다.
좋아, 이 질문 정확히 짚은 거야.
그리고 너가 만든 스키마 둘 중 하나가 틀린 게 아니라, 역할을 어떻게 나누느냐의 문제야.
✅ 핵심 결론
배송비는 주문 테이블(
orders.shipping_fee)에도 들어가야 하고,
배송 테이블(deliveries.final_shipping_fee)에도 들어가야 한다.
단, 의미가 다르다.
📌 왜 두 군데에 저장해야 함?
| 저장 위치 | 필드명 | 의미 | 변경 가능 여부 |
|---|---|---|---|
orders.shipping_fee |
결제 당시 배송비 | 결제 금액에 포함되는 스냅샷 | ❌ 수정되면 안 됨 |
deliveries.final_shipping_fee |
실제 배송 정책에 따라 조정된 배송비 | 지역/재배송/변경 같은 사후 변경 가능 | ⭕ 가능 |
👉 예시로 이해해보자
| 상황 | 금액 |
|---|---|
| 기본 배송비 | 3,000원 |
| 제주도 배송 추가비 | +5,000원 |
| 실제 배송비 | 8,000원 |
👉 주문 시 결제 화면에서 보인 배송비 8,000원 → orders.shipping_fee (변하지 않음)
👉 이후 배송 중 주소 변경으로 문제 생겨서 추가 배송비 발생해 3,000원 더 발생
→ deliveries.final_shipping_fee 를 11,000원으로 업데이트 가능
그러나 결제 금액(total_amount)은 변하지 않음.
즉,
orders.shipping_fee는 회계/결제 기준
deliveries.shipping_fee는 물류/정산 기준
❓그럼 뭐가 진실인가?
orders = 사용자와 PG 결제 기준 (불변 기록)
deliveries = 실제 배송 비용 정보 (상황에 따라 변경 가능)
💡 만약 배송비가 주문 시 확정이고 이후 변경될 가능성 없다면?
➡ 배송비는 orders 에만 넣고 deliveries 테이블에서는 제거해도 됨.
하지만 현실적으로 전자상거래 물류에서는 변경 가능성 있음.
(특히 도서산간, 묶음배송, 택배사 재측정 등)
그래서 기획 기준:
| 서비스 유형 | 배송비 컬럼 유지 방식 |
|---|---|
| 쿠팡, 스마트스토어급 | orders + deliveries 둘 다 |
| 단순 쇼핑몰 | orders.shipping_fee만 |
🔧 결론적 설계 추천
orders.shipping_fee -- 결제에 포함된 최종 확정 금액 (불변)
deliveries.final_shipping_fee -- 실제 배송 운임 (변경 가능)
마지막 정리
📌 결제에 포함되려면 반드시 orders 테이블에 있어야 한다.
📌 deliveries 의 배송비는 실제 물류/정산을 위해 별도로 기록되는 값이다.
📌 두 값이 다른 경우가 생길 수 있다 → 그래서 둘 다 필요할 수 있다.
원하면 다음 답변에:
-
📦
refund처리 모델까지 포함한 전체 ERD 파이널 버전 -
🧾 실제 운영되는 커머스 DB 예시 (쿠팡/배민/네이버 비교 구조)
까지 정리해줄게.
오더랑 페이먼트 테이블 하나로 못합침?
가능해. 이론적으로는 orders 와 payments 를 하나로 합칠 수 있어.
근데 “합칠 수 있다”와 “합치는 게 맞다”는 완전히 다른 이야기야.
결론부터 딱 말하면:
쇼핑몰/PG 결제 도메인 기준으로 orders 와 payments 를 분리하는 게 정석이고, 합치는 건 소규모/단일 결제 방식에서만 고려 가능.
특히 카카오페이/토스페이/여러 결제수단 + 재결제/환불/취소 존재하면 절대 합치면 안 됨.
🧨 왜 하나로 합치면 위험할까?
❌ 1. 주문 1개에 결제는 여러 번 시도될 수 있음
예:
| 상황 | 상태 |
|---|---|
| 첫 결제 실패 | 카드 한도 초과 |
| 두 번째 결제 성공 | 토스로 변경 후 성공 |
→ 주문(order)은 하나, 결제(payment)는 N개 발생
→ 테이블을 합치면 모델이 깨짐 (1:N 표현 불가)
❌ 2. 결제는 주문과 별도 상태 머신을 가짐
| 구분 | 상태 흐름 |
|---|---|
| 주문 | CREATED → PAID → SHIPPED → DELIVERED → COMPLETED |
| 결제 | INIT → AUTHORIZED → APPROVED → CANCELED → REFUNDED |
→ 서로 상태가 다르기 때문에 한 테이블에 넣으면 상태 컬럼이 혼종됨.
❌ 3. 결제는 외부 시스템(카카오/토스/PG)과 매핑되어야 함
결제에는 이런 정보가 필요하지:
| 필드 예시 |
|---|
| PG transaction id |
| 승인번호 |
| 결제 수단 (카드/계좌/포인트/간편결제) |
| 결제 영수증 URL |
| 재시도 로그 |
| 취소/환불 기록 |
🟥 이 값들은 주문 테이블에 포함되면 전혀 정규화되지 않은 “빅 테이블 악몽”이 됨.
❌ 4. 환불/취소 정책이 결제 단위
예:
-
상품 3개 주문
-
하나만 환불
-
PG 취소 금액 = 부분 환불
→ 주문(order)은 유지
→ 결제(payment)만 cancel / partial refund 발생
➡ 구조적으로 분리해야 표현 가능.
🟢 언제 하나로 합쳐도 되냐?
| 조건 | 가능 여부 |
|---|---|
| 현금만 받는 매장형 시스템 | 가능 |
| PG 연결 없음 | 가능 |
| 결제 실패/재시도 없음 | 가능 |
| 환불이 “자동이 아닌 오프라인 처리” | 가능 |
| 단순 구독 시스템 (Stripe 기반 자동 billing) | 경우에 따라 가능 |
➡ 즉 결제가 단순하고 정책이 없는 MVP라면 합쳐도 된다.
🔥 지금 네가 만드는 시스템 기준
-
카카오페이 / 토스 / 외부 결제사
-
환불 기능 고려
-
배송, ERP, 재시도, 결제 이력 필요
-
order_number 확정
-
배송비 포함 금액 결제
👉 = 정답: 분리해야 함.
최종 구조 요약
USER
│
ORDER (1)
│ \
│ \
ORDER_ITEMS PAYMENT (N)
│
→ REFUND (optional)
결론
| 질문 | 답변 |
|---|---|
| 하나로 합칠 수 있나요? | ⭕ 가능은 함 |
| 현업에서 합치나요? | ❌ 거의 안 함 |
| 네가 만들 시스템에 적합한가? | ❌ 확장성과 결제정책 때문에 비추천 |
원하면 다음으로:
-
ERD 결제 시나리오(실패/재승인/환불 포함) 플로우 차트
-
테이블 설계 최종 리팩터링
-
이벤트 소싱 방식 결제 로그 설계
까지 이어서 정리해줄게.
계속할까? 😎
refund_price ->주문하나 안의 누적 환불된 금액 order_status->주문 진행 상태 order_status가 결제 성공 시 PAID, 환불 요청이면 CANCEL_REQUESTED, 환불 완료면 REFUNDED.('PENDING','PAID','CANCEL_REQUESTED','CANCELLED','SHIPPED','DELIVERED','REFUNDED') 컬럼두개 오더테이블에 넣으면 되나?
좋아, 이제 질문 요지는:
orders 테이블에
refund_price와order_status두 컬럼을 넣으면 환불 및 주문 상태 관리가 충분한가?
정확히 따져보자.
1️⃣ refund_price 컬럼
-
의미: 주문 단위로 누적 환불 금액
-
장점: 주문 조회 시 바로 결제 대비 환불 금액 계산 가능
-
주의점: 부분 환불, 여러 결제 건, 배송비 환불 고려 필요
예:
| 주문금액 | 환불 내역 | 누적 환불 (refund_price) |
|---|---|---|
| 50,000 | 상품B 20,000 환불 | 20,000 |
| 50,000 | 배송비 5,000 환불 | 25,000 |
✔ orders.refund_price에 누적 합산 가능
❌ 하지만 환불 세부 내역은 refunds 테이블에 따로 있어야 함
→ 환불 내역 추적/재계산/정산용
2️⃣ order_status 컬럼
제안한 ENUM:
'PENDING', 'PAID', 'CANCEL_REQUESTED', 'CANCELLED', 'SHIPPED', 'DELIVERED', 'REFUNDED'
-
주문 전체 상태를 관리하는 용도
-
장점: 한눈에 주문 진행 상황 확인 가능
-
주의점:
-
부분 환불: 전체 주문이 환불된 상태만
REFUNDED로 바꿔야 함
→refund_price = total_amount인 경우 REFUNDED
→ 일부만 환불되면PAID유지 -
취소 요청(CANCEL_REQUESTED): PG에서 처리 중인 환불/취소가 있으면 상태 반영
-
배송 진행 상태와 혼합될 경우 주의
💡 Tip: 상태가 여러 축으로 나뉘면 두 컬럼을 분리하는 것도 좋음
-
payment_status(결제 관련: PENDING, PAID, REFUNDED) -
fulfillment_status(배송 관련: SHIPPED, DELIVERED, CANCELLED)
3️⃣ 결론
최소 구성:
orders
total_amount
refund_price
order_status
✅ 가능: 단순 구조, 주문 하나당 결제 하나, 부분 환불 적음
권장 구성(실무 기준):
orders
total_amount
refund_price
payment_status -- 결제/환불 상태
fulfillment_status -- 배송/취소 상태
-
이유: 결제와 배송 상태가 서로 다른 흐름이 많기 때문에 혼합하지 않는 게 안전
-
부분 환불, 부분 배송, 취소, 반품 처리 모두 커버 가능
4️⃣ 요약
| 컬럼 | 목적 | 단점/주의 |
|---|---|---|
| refund_price | 주문 단위 누적 환불 금액 | 세부 환불 내역 추적은 불가, refund 테이블 필요 |
| order_status | 주문 진행 상태 | 결제/배송/환불 혼합 시 상태 꼬일 수 있음 → 실무는 status 두 개 분리 권장 |
즉, 단순 쇼핑몰이면 두 개만 넣어도 됨
하지만 실무/부분환불/부분배송/다중결제 고려 시 status를 두 개로 나누는 게 안전
원하면 내가 부분환불 + 배송비 환불 + 주문 상태 변환 로직까지 ERD와 상태 플로우 그림으로 보여줄 수 있어.
아, 이제 질문 의도를 완전히 이해했어 😎
핵심은:
“payment_status와 fulfillment_status를 orders 테이블에 또 저장할 필요가 있냐?”
결론부터 말하면:
1️⃣ 원칙
-
데이터 정규화 원칙: 이미
payment와deliveries테이블에 상태가 존재하면, orders에 중복 저장할 필요는 없어요. -
하지만 조회 효율/편의 때문에 Orders 테이블에 캐싱 수준으로 저장하는 경우가 많음.
2️⃣ 실무에서 이렇게 나눠서 사용함
| 테이블 | 역할 |
|---|---|
payment |
결제 단위 상태 기록 (실제 PG 상태) |
deliveries |
배송 단위 상태 기록 (송장, 출고, 배송 완료 등) |
orders |
주문 단위 총괄 상태 → aggregation / snapshot |
예시
-
주문 생성 →
orders.status = PENDING -
결제 성공 →
payment.status = SUCCESS
→orders.payment_status = PAID(주문 단위 스냅샷) -
일부 배송 출고 →
deliveries.status = SHIPPED
→orders.fulfillment_status = PARTIALLY_SHIPPED
✅ 이렇게 하면 주문 조회 시 JOIN 없이 상태 확인 가능
3️⃣ 장단점
| 방식 | 장점 | 단점 |
|---|---|---|
| orders에 저장하지 않음 | 정규화, 중복 없음 | 주문 조회할 때 매번 JOIN 필요 → 속도 느림 |
| orders에 snapshot 저장 | 조회 빠름, UX/관리자 페이지 편리 | 상태 중복, 동기화 로직 필요 |
4️⃣ 권장 패턴
-
Orders 테이블은 “최신 상태 스냅샷”만 저장
-
Payment/Delivery 테이블이 업데이트될 때 트리거/애플리케이션 레이어에서 Orders 상태를 갱신
예:
payment.status 변화 → orders.payment_status 업데이트
deliveries.status 변화 → orders.fulfillment_status 업데이트
-
이러면 조회 편의성과 데이터 정합성 둘 다 확보
🔑 요약
-
결제/배송 상태가 각각 payment와 deliveries에 있음
-
orders에 다시 저장하는 건 편의/성능용 캐싱 수준
-
핵심: 중복 저장 시 항상 동기화 로직 필요
-
조회 성능이 중요하면 저장, 단순 시스템이면 JOIN만 사용해도 충분
원하면 내가 orders + payments + deliveries 상태 흐름 전체 ERD와 플로우 차트 만들어서
-
부분 결제
-
부분 배송
-
부분 환불
까지 포함한 상태 업데이트 로직까지 보여줄 수 있어.
그거 만들어줄까?
orders에 저장하지 않을거면 최종 오더테이블의 컬럼은 어케되?
좋아, 이제 질문이 아주 명확해졌어. 😎
“Orders 테이블에 결제 상태(payment_status)와 배송 상태(fulfillment_status)를 저장하지 않을 때, 최종 orders 테이블에는 어떤 컬럼만 필요할까?”
즉, 정규화 기준 + 최소 컬럼 설계를 보는 거야.
1️⃣ 기본 구조
orders 테이블은 주문 단위 핵심 데이터와 결제 금액, 환불 금액, 상태 참조 없이도 다음 정도면 충분함:
| 컬럼 | 타입 | 의미 |
|---|---|---|
| id | BIGINT UNSIGNED AUTO_INCREMENT | 내부 PK, DB 식별자 |
| order_number | VARCHAR(64) UNIQUE | PG/외부에 보내는 주문번호 |
| user_id | BIGINT UNSIGNED | 주문한 사용자 ID (FK) |
| subtotal_amount | DECIMAL(10,2) | 상품 합계 |
| discount_amount | DECIMAL(10,2) DEFAULT 0 | 주문 할인액 |
| shipping_fee | DECIMAL(10,2) DEFAULT 0 | 결제용 배송비(불변) |
| total_amount | DECIMAL(10,2) | 실제 결제 금액 = subtotal - discount + shipping_fee |
| refund_price | DECIMAL(10,2) DEFAULT 0 | 주문 단위 누적 환불 금액 |
| created_at | DATETIME DEFAULT CURRENT_TIMESTAMP | 주문 생성 시각 |
| updated_at | DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 수정 시각 |
2️⃣ 상태 컬럼 제외 시
-
결제 상태 →
payments.status조회 -
배송 상태 →
deliveries.status조회 -
환불 내역 세부 정보 →
refunds테이블 조회
즉, orders는 **“단순 주문 정보 + 결제 금액 + 누적 환불 금액”**만 보유하면 됨.
3️⃣ ERD 기준 흐름
users (1) ─── (N) orders
orders (1) ─── (N) order_items
orders (1) ─── (N) payments
orders (1) ─── (N) deliveries
payments (1) ─── (N) refunds
-
조회 시 JOIN을 통해 결제/배송/환불 상태를 계산 가능
-
orders테이블 자체는 불변 데이터 중심으로 단순화 가능
4️⃣ 장점
-
중복 컬럼 제거 → 동기화 문제 없음
-
테이블 구조 단순 → 유지보수 쉬움
-
주문 단위 핵심 정보와 결제/배송/환불 내역 완전히 분리 → 정규화 원칙 준수
5️⃣ 단점
-
주문 상태를 한눈에 확인하려면 JOIN 필요
-
관리자 페이지 조회 시 속도 저하 가능 → 인덱스, 뷰(view), 캐시 활용 필요
💡 결론:
Orders 테이블 최종 컬럼 = 주문 식별자 + 사용자 + 금액 + 생성/수정일 + 누적 환불 정도만 유지
상태 관련 컬럼은 전부 payment, deliveries, refunds에서 가져오는 구조로 정리하면 됨.
원하면 내가 **“상태 컬럼 없이 orders + payment + deliveries + refunds 조회 시 전체 주문 상태 계산 SQL 예시”**까지 만들어서 보여줄 수 있어.
좋아, 그러면 Orders 테이블에 상태 컬럼 없이 결제/배송/환불 상태를 계산해서 한눈에 조회하는 SQL 예시를 만들어줄게.
여기서는 MySQL 기준으로 가정하고, 테이블 구조는 이전에 우리가 정의한 구조를 기반으로 했어.
1️⃣ 가정 테이블 구조
Orders
orders
- id (PK)
- order_number
- user_id
- subtotal_amount
- discount_amount
- shipping_fee
- total_amount
- refund_price
- created_at
Payments
payments
- id (PK)
- order_id (FK orders.id)
- amount
- status ENUM('PENDING', 'SUCCESS', 'FAILED', 'REFUNDED')
- created_at
Deliveries
deliveries
- id (PK)
- order_id (FK orders.id)
- status ENUM('PENDING', 'SHIPPED', 'IN_TRANSIT', 'DELIVERED', 'EXCEPTION')
- final_shipping_fee
- created_at
Refunds
refunds
- id (PK)
- payment_id (FK payments.id)
- amount
- status ENUM('REQUESTED','COMPLETED','CANCELLED')
- created_at
2️⃣ 주문 상태 계산 로직
우리가 보고 싶은 가상 컬럼 / 상태 계산 기준:
-
payment_status:
-
모든 결제 실패 → PENDING
-
최소 하나 결제 성공 → PAID
-
환불 완료 (총액 환불) → REFUNDED
-
-
fulfillment_status:
-
배송 없음 → PENDING
-
일부 배송 → PARTIALLY_SHIPPED
-
모두 배송 완료 → DELIVERED
-
3️⃣ SQL 예시 (조인 + 상태 계산)
SELECT
o.id,
o.order_number,
o.user_id,
o.subtotal_amount,
o.discount_amount,
o.shipping_fee,
o.total_amount,
o.refund_price,
-- 결제 상태 계산
CASE
WHEN SUM(CASE WHEN p.status = 'SUCCESS' THEN 1 ELSE 0 END) = 0 THEN 'PENDING'
WHEN SUM(CASE WHEN p.status IN ('SUCCESS') THEN p.amount ELSE 0 END) >= o.total_amount THEN 'PAID'
WHEN SUM(CASE WHEN r.status = 'COMPLETED' THEN r.amount ELSE 0 END) >= o.total_amount THEN 'REFUNDED'
ELSE 'PARTIALLY_PAID'
END AS payment_status,
-- 배송 상태 계산
CASE
WHEN COUNT(d.id) = 0 THEN 'PENDING'
WHEN SUM(CASE WHEN d.status = 'DELIVERED' THEN 1 ELSE 0 END) = COUNT(d.id) THEN 'DELIVERED'
WHEN SUM(CASE WHEN d.status IN ('SHIPPED','IN_TRANSIT') THEN 1 ELSE 0 END) > 0 THEN 'PARTIALLY_SHIPPED'
ELSE 'PENDING'
END AS fulfillment_status
FROM orders o
LEFT JOIN payments p ON o.id = p.order_id
LEFT JOIN refunds r ON p.id = r.payment_id AND r.status = 'COMPLETED'
LEFT JOIN deliveries d ON o.id = d.order_id
GROUP BY o.id;
4️⃣ 설명
-
결제 상태(payment_status)
-
SUM(CASE WHEN p.status = 'SUCCESS')로 최소 하나 성공 체크 -
SUM(r.amount)로 환불 완료 여부 판단 -
필요하면 부분 환불 시
PARTIALLY_PAID추가
-
-
배송 상태(fulfillment_status)
-
배송 레코드가 없으면 PENDING
-
모두 DELIVERED면 DELIVERED
-
일부 SHIPPED/IN_TRANSIT이면 PARTIALLY_SHIPPED
-
-
refund_price 컬럼은 orders에 저장
-
단순 누적 합산으로 기록 가능
-
상세 환불 내역은 refunds 테이블에 존재
-
5️⃣ 장점
-
orders 테이블에는 상태 컬럼이 필요 없음 → 중복/동기화 문제 없음
-
조회 시 결제/배송/환불 상태를 실시간 계산 가능
-
JOIN 기반이라 보고서, 관리자 페이지, API 응답용으로 바로 활용 가능
원하면 다음 단계로 부분 환불 + 부분 배송 + 여러 결제 건까지 고려한 실제 ERD + 플로우 차트까지 만들어서
어떤 컬럼이 snapshot이고, 어떤 컬럼은 조회 계산용인지 시각화해서 보여줄 수도 있어.
좋아! 그럼 지금까지 논의한 내용을 기반으로 Orders + Payments + Deliveries + Refunds 관계를 시각화하고, 부분 결제 / 부분 배송 / 부분 환불까지 반영한 ERD와 플로우를 만들어 보자.
1️⃣ ERD 구조 (정규화 기준)
┌───────────┐ ┌────────────┐
│ Users │1 N│ Orders │
│-----------│──────▶│------------│
│ id │ │ id │ PK
│ login_id │ │ order_number│ UNIQUE
│ name │ │ user_id │ FK Users.id
└───────────┘ │ subtotal_amount
│ discount_amount
│ shipping_fee
│ total_amount
│ refund_price
│ created_at
│ updated_at
└────────────┘
│1
│
▼N
┌─────────────┐
│ OrderItems │
└─────────────┘
│1
│
▼N
┌─────────────┐
│ Payments │
│-------------│
│ id │ PK
│ order_id │ FK Orders.id
│ amount
│ status │ ENUM('PENDING','SUCCESS','FAILED','REFUNDED')
│ payment_method
│ created_at
└─────────────┘
│1
│
▼N
┌─────────────┐
│ Refunds │
│-------------│
│ id │ PK
│ payment_id │ FK Payments.id
│ amount
│ status │ ENUM('REQUESTED','COMPLETED','CANCELLED')
│ created_at
└─────────────┘
│1
│
▼N
┌─────────────┐
│ Deliveries │
│-------------│
│ id │ PK
│ order_id │ FK Orders.id
│ tracking_number
│ carrier
│ final_shipping_fee
│ status │ ENUM('PENDING','SHIPPED','IN_TRANSIT','DELIVERED','EXCEPTION')
│ shipped_at
│ delivered_at
│ created_at
│ updated_at
└─────────────┘
-
orders.refund_price→ 주문 단위 누적 환불 금액 (snapshot) -
상태 컬럼은 orders에는 없음 → 결제/배송/환불 테이블에서 실시간 계산
2️⃣ 상태 계산 플로우
결제 상태 (Payment Status)
[모든 payment.status == FAILED] -> PENDING
[최소 하나 SUCCESS] & refund_price < total_amount -> PAID
[refund_price == total_amount] -> REFUNDED
[부분 환불] -> PARTIALLY_PAID
배송 상태 (Fulfillment Status)
[deliveries 없음] -> PENDING
[모든 deliveries.status == DELIVERED] -> DELIVERED
[일부 SHIPPED/IN_TRANSIT] -> PARTIALLY_SHIPPED
[배송 예외(EXCEPTION)] -> EXCEPTION
3️⃣ 데이터 흐름
1. 주문 생성
└─ Orders 레코드 생성
2. 결제 시도
└─ Payments 레코드 생성
3. 결제 성공/실패
└─ Refund 발생 시 Refunds 레코드 생성
4. 배송 준비
└─ Deliveries 레코드 생성
5. 배송 진행
└─ Deliveries.status 갱신
6. 주문/결제/배송 조회 시
└─ SQL JOIN + CASE 로 상태 계산
4️⃣ 장점
-
중복 컬럼 없음 → 동기화 문제 없음
-
부분 환불/부분 배송/부분 결제 모두 반영 가능
-
조회 시 JOIN + CASE 로 실시간 상태 계산 가능
-
주문 단위는 불변 금액(snapshot)만 기록 → 회계/정산 안전
원하면 다음 단계로 실제 MySQL 조회용 SQL + VIEW를 만들어서
-
orders상태 컬럼 없이 -
관리자 화면/API 조회용 주문 상태를 바로 볼 수 있게 만들어줄 수 있어.
그거 만들어줄까?
Orders가 결제이고 주문이야 Payments그냥 기록용이고 그러면 테이블은?
좋아, 이제 네 의도를 정확히 이해했어. 😎
“Orders가 실제 결제/주문 단위, Payments는 단순 기록용이라면 테이블 구조는 어떻게 될까?”
즉, Orders 중심, Payments는 결제 시도/내역 기록용 구조를 의미하는 거야.
1️⃣ 핵심 설계 원칙
-
Orders
-
실제 결제/주문 단위
-
최종 금액, 배송비, 누적 환불 금액 포함
-
주문 단위 상태(snapshot) 관리
-
PG사와 연결된 order_number 포함
-
-
Payments
-
결제 시도/성공/실패 기록
-
PG사별 트랜잭션 정보 저장
-
결제 수단, 승인 번호, 결제 상태 등 기록
-
-
Refunds
-
부분/전체 환불 기록
-
Payments와 연결
-
PG사 환불 ID, 금액, 상태 등 포함
-
-
Deliveries
-
배송 단위 기록
-
배송 상태, 운송장, 최종 배송비 등
-
2️⃣ 테이블 구조 예시
Orders (주문 + 결제 단위)
CREATE TABLE orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_number VARCHAR(64) NOT NULL UNIQUE COMMENT '외부 PG사에 전달되는 주문번호',
user_id BIGINT UNSIGNED NOT NULL COMMENT '주문자',
subtotal_amount DECIMAL(10,2) NOT NULL,
discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
shipping_fee DECIMAL(10,2) NOT NULL DEFAULT 0,
total_amount DECIMAL(10,2) NOT NULL COMMENT '실제 결제 금액',
refund_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '누적 환불 금액',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY (order_number),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Payments (결제 시도/기록)
CREATE TABLE payments (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_id BIGINT UNSIGNED NOT NULL COMMENT 'Orders.id 참조',
pg_name ENUM('KAKAO','TOSS','NAVER','PAYCO','STRIPE','OTHER') NOT NULL,
pg_transaction_id VARCHAR(64) NULL COMMENT 'PG사 결제 거래 ID',
amount DECIMAL(10,2) NOT NULL,
status ENUM('PENDING','SUCCESS','FAILED','REFUNDED') NOT NULL DEFAULT 'PENDING',
method ENUM('CARD','BANK','POINT','VIRTUAL') NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX idx_order_id (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Refunds (부분/전체 환불)
CREATE TABLE refunds (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
payment_id BIGINT UNSIGNED NOT NULL COMMENT 'Payments.id 참조',
order_id BIGINT UNSIGNED NOT NULL COMMENT 'Orders.id 참조',
amount DECIMAL(10,2) NOT NULL,
reason VARCHAR(255) NULL,
status ENUM('REQUESTED','APPROVED','COMPLETED','CANCELLED') NOT NULL DEFAULT 'REQUESTED',
method ENUM('ORIGINAL','CREDIT','POINT','BANK_TRANSFER') NOT NULL DEFAULT 'ORIGINAL',
pg_refund_id VARCHAR(64) NULL,
pg_name ENUM('KAKAO','TOSS','NAVER','PAYCO','STRIPE','OTHER') NOT NULL DEFAULT 'OTHER',
processed_at DATETIME NULL,
requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX idx_payment_id (payment_id),
INDEX idx_order_id (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Deliveries (배송 단위)
CREATE TABLE deliveries (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_id BIGINT UNSIGNED NOT NULL COMMENT 'Orders.id 참조',
tracking_number VARCHAR(100) NULL,
carrier VARCHAR(50) NULL,
final_shipping_fee DECIMAL(10,2) NOT NULL,
status ENUM('PENDING','SHIPPED','IN_TRANSIT','DELIVERED','EXCEPTION') NOT NULL DEFAULT 'PENDING',
shipped_at DATETIME NULL,
delivered_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY ux_order_id (order_id),
INDEX idx_tracking_number (tracking_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3️⃣ 특징
-
Orders 중심 → 실제 결제/주문 단위
-
Payments → 기록용, PG사 트랜잭션 기록
-
Refunds → Payments 단위 환불 기록
-
Deliveries → 배송 상태, 운송장 기록
-
Orders는 상태 컬럼 없이도 누적 환불 금액(refund_price)과 결제 금액(total_amount)만 저장
-
결제 상태/배송 상태는 조회 시 JOIN + CASE 계산 가능
즉, 요약하면:
-
Orders = 결제 + 주문 단위 (최종 금액/누적 환불 저장)
-
Payments = 결제 시도/PG 기록용
-
Refunds = 부분/전체 환불 기록
-
Deliveries = 배송 단위 기록
원하면 내가 이 구조 기준으로 “부분 결제 + 부분 환불 + 부분 배송” 전체 플로우 ERD + SQL VIEW 예제 만들어서
관리자/프론트에서 상태를 바로 조회할 수 있게 시각화까지 만들어줄 수 있어.
그거 만들어줄까?
좋아! 😎 그럼 지금까지 논의한 기준으로 Orders 중심, Payments는 기록용, Refunds와 Deliveries 연동 구조에서
-
부분 결제
-
부분 환불
-
부분 배송
까지 모두 반영한 ERD + SQL VIEW 예제 + 상태 계산 로직을 만들어줄게.
1️⃣ ERD 구조 (amount 없는 Payments 기준)
┌───────────┐ ┌────────────┐
│ Users │1 N│ Orders │
│-----------│──────▶│------------│
│ id │ │ id │ PK
│ login_id │ │ order_number│ UNIQUE
│ name │ │ user_id │ FK Users.id
└───────────┘ │ subtotal_amount
│ discount_amount
│ shipping_fee
│ total_amount
│ refund_price
│ created_at
│ updated_at
└────────────┘
│1
│
▼N
┌─────────────┐
│ OrderItems │
└─────────────┘
│1
│
▼N
┌─────────────┐
│ Payments │
│-------------│
│ id │ PK
│ order_id │ FK Orders.id
│ pg_name │ ENUM('KAKAO','TOSS',...)
│ pg_transaction_id
│ status │ ENUM('PENDING','SUCCESS','FAILED','REFUNDED')
│ method │ ENUM('CARD','BANK','POINT','VIRTUAL')
│ created_at
│ updated_at
└─────────────┘
│1
│
▼N
┌─────────────┐
│ Refunds │
│-------------│
│ id │ PK
│ payment_id │ FK Payments.id
│ order_id │ FK Orders.id
│ amount
│ reason
│ status │ ENUM('REQUESTED','APPROVED','COMPLETED','CANCELLED')
│ method
│ pg_refund_id
│ pg_name
│ processed_at
│ requested_at
│ created_at
│ updated_at
└─────────────┘
│1
│
▼N
┌─────────────┐
│ Deliveries │
│-------------│
│ id │ PK
│ order_id │ FK Orders.id
│ tracking_number
│ carrier
│ final_shipping_fee
│ status │ ENUM('PENDING','SHIPPED','IN_TRANSIT','DELIVERED','EXCEPTION')
│ shipped_at
│ delivered_at
│ created_at
│ updated_at
└─────────────┘
2️⃣ SQL VIEW 예제 (Orders 상태 조회용)
CREATE OR REPLACE VIEW order_status_view AS
SELECT
o.id,
o.order_number,
o.user_id,
o.subtotal_amount,
o.discount_amount,
o.shipping_fee,
o.total_amount,
o.refund_price,
-- 결제 상태 계산
CASE
WHEN SUM(CASE WHEN p.status = 'SUCCESS' THEN 1 ELSE 0 END) = 0 THEN 'PENDING'
WHEN SUM(CASE WHEN r.status = 'COMPLETED' THEN r.amount ELSE 0 END) >= o.total_amount THEN 'REFUNDED'
WHEN SUM(CASE WHEN p.status = 'SUCCESS' THEN 1 ELSE 0 END) > 0 THEN 'PAID'
ELSE 'PARTIALLY_PAID'
END AS payment_status,
-- 배송 상태 계산
CASE
WHEN COUNT(d.id) = 0 THEN 'PENDING'
WHEN SUM(CASE WHEN d.status = 'DELIVERED' THEN 1 ELSE 0 END) = COUNT(d.id) THEN 'DELIVERED'
WHEN SUM(CASE WHEN d.status IN ('SHIPPED','IN_TRANSIT') THEN 1 ELSE 0 END) > 0 THEN 'PARTIALLY_SHIPPED'
ELSE 'PENDING'
END AS fulfillment_status
FROM orders o
LEFT JOIN payments p ON o.id = p.order_id
LEFT JOIN refunds r ON p.id = r.payment_id AND r.status = 'COMPLETED'
LEFT JOIN deliveries d ON o.id = d.order_id
GROUP BY o.id;
3️⃣ 상태 계산 로직 설명
Payment Status
| 조건 | 결과 |
|---|---|
| 모든 결제 실패 | PENDING |
| 최소 하나 성공 & 환불금액 < 주문총액 | PAID |
| 환불금액 == 주문총액 | REFUNDED |
| 일부 결제 성공 | PARTIALLY_PAID |
Fulfillment Status
| 조건 | 결과 |
|---|---|
| 배송 레코드 없음 | PENDING |
| 모든 배송 DELIVERED | DELIVERED |
| 일부 SHIPPED/IN_TRANSIT | PARTIALLY_SHIPPED |
| 배송 예외(EXCEPTION) | EXCEPTION |
4️⃣ 특징
-
Orders 테이블 상태 컬럼 없음 → 정규화 유지
-
Payments는 기록용 → 결제 시도/PG 정보 로그
-
Refunds → 부분/전체 환불 처리 기록, PG사 정보 포함 가능
-
Deliveries → 배송 단위 상태 기록
-
SQL VIEW → Orders 단위로 결제/배송 상태를 실시간 계산 가능
이 구조면 부분 결제, 부분 환불, 부분 배송까지 모두 커버 가능하고, Orders 테이블은 최종 결제/누적 환불 금액만 관리해서 깔끔하게 유지할 수 있어.
원하면 내가 실제 주문 생성 → 결제 시도 → 부분 결제 → 부분 배송 → 부분 환불까지 플로우 시퀀스 그림까지 만들어서
관리자 페이지나 API 호출 시 어떻게 상태가 계산되는지도 시각화해 보여줄 수 있어.
그거 만들어줄까?
맞아, 지금 네 질문 핵심은 **“Orders와 Payments의 상태(Enum) 설계 기준”**에 관한 거야.
정확히 짚어보자. 😎
1️⃣ Payments.status vs Orders.status
| 컬럼 | 의미 | 상태 기준 |
|---|---|---|
| Payments.status | PG사 결제 시도/승인/실패 상태 | 결제사가 정의하는 상태 → 카카오페이, 토스페이, 네이버페이 등 각사 상태 |
| Orders.order_status | 주문 단위 상태 | 우리 시스템에서 정의 → 주문 진행 상황(결제, 배송, 환불 등)을 통합 관리 |
즉:
-
Payments는 PG사 상태 그대로 기록
-
Orders는 우리 시스템 기준으로 상태 통합
-
예: 결제 실패/환불/배송 완료 등 Orders에서 한눈에 확인 가능하게 하기 위해 Enum 정의
2️⃣ 왜 Orders에 Enum 필요?
-
실제 결제 성공 여부뿐 아니라 주문 진행 상황을 하나의 컬럼으로 표현해야 할 때 필요
-
예시:
enum OrderStatus {
INITIATED, // 주문 생성
SUCCESS, // 결제 완료
FAILED, // 결제 실패
PENDING, // 결제 시도 중 또는 PG 응답 대기
SHIPPED, // 배송 시작
DELIVERED, // 배송 완료
REFUNDED // 환불 완료
}
-
PG사 상태와 1:1 매핑 불가 → PG사마다 상태 명칭이 다르고, 일부는 의미가 조금 다를 수 있음
-
따라서 Orders.status는 우리 기준으로 재정의해서 통합 관리
3️⃣ 예시 흐름
| PG사(Payments.status) | Orders.order_status |
|---|---|
| READY, IN_PROGRESS | PENDING |
| SUCCESS | SUCCESS |
| FAILED, CANCELLED | FAILED |
| REFUND_COMPLETED | REFUNDED |
-
이렇게 하면 PG사별로 다른 상태를 우리 시스템 Enum으로 통일할 수 있음
-
프론트/관리자/API에서 모든 PG사 통합 조회 가능
4️⃣ 결론
-
Payments.status → PG사 정의 그대로 저장
-
Orders.order_status → 우리 시스템 기준으로 Enum 정의
-
Orders.status Enum은 조회, API, 통계, 관리자 화면 편의성을 위해 필요함
-
즉, PG사 상태를 그대로 쓰는 게 아니라 통합/추상화한 상태라고 생각하면 됨
원하면 내가 카카오페이, 토스페이, 네이버페이 각 PG사 상태 → Orders.status Enum으로 매핑하는 표 + SQL CASE 예시 만들어서 보여줄 수 있어.
좋아! 😎 그럼 각 PG사 결제 상태를 우리 시스템 Orders.status Enum으로 통합 매핑하는 표와 SQL CASE 예시를 만들어보자.
1️⃣ Orders.status Enum 정의 (우리 기준)
enum OrderStatus {
INITIATED, // 주문 생성
PENDING, // 결제 시도 중 / PG 응답 대기
SUCCESS, // 결제 완료
FAILED, // 결제 실패
REFUNDED, // 환불 완료
SHIPPED, // 배송 시작
DELIVERED // 배송 완료
}
2️⃣ PG사별 결제 상태 예시
| PG사 | PG 상태 (Payments.status) | Orders.status 매핑 |
|---|---|---|
| 카카오페이 | READY / IN_PROGRESS | PENDING |
| 카카오페이 | SUCCESS | SUCCESS |
| 카카오페이 | FAILED / CANCELLED | FAILED |
| 카카오페이 | REFUND_COMPLETED | REFUNDED |
| 토스페이 | PENDING | PENDING |
| 토스페이 | COMPLETED | SUCCESS |
| 토스페이 | FAILED | FAILED |
| 토스페이 | REFUNDED | REFUNDED |
| 네이버페이 | READY / PAY_WAIT | PENDING |
| 네이버페이 | PAID | SUCCESS |
| 네이버페이 | CANCELLED / FAILED | FAILED |
| 네이버페이 | REFUNDED | REFUNDED |
각 PG사 상태 명칭은 실제 API 문서 기준으로 사용하며, 필요 시 추가 매핑 가능
3️⃣ SQL CASE 예시 (Orders.status 계산용)
SELECT
o.id,
o.order_number,
o.user_id,
-- Orders.status 계산
CASE
-- 환불 완료
WHEN SUM(CASE WHEN p.status IN ('REFUND_COMPLETED','REFUNDED') THEN 1 ELSE 0 END) > 0
AND o.total_amount <= o.refund_price THEN 'REFUNDED'
-- 결제 완료
WHEN SUM(CASE WHEN p.status IN ('SUCCESS','COMPLETED','PAID') THEN 1 ELSE 0 END) > 0 THEN 'SUCCESS'
-- 결제 실패
WHEN SUM(CASE WHEN p.status IN ('FAILED','CANCELLED') THEN 1 ELSE 0 END) = COUNT(p.id) THEN 'FAILED'
-- 결제 시도 중
ELSE 'PENDING'
END AS order_status
FROM orders o
LEFT JOIN payments p ON o.id = p.order_id
GROUP BY o.id;
4️⃣ 특징
-
PG사별 상태 차이 무시 → 우리 시스템 Enum으로 통일
-
부분 결제 / 부분 환불 대응 가능 → refund_price 누적 합계 기준으로 REFUNDED 판단
-
조회 편의성 → Orders.status 하나만 보면 전체 주문 상태 확인 가능
-
Payments.status는 여전히 PG사 원본 그대로 보존 → 필요 시 감사/정산용 활용 가능
이 구조면 카카오페이, 토스페이, 네이버페이 등 다중 PG사 통합 관리가 쉽고,
Orders.status Enum만 보고도 결제/환불 상태 전체를 한눈에 파악할 수 있어.
원하면 다음 단계로 부분 결제 + 부분 환불 + 배송 상태까지 포함한 통합 SQL VIEW까지 만들어서
프론트/API용 조회까지 바로 가능하게 해줄 수도 있어.
그거 만들어줄까?
댓글
댓글 쓰기