๐ ํ๋ก์ ํธ ๋ฉํ ์ ๋ณด
| ํญ๋ชฉ | ๋ด์ฉ |
|---|
| ํ๋ก์ ํธ๋ช
| ๋ฝ์๋ ๊ธฐ (BBOSSIREGI) |
| ๊ธฐ๊ฐ | 2026.02.24 ~ 2026.03.19 (24์ผ) |
| ํ์ | 4๋ช
|
| ์คํ๋ฆฐํธ | 1์ฃผ ๋จ์ (์ด 4์คํ๋ฆฐํธ) |
์ฐ์ ์์ ์ ์
| ๋ ๋ฒจ | ์ค๋ช
| ๊ธฐ์ค |
|---|
| P0 | ํ์ (Blocker) | ์ด๊ฑฐ ์์ผ๋ฉด ๋ค์ ์์
๋ถ๊ฐ |
| P1 | ํต์ฌ (Must Have) | MVP ํ์ ๊ธฐ๋ฅ |
| P2 | ์ค์ (Should Have) | ์์ผ๋ฉด ์ข์ |
| P3 | ์ ํ (Nice to Have) | ์๊ฐ ๋๋ฉด |
๐๏ธ Sprint 1: ์ธํ๋ผ ๊ธฐ๋ฐ ๊ตฌ์ถ (Day 1-5)
Day 1 (2/24 ์) - ์ค๊ณ ๋ฐ AWS ๊ธฐ๋ฐ
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S1-001 | P0 | AWS ๊ณ์ ๋ฐ IAM ์ค์ | ์ธํ๋ผ | IAM ์ ์ฑ
JSON | Admin, Developer, ReadOnly ์ญํ ์์ฑ ์๋ฃ |
S1-002 | P0 | ERD ์ค๊ณ ํ์ | ๋ฐฑ์๋ | ERD ๋ค์ด์ด๊ทธ๋จ | ์ต์ 5๊ฐ ํ
์ด๋ธ, ๊ด๊ณ ์ ์ ์๋ฃ |
S1-003 | P0 | ์์คํ
์ํคํ
์ฒ ๋ค์ด์ด๊ทธ๋จ | ์ ์ฒด | draw.io ํ์ผ | ๋ชจ๋ ์ปดํฌ๋ํธ + ๋ฐ์ดํฐ ํ๋ฆ ํ์ |
S1-004 | P0 | MVP API ๋ช
์ธ ์์ฑ | ๋ฐฑ์๋ | OpenAPI 3.0 Spec | ์ต์ 10๊ฐ ์๋ํฌ์ธํธ ์ ์ |
S1-005 | P0 | Git ๋ ํฌ์งํ ๋ฆฌ ๊ตฌ์ฑ | ์ ์ฒด | GitHub Repo | mono-repo ๊ตฌ์กฐ, branch ์ ๋ต ๋ฌธ์ํ |
๐ S1-002: ERD ๋ช
์ธ
-- ํ์ ํ
์ด๋ธ (์ต์ ๊ตฌํ)
users (id, email, password_hash, name, created_at)
products (id, name, description, price, image_url, created_at)
time_deals (id, product_id, deal_price, stock_quantity, reserved_quantity, start_at, end_at, status)
orders (id, user_id, time_deal_id, quantity, total_price, status, created_at)
order_events (id, order_id, event_type, payload, created_at) -- ์ด๋ฒคํธ ์์ฑ์ฉ
๐ S1-004: API ๋ช
์ธ (ํ์ 10๊ฐ)
# ์ธ์ฆ (2๊ฐ)
POST /api/v1/auth/register # ํ์๊ฐ์
POST /api/v1/auth/login # ๋ก๊ทธ์ธ
# ์ํ (3๊ฐ)
GET /api/v1/products # ์ํ ๋ชฉ๋ก
GET /api/v1/products/:id # ์ํ ์์ธ
POST /api/v1/admin/products # ์ํ ๋ฑ๋ก (๊ด๋ฆฌ์)
# ํ์๋ (3๊ฐ)
GET /api/v1/timedeals # ํ์๋ ๋ชฉ๋ก
GET /api/v1/timedeals/:id # ํ์๋ ์์ธ (์ฌ๊ณ ํฌํจ)
POST /api/v1/admin/timedeals # ํ์๋ ์์ฑ (๊ด๋ฆฌ์)
# ์ฃผ๋ฌธ (2๊ฐ)
POST /api/v1/orders # ์ฃผ๋ฌธ ์์ฑ (์ฌ๊ณ ์์ฝ)
GET /api/v1/orders/:id # ์ฃผ๋ฌธ ์ํ ์กฐํ
Day 2 (2/25 ํ) - ๋คํธ์ํฌ ๋ฐ DB ๊ตฌ์ถ
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S1-006 | P0 | VPC ์์ฑ | ์ธํ๋ผ | Terraform ์ฝ๋ | CIDR: 10.0.0.0/16 |
S1-007 | P0 | ์๋ธ๋ท ๊ตฌ์ฑ | ์ธํ๋ผ | Terraform ์ฝ๋ | Public 2๊ฐ, Private 2๊ฐ (Multi-AZ) |
S1-008 | P0 | NAT Gateway + IGW | ์ธํ๋ผ | Terraform ์ฝ๋ | Private ์๋ธ๋ท ์ธํฐ๋ท ์ ๊ทผ ๊ฐ๋ฅ |
S1-009 | P0 | ๋ณด์ ๊ทธ๋ฃน ์ค์ | ์ธํ๋ผ | Terraform ์ฝ๋ | ALB, EKS, RDS ๋ณ๋ SG |
S1-010 | P0 | RDS PostgreSQL ๊ตฌ์ถ | ์ธํ๋ผ | Terraform ์ฝ๋ | db.t3.micro, Multi-AZ ๋นํ์ฑํ (๋น์ฉ) |
S1-011 | P0 | DB ์คํค๋ง ์์ฑ | ๋ฐฑ์๋ | DDL SQL ํ์ผ | ๋ชจ๋ ํ
์ด๋ธ + ์ธ๋ฑ์ค ์์ฑ ์๋ฃ |
๐ S1-006~009: VPC ๋ช
์ธ
# terraform/vpc.tf
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "bbossiregi-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-2a", "ap-northeast-2c"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]
enable_nat_gateway = true
single_nat_gateway = true # ๋น์ฉ ์ ๊ฐ
tags = {
Project = "bbossiregi"
Environment = "dev"
}
}
๐ S1-009: ๋ณด์ ๊ทธ๋ฃน ๋ช
์ธ
| SG ์ด๋ฆ | Inbound | Outbound | ์ฉ๋ |
|---|
sg-alb | 80, 443 from 0.0.0.0/0 | All | ALB |
sg-eks | All from sg-alb | All | EKS ๋
ธ๋ |
sg-rds | 5432 from sg-eks | None | RDS |
๐ S1-011: DDL ๋ช
์ธ
-- migrations/001_init.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
role VARCHAR(20) DEFAULT 'user', -- user, admin
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
image_url VARCHAR(500),
category VARCHAR(50), -- food, toy, health, etc
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE time_deals (
id SERIAL PRIMARY KEY,
product_id INTEGER REFERENCES products(id),
deal_price DECIMAL(10,2) NOT NULL,
original_price DECIMAL(10,2) NOT NULL,
stock_quantity INTEGER NOT NULL,
reserved_quantity INTEGER DEFAULT 0,
sold_quantity INTEGER DEFAULT 0,
start_at TIMESTAMP NOT NULL,
end_at TIMESTAMP NOT NULL,
status VARCHAR(20) DEFAULT 'scheduled', -- scheduled, active, ended, soldout
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT valid_stock CHECK (reserved_quantity >= 0),
CONSTRAINT valid_sold CHECK (sold_quantity >= 0),
CONSTRAINT valid_total CHECK (reserved_quantity + sold_quantity <= stock_quantity)
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
time_deal_id INTEGER REFERENCES time_deals(id),
quantity INTEGER NOT NULL DEFAULT 1,
unit_price DECIMAL(10,2) NOT NULL,
total_price DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'pending', -- pending, reserved, confirmed, canceled, failed
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE order_events (
id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(id),
event_type VARCHAR(50) NOT NULL, -- CREATED, STOCK_RESERVED, PAYMENT_COMPLETED, CONFIRMED, CANCELED
payload JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ์ธ๋ฑ์ค
CREATE INDEX idx_time_deals_status ON time_deals(status);
CREATE INDEX idx_time_deals_start_at ON time_deals(start_at);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_order_events_order_id ON order_events(order_id);
Day 3 (2/26 ์) - ๋ฐฑ์๋ ํต์ฌ API
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S1-012 | P0 | Go ํ๋ก์ ํธ ์ด๊ธฐํ | ๋ฐฑ์๋ | main.go | Gin ํ๋ ์์ํฌ ์ค์ ์๋ฃ |
S1-013 | P0 | DB ์ฐ๊ฒฐ ์ค์ | ๋ฐฑ์๋ | db/postgres.go | Connection pool ์ค์ |
S1-014 | P1 | ์ ์ ์ธ์ฆ API | ๋ฐฑ์๋ | handlers/auth.go | JWT ํ ํฐ ๋ฐ๊ธ |
S1-015 | P1 | ์ํ CRUD API | ๋ฐฑ์๋ | handlers/product.go | ๋ชฉ๋ก/์์ธ/๋ฑ๋ก |
S1-016 | P1 | ํ์๋ API | ๋ฐฑ์๋ | handlers/timedeal.go | ๋ชฉ๋ก/์์ธ/๋ฑ๋ก |
S1-017 | P0 | ํ์๋ ์ค์ผ์ค๋ง ๋ก์ง | ๋ฐฑ์๋ | services/scheduler.go | ์๋ ์ํ ๋ณ๊ฒฝ |
๐ S1-014: ์ธ์ฆ API ๋ช
์ธ
// POST /api/v1/auth/register
// Request
{
"email": "user@example.com",
"password": "password123",
"name": "ํ๊ธธ๋"
}
// Response 201
{
"id": 1,
"email": "user@example.com",
"name": "ํ๊ธธ๋",
"created_at": "2026-02-26T10:00:00Z"
}
// Error 400: ์ด๋ฉ์ผ ์ค๋ณต
// Error 400: ์ ํจ์ฑ ๊ฒ์ฌ ์คํจ
// POST /api/v1/auth/login
// Request
{
"email": "user@example.com",
"password": "password123"
}
// Response 200
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}
// Error 401: ์ธ์ฆ ์คํจ
๐ S1-016: ํ์๋ API ๋ช
์ธ
// GET /api/v1/timedeals
// Response 200
{
"data": [
{
"id": 1,
"product": {
"id": 1,
"name": "ํ๋ฆฌ๋ฏธ์ ์ฌ๋ฃ 3kg",
"image_url": "https://..."
},
"deal_price": 29900,
"original_price": 45000,
"discount_rate": 33,
"stock_quantity": 100,
"available_quantity": 87, // stock - reserved - sold
"start_at": "2026-02-27T14:00:00Z",
"end_at": "2026-02-27T15:00:00Z",
"status": "scheduled"
}
],
"meta": {
"total": 10,
"page": 1,
"per_page": 20
}
}
// GET /api/v1/timedeals/:id
// Response 200 (์ค์๊ฐ ์ฌ๊ณ ํฌํจ)
{
"id": 1,
"product": { ... },
"deal_price": 29900,
"stock_quantity": 100,
"reserved_quantity": 10,
"sold_quantity": 3,
"available_quantity": 87,
"start_at": "2026-02-27T14:00:00Z",
"end_at": "2026-02-27T15:00:00Z",
"status": "active",
"remaining_seconds": 1823 // ๋จ์ ์๊ฐ (์ด)
}
๐ S1-017: ์ค์ผ์ค๋ฌ ๋ก์ง ๋ช
์ธ
// services/scheduler.go
// ๋งค ๋ถ๋ง๋ค ์คํ
func (s *Scheduler) Run() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
s.activateDeals() // scheduled โ active
s.expireDeals() // active โ ended (์๊ฐ ๋ง๋ฃ)
s.checkSoldOut() // active โ soldout (์ฌ๊ณ ์์ง)
}
}
// ์ํ ์ ์ด ๊ท์น
// scheduled โ active: start_at <= now AND now < end_at
// active โ ended: now >= end_at
// active โ soldout: available_quantity <= 0
Day 4 (2/27 ๋ชฉ) - ์ฃผ๋ฌธ ๋ฐ ์ปจํ
์ด๋ํ
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S1-018 | P1 | ์ฃผ๋ฌธ ์์ฑ API (์ฌ๊ฐ ํจํด) | ๋ฐฑ์๋ | handlers/order.go | Reserve โ Confirm ํ๋ก์ฐ |
S1-019 | P1 | ์ฌ๊ณ ์์ฝ ๋ก์ง | ๋ฐฑ์๋ | services/stock.go | ๋์์ฑ ์ ์ด (SELECT FOR UPDATE) |
S1-020 | P1 | ์ฃผ๋ฌธ ์ทจ์ API | ๋ฐฑ์๋ | handlers/order.go | ๋ณด์ ํธ๋์ญ์
|
S1-021 | P1 | Dockerfile ์์ฑ (๋ฐฑ์๋) | ์ธํ๋ผ | Dockerfile | ๋ฉํฐ์คํ
์ด์ง ๋น๋ |
S1-022 | P1 | ECR ๋ ํฌ์งํ ๋ฆฌ ์์ฑ | ์ธํ๋ผ | Terraform ์ฝ๋ | ์ด๋ฏธ์ง ํธ์ ์ฑ๊ณต |
S1-023 | P1 | EKS ํด๋ฌ์คํฐ ๊ตฌ์ถ | ์ธํ๋ผ | Terraform ์ฝ๋ | ๋
ธ๋ ๊ทธ๋ฃน 1๊ฐ, t3.medium x 2 |
๐ S1-018: ์ฃผ๋ฌธ ์์ฑ API (์ฌ๊ฐ ํจํด)
// POST /api/v1/orders
// Request
{
"time_deal_id": 1,
"quantity": 2
}
// ๋ด๋ถ ํ๋ก์ฐ (์ฌ๊ฐ ํจํด)
// Step 1: ์ฃผ๋ฌธ ์์ฑ (status: pending)
// Step 2: ์ฌ๊ณ ์์ฝ (reserved_quantity += quantity)
// Step 3: ์ฃผ๋ฌธ ์ํ ๋ณ๊ฒฝ (status: reserved)
// Step 4: (ํฅํ) ๊ฒฐ์ ์ฒ๋ฆฌ
// Step 5: (ํฅํ) ์ฃผ๋ฌธ ํ์ (status: confirmed, sold_quantity += quantity, reserved_quantity -= quantity)
// Response 201
{
"id": 123,
"time_deal_id": 1,
"quantity": 2,
"unit_price": 29900,
"total_price": 59800,
"status": "reserved",
"created_at": "2026-02-27T14:05:00Z"
}
// Error 409: ์ฌ๊ณ ๋ถ์กฑ
{
"error": "INSUFFICIENT_STOCK",
"message": "์ฌ๊ณ ๊ฐ ๋ถ์กฑํฉ๋๋ค",
"available": 1,
"requested": 2
}
// Error 400: ํ์๋ ๋นํ์ฑ
{
"error": "DEAL_NOT_ACTIVE",
"message": "ํ์๋์ด ์งํ ์ค์ด ์๋๋๋ค"
}
๐ S1-019: ์ฌ๊ณ ์์ฝ ๋ก์ง (๋์์ฑ ์ ์ด)
// services/stock.go
func (s *StockService) Reserve(ctx context.Context, dealID, quantity int) error {
tx, _ := s.db.BeginTx(ctx, nil)
defer tx.Rollback()
// SELECT FOR UPDATE - ํ ์ ๊ธ
var deal TimeDeal
err := tx.QueryRowContext(ctx, `
SELECT id, stock_quantity, reserved_quantity, sold_quantity, status
FROM time_deals
WHERE id = $1
FOR UPDATE
`, dealID).Scan(&deal.ID, &deal.StockQty, &deal.ReservedQty, &deal.SoldQty, &deal.Status)
if err != nil {
return ErrDealNotFound
}
if deal.Status != "active" {
return ErrDealNotActive
}
available := deal.StockQty - deal.ReservedQty - deal.SoldQty
if available < quantity {
return ErrInsufficientStock
}
// ์ฌ๊ณ ์์ฝ
_, err = tx.ExecContext(ctx, `
UPDATE time_deals
SET reserved_quantity = reserved_quantity + $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`, quantity, dealID)
return tx.Commit()
}
๐ S1-021: Dockerfile ๋ช
์ธ
# backend/Dockerfile
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
# Runtime stage
FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
๐ S1-023: EKS ๋ช
์ธ
# terraform/eks.tf
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "19.0.0"
cluster_name = "bbossiregi-eks"
cluster_version = "1.29"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
eks_managed_node_groups = {
default = {
name = "default-ng"
instance_types = ["t3.medium"]
min_size = 2
max_size = 4
desired_size = 2
}
}
}
Day 5 (2/28 ๊ธ) - ๋ฐฐํฌ ๋ฐ HTTPS
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S1-024 | P1 | K8s Deployment ๋งค๋ํ์คํธ | ์ธํ๋ผ | k8s/deployment.yaml | replicas: 2 |
S1-025 | P1 | K8s Service ๋งค๋ํ์คํธ | ์ธํ๋ผ | k8s/service.yaml | ClusterIP |
S1-026 | P1 | ALB Ingress Controller | ์ธํ๋ผ | k8s/ingress.yaml | ALB ์๋ ์์ฑ |
S1-027 | P1 | ACM ์ธ์ฆ์ ๋ฐ๊ธ | ์ธํ๋ผ | Terraform ์ฝ๋ | *.bbossiregi.com |
S1-028 | P2 | Route53 ๋๋ฉ์ธ ์ฐ๊ฒฐ | ์ธํ๋ผ | Terraform ์ฝ๋ | api.bbossiregi.com |
S1-029 | P1 | Lambda ํ์๋ ์ค์ผ์ค๋ฌ | ์ธํ๋ผ | lambda/scheduler.py | EventBridge ์ฐ๋ |
S1-030 | P0 | Sprint 1 ํ๊ณ | ์ ์ฒด | KPT ๋ฌธ์ | ํ ํ๊ณ ์๋ฃ |
๐ S1-024: Deployment ๋ช
์ธ
# k8s/backend/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: bbossiregi
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: 123456789.dkr.ecr.ap-northeast-2.amazonaws.com/bbossiregi-backend:v1
ports:
- containerPort: 8080
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-credentials
key: host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
๐ S1-029: Lambda ์ค์ผ์ค๋ฌ ๋ช
์ธ
# lambda/scheduler.py
import boto3
import psycopg2
from datetime import datetime
def handler(event, context):
"""
EventBridge์์ ๋งค ๋ถ๋ง๋ค ํธ์ถ
ํ์๋ ์ํ ์๋ ๋ณ๊ฒฝ
"""
conn = get_db_connection()
cur = conn.cursor()
now = datetime.utcnow()
# scheduled โ active
cur.execute("""
UPDATE time_deals
SET status = 'active', updated_at = %s
WHERE status = 'scheduled'
AND start_at <= %s AND end_at > %s
""", (now, now, now))
activated = cur.rowcount
# active โ ended
cur.execute("""
UPDATE time_deals
SET status = 'ended', updated_at = %s
WHERE status = 'active' AND end_at <= %s
""", (now, now))
ended = cur.rowcount
# active โ soldout
cur.execute("""
UPDATE time_deals
SET status = 'soldout', updated_at = %s
WHERE status = 'active'
AND stock_quantity <= reserved_quantity + sold_quantity
""", (now,))
soldout = cur.rowcount
conn.commit()
return {
'activated': activated,
'ended': ended,
'soldout': soldout
}
๐๏ธ Sprint 2: ํ๋ก ํธ์๋ + ํ
์คํธ (Day 6-10)
Day 6 (3/3 ์) - ํ๋ก ํธ์๋ ๊ธฐ๋ฐ
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S2-001 | P0 | React ํ๋ก์ ํธ ์ด๊ธฐํ | ํ๋ก ํธ | package.json | Vite + React 19 |
S2-002 | P1 | ๋ผ์ฐํฐ ์ค์ | ํ๋ก ํธ | router.jsx | 5๊ฐ ํ์ด์ง ๋ผ์ฐํธ |
S2-003 | P1 | API ํด๋ผ์ด์ธํธ ์ค์ | ํ๋ก ํธ | api/client.js | Axios + ์ธํฐ์
ํฐ |
S2-004 | P1 | ๋ก๊ทธ์ธ/ํ์๊ฐ์
ํ์ด์ง | ํ๋ก ํธ | pages/Auth.jsx | ํผ ์ ํจ์ฑ ๊ฒ์ฌ ํฌํจ |
S2-005 | P1 | ์ ์ญ ์ํ ๊ด๋ฆฌ ์ค์ | ํ๋ก ํธ | store/auth.js | Zustand |
๐ S2-002: ๋ผ์ฐํธ ๋ช
์ธ
// src/router.jsx
const routes = [
{ path: '/', element: <Home /> }, // ๋ฉ์ธ (ํ์๋ ๋ชฉ๋ก)
{ path: '/login', element: <Login /> }, // ๋ก๊ทธ์ธ
{ path: '/register', element: <Register /> }, // ํ์๊ฐ์
{ path: '/deals/:id', element: <DealDetail /> }, // ํ์๋ ์์ธ
{ path: '/orders', element: <OrderList /> }, // ๋ด ์ฃผ๋ฌธ ๋ชฉ๋ก
{ path: '/admin', element: <AdminDashboard /> }, // ๊ด๋ฆฌ์ (Protected)
];
Day 7 (3/4 ํ) - ํ์๋ UI
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S2-006 | P1 | ํ์๋ ๋ชฉ๋ก ํ์ด์ง | ํ๋ก ํธ | pages/Home.jsx | ์นด๋ ๊ทธ๋ฆฌ๋ ๋ ์ด์์ |
S2-007 | P1 | ์นด์ดํธ๋ค์ด ํ์ด๋จธ ์ปดํฌ๋ํธ | ํ๋ก ํธ | components/Countdown.jsx | ์ค์๊ฐ ์
๋ฐ์ดํธ |
S2-008 | P1 | ํ์๋ ์์ธ ํ์ด์ง | ํ๋ก ํธ | pages/DealDetail.jsx | ์ฌ๊ณ ํ๋ก๊ทธ๋ ์ค๋ฐ |
S2-009 | P1 | ๊ตฌ๋งค ๋ฒํผ ์ปดํฌ๋ํธ | ํ๋ก ํธ | components/BuyButton.jsx | ์ํ๋ณ UI ๋ถ๊ธฐ |
๐ S2-007: ์นด์ดํธ๋ค์ด ๋ช
์ธ
// components/Countdown.jsx
// Props: targetTime (ISO string)
// ํ์ ํ์: "01:23:45" (์:๋ถ:์ด)
// ์์ ๊ท์น:
// - 1์๊ฐ ์ด์: ๊ฒ์
// - 10๋ถ ์ดํ: ์ฃผํฉ
// - 1๋ถ ์ดํ: ๋นจ๊ฐ + ๊น๋นก์
// ์ข
๋ฃ ์: "์ข
๋ฃ๋จ" ํ์
๐ S2-009: ๊ตฌ๋งค ๋ฒํผ ์ํ
// ์ํ๋ณ ๋ฒํผ UI
{
'scheduled': { text: '์คํ ์์ ', disabled: true, color: 'gray' },
'active': { text: '๊ตฌ๋งคํ๊ธฐ', disabled: false, color: 'black' },
'soldout': { text: 'ํ์ ', disabled: true, color: 'red' },
'ended': { text: '์ข
๋ฃ๋จ', disabled: true, color: 'gray' },
}
Day 8 (3/5 ์) - ์ฃผ๋ฌธ ํ๋ก์ฐ UI
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S2-010 | P1 | ์ฃผ๋ฌธ ์์ฑ ํ๋ก์ฐ | ํ๋ก ํธ | hooks/useOrder.js | ๋๊ด์ UI ์
๋ฐ์ดํธ |
S2-011 | P1 | ์ฃผ๋ฌธ ๊ฒฐ๊ณผ ๋ชจ๋ฌ | ํ๋ก ํธ | components/OrderModal.jsx | ์ฑ๊ณต/์คํจ ๋ถ๊ธฐ |
S2-012 | P1 | ๋ด ์ฃผ๋ฌธ ๋ชฉ๋ก ํ์ด์ง | ํ๋ก ํธ | pages/OrderList.jsx | ์ํ๋ณ ํํฐ๋ง |
S2-013 | P2 | ํ ์คํธ ์๋ฆผ ์ปดํฌ๋ํธ | ํ๋ก ํธ | components/Toast.jsx | ์ฑ๊ณต/์๋ฌ/์ ๋ณด |
Day 9 (3/6 ๋ชฉ) - ํ
์คํธ ๊ธฐ๋ฐ
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S2-014 | P1 | ๋จ์ ํ
์คํธ ์ค์ (๋ฐฑ์๋) | ๋ฐฑ์๋ | *_test.go | go test ์คํ ๊ฐ๋ฅ |
S2-015 | P1 | ์ฌ๊ณ ์์ฝ ํ
์คํธ | ๋ฐฑ์๋ | stock_test.go | ๋์์ฑ ํ
์คํธ ํฌํจ |
S2-016 | P1 | API ํตํฉ ํ
์คํธ | ๋ฐฑ์๋ | integration_test.go | ์ฃผ์ ํ๋ก์ฐ ์ปค๋ฒ |
S2-017 | P2 | ํ๋ก ํธ์๋ ํ
์คํธ ์ค์ | ํ๋ก ํธ | vitest.config.js | Vitest |
๐ S2-015: ๋์์ฑ ํ
์คํธ ๋ช
์ธ
// services/stock_test.go
func TestConcurrentReservation(t *testing.T) {
// Given: ์ฌ๊ณ 10๊ฐ์ธ ํ์๋
deal := createTestDeal(stock: 10)
// When: ๋์์ 15๋ช
์ด 1๊ฐ์ฉ ์์ฝ ์๋
var wg sync.WaitGroup
successCount := atomic.Int32{}
failCount := atomic.Int32{}
for i := 0; i < 15; i++ {
wg.Add(1)
go func() {
defer wg.Done()
err := stockService.Reserve(ctx, deal.ID, 1)
if err == nil {
successCount.Add(1)
} else {
failCount.Add(1)
}
}()
}
wg.Wait()
// Then: ์ ํํ 10๋ช
๋ง ์ฑ๊ณต
assert.Equal(t, int32(10), successCount.Load())
assert.Equal(t, int32(5), failCount.Load())
// And: ์ฌ๊ณ ์ ํฉ์ฑ ์ ์ง
deal = getDeal(deal.ID)
assert.Equal(t, 10, deal.ReservedQuantity)
}
Day 10 (3/7 ๊ธ) - CI/CD ๊ตฌ์ถ
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S2-018 | P1 | GitHub Actions ์ํฌํ๋ก์ฐ | ์ธํ๋ผ | .github/workflows/ci.yml | ํ
์คํธ + ๋น๋ |
S2-019 | P1 | ECR ํธ์ ์๋ํ | ์ธํ๋ผ | .github/workflows/cd.yml | main ๋ธ๋์น ๋จธ์ง ์ |
S2-020 | P1 | K8s ๋ฐฐํฌ ์๋ํ | ์ธํ๋ผ | .github/workflows/cd.yml | kubectl apply |
S2-021 | P2 | Slack ์๋ฆผ ์ฐ๋ | ์ธํ๋ผ | .github/workflows/*.yml | ์ฑ๊ณต/์คํจ ์๋ฆผ |
S2-022 | P0 | Sprint 2 ํ๊ณ | ์ ์ฒด | KPT ๋ฌธ์ | ํ ํ๊ณ ์๋ฃ |
๐ S2-018: CI ์ํฌํ๋ก์ฐ ๋ช
์ธ
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
jobs:
test-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- run: cd backend && go test -v ./...
test-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: cd frontend && npm ci && npm test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: golangci/golangci-lint-action@v4
with:
working-directory: backend
๐๏ธ Sprint 3: ๋ชจ๋ํฐ๋ง + ๋ถํํ
์คํธ (Day 11-15)
Day 11-12: ๋ชจ๋ํฐ๋ง ์คํ
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S3-001 | P1 | Prometheus ์ค์น | ์ธํ๋ผ | helm chart | ๋ฉํธ๋ฆญ ์์ง ํ์ธ |
S3-002 | P1 | Grafana ์ค์น | ์ธํ๋ผ | helm chart | ๋์๋ณด๋ ์ ์ ๊ฐ๋ฅ |
S3-003 | P1 | ๋ฐฑ์๋ ๋ฉํธ๋ฆญ ๋
ธ์ถ | ๋ฐฑ์๋ | /metrics ์๋ํฌ์ธํธ | Prometheus ํฌ๋งท |
S3-004 | P1 | Grafana ๋์๋ณด๋ ๊ตฌ์ฑ | ์ธํ๋ผ | dashboard.json | RPS, P99, ์๋ฌ์จ |
S3-005 | P2 | CloudWatch ๋ก๊ทธ ์ฐ๋ | ์ธํ๋ผ | Fluent Bit | ๋ก๊ทธ ์ค์ํ |
๐ S3-003: ๋ฐฑ์๋ ๋ฉํธ๋ฆญ ๋ช
์ธ
// ๋
ธ์ถํ ๋ฉํธ๋ฆญ
http_requests_total{method, path, status} // ์์ฒญ ์
http_request_duration_seconds{method, path} // ์๋ต ์๊ฐ
stock_reservation_total{deal_id, result} // ์ฌ๊ณ ์์ฝ ์
active_deals_count // ํ์ฑ ํ์๋ ์
Day 13-14: ๋ถํ ํ
์คํธ
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S3-006 | P1 | k6 ํ
์คํธ ์คํฌ๋ฆฝํธ ์์ฑ | ์ ์ฒด | k6/load-test.js | ์๋๋ฆฌ์ค 3๊ฐ |
S3-007 | P1 | ๋ถํ ํ
์คํธ ์คํ | ์ ์ฒด | ๊ฒฐ๊ณผ ๋ฆฌํฌํธ | 1000 VU ํ
์คํธ |
S3-008 | P1 | ๋ณ๋ชฉ ์ง์ ๋ถ์ | ์ ์ฒด | ๋ถ์ ๋ฌธ์ | ๊ฐ์ ํฌ์ธํธ ๋์ถ |
S3-009 | P1 | ์ฑ๋ฅ ์ต์ ํ ์ ์ฉ | ๋ฐฑ์๋ | ์ฝ๋ ์์ | P99 < 500ms |
๐ S3-006: k6 ํ
์คํธ ์๋๋ฆฌ์ค
// k6/load-test.js
// ์๋๋ฆฌ์ค 1: ํ์๋ ์กฐํ ๋ถํ
export function browsing() {
http.get(`${BASE_URL}/api/v1/timedeals`);
sleep(1);
}
// ์๋๋ฆฌ์ค 2: ๋์ ๊ตฌ๋งค ๋ถํ (ํ์๋ ์คํ ์๋ฎฌ๋ ์ด์
)
export function purchase() {
const res = http.post(`${BASE_URL}/api/v1/orders`, JSON.stringify({
time_deal_id: 1,
quantity: 1
}), { headers: { 'Content-Type': 'application/json' } });
check(res, {
'status is 201 or 409': (r) => r.status === 201 || r.status === 409,
});
}
// ์๋๋ฆฌ์ค 3: ํผํฉ ๋ถํ
export const options = {
scenarios: {
browsing: {
executor: 'constant-vus',
vus: 100,
duration: '5m',
exec: 'browsing',
},
purchase: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '30s', target: 500 }, // 30์ด๊ฐ 500๋ช
๊น์ง ์ฆ๊ฐ
{ duration: '1m', target: 1000 }, // 1๋ถ๊ฐ 1000๋ช
๊น์ง ์ฆ๊ฐ
{ duration: '30s', target: 0 }, // 30์ด๊ฐ ๊ฐ์
],
exec: 'purchase',
},
},
};
Day 15: HPA + ์ค์ผ์ผ๋ง ํ
์คํธ
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S3-010 | P1 | HPA ์ค์ | ์ธํ๋ผ | k8s/hpa.yaml | CPU 70% ๊ธฐ์ค |
S3-011 | P1 | ์ค์ผ์ผ๋ง ํ
์คํธ | ์ธํ๋ผ | ํ
์คํธ ๊ฒฐ๊ณผ | 30์ด ๋ด ์ค์ผ์ผ ์์ |
S3-012 | P2 | PDB ์ค์ | ์ธํ๋ผ | k8s/pdb.yaml | minAvailable: 1 |
S3-013 | P0 | Sprint 3 ํ๊ณ + ์ค๊ฐ ๋ฐํ ์ค๋น | ์ ์ฒด | ๋ฐํ ์๋ฃ | 3/9 ์ค๊ฐ ๋ฐํ |
๐ S3-010: HPA ๋ช
์ธ
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: backend-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: backend
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 4
periodSeconds: 60
๐๏ธ Sprint 4: ๋ง๋ฌด๋ฆฌ + ๋ฐํ (Day 16-20)
Day 16-17: ๋ฌธ์ํ
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S4-001 | P0 | ์ํคํ
์ฒ ๋ฌธ์ ์ต์ข
ํ | ์ ์ฒด | ADR ๋ฌธ์ | ์์ฌ๊ฒฐ์ ๊ธฐ๋ก |
S4-002 | P0 | API ๋ฌธ์ ์์ฑ | ๋ฐฑ์๋ | Swagger UI | ์ ์ฒด API ๋ฌธ์ํ |
S4-003 | P0 | ์ด์ ๊ฐ์ด๋ ์์ฑ | ์ธํ๋ผ | RUNBOOK.md | ์ฅ์ ๋์ ์ ์ฐจ |
S4-004 | P1 | README ์์ฑ | ์ ์ฒด | README.md | ์ค์น/์คํ ๊ฐ์ด๋ |
Day 18-19: ๋ฐํ ์ค๋น
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S4-005 | P0 | ๊ฒฐ๊ณผ ๋ณด๊ณ ์ ์์ฑ | ์ ์ฒด | ๋ณด๊ณ ์.pdf | ํ
ํ๋ฆฟ ์ค์ |
S4-006 | P0 | ๋ฐํ PPT ์์ฑ | ์ ์ฒด | ๋ฐํ.pptx | 15๋ถ ๋ถ๋ |
S4-007 | P0 | ์์ฐ ์์ ์ ์ | ์ ์ฒด | demo.mp4 | 3-5๋ถ |
S4-008 | P1 | ๋ฐํ ๋ฆฌํ์ค | ์ ์ฒด | - | 2ํ ์ด์ |
Day 20 (3/19): ์ต์ข
๋ฐํ
| ID | ์ฐ์ ์์ | ํ์คํฌ | ๋ด๋น | ์ฐ์ถ๋ฌผ | ์๋ฃ ๊ธฐ์ค |
|---|
S4-009 | P0 | ์ต์ข
๋ฐํ | ์ ์ฒด | - | 15๋ถ ๋ฐํ ์๋ฃ |
S4-010 | P0 | ๊ฒฐ๊ณผ๋ฌผ ์ ์ถ | ํ์ฅ | ๊ตฌ๊ธํผ | 13:00๊น์ง |
S4-011 | P0 | ํ๋ก์ ํธ ํ๊ณ | ์ ์ฒด | KPT ๋ฌธ์ | ์ต์ข
ํ๊ณ |
๐ Definition of Done (DoD)
์ฝ๋
์ธํ๋ผ
๋ฌธ์