CarboAny / src /lib /methodologyMatcher.ts
Esketch's picture
deploy: [P4] Strangler Pattern API Adapter Release (Orphan Clean Build v2)
daaf9d7
Raw
History Blame Contribute Delete
14.3 kB
// AI Methodology Matcher: vector similarity matching via pgvector
// Patent claims 1, 3: AI-based methodology auto-matching
//
// ๋‹ค์ธต ํด๋ฐฑ ์ „๋žต:
// 1) embed-text Edge Function (OpenAI text-embedding-3-small ๋˜๋Š” ์„œ๋ฒ„ pseudo)
// 2) ์‹คํŒจ ์‹œ ํด๋ผ์ด์–ธํŠธ pseudo-embedding (djb2 + mulberry32) โ€” Edge Function ์‹œ๋“œ์™€ ๋™์ผ ์•Œ๊ณ ๋ฆฌ์ฆ˜
// 3) pgvector RPC match_methodology ๋Š” ์–ด๋А embedding source ๋“  ๋™์ผํ•˜๊ฒŒ ์ž‘๋™
// 4) RPC ์‹คํŒจ ์‹œ ํด๋ผ์ด์–ธํŠธ ์ธก ํ…์ŠคํŠธ ์œ ์‚ฌ๋„(fallback): Jaccard on tokens
import { supabase } from './supabase'
import { invokeEdgeFunction } from './apiAdapter'
import type { MethodologyMatch } from '../types'
export interface DataSpec {
parameter: string
name: string
purpose: string
source: string
}
export interface MethodologyExplanation {
success: boolean
eligibilityScore: number
explanation: string
keyConstraints: string[]
requiredData: DataSpec[]
recommendedData: DataSpec[]
}
export interface MethodologyVector {
id: string
methodologyCode: string
registry: string
nameKo: string
nameEn: string | null
applicability: string
emissionFactors: Record<string, unknown> | null
requiredParams: string[]
similarity: number
}
export interface MatchResult {
matches: MethodologyVector[]
queryText: string
matchedAt: string
/** ๋งค์นญ ์†Œ์Šค ๊ฒฝ๋กœ (๋””๋ฒ„๊น…ยทUI ๋ฐฐ์ง€์šฉ) */
source: 'edge-openai' | 'edge-pseudo' | 'client-pseudo' | 'text-jaccard'
}
const EMBED_DIM = 1536
// --- Deterministic pseudo-embedding (Edge Function๊ณผ ๋™์ผ ์•Œ๊ณ ๋ฆฌ์ฆ˜) ---
function djb2(str: string): number {
let hash = 5381
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0
}
return hash
}
function mulberry32(seed: number) {
let s = seed >>> 0
return () => {
s = (s + 0x6d2b79f5) >>> 0
let t = s
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
function clientPseudoEmbedding(text: string): number[] {
const normalized = text.trim().toLowerCase()
const seed = djb2(normalized)
const rng = mulberry32(seed)
const vec = new Array<number>(EMBED_DIM)
let sumSq = 0
for (let i = 0; i < EMBED_DIM; i++) {
const v = rng() * 2 - 1
vec[i] = v
sumSq += v * v
}
const norm = Math.sqrt(sumSq) || 1
for (let i = 0; i < EMBED_DIM; i++) vec[i] /= norm
return vec
}
// --- Embedding acquisition (fallback chain) ---
async function getEmbedding(text: string): Promise<{ vec: number[]; source: 'edge-openai' | 'edge-pseudo' | 'client-pseudo' }> {
try {
const payload = await invokeEdgeFunction<{ text: string }, { embedding: number[]; source?: string }>('embed-text', { text })
if (Array.isArray(payload?.embedding) && payload.embedding.length === EMBED_DIM) {
return {
vec: payload.embedding,
source: payload.source === 'openai' ? 'edge-openai' : 'edge-pseudo',
}
}
throw new Error('invalid embedding response shape')
} catch (err) {
// ๋กœ์ปฌ dev / ์˜คํ”„๋ผ์ธ / Edge Function ๋ฏธ๋ฐฐํฌ โ†’ ํด๋ผ์ด์–ธํŠธ ํด๋ฐฑ
console.warn('[methodologyMatcher] embed-text ํ˜ธ์ถœ ์‹คํŒจ, ํด๋ผ์ด์–ธํŠธ pseudo๋กœ ํด๋ฐฑ:', err)
return { vec: clientPseudoEmbedding(text), source: 'client-pseudo' }
}
}
// --- Text Jaccard (์ตœํ›„ ํด๋ฐฑ โ€” pgvector RPC ์ž์ฒด๊ฐ€ ์‹คํŒจํ•  ๋•Œ) ---
function tokenize(s: string): Set<string> {
return new Set(
s
.toLowerCase()
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
.split(/\s+/)
.filter((t) => t.length > 0),
)
}
function jaccard(a: Set<string>, b: Set<string>): number {
if (a.size === 0 || b.size === 0) return 0
let inter = 0
for (const t of a) if (b.has(t)) inter += 1
const uni = a.size + b.size - inter
return uni === 0 ? 0 : inter / uni
}
async function textJaccardMatch(
query: string,
matchCount: number,
): Promise<MethodologyVector[]> {
const all = await getMethodologyVectors()
const qTokens = tokenize(query)
return all
.map((m) => ({
...m,
similarity: jaccard(qTokens, tokenize(`${m.nameKo} ${m.applicability}`)),
}))
.sort((a, b) => b.similarity - a.similarity)
.slice(0, matchCount)
}
// --- Match methodologies by text description ---
export async function matchMethodologies(
activityDescription: string,
matchCount = 5,
minScore = 0.5,
): Promise<MatchResult> {
const { vec: embedding, source: embedSource } = await getEmbedding(activityDescription)
try {
const { data, error } = await supabase.rpc('match_methodology', {
p_embedding: embedding,
p_match_count: matchCount,
p_min_score: minScore,
})
if (error) throw error
const matches: MethodologyVector[] = (data ?? []).map((row: Record<string, unknown>) => ({
id: row.id as string,
methodologyCode: row.methodology_code as string,
registry: row.registry as string,
nameKo: row.name_ko as string,
nameEn: (row.name_en as string) ?? null,
applicability: row.applicability as string,
emissionFactors: (row.emission_factors as Record<string, unknown>) ?? null,
requiredParams: (row.required_params as string[]) ?? [],
similarity: Number(row.similarity),
}))
return {
matches,
queryText: activityDescription,
matchedAt: new Date().toISOString(),
source: embedSource,
}
} catch (err) {
// pgvector RPC ์‹คํŒจ (์˜ˆ: methodology_vectors seed ๋ฏธ๋ฐ˜์˜ DB) โ†’ ํ…์ŠคํŠธ Jaccard ํด๋ฐฑ
console.warn('[methodologyMatcher] match_methodology RPC ์‹คํŒจ, ํ…์ŠคํŠธ ์œ ์‚ฌ๋„ ํด๋ฐฑ:', err)
const matches = await textJaccardMatch(activityDescription, matchCount)
return {
matches,
queryText: activityDescription,
matchedAt: new Date().toISOString(),
source: 'text-jaccard',
}
}
}
// --- Convert MethodologyVector to existing MethodologyMatch type ---
export function toMethodologyMatch(
vector: MethodologyVector,
campaignContext: {
activityType: string
organization: string
reductionTons: number
dataRows: number
sourceFileId?: string
sourceFileName?: string
},
): MethodologyMatch {
const emissionFactor = vector.emissionFactors
? Object.entries(vector.emissionFactors)
.map(([k, v]) => `${k}: ${v}`)
.join(', ')
: 'โ€”'
const score = Math.round(vector.similarity * 100);
return {
id: vector.id,
activityType: campaignContext.activityType,
organization: campaignContext.organization,
methodology: vector.nameKo,
methodologyCode: vector.methodologyCode,
registry: vector.registry as MethodologyMatch['registry'],
fitScore: score,
status: score >= 80 ? 'matched' : 'review',
matchedAt: new Date().toISOString(),
reductionTons: campaignContext.reductionTons,
sourceFileId: campaignContext.sourceFileId,
sourceFileName: campaignContext.sourceFileName,
dataRows: campaignContext.dataRows,
emissionFactor,
requiredParams: vector.requiredParams,
description: vector.applicability,
}
}
// --- Query all methodology vectors (admin) ---
export async function getMethodologyVectors(): Promise<MethodologyVector[]> {
const { data, error } = await supabase
.from('methodology_vectors')
.select('id, methodology_code, registry, name_ko, name_en, applicability, emission_factors, required_params')
.order('methodology_code', { ascending: true })
if (error) throw error
return (data ?? []).map((row) => ({
id: row.id,
methodologyCode: row.methodology_code,
registry: row.registry,
nameKo: row.name_ko,
nameEn: row.name_en,
applicability: row.applicability,
emissionFactors: row.emission_factors,
requiredParams: row.required_params ?? [],
similarity: 0,
}))
}
// --- Local Knowledge Base for Methodology Suitability & Data Specification Fallback ---
const LOCAL_KNOWLEDGE: Record<string, Omit<MethodologyExplanation, 'success'>> = {
"GS-PWR-v3.2": {
eligibilityScore: 96,
explanation: "GS-PWR-v3.2 ๋ฐฉ๋ฒ•๋ก ์€ ํ…€๋ธ”๋Ÿฌยท๋‹คํšŒ์šฉ๊ธฐ ์‚ฌ์šฉ์„ ํ†ตํ•ด ์Œ์šฉ ์†Œ๋น„ ์‹œ ์‚ฌ์šฉ๋˜๋Š” ์ผํšŒ์šฉ ์ปต(์ข…์ด/ํ”Œ๋ผ์Šคํ‹ฑ) ๋ฐœ์ƒ์„ ์–ต์ œํ•˜์—ฌ, ์ƒ์‚ฐ ๋ฐ ๋งค๋ฆฝ/์†Œ๊ฐ ์ฃผ๊ธฐ์˜ ์˜จ์‹ค๊ฐ€์Šค ๋ฐฐ์ถœ์„ ์›์ฒœ์ ์œผ๋กœ ํšŒํ”ผํ•ฉ๋‹ˆ๋‹ค. ์ œ์ฃผ ๋‹คํšŒ์šฉ ์ปต ํŒŒ์ผ๋Ÿฟ ๋“ฑ ์ผ์ƒ ์† ์‹œ๋ฏผ ์ฐธ์—ฌํ˜• ์ˆœํ™˜ ํ–‰๋™์— ๊ทน์šฐ์ˆ˜ํ•œ ์ ๊ฒฉ์„ฑ์„ ์ง€๋‹™๋‹ˆ๋‹ค. [Waste Prevention and Avoidance of Single-use Cups]",
keyConstraints: [
"1ํšŒ ์‚ฌ์šฉ๋‹น ์ผํšŒ์šฉ์ปต 1๊ฐœ(์ข…์ด์ปต ๊ธฐ๋ณธ ๋ฐฐ์ถœ๊ณ„์ˆ˜ 21gCO2e) ๋Œ€์ฒด ์‚ฐ์ •",
"๋™์ผ ๊ฐ€๋งน์  ๋‚ด 24์‹œ๊ฐ„ ๋‚ด ๋ฐ˜๋ณต ์ฐธ์—ฌ ์–ด๋ทฐ์ง• ๋ฐฉ์ง€ ํ•„ํ„ฐ ์š”ํ•จ (QA/QC ๋“ฑ๊ธ‰ ์ง€์ •)"
],
requiredData: [
{ parameter: "cup_count", name: "๋‹คํšŒ์šฉ ์ปต ์‚ฌ์šฉ ํšŸ์ˆ˜ (ํšŒ)", purpose: "ํšŒํ”ผ๋œ ์ปต ๊ฐœ์ˆ˜๋‹น BE(๋ฒ ์ด์Šค๋ผ์ธ ๋ฐฐ์ถœ๋Ÿ‰) ์ •๋ฐ€ ์‚ฐ์ •", source: "์˜์ˆ˜์ฆ OCR ํ• ์ธ ์ธ์‹ / POS API ๋กœ๊ทธ" },
{ parameter: "store_id", name: "๊ฐ€๋งน์  ID ๋ฐ ์ƒํ˜ธ๋ช…", purpose: "์‹ค์žฌ ๊ฐ€๋งน์  ๋Œ€์กฐ ๋ฐ ์ง€์—ญ ๊ฐ์ถ• ๊ณตํ—Œ ์ถ”์ ", source: "์˜์ˆ˜์ฆ ์ƒํ˜ธ/์ฃผ์†Œ ํŒŒ์‹ฑ" }
],
recommendedData: [
{ parameter: "gps_coords", name: "GPS ์œ„์น˜ ์ •๋ณด", purpose: "๋™์ผ ๊ฐ€๋งน์  ๋‚ด ์ดˆ๊ณ ๋นˆ๋„ ์–ด๋ทฐ์ง• ๋ฐฉ์ง€ ๊ต์ฐจ ๋Œ€์กฐ", source: "๋‹จ๋ง๊ธฐ ์œ„์น˜ ์„œ๋น„์Šค" },
{ parameter: "photo_proof", name: "ํ…€๋ธ”๋Ÿฌ/๋‹คํšŒ์šฉ์ปต ์‚ฌ์šฉ ์‹ค๋ฌผ ์‚ฌ์ง„", purpose: "์œ„์กฐ ์˜์ˆ˜์ฆ ์˜์‹ฌ ์‹œ 2์ฐจ ๋ณด์กฐ MRV ๊ฒ€์ฆ ์ฆ๋น™", source: "์ฐธ์—ฌ์ž ์‹ค์‹œ๊ฐ„ ์‚ฌ์ง„ ์ดฌ์˜" }
]
},
"K-VER-LIVESTOCK": {
eligibilityScore: 92,
explanation: "K-VER-LIVESTOCK ๋ฐฉ๋ฒ•๋ก ์€ ๋ฐ˜์ถ”๋™๋ฌผ(์†Œ/์ –์†Œ ๋“ฑ)์˜ ์žฅ๋‚ด ๋ฐœํšจ๋กœ ์ธํ•œ ๋ฉ”ํƒ„๊ฐ€์Šค(CH4) ๋ฐฐ์ถœ์„ ์ €๋ฉ”ํƒ„ ์‚ฌ๋ฃŒ ๊ธ‰์—ฌ๋ฅผ ํ†ตํ•ด ๋ฌผ๋ฆฌ์ ์œผ๋กœ ์ €๊ฐํ•ฉ๋‹ˆ๋‹ค. ๋Œ€๋‹จ์œ„ ์ถ•์‚ฐ ๋†๊ฐ€ ๋ฐ ๊ณต๊ณต ๊ธฐ๊ธˆ ์ง€์› ์‚ฌ์—…์— ๋†’์€ ์ •๋ฐ€์„ฑ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค. [Enteric Methane Reduction via Low-Methane Feed]",
keyConstraints: [
"๋†๋ฆผ์ถ•์‚ฐ์‹ํ’ˆ๋ถ€ ๋˜๋Š” ๊ณต์ธ ๊ฒ€์ฆ์› ์Šน์ธ ์ €๋ฉ”ํƒ„ ์‚ฌ๋ฃŒ ๊ณต๊ธ‰๋ช…์„ธ์„œ ๋Œ€์กฐ ํ•„์ˆ˜",
"๋†์žฅ์ฃผ ์‚ฌ๋ฃŒ ๊ตฌ์ž… ์˜์ˆ˜์ฆ ๋ฐ ๊ธ‰์—ฌ ๋‘์ˆ˜(๋งˆ๋ฆฌ์ˆ˜) ์ผ์ผ ๊ธ‰์—ฌ ๋Œ€์žฅ ๊ฐ์‚ฌ ์ฆ๋น™ ํ•„์ˆ˜"
],
requiredData: [
{ parameter: "feed_purchase_tons", name: "์ €๋ฉ”ํƒ„ ์‚ฌ๋ฃŒ ๊ตฌ์ž…๋Ÿ‰ (ํ†ค)", purpose: "๊ณต๊ธ‰ ๋Œ€๋น„ ์ ์ • ๊ธ‰์—ฌ๋Ÿ‰ ํ™˜์‚ฐ ๋ฐ ๋ฒ ์ด์Šค๋ผ์ธ ๋Œ€์กฐ", source: "์‚ฌ๋ฃŒ ๊ณต๊ธ‰ ์ฆ๋ช…์„œ / ๋งค์ž… ๊ณ„์‚ฐ์„œ" },
{ parameter: "head_count", name: "๊ธ‰์—ฌ ๋Œ€์ƒ ๊ฐ€์ถ• ๋‘์ˆ˜ (๋‘)", purpose: "๋งˆ๋ฆฌ๋‹น ๊ธฐ์ค€ ๊ธ‰์—ฌ๋Ÿ‰ ๋ฐ ๋ฐฐ์ถœ ์›๋‹จ์œ„ ์ผ์น˜ ํ™•์ธ", source: "์ด๋ ฅ๊ด€๋ฆฌ์‹œ์Šคํ…œ ๋“ฑ๋ก ์ •๋ณด" }
],
recommendedData: [
{ parameter: "monthly_milk_yield", name: "์›”๋ณ„ ์šฐ์œ  ์ƒ์‚ฐ๋Ÿ‰/์ฒด์ค‘ ํ†ต๊ณ„", purpose: "๊ธ‰์—ฌ ํ™œ๋™์œผ๋กœ ์ธํ•œ ์ƒ์‚ฐ์„ฑ ๋ณ€ํ™” ๋ฐ ๊ฐ„์ ‘ ์˜ํ–ฅ ๋ชจ๋‹ˆํ„ฐ๋ง", source: "๋†๊ฐ€ ์ถœํ•˜ ๋Œ€์žฅ" }
]
},
"VCS-VM0042": {
eligibilityScore: 94,
explanation: "VCS-VM0042 ๋ฐฉ๋ฒ•๋ก ์€ ํ™”์„์—ฐ๋ฃŒ๋ฅผ ์—ฐ์†Œํ•˜๋Š” ๊ธฐ์กด ์ด๋™ ์ฐจ๋Ÿ‰(์Šน์šฉ์ฐจ/๋ฒ„์Šค ๋“ฑ)์„ ์ „๊ธฐ ์ž์ „๊ฑฐ, ์ผ๋ฐ˜ ์ž์ „๊ฑฐ ๋ฐ ๋„๋ณด ๋“ฑ ๋ฌด๋™๋ ฅ/์นœํ™˜๊ฒฝ ๊ตํ†ต์ˆ˜๋‹จ์œผ๋กœ ๋Œ€์ฒดํ•˜์—ฌ ๋Œ€๊ธฐ ์ค‘ ์ง์ ‘์ ์ธ ๋ฐฐ์ถœ ์œ ์ž…์„ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค. [Non-Motorized and Active Transport System conversion]",
keyConstraints: [
"์ฐธ์—ฌ์ž์˜ ๋ฌด๋™๋ ฅ ์ด๋™ ๊ฑฐ๋ฆฌ ๋ฐ ๋Œ€์ฒด ๊ตํ†ตํŽธ ์‹ค์žฌ์„ฑ ์ž…์ฆ์„ ์œ„ํ•œ GPS ๋ฐ์ดํ„ฐ ํ•„์ˆ˜",
"์ด์ค‘ ๊ณ„์‚ฐ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ๊ธฐํƒ€ ํƒ„์†Œ ํฌ๋ ˆ๋”ง ์ค‘๋ณต ๊ฑฐ๋ž˜ ์—ฌ๋ถ€ ๊ฒ€์ฆ ์š”๋ง"
],
requiredData: [
{ parameter: "trip_distance_km", name: "์ด๋™ ๊ฑฐ๋ฆฌ (km)", purpose: "์ˆ˜๋‹จ ์ „ํ™˜์— ๋”ฐ๋ฅธ ๋ฒ ์ด์Šค๋ผ์ธ ๊ฐ€์†”๋ฆฐ ์—ฐ์†Œ ๋Œ€์ฒด๋Ÿ‰ ์‚ฐ์ •", source: "๋‹จ๋ง๊ธฐ GPS ํŠธ๋ž™ ์‹ค์ " },
{ parameter: "mode_of_transport", name: "์ด๋™ ์ˆ˜๋‹จ ์œ ํ˜• (์ž์ „๊ฑฐ/๋„๋ณด)", purpose: "์ด๋™์ˆ˜๋‹จ๋ณ„ ๋ฐฐ์ถœ ์›๋‹จ์œ„ ๋ฐ ์นผ๋กœ๋ฆฌ ์†Œ๋น„ ๋งค์นญ", source: "์„ผ์„œ ๋ฐ์ดํ„ฐ ๋˜๋Š” UI ์„ ํƒ" }
],
recommendedData: [
{ parameter: "gps_waypoints", name: "GPS ์›จ์ดํฌ์ธํŠธ ๊ฒฝ๋กœ", purpose: "ํ‰๊ท  ์ด๋™ ์†๋„ ํŒ์ •์„ ํ†ตํ•ด ์ฐจ๋Ÿ‰ ํƒ‘์Šน ๋ถ€์ •ํ–‰์œ„ ํ•„ํ„ฐ๋ง QA/QC", source: "๋‹จ๋ง๊ธฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์œ„์น˜ ์ˆ˜์ง‘" }
]
}
};
/**
* ํŠน์ • ํƒ„์†Œ ๋ฐฉ๋ฒ•๋ก ์— ๋Œ€ํ•˜์—ฌ ์บ ํŽ˜์ธ๊ณผ์˜ ์ ํ•ฉ์„ฑ(RAG) ์„ค๋ช… ๋ฐ ํ•„์ˆ˜/๊ถŒ์žฅ ์ˆ˜์ง‘ ๋ฐ์ดํ„ฐ ์‚ฌ์–‘์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
* ์šฐ์„ ์ ์œผ๋กœ Deno Edge Function์„ ํ†ตํ•ด AI ์ง„๋‹จ์„ ์ƒ์„ฑํ•˜๋ฉฐ, ์‹คํŒจ/์˜คํ”„๋ผ์ธ ์‹œ ํด๋ผ์ด์–ธํŠธ ๋กœ์ปฌ RAG ํ…œํ”Œ๋ฆฟ์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ํด๋ฐฑํ•ฉ๋‹ˆ๋‹ค.
*/
export async function explainMethodologySuitability(
methodologyCode: string,
campaignTitle: string,
campaignDescription: string
): Promise<MethodologyExplanation> {
try {
const data = await invokeEdgeFunction<{
methodologyCode: string
campaignTitle: string
campaignDescription: string
}, MethodologyExplanation>('explain-methodology', {
methodologyCode,
campaignTitle,
campaignDescription
});
if (data && data.success) {
return data;
}
throw new Error('invalid explain-methodology response structure');
} catch (err) {
console.warn(`[methodologyMatcher] explain-methodology Edge Function ํ˜ธ์ถœ ์‹คํŒจ, ๋กœ์ปฌ RAG ํ…œํ”Œ๋ฆฟ ํด๋ฐฑ:`, err);
// Find local knowledge fallback
const local = LOCAL_KNOWLEDGE[methodologyCode];
if (local) {
return {
success: true,
...local
};
}
// Generic fallback for unrecognized codes
return {
success: true,
eligibilityScore: 70,
explanation: `์ œ์‹œํ•˜์‹  ${methodologyCode} ํƒ„์†Œ ๋ฐฉ๋ฒ•๋ก ์€ ๋ณธ ์บ ํŽ˜์ธ(${campaignTitle}) ๊ธฐํš์„œ์˜ ํ•ต์‹ฌ ๊ฐ์ถ• ๋ชฉํ‘œ์— ๋Œ€ํ•ด ์ผ๋ฐ˜์ ์ธ ์ ๊ฒฉ์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ ๊ณต์‹ ๋“ฑ๋ก ์ „์— ๋ฐฉ๋ฒ•๋ก ์˜ ์„ธ๋ถ€ ์กฐํ•ญ ๋ฐ ํ™œ๋™ ๋ฒ”์œ„ ์ ํ•ฉ์„ฑ์„ ์žฌํ™•์ธํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.`,
keyConstraints: [
"๋ฐฉ๋ฒ•๋ก  ๊ณ ์œ ์˜ ๊ฐ์ถ• ๋ฐฐ์ถœ๊ณ„์ˆ˜ ํƒ€๋‹น์„ฑ ๊ฒ€ํ†  ํ•„์š”",
"์™ธ๋ถ€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ณต์‹ ์ ๊ฒฉ ์‹ฌ์‚ฌ(VVB)๋ฅผ ์œ„ํ•œ ์ตœ์†Œ 3๊ฐœ์›”์˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์š”ํ•จ"
],
requiredData: [
{ parameter: "activity_quantity", name: "๊ธฐ๋ณธ ๊ฐ์ถ• ํ™œ๋™๋Ÿ‰", purpose: "ํ™œ๋™๋‹น ๋ฐฐ์ถœ๋Ÿ‰ ์ ˆ๊ฐ ์ง€ํ‘œ ์‚ฐ์ •", source: "์‚ฌ์šฉ์ž ์ฆ๋น™ ์ œ์ถœ ๋ฐ ์ง์ ‘ ์ž…๋ ฅ" }
],
recommendedData: []
};
}
}