@hanmariyang/drafting 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/api/dist/db/index.js +107 -0
  4. package/api/dist/db/repos.js +670 -0
  5. package/api/dist/index.js +89 -0
  6. package/api/dist/lib/ai.js +314 -0
  7. package/api/dist/lib/config.js +57 -0
  8. package/api/dist/lib/crypto.js +71 -0
  9. package/api/dist/lib/design-system-gen.js +332 -0
  10. package/api/dist/lib/fixtures.js +150 -0
  11. package/api/dist/lib/gateway.js +55 -0
  12. package/api/dist/lib/handoff.js +283 -0
  13. package/api/dist/lib/items-gen.js +211 -0
  14. package/api/dist/lib/lint-service.js +118 -0
  15. package/api/dist/lib/lint.js +141 -0
  16. package/api/dist/lib/mockup-gen.js +136 -0
  17. package/api/dist/lib/numbering.js +75 -0
  18. package/api/dist/lib/provider-errors.js +31 -0
  19. package/api/dist/lib/render.js +154 -0
  20. package/api/dist/lib/style-guide.js +47 -0
  21. package/api/dist/lib/templates.js +85 -0
  22. package/api/dist/lib/types.js +1 -0
  23. package/api/dist/lib/wireframes.js +137 -0
  24. package/api/dist/providers/byok/anthropic.js +75 -0
  25. package/api/dist/providers/byok/openai-compat.js +95 -0
  26. package/api/dist/providers/cli.js +391 -0
  27. package/api/dist/providers/index.js +68 -0
  28. package/api/dist/providers/managed.js +22 -0
  29. package/api/dist/providers/sse.js +37 -0
  30. package/api/dist/providers/stub.js +55 -0
  31. package/api/dist/providers/types.js +1 -0
  32. package/api/dist/routes/backup.js +24 -0
  33. package/api/dist/routes/deliverables.js +588 -0
  34. package/api/dist/routes/documents.js +205 -0
  35. package/api/dist/routes/helpers.js +43 -0
  36. package/api/dist/routes/interview.js +141 -0
  37. package/api/dist/routes/keys.js +70 -0
  38. package/api/dist/routes/projects.js +103 -0
  39. package/api/dist/routes/settings.js +134 -0
  40. package/api/dist/routes/share.js +39 -0
  41. package/api/dist/routes/suggestions.js +144 -0
  42. package/api/templates/design-system.json +17 -0
  43. package/api/templates/feature-spec.json +73 -0
  44. package/api/templates/ia.json +52 -0
  45. package/api/templates/prd.json +60 -0
  46. package/api/templates/user-flow.json +61 -0
  47. package/bin/drafting.mjs +79 -0
  48. package/db/schema.sql +154 -0
  49. package/package.json +62 -0
  50. package/web/dist/assets/index-CS06cWP3.js +125 -0
  51. package/web/dist/assets/index-DWoYeaZU.css +1 -0
  52. package/web/dist/index.html +14 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hanmariyang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # Drafting — AI 기획 워크스페이스
2
+
3
+ > IT 기획자·PM이 **단계별 AI 인터뷰**로 PRD·기능명세서 같은 기획 문서를 빠르게 완성하는
4
+ > **셀프호스팅 오픈소스** 도구. 자신의 AI 키를 연결(BYOK)해 로컬/서버에 설치하고, 결과물을
5
+ > MD·HTML로 팀·클라이언트와 공유한다.
6
+
7
+ **AI는 제안을 쓰고, 문서는 당신이 씁니다.** 마스코트 **초안이**(제안을 물어오는 초안 종이)가 첫 실행·빈 상태·에러에서 안내합니다. AI가 쓴 모든 문장은 제안(그린 하이라이트)으로 들어오고, 수락해야만 문서가 됩니다. 랜딩: [hanmariyang.github.io/drafting](https://hanmariyang.github.io/drafting)
8
+
9
+ ---
10
+
11
+ ## 무엇을 하나
12
+
13
+ 1. **AI 인터뷰** — 문서 유형별 질문(힌트·예시 포함)에 답하면
14
+ 2. **스트리밍 초안** — AI가 섹션 단위로 초안을 실시간 생성하고, **완료된 섹션은 즉시 편집 가능**
15
+ 3. **구조 편집기** — 분할 미리보기·드래그 재정렬·자동 저장·버전 히스토리
16
+ 4. **내보내기** — Markdown 다운로드 / 읽기 전용 HTML 공유 링크(만료 설정 가능)
17
+ 5. **엔진 2모드** — 기본은 **Claude Code(구독)**: 로컬 CLI 를 데몬처럼 구동해 **API 키가 필요 없다**
18
+ (데스크톱 앱·호스트 셀프호스트). CLI 가 없는 docker 환경은 **BYOK**(Anthropic·OpenAI·OpenRouter 키,
19
+ 암호화 저장) 폴백. 설정에서 언제든 전환.
20
+
21
+ 문서는 **체인**을 이룬다: `PRD → 기능명세 → IA → 유저플로우`. 하위 문서는 상위 컨텍스트를
22
+ 승계하며, 상위가 바뀌면 하위에 **"컨텍스트 갱신 필요"** 배지가 뜬다 — 단, **자동으로 덮어쓰지
23
+ 않는다**(사용자 명시 승인).
24
+
25
+ ---
26
+
27
+ ## 빠른 시작 — Docker (권장, 원클릭)
28
+
29
+ 빌드 없이 배포된 이미지로 바로 실행:
30
+
31
+ ```bash
32
+ docker run -p 8477:8080 -v drafting:/data ghcr.io/hanmariyang/drafting:latest
33
+ # → http://localhost:8477 접속
34
+ ```
35
+
36
+ 또는 소스에서 빌드:
37
+
38
+ ```bash
39
+ cp .env.example .env # (선택) 값 조정
40
+ docker compose up --build # 빌드 후 기동
41
+ # → http://localhost:8477 접속
42
+ ```
43
+
44
+ 첫 접속 시 설정 위저드가 뜬다. **Claude Code CLI 가 감지되면 키 없이 바로 시작**, 아니면 BYOK 키 1개 등록.
45
+
46
+ > **조직 계정이라면**: 회사·조직 계정으로 로그인된 Claude Code 는 조직이 구독 접근을 막아둔 경우
47
+ > 생성이 거부된다(`disabled Claude subscription access for Claude Code`). '키 없이 시작' 을 누르면
48
+ > 앱이 실제 생성 권한을 먼저 확인하고, 막혀 있으면 **BYOK 키 등록** 화면으로 안내한다.
49
+ > 이 경우 개인 구독 계정으로 CLI 를 다시 로그인(`claude /login`)하거나, Anthropic/OpenRouter API 키를 등록하면 된다.
50
+
51
+ > **OpenAI 호환 게이트웨이(LiteLLM·Azure·사내 프록시)를 쓰려면**: 설정 화면의
52
+ > **"OpenAI 호환 게이트웨이"** 에 base URL 을 넣거나(예: `https://gateway.example.com/v1`),
53
+ > 환경변수 `OPENAI_BASE_URL`(또는 `LITELLM_BASE_URL`)을 설정한다. 그다음 **openai** 키 칸에
54
+ > 게이트웨이 키를 등록하고, 모델 설정에 게이트웨이가 제공하는 모델 id 를 지정하면 된다.
55
+ > 게이트웨이 주소·키는 설정/환경에만 두며 저장소 코드에는 넣지 않는다.
56
+
57
+ > **키 없이 데모만 보고 싶다면**: `.env`에 `AI_STUB=1`을 설정하고 `docker compose up`.
58
+ > 오프라인 스텁 AI가 결정적 초안을 생성하므로 전체 흐름을 키 없이 체험할 수 있다.
59
+
60
+ 포트를 바꾸려면 `.env`의 `DRAFTING_PORT`를 조정한다.
61
+
62
+ ---
63
+
64
+ ## 로컬 개발 (Docker 없이)
65
+
66
+ 요구: **Node ≥ 22** (권장 24/26 — 내장 `node:sqlite` 사용, 네이티브 컴파일 없음).
67
+
68
+ ```bash
69
+ npm install # 루트에서 (api + web 워크스페이스 동시 설치)
70
+ npm run dev # api(:8080) + web(:5173) 동시 기동
71
+ # → http://localhost:5173 (vite가 /api·/s 를 :8080 으로 프록시)
72
+ ```
73
+
74
+ 개별 실행:
75
+
76
+ ```bash
77
+ npm run dev:api # Fastify API (tsx/watch) :8080
78
+ npm run dev:web # Vite dev server :5173
79
+ ```
80
+
81
+ 테스트:
82
+
83
+ ```bash
84
+ npm test # api 유닛/통합 테스트 (node:test, 스텁 AI)
85
+ ```
86
+
87
+ 프로덕션 빌드:
88
+
89
+ ```bash
90
+ npm run build # web → web/dist, api → api/dist
91
+ npm start # node api/dist/index.js (api가 web/dist 정적 서빙)
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 구조
97
+
98
+ ```
99
+ .
100
+ ├── api/ # Fastify + node:sqlite 백엔드 (TypeScript, ESM)
101
+ │ ├── src/
102
+ │ │ ├── index.ts # 서버 부트스트랩 (SPA 정적 서빙 포함)
103
+ │ │ ├── db/ # 스키마 적용 + 리포지토리 (Project·Document·Section·InterviewSession …)
104
+ │ │ ├── lib/ # config · crypto(BYOK 암호화) · templates · ai(스트리밍 오케스트레이션) · render
105
+ │ │ ├── providers/ # AI 추상 레이어 (§ 아키텍처)
106
+ │ │ └── routes/ # projects · documents · interview · keys · settings · share
107
+ │ ├── templates/ # 인터뷰 템플릿 JSON (외부 파일 — 하드코딩 아님)
108
+ │ └── test/ # node:test 스위트
109
+ ├── web/ # Vite + React SPA (단일 워크스페이스 화면)
110
+ │ └── src/
111
+ │ ├── pages/ # StartScreen · ProjectView · DocumentWorkspace · Settings
112
+ │ └── components/ # InterviewPanel · SectionCanvas · Onboarding · Version/Share/Context 모달
113
+ ├── db/schema.sql # SQLite 스키마 (부팅 시 idempotent 적용)
114
+ ├── docs/spec/ # 선결 과제 설계서 (context-chain, ux-mode-transition)
115
+ ├── docker-compose.yml # 단일 서비스 원클릭
116
+ └── Dockerfile # 멀티스테이지 (web 빌드 + api 빌드 → 런타임)
117
+ ```
118
+
119
+ ### 데이터 모델
120
+
121
+ `Project 1—N Document 1—N Section`. `Document`는 `parent_document_id`로 체인을 이룬다.
122
+ `InterviewSession`이 답변·진행률을 저장(자동 저장·재개). 부가: `api_keys`(암호화),
123
+ `document_versions`(스냅샷), `share_links`, `settings`. 스키마는 `db/schema.sql` 참조.
124
+
125
+ ---
126
+
127
+ ## 아키텍처 — AI 호출 추상 레이어
128
+
129
+ 모든 AI 호출은 `api/src/providers/`의 추상 인터페이스(`AIProvider`)를 경유한다. **라우트/로직에서
130
+ 제공자 API를 직접 호출하지 않는다.**
131
+
132
+ ```
133
+ AIProvider (interface)
134
+ ├── BYOKProvider (v1) Anthropic / OpenAI / OpenRouter — 사용자 키
135
+ │ ├── AnthropicProvider
136
+ │ └── OpenAICompatProvider ── OpenAIProvider · OpenRouterProvider
137
+ ├── StubProvider 오프라인 결정적 (AI_STUB=1, 테스트/데모)
138
+ └── ManagedProvider (v2) 관리형 티어 — 인터페이스만, 구현 미포함
139
+ ```
140
+
141
+ `MANAGED_TIER=true`로 설정하면 `ManagedProvider`로 전환되는 구조를 유지한다(관리형 클라우드
142
+ 티어 대비, 아래 수익 모델 참조). v1은 `ManagedProvider`를 구현하지 않는다.
143
+
144
+ ---
145
+
146
+ ## 수익 모델 (P-03) — 선택 **A: 관리형 클라우드 티어**
147
+
148
+ v1은 **BYOK + 셀프호스팅** 단일 경로다. 장기 방향은 **관리형 클라우드 티어(옵션 A)** 로,
149
+ BYOK·셀프호스팅은 그 상위(고급) 옵션으로 격상한다. v1 구현 범위에는 포함하지 않으나,
150
+ 위 AI 추상 레이어가 `MANAGED_TIER` 분기를 수용하도록 설계되어 있어 티어 전환이 가능하다.
151
+
152
+ ---
153
+
154
+ ## 보안 — BYOK 키 저장
155
+
156
+ - 제공자 키는 **AES-256-GCM으로 암호화**되어 저장된다. **평문으로 DB에 저장하지 않는다.**
157
+ - 마스터 키는 `APP_ENCRYPTION_KEY`(32바이트 base64/hex)에서 오거나, 없으면 첫 부팅 시 생성해
158
+ `data/master.key`(퍼미션 600)에 보관한다. **프로덕션에서는 `APP_ENCRYPTION_KEY`를 명시**해
159
+ 데이터 디렉터리를 지워도 키를 복구할 수 있게 하라.
160
+ - API는 키 메타데이터(마지막 4자리 등)만 반환하며 키 원문을 절대 노출하지 않는다.
161
+ - 공유 HTML은 서버에서 `<script>`/inline 핸들러를 제거해 렌더한다(최소 sanitization).
162
+
163
+ ---
164
+
165
+ ## 환경 변수
166
+
167
+ | 변수 | 기본값 | 설명 |
168
+ |------|--------|------|
169
+ | `DRAFTING_PORT` | `8477` | 호스트 노출 포트 (compose → 컨테이너 8080) |
170
+ | `PORT` | `8080` | api 리슨 포트 |
171
+ | `DATABASE_PATH` | `data/drafting.sqlite` | SQLite 파일 경로 |
172
+ | `APP_ENCRYPTION_KEY` | (없으면 자동 생성) | BYOK 암호화 마스터 키 (32바이트) |
173
+ | `MANAGED_TIER` | `false` | true 시 ManagedProvider(v2, 미구현)로 전환 |
174
+ | `AI_STUB` | (off) | `1` 시 오프라인 스텁 AI (키 불필요) |
175
+
176
+ AI 제공자 키는 **이 파일이 아니라** 앱 내 설정 위저드에서 입력한다(BYOK).
177
+
178
+ ---
179
+
180
+ ## MVP 범위 & 가정 (AI 가정 — 기획 단계 미확정 항목)
181
+
182
+ - **문서 범위**: MVP는 **PRD + 기능명세서**에 집중. IA·유저플로우 템플릿도 포함하나 v2에서 고도화.
183
+ - **AI 인터랙션**: 하이브리드(챗 인터뷰 초안 → 구조 편집기 정제).
184
+ - **기본 제공자**: OpenRouter, BYOK. Anthropic·OpenAI도 지원.
185
+ - **인증**: 로컬 단독 실행(무인증)이 기본. 팀 협업(멀티유저)은 v2.
186
+ - **Notion 내보내기 / PDF**: **v1 미포함**(위원회 결정으로 v2 이연).
187
+ - **템플릿**: 문서 체인(PRD→기능명세→IA→유저플로우) 인터뷰 템플릿은 독립형 JSON
188
+ (`api/templates/`) — 커뮤니티가 파일로 확장 가능.
189
+
190
+ ---
191
+
192
+ ## 사양 커버리지 (개발지시서 대응)
193
+
194
+ | 영역 | 구현 |
195
+ |------|------|
196
+ | P-01 상태 일관성 | `docs/spec/context-chain.md` + 스테일 배지 + 명시 승인 refresh (자동 덮어쓰기 없음) |
197
+ | P-02 UX 모드 최소화 | `docs/spec/ux-mode-transition.md` + 단일 워크스페이스 화면(전용 뷰어 없음) |
198
+ | P-03 수익 모델 | 위 §수익 모델 (옵션 A) + `MANAGED_TIER` 분기 |
199
+ | SPEC-01/02/03 인터뷰 | 템플릿 4종·힌트/예시·자동저장/재개 |
200
+ | SPEC-04 컨텍스트 승계 | 상위 섹션 주입(`getParentContext`) |
201
+ | SPEC-06/07 스트리밍/재생성 | SSE 섹션 스트리밍 · 섹션 단위 재생성 |
202
+ | SPEC-08/09/10 편집기 | 분할 동기 스크롤 · 드래그 재정렬 · 2초 자동 저장 |
203
+ | SPEC-11/12 프로젝트/버전 | 프로젝트 그루핑 · 버전 스냅샷·복원 |
204
+ | SPEC-05 의존 시각화 | 프로젝트 문서 체인 그래프 |
205
+ | SPEC-13/14 내보내기 | MD 다운로드 · 만료형 HTML 공유 링크 |
206
+ | SPEC-18/19 다중 제공자/설정 | 3종 키 등록·테스트 · 유형별 모델·토큰 |
207
+ | SPEC-20/21/22 설치/온보딩/버전 | Docker 원클릭 · 건너뛰기 불가 위저드 · 버전 배너 |
208
+
209
+ **금지 사항 준수**: G-01(평문 키 저장 금지) · G-02(하위 자동 덮어쓰기 금지) · G-03/04(Notion·PDF
210
+ v1 제외) · G-05(3단계 별도 화면 금지) · G-06(템플릿 외부 파일) · G-07(AI 추상 레이어) · G-08(범위
211
+ 확장 금지 — 성공 지표 확정 후 PRD 갱신).
212
+
213
+ ---
214
+
215
+ ## 라이선스
216
+
217
+ MIT.
@@ -0,0 +1,107 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { config } from "../lib/config.js";
5
+ let db = null;
6
+ export function getDb() {
7
+ if (db)
8
+ return db;
9
+ fs.mkdirSync(path.dirname(config.databasePath), { recursive: true });
10
+ db = new DatabaseSync(config.databasePath);
11
+ db.exec('PRAGMA foreign_keys = ON;');
12
+ const schema = fs.readFileSync(config.schemaPath, 'utf8');
13
+ db.exec(schema);
14
+ migrate(db);
15
+ return db;
16
+ }
17
+ /** Open an in-memory db for tests, applying the schema. */
18
+ export function openMemoryDb() {
19
+ const mem = new DatabaseSync(':memory:');
20
+ mem.exec('PRAGMA foreign_keys = ON;');
21
+ mem.exec(fs.readFileSync(config.schemaPath, 'utf8'));
22
+ migrate(mem);
23
+ return mem;
24
+ }
25
+ /**
26
+ * Idempotent, additive migrations for DBs created before a column existed.
27
+ * The base schema uses CREATE TABLE IF NOT EXISTS, so pre-existing tables keep
28
+ * their old shape — bring them forward here. Existing data is stub/demo only,
29
+ * so we only add columns (no destructive rewrites). Any legacy section without
30
+ * an explicit status is backfilled to 'accepted' so old demo docs still export.
31
+ */
32
+ function migrate(d) {
33
+ const cols = d.prepare('PRAGMA table_info(sections)').all();
34
+ if (!cols.some((c) => c.name === 'status')) {
35
+ d.exec("ALTER TABLE sections ADD COLUMN status TEXT NOT NULL DEFAULT 'accepted'");
36
+ }
37
+ // v0.4: structure-doc suggestions target a plan_item. Add the column to
38
+ // suggestions tables created before it existed (default NULL, no REFERENCES on
39
+ // ADD COLUMN — the base schema carries the FK for fresh DBs).
40
+ const sugCols = d.prepare('PRAGMA table_info(suggestions)').all();
41
+ if (sugCols.length && !sugCols.some((c) => c.name === 'target_item_id')) {
42
+ d.exec('ALTER TABLE suggestions ADD COLUMN target_item_id TEXT');
43
+ }
44
+ }
45
+ export function setDb(instance) {
46
+ db = instance;
47
+ }
48
+ export function closeDb() {
49
+ if (db) {
50
+ try {
51
+ db.close();
52
+ }
53
+ catch {
54
+ /* already closed */
55
+ }
56
+ db = null;
57
+ }
58
+ }
59
+ /** 전체 워크스페이스 스냅샷 — WAL 체크포인트 후 DB 파일 바이트를 반환한다. */
60
+ export function backupBytes() {
61
+ const d = getDb();
62
+ try {
63
+ d.exec('PRAGMA wal_checkpoint(TRUNCATE)');
64
+ }
65
+ catch {
66
+ /* WAL 아닐 수 있음 */
67
+ }
68
+ return fs.readFileSync(config.databasePath);
69
+ }
70
+ /**
71
+ * 업로드된 백업으로 DB 를 교체한다. 먼저 임시 파일로 열어 스키마를 검증(projects
72
+ * 조회)한 뒤에만 라이브 DB 를 닫고 파일을 바꿔 다시 연다. 유효하지 않으면 원본 보존.
73
+ */
74
+ export function restoreFromBytes(buf) {
75
+ const target = config.databasePath;
76
+ const tmp = `${target}.restore-tmp`;
77
+ fs.writeFileSync(tmp, buf);
78
+ try {
79
+ const t = new DatabaseSync(tmp);
80
+ t.prepare('SELECT count(*) AS n FROM projects').get(); // 우리 스키마가 아니면 throw
81
+ t.close();
82
+ }
83
+ catch (e) {
84
+ try {
85
+ fs.unlinkSync(tmp);
86
+ }
87
+ catch {
88
+ /* ignore */
89
+ }
90
+ throw new Error(`유효한 Drafting 백업이 아닙니다: ${e.message}`);
91
+ }
92
+ closeDb();
93
+ // WAL/SHM 사이드카를 지워 새 본 파일과 섞이지 않게 한다.
94
+ for (const ext of ['-wal', '-shm']) {
95
+ try {
96
+ fs.unlinkSync(target + ext);
97
+ }
98
+ catch {
99
+ /* 없을 수 있음 */
100
+ }
101
+ }
102
+ fs.renameSync(tmp, target);
103
+ getDb(); // 재오픈 + 마이그레이션
104
+ }
105
+ export function nowIso() {
106
+ return new Date().toISOString();
107
+ }