import "@supabase/functions-js/edge-runtime.d.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; import { PERPLEXITY_MODEL } from "../_shared/config.ts"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; interface AdjustStrategyRequest { clinicId: string; } Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response("ok", { headers: corsHeaders }); } try { const body = (await req.json()) as AdjustStrategyRequest; if (!body.clinicId) throw new Error("clinicId is required"); const PERPLEXITY_API_KEY = Deno.env.get("PERPLEXITY_API_KEY"); if (!PERPLEXITY_API_KEY) throw new Error("PERPLEXITY_API_KEY not configured"); const supabaseUrl = Deno.env.get("SUPABASE_URL")!; const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; const supabase = createClient(supabaseUrl, supabaseKey); const clinicId = body.clinicId; // ─── 1. Get channel deltas from weekly comparison view ─── const { data: deltas } = await supabase .from("channel_weekly_delta") .select("*") .eq("clinic_id", clinicId); // ─── 2. Get current active content plan ─── const { data: activePlan } = await supabase .from("content_plans") .select("id, channel_strategies, content_strategy, calendar") .eq("clinic_id", clinicId) .eq("is_active", true) .order("created_at", { ascending: false }) .limit(1) .single(); // ─── 3. Get latest analysis run for KPI targets ─── const { data: latestRun } = await supabase .from("analysis_runs") .select("id, report") .eq("clinic_id", clinicId) .eq("status", "complete") .order("created_at", { ascending: false }) .limit(1) .single(); const report = (latestRun?.report || {}) as Record; const kpiTargets = (report.kpiTargets || []) as Record[]; // ─── 4. Calculate KPI progress ─── const kpiProgress = kpiTargets.map((kpi) => { const current = parseFloat(String(kpi.current || "0").replace(/[^0-9.]/g, "")) || 0; const target = parseFloat(String(kpi.target3Month || "0").replace(/[^0-9.]/g, "")) || 0; const progress = target > 0 ? Math.min((current / target) * 100, 200) : 0; return { metric: kpi.metric, current: kpi.current, target: kpi.target3Month, progress: Math.round(progress), }; }); // ─── 5. Build channel delta summary ─── const deltaLines = (deltas || []).map((d: Record) => { const channel = d.channel as string; const currentFollowers = d.current_followers as number || 0; const prevFollowers = d.prev_followers as number || 0; const change = currentFollowers - prevFollowers; const changePercent = prevFollowers > 0 ? ((change / prevFollowers) * 100).toFixed(1) : "N/A"; return `${channel}: ${prevFollowers} → ${currentFollowers} (${change >= 0 ? "+" : ""}${changePercent}%)`; }); const underperforming = (deltas || []) .filter((d: Record) => { const current = d.current_followers as number || 0; const prev = d.prev_followers as number || 0; return prev > 0 && current <= prev; }) .map((d: Record) => d.channel as string); // ─── 6. Call Perplexity for strategy adjustment ─── const userPrompt = ` 현재 콘텐츠 전략의 성과를 분석하고 조정 추천을 해주세요. ## 채널 변화량 (최근 1주) ${deltaLines.join("\n") || "데이터 수집 중 (아직 비교 데이터 없음)"} ## KPI 달성률 ${kpiProgress.map((k) => `${k.metric}: ${k.progress}% (${k.current} / ${k.target})`).join("\n") || "KPI 설정 전"} ## 부진 채널 ${underperforming.join(", ") || "없음"} ## 현재 전략 요약 ${JSON.stringify(activePlan?.channel_strategies || [], null, 2).slice(0, 2000)} ## 요청 출력 JSON { "strategySuggestions": [ { "adjustmentType": "frequency_change|pillar_shift|channel_add|channel_pause|content_format_change|tone_shift", "channel": "채널명", "description": "변경 내용 설명", "reason": "근거 (데이터 기반)", "priority": "high|medium|low", "beforeValue": "변경 전", "afterValue": "변경 후" } ], "overallAssessment": "전체 전략 평가 (2-3문장)", "topPerformingChannel": "가장 성과 좋은 채널", "focusArea": "다음 2주 집중 영역" } `; console.log(`[adjust-strategy] Calling Perplexity for clinic ${clinicId}...`); const aiRes = await fetch("https://api.perplexity.ai/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}`, }, body: JSON.stringify({ model: PERPLEXITY_MODEL, messages: [ { role: "system", content: "You are a Korean medical marketing performance analyst. Respond ONLY with valid JSON. Use Korean for text fields.", }, { role: "user", content: userPrompt }, ], temperature: 0.3, }), }); const aiData = await aiRes.json(); let responseText = aiData.choices?.[0]?.message?.content || ""; const jsonMatch = responseText.match(/```(?:json)?\n?([\s\S]*?)```/); if (jsonMatch) responseText = jsonMatch[1]; if (!responseText.trim().startsWith("{")) { const rawMatch = responseText.match(/\{[\s\S]*\}/); if (rawMatch) responseText = rawMatch[0]; } let adjustments: Record; try { adjustments = JSON.parse(responseText); } catch { console.error("[adjust-strategy] JSON parse failed"); adjustments = { strategySuggestions: [], overallAssessment: "AI 분석 실패 — 수동 검토 필요", topPerformingChannel: "N/A", focusArea: "데이터 수집 대기", }; } // ─── 7. Store in performance_metrics ─── const channelDeltas: Record = {}; for (const d of deltas || []) { const ch = d.channel as string; channelDeltas[ch] = { currentFollowers: d.current_followers, prevFollowers: d.prev_followers, change: (d.current_followers as number || 0) - (d.prev_followers as number || 0), }; } const { data: perfMetric, error: perfErr } = await supabase .from("performance_metrics") .insert({ clinic_id: clinicId, run_id: latestRun?.id || null, prev_run_id: null, channel_deltas: channelDeltas, kpi_progress: kpiProgress, top_performing_content: { channel: adjustments.topPerformingChannel }, underperforming_channels: underperforming, strategy_suggestions: adjustments.strategySuggestions || [], }) .select("id") .single(); if (perfErr) console.error("[adjust-strategy] performance_metrics insert error:", perfErr); // ─── 8. Store individual strategy_adjustments ─── const suggestions = (adjustments.strategySuggestions || []) as Record[]; for (const s of suggestions) { await supabase.from("strategy_adjustments").insert({ clinic_id: clinicId, plan_id: activePlan?.id || null, performance_id: perfMetric?.id || null, adjustment_type: s.adjustmentType || "other", description: s.description || "", reason: s.reason || "", before_value: { value: s.beforeValue }, after_value: { value: s.afterValue }, }); } console.log(`[adjust-strategy] Created ${suggestions.length} adjustments for clinic ${clinicId}`); return new Response( JSON.stringify({ success: true, performanceMetricId: perfMetric?.id || null, adjustments, channelDeltas, kpiProgress, underperforming, }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } catch (error) { console.error("[adjust-strategy] Error:", error); return new Response( JSON.stringify({ success: false, error: error.message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } });