- supporters/: Astro 사이트 소스 (pages·components·posts·factSheet/authors/videos 데이터, 뉴스룸 916건 빌더, 발행 게이트 check.mjs, vercel.json) - supporters/docs/: v1~v3 스크린샷 기록 - docs/INFINITH_Viewclinic_Supporters_Site_Design_v0.1.md: 서포터즈 사이트 설계 문서 - docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD.md: 다음 세션 자동 빌드 프롬프트 - docs/reports/Viewclinic_Supporters_vs_Official_AEO_Audit.xlsx + scripts/build_supporters_audit_xlsx.py: 서포터즈 vs 공식 사이트 AEO/GEO 감사 비교 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
123 lines
9.6 KiB
Plaintext
123 lines
9.6 KiB
Plaintext
---
|
|
import Base from '../layouts/Base.astro';
|
|
import news from '../data/news.json';
|
|
import { SITE_NAME, fact } from '../lib';
|
|
const items = news.items as { date: string; title: string; outlet: string; url: string; kind: 'release' | 'column' | 'mention'; origin: string; topic: string }[];
|
|
const site = Astro.site!.toString().replace(/\/$/, '');
|
|
const years = [...new Set(items.map((i) => i.date.slice(0, 4)))].sort().reverse();
|
|
const topics = [...new Set(items.map((i) => i.topic))];
|
|
const topicCount = Object.fromEntries(topics.map((t) => [t, items.filter((i) => i.topic === t).length]));
|
|
topics.sort((a, b) => topicCount[b] - topicCount[a]);
|
|
const KIND: Record<string, string> = { release: '보도자료', column: '원장 기고·인터뷰', mention: '언론 언급' };
|
|
const counts = { release: items.filter((i) => i.kind === 'release').length, column: items.filter((i) => i.kind === 'column').length, mention: items.filter((i) => i.kind === 'mention').length };
|
|
const outlets = new Set(items.map((i) => i.outlet)).size;
|
|
const ld = [
|
|
{ '@type': 'CollectionPage', '@id': `${site}/newsroom#page`, name: '뷰성형외과 뉴스룸', url: `${site}/newsroom`, inLanguage: 'ko', about: { '@id': `${fact.url}/#clinic` }, publisher: { '@type': 'Organization', name: SITE_NAME, url: site }, dateModified: news.generatedAt },
|
|
{ '@type': 'ItemList', '@id': `${site}/newsroom#list`, name: '뷰성형외과 언론 보도 목록', numberOfItems: items.length, itemListOrder: 'https://schema.org/ItemListOrderDescending', itemListElement: items.slice(0, 200).map((i, idx) => ({ '@type': 'ListItem', position: idx + 1, url: i.url, name: i.title })) },
|
|
];
|
|
---
|
|
<Base title="뉴스룸 · 뷰성형외과 언론 보도 링크 트리" description={`뷰성형외과가 배포한 보도자료와 원장 기고·인터뷰, 언론 언급 기사 ${items.length}건을 연도·주제별로 정리한 링크 모음. ${years[years.length - 1]}년부터 ${years[0]}년까지, 매체 ${outlets}곳.`} jsonLd={ld}>
|
|
<section class="section dark" style="padding:3.5rem 0 3rem">
|
|
<div class="wrap-wide">
|
|
<div class="sec-head"><div class="eyebrow" style="color:var(--purple-300)">Newsroom</div><h1 class="serif" style="font-size:2rem;color:#fff;display:inline-block;background:linear-gradient(to right,var(--purple-300),var(--blue-300));-webkit-background-clip:text;background-clip:text;color:transparent">Press Link Tree</h1><p class="sub">뷰성형외과가 배포한 보도자료와 원장 기고, 언론 언급 기사를 연도별로 모았습니다. 기사 본문은 싣지 않고 원문으로 연결합니다.</p></div>
|
|
<div class="stats">
|
|
<div class="stat"><b>{items.length.toLocaleString()}</b><span class="label">기사 링크</span><span class="desc">{years[years.length - 1]}년부터 {years[0]}년까지. 병원 언론보도 게시판 {items.filter((i) => i.origin === 'board').length}건 + 네이버 뉴스 수집 {items.filter((i) => i.origin === 'naver').length}건, 중복 제거.</span></div>
|
|
<div class="stat"><b>{counts.release.toLocaleString()}</b><span class="label">보도자료</span><span class="desc">병원이 주체로 배포한 기사. 수상·도입·행사·협약.</span></div>
|
|
<div class="stat"><b>{counts.column.toLocaleString()}</b><span class="label">원장 기고·인터뷰</span><span class="desc">뷰성형외과 원장 인용이 들어간 시술 설명 기사.</span></div>
|
|
<div class="stat"><b>{outlets}</b><span class="label">매체</span><span class="desc">메디컬투데이·전민일보·아주경제·머니S·뉴시스 등.</span></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section light" style="padding-top:2.5rem">
|
|
<div class="wrap-wide">
|
|
<div class="filters" id="filters">
|
|
<div class="fgroup"><span class="flabel">종류</span>
|
|
<button class="chipbtn active" data-kind="all">전체</button>
|
|
<button class="chipbtn" data-kind="release">보도자료 {counts.release}</button>
|
|
<button class="chipbtn" data-kind="column">기고·인터뷰 {counts.column}</button>
|
|
<button class="chipbtn" data-kind="mention">언론 언급 {counts.mention}</button>
|
|
</div>
|
|
<div class="fgroup"><span class="flabel">주제</span>
|
|
<button class="chipbtn active" data-topic="all">전체</button>
|
|
{topics.map((t) => <button class="chipbtn" data-topic={t}>{t} {topicCount[t]}</button>)}
|
|
</div>
|
|
<div class="fgroup"><span class="flabel">연도</span>
|
|
<div class="yearnav">{years.map((y) => <a href={`#y${y}`}>{y}</a>)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{years.map((y) => {
|
|
const list = items.filter((i) => i.date.startsWith(y));
|
|
return (
|
|
<section class="year" id={`y${y}`} data-year={y}>
|
|
<div class="yearhead"><h2 class="serif" style="margin-top:0">{y}</h2><span class="ycount">{list.length}건</span></div>
|
|
<ol class="newslist">
|
|
{list.map((i) => (
|
|
<li class="newsitem" data-kind={i.kind} data-topic={i.topic}>
|
|
<span class="ndate">{i.date}</span>
|
|
<span class={`nkind ${i.kind}`}>{KIND[i.kind]}</span>
|
|
<a class="ntitle" href={i.url} rel="noopener nofollow" target="_blank">{i.title}</a>
|
|
<span class="noutlet">{i.outlet}</span>
|
|
<span class="ntopic">{i.topic}</span>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</section>
|
|
);
|
|
})}
|
|
<p class="disclosure">수집 기준: 뷰성형외과 홈페이지 언론보도 게시판(2012~2021년 7월, 이후 갱신 없음)과 네이버 뉴스 검색(2026-09-04 기준, "뷰성형외과" 등 6개 질의). 규제·사건 등 제3자 보도는 이 링크 트리의 범위(병원 배포 기사)에 넣지 않았습니다. 기사 제목과 매체명만 표기하고 본문은 인용하지 않습니다. 원문 링크가 사라진 기사는 병원 게시판 사본으로 연결됩니다.</p>
|
|
</div>
|
|
</section>
|
|
</Base>
|
|
<style>
|
|
.filters { display: grid; gap: 0.7rem; margin-bottom: 2rem; background: var(--primary-50); border-radius: 16px; padding: 1.1rem 1.25rem; }
|
|
.fgroup { display: flex; flex-wrap: wrap; align-items: center; gap: 0.45rem; }
|
|
.flabel { font-size: 0.78rem; font-weight: 700; color: var(--slate-500); letter-spacing: 0.06em; margin-right: 0.3rem; min-width: 2.4em; }
|
|
.chipbtn { border: 1px solid var(--slate-200); background: #fff; color: var(--slate-600); border-radius: 999px; padding: 0.3rem 0.8rem; font-size: 0.82rem; font-weight: 600; cursor: pointer; font-family: inherit; }
|
|
.chipbtn.active { background: linear-gradient(to right, var(--grad-start), var(--grad-end)); color: #fff; border-color: transparent; }
|
|
.yearnav { display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
|
.yearnav a { font-size: 0.82rem; font-weight: 600; color: var(--accent); padding: 0.2rem 0.5rem; border-radius: 6px; }
|
|
.yearnav a:hover { background: var(--st-good-bg); text-decoration: none; }
|
|
.year { margin: 0 0 2.2rem; scroll-margin-top: 90px; }
|
|
.year.hidden { display: none; }
|
|
.yearhead { display: flex; align-items: baseline; gap: 0.8rem; border-bottom: 2px solid var(--primary-900); padding-bottom: 0.4rem; margin-bottom: 0.6rem; }
|
|
.yearhead h2 { margin: 0; font-size: 1.8rem; }
|
|
.ycount { color: var(--slate-500); font-size: 0.85rem; font-weight: 600; }
|
|
.newslist { list-style: none; padding: 0; margin: 0; }
|
|
.newsitem { display: grid; grid-template-columns: 6.2em 6.4em 1fr auto auto; gap: 0.6rem 0.9rem; align-items: baseline; padding: 0.55rem 0.4rem; border-bottom: 1px solid var(--slate-100); font-size: 0.93rem; }
|
|
.newsitem.hidden { display: none; }
|
|
.newsitem:hover { background: var(--primary-50); }
|
|
.ndate { color: var(--slate-500); font-variant-numeric: tabular-nums; font-size: 0.85rem; }
|
|
.nkind { font-size: 0.72rem; font-weight: 700; border-radius: 999px; padding: 0.1rem 0.55rem; text-align: center; border: 1px solid transparent; white-space: nowrap; }
|
|
.nkind.release { background: var(--st-good-bg); color: var(--st-good-text); border-color: var(--st-good-border); }
|
|
.nkind.column { background: var(--st-info-bg); color: var(--st-info-text); border-color: var(--st-info-border); }
|
|
.nkind.mention { background: var(--slate-100); color: var(--slate-600); }
|
|
.ntitle { color: var(--primary-900); font-weight: 500; }
|
|
.noutlet { color: var(--slate-500); font-size: 0.83rem; white-space: nowrap; }
|
|
.ntopic { color: var(--accent); font-size: 0.75rem; font-weight: 600; white-space: nowrap; }
|
|
@media (max-width: 760px) { .newsitem { grid-template-columns: 1fr; gap: 0.15rem; } .noutlet, .ntopic { display: inline; margin-right: 0.6rem; } }
|
|
</style>
|
|
<script>
|
|
const state = { kind: 'all', topic: 'all' };
|
|
const items = Array.from(document.querySelectorAll<HTMLElement>('.newsitem'));
|
|
const years = Array.from(document.querySelectorAll<HTMLElement>('.year'));
|
|
function apply() {
|
|
for (const el of items) {
|
|
const ok = (state.kind === 'all' || el.dataset.kind === state.kind) && (state.topic === 'all' || el.dataset.topic === state.topic);
|
|
el.classList.toggle('hidden', !ok);
|
|
}
|
|
for (const y of years) {
|
|
const visible = y.querySelectorAll('.newsitem:not(.hidden)').length;
|
|
y.classList.toggle('hidden', visible === 0);
|
|
const c = y.querySelector('.ycount'); if (c) c.textContent = `${visible}건`;
|
|
}
|
|
}
|
|
document.querySelectorAll<HTMLButtonElement>('.chipbtn').forEach((b) => b.addEventListener('click', () => {
|
|
const key = b.dataset.kind ? 'kind' : 'topic'; const val = (b.dataset.kind ?? b.dataset.topic)!;
|
|
(state as any)[key] = val;
|
|
b.parentElement!.querySelectorAll('.chipbtn').forEach((x) => x.classList.toggle('active', x === b));
|
|
apply();
|
|
}));
|
|
</script>
|