운영 중 4개 이슈 동시 발생 — AI로 데이터 정합성 문제 해결하기
1189회차 데이터가 사라졌다 — 캐시 TTL의 함정
1189회 추첨 다음 날, 앱을 확인해봤더니 새 회차 당첨번호 [9, 19, 29, 35, 37, 38] + 보너스 31이 화면에 안 나오고 있었습니다.
DB를 직접 확인해보니 데이터는 있었어요. WinningStore 테이블에 180건(1등 13건, 2등 167건)이 멀쩡히 들어가 있었습니다. 데이터가 없는 게 아니라 화면에 안 보이는 문제였습니다.
Before — 초급 프롬프트:데이터가 안 보여. 고쳐줘.
After — 중급 프롬프트:
당신은 Supabase + Next.js 캐시 디버깅 전문가입니다.
[상황]
- Supabase DB에 1189회 당첨 데이터 180건이 정상 존재 (SELECT 확인 완료)
- 하지만 앱 화면에서 1189회 데이터가 표시되지 않음
- unstable_cache로 캐싱 사용 중
[현재 캐시 설정]
- /api/lottery/draws: 86,400초 (24시간) + stale-while-revalidate 3,600초
- /api/lottery/winning-stores: 43,200초 (12시간) + stale-while-revalidate 3,600초
- /api/lottery/statistics: 3,600초 (1시간) + stale-while-revalidate 600초
[요청]
1. 데이터는 있는데 화면에 안 나오는 가능 원인
2. 캐시 무효화 방법 (엔드포인트별 TTL이 다른 구조에서)
3. 새 회차 데이터 입력 시 자동으로 캐시가 갱신되는 구조
핵심은 캐시 설정을 구체적으로 알려준 것이었습니다. 당첨 데이터(draws) 엔드포인트의 TTL이 24시간으로 되어 있었는데, 토요일 밤에 새 데이터가 들어와도 일요일 밤까지 이전 캐시를 보여주는 구조였거든요.
AI가 제안한 해결: 데이터 크롤링 완료 후 해당 엔드포인트의 캐시를 DELETE 요청으로 강제 무효화하는 방식. analytics-cached/route.ts에 DELETE 핸들러를 추가해서, 패턴 매칭으로 특정 캐시 키만 날리는 구조를 만들었습니다.
스키마 9개 필드가 문서와 달랐다
같은 날, 통계 페이지도 빈 값을 보여주고 있었습니다. 원인을 추적해보니 마이그레이션 파일(create_winning_summary_stats.sql)에 적힌 필드명과 실제 프로덕션 DB의 필드가 완전히 달랐습니다.
단순히 total_stores vs total_unique_stores 하나가 아니라, 테이블 전체가 달랐어요:
| 마이그레이션 파일 (구버전) | 프로덕션 DB (실제) |
|---|---|
| total_stores | total_unique_stores |
| total_wins | total_rounds |
| auto_first_total | min_round |
| manual_first_total | max_round |
| — | avg_first_per_round |
| — | avg_second_per_round |
| — | last_updated_round |
6개 필드 중 4개 이름이 다르고, 3개 필드는 아예 마이그레이션에 없었습니다.
당신은 데이터베이스 마이그레이션 전문가입니다.
[문제]
- create_winning_summary_stats.sql 마이그레이션 파일의 필드명과
실제 프로덕션 DB의 필드명이 9개 중 7개가 불일치
- 코드에서는 프로덕션 필드명(total_unique_stores, total_rounds 등)을 사용
- 마이그레이션 파일에는 구버전 이름(total_stores, total_wins 등)이 남아있음
[원인 추정]
AI한테 마이그레이션 파일을 먼저 만들게 하고,
이후 Supabase 대시보드에서 직접 컬럼명을 변경했는데
마이그레이션 파일은 업데이트하지 않음
[요청]
1. 프로덕션 스키마를 기준으로 마이그레이션 파일을 재작성
2. 앞으로 스키마 변경 시 마이그레이션 파일과 코드가 동기화되는 워크플로우
AI한테 마이그레이션을 만들게 한 후, 나중에 Supabase 대시보드에서 직접 컬럼명을 바꾸면서 마이그레이션 파일은 안 고친 게 원인이었습니다. 마이그레이션 파일 53줄을 프로덕션 기준으로 재작성하고, 45줄을 삭제했습니다.
이 경험에서 배운 규칙: DB 스키마는 반드시 마이그레이션 파일을 통해서만 변경한다. 대시보드에서 직접 수정하면 문서와 실제가 어긋나고, 다음 배포에서 문제가 터집니다.
중복 주소 69건 — "복권명당우장산역점"이 4개
가장 재밌는 문제였습니다. 당첨 판매점 통계를 만드는데, 같은 가게가 여러 번 카운트되고 있었어요. 5개 조사 스크립트(check-duplicates.js, check-schema.js, deep-dive-issues.js, investigate-winning-stores.js)를 AI한테 만들게 해서 돌려봤더니, 69개의 중복 주소가 발견됐습니다.
"복권명당우장산역점"이라는 판매점이 4번 등장하는데, 주소가 미묘하게 달랐습니다:
서울 강서구 마곡동(가양제1동) 772-8 110호서울 강서구 마곡동 772-8번지서울 강서구 내발산동 719-6번지서울 강서구 내발산동 719-6번지 가판(우리은행옆)
당신은 데이터 정제(cleansing) 전문가입니다.
[문제]
- 판매점 주소에 69개 중복 발견
- 같은 가게인데 주소 표기가 미묘하게 다름
[중복 원인 패턴 4가지]
1. 괄호 안 동 이름: (가양제1동), (가양동) — 같은 곳인데 표기 다름
2. 건물명/층수: "00빌딩 1층", "가판(우리은행옆)"
3. 번지 표기: "772-8번지" vs "772-8 110호"
4. 띄어쓰기 차이
[요청]
1. normalizeAddress() 정규화 함수 — 위 패턴을 모두 처리
2. 정규화 전후 중복 수 비교
3. 기존 데이터에 적용하는 업데이트 스크립트
AI가 만든 첫 번째 버전은 괄호 제거만 했습니다. 테스트해보니 69건에서 45건으로만 줄었어요.
[피드백]
괄호 제거만으로는 부족합니다. 69 → 45로만 줄었어요.
추가로 처리해야 할 패턴:
- "1층", "2층" 등 층수 정보 → 제거
- "00빌딩", "00타워" 등 건물명 → 제거
- "번지" 텍스트 → 통일
- 번지 뒤 "-" 이후 세부번호 → 제거
[요청]
정규화 regex를 확장해주세요. 목표는 69 → 30건 이하.
피드백 후 AI가 만든 최종 정규화 함수:
function normalizeAddress(address) {
return address
.replace(/\s+/g, ' ') // 여러 공백 → 단일 공백
.trim()
.replace(/\s*-\s*/g, '-') // 하이픈 주변 공백 정리
.replace(/\s*,\s*/g, ', ') // 콤마 주변 공백 정리
.replace(/\([^)]*\)/g, '') // 괄호 전체 제거
.replace(/(\d+-\d+)\s*번지?/g, '$1번지') // 번지 표기 통일
.replace(/\s+(가판|상가|점포|호|층|빌딩|건물|옆).*/i, ''); // 상세정보 제거
}
핵심은 마지막 줄입니다. "가판", "상가", "층", "빌딩" 같은 키워드가 나오면 그 뒤 전체를 날리는 방식. "772-8번지 가판(우리은행옆)"에서 "772-8번지"만 남기는 거예요.
이 함수를 크론 잡(api/cron/complete/route.ts)과 통계 스크립트(WinningStoreStats_Update.js) 두 곳에 적용했습니다. 최종 결과: 69건 → 약 25건. 나머지 25건은 실제로 같은 이름의 다른 지점(프랜차이즈)이라 중복이 아니었습니다. "노다지"라는 판매점이 9개 나왔는데, 실제로 서울에만 9개 지점이 있었거든요.
판매점 통계 재집계 — 657줄 스크립트
중복 주소를 정리하고 나면 판매점 통계 전체를 다시 만들어야 했습니다. WinningStoreStats_Update.js가 657줄짜리 배치 스크립트인데, 1,000개 레코드씩 분할 처리하면서 1등 1,004건, 2등 6,451건의 당첨 기록에서 고유 판매점 1,000개의 통계를 재집계했습니다.
당신은 데이터 배치 처리 전문가입니다.
[상황]
- 주소 정규화 적용 후 판매점 통계 전체 재집계 필요
- WinningStore 테이블: 1등 1,004건, 2등 6,451건
- 고유 판매점: 약 1,000개
- 유사도 판단: Levenshtein distance 90% 이상이면 같은 판매점
[요청]
1. 1,000개씩 배치 처리하는 통계 재집계 스크립트
2. 인터넷 복권 판매점은 별도 통합 규칙 적용
3. 등수별(1등/2등), 판매 방법별(자동/수동/반자동) 분류
인터넷 복권 판매점이 특수 케이스였습니다. 오프라인 판매점과 달리 주소가 없고, "인터넷 복권 판매 사이트"라는 텍스트만 있거든요. 이건 별도 통합 규칙으로 하나로 묶어야 했습니다.
8개 파일 변경, 690줄 추가
최종적으로 이 이슈 대응에서 변경된 파일 목록:
| 파일 | 변경 내용 |
|------|----------|
| docs/ISSUE_REPORT_20251115.md (신규) | 289줄 이슈 리포트 |
| scripts/check-duplicates.js (신규) | 113줄 중복 탐지 |
| scripts/check-schema.js (신규) | 29줄 스키마 검증 |
| scripts/deep-dive-issues.js (신규) | 111줄 심층 조사 |
| scripts/investigate-winning-stores.js (신규) | 111줄 판매점 조사 |
| scripts/WinningStoreStats_Update.js | 통계 재집계 수정 |
| src/app/api/cron/complete/route.ts | 정규화 함수 적용 |
| supabase/migrations/create_winning_summary_stats.sql | 53줄 추가, 45줄 삭제 |
총 690줄 추가, 45줄 삭제. 5개 조사 스크립트를 먼저 만들어서 문제를 파악한 다음, 수정 코드를 작성하는 순서였습니다.
4개 이슈의 공통점 — AI가 못 찾는 버그
| 이슈 | 증상 | 원인 | 해결 |
|------|------|------|------|
| 데이터 미표시 | 1189회 안 보임 | 캐시 TTL 24시간 | DELETE 엔드포인트 강제 무효화 |
| 스키마 불일치 | 통계 빈 값 | 대시보드 직접 수정 후 마이그레이션 미반영 | 53줄 재작성 |
| 중복 주소 | 같은 가게 4번 카운트 | 동행복권 주소 표기 불일치 | normalizeAddress() + regex 2단계 개선 |
| 통계 오류 | Top 100 랭킹 왜곡 | 중복 판매점으로 카운트 분산 | 657줄 재집계 스크립트 |
4개 이슈의 공통점이 있습니다. 전부 AI가 먼저 발견할 수 없는 종류의 버그였다는 것. 캐시 TTL이 24시간이라는 건 코드 리뷰로 알 수 있지만, "새 회차 데이터가 들어왔을 때 캐시가 안 풀린다"는 건 실제 운영을 해봐야 알 수 있습니다. 주소 중복도 마찬가지로, "복권명당우장산역점"이 4가지 주소로 등록되어 있다는 건 실제 데이터를 봐야 발견됩니다.
AI한테 시킬 때 "통계가 이상해"가 아니라 "이 판매점이 4번 카운트되고 있는데, 주소가 미묘하게 다르다. 정규화 함수를 만들어달라"처럼 문제를 구체적으로 진단한 다음 요청하는 게 핵심이었습니다.