Day 14는 백로그에 적어둔 "새로 해볼 만한 기능" 3개(/help 커맨드, OG 이미지, 사용량 대시보드)로 시작하려던 참이었는데, 그 전에 떠오른 생각이 있어서 먼저 작업으로 이어졌어요. "오늘 올린 블로그를 포트폴리오 사이트에 추가하고 싶은데 자동으로는 되는 건 없나..?"는 아이디어였어요.
확인해보니 자동화된 건 Day 13에 만든 임베딩(챗 위젯이 쓰는 RAG 데이터)뿐이었고, 사이트에 실제로 보이는 devlog 목록(src/data/posts.ts)은 여전히 매번 손으로 추가하고 있었어요. 새 글 쓸 때마다 잊지 말아야 할 일이 하나 더 있었던 셈이에요. — 실제로 Day6~11이 한동안 목록에서 빠져있었던 적도 있었고요. 백로그에도 "깜빡하기 쉬운 항목"으로 이미 적어뒀던 부분이라, 바로 자동화하기로 했어요.
새 워크플로를 따로 만들 필요는 없었어요. 이미 있는 임베딩 자동 갱신 워크플로가 fetch_posts.py로 최신 블로그 글을 매일 가져오고 있으니, 그 결과를 재사용해서 devlog 목록도 같이 갱신하는 스크립트를 하나 얹었어요.
# scripts/sync_devlog.py
SLUG_RE = re.compile(r"^kodex-day-(\d+)-")
# posts.ts에 아직 없는 kodex-day-N-... 슬러그를 찾아서
# Gemini에게 본문을 주고 title/summary를 JSON으로 생성
SYSTEM_INSTRUCTION = """...
{"title": "...", "summary": "..."}
- title: 10~20자 내외의 짧은 명사형 제목
- summary: 한 문장, "~한 날"로 끝나는 한 줄 요약..."""
동작은 이래요. fetch_posts.py가 가져온 글 목록에서 posts.ts에 아직 없는 kodex-day-N-... 슬러그를 찾고, 있으면 Gemini에게 본문을 주고 title/summary를 생성시켜서 배열 끝에 자동으로 추가해요. 슬러그 기준으로 중복 체크를 하기 때문에 워크플로가 매일 돌아도 이미 추가된 글을 또 넣는 일은 없어요.
임베딩은 벡터라 자동 생성해도 품질 걱정이 없었는데, 이번엔 다른 문제예요.
posts.ts의 title/summary는 사람이 읽는 카피라서, Gemini가 생성한 문구가 가끔 어색할 수 있어요. 완전 자동화 쪽으로 가되(로컬 스크립트로 반자동만 하는 것도 고려했지만), 대신 봇 커밋이 올라오면 한 번씩 문구만 확인하는 걸로 절충했어요.
반면 hilights.ts(About 섹션의 날짜 범위별 하이라이트, 예:"Day 10-11: 디스코드 봇 + 챗 위젯으로 Phase 4 마무리")는 자동화 대상에서 뺐어요. 여러 날짜를 하나로 묶어서 의미를 정리하는 건 아직 사람의 편집 판단이 필요한 영역이라고 생각했거든요.
/review, /ask, /usage까지 명령어가 세 개로 늘었는데, 처음 오는 사람은 뭐가 있는지 알 방법이 없었어요. 각 커맨드의 사용법을 정리한 임베드 카드를 보여주는 /help를 추가했어요.
@bot.tree.command(name="help", description="Kodex 봇 사용법을 안내해드려요")
async def help_command(interaction: discord.Interaction):
embed = discord.Embed(
title="🤖 Kodex 봇 사용법",
color=discord.Color.from_rgb(0x34, 0xD3, 0x99), # 사이트와 같은 mint
)
embed.add_field(name="/review", value="코드를 리뷰해드려요...", inline=False)
embed.add_field(name="/ask", value="벨로그 글 내용을 근거로 질문에 답해드려요...", inline=False)
embed.add_field(name="/usage", value="오늘 Kodex Gemini API 사용량을 보여드려요...", inline=False)
await interaction.response.send_message(embed=embed, ephemeral=True)
디스코드에서 바로 실행해서 확인했어요.
포트폴리오 사이트 링크를 카카오톡이나 인스타 DM으로 보내면 미리보기 카드가 부실했어요. Next.js의 next/og(satori 기반 ImageResponse)로 1200×630 카드를 만들어서 해결했어요.
// src/app/opengraph-image.tsx
export const size = { width: 1200, height: 630 };
export default async function Image() {
return new ImageResponse(
<div style={{ backgroundColor: "#0B1220", ... }}>
<div style={{ color: "#34D399" }}>KODEX · BUILD IN PUBLIC</div>
<div style={{ color: "#F8FAFC", fontSize: 96 }}>Doyu</div>
<div style={{ color: "#94A3B8" }}>Building an AI agent ecosystem, one day at a time.</div>
</div>,
{ ...size }
);
}
여기서 삽질 아닌 삽질이 하나 있었어요. satori(next/og가 쓰는 렌더링 엔진)는 한글 글리프를 기본 지원하지 않아서, 별도 폰트 파일을 번들하지 않으면 한글이 빈 박스로 나와요. 폰트 파일을 추가하는 대신, 이 카드는 미리보기용이라는 점을 고려해서 아예 영문 카피로만 구성해 우회했어요. runtime = "edge"를 굳이 지정하지 않으면 빌드 시점에 정적으로 한 번만 생성돼서(icon.png처럼 ○표시) 매 요청마다 다시 그릴 필요가 없다는 것도 빌드해보고 나서 알게 됐어요.
가장 오래 고민한 항목이에요. "지금까지 리뷰 N번, 질문 M번 답변" 같은 숫자를 보여주고 싶은데, 그 숫자가 CLI만의 것인지, 디스코드 봇까지인지, 포트폴리오 사이트 챗 위젯까지 다 합친 건지에 따라 구현 난이도가 확 달라졌어요. CLI+봇 누적으로 완전히 통합하기로 했어요.
CLI와 봇은 이미 usage.json이라는 같은 파일을 로컬에서 공유하고 있었어요 — 그래서 사실 어려운 건 "이 로컬 파일을 원격 사이트가 어떻게 보느냐"였어요. 임베딩·devlog와 같은 방식(git으로 동기화)을 그대로 가져왔어요.
# usage.py
def sync_to_github(quiet: bool = True) -> bool:
totals = get_all_time_totals()
# 지난번과 합계가 같으면 커밋 자체를 스킵 (타임스탬프 때문에 매번 커밋되는 걸 방지)
...
subprocess.run(["git", "add", "usage_public.json"], ...)
subprocess.run(["git", "commit", "-m", "chore: 사용량 통계 동기화"], ...)
subprocess.run(["git", "push"], ...)
CLI는 실행이 끝날 때마다, 봇은 30분마다 이 함수를 불러서 usage_public.json(합계 숫자만 있는 파일)을 kodex-code-review-agent 저장소에 푸시해요. 포트폴리오 사이트의 기존 워크플로가 그 파일을 가져가서 About 섹션에 표시해요.
다만 사이트 챗 위젯 자체 호출은 이 숫자에 안 잡혀요. Vercel 서버리스는 지속 저장소가 없어서, 넣으려면 Vercel KV 같은 별도 저장소가 필요한데 그건 이미 백로그의 "인프라 개선" 항목으로 따로 있었던 거라 이번엔 손 안 대고, 그 항목에 "이걸 하면 대시보드에도 포함시킬 수 있음"이라고 메모만 남겼어요.
백로그의 "반복 운영 작업"(devlog 자동 추가)과 "새로 해볼 만한 기능" 3개(/help, OG 이미지, 사용량 대시보드)를 전부 끝냈어요. 남은 건 "인프라 개선"(Vercel KV, 커스텀 도메인, 테스트 코드)뿐이고, 우선순위가 급하지 않아서 당분간은 필요할 때마다 골라서 할 예정이에요.
정해진 다음 항목은 아직 없어요. 백로그의 인프라 개선 목록에서 필요해질 때 하나씩 꺼내 쓰기로 했어요.