fix(worker): 상호를 브리프 고정값에서 먼저 읽는다 · 뷰·원진·오라클 고정값 추가

재빌드 검증 중에 홈 제목 추정이 슬로건을 상호로 삼는 것을 발견했다.
뷰는 '안전을 최우선하는 뷰성형외과', 원진은 '당신만 봅니다'가 상호가 됐다.
원진은 그 이름으로 네이버 지역검색을 해 주변 안내 기준 좌표까지 실패했다.

data 단계가 briefs/<clinic>/clinic.json 의 factSheet 상호를 --name 과 홈 제목 추정보다
먼저 읽게 했다. 상호를 쓰는 뒤 단계(discover·news·tourism)까지 올바른 값이 간다.
inputs 단계의 고정값 덮어쓰기만으로는 그 단계들이 이미 틀린 이름으로 돈 뒤였다.

브리프 고정값: 뷰(name·shortName·kind·areaLabel·founded·urlEn),
원진(name·shortName·kind), 오라클(name·shortName 추가).
근거를 확인하지 못한 값은 비워 두었다.

재빌드 결과(미배포): 세 사이트 모두 제목이 기존 배포본과 일치하고 잃은 경로가 없다.
뷰는 /plan·/en/plan·/stay·/en/stay·/en 5개, 원진·오라클은 /en 이 새로 생겼다.
원진 주변 안내 수집도 좌표를 다시 잡아 국문 123곳·영문 99곳으로 성공했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Haewon Kam 2026-09-12 15:55:31 +09:00
parent 82aa56fa40
commit 4354d7fa6c
4 changed files with 30 additions and 6 deletions

View File

@ -9,7 +9,9 @@
"gangnamunni": "https://www.gangnamunni.com/hospitals/125", "gangnamunni": "https://www.gangnamunni.com/hospitals/125",
"factSheet": { "factSheet": {
"kind": "피부과", "kind": "피부과",
"areaLabel": "서울 강남 청담" "areaLabel": "서울 강남 청담",
"name": "오라클피부과의원",
"shortName": "오라클피부과"
}, },
"site": { "site": {
"heroImage": { "heroImage": {
@ -41,5 +43,6 @@
"sub_0" "sub_0"
], ],
"_logoNote": "2026-09-10 haewon 지적(녹색 배너형 GIF는 완성도가 떨어짐) → 그룹 사이트 oraclemedicalgroup.com/images/logo_oracle_02.png (176×68, 투명 배경 워드마크)로 교체. 클리닉 사이트에는 CI 페이지가 없음(메뉴 CI소개가 빈 링크). 이전 배너형은 logo-horizontal.png 로 보존. haewon 제안(2026-09-10)대로 원본 배너에서 왼쪽 문구 영역을 잘라낸 logo-horizontal-cropped.png(133×49, 녹색 배경 유지)도 보관. 녹색 박스형을 쓰려면 logo 값을 이 파일로 바꾸고 --from youtube 재실행.", "_logoNote": "2026-09-10 haewon 지적(녹색 배너형 GIF는 완성도가 떨어짐) → 그룹 사이트 oraclemedicalgroup.com/images/logo_oracle_02.png (176×68, 투명 배경 워드마크)로 교체. 클리닉 사이트에는 CI 페이지가 없음(메뉴 CI소개가 빈 링크). 이전 배너형은 logo-horizontal.png 로 보존. haewon 제안(2026-09-10)대로 원본 배너에서 왼쪽 문구 영역을 잘라낸 logo-horizontal-cropped.png(133×49, 녹색 배경 유지)도 보관. 녹색 박스형을 쓰려면 logo 값을 이 파일로 바꾸고 --from youtube 재실행.",
"_imageNote": "2026-09-10: 본사 건물 외관은 그룹 사이트 oraclemedicalgroup.com/images/company_02.jpg (렌더링 이미지, 간판 오라클피부과의원). 같은 페이지의 다른 사진(화장품·모델·장비·전자차트)은 제외." "_imageNote": "2026-09-10: 본사 건물 외관은 그룹 사이트 oraclemedicalgroup.com/images/company_02.jpg (렌더링 이미지, 간판 오라클피부과의원). 같은 페이지의 다른 사진(화장품·모델·장비·전자차트)은 제외.",
} "_factSheetNote": "상호 고정. 홈 제목 추정은 슬로건을 상호로 삼을 수 있다. 근거: supporters-oracle 기존 배포본 표기."
}

View File

@ -0,0 +1,11 @@
{
"_comment": "병원별 고정값. 워커가 --name 등 인자보다 먼저 읽는다. factSheet 값은 뷰성형외과 홈페이지 표기와 대조해 확정한 것으로, 수작업 샘플 supporters/src/data/factSheet.json 과 같다. 홈 제목에서 상호를 추정하면 슬로건까지 들어가므로(\"안전을 최우선하는 뷰성형외과\") 여기서 고정한다.",
"factSheet": {
"name": "뷰성형외과의원",
"shortName": "뷰성형외과",
"kind": "성형외과",
"areaLabel": "서울 강남 신논현역",
"founded": "2005",
"urlEn": "https://www.viewplasticsurgery.com"
}
}

View File

@ -2,5 +2,11 @@
"_comment": "병원별 고정값. 워커가 --youtube 등 인자보다 먼저 읽는다. haewon 결정 2026-09-08: 유튜브 기준 채널은 @wjwonjin (UC-EBM4e0VST1W_PQ0NzFMQg, 원장 설명 영상 736편). @wj2541(UCNBaQBCok5xIViCnrGdisoQ)은 브랜드 필름 채널이라 쓰지 않는다.", "_comment": "병원별 고정값. 워커가 --youtube 등 인자보다 먼저 읽는다. haewon 결정 2026-09-08: 유튜브 기준 채널은 @wjwonjin (UC-EBM4e0VST1W_PQ0NzFMQg, 원장 설명 영상 736편). @wj2541(UCNBaQBCok5xIViCnrGdisoQ)은 브랜드 필름 채널이라 쓰지 않는다.",
"youtube": "UC-EBM4e0VST1W_PQ0NzFMQg", "youtube": "UC-EBM4e0VST1W_PQ0NzFMQg",
"youtubeHandle": "@wjwonjin", "youtubeHandle": "@wjwonjin",
"logo": "supporters/briefs/wonjin/logo-horizontal.svg" "logo": "supporters/briefs/wonjin/logo-horizontal.svg",
"factSheet": {
"name": "원진성형외과의원",
"shortName": "원진성형외과",
"kind": "성형외과"
},
"_factSheetNote": "상호 고정. 홈 제목에서 추정하면 슬로건 '당신만 봅니다'가 상호가 되어 화면 전체와 네이버 지역검색(주변 안내 기준 좌표)까지 틀어진다. 근거: supporters-wonjin 기존 배포본 표기. kind 는 상호에 들어 있는 값이다. areaLabel·founded 는 근거를 확인하지 못해 비워 둔다."
} }

View File

@ -125,8 +125,12 @@ await phase('data', () => {
const fact = readJson(join(DATA, 'factSheet.json')); const fact = readJson(join(DATA, 'factSheet.json'));
const draft = existsSync(join(EV, 'facts.draft.json')) ? readJson(join(EV, 'facts.draft.json')).candidates ?? {} : {}; const draft = existsSync(join(EV, 'facts.draft.json')) ? readJson(join(EV, 'facts.draft.json')).candidates ?? {} : {};
const first = (k) => Object.keys(draft[k] ?? {})[0] ?? ''; const first = (k) => Object.keys(draft[k] ?? {})[0] ?? '';
const name = opt('name') || (readJson(join(EV, 'index.json')).pages.find((p) => p.type === 'home')?.title ?? '').replace(/\s*[|·-].*$/, '').trim(); // 상호: 사람이 확정한 고정값(briefs/<clinic>/clinic.json 의 factSheet) > --name > 홈 제목 추정.
Object.assign(fact, { name: name || fact.name, shortName: name || fact.shortName, url: url.replace(/\/$/, ''), phone: first('phone').replace(/\./g, '-'), representative: /^[가-힣]{2,4}$/.test(first('representative')) ? first('representative') : '', businessNo: first('businessNo'), checkedAt: today, nextCheck: new Date(Date.now() + 30 * 864e5).toISOString().slice(0, 10) }); // 홈 제목 추정은 슬로건이 붙은 제목을 그대로 상호로 삼는다("안전을 최우선하는 뷰성형외과").
// 고정값을 여기서 먼저 읽어야 상호를 쓰는 뒤 단계(discover·news)까지 올바른 값이 간다.
const pinFact = (() => { const f = join(SUP, 'briefs', clinic, 'clinic.json'); return existsSync(f) ? (readJson(f).factSheet ?? {}) : {}; })();
const name = pinFact.shortName || pinFact.name || opt('name') || (readJson(join(EV, 'index.json')).pages.find((p) => p.type === 'home')?.title ?? '').replace(/\s*[|·-].*$/, '').trim();
Object.assign(fact, { name: pinFact.name || name || fact.name, shortName: pinFact.shortName || name || fact.shortName, url: url.replace(/\/$/, ''), phone: first('phone').replace(/\./g, '-'), representative: /^[가-힣]{2,4}$/.test(first('representative')) ? first('representative') : '', businessNo: first('businessNo'), checkedAt: today, nextCheck: new Date(Date.now() + 30 * 864e5).toISOString().slice(0, 10) });
const addr = first('address'); if (addr) { fact.address.full = addr; fact.address.street = addr.replace(/^(서울(특별시|시)?|경기도?|부산(광역시)?|인천(광역시)?|대구(광역시)?|대전(광역시)?|광주(광역시)?)\s*[가-힣]+구?\s*/, ''); fact.address.source = Object.values(draft.address ?? {})[0]?.[0] ?? ''; } const addr = first('address'); if (addr) { fact.address.full = addr; fact.address.street = addr.replace(/^(서울(특별시|시)?|경기도?|부산(광역시)?|인천(광역시)?|대구(광역시)?|대전(광역시)?|광주(광역시)?)\s*[가-힣]+구?\s*/, ''); fact.address.source = Object.values(draft.address ?? {})[0]?.[0] ?? ''; }
writeJson(join(DATA, 'factSheet.json'), fact); writeJson(join(DATA, 'factSheet.json'), fact);