v2.2.321: /naver-shopping "분야 내 인기 검색어" 질문이 무의미한 1행 표만 주던 문제 수정
- 쇼핑인사이트 API 는 분야 자체의 클릭 추이만 주고 분야 안의 키워드 순위 API가 없음 — intentParser 에 scope(category/keywords) 필드 추가해 "인기 검색어" 류 질문을 감지하고, 검증된 검색어트렌드 rankKeywords 로 우회 처리하도록 수정
This commit is contained in:
@@ -72,7 +72,8 @@ function cmdShoppingHelp(view: Webview): void {
|
||||
'**/naver-shopping** — 네이버 쇼핑인사이트(분야별 트렌드)',
|
||||
'',
|
||||
'- `/naver-shopping 패션의류 vs 화장품/미용 최근 일주일` — 명시 분야 비교',
|
||||
'- `/naver-shopping 요즘 뜨는 뷰티 카테고리 알려줘` — 탐색(후보 제안 + 검증)',
|
||||
'- `/naver-shopping 요즘 뜨는 뷰티 카테고리 알려줘` — 분야 자체 탐색(후보 제안 + 검증)',
|
||||
'- `/naver-shopping 디지털/가전 인기 검색어` — 분야 "안"의 인기 키워드 후보(검색어트렌드로 검증)',
|
||||
'- `/naver-shopping categories` — 대분류(11종) 목록 보기',
|
||||
'- `/naver-shopping categories 원피스` — 카테고리 검색(이름 · 경로 · cat_id)',
|
||||
'',
|
||||
@@ -119,6 +120,29 @@ async function runShopping(arg: string, view: Webview, context?: vscode.Extensio
|
||||
const ai = new AIService();
|
||||
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 unresolved: string[] = [];
|
||||
for (const name of intent.categories) {
|
||||
@@ -132,13 +156,6 @@ async function runShopping(arg: string, view: Webview, context?: vscode.Extensio
|
||||
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`);
|
||||
const result = await rankCategories(context, resolved, range, filters);
|
||||
|
||||
|
||||
@@ -20,7 +20,17 @@ export type IntentMode = 'compare' | 'discover';
|
||||
|
||||
export interface ShoppingIntent {
|
||||
mode: IntentMode;
|
||||
/**
|
||||
* 'category' = 분야 자체의 클릭 추이/비교를 원함(쇼핑인사이트 API 그대로 처리).
|
||||
* 'keywords' = 그 분야 "안에서" 어떤 상품/검색어가 인기인지 궁금함(예: "디지털/가전
|
||||
* 인기 검색어", "요즘 뭐가 잘 팔려") — 쇼핑인사이트엔 분야 내 개별 키워드 순위를
|
||||
* 주는 API가 없어(카테고리 자체의 클릭 추이만 제공), 검색어트렌드 API로 우회
|
||||
* 검증한다(handlers.ts 에서 rankKeywords 재사용).
|
||||
*/
|
||||
scope: 'category' | 'keywords';
|
||||
categories: string[];
|
||||
/** scope='keywords' 일 때만 사용 — 그 분야 맥락에서 제안된 검색어 후보. */
|
||||
keywords: string[];
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
timeUnit?: 'date' | 'week' | 'month';
|
||||
@@ -132,12 +142,20 @@ function _buildUserMessage(userText: string, todayYmd: string): string {
|
||||
|
||||
const SHOPPING_SYSTEM_PROMPT = `당신은 네이버 쇼핑인사이트(분야별 트렌드) API 질의 파서입니다. 사용자의 자연어 질문을 분석해 아래 JSON 하나만 출력하세요. 다른 텍스트(설명, 코드펜스, 머리말) 절대 금지.
|
||||
|
||||
- "mode": 사용자가 구체적인 분야명(예: "패션의류", "화장품", "가전")을 하나 이상 언급했으면 "compare". "요즘 뭐가 뜨나"/"인기 분야"/"top N" 처럼 분야를 직접 지목하지 않고 탐색을 원하면 "discover".
|
||||
- "categories": mode="compare"면 사용자가 언급한 분야명을 그대로(정규화하지 말고 원문 그대로) 배열로. mode="discover"면 질문 맥락에 맞는 분야 후보를 당신의 지식으로 최대 9개 제안(3개씩 묶어 순차 조회하므로 3의 배수 권장).
|
||||
⚠️ 중요한 API 제약: 쇼핑인사이트는 "분야 자체"의 클릭 추이만 제공하고, 분야 "안에서" 어떤 상품/검색어가 인기인지는 이 API로 알 수 없다(그런 API 자체가 없음). 이 구분이 scope 필드다.
|
||||
|
||||
- "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}
|
||||
|
||||
⚠️ 반드시 아래 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(
|
||||
ai: IAIService,
|
||||
@@ -157,18 +175,21 @@ export async function parseShoppingIntent(
|
||||
raw = result.content || '';
|
||||
} catch (e: any) {
|
||||
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);
|
||||
if (!obj) {
|
||||
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 scope = _asScope(obj.scope);
|
||||
return {
|
||||
mode: _asMode(obj.mode),
|
||||
scope,
|
||||
categories: categories.length > 0 ? categories : [trimmed],
|
||||
keywords: scope === 'keywords' ? _asStringArray(obj.keywords) : [],
|
||||
startDate: _asDate(obj.startDate),
|
||||
endDate: _asDate(obj.endDate),
|
||||
timeUnit: _asTimeUnit(obj.timeUnit),
|
||||
|
||||
Reference in New Issue
Block a user