/** * 고객 피드백 누적 저장소. * * 단일 운영자(대표) 모드에서 슬랙·이메일·CS 채널에 흩어진 고객 피드백을 * `/feedback <텍스트>` 한 줄로 모아 둔다. 패턴 분석은 `/feedback summary` 로 * LLM 이 누적 데이터를 보고 카테고리 분포 + 반복 주제를 추출. * * 저장 형식: JSON Lines (.jsonl) — 한 줄 = 한 entry. 누적·append-only, 사람이 * 직접 편집 가능, grep / 백업 친화. 위치: `/.astra/customer-feedback.jsonl`. * * 워크스페이스 폴더가 없으면 저장 불가 (null 반환). 사용자가 `/feedback path` 로 * 위치 확인 가능. 민감 정보 포함 가능성 있으므로 외부로 안 보냄 — 로컬 only. */ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; const STORE_REL_PATH = '.astra/customer-feedback.jsonl'; export interface FeedbackEntry { /** unique id — timestamp 기반 (정렬·dedup 용도). */ id: string; /** ISO timestamp of when this entry was captured. */ timestamp: string; /** 사용자가 입력한 원본 텍스트 (그대로 보존). */ text: string; /** 선택적 출처 — 'slack' / 'email' / 'cs' / 'review' 등. */ source?: string; /** LLM 이 부여한 카테고리 (1~3개). */ categories?: string[]; /** LLM 판정 — 'positive' / 'neutral' / 'negative'. */ sentiment?: 'positive' | 'neutral' | 'negative'; } export function getFeedbackFilePath(): string | null { const folders = vscode.workspace.workspaceFolders; if (!folders || folders.length === 0) return null; return path.join(folders[0].uri.fsPath, STORE_REL_PATH); } export function readFeedback(): FeedbackEntry[] { const filePath = getFeedbackFilePath(); if (!filePath || !fs.existsSync(filePath)) return []; const out: FeedbackEntry[] = []; let content = ''; try { content = fs.readFileSync(filePath, 'utf-8'); } catch { return []; } for (const line of content.split('\n')) { const trimmed = line.trim(); if (!trimmed) continue; try { const entry = JSON.parse(trimmed); if (entry && typeof entry.id === 'string' && typeof entry.text === 'string') { out.push(entry as FeedbackEntry); } } catch { /* skip malformed line — append-only 라 손상 1줄이 전체 무력화하면 안 됨 */ } } return out; } export function appendFeedback(entry: FeedbackEntry): { ok: true; filePath: string } | { ok: false; error: string } { const filePath = getFeedbackFilePath(); if (!filePath) return { ok: false, error: '워크스페이스 폴더가 없어 저장 불가. VS Code 에서 폴더를 열어 주세요.' }; try { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.appendFileSync(filePath, JSON.stringify(entry) + '\n', 'utf-8'); return { ok: true, filePath }; } catch (e: any) { return { ok: false, error: e?.message || String(e) }; } } /** 누적 항목 수 — 빠른 확인용. */ export function countFeedback(): number { const filePath = getFeedbackFilePath(); if (!filePath || !fs.existsSync(filePath)) return 0; try { const content = fs.readFileSync(filePath, 'utf-8'); return content.split('\n').filter((l) => l.trim()).length; } catch { return 0; } }