diff --git a/.astra/tests/engine/.astra/cache/7fa9e2c0ed212d5dbde1172e996cde86955f34dda22a6e02b95c9adc0a456927.json b/.astra/tests/engine/.astra/cache/7fa9e2c0ed212d5dbde1172e996cde86955f34dda22a6e02b95c9adc0a456927.json index ea6009e..92fcd55 100644 --- a/.astra/tests/engine/.astra/cache/7fa9e2c0ed212d5dbde1172e996cde86955f34dda22a6e02b95c9adc0a456927.json +++ b/.astra/tests/engine/.astra/cache/7fa9e2c0ed212d5dbde1172e996cde86955f34dda22a6e02b95c9adc0a456927.json @@ -1,5 +1,5 @@ { "result": "직답 결과 — single-pass mock 응답입니다.", - "createdAt": 1785921598765, + "createdAt": 1785978343885, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/engine/.astra/cache/8c208151bed9108b665cd93e98fc10d377a9fef641dd359504b8d53aecd0a4c3.json b/.astra/tests/engine/.astra/cache/8c208151bed9108b665cd93e98fc10d377a9fef641dd359504b8d53aecd0a4c3.json index ca5ca1b..7639f08 100644 --- a/.astra/tests/engine/.astra/cache/8c208151bed9108b665cd93e98fc10d377a9fef641dd359504b8d53aecd0a4c3.json +++ b/.astra/tests/engine/.astra/cache/8c208151bed9108b665cd93e98fc10d377a9fef641dd359504b8d53aecd0a4c3.json @@ -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" } \ No newline at end of file diff --git a/.astra/tests/engine/.astra/missions/wiki_on.json b/.astra/tests/engine/.astra/missions/wiki_on.json index b96da35..286a5ca 100644 --- a/.astra/tests/engine/.astra/missions/wiki_on.json +++ b/.astra/tests/engine/.astra/missions/wiki_on.json @@ -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": { diff --git a/.astra/tests/stress/.astra/cache/21818066876cbf8515758bc351bb3d9d44f32b0e4cd024b2e055db3a0d34417e.json b/.astra/tests/stress/.astra/cache/21818066876cbf8515758bc351bb3d9d44f32b0e4cd024b2e055db3a0d34417e.json index cb81ec4..78164e7 100644 --- a/.astra/tests/stress/.astra/cache/21818066876cbf8515758bc351bb3d9d44f32b0e4cd024b2e055db3a0d34417e.json +++ b/.astra/tests/stress/.astra/cache/21818066876cbf8515758bc351bb3d9d44f32b0e4cd024b2e055db3a0d34417e.json @@ -1,5 +1,5 @@ { "result": "Final report with inconsistencies. This should be long enough to pass validation.", - "createdAt": 1785921605167, + "createdAt": 1785978351712, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/4fc755e372f1dd80d6bffc7b2ef973124fb64ba505f767c53a783833bbc3fa6a.json b/.astra/tests/stress/.astra/cache/4fc755e372f1dd80d6bffc7b2ef973124fb64ba505f767c53a783833bbc3fa6a.json index 685fc13..9c9c63e 100644 --- a/.astra/tests/stress/.astra/cache/4fc755e372f1dd80d6bffc7b2ef973124fb64ba505f767c53a783833bbc3fa6a.json +++ b/.astra/tests/stress/.astra/cache/4fc755e372f1dd80d6bffc7b2ef973124fb64ba505f767c53a783833bbc3fa6a.json @@ -1,5 +1,5 @@ { "result": "Final report with inconsistencies. This should be long enough to pass validation.", - "createdAt": 1785921605166, + "createdAt": 1785978351711, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/6e559207c4542d959700ff14f360e6575e54853929e991e579e318f2f5a19030.json b/.astra/tests/stress/.astra/cache/6e559207c4542d959700ff14f360e6575e54853929e991e579e318f2f5a19030.json index 83029c8..866f454 100644 --- a/.astra/tests/stress/.astra/cache/6e559207c4542d959700ff14f360e6575e54853929e991e579e318f2f5a19030.json +++ b/.astra/tests/stress/.astra/cache/6e559207c4542d959700ff14f360e6575e54853929e991e579e318f2f5a19030.json @@ -1,5 +1,5 @@ { "result": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]", - "createdAt": 1785921605162, + "createdAt": 1785978351707, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/f65136cebc95448a7e93a45745cb73b3a5a01af5d82391ec29a25bd72b8239a5.json b/.astra/tests/stress/.astra/cache/f65136cebc95448a7e93a45745cb73b3a5a01af5d82391ec29a25bd72b8239a5.json index a962ff5..ec860e9 100644 --- a/.astra/tests/stress/.astra/cache/f65136cebc95448a7e93a45745cb73b3a5a01af5d82391ec29a25bd72b8239a5.json +++ b/.astra/tests/stress/.astra/cache/f65136cebc95448a7e93a45745cb73b3a5a01af5d82391ec29a25bd72b8239a5.json @@ -1,5 +1,5 @@ { "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", - "createdAt": 1785921605164, + "createdAt": 1785978351709, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1785921605143.json b/.astra/tests/stress/.astra/missions/stress_conflict_1785978351690.json similarity index 75% rename from .astra/tests/stress/.astra/missions/stress_conflict_1785921605143.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1785978351690.json index 36d4387..cc3d2a8 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1785921605143.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1785978351690.json @@ -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": { diff --git a/PATCHNOTES.md b/PATCHNOTES.md index bf92907..8dbe840 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -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 모드)으로 갭을 메웠다. diff --git a/package.json b/package.json index dc46916..2651f98 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/features/naverTrend/categoryCodes.ts b/src/features/naverTrend/categoryCodes.ts index d730e99..d571f4a 100644 --- a/src/features/naverTrend/categoryCodes.ts +++ b/src/features/naverTrend/categoryCodes.ts @@ -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; diff --git a/src/features/naverTrend/handlers.ts b/src/features/naverTrend/handlers.ts index 11264bf..622d051 100644 --- a/src/features/naverTrend/handlers.ts +++ b/src/features/naverTrend/handlers.ts @@ -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 { 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; } diff --git a/tests/naverTrend.test.ts b/tests/naverTrend.test.ts index 44da8e3..0064227 100644 --- a/tests/naverTrend.test.ts +++ b/tests/naverTrend.test.ts @@ -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); + } + }); +});