- 분야: DB
- 기술 스택: TypeScript · Cloudflare D1 · SQL
문제와 원인
오답 목록은 created_at DESC, id DESC로 정렬했지만 다음 페이지는 id < 마지막 ID만 검사했다. 첫 페이지의 801보다 과거에 생성된 803·802·804가 모두 ID 조건에서 빠져 두 번째 페이지가 비었다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
| 정렬 순서 | 기록 ID | 기록 시각 UTC |
|---|---|---|
| 1 | 801 | 2026-01-04T00:00:00Z |
| 2 | 803 | 2026-01-03T00:00:00Z |
| 3 | 802 | 2026-01-03T00:00:00Z |
| 4 | 804 | 2026-01-01T00:00:00Z |
수정 전 코드
readLatestIncorrectAttempts
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
AND id < ?
수정 후 코드
readLatestIncorrectAttempts
커서보다 과거인 기록을 포함하고, 같은 시각이면 더 작은 ID를 포함하도록 정렬과 비교 조건을 맞췄다. 마지막 기록의 시각을 두 번, ID를 한 번 바인딩한다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
AND (created_at < ? OR (created_at = ? AND id < ?))
ORDER BY created_at DESC, id DESC
encodeAttemptCursor
커서는 마지막 기록의 [기록 시각, ID]로 인코딩한다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
export function encodeAttemptCursor(row: AttemptCursor) {
return JSON.stringify([row.createdAt, row.id]);
}
이전 숫자 커서는 해당 사용자·시험의 기록에서 시각을 조회해 복합 커서로 변환한다. 해석할 수 없는 커서는 HTTP 400으로 거부한다.
검증 결과
Wrangler의 로컬 D1에 합성 본인 오답 4건과 다른 사용자의 ID 899 기록 1건을 넣었다. 원본 전후 readLatestIncorrectAttempts를 페이지 크기 1로 실행하고 각 페이지의 마지막 기록을 다음 커서로 전달했다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
| 비교 항목 | 수정 전 | 수정 후 |
|---|---|---|
| 첫 페이지 | [801] | [801] |
| 후속 페이지 | 두 번째 페이지 []에서 종료 | [803] → [802] → [804] |
| 같은 시각의 803·802 | 두 기록 누락 | 803 다음 802 반환 |
| 과거 시각의 큰 ID 804 | 누락 | 마지막 페이지에 반환 |
| 대상 4건 중 반환 | 1/4건 | 4/4건 |
| 누락 | 3건 | 0건 |
| 중복·다른 사용자 기록 | 각각 0건 | 각각 0건 |
같은 입력 행의 SQL 비교에서 before_cursor·after_cursor 값 1은 첫 기록 다음에 포함되는 행을 뜻한다. 기존 조건은 나머지 3건을 제외했고 복합 조건은 모두 포함했다.