동작 시나리오: 주문
- Admin은 고객의 배송상태를 변경할 수 있다.
- 회원은 장바구니에 담은 물건을 주문할 수 있다.
- 각 주문에 대해 주문일자, 배송지, 주문한 상품 총 결제 금액을 확인할 수 있다.
- 회원은 자신이 주문했던 전체 내역을 조회해볼 수 있다.
- '최근 6개월' 등 특정 기간을 설정하여 최신순으로 조회할 수 있다.
- 회원은 자신의 특정 주문에 대해서 상태(배송중, 주문취소 등) 등을 상세 조회할 수 있다.
- 회원은 언제든지 본인이 주문한 상품을 주문 취소할 수 있다.
회원 배송상태 변경
UPDATE order_item
SET status_id = {변경할 상태 ID}
WHERE order_item_id = {클릭한 주문상품 ID};
회원 주문 생성
INSERT INTO orders (member_id, order_date, shipping_address, total_price)
VALUES ({회원 ID}, NOW(), {입력한 배송지 주소}, {계산된 총 결제 금액});
-- 장바구니에서 선택한 상품의 개수만큼 반복
INSERT INTO order_item (order_id, product_id, status_id, quantity, order_price)
VALUES ({생성된 주문 ID}, {상품 ID}, 1, {선택 수량}, {주문시점 가격});
-- 장바구니 비우기
DELETE FROM cart WHERE cart_id = {주문한 장바구니ID};
자신의 주문 내역 특정 기간 최신순 조회
SELECT *
FROM orders
WHERE member_id = {회원 ID}
AND order_date >= DATE_SUB(NOW(), INTERVAL {설정기간(ex. 6)} MONTH)
ORDER BY order_date DESC;
자신의 특정 주문 내역 조회
SELECT
o.order_id, o.order_date, o.shipping_address, o.total_price,
p.product_name, oi.quantity, oi.order_price, os.status_name
FROM orders o
JOIN order_item oi ON o.order_id = oi.order_id
JOIN product p ON oi.product_id = p.product_id
JOIN order_status os ON oi.status_id = os.status_id
WHERE o.order_id = {주문 ID} AND o.member_id = {회원 ID};
회원의 주문 취소
UPDATE order_item
SET status_id = 5
WHERE order_item_id = {취소할 주문상품 ID};
시간별 매출 통계 조회
-- 특정 날짜 하루동안 시간에 따른 매출 통계 조회
SELECT
DATE_FORMAT(order_date, '%Y-%m-%d %H') AS od,
SUM(total_price) AS revenue,
COUNT(order_id) AS order_count
FROM orders
WHERE order_date BETWEEN '2026-05-23 00:00:00' AND '2026-05-23 23:59:59'
GROUP BY DATE_FORMAT(order_date, '%Y-%m-%d %H:00')
ORDER BY od ASC;

-- 누적 데이터로 시간대별 매출 통계 조회
SELECT
DATE_FORMAT(order_date, '%H') AS od,
SUM(total_price) AS revenue,
COUNT(order_id) AS order_count
FROM orders
WHERE order_date BETWEEN '2026-01-01 00:00:00' AND '2026-05-23 23:59:59'
GROUP BY DATE_FORMAT(order_date, '%H')
ORDER BY od ASC;

일별 매출 통계 조회
SELECT
DATE_FORMAT(order_date, '%Y-%m-%d') AS od,
SUM(total_price) AS revenue,
COUNT(order_id) AS order_count
FROM orders
WHERE order_date BETWEEN '2026-05-01 00:00:00' AND '2026-05-23 23:59:59'
GROUP BY DATE_FORMAT(order_date, '%Y-%m-%d')
ORDER BY od ASC;

월별 매출 통계 조회
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS od,
SUM(total_price) AS revenue,
COUNT(order_id) AS order_count
FROM orders
WHERE order_date BETWEEN '2025-01-01 00:00:00' AND '2026-05-31 23:59:59'
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY od ASC;

연별 매출 통계 조회
SELECT
DATE_FORMAT(order_date, '%Y') AS od,
SUM(total_price) AS revenue,
COUNT(order_id) AS order_count
FROM orders
WHERE order_date BETWEEN '2020-01-01 00:00:00' AND '2026-12-31 23:59:59'
GROUP BY DATE_FORMAT(order_date, '%Y')
ORDER BY od ASC;

schema.sql
-- 주문상태 테이블
CREATE TABLE order_status (
status_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
status_name VARCHAR(50) NOT NULL UNIQUE
);
-- 주문 테이블
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
order_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
member_id BIGINT NOT NULL,
order_date DATETIME(6) NOT NULL,
shipping_address VARCHAR(255) NOT NULL,
total_price BIGINT NOT NULL,
FOREIGN KEY (member_id)
REFERENCES member(member_id)
ON DELETE RESTRICT
);
CREATE INDEX idx_orders_order_date ON orders(order_date);
CREATE INDEX idx_orders_member_id_order_date ON orders(member_id, order_date);
-- 주문목록 테이블
DROP TABLE IF EXISTS order_item;
CREATE TABLE order_item (
order_item_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
status_id BIGINT NOT NULL,
quantity BIGINT NOT NULL DEFAULT 1,
order_price BIGINT NOT NULL,
FOREIGN KEY (order_id)
REFERENCES orders(order_id)
ON DELETE CASCADE,
FOREIGN KEY (product_id)
REFERENCES product(product_id)
ON DELETE RESTRICT,
FOREIGN KEY (status_id)
REFERENCES order_status(status_id)
ON DELETE RESTRICT
);
data.sql
-- [주문상태] 데이터 (고정)
INSERT INTO order_status (status_name)
VALUES ('주문접수'),
('결제완료'),
('배송중'),
('배송완료'),
('주문취소');
-- [주문] 더미 데이터
INSERT INTO orders (member_id, order_date, shipping_address, total_price)
VALUES (1, '2026-05-10 10:30:00', '서울 강남구 테헤란로 123 101호', 239000), -- 1번 주문
(2, '2026-05-12 14:15:22', '부산 해운대구 해운대로 789 303호', 109700), -- 2번 주문
(1, '2026-05-15 09:00:11', '서울 송파구 올림픽로 456 202호', 59700), -- 3번 주문
(4, '2026-05-18 18:45:50', '서울 마포구 홍익로 321 404호', 248000), -- 4번 주문
(2, '2026-05-20 11:20:30', '부산 해운대구 해운대로 789 303호', 124900); -- 5번 주문
-- [주문목록] 더미 데이터
INSERT INTO order_item (order_id, product_id, status_id, quantity, order_price)
VALUES
-- 1번 주문: 트위드 자켓(150,000) 1개 + 플리츠 스커트(89,000) 1개 = 총 239,000원(배송완료)
(1, 1, 4, 1, 150000),
(1, 2, 4, 1, 89000),
-- 2번 주문: 오버사이즈 티셔츠(29,900) 2개 + 슬림핏 청바지(49,900) 1개 = 총 109,700원(배송중)
(2, 4, 3, 2, 29900),
(2, 5, 3, 1, 49900),
-- 3번 주문: 히트텍 롱슬리브(19,900) 3개 = 총 59,700원(주문취소)
(3, 6, 5, 3, 19900),
-- 4번 주문: 드라이핏 반팔(45,000) 2개 + 트랙 재킷(89,000) 1개 + 조거 팬츠(69,000) 1개 = 총 248,000원(결제완료)
(4, 8, 2, 2, 45000),
(4, 9, 2, 1, 89000),
(4, 10, 2, 1, 69000),
-- 5번 주문: 리넨 블라우스(65,000) 1개 + 후리스 집업(59,900) 1개 = 총 124,900원(주문접수)
(5, 3, 1, 1, 65000),
(5, 7, 1, 1, 59900);
1) 특정 회원 특정 주문 상세 조회
SELECT
o.order_id, o.order_date, o.shipping_address, o.total_price,
p.product_name, oi.quantity, oi.order_price, os.status_name
FROM orders o
JOIN order_item oi ON o.order_id = oi.order_id
JOIN product p ON oi.product_id = p.product_id
JOIN order_status os ON oi.status_id = os.status_id
WHERE o.order_id = {주문 ID} AND o.member_id = {회원 ID};

2) 특정 회원의 주문 내역 전체 조회(특정 기간 이후)
SELECT *
FROM orders
WHERE member_id = {회원 ID}
AND order_date >= DATE_SUB(NOW(), INTERVAL {설정기간} MONTH)
ORDER BY order_date DESC;

1) 새로운 주문 생성
INSERT INTO orders (member_id, order_date, shipping_address, total_price)
VALUES ({회원 ID}, NOW(), {입력한 배송지 주소}, {계산된 총 결제 금액});
-- 장바구니에서 선택한 상품의 개수만큼 반복
INSERT INTO order_item (order_id, product_id, status_id, quantity, order_price)
VALUES ({생성된 주문 ID}, {상품 ID}, 1, {선택 수량}, {주문시점 가격});



1) 주문 상태 변경
UPDATE order_item
SET status_id = {변경할 상태 ID}
WHERE order_item_id = {클릭한 주문상품 ID};


2) 사용자가 특정 상품 주문 취소
UPDATE order_item
SET status_id = 5
WHERE order_item_id = {취소하려는 아이템 ID}
AND order_id IN (SELECT order_id FROM orders WHERE member_id = {로그인한 회원 ID});
order_id IN (SELECT order_id FROM orders WHERE member_id = {로그인한 회원 ID}); 이건 취소하려는 아이템이 진짜 이 회원이 주문한 아이템이 맞는지 검증하는 구문.

1) 주문 이력이 있는 회원 삭제 시도

2) 이미 팔린 적이 있는 상품 삭제 시도

schema.sql
-- 장바구니 테이블
DROP TABLE IF EXISTS cart;
CREATE TABLE cart (
member_id BIGINT NOT NULL PRIMARY KEY,
FOREIGN KEY (member_id)
REFERENCES member(member_id)
ON DELETE CASCADE
);
-- 장바구니 아이템 테이블
DROP TABLE IF EXISTS cart_item;
CREATE TABLE cart_item (
cart_item_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
member_id BIGINT NOT NULL,
product_detail_id BIGINT NOT NULL,
quantity BIGINT NOT NULL DEFAULT 1,
CONSTRAINT check_cart_quantity CHECK(quantity>=1),
UNIQUE (member_id, product_detail_id),
FOREIGN KEY (member_id)
REFERENCES cart(member_id)
ON DELETE CASCADE,
FOREIGN KEY (product_detail_id)
REFERENCES product_detail(product_detail_id)
ON DELETE CASCADE
);
data.sql
-- [장바구니] 더미 데이터
INSERT INTO cart (cart_id, member_id)
VALUES (1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5);
-- [장바구니 아이템] 더미 데이터
INSERT INTO cart_item (member_id, product_detail_id, quantity) VALUES
-- 1번 회원
(1, 1, 1),
(1, 3, 2),
(1, 14, 1),
(1, 21, 3),
-- 2번 회원
(2, 5, 1),
(2, 6, 1),
(2, 17, 2),
(2, 25, 1),
(2, 37, 1),
-- 3번 회원
(3, 9, 1),
(3, 10, 1),
(3, 23, 2),
-- 4번 회원
(4, 2, 1),
(4, 13, 2),
(4, 29, 1),
(4, 33, 1),
(4, 38, 1),
-- 5번 회원
(5, 11, 1),
(5, 18, 1),
(5, 31, 2);
1) 내 장바구니 목록 조회
SELECT
ci.cart_item_id,
p.product_name,
pd.image_url,
ci.quantity,
(p.price + pd.surcharge) * ci.quantity AS total_price,
MAX(CASE WHEN od.option_type_id = 1 THEN od.option_value END) AS size_option,
MAX(CASE WHEN od.option_type_id = 2 THEN od.option_value END) AS color_option
FROM cart_item ci
JOIN product_detail pd ON ci.product_detail_id = pd.product_detail_id
JOIN product p ON pd.product_id = p.product_id
JOIN product_option po ON pd.product_detail_id = po.product_detail_id
JOIN option_detail od ON po.option_detail_id = od.option_detail_id
WHERE ci.member_id = {조회하려는 회원 ID}
GROUP BY ci.cart_item_id;

1) 장바구니에 새로운 상품 담기
INSERT INTO cart_item (member_id, product_detail_id, quantity)
VALUES ({회원 ID}, {담으려는 상품상세 ID}, {선택한 수량});


2) 이미 담긴 상품을 또 담으려고 할 때
근데 시중의 앱을 다시 보니 그냥 똑같은 상품이 하나 더 추가됨 → 에러가 나는게 아님
그래서 찾아보니 INSERT하다가 UNIQUE 제약조건에 걸리면 UPDATE하라고 할 수 있다고 함!
INSERT INTO cart_item (member_id, product_detail_id, quantity)
VALUES ({회원 ID}, {상품상세 ID}, {추가할 수량}) AS new_data
ON DUPLICATE KEY UPDATE quantity = cart_item.quantity + new_data.quantity;



1) 장바구니 상품 수량 변경
UPDATE cart_item
SET quantity = {새로운 수량}
WHERE cart_item_id = {변경할 장바구니 아이템 ID};


2) 장바구니 상품 수량을 0개 이하로 수정 시도

1) 장바구니에서 특정 상품 하나 삭제
DELETE FROM cart_item
WHERE cart_item_id = {삭제할 장바구니 아이템 ID};


2) 장바구니에 있는 모든 상품 주문 = 장바구니 내용 삭제
DELETE FROM cart_item
WHERE member_id = {회원 ID};


3) 장바구니에서 몇 개만 선택하여 주문
DELETE FROM cart_item
WHERE member_id = {회원 ID}
AND cart_item_id IN ({선택한 장바구니 아이템 ID 1}, {선택한 장바구니 아이템 ID 2}, ...);


-- 이벤트 스케줄러 활성화
SET GLOBAL event_scheduler = ON;
-- 켜졌는지 확인하는 쿼리 -> ON으로 나와야 함
SHOW VARIABLES LIKE 'event_scheduler';
DELIMITER //
CREATE PROCEDURE AutoUpdateOrderStatus()
BEGIN
-- 주문접수(1) -> 결제완료(2): 주문한 지 1분 경과
UPDATE order_item oi
JOIN orders o ON oi.order_id = o.order_id
SET oi.status_id = 2
WHERE oi.status_id = 1 AND o.order_date <= DATE_SUB(NOW(), INTERVAL 1 MINUTE);
-- 결제완료(2) -> 배송중(3): 주문한 지 24시간 후
UPDATE order_item oi
JOIN orders o ON oi.order_id = o.order_id
SET oi.status_id = 3
WHERE oi.status_id = 2 AND o.order_date <= DATE_SUB(NOW(), INTERVAL 24 HOUR);
-- 배송중(3) -> 배송완료(4): 주문한 지 48시간 후
UPDATE order_item oi
JOIN orders o ON oi.order_id = o.order_id
SET oi.status_id = 4
WHERE oi.status_id = 3 AND o.order_date <= DATE_SUB(NOW(), INTERVAL 48 HOUR);
END //
DELIMITER ;
CREATE EVENT auto_update_order_status_event
ON SCHEDULE EVERY 1 MINUTE
DO CALL AutoUpdateOrderStatus();
⇒ 현재 설계에서는 상태 변경 시점을 추적하는 별도의 컬럼이 없어서 최초 결제일인 orders.order_date를 기준으로 INTERVAL을 각각 1분, 24시간, 48시간으로 누적 계산하여 상태를 업데이트하도록 했습니다.
주문 생성 과정
재고 관리
이걸 이제 프로시저롤 만들면 프로시저 호출만 하면 돼서 편함! 아래 프로시저는 제미나이 만들어줬습니당.
DELIMITER //
CREATE PROCEDURE CheckoutCartWithShipping(
IN p_member_id BIGINT,
IN p_shipping_address VARCHAR(255)
)
BEGIN
-- 변수 선언
DECLARE v_item_total BIGINT DEFAULT 0; -- 상품 총액
DECLARE v_shipping_fee INT DEFAULT 0; -- 배송비
DECLARE v_final_price BIGINT DEFAULT 0; -- 최종 결제액
-- 에러 발생 시 롤백
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT 'FAIL: 데이터베이스 오류로 주문이 취소되었습니다.' AS result;
END;
START TRANSACTION;
-- 결제하는 회원의 등급별 배송비 조회
SELECT mg.shipping_fee INTO v_shipping_fee
FROM member m
JOIN member_grade mg ON m.grade_name = mg.grade_name
WHERE m.member_id = p_member_id;
-- 장바구니에 담긴 상품들의 순수 결제 금액 계산
SELECT SUM((p.price + pd.surcharge) * ci.quantity) INTO v_item_total
FROM cart_item ci
JOIN product_detail pd ON ci.product_detail_id = pd.product_detail_id
JOIN product p ON pd.product_id = p.product_id
WHERE ci.member_id = p_member_id;
-- 장바구니가 비어있다면 롤백
IF v_item_total IS NULL THEN
ROLLBACK;
SELECT 'FAIL: 장바구니가 비어있습니다.' AS result;
ELSE
-- 최종 결제 금액 계산 (상품 총액 + 배송비)
SET v_final_price = v_item_total + v_shipping_fee;
-- 회원 누적 구매 금액 업데이트
UPDATE member
SET total_purchase_amount = total_purchase_amount + v_item_total
WHERE member_id = p_member_id;
-- 재고 차감
UPDATE product_detail pd
JOIN cart_item ci ON pd.product_detail_id = ci.product_detail_id
SET pd.stock_quantity = pd.stock_quantity - ci.quantity
WHERE ci.member_id = p_member_id
AND pd.stock_quantity >= ci.quantity;
-- 주문 생성
INSERT INTO orders (member_id, order_date, shipping_address, total_price)
VALUES (p_member_id, NOW(), p_shipping_address, v_final_price);
-- 주문 상세 내역 다중 삽입
INSERT INTO order_item (order_id, product_id, status_id, quantity, order_price)
SELECT
LAST_INSERT_ID(),
pd.product_id,
1,
ci.quantity,
(p.price + pd.surcharge)
FROM cart_item ci
JOIN product_detail pd ON ci.product_detail_id = pd.product_detail_id
JOIN product p ON pd.product_id = p.product_id
WHERE ci.member_id = p_member_id;
-- 장바구니 비우기
DELETE FROM cart_item WHERE member_id = p_member_id;
COMMIT;
SELECT CONCAT('SUCCESS: 주문 완료. 상품(', v_item_total, '원) + 배송비(', v_shipping_fee, '원) = 총 ', v_final_price, '원 결제됨') AS result;
END IF;
END //
DELIMITER ;
장바구니 파트 CRUD를 작성하였다.