feat: Sprint 1 — 7 data quality quick wins
WP-1: YouTube channel ID regex {20,} → {22} (exactly 24 chars)
WP-2: Naver Place category filtering in enrich-channels (성형/피부)
WP-3: Google Maps stores mapsUrl separately from clinicWebsite
WP-4: Naver Blog separates officialBlogUrl from search results
WP-5: 강남언니 rawRating + normalized rating (≤5 → ×2), Firecrawl
prompt explicitly states "out of 10, NOT out of 5"
WP-6: Perplexity model centralized in _shared/config.ts (env override)
WP-7: Apify Instagram timeout 30s → 45s
Frontend: transformReport uses mapsUrl and officialBlogUrl when available
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claude/bold-hawking
parent
1071328574
commit
80c57147e7
|
|
@ -994,8 +994,10 @@ export function mergeEnrichment(
|
||||||
name: '구글 지도',
|
name: '구글 지도',
|
||||||
status: 'active' as const,
|
status: 'active' as const,
|
||||||
details: `평점: ${gm.rating ?? '-'} / 리뷰: ${gm.reviewCount ?? '-'}`,
|
details: `평점: ${gm.rating ?? '-'} / 리뷰: ${gm.reviewCount ?? '-'}`,
|
||||||
// Always use Google Maps search URL — gm.website is the clinic's own site, not Maps
|
// Use Maps URL from enrichment if available, fallback to search URL
|
||||||
url: gm.name ? `https://www.google.com/maps/search/${encodeURIComponent(String(gm.name))}` : '',
|
url: (gm as Record<string, unknown>).mapsUrl
|
||||||
|
? String((gm as Record<string, unknown>).mapsUrl)
|
||||||
|
: gm.name ? `https://www.google.com/maps/search/${encodeURIComponent(String(gm.name))}` : '',
|
||||||
};
|
};
|
||||||
if (gmChannelIdx >= 0) {
|
if (gmChannelIdx >= 0) {
|
||||||
merged.otherChannels[gmChannelIdx] = gmChannel;
|
merged.otherChannels[gmChannelIdx] = gmChannel;
|
||||||
|
|
@ -1090,8 +1092,10 @@ export function mergeEnrichment(
|
||||||
name: '네이버 블로그',
|
name: '네이버 블로그',
|
||||||
status: 'active' as const,
|
status: 'active' as const,
|
||||||
details: `검색 결과: ${nb.totalResults?.toLocaleString() ?? '-'}건 / 최근 포스트 ${nb.posts?.length ?? 0}개`,
|
details: `검색 결과: ${nb.totalResults?.toLocaleString() ?? '-'}건 / 최근 포스트 ${nb.posts?.length ?? 0}개`,
|
||||||
// Always link to Naver blog search — individual post links may be unrelated personal blogs
|
// Prefer official blog URL from Phase 1, fallback to search URL
|
||||||
url: nb.searchQuery ? `https://search.naver.com/search.naver?where=blog&query=${encodeURIComponent(String(nb.searchQuery))}` : '',
|
url: (nb as Record<string, unknown>).officialBlogUrl
|
||||||
|
? String((nb as Record<string, unknown>).officialBlogUrl)
|
||||||
|
: nb.searchQuery ? `https://search.naver.com/search.naver?where=blog&query=${encodeURIComponent(String(nb.searchQuery))}` : '',
|
||||||
};
|
};
|
||||||
if (nbChannelIdx >= 0) {
|
if (nbChannelIdx >= 0) {
|
||||||
merged.otherChannels[nbChannelIdx] = nbChannel;
|
merged.otherChannels[nbChannelIdx] = nbChannel;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Shared configuration constants for Edge Functions.
|
||||||
|
* Centralizes API model names and defaults to prevent hardcoding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const PERPLEXITY_MODEL = Deno.env.get("PERPLEXITY_MODEL") || "sonar";
|
||||||
|
|
@ -66,7 +66,7 @@ async function verifyYouTube(handle: string, apiKey: string): Promise<VerifiedCh
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try as channel ID directly (starts with UC)
|
// Try as channel ID directly (starts with UC)
|
||||||
if (cleanHandle.startsWith('UC')) {
|
if (cleanHandle.startsWith('UC') && cleanHandle.length === 24) {
|
||||||
const res = await fetch(`${YT_BASE}/channels?part=id,snippet&id=${cleanHandle}&key=${apiKey}`);
|
const res = await fetch(`${YT_BASE}/channels?part=id,snippet&id=${cleanHandle}&key=${apiKey}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.items?.[0]) {
|
if (data.items?.[0]) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import "@supabase/functions-js/edge-runtime.d.ts";
|
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||||
|
import { PERPLEXITY_MODEL } from "../_shared/config.ts";
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
|
@ -58,7 +59,7 @@ Deno.serve(async (req) => {
|
||||||
Authorization: `Bearer ${PERPLEXITY_API_KEY}`,
|
Authorization: `Bearer ${PERPLEXITY_API_KEY}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: "sonar",
|
model: PERPLEXITY_MODEL,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import "@supabase/functions-js/edge-runtime.d.ts";
|
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||||
import type { VerifiedChannels } from "../_shared/verifyHandles.ts";
|
import type { VerifiedChannels } from "../_shared/verifyHandles.ts";
|
||||||
|
import { PERPLEXITY_MODEL } from "../_shared/config.ts";
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
|
@ -200,7 +201,7 @@ Deno.serve(async (req) => {
|
||||||
url: guVerified!.url as string,
|
url: guVerified!.url as string,
|
||||||
formats: ["json"],
|
formats: ["json"],
|
||||||
jsonOptions: {
|
jsonOptions: {
|
||||||
prompt: "Extract: hospital name, overall rating (out of 10), total review count, doctors with names/ratings/review counts/specialties, procedures offered, address, certifications/badges",
|
prompt: "Extract: hospital name, overall rating (강남언니 rating is always out of 10, NOT out of 5), total review count, doctors with names/ratings/review counts/specialties, procedures offered, address, certifications/badges",
|
||||||
schema: {
|
schema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
|
|
@ -218,7 +219,10 @@ Deno.serve(async (req) => {
|
||||||
const hospital = data.data?.json;
|
const hospital = data.data?.json;
|
||||||
if (hospital?.hospitalName) {
|
if (hospital?.hospitalName) {
|
||||||
channelData.gangnamUnni = {
|
channelData.gangnamUnni = {
|
||||||
name: hospital.hospitalName, rating: hospital.rating, ratingScale: "/10",
|
name: hospital.hospitalName,
|
||||||
|
rawRating: hospital.rating,
|
||||||
|
rating: typeof hospital.rating === 'number' && hospital.rating > 0 && hospital.rating <= 5 ? hospital.rating * 2 : hospital.rating,
|
||||||
|
ratingScale: "/10",
|
||||||
totalReviews: hospital.totalReviews, doctors: (hospital.doctors || []).slice(0, 10),
|
totalReviews: hospital.totalReviews, doctors: (hospital.doctors || []).slice(0, 10),
|
||||||
procedures: hospital.procedures || [], address: hospital.address,
|
procedures: hospital.procedures || [], address: hospital.address,
|
||||||
badges: hospital.badges || [], sourceUrl: guVerified!.url as string,
|
badges: hospital.badges || [], sourceUrl: guVerified!.url as string,
|
||||||
|
|
@ -232,12 +236,20 @@ Deno.serve(async (req) => {
|
||||||
const naverHeaders = { "X-Naver-Client-Id": NAVER_CLIENT_ID, "X-Naver-Client-Secret": NAVER_CLIENT_SECRET };
|
const naverHeaders = { "X-Naver-Client-Id": NAVER_CLIENT_ID, "X-Naver-Client-Secret": NAVER_CLIENT_SECRET };
|
||||||
|
|
||||||
tasks.push((async () => {
|
tasks.push((async () => {
|
||||||
|
// Get verified Naver Blog handle from Phase 1 for official blog URL
|
||||||
|
const nbVerified = verified.naverBlog as Record<string, unknown> | null;
|
||||||
|
const officialBlogHandle = nbVerified?.handle ? String(nbVerified.handle) : null;
|
||||||
|
|
||||||
const query = encodeURIComponent(`${clinicName} 후기`);
|
const query = encodeURIComponent(`${clinicName} 후기`);
|
||||||
const res = await fetch(`https://openapi.naver.com/v1/search/blog.json?query=${query}&display=10&sort=sim`, { headers: naverHeaders });
|
const res = await fetch(`https://openapi.naver.com/v1/search/blog.json?query=${query}&display=10&sort=sim`, { headers: naverHeaders });
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
channelData.naverBlog = {
|
channelData.naverBlog = {
|
||||||
totalResults: data.total || 0, searchQuery: `${clinicName} 후기`,
|
totalResults: data.total || 0, searchQuery: `${clinicName} 후기`,
|
||||||
|
// Official blog URL from Phase 1 verified handle
|
||||||
|
officialBlogUrl: officialBlogHandle ? `https://blog.naver.com/${officialBlogHandle}` : null,
|
||||||
|
officialBlogHandle: officialBlogHandle,
|
||||||
|
// Blog mentions (third-party posts, NOT the official blog)
|
||||||
posts: (data.items || []).slice(0, 10).map((item: Record<string, string>) => ({
|
posts: (data.items || []).slice(0, 10).map((item: Record<string, string>) => ({
|
||||||
title: (item.title || "").replace(/<[^>]*>/g, ""),
|
title: (item.title || "").replace(/<[^>]*>/g, ""),
|
||||||
description: (item.description || "").replace(/<[^>]*>/g, ""),
|
description: (item.description || "").replace(/<[^>]*>/g, ""),
|
||||||
|
|
@ -294,7 +306,9 @@ Deno.serve(async (req) => {
|
||||||
if (place) {
|
if (place) {
|
||||||
channelData.googleMaps = {
|
channelData.googleMaps = {
|
||||||
name: place.title, rating: place.totalScore, reviewCount: place.reviewsCount,
|
name: place.title, rating: place.totalScore, reviewCount: place.reviewsCount,
|
||||||
address: place.address, phone: place.phone, website: place.website,
|
address: place.address, phone: place.phone,
|
||||||
|
clinicWebsite: place.website, // clinic's own website (not Maps URL)
|
||||||
|
mapsUrl: place.url || (place.title ? `https://www.google.com/maps/search/${encodeURIComponent(String(place.title))}` : ''),
|
||||||
category: place.categoryName, openingHours: place.openingHours,
|
category: place.categoryName, openingHours: place.openingHours,
|
||||||
topReviews: ((place.reviews as Record<string, unknown>[]) || []).slice(0, 10).map(r => ({
|
topReviews: ((place.reviews as Record<string, unknown>[]) || []).slice(0, 10).map(r => ({
|
||||||
stars: r.stars, text: r.text, publishedAtDate: r.publishedAtDate,
|
stars: r.stars, text: r.text, publishedAtDate: r.publishedAtDate,
|
||||||
|
|
@ -319,7 +333,7 @@ Deno.serve(async (req) => {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: "sonar", messages: [
|
model: PERPLEXITY_MODEL, messages: [
|
||||||
{ role: "system", content: "You are a Korean medical marketing analyst. Always respond in Korean. Provide data in valid JSON format." },
|
{ role: "system", content: "You are a Korean medical marketing analyst. Always respond in Korean. Provide data in valid JSON format." },
|
||||||
{ role: "user", content: q.prompt },
|
{ role: "user", content: q.prompt },
|
||||||
], temperature: 0.3,
|
], temperature: 0.3,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import "@supabase/functions-js/edge-runtime.d.ts";
|
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||||
import { extractSocialLinks, mergeSocialLinks } from "../_shared/extractSocialLinks.ts";
|
import { extractSocialLinks, mergeSocialLinks } from "../_shared/extractSocialLinks.ts";
|
||||||
|
import { PERPLEXITY_MODEL } from "../_shared/config.ts";
|
||||||
import { verifyAllHandles, type VerifiedChannels } from "../_shared/verifyHandles.ts";
|
import { verifyAllHandles, type VerifiedChannels } from "../_shared/verifyHandles.ts";
|
||||||
import { RESEARCH_SYSTEM_PROMPT, buildResearchUserPrompt } from "../_shared/researchPrompt.ts";
|
import { RESEARCH_SYSTEM_PROMPT, buildResearchUserPrompt } from "../_shared/researchPrompt.ts";
|
||||||
|
|
||||||
|
|
@ -31,7 +32,7 @@ function extractHandle(raw: string, platform: string): string | null {
|
||||||
if (m) return m[1] ? `@${m[1]}` : m[2] || m[3] || null;
|
if (m) return m[1] ? `@${m[1]}` : m[2] || m[3] || null;
|
||||||
h = h.replace(/^@/, '');
|
h = h.replace(/^@/, '');
|
||||||
if (h.includes('http') || h.includes('/') || h.includes('.com')) return null;
|
if (h.includes('http') || h.includes('/') || h.includes('.com')) return null;
|
||||||
if (/^UC[a-zA-Z0-9_-]{20,}$/.test(h)) return h;
|
if (/^UC[a-zA-Z0-9_-]{22}$/.test(h)) return h; // YouTube channel IDs are exactly 24 chars (UC + 22)
|
||||||
if (/^[a-zA-Z0-9._-]+$/.test(h) && h.length >= 2) return `@${h}`;
|
if (/^[a-zA-Z0-9._-]+$/.test(h) && h.length >= 2) return `@${h}`;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +153,7 @@ Deno.serve(async (req) => {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: "sonar",
|
model: PERPLEXITY_MODEL,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: "Respond with ONLY the clinic name in Korean, nothing else." },
|
{ role: "system", content: "Respond with ONLY the clinic name in Korean, nothing else." },
|
||||||
{ role: "user", content: `${url} 이 URL의 병원/클리닉 한국어 이름이 뭐야?` },
|
{ role: "user", content: `${url} 이 URL의 병원/클리닉 한국어 이름이 뭐야?` },
|
||||||
|
|
@ -300,7 +301,7 @@ Deno.serve(async (req) => {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: "sonar",
|
model: PERPLEXITY_MODEL,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: "You are a social media researcher. Search the web and find social media accounts. Respond ONLY with valid JSON." },
|
{ role: "system", content: "You are a social media researcher. Search the web and find social media accounts. Respond ONLY with valid JSON." },
|
||||||
{ role: "user", content: `${searchName} 병원의 인스타그램, 유튜브, 페이스북, 틱톡, 네이버블로그 계정을 검색해서 찾아줘. 검색 결과에서 발견된 계정을 모두 알려줘. 인스타그램은 여러 계정이 있을 수 있어.\n\n{"instagram": ["handle1", "handle2"], "youtube": "channel URL or handle", "facebook": "page name or URL", "tiktok": "handle", "naverBlog": "blog ID"}` },
|
{ role: "user", content: `${searchName} 병원의 인스타그램, 유튜브, 페이스북, 틱톡, 네이버블로그 계정을 검색해서 찾아줘. 검색 결과에서 발견된 계정을 모두 알려줘. 인스타그램은 여러 계정이 있을 수 있어.\n\n{"instagram": ["handle1", "handle2"], "youtube": "channel URL or handle", "facebook": "page name or URL", "tiktok": "handle", "naverBlog": "blog ID"}` },
|
||||||
|
|
@ -333,7 +334,7 @@ Deno.serve(async (req) => {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: "sonar",
|
model: PERPLEXITY_MODEL,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: "You search for clinic listings on medical platforms. Respond ONLY with valid JSON." },
|
{ role: "system", content: "You search for clinic listings on medical platforms. Respond ONLY with valid JSON." },
|
||||||
{ role: "user", content: `${resolvedName} 병원 강남언니 gangnamunni.com 페이지를 찾아줘.\n\n{"gangnamUnni": {"url": "https://gangnamunni.com/hospitals/...", "rating": 9.5, "reviews": 1000}}` },
|
{ role: "user", content: `${resolvedName} 병원 강남언니 gangnamunni.com 페이지를 찾아줘.\n\n{"gangnamUnni": {"url": "https://gangnamunni.com/hospitals/...", "rating": 9.5, "reviews": 1000}}` },
|
||||||
|
|
@ -377,7 +378,7 @@ Deno.serve(async (req) => {
|
||||||
for (const handle of candidates.slice(0, 6)) {
|
for (const handle of candidates.slice(0, 6)) {
|
||||||
try {
|
try {
|
||||||
const apifyRes = await fetch(
|
const apifyRes = await fetch(
|
||||||
`${APIFY_BASE}/acts/apify~instagram-profile-scraper/runs?token=${APIFY_TOKEN}&waitForFinish=30`,
|
`${APIFY_BASE}/acts/apify~instagram-profile-scraper/runs?token=${APIFY_TOKEN}&waitForFinish=45`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,8 @@ Deno.serve(async (req) => {
|
||||||
reviewCount: place.reviewsCount,
|
reviewCount: place.reviewsCount,
|
||||||
address: place.address,
|
address: place.address,
|
||||||
phone: place.phone,
|
phone: place.phone,
|
||||||
website: place.website,
|
clinicWebsite: place.website,
|
||||||
|
mapsUrl: place.url || (place.title ? `https://www.google.com/maps/search/${encodeURIComponent(String(place.title))}` : ''),
|
||||||
category: place.categoryName,
|
category: place.categoryName,
|
||||||
openingHours: place.openingHours,
|
openingHours: place.openingHours,
|
||||||
topReviews: ((place.reviews as Record<string, unknown>[]) || [])
|
topReviews: ((place.reviews as Record<string, unknown>[]) || [])
|
||||||
|
|
@ -233,7 +234,7 @@ Deno.serve(async (req) => {
|
||||||
url: hospitalUrl,
|
url: hospitalUrl,
|
||||||
formats: ["json"],
|
formats: ["json"],
|
||||||
jsonOptions: {
|
jsonOptions: {
|
||||||
prompt: "Extract: hospital name, overall rating (out of 10), total review count, doctors with names/ratings/review counts/specialties, procedures offered, address, certifications/badges",
|
prompt: "Extract: hospital name, overall rating (강남언니 rating is always out of 10, NOT out of 5), total review count, doctors with names/ratings/review counts/specialties, procedures offered, address, certifications/badges",
|
||||||
schema: {
|
schema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
|
|
@ -267,7 +268,8 @@ Deno.serve(async (req) => {
|
||||||
if (hospital?.hospitalName) {
|
if (hospital?.hospitalName) {
|
||||||
enrichment.gangnamUnni = {
|
enrichment.gangnamUnni = {
|
||||||
name: hospital.hospitalName,
|
name: hospital.hospitalName,
|
||||||
rating: hospital.rating,
|
rawRating: hospital.rating,
|
||||||
|
rating: typeof hospital.rating === 'number' && hospital.rating > 0 && hospital.rating <= 5 ? hospital.rating * 2 : hospital.rating,
|
||||||
ratingScale: "/10",
|
ratingScale: "/10",
|
||||||
totalReviews: hospital.totalReviews,
|
totalReviews: hospital.totalReviews,
|
||||||
doctors: (hospital.doctors || []).slice(0, 10),
|
doctors: (hospital.doctors || []).slice(0, 10),
|
||||||
|
|
@ -316,27 +318,39 @@ Deno.serve(async (req) => {
|
||||||
})()
|
})()
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4b. Local search — Naver Place
|
// 4b. Local search — Naver Place (with category filtering to avoid same-name clinics)
|
||||||
tasks.push(
|
tasks.push(
|
||||||
(async () => {
|
(async () => {
|
||||||
const query = encodeURIComponent(clinicName);
|
const queries = [`${clinicName} 성형외과`, `${clinicName} 성형`, clinicName];
|
||||||
|
for (const q of queries) {
|
||||||
|
const query = encodeURIComponent(q);
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://openapi.naver.com/v1/search/local.json?query=${query}&display=5&sort=comment`,
|
`https://openapi.naver.com/v1/search/local.json?query=${query}&display=5&sort=comment`,
|
||||||
{ headers: naverHeaders }
|
{ headers: naverHeaders }
|
||||||
);
|
);
|
||||||
if (!res.ok) return;
|
if (!res.ok) continue;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const place = (data.items || [])[0];
|
const items = (data.items || []) as Record<string, string>[];
|
||||||
if (place) {
|
// Prefer category matching 성형 or 피부
|
||||||
|
const match = items.find(i =>
|
||||||
|
(i.category || '').includes('성형') || (i.category || '').includes('피부')
|
||||||
|
) || items.find(i => {
|
||||||
|
const name = (i.title || '').replace(/<[^>]*>/g, '').toLowerCase();
|
||||||
|
return name.includes(clinicName.replace(/성형외과|병원|의원/g, '').trim().toLowerCase());
|
||||||
|
}) || null;
|
||||||
|
|
||||||
|
if (match) {
|
||||||
enrichment.naverPlace = {
|
enrichment.naverPlace = {
|
||||||
name: (place.title || "").replace(/<[^>]*>/g, ""),
|
name: (match.title || "").replace(/<[^>]*>/g, ""),
|
||||||
category: place.category,
|
category: match.category,
|
||||||
address: place.roadAddress || place.address,
|
address: match.roadAddress || match.address,
|
||||||
telephone: place.telephone,
|
telephone: match.telephone,
|
||||||
link: place.link,
|
link: match.link,
|
||||||
mapx: place.mapx,
|
mapx: match.mapx,
|
||||||
mapy: place.mapy,
|
mapy: match.mapy,
|
||||||
};
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import "@supabase/functions-js/edge-runtime.d.ts";
|
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||||
import { normalizeInstagramHandle } from "../_shared/normalizeHandles.ts";
|
import { normalizeInstagramHandle } from "../_shared/normalizeHandles.ts";
|
||||||
|
import { PERPLEXITY_MODEL } from "../_shared/config.ts";
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
|
@ -110,7 +111,7 @@ ${JSON.stringify(scrapeData.branding || {}, null, 2).slice(0, 1000)}
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: "sonar",
|
model: PERPLEXITY_MODEL,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: "You are a Korean medical marketing analyst. Respond ONLY with valid JSON, no markdown code blocks. Use Korean for text fields. 강남언니 rating is 10-point scale. Use ONLY the provided real data — never invent metrics." },
|
{ role: "system", content: "You are a Korean medical marketing analyst. Respond ONLY with valid JSON, no markdown code blocks. Use Korean for text fields. 강남언니 rating is 10-point scale. Use ONLY the provided real data — never invent metrics." },
|
||||||
{ role: "user", content: reportPrompt },
|
{ role: "user", content: reportPrompt },
|
||||||
|
|
@ -217,7 +218,7 @@ ${JSON.stringify(analyzeResult.data?.analysis || {}, null, 2).slice(0, 4000)}
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: "sonar",
|
model: PERPLEXITY_MODEL,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: "You are a Korean medical marketing analyst. Respond ONLY with valid JSON, no markdown code blocks. Korean for text fields. 강남언니 rating uses 10-point scale." },
|
{ role: "system", content: "You are a Korean medical marketing analyst. Respond ONLY with valid JSON, no markdown code blocks. Korean for text fields. 강남언니 rating uses 10-point scale." },
|
||||||
{ role: "user", content: reportPrompt },
|
{ role: "user", content: reportPrompt },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue