diff --git a/README.md b/README.md index e7b58c53..c78097f5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 에이전트는 [짧은 시작 안내](https://mcp.aka.page/llms.txt)를 참고하세요. 무설치 요청에는 npm 설치나 전체 스키마 탐색이 필요하지 않습니다. 웹에서는 [공개 첫 화면](https://mcp.aka.page/)에서 시작할 수 있습니다. [검색·크롤러 안내와 배포 구조](docs/web-discovery.md)는 별도 문서에 정리했습니다. -웹 읽기만 가능한 채팅 에이전트는 공개 GET API에 `&format=html`을 추가해 결과를 읽을 수 있습니다. [첫 HTML 조회](https://mcp.aka.page/api/daiso/products?q=%EC%88%98%EB%82%A9%EB%B0%95%EC%8A%A4&pageSize=3&format=html) · [사용 방법](docs/web-discovery.md). +**웹 읽기 전용 에이전트는 모든 `/api/*` GET 조회 URL에 `format=html`을 반드시 붙이세요.** 상품 검색·상세·매장·재고·후속 조회 모두 적용합니다. [첫 HTML 조회](https://mcp.aka.page/api/daiso/products?q=%EC%88%98%EB%82%A9%EB%B0%95%EC%8A%A4&pageSize=3&format=html) · [사용 방법](docs/web-discovery.md). ## 원격 MCP로 바로 시작하기 diff --git a/docs/ai-instruction.md b/docs/ai-instruction.md index bf3cb7cb..48b9b5f7 100644 --- a/docs/ai-instruction.md +++ b/docs/ai-instruction.md @@ -13,7 +13,7 @@ curl -fsS --max-time 20 'https://mcp.aka.page' \ 첫 조회 전에 `npx`, 패키지·스킬 설치, health/help, 전체 스키마나 도구 목록 탐색을 하지 않습니다. 위 예제의 검색어를 사용자 요청으로 바꾸고 조회 결과부터 제공합니다. 사용자 조건이나 호출 오류 때문에 필요한 경우에만 추가 안내를 읽습니다. 호출에 성공하면 추가 진단 없이 결과를 보여줍니다. -`GET https://mcp.aka.page/api/daiso/products?q=수납박스`는 같은 서비스의 REST 직접 조회입니다. GET만 가능한 환경에서는 사용할 수 있지만 MCP 도구를 호출했다고 설명하지 않습니다. HTTP·네트워크 오류만으로 로그인 필요나 서버 장애를 단정하지 않고 실제 오류와 실행 환경을 확인합니다. +`GET https://mcp.aka.page/api/daiso/products?q=수납박스&format=html`는 같은 서비스의 REST 직접 조회입니다. **웹 읽기 전용 에이전트는 모든 `/api/*` GET 조회 URL에 `format=html`을 반드시 붙입니다. 상품 검색·상세·매장·재고·후속 조회 모두 적용합니다.** JSON 주소가 읽히지 않으면 HTML 주소로 재시도하고, 그 실패만으로 로그인 필요나 실행 불가를 단정하지 않습니다. 이 REST 조회를 MCP 도구 호출이라고 설명하지 않습니다. HTTP·네트워크 오류만으로 로그인 필요나 서버 장애를 단정하지 않고 실제 오류와 실행 환경을 확인합니다. 당신은 다이소/통합 가격 후보 비교/주변 음식점·카페/GS25/세븐일레븐/CU/이마트24/올리브영/메가박스/롯데시네마/CGV 생활·쇼핑·영화 도우미입니다. diff --git a/docs/web-discovery.md b/docs/web-discovery.md index bcfdcdb2..5f14fd8a 100644 --- a/docs/web-discovery.md +++ b/docs/web-discovery.md @@ -44,3 +44,5 @@ OpenAI 검색·사용자 요청·학습 봇의 역할과 공개 IP는 [공식 HTML은 원래 응답의 성공·오류와 HTTP 상태를 보존합니다. 상품 검색 결과를 매장 재고로 단정하거나 오류를 재고 0으로 해석하면 안 됩니다. POST와 MCP 응답 형식은 변경하지 않습니다. 웹 읽기 도구가 HTML 접근도 거부하는 경우에는 이 옵션만으로 도구 내부 문제를 해결하지 못합니다. `llms.txt`와 `robots.txt`는 정적 Assets에서도 `text/plain; charset=utf-8`을 명시합니다. UTF-8 본문을 다른 인코딩으로 추측하는 클라이언트에서 한글이 깨지는 것을 방지합니다. + +**웹 읽기 전용 에이전트는 모든 `/api/*` GET 조회 URL에 `format=html`을 반드시 붙이세요.** 검색·상세·매장·재고·후속 조회 모두 적용합니다. `/prompt`의 GET URL 예시도 모두 이 형식으로 제공합니다. JSON 스키마 예시는 필드 설명이고 웹 읽기용 URL 안내가 아닙니다. 일반 JSON API와 MCP 클라이언트의 계약은 유지합니다. diff --git a/src/pages/agentStart.ts b/src/pages/agentStart.ts index 19cdbb92..3bf7ee3a 100644 --- a/src/pages/agentStart.ts +++ b/src/pages/agentStart.ts @@ -1,7 +1,12 @@ +/** 웹 읽기 전용 환경에서 조회 형식을 선택하는 공통 규칙 */ +export const WEB_READ_RULE = '웹 읽기 전용 에이전트: 모든 /api/* GET 조회 URL에 format=html을 반드시 포함하세요. 상품 검색뿐 아니라 상세·매장·재고·후속 조회에도 적용합니다. 아래 문서의 JSON 응답 예시는 필드 설명이며 웹 읽기로 JSON 주소를 열라는 뜻이 아닙니다. JSON URL이 읽히지 않으면 format=html로 다시 조회하고, 그 실패만으로 서비스 장애·로그인 필요·이 환경에서 실행할 수 없다고 단정하지 마세요.'; + /** 설치 없이 첫 실제 조회를 실행하는 에이전트 안내 */ export function buildAgentStartText(baseUrl: string): string { return `# Daiso MCP: 설치 없이 시작하기 +**${WEB_READ_RULE}** + 공개 원격 MCP: https://mcp.aka.page 전송: Streamable HTTP · 인증: 없음 (사용자 API 키 불필요) 샌드박스·셸 없이 웹 읽기만 가능한 채팅 에이전트는 아래 HTML GET 주소를 먼저 여세요. npm 설치나 HTTP POST 실행이 필요 없습니다. diff --git a/src/pages/apiResult.ts b/src/pages/apiResult.ts index 35d8956e..a0c26168 100644 --- a/src/pages/apiResult.ts +++ b/src/pages/apiResult.ts @@ -1,4 +1,5 @@ /** 웹 읽기 에이전트용 공개 GET 결과 표시 */ +import { WEB_READ_RULE } from './agentStart.js'; import type { MiddlewareHandler } from 'hono'; import type { AppBindings } from '../api/response.js'; @@ -33,9 +34,10 @@ export const apiResultHtml: MiddlewareHandler<{ Bindings: AppBindings }> = async const title = failed ? '조회 실패' : '조회 성공'; const html = `${title} — 다이소 MCP

${title}

+

${escapeHtml(WEB_READ_RULE)}

요청: ${source} · HTTP ${c.res.status}

실제 API 조회 결과입니다. 오류를 상품 없음이나 재고 0으로 해석하지 마세요. 매장 재고는 상품 ID와 매장명 또는 사용자 위치를 확인한 뒤 별도로 조회하세요.

${renderValue(data, url.pathname === '/api/daiso/products')} -

동일 조회의 JSON 원본 · 설치 없는 조회 안내 · API 경로와 필수 파라미터 안내

+

개발자용 JSON 원본 (웹 읽기용 아님) · 설치 없는 조회 안내 · API 경로와 필수 파라미터 안내

다른 공개 GET 조회도 기존 파라미터에 format=html을 추가하면 HTML로 읽을 수 있습니다. 웹 읽기만으로 MCP 연결이 생기는 것은 아닙니다.

`; const headers = new Headers(c.res.headers); headers.set('Content-Type', 'text/html; charset=utf-8'); diff --git a/src/pages/discovery.ts b/src/pages/discovery.ts index 2f69f0b5..a2ac11c1 100644 --- a/src/pages/discovery.ts +++ b/src/pages/discovery.ts @@ -1,4 +1,4 @@ -import { buildAgentStartText } from './agentStart.js'; +import { buildAgentStartText, WEB_READ_RULE } from './agentStart.js'; const BASE = 'https://mcp.aka.page'; @@ -22,6 +22,7 @@ export function buildDiscoveryAssets(info: { services: { name: string }[]; tools

다이소 MCP

설치 없이 상품·매장·재고와 영화관 정보를 조회하세요. 사용자 API 키나 로그인 없이 쓸 수 있는 공개 서비스입니다.

+

${escapeHtml(WEB_READ_RULE)}

바로 조회하기

다이소 수납박스 상품 3개 조회

위 링크는 셸·설치 없이 웹 읽기로 호출하는 공개 REST GET의 HTML 결과입니다. 다른 공개 GET 조회도 기존 파라미터에 format=html을 추가하세요. 검색한 상품의 실제 ID로 재고를 조회하세요. 상품 검색만으로 매장 재고를 단정하지 마세요.

에이전트에서 설치 없이 사용하기

원격 MCP 주소: ${BASE}
전송: Streamable HTTP · 인증: 없음

@@ -32,7 +33,7 @@ export function buildDiscoveryAssets(info: { services: { name: string }[]; tools -H 'Accept: application/json, text/event-stream' \\ --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"daiso_search_products","arguments":{"query":"수납박스","pageSize":3}}}'

웹 읽기 기능으로 이 페이지를 읽는 것만으로 MCP 연결이 생기지는 않습니다. 샌드박스나 셸 없이 웹 읽기만 가능한 채팅 에이전트는 위 HTML REST 링크를 바로 여세요. 오류를 상품 없음으로 해석하지 마세요.

-

짧은 시작 안내 · 전체 API 사용 안내 · 현재 서비스·도구 목록 JSON

+

짧은 시작 안내 · 전체 API 사용 안내 · 개발자용 서비스·도구 목록 JSON

지원 서비스

${services}

\n`, diff --git a/src/pages/promptCoreText.ts b/src/pages/promptCoreText.ts index 8fd97f22..36e7b387 100644 --- a/src/pages/promptCoreText.ts +++ b/src/pages/promptCoreText.ts @@ -6,7 +6,7 @@ export function buildPromptCoreText(baseUrl: string): string { return `# 다이소 MCP API 다이소 제품 검색, 매장 찾기, 재고 확인을 위한 API입니다. -아래 상세 REST API 요청은 GET 방식이며, 결과는 JSON으로 반환됩니다. +아래 상세 REST API 요청은 GET 방식입니다. 웹 읽기용 URL 예시는 format=html을 포함합니다. 응답 예시 JSON은 기본 JSON 응답의 구조를 설명하며 HTML에도 같은 실제 필드가 표시됩니다. JSON API 클라이언트는 format을 생략하세요. Base URL: ${baseUrl} @@ -18,7 +18,7 @@ Base URL: ${baseUrl} **설명**: 키워드로 다이소 제품을 검색합니다. -**URL**: ${baseUrl}/api/daiso/products?q={검색어} +**URL**: ${baseUrl}/api/daiso/products?q={검색어}&format=html **필수 파라미터**: - q: 검색 키워드 (예: 수납박스, 펜, 정리함) @@ -28,8 +28,8 @@ Base URL: ${baseUrl} - pageSize: 페이지당 결과 수 (기본값: 30, 최대: 100) **예시**: -- ${baseUrl}/api/daiso/products?q=수납박스 -- ${baseUrl}/api/daiso/products?q=펜&page=2&pageSize=10 +- ${baseUrl}/api/daiso/products?q=수납박스&format=html +- ${baseUrl}/api/daiso/products?q=펜&page=2&pageSize=10&format=html **응답 예시**: \`\`\`json @@ -58,10 +58,10 @@ Base URL: ${baseUrl} **설명**: 제품 ID로 상세 정보를 조회합니다. -**URL**: ${baseUrl}/api/daiso/products/{제품ID} +**URL**: ${baseUrl}/api/daiso/products/{제품ID}?format=html **예시**: -- ${baseUrl}/api/daiso/products/1234567890 +- ${baseUrl}/api/daiso/products/1234567890?format=html **응답 예시**: \`\`\`json @@ -86,7 +86,7 @@ Base URL: ${baseUrl} **설명**: 키워드 또는 지역으로 다이소 매장을 검색합니다. -**URL**: ${baseUrl}/api/daiso/stores?keyword={키워드} +**URL**: ${baseUrl}/api/daiso/stores?keyword={키워드}&format=html **필수 파라미터** (둘 중 하나 필수): - keyword: 매장명 또는 주소 키워드 (예: 강남, 홍대, 안산) @@ -98,9 +98,9 @@ Base URL: ${baseUrl} - limit: 최대 결과 수 (기본값: 50) **예시**: -- ${baseUrl}/api/daiso/stores?keyword=강남 -- ${baseUrl}/api/daiso/stores?sido=서울&gugun=마포구 -- ${baseUrl}/api/daiso/stores?keyword=홍대&limit=10 +- ${baseUrl}/api/daiso/stores?keyword=강남&format=html +- ${baseUrl}/api/daiso/stores?sido=서울&gugun=마포구&format=html +- ${baseUrl}/api/daiso/stores?keyword=홍대&limit=10&format=html **응답 예시**: \`\`\`json @@ -134,7 +134,7 @@ Base URL: ${baseUrl} **설명**: 특정 제품의 매장별 재고와 온라인 재고를 확인합니다. -**URL**: ${baseUrl}/api/daiso/inventory?productId={제품ID} +**URL**: ${baseUrl}/api/daiso/inventory?productId={제품ID}&format=html **필수 파라미터**: - productId: 제품 ID (제품 검색 API에서 조회한 id 값) @@ -152,9 +152,9 @@ Base URL: ${baseUrl} - 안산 중앙역 같은 역명 키워드가 비면 안산중앙역, 안산중앙, 고잔처럼 붙여쓴 변형으로 재시도하세요. **예시**: -- ${baseUrl}/api/daiso/inventory?productId=1234567890 -- ${baseUrl}/api/daiso/inventory?productId=1234567890&lat=37.3219&lng=126.8309 -- ${baseUrl}/api/daiso/inventory?productId=1234567890&keyword=안산 +- ${baseUrl}/api/daiso/inventory?productId=1234567890&format=html +- ${baseUrl}/api/daiso/inventory?productId=1234567890&lat=37.3219&lng=126.8309&format=html +- ${baseUrl}/api/daiso/inventory?productId=1234567890&keyword=안산&format=html **응답 예시**: \`\`\`json @@ -197,14 +197,14 @@ Base URL: ${baseUrl} **설명**: 특정 상품이 특정 매장 내 어디에 진열되어 있는지(구역/층) 조회합니다. -**URL**: ${baseUrl}/api/daiso/display-location?productId={제품ID}&storeCode={매장코드} +**URL**: ${baseUrl}/api/daiso/display-location?productId={제품ID}&storeCode={매장코드}&format=html **필수 파라미터**: - productId: 제품 ID (제품 검색 API에서 조회한 id 값) - storeCode: 매장 코드 (재고 확인 API 결과의 storeCode 값) **예시**: -- ${baseUrl}/api/daiso/display-location?productId=1234567890&storeCode=04515 +- ${baseUrl}/api/daiso/display-location?productId=1234567890&storeCode=04515&format=html --- @@ -212,7 +212,7 @@ Base URL: ${baseUrl} **설명**: 위치 기반으로 주변 올리브영 매장을 검색합니다. -**URL**: ${baseUrl}/api/oliveyoung/stores?keyword={키워드} +**URL**: ${baseUrl}/api/oliveyoung/stores?keyword={키워드}&format=html **선택 파라미터**: - keyword: 매장명/지역 키워드 (예: 명동, 강남) @@ -222,8 +222,8 @@ Base URL: ${baseUrl} - limit: 최대 결과 수 (기본값: 20) **예시**: -- ${baseUrl}/api/oliveyoung/stores?keyword=명동 -- ${baseUrl}/api/oliveyoung/stores?lat=37.498&lng=127.027&limit=5 +- ${baseUrl}/api/oliveyoung/stores?keyword=명동&format=html +- ${baseUrl}/api/oliveyoung/stores?lat=37.498&lng=127.027&limit=5&format=html --- @@ -235,7 +235,7 @@ Base URL: ${baseUrl} - 응답의 \`products[].imageUrl\`이 있으면 각 상품 이미지를 반드시 마크다운 이미지로 함께 렌더링합니다. - 여러 상품이 나오면 첫 상품만 대표로 보여주지 말고, 이미지가 있는 상품은 전부 표시합니다. -**URL**: ${baseUrl}/api/oliveyoung/products?keyword={검색어} +**URL**: ${baseUrl}/api/oliveyoung/products?keyword={검색어}&format=html **필수 파라미터**: - keyword: 상품 검색어 (예: 마스크팩, 선크림, 립밤) @@ -247,8 +247,8 @@ Base URL: ${baseUrl} - includeSoldOut: 품절 포함 여부 (기본값: false) **예시**: -- ${baseUrl}/api/oliveyoung/products?keyword=마스크팩 -- ${baseUrl}/api/oliveyoung/products?keyword=립밤&size=10 +- ${baseUrl}/api/oliveyoung/products?keyword=마스크팩&format=html +- ${baseUrl}/api/oliveyoung/products?keyword=립밤&size=10&format=html --- @@ -256,7 +256,7 @@ Base URL: ${baseUrl} **설명**: 상품 키워드 기준 올리브영 재고를 조회하고 주변 매장 목록을 함께 반환합니다. 상위 상품에는 storeInventory가 포함되며 매장별 재고 3개, 재고 9개 이상, 품절, 미판매 같은 상태를 제공합니다. -**URL**: ${baseUrl}/api/oliveyoung/inventory?keyword={검색어} +**URL**: ${baseUrl}/api/oliveyoung/inventory?keyword={검색어}&format=html **필수 파라미터**: - keyword: 상품 검색어 (예: 선크림, 립밤) @@ -270,8 +270,8 @@ Base URL: ${baseUrl} - includeSoldOut: 품절 포함 여부 (기본값: false) **예시**: -- ${baseUrl}/api/oliveyoung/inventory?keyword=선크림 -- ${baseUrl}/api/oliveyoung/inventory?keyword=립밤&storeKeyword=명동 +- ${baseUrl}/api/oliveyoung/inventory?keyword=선크림&format=html +- ${baseUrl}/api/oliveyoung/inventory?keyword=립밤&storeKeyword=명동&format=html --- diff --git a/src/pages/promptReferenceText.ts b/src/pages/promptReferenceText.ts index c1966f2e..e3e93beb 100644 --- a/src/pages/promptReferenceText.ts +++ b/src/pages/promptReferenceText.ts @@ -5,6 +5,8 @@ export function buildPromptReferenceText(baseUrl: string): string { return `## 응답 형식 +아래 JSON은 기본 JSON 응답의 구조입니다. 웹 읽기 전용 에이전트는 같은 API에 format=html을 반드시 포함해 HTML 결과를 읽으세요. + ### 성공 응답 \`\`\`json { @@ -93,7 +95,7 @@ export function buildPromptReferenceText(baseUrl: string): string { 5. **재고 확인 워크플로우**: - 먼저 /api/daiso/products로 제품 검색 - 결과에서 원하는 제품의 id 확인 - - /api/daiso/inventory에 해당 id로 재고 조회 + - /api/daiso/inventory에 해당 id와 확인한 매장 조건, format=html로 재고 조회 - 진열 위치가 필요하면 재고 응답의 storeCode를 확인한 뒤 /api/daiso/display-location에 productId + storeCode로 조회 - 다이소 재고 조회는 storeCode가 필요하지 않습니다. 6. **최소 정보 요청 처리**: diff --git a/src/pages/promptRetailCinemaText.ts b/src/pages/promptRetailCinemaText.ts index 635acd2d..2a4c7597 100644 --- a/src/pages/promptRetailCinemaText.ts +++ b/src/pages/promptRetailCinemaText.ts @@ -5,9 +5,9 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { return `### 디트릭스 독립·예술영화관 -- 극장 카탈로그: ${baseUrl}/api/dtryx/cinemas?region=서울 -- 상영작: ${baseUrl}/api/dtryx/movies?keyword=모모&includePlayDates=true -- 회차와 잔여 좌석: ${baseUrl}/api/dtryx/seats?region=서울 +- 극장 카탈로그: ${baseUrl}/api/dtryx/cinemas?region=서울&format=html +- 상영작: ${baseUrl}/api/dtryx/movies?keyword=모모&includePlayDates=true&format=html +- 회차와 잔여 좌석: ${baseUrl}/api/dtryx/seats?region=서울&format=html 상영작 조회는 cinemaCode 또는 keyword가 필요합니다. 날짜 기본값은 한국 날짜 기준 오늘이며 YYYYMMDD 또는 YYYY-MM-DD를 받습니다. 지역·극장명·브랜드 필터는 함께 적용합니다. 카탈로그는 확인된 22곳이며 전체 제휴 극장이 아닙니다. 일부 실패는 failedCinemas로 표시하고 전체 조회 실패는 오류입니다. @@ -16,15 +16,15 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 새 외부 API 키 없이 기존 다이소, GS25, 세븐일레븐, 이마트24 상품 검색을 묶어 가격 후보를 비교합니다. 실제 매장 재고와 행사가까지 확정하는 기능이 아니라 "어디가 싸 보이는지"를 빠르게 고르는 1차 검색입니다. **URL**: -- ${baseUrl}/api/compare/products?keyword={검색어} +- ${baseUrl}/api/compare/products?keyword={검색어}&format=html **선택 파라미터**: - services: daiso,gs25,seveneleven,emart24 중 쉼표 구분 목록 - limit: 서비스별 최대 결과 수 (기본값: 5) **예시**: -- ${baseUrl}/api/compare/products?keyword=콜라&limit=3 -- ${baseUrl}/api/compare/products?keyword=컵라면&services=seveneleven,emart24 +- ${baseUrl}/api/compare/products?keyword=콜라&limit=3&format=html +- ${baseUrl}/api/compare/products?keyword=컵라면&services=seveneleven,emart24&format=html --- @@ -33,8 +33,8 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 네이버 지역 검색으로 특정 지역의 음식점, 카페, 디저트 가게 등 주변 장소를 조회합니다. 좌표 반경 검색이 아니라 \`강남역 카페\` 같은 키워드 기반 검색입니다. **URL**: -- ${baseUrl}/api/places/search?location={지역}&category={카테고리} -- ${baseUrl}/api/places/search?location={지역}&keyword={검색어} +- ${baseUrl}/api/places/search?location={지역}&category={카테고리}&format=html +- ${baseUrl}/api/places/search?location={지역}&keyword={검색어}&format=html **선택 파라미터**: - location: 지역/역/주소 키워드 (예: 강남역, 성수동) @@ -44,8 +44,8 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - sort: random 또는 comment **예시**: -- ${baseUrl}/api/places/search?location=강남역&category=cafe&limit=5 -- ${baseUrl}/api/places/search?location=성수동&keyword=브런치&limit=5 +- ${baseUrl}/api/places/search?location=강남역&category=cafe&limit=5&format=html +- ${baseUrl}/api/places/search?location=성수동&keyword=브런치&limit=5&format=html --- @@ -54,10 +54,10 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: GS25 매장 탐색, 상품 키워드 검색, 재고 조회를 제공합니다. **URL**: -- ${baseUrl}/api/gs25/stores?keyword={키워드} -- ${baseUrl}/api/gs25/products?keyword={검색어} -- ${baseUrl}/api/gs25/inventory?keyword={검색어} -- ${baseUrl}/api/gs25/inventory?itemCode={상품코드} +- ${baseUrl}/api/gs25/stores?keyword={키워드}&format=html +- ${baseUrl}/api/gs25/products?keyword={검색어}&format=html +- ${baseUrl}/api/gs25/inventory?keyword={검색어}&format=html +- ${baseUrl}/api/gs25/inventory?itemCode={상품코드}&format=html **선택 파라미터(공통)**: - lat: 위도 (선택) @@ -66,10 +66,10 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - serviceCode: 서비스 코드 (기본값: 01) **예시**: -- ${baseUrl}/api/gs25/stores?keyword=강남&limit=10 -- ${baseUrl}/api/gs25/products?keyword=오감자&limit=20 -- ${baseUrl}/api/gs25/inventory?keyword=오감자&storeKeyword=강남&storeLimit=10 -- ${baseUrl}/api/gs25/inventory?itemCode=8801056038861&storeKeyword=안산%20중앙역&storeLimit=10 +- ${baseUrl}/api/gs25/stores?keyword=강남&limit=10&format=html +- ${baseUrl}/api/gs25/products?keyword=오감자&limit=20&format=html +- ${baseUrl}/api/gs25/inventory?keyword=오감자&storeKeyword=강남&storeLimit=10&format=html +- ${baseUrl}/api/gs25/inventory?itemCode=8801056038861&storeKeyword=안산%20중앙역&storeLimit=10&format=html --- @@ -78,11 +78,11 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 세븐일레븐 상품 검색, 매장 검색, 재고 수량 조회, 인기 검색어, 카탈로그 스냅샷을 제공합니다. **URL**: -- ${baseUrl}/api/seveneleven/products?query={검색어} -- ${baseUrl}/api/seveneleven/stores?keyword={매장키워드} -- ${baseUrl}/api/seveneleven/inventory?keyword={검색어} -- ${baseUrl}/api/seveneleven/popwords?label={라벨} -- ${baseUrl}/api/seveneleven/catalog?includeIssues={true|false}&includeExhibition={true|false}&limit={개수} +- ${baseUrl}/api/seveneleven/products?query={검색어}&format=html +- ${baseUrl}/api/seveneleven/stores?keyword={매장키워드}&format=html +- ${baseUrl}/api/seveneleven/inventory?keyword={검색어}&format=html +- ${baseUrl}/api/seveneleven/popwords?label={라벨}&format=html +- ${baseUrl}/api/seveneleven/catalog?includeIssues={true|false}&includeExhibition={true|false}&limit={개수}&format=html **선택 파라미터**: - page: 페이지 번호 (기본값: 1) @@ -94,11 +94,11 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - label: 인기 검색어 라벨 (기본값: home) **예시**: -- ${baseUrl}/api/seveneleven/products?query=삼각김밥&size=20 -- ${baseUrl}/api/seveneleven/stores?keyword=안산%20중앙역&limit=10 -- ${baseUrl}/api/seveneleven/inventory?keyword=핫식스&storeKeyword=안산%20중앙역&storeLimit=10 -- ${baseUrl}/api/seveneleven/popwords?label=home -- ${baseUrl}/api/seveneleven/catalog?includeIssues=true&includeExhibition=true&limit=10 +- ${baseUrl}/api/seveneleven/products?query=삼각김밥&size=20&format=html +- ${baseUrl}/api/seveneleven/stores?keyword=안산%20중앙역&limit=10&format=html +- ${baseUrl}/api/seveneleven/inventory?keyword=핫식스&storeKeyword=안산%20중앙역&storeLimit=10&format=html +- ${baseUrl}/api/seveneleven/popwords?label=home&format=html +- ${baseUrl}/api/seveneleven/catalog?includeIssues=true&includeExhibition=true&limit=10&format=html --- @@ -106,7 +106,7 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 사용자 좌표 또는 위치 키워드 기준으로 메가박스 지점을 거리순으로 조회합니다. -**URL**: ${baseUrl}/api/megabox/theaters?lat={위도}&lng={경도} +**URL**: ${baseUrl}/api/megabox/theaters?lat={위도}&lng={경도}&format=html **선택 파라미터**: - keyword: 위치 키워드 (예: 안산 중앙역, 강남역) @@ -117,9 +117,9 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - limit: 최대 결과 수 (기본값: 10) **예시**: -- ${baseUrl}/api/megabox/theaters?keyword=안산%20중앙역&limit=5 -- ${baseUrl}/api/megabox/theaters?lat=37.4982&lng=127.0264 -- ${baseUrl}/api/megabox/theaters?areaCode=11&limit=5 +- ${baseUrl}/api/megabox/theaters?keyword=안산%20중앙역&limit=5&format=html +- ${baseUrl}/api/megabox/theaters?lat=37.4982&lng=127.0264&format=html +- ${baseUrl}/api/megabox/theaters?areaCode=11&limit=5&format=html --- @@ -127,7 +127,7 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 날짜/지점 조건으로 메가박스 영화와 상영 회차를 조회합니다. theaterId가 없고 위치 키워드/좌표가 있으면 가장 가까운 지점을 먼저 선택합니다. -**URL**: ${baseUrl}/api/megabox/movies?playDate={YYYYMMDD} +**URL**: ${baseUrl}/api/megabox/movies?playDate={YYYYMMDD}&format=html **선택 파라미터**: - playDate: 조회 날짜 (YYYYMMDD, 기본값: 오늘) @@ -139,9 +139,9 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - areaCode: 지역 코드 (기본값: 11) **예시**: -- ${baseUrl}/api/megabox/movies?playDate=20260315&keyword=안산%20중앙역 -- ${baseUrl}/api/megabox/movies?playDate=20260304&theaterId=1372 -- ${baseUrl}/api/megabox/movies?playDate=20260304&movieId=25104500 +- ${baseUrl}/api/megabox/movies?playDate=20260315&keyword=안산%20중앙역&format=html +- ${baseUrl}/api/megabox/movies?playDate=20260304&theaterId=1372&format=html +- ${baseUrl}/api/megabox/movies?playDate=20260304&movieId=25104500&format=html --- @@ -149,7 +149,7 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 영화/지점/날짜 기준으로 회차별 잔여 좌석 수를 조회합니다. theaterId가 없고 위치 키워드/좌표가 있으면 가장 가까운 지점을 먼저 선택합니다. -**URL**: ${baseUrl}/api/megabox/seats?playDate={YYYYMMDD} +**URL**: ${baseUrl}/api/megabox/seats?playDate={YYYYMMDD}&format=html **선택 파라미터**: - playDate: 조회 날짜 (YYYYMMDD, 기본값: 오늘) @@ -162,9 +162,9 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - limit: 최대 결과 수 (기본값: 50) **예시**: -- ${baseUrl}/api/megabox/seats?playDate=20260315&keyword=안산%20중앙역 -- ${baseUrl}/api/megabox/seats?playDate=20260304&theaterId=1372 -- ${baseUrl}/api/megabox/seats?playDate=20260304&movieId=25104500&limit=20 +- ${baseUrl}/api/megabox/seats?playDate=20260315&keyword=안산%20중앙역&format=html +- ${baseUrl}/api/megabox/seats?playDate=20260304&theaterId=1372&format=html +- ${baseUrl}/api/megabox/seats?playDate=20260304&movieId=25104500&limit=20&format=html --- @@ -172,7 +172,7 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 지역 코드 또는 위치 키워드 기준으로 CGV 극장 목록을 조회합니다. -**URL**: ${baseUrl}/api/cgv/theaters?playDate={YYYYMMDD} +**URL**: ${baseUrl}/api/cgv/theaters?playDate={YYYYMMDD}&format=html **선택 파라미터**: - playDate: 조회 날짜 (YYYYMMDD, 기본값: 오늘) @@ -183,9 +183,9 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - limit: 최대 결과 수 (기본값: 30) **예시**: -- ${baseUrl}/api/cgv/theaters?playDate=20260304®ionCode=01 -- ${baseUrl}/api/cgv/theaters?playDate=20260315&keyword=안산%20중앙역 -- ${baseUrl}/api/cgv/theaters?playDate=20260304&limit=10 +- ${baseUrl}/api/cgv/theaters?playDate=20260304®ionCode=01&format=html +- ${baseUrl}/api/cgv/theaters?playDate=20260315&keyword=안산%20중앙역&format=html +- ${baseUrl}/api/cgv/theaters?playDate=20260304&limit=10&format=html --- @@ -193,7 +193,7 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 날짜/극장 조건으로 CGV 영화 목록을 조회합니다. theaterCode가 없으면 keyword 또는 lat,lng 기준으로 가장 가까운 극장을 먼저 선택합니다. -**URL**: ${baseUrl}/api/cgv/movies?playDate={YYYYMMDD} +**URL**: ${baseUrl}/api/cgv/movies?playDate={YYYYMMDD}&format=html **선택 파라미터**: - playDate: 조회 날짜 (YYYYMMDD, 기본값: 오늘) @@ -203,8 +203,8 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - lng: 경도 **예시**: -- ${baseUrl}/api/cgv/movies?playDate=20260304&theaterCode=0056 -- ${baseUrl}/api/cgv/movies?playDate=20260315&keyword=안산%20중앙역 +- ${baseUrl}/api/cgv/movies?playDate=20260304&theaterCode=0056&format=html +- ${baseUrl}/api/cgv/movies?playDate=20260315&keyword=안산%20중앙역&format=html --- @@ -212,7 +212,7 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 날짜/극장/영화 조건으로 CGV 상영 시간표를 조회합니다. theaterCode가 없으면 keyword 또는 lat,lng 기준으로 가장 가까운 극장을 먼저 선택합니다. 잔여 좌석은 각 회차의 \`remainingSeats\` 필드로 함께 내려옵니다. -**URL**: ${baseUrl}/api/cgv/timetable?playDate={YYYYMMDD} +**URL**: ${baseUrl}/api/cgv/timetable?playDate={YYYYMMDD}&format=html **선택 파라미터**: - playDate: 조회 날짜 (YYYYMMDD, 기본값: 오늘) @@ -224,9 +224,9 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - limit: 최대 결과 수 (기본값: 50) **예시**: -- ${baseUrl}/api/cgv/timetable?playDate=20260304&theaterCode=0056 -- ${baseUrl}/api/cgv/timetable?playDate=20260315&keyword=안산%20중앙역 -- ${baseUrl}/api/cgv/timetable?playDate=20260304&movieCode=200001 +- ${baseUrl}/api/cgv/timetable?playDate=20260304&theaterCode=0056&format=html +- ${baseUrl}/api/cgv/timetable?playDate=20260315&keyword=안산%20중앙역&format=html +- ${baseUrl}/api/cgv/timetable?playDate=20260304&movieCode=200001&format=html --- @@ -234,7 +234,7 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 위치 키워드 또는 사용자 좌표 기준으로 롯데시네마 지점을 거리순으로 조회합니다. -**URL**: ${baseUrl}/api/lottecinema/theaters?keyword={위치키워드} +**URL**: ${baseUrl}/api/lottecinema/theaters?keyword={위치키워드}&format=html **선택 파라미터**: - keyword: 위치 키워드 (예: 안산 중앙역, 잠실역) @@ -244,8 +244,8 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - limit: 최대 결과 수 (기본값: 10) **예시**: -- ${baseUrl}/api/lottecinema/theaters?keyword=안산%20중앙역 -- ${baseUrl}/api/lottecinema/theaters?lat=37.5133&lng=127.1042 +- ${baseUrl}/api/lottecinema/theaters?keyword=안산%20중앙역&format=html +- ${baseUrl}/api/lottecinema/theaters?lat=37.5133&lng=127.1042&format=html --- @@ -253,7 +253,7 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 날짜/지점/영화 조건으로 롯데시네마 영화와 상영 회차를 조회합니다. theaterId가 없으면 위치 키워드 기준 최근접 지점을 선택할 수 있습니다. -**URL**: ${baseUrl}/api/lottecinema/movies?playDate={YYYYMMDD} +**URL**: ${baseUrl}/api/lottecinema/movies?playDate={YYYYMMDD}&format=html **선택 파라미터**: - playDate: 조회 날짜 (YYYYMMDD, 기본값: 오늘) @@ -264,9 +264,9 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - lng: 경도 (theaterId가 없을 때 사용) **예시**: -- ${baseUrl}/api/lottecinema/movies?playDate=20260315&keyword=안산%20중앙역 -- ${baseUrl}/api/lottecinema/movies?playDate=20260310&theaterId=1016 -- ${baseUrl}/api/lottecinema/movies?playDate=20260310&theaterId=1016&movieId=23816 +- ${baseUrl}/api/lottecinema/movies?playDate=20260315&keyword=안산%20중앙역&format=html +- ${baseUrl}/api/lottecinema/movies?playDate=20260310&theaterId=1016&format=html +- ${baseUrl}/api/lottecinema/movies?playDate=20260310&theaterId=1016&movieId=23816&format=html --- @@ -274,7 +274,7 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { **설명**: 영화/지점/날짜 기준으로 회차별 잔여 좌석 수를 조회합니다. theaterId가 없으면 위치 키워드 기준 최근접 지점을 선택할 수 있습니다. -**URL**: ${baseUrl}/api/lottecinema/seats?playDate={YYYYMMDD} +**URL**: ${baseUrl}/api/lottecinema/seats?playDate={YYYYMMDD}&format=html **선택 파라미터**: - playDate: 조회 날짜 (YYYYMMDD, 기본값: 오늘) @@ -286,8 +286,8 @@ export function buildPromptRetailCinemaText(baseUrl: string): string { - limit: 최대 결과 수 (기본값: 50) **예시**: -- ${baseUrl}/api/lottecinema/seats?playDate=20260315&keyword=안산%20중앙역 -- ${baseUrl}/api/lottecinema/seats?playDate=20260310&theaterId=1016&movieId=23816 +- ${baseUrl}/api/lottecinema/seats?playDate=20260315&keyword=안산%20중앙역&format=html +- ${baseUrl}/api/lottecinema/seats?playDate=20260310&theaterId=1016&movieId=23816&format=html --- diff --git a/tests/pages/htmlGuidance.test.ts b/tests/pages/htmlGuidance.test.ts new file mode 100644 index 00000000..4c8108a4 --- /dev/null +++ b/tests/pages/htmlGuidance.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { buildDiscoveryAssets } from '../../src/pages/discovery.js'; +import { buildAgentStartText } from '../../src/pages/agentStart.js'; +import { generatePromptText } from '../../src/pages/prompt.js'; +import app from '../../src/index.js'; + +const rule = '모든 /api/* GET 조회 URL에 format=html을 반드시 포함하세요'; +describe('웹 읽기 전용 에이전트 안내', () => { + it('첫 화면과 llms 및 prompt 앞부분에서 HTML 필수 규칙을 발견한다', () => { + const assets = buildDiscoveryAssets({ services: [], tools: [] }); + for (const text of [assets['discovery.html'], buildAgentStartText('https://guide.example'), generatePromptText('https://guide.example')]) { + expect(text.slice(0,2500)).toContain(rule); + expect(text).toContain('실행할 수 없다고 단정하지'); + } + }); + it('전체 상세 GET 예시가 HTML 형식이고 JSON 스키마 설명과 혼동하지 않는다', () => { + const prompt = generatePromptText('https://guide.example'); + const urls = prompt.match(/https:\/\/guide\.example\/api\/[^\s]+/g)!; + expect(urls.length).toBeGreaterThan(30); + for (const url of urls) expect(url).toMatch(/[?&]format=html/); + expect(prompt).not.toContain('아래 상세 REST API 요청은 GET 방식이며, 결과는 JSON으로 반환됩니다.'); + expect(prompt).toContain('기본 JSON 응답의 구조'); + }); + it('HTML 조회 결과에서도 이후 조회 규칙과 개발자용 JSON 구분을 유지한다', async () => { + const response = await app.request('/api/daiso/products?format=html'); + const text = await response.text(); + expect(text).toContain(rule); + expect(text).toContain('개발자용 JSON 원본'); + expect(text).toContain('웹 읽기용 아님'); + }); +});