목록 · /engineering:debug · Claude Code · Opus 5.5 · 생각 깊이 중간 · 마지막 답 원문
디버그 리포트: 주문 내역 1쪽에 최근 주문이 안 보임
list_orders의 페이지 계산이 한 쪽씩 밀려 있었고, 정렬 기준도 없었습니다. 둘 다 고쳤고, 고친 뒤 다시 돌려서 확인했습니다.
재현
주문 25개짜리 테스트 DB를 scratchpad에 따로 만들어 list_orders(user_id, page)를 호출했습니다. 실제 shop.db는 건드리지 않았습니다.
| 1쪽 | 2쪽 | |
|---|---|---|
| 기대 | 최신 주문 20개 (25→6) | 나머지 5개 (5→1) |
| 수정 전 | [21, 22, 23, 24, 25] (5개만) |
[] |
| 수정 후 | [25, 24, … , 6] (20개) |
[5, 4, 3, 2, 1] |
수정 전에는 1~20번 주문이 어느 쪽에서도 보이지 않았습니다.
원인
shop.py:25 list_orders에 문제가 두 개 있었습니다.
- 페이지 번호 계산이 하나 밀림. 쪽 번호는 1부터 받는데, 계산은 0부터 시작하는 것처럼
offset = page * PAGE_SIZE로 하고 있었습니다. 그래서 1쪽을 열면 앞의 20개를 건너뛰고 21번째부터 보여 줬습니다. 주문이 25개면 1쪽에 5개만 나오고 2쪽은 비고, 처음 20개는 볼 방법이 없었습니다. ORDER BY가 없음. 정렬 기준이 없으면 SQLite가 행을 돌려주는 순서는 보장되지 않습니다. 인덱스나 VACUUM 상태에 따라 바뀝니다. 그래서 "1쪽 = 최근 주문"이라는 전제 자체가 성립하지 않았습니다. 테스트 DB에서는 우연히 최신 5개가 1쪽에 나왔지만, 실제 DB에서는 오래된 주문 5개가 나와도 이상하지 않습니다. 손님이 말한 "최근 주문이 하나도 안 보인다"는 이 두 문제가 겹쳐서 생긴 것으로 봅니다.
수정 내용 (shop.py)
if page < 1:
raise ValueError("page는 1부터 시작")
offset = (page - 1) * PAGE_SIZE
orders = db.execute(
"SELECT id, created_at FROM orders WHERE user_id = ?"
" ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?",
...
- 페이지 계산을 1부터 시작하는 방식으로 바로잡았습니다.
- 최신순으로 정렬하게 했습니다. 주문 시각이 같을 때도 순서가 흔들리지 않도록
id DESC를 두 번째 기준으로 넣었습니다. page가 0이나 음수로 들어오면 음수 offset으로 이상한 결과를 내는 대신 바로 오류를 냅니다.
확인할 점: 지금까지 화면에서 page=0을 첫 쪽으로 넘기고 있었다면, 이제는 오류가 납니다. 호출하는 쪽이 1부터 넘기는지 확인해 주세요.
재발 방지
- 회귀 테스트: 주문 25개를 넣고 1쪽이 최신 20개, 2쪽이 나머지 5개, 3쪽이 빈 목록인지 검사합니다. 같은 시각 주문이 섞여도 순서가 같은지도 봅니다. 재현 스크립트(
scratchpad/repro.py)를 그대로 테스트로 옮기면 됩니다. - 성능: 정렬이 추가됐으니 주문이 많아지면
orders(user_id, created_at DESC, id DESC)인덱스를 두는 게 좋습니다.
이번 범위 밖이지만 같은 파일에서 본 문제
요청 범위가 아니라서 손대지 않았습니다. 원하면 이어서 고치겠습니다.
- SQL 인젝션: search_products가 f-string으로 SQL을 만듭니다.
- 남의 주문 조회: get_order가 current_user_id를 받기만 하고 검사하지 않아서, 다른 손님의 주문도 조회됩니다.
- 경로 조작: download_invoice에서 ../가 들어간 파일명으로 다른 디렉터리의 파일을 읽을 수 있습니다.
- 비밀값 노출: ADMIN_TOKEN이 코드에 그대로 박혀 있습니다. 비교도 일반 ==라서 타이밍 공격에 취약합니다.
- 주문 처리 예외: place_order는 없는 상품이면 None에서 터지고, 동시에 주문이 들어오면 재고가 꼬일 수 있습니다.
- 쿠폰 오류 숨김: apply_coupon이 오류가 나면 조용히 None을 돌려줍니다.
- N+1 쿼리: list_orders가 주문마다 상품 목록을 따로 조회합니다.