Skip to main content
← 블로그

count랑 language만 보내자 — Public Library를 매일 새벽 AI가 채우게 만들기

VauDium ·

Fecit Public Library가 텅 빈 느낌이 들면 안 된다는 문제에서 시작했어요. admin이 토픽 리스트를 직접 보내던 batch endpoint에서, count·language만 받고 나머지는 AI가 결정하는 endpoint를 거쳐, 매일 새벽 03:00에 자동으로 굴러가는 잡까지. 인터페이스를 좁히는 만큼 AI에게 맡길 수 있는 게 늘어났습니다.

count랑 language만 보내자

Public Library에 콘텐츠가 새로 올라오지 않으면 — 살아 있다는 느낌 자체가 사라져요.

기존엔 admin이 콘솔에서 topics: [...] 리스트를 직접 보내는 batch endpoint(/generate/using/ai/batch)가 있었어요. 매번 좋은 주제 다섯 개를 떠올려서 입력해야 했고, 그러다 보니 결국 안 하게 됐어요.

“Public Library에 Overview를 넣는 기능을 만들 수 있을까? 지금도 있긴 한데 그 과정을 자동화 하고 싶단 말이지.”

자동화하려고 보니, 단계가 단순한 게 아니었어요.

첫 번째 결정 — 어디까지 AI에게 맡길까

처음 떠오른 모양은 이거였어요.

“관리자가 추천 토픽 풀을 등록해두면, 스케줄러가 주기적으로 N개씩 소비해가며 Overview를 생성.”

토픽 풀은 누가 채워? 결국 사람이.

다음 모양:

“토픽 선정까지 AI가 자동.”

이게 진짜 자동화였어요. 인터페이스에 topic이 남아 있는 한, 어딘가에선 사람이 그걸 채워야 했거든요. 인터페이스를 좁히면, 책임이 AI에게 넘어가요.

새 ticket은 결국 이렇게 됐어요:

class GenerateOverviewsAutoTicket(BaseModel):
    count: int
    language: Optional[str] = None

countlanguage만. category, point_color, 그리고 topic 자체 — 전부 AI가 결정합니다.

두 번째 결정 — 누가 호출하지

이제 자동 endpoint는 있는데, 매일 정해진 시각에 누가 누르나?

세 가지 옵션:

  1. 시스템 cron + 스크립트
  2. API 서버 안에 APScheduler 끼워넣기
  3. 이미 있는 fecit/2/job 서버에 등록

3번을 선택했어요. APScheduler 기반 잡 서버가 이미 한 군데서 돌고 있는데, 굳이 두 군데로 쪼갤 이유가 없어요. 잡 추가는 등록 한 줄:

job_server.register_cron(
    "auto_generate_public_overviews",
    auto_generate_public_overviews,
    hour=3, minute=0, second=0,
)

매일 KST 03:00. 잠든 사이에 라이브러리가 채워져요.

세 번째 결정 — 인증

기존 auto endpoint는 @authorize_achiever() + @authorize_service_admin로 막혀 있었어요. 즉, admin 한 명의 로그인 bearer token이 필요했죠. 잡 서버가 그걸 들고 다니게 하긴 어색해요.

다른 잡들이 이미 쓰고 있던 패턴이 답이었어요 — x-api-key 헤더.

@router.post("/generate/using/ai/auto", status_code=204)
@authorize_api_key(DEFAULT_API_KEY)
async def generate_overviews_auto(...):
    achiever = await load_auto_overview_owner()
    ...

load_auto_overview_owner()는 owner로 사용할 achiever id를 하드코딩해서 로드합니다. 호출자가 “누구인지”를 알려주지 않아도 되도록 — owner는 시스템 차원에서 고정.

토픽까지 맡긴 prompt가 흥미로웠어요

토픽 suggester의 핵심은 이거였어요.

excludeTitles: [최근 동일 언어 제목 200개]

“이미 있는 거랑 의미상 안 겹치게” — 라는 단순한 룰만 줘도, AI가 알아서 새로운 주제를 골라요. 같은 활동을 다른 표현으로 적는 paraphrase도 금지한다고 적어두면 더 다양해져요.

그리고 한 가지 더 — 만들어지는 콘텐츠를 보다가 발견한 패턴:

“X분 뭐하기” 같은 형태로 제목인데, X분은 뺄 수 없어?

duration 필드가 따로 있는데 제목에도 시간이 들어가니 중복이고, 패턴도 식상해요. prompt에 한 줄 추가:

topic에 “X분/X시간” 같은 시간 표현 금지 (시간은 duration 필드).

다음 실행부터는 “이메일 정리 및 우선순위 지정 루틴” 같은 깔끔한 제목으로 나왔어요.

의미 체인이 또렷해야 좋은 콘텐츠가 나와요

품질을 더 올리려고 출력 스키마를 늘렸어요 — target, obstacle, expectation, duration, difficulty, materials, tools. 처음 prompt에서는 의미를 살짝 잘못 잡았었어요.

target은 변화의 대상인데? 현재라고 표현되고 있어

그래서 의미 체인을 정리했어요.

  • target = 변화 전의 현재(미해결) 상태
  • obstacle = target → expectation 사이에서 마주치는 난관
  • expectation = target이 도달했으면 하는 상태

세 필드의 관계가 또렷해지니까, AI가 채우는 내용도 자연스럽게 정렬됐어요. “target = 평가 기준” 같은 잘못된 지시가 prompt에 들어가 있으면, AI는 시키는 대로 잘못된 컨텐츠를 만듭니다.

인터페이스를 좁히는 만큼

전체 흐름을 정리하면:

  1. 잡 서버가 KST 03:00에 auto_generate_public_overviews 발화
  2. job → API의 /generate/using/ai/auto{count: 5, language: "en"}, 그다음 {count: 5, language: "ko"} 호출 (x-api-key 헤더)
  3. API는 owner achiever 로드 → 최근 동일 언어 제목 200개를 exclude로 → AI suggester가 [{topic, category, point_color}] 반환
  4. 각 항목에 대해 template + Overview 생성, Visibility.PUBLIC로 Public Library에 등록

호출하는 쪽이 알아야 할 건 몇 개를 어떤 언어로 뿐. 그 외는 전부 시스템 안에서 처리돼요.

좁은 인터페이스가 곧 자동화의 표면. 사람이 채워야 하는 인풋이 사라질수록, 잠든 사이에도 시스템이 자기 일을 합니다.