OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 75 / 90
stack-analysis
CHAPTER 75 / 90
읽기 약 2
FUNCTION

주문 상태 머신: 주문→결제→배송→완료


핵심 개념

state machine·전환 규칙·이벤트·동시성 — 견고한 주문 흐름.

본문

주문 상태 머신

📋 코드 (15줄)
pending (주문 생성)
   ↓ 결제 성공
paid (결제 완료)
   ↓ 사장 발송
shipped (배송 중)
   ↓ 도착 + 자동 7일 또는 사용자 확정
delivered (배송 완료)
   ↓
completed (구매 확정)


[취소 경로]
pending → cancelled (사용자 취소)
paid → refund_requested → refunded
shipped → return_requested → returning → returned → refunded

XState로 모델링

TYPESCRIPT📋 코드 (76줄)
import { createMachine, interpret, assign } from 'xstate';

const orderMachine = createMachine({
  id: 'order',
  initial: 'pending',
  context: {
    orderId: '',
    paidAt: null,
    shippedAt: null,
    deliveredAt: null,
  },
  states: {
    pending: {
      on: {
        PAYMENT_SUCCESS: { target: 'paid', actions: 'recordPaidAt' },
        CANCEL: 'cancelled',
        EXPIRE: 'cancelled',  // 30분 후 자동
      },
      after: {
        1800000: 'cancelled',  // 30분
      },
    },
    paid: {
      on: {
        SHIP: { target: 'shipped', actions: 'recordShippedAt' },
        REFUND_REQUEST: 'refund_requested',
      },
    },
    shipped: {
      on: {
        DELIVER: { target: 'delivered', actions: 'recordDeliveredAt' },
        RETURN_REQUEST: 'return_requested',
      },
    },
    delivered: {
      on: {
        CONFIRM: 'completed',
        RETURN_REQUEST: 'return_requested',
      },
      after: {
        604800000: 'completed',  // 7일 후 자동 확정
      },
    },
    completed: { type: 'final' },
    cancelled: { type: 'final' },
    refund_requested: {
      on: {
        APPROVE: 'refunded',
        REJECT: 'paid',
      },
    },
    return_requested: {
      on: {
        APPROVE: 'returning',
        REJECT: 'shipped',
      },
    },
    returning: {
      on: {
        RECEIVED: 'returned',
      },
    },
    returned: {
      on: {
        REFUND: 'refunded',
      },
    },
    refunded: { type: 'final' },
  },
}, {
  actions: {
    recordPaidAt: assign({ paidAt: () => new Date() }),
    recordShippedAt: assign({ shippedAt: () => new Date() }),
    recordDeliveredAt: assign({ deliveredAt: () => new Date() }),
  },
});

DB로 상태 관리 — 단순화

TYPESCRIPT📋 코드 (53줄)
const ALLOWED_TRANSITIONS: Record<string, string[]> = {
  pending: ['paid', 'cancelled'],
  paid: ['shipped', 'refund_requested'],
  shipped: ['delivered', 'return_requested'],
  delivered: ['completed', 'return_requested'],
  refund_requested: ['refunded', 'paid'],
  return_requested: ['returning', 'shipped'],
  returning: ['returned'],
  returned: ['refunded'],
  // final: completed, cancelled, refunded
};


async function transitionOrder(orderId: string, newStatus: string, actor: { userId?: string; system?: boolean }) {
  return db.$transaction(async (tx) => {
    const order = await tx.order.findUnique({ where: { id: orderId } });
    if (!order) throw new NotFoundError('Order');

    const allowed = ALLOWED_TRANSITIONS[order.status] ?? [];
    if (!allowed.includes(newStatus)) {
      throw new ConflictError(`Invalid transition: ${order.status} → ${newStatus}`);
    }

    const timestampField = {
      paid: 'paidAt',
      shipped: 'shippedAt',
      delivered: 'deliveredAt',
      completed: 'completedAt',
      cancelled: 'cancelledAt',
    }[newStatus];

    const updated = await tx.order.update({
      where: { id: orderId },
      data: {
        status: newStatus,
        ...(timestampField && { [timestampField]: new Date() }),
      },
    });

    // 이력 기록
    await tx.orderStatusHistory.create({
      data: {
        orderId,
        from: order.status,
        to: newStatus,
        actorId: actor.userId,
        system: actor.system ?? false,
      },
    });

    return updated;
  });
}

자동 전환 (스케줄러)

TYPESCRIPT📋 코드 (35줄)
// 30분 후 미결제 주문 자동 취소
const cancelExpiredScheduler = new Worker('cancel-expired', async () => {
  const expired = await db.order.findMany({
    where: {
      status: 'pending',
      createdAt: { lt: new Date(Date.now() - 30 * 60 * 1000) },
    },
  });

  for (const order of expired) {
    await transitionOrder(order.id, 'cancelled', { system: true });
    // 예약 해제
    await db.inventoryReservation.deleteMany({ where: { orderId: order.id } });
  }
});


// 배송 완료 7일 후 자동 확정
const autoConfirmScheduler = new Worker('auto-confirm', async () => {
  const ready = await db.order.findMany({
    where: {
      status: 'delivered',
      deliveredAt: { lt: new Date(Date.now() - 7 * 86400 * 1000) },
    },
  });

  for (const order of ready) {
    await transitionOrder(order.id, 'completed', { system: true });
  }
});


// Cron 스케줄
scheduler.upsertJobScheduler('cancel-expired', { pattern: '*/5 * * * *' }, { name: 'check' });
scheduler.upsertJobScheduler('auto-confirm', { pattern: '0 * * * *' }, { name: 'check' });

이벤트 기반 알림

TYPESCRIPT📋 코드 (18줄)
// 상태 변경 시 자동 이메일·SMS·푸시
async function transitionOrder(orderId: string, newStatus: string, actor: any) {
  // ... 위 로직

  // 이벤트 발행
  await eventBus.publish(`order.${newStatus}`, { orderId, order: updated });
}


// 구독자
eventBus.subscribe('order.paid', async ({ orderId }) => {
  const order = await db.order.findUnique({ where: { id: orderId }, include: { user: true } });
  await emailQueue.add('order_paid', { orderId, email: order!.user.email });
});

eventBus.subscribe('order.shipped', async ({ orderId, order }) => {
  await smsQueue.add('order_shipped', { phone: order.shippingAddress.phone, trackingNumber: order.trackingNumber });
});

다음 챕터

CH.76 "재고 관리 시스템".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 프로젝트의 주문 상태 머신 부분을 분석해서
실전 분석 + 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

주문 상태 머신 관련 실제 서비스 5개를
비교 분석해서 패턴 추출를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 코드베이스에서 주문 상태 머신
최적화 가능 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 풀스택 시장의
주문 상태 머신 트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
주문 상태 머신: 주문→결제→배송→완료 이 3가지만 확실히 잡으세요
1.주문 상태 머신 — 명시적 전환 규칙 + 이력 기록
2.XState 또는 단순 ALLOWED_TRANSITIONS 매핑
3.자동 전환 = cron + 이벤트 발행으로 알림 자동


공유하기
진행도 75 / 90