v2.2.321: /naver-shopping "분야 내 인기 검색어" 질문이 무의미한 1행 표만 주던 문제 수정
- 쇼핑인사이트 API 는 분야 자체의 클릭 추이만 주고 분야 안의 키워드 순위 API가 없음 — intentParser 에 scope(category/keywords) 필드 추가해 "인기 검색어" 류 질문을 감지하고, 검증된 검색어트렌드 rankKeywords 로 우회 처리하도록 수정
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "직답 결과 — single-pass mock 응답입니다.",
|
"result": "직답 결과 — single-pass mock 응답입니다.",
|
||||||
"createdAt": 1785978343885,
|
"createdAt": 1785979804062,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "---\nid: wiki_on\ndate: 2026-08-06T01:05:43.891Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\n직답 결과 — single-pass mock 응답입니다.\n\n직답 결과 — single-pass mock 응답입니다.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `0/100` | ✅ Low |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[DIRECT]** 답변 작성 중... (단일 호출 fast-path) (17ms)\n",
|
"result": "---\nid: wiki_on\ndate: 2026-08-06T01:30:04.066Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\n직답 결과 — single-pass mock 응답입니다.\n\n직답 결과 — single-pass mock 응답입니다.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `0/100` | ✅ Low |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[DIRECT]** 답변 작성 중... (단일 호출 fast-path) (27ms)\n",
|
||||||
"createdAt": 1785978343891,
|
"createdAt": 1785979804069,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"missionId": "wiki_on",
|
"missionId": "wiki_on",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"startTime": "2026-08-06T01:05:43.866Z",
|
"startTime": "2026-08-06T01:30:04.033Z",
|
||||||
"totalElapsedMs": 27,
|
"totalElapsedMs": 39,
|
||||||
"results": {
|
"results": {
|
||||||
"direct": "직답 결과 — single-pass mock 응답입니다."
|
"direct": "직답 결과 — single-pass mock 응답입니다."
|
||||||
},
|
},
|
||||||
@@ -12,16 +12,16 @@
|
|||||||
{
|
{
|
||||||
"from": "idle",
|
"from": "idle",
|
||||||
"to": "direct",
|
"to": "direct",
|
||||||
"durationMs": 17,
|
"durationMs": 27,
|
||||||
"message": "답변 작성 중... (단일 호출 fast-path)",
|
"message": "답변 작성 중... (단일 호출 fast-path)",
|
||||||
"ts": "2026-08-06T01:05:43.883Z"
|
"ts": "2026-08-06T01:30:04.060Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "direct",
|
"from": "direct",
|
||||||
"to": "completed",
|
"to": "completed",
|
||||||
"durationMs": 9,
|
"durationMs": 11,
|
||||||
"message": "미션 완료",
|
"message": "미션 완료",
|
||||||
"ts": "2026-08-06T01:05:43.892Z"
|
"ts": "2026-08-06T01:30:04.071Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"resilienceMetrics": {
|
"resilienceMetrics": {
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||||
"createdAt": 1785978351712,
|
"createdAt": 1785979811217,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||||
"createdAt": 1785978351711,
|
"createdAt": 1785979811215,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
|
"result": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
|
||||||
"createdAt": 1785978351707,
|
"createdAt": 1785979811209,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||||
"createdAt": 1785978351709,
|
"createdAt": 1785979811212,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+9
-9
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"missionId": "stress_conflict_1785978351690",
|
"missionId": "stress_conflict_1785979811195",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"startTime": "2026-08-06T01:05:51.690Z",
|
"startTime": "2026-08-06T01:30:11.195Z",
|
||||||
"totalElapsedMs": 23,
|
"totalElapsedMs": 23,
|
||||||
"results": {
|
"results": {
|
||||||
"outline": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
|
"outline": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
|
||||||
@@ -14,30 +14,30 @@
|
|||||||
{
|
{
|
||||||
"from": "idle",
|
"from": "idle",
|
||||||
"to": "outline",
|
"to": "outline",
|
||||||
"durationMs": 16,
|
"durationMs": 13,
|
||||||
"message": "답변 구조 잡는 중...",
|
"message": "답변 구조 잡는 중...",
|
||||||
"ts": "2026-08-06T01:05:51.706Z"
|
"ts": "2026-08-06T01:30:11.208Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "outline",
|
"from": "outline",
|
||||||
"to": "section",
|
"to": "section",
|
||||||
"durationMs": 3,
|
"durationMs": 3,
|
||||||
"message": "본문 작성 중...",
|
"message": "본문 작성 중...",
|
||||||
"ts": "2026-08-06T01:05:51.709Z"
|
"ts": "2026-08-06T01:30:11.211Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "section",
|
"from": "section",
|
||||||
"to": "polish",
|
"to": "polish",
|
||||||
"durationMs": 1,
|
"durationMs": 3,
|
||||||
"message": "최종 다듬기 중...",
|
"message": "최종 다듬기 중...",
|
||||||
"ts": "2026-08-06T01:05:51.710Z"
|
"ts": "2026-08-06T01:30:11.214Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "polish",
|
"from": "polish",
|
||||||
"to": "completed",
|
"to": "completed",
|
||||||
"durationMs": 3,
|
"durationMs": 4,
|
||||||
"message": "미션 완료",
|
"message": "미션 완료",
|
||||||
"ts": "2026-08-06T01:05:51.713Z"
|
"ts": "2026-08-06T01:30:11.218Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"resilienceMetrics": {
|
"resilienceMetrics": {
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
# Astra Patch Notes
|
# Astra Patch Notes
|
||||||
|
|
||||||
|
## v2.2.321 (2026-08-05)
|
||||||
|
### 🛍️ `/naver-shopping` "분야 안의 인기 검색어" 질문이 쓸모없는 1행 표만 주던 문제 수정
|
||||||
|
실사례: `/naver-shopping 디지털/가전 인기 검색어` → "디지털/가전" 분야 자체의 클릭 비율 1행짜리 표만 나와 사용자가 "쓸 수 있는 데이터가 없다"고 피드백. 원인: 쇼핑인사이트 API는 "분야 자체"의 클릭 추이만 제공하고, 분야 *안*에서 어떤 상품/검색어가 인기인지는 애초에 이 API로 알 수 없다(그런 API가 없음) — intentParser 가 "인기 검색어"라는 표현도 그냥 분야명 비교(compare)로 잘못 처리했다.
|
||||||
|
- **scope 필드 추가(intentParser)**: `category`(분야 자체 트렌드/비교, 기존 동작) vs `keywords`(분야 "안"의 인기 상품/검색어가 궁금함 — "인기 검색어"/"뭐가 잘 팔려"/"인기 상품" 같은 표현) 를 LLM 이 구분해서 판정.
|
||||||
|
- **scope="keywords" 처리**: 분야명을 맥락으로 LLM 이 검색어 후보를 제안하고, 이미 검증된 검색어트렌드 API(`rankKeywords`)로 순위를 매겨 보여준다 — 쇼핑인사이트에 없는 "분야 내 키워드 순위" API를 검색어트렌드로 우회 검증하는 방식. 결과에는 "쇼핑 카테고리로 필터링된 수치가 아님"을 명시.
|
||||||
|
검증: tsc 무오류 + jest 1015 통과(신규 2) + esbuild 정상.
|
||||||
|
|
||||||
## v2.2.320 (2026-08-05)
|
## v2.2.320 (2026-08-05)
|
||||||
### 🐛 `/naver-shopping` "CATEGORY_TREE is not iterable" 크래시 수정 + 카테고리 목록 조회
|
### 🐛 `/naver-shopping` "CATEGORY_TREE is not iterable" 크래시 수정 + 카테고리 목록 조회
|
||||||
실사례: 배포 직후 `/naver-shopping 요즘 뜨는 뷰티 카테고리 알려줘` 실행 시 `CATEGORY_TREE is not iterable` 에러. 원인: `import * as x from './data.json'`(네임스페이스 import)를 esbuild 로 번들하면 배열 최상위 JSON 이 `{ default: [...] }` 로 감싸져 `x` 자체가 배열이 아니게 됨 — ts-jest 는 require() 그대로라 이 클래스의 버그를 테스트로 못 잡는다(실제로 기존 유닛 테스트는 전부 통과했었음).
|
실사례: 배포 직후 `/naver-shopping 요즘 뜨는 뷰티 카테고리 알려줘` 실행 시 `CATEGORY_TREE is not iterable` 에러. 원인: `import * as x from './data.json'`(네임스페이스 import)를 esbuild 로 번들하면 배열 최상위 JSON 이 `{ default: [...] }` 로 감싸져 `x` 자체가 배열이 아니게 됨 — ts-jest 는 require() 그대로라 이 클래스의 버그를 테스트로 못 잡는다(실제로 기존 유닛 테스트는 전부 통과했었음).
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "astra",
|
"name": "astra",
|
||||||
"displayName": "Astra",
|
"displayName": "Astra",
|
||||||
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
|
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
|
||||||
"version": "2.2.320",
|
"version": "2.2.321",
|
||||||
"publisher": "g1nation",
|
"publisher": "g1nation",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ function cmdShoppingHelp(view: Webview): void {
|
|||||||
'**/naver-shopping** — 네이버 쇼핑인사이트(분야별 트렌드)',
|
'**/naver-shopping** — 네이버 쇼핑인사이트(분야별 트렌드)',
|
||||||
'',
|
'',
|
||||||
'- `/naver-shopping 패션의류 vs 화장품/미용 최근 일주일` — 명시 분야 비교',
|
'- `/naver-shopping 패션의류 vs 화장품/미용 최근 일주일` — 명시 분야 비교',
|
||||||
'- `/naver-shopping 요즘 뜨는 뷰티 카테고리 알려줘` — 탐색(후보 제안 + 검증)',
|
'- `/naver-shopping 요즘 뜨는 뷰티 카테고리 알려줘` — 분야 자체 탐색(후보 제안 + 검증)',
|
||||||
|
'- `/naver-shopping 디지털/가전 인기 검색어` — 분야 "안"의 인기 키워드 후보(검색어트렌드로 검증)',
|
||||||
'- `/naver-shopping categories` — 대분류(11종) 목록 보기',
|
'- `/naver-shopping categories` — 대분류(11종) 목록 보기',
|
||||||
'- `/naver-shopping categories 원피스` — 카테고리 검색(이름 · 경로 · cat_id)',
|
'- `/naver-shopping categories 원피스` — 카테고리 검색(이름 · 경로 · cat_id)',
|
||||||
'',
|
'',
|
||||||
@@ -119,6 +120,29 @@ async function runShopping(arg: string, view: Webview, context?: vscode.Extensio
|
|||||||
const ai = new AIService();
|
const ai = new AIService();
|
||||||
const intent = await parseShoppingIntent(ai, trimmed, ymd(new Date()));
|
const intent = await parseShoppingIntent(ai, trimmed, ymd(new Date()));
|
||||||
|
|
||||||
|
const range = {
|
||||||
|
startDate: intent.startDate || defaultRange().startDate,
|
||||||
|
endDate: intent.endDate || defaultRange().endDate,
|
||||||
|
timeUnit: intent.timeUnit || 'date' as TimeUnit,
|
||||||
|
};
|
||||||
|
const filters = buildFilters(intent);
|
||||||
|
|
||||||
|
if (intent.scope === 'keywords') {
|
||||||
|
if (intent.keywords.length === 0) {
|
||||||
|
chunk(view, `\n❓ "${trimmed}" 맥락에서 제안할 검색어 후보를 찾지 못했습니다. \`/naver-trend\` 로 직접 키워드를 물어보세요.\n`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const category = intent.categories[0] || trimmed;
|
||||||
|
chunk(view, `\n📊 조회 중 — "${category}" 맥락의 키워드 후보 ${intent.keywords.length}개 검증 중…\n`);
|
||||||
|
const kwResult = await rankKeywords(context, intent.keywords, range, filters);
|
||||||
|
chunk(view, `\n**"${category}" 관련 인기 키워드 후보 TOP ${kwResult.items.length}** (${range.startDate}~${range.endDate})\n\n`);
|
||||||
|
chunk(view, formatRankedTable(kwResult, '키워드'));
|
||||||
|
chunk(view, formatErrors(kwResult.errors));
|
||||||
|
chunk(view, '\nℹ️ 쇼핑인사이트는 "분야 자체"의 클릭 추이만 제공하고 분야 안의 개별 키워드 순위를 주는 API가 없어, 검색어트렌드(네이버 통합검색 전체 기준)로 후보를 검증했습니다 — 쇼핑 카테고리로 필터링된 수치는 아닙니다.\n');
|
||||||
|
chunk(view, `\n${DISCLAIMER}\n`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const resolved: Array<{ name: string; path?: string; code: string }> = [];
|
const resolved: Array<{ name: string; path?: string; code: string }> = [];
|
||||||
const unresolved: string[] = [];
|
const unresolved: string[] = [];
|
||||||
for (const name of intent.categories) {
|
for (const name of intent.categories) {
|
||||||
@@ -132,13 +156,6 @@ async function runShopping(arg: string, view: Webview, context?: vscode.Extensio
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const range = {
|
|
||||||
startDate: intent.startDate || defaultRange().startDate,
|
|
||||||
endDate: intent.endDate || defaultRange().endDate,
|
|
||||||
timeUnit: intent.timeUnit || 'date' as TimeUnit,
|
|
||||||
};
|
|
||||||
const filters = buildFilters(intent);
|
|
||||||
|
|
||||||
chunk(view, `\n📊 조회 중 — ${resolved.map(r => r.name).join(', ')} (${range.startDate} ~ ${range.endDate})…\n`);
|
chunk(view, `\n📊 조회 중 — ${resolved.map(r => r.name).join(', ')} (${range.startDate} ~ ${range.endDate})…\n`);
|
||||||
const result = await rankCategories(context, resolved, range, filters);
|
const result = await rankCategories(context, resolved, range, filters);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,17 @@ export type IntentMode = 'compare' | 'discover';
|
|||||||
|
|
||||||
export interface ShoppingIntent {
|
export interface ShoppingIntent {
|
||||||
mode: IntentMode;
|
mode: IntentMode;
|
||||||
|
/**
|
||||||
|
* 'category' = 분야 자체의 클릭 추이/비교를 원함(쇼핑인사이트 API 그대로 처리).
|
||||||
|
* 'keywords' = 그 분야 "안에서" 어떤 상품/검색어가 인기인지 궁금함(예: "디지털/가전
|
||||||
|
* 인기 검색어", "요즘 뭐가 잘 팔려") — 쇼핑인사이트엔 분야 내 개별 키워드 순위를
|
||||||
|
* 주는 API가 없어(카테고리 자체의 클릭 추이만 제공), 검색어트렌드 API로 우회
|
||||||
|
* 검증한다(handlers.ts 에서 rankKeywords 재사용).
|
||||||
|
*/
|
||||||
|
scope: 'category' | 'keywords';
|
||||||
categories: string[];
|
categories: string[];
|
||||||
|
/** scope='keywords' 일 때만 사용 — 그 분야 맥락에서 제안된 검색어 후보. */
|
||||||
|
keywords: string[];
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
timeUnit?: 'date' | 'week' | 'month';
|
timeUnit?: 'date' | 'week' | 'month';
|
||||||
@@ -132,12 +142,20 @@ function _buildUserMessage(userText: string, todayYmd: string): string {
|
|||||||
|
|
||||||
const SHOPPING_SYSTEM_PROMPT = `당신은 네이버 쇼핑인사이트(분야별 트렌드) API 질의 파서입니다. 사용자의 자연어 질문을 분석해 아래 JSON 하나만 출력하세요. 다른 텍스트(설명, 코드펜스, 머리말) 절대 금지.
|
const SHOPPING_SYSTEM_PROMPT = `당신은 네이버 쇼핑인사이트(분야별 트렌드) API 질의 파서입니다. 사용자의 자연어 질문을 분석해 아래 JSON 하나만 출력하세요. 다른 텍스트(설명, 코드펜스, 머리말) 절대 금지.
|
||||||
|
|
||||||
- "mode": 사용자가 구체적인 분야명(예: "패션의류", "화장품", "가전")을 하나 이상 언급했으면 "compare". "요즘 뭐가 뜨나"/"인기 분야"/"top N" 처럼 분야를 직접 지목하지 않고 탐색을 원하면 "discover".
|
⚠️ 중요한 API 제약: 쇼핑인사이트는 "분야 자체"의 클릭 추이만 제공하고, 분야 "안에서" 어떤 상품/검색어가 인기인지는 이 API로 알 수 없다(그런 API 자체가 없음). 이 구분이 scope 필드다.
|
||||||
- "categories": mode="compare"면 사용자가 언급한 분야명을 그대로(정규화하지 말고 원문 그대로) 배열로. mode="discover"면 질문 맥락에 맞는 분야 후보를 당신의 지식으로 최대 9개 제안(3개씩 묶어 순차 조회하므로 3의 배수 권장).
|
|
||||||
|
- "scope": 사용자가 분야들의 트렌드/추이 자체를 비교·조회하고 싶으면 "category". "인기 검색어", "뭐가 잘 팔려", "인기 상품", "요즘 뜨는 상품/키워드" 처럼 분야 "안의" 개별 상품·검색어가 궁금하면 "keywords".
|
||||||
|
- "mode": scope="category"일 때 — 구체적인 분야명을 하나 이상 언급했으면 "compare", "요즘 뜨는 분야가 뭐야"처럼 분야 자체를 탐색하고 싶으면 "discover". scope="keywords"면 항상 "discover".
|
||||||
|
- "categories": scope="category"·mode="compare"면 사용자가 언급한 분야명 그대로(정규화 말고 원문 그대로) 배열로. scope="category"·mode="discover"면 후보 분야를 최대 9개 제안(3개씩 순차 조회하므로 3의 배수 권장). scope="keywords"면 사용자가 언급한 분야명(보통 1개)만 넣기.
|
||||||
|
- "keywords": scope="keywords"일 때만 사용 — 그 분야 맥락에 맞는 검색어/상품 후보를 당신의 지식으로 최대 10개 제안(개별 순위를 매기려면 키워드 1개=API 그룹 1개라 5개씩 순차 조회함을 감안). scope="category"면 빈 배열.
|
||||||
${COMMON_FIELDS_PROMPT}
|
${COMMON_FIELDS_PROMPT}
|
||||||
|
|
||||||
⚠️ 반드시 아래 JSON 형식 정확히 한 번:
|
⚠️ 반드시 아래 JSON 형식 정확히 한 번:
|
||||||
{"mode":"compare"|"discover","categories":["..."],"startDate":"","endDate":"","timeUnit":"","device":"","gender":"","ages":[]}`;
|
{"scope":"category"|"keywords","mode":"compare"|"discover","categories":["..."],"keywords":["..."],"startDate":"","endDate":"","timeUnit":"","device":"","gender":"","ages":[]}`;
|
||||||
|
|
||||||
|
function _asScope(v: unknown): 'category' | 'keywords' {
|
||||||
|
return v === 'keywords' ? 'keywords' : 'category';
|
||||||
|
}
|
||||||
|
|
||||||
export async function parseShoppingIntent(
|
export async function parseShoppingIntent(
|
||||||
ai: IAIService,
|
ai: IAIService,
|
||||||
@@ -157,18 +175,21 @@ export async function parseShoppingIntent(
|
|||||||
raw = result.content || '';
|
raw = result.content || '';
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logError('naverTrend intentParser(shopping): AI 호출 실패 — compare 모드로 원문 사용.', { error: e?.message ?? String(e) });
|
logError('naverTrend intentParser(shopping): AI 호출 실패 — compare 모드로 원문 사용.', { error: e?.message ?? String(e) });
|
||||||
return { mode: 'compare', categories: [trimmed], parsed: false };
|
return { mode: 'compare', scope: 'category', categories: [trimmed], keywords: [], parsed: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = _parseJsonLoose(raw);
|
const obj = _parseJsonLoose(raw);
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
logInfo('naverTrend intentParser(shopping): 파싱 실패 — compare 모드로 원문 사용.', { rawHead: raw.slice(0, 100) });
|
logInfo('naverTrend intentParser(shopping): 파싱 실패 — compare 모드로 원문 사용.', { rawHead: raw.slice(0, 100) });
|
||||||
return { mode: 'compare', categories: [trimmed], parsed: false };
|
return { mode: 'compare', scope: 'category', categories: [trimmed], keywords: [], parsed: false };
|
||||||
}
|
}
|
||||||
const categories = _asStringArray(obj.categories);
|
const categories = _asStringArray(obj.categories);
|
||||||
|
const scope = _asScope(obj.scope);
|
||||||
return {
|
return {
|
||||||
mode: _asMode(obj.mode),
|
mode: _asMode(obj.mode),
|
||||||
|
scope,
|
||||||
categories: categories.length > 0 ? categories : [trimmed],
|
categories: categories.length > 0 ? categories : [trimmed],
|
||||||
|
keywords: scope === 'keywords' ? _asStringArray(obj.keywords) : [],
|
||||||
startDate: _asDate(obj.startDate),
|
startDate: _asDate(obj.startDate),
|
||||||
endDate: _asDate(obj.endDate),
|
endDate: _asDate(obj.endDate),
|
||||||
timeUnit: _asTimeUnit(obj.timeUnit),
|
timeUnit: _asTimeUnit(obj.timeUnit),
|
||||||
|
|||||||
@@ -85,6 +85,22 @@ describe('parseShoppingIntent — tolerant JSON 파싱', () => {
|
|||||||
expect(r.mode).toBe('compare');
|
expect(r.mode).toBe('compare');
|
||||||
expect(r.categories).toEqual(['패션의류 트렌드']);
|
expect(r.categories).toEqual(['패션의류 트렌드']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('scope="keywords" — 분야 자체가 아니라 분야 "안"의 인기 검색어를 묻는 질문', async () => {
|
||||||
|
const ai = mockAi('{"scope":"keywords","mode":"discover","categories":["디지털/가전"],"keywords":["무선이어폰","보조배터리","블루투스스피커"],"startDate":"","endDate":"","timeUnit":"","device":"","gender":"","ages":[]}');
|
||||||
|
const r = await parseShoppingIntent(ai, '디지털/가전 인기 검색어', '2026-08-04');
|
||||||
|
expect(r.parsed).toBe(true);
|
||||||
|
expect(r.scope).toBe('keywords');
|
||||||
|
expect(r.categories).toEqual(['디지털/가전']);
|
||||||
|
expect(r.keywords).toEqual(['무선이어폰', '보조배터리', '블루투스스피커']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('scope 미명시 응답은 기존 동작과 동일하게 category 로 기본값 처리', async () => {
|
||||||
|
const ai = mockAi('{"mode":"compare","categories":["패션의류"],"startDate":"","endDate":"","timeUnit":"","device":"","gender":"","ages":[]}');
|
||||||
|
const r = await parseShoppingIntent(ai, '패션의류 트렌드', '2026-08-04');
|
||||||
|
expect(r.scope).toBe('category');
|
||||||
|
expect(r.keywords).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseSearchTrendIntent — tolerant JSON 파싱', () => {
|
describe('parseSearchTrendIntent — tolerant JSON 파싱', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user