尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证전자책 & 오디오북 퍼블리싱 플랫폼 - Next.js + Supabase
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
읽는 책이 아니라 적용하는 책 — 인터랙티브 워크북 출판 플랫폼 크리에이터가 원고에 워크시트·체크리스트·성찰 질문을 끼워 넣어 출간하면, 독자는 읽으면서 직접 작성하고 그 결과를 계정에 남긴다. 현재 상태: MVP 재구성 중 (M5 완료, 다음은 M6 실사용 검증) 이 저장소는 2026-08-04부터 MVP 재정의 작업 중입니다. 범위 밖 기능을 삭제하고 핵심 루프 하나에 집중합니다. 재구성 이전 코드는 pre-mvp-archive 태그에 보존돼 있습니다. 크리에이터가 워크북형 전자책을 출간한다 → 독자가 구매해 읽으며 직접 작성한다 → 작성 내용이 계정에 남고 내보낼 수 있다 → 크리에이터가 참여 반응을 본다 이 루프의 심장은 독자 응답 데이터다. M1에서 스키마와 도메인 모델을 세웠고(workbook_blocks / workbook_block_fields / workbook_responses), M2에서 저작 측 쓰기 경로를, M3에서 독자 측 쓰기 경로를 붙였다. 이제 독자가 쓴 답은 계정에 남고, 다른 기기에서 이어서 쓸 수 있다. localStorage는 오프라인 캐시로 강등됐다 — 진실의 원천은 DB다.查看完整 README ↗
해 본 사람이 쓴 실전 워크북 — 읽는 책이 아니라 적용하는 책
2026-10-12 서비스명을 Inspic에서 SecretTome으로 바꿨습니다(도메인
secrettome.com).
크리에이터가 원고에 워크시트·체크리스트·성찰 질문을 끼워 넣어 출간하면, 독자는 읽으면서 직접 작성하고 그 결과를 계정에 남긴다.
현재 상태: MVP 재구성 중 (M5 완료, 다음은 M6 실사용 검증) 이 저장소는 2026-08-04부터 MVP 재정의 작업 중입니다. 범위 밖 기능을 삭제하고 핵심 루프 하나에 집중합니다. 재구성 이전 코드는
pre-mvp-archive태그에 보존돼 있습니다.
크리에이터가 워크북형 전자책을 출간한다 → 독자가 구매해 읽으며 직접 작성한다 → 작성 내용이 계정에 남고 내보낼 수 있다 → 크리에이터가 참여 반응을 본다
이 루프의 심장은 독자 응답 데이터다. M1에서 스키마와 도메인 모델을 세웠고(workbook_blocks / workbook_block_fields / workbook_responses), M2에서 저작 측 쓰기 경로를, M3에서 독자 측 쓰기 경로를 붙였다. 이제 독자가 쓴 답은 계정에 남고, 다른 기기에서 이어서 쓸 수 있다. localStorage는 오프라인 캐시로 강등됐다 — 진실의 원천은 DB다.
M4에서 그 앞단인 판매를 닫았다. 결제 승인 뒤 어디서 끊겨도 구매가 생기거나 돈이 돌아간다. 구매 기록을 만들 수 있는 것은 승인을 확인한 서버뿐이고(클라이언트 INSERT 경로를 없앴다), 유료 책의 첫 챕터는 누구나 읽을 수 있다.
| 내용 | 상태 | |
|---|---|---|
| M0 | 범위 밖 코드 삭제, 중복 라우트 통합, 문서 정합화 | 완료 |
| M1 | 도메인 재설계 — 워크북 응답 스키마, 인증 트리거 이관, 테스트 도입 | 완료 |
| M2 | 워크북 저작 — 블록 정의 동기화, 이미지 저장소, 공개 전 검수 | 완료 |
| M3 | 워크북 독서 (독자 루프) — 응답이 DB에 저장, 리더 재작성 | 완료 |
| M4 | 판매·접근 제어 — 결제 보상 트랜잭션, webhook, 첫 챕터 미리보기 | 완료 |
| M5 | 응답 회수 — 내 워크북, 응답 포함 PDF, 크리에이터 참여 지표 | 완료 |
| M6 | 실사용 검증 — 본인 콘텐츠 1권 + 저자 2~3명 | 예정 |
각 마일스톤은 "코드가 존재한다"가 아니라 **"사용자가 끝까지 통과한다"**로 완료를 판정한다.
auth-store. Google·Kakao 로그인은 제공자 계정을 만들 때 다시 붙입니다 (/auth/callback은 남겨 둠). 프로필 행은 auth.users INSERT 트리거가 만듭니다chapter-images)에 업로드docs/agent-knowledge/paged-reader-decision.md). 답은 workbook_responses에 저장되고 다른 기기에서 이어집니다. 저장 상태를 화면에 표시합니다. 로그인 없이도 열리며, 답이 계정에 남지 않는 동안에는 화면이 그렇게 말합니다@react-pdf/renderer). EPUB(자체 생성기)은 /api/epub으로 남아 있지만 화면에서는 가려 두었습니다 — 응답을 싣지 않아 워크북 루프에 쓰이지 않습니다ko로 만듭니다(books.language 기본값)/creator/analytics)PUT /api/books/[bookId])가 차단 항목을 검사해 막히면 그 사유를 보여 줍니다. 경고는 공개 뒤 알림으로 알립니다. /create/preview/[bookId]의 미리보기와 검수 패널은 저자가 원할 때 확인하는 곳입니다M0에서 삭제했습니다. 되돌리려면 pre-mvp-archive 태그를 참조하세요.
TTS·오디오북 전체 · 죽은 리더 서브시스템(하이라이트/북마크/진행률/페이지네이션 — 쪽 넘김은 2026-10-22 C안으로 새로 들였고, 진행률 저장은 여전히 제외) · 시리즈 연재 시스템 · 알림 · 다국어(next-intl) · AI 보조(요약/교정/번역/커버) · 협업 저작 · 리뷰·팔로우 · 워크북 템플릿 7종(Toggle, N열, Before/After, 사분면, OKR, 습관 트래커, WOOP)
프레임워크 Next.js 16 (App Router)
언어 TypeScript 5.9
UI React 19 + Tailwind CSS 4 + Radix UI
상태관리 Zustand 5
데이터 페칭 TanStack Query
백엔드 Supabase (Auth + PostgreSQL + Storage)
에디터 Tiptap 3
결제 Toss Payments
PDF/EPUB @react-pdf/renderer, pdf-lib, 자체 EPUB 생성기
XSS 방지 DOMPurify (isomorphic-dompurify)
배포 Vercel
src/
├── app/
│ ├── api/ # API 라우트 22개
│ │ ├── analytics/ # 판매 + 워크북 참여 지표
│ │ ├── books/ # 책 CRUD, 접근 권한, 커버, 본문 이미지, 상세,
│ │ │ # 공개 전 검수, 독자 응답
│ │ ├── chapters/ # 챕터 CRUD
│ │ ├── epub/ pdf/ # 내보내기 (EPUB은 화면에서 가림)
│ │ ├── explore/ # 탐색
│ │ ├── landing/ # 랜딩 데이터
│ │ ├── payments/ # 결제 요청/승인/webhook
│ │ ├── purchases/ # 구매 내역
│ │ └── upload/ # 파일 업로드
│ ├── auth/ # 로그인/회원가입/콜백
│ ├── book/[bookId]/ # 책 상세 (공개, 서버 컴포넌트 + 메타데이터)
│ ├── create/ # 생성 · 업로드 · 편집 · 미리보기
│ ├── creator/ # 크리에이터 스튜디오 (+ 분석)
│ ├── explore/ # 탐색
│ ├── my/ # 내 서재 · 구매 내역 · 설정
│ ├── payments/ # 결제 플로우
│ └── reader/[bookId]/ # 리더
├── components/
│ ├── editor/ # Tiptap 에디터 + 워크북 노드 5종
│ ├── reader/ # 본문 렌더러 + 응답 provider + 워크북 템플릿 5종
│ ├── landing/ explore/ dashboard/ analytics/ preview/ upload/ layout/ ui/
├── lib/
│ ├── workbook/ # 워크북 도메인 — 블록 추출·동기화, 응답 검증·병합·
│ │ # 복원, 오프라인 캐시, 타입
│ ├── publish-checks* # 공개 전 검수 (순수 판정 + DB 로더)
│ ├── upload-parser.ts # 원고 → 챕터 파싱 (txt/md/docx)
│ └── … # supabase, sanitize, access-control, epub, pdf, toss
├── stores/ # Zustand (auth)
└── types/ # 공통 타입
supabase/migrations/ # 00001 초기 스키마(M1 통합 리셋) + 00002 블록 동기화·이미지 버킷(M2)
# + 00003 결제 이행 RPC·구매 INSERT 봉인·첫 챕터 미리보기(M4)
# + 00004 참여 지표에서 소유자 응답 제외(M5)
workbook_blocks 블록 정의. id = 에디터의 data-node-id (생성 시 1회 부여, 불변)
└ workbook_block_fields 블록 안의 문항. (block_id, field_key) 유일
workbook_responses 독자 응답. (user_id, block_id, field_key) 유일
응답은 오직 (block_id, field_key)로만 정의와 만난다. 배열 인덱스나 순서는 정체성에 들어가지 않으므로, 크리에이터가 문항을 추가·삭제·이동해도 남은 응답은 제자리에 붙는다.
workbook_responses.block_id에는 FK가 없다. 크리에이터가 문항을 지웠다고 독자가 쓴 내용까지 지워지면 안 되기 때문이다. 정의가 사라진 응답은 orphanedResponses()로 따로 다룬다.
data-* 속성에는 독자 응답을 담지 않는다. 챕터 HTML은 문항만 싣고, 답은 전부 workbook_responses에 있다.
쓰기 경로는 방향마다 하나뿐이다.
저작: 챕터 저장 → syncChapterWorkbookBlocks() → sync_chapter_workbook_blocks RPC
독자: 리더 입력 → WorkbookResponsesProvider → PUT /api/books/[id]/responses
저작 측 RPC는 upsert와 삭제를 한 트랜잭션으로 처리하고, 응답 테이블은 건드리지 않는다.
독자 측은 리더가 (block_id, field_key, 값)만 보낸다. 어느 챕터인지, 어느 값 컬럼에 넣을지는 서버가 DB의 블록 정의에서 읽어 정한다. 그래서 정의가 DB에 없는 블록에는 응답이 매달리지 않고, 남의 챕터 ID를 실어 보낼 수도 없다.
npm install
cp .env.example .env.local # 로컬 값 입력
npm run dev
Supabase 프로젝트에는 supabase/migrations/를 파일명 순서대로 적용합니다 (supabase db push 또는 SQL 에디터). 00002는 블록 동기화 RPC와 chapter-images 버킷을, 00003은 결제 이행 RPC와 첫 챕터 미리보기 정책을 만듭니다 — 적용하지 않으면 챕터 저장 시 블록 정의가 반영되지 않고, 본문 이미지 업로드와 결제 승인 반영이 실패합니다.
Toss webhook은 상점 관리자에서 https://<도메인>/api/payments/webhook을 등록하세요. 등록하지 않아도 결제는 되지만, 승인 직후 창을 닫은 경우를 메우지 못합니다.
npm run typecheck # 통과 (에러 0)
npm run build # 통과
npm test # 통과 (320개)
npm run lint # 10개 에러 — 재구성 이전부터 존재하는 부채 (아래 참조)
| 항목 | 위치 | 해소 시점 |
|---|---|---|
| 미리보기에서 쓴 답(익명 캐시)을 구매·로그인 후 옮겨 주지 않음 | lib/workbook/response-cache.ts | 미정 |
| EPUB 내보내기에는 응답이 담기지 않음 (빈 워크시트) — 그래서 화면에서 가림 | api/epub, dashboard/ExportMenu | 필요해지면 |
| 실제 Toss 테스트 결제로 4개 시나리오를 밟아 보지 않음 (DB·보상 로직은 테스트가 덮음) | — | 키 확보 시 |
페이지 대부분이 "use client" — 공개 콘텐츠 SEO 부재 | app/** | M6 이후 |
React Compiler lint 에러 9개 (setState-in-effect, any 5개 등) | 아래 파일들 | 별도 정리 |
M1에서 해소됨: 응답의 배열 인덱스 매칭 · data-node-id 재생성 · 클라이언트 프로필 생성 이중 경로 · 테스트 0개 · SMART 블록의 data-template-type 불일치.
M5에서 해소됨: 크리에이터 미리보기 응답이 집계에 섞이던 것 · PDF에 한글이 깨져 나오던 것 · /api/pdf·/api/epub의 권한 판정이 구매를 보지 않던 것 · 두 라우트가 없는 profiles 테이블을 조회해 저자명이 늘 비어 있던 것.
M2에서 해소됨: 블록 정의가 DB에 반영되지 않던 것 · 본문 이미지 base64 인라인 · 업로드 파서가 라우트에 묶여 있던 것 · 출간해도 visibility가 private이라 아무에게도 보이지 않던 것.
M3에서 해소됨: 워크북 응답이 localStorage에만 있던 것(기기 간 유실, 크리에이터 조회 불가) · 리더의 하드코딩된 fontSize/theme · 로그인 후 원래 보던 화면으로 돌아오지 못하던 것.
M4에서 해소됨: 로그인 사용자가 자기 이름으로 purchases 행을 넣어 유료 책을 열 수 있던 것 · 승인 후 구매 기록 생성 실패 시 보상 없던 것 · 재확인(ALREADY_PROCESSED_PAYMENT)을 승인 실패로 보고 끝난 결제를 aborted로 덮던 것 · 창을 닫으면 결제가 유실되던 것(webhook 없음) · UNIQUE(user_id, book_id)가 재구매를 막던 것 · 응답 캐시가 사용자별로 나뉘지 않던 것 · .env.example의 Toss 클라이언트 키 이름이 코드와 달랐던 것.
React Compiler 에러 위치: editor/SlashCommandMenu, editor/extensions/SlashCommand(any 5개), explore/SearchBar, preview/PreviewFrame, upload/FileDropzone
data-* 속성과 input[type=checkbox]만 선별 허용getUser()로 JWT를 서버 검증하며, 실패 시 미인증 처리 (검증 없는 getSession() 폴백 없음)books.price를 읽어 결정하고, 반영할 금액은 Toss가 승인한 값을 씁니다. 구매 기록에는 INSERT 정책이 없습니다 — 승인을 확인한 서버가 fulfill_payment()로만 만듭니다. webhook은 본문을 믿지 않고 Toss에 다시 물어 확인합니다workbook_response_stats() 집계 함수로만 조회합니다src/lib/supabase/__tests__/rls.test.ts). 유료 콘텐츠 차단과 응답 격리가 여기서 고정됩니다Private