diff --git a/docs/daiso-jp-network-analysis.md b/docs/daiso-jp-network-analysis.md index 828575c6..85c601c5 100644 --- a/docs/daiso-jp-network-analysis.md +++ b/docs/daiso-jp-network-analysis.md @@ -64,8 +64,10 @@ npx daiso jp-stores 東京都渋谷区 --json 앞의 검색 선조회 순서 변경은 실제 운영403을 해소하지 못해 철회했습니다. 로컬과 자체 배포에서 별도 설정이 없으면 기존 상세→403시 정확한코드검색 흐름을 유지합니다. 운영 Worker는 `DAISO_JP_PRODUCT_RELAY_ENABLED="true"`를 명시하고 기존 DTRYX_RELAY_URL/TOKEN/ACCESS_CLIENT_ID/ACCESS_CLIENT_SECRET 설정으로 일본 상세만 HTTP 중계에 연결합니다. 이 플래그가 없으면 DTRYX 설정만으로 일본 전송이 바뀌지 않습니다. -`POST /v1/daiso-jp/product`는 `{ "id": "4550746075917" }`만 받으며 임의 URL을 허용하지 않습니다. Mac에서 공식 `https://jp.daisonet.com/products/{id}.js`만 읽고 요청ID와 대조합니다. 리다이렉트는 거절하고 본문2MiB·전체15초 제한을 적용합니다. Worker도 같은 파서로 응답ID를 다시 검증합니다. 재시작할 때 공유 파서의 `src/services/daisoJp/parser.ts`와 types.ts도 runtime에 배치해야 합니다. +`POST /v1/dtryx/daiso-jp-product`는 `{ "id": "4550746075917" }`만 받으며 임의 URL을 허용하지 않습니다. Mac에서 공식 `https://jp.daisonet.com/products/{id}.js`만 읽고 요청ID와 대조합니다. 리다이렉트는 거절하고 본문2MiB·전체15초 제한을 적용합니다. Worker도 같은 파서로 응답ID를 다시 검증합니다. 재시작할 때 공유 파서의 `src/services/daisoJp/parser.ts`와 types.ts도 runtime에 배치해야 합니다. 상품 검색·매장 검색은 기존 직접 경로를 유지하며 상세에는 브라우저·가상기기·새 로그인·유료 프록시가 필요하지 않습니다. 상세는 연결시험 후 재요청하는 대신 명시적 중계 경로를 사용합니다. 공통 DTRYX 중계의 건강 상태는 기존5분 Cron으로 확인되며 일본 상세는 deep/full CLI 계약에서 실제 조회합니다. 별도 직접상세 probe를 늘리지 않습니다. 일본 상세는 DTRYX/CGV와 동일한 호출 예산 원장을 공유하지만 서비스별 접수·동시성 큐는 별도입니다. 상품정보 캐시는 기존 REST와 동일하게30분이며 인증된 force-fresh는 Worker와 중계 양쪽을 우회합니다. 자체 할당량429·중계인증·원본차단·미확인판매 여부를 성공이나 재고0으로 바꾸지 않습니다. 원본 HTTP상태는 비공개 중계로그에서 확인하고 Worker 공개진단의 upstreamStatus는 중계응답 상태입니다. 일본 오프라인 매장 재고 미지원 범위는 그대로입니다. + +기존 운영 Tunnel ingress는 `^/v1/dtryx/.*$`만4320에 전달합니다. 새 작업도 이 경로 안의 `/v1/dtryx/daiso-jp-product`에 배치하여 기존 인증·터널 범위를 유지합니다. 별도 `/v1/daiso-jp/` 경로는 올리브영 서비스로 전달돼401을 반환하는 것을 확인해 사용하지 않습니다. diff --git a/scripts/relay/daiso-jp.ts b/scripts/relay/daiso-jp.ts index e6b762b3..61585a43 100644 --- a/scripts/relay/daiso-jp.ts +++ b/scripts/relay/daiso-jp.ts @@ -3,7 +3,7 @@ import { parseProductDetail } from '../../src/services/daisoJp/parser.js'; import { createHttpRelay, RelayError, abortable, readJson, type HttpRelayOptions } from './http-relay.js'; export function createJapanProductRelay(token: string, options: Pick) { return createHttpRelay(token, { - ...options, prefix: '/v1/daiso-jp/', operations: ['product'], cacheTtl: () => 1800000, + ...options, prefix: '/v1/dtryx/', operations: ['daiso-jp-product'], cacheTtl: () => 1800000, validate(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) throw new RelayError(400); const p = value as Record; diff --git a/scripts/relay/dtryx-start.ts b/scripts/relay/dtryx-start.ts index 0dadc554..a4337020 100644 --- a/scripts/relay/dtryx-start.ts +++ b/scripts/relay/dtryx-start.ts @@ -77,7 +77,7 @@ async function main() { const japanHandler = observeRelay(japanRelay, log.append); const handler = (request: Request) => new URL(request.url).pathname.startsWith('/v1/convenience/') ? convenience ? convenience(request) : Promise.resolve(Response.json({ error: 'Not found' }, { status: 404 })) - : new URL(request.url).pathname.startsWith('/v1/daiso-jp/') ? japanHandler(request) + : new URL(request.url).pathname === '/v1/dtryx/daiso-jp-product' ? japanHandler(request) : new URL(request.url).pathname.startsWith('/v1/cgv/') ? cgvHandler(request) : dtryxHandler(request); const server = createServer(async (req, res) => { const controller = new AbortController(); diff --git a/src/services/daisoJp/productRelay.ts b/src/services/daisoJp/productRelay.ts index 32cf959c..a41a7d15 100644 --- a/src/services/daisoJp/productRelay.ts +++ b/src/services/daisoJp/productRelay.ts @@ -34,7 +34,7 @@ export async function requestJapanProductRelay(id: string, options: JapanProduct }); try { const work = (async () => { - const response = await fetch(`${relayUrl!.replace(/\/$/, '')}/v1/daiso-jp/product`, { + const response = await fetch(`${relayUrl!.replace(/\/$/, '')}/v1/dtryx/daiso-jp-product`, { method: 'POST', redirect: 'manual', headers, body: JSON.stringify({ id }), signal: controller.signal, }); if (response.status !== 200) { diff --git a/tests/relay/daiso-jp.test.ts b/tests/relay/daiso-jp.test.ts index 5b42ecd5..2bc803a1 100644 --- a/tests/relay/daiso-jp.test.ts +++ b/tests/relay/daiso-jp.test.ts @@ -1,7 +1,7 @@ import { expect, it, vi } from 'vitest'; import { createJapanProductRelay } from '../../scripts/relay/daiso-jp.js'; const raw = { handle: '4550746075917', title: 'ボウル', price: 22000, available: true }; -const req = (body: unknown, token = 'test-token') => new Request('http://localhost/v1/daiso-jp/product', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: JSON.stringify(body) }); +const req = (body: unknown, token = 'test-token') => new Request('http://localhost/v1/dtryx/daiso-jp-product', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: JSON.stringify(body) }); it('인증된 고정 상품 JSON만 읽고 실제 상품 코드와 대조한다', async () => { const fetcher = vi.fn(async () => Response.json(raw)); const takeQuota = vi.fn(async () => true); diff --git a/tests/relay/dtryx-start-cgv.test.ts b/tests/relay/dtryx-start-cgv.test.ts index 559bd23e..1d5cbef3 100644 --- a/tests/relay/dtryx-start-cgv.test.ts +++ b/tests/relay/dtryx-start-cgv.test.ts @@ -52,8 +52,8 @@ it('CGV와 디트릭스 조회가 같은 쿼터를 소비하고 CGV 건강 상 expect((await invoke('/v1/cgv/theaters', {}, 'wrong')).status).toBe(401); expect((await invoke('/v1/cgv/theaters', {})).status).toBe(200); expect((await invoke('/v1/dtryx/movies', { brandCode: 'dtryx', cinemaCode: '000067' })).status).toBe(200); - expect((await invoke('/v1/daiso-jp/product', { id: '4550746075917' }, 'wrong')).status).toBe(401); - const japan = await invoke('/v1/daiso-jp/product', { id: '4550746075917' }); + expect((await invoke('/v1/dtryx/daiso-jp-product', { id: '4550746075917' }, 'wrong')).status).toBe(401); + const japan = await invoke('/v1/dtryx/daiso-jp-product', { id: '4550746075917' }); expect(japan.status).toBe(200); expect(japan.body.handle).toBe('4550746075917'); const after = await invoke('/v1/cgv/health'); diff --git a/tests/services/daisoJp/client.test.ts b/tests/services/daisoJp/client.test.ts index 5e35683b..678e4f26 100644 --- a/tests/services/daisoJp/client.test.ts +++ b/tests/services/daisoJp/client.test.ts @@ -16,7 +16,7 @@ describe('일본 공식 클라이언트', () => { vi.stubGlobal('fetch', fetcher); expect(await getProduct('4550746075917', { productRelay: { relayUrl: 'https://relay.example', relayToken: 'test-token' } })).toMatchObject({ id: '4550746075917', onlineAvailable: true }); expect(fetcher).toHaveBeenCalledTimes(1); - expect(fetcher.mock.calls[0][0]).toBe('https://relay.example/v1/daiso-jp/product'); + expect(fetcher.mock.calls[0][0]).toBe('https://relay.example/v1/dtryx/daiso-jp-product'); }); it('검색어를 인코딩하고 페이지와 출처 제한을 돌려준다', async () => { respond(fixture('products')); diff --git a/tests/services/daisoJp/product-relay.test.ts b/tests/services/daisoJp/product-relay.test.ts index d2132bb9..b175a3af 100644 --- a/tests/services/daisoJp/product-relay.test.ts +++ b/tests/services/daisoJp/product-relay.test.ts @@ -9,7 +9,7 @@ it('명시한 중계에서 같은 상품만 인증된 고정 경로로 읽는다 const fetcher = vi.fn(async () => Response.json(raw)); vi.stubGlobal('fetch', fetcher); expect(await requestJapanProductRelay(raw.handle, { ...options, bypassCache: true, accessClientId: 'test-id', accessClientSecret: 'test-secret' })).toMatchObject({ id: raw.handle, onlineAvailable: true, price: 220 }); const [url, init] = fetcher.mock.calls[0] as unknown as [string, RequestInit]; - expect(url).toBe('https://relay.example/v1/daiso-jp/product'); + expect(url).toBe('https://relay.example/v1/dtryx/daiso-jp-product'); expect(init.redirect).toBe('manual'); expect(init.headers).toMatchObject({ Authorization: 'Bearer test-token', 'x-relay-cache-bypass': 'true', 'CF-Access-Client-Id': 'test-id' }); });