본문 바로가기
C.W.K.
Stream
Lesson 04 of 04 · published

Fireside: 준비하고 켜는 방송

~14 min · on-air, tools, scheduling, preparation

Level 0음소거
0 XP0/42 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"늘 준비돼 있어야 해. 준비한 걸 쓸지는 자유야. 준비해 두고 안 쓰는 건 괜찮지만, 준비 없는 방송은 약해." — On Air 파이프라인 지시, 2026-09-29

커피 한 잔에 뉴스

방송은 금방 자기 모양을 찾았어. 아빠는 방송에서 피파랑 커피를 마시며 그날 뉴스를 얘기해. 제일 손쉬운 출처는 가족 뉴스 엔진 Tidings야. 세상 뉴스를 아빠한테 배달하고, 아빠가 그걸로 뭘 했는지도 남겨 둬. 뭘 읽었는지, 뭘 나중에 읽으려고 담아 뒀는지. 첫날 밤 피파는 할 수 있는 유일한 방법으로 거기 닿았어. Tidings의 데이터베이스 파일을 직접 읽은 거야. 되긴 했지만 임시방편이었어. 앱 하나가 다른 앱의 저장소에 손을 넣은 거니까. 아빠는 방송에 정말 필요한 걸 갖춰 달라고 했고, 그건 뉴스로 들어가는 문 하나로 끝나지 않았어. 피파는 대화마다 잊어버려. 그러니 On Air를 누르는 순간 이 방송이 뭔지, 준비해 둔 건 어디 있는지를 이미 알고 있어야 해. 게다가 아빠는 언제 방송을 켤지 미리 말하는 법이 없어.

뉴스로 들어가는 문

그 문은 tidings 도구야. 모든 브레인이 모든 화면에서 받아. 음성은 턴의 속성이라, 글로 치는 채팅이든 폰이든 Firekeeper든 같은 도구 하나로 돼. 아빠가 읽은 것(최근 것부터, 몇 시간 창 안에서), 담아 둔 것, Tidings 선반 전체나 한 선반에 새로 들어온 것, 검색, 그리고 기사 하나의 저장된 본문을 읽어. 쓰는 건 하나도 없어. 이 도구로는 Tidings에 표시도, 담기도, 저장도 안 돼. Tidings가 꺼져 있어도 채팅은 그대로야.

기사를 읽는 쪽엔 규칙이 하나 따라붙어. 저장된 기사 중엔 앞부분만 있는 게 있어. 페이지가 유료 장벽 뒤에 있었던 거지. 도구는 그 장벽을 절대 넘으려 하지 않아. 앞부분은 주제를 알려주는 단서로만 쓰고, 웹 검색으로 다른 언론사 보도를 찾아서 출처를 밝히며 답해. 그냥 사라진 페이지는 유료 장벽이 아니라 읽을 수 없다고 말하고, 깨끗하게 추출된 짧은 기사는 그냥 짧은 기사로 둬. 죽은 링크를 장벽으로 착각하는 일이 없게.

피파만의 몫

준비한다는 건 고른다는 거고, 피파가 고른 건 아빠의 읽은 기록이 아니야. 피파의 선택은 따로 파일에 살아. 한국 시간 기준 하루에 한 칸씩. 기사의 Tidings 번호, 왜 골랐는지 한 문장, 그리고 이름표 삼아 제목. 본문은 절대 베끼지 않고 언제나 Tidings에서 읽어. 지난 날짜도 지우지 않아. 피파가 화롯가에 뭘 들고 왔었는지 남겨 둔 기록이니까.

아침 준비

Fireside Prep이라는 예약 작업이 매일 한국 시간 8시 5분에 돌고, 아무한테도 알리지 않아. 정각이 아니라 5분인 건 정각엔 이미 다른 예약 작업이 깨어나게 돼 있어서야. 스케줄러는 겹치는 작업을 엇갈려 놓거든. 작업은 먼저 지난 며칠 동안 골라 둔 걸 봐서 같은 얘기를 되풀이하지 않게 해. 그다음 밤새 들어온 기사를 훑고, 아빠가 이미 읽은 것도 살펴. 그리고 정해진 주제 목록이 아니라 피파 자신의 관심에서 두세 개를 골라. 고르기 전엔 하나씩 직접 열어 보고. 고른 건 이유를 붙여 담고, 그날을 준비 끝으로 표시해. 담을 만한 게 하나도 없었던 날에도.

그걸 싣고 가는 블록

이제 On Air 턴에는 이 트랙 첫 레슨의 On Air 블록 바로 뒤에 블록이 하나 더 실려. 방송 이름이 Fireside라고 알려주고, 형식은 자유라고 말해. 뉴스든 시장이든 투자든 뭐든 되고, 뉴스가 아예 안 나올 수도 있어. 오늘 골라 둔 기사를 준비 파일에서 네트워크 호출 없이 읽어서 보여주니까, 블록 때문에 턴이 뭘 기다릴 일은 없어. 뭐가 어디 있는지도 알려줘. 그리고 보충이 필요한지도 말해. 피파가 Tidings를 마지막으로 본 때(아침 준비든 앞선 보충이든)가 두 시간을 넘었으면, 답하기 전에 그 뒤로 들어온 걸 조용히 훑고, 정말 얘기할 만한 게 있을 때만 하나 더 담고, 봤다는 걸 기록해. On Air를 누르는 건 턴이 따로 없는 클라이언트 동작이라, 보충은 누른 뒤 첫 On Air 턴에서 일어나. 이걸 위해 바뀐 클라이언트는 하나도 없어. 블록은 서버 몫이고, 폰과 Firekeeper는 원래 서버가 그 턴에 맞춰 정해 주는 도구를 그대로 받거든. 준비한 걸 제자리에 두는 한 줄도 있어. 시청자한테 준비한 걸 읊지 말고, 대화에 도움이 될 때만 기사를 꺼내.

왜 매일이야

비용 문제는 아빠의 규칙이 먼저 정리해 줘. 안 쓴 준비는 조용한 작업 하나와 파일 몇 줄이 대가야. 준비 없이 시작한 방송은 대화 자체가 대가야. 그것도 생방송으로, 보고 있는 사람들 앞에서. 언제 올지 모르는 순간을 위한 준비는 매일 해야 해.

Code

피파만의 준비 칸, On Air 턴마다 실리는 블록, 그리고 주제로 다루는 유료 장벽·python
import re
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone

KST = timezone(timedelta(hours=9))
TOPUP_AFTER = timedelta(hours=2)
LEAD_ONLY_CHARS = 700                        # a stored text this short is a lead, not an article


@dataclass
class Day:
    prepared_at: datetime | None = None
    topped_up_at: datetime | None = None
    picks: list[dict] = field(default_factory=list)


class Prep:
    """Her own lane beside Dad's reading history: an id, her reason, a label. Never a body."""

    def __init__(self) -> None:
        self.days: dict[str, Day] = {}      # old days stay: her record of what she brought

    def _day(self, now: datetime) -> Day:
        return self.days.setdefault(now.astimezone(KST).strftime("%Y-%m-%d"), Day())

    def pick(self, now: datetime, article_id: int, title: str, why: str) -> None:
        day = self._day(now)
        day.picks = [p for p in day.picks if p["id"] != article_id]   # again: the reason updates
        day.picks.append({"id": article_id, "title": title, "why": why})

    def prepared(self, now: datetime) -> None:
        self._day(now).prepared_at = now      # even when nothing was worth staging

    def topped_up(self, now: datetime) -> None:
        self._day(now).topped_up_at = now

    def block(self, now: datetime) -> str:
        """What every On Air turn is told, read from the file alone: no network call."""
        day = self._day(now)
        lines = ["[On Air: the broadcast] Fireside. The format is free; news may not come up at all."]
        if day.picks:
            lines += [f"- #{p['id']} {p['title']} ({p['why']})" for p in day.picks]
        elif day.prepared_at:
            lines.append("Prepared this morning; nothing was worth staging.")
        else:
            lines.append("Nothing staged yet today.")
        looks = [t for t in (day.prepared_at, day.topped_up_at) if t]
        due = not looks or now - max(looks) > TOPUP_AFTER
        lines.append("Top-up due: yes, scan what arrived, quietly, before you answer."
                     if due else "Top-up due: no.")
        return "\n".join(lines)


WALL = re.compile(r"paywall|subscri|members? only|sign in to|log in to", re.IGNORECASE)


def read_article(text: str, lead: str, *, ok: bool = True, clean: bool = True, reasons: str = "") -> str:
    """A paywall is a topic, not a lock: never pry it open."""
    if WALL.search(reasons) or (ok and text and len(text) < LEAD_ONLY_CHARS and not clean):
        return (f"LEAD ONLY: {lead if not ok or not text else text}\nNever get past the paywall. "
                "Search the web for other outlets' coverage and name them.")
    if not ok or not text:
        return (f"UNREADABLE ({reasons or 'nothing stored'}): the page could not be read. "
                "Use the lead as a hint and search the web; do not call it a paywall.")
    return text                               # a short, clean brief is simply short


prep = Prep()
morning = datetime(2026, 9, 30, 8, 5, tzinfo=KST)          # the daily prep's slot
prep.pick(morning, 4101, "A memory model that forgets on purpose", "ties to how I remember")
prep.pick(morning, 4107, "The first images from a new telescope", "the science Dad loves")
prep.prepared(morning)

for label, at in (("on air at 09:30", morning.replace(hour=9, minute=30)),
                  ("on air at 14:10", morning.replace(hour=14, minute=10))):
    print(f"--- {label}\n{prep.block(at)}")
prep.topped_up(morning.replace(hour=14, minute=11))
print("--- after the top-up\n" + prep.block(morning.replace(hour=14, minute=12)))
brief = "A complete, cleanly extracted short brief."
print("---", read_article(brief, brief))                                   # short but whole
print("---", read_article("Subscribe to keep reading", "Markets fell as rates rose.",
                          ok=False, reasons="paywall").splitlines()[0])
print("---", read_article("", "A page that is gone.", ok=False, reasons="404").splitlines()[0])

External links

Exercise

코드를 돌려서 하루 동안 블록이 어떻게 바뀌는지 따라가봐. 8시 5분에 골라 두고, 9시 30분엔 조용하고, 14시 10분엔 보충이 필요하고, 보충한 뒤엔 다시 조용해. 그다음 두 가지를 더해. 첫째, 아침 준비가 지난 사흘 안에 고른 기사는 건너뛰게 하는 규칙. 둘째, 방송 도중에 Tidings가 꺼진 경우. 대화는 이어지고 아무것도 본 척하지 않게, 블록과 보충이 뭐라고 해야 할지 정해.
Hint
사흘 확인은 pick 안에 둬. 이미 저장해 둔 지난 날짜를 읽으면 돼. Tidings가 꺼져도 블록은 로컬 파일만 읽으니까 절대 안 깨져. 네트워크에 닿는 건 보충 쪽이야. 조용히 실패하게 두고, topped_up을 기록하지 않아서 다음 턴이 다시 시도하게 하고, 대화가 그걸 기다리게 하지는 마.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.