전체 구성

브라우저 ksh.ai.kr — Cloudflare DNS (DNS only) Netlify Next.js App Router — 화면 · API · sitemap · robots 를 한 프로젝트가 응답 요청마다 서버리스 함수가 새로 뜬다 Supabase 글 · 댓글 · 세션 · PGroonga 검색 Cloudflare R2 업로드 이미지 Google OAuth 로그인만. 토큰은 저장하지 않는다

글 한 편을 읽을 때

GET /p/2026-08-08-k7f3m Netlify 함수 — Next.js 서버 컴포넌트 DB 는 Transaction pooler (6543) 로 붙는다 세션 쿠키 → viewerIsAdmin() 초안은 관리자만 본다. 아니면 403 이 아니라 404 getBySlug() — 글 한 건 읽기 이미 렌더된 HTML · CSS 를 그대로 읽는다 incrementView() — 조회수 기록 누계 +1, 오늘 칸 +1 withToc() — h2·h3 에 id 부여, 목차 추출 목차만 읽을 때 만든다 완성된 HTML 응답 + scoped_css 크롤러가 본문을 그대로 본다 Supabase Postgres 17 본문 이미지는 별도 요청이다 브라우저가 이미지를 다시 요청 /uploads/ab12.webp Netlify 함수 302 Location: … cdn.ksh.ai.kr (R2) 바이트가 함수를 지나지 않는다

글을 저장할 때

관리자 에디터 /admin/new POST /api/v1/posts requireAdmin() 세션 → ADMIN_EMAILS 확인 트랜잭션 하나 안에서 slug 생성 — 2026-08-08-k7f3m 카테고리 확인 · 형식과 렌더 모드 조합 검증 INSERT posts — content_source 는 원본 그대로 태그 upsert → post_tags 재작성 render() 실행 후 결과를 UPDATE content_rendered · scoped_css · content_text

render() — 세 갈래

MARKDOWN 항상 INLINE 이다 · 마크다운을 HTML 로 렌더 · sanitize — 허용 목록으로 거른다 · 본문 텍스트 추출 → 검색 색인 → html · text HTML / INLINE 사이트 안에 그대로 꽂힌다 · <style> 수집 · <script> 제거 · <body> 안쪽만 취한다 · sanitize — 허용 목록으로 거른다 · CSS 를 .post-content 로 스코프 → html · css · text · 경고 HTML / ISOLATED iframe 안에서 원본 그대로 · 정규화만 한다 — sanitize 안 함 · <script> 도 살아 있다 · 본문이 검색에 색인되지 않는다 · 그래서 summary 가 필수다 → 경고만 (html · text 없음)

세 갈래의 차이는 신뢰 경계를 어디에 두느냐 하나입니다. 앞의 둘은 내 사이트의 DOM 을 공유하므로 허용 목록을 통과해야 하고, CSS 는 .post-content 밖으로 새어나가지 못하게 선택자마다 접두사가 붙습니다. ISOLATED 는 iframe 이라 무엇을 넣든 사이트를 건드릴 수 없고, 그 대가로 본문이 검색에 잡히지 않습니다.

이미지를 올릴 때

에디터에 넣는다 파일 · 붙여넣기 · 드래그 POST /uploads/image 10MB 까지 시그니처로 형식 판정 확장자는 믿지 않는다 SVG — sanitize 후 원본 저장 벡터라 래스터로 굽지 않는다 그 외 — WebP 로 변환 형식이 하나로 모이고 작아진다 R2 에 저장 + uploads 행 기록 /uploads/{key} 반환

로그인

브라우저 블로그 (Netlify 함수) Google GET /api/v1/auth/google/login nonce 서명 → state 쿠키 (httpOnly) 302 → Google 동의 화면 계정 선택 · 동의 /callback?code=…&state=… state 서명 검증 + nonce 상수시간 비교 code → 토큰 교환 · userinfo sub · email · name · picture users upsert → sessions INSERT → 세션 쿠키