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

쿠폰/할인 로직


핵심 개념

쿠폰 종류·중복 사용·검증·자동 적용 — 마케팅 표준.

본문

쿠폰 종류

📋 코드 (7줄)
1. 정액 (5,000원 할인)
2. 정률 (10% 할인)
3. 무료 배송
4. 1+1 / N개 구매 시 1개 무료
5. 카테고리별·상품별
6. 첫 구매 한정
7. 신규 가입 한정

데이터 모델

SQL📋 코드 (31줄)
CREATE TABLE coupons (
  id UUID PRIMARY KEY,
  code VARCHAR UNIQUE NOT NULL,
  name VARCHAR NOT NULL,
  description TEXT,
  type VARCHAR NOT NULL,  -- 'fixed', 'percentage', 'free_shipping', 'bogo'
  value DECIMAL(10,2) NOT NULL,  -- 5000 또는 10 또는 100
  min_order_amount DECIMAL(10,0),
  max_discount DECIMAL(10,0),    -- 정률의 상한
  usage_limit INT,                -- 전체 사용 한도
  usage_per_user INT DEFAULT 1,
  applicable_to VARCHAR DEFAULT 'all',  -- all, categories, products
  applicable_ids UUID[],          -- 대상 카테고리·상품
  conditions JSONB,               -- { firstOrder: true, minQuantity: 2, ... }
  starts_at TIMESTAMP,
  expires_at TIMESTAMP,
  status VARCHAR DEFAULT 'active',
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE coupon_redemptions (
  id UUID PRIMARY KEY,
  coupon_id UUID NOT NULL,
  user_id UUID NOT NULL,
  order_id UUID,
  amount_discounted DECIMAL(10,0),
  used_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX ON coupon_redemptions(coupon_id);
CREATE INDEX ON coupon_redemptions(user_id, coupon_id);

쿠폰 검증 + 적용

TYPESCRIPT📋 코드 (87줄)
async function applyCoupon(
  userId: string,
  cartItems: CartItem[],
  subtotal: number,
  couponCode: string,
) {
  const coupon = await db.coupon.findUnique({
    where: { code: couponCode.toUpperCase() },
  });
  if (!coupon || coupon.status !== 'active') {
    throw new NotFoundError('Coupon not found');
  }

  // 1. 기간 검증
  const now = new Date();
  if (coupon.startsAt && now < coupon.startsAt) {
    throw new BadRequestError('Coupon not yet active');
  }
  if (coupon.expiresAt && now > coupon.expiresAt) {
    throw new BadRequestError('Coupon expired');
  }

  // 2. 사용량 검증
  if (coupon.usageLimit) {
    const totalUsed = await db.couponRedemption.count({ where: { couponId: coupon.id } });
    if (totalUsed >= coupon.usageLimit) {
      throw new ConflictError('Coupon fully redeemed');
    }
  }

  const userUsed = await db.couponRedemption.count({
    where: { couponId: coupon.id, userId },
  });
  if (userUsed >= coupon.usagePerUser) {
    throw new ConflictError('Already used');
  }

  // 3. 최소 주문금액
  if (coupon.minOrderAmount && subtotal < coupon.minOrderAmount) {
    throw new BadRequestError(`Minimum order: ₩${coupon.minOrderAmount.toLocaleString()}`);
  }

  // 4. 조건 검증 (firstOrder 등)
  if (coupon.conditions?.firstOrder) {
    const prevOrders = await db.order.count({
      where: { userId, status: { in: ['paid', 'completed'] } },
    });
    if (prevOrders > 0) throw new ConflictError('First order only');
  }

  // 5. 적용 가능 상품 필터
  const applicableItems = filterApplicable(cartItems, coupon);
  const applicableSubtotal = applicableItems.reduce((s, i) => s + i.unitPrice * i.quantity, 0);

  // 6. 할인액 계산
  let discount = 0;
  switch (coupon.type) {
    case 'fixed':
      discount = Math.min(coupon.value, applicableSubtotal);
      break;
    case 'percentage':
      discount = applicableSubtotal * (coupon.value / 100);
      if (coupon.maxDiscount) discount = Math.min(discount, coupon.maxDiscount);
      break;
    case 'free_shipping':
      discount = 0;  // 별도 처리
      break;
  }

  return {
    coupon,
    discount: Math.round(discount),
    applicableItems: applicableItems.map(i => i.id),
  };
}


function filterApplicable(items: CartItem[], coupon: Coupon): CartItem[] {
  if (coupon.applicableTo === 'all') return items;
  if (coupon.applicableTo === 'products') {
    return items.filter(i => coupon.applicableIds.includes(i.productId));
  }
  if (coupon.applicableTo === 'categories') {
    return items.filter(i => coupon.applicableIds.includes(i.categoryId));
  }
  return items;
}

중복 사용 (쿠폰 + 포인트)

TYPESCRIPT📋 코드 (26줄)
const COUPON_STACKING_RULES = {
  // 동시 사용 가능
  free_shipping: ['fixed', 'percentage'],

  // 단독 사용
  fixed: [],
  percentage: [],
};


async function applyMultipleCoupons(userId: string, cart: any, codes: string[]) {
  if (codes.length > 2) throw new BadRequestError('Max 2 coupons');

  const coupons = await Promise.all(codes.map(c => applyCoupon(userId, cart.items, cart.subtotal, c)));

  // stacking 검증
  if (coupons.length === 2) {
    const [a, b] = coupons;
    const aAllows = COUPON_STACKING_RULES[a.coupon.type] ?? [];
    if (!aAllows.includes(b.coupon.type)) {
      throw new ConflictError('Cannot combine these coupons');
    }
  }

  return coupons;
}

자동 적용 (가장 좋은 쿠폰)

TYPESCRIPT📋 코드 (29줄)
async function findBestCoupon(userId: string, cart: any) {
  const eligibleCoupons = await db.coupon.findMany({
    where: {
      status: 'active',
      startsAt: { lte: new Date() },
      OR: [
        { expiresAt: null },
        { expiresAt: { gt: new Date() } },
      ],
    },
  });

  let best = null;
  let bestDiscount = 0;

  for (const coupon of eligibleCoupons) {
    try {
      const { discount } = await applyCoupon(userId, cart.items, cart.subtotal, coupon.code);
      if (discount > bestDiscount) {
        best = coupon;
        bestDiscount = discount;
      }
    } catch {
      // 적용 불가 → 무시
    }
  }

  return best;
}

결제 시 사용 기록

TYPESCRIPT📋 코드 (23줄)
async function placeOrder(orderId: string, couponId: string | null) {
  if (!couponId) return;

  return db.$transaction(async (tx) => {
    const order = await tx.order.findUnique({ where: { id: orderId } });
    await tx.couponRedemption.create({
      data: {
        couponId,
        userId: order!.userId,
        orderId,
        amountDiscounted: order!.discount,
      },
    });
  });
}


// 환불 시 — 쿠폰 복구
async function refundOrder(orderId: string) {
  const order = await db.order.findUnique({ where: { id: orderId } });
  await db.couponRedemption.deleteMany({ where: { orderId } });
  // → 사용자가 다시 사용 가능
}

다음 챕터

CH.79 "관리자 패널: 주문/상품/사용자 관리".


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.쿠폰 검증은 6단계 — 기간·전체사용량·사용자사용량·최소금액·조건·적용대상
2.중복 사용은 명시적 룰 (COUPON_STACKING_RULES)
3.환불 시 쿠폰 복구 — coupon_redemptions 삭제


공유하기
진행도 78 / 90