${title}
${escapeHtml(WEB_READ_RULE)}
-요청: ${source} · HTTP ${c.res.status}
실제 API 조회 결과입니다. 오류를 상품 없음이나 재고 0으로 해석하지 마세요. 매장 재고는 상품 ID와 매장명 또는 사용자 위치를 확인한 뒤 별도로 조회하세요.
-${renderValue(data, url.pathname === '/api/daiso/products')}
+요청: ${source} · HTTP ${c.res.status}
실제 API 조회 결과입니다. 오류를 상품 없음이나 재고 0으로 해석하지 마세요. ${inventoryGuide}
+${renderValue(data, productLinks)}
개발자용 JSON 원본 (웹 읽기용 아님) · 설치 없는 조회 안내 · API 경로와 필수 파라미터 안내
다른 공개 GET 조회도 기존 파라미터에 format=html을 추가하면 HTML로 읽을 수 있습니다. 웹 읽기만으로 MCP 연결이 생기는 것은 아닙니다.
`;
const headers = new Headers(c.res.headers);
diff --git a/src/pages/openapiFullSpec.ts b/src/pages/openapiFullSpec.ts
index fe84d16e..348269b4 100644
--- a/src/pages/openapiFullSpec.ts
+++ b/src/pages/openapiFullSpec.ts
@@ -3,6 +3,7 @@
*/
import { OPENAPI_PATHS_DAISO_OLIVEYOUNG } from './openapiSpecPathsDaisoOliveyoung.js';
+import { OPENAPI_PATHS_DAISO_JP } from './openapiSpecPathsDaisoJp.js';
import { OPENAPI_PATHS_CU } from './openapiSpecPathsCu.js';
import { OPENAPI_PATHS_EMART24 } from './openapiSpecPathsEmart24.js';
import { OPENAPI_PATHS_MEGABOX } from './openapiSpecPathsMegabox.js';
@@ -54,6 +55,7 @@ export function generateFullOpenApiSpec(baseUrl: string): object {
paths: {
...OPENAPI_PATHS_DAISO_OLIVEYOUNG,
...OPENAPI_PATHS_CU,
+ ...OPENAPI_PATHS_DAISO_JP,
...OPENAPI_PATHS_EMART24,
...OPENAPI_PATHS_GS25,
...OPENAPI_PATHS_SEVENELEVEN,
diff --git a/src/pages/openapiSpecPathsDaisoJp.ts b/src/pages/openapiSpecPathsDaisoJp.ts
new file mode 100644
index 00000000..7d964c32
--- /dev/null
+++ b/src/pages/openapiSpecPathsDaisoJp.ts
@@ -0,0 +1,153 @@
+/** 일본 다이소 전체 REST 명세. */
+const product = {
+ type: 'object',
+ required: [
+ 'id',
+ 'name',
+ 'country',
+ 'price',
+ 'currency',
+ 'url',
+ 'onlineAvailable',
+ 'storeInventorySupported',
+ ],
+ properties: {
+ id: { type: 'string', pattern: '^\\d{13}$' },
+ name: { type: 'string' },
+ country: { type: 'string', const: 'JP' },
+ price: { type: ['number', 'null'], description: '세금 포함 엔화 가격. 확인 불가 시 null.' },
+ currency: { type: 'string', const: 'JPY' },
+ url: { type: 'string', format: 'uri' },
+ imageUrl: { type: 'string', format: 'uri' },
+ onlineAvailable: {
+ type: ['boolean', 'null'],
+ description: '온라인 판매 가능 여부. 매장 재고가 아니며 검색 목록에서는 null.',
+ },
+ storeInventorySupported: { type: 'boolean', const: false },
+ },
+};
+const store = {
+ type: 'object',
+ required: ['id', 'name', 'address', 'hours', 'url', 'latitude', 'longitude', 'brand'],
+ properties: {
+ id: { type: 'string' },
+ name: { type: 'string' },
+ address: { type: 'string' },
+ hours: { type: 'string' },
+ url: { type: 'string', format: 'uri' },
+ latitude: { type: ['number', 'null'] },
+ longitude: { type: ['number', 'null'] },
+ brand: { type: 'string' },
+ },
+};
+const pagination = {
+ country: { type: 'string', const: 'JP' },
+ page: { type: 'integer' },
+ hasNext: { type: 'boolean' },
+ note: { type: 'string' },
+};
+function responses(data: object) {
+ return {
+ '200': {
+ description: '일본 상품 또는 매장 조회 결과. JPY 가격이며 매장 재고 미지원.',
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ required: ['success', 'data'],
+ properties: { success: { type: 'boolean', const: true }, data },
+ },
+ },
+ 'text/html': { schema: { type: 'string' } },
+ },
+ },
+ '400': { description: '잘못된 검색어·상품 ID·페이지' },
+ '502': { description: '원본 응답 형식 변경. 빈 결과가 아닙니다.' },
+ '503': { description: '일본 다이소 연결 오류 또는 원본 차단' },
+ };
+}
+const format = {
+ name: 'format',
+ in: 'query',
+ description: '웹 읽기 에이전트는 반드시 html 지정',
+ schema: { type: 'string', enum: ['html', 'json'] },
+};
+const page = {
+ name: 'page',
+ in: 'query',
+ schema: { type: 'integer', minimum: 1, maximum: 100, default: 1 },
+};
+export const OPENAPI_PATHS_DAISO_JP = {
+ '/api/daiso-jp/products': {
+ get: {
+ operationId: 'daisoJpSearchProducts',
+ summary: '일본 다이소 온라인 상품 검색',
+ description:
+ '일본어 키워드 또는 JAN 코드. 가격은 JPY이며 매장 재고가 아닙니다. 원본 페이지 단위 조회입니다.',
+ parameters: [
+ {
+ name: 'q',
+ in: 'query',
+ required: true,
+ schema: { type: 'string', minLength: 1, maxLength: 100 },
+ },
+ page,
+ format,
+ ],
+ responses: responses({
+ type: 'object',
+ required: ['country', 'currency', 'query', 'page', 'products', 'hasNext', 'note'],
+ properties: {
+ ...pagination,
+ currency: { type: 'string', const: 'JPY' },
+ query: { type: 'string' },
+ products: { type: 'array', items: product },
+ },
+ }),
+ },
+ },
+ '/api/daiso-jp/products/{id}': {
+ get: {
+ operationId: 'daisoJpGetProduct',
+ summary: '일본 다이소 온라인 상품 상세',
+ description: 'onlineAvailable은 온라인 판매 가능 여부입니다. 매장 재고·수량 미지원.',
+ parameters: [
+ {
+ name: 'id',
+ in: 'path',
+ required: true,
+ schema: { type: 'string', pattern: '^\\d{13}$' },
+ },
+ format,
+ ],
+ responses: responses(product),
+ },
+ },
+ '/api/daiso-jp/stores': {
+ get: {
+ operationId: 'daisoJpFindStores',
+ summary: '일본 다이소 매장 검색',
+ description:
+ '공식 사이트의 매장명·주소 키워드 검색. 계열 브랜드는 brand로 구분합니다. 지명만으로 특정 도시를 확정하지 마세요.',
+ parameters: [
+ {
+ name: 'keyword',
+ in: 'query',
+ required: true,
+ schema: { type: 'string', minLength: 1, maxLength: 100 },
+ },
+ page,
+ format,
+ ],
+ responses: responses({
+ type: 'object',
+ required: ['country', 'keyword', 'page', 'stores', 'hasNext', 'note'],
+ properties: {
+ ...pagination,
+ keyword: { type: 'string' },
+ stores: { type: 'array', items: store },
+ },
+ }),
+ },
+ },
+};
diff --git a/src/pages/prompt.ts b/src/pages/prompt.ts
index 8745abab..ab67c69f 100644
--- a/src/pages/prompt.ts
+++ b/src/pages/prompt.ts
@@ -19,6 +19,17 @@ export function generatePromptText(baseUrl: string): string {
buildPromptCoreText(baseUrl),
buildPromptRetailCinemaText(baseUrl),
buildPromptReferenceText(baseUrl),
+ `
+## 일본 다이소 (daiso_jp)
+일본어 키워드로 일본 다이소를 검색합니다. 한국 다이소와 상품·매장 ID가 다르며 가격은 JPY입니다.
+- 상품: ${baseUrl}/api/daiso-jp/products?q=%E3%83%9C%E3%82%A6%E3%83%AB&format=html
+- 상세: ${baseUrl}/api/daiso-jp/products/4550746075917?format=html
+- 매장: ${baseUrl}/api/daiso-jp/stores?keyword=%E6%B8%8B%E8%B0%B7&format=html
+매장명·주소를 검색하므로 지역을 확인하세요. 다음 페이지는 page를 증가시키되 hasNext=true일 때만 요청하세요.
+온라인 판매 가능 여부는 매장 재고가 아닙니다. 일본 매장 재고 조회는 아직 지원하지 않습니다.
+MCP: daiso_jp_search_products(query,page), daiso_jp_get_product(id), daiso_jp_find_stores(keyword,page).
+CLI: jp-products ボウル / jp-product 4550746075917 / jp-stores 渋谷.
+`,
].join('');
}
diff --git a/src/services/daisoJp/client.ts b/src/services/daisoJp/client.ts
new file mode 100644
index 00000000..b735302c
--- /dev/null
+++ b/src/services/daisoJp/client.ts
@@ -0,0 +1,79 @@
+import { ServiceError } from '../../core/errors.js';
+import { fetchJson, fetchText, HttpError, UnexpectedHtmlResponseError } from '../../utils/http.js';
+import { parseProductDetail, parseProductSearch, parseStoreSearch } from './parser.js';
+import type { JapanProduct, JapanProductSearchResult, JapanStoreSearchResult } from './types.js';
+
+const OPTIONS = { timeout: 20000, retries: 0 };
+
+function validateSearch(query: string, page: number): string {
+ if (typeof query !== 'string' || !query.trim() || query.trim().length > 100) {
+ throw new Error('검색어는 1~100자로 입력해주세요.');
+ }
+ if (!Number.isInteger(page) || page < 1 || page > 100) {
+ throw new Error('페이지는 1~100의 정수여야 합니다.');
+ }
+ return query.trim();
+}
+
+async function upstream