소셜 하나를 옮길 때 무엇을 순서대로 하고 무엇을 물어야 하는지를 담았습니다. 이번 소셜의 실제 결정은 착수 문서가 따로 갖습니다.
이 절이 답하는 것 — 언제 열고, 무엇을 하면 끝나는가.
소셜 하나를 착수하기 직전에 엽니다. X 를 옮기면서 실제로 밟은 순서를 그대로 적어 두었고, 그때 대화로만 나왔던 판단들을 §4 로 끌어냈습니다.
이 페이지는 바뀌지 않습니다. 소셜마다 바뀌는 것 — 그 소셜에서 실제로 무엇을 결정해야 하는지 — 은 별도 착수 문서가 갖습니다.
고르면 답이 문장으로 조립됩니다. 그대로 돌려주시면 §2 의 단계 2 부터 진행됩니다.
다음 소셜은 같은 자리에 {social}-kickoff.html 로 만듭니다.
guides/social-generator-rubric.md(R-1 ~ R-18)는 무엇을 지켜야 하나입니다. 구속력은 거기에만 있습니다.
이 페이지는 어떤 순서로 하고 무엇을 물어야 하나입니다. 규칙을 다시 설명하지 않고, 어느 단계에서 어느 규칙을 훑는지만 가리킵니다.
이 절이 답하는 것 — 순서가 어떻게 되고, 사람 승인이 걸리는 지점이 어디인가.
| 단계 | 산출물 | 그 단계에서 훑는 규칙 |
|---|---|---|
| 0 | 없음 — 읽기만 | §3 참조. 여기를 건너뛰면 이미 있는 답을 다시 구한다 |
| 1 | {social}-kickoff.html결정 카드 + URL 매핑표 + 필드 배치표 | §4 의 결정 축을 그 소셜에 대입해 답해야 할 것만 남긴다. 표 둘의 규격은 §4 끝 |
| 2 | docs/features/social-generator/{social}.md | 결정 로그 · URL 종류별 산출 · 필드 매핑 · 확인 항목 |
| 3 | 스키마 · 타입 · classify 변경 | §5 체크포인트. 빠뜨리면 mongoose 가 조용히 버린다 |
| 4 | 생성기 + 단위 테스트 | R-1 ~ R-18 전부 |
| 5 | 점검 결과 | 루브릭 체크리스트 23항목 + 실물 Writer 통과 확인 |
자기 자신을 인용하는 트윗에 루프가 끝나지 않아 66초 만에 힙이 터졌습니다. 단위 테스트 23개는 전부 통과하고 있었습니다 — 정상 입력만 넣었기 때문입니다.
그래서 단계 5 는 "테스트가 초록인가" 가 아니라 적대적 입력을 직접 넣어 보는 것입니다. 순환·빈 문자열·한쪽만 있는 응답이 그 축입니다.
이 절이 답하는 것 — 무엇이 어느 파일에 이미 있고, 건너뛰면 무슨 손해가 나는가.
| 어디 | 무엇이 있나 | X 에서 실제로 |
|---|---|---|
| scaffold/{social}.json → liveTests | 유료 실호출 원문. 요청 URL·응답 본문·그때 확정한 것 | 🔴 안 보고 실호출을 다시 요청했다. 중첩 author 가 33키라는 것도, 정지 계정 응답 모양도 이미 거기 있었다 |
| scaffold/{social}.json → objectFields[] | 필드별 keep/skip/key 판정과 traderValue 등급 | skip 의 근거가 "응답에 존재하나 fetcher 미사용" 이라 가치 판단이 아님을 발견 → X-8 |
| scaffold/{social}.json → urlPatterns[] | URL 형태별 관측 건수 | trending 82건을 발견해 unknown 에서 건져냈다 |
| qna/{social}.md | 사람이 내린 판정과 그 이유 | trending 을 unknown 으로 두기로 한 판단의 근거 |
| sources/{social}.html | 위 JSON 을 사람이 읽는 형태로 렌더한 것. 필드마다 변동성·파싱 상태·최종 결정·트레이딩 유용성 네 칸이 한 줄에 보인다. 부록에 미채택 대안 소스의 raw 스키마가 있다 | 🔴 YouTube 에서 이 표에 없어서 안 봤다. 사람이 지적해 발견됐다. 부록에서 apify 전용 필드(channelBannerUrl·isChannelVerified)를 찾았고, 그것이 "Data API 로는 못 얻는다" 의 근거가 됐다 |
| {social}/{social}.types.ts | fetcher 가 이미 주는 것. 두 경로의 필드 수를 여기서 센다 | R-18 판정의 입력. YouTube 는 7 vs 2 가 여기서 나온다 |
audit 이 스스로 경고한 패턴입니다 — "기존 문서 덤프가 ... 로 절단돼 있어 놓치고 있었음(IG alt · TikTok isSlideshow 와 동일 패턴)".
X 에서 또 나왔습니다. 프로필 응답의 entities 가 {...} 로 잘려 있어서 그 한 필드 때문에 실호출 1건이 필요했습니다. 값이 ... 나 {...} 로 끝나면 그 자리는 "확인 안 됨" 으로 셉니다.
sources/{social}.html 은 scaffold/render.py 가 JSON 에서 만들어 내는 렌더 결과입니다. 그런데 자동 재렌더가 없어서, JSON 을 고쳐도 HTML 은 예전 그대로 남습니다.
YouTube 에서 실제로 갈렸습니다. 이미지 자산 작업이 JSON 의 썸네일 판정을 remove 에서 "재검토 · top-level 대상" 으로 바꾸고 영상 썸네일 행을 새로 넣었는데, HTML 은 8일 전 상태라 그 행이 아예 없었습니다. HTML 만 봤다면 이미지 관련 결정을 통째로 못 봤을 것입니다.
그래서 판정과 필드 목록은 JSON 에서 읽고, HTML 은 한눈에 훑는 용도와 부록(미채택 소스 raw 스키마) 때문에 엽니다. 부록은 JSON 에도 있지만 HTML 쪽이 읽기 쉽습니다.
이 절이 답하는 것 — 소셜이 바뀌어도 똑같이 물어야 하는 것들.
단계 1 에서 이 일곱 줄을 그 소셜에 대입해 착수 문서의 결정 목록을 만듭니다. 이미 답이 정해지는 축은 빼고, 사람이 골라야 하는 것만 남깁니다.
| 결정 축 | 무엇을 묻나 | X 는 이렇게 답했다 |
|---|---|---|
| fan-out 범위 | 한 URL 이 객체 몇 개가 되나 | 인용 사슬을 깊이 2까지 편다. 묶음 1~3개, 추가 호출 0건 (X-1) |
| 딸려오는 객체의 채워짐 R-18 | 같은 객체를 만드는 두 경로가 같은 필드를 내나 | 공짜로 통과했다 — 트윗의 author 가 33키 완전 프로필이었다. 다른 소셜은 대개 통과 못 한다 |
| 비객체 URL | 객체가 아닌 주소를 어떻게 끝내나 | 검색·intent·trending 을 호출 0회로 행 1건씩 만든다 (X-10~12) |
| 비용·게이트 | 유료면 누가 판정하나 | 커뮤니티가 20 credits 라 opt-in. 생성기가 스스로 판정한다 (X-4) |
| 실패 어휘 | 새 FetchStatus 가 필요한가 | SUSPENDED 신설. 정지와 미존재는 재시도 정책이 갈린다 (X-3) |
data 키 | 최상위가 아닌 값을 뭘 담나 | 스레드 3종. 소셜 원래 이름 그대로 (X-6 · G-7) |
| 보조 호출 | 실패를 흡수해도 되나 | trending 제목만 흡수 — 실패해도 시각이 남기 때문 (R-15 예외) |
| 이미지 자산 2026-08-10 신설 | 어느 이미지를 어느 자리에 매핑하나 | profilePicture → avatarUrl, media → mediaUrls. 8소셜이 전부 이미지 필드를 갖는데 이름이 다 다릅니다 — avatar·iconImg·profilePicUrl·thumbnails·iconUrl. fetcher 는 원본 이름을 유지하고 통일은 여기서만 일어납니다 |
| 지표 그릇 2026-08-10 신설 | *MetricsInput 에 자리가 있나 | followers 는 있었습니다. following·contentCount 는 조회축이 아니라 data 로 내렸습니다 (X-13). 팔로워류가 소셜마다 fans·followersCount·subscriberCount 로 갈리는데 그릇은 followers 하나입니다 |
축마다 선택지를 나열하는 것으로는 부족합니다. X 에서 결정을 가른 것은 늘 숫자 하나였습니다 —
trending 82건, 커뮤니티 20 credits, quoted_tweet 의 33키, 관측 0건.
그래서 착수 문서의 선택지에는 그 숫자를 함께 적습니다. 없으면 단계 0 으로 돌아가 찾습니다.
결정 카드만으로는 부족합니다. 카드는 사람이 이미 문제라고 알아챈 것만 담기 때문입니다. 그래서 착수 문서에는 카드와 함께 표 둘을 넣습니다. 순서가 있습니다 — 무엇이 만들어지는지가 정해져야 그 객체의 필드가 어디로 가는지를 정할 수 있습니다.
그 소셜로 들어올 수 있는 URL 형태를 전부 늘어놓고, classify() 가 어떤
sourceType 으로 가르는지, 그래서 어떤 객체가 만들어지는지까지 잇습니다.
칸은 URL 형태 · 관측 건수 · sourceType · sourceKey · 무엇이 되나 · 걸린 결정 여섯입니다.
결정을 가르는 것이 늘 그 숫자였습니다. YouTube 에서 playlist 를 0건 으로 확인하고
권장안이 뒤집혔습니다 — 0건짜리를 위해 ContentSubtype 어휘를 늘릴 이유가 없기 때문입니다.
X 에서 trending 을 살린 근거도 같은 칸의 82건 이었습니다.
확정된 뒤에는 이 표에 호출 수 · 묶음 개수 · rootRef · status 가 붙어
{social}.md 의 "URL 종류별 산출" 절이 됩니다(x.md §4 가 그 형태입니다).
착수 시점에는 아직 결정이 안 나서 그 네 칸을 채울 수 없으므로, 여기서는 여섯 칸까지만 만듭니다.
fetcher 가 주는 값을 하나도 빠짐없이 늘어놓고, 각각이 스키마의 어느 자리로 가는지 적습니다. 객체마다 표 하나씩 만들고, 각 표를 세 구역으로 나눕니다.
| 구역 | 무엇이 들어가나 | 판정 기준 |
|---|---|---|
| top-level | platformKey·displayName·bio·outboundUrls·avatarUrl 같은 스키마 본체 필드 | 소셜이 바뀌어도 같은 뜻인 값. 자리 이름이 이미 정해져 있습니다 |
| metrics | *MetricsInput 에 자리가 있는 수치 | 독립적인 조회·정렬·비교 축인가. 숫자라서가 아닙니다 — following 은 숫자인데 data 입니다 |
| data | 그 소셜에서만 뜻이 통하는 부가값 | 키 이름은 소셜 원래 필드명 그대로 (G-7) |
YouTube 에서 배치표를 짜자마자 결정 카드에 없던 문제 넷이 한꺼번에 드러났습니다.
subscriberCount 가 들어갈 이름이 미정이고, 채널 누적 viewCount 는 갈 자리가 아예 없고,
handle 은 영구히 비고(핸들을 주는 필드가 audit 에서 remove 됐습니다),
아바타는 audit 판정이 아직 remove 인 채였습니다.
넷 다 "빠진 자리" 라서 카드를 쓰는 단계에서는 안 보입니다. 빈칸은 늘어놓아야 보입니다.
배치표에서 "자리 없음" 이 나온 행은 그대로 선행 작업이 됩니다 — 입력 계약에 필드를 더하거나, 그 값을 버리기로 정하거나 둘 중 하나여야 하고, 정하지 않고 넘어가면 mongoose 가 조용히 버립니다.
이 절이 답하는 것 — 스키마에 없는 것을 더할 때 함께 고쳐야 하는 자리.
한 곳만 고치면 조용히 틀립니다. X 에서 AccountDataInput 을 빠뜨렸다가 나중에 발견했습니다. 입력 계약에 필드가 없으면 mongoose strict 가 값을 조용히 버립니다 — 예외도 로그도 없습니다.
무엇을 고쳐야 하는지는 §4 의 배치표가 이미 말해 줍니다. 표에서 "자리 없음" 으로 남은 행이 전부 여기의 입력입니다 — 새로 만들 필드가 없으면 이 절은 통째로 건너뜁니다.
social-graph.types.ts 의 *Input / *DataInput / *MetricsInput여기가 없으면 타입 에러가 나서 그나마 시끄럽다objects/*.model.ts 의 @prop 선언여기가 없으면 mongoose 가 조용히 버린다static of()R-10. 스프레드 금지, 필드 단위 명시 매핑{social}.types.ts값이 응답에 있는데 타입이 버리고 있지 않은지ContentSubtype / VenueSubtype — social-graph.consts.tstoken_links 의 허용값은 enum 을 펼쳐 쓰므로 자동으로 따라온다FetchStatus 에 값을 더했다면 GenerateResult 의 기존 갈래에 편입R-13. 갈래를 새로 만들면 attempted 조합을 컴파일러가 못 막는다{Social}SourceType 과 그 소셜 classify 의 분기R-16. 키를 못 뽑으면 'unknown' 으로SocialGeneratorRouter 에 등록 — 배열과 inject 둘 다하나만 고치면 런타임에 undefined 가 들어가 그 소셜이 통째로 미해석이 된다router-cases.jsonX 는 7건이 깨졌고 전부 의도한 변경이었다SocialGraphWriter 에 산출을 통과시켜 본다R-1·R-3·R-4·R-5 는 Writer 가 런타임에 던지는 규칙이다guides/social-generator-rubric.md(R-*), 근거는
docs/features/social-generator/decisions.md(G-*), 소셜별 결정은
docs/features/social-generator/{social}.md 입니다.
이 페이지는 소셜이 바뀌어도 그대로입니다. 이번 소셜의 결정은 docs/features/social-generator/{social}-kickoff.html 가 갖고, 확정되면 {social}.md 로 옮겨 적습니다.