#!/usr/bin/env bun /** * export-methodology-vectors.ts * * WS-5 산출물: methodology_vectors 테이블을 CSV로 덤프하여 * 외부 검증인·감사관이 방법론 데이터를 검토할 수 있도록 한다. * * 사용법: * bun scripts/export-methodology-vectors.ts [--output ] [--format ] * * 필요 환경 변수 (프로젝트 루트 .env.local 또는 환경 변수): * SUPABASE_URL — Supabase 프로젝트 URL * SUPABASE_ANON_KEY — Supabase anon key (읽기 전용) * 또는: * SUPABASE_SERVICE_KEY — Service role key (embedding 포함 전체 내보내기 시) * * 주의: * - 프로덕션 Supabase(emkxmdksuartspqvozkh)에 대해 db reset / 강제 push 금지. * - 이 스크립트는 SELECT만 수행하며 데이터를 변경하지 않는다. * - embedding 벡터(1536차원)는 기본적으로 CSV에서 제외된다(파일 크기 문제). * --include-embedding 플래그로 활성화 가능. */ import { createClient } from '@supabase/supabase-js' import { writeFile } from 'node:fs/promises' import { resolve } from 'node:path' // ── 타입 정의 ────────────────────────────────────────────────── interface MethodologyVector { id: string methodology_code: string registry: string name_ko: string name_en: string applicability: string emission_factors: Record required_params: string[] embedding?: number[] | null created_at: string updated_at: string } interface ExportRow { id: string methodology_code: string registry: string name_ko: string name_en: string applicability: string emission_factors_json: string required_params: string embedding_dim?: number // 차원 수만 기록 (벡터 전체 대신) created_at: string updated_at: string } // ── 인수 파싱 ────────────────────────────────────────────────── const args = process.argv.slice(2) const outputPath = (() => { const idx = args.indexOf('--output') return idx !== -1 ? args[idx + 1] : `methodology-vectors-${new Date().toISOString().slice(0, 10)}.csv` })() const format = (() => { const idx = args.indexOf('--format') return idx !== -1 ? args[idx + 1] : 'csv' })() as 'csv' | 'json' const includeEmbedding = args.includes('--include-embedding') // ── Supabase 클라이언트 ──────────────────────────────────────── const supabaseUrl = process.env.SUPABASE_URL const supabaseKey = process.env.SUPABASE_SERVICE_KEY ?? process.env.SUPABASE_ANON_KEY if (!supabaseUrl || !supabaseKey) { console.error( 'ERROR: SUPABASE_URL 및 SUPABASE_ANON_KEY (또는 SUPABASE_SERVICE_KEY)를 환경 변수로 설정하세요.' ) console.error(' export SUPABASE_URL=https://your-project.supabase.co') console.error(' export SUPABASE_ANON_KEY=your-anon-key') process.exit(1) } const supabase = createClient(supabaseUrl, supabaseKey) // ── 데이터 조회 ──────────────────────────────────────────────── async function fetchMethodologyVectors(): Promise { const select = includeEmbedding ? '*' : 'id, methodology_code, registry, name_ko, name_en, applicability, emission_factors, required_params, created_at, updated_at' const { data, error } = await supabase .from('methodology_vectors') .select(select) .order('registry', { ascending: true }) .order('methodology_code', { ascending: true }) if (error) { throw new Error(`Supabase 조회 실패: ${error.message}`) } return (data ?? []) as unknown as MethodologyVector[] } // ── CSV 변환 ─────────────────────────────────────────────────── function toCsvRow(row: ExportRow): string { const fields = [ row.id, row.methodology_code, row.registry, row.name_ko, row.name_en, row.applicability.replace(/"/g, '""'), // CSV 이스케이프 row.emission_factors_json.replace(/"/g, '""'), row.required_params, includeEmbedding ? String(row.embedding_dim ?? '') : '', row.created_at, row.updated_at, ] return fields.map((f) => `"${f}"`).join(',') } function buildCsvHeader(): string { const cols = [ 'id', 'methodology_code', 'registry', 'name_ko', 'name_en', 'applicability', 'emission_factors_json', 'required_params', ...(includeEmbedding ? ['embedding_dimensions'] : []), 'created_at', 'updated_at', ] return cols.join(',') } // ── 메인 ────────────────────────────────────────────────────── async function main() { console.log('methodology_vectors 테이블 내보내기 시작...') console.log(` Supabase URL: ${supabaseUrl}`) console.log(` 출력 형식: ${format}`) console.log(` 출력 경로: ${resolve(outputPath)}`) console.log(` 임베딩 포함: ${includeEmbedding}`) console.log() let rows: MethodologyVector[] try { rows = await fetchMethodologyVectors() } catch (err) { console.error('데이터 조회 실패:', err) process.exit(1) } console.log(` 조회된 방법론 수: ${rows.length}건`) // 변환 const exportRows: ExportRow[] = rows.map((r) => ({ id: r.id, methodology_code: r.methodology_code, registry: r.registry, name_ko: r.name_ko, name_en: r.name_en, applicability: r.applicability, emission_factors_json: JSON.stringify(r.emission_factors), required_params: (r.required_params ?? []).join(';'), embedding_dim: Array.isArray(r.embedding) ? r.embedding.length : undefined, created_at: r.created_at, updated_at: r.updated_at, })) let output: string if (format === 'json') { output = JSON.stringify( rows.map((r) => ({ ...r, embedding: includeEmbedding ? r.embedding : `<${Array.isArray(r.embedding) ? r.embedding.length : 0}-dim vector omitted>`, })), null, 2 ) } else { // CSV const lines = [buildCsvHeader(), ...exportRows.map(toCsvRow)] output = lines.join('\n') } const absPath = resolve(outputPath) try { await writeFile(absPath, output, 'utf-8') } catch (err) { console.error('파일 쓰기 실패:', err) process.exit(1) } console.log(`\n내보내기 완료: ${absPath}`) console.log() console.log('방법론 코드 목록:') for (const r of exportRows) { console.log(` [${r.registry.padEnd(14)}] ${r.methodology_code.padEnd(20)} — ${r.name_ko}`) } console.log() console.log('외부 검증인 사용 지침:') console.log(' 1. emission_factors_json: 방법론별 배출계수 기본값 확인') console.log(' 2. required_params: 해당 방법론에 필요한 모니터링 변수 목록 (세미콜론 구분)') console.log(' 3. 임베딩(--include-embedding): 프로덕션 재시드 후 OpenAI 실제 벡터로 교체 필요') console.log(' 4. 방법론 상세: docs/methodology/ 디렉터리 참조') } main().catch((err) => { console.error('예기치 않은 오류:', err) process.exit(1) })