8000 fix: 일본 상품 중계를 기존 터널 경로로 연결 by hmmhmmhm · Pull Request #293 · hmmhmmhm/daiso-mcp · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/daiso-jp-network-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -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을 반환하는 것을 확인해 사용하지 않습니다.
2 changes: 1 addition & 1 deletion scripts/relay/daiso-jp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpRelayOptions, 'takeQuota' | 'fetcher' | 'onEvent'>) {
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<string, unknown>;
Expand Down
2 changes: 1 addition & 1 deletion scripts/relay/dtryx-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/services/daisoJp/productRelay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion tests/relay/daiso-jp.test.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
4 changes: 2 additions & 2 deletions tests/relay/dtryx-start-cgv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion tests/services/daisoJp/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
2 changes: 1 addition & 1 deletion tests/services/daisoJp/product-relay.test.ts
Original file line number Diff line number Diff line change
64E8 Expand Up @@ -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' });
});
Expand Down
Loading
0