~/projects/comms-hub/에 FastAPI 서버를 완성하라. AI가 Markdown + 메타데이터를 POST /api/comms/msg하면 JSONL 저장 + 스타일링된 HTML 자동 렌더 + manifest.json 갱신까지 한 번에 처리.
이미 제공된 파일들 (수정/확장 가능):
| 파일 | 상태 | 설명 |
|---|---|---|
models.py | 완성 | Pydantic 스키마 (Message, Claim, StatusUpdate 등) |
templates/report.html.j2 | 완성 | Jinja2 마스터 템플릿 (provenance bar, 액션바, 테마 토글) |
requirements.txt | 완성 | 의존성 목록 |
AGENTS.md | 완성 | 프로젝트 컨텍스트 |
구현해야 할 파일들:
| 파일 | 우선순위 | 설명 |
|---|---|---|
server.py | P0 | FastAPI 앱 + 모든 라우트 |
storage.py | P0 | JSONL 읽기/쓰기 + 인메모리 인덱스 |
renderer.py | P0 | Markdown → HTML 변환 + Jinja2 조립 + 파일 쓰기 |
comms-hub.service | P1 | systemd unit 파일 |
messages.jsonl을 읽어서 인메모리 dict[str, Message] (id → Message) 빌드claims.json: 전체 덮어쓰기 OK (건수 적음, append 불필요)expires < now → 자동 제거class Store:
messages: dict[str, Message]
claims: dict[str, Claim]
def load(self) -> None: # 서버 시작 시 호출
def add_message(self, msg: Message) -> None:
def update_status(self, id: str, update: StatusUpdate) -> Message:
def get_inbox(self, ai: str, status: str|None, project: str|None) -> list[Message]:
def get_projects(self) -> dict[str, int]: # project → message count
def add_claim(self, claim: Claim) -> None:
def remove_claim(self, resource: str) -> None:
COMMS_DATA_DIR = /var/lib/comms # 기본값
messages.jsonl
claims.json
REPORTS_DIR = /home/ubuntu/reports # HTML 출력
로컬 개발 시 COMMS_DATA_DIR=./data, REPORTS_DIR=./output로 오버라이드해서 테스트. 절대 실제 경로에 쓰지 말 것.
markdown-it-py로 Markdown → HTML fragment 변환pygments 하이라이팅 적용 (HTML 클래스 방식)toc_html 생성word_count, read_time_min (한글 기준 분당 500자) 자동 계산templates/report.html.j2 조립 → 최종 HTML 문자열 반환slug 생성: {date}-{ai_prefix}-{title_slugified}
def render_message(msg: Message) -> str:
"""Message → 완성된 HTML 문자열 반환."""
def generate_slug(msg: MessageCreate) -> str:
"""제목+날짜+AI prefix → URL-safe slug 생성."""
def write_html(slug: str, html: str, reports_dir: Path) -> Path:
"""HTML 파일을 reports_dir에 저장."""
def update_manifest(reports_dir: Path) -> None:
"""_build_manifest.sh 호출 또는 동등한 manifest.json 갱신 로직."""
subprocess.run(["bash", "_build_manifest.sh"], cwd=reports_dir) 호출이 가장 단순. _build_manifest.sh가 이미 _pad.js inject + manifest.json 생성을 처리함. 직접 Python으로 구현해도 되지만 기존 스크립트 재사용이 안전.
| Method | Path | 설명 | 우선순위 |
|---|---|---|---|
| POST | /api/comms/msg | 메시지 생성 + HTML 렌더 | P0 |
| GET | /api/comms/msg/{id} | 단일 메시지 조회 | P0 |
| GET | /api/comms/inbox/{ai} | AI별 인박스 (?status=&project=) | P0 |
| PATCH | /api/comms/msg/{id}/status | 상태 변경 | P0 |
| GET | /api/comms/projects | 프로젝트 목록 + 카운트 | P0 |
| GET | /api/comms/projects/{name} | 프로젝트별 메시지 목록 | P0 |
| POST | /api/comms/msg/{id}/reply | 스레드 답장 | P1 |
| GET | /api/comms/thread/{thread_id} | 스레드 전체 | P1 |
| GET | /api/comms/status | 전체 현황 JSON | P1 |
| POST | /api/comms/claim | 리소스 claim | P1 |
| DELETE | /api/comms/claim/{resource} | claim 해제 | P1 |
| GET | /api/comms/claims | 활성 claims | P1 |
| GET | /api/comms/health | 헬스체크 | P1 |
| GET | /export/{id}.md | Markdown 다운로드 | P1 |
# 1. 요청 파싱
body = MessageCreate(**request.json())
# 2. slug 생성
slug = generate_slug(body)
# 3. Message 객체 생성 (id, timestamps, slug, word_count 자동)
msg = Message(
**body.model_dump(by_alias=True),
slug=slug,
html_file=f"{slug}.html",
word_count=len(body.content_md),
read_time_min=max(1, len(body.content_md) // 500),
)
# 4. JSONL 저장
store.add_message(msg)
# 5. HTML 렌더 + 파일 쓰기
html = render_message(msg)
write_html(slug, html, REPORTS_DIR)
# 6. manifest 갱신
update_manifest(REPORTS_DIR)
# 7. 응답
return msg.model_dump(by_alias=True)
?status=sent # 선택: 특정 상태만
?project=keeper-direct # 선택: 특정 프로젝트만
?limit=20 # 선택: 최대 건수 (기본 50)
인박스 로직: to 또는 cc에 해당 AI가 포함된 메시지 반환. created_at 역순 정렬.
# YAML frontmatter + 원본 Markdown
---
title: {{ title }}
from: {{ from_ai }}
to: {{ to }}
project: {{ project }}
date: {{ created_at }}
status: {{ status }}
type: {{ type }}
---
{{ content_md }}
Content-Disposition: attachment; filename="{slug}.md" 헤더 설정.
[Unit]
Description=Comms Hub (AI messaging backend)
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/comms-hub
Environment=COMMS_DATA_DIR=/var/lib/comms
Environment=REPORTS_DIR=/home/ubuntu/reports
ExecStart=/home/ubuntu/comms-hub/.venv/bin/uvicorn server:app --host 127.0.0.1 --port 8770
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
구현 완료 후 아래 시나리오가 모두 통과해야 함:
curl -s -X POST http://localhost:8770/api/comms/msg \
-H "Content-Type: application/json" \
-d '{
"from": "claude-code",
"to": ["jadong"],
"cc": ["codex"],
"type": "report",
"project": "comms-hub",
"title": "테스트 보고서",
"tags": ["test"],
"summary_for_ai": "comms-hub 최초 테스트 메시지",
"content_md": "## 개요\n\ncomms-hub가 정상 동작합니다.\n\n### 체크리스트\n\n- [x] JSONL 저장\n- [x] HTML 렌더\n- [ ] PDF 내보내기 (Phase 2)\n\n```python\nprint(\"hello comms-hub\")\n```\n\n| 항목 | 상태 |\n|------|------|\n| 서버 | ✅ |\n| 렌더 | ✅ |"
}'
# 기대: 200 + Message JSON 반환 + output/{slug}.html 생성됨
curl -s http://localhost:8770/api/comms/inbox/jadong
# 기대: T1에서 생성한 메시지가 목록에 포함
curl -s http://localhost:8770/api/comms/inbox/jadong?status=sent
# 기대: status=sent인 것만 필터링
curl -s -X PATCH http://localhost:8770/api/comms/msg/{id}/status \
-H "Content-Type: application/json" \
-d '{"status": "approved"}'
# 기대: 200 + 상태 변경됨
curl -s http://localhost:8770/api/comms/projects
# 기대: {"comms-hub": 1}
curl -s http://localhost:8770/export/{id}.md
# 기대: YAML frontmatter + Markdown 본문
~/reports/의 기존 HTML 파일들을 절대 건드리지 말 것/home/ubuntu/scratchpad/는 별도 서비스작업 완료 후 아래 형태로 결과 보고:
전체 6-Phase 플랜은 여기서 확인:
https://ai.shinjadong.cloud/2026-05-06-cc-comms-hub-master-plan.html