🏪 마켓 둘러보기
🧩 스킬

데이터를 노션 대시보드로 자동 적재

클립납치단·2026년 6월 19일·⬇ 0

⓪ 이 스킬이면 뭘 할 수 있나?

손에 있는 아무 데이터(SQLite 행, JSON 배열, 크롤 결과, 주문/매출 내역…)를 노션 데이터베이스 대시보드로 한 방에 올릴 수 있어요. 핵심 매력 셋:

  • 설치 0 — 파이썬 표준 라이브러리(urllib)만. pip install 안 함, 가상환경 안 만듦.
  • 중복 0 — 고유키 컬럼으로 멱등 UPSERT. 같은 스크립트를 100번 돌려도 행이 안 불어나고 갱신만 됨.
  • MCP 불필요 — 노션 통합 토큰으로 REST를 직접 친다. 헤드리스·cron·남의 서버 어디서든 동작.

"행으로 쌓이는 데이터면 무엇이든 대시보드化" — 주문 현황, 매출, 출퇴근 기록, 수집물 목록 전부 같은 패턴.

① 한 줄 정의

NOTION_TOKEN(통합 토큰)으로 노션 REST API를 직접 호출해 DB를 1회 생성·재사용하고, 고유키로 INSERT/UPDATE를 가르는 멱등 push 스크립트.

② 언제 쓰나?

  • 모아둔 데이터를 사람이 보기 좋게 노션 표/보드로 보고 싶을 때
  • 주기적으로(매일·매시간) 같은 DB를 갱신해야 할 때 — 중복 없이 누적
  • 설치 권한이 빡빡한 환경(시스템 파이썬에 pip 없음 등)에서 의존성 없이 돌려야 할 때

③ 사전 요구 (1회 세팅)

1. 통합 토큰 발급 — notion.so/my-integrations → 통합 생성 → Internal Integration Token 복사 (ntn_… 형태)

2. 부모 페이지에 통합 연결 — 대시보드를 둘 노션 페이지 → 메뉴 → Connections(연결) → 만든 통합 추가. ⚠️ 이걸 안 하면 토큰이 있어도 403.

3. 부모 페이지 ID — 그 페이지 URL 끝의 32자.

4. 자격증명을 환경/.env 파일에 저장 (코드·저장소에 절대 박지 말 것):

```

NOTION_TOKEN=ntn_…

NOTION_PARENT_PAGE=<32자 page_id>

```

5. python3 (3.8+). 외부 패키지 불필요.

④ 연동·인증 방식 (정확히)

  • 정식 MCP가 아니다. 통합 토큰으로 REST를 직접 호출한다.
  • 엔드포인트: https://api.notion.com/v1
  • 필수 헤더 2개:

- Authorization: Bearer <NOTION_TOKEN>

- Notion-Version: 2022-06-28

- (+ 바디 있으면 Content-Type: application/json)

⑤ 단계별 절차

1. 자격증명 로드(NOTION_TOKEN, NOTION_PARENT_PAGE).

2. get_or_create_db().notion_db_id 캐시 파일이 있으면 GET으로 유효성 확인 후 재사용, 없으면 생성. 매번 새로 만들면 DB가 폭발하니 캐시 필수.

3. existing(dbid) — DB를 query해 고유키 → page_id 맵 확보(페이지네이션 포함).

4. 행마다: 키가 이미 있으면 PATCH /pages/{id}(갱신), 없으면 POST /pages(생성).

골격 코드 (그대로 복사 → SCHEMA·props 컬럼만 교체)

import json, urllib.request, urllib.error
from pathlib import Path

API, VER = "https://api.notion.com/v1", "2022-06-28"
TOKEN, PARENT = "{NOTION_TOKEN}", "{NOTION_PARENT_PAGE}"   # ← .env에서 로드 (아래 자격증명 로드 참고)
DBID_CACHE = Path(__file__).parent / ".notion_db_id"
DB_TITLE = "내 대시보드"

def api(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(f"{API}{path}", data=data, method=method)
    req.add_header("Authorization", f"Bearer {TOKEN}")
    req.add_header("Notion-Version", VER)
    req.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return json.loads(r.read().decode())
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"{method} {path} -> {e.code}: {e.read().decode()}")

# 컬럼 스키마 — 첫 컬럼은 반드시 title 타입 1개
SCHEMA = {
    "제목":  {"title": {}},
    "수량":  {"number": {}},
    "분류":  {"select": {"options": [{"name":"A","color":"green"},{"name":"B","color":"gray"}]}},
    "완료":  {"checkbox": {}},
    "링크":  {"url": {}},
    "키":    {"rich_text": {}},      # ← 중복 판정용 고유키
    "날짜":  {"date": {}},
}

def get_or_create_db():
    if DBID_CACHE.exists():
        dbid = DBID_CACHE.read_text().strip()
        try: api("GET", f"/databases/{dbid}"); return dbid
        except RuntimeError: pass
    res = api("POST", "/databases", {
        "parent": {"type":"page_id","page_id": PARENT},
        "title": [{"type":"text","text":{"content": DB_TITLE}}],
        "properties": SCHEMA,
    })
    DBID_CACHE.write_text(res["id"]); return res["id"]

def existing(dbid):                  # 키 -> page_id
    out, cur = {}, None
    while True:
        body = {"page_size":100}
        if cur: body["start_cursor"] = cur
        res = api("POST", f"/databases/{dbid}/query", body)
        for pg in res["results"]:
            k = pg["properties"].get("키",{}).get("rich_text",[])
            if k: out[k[0]["plain_text"]] = pg["id"]
        if not res.get("has_more"): break
        cur = res["next_cursor"]
    return out

def props(row):                      # dict -> 노션 properties
    return {
        "제목": {"title":[{"text":{"content": row["title"][:90]}}]},
        "수량": {"number": row.get("n",0)},
        "분류": {"select":{"name": row.get("cat","A")}},
        "완료": {"checkbox": bool(row.get("done"))},
        "링크": {"url": row.get("url") or None},
        "키":   {"rich_text":[{"text":{"content": row["key"]}}]},
        "날짜": {"date":{"start": row["date"][:10]}},
    }

def push(rows):
    dbid = get_or_create_db()
    seen = existing(dbid)
    new = upd = 0
    for r in rows:
        if r["key"] in seen:
            api("PATCH", f"/pages/{seen[r['key']]}", {"properties": props(r)}); upd += 1
        else:
            api("POST", "/pages", {"parent":{"database_id":dbid}, "properties": props(r)}); new += 1
    print(f"신규 {new} · 갱신 {upd} -> https://notion.so/{dbid.replace('-','')}")

자격증명 로드 (런타임 .env 우선 → 폴백)

토큰을 코드에 박지 말고 .env에서 읽는다. 어느 머신에서 돌려도 되게 다중경로 폴백:

import os
env = {}
for sp in [Path(__file__).parent / ".env", Path("{SECRETS_ENV_PATH}")]:
    if sp.exists():
        for ln in sp.read_text().splitlines():
            if "=" in ln and not ln.startswith("#"):
                k, v = ln.split("=", 1)
                env.setdefault(k.strip(), v.strip().strip('"'))
TOKEN  = os.environ.get("NOTION_TOKEN")        or env.get("NOTION_TOKEN")
PARENT = os.environ.get("NOTION_PARENT_PAGE")  or env.get("NOTION_PARENT_PAGE")

⑥ 컬럼 타입 치트시트

| 타입 | properties 값 | 스키마 |

|---|---|---|

| 제목(필수1) | {"title":[{"text":{"content":"..."}}]} | {"title":{}} |

| 텍스트 | {"rich_text":[{"text":{"content":"..."}}]} | {"rich_text":{}} |

| 숫자 | {"number": 12} | {"number":{}} |

| 선택 | {"select":{"name":"A"}} | {"select":{"options":[...]}} |

| 체크 | {"checkbox": true} | {"checkbox":{}} |

| URL | {"url":"https://..."} | {"url":{}} |

| 날짜 | {"date":{"start":"2026-06-11"}} | {"date":{}} |

⑦ 함정·기본값

1. 403 / 접근 불가 — 부모 페이지에 통합을 Connections로 추가 안 하면 토큰이 있어도 안 됨. 페이지를 옮겼다면 옮긴 위치에서도 권한 상속 확인.

2. DB 중복 생성.notion_db_id 캐시 파일 필수. 안 하면 실행할 때마다 새 DB가 생김.

3. title 컬럼 누락 — 모든 DB는 title 타입 컬럼이 정확히 1개 필요. 없으면 생성 400.

4. select 옵션 — 스키마에 없는 select 값도 자동 추가되긴 하지만, 색·순서를 통제하려면 스키마에 미리 선언.

5. url 빈 문자열""를 보내면 400. 값이 없으면 None(null)로 보낼 것.

6. page_size 100 한계 — 행이 많으면 start_cursor로 페이지네이션(골격에 포함됨).

7. rich_text 2000자 한계 — 긴 본문은 슬라이스해서 넣을 것.

8. 컬럼(뷰) 순서 — REST는 properties 순서를 줘도 뷰 컬럼 순서를 보장하지 않는다. 순서를 강제하려면 노션 UI나 별도 뷰 설정이 필요(데이터 push 자체는 REST로 항상 가능).

🛷 산타클로드에서 이 스킬 담기 →
0개의 댓글