v2.2.320: /naver-shopping "CATEGORY_TREE is not iterable" 크래시 수정 + 카테고리 목록 조회

- import * as (namespace import) 로 배열 JSON 을 불러오면 esbuild 번들에서
  { default: [...] } 로 감싸져 배열이 아니게 되는 문제 — import x = require(...)
  로 전환해 esbuild/tsc/ts-jest 세 경로 모두 동일하게 동작하도록 수정
- 실제 esbuild 번들 산출물을 실행하는 회귀 테스트 추가 (ts-jest만으론 이 버그를 못 잡음)
- /naver-shopping categories [검색어] — 대분류 목록/세부 카테고리 검색 명령 추가
This commit is contained in:
2026-08-06 10:06:20 +09:00
parent c79eda1862
commit 29ad4cc023
13 changed files with 106 additions and 26 deletions
+9 -1
View File
@@ -13,7 +13,15 @@
* isnow890/naver-search-mcp, MIT) 에서 fuzzy 매칭으로 찾는다.
*/
import * as categoryTreeRaw from './data/naverShoppingCategories.json';
// ES 네임스페이스 import(`import * as x`)는 esbuild 의 JSON 로더에서 배열 최상위
// JSON 을 `{ default: [...] }` 로 감싸버려 `x` 자체는 배열이 아니게 된다(ts-jest 는
// require() 그대로라 이 문제가 안 보여 테스트를 통과하고도 실제 번들에서만
// "CATEGORY_TREE is not iterable" 로 터졌다). ES default import 는 esModuleInterop
// 이 필요한데, 이 프로젝트는 그 옵션을 켜면 `jest.spyOn(fs, 'statSync')` 같은 기존
// 테스트의 CJS 네임스페이스 모킹이 깨진다(Cannot redefine property). 그래서 여기선
// TS 의 `import x = require(...)` — 순수 CJS require 그대로 — 를 써서 esbuild·tsc·
// ts-jest 세 경로 모두에서 동일하게 배열 그대로를 받는다.
import categoryTreeRaw = require('./data/naverShoppingCategories.json');
export interface CategoryRow {
code: string;
+32 -2
View File
@@ -18,7 +18,7 @@ import * as vscode from 'vscode';
import { registerSlashCommand, chunk } from '../datacollect/slashRouter';
import { AIService } from '../../core/services';
import { parseShoppingIntent, parseSearchTrendIntent } from './intentParser';
import { findCategoryCode, listTopLevelCategoryNames, type CategoryMatch } from './categoryCodes';
import { findCategoryCode, listTopLevelCategoryNames, ROOT_CATEGORY_CODES, type CategoryMatch } from './categoryCodes';
import { rankCategories, rankKeywords, type RankedResult } from './rankEngine';
import type { CommonFilters, TimeUnit } from './naverApi';
@@ -73,17 +73,47 @@ function cmdShoppingHelp(view: Webview): void {
'',
'- `/naver-shopping 패션의류 vs 화장품/미용 최근 일주일` — 명시 분야 비교',
'- `/naver-shopping 요즘 뜨는 뷰티 카테고리 알려줘` — 탐색(후보 제안 + 검증)',
'- `/naver-shopping categories` — 대분류(11종) 목록 보기',
'- `/naver-shopping categories 원피스` — 카테고리 검색(이름 · 경로 · cat_id)',
'',
'- API 키 설정: Astra 설정 → Naver Trend 탭에서 Client ID/Secret 입력',
'',
].join('\n'));
}
/** `/naver-shopping categories [검색어]` — 검색어 없으면 대분류 11종, 있으면 트리 fuzzy 검색 결과. */
function cmdListCategories(view: Webview, query: string): void {
if (!query) {
const lines = [
'',
'**네이버쇼핑 대분류 (11종)**',
'',
...listTopLevelCategoryNames().map(n => `- ${n}${ROOT_CATEGORY_CODES[n] ? ` (cat_id: ${ROOT_CATEGORY_CODES[n]})` : ''}`),
'',
'세부 카테고리 검색: `/naver-shopping categories <키워드>` (예: `/naver-shopping categories 원피스`)',
'',
];
chunk(view, lines.join('\n'));
return;
}
const matches = findCategoryCode(query, 15);
if (matches.length === 0) {
chunk(view, `\n❓ "${query}" 에 매칭되는 카테고리를 찾지 못했습니다.\n`);
return;
}
const lines = ['', `**"${query}" 카테고리 검색 결과**`, '', '| 이름 | 경로 | cat_id |', '|---|---|---|'];
for (const m of matches) lines.push(`| ${m.name} | ${m.path} | ${m.code} |`);
chunk(view, lines.join('\n') + '\n');
}
async function runShopping(arg: string, view: Webview, context?: vscode.ExtensionContext): Promise<boolean> {
if (!context) { chunk(view, '\n⚠️ ExtensionContext 없음.\n'); return true; }
const trimmed = arg.trim();
if (!trimmed) { cmdShoppingHelp(view); return true; }
const listMatch = trimmed.match(/^(?:categories|category|카테고리|list)\b\s*(.*)$/i);
if (listMatch) { cmdListCategories(view, listMatch[1].trim()); return true; }
try {
chunk(view, '\n🔍 질문 분석 중…\n');
const ai = new AIService();
@@ -98,7 +128,7 @@ async function runShopping(arg: string, view: Webview, context?: vscode.Extensio
}
if (resolved.length === 0) {
chunk(view, `\n❓ 분야를 찾지 못했습니다: ${unresolved.join(', ')}\n\n지원 대분류 예시: ${listTopLevelCategoryNames().join(', ')}\n(세부 카테고리명으로도 검색 가능 — 예: "원피스", "무선이어폰")\n`);
chunk(view, `\n❓ 분야를 찾지 못했습니다: ${unresolved.join(', ')}\n\n지원 대분류 예시: ${listTopLevelCategoryNames().join(', ')}\n(세부 카테고리명으로도 검색 가능 — 예: "원피스", "무선이어폰")\n전체 목록/검색: \`/naver-shopping categories [검색어]\`\n`);
return true;
}