// QA/QC Audit & Issue Tracking Store // Manages history of validations and identified defects. export type AuditResult = 'pass' | 'fail' | 'minor_issue'; export type IssueStatus = 'open' | 'fixed' | 'verified'; export interface AuditLog { id: string; scenarioId: string; scenarioTitle: string; timestamp: string; performer: string; result: AuditResult; notes: string; relatedIssueId?: string; } export interface SystemIssue { id: string; title: string; description: string; severity: 'high' | 'medium' | 'low'; status: IssueStatus; domain: string; createdAt: string; resolvedAt?: string; } const LOG_KEY = 'carboany_audit_logs'; const ISSUE_KEY = 'carboany_system_issues'; /* --- Audit Logs API --- */ export function getAuditLogs(): AuditLog[] { const raw = localStorage.getItem(LOG_KEY); if (!raw) return []; const logs = JSON.parse(raw) as AuditLog[]; // Self-heal: 이전 Date.now() 기반 id가 중복된 경우 UUID로 재발급 const seen = new Set(); let mutated = false; for (const log of logs) { if (!log.id || seen.has(log.id)) { log.id = newId('log'); mutated = true; } seen.add(log.id); } if (mutated) { localStorage.setItem(LOG_KEY, JSON.stringify(logs)); } return logs; } function newId(prefix: string): string { // crypto.randomUUID()는 동기 루프에서도 충돌하지 않음. Date.now 기반은 ms 단위 중복 위험. if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return `${prefix}-${crypto.randomUUID()}`; } return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; } export function recordAudit(log: Omit): AuditLog { const logs = getAuditLogs(); const newLog: AuditLog = { ...log, id: newId('log'), timestamp: new Date().toISOString(), }; logs.unshift(newLog); // Newest first localStorage.setItem(LOG_KEY, JSON.stringify(logs)); return newLog; } /* --- Issues API --- */ export function getSystemIssues(): SystemIssue[] { const raw = localStorage.getItem(ISSUE_KEY); return raw ? JSON.parse(raw) : []; } export function reportIssue(issue: Omit): SystemIssue { const issues = getSystemIssues(); const newIssue: SystemIssue = { ...issue, id: newId('issue'), status: 'open', createdAt: new Date().toISOString(), }; issues.unshift(newIssue); localStorage.setItem(ISSUE_KEY, JSON.stringify(issues)); return newIssue; } export function updateIssueStatus(id: string, status: IssueStatus): void { const issues = getSystemIssues(); const idx = issues.findIndex(i => i.id === id); if (idx >= 0) { issues[idx].status = status; if (status === 'fixed' || status === 'verified') { issues[idx].resolvedAt = new Date().toISOString(); } localStorage.setItem(ISSUE_KEY, JSON.stringify(issues)); } } /* --- Initial Seed for History (Previously done tasks) --- */ export function seedPreviousHistory() { if (getAuditLogs().length > 0) return; const prevTasks = [ { sid: 'p-01', title: '사용자 온보딩', result: 'pass' as const, notes: 'AuthContext 내 자동 참여 로직 확인 완료' }, { sid: 'a-05', title: '관리자 대시보드 실데이터', result: 'pass' as const, notes: 'Stage 1-3 LIVE 연동 확인' }, { sid: 's-03', title: 'ESG 보고서 ↔ Scope3 연동', result: 'pass' as const, notes: 'SME 기부 실적 오버레이 성공' }, ]; prevTasks.forEach(t => { recordAudit({ scenarioId: t.sid, scenarioTitle: t.title, performer: 'Gemini CLI', result: t.result, notes: t.notes }); }); }