/code-review high 1회차 · Claude Code · Opus 5.5 · 클로드의 마지막 답 원문 · 목록

points.py에서 10건을 찾았고, 그중 4건은 바로 고쳐야 할 버그예요. 이 4건은 직접 코드를 돌려 재현까지 했어요. 수정은 하지 않았어요.

  1. 생일 적립률이 계속 2배로 불어나요 (points.py:35): 생일날 결제할 때마다 적립률을 지갑에 덮어써서 원래대로 돌아오지 않아요. 생일에 1만 원씩 세 번 결제하면 1000, 2000, 2000(하루 한도)이 적립되고, 다음 날 1만 원 결제에도 500이 아니라 4000이 쌓여요.
  2. 포인트를 쓰면 만료된 적립분부터 깎여요 (points.py:54): 잔액 확인은 유효한 포인트만 세는데, 차감은 만료된 적립분부터 해요. 500을 쓰고도 잔액이 500 그대로라서 같은 포인트를 또 쓸 수 있어요.
  3. 하루 적립 한도가 우회돼요 (points.py:37): 오늘 적립한 양을 "적립한 양"이 아니라 "지금 남은 양"으로 세요. 5000을 적립하고 같은 날 5000을 쓰면 또 5000을 적립할 수 있어요.
  4. 음수를 넣으면 포인트가 생겨요 (points.py:51): 500을 적립한 뒤 use(-1000)을 하면 잔액이 1500이 돼요.

나머지 6건이에요. - 유효기간이 하루 짧아요 (points.py:45): 규칙은 "365일째 되는 날까지"인데 실제로는 364일째까지만 쓸 수 있어요. 기존 코드부터 있던 문제예요. - 2월 29일생은 평년에 생일 2배를 못 받아요: 평년에 2월 28일이나 3월 1일로 칠지 사장님께 확인이 필요해요. - 하루 한도를 채운 뒤에도 0포인트 기록이 계속 쌓여요. - 적립·사용 때마다 전체 기록을 다시 훑고 정렬해요: 오래된 손님일수록 느려져요. - 적립률을 지갑에 따로 저장할 필요가 없어요: 1번 버그의 원인이기도 해요. - 쓰지 않는 date import가 남아 있어요.

고칠 때는 적립률을 earn() 안에서 그때그때 계산하고, 적립 기록마다 처음 적립한 양과 남은 양을 따로 저장하면 1번과 3번이 함께 해결돼요.

찾은 문제 목록 (ReportFindings 도구로 넘긴 원문, 10건)

#위치분류요약일어나는 상황
1points.py:35정확성(버그)Birthday doubling permanently mutates self.rate
`self.rate = self.rate * 2` doubles the instance's rate permanently and never resets it, so the rate keeps compounding on every birthday purchase and stays inflated after the birthday.
Reproduced: birthday 3/1, three 10,000-won purchases on 2026-03-01 earn 1000, 2000, 2000 (cap) instead of 1000 each; a 10,000-won purchase on 3/2 earns 4000 (rate 0.4) instead of 500. Use a local `rate = POINT_RATE * (2 if birthday else 1)` instead of instance state.
2points.py:54정확성(버그)use() deducts from expired entries
use() checks against balance(), which excludes expired entries, but then deducts oldest-first from ALL entries, including expired ones, so valid points are not reduced.
Reproduced: earn 500 on 2025-01-01 (expired) and 500 on 2026-01-10, then use(500) on 2026-01-10. The expired entry goes to 0 and the valid entry stays 500, so the balance is still 500 after spending 500. The customer can spend the same points again.
3points.py:37정확성(버그)Daily cap counts remaining, not earned, points
earned_today sums entry[1], which use() decrements, so it counts what is left rather than what was earned today. Spending points reopens room under the daily cap.
Reproduced: earn 5000 (cap) on 2026-01-01, use(5000) the same day, earn again on the same day and get another 5000. A customer can earn without limit by alternating earn and use. Store the earned amount separately from the remaining amount.
4points.py:51정확성(버그)use() accepts negative points and mints balance
use() never checks that points is positive. A negative value passes the balance check, and `min(entry[1], left)` turns negative, so the entry grows.
Reproduced: earn 500, then use(-1000) leaves a balance of 1500. Any caller passing a negative or reversed value creates points from nothing. earn() also accepts a negative amount and records negative points.
5points.py:45정확성(버그)Expiry check is off by one (364 days, not 365)
`(today - d).days < EXPIRE_DAYS - 1` keeps points only for days 0..363, which is 364 days, but the rule says they can be used through day 365 (days == 364).
Points earned on 2026-01-01 should be usable through 2026-12-31 (day 365). On 2026-12-31 the diff is 364 days, 364 < 364 is False, so balance() drops them and use() refuses a valid spend. The condition should be `< EXPIRE_DAYS`.
6points.py:29정확성(버그)Feb 29 birthdays never get the bonus
_is_birthday compares month/day exactly, so a Feb 29 birthday never matches in non-leap years.
A customer born 2000-02-29 gets the 2x bonus only once every 4 years. The rule for non-leap years (Feb 28 or Mar 1) is undefined, and this code silently picks 'never'.
7points.py:39정확성(버그)Zero-point entries appended once cap is hit
Once the daily cap is reached, earn() still appends a [today, 0] entry for every later purchase.
A regular customer making many purchases after hitting the cap accumulates useless zero entries. Each one slows down every earn() sum and every use() sort over the wallet's lifetime.
8points.py:37효율Full scans/sort of entries on every earn/use
earn() scans every entry to compute today's total, and use() re-sorts the whole list and calls balance() (another full scan) on every call, even though entries are appended chronologically.
Work grows linearly with lifetime purchase history and expired entries are never pruned, so long-time customers get progressively slower operations. Keep a per-day earned counter, rely on append order (or bisect), and prune expired entries.
9points.py:23단순화self.rate is unnecessary instance state
self.rate is derivable from POINT_RATE and the birthday check, so storing it as mutable instance state adds nothing and is what caused the compounding bug.
Any future code reading wallet.rate sees a stale, inflated value. Remove the attribute and compute the rate locally in earn().
10points.py:10단순화Unused `date` import
`from datetime import date` is imported but never used in the module.
Dead import adds lint noise; remove it or use it for type hints.