diff --git a/supporters/scripts/build_planner_data.mjs b/supporters/scripts/build_planner_data.mjs index b7efae5..ac33cff 100644 --- a/supporters/scripts/build_planner_data.mjs +++ b/supporters/scripts/build_planner_data.mjs @@ -38,7 +38,14 @@ const base = readJson(basePath); const notesPath = join(DATA, 'recoveryNotes.json'); const notes = existsSync(notesPath) ? readJson(notesPath) : { items: [] }; const shortName = fact.shortName || fact.name || clinic; -let attached = 0, unmatched = []; +// 병원 원문 중 '회복기 관리·시기' 문장만 쓴다. 효과 주장·홍보문·정형 합병증 고지·표 조각·재수술 시기 조언은 규칙표에 넣지 않는다(의료광고 규정, 그리고 플래너의 목적 밖). +const CARE = /일상\s?생활|일상\s?복귀|실밥|봉합|샤워|세안|화장|운동|사우나|찜질|음주|흡연|술|담배|압박|붓기|부기|냉찜질|온찜질|자외선|차단제|마사지|누워|수면|식사|음식|유동식|안정|보호자|컷트|펌|염색|가글|양치|콘택트|안경|모자|주의해야|피해|삼가|금지|퇴원|입원/; +const NOT_CARE = /티가 나지|자연스러|가능할 만큼|부담이 적|원해요|싶어요|예측 가능|만족|아름|예쁘|낮아지지 않|보존되고|수유가 가능|합병증인|재수술|수술을 결정|\|/; +const isCare = (ko) => ko.length >= 6 && CARE.test(ko) && !NOT_CARE.test(ko); +// 페이지 제목에서 세부 시술명만: "코성형(매부리코성형)-당신만 봅니다-원진성형외과" → "매부리코성형" +const subName = (title) => { const full = String(title); const m = full.match(/\(([^)]+)\)/); if (m) return m[1].replace(/FAQ.*$/, '').trim(); return full.split(/\s*[-|]\s*/)[0].replace(/FAQ.*$/, '').trim(); }; +let attached = 0, unmatched = [], dropped = 0; +const seen = new Set(); for (const it of notes.items ?? []) { const target = base.procedures.find((p) => p.matchKeywords && new RegExp(p.matchKeywords).test(it.procedure)); if (!target) { unmatched.push(it.procedure); continue; } @@ -47,16 +54,20 @@ for (const it of notes.items ?? []) { if (!base.clinicSources.some((s) => s.id === srcId)) { base.clinicSources.push({ id: srcId, label: { ko: `${shortName} 홈페이지 · ${it.procedure} 시술 안내`, en: `${shortName} website · ${it.procedureEn || it.procedure}` }, url: it.url, accessed: it.fetchedAt || notes.generatedAt || '' }); } + const sub = subName(it.procedure), subEn = (it.procedureEn || sub).split(/\s*[-|]\s*/)[0].trim(); (it.ko ?? []).forEach((ko, i) => { + if (!isCare(ko)) { dropped++; return; } + const key = `${target.id}|${ko.replace(/\s+/g, '')}`; if (seen.has(key)) return; seen.add(key); const en = (it.en ?? [])[i] || ko; // 번역 실패 문장은 한글 원문 그대로 (워커 recovery 단계와 같은 규칙) const cid = `${srcId}-${i}`; - base.constraints[cid] = { ko: ko.trim(), en: en.trim() }; + base.constraints[cid] = { ko: `${sub}: ${ko.trim()}`, en: `${subEn}: ${en.trim()}` }; if (!target.constraints.includes(cid)) target.constraints.unshift(cid); // 병원 원문이 잠정 안내보다 먼저 보이게 attached++; }); - if (!target.sources.includes(srcId)) target.sources.push(srcId); + if (target.constraints.some((c) => c.startsWith(srcId + '-')) && !target.sources.includes(srcId)) target.sources.push(srcId); } for (const p of base.procedures) delete p.matchKeywords; +base.clinicSources = base.clinicSources.filter((src) => base.procedures.some((p) => p.sources.includes(src.id))); base._comment = `${shortName} 회복 일정 플래너 규칙표 (${industry === 'derm' ? '피부과' : '성형외과'} 기본표 + 병원 시술 페이지 원문 ${attached}문장). clinic = 병원 원문, provisional = 병원 확인 전 잠정값(화면 배지). 생성 ${new Date().toISOString().slice(0, 10)}, 워커 planner 단계.`; writeJson(join(DATA, 'procedures.json'), base); @@ -80,4 +91,4 @@ if (existsSync(curated)) { writeJson(join(DATA, 'places.json'), out); placesSummary = `범용 기본표 ${out.places.length}곳 (origin ${origin.lat ? `${origin.lat}, ${origin.lng}` : '없음'})`; } -console.log(`planner: 업종 ${industry} · 시술 ${base.procedures.length}종 · 병원 원문 ${attached}문장 붙임${unmatched.length ? ` · 매칭 안 된 시술 ${unmatched.length}(${unmatched.slice(0, 4).join(', ')})` : ''} · ${placesSummary}`); +console.log(`planner: 업종 ${industry} · 시술 ${base.procedures.length}종 · 병원 원문 ${attached}문장 붙임(관리 문장 아님 ${dropped}건 제외)${unmatched.length ? ` · 매칭 안 된 시술 ${unmatched.length}(${unmatched.slice(0, 4).join(', ')})` : ''} · ${placesSummary}`); diff --git a/templates/supporters-astro/briefs/wonjin/planner.places.json b/templates/supporters-astro/briefs/wonjin/planner.places.json index 7432082..f63241d 100644 --- a/templates/supporters-astro/briefs/wonjin/planner.places.json +++ b/templates/supporters-astro/briefs/wonjin/planner.places.json @@ -5,8 +5,8 @@ "ko": "원진성형외과 (강남대로 419)", "en": "Wonjin Plastic Surgery, Gangnam-daero" }, - "lat": 37.499765, - "lng": 127.0261518 + "lat": 37.4998653, + "lng": 127.0262161 }, "categories": { "sightseeing": { diff --git a/templates/supporters-astro/scripts/build_planner_data.mjs b/templates/supporters-astro/scripts/build_planner_data.mjs index b7efae5..ac33cff 100644 --- a/templates/supporters-astro/scripts/build_planner_data.mjs +++ b/templates/supporters-astro/scripts/build_planner_data.mjs @@ -38,7 +38,14 @@ const base = readJson(basePath); const notesPath = join(DATA, 'recoveryNotes.json'); const notes = existsSync(notesPath) ? readJson(notesPath) : { items: [] }; const shortName = fact.shortName || fact.name || clinic; -let attached = 0, unmatched = []; +// 병원 원문 중 '회복기 관리·시기' 문장만 쓴다. 효과 주장·홍보문·정형 합병증 고지·표 조각·재수술 시기 조언은 규칙표에 넣지 않는다(의료광고 규정, 그리고 플래너의 목적 밖). +const CARE = /일상\s?생활|일상\s?복귀|실밥|봉합|샤워|세안|화장|운동|사우나|찜질|음주|흡연|술|담배|압박|붓기|부기|냉찜질|온찜질|자외선|차단제|마사지|누워|수면|식사|음식|유동식|안정|보호자|컷트|펌|염색|가글|양치|콘택트|안경|모자|주의해야|피해|삼가|금지|퇴원|입원/; +const NOT_CARE = /티가 나지|자연스러|가능할 만큼|부담이 적|원해요|싶어요|예측 가능|만족|아름|예쁘|낮아지지 않|보존되고|수유가 가능|합병증인|재수술|수술을 결정|\|/; +const isCare = (ko) => ko.length >= 6 && CARE.test(ko) && !NOT_CARE.test(ko); +// 페이지 제목에서 세부 시술명만: "코성형(매부리코성형)-당신만 봅니다-원진성형외과" → "매부리코성형" +const subName = (title) => { const full = String(title); const m = full.match(/\(([^)]+)\)/); if (m) return m[1].replace(/FAQ.*$/, '').trim(); return full.split(/\s*[-|]\s*/)[0].replace(/FAQ.*$/, '').trim(); }; +let attached = 0, unmatched = [], dropped = 0; +const seen = new Set(); for (const it of notes.items ?? []) { const target = base.procedures.find((p) => p.matchKeywords && new RegExp(p.matchKeywords).test(it.procedure)); if (!target) { unmatched.push(it.procedure); continue; } @@ -47,16 +54,20 @@ for (const it of notes.items ?? []) { if (!base.clinicSources.some((s) => s.id === srcId)) { base.clinicSources.push({ id: srcId, label: { ko: `${shortName} 홈페이지 · ${it.procedure} 시술 안내`, en: `${shortName} website · ${it.procedureEn || it.procedure}` }, url: it.url, accessed: it.fetchedAt || notes.generatedAt || '' }); } + const sub = subName(it.procedure), subEn = (it.procedureEn || sub).split(/\s*[-|]\s*/)[0].trim(); (it.ko ?? []).forEach((ko, i) => { + if (!isCare(ko)) { dropped++; return; } + const key = `${target.id}|${ko.replace(/\s+/g, '')}`; if (seen.has(key)) return; seen.add(key); const en = (it.en ?? [])[i] || ko; // 번역 실패 문장은 한글 원문 그대로 (워커 recovery 단계와 같은 규칙) const cid = `${srcId}-${i}`; - base.constraints[cid] = { ko: ko.trim(), en: en.trim() }; + base.constraints[cid] = { ko: `${sub}: ${ko.trim()}`, en: `${subEn}: ${en.trim()}` }; if (!target.constraints.includes(cid)) target.constraints.unshift(cid); // 병원 원문이 잠정 안내보다 먼저 보이게 attached++; }); - if (!target.sources.includes(srcId)) target.sources.push(srcId); + if (target.constraints.some((c) => c.startsWith(srcId + '-')) && !target.sources.includes(srcId)) target.sources.push(srcId); } for (const p of base.procedures) delete p.matchKeywords; +base.clinicSources = base.clinicSources.filter((src) => base.procedures.some((p) => p.sources.includes(src.id))); base._comment = `${shortName} 회복 일정 플래너 규칙표 (${industry === 'derm' ? '피부과' : '성형외과'} 기본표 + 병원 시술 페이지 원문 ${attached}문장). clinic = 병원 원문, provisional = 병원 확인 전 잠정값(화면 배지). 생성 ${new Date().toISOString().slice(0, 10)}, 워커 planner 단계.`; writeJson(join(DATA, 'procedures.json'), base); @@ -80,4 +91,4 @@ if (existsSync(curated)) { writeJson(join(DATA, 'places.json'), out); placesSummary = `범용 기본표 ${out.places.length}곳 (origin ${origin.lat ? `${origin.lat}, ${origin.lng}` : '없음'})`; } -console.log(`planner: 업종 ${industry} · 시술 ${base.procedures.length}종 · 병원 원문 ${attached}문장 붙임${unmatched.length ? ` · 매칭 안 된 시술 ${unmatched.length}(${unmatched.slice(0, 4).join(', ')})` : ''} · ${placesSummary}`); +console.log(`planner: 업종 ${industry} · 시술 ${base.procedures.length}종 · 병원 원문 ${attached}문장 붙임(관리 문장 아님 ${dropped}건 제외)${unmatched.length ? ` · 매칭 안 된 시술 ${unmatched.length}(${unmatched.slice(0, 4).join(', ')})` : ''} · ${placesSummary}`);