- 파트별 완성, 웹 실접속 증빙, 턴 의도 브리핑, 본문 불일치 검증, 하위 질문 체크리스트, 사이트 심층 수집 (PATCHNOTES.md v2.2.313~318 참조) - /naver-shopping, /naver-trend: 네이버 데이터랩 API 연동. compare/discover 자동 판별, API 배치 상한 캡슐화, 카테고리 트리 fuzzy 매칭, 자격증명은 Astra 설정 패널 SecretStorage 로 관리
110 lines
5.3 KiB
TypeScript
110 lines
5.3 KiB
TypeScript
/**
|
|
* /naver-shopping, /naver-trend 순수 로직 테스트.
|
|
* 실제 네이버 API 호출은 통합 영역이라 제외 — 카테고리 매칭 + intent 파서(tolerant
|
|
* JSON 파싱, AI 호출 mock)만 고정.
|
|
*/
|
|
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';
|
|
|
|
function mockAi(content: string, shouldThrow = false): IAIService {
|
|
return {
|
|
call: async () => content,
|
|
chat: async (): Promise<AIChatResult> => {
|
|
if (shouldThrow) throw new Error('AI 호출 실패(mock)');
|
|
return { content, engine: 'lmstudio', model: 'mock', empty: false };
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('findCategoryCode — 카테고리 이름/코드 매칭', () => {
|
|
test('검증된 대분류 루트 코드 정확 매치', () => {
|
|
const m = findCategoryCode('패션의류');
|
|
expect(m[0]).toMatchObject({ code: ROOT_CATEGORY_CODES['패션의류'], source: 'root' });
|
|
});
|
|
|
|
test('부분 일치도 루트 코드로 매치', () => {
|
|
const m = findCategoryCode('화장품');
|
|
expect(m[0]).toMatchObject({ code: '50000002', source: 'root' });
|
|
});
|
|
|
|
test('숫자만 입력하면 raw cat_id 로 그대로 통과', () => {
|
|
const m = findCategoryCode('50012345');
|
|
expect(m).toEqual([{ name: '50012345', path: '50012345', code: '50012345', source: 'raw' }]);
|
|
});
|
|
|
|
test('트리에 있는 세부 카테고리명은 tree 매치로 코드 반환', () => {
|
|
const m = findCategoryCode('원피스');
|
|
expect(m.length).toBeGreaterThan(0);
|
|
expect(m[0].source).toBe('tree');
|
|
expect(m[0].code).toMatch(/^\d+$/);
|
|
});
|
|
|
|
test('빈 문자열은 빈 배열', () => {
|
|
expect(findCategoryCode('')).toEqual([]);
|
|
});
|
|
|
|
test('매칭 안 되는 이름은 빈 배열', () => {
|
|
expect(findCategoryCode('존재하지않는분야이름절대매칭안됨xyz')).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('parseShoppingIntent — tolerant JSON 파싱', () => {
|
|
test('정상 JSON', async () => {
|
|
const ai = mockAi('{"mode":"compare","categories":["패션의류","화장품/미용"],"startDate":"","endDate":"","timeUnit":"","device":"","gender":"","ages":[]}');
|
|
const r = await parseShoppingIntent(ai, '패션의류랑 화장품 비교해줘', '2026-08-04');
|
|
expect(r.parsed).toBe(true);
|
|
expect(r.mode).toBe('compare');
|
|
expect(r.categories).toEqual(['패션의류', '화장품/미용']);
|
|
});
|
|
|
|
test('코드펜스로 감싼 JSON', async () => {
|
|
const ai = mockAi('설명입니다\n```json\n{"mode":"discover","categories":["뷰티","디지털/가전"],"startDate":"","endDate":"","timeUnit":"","device":"","gender":"","ages":[]}\n```');
|
|
const r = await parseShoppingIntent(ai, '요즘 뜨는 카테고리', '2026-08-04');
|
|
expect(r.parsed).toBe(true);
|
|
expect(r.mode).toBe('discover');
|
|
expect(r.categories).toEqual(['뷰티', '디지털/가전']);
|
|
});
|
|
|
|
test('완전히 깨진 응답 → compare 모드로 원문 폴백', async () => {
|
|
const ai = mockAi('이건 JSON이 아닙니다 그냥 텍스트');
|
|
const r = await parseShoppingIntent(ai, '패션의류 트렌드', '2026-08-04');
|
|
expect(r.parsed).toBe(false);
|
|
expect(r.mode).toBe('compare');
|
|
expect(r.categories).toEqual(['패션의류 트렌드']);
|
|
});
|
|
|
|
test('AI 호출 자체가 실패해도 안전한 기본값으로 폴백(예외 전파 안 함)', async () => {
|
|
const ai = mockAi('', true);
|
|
const r = await parseShoppingIntent(ai, '패션의류 트렌드', '2026-08-04');
|
|
expect(r.parsed).toBe(false);
|
|
expect(r.mode).toBe('compare');
|
|
expect(r.categories).toEqual(['패션의류 트렌드']);
|
|
});
|
|
});
|
|
|
|
describe('parseSearchTrendIntent — tolerant JSON 파싱', () => {
|
|
test('정상 JSON', async () => {
|
|
const ai = mockAi('{"mode":"compare","keywords":["원피스","블라우스"],"startDate":"","endDate":"","timeUnit":"","device":"","gender":"","ages":[]}');
|
|
const r = await parseSearchTrendIntent(ai, '원피스랑 블라우스 비교', '2026-08-04');
|
|
expect(r.parsed).toBe(true);
|
|
expect(r.keywords).toEqual(['원피스', '블라우스']);
|
|
});
|
|
|
|
test('balanced-brace 추출 — 앞뒤에 잡텍스트가 섞여도 파싱', async () => {
|
|
const ai = mockAi('물론이죠! {"mode":"discover","keywords":["여름 원피스","린넨 원피스"],"startDate":"","endDate":"","timeUnit":"","device":"","gender":"","ages":[]} 이상입니다.');
|
|
const r = await parseSearchTrendIntent(ai, '20대 여성 원피스 인기 키워드', '2026-08-04');
|
|
expect(r.parsed).toBe(true);
|
|
expect(r.mode).toBe('discover');
|
|
expect(r.keywords).toEqual(['여름 원피스', '린넨 원피스']);
|
|
});
|
|
|
|
test('빈 응답 → compare 모드로 원문 폴백', async () => {
|
|
const ai = mockAi('');
|
|
const r = await parseSearchTrendIntent(ai, '원피스 키워드', '2026-08-04');
|
|
expect(r.parsed).toBe(false);
|
|
expect(r.mode).toBe('compare');
|
|
expect(r.keywords).toEqual(['원피스 키워드']);
|
|
});
|
|
});
|