@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.
- package/LICENSE +21 -0
- package/README.md +217 -0
- package/api/dist/db/index.js +107 -0
- package/api/dist/db/repos.js +670 -0
- package/api/dist/index.js +89 -0
- package/api/dist/lib/ai.js +314 -0
- package/api/dist/lib/config.js +57 -0
- package/api/dist/lib/crypto.js +71 -0
- package/api/dist/lib/design-system-gen.js +332 -0
- package/api/dist/lib/fixtures.js +150 -0
- package/api/dist/lib/gateway.js +55 -0
- package/api/dist/lib/handoff.js +283 -0
- package/api/dist/lib/items-gen.js +211 -0
- package/api/dist/lib/lint-service.js +118 -0
- package/api/dist/lib/lint.js +141 -0
- package/api/dist/lib/mockup-gen.js +136 -0
- package/api/dist/lib/numbering.js +75 -0
- package/api/dist/lib/provider-errors.js +31 -0
- package/api/dist/lib/render.js +154 -0
- package/api/dist/lib/style-guide.js +47 -0
- package/api/dist/lib/templates.js +85 -0
- package/api/dist/lib/types.js +1 -0
- package/api/dist/lib/wireframes.js +137 -0
- package/api/dist/providers/byok/anthropic.js +75 -0
- package/api/dist/providers/byok/openai-compat.js +95 -0
- package/api/dist/providers/cli.js +391 -0
- package/api/dist/providers/index.js +68 -0
- package/api/dist/providers/managed.js +22 -0
- package/api/dist/providers/sse.js +37 -0
- package/api/dist/providers/stub.js +55 -0
- package/api/dist/providers/types.js +1 -0
- package/api/dist/routes/backup.js +24 -0
- package/api/dist/routes/deliverables.js +588 -0
- package/api/dist/routes/documents.js +205 -0
- package/api/dist/routes/helpers.js +43 -0
- package/api/dist/routes/interview.js +141 -0
- package/api/dist/routes/keys.js +70 -0
- package/api/dist/routes/projects.js +103 -0
- package/api/dist/routes/settings.js +134 -0
- package/api/dist/routes/share.js +39 -0
- package/api/dist/routes/suggestions.js +144 -0
- package/api/templates/design-system.json +17 -0
- package/api/templates/feature-spec.json +73 -0
- package/api/templates/ia.json +52 -0
- package/api/templates/prd.json +60 -0
- package/api/templates/user-flow.json +61 -0
- package/bin/drafting.mjs +79 -0
- package/db/schema.sql +154 -0
- package/package.json +62 -0
- package/web/dist/assets/index-CS06cWP3.js +125 -0
- package/web/dist/assets/index-DWoYeaZU.css +1 -0
- package/web/dist/index.html +14 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as repo from "../db/repos.js";
|
|
3
|
+
import { parse } from "./helpers.js";
|
|
4
|
+
import { config } from "../lib/config.js";
|
|
5
|
+
import { aiMode } from "../providers/index.js";
|
|
6
|
+
import { cliAvailable, resolveCliBin, resetCliBinCache, verifyCliAccess } from "../providers/cli.js";
|
|
7
|
+
import { getDecryptedKey } from "../db/repos.js";
|
|
8
|
+
import { probeGateway, fetchOpenRouterModels } from "../lib/gateway.js";
|
|
9
|
+
const PROVIDERS = ['anthropic', 'openai', 'openrouter'];
|
|
10
|
+
const modelEntry = z.object({
|
|
11
|
+
provider: z.enum(PROVIDERS).optional(),
|
|
12
|
+
model: z.string().optional(),
|
|
13
|
+
maxTokens: z.number().int().positive().optional(),
|
|
14
|
+
});
|
|
15
|
+
export async function settingsRoutes(app) {
|
|
16
|
+
// meta: version, onboarding, feature flags (SPEC-22)
|
|
17
|
+
app.get('/api/meta', async () => {
|
|
18
|
+
const keys = repo.listKeyMeta();
|
|
19
|
+
return {
|
|
20
|
+
version: config.version,
|
|
21
|
+
managedTier: config.managedTier,
|
|
22
|
+
aiStub: config.aiStub,
|
|
23
|
+
onboardingComplete: repo.getSetting('onboarding_complete') ?? false,
|
|
24
|
+
keysConfigured: keys.map((k) => k.provider),
|
|
25
|
+
aiMode: aiMode(),
|
|
26
|
+
cliAvailable: cliAvailable(),
|
|
27
|
+
cliBin: resolveCliBin(),
|
|
28
|
+
agentBinPath: repo.getSetting('agent_bin_path') ?? '',
|
|
29
|
+
openaiBaseUrl: repo.getSetting('openai_base_url') || config.openaiBaseUrl || '',
|
|
30
|
+
// In v1 there is no update server; the client shows the running version.
|
|
31
|
+
// A real deployment can point this at a release feed.
|
|
32
|
+
latestVersion: repo.getSetting('latest_version') ?? config.version,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
app.get('/api/settings', async () => {
|
|
36
|
+
return {
|
|
37
|
+
providerModels: repo.getSetting('provider_models') ?? { default: {} },
|
|
38
|
+
onboardingComplete: repo.getSetting('onboarding_complete') ?? false,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
// per-doc-type model + token budget (SPEC-19)
|
|
42
|
+
app.put('/api/settings/models', async (req) => {
|
|
43
|
+
const body = parse(z.object({
|
|
44
|
+
default: modelEntry.optional(),
|
|
45
|
+
prd: modelEntry.optional(),
|
|
46
|
+
'feature-spec': modelEntry.optional(),
|
|
47
|
+
ia: modelEntry.optional(),
|
|
48
|
+
'user-flow': modelEntry.optional(),
|
|
49
|
+
}), req.body);
|
|
50
|
+
const current = (repo.getSetting('provider_models') ?? {});
|
|
51
|
+
const merged = { ...current, ...body };
|
|
52
|
+
repo.setSetting('provider_models', merged);
|
|
53
|
+
return merged;
|
|
54
|
+
});
|
|
55
|
+
// OpenAI 호환 게이트웨이(LiteLLM·Azure·사내 프록시) 엔드포인트 설정.
|
|
56
|
+
// base 를 비우면 표준 OpenAI. 회사 게이트웨이 주소·키는 여기(런타임 설정)에만 둔다.
|
|
57
|
+
app.put('/api/settings/openai-endpoint', async (req) => {
|
|
58
|
+
const body = parse(z.object({
|
|
59
|
+
baseUrl: z.string().optional(),
|
|
60
|
+
headers: z.record(z.string()).optional(),
|
|
61
|
+
}), req.body ?? {});
|
|
62
|
+
if (body.headers)
|
|
63
|
+
repo.setSetting('openai_headers', body.headers);
|
|
64
|
+
const raw = (body.baseUrl ?? '').trim();
|
|
65
|
+
if (!raw) {
|
|
66
|
+
repo.setSetting('openai_base_url', '');
|
|
67
|
+
repo.setSetting('openai_models', []);
|
|
68
|
+
return { baseUrl: '', models: [], detected: 'cleared' };
|
|
69
|
+
}
|
|
70
|
+
// /v1 자동 감지 + 모델 목록 조회 (키가 있으면). 사용자는 호스트만 붙이면 된다.
|
|
71
|
+
const headers = repo.getSetting('openai_headers') ?? {};
|
|
72
|
+
const key = getDecryptedKey('openai');
|
|
73
|
+
let stored = raw.replace(/\/+$/, '');
|
|
74
|
+
let models = [];
|
|
75
|
+
let detected = key ? 'as-is' : 'no-key';
|
|
76
|
+
if (key) {
|
|
77
|
+
const probe = await probeGateway(raw, key, headers);
|
|
78
|
+
if (probe) {
|
|
79
|
+
stored = probe.chatBase;
|
|
80
|
+
models = probe.models;
|
|
81
|
+
detected = stored.endsWith('/v1') ? 'v1' : 'root';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
repo.setSetting('openai_base_url', stored);
|
|
85
|
+
// 게이트웨이 모델 목록 저장 — 모델 미지정 시 첫 모델을 기본값으로 쓴다(claude 기본값 거부 방지).
|
|
86
|
+
repo.setSetting('openai_models', models);
|
|
87
|
+
return { baseUrl: stored, models, detected };
|
|
88
|
+
});
|
|
89
|
+
// 저장된 게이트웨이 base + openai 키로 모델 목록 재조회 (드롭다운 새로고침·마운트용)
|
|
90
|
+
// 표준 OpenRouter(BYOK) 모델 목록 — 공개 /models(키 있으면 반영). 드롭다운용.
|
|
91
|
+
app.get('/api/settings/openrouter-models', async () => {
|
|
92
|
+
const key = getDecryptedKey('openrouter') ?? undefined;
|
|
93
|
+
const models = await fetchOpenRouterModels(key);
|
|
94
|
+
return { models };
|
|
95
|
+
});
|
|
96
|
+
app.get('/api/settings/openai-models', async () => {
|
|
97
|
+
const base = repo.getSetting('openai_base_url') || config.openaiBaseUrl || '';
|
|
98
|
+
const key = getDecryptedKey('openai');
|
|
99
|
+
if (!base || !key)
|
|
100
|
+
return { models: [], baseUrl: base };
|
|
101
|
+
const headers = repo.getSetting('openai_headers') ?? {};
|
|
102
|
+
const probe = await probeGateway(base, key, headers);
|
|
103
|
+
if (probe?.models?.length)
|
|
104
|
+
repo.setSetting('openai_models', probe.models);
|
|
105
|
+
return { models: probe?.models ?? [], baseUrl: probe?.chatBase ?? base };
|
|
106
|
+
});
|
|
107
|
+
// 엔진 모드: CLI(구독) vs BYOK(API 키)
|
|
108
|
+
app.put('/api/settings/ai-mode', async (req) => {
|
|
109
|
+
const body = parse(z.object({ mode: z.enum(['cli', 'byok']) }), req.body);
|
|
110
|
+
repo.setSetting('ai_mode', body.mode);
|
|
111
|
+
return { aiMode: body.mode };
|
|
112
|
+
});
|
|
113
|
+
// 실제 생성 권한까지 검증 — 조직이 Claude Code 접근을 막은 계정을 온보딩에서 미리 잡는다.
|
|
114
|
+
app.post('/api/settings/cli/test', async () => {
|
|
115
|
+
resetCliBinCache();
|
|
116
|
+
return verifyCliAccess();
|
|
117
|
+
});
|
|
118
|
+
// CLI 바이너리 경로 수동 지정(자동 탐색이 실패하는 비표준 설치용). 빈 문자열이면 해제.
|
|
119
|
+
app.put('/api/settings/agent-bin', async (req) => {
|
|
120
|
+
const body = parse(z.object({ path: z.string() }), req.body);
|
|
121
|
+
repo.setSetting('agent_bin_path', body.path.trim());
|
|
122
|
+
resetCliBinCache();
|
|
123
|
+
return { cliBin: resolveCliBin(), cliAvailable: cliAvailable() };
|
|
124
|
+
});
|
|
125
|
+
app.post('/api/settings/onboarding/complete', async () => {
|
|
126
|
+
repo.setSetting('onboarding_complete', true);
|
|
127
|
+
return { ok: true };
|
|
128
|
+
});
|
|
129
|
+
// allow re-opening the wizard (test/dev convenience)
|
|
130
|
+
app.post('/api/settings/onboarding/reset', async () => {
|
|
131
|
+
repo.setSetting('onboarding_complete', false);
|
|
132
|
+
return { ok: true };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as repo from "../db/repos.js";
|
|
2
|
+
import { documentToHtml } from "../lib/render.js";
|
|
3
|
+
import { isExpired } from "./documents.js";
|
|
4
|
+
// Public, read-only share view (SPEC-14). No auth — bearer is the token itself.
|
|
5
|
+
export async function shareRoutes(app) {
|
|
6
|
+
app.get('/s/:token', async (req, reply) => {
|
|
7
|
+
const { token } = req.params;
|
|
8
|
+
const link = repo.getShareByToken(token);
|
|
9
|
+
reply.header('Content-Type', 'text/html; charset=utf-8');
|
|
10
|
+
if (!link || link.revoked === 1) {
|
|
11
|
+
reply.code(404);
|
|
12
|
+
return notice('링크를 찾을 수 없습니다', '이 공유 링크는 존재하지 않거나 취소되었습니다.');
|
|
13
|
+
}
|
|
14
|
+
if (isExpired(link.expires_at)) {
|
|
15
|
+
reply.code(410);
|
|
16
|
+
return notice('만료된 링크', '이 공유 링크는 만료되었습니다. 문서 소유자에게 새 링크를 요청하세요.');
|
|
17
|
+
}
|
|
18
|
+
const doc = repo.getDocument(link.document_id);
|
|
19
|
+
if (!doc) {
|
|
20
|
+
reply.code(404);
|
|
21
|
+
return notice('문서 없음', '공유된 문서가 삭제되었습니다.');
|
|
22
|
+
}
|
|
23
|
+
return documentToHtml(doc.id, { readOnly: true });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function notice(title, message) {
|
|
27
|
+
return `<!doctype html><html lang="ko"><head><meta charset="utf-8" />
|
|
28
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
29
|
+
<title>${title}</title>
|
|
30
|
+
<style>
|
|
31
|
+
:root { color-scheme: light dark; }
|
|
32
|
+
body { display: grid; place-items: center; min-height: 100vh; margin: 0;
|
|
33
|
+
font: 16px/1.6 -apple-system, "Pretendard", system-ui, sans-serif; background: #131315; color: #e7e7ea; }
|
|
34
|
+
.card { text-align: center; padding: 2rem; }
|
|
35
|
+
h1 { font-size: 1.4rem; margin-bottom: 0.5rem; }
|
|
36
|
+
p { color: #9a9aa0; }
|
|
37
|
+
</style></head>
|
|
38
|
+
<body><div class="card"><h1>${title}</h1><p>${message}</p></div></body></html>`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as repo from "../db/repos.js";
|
|
3
|
+
import { HttpError, parse } from "./helpers.js";
|
|
4
|
+
import { rewriteSection } from "../lib/ai.js";
|
|
5
|
+
import { applyLintFixByKey } from "../lib/lint-service.js";
|
|
6
|
+
const SUG_STATUSES = ['open', 'accepted', 'rejected', 'dismissed'];
|
|
7
|
+
export async function suggestionRoutes(app) {
|
|
8
|
+
// ── document proposal queue (green dots / review panel) ──────────────────────
|
|
9
|
+
app.get('/api/documents/:id/suggestions', async (req) => {
|
|
10
|
+
const { id } = req.params;
|
|
11
|
+
if (!repo.getDocument(id))
|
|
12
|
+
throw new HttpError(404, 'document not found');
|
|
13
|
+
const q = parse(z.object({ status: z.enum(SUG_STATUSES).optional() }), req.query ?? {});
|
|
14
|
+
return repo.listSuggestions(id, q.status);
|
|
15
|
+
});
|
|
16
|
+
// ── accept a suggestion (SYSTEM.md §0.4 — 1st option) ────────────────────────
|
|
17
|
+
// Section becomes 'accepted'. For a 'revise', the proposed text (quote_after)
|
|
18
|
+
// is already the section body; accept just confirms it.
|
|
19
|
+
app.post('/api/suggestions/:id/accept', async (req) => {
|
|
20
|
+
const { id } = req.params;
|
|
21
|
+
const sug = repo.getSuggestion(id);
|
|
22
|
+
if (!sug)
|
|
23
|
+
throw new HttpError(404, 'suggestion not found');
|
|
24
|
+
applyAccept(sug);
|
|
25
|
+
const resolved = repo.resolveSuggestion(id, 'accepted');
|
|
26
|
+
return {
|
|
27
|
+
suggestion: resolved,
|
|
28
|
+
section: sug.section_id ? repo.getSection(sug.section_id) : null,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
// ── reject a suggestion (SYSTEM.md §0.4 — 2nd option) ────────────────────────
|
|
32
|
+
// add -> section 'rejected' (body kept, excluded from export)
|
|
33
|
+
// revise -> restore the ORIGINAL text (quote_before), section back to accepted
|
|
34
|
+
app.post('/api/suggestions/:id/reject', async (req) => {
|
|
35
|
+
const { id } = req.params;
|
|
36
|
+
const sug = repo.getSuggestion(id);
|
|
37
|
+
if (!sug)
|
|
38
|
+
throw new HttpError(404, 'suggestion not found');
|
|
39
|
+
applyReject(sug);
|
|
40
|
+
const resolved = repo.resolveSuggestion(id, 'rejected');
|
|
41
|
+
return {
|
|
42
|
+
suggestion: resolved,
|
|
43
|
+
section: sug.section_id ? repo.getSection(sug.section_id) : null,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
// ── rewrite from a user instruction (SYSTEM.md §0.4 — 3rd option) ─────────────
|
|
47
|
+
// Runs the provider (stub-capable), replaces the section with a fresh proposal,
|
|
48
|
+
// resolves this suggestion as 'dismissed', and returns the NEW suggestion.
|
|
49
|
+
app.post('/api/suggestions/:id/rewrite', async (req) => {
|
|
50
|
+
const { id } = req.params;
|
|
51
|
+
const sug = repo.getSuggestion(id);
|
|
52
|
+
if (!sug)
|
|
53
|
+
throw new HttpError(404, 'suggestion not found');
|
|
54
|
+
if (!sug.section_id) {
|
|
55
|
+
throw new HttpError(400, 'this suggestion is not tied to a section and cannot be rewritten');
|
|
56
|
+
}
|
|
57
|
+
const body = parse(z.object({ instruction: z.string().min(1) }), req.body);
|
|
58
|
+
const result = await rewriteSection(sug.section_id, body.instruction);
|
|
59
|
+
if (!result)
|
|
60
|
+
throw new HttpError(404, 'section not found');
|
|
61
|
+
repo.resolveSuggestion(id, 'dismissed');
|
|
62
|
+
return { suggestion: result.suggestion, section: result.section };
|
|
63
|
+
});
|
|
64
|
+
// ── accept all open suggestions in a document ────────────────────────────────
|
|
65
|
+
app.post('/api/documents/:id/accept-all', async (req) => {
|
|
66
|
+
const { id } = req.params;
|
|
67
|
+
const doc = repo.getDocument(id);
|
|
68
|
+
if (!doc)
|
|
69
|
+
throw new HttpError(404, 'document not found');
|
|
70
|
+
const open = repo.listSuggestions(id, 'open');
|
|
71
|
+
// 파괴적 제안(lint 정리 = 대상 항목 제외, delete = 섹션 제외)은 일괄 수락에서
|
|
72
|
+
// 제외한다 — 정합성 지적을 '모두 수락'하면 지적된 항목이 통째로 사라지는 사고 방지.
|
|
73
|
+
// 파괴적 제안은 카드에서 개별적으로만 수락할 수 있다.
|
|
74
|
+
const safe = open.filter((s) => s.kind !== 'lint' && s.kind !== 'delete');
|
|
75
|
+
for (const sug of safe) {
|
|
76
|
+
applyAccept(sug);
|
|
77
|
+
repo.resolveSuggestion(sug.id, 'accepted');
|
|
78
|
+
}
|
|
79
|
+
// Accepting all resolves a stale document (context is now reviewed).
|
|
80
|
+
if (doc.context_stale === 1 && repo.countOpenSuggestions(id) === 0) {
|
|
81
|
+
repo.refreshContext(id);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
accepted: safe.length,
|
|
85
|
+
skippedDestructive: open.length - safe.length,
|
|
86
|
+
sections: repo.listSections(id),
|
|
87
|
+
openRemaining: repo.countOpenSuggestions(id),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function applyAccept(sug) {
|
|
92
|
+
// lint suggestion accepted -> apply the code's default remediation (§4.2)
|
|
93
|
+
if (sug.kind === 'lint') {
|
|
94
|
+
const doc = repo.getDocument(sug.document_id);
|
|
95
|
+
if (doc && sug.quote_before)
|
|
96
|
+
applyLintFixByKey(doc.project_id, sug.quote_before);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// structure-doc item proposal accepted -> the item becomes the document (§1.3)
|
|
100
|
+
if (sug.target_item_id) {
|
|
101
|
+
repo.setItemStatus(sug.target_item_id, 'accepted');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!sug.section_id)
|
|
105
|
+
return; // document-level (e.g. question/stale) — nothing to flip
|
|
106
|
+
const section = repo.getSection(sug.section_id);
|
|
107
|
+
if (!section)
|
|
108
|
+
return;
|
|
109
|
+
if (sug.kind === 'delete') {
|
|
110
|
+
// a delete proposal, once accepted, excludes the section from the document
|
|
111
|
+
repo.setSectionStatus(section.id, 'rejected');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// add / revise / question / stale -> the (proposed) body becomes the document
|
|
115
|
+
repo.setSectionStatus(section.id, 'accepted');
|
|
116
|
+
}
|
|
117
|
+
function applyReject(sug) {
|
|
118
|
+
// lint suggestion rejected = waive (§4.3) — nothing to mutate; the rejected
|
|
119
|
+
// status itself records the waive (read by lint-service.waivedKeys).
|
|
120
|
+
if (sug.kind === 'lint')
|
|
121
|
+
return;
|
|
122
|
+
// structure-doc item proposal rejected -> the item is excluded from the document
|
|
123
|
+
if (sug.target_item_id) {
|
|
124
|
+
repo.setItemStatus(sug.target_item_id, 'rejected');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!sug.section_id)
|
|
128
|
+
return;
|
|
129
|
+
const section = repo.getSection(sug.section_id);
|
|
130
|
+
if (!section)
|
|
131
|
+
return;
|
|
132
|
+
if (sug.kind === 'revise' && sug.quote_before) {
|
|
133
|
+
// keep the original text (SYSTEM.md §0.4): revert body, re-accept the section
|
|
134
|
+
repo.updateSection(section.id, { body: sug.quote_before });
|
|
135
|
+
repo.setSectionStatus(section.id, 'accepted');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (sug.kind === 'add') {
|
|
139
|
+
// reject an added section: body kept but excluded from export
|
|
140
|
+
repo.setSectionStatus(section.id, 'rejected');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// delete/question/stale rejected -> leave the section as-is
|
|
144
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "design-system",
|
|
3
|
+
"docType": "design-system",
|
|
4
|
+
"name": "디자인 시스템",
|
|
5
|
+
"description": "제품의 색·타이포·간격·형태·컴포넌트·톤을 인터뷰로 설계한다. 와이어프레임·시안이 이 시스템으로 렌더된다.",
|
|
6
|
+
"draftGuidance": "인터뷰 답변과 상위 PRD 맥락으로 이 제품에 맞는 디자인 시스템을 설계한다. 색은 단일 강조색이 아니라 역할(배경·표면·본문·보조·경계·강조·상태) 체계로 잡고, 강조색은 주요 액션·활성 상태에만 절제해서 쓴다.",
|
|
7
|
+
"questions": [
|
|
8
|
+
{ "id": "personality", "prompt": "제품이 주는 느낌을 형용사 3개로 표현하면? (사용자가 첫인상으로 느꼈으면 하는 것)", "hint": "예: 신뢰·차분·전문 / 활기·친근·경쾌", "example": "신뢰감 있고 차분하며 전문적" },
|
|
9
|
+
{ "id": "reference", "prompt": "비슷한 느낌의 제품이나 브랜드가 있나요? 왜 그 느낌인가요?", "hint": "구체적인 제품명 + 어떤 점이 좋은지", "example": "Linear — 절제된 색과 촘촘한 정보 밀도가 전문적이라서" },
|
|
10
|
+
{ "id": "context", "prompt": "주로 어디서·어떤 환경에서 쓰나요? (모바일/데스크톱, 밝은 곳/어두운 곳)", "hint": "라이트/다크, 화면 크기, 사용 상황", "example": "데스크톱 위주, 밝은 사무실, 라이트 모드" },
|
|
11
|
+
{ "id": "color", "prompt": "브랜드 색이나 강조색이 있나요? 없다면 성격에서 도출합니다. 피해야 할 색은?", "hint": "HEX 있으면 좋고, 금지색도 적어주세요", "example": "강조는 딥 그린 계열, 빨강은 피하고 싶음" },
|
|
12
|
+
{ "id": "typography", "prompt": "서체 성격과 정보 밀도 취향은? (모던 산세리프 / 클래식 세리프 / 친근 라운드 / 기능적 모노, 촘촘 vs 여유)", "hint": "읽는 느낌 + 화면당 정보량", "example": "모던 산세리프, 정보는 촘촘하게" },
|
|
13
|
+
{ "id": "components", "prompt": "형태 취향은? (각진 vs 둥근 모서리, 그림자·보더, 버튼·카드 느낌)", "hint": "딱딱함↔부드러움, 플랫↔입체", "example": "약간 둥근 모서리, 그림자는 최소, 플랫한 카드" },
|
|
14
|
+
{ "id": "accessibility", "prompt": "접근성이나 지켜야 할 제약이 있나요? (고대비, 색맹 대응, 특정 규정·금지)", "hint": "없으면 '없음'", "example": "고대비 필요, 색만으로 상태 구분 금지" }
|
|
15
|
+
],
|
|
16
|
+
"sections": ["색 시스템", "타이포그래피", "간격·형태", "컴포넌트", "톤 & 보이스"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "feature-spec",
|
|
3
|
+
"docType": "feature-spec",
|
|
4
|
+
"name": "기능명세서",
|
|
5
|
+
"description": "PRD의 각 기능을 동작·데이터·수용 기준 수준으로 구체화하는 문서.",
|
|
6
|
+
"draftGuidance": "너는 시니어 프로덕트 엔지니어다. 상위 PRD 컨텍스트가 주어지면 그 목표와 정합하도록, 각 기능을 사용자 스토리·동작 규칙·수용 기준(체크리스트)·엣지케이스 수준으로 구체화한다. 구현 결정은 검증 가능하게 쓴다. 한국어로 작성한다.",
|
|
7
|
+
"questions": [
|
|
8
|
+
{
|
|
9
|
+
"id": "target_feature",
|
|
10
|
+
"prompt": "이번 명세의 대상 기능은 무엇인가요? (PRD의 어떤 항목)",
|
|
11
|
+
"hint": "한 번에 한 기능에 집중.",
|
|
12
|
+
"example": "단계별 AI 인터뷰"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "user_story",
|
|
16
|
+
"prompt": "핵심 사용자 스토리를 적어주세요. (누가/무엇을/왜)",
|
|
17
|
+
"hint": "As a … I want … so that … 형식.",
|
|
18
|
+
"example": "PM으로서 질문에 답하면 초안이 생성되길 원한다, 빈 화면을 피하려고."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "rules",
|
|
22
|
+
"prompt": "이 기능의 핵심 동작 규칙·제약은 무엇인가요?",
|
|
23
|
+
"hint": "허용/금지, 상태 전이, 검증 규칙.",
|
|
24
|
+
"example": "답변 미완료 시 초안 생성 버튼 비활성, 세션 자동 저장."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "data",
|
|
28
|
+
"prompt": "어떤 데이터를 입력받고 무엇을 저장/출력하나요?",
|
|
29
|
+
"hint": "입력·저장·출력 필드.",
|
|
30
|
+
"example": "입력: 질문 답변 / 저장: 세션·섹션 / 출력: 스트리밍 섹션"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "acceptance",
|
|
34
|
+
"prompt": "완료를 판단할 수용 기준은 무엇인가요?",
|
|
35
|
+
"hint": "체크리스트로 검증 가능하게.",
|
|
36
|
+
"example": "탭 닫고 재접속 시 이전 답변 복원, 재생성은 해당 섹션만 교체."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "edge",
|
|
40
|
+
"prompt": "주의할 엣지케이스·실패 모드는 무엇인가요?",
|
|
41
|
+
"hint": "네트워크 끊김, 빈 입력, 동시 편집 등.",
|
|
42
|
+
"example": "스트리밍 중 새로고침, AI 키 미등록, 매우 긴 답변."
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"sections": [
|
|
46
|
+
"기능 개요",
|
|
47
|
+
"사용자 스토리",
|
|
48
|
+
"동작 규칙 및 제약",
|
|
49
|
+
"데이터 입출력",
|
|
50
|
+
"수용 기준",
|
|
51
|
+
"엣지케이스 및 실패 처리"
|
|
52
|
+
],
|
|
53
|
+
"itemSchema": {
|
|
54
|
+
"root": "groups",
|
|
55
|
+
"note": "번호(F-)는 서버가 매긴다. LLM 은 내용만 반환한다.",
|
|
56
|
+
"shape": {
|
|
57
|
+
"groups": [
|
|
58
|
+
{
|
|
59
|
+
"title": "string",
|
|
60
|
+
"features": [
|
|
61
|
+
{
|
|
62
|
+
"title": "string",
|
|
63
|
+
"body": "string (수용 기준, 줄바꿈 구분)",
|
|
64
|
+
"priority": "P0|P1|P2",
|
|
65
|
+
"source": "REQ-01|Q3|PRD §4",
|
|
66
|
+
"links": { "reqs": ["REQ-01"], "pages": ["PG-02"], "flows": ["FLOW-01"] }
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ia",
|
|
3
|
+
"docType": "ia",
|
|
4
|
+
"name": "정보구조 (IA)",
|
|
5
|
+
"description": "제품의 화면·네비게이션·콘텐츠 계층 구조를 정의하는 문서. (v2 우선 확장)",
|
|
6
|
+
"draftGuidance": "너는 인포메이션 아키텍트다. 상위 문서의 기능들을 화면과 네비게이션 계층으로 조직한다. 화면 목록, 계층(트리), 화면 간 이동, 주요 콘텐츠 블록을 명확히 한다. 한국어로 작성한다.",
|
|
7
|
+
"questions": [
|
|
8
|
+
{
|
|
9
|
+
"id": "top_nav",
|
|
10
|
+
"prompt": "최상위 네비게이션(주요 영역)은 무엇으로 나눌까요?",
|
|
11
|
+
"hint": "3~6개의 최상위 영역.",
|
|
12
|
+
"example": "프로젝트, 문서 워크스페이스, 설정"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "screens",
|
|
16
|
+
"prompt": "핵심 화면 목록을 적어주세요.",
|
|
17
|
+
"hint": "각 화면의 목적 한 줄.",
|
|
18
|
+
"example": "프로젝트 목록, 문서 워크스페이스, BYOK 설정 위저드"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "hierarchy",
|
|
22
|
+
"prompt": "콘텐츠 계층 구조는 어떻게 되나요? (상위→하위)",
|
|
23
|
+
"hint": "프로젝트 > 문서 > 섹션 식.",
|
|
24
|
+
"example": "프로젝트 > 문서(PRD/기능명세) > 섹션"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "flows_entry",
|
|
28
|
+
"prompt": "사용자가 가장 자주 진입하는 경로는 무엇인가요?",
|
|
29
|
+
"hint": "주요 진입점과 목적지.",
|
|
30
|
+
"example": "대시보드 → 프로젝트 → 문서 작성"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"sections": [
|
|
34
|
+
"네비게이션 구조",
|
|
35
|
+
"화면 목록",
|
|
36
|
+
"콘텐츠 계층",
|
|
37
|
+
"주요 이동 경로"
|
|
38
|
+
],
|
|
39
|
+
"itemSchema": {
|
|
40
|
+
"root": "pages",
|
|
41
|
+
"note": "번호(PG-)는 서버가 매긴다. LLM 은 내용만 반환한다.",
|
|
42
|
+
"shape": {
|
|
43
|
+
"pages": [
|
|
44
|
+
{
|
|
45
|
+
"title": "string",
|
|
46
|
+
"page_type": "LIST|DETAIL|FORM|DASH|SETTINGS|GENERIC",
|
|
47
|
+
"links": { "features": ["F-01-1"] }
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "prd",
|
|
3
|
+
"docType": "prd",
|
|
4
|
+
"name": "PRD (제품 요구사항 문서)",
|
|
5
|
+
"description": "제품의 배경·문제·목표·범위를 정의하는 최상위 기획 문서.",
|
|
6
|
+
"draftGuidance": "너는 숙련된 프로덕트 매니저다. 아래 인터뷰 답변을 바탕으로 명확하고 검증 가능한 PRD 섹션을 작성한다. 모호한 미사여구를 피하고, 측정 가능한 목표와 구체적 사용자 시나리오를 쓴다. 한국어로 작성한다.",
|
|
7
|
+
"questions": [
|
|
8
|
+
{
|
|
9
|
+
"id": "problem",
|
|
10
|
+
"prompt": "어떤 문제를 해결하려 하나요? 지금 사용자는 이 문제를 어떻게 겪고 있나요?",
|
|
11
|
+
"hint": "현상(as-is)의 고통 지점을 구체적으로.",
|
|
12
|
+
"example": "IT 기획자가 PRD를 매번 백지에서 시작해 반나절씩 소모한다."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "audience",
|
|
16
|
+
"prompt": "핵심 대상 사용자는 누구인가요? (역할·상황)",
|
|
17
|
+
"hint": "1차 사용자와 2차 이해관계자를 구분.",
|
|
18
|
+
"example": "1차: 스타트업 PM / 2차: 개발팀 리드, 외부 클라이언트"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "value",
|
|
22
|
+
"prompt": "이 제품이 제공하는 핵심 가치 한 문장은 무엇인가요?",
|
|
23
|
+
"hint": "'무엇을 더 빠르게/저렴하게/정확하게' 형태로.",
|
|
24
|
+
"example": "AI 인터뷰로 PRD 초안을 30분 내 완성한다."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "goals",
|
|
28
|
+
"prompt": "성공을 어떻게 측정하나요? 측정 가능한 목표 지표를 적어주세요.",
|
|
29
|
+
"hint": "정량 지표 위주(시간·비율·횟수).",
|
|
30
|
+
"example": "문서 1건 작성 시간 4시간 → 30분, 주간 활성 프로젝트 20건."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "features",
|
|
34
|
+
"prompt": "MVP에 반드시 있어야 하는 핵심 기능 3~5가지는 무엇인가요?",
|
|
35
|
+
"hint": "‘있으면 좋은 것’은 제외하고 필수만.",
|
|
36
|
+
"example": "단계별 AI 인터뷰, 스트리밍 초안, 구조 편집기, MD 내보내기"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "nonscope",
|
|
40
|
+
"prompt": "이번 버전에서 명시적으로 다루지 않을 범위(비범위)는 무엇인가요?",
|
|
41
|
+
"hint": "스코프 크리프 방지용.",
|
|
42
|
+
"example": "실시간 협업, 결제, 모바일 앱은 v2 이후."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "risks",
|
|
46
|
+
"prompt": "가장 큰 리스크와 가정은 무엇인가요?",
|
|
47
|
+
"hint": "틀리면 제품이 흔들리는 전제.",
|
|
48
|
+
"example": "사용자가 자신의 AI 키를 기꺼이 등록한다는 가정."
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"sections": [
|
|
52
|
+
"개요 및 배경",
|
|
53
|
+
"문제 정의",
|
|
54
|
+
"목표와 성공 지표",
|
|
55
|
+
"대상 사용자",
|
|
56
|
+
"핵심 기능 (MVP)",
|
|
57
|
+
"범위 및 비범위",
|
|
58
|
+
"리스크와 가정"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "user-flow",
|
|
3
|
+
"docType": "user-flow",
|
|
4
|
+
"name": "유저플로우",
|
|
5
|
+
"description": "주요 시나리오별 사용자 단계와 분기·예외를 정의하는 문서. (v2 우선 확장)",
|
|
6
|
+
"draftGuidance": "너는 UX 디자이너다. 상위 문서(IA/기능명세)에 정합하도록 핵심 시나리오의 단계별 흐름을 작성한다. 각 단계의 사용자 행동·시스템 반응·분기(성공/실패)를 명확히 한다. 한국어로 작성한다.",
|
|
7
|
+
"questions": [
|
|
8
|
+
{
|
|
9
|
+
"id": "scenario",
|
|
10
|
+
"prompt": "어떤 핵심 시나리오를 흐름으로 그릴까요?",
|
|
11
|
+
"hint": "가장 중요한 여정 1개.",
|
|
12
|
+
"example": "신규 사용자가 첫 PRD를 완성하기까지"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "entry",
|
|
16
|
+
"prompt": "시작 지점과 사용자의 목표는 무엇인가요?",
|
|
17
|
+
"hint": "트리거와 최종 목적.",
|
|
18
|
+
"example": "설치 직후 → PRD 초안 확보"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "happy_path",
|
|
22
|
+
"prompt": "가장 이상적인 성공 경로의 단계들을 순서대로 적어주세요.",
|
|
23
|
+
"hint": "행동 → 반응 → 다음.",
|
|
24
|
+
"example": "키 등록 → 프로젝트 생성 → 인터뷰 → 스트리밍 → 편집 → 내보내기"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "branches",
|
|
28
|
+
"prompt": "주요 분기·예외(실패) 경로는 무엇인가요?",
|
|
29
|
+
"hint": "이탈·오류·되돌아가기.",
|
|
30
|
+
"example": "AI 키 오류 시 설정으로 유도, 스트리밍 중단 시 재개."
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"sections": [
|
|
34
|
+
"시나리오 개요",
|
|
35
|
+
"진입 및 목표",
|
|
36
|
+
"성공 경로 (Happy Path)",
|
|
37
|
+
"분기 및 예외 경로"
|
|
38
|
+
],
|
|
39
|
+
"itemSchema": {
|
|
40
|
+
"root": "flows",
|
|
41
|
+
"note": "번호(FLOW-)는 서버가 매긴다. LLM 은 내용만 반환한다.",
|
|
42
|
+
"shape": {
|
|
43
|
+
"flows": [
|
|
44
|
+
{
|
|
45
|
+
"title": "string",
|
|
46
|
+
"source": "F-01",
|
|
47
|
+
"links": { "features": ["F-01"] },
|
|
48
|
+
"steps": [
|
|
49
|
+
{
|
|
50
|
+
"title": "string",
|
|
51
|
+
"page": "PG-02|null",
|
|
52
|
+
"node": "start|screen|decision|end",
|
|
53
|
+
"branch": { "label": "예", "from_step": "FLOW-01.4" },
|
|
54
|
+
"note": "겹침 재판정"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|