From 6c4bc985010e2ac360d7a811c0c421c6d885129d Mon Sep 17 00:00:00 2001 From: server-cron Date: Wed, 27 May 2026 13:22:59 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat(sync):=20docs/*.md=20=E2=86=92=20Notio?= =?UTF-8?q?n=20=EC=97=AD=EB=B0=A9=ED=96=A5=20=EB=8F=99=EA=B8=B0=ED=99=94?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/md_to_notion.py: git diff 기반 변경 감지 → Notion 업로드 (신규/수정/삭제 처리, MD→Notion blocks 변환, rate limit 대응) - .github/workflows/md-to-notion.yml: docs/**/*.md push 시 자동 실행 업로드 후 .notion-sync.json을 커밋·푸시해 server-sync 충돌 방지 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/md-to-notion.yml | 51 ++++ scripts/md_to_notion.py | 437 +++++++++++++++++++++++++++++ 2 files changed, 488 insertions(+) create mode 100644 .github/workflows/md-to-notion.yml create mode 100644 scripts/md_to_notion.py diff --git a/.github/workflows/md-to-notion.yml b/.github/workflows/md-to-notion.yml new file mode 100644 index 0000000..9e6033e --- /dev/null +++ b/.github/workflows/md-to-notion.yml @@ -0,0 +1,51 @@ +name: MD to Notion Sync + +on: + push: + branches: [main, develop] + paths: + - 'docs/**/*.md' + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + NOTION_ABOUT: ${{ secrets.NOTION_ABOUT }} + NOTION_ARCHITECTURE: ${{ secrets.NOTION_ARCHITECTURE }} + NOTION_BLOG: ${{ secrets.NOTION_BLOG }} + NOTION_CONTRIBUTE: ${{ secrets.NOTION_CONTRIBUTE }} + NOTION_DOCS: ${{ secrets.NOTION_DOCS }} + NOTION_INSTALL: ${{ secrets.NOTION_INSTALL }} + NOTION_POC: ${{ secrets.NOTION_POC }} + NOTION_RELEASE: ${{ secrets.NOTION_RELEASE }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: pip install requests + + - name: Sync docs to Notion + run: python3 scripts/md_to_notion.py + + - name: Commit updated sync maps + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add 'docs/**/.notion-sync.json' || git add docs/ + if ! git diff --staged --quiet; then + git commit -m "chore: .notion-sync.json 업데이트 (md→Notion 동기화)" + git push + fi diff --git a/scripts/md_to_notion.py b/scripts/md_to_notion.py new file mode 100644 index 0000000..e00027d --- /dev/null +++ b/scripts/md_to_notion.py @@ -0,0 +1,437 @@ +""" +md_to_notion.py — docs/*.md 변경 감지 → Notion DB 업로드 + +Env vars (필수): + NOTION_TOKEN Notion API 토큰 + +Env vars (선택): + SAVE_DIR 처리할 섹션 경로 (기본: 모든 섹션) + +사용법: + source .env && python3 scripts/md_to_notion.py + source .env && SAVE_DIR=docs/about python3 scripts/md_to_notion.py +""" +import json +import os +import re +import subprocess +import time +from pathlib import Path + +import requests + +REPO_DIR = Path(__file__).parent.parent + +# .env 파일 자동 로드 (미설정 키만 주입) +_env_file = REPO_DIR / ".env" +if _env_file.exists(): + for _line in _env_file.read_text().splitlines(): + _line = _line.strip() + if not _line or _line.startswith("#") or "=" not in _line: + continue + _k, _v = _line.split("=", 1) + os.environ.setdefault(_k.strip(), _v.strip()) + +NOTION_TOKEN = os.environ.get("NOTION_TOKEN", "") + +HEADERS = { + "Authorization": f"Bearer {NOTION_TOKEN}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json", +} + +SECTION_DIRS = [ + "docs/about", + "docs/architecture", + "docs/blog", + "docs/contribute", + "docs/guide", + "docs/install", + "docs/poc", + "docs/release-notes", +] + +SECTION_DB_MAP = { + "docs/about": os.environ.get("NOTION_ABOUT", ""), + "docs/architecture": os.environ.get("NOTION_ARCHITECTURE", ""), + "docs/blog": os.environ.get("NOTION_BLOG", ""), + "docs/contribute": os.environ.get("NOTION_CONTRIBUTE", ""), + "docs/guide": os.environ.get("NOTION_DOCS", ""), + "docs/install": os.environ.get("NOTION_INSTALL", ""), + "docs/poc": os.environ.get("NOTION_POC", ""), + "docs/release-notes": os.environ.get("NOTION_RELEASE", ""), +} + +_LANG_MAP = { + "text": "plain text", "txt": "plain text", + "cpp": "c++", "csharp": "c#", "fsharp": "f#", +} + + +def log(msg: str): + print(f"[md_to_notion] {msg}", flush=True) + + +# ── Frontmatter ────────────────────────────────────────────────────────────── + +def parse_frontmatter(text: str) -> tuple[dict, str]: + """YAML frontmatter 분리. (fm_dict, body) 반환.""" + m = re.match(r"^---\s*\n(.*?)\n---\s*\n", text, re.DOTALL) + if not m: + return {}, text + fm: dict = {} + for line in m.group(1).splitlines(): + kv = line.split(":", 1) + if len(kv) == 2: + k, v = kv[0].strip(), kv[1].strip().strip('"') + fm[k] = v + body = text[m.end():] + return fm, body + + +# ── Inline MD → Notion rich_text ───────────────────────────────────────────── + +def _make_rt(text: str, bold=False, italic=False, code=False, href=None) -> dict: + rt: dict = { + "type": "text", + "text": {"content": text}, + "annotations": { + "bold": bold, "italic": italic, "code": code, + "strikethrough": False, "underline": False, "color": "default", + }, + } + if href: + rt["text"]["link"] = {"url": href} + return rt + + +_INLINE_RE = re.compile( + r"(\*\*(.+?)\*\*)" # bold + r"|(\*(.+?)\*)" # italic + r"|(`(.+?)`)" # code + r"|(\[(.+?)\]\((.+?)\))" # link +) + + +def inline_md_to_rich_text(text: str) -> list: + """인라인 MD 패턴 → Notion rich_text 객체 배열.""" + result = [] + last = 0 + for m in _INLINE_RE.finditer(text): + if m.start() > last: + result.append(_make_rt(text[last:m.start()])) + if m.group(1): # bold + result.append(_make_rt(m.group(2), bold=True)) + elif m.group(3): # italic + result.append(_make_rt(m.group(4), italic=True)) + elif m.group(5): # code + result.append(_make_rt(m.group(6), code=True)) + elif m.group(7): # link + result.append(_make_rt(m.group(8), href=m.group(9))) + last = m.end() + if last < len(text): + result.append(_make_rt(text[last:])) + return result or [_make_rt("")] + + +# ── MD body → Notion blocks ─────────────────────────────────────────────────── + +def _block(btype: str, **kwargs) -> dict: + return {"object": "block", "type": btype, btype: kwargs} + + +def _rich_block(btype: str, text: str, **kwargs) -> dict: + return _block(btype, rich_text=inline_md_to_rich_text(text), **kwargs) + + +def md_body_to_blocks(body: str) -> list: + """MD 본문 → Notion block objects 배열 (라인 기반 파싱).""" + blocks: list = [] + lines = body.splitlines() + i = 0 + + while i < len(lines): + line = lines[i] + + # 코드 펜스 + if line.startswith("```"): + lang = line[3:].strip() or "plain text" + lang = _LANG_MAP.get(lang, lang) + code_lines = [] + i += 1 + while i < len(lines) and not lines[i].startswith("```"): + code_lines.append(lines[i]) + i += 1 + blocks.append(_block("code", + rich_text=[_make_rt("\n".join(code_lines))], + language=lang, + )) + i += 1 + continue + + # 헤딩 + if line.startswith("#### "): + blocks.append(_rich_block("heading_3", line[5:])) + elif line.startswith("### "): + blocks.append(_rich_block("heading_3", line[4:])) + elif line.startswith("## "): + blocks.append(_rich_block("heading_2", line[3:])) + elif line.startswith("# "): + blocks.append(_rich_block("heading_1", line[2:])) + + # 인용 + elif line.startswith("> "): + blocks.append(_rich_block("quote", line[2:])) + + # 구분선 + elif re.match(r"^-{3,}$|^\*{3,}$", line.strip()): + blocks.append(_block("divider")) + + # 이미지 + elif m := re.match(r"^!\[.*?\]\((.+?)\)$", line): + blocks.append(_block("image", + type="external", external={"url": m.group(1)} + )) + + # 불릿 리스트 + elif m := re.match(r"^(\s*)[-*] (.+)", line): + indent = len(m.group(1)) + text = m.group(2) + blk = _rich_block("bulleted_list_item", text) + if indent > 0 and blocks and blocks[-1]["type"] == "bulleted_list_item": + blocks[-1]["bulleted_list_item"].setdefault("children", []).append(blk) + else: + blocks.append(blk) + + # 번호 리스트 + elif m := re.match(r"^(\s*)\d+\. (.+)", line): + indent = len(m.group(1)) + text = m.group(2) + blk = _rich_block("numbered_list_item", text) + if indent > 0 and blocks and blocks[-1]["type"] == "numbered_list_item": + blocks[-1]["numbered_list_item"].setdefault("children", []).append(blk) + else: + blocks.append(blk) + + # 빈 줄 스킵 + elif line.strip() == "": + pass + + # 일반 단락 + else: + blocks.append(_rich_block("paragraph", line)) + + i += 1 + + return blocks + + +# ── Notion API ──────────────────────────────────────────────────────────────── + +def _api(method: str, path: str, **kwargs): + url = f"https://api.notion.com/v1{path}" + headers = {**HEADERS, "Authorization": f"Bearer {NOTION_TOKEN}"} + r = getattr(requests, method)(url, headers=headers, **kwargs) + if r.status_code not in (200, 204): + log(f" API 오류 {r.status_code}: {path} — {r.text[:200]}") + return r + + +def get_child_block_ids(page_id: str) -> list[str]: + """페이지의 최상위 블록 ID 목록 조회 (페이징).""" + ids = [] + params: dict = {} + while True: + r = _api("get", f"/blocks/{page_id}/children", params=params) + data = r.json() + for b in data.get("results", []): + ids.append(b["id"]) + if not data.get("has_more"): + break + params = {"start_cursor": data["next_cursor"]} + return ids + + +def delete_block(block_id: str): + _api("delete", f"/blocks/{block_id}") + time.sleep(0.35) # Notion 레이트 리밋 (3 req/s) + + +def append_blocks(page_id: str, blocks: list): + """blocks를 100개 단위로 나눠 페이지에 추가.""" + for i in range(0, len(blocks), 100): + chunk = blocks[i:i + 100] + _api("patch", f"/blocks/{page_id}/children", json={"children": chunk}) + + +def get_page_last_edited(page_id: str) -> str: + r = _api("get", f"/pages/{page_id}") + return r.json().get("last_edited_time", "") + + +def update_page_content(page_id: str, blocks: list): + """기존 블록 전체 삭제 후 새 블록 추가.""" + for bid in get_child_block_ids(page_id): + delete_block(bid) + if blocks: + append_blocks(page_id, blocks) + + +def create_page(database_id: str, title: str, order: int, blocks: list) -> str: + """Notion DB에 새 페이지 생성. page_id 반환.""" + payload = { + "parent": {"database_id": database_id}, + "properties": { + "제목": {"title": [{"text": {"content": title}}]}, + }, + "children": blocks[:100], + } + r = _api("post", "/pages", json=payload) + page_id = r.json().get("id", "") + if page_id and len(blocks) > 100: + append_blocks(page_id, blocks[100:]) + return page_id + + +def archive_page(page_id: str): + _api("patch", f"/pages/{page_id}", json={"archived": True}) + + +# ── Sync map ────────────────────────────────────────────────────────────────── + +def load_sync_map(section_dir: str) -> dict: + sync_file = REPO_DIR / section_dir / ".notion-sync.json" + if not sync_file.exists(): + return {} + with open(sync_file, encoding="utf-8") as f: + return json.load(f) + + +def save_sync_map(section_dir: str, sync_map: dict): + sync_file = REPO_DIR / section_dir / ".notion-sync.json" + with open(sync_file, "w", encoding="utf-8") as f: + json.dump(sync_map, f, ensure_ascii=False, indent=2) + + +def file_to_page_id(sync_map: dict, filepath: str) -> str | None: + for page_id, entry in sync_map.items(): + if entry.get("file") == filepath: + return page_id + return None + + +# ── Git diff ────────────────────────────────────────────────────────────────── + +def get_changed_files(section_dir: str) -> dict[str, list[str]]: + result = {"modified": [], "added": [], "deleted": []} + try: + out = subprocess.check_output( + ["git", "-c", "core.quotepath=false", "diff", "--name-status", + "HEAD~1", "HEAD", "--", section_dir], + cwd=REPO_DIR, text=True, + ) + except subprocess.CalledProcessError: + log(f"git diff 실패 — {section_dir} 스킵") + return result + + for line in out.strip().splitlines(): + if not line: + continue + parts = line.split("\t", 1) + if len(parts) < 2: + continue + status, filepath = parts[0][0], parts[1] + if not filepath.endswith(".md"): + continue + if status == "M": + result["modified"].append(filepath) + elif status == "A": + result["added"].append(filepath) + elif status == "D": + result["deleted"].append(filepath) + return result + + +# ── Main ────────────────────────────────────────────────────────────────────── + +def process_section(section_dir: str): + sync_map = load_sync_map(section_dir) + changed = get_changed_files(section_dir) + total = sum(len(v) for v in changed.values()) + if total == 0: + log(f"{section_dir}: 변경 없음") + return + + log(f"{section_dir}: 수정 {len(changed['modified'])}개 / 신규 {len(changed['added'])}개 / 삭제 {len(changed['deleted'])}개") + sync_dirty = False + + # 수정 + for filepath in changed["modified"]: + page_id = file_to_page_id(sync_map, filepath) + if not page_id: + log(f" [수정] {filepath} → sync_map 미등록, 스킵") + continue + text = (REPO_DIR / filepath).read_text(encoding="utf-8") + _, body = parse_frontmatter(text) + blocks = md_body_to_blocks(body) + log(f" [수정] {filepath} → {len(blocks)}개 블록 업로드 중...") + update_page_content(page_id, blocks) + sync_map[page_id]["last_edited"] = get_page_last_edited(page_id) + sync_dirty = True + log(f" [수정] 완료: {filepath}") + + # 신규 + for filepath in changed["added"]: + db_id = SECTION_DB_MAP.get(section_dir, "") + if not db_id: + log(f" [신규] {filepath} → DATABASE_ID 없음, 스킵") + continue + text = (REPO_DIR / filepath).read_text(encoding="utf-8") + fm, body = parse_frontmatter(text) + title = fm.get("title", Path(filepath).stem) + order = int(fm.get("sidebar_position", 99)) + blocks = md_body_to_blocks(body) + log(f" [신규] {filepath} → '{title}' 페이지 생성 중...") + page_id = create_page(db_id, title, order, blocks) + if page_id: + sync_map[page_id] = { + "file": filepath, + "last_edited": get_page_last_edited(page_id), + "order": order, + "parent_id": None, + } + sync_dirty = True + log(f" [신규] 완료: page_id={page_id}") + else: + log(f" [신규] 실패: {filepath}") + + # 삭제 + for filepath in changed["deleted"]: + page_id = file_to_page_id(sync_map, filepath) + if not page_id: + log(f" [삭제] {filepath} → sync_map 미등록, 스킵") + continue + log(f" [삭제] {filepath} → 아카이브 중...") + archive_page(page_id) + del sync_map[page_id] + sync_dirty = True + log(f" [삭제] 완료: {filepath}") + + if sync_dirty: + save_sync_map(section_dir, sync_map) + + +def main(): + if not NOTION_TOKEN: + log("오류: NOTION_TOKEN 미설정") + return + + save_dir = os.environ.get("SAVE_DIR", "") + sections = [save_dir] if save_dir else SECTION_DIRS + + for section in sections: + process_section(section) + + +if __name__ == "__main__": + main() From 20f2e5b44c15906d0afc797aaf881265e546ab75 Mon Sep 17 00:00:00 2001 From: server-cron Date: Wed, 27 May 2026 13:40:57 +0900 Subject: [PATCH 2/9] =?UTF-8?q?test:=20sbin.md=20=EC=8B=A0=EA=B7=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(md=5Fto=5Fnotion=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/about/sbin.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/about/sbin.md diff --git a/docs/about/sbin.md b/docs/about/sbin.md new file mode 100644 index 0000000..2e76cb4 --- /dev/null +++ b/docs/about/sbin.md @@ -0,0 +1,9 @@ +--- +id: 테스트로-이름-수정 +title: "테스트로 이름 수정" +sidebar_position: 1 +slug: "1" +--- + +sbin + From 5b0960314d547481a214287b99351301c0a31a44 Mon Sep 17 00:00:00 2001 From: server-cron Date: Wed, 27 May 2026 14:19:02 +0900 Subject: [PATCH 3/9] =?UTF-8?q?chore(tmp):=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9=20feat/md-to-notion-clean=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/md-to-notion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/md-to-notion.yml b/.github/workflows/md-to-notion.yml index 9e6033e..8a1a01e 100644 --- a/.github/workflows/md-to-notion.yml +++ b/.github/workflows/md-to-notion.yml @@ -2,7 +2,7 @@ name: MD to Notion Sync on: push: - branches: [main, develop] + branches: [main, develop, feat/md-to-notion-clean] paths: - 'docs/**/*.md' From 7ece37fa3669752b80f405d094fd7181e2276484 Mon Sep 17 00:00:00 2001 From: server-cron Date: Wed, 27 May 2026 14:20:13 +0900 Subject: [PATCH 4/9] commit --- docs/about/sbin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/about/sbin.md b/docs/about/sbin.md index 2e76cb4..40df21b 100644 --- a/docs/about/sbin.md +++ b/docs/about/sbin.md @@ -5,5 +5,5 @@ sidebar_position: 1 slug: "1" --- -sbin +sbin 이 2026-05-27 14:19 에 수정 From 205371eb86e2dada62ac45e1de4a990581d25d17 Mon Sep 17 00:00:00 2001 From: server-cron Date: Wed, 27 May 2026 14:23:35 +0900 Subject: [PATCH 5/9] =?UTF-8?q?chore:=20docs/about/.notion-sync.json=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20(sbin.md=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/about/.notion-sync.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/about/.notion-sync.json b/docs/about/.notion-sync.json index 0d0148d..4ead38b 100644 --- a/docs/about/.notion-sync.json +++ b/docs/about/.notion-sync.json @@ -1,8 +1,14 @@ { "365e15b4-0359-8017-817e-d1e2699828db": { "file": "docs/about/테스트로-이름-수정.md", - "last_edited": "2026-05-22T05:56:00.000Z", - "content_hash": "81cb0ff07eb46c96c3e42e83be1dfef2196cf981c375978e3f136a28f623ee63", + "last_edited": "2026-05-27T04:08:00.000Z", + "content_hash": "adb4921510a4919dbfae2d89903400fbc238f4e7ef79b1630eb80a9c6efaed5b", + "order": 1, + "parent_id": null + }, + "36de15b4-0359-814b-9f2b-ddbefb377eda": { + "file": "docs/about/sbin.md", + "last_edited": "2026-05-27T04:41:00.000Z", "order": 1, "parent_id": null } From bec9ed9594ec2461da60920050fb5a32d8ed5c21 Mon Sep 17 00:00:00 2001 From: server-cron Date: Wed, 27 May 2026 14:32:31 +0900 Subject: [PATCH 6/9] =?UTF-8?q?test:=20sbin.md=20=EB=B3=B8=EB=AC=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(md=E2=86=92Notion=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/about/sbin.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/about/sbin.md b/docs/about/sbin.md index 40df21b..8fe3a1f 100644 --- a/docs/about/sbin.md +++ b/docs/about/sbin.md @@ -1,9 +1,13 @@ --- id: 테스트로-이름-수정 -title: "테스트로 이름 수정" +title: "sbin" sidebar_position: 1 slug: "1" --- sbin 이 2026-05-27 14:19 에 수정 +## 내용 수정 테스트 + +이 내용이 Notion에 반영되면 수정 케이스 동작 확인. + From 1a33b6fb147d1a35cb8ba174a75a39722b82f933 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 May 2026 05:32:50 +0000 Subject: [PATCH 7/9] =?UTF-8?q?chore:=20.notion-sync.json=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20(md=E2=86=92Notion=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/about/.notion-sync.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/about/.notion-sync.json b/docs/about/.notion-sync.json index 4ead38b..0b4fc82 100644 --- a/docs/about/.notion-sync.json +++ b/docs/about/.notion-sync.json @@ -8,7 +8,7 @@ }, "36de15b4-0359-814b-9f2b-ddbefb377eda": { "file": "docs/about/sbin.md", - "last_edited": "2026-05-27T04:41:00.000Z", + "last_edited": "2026-05-27T05:32:00.000Z", "order": 1, "parent_id": null } From f4af603d61555a4fd9f1105fa34301e5ac7b18ec Mon Sep 17 00:00:00 2001 From: server-cron Date: Wed, 27 May 2026 14:41:44 +0900 Subject: [PATCH 8/9] =?UTF-8?q?refactor:=20notion=5Fto=5Fdocs=5Fgeneric.py?= =?UTF-8?q?=20=E2=86=92=20notion=5Fto=5Fmd.py=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/notion-sync.yml | 16 ++++++++-------- CLAUDE.md | 12 ++++++------ ...notion_to_docs_generic.py => notion_to_md.py} | 2 +- scripts/server-sync.sh | 16 ++++++++-------- scripts/sync-local.sh | 16 ++++++++-------- 5 files changed, 31 insertions(+), 31 deletions(-) rename scripts/{notion_to_docs_generic.py => notion_to_md.py} (99%) diff --git a/.github/workflows/notion-sync.yml b/.github/workflows/notion-sync.yml index 085a6ab..43ad2fc 100644 --- a/.github/workflows/notion-sync.yml +++ b/.github/workflows/notion-sync.yml @@ -49,49 +49,49 @@ jobs: if [ -n "$_BLOG_DB" ]; then NOTION_DATABASE_ID="$_BLOG_DB" SAVE_DIR=docs/blog FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$_CONTRIBUTE_DB" ]; then NOTION_DATABASE_ID="$_CONTRIBUTE_DB" SAVE_DIR=docs/contribute FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$_ABOUT_DB" ]; then NOTION_DATABASE_ID="$_ABOUT_DB" SAVE_DIR=docs/about FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$_ARCHITECTURE_DB" ]; then NOTION_DATABASE_ID="$_ARCHITECTURE_DB" SAVE_DIR=docs/architecture FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$_POC_DB" ]; then NOTION_DATABASE_ID="$_POC_DB" SAVE_DIR=docs/poc FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$_DOCS_DB" ]; then NOTION_DATABASE_ID="$_DOCS_DB" SAVE_DIR=docs/guide FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$_RELEASE_DB" ]; then NOTION_DATABASE_ID="$_RELEASE_DB" SAVE_DIR=docs/release-notes FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$_INSTALL_DB" ]; then NOTION_DATABASE_ID="$_INSTALL_DB" SAVE_DIR=docs/install FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi diff --git a/CLAUDE.md b/CLAUDE.md index 3180659..4aa6b6d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ docs-web/ │ └── CNAME # doc.scenemaker.solbox.com │ ├── scripts/ -│ ├── notion_to_docs_generic.py # Notion DB → docs/ 범용 동기화 (계층 구조 지원) +│ ├── notion_to_md.py # Notion DB → docs/ 범용 동기화 (계층 구조 지원) │ ├── translate_to_en.py # 변경된 docs/ → DeepL 번역 → docs_en/ 저장 │ ├── notion_to_blog.py # 블로그 전용 동기화 (레거시) │ └── notion_to_contribute.py # 기여 전용 동기화 (레거시) @@ -127,7 +127,7 @@ docs/poc/vision-bench/child.md (slug: "1") → /docs/poc/vision-bench/1 - 자식 파일은 `slug: "{child-order}"` (부모 디렉토리 기준 상대 슬러그) - **slug는 섹션 내 상대경로**. `"architecture/1"` 같은 전체 경로 쓰면 안 된다. - 새 파일은 섹션 내에서 `sidebar_position` 순서에 맞춰 다음 번호를 부여한다. -- **Notion 동기화 파일**: `scripts/notion_to_docs_generic.py`가 `slug` 자동 생성. 수동 추가 파일만 직접 slug를 넣어야 한다. +- **Notion 동기화 파일**: `scripts/notion_to_md.py`가 `slug` 자동 생성. 수동 추가 파일만 직접 slug를 넣어야 한다. --- @@ -144,7 +144,7 @@ GitHub Actions가 아닌 **서버에서 직접 crontab으로 2분마다** 실행 **`scripts/server-sync.sh` 흐름:** 1. `git pull --rebase origin main` — 원격 최신 코드 반영 -2. 8개 Notion DB 병렬 동기화 (`notion_to_docs_generic.py`, `FETCH_MODE=ALL`) +2. 8개 Notion DB 병렬 동기화 (`notion_to_md.py`, `FETCH_MODE=ALL`) 3. `translate_to_en.py` — 변경된 KR 파일 DeepL 번역 → `docs_en/` 저장 4. 변경사항 있으면 `git commit + push` → `deploy.yml` 트리거 → GH Pages 배포 @@ -174,7 +174,7 @@ tail -f /var/log/notion-sync.log | `NOTION_TOKEN` | API 인증 토큰 | | `DEEPL_API_KEY` | DeepL 번역 API 키 | -### 동기화 내부 로직 (`notion_to_docs_generic.py`) +### 동기화 내부 로직 (`notion_to_md.py`) Notion "하위 항목" relation을 읽어 자동으로 계층 구조를 결정한다. @@ -190,7 +190,7 @@ Notion "하위 항목" relation을 읽어 자동으로 계층 구조를 결정 ### 수동 동기화 (단일 섹션) ```bash -NOTION_TOKEN=... NOTION_DATABASE_ID=... SAVE_DIR=docs/guide python3 scripts/notion_to_docs_generic.py +NOTION_TOKEN=... NOTION_DATABASE_ID=... SAVE_DIR=docs/guide python3 scripts/notion_to_md.py ``` --- @@ -262,6 +262,6 @@ NOTION_TOKEN=... NOTION_DATABASE_ID=... SAVE_DIR=docs/guide python3 scripts/noti - `.docusaurus/`, `build/`, `node_modules/` 커밋 금지 - **`_category_.json` 수동 편집 시 주의**: - **섹션 루트** (`docs/{section}/_category_.json`): 수동 관리. `link.type: "doc"` 사용 시 실제 존재하는 doc ID인지 반드시 확인 (`npm run build` 로컬 통과 필수) - - **서브디렉토리** (`docs/{section}/{subdir}/_category_.json`): `notion_to_docs_generic.py`가 자동 생성·관리. 직접 수정하면 다음 sync에 덮어씌워짐 + - **서브디렉토리** (`docs/{section}/{subdir}/_category_.json`): `notion_to_md.py`가 자동 생성·관리. 직접 수정하면 다음 sync에 덮어씌워짐 - 부모 `index.md`에 `id:` 필드 추가 금지 — `_category_.json`의 `link.id`가 파일 경로 기반 ID(`{section}/{slug}/index`)로 고정되어 있어 충돌 발생 - **KR 파일이 이동(rename)된 경우** 대응하는 stale EN 파일을 수동 삭제해야 함 — `translate_to_en.py`는 git diff 삭제 감지만 하므로 rename은 자동 처리 안 됨 diff --git a/scripts/notion_to_docs_generic.py b/scripts/notion_to_md.py similarity index 99% rename from scripts/notion_to_docs_generic.py rename to scripts/notion_to_md.py index 577ec09..e4aea94 100644 --- a/scripts/notion_to_docs_generic.py +++ b/scripts/notion_to_md.py @@ -1,5 +1,5 @@ """ -notion_to_docs_generic.py — Notion DB → Docusaurus docs 섹션 동기화 +notion_to_md.py — Notion DB → Docusaurus docs 섹션 동기화 Env vars (필수): NOTION_TOKEN Notion API 토큰 diff --git a/scripts/server-sync.sh b/scripts/server-sync.sh index 6ea202f..e56633c 100755 --- a/scripts/server-sync.sh +++ b/scripts/server-sync.sh @@ -19,35 +19,35 @@ pids=() [ -n "$NOTION_ABOUT" ] && \ NOTION_DATABASE_ID="$NOTION_ABOUT" SAVE_DIR=docs/about FETCH_MODE=ALL \ - python3 scripts/notion_to_docs_generic.py & pids+=($!) + python3 scripts/notion_to_md.py & pids+=($!) [ -n "$NOTION_ARCHITECTURE" ] && \ NOTION_DATABASE_ID="$NOTION_ARCHITECTURE" SAVE_DIR=docs/architecture FETCH_MODE=ALL \ - python3 scripts/notion_to_docs_generic.py & pids+=($!) + python3 scripts/notion_to_md.py & pids+=($!) [ -n "$NOTION_BLOG" ] && \ NOTION_DATABASE_ID="$NOTION_BLOG" SAVE_DIR=docs/blog FETCH_MODE=ALL \ - python3 scripts/notion_to_docs_generic.py & pids+=($!) + python3 scripts/notion_to_md.py & pids+=($!) [ -n "$NOTION_CONTRIBUTE" ] && \ NOTION_DATABASE_ID="$NOTION_CONTRIBUTE" SAVE_DIR=docs/contribute FETCH_MODE=ALL \ - python3 scripts/notion_to_docs_generic.py & pids+=($!) + python3 scripts/notion_to_md.py & pids+=($!) [ -n "$NOTION_DOCS" ] && \ NOTION_DATABASE_ID="$NOTION_DOCS" SAVE_DIR=docs/guide FETCH_MODE=ALL \ - python3 scripts/notion_to_docs_generic.py & pids+=($!) + python3 scripts/notion_to_md.py & pids+=($!) [ -n "$NOTION_INSTALL" ] && \ NOTION_DATABASE_ID="$NOTION_INSTALL" SAVE_DIR=docs/install FETCH_MODE=ALL \ - python3 scripts/notion_to_docs_generic.py & pids+=($!) + python3 scripts/notion_to_md.py & pids+=($!) [ -n "$NOTION_POC" ] && \ NOTION_DATABASE_ID="$NOTION_POC" SAVE_DIR=docs/poc FETCH_MODE=ALL \ - python3 scripts/notion_to_docs_generic.py & pids+=($!) + python3 scripts/notion_to_md.py & pids+=($!) [ -n "$NOTION_RELEASE" ] && \ NOTION_DATABASE_ID="$NOTION_RELEASE" SAVE_DIR=docs/release-notes FETCH_MODE=ALL \ - python3 scripts/notion_to_docs_generic.py & pids+=($!) + python3 scripts/notion_to_md.py & pids+=($!) # 모든 동기화 완료 대기 failed=0 diff --git a/scripts/sync-local.sh b/scripts/sync-local.sh index 54dc1bc..4dae4b4 100755 --- a/scripts/sync-local.sh +++ b/scripts/sync-local.sh @@ -23,7 +23,7 @@ pids=() BLOG_DB="${NOTION_BLOG:-$NOTION_DATABASE_ID}" if [ -n "$BLOG_DB" ]; then NOTION_DATABASE_ID="$BLOG_DB" SAVE_DIR=docs/blog FETCH_MODE="$FETCH_MODE" \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi @@ -31,44 +31,44 @@ fi CONTRIBUTE_DB="${NOTION_CONTRIBUTE:-$NOTION_CONTRIBUTE_DATABASE_ID}" if [ -n "$CONTRIBUTE_DB" ]; then NOTION_DATABASE_ID="$CONTRIBUTE_DB" SAVE_DIR=docs/contribute FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi # Generic docs sections if [ -n "$NOTION_ABOUT" ]; then NOTION_DATABASE_ID="$NOTION_ABOUT" SAVE_DIR=docs/about FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$NOTION_ARCHITECTURE" ]; then NOTION_DATABASE_ID="$NOTION_ARCHITECTURE" SAVE_DIR=docs/architecture FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$NOTION_POC" ]; then NOTION_DATABASE_ID="$NOTION_POC" SAVE_DIR=docs/poc FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$NOTION_DOCS" ]; then NOTION_DATABASE_ID="$NOTION_DOCS" SAVE_DIR=docs/guide FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$NOTION_RELEASE" ]; then NOTION_DATABASE_ID="$NOTION_RELEASE" SAVE_DIR=docs/release-notes FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi if [ -n "$NOTION_INSTALL" ]; then NOTION_DATABASE_ID="$NOTION_INSTALL" SAVE_DIR=docs/install FETCH_MODE=ALL \ - python scripts/notion_to_docs_generic.py & + python scripts/notion_to_md.py & pids+=($!) fi From 36fb0ca04b67f160b9dac4ad6267a973e4a823ac Mon Sep 17 00:00:00 2001 From: server-cron Date: Wed, 27 May 2026 14:44:40 +0900 Subject: [PATCH 9/9] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9=20=EC=9E=84=EC=8B=9C=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/md-to-notion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/md-to-notion.yml b/.github/workflows/md-to-notion.yml index 8a1a01e..9e6033e 100644 --- a/.github/workflows/md-to-notion.yml +++ b/.github/workflows/md-to-notion.yml @@ -2,7 +2,7 @@ name: MD to Notion Sync on: push: - branches: [main, develop, feat/md-to-notion-clean] + branches: [main, develop] paths: - 'docs/**/*.md'