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
@@ -1,5 +1,5 @@
{
"result": "직답 결과 — single-pass mock 응답입니다.",
"createdAt": 1785921598765,
"createdAt": 1785978343885,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "---\nid: wiki_on\ndate: 2026-08-05T09:19:58.767Z\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) (22ms)\n",
"createdAt": 1785921598767,
"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",
"createdAt": 1785978343891,
"modelVersion": "unknown"
}
@@ -1,7 +1,7 @@
{
"missionId": "wiki_on",
"status": "completed",
"startTime": "2026-08-05T09:19:58.741Z",
"startTime": "2026-08-06T01:05:43.866Z",
"totalElapsedMs": 27,
"results": {
"direct": "직답 결과 — single-pass mock 응답입니다."
@@ -12,16 +12,16 @@
{
"from": "idle",
"to": "direct",
"durationMs": 22,
"durationMs": 17,
"message": "답변 작성 중... (단일 호출 fast-path)",
"ts": "2026-08-05T09:19:58.763Z"
"ts": "2026-08-06T01:05:43.883Z"
},
{
"from": "direct",
"to": "completed",
"durationMs": 5,
"durationMs": 9,
"message": "미션 완료",
"ts": "2026-08-05T09:19:58.768Z"
"ts": "2026-08-06T01:05:43.892Z"
}
],
"resilienceMetrics": {
@@ -1,5 +1,5 @@
{
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
"createdAt": 1785921605167,
"createdAt": 1785978351712,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
"createdAt": 1785921605166,
"createdAt": 1785978351711,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
"createdAt": 1785921605162,
"createdAt": 1785978351707,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
"createdAt": 1785921605164,
"createdAt": 1785978351709,
"modelVersion": "unknown"
}
@@ -1,8 +1,8 @@
{
"missionId": "stress_conflict_1785921605143",
"missionId": "stress_conflict_1785978351690",
"status": "completed",
"startTime": "2026-08-05T09:20:05.143Z",
"totalElapsedMs": 25,
"startTime": "2026-08-06T01:05:51.690Z",
"totalElapsedMs": 23,
"results": {
"outline": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
"section_0": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
@@ -14,30 +14,30 @@
{
"from": "idle",
"to": "outline",
"durationMs": 18,
"durationMs": 16,
"message": "답변 구조 잡는 중...",
"ts": "2026-08-05T09:20:05.161Z"
"ts": "2026-08-06T01:05:51.706Z"
},
{
"from": "outline",
"to": "section",
"durationMs": 2,
"durationMs": 3,
"message": "본문 작성 중...",
"ts": "2026-08-05T09:20:05.163Z"
"ts": "2026-08-06T01:05:51.709Z"
},
{
"from": "section",
"to": "polish",
"durationMs": 2,
"durationMs": 1,
"message": "최종 다듬기 중...",
"ts": "2026-08-05T09:20:05.165Z"
"ts": "2026-08-06T01:05:51.710Z"
},
{
"from": "polish",
"to": "completed",
"durationMs": 3,
"message": "미션 완료",
"ts": "2026-08-05T09:20:05.168Z"
"ts": "2026-08-06T01:05:51.713Z"
}
],
"resilienceMetrics": {
+8
View File
@@ -1,5 +1,13 @@
# Astra Patch Notes
## v2.2.320 (2026-08-05)
### 🐛 `/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() 그대로라 이 클래스의 버그를 테스트로 못 잡는다(실제로 기존 유닛 테스트는 전부 통과했었음).
- **수정**: `import x = require(...)`(TS 의 순수 CJS require 문법)로 전환 — esbuild·tsc·ts-jest 세 경로 모두 동일하게 파싱된 배열을 그대로 받는다. (`esModuleInterop: true` 로도 고칠 수 있었지만, 이 프로젝트에서는 그 옵션이 `jest.spyOn(fs, 'statSync')` 같은 기존 CJS 네임스페이스 모킹을 깨서(Cannot redefine property) 되돌림.)
- **회귀 테스트 추가**: `tests/naverTrend.test.ts` 에 esbuild 로 실제 번들링한 산출물을 실행해 검증하는 테스트 추가 — ts-jest 만으로는 못 잡는 이 클래스의 버그를 앞으로 CI 가 잡는다.
- **카테고리 목록 조회 추가**: `/naver-shopping categories` (대분류 11종), `/naver-shopping categories <검색어>` (트리 fuzzy 검색 — 이름·경로·cat_id 표) — 어떤 분야명을 물어야 할지 몰라서 못 쓰는 문제 해결.
검증: tsc 무오류 + jest 1013 통과(신규 1) + esbuild 정상.
## v2.2.319 (2026-08-05)
### 🛍️ 네이버 쇼핑인사이트 · 검색어트렌드 연동 — `/naver-shopping`, `/naver-trend`
사용자 요청: 네이버 쇼핑인사이트·검색어트렌드 API로 "오늘 검색어 많은 키워드 10개" 같은 자연어 질문에 답하고 싶음. 두 API 모두 비교 대상(분야 코드/키워드 묶음)을 미리 지정해야 상대 비율(ratio)만 돌려주는 구조라 "전체 실시간 top-N"을 직접 못 준다 — LLM이 도메인 지식으로 후보를 제안하고 실제 API로 검증해 순위를 매기는 방식(discover 모드)으로 갭을 메웠다.
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "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.",
"version": "2.2.319",
"version": "2.2.320",
"publisher": "g1nation",
"license": "MIT",
"icon": "assets/icon.png",
+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;
}
+34
View File
@@ -6,6 +6,10 @@
import { findCategoryCode, ROOT_CATEGORY_CODES } from '../src/features/naverTrend/categoryCodes';
import { parseShoppingIntent, parseSearchTrendIntent } from '../src/features/naverTrend/intentParser';
import type { IAIService, AIChatResult } from '../src/core/services';
import * as esbuild from 'esbuild';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
function mockAi(content: string, shouldThrow = false): IAIService {
return {
@@ -107,3 +111,33 @@ describe('parseSearchTrendIntent — tolerant JSON 파싱', () => {
expect(r.keywords).toEqual(['원피스 키워드']);
});
});
// v2.2.319 회귀: `import * as x from './data.json'`(namespace import) 를 esbuild 로
// 번들하면 배열 최상위 JSON 이 `{ default: [...] }` 로 감싸져 x 자체가 배열이 아니게
// 된다. ts-jest 는 require() 를 그대로 써서 이 버그가 안 보여 통과했지만 실제 배포
// 번들(esbuild)에서 "CATEGORY_TREE is not iterable" 로 터졌다. default import 로
// 고친 뒤에도 재발 안 하도록 실제 esbuild 번들 결과를 실행해 검증한다.
describe('categoryCodes.ts — esbuild 번들 회귀 방지', () => {
test('esbuild 로 번들링한 결과에서도 카테고리 트리가 정상 로드됨', () => {
const entry = path.join(__dirname, '../src/features/naverTrend/categoryCodes.ts');
const result = esbuild.buildSync({
entryPoints: [entry],
bundle: true,
platform: 'node',
format: 'cjs',
write: false,
});
const tmpFile = path.join(os.tmpdir(), `categoryCodes.bundle.${process.pid}.${Date.now()}.js`);
fs.writeFileSync(tmpFile, result.outputFiles[0].text);
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mod = require(tmpFile);
const names = mod.listTopLevelCategoryNames();
expect(Array.isArray(names)).toBe(true);
expect(names.length).toBeGreaterThan(5);
expect(mod.findCategoryCode('패션의류')[0]).toMatchObject({ code: '50000000', source: 'root' });
} finally {
fs.unlinkSync(tmpFile);
}
});
});