diff --git a/docs/quality-improvements-2026-10-05.md b/docs/quality-improvements-2026-10-05.md index 3445b1e7..7e776d2e 100644 --- a/docs/quality-improvements-2026-10-05.md +++ b/docs/quality-improvements-2026-10-05.md @@ -40,3 +40,5 @@ 수정 후 검증은 228개 파일·2,631개 테스트, `npm run check`, 100% Statements/Branches/Functions/Lines coverage, build, Worker dry run을 통과했습니다. 독립 리뷰에서 비교 결과 한도·GS25 미확인 합계·메타데이터의 잘못된 0·REST/MCP 빈 결과 차이를 찾아 추가 수정하고 재검증했습니다. 운영 확인은 배포 후 같은 상품 코드·매장·지역의 결과를 REST/MCP/공개 CLI로 확인해야 합니다. 정상 검색 결과만으로 모든 실패 경로가 정상이라는 결론을 내리지 않습니다. 편의점 공유 매분 30회·일일 3,000회 원장과 인증 세션을 보존하고, 예산이 부족하면 재시도 시각을 기다립니다. + +수정 전의 잘못된 성공 응답이 Edge Cache에서 재사용되지 않도록 관련 조회 경로의 캐시 namespace도 갱신했습니다. 비교의 오래된 403 부분 결과를 캐시에 주입한 실제 앱 회귀 테스트로 새 조회가 수행되는 것을 확인했습니다. diff --git a/src/api/routes/cgvRoutes.ts b/src/api/routes/cgvRoutes.ts index 0e7487f2..c4a8b453 100644 --- a/src/api/routes/cgvRoutes.ts +++ b/src/api/routes/cgvRoutes.ts @@ -14,7 +14,7 @@ export function registerCgvRoutes(app: Hono<{ Bindings: AppBindings }>): void { { ttlSeconds: 60 * 60 * 24, staleWhileRevalidateSeconds: 60 * 5, - keyPrefix: 'cgv-theaters-v2', + keyPrefix: 'cgv-theaters-v3', }, () => handleCgvFindTheaters(c), ), @@ -26,7 +26,7 @@ export function registerCgvRoutes(app: Hono<{ Bindings: AppBindings }>): void { { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'cgv-movies-v2', + keyPrefix: 'cgv-movies-v3', }, () => handleCgvSearchMovies(c), ), @@ -38,7 +38,7 @@ export function registerCgvRoutes(app: Hono<{ Bindings: AppBindings }>): void { { ttlSeconds: 60 * 3, staleWhileRevalidateSeconds: 30, - keyPrefix: 'cgv-timetable-v2', + keyPrefix: 'cgv-timetable-v3', }, () => handleCgvGetTimetable(c), ), diff --git a/src/api/routes/compareRoutes.ts b/src/api/routes/compareRoutes.ts index ba7eb4fa..2f7dfdaa 100644 --- a/src/api/routes/compareRoutes.ts +++ b/src/api/routes/compareRoutes.ts @@ -10,7 +10,7 @@ export function registerCompareRoutes(app: Hono<{ Bindings: AppBindings }>): voi { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'compare-products-v1', + keyPrefix: 'compare-products-v2', }, () => handleCompareProducts(c), ), diff --git a/src/api/routes/cuRoutes.ts b/src/api/routes/cuRoutes.ts index fa35e579..c83d735e 100644 --- a/src/api/routes/cuRoutes.ts +++ b/src/api/routes/cuRoutes.ts @@ -14,7 +14,7 @@ export function registerCuRoutes(app: Hono<{ Bindings: AppBindings }>): void { { ttlSeconds: 60 * 60 * 24, staleWhileRevalidateSeconds: 60 * 5, - keyPrefix: 'cu-stores-v3', + keyPrefix: 'cu-stores-v4', }, () => handleCuFindStores(c), ), @@ -26,7 +26,7 @@ export function registerCuRoutes(app: Hono<{ Bindings: AppBindings }>): void { { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'cu-inventory-v3', + keyPrefix: 'cu-inventory-v4', }, () => handleCuCheckInventory(c), ), diff --git a/src/api/routes/daisoRoutes.ts b/src/api/routes/daisoRoutes.ts index 6828ced0..bb1acfbb 100644 --- a/src/api/routes/daisoRoutes.ts +++ b/src/api/routes/daisoRoutes.ts @@ -20,7 +20,7 @@ export function registerDaisoRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 30, staleWhileRevalidateSeconds: 60 * 3, - keyPrefix: 'daiso-products-v1', + keyPrefix: 'daiso-products-v2', }, () => handleSearchProducts(c), ), @@ -32,7 +32,7 @@ export function registerDaisoRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 60, staleWhileRevalidateSeconds: 60 * 5, - keyPrefix: 'daiso-product-detail-v1', + keyPrefix: 'daiso-product-detail-v2', }, () => handleGetProduct(c), ), @@ -44,7 +44,7 @@ export function registerDaisoRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 60 * 24, staleWhileRevalidateSeconds: 60 * 5, - keyPrefix: 'daiso-stores-v1', + keyPrefix: 'daiso-stores-v2', }, () => handleFindStores(c), ), @@ -56,7 +56,7 @@ export function registerDaisoRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'daiso-inventory-v1', + keyPrefix: 'daiso-inventory-v2', }, () => handleCheckInventory(c), ), @@ -68,7 +68,7 @@ export function registerDaisoRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'daiso-display-location-v1', + keyPrefix: 'daiso-display-location-v2', }, () => handleGetDisplayLocation(c), ), diff --git a/src/api/routes/dtryxRoutes.ts b/src/api/routes/dtryxRoutes.ts index 1131f471..1c15ce18 100644 --- a/src/api/routes/dtryxRoutes.ts +++ b/src/api/routes/dtryxRoutes.ts @@ -18,7 +18,7 @@ export function registerDtryxRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 60 * 24, staleWhileRevalidateSeconds: 60 * 5, - keyPrefix: 'dtryx-cinemas-v1', + keyPrefix: 'dtryx-cinemas-v2', }, () => handleDtryxListCinemas(c), ), @@ -30,7 +30,7 @@ export function registerDtryxRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'dtryx-movies-v1', + keyPrefix: 'dtryx-movies-v2', }, () => handleDtryxListNowShowing(c), ), @@ -42,7 +42,7 @@ export function registerDtryxRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 3, staleWhileRevalidateSeconds: 30, - keyPrefix: 'dtryx-seats-v1', + keyPrefix: 'dtryx-seats-v2', }, () => handleDtryxGetRemainingSeats(c), ), diff --git a/src/api/routes/emart24Routes.ts b/src/api/routes/emart24Routes.ts index a7d95413..af0ae45f 100644 --- a/src/api/routes/emart24Routes.ts +++ b/src/api/routes/emart24Routes.ts @@ -18,7 +18,7 @@ export function registerEmart24Routes(app: Hono<{ Bindings: AppBindings }>): voi { ttlSeconds: 60 * 30, staleWhileRevalidateSeconds: 60 * 3, - keyPrefix: 'emart24-stores-v1', + keyPrefix: 'emart24-stores-v2', }, () => handleEmart24FindStores(c), ), @@ -30,7 +30,7 @@ export function registerEmart24Routes(app: Hono<{ Bindings: AppBindings }>): voi { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'emart24-products-v1', + keyPrefix: 'emart24-products-v2', }, () => handleEmart24SearchProducts(c), ), @@ -42,7 +42,7 @@ export function registerEmart24Routes(app: Hono<{ Bindings: AppBindings }>): voi { ttlSeconds: 60 * 5, staleWhileRevalidateSeconds: 60, - keyPrefix: 'emart24-inventory-v1', + keyPrefix: 'emart24-inventory-v2', }, () => handleEmart24CheckInventory(c), ), diff --git a/src/api/routes/gs25Routes.ts b/src/api/routes/gs25Routes.ts index 5915bd65..aacdd9ce 100644 --- a/src/api/routes/gs25Routes.ts +++ b/src/api/routes/gs25Routes.ts @@ -18,7 +18,7 @@ export function registerGs25Routes(app: Hono<{ Bindings: AppBindings }>): void { { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'gs25-stores-v2', + keyPrefix: 'gs25-stores-v3', }, () => handleGs25FindStores(c), ), @@ -30,7 +30,7 @@ export function registerGs25Routes(app: Hono<{ Bindings: AppBindings }>): void { { ttlSeconds: 60 * 3, staleWhileRevalidateSeconds: 60, - keyPrefix: 'gs25-products-v2', + keyPrefix: 'gs25-products-v3', }, () => handleGs25SearchProducts(c), ), @@ -42,7 +42,7 @@ export function registerGs25Routes(app: Hono<{ Bindings: AppBindings }>): void { { ttlSeconds: 60 * 2, staleWhileRevalidateSeconds: 30, - keyPrefix: 'gs25-inventory-v2', + keyPrefix: 'gs25-inventory-v3', }, () => handleGs25CheckInventory(c), ), diff --git a/src/api/routes/lottecinemaRoutes.ts b/src/api/routes/lottecinemaRoutes.ts index a8cd377a..3c131dbc 100644 --- a/src/api/routes/lottecinemaRoutes.ts +++ b/src/api/routes/lottecinemaRoutes.ts @@ -18,7 +18,7 @@ export function registerLotteCinemaRoutes(app: Hono<{ Bindings: AppBindings }>): { ttlSeconds: 60 * 60 * 24, staleWhileRevalidateSeconds: 60 * 5, - keyPrefix: 'lottecinema-theaters-v2', + keyPrefix: 'lottecinema-theaters-v3', }, () => handleLotteCinemaFindNearbyTheaters(c), ), @@ -30,7 +30,7 @@ export function registerLotteCinemaRoutes(app: Hono<{ Bindings: AppBindings }>): { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'lottecinema-movies-v2', + keyPrefix: 'lottecinema-movies-v3', }, () => handleLotteCinemaListNowShowing(c), ), @@ -42,7 +42,7 @@ export function registerLotteCinemaRoutes(app: Hono<{ Bindings: AppBindings }>): { ttlSeconds: 60 * 3, staleWhileRevalidateSeconds: 30, - keyPrefix: 'lottecinema-seats-v2', + keyPrefix: 'lottecinema-seats-v3', }, () => handleLotteCinemaGetRemainingSeats(c), ), diff --git a/src/api/routes/megaboxRoutes.ts b/src/api/routes/megaboxRoutes.ts index 53d689c2..cfa7d772 100644 --- a/src/api/routes/megaboxRoutes.ts +++ b/src/api/routes/megaboxRoutes.ts @@ -18,7 +18,7 @@ export function registerMegaboxRoutes(app: Hono<{ Bindings: AppBindings }>): voi { ttlSeconds: 60 * 60 * 24, staleWhileRevalidateSeconds: 60 * 5, - keyPrefix: 'megabox-theaters-v2', + keyPrefix: 'megabox-theaters-v3', }, () => handleMegaboxFindNearbyTheaters(c), ), @@ -30,7 +30,7 @@ export function registerMegaboxRoutes(app: Hono<{ Bindings: AppBindings }>): voi { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'megabox-movies-v2', + keyPrefix: 'megabox-movies-v3', }, () => handleMegaboxListNowShowing(c), ), @@ -42,7 +42,7 @@ export function registerMegaboxRoutes(app: Hono<{ Bindings: AppBindings }>): voi { ttlSeconds: 60 * 3, staleWhileRevalidateSeconds: 30, - keyPrefix: 'megabox-seats-v2', + keyPrefix: 'megabox-seats-v3', }, () => handleMegaboxGetRemainingSeats(c), ), diff --git a/src/api/routes/oliveyoungRoutes.ts b/src/api/routes/oliveyoungRoutes.ts index fa579858..84120fa8 100644 --- a/src/api/routes/oliveyoungRoutes.ts +++ b/src/api/routes/oliveyoungRoutes.ts @@ -19,7 +19,7 @@ export function registerOliveyoungRoutes(app: Hono<{ Bindings: AppBindings }>): { ttlSeconds: 60 * 5, staleWhileRevalidateSeconds: 60, - keyPrefix: 'oliveyoung-products-v2', + keyPrefix: 'oliveyoung-products-v3', }, () => handleOliveyoungSearchProducts(c), ), @@ -31,7 +31,7 @@ export function registerOliveyoungRoutes(app: Hono<{ Bindings: AppBindings }>): { ttlSeconds: 60 * 60 * 24, staleWhileRevalidateSeconds: 60 * 5, - keyPrefix: 'oliveyoung-stores-v2', + keyPrefix: 'oliveyoung-stores-v3', }, () => handleOliveyoungFindStores(c), ), @@ -43,7 +43,7 @@ export function registerOliveyoungRoutes(app: Hono<{ Bindings: AppBindings }>): { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'oliveyoung-inventory-v2', + keyPrefix: 'oliveyoung-inventory-v3', }, () => handleOliveyoungCheckInventory(c), ), diff --git a/src/api/routes/opinetRoutes.ts b/src/api/routes/opinetRoutes.ts index 592749a6..47a89314 100644 --- a/src/api/routes/opinetRoutes.ts +++ b/src/api/routes/opinetRoutes.ts @@ -15,7 +15,7 @@ export function registerOpinetRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 60, staleWhileRevalidateSeconds: 60 * 30, - keyPrefix: 'opinet-average-v2', + keyPrefix: 'opinet-average-v3', }, () => handleOpinetAveragePrices(c), ), @@ -27,7 +27,7 @@ export function registerOpinetRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 30, staleWhileRevalidateSeconds: 60 * 10, - keyPrefix: 'opinet-lowest-v2', + keyPrefix: 'opinet-lowest-v3', }, () => handleOpinetLowestStations(c), ), @@ -39,7 +39,7 @@ export function registerOpinetRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 20, staleWhileRevalidateSeconds: 60 * 5, - keyPrefix: 'opinet-around-v2', + keyPrefix: 'opinet-around-v3', }, () => handleOpinetStationsAround(c), ), @@ -51,7 +51,7 @@ export function registerOpinetRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 60, staleWhileRevalidateSeconds: 60 * 10, - keyPrefix: 'opinet-station-v2', + keyPrefix: 'opinet-station-v3', }, () => handleOpinetStationDetail(c), ), diff --git a/src/api/routes/placesRoutes.ts b/src/api/routes/placesRoutes.ts index b2829c81..a92e4a25 100644 --- a/src/api/routes/placesRoutes.ts +++ b/src/api/routes/placesRoutes.ts @@ -13,7 +13,7 @@ export function registerPlacesRoutes(app: Hono<{ Bindings: AppBindings }>): void { ttlSeconds: 60 * 60, staleWhileRevalidateSeconds: 60 * 10, - keyPrefix: 'places-search-v1', + keyPrefix: 'places-search-v2', }, () => handlePlacesSearch(c), ), diff --git a/src/api/routes/sevenelevenRoutes.ts b/src/api/routes/sevenelevenRoutes.ts index dcd38f6c..6e7a5c2d 100644 --- a/src/api/routes/sevenelevenRoutes.ts +++ b/src/api/routes/sevenelevenRoutes.ts @@ -20,7 +20,7 @@ export function registerSevenElevenRoutes(app: Hono<{ Bindings: AppBindings }>): { ttlSeconds: 60 * 3, staleWhileRevalidateSeconds: 60, - keyPrefix: 'seveneleven-products-v1', + keyPrefix: 'seveneleven-products-v2', }, () => handleSevenElevenSearchProducts(c), ), @@ -32,7 +32,7 @@ export function registerSevenElevenRoutes(app: Hono<{ Bindings: AppBindings }>): { ttlSeconds: 60 * 3, staleWhileRevalidateSeconds: 60, - keyPrefix: 'seveneleven-stores-v1', + keyPrefix: 'seveneleven-stores-v2', }, () => handleSevenElevenSearchStores(c), ), @@ -44,7 +44,7 @@ export function registerSevenElevenRoutes(app: Hono<{ Bindings: AppBindings }>): { ttlSeconds: 60 * 10, staleWhileRevalidateSeconds: 60, - keyPrefix: 'seveneleven-inventory-v1', + keyPrefix: 'seveneleven-inventory-v2', }, () => handleSevenElevenCheckInventory(c), ), @@ -56,7 +56,7 @@ export function registerSevenElevenRoutes(app: Hono<{ Bindings: AppBindings }>): { ttlSeconds: 60 * 5, staleWhileRevalidateSeconds: 60, - keyPrefix: 'seveneleven-popwords-v1', + keyPrefix: 'seveneleven-popwords-v2', }, () => handleSevenElevenGetSearchPopwords(c), ), @@ -68,7 +68,7 @@ export function registerSevenElevenRoutes(app: Hono<{ Bindings: AppBindings }>): { ttlSeconds: 60 * 5, staleWhileRevalidateSeconds: 60, - keyPrefix: 'seveneleven-catalog-v1', + keyPrefix: 'seveneleven-catalog-v2', }, () => handleSevenElevenGetCatalogSnapshot(c), ), diff --git a/tests/api/cu-relay-keyword.test.ts b/tests/api/cu-relay-keyword.test.ts index fe787bad..50452c53 100644 --- a/tests/api/cu-relay-keyword.test.ts +++ b/tests/api/cu-relay-keyword.test.ts @@ -141,7 +141,7 @@ it.each([ expect(text).toContain('강남예시점'); expect(text).not.toContain('시청예시점'); const newKey = new URL(url); - newKey.searchParams.append('__cache_prefix', `${prefix}-v3`); + newKey.searchParams.append('__cache_prefix', `${prefix}-v4`); expect(match.mock.calls[0][0].url).toBe(newKey.href); expect(put.mock.calls[0][0].url).toBe(newKey.href); expect(response.headers.get('Cache-Control')).toContain(`max-age=${ttl}`); diff --git a/tests/app/quality-cache-migration.test.ts b/tests/app/quality-cache-migration.test.ts new file mode 100644 index 00000000..a0962797 --- /dev/null +++ b/tests/app/quality-cache-migration.test.ts @@ -0,0 +1,18 @@ +import { afterEach, expect, it, vi } from 'vitest'; +import app from '../../src/index.js'; + +afterEach(() => vi.unstubAllGlobals()); +it('개선 전 비교 캐시의 실패 결과 대신 수정된 조회를 반환한다', async () => { + const old = Response.json({ success: true, data: { results: [], errors: [{ service: 'gs25', message: 'old 403' }] } }); + const put = vi.fn(); + vi.stubGlobal('caches', { default: { + match: async (request: Request) => new URL(request.url).searchParams.get('__cache_prefix') === 'compare-products-v1' ? old.clone() : undefined, + put, + } }); + const fetcher = vi.fn(async () => Response.json({ resultSet: { result: [{ totalSize: 1, resultDocuments: [{ PD_NO: 'P', PDNM: '현재 상품', PD_PRC: '1000' }] }] } })); + vi.stubGlobal('fetch', fetcher); + const response = await app.request('/api/compare/products?keyword=콜라&services=daiso&limit=1'); + expect(await response.json()).toMatchObject({ data: { results: [{ code: 'P', name: '현재 상품' }], errors: [] } }); + expect(fetcher).toHaveBeenCalledTimes(1); + expect(put).toHaveBeenCalledTimes(1); +});