MyGo 云音乐
用 Go 与 MyGo 写成的第三方网易云音乐桌面客户端。原生界面,没有 WebView;边下边播,无损与 Hi-Res,逐字歌词与「律动」,刘海播放器。
Translation pending
읽고 쓰는 사람들의 자리
Translation pending
MediaTypeScript · PLpgSQLGitHub @mins0o0ngOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
읽고 쓰는 사람들의 자리. 기획안 v1.0 의 마일스톤 1 + 2 + 2-1 + 디자인 옵션 2b. Next.js (App Router) + Supabase. 이 지면은 초대받은 사람이 씁니다. 가입은 누구나 할 수 있지만, 글은 편집실이 기고 권한을 켠 사람만 쓸 수 있습니다 (M2-1 — 아래 기고 권한 참고). 배포 절차는 DEPLOY.md. 보안 모델은 docs/RLS.md, 작업 이력은 docs/. 고딕 볼드형 — 초대형 블랙 고딕 표제, 냉회색 지면, 에디터 픽만 검정 띠 60 냉회색 지면 · 30 초대형 먹 활자 · 10 시안 색은 전부 app/tokens.css 한 곳에 있고, 컴포넌트는 리터럴 색을 쓰지 않는다. 서체는 표제 Black Han Sans(400 한 종), 본문 Noto Sans KR — next/font/google. 2b 의 위계는 색이 아니라 굵기로 만들어지므로, 괘선은 검정 한 색에 두께만 4px / 1px 두 값이다. 기획안 §8 의 "Pretendard 서빙 방식 미결정"은 디자인이 이 둘을 지정하면서 정리됐다.Read the full README ↗
읽고 쓰는 사람들의 자리.
기획안 v1.0 의 마일스톤 1 + 2 + 2-1 + 디자인 옵션 2b. Next.js (App Router) + Supabase.
이 지면은 초대받은 사람이 씁니다. 가입은 누구나 할 수 있지만, 글은 편집실이 기고 권한을 켠 사람만 쓸 수 있습니다 (M2-1 — 아래 기고 권한 참고).
배포 절차는 DEPLOY.md. 보안 모델은 docs/RLS.md, 작업 이력은 docs/.
고딕 볼드형 — 초대형 블랙 고딕 표제, 냉회색 지면, 에디터 픽만 검정 띠 60 냉회색 지면 · 30 초대형 먹 활자 · 10 시안
색은 전부 app/tokens.css 한 곳에 있고, 컴포넌트는 리터럴 색을 쓰지 않는다.
| 역할 | 값 | 원본 변수 |
|---|---|---|
| 지면 | #f4f5f6 | --g |
| 표제·이름 | #0c0c0c | --ink |
| 부제 | #33383a | --sub |
| 날짜·카운터 | #5e6467 | --meta |
| 사진 자리 | #dfe2e4 | --plate |
| 괘선 | #0c0c0c | --rule |
| 스팟 (시안) | #0090c4 | --spot |
| 에디터 픽 띠 | #0c0c0c / #ffffff / #c9cdcf | --band |
| 순번·아바타 | #c4c9cb / #cdd1d3 | --num / --avatar |
서체는 표제 Black Han Sans(400 한 종), 본문 Noto Sans KR — next/font/google.
2b 의 위계는 색이 아니라 굵기로 만들어지므로, 괘선은 검정 한 색에 두께만 4px / 1px 두 값이다.
기획안 §8 의 "Pretendard 서빙 방식 미결정"은 디자인이 이 둘을 지정하면서 정리됐다.
디자인의 홈 아트보드를 그대로 옮겼다 — 마스트헤드(제호 + 가제 + 카테고리 + 로그인), 굵은/가는 이중 괘선, 리드 기사(사진 좌·표제 우), 순번 붙은 카드 열, 에디터 픽 검정 띠, "이번 달에 쓴 사람들", 푸터.
1440px 고정이던 원본을 유동 레이아웃으로 옮겼다. 디자인 값이 상한이 된다 (여백 100px, 카드 280px, 간격 40px).
npm install
cp .env.example .env.local # 값을 채운다
npm run dev
Supabase 프로젝트를 만들고 .env.local 에 네 값을 넣은 뒤, 명령 두 줄이면 끝난다.
npm run db:migrate # supabase/migrations/ 전부, 순서대로
npm run db:admin -- 내주소@example.com # 편집장 계정 + profile + 권한
.env.local 에 필요한 값 (전부 대시보드에서 복사):
| 변수 | 어디서 | 쓰는 곳 |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Settings → API | 앱 전체 |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Settings → API | 앱 전체 (RLS 가 보호) |
SUPABASE_SERVICE_ROLE_KEY | Settings → API | 편집실·초대·db:admin |
DATABASE_URL | Connect → Session pooler (문자열 그대로, [YOUR-PASSWORD] 도 그대로) | db:migrate 만 |
SUPABASE_DB_PASSWORD | 데이터베이스 비밀번호 원문 (인코딩하지 않는다) | db:migrate 만 |
비밀번호를 URL 에 직접 박지 않는 이유: Supabase 는 비밀번호에 특수문자를 요구하고,
그 글자들(@ : / #)이 전부 URL 구분자다. 인코딩은 scripts/db-url.mjs 가
한 곳에서 한다.
이미 손으로 만들어 둔 DB 라면 npm run db:migrate -- --adopt 를 쓴다 —
이미 적용된 마이그레이션은 실행하지 않고 기록만 하고 나머지만 돌린다.
그냥 돌리면 실행기가 그 상태를 알아보고 멈춘 뒤 안내한다.
db:migrate 는 실행 이력을 DB 의 schema_migrations 에 남긴다. 몇 번을 돌려도
안 돌린 것만 돌아가므로, 마이그레이션이 추가되면 다시 실행하면 된다.
--dry 를 붙이면 무엇이 돌아갈지만 보여준다.
DATABASE_URL 이 따로 필요한 이유: service_role 키는 PostgREST 를 통과하므로
테이블만 다룰 수 있고 DDL 은 못 돌린다. 이 값은 마이그레이션 실행기 전용이고
앱 코드는 읽지 않는다 — Vercel 에 넣을 필요도 없다.
db:admin 이 따로 필요한 이유: is_admin 은 애플리케이션 어디에서도 켤 수 없다.
앱에서 편집장을 만들 수 있으면 그 경로가 곧 권한 상승 경로가 되기 때문이다.
그래서 첫 편집장은 반드시 앱 바깥에서 만들어야 하고, 이 계정이 없으면 아무도
/editor 에 들어가지 못해 누구도 기고 권한을 받을 수 없다. 이 명령은 계정이
없으면 만들고, profile 이 없으면 만들고, 권한을 켠다. 이미 다 돼 있으면 아무것도
바꾸지 않는다.
마지막으로 supabase/check_rls.sql 을 SQL Editor 에서 한 번 돌려 전부 PASS 인지 본다.
supabase/seed.sql은 남겨 두었다. SQL 로 직접 하고 싶을 때를 위한 것이고,npm run db:admin을 쓰면 필요 없다.
/write 는 로그인 세션 + profiles.can_write 로 열린다. M1·M2 의
ADMIN_PASSWORD 자물쇠(lib/adminGate.ts)는 M2-1 에서 삭제됐고, 환경변수
ADMIN_PASSWORD 와 ADMIN_AUTHOR_ID 도 함께 사라졌다.
| 상태 | 할 수 있는 일 |
|---|---|
| 비로그인 | 발행글 읽기 |
| 가입만 함 | 읽기 + /me (아직 글은 못 씀) |
can_write | 자기 글 쓰기·발행·수정·삭제 |
is_admin | 위 전부 + /editor 초대·권한 관리 + 남의 글 수정 |
권한을 켜는 경로는 두 가지다.
/editor 명단에서 "기고 권한 주기"./editor 에서 이메일로 초대해두면
그 주소로 가입하는 순간 자동으로 켜진다. 메일은 나가지 않는다 — 명단에
적어둘 뿐이고, 초대 사실은 편집장이 직접 알린다.is_admin 은 애플리케이션 어디에서도 켤 수 없다. npm run db:admin 이나 대시보드로만
지정한다 — 편집장을 앱에서 만들 수 있으면 그 경로가 곧 권한 상승 경로가 되기 때문이다.
| 경로 | 언제 |
|---|---|
/write | 사이트에서 직접 쓴다. 자동저장·툴바·미리보기 |
| 노션 자동 동기화 | 노션에 쓰고 상태만 바꾼다. 10분마다 지면에 반영 ← M2-4 |
/write/import | 한 번만 옮길 원고를 붙여넣는다. 여러 편을 한 번에 초안으로 |
supabase/seed_posts.sql |
글을 발행하는 데 SQL 은 필요 없다. npm run db:migrate 는 최초 1회 스키마
세팅이고, 그 뒤로는 화면에서 끝난다. M1 때 원고를 INSERT 문으로 옮기던 경로
(seed_posts.sql)가 "글 한 편 올리려면 SQL 을 짠다" 는 인상을 남겼는데,
/write/import 가 그 경로를 대신한다.
가져오기는 반드시 초안으로 들어간다. 발행은 각 글을 열어 부제와 카테고리를 확인한 뒤에 누른다 — 그 둘은 편집 결정이지 파서가 정할 일이 아니다. 쪼개는 규칙은 화면의 "쪼개는 규칙"에 접혀 있고, 저장 전에 몇 편이 어떻게 인식됐는지 미리 보여준다.
노션 「웹매거진 원고」 데이터베이스가 원본이고, 사이트는 사본이다. 10분마다 Vercel Cron 이 가져오고, 편집실의 지금 동기화 버튼으로 즉시 당길 수도 있다.
| 노션 속성 | 하는 일 |
|---|---|
| 제목 | posts.title |
| 부제 | posts.deck — 발행하려면 반드시 채워야 한다 |
| 카테고리 | essay / place / love / life / pick — 발행하려면 필수 |
| 상태 | 작성중 안 가져옴 · 초안 · 발행 · 숨김 |
| 썸네일 | 비우면 페이지 커버를 쓴다 |
| 썸네일 비율 | 3:2 / 3:4 / 1:1 |
| 필자 핸들 | 비우면 동기화를 돌린 편집장 앞으로 |
| 동기화 | 결과가 자동으로 적힌다. 직접 고치지 말 것 |
설정 (한 번만)
⋯ → 연결 → 그 통합 선택
— 이걸 빼먹으면 토큰이 맞아도 404 가 난다. 가장 흔한 실수다..env.local 에 NOTION_TOKEN, NOTION_DATABASE_ID, CRON_SECRET단방향이다. 노션에서 온 글은 /write 에서 편집이 막힌다. 열어두면 사이트에서
고친 내용을 다음 동기화가 말없이 덮어쓴다 — 사라진 줄도 모르는 편집이 가장 나쁘다.
/me 목록에서는 노션 표가 붙고 수정·삭제 대신 "노션에서 고치세요" 가 나온다.
이미지는 Supabase Storage 로 복사한다. 노션이 호스팅하는 이미지 URL 은 서명된
링크라 한 시간이면 만료된다. 그대로 저장하면 동기화 직후에는 멀쩡하다가 한 시간 뒤
전부 깨지므로, 본문과 커버의 이미지를 post-images 버킷으로 옮기고 영구 주소로
바꿔 넣는다. 파일 이름은 원본 URL 의 해시라 다시 동기화해도 늘어나지 않는다.
노션에서 행을 지우면 사이트에서는 숨김이 된다. 삭제가 아니다 — 원본이 사라졌다고 좋아요와 댓글까지 cascade 로 지우면 되돌릴 수 없다.
발행을 막는 조건이 셋 있다. 부제·카테고리·본문 중 하나라도 비어 있으면 그 행은
실패 로 표시되고 지면에 나가지 않는다. 이유는 노션의 "동기화" 칸에 적힌다.
매직링크 메일 왕복이 번거로울 때 쓰는 지름길이다. 비밀번호 한 칸으로 편집장 계정에 로그인한다.
npm run db:admin -- [email protected] --password '정할비밀번호'
# .env.local 에
[email protected]
ADMIN_EMAIL 이 있을 때만 /login 에 "편집실 열쇠" 칸이 생기고, 지우면 화면과 서버
액션이 함께 닫힌다. 빌드할 때마다 켜져 있다고 경고한다.
"비밀번호를 맞히면 관리자 권한을 준다" 가 아니라 "그 계정으로 로그인한다" 이다.
전자는 M2-1 이 막은 자가 승격 경로를 앱에 다시 뚫는 것이고, auth.uid() 가 비어
글의 필자를 세션에서 꺼낼 수 없어 service_role 쓰기까지 되살아난다. 후자는 진짜
세션이라 RLS·초대제·컬럼 GRANT 가 전부 그대로 작동한다.
임시 통로다. 열쇠가 새면 지면 전체가 넘어간다. 다른 사람을 초대해 각자 자기
계정으로 들어가기 시작하면 ADMIN_EMAIL 을 지울 것.
middleware.ts 토큰 갱신 + 보호 라우트
app/
tokens.css 디자인 2b 토큰 — 색·활자·괘선·레이아웃
globals.css 리셋 + .page / .plate / .kicker / .rule-*
page.tsx 홈 (ISR 60초)
p/[id]/ 글 상세
category/[slug]/ 카테고리 목록 (ISR 60초)
write/ 새 글 · 수정 · 서버 액션 (기고 권한 필요)
write/import/ 원고 붙여넣어 초안으로 들이기 ← M2-3
api/sync/notion/ Vercel Cron 이 10분마다 부른다 ← M2-4
u/[handle]/ 필자 페이지 (ISR 60초) ← M2-1
me/ 내 글 — 초안·발행·삭제 (동적) ← M2-1
editor/ 편집실 — 초대·권한 관리 (동적) ← M2-1
login/ signup/ 매직링크 요청
auth/actions.ts 로그인·가입·프로필 생성 서버 액션
auth/callback/ 매직링크가 돌아오는 곳 (초대장 수락도 여기서)
auth/complete/ 핸들이 겹쳤을 때 다시 고르는 화면
legal/ 이용약관 · 개인정보처리방침 (초안)
feed.xml/ RSS
components/
PostCard.tsx 순번 붙은 카드. 썸네일 없이도 성립 ← M1 전제 조건
PostEditor.tsx 마크다운 textarea + 미리보기 + 자동저장 ← M2-2
MarkdownToolbar.tsx 굵게·제목·인용·링크·이미지 등 여덟 개 ← M2-2
useAutosave.ts localStorage 복구 + 초안 서버 자동저장 ← M2-2
MyPostRow.tsx /me 의 글 한 줄 + 삭제 확인 ← M2-1
ImportForm.tsx 붙여넣기 + 쪼갠 결과 미리보기 ← M2-3
NeedsInvite.tsx 초대 없이 /write 에 온 사람 ← M2-1
Markdown.tsx 본문 렌더러 (원시 HTML 비활성)
auth/AuthNav.tsx 마스트헤드 로그인 표시 (클라이언트 — ISR 을 지키기 위해)
auth/KeyForm.tsx 편집실 열쇠 — 비밀번호 한 칸 (임시) ← M2-2
engage/LikeButton.tsx 좋아요 (낙관적 업데이트) ← M3
engage/Comments.tsx 댓글 (목록·작성·숨김) ← M3-1
editor/NotionSync.tsx 동기화 버튼 + 행별 결과 ← M2-4
editor/ 초대 폼 · 권한 스위치 ← M2-1
lib/
site.ts 제호·태그라인
supabase.ts publicClient / sessionClient / adminClient
supabaseBrowser.ts 브라우저 클라이언트
auth.ts 세션·프로필 헬퍼, 핸들 규칙, canWrite/isEditor
keyLogin.ts 편집실 열쇠 켜짐/꺼짐 (임시) ← M2-2
format.ts 경로·날짜·숫자 표기 — 순수 함수만 ← 클라이언트도 쓴다
importMarkdown.ts 붙여넣은 원고를 글 단위로 쪼갠다 ← M2-3
notion.ts 노션 REST + 블록→마크다운 ← M2-4
notionSync.ts upsert · 이미지 복사 · 고아 숨김 ← M2-4
posts.ts 공개 읽기 · 에디터 픽 · 필자 페이지
authorPosts.ts 내 글 읽기 (세션 — RLS 가 소유권을 건다) ← M2-1
contributors.ts 편집실 읽기·쓰기 (service role) ← M2-1
env.mjs 빌드 시점 환경변수 검사
scripts/
db-migrate.mjs 마이그레이션 실행기 (이력은 schema_migrations) ← M2-2
db-admin.mjs 편집장 지정 — 계정·profile·권한 한 번에 ← M2-2
rls-test.mjs 정책 우회 테스트 ← M2 / M2-1 완료 기준
supabase/
migrations/ ..._init · ..._auth · ..._lock_anon · ..._contrib · ..._notion · ..._engage
check_rls.sql 권한·RLS 점검 (PASS/FAIL 표)
seed.sql
docs/
RLS.md 보안 모델 — 정책·컬럼 GRANT·함정·확인법
2026-09-07-작업기록.md
lib/format.ts 가 따로 있는 이유: lib/posts.ts 는 lib/supabase.ts 를 통해
next/headers 를 끌고 온다. 클라이언트 컴포넌트가 날짜 표기 하나 때문에
posts.ts 를 import 하면 빌드가 통째로 실패한다.
| 화면 | 방식 |
|---|---|
| 홈, 카테고리, 필자 페이지 | ISR 60초 |
| 글 상세 | 온디맨드 (발행 시 revalidatePath) |
글쓰기, /me, /editor | 동적 |
썸네일이 없는 카드. 디자인의 모든 카드에는 사진이 있지만, 기획안 §1 은 썸네일 없이도 카드가 성립할 것을 M1 의 전제 조건으로 못박는다. 2b 의 30% 는 색면이 아니라 "초대형 먹 활자"이므로, 사진이 없는 카드는 회색 자리를 비워두는 대신 표제를 한 단계 키워 그 자리를 활자로 채운다. 디자인의 논리를 그대로 따른 것이고, 덕분에 AI 썸네일(M4)이 영영 안 붙어도 홈이 성립한다.
사진 자리의 12px 안여백. 목업에서 "가로 3:2" 같은 비율 주석을 얹기 위한 공간이라 옮기지 않았다. 그대로 두면 실제 사진 둘레에 회색 액자가 남는다.
비율. 목업의 사진 상자는 라벨과 실제 크기가 어긋난다(리드는 "3:2" 라벨에
740×416 ≈ 16:9). 저장된 thumbnail_ratio 로 aspect-ratio 를 잡았다 — 비율을
값으로 저장하는 이유가 §3.2 에 적힌 그대로이기 때문이다. 에디터 픽 띠 안의 사진만
저장값과 무관하게 3:1 로 자른다. 띠 고유의 판형이다.
글 상세 화면. 기획안 §8 이 미착수로 남긴 부분이다. 홈의 규칙(초대형 고딕 표제, 먹 괘선, 시안은 카테고리와 인터랙션에만)을 옮겨 구성했다. 사진은 본문 단과 같은 폭으로 묶었다 — 지면 전체 폭으로 펼치면 3:2 사진이 800px 넘게 자라 본문을 압도한다.
마스트헤드의 검색·북마크 아이콘. 디자인에 있어 그대로 두었지만 아직 아무 기능에도 연결돼 있지 않다. 눌러도 반응하지 않는 UI 이므로 M2 에서 기능을 붙이거나, 그전에 빼는 편이 나을 수 있다.
푸터의 이용약관·개인정보. 디자인에도 링크가 아닌 글자로 놓여 있고, 실제 법적 의무는 가입 기능이 붙는 M2 부터 생긴다(§6 M2). 문서가 생길 때까지 링크를 걸지 않았다.
카테고리 라벨. 디자인 표기를 따라 영문 대문자다 (ESSAY / PLACE / LOVE / LIFE /
EDITOR'S PICK). enum 값은 기획안 §3.1 그대로 essay/place/love/life/pick.
1. likes 에 anon insert 정책을 만들지 않았다.
기획안 §3.2 는 비로그인 좋아요를 위해 쿠키 UUID 를 actor_key 에 넣자고 하지만,
§5.3 이 말한 대로 anon 키는 클라이언트에 노출된다. 그 상태로 insert 를 열면
누구나 actor_key 를 난수로 바꿔가며 좋아요를 무한 삽입할 수 있다.
마이그레이션의 RLS 정책은 로그인 사용자만 다루고, 비로그인 경로는 M3 에서
서버 액션(service role) 전용으로 붙인다.
2. 카테고리 표시 이름을 lib/categories.ts 로 뺐다.
스키마는 기획안대로 Postgres enum 이지만, enum 은 변경에 마이그레이션이 필요하고
카테고리는 매거진에서 가장 자주 바뀌는 축이다(§8 도 인정).
3. published_at 을 DB 트리거로 박는다.
애플리케이션이 잊어도 posts_feed_idx 가 비지 않도록 보장한다.
4. 댓글 카운터 트리거가 UPDATE 도 처리한다.
댓글은 소프트 삭제라(§3.2) INSERT/DELETE 만으로는 deleted_at 이 채워질 때
카운터가 내려가지 않는다.
5. "이번 달에 쓴 사람들"을 M1 에 넣었다.
기획안은 M2-1 로 잡았지만 디자인 홈에 있고, 이번 달 글만 받아 JS 에서 집계하면
되는 수준이라 먼저 만들었다. 필자 페이지 /u/[handle] 은 M2-1 에서 붙었다.
6. 컬럼 단위 GRANT 를 추가했다 (M2).
1차 마이그레이션의 RLS 정책만으로는 가입한 사람이 자기 profile 에
is_admin = true 를 써넣을 수 있었다. RLS 는 어느 행을 건드릴지만 정하지,
그 행의 어느 칸을 건드릴지는 정하지 않기 때문이다. is_admin() 이 true 가 되면
posts_update_own 이 관리자에게 남의 글 수정을 허용하므로, 누구나 모든 글을
고치고 내릴 수 있게 된다. 가입이 열리는 순간 실제 권한 상승 경로다.
revoke insert, update … grant (컬럼 목록) 으로 막았고, 같은 방식으로
like_count 직접 조작과 댓글의 post_id 변경도 막았다.
scripts/rls-test.mjs 가 이 세 가지를 검사한다. M2-1 의 can_write 도 같은
목록에서 빠져 있다 — 스스로 켤 수 있으면 초대제 전체가 장식이 된다.
8. 타인 기고를 심사제가 아니라 초대제로 만들었다 (M2-1).
기획안 §6 M2-1 은 "타인 기고" 만 말하고 편집권을 다루지 않는다. 그런데 M2 까지의
posts 정책은 author_id = auth.uid() 만 보므로, /write 를 세션 인증으로 여는
순간 가입한 누구나 즉시 status = 'published' 로 홈 1면에 올릴 수 있다.
편집권이 없는 매거진은 게시판이고, 스팸글 한 건에 매체 전체의 신뢰가 걸린다.
선택지는 셋이었다. (a) 누구나 발행 (b) 글마다 심사 큐(draft → review → published)
(c) 사람마다 한 번 초대. (c) 를 골랐다 — 심사는 글마다 편집자가 붙어야 하지만
초대는 사람마다 한 번만 붙으면 되고, 이 규모에서 지속 가능한 쪽은 후자다.
발행량이 늘어 초대만으로 품질이 안 잡히면 그때 post_status 에 'review' 를
더하면 된다. 스키마는 그 방향으로 열려 있다.
9. /write 가 service role 을 쓰지 않는다 (M2-1).
M1·M2 의 savePost 는 adminClient() 로 RLS 를 우회하고 author_id 를
환경변수(ADMIN_AUTHOR_ID)에서 꺼냈다. 이제 세션 클라이언트로 저장하고
author_id 를 세션에서 꺼낸다. 소유권 판정이 DB 한 곳에서만 일어나고
(두 곳에서 판정하면 언젠가 어긋난다), service role 키가 닿는 경로가 줄어든다.
lib/adminPosts.ts 와 lib/adminGate.ts 는 삭제됐다.
10. 좋아요가 글의 updated_at 을 밀던 버그를 고쳤다 (M2-1).
sync_like_count() 가 posts 를 UPDATE 하고, 그 UPDATE 가
posts_touch_updated_at 을 깨우고 있었다. 아무도 글을 고치지 않았는데 좋아요
한 번에 updated_at 이 now() 로 밀린다. 지금까지는 updated_at 을 읽는 화면이
없어 조용했지만, /me 가 "마지막 수정"을 보여주고 M3 에서 좋아요가 붙는 순간
눈에 보이는 오작동이 된다. 트리거에 WHEN 조건을 붙여 사람이 고치는 칸이
바뀔 때만 돌게 했다.
7. 마스트헤드의 로그인 표시를 클라이언트에서 읽는다.
서버에서 cookies() 를 읽으면 이 헤더를 쓰는 모든 페이지가 동적 렌더링이 되어
ISR 이 사라진다. 캐시된 지면에 개인 상태를 섞지 않는다는 §2.3 의 원칙이
좋아요·댓글뿐 아니라 로그인 표시에도 그대로 적용된다.
next/image 도 이 결정 후에 붙인다.lib/site.ts 한 곳에 있다.ADMIN_EMAIL 지우기 — 다른 사람을 초대해 각자 자기 계정으로 들어가기
시작하면. 남겨두면 비밀번호 하나로 편집장이 되는 통로가 계속 열려 있다. npm run test:rls 전부 통과 — 기획안 §6 M2 / M2-1 의 완료 기준.
2026-09-12, 실제 Supabase 프로젝트에서 29 통과 0 실패
(실행 8).
같은 실행에서 check_rls.sql 도 통과했고 마이그레이션 여섯 개가 전부
적용됐다.
다시 돌리려면 Actions 탭 → **DB 점검** → Run workflow.
필요한 secret: `SUPABASE_URL` · `SUPABASE_ANON_KEY` ·
`SUPABASE_SERVICE_ROLE_KEY` · `SUPABASE_DATABASE_URL`
(선택: `SUPABASE_DB_PASSWORD` · `ADMIN_LOGIN_PASSWORD`).
**개발용 프로젝트에서만 돌릴 것** — 테스트 계정 세 개를 만들었다 지운다.
이용약관·개인정보처리방침의 TODO 채우기 — 두 문서 모두 초안이고 화면에 초안 안내가 떠 있다. 특히 약관 8조(게시물 관리)와 9조(권리 귀속)는 남의 글을 받는 매체에서 비어 있으면 안 되는 조항이다.
Supabase Authentication → URL Configuration 에 Redirect URL 등록 (DEPLOY.md 1.5)
셋업이 명령 두 줄이 됐다. 마이그레이션 네 개를 SQL Editor 에 하나씩 붙여넣고,
대시보드에서 계정을 만들고, UUID 를 복사해 seed.sql 을 고치고, 다시 실행하던
과정이 npm run db:migrate 와 npm run db:admin -- 주소 로 줄었다. 사람이
"몇 번까지 돌렸더라" 를 기억할 일이 없어진 것이 핵심이다.
자동저장이 붙었다. 브라우저(localStorage, 0.6초)와 서버(초안일 때만, 3초 멈추면)
두 겹이다. 새 글은 첫 자동저장에서 초안이 만들어지고 주소가 /write/<id> 로
바뀌므로 새로고침해도 이어진다. 발행된 글은 서버 자동저장을 하지 않는다 —
지면에 나가 있는 글을 사람이 저장을 누르지도 않았는데 덮어쓰면 안 되기 때문이고,
그 경우에도 브라우저 초안 복구는 그대로 돈다.
마크다운 툴바를 여덟 개만 뒀다. 굵게·기울임·소제목·인용·목록·링크·이미지·구분선.
리치 에디터를 쓰지 않는다는 §6 M1 의 결정은 유지하되, 초대제로 외부 필자를
받기 시작한 이상 ## 와 ![]() 를 외우게 하는 건 다른 문제라 그 사이만 메웠다.
초대 사실을 알리는 건 사람 몫이다. /editor 의 초대는 명단에 적어둘 뿐
메일을 보내지 않는다. Supabase 매직링크와 별개인 발송 경로를 하나 더 만들면
그때부터 스팸 신고와 도메인 평판을 관리해야 하는데, 초대가 한 달에 몇 건인
단계에서는 그 비용이 이득보다 크다. 초대 건수가 주 단위로 쌓이면 그때 붙인다.
가입한 사람이 여전히 할 수 있는 일이 적다. 초대받지 않으면 읽기와 /me
뿐이다. M2 때보다는 낫지만(NeedsInvite 가 왜 못 쓰는지 설명한다), 좋아요와
댓글이 붙는 M3 전까지 읽기 계정의 쓸모는 제한적이다.
권한 회수는 발행글을 내리지 않는다. can_write = false 는 새 글과 수정을
막을 뿐, 이미 발행된 글은 그대로 지면에 남는다. 내리려면 편집장이
status = 'hidden' 으로 바꿔야 하는데 그 UI 가 아직 없다 — 지금은 대시보드에서
직접 해야 한다.
/editor 명단에 페이지네이션이 없다. 200명에서 자른다. 가입자가 그 이상으로
늘면 검색을 붙여야 한다.
사이트 에디터에는 아직 이미지 업로드가 없다. M2-4 가 post-images 버킷을
만들고 노션 이미지를 거기로 옮기지만, /write 의 툴바는 여전히  를
넣어줄 뿐이다. 버킷이 생겼으니 남은 일은 업로드 UI 하나다 — 그리고 next/image
전환도 그 다음이다(§8 미결정).
미리보기가 실제 지면과 다르다. PostEditor.module.css 의 .preview 와
app/p/[id]/page.module.css 의 .body 가 서로 다른 스타일이다. 2b 는 활자 위계가
디자인의 전부라서, 미리보기에서 멀쩡하던 글이 발행하면 다르게 보인다.
지면 CSS 를 공유 모듈로 빼면 해결된다.
| 계획 | 상태 | |
|---|---|---|
| M1 | 홈·카드·글쓰기·RSS | ✅ |
| M2 | 매직링크 인증·가입·RLS | ✅ 실제 프로젝트 검증 완료 (2026-09-12, 29/29) |
| M2-1 | 타인 기고 | ✅ 초대제로 구현 |
| M2-2 | 계획에 없던 것 | 셋업 자동화·자동저장·툴바·편집실 열쇠 |
| M2-3 | 계획에 없던 것 | 원고 붙여넣기 가져오기 |
| M2-4 | 계획에 없던 것 | 노션 자동 동기화 |
| M3 | 좋아요 | ✅ 로그인 사용자만 · 낙관적 업데이트 |
| M3-1 | 댓글 | ✅ 소프트 삭제 · 편집장 숨김 |
| M4 | AI 썸네일 | ❌ |
계획에서 벗어났던 방향: M2-2 ~ M2-4 는 전부 "글을 넣는 경로" 에 대한 투자였다. 같은 문제를 세 번 다르게 푼 셈이고, 그 사이 원래 계획의 후반부(좋아요·댓글 — 독자가 하는 일)가 밀려 있었다. M3 · M3-1 로 복귀했다.
중복: /write/import(M2-3, 약 330줄)와 노션 동기화(M2-4)는 같은 문제를 푼다.
노션 파이프라인이 실제로 도는 것을 확인하면 /write/import 는 지운다.
아직 안 쓰이는 것: 자동저장·마크다운 툴바(약 300줄)는 /write 를 위한 것인데,
원고를 노션에서 쓰면 /write 를 거의 열지 않는다. 외부 기고자를 초대하기 전까지는
사실상 죽은 코드다. 초대가 시작되면 그때 살아난다 — 그래서 지우지 않는다.
M3 (좋아요) · M3-1 (댓글) · M4 (AI 썸네일) — 기획안 §6 참고. 스키마와 RLS 는 이미 이들을 전제로 깔려 있다.
M3 에서 결정한 것 둘 (마이그레이션 6).
비로그인 좋아요는 만들지 않았다. 쿠키 UUID 는 시크릿 창으로 무한히 새로 만들
수 있고, IP 해시는 개인정보 문제와 NAT 뒤 사용자 문제를 동시에 만든다. 기고를
초대제로 좁힌 지면에서 좋아요만 익명으로 여는 것도 일관되지 않는다. 그래서
로그인 사용자만 누르고, likes 의 기존 RLS 정책이 그대로 방어선이 된다 —
service_role 서버 액션이 필요 없어졌다.
likes_read_all 을 본인 행만으로 좁혔다. using (true) 였을 때는 anon 키로
"누가 어떤 글에 좋아요했는지" 를 전부 뽑을 수 있었다. 개수는 이미
posts.like_count 에 있으므로 화면이 likes 에서 알아야 할 것은 "내가 눌렀나"
하나뿐이다.
npm run dev # 개발 서버
npm run build # 프로덕션 빌드 (Supabase 연결 필요)
npm run typecheck # tsc --noEmit
npm run db:migrate # 마이그레이션 (안 돌린 것만). --dry 로 미리보기
npm run db:admin -- [email protected] # 편집장 지정
npm run test:rls # RLS 정책 우회 테스트 ← M2 / M2-1 완료 기준