chore(supporters): 플래너 검증·출처 줄을 Productize 템플릿과 동일하게 (관광 데이터 없을 때 건너뜀)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Haewon Kam 2026-09-11 11:28:05 +09:00
parent 65fb9be9c6
commit 6b01cc9f7b
2 changed files with 9 additions and 5 deletions

View File

@ -97,17 +97,20 @@ ok('규칙표 정합성: 숫자마다 status, provisional 은 source 없어도
for (const pl of PL.places) { assert.ok(PL.categories[pl.category], `${pl.id} 카테고리`); assert.ok(pl.name.ko && pl.name.en, `${pl.id} 이름`); }
});
const HAS_TOUR = Object.values(TKO.places ?? {}).some((v) => Array.isArray(v) && v.length); // 템플릿 빈 데이터면 관광공사 검사는 건너뛴다
ok('TourAPI 어댑터: 카테고리 매핑, 사우나 제외, 음식점은 일반식만, 거리 등급', () => {
assert.equal(levelByDistance(400), 1); assert.equal(levelByDistance(2500), 2); assert.equal(levelByDistance(9000), 3);
assert.equal(bookingName('K-그랜드 호스텔 강남 1(K-GRAND HOSTEL GANGNAM1)'), 'K-GRAND HOSTEL GANGNAM1');
if (!HAS_TOUR) { console.log(' (관광공사 데이터 없음: 변환 검사 건너뜀)'); return; }
const tp = tourToPlaces(TKO, 'ko');
assert.ok(tp.length > 100, `변환 ${tp.length}건`);
assert.ok(tp.length > 50, `변환 ${tp.length}건`);
assert.ok(tp.every((p) => p.source === 'tourapi'));
assert.ok(!tp.some((p) => p.id.startsWith('tour-wellness-')), '사우나·스파가 후보에 들어감');
assert.ok(tp.filter((p) => p.category === 'food').every((p) => p.dietOk.length === 1 && p.dietOk[0] === 'normal'));
assert.equal(levelByDistance(400), 1); assert.equal(levelByDistance(2500), 2); assert.equal(levelByDistance(9000), 3);
assert.equal(bookingName('K-그랜드 호스텔 강남 1(K-GRAND HOSTEL GANGNAM1)'), 'K-GRAND HOSTEL GANGNAM1');
});
ok('큐레이션이 TourAPI 보다 먼저 나오고 유동식 단계에는 TourAPI 음식점이 없음', () => {
if (!HAS_TOUR) return;
const tp = tourToPlaces(TKO, 'ko');
const all = [...PL.places.map((x) => ({ ...x, source: 'curated' })), ...tp];
const plan = buildPlan({ procedure: byId('contour'), surgeryDate: '2026-11-03', nationality: null, prefs: ['food'] }, all);
@ -117,6 +120,7 @@ ok('큐레이션이 TourAPI 보다 먼저 나오고 유동식 단계에는 TourA
});
ok('축제: 체류 기간과 겹치는 것만, L3 날에만 붙음', () => {
if (!HAS_TOUR) return;
const fest = festivalsDuring(TKO, '2026-11-01', '2026-11-17');
for (const f of fest) assert.ok(f.start <= '2026-11-17' && f.end >= '2026-11-01');
const plan = buildPlan({ procedure: byId('skin'), surgeryDate: '2026-11-03', nationality: null, prefs: [], festivals: fest }, PL.places);

View File

@ -229,7 +229,7 @@ const originLabel = (PL as any).origin?.label?.[lang] ?? '';
const from = firstL3 && firstL3 <= f.end ? fill(t.fest_from, { d: fmtDate(firstL3 < f.start ? f.start : firstL3) }) : t.fest_notYet;
return `<div class="card fest-card" id="fest-${esc(f.id)}">${f.image ? `<img src="${esc(f.image)}" alt="${esc(f.title)}" loading="lazy" onerror="this.remove()" />` : ''}<div class="cat">${esc(fill(t.fest_when, { a: f.start.slice(5).replace('-', '.'), b: f.end.slice(5).replace('-', '.') }))} · ${esc(f.travelLabel ?? fill(t.km, { km: f.distanceKm }))}</div><h3>${esc(f.title)}</h3><p>${esc(f.blurb)}</p><p class="fine"><span class="tag ${firstL3 && firstL3 <= f.end ? 'l3' : 'l0'}">${esc(from)}</span> ${f.url ? `<a href="${esc(f.url)}" rel="noopener" target="_blank">${esc(t.readMoreShort)}</a> · ` : ''}<a href="${mapUrl(f.title)}" rel="noopener" target="_blank">${esc(t.map)}</a></p></div>`;
}).join('');
const festSec = `<section class="section light"><div class="wrap-wide">${secHead('Events', t.h_fest, t.sub_fest)}${festCards ? `<div class="grid">${festCards}</div>` : `<p class="sub" style="color:var(--slate-500)">${esc(t.fest_none)}</p>`}<p class="fine" style="margin-top:1rem">${esc(fill(t.source_tour, { date: tourDate }))}</p></div></section>`;
const festSec = `<section class="section light"><div class="wrap-wide">${secHead('Events', t.h_fest, t.sub_fest)}${festCards ? `<div class="grid">${festCards}</div>` : `<p class="sub" style="color:var(--slate-500)">${esc(t.fest_none)}</p>`}${tourDate ? `<p class="fine" style="margin-top:1rem">${esc(fill(t.source_tour, { date: tourDate }))}</p>` : ''}</div></section>`;
// 다크 섹션: 예약과 저장 (큐레이션 숙소 + TourAPI 숙박 가까운 순 6곳)
const checkin = plan.arriveBy, checkout = plan.earliestDeparture ?? addDays(plan.surgeryDate, 14);
@ -248,7 +248,7 @@ const originLabel = (PL as any).origin?.label?.[lang] ?? '';
<div class="card"><div class="cat">Save</div><h3>${esc(t.ics)}</h3><p>${esc(t.hotelHint)}</p><div class="plan-actions"><button type="button" class="btn ghost small" data-action="ics">${esc(t.ics)}</button><button type="button" class="btn ghost small" data-action="share">${esc(t.shareLink)}</button><button type="button" class="btn ghost small" data-action="print">${esc(t.print)}</button></div></div>
</div>
<p class="disclosure">${esc(t.affiliate)} ${esc(t.medical)}</p>
<p class="note" style="margin-top:0.6rem">${esc(t.sourceLabel)}</p><ul class="sources-list">${srcs}<li>${esc(fill(t.source_tour, { date: tourDate }))}</li></ul>
<p class="note" style="margin-top:0.6rem">${esc(t.sourceLabel)}</p><ul class="sources-list">${srcs}${tourDate ? `<li>${esc(fill(t.source_tour, { date: tourDate }))}</li>` : ''}</ul>
</div></section>`;
return scheduleSec + rulesSec + daysSec + festSec + bookSec;