@pghoya2956/livemap 1.0.1 → 1.1.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,61 @@
2
2
 
3
3
  버전마다 `## [X.Y.Z] - YYYY-MM-DD` 절을 둔다. 릴리스 워크플로가 태그 버전의 절이 있는지 확인한다.
4
4
 
5
+ ## [1.1.1] - 2026-09-17
6
+
7
+ 작업 어댑터가 계획 항목·결정·열린 질문을 파일 하나에서만 센다. 생성물의 키와 형식은 그대로고 값만 바뀐다.
8
+
9
+ - 계획 항목(`pnDone`·`pnOpen`)은 계획 문서(`task_plan.md`, 없으면 `plan.md`)에서만 센다. 1.1.0은 `spec/final.md`의 계획 초안 체크박스를 더해, 실행 중 체크하지 않는 그 목록만큼 분모가 부풀었다(실제 프로젝트 하나에서 작업 48/61이 48/108로 보임). 로드맵 카드·작업 화면·개요 합계가 같이 바로잡힌다. 계획 문서가 없고 스펙에만 체크박스가 있는 작업은 계획 항목이 0이 된다.
10
+ - 결정(`dec`)·열린 질문(`oq`)은 `spec/final.md`에서만 센다. 계획 문서에 옮겨 적은 줄은 세지 않는다.
11
+ - 같은 계획 항목 번호가 스펙과 계획 문서에 모두 있으면 계획 문서의 줄을 정의로 삼는다. 기능 단계 `refs`의 `PN-nn`이 계획 문서의 완료 여부를 따른다(1.1.0은 체크되지 않는 스펙 줄을 읽어 늘 미완이었다).
12
+
13
+ ## [1.1.0] - 2026-09-17
14
+
15
+ 상황판 화면을 React 다크 모니터형으로 다시 만들고, 로드맵 항목을 묶는 마일스톤과 날짜·결정 대기 자료를 더했다. README 「버전」이 major로 정한 항목(config 키, 여정 형식, 어댑터 계약, 명령·종료 코드, 생성물 파일 이름, export 배치, 예산 설정 경로)은 바꾸거나 지우지 않고, 로드맵 형식·어댑터 계약·생성물은 추가만 했다.
16
+
17
+ 화면
18
+
19
+ - 화면 원본을 `ui/`(React)로 옮기고 `scripts/build-ui.mjs`(esbuild)가 `site/index.html`·`map.css`·`map.js`로 번들한다. 파일 이름·위치와 export 배치는 같다. 번들은 커밋하고 CI·릴리스가 다시 빌드해 차이가 없는지 본다. React는 번들 안에 있고 런타임 의존성은 계속 0이다. 번들 끝에 React MIT 고지를 남긴다. `map.js`는 약 33KB에서 약 250KB(gzip 약 77KB)가 된다.
20
+ - 개요: 상단 바(내비 5·배포·경고·신선도 알약·제품 호스트), 전광판, 8패널(마일스톤, 진척, 최근 변경, 기능 지도, 기능별 변경, 특보, 화면 캡처, 기능 현황). 다크 한 벌이고 글자 대비 4.5:1 이상, 상태는 모양으로도 구분한다. 목록은 들어가는 행만 그리고 나머지는 "외 n →" 링크다.
21
+ - 해시 라우터와 하위 화면(기능 목록·스토리보드·단계 상세, 로드맵, 작업, 더보기 6탭)을 React로 옮겼다. 정보와 배치는 1.0.1과 같고, 로드맵은 마일스톤별로 접고, 작업은 진행·대기가 기본 필터다. 경로(`#/overview`·`#/journeys/…`·`#/roadmap/…`·`#/tasks/…`·`#/more/…`)는 같고 `#/roadmap/<id>`가 마일스톤 id도 받는다.
22
+ - 화면 어휘: 여정 → 기능, 장면 → 단계, `planned` → 계획, `next` → 구상. 파일 키·값과 프로젝트 문구는 그대로다.
23
+ - 하위 화면의 여정·로드맵 파일 경로 문구를 고정 문자열 대신 설정 값(`data.json.sources`)으로 보인다.
24
+ - 기능이 13개 이상이면 완성 기능을 기능 지도에서 접는다(지도 12개 이하 유지, 기능 화면에는 전부).
25
+ - 전광판 정지 버튼, 모션 줄임, 기능 지도 키보드 선택, 1279px 이하 두 줄 상단 바.
26
+ - 마지막 방문 뒤 커밋 표시가 시각을 문자열로 비교해 `Z`와 `+09:00`이 섞이면 틀리던 것을 `Date.parse` 비교로 고쳤다.
27
+
28
+ 로드맵과 마일스톤
29
+
30
+ - 로드맵 파일에 `## 마일스톤: <제목>` 절(`id`·`상태`·`완료일`·`목표일`·`결정 대기`)과 항목 키 `마일스톤`을 더했다. 항목 순서·자동 id는 항목 절만 세어 1.0.1과 같다. 마일스톤 절은 1.1.0 이상에서만 쓴다. 1.0.x는 이 절을 로드맵 항목으로 읽으므로 엔진을 먼저 올린다(`docs/semantic-authoring.md`).
31
+ - 로드맵 어댑터가 로드맵 파일 git 이력에서 항목 완료일(`completedAt`)과 결정 대기 시작일(`waitingSince`)을 계산한다. 이름 바꾸기를 따라가고, 미커밋 변경·git 없음은 null, 얕은 클론은 null과 partial이다.
32
+ - 결정 대기 `<주체>: <질문>`을 주체와 질문으로 나눈다(콜론 뒤 공백 필요).
33
+ - 로드맵 항목 막힘(`blockedBy`), 마일스톤 진척·막힘, 현재 마일스톤을 파생한다.
34
+
35
+ 생성물(필드·노드 추가만)
36
+
37
+ - `overview.json`: 첫 화면 자료 전부(`roadmapItems`, `milestones`, `currentMilestone`, `activity`, `changes`, `links`, `captures`, `journeys[]`의 `goal`·`counts`·`roadmapItem`·`milestone`·`commits`·`week`·`series`, 단계 `hits`, `counts`·`signals` 추가 키). 커밋 제목은 사람 커밋만, 관례 접두어·내부 ID·식별자를 지워 싣고 `[bot]` 작성자 커밋은 자동 수로 따로 센다. 크기는 기능·커밋 수에 따라 커진다(엔진 픽스처 3,697B, 실제 프로젝트 하나에서 25,637B).
38
+ - `data.json`: `roadmap[]`의 `milestone`·`completedAt`·`waitingSince`·`waitingWho`·`waitingWhat`·`blockedBy`, `milestones[]`, `issues[]`, `sources`, `tasks[].roadmapItems`. 기존 경로·값과 `commits` 순서는 그대로다.
39
+ - `graph.json`: 노드 종류 `release`(마일스톤, 1.x 임시 이름), `release → milestone` `contains` 엣지, 로드맵 항목 노드 속성, 최상위 `issues[]`.
40
+ - `overviewSlice(d, opts)`가 선택 인자 `opts.sinceDays`(기본 14)를 받는다.
41
+ - 1.0.1이 만들던 필드 중 새 화면이 쓰지 않는 것(`line`, `running`, `waiting`, `tasks`, `openQuestions`, `areas`, `recent`, `roadmap[]`)도 남긴다. 정리는 2.0.0 후보다.
42
+
43
+ 어댑터 계약
44
+
45
+ - `g.issue(level, label, message)`: 프로젝트 어댑터가 `'error'`·`'warn'`을 낸다. 실행 중 어댑터 이름과 함께 `issues[]`에 남고, `check`가 `✗`·`△` 줄로 출력하며(error는 종료 코드 1), 더보기 > 이 상황판에 목록으로 나온다.
46
+
47
+ check
48
+
49
+ - 마일스톤 규칙 오류 5종·경고 7종(마일스톤 절이 있을 때만, 진행 항목에 마일스톤 키 없음 포함), 진행 항목의 선행 미완 경고, 배우 사전에 없는 배우 경고, `g.issue` 줄. 새 경고는 종료 코드를 바꾸지 않는다.
50
+
51
+ 예산 검사와 CI
52
+
53
+ - 예산 검사 선택자를 새 화면에 맞췄다(`body` 배경, `#root` 식별자, 기능 지도 → 기능 화면 → 단계 → 상세 3번 클릭, 내비 링크 모두 보임). 숨은 스크롤 0, 모션 줄임 애니메이션 0, 상태 모양 검사를 더했다. 임계값·설정 키·경로·스크린샷 경로는 같고, 스크린샷은 모션 줄임에서 서체를 기다린 뒤 찍는다.
54
+ - 스모크가 하위 화면 전 경로 콘솔 오류, 설정 경로를 바꾼 픽스처의 화면 문구, 클릭 경로 크롤(`scripts/route-crawl.mjs`: 개요 클릭 대상과 라우트 패턴별 모든 개체를 `serve`·`serve --static` 두 방식으로 방문)을 본다.
55
+
56
+ 문서
57
+
58
+ - `docs/view-budget.md`(첫 화면 질문, 새 규칙, 패널 바꾸는 절차), `docs/hosting-and-csp.md`(React 클라이언트 렌더와 CSSOM, 배포 뒤 검증, 게이트 뒤 확인 방법), `docs/semantic-authoring.md`(화면 어휘, 배우 사전, 로드맵·마일스톤 작성), `docs/semantic-schema.md`, `docs/adapter-contract.md`, `docs/migrate.md`(전환 전후 측정).
59
+
5
60
  ## [1.0.1] - 2026-09-17
6
61
 
7
62
  - 문서: `npx`로 옵션을 넘길 때 `--`가 필요하다는 안내, 로컬 엔진을 `--install-links`로 끼워 보는 방법을 README에 더했다.
@@ -1,5 +1,8 @@
1
1
  // 화면 예산 검사: 첫 화면(개요)이 단순함 규칙을 지키는지 잰다. 어기면 CI가 실패한다.
2
- // 1440×900에서 스크롤 없음 · 패널 8 이하 · 목록 패널 6행 이하 · 여정 매트릭스 12행 이하 · 시스템 식별자 0 · 내비 5
2
+ // CSP 아래 오류 0(바탕 --bg) · 1440×900에서 스크롤 없음 · 패널 8 이하 · 목록 묶음마다 6행 이하 · 기능 지도 12행 이하
3
+ // · 내비 5가 모두 보임 · #root 글자에 시스템 식별자 0 · 기능 지도 선택 → a.open → 단계 카드 → 단계 상세 3번 안
4
+ // · 패널 숨은 스크롤 0 · 모션 줄임에서 애니메이션 0 · 범례 상태 모양 4종 구분 · 모션 줄임 스크린샷
5
+ // 임계값은 map/config.json의 budget(viewport·maxPanels·maxRowsPerPanel·navItems)이다.
3
6
  import { test, expect } from '@playwright/test';
4
7
  import { readFileSync } from 'node:fs';
5
8
  import { resolve } from 'node:path';
@@ -8,35 +11,41 @@ import { resolve } from 'node:path';
8
11
  const cfg = JSON.parse(readFileSync(resolve(process.cwd(), 'map/config.json'), 'utf8')).budget;
9
12
  const IDENT = /\/api\/|\.tsx\b|\.mjs\b|\.sql\b|\bweb\/src\b|\b[0-9a-f]{7,40}\b/;
10
13
 
14
+ const openOverview = async (page) => {
15
+ await page.goto('/map/#/overview');
16
+ await page.waitForSelector('.panel', { timeout: 10_000 });
17
+ await page.evaluate(() => document.fonts.ready);
18
+ };
19
+
11
20
  test('CSP 아래서 오류 없이 렌더된다', async ({ page }) => {
12
21
  const errors = [];
13
22
  page.on('pageerror', (e) => errors.push(String(e.message)));
14
23
  page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
15
- await page.goto('/map/#/overview');
16
- await page.waitForSelector('.panel', { timeout: 10_000 });
24
+ await page.addInitScript(() => document.addEventListener('securitypolicyviolation', (e) => console.error(`CSP ${e.violatedDirective} ${e.blockedURI}`)));
25
+ await openOverview(page);
17
26
  expect(errors, errors.join('\n')).toEqual([]);
18
- const bg = await page.evaluate(() => getComputedStyle(document.querySelector('.side')).backgroundColor);
19
- expect(bg).not.toBe('rgba(0, 0, 0, 0)');
27
+ const bg = await page.evaluate(() => getComputedStyle(document.body).backgroundColor);
28
+ expect(bg).toBe('rgb(5, 7, 10)');
20
29
  });
21
30
 
22
31
  test('개요는 한 화면에 들어간다', async ({ page }) => {
23
- await page.goto('/map/#/overview');
24
- await page.waitForSelector('.panel');
32
+ await openOverview(page);
25
33
  const [scrollH, clientH] = await page.evaluate(() => [document.documentElement.scrollHeight, document.documentElement.clientHeight]);
26
34
  expect(scrollH, `스크롤 높이 ${scrollH} > 뷰포트 ${clientH}`).toBeLessThanOrEqual(clientH);
27
35
  const bodyW = await page.evaluate(() => document.documentElement.scrollWidth);
28
36
  expect(bodyW).toBeLessThanOrEqual(cfg.viewport[0]);
29
37
  });
30
38
 
31
- test('패널·행·내비 수가 예산 안이다', async ({ page }) => {
32
- await page.goto('/map/#/overview');
33
- await page.waitForSelector('.panel');
39
+ test('패널·행·내비 수가 예산 안이고 내비가 모두 보인다', async ({ page }) => {
40
+ await openOverview(page);
34
41
  expect(await page.locator('.panel').count()).toBeLessThanOrEqual(cfg.maxPanels);
35
- expect(await page.locator('.nav a').count()).toBeLessThanOrEqual(cfg.navItems);
42
+ const nav = page.locator('.nav a');
43
+ expect(await nav.count()).toBeLessThanOrEqual(cfg.navItems);
44
+ for (let i = 0; i < await nav.count(); i += 1) await expect(nav.nth(i)).toBeVisible();
36
45
  const lists = page.locator('.panel[data-budget="list"]');
37
46
  for (let i = 0; i < await lists.count(); i += 1) {
38
47
  const rows = await lists.nth(i).locator('.row, .bar').count();
39
- // 한 패널에 목록이 둘이면 각각 6행 이하로 본다(작업 6 + 다음 한 걸음 4, 변화 6 + 3).
48
+ // 한 패널에 목록 묶음(.rows)이 여럿이면 묶음마다 6행 이하로 본다.
40
49
  const groups = await lists.nth(i).locator('.rows, .bars').count();
41
50
  expect(rows, `패널 ${i} 행 ${rows}`).toBeLessThanOrEqual(cfg.maxRowsPerPanel * Math.max(1, groups));
42
51
  }
@@ -45,26 +54,69 @@ test('패널·행·내비 수가 예산 안이다', async ({ page }) => {
45
54
  });
46
55
 
47
56
  test('첫 화면에는 시스템 식별자가 없다', async ({ page }) => {
48
- await page.goto('/map/#/overview');
49
- await page.waitForSelector('.panel');
50
- const text = await page.locator('.main').innerText();
57
+ await openOverview(page);
58
+ const text = await page.locator('#root').innerText();
51
59
  const hit = text.split('\n').find((l) => IDENT.test(l));
52
60
  expect(hit, `식별자 노출: ${hit}`).toBeUndefined();
53
61
  });
54
62
 
55
- test('여정 장면 상세까지 3번 안에 닿는다', async ({ page }) => {
56
- await page.goto('/map/#/overview');
57
- await page.waitForSelector('.jrow');
63
+ test('기능 지도 선택 → a.open → 단계 상세까지 3번 안에 닿는다', async ({ page }) => {
64
+ await openOverview(page);
58
65
  await page.locator('.jrow').first().click();
66
+ await page.locator('a.open').click();
59
67
  await page.waitForSelector('.scene');
60
68
  await page.locator('.scene').first().click();
61
69
  await page.waitForSelector('.detail');
62
70
  expect(await page.locator('.detail .node').count()).toBeGreaterThan(0);
63
71
  });
64
72
 
65
- test('개요 스크린샷', async ({ page }) => {
66
- await page.goto('/map/#/overview');
67
- await page.waitForSelector('.panel');
68
- await page.waitForTimeout(500);
69
- await page.screenshot({ path: resolve(process.cwd(), 'map/.out/overview-1440.png') });
73
+ test('패널 숨은 스크롤이 없다', async ({ page }) => {
74
+ await openOverview(page);
75
+ // .panel·.pb·.rows와 패널 안 overflow-y auto/scroll 자손. 줄 수 제한 글자와 지도·캡처 상자는 뺀다.
76
+ const hidden = await page.evaluate(() => {
77
+ const set = new Set(document.querySelectorAll('.panel, .pb, .rows'));
78
+ document.querySelectorAll('.panel *').forEach((e) => { const o = getComputedStyle(e).overflowY; if (o === 'auto' || o === 'scroll') set.add(e); });
79
+ return [...set].filter((e) => !e.closest('.mapwrap, .live'))
80
+ .filter((e) => { const c = getComputedStyle(e).webkitLineClamp; return !c || c === 'none'; })
81
+ .filter((e) => e.scrollHeight - e.clientHeight > 1)
82
+ .map((e) => `${e.className} ${e.closest('.panel')?.querySelector('h2')?.textContent || ''} +${e.scrollHeight - e.clientHeight}px`);
83
+ });
84
+ expect(hidden, hidden.join('\n')).toEqual([]);
85
+ });
86
+
87
+ test('범례 상태 모양 4종이 서로 다르다', async ({ page }) => {
88
+ await openOverview(page);
89
+ test.skip(await page.locator('.layers svg.stepmark').count() === 0, '기능이 없어 범례가 없다');
90
+ // 동작 = 채운 원(테두리 없음), 목업 = 반원 채움 + 실선, 계획 = 채움 없음 + 실선, 구상 = 채움 없음 + 점선
91
+ const kinds = await page.evaluate(() => {
92
+ const WORD = { 동작: 'live', 목업: 'mock', 계획: 'planned', 구상: 'next' };
93
+ return [...document.querySelectorAll('.layers svg.stepmark')].map((svg) => {
94
+ const word = Object.keys(WORD).find((w) => (svg.closest('button')?.textContent || '').includes(w));
95
+ const shapes = [...svg.querySelectorAll('circle, path')].map((s) => { const cs = getComputedStyle(s); return { tag: s.tagName, fill: cs.fill !== 'none' && cs.fillOpacity !== '0', stroke: cs.stroke !== 'none' && parseFloat(cs.strokeWidth) > 0, dash: cs.strokeDasharray !== 'none' }; });
96
+ const filledCircle = shapes.some((x) => x.tag === 'circle' && x.fill && !x.stroke);
97
+ const filledPath = shapes.some((x) => x.tag === 'path' && x.fill);
98
+ const solid = shapes.some((x) => x.stroke && !x.dash), dashed = shapes.some((x) => x.stroke && x.dash);
99
+ const kind = filledCircle && !solid && !dashed ? 'live' : filledPath && solid ? 'mock' : dashed ? 'next' : solid ? 'planned' : 'unknown';
100
+ return { word, expect: WORD[word], kind };
101
+ });
102
+ });
103
+ expect(kinds.filter((k) => k.expect).length, JSON.stringify(kinds)).toBe(4);
104
+ for (const k of kinds.filter((x) => x.expect)) expect(k.kind, `${k.word} 표식`).toBe(k.expect);
105
+ });
106
+
107
+ test.describe('모션 줄임', () => {
108
+ test.use({ reducedMotion: 'reduce' });
109
+
110
+ test('모션 줄임에서 애니메이션이 없다', async ({ page }) => {
111
+ await openOverview(page);
112
+ await page.waitForTimeout(500);
113
+ const names = await page.evaluate(() => document.getAnimations().map((a) => a.animationName || a.constructor.name));
114
+ expect(names, names.join(', ')).toEqual([]);
115
+ });
116
+
117
+ test('개요 스크린샷(모션 줄임)', async ({ page }) => {
118
+ await openOverview(page);
119
+ await page.waitForTimeout(300);
120
+ await page.screenshot({ path: resolve(process.cwd(), 'map/.out/overview-1440.png') });
121
+ });
70
122
  });
@@ -9,15 +9,30 @@
9
9
  export default function name(g, fs, cfg) {
10
10
  // ... 노드·엣지 추가
11
11
  return null; // 정상
12
- // return '설명'; // partial: 일부만 읽음(예: 파일 없음). 생성은 계속되고 상단에 주황
13
- // throw new Error('…'); // failed: 빨간 + check 오류. 그래도 다른 어댑터는 돈다
12
+ // return '설명'; // partial: 일부만 읽음(예: 파일 없음). 생성은 계속되고 더보기 > 이 상황판의 어댑터 상태에 남는다
13
+ // throw new Error('…'); // failed: 개요 특보 "자료 일부 누락" + check 오류. 그래도 다른 어댑터는 돈다
14
14
  }
15
15
  ```
16
16
 
17
- - `g` — 그래프. `g.add(kind, id, label, props, src)`, `g.link(fromKind, fromId, edgeKind, toKind, toId)`, `g.get`, `g.of(kind)`, `g.in`, `g.out`.
17
+ - `g` — 그래프. `g.add(kind, id, label, props, src)`, `g.link(fromKind, fromId, edgeKind, toKind, toId)`, `g.get`, `g.of(kind)`, `g.in`, `g.out`, `g.issue(level, label, message)`(1.1.0부터).
18
18
  - `fs` — 저장소 접근. `read(rel)`, `has(rel)`, `isDir(rel)`, `walk(dir, pred)`, `ls(dir)`, `git(...args)`(실패 시 빈 문자열), `hasGit()`, `resolveRef(name)`(main → origin/main → HEAD), `lastCommit(rel)`, `lineOf(text, needle)`.
19
19
  - `cfg` — `map/config.json` 전체. 자기 키(`cfg.<name>`)만 읽고, 다른 어댑터의 키는 `?.`로 방어한다.
20
20
 
21
+ ## 오류·경고 보고(g.issue)
22
+
23
+ 어댑터가 읽은 사실에서 프로젝트 규칙 위반을 찾았으면 `g.issue(level, label, message)`로 낸다. 반환값 partial·throw는 "어댑터가 제대로 읽었나"를, `g.issue`는 "읽은 내용에 문제가 있나"를 알린다. 1.1.0부터 쓸 수 있다.
24
+
25
+ ```js
26
+ g.issue('warn', '로드맵', '결정 대기 30일 넘음: 결제 흐름');
27
+ g.issue('error', '여정 파일', '필수 키 없음: owner');
28
+ ```
29
+
30
+ - `level`은 `'error'` 또는 `'warn'`이다. 그 밖의 값이거나 `label`·`message`가 문자열이 아니면 `throw`하고, 그 어댑터는 failed가 된다. 다른 어댑터는 계속 돈다.
31
+ - 엔진이 지금 실행 중인 어댑터 이름을 함께 기록한다(`config.adapters`의 이름). 어댑터가 throw하기 전에 낸 문제도 남는다.
32
+ - `graph.json`·`data.json`의 최상위 `issues[]`에 `{level, label, message, adapter}`로 남는다.
33
+ - `livemap check`는 기존 검사 뒤에 error를 `✗ {label}: {message}`로, warn을 `△ {label}: {message}`로 출력한다. error는 종료 코드 1에 센다.
34
+ - 개요에는 나오지 않고 더보기 > 이 상황판(`#/more/about`)에 목록으로 나온다.
35
+
21
36
  ## 어디에 두나
22
37
 
23
38
  엔진은 `config.adapters`의 이름마다 프로젝트 `map/adapters/<name>.mjs`를 먼저 찾고, 없으면 패키지에 딸린 참조 어댑터(`src/adapters/<name>.mjs`)를 쓴다. 프로젝트 파일이 참조 어댑터와 이름이 같으면 `build`·`check`가 "프로젝트 어댑터가 참조 어댑터를 가림: <name>" 한 줄을 알린다. 참조 어댑터의 결함은 엔진 저장소에서 고치고, 프로젝트만의 스택은 다른 이름의 프로젝트 어댑터로 둔다.
@@ -44,8 +59,12 @@ export default function name(g, fs, cfg) {
44
59
  | ledger | `running-i`·`waiting-i` | tasks |
45
60
  | deploy | `head`·`homelab` | git, deploy |
46
61
  | testreport | `last` | testreport |
62
+ | milestone | 로드맵 항목 id(1.x 이름) | roadmap |
63
+ | release | 마일스톤 id(1.x 임시 이름, 1.1.0부터) | roadmap |
64
+
65
+ 로드맵 항목과 마일스톤의 노드 종류 이름은 1.x 동안 `milestone`·`release`이고, 2.0.0에서 `roadmapItem`·`milestone`으로 바꾼다(어댑터 계약 변경이라 major).
47
66
 
48
- 엣지: `shows`(step→screen, 파생이 만든다), `calls`(screen→api), `invokes`(api→function), `touches`(function→table), `covers`(test→screen|api|function), `changes`(commit→screen|api|migration), `defines`(task→decision), `contains`(migration→table|function). 새 종류가 필요하면 엔진 저장소의 `src/lib/graph.mjs` 목록에 더한다(minor 릴리스). 화면이 그 종류를 그리려면 `src/derive.mjs`도 손봐야 하므로, 먼저 기존 종류로 표현할 수 없는지 본다.
67
+ 엣지: `shows`(step→screen, 파생이 만든다), `calls`(screen→api), `invokes`(api→function), `touches`(function→table), `covers`(test→screen|api|function), `changes`(commit→screen|api|migration), `defines`(task→decision), `contains`(migration→table|function, release→milestone), `tracks`(milestone→task). 새 종류가 필요하면 엔진 저장소의 `src/lib/graph.mjs` 목록에 더한다(minor 릴리스). 화면이 그 종류를 그리려면 `src/derive.mjs`와 화면 원본 `ui/`도 손봐야 하므로, 먼저 기존 종류로 표현할 수 없는지 본다.
49
68
 
50
69
  ## 순서
51
70
 
@@ -58,7 +58,16 @@ async function serveMap(res, sub) {
58
58
  - Google Fonts 등 외부 스타일·폰트
59
59
  - `data:` 이미지
60
60
 
61
- 엔진 화면은 그래서 `index.html`(마크업만) + `map.css` + `map.js`이고, 동적 폭은 `data-w` 속성을 붙인 JS에서 `el.style.width = …`로 적용한다(CSSOM 조작은 허용된다). 화면을 고칠 셋을 지키면 어느 CSP에서도 뜬다. 로컬 serve와 예산 검사가 같은 CSP를 걸어 회귀를 잡는다.
61
+ 엔진 서빙(`serve`) 거는 정책은 `default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'`다. 엔진 화면은정책 아래에서 뜨도록 만들고, 로컬 serve와 예산 검사가 같은 정책으로 확인한다. 배포 서버 정책은 이것과 같거나 더 느슨해야 한다(`data/*.json`을 읽으므로 `connect-src 'self'`가 필요하다).
62
+
63
+ 엔진 화면은 그래서 `index.html`(`<div id="root">`와 같은 출처 `map.css`·`map.js`만) + `map.css` + `map.js`다. 1.1.0부터 화면 원본은 엔진 저장소 `ui/`의 React이고, esbuild가 이 세 파일로 번들한다. 파일 이름·위치는 1.0.1과 같다.
64
+
65
+ - React는 브라우저에서만 렌더한다(`createRoot`). 크기·위치 같은 동적 값은 React `style` prop으로 주는데, React는 이것을 `element.style`(CSSOM)로 넣으므로 `style-src 'self'`가 막지 않는다. 마크업에 `style="…"` 속성으로 남지 않는다.
66
+ - 서버 렌더·미리 렌더한 HTML은 쓰지 않는다. 렌더 결과가 마크업의 `style` 속성으로 남아 CSP 위반이 된다.
67
+ - `<style>` 요소를 주입하는 CSS-in-JS·컴포넌트 라이브러리, 외부 서체, `data:` 이미지, `eval`을 쓰지 않는다. 그림은 SVG 속성과 `map.css` 클래스, 애니메이션은 `map.css`의 `@keyframes`, 캡처는 같은 출처 `captures/<id>.jpg`다.
68
+ - 번들 끝에 React 라이선스 고지가 있다(`legalComments: 'eof'`).
69
+
70
+ 화면을 고칠 때 이 규칙을 지키면 위 정책에서 뜬다.
62
71
 
63
72
  ## CI
64
73
 
@@ -95,4 +104,19 @@ Dockerfile에는 `COPY map/.out/site/ ./map/site/` 한 줄. 생성기·어댑터
95
104
 
96
105
  ## 배포 뒤 검증
97
106
 
98
- 상태 코드 200은 렌더를 증명하지 않는다. Chromium으로 열어 `.panel` 개수, 사이드바 배경색, 콘솔 오류 0을 본다. 포트포워드 뒤에서 프로덕션 호스트 헤더가 필요하면 `page.route('**/*', …)`로 요청을 로컬 포트로 바꿔 태운다(Chromium은 `extraHTTPHeaders`의 Host를 거부한다).
107
+ 상태 코드 200은 렌더를 증명하지 않는다. 배포한 `/map/`을 Chromium으로 열어 다음을 본다.
108
+
109
+ | 확인 | 기대 |
110
+ |---|---|
111
+ | 셸 | 상단 바(`.top`)와 내비 링크 5개가 보인다 |
112
+ | 스타일 적용 | `body` 계산 배경색이 `rgb(5, 7, 10)`이다. 투명이면 `map.css`가 막혔거나 경로가 틀렸다 |
113
+ | 패널 | `.panel` 8개 |
114
+ | 콘솔 | 오류 0, CSP 위반(`securitypolicyviolation`) 0 |
115
+ | 서체 | `fonts/PretendardVariable.woff2` 응답 200 |
116
+ | 자료 | `data/overview.json` 응답 200, `cache-control: no-store` |
117
+
118
+ 상황판은 보통 인증 게이트 뒤에 있어 검증 스크립트가 로그인 화면에 막힌다. 게이트를 우회하는 설정을 서버에 더하지 말고, 서버 자체에 바로 붙는 경로를 쓴다.
119
+
120
+ - 게이트 앞단을 거치지 않는 포트포워드(클러스터면 서비스·파드로, 단독 서버면 SSH 터널)로 앱 서버 포트를 로컬에 연다.
121
+ - 앱 서버가 Host 헤더로 경로나 CSP를 나누면 공개 도메인 Host가 필요하다. Chromium은 `extraHTTPHeaders`의 Host를 거부하므로, 브라우저는 공개 주소를 열게 두고 `page.route('**/*', …)`로 요청을 로컬 포트로 바꿔 태우거나 Host를 공개 도메인으로 바꿔 주는 loopback 프록시를 둔다.
122
+ - 이렇게 확인한 것은 서버와 export 폴더다. 게이트 설정 자체(로그인 뒤 `/map/` 접근)는 사람이 브라우저로 한 번 연다.
package/docs/migrate.md CHANGED
@@ -12,6 +12,11 @@
12
12
  4. `config.json`에 `"engine": 1`을 더한다.
13
13
  5. npm 스크립트를 `livemap` 명령으로 바꾼다(`livemap init`이 없는 스크립트만 넣고 값이 다른 스크립트는 보고한다). 어댑터 단위 검사 스크립트는 지운다. 참조 어댑터 검사는 엔진 저장소 CI가 돈다.
14
14
  6. 배포: 이미지 빌드 전에 `npm run map` → `npm run map:export`, 서버는 export 폴더 한 루트를 `/map/`로 준다(`hosting-and-csp.md`).
15
- 7. 확인: 전환 전후 `data.json`이 `generatedAt`을 빼면 같고 `check` 출력이 같은지, `npm run map:budget`이 통과하는지 본다.
15
+ 7. 확인: 전환 전(옛 설치)과 전환 뒤(패키지)를 같은 커밋에서 연속으로 돌려 비교한다.
16
+ - 비교 대상은 `map/.out/`의 `graph.json`·`data.json`·`overview.json`과 `npm run map:check` 출력이다. 전환 전 결과를 다른 폴더에 복사해 두고 전환 뒤 같은 명령으로 다시 만든다.
17
+ - 옛 설치와 같은 판의 엔진으로 옮기면 세 파일이 `generatedAt`을 빼면 같고 `check` 출력도 같다.
18
+ - 전환하면서 엔진 판도 올리면 파일이 같지 않다. minor 판은 필드·노드를 더하기만 하므로, 전환 전 파일의 모든 경로·값이 전환 뒤 파일에 있는지(포함) 보고 새로 생긴 경로가 그 판의 CHANGELOG 항목인지 확인한다. `check`는 전환 전 출력에 CHANGELOG가 적은 새 줄만 더해져야 하고 오류 수는 같아야 한다.
19
+ - 활동 숫자는 실행 시각 기준 14일 창(`git.sinceDays`)이라 두 실행 사이에 창 경계를 넘는 커밋이 있으면 달라진다. 차이가 경계 커밋뿐인지 보고 연속으로 다시 돌린다. `npm ci`와 설치 시간은 두 실행 사이가 아니라 앞에 둔다.
20
+ - 끝으로 `npm run map:budget`이 통과하고 스크린샷 `map/.out/overview-1440.png`이 전환 전과 같은 정보를 보이는지 본다.
16
21
 
17
22
  로컬에서 고친 엔진을 끼워 볼 때는 `npm install --no-save --install-links <엔진 저장소 경로>`를 쓴다. `--install-links` 없이 폴더를 설치하면 심링크가 되어 예산 설정이 `@playwright/test`를 엔진 저장소 쪽에서 찾다 실패한다.
@@ -1,6 +1,32 @@
1
1
  # 여정 파일 작성
2
2
 
3
- `map/semantic/journeys.json`은 상황판에서 유일하게 손으로 유지하는 층이다. 스토리 맵의 backbone(배우가 하는 활동을 순서대로)과 같다. 코드가 아니라 제품의 뜻을 적는 자리이므로 사용자 어휘만 쓰고 라우트·파일명은 `screens`·`capture` 필드에만 둔다.
3
+ `map/semantic/journeys.json`(설정 `semantic`)은 제품의 뜻을 사람이 적는 파일이다. 스토리 맵의 backbone(배우가 하는 활동을 순서대로)과 같다. 코드가 아니라 제품의 뜻을 적는 자리이므로 사용자 어휘만 쓰고 라우트·파일명은 `screens`·`capture` 필드에만 둔다.
4
+
5
+ 사람이 적는 곳은 넷이고 나머지는 생성기가 저장소를 스캔해 만든다.
6
+
7
+ | 파일 | 적는 것 |
8
+ |---|---|
9
+ | 여정 파일(`semantic`) | 배우·목표·장면과 장면 상태 |
10
+ | 로드맵(`roadmap.file`) | 로드맵 항목과 마일스톤(아래 「로드맵과 마일스톤」) |
11
+ | 작업 장부(`tasks.index`) | 지금 실행 중·대기 중인 작업 |
12
+ | 작업 폴더 문서(`tasks.dir`) | 스펙·계획의 결정(DEC)·계획 항목(PN)·열린 질문 |
13
+
14
+ ## 화면 어휘
15
+
16
+ 1.1.0 화면은 외부 독자에게 낯선 말을 바꿔 보인다. 파일의 키·값과 이 문서의 작성 어휘는 그대로다.
17
+
18
+ | 여정 파일 | 화면 |
19
+ |---|---|
20
+ | journey(여정) | 기능 |
21
+ | step(장면) | 단계 |
22
+ | `live` | 동작 |
23
+ | `mock` | 목업 |
24
+ | `planned` | 계획 |
25
+ | `next` | 구상 |
26
+
27
+ 더보기 > 이 상황판은 이 파일을 "기능 정본 파일"로, 로드맵 파일을 "로드맵 정본"으로 부르고 설정한 경로를 함께 보인다.
28
+
29
+ 엔진은 프로젝트가 적은 문구(여정 제목, 레인 이름, 장면 이름 등)를 고치지 않는다. 레인 이름에 "여정"이 들어 있으면 화면에도 그대로 나온다.
4
30
 
5
31
  ## 스키마
6
32
 
@@ -42,6 +68,7 @@
42
68
  - `refs` — `DEC-nn`·`PN-nn`(tasks 스펙 문서에서 정의된 것으로 자동 연결), 위키 결정 slug(`decisions/<slug>.md`). 해결되지 않는 DEC/PN 참조는 오류다.
43
69
  - `reviewedAt` — 사람이 이 장면을 마지막으로 확인한 날. 그 뒤 화면 파일이 바뀌면 "확인 필요" 경고가 뜬다. 상태를 바꿀 때 같이 갱신한다.
44
70
  - `actor` — 장면 단위 배우가 여정 배우와 다를 때만(예: 예약 여정 안의 "리조트 접수함").
71
+ - `actors` — 배우 사전. 여정·장면의 `actor`는 이 사전의 키를 적고, 화면은 값(이름)을 보인다. 사전이 있는데 여정 `actor`나 여정과 다른 장면 `actor`가 키에 없으면 `check`가 경고한다(1.1.0부터, `△ <여정 제목>: 배우 사전에 없는 값 <값>` 또는 `△ <여정 제목> › <장면 이름>: 배우 사전에 없는 값 <값>`). 사전이 없으면 검사하지 않는다. 경고라 종료 코드는 바뀌지 않는다. 사전에 키를 더하거나 `actor`를 사전 키로 고친다.
45
72
 
46
73
  ## 인터뷰 질문
47
74
 
@@ -58,7 +85,7 @@
58
85
  ## 규칙
59
86
 
60
87
  - 장면 id는 여정 안에서 유일해야 한다(check 오류). intent는 `next`가 아니면 비우지 않는다(check 경고).
61
- - 여정은 12 이하로 둔다. 개요 매트릭스가 화면에 들어가는 한계이며, 그보다 많으면 레인을 합칠 때다.
88
+ - 개요 기능 지도에는 여정이 12개까지 올라간다. 13개부터는 미완성 여정(파일 순서), 현재 마일스톤의 완성 여정, 나머지 완성 여정(14일 커밋 많은 순) 순서로 12개를 올리고 남은 완성 여정은 지도에서 접는다. 접힌 여정도 기능 화면에는 전부 보인다. 미완성 여정만 13개를 넘으면 앞 12개만 지도에 오르므로, 그때는 여정 하나가 목표 하나인지 다시 본다.
62
89
  - 장면 이름은 명사구 2~5어절, intent는 한 문장. 시스템 말투("데이터를 조회한다")가 아니라 사용자 말투("오늘 누가 오는지 본다").
63
90
  - 여정에 없는 화면은 "미분류"로 센다. 화면을 어딘가 억지로 넣기보다, 정말 어느 여정에도 안 속하면 그 화면이 필요한지 묻는다.
64
91
  - `next`는 스펙도 없는 것이다. 스펙이 생기면 `planned`, 화면이 생기면 `mock`, 실데이터가 붙으면 `live`. 각 전이는 병합과 같은 커밋에서 적는다.
@@ -75,3 +102,89 @@
75
102
  | A | 그 검사가 main 최신 커밋에서 통과(`npm run test:report`) |
76
103
 
77
104
  `live`라고 적었는데 D가 나오면 주장이 틀린 것이다. 상태를 `mock`으로 내리거나 화면을 고친다. 상황판은 둘 중 무엇이 맞는지 판단하지 않고 어긋남만 보여준다.
105
+
106
+ ## 로드맵과 마일스톤
107
+
108
+ 설정 `roadmap.file`(기본 `tasks/roadmap.md`)은 무엇을 어떤 순서로 만드는지 적는 파일이다. `## ` 절 하나가 로드맵 항목 하나이고, 절의 첫 문단이 목표, `- 키: 값` 줄이 속성이다. 파일 안 순서가 항목 순서다. 모르는 키 줄은 무시한다.
109
+
110
+ ```markdown
111
+ ## 예약금 결제
112
+
113
+ 견적을 예약으로 바꾸고 예약금까지 내서 확정한다.
114
+
115
+ - id: deposit
116
+ - 상태: 진행
117
+ - 진행 방식: 스펙 주도
118
+ - 작업: 20260901-deposit
119
+ - 장면: booking/request, booking/pay
120
+ - 선행: search
121
+ - 결정 대기: 사용자: 환불 규칙을 먼저 정할지
122
+ - 완료 기준: 테스트 결제가 끝까지 통과한다
123
+ - 마일스톤: first-release
124
+ ```
125
+
126
+ | 키 | 값 |
127
+ |---|---|
128
+ | `id` | 영문 소문자·숫자·`-`. 없으면 항목 순서로 `m{n}`이 붙으니 적어 둔다 |
129
+ | `상태` | 완료·진행·다음·대기·이후. 그 밖이면 check 오류 |
130
+ | `진행 방식` | 자유 문장(예: 스펙 주도) |
131
+ | `작업` | 작업 폴더 이름, 쉼표로 여럿. 없는 폴더는 check 오류 |
132
+ | `장면` | `<여정 id>/<장면 id>`, 쉼표로 여럿. 없는 장면은 check 오류 |
133
+ | `선행` | 먼저 끝나야 하는 항목 id, 쉼표로 여럿. 없는 id는 check 오류, 진행 항목의 선행이 완료가 아니면 경고(1.1.0부터) |
134
+ | `결정 대기` | 이 항목을 멈추게 하는 결정. 자유 문장 또는 `<주체>: <질문>` |
135
+ | `완료 기준` | 자유 문장 |
136
+ | `마일스톤` | 이 항목이 속한 마일스톤 `id` 하나(1.1.0부터). 없으면 미배정 |
137
+
138
+ 엔진은 로드맵 파일의 git 이력에서 항목이 완료로 바뀐 마지막 커밋 시각(완료일)과 결정 대기가 적힌 구간의 첫 커밋 시각(결정 대기 시작일)을 계산한다. 날짜 키를 따로 적지 않는다. 커밋하지 않은 변경은 날짜가 없고, git이 없거나 얕은 클론이면 날짜를 계산하지 않는다(얕은 클론이면 로드맵 어댑터가 partial). 파일 이름을 바꿔도 이력을 따라간다.
139
+
140
+ ### 결정 대기의 주체
141
+
142
+ `결정 대기` 값이 `<주체>: <질문>` 꼴이면 엔진이 주체와 질문을 나눠 화면에 "사용자 결정 대기 3일째"처럼 보인다. 주체는 콜론이 없는 1~20자이고 콜론(반각·전각) 뒤에 공백이 있어야 한다. 그래서 `https://…`나 `10:30 회의`는 주체로 읽지 않고, 콜론이 없는 문장은 주체 미지정이다.
143
+
144
+ 주의: 형식과 상관없이 앞 20자 안에 콜론과 공백이 있으면 앞말이 주체로 읽힌다. "주의: 환불 규칙 확인"은 주체가 "주의"가 된다. 주체를 적지 않을 문장에는 콜론 대신 다른 부호를 쓴다.
145
+
146
+ ### 마일스톤
147
+
148
+ 마일스톤은 로드맵 항목을 묶어 한 번에 내놓는 릴리스 단위다. 개요는 현재 마일스톤의 진척을 가장 크게 보이고 끝난 마일스톤을 접는다. 같은 로드맵 파일 안에 제목이 `마일스톤:`으로 시작하는 `## ` 절로 적는다. 새 설정 키는 없다.
149
+
150
+ ```markdown
151
+ ## 마일스톤: 첫 공개
152
+
153
+ 새 계정만으로 검색·가입·예약 요청·예약금 테스트 결제가 이어진다.
154
+
155
+ - id: first-release
156
+ - 상태: 진행
157
+ - 목표일: 2026-10-31
158
+ ```
159
+
160
+ | 키 | 값 | 필요 |
161
+ |---|---|---|
162
+ | 제목 | `## 마일스톤: <제목>`(반각·전각 콜론) | 필수 |
163
+ | 첫 문단 | 목표 한 문장 | 선택 |
164
+ | `id` | 영문 소문자·숫자·`-`. 로드맵 항목 id와 겹치면 안 된다 | 필수 |
165
+ | `상태` | 완료·진행·다음·대기·이후(로드맵 항목과 같은 다섯 값) | 필수 |
166
+ | `완료일` | `YYYY-MM-DD` | 상태가 완료면 적는다 |
167
+ | `목표일` | `YYYY-MM-DD` | 선택 |
168
+ | `결정 대기` | 자유 문장 또는 `<주체>: <질문>` | 선택 |
169
+
170
+ - 소속은 항목 쪽 `- 마일스톤: <id>` 키 하나로 정한다. 한 항목은 한 마일스톤에만 속한다.
171
+ - 마일스톤 순서는 파일 안 마일스톤 절 순서다. 항목 순서와 자동 id는 항목 절만 세므로, 마일스톤 절을 끼워도 기존 항목의 순서·id가 바뀌지 않는다. 마일스톤 절은 파일 머리나 소속 항목 바로 앞에 둔다.
172
+ - 엔진이 계산하는 것: 소속 항목, 진척(소속 항목 장면 중 동작 수, 소속 항목 작업의 계획 항목 완료 수), 막힘, 결정 대기 시작일, 현재 마일스톤(파일 순서로 상태가 진행인 첫 마일스톤, 없으면 다음인 첫 마일스톤).
173
+ - 마일스톤 절은 엔진 1.1.0 이상에서만 뜻을 가진다. 1.0.x는 이 절을 로드맵 항목으로 읽어 항목 수와 순서가 틀어진다. 엔진을 1.1.0 이상으로 올린 커밋 뒤에 절을 더한다. 항목의 `마일스톤` 키는 1.0.x가 무시하므로 먼저 적어도 된다.
174
+
175
+ 마일스톤 절이 있을 때 `check`가 더하는 줄이다. 오류는 종료 코드 1, 경고는 출력만 한다. 마일스톤 절이 없으면 이 줄은 나오지 않는다.
176
+
177
+ | 수준 | 조건 | 출력 |
178
+ |---|---|---|
179
+ | 오류 | 마일스톤 절에 `id` 없음 | `마일스톤 {제목}: id 없음` |
180
+ | 오류 | 마일스톤 id 중복, 또는 로드맵 항목 id와 같음 | `마일스톤 id 중복: {id}` |
181
+ | 오류 | 상태가 다섯 값 밖이거나 비어 있음 | `마일스톤 {제목}: 알 수 없는 상태 {값}` |
182
+ | 오류 | `완료일`·`목표일`이 `YYYY-MM-DD`가 아니거나 없는 날짜 | `마일스톤 {제목}: 날짜 형식 {키} {값}` |
183
+ | 오류 | 항목의 `마일스톤` 값이 없는 id | `로드맵 {항목 제목}: 마일스톤 없음 {id}` |
184
+ | 경고 | 완료인데 소속 항목 중 완료 아님 | `마일스톤 {제목}: 완료인데 미완료 항목 {n}` |
185
+ | 경고 | 완료가 아닌데 소속 항목(1개 이상)이 모두 완료 | `마일스톤 {제목}: 항목이 모두 완료인데 상태 {상태}` |
186
+ | 경고 | 다음·대기·이후인데 소속 항목 중 진행 | `마일스톤 {제목}: 진행 항목이 있는데 상태 {상태}` |
187
+ | 경고 | 소속 항목 0 | `마일스톤 {제목}: 묶인 항목 없음` |
188
+ | 경고 | 완료인데 `완료일` 없음, 또는 완료가 아닌데 `완료일` 있음 | `마일스톤 {제목}: 완료일과 상태가 맞지 않음` |
189
+ | 경고 | 진행 마일스톤 2개 이상 | `진행 마일스톤 {n}개: {제목들}` |
190
+ | 경고 | 마일스톤이 있는데 진행 항목에 `마일스톤` 키 없음 | `로드맵 {항목 제목}: 진행인데 마일스톤 없음` |
@@ -1,6 +1,10 @@
1
1
  # 프로젝트 상황판의 시맨틱 레이어
2
2
 
3
- 상황판은 두 종류의 사실을 하나의 그래프로 잇는다. 사람이 뜻을 붙이는 노드(여정·단계·결정)와 코드에서 긁어내는 노드(화면·API·함수·테이블·검사·커밋)다. 손으로 유지하는 것은 여정 파일 하나이고, 나머지는 생성기가 저장소를 스캔해 만든다. 둘이 어긋나면(단계가 가리키는 라우트가 코드에 없음) 화면에 경고로 드러난다.
3
+ 상황판은 두 종류의 사실을 하나의 그래프로 잇는다. 사람이 뜻을 붙이는 노드(여정·단계·결정·로드맵 항목·마일스톤)와 코드에서 긁어내는 노드(화면·API·함수·테이블·검사·커밋)다. 둘이 어긋나면(단계가 가리키는 라우트가 코드에 없음) 화면에 경고로 드러난다.
4
+
5
+ 사람이 적는 곳은 넷이다: 여정 파일(`semantic`), 로드맵(`roadmap.file`, 마일스톤 포함), 작업 장부(`tasks.index`), 작업 폴더 문서(`tasks.dir`의 스펙·계획). 나머지는 생성기가 저장소를 스캔해 만든다. 작성 방법은 `semantic-authoring.md`에 있다.
6
+
7
+ 화면은 journey를 "기능", step을 "단계"로 부른다. 노드 종류·파일 키는 그대로다.
4
8
 
5
9
  ## 노드
6
10
 
@@ -12,12 +16,16 @@
12
16
  | api | 생성(BFF) | method+path | 서버 진입점. 호출하는 DB 함수·Auth |
13
17
  | function | 생성(migration) | name | DB 함수. 읽고 쓰는 테이블, BFF 사용 여부 |
14
18
  | table | 생성(migration) | schema.name | 저장 구조 |
19
+ | migration | 생성(migration) | file | migration 파일 하나. 만드는 테이블·함수, grant·RLS 수, 마지막 변경 |
15
20
  | test | 생성(tests/) | file | 검사 파일. 다루는 라우트·API |
21
+ | testreport | 생성(JUnit 리포트) | last | 마지막 검사 실행의 건수·실패·건너뜀과 그 커밋이 최신인지 |
16
22
  | commit | 생성(git) | sha | 최근 변경. 건드린 파일 → 영향받는 화면·여정 |
17
- | decision | 생성(위키 index) | file | 결정 페이지와 상태(current/proposed/superseded) |
18
- | plan | 생성(task 문서) | file | PN 체크 진척과 열린 질문 |
23
+ | decision | 생성(위키 index, 작업 문서) | file 또는 번호 | 위키 결정 페이지와 상태(current/proposed/superseded). 작업 문서의 `- DEC-nn`(스펙 결정)과 `- [ ] PN-nn`(계획 항목, 완료 여부)도 이 종류로 둔다 |
24
+ | task | 생성(작업 폴더 `tasks.dir`) | 폴더 이름 | 작업 하나. 제목·단계(스펙 초안~검증)·상태·결정 수·계획 항목 완료/미완·열린 질문 수. 계획 항목은 계획 문서(`task_plan.md`, 없으면 `plan.md`)에서만, 결정·열린 질문은 `spec/final.md`에서만 센다 |
19
25
  | ledger | 생성(tasks/index.md) | 행 | 지금 실행 중·대기 중인 작업 |
20
- | milestone | 생성(tasks/roadmap.md) | id | 로드맵 항목. 순서·상태·진행 방식·장면·작업·선행·결정 대기·완료 기준 |
26
+ | deploy | 생성(git·배포 매니페스트) | head, 배포 대상 | 브랜치 머리 커밋, 매니페스트 이미지 태그의 sha와 뒤처진 커밋 수 |
27
+ | milestone | 손(로드맵 `## 제목` 절) | id | 로드맵 항목. 순서·상태·진행 방식·장면·작업·선행·결정 대기·완료 기준·마일스톤, git 이력에서 계산한 완료일(`completedAt`)·결정 대기 시작일(`waitingSince`) |
28
+ | release | 손(로드맵 `## 마일스톤: 제목` 절, 1.1.0부터) | id | 마일스톤. 순서·상태·목표·완료일·목표일·결정 대기와 그 시작일. 1.x 동안의 임시 이름이고 2.0.0에서 로드맵 항목은 `roadmapItem`, 마일스톤은 `milestone`으로 바꾼다 |
21
29
 
22
30
  ## 엣지
23
31
 
@@ -30,7 +38,9 @@
30
38
  - test → screen | api (covers): 검사 파일의 `goto('/…')`·`'/api/…'` 문자열
31
39
  - commit → screen | api | function (touches): 파일 경로 → 노드(페이지 파일·닫힘·server.mjs·migration)
32
40
  - milestone → task (tracks): 로드맵 항목의 `작업`. 장면·선행은 derive에서 해석하고 없으면 check 오류
33
- - stepdecision | plan (refs): `refs: ["DEC-57", "PN-15", "trust-boundary"]` 문자열 매칭
41
+ - releasemilestone (contains): 로드맵 항목의 `마일스톤` 키. 없는 마일스톤 id면 엣지 없이 check 오류
42
+ - task → decision (defines): 작업 문서의 `- DEC-nn`·`- [ ] PN-nn` 줄
43
+ - step → decision (refs): `refs: ["DEC-57", "PN-15", "trust-boundary"]`. derive가 문자열로 해석한다: `DEC-nn`·`PN-nn`은 그 번호를 정의한 작업(defines)으로, 나머지는 위키 결정 slug로 찾는다
34
44
 
35
45
  ## 상태 규칙
36
46
 
@@ -38,6 +48,12 @@
38
48
  - screen.fixedVia: 닫힘이 `fixedPattern`(코드에 고정된 표시값)을 읽는 파일. source는 바꾸지 않고 "하드코딩 표시"로 따로 센다.
39
49
  - step.status는 손으로 적되 screens의 source와 대조해 어긋나면 경고(live 단계인데 mock 화면 등).
40
50
  - journey.status는 단계에서 유도: 전부 live면 live, 하나라도 live면 partial, 아니면 단계 다수 상태.
51
+ - 로드맵 항목 막힘(`blockedBy`): `waiting`(완료 아님 + 결정 대기), `deps`(진행·다음인데 선행 미완), `task`(추적 작업 중 대기). 마일스톤은 자신의 결정 대기가 있거나 소속 비완료 항목 중 막힌 것이 있으면 `blocked`.
52
+ - 현재 마일스톤: 파일 순서로 상태가 진행인 첫 마일스톤, 없으면 다음인 첫 마일스톤, 없으면 없음.
53
+
54
+ ## 문제 기록(issues)
55
+
56
+ 어댑터가 `g.issue(level, label, message)`로 낸 오류·경고는 노드가 아니라 `graph.json`·`data.json` 최상위 `issues[]`에 `{level, label, message, adapter}`로 남는다(1.1.0부터, `adapter-contract.md`).
41
57
 
42
58
  ## 다른 프로젝트에 옮길 때 바꾸는 것
43
59
 
@@ -6,35 +6,57 @@
6
6
  - 산출물은 프로젝트 `map/.out/overview-1440.png`와 `map/.out/budget-results/`다.
7
7
  - 대상은 엔진 `serve`(포트 `MAP_PORT`, 기본 4181)다. `LIVEMAP_BUDGET_STATIC=<export 폴더>`면 `serve --static`을 잰다.
8
8
 
9
+ ## 첫 화면이 답하는 질문
10
+
11
+ 개요는 처음 보는 동료·외부 독자가 30초 안에 읽는 화면이다. 읽는 순서대로 다음 네 질문에 답하고, 답하지 않는 패널은 첫 화면에 못 들어온다.
12
+
13
+ 1. 지금 어느 마일스톤이고 얼마나 왔나: 진척 계기(화면에서 가장 큰 글자)와 마일스톤 패널.
14
+ 2. 무엇이 동작하고 무엇이 남았나: 기능 지도(가장 넓은 패널)와 기능 현황.
15
+ 3. 무엇을 기다리고, 이상은 없나: 특보와 상단 바 알약.
16
+ 4. 최근 얼마나 바뀌었나: 최근 변경, 기능별 변경, 전광판.
17
+
18
+ 운영자 신호(계획 항목 수, 열린 질문, 등급, 미분류, 어댑터 상태)는 전광판 숫자와 더보기에 둔다.
19
+
9
20
  ## 규칙과 이유
10
21
 
11
22
  | 규칙 | 값(`config.budget`) | 이유 |
12
23
  |---|---|---|
13
- | 화면은 세 질문만 | 어디까지 됐나 · 지금 무엇을 하나 · 무엇이 바뀌었나 | 셋에 답하지 않는 패널은 화면에 못 들어온다 |
14
- | 스크롤 0 | viewport 1440×900 | 눈에 전체 상태. 한국 상황판 문법 |
15
- | 패널 수 | ≤ 8 | 지금 5. 더하려면 하나를 뺀다 |
16
- | 목록 패널 행 수 | ≤ 6 (목록이 둘이면 각각) | 훑어 읽을 수 있는 한계 |
17
- | 여정 매트릭스 행 | ≤ 12 | 여정이 그보다 많으면 레인을 합친다 |
18
- | 시스템 식별자 | 0 | 첫 화면은 사용자 어휘만. 경로·파일명·sha는 상세 층 |
19
- | 내비 | ≤ 5 | 개요·여정·작업·변화·더보기 |
20
- | 깊이 | 3 | 개요목록 → 상세. 화면·API·DB 표는 장면 상세에서만 도달 |
21
- | CSP 아래 렌더 | 콘솔 오류 0, 사이드바 배경 적용 | 인라인 의존 회귀 방지 |
24
+ | 스크롤 0 | `viewport` 1440×900 | 눈에 전체 상태. 한국 상황판 문법 |
25
+ | 숨은 스크롤 0 | 모든 `.panel`, 그 안의 `.pb`·`.rows`, `overflow-y`가 `auto`·`scroll`인 자손에서 `scrollHeight − clientHeight ≤ 1`. 줄 수 제한(`-webkit-line-clamp`) 글자와 기능 지도(`.mapwrap`)·캡처(`.live`) 상자는 뺀다 | 패널 스크롤은 검사가 수 없는 곳에서 행을 자르고 공유 스크린샷에 잘린 행이 실린다. 목록은 들어가는 행만 그리고 나머지는 머리줄 "외 n →" 링크로 보낸다 |
26
+ | 패널 수 | ≤ 8 (`maxPanels`) | 지금 8. 더하려면 하나를 뺀다 |
27
+ | 목록 패널 행 수 | ≤ 6 (`maxRowsPerPanel`, 목록 묶음 `.rows`마다) | 훑어 읽을 수 있는 한계 |
28
+ | 기능 지도 행 | ≤ 12 (`.jrow`) | 기능이 13개부터는 완성 기능이 지도에서 접히고 바닥 칩 "완성 기능 n 접힘"이 기능 화면으로 이어진다. 기능 화면에는 전부 보인다 |
29
+ | 시스템 식별자 | 0 (`#root` 글자 전체) | 첫 화면은 사용자 어휘만. 경로·파일명·sha는 상세 층 |
30
+ | 내비 | ≤ 5 (`navItems`), 모두 보임 | 개요·기능·로드맵·작업·더보기 |
31
+ | 깊이 | 3 | 기능 지도 기능 선택 기능 현황의 "기능 화면 " → 단계 카드 → 단계 상세. 화면·API·DB 표는 단계 상세에서만 도달 |
32
+ | CSP 아래 렌더 | 콘솔 오류·CSP 위반 0, `body` 배경이 `--bg`(`rgb(5, 7, 10)`) | 인라인 의존 회귀 방지(`hosting-and-csp.md`) |
33
+ | 모션 줄임 | `reducedMotion: 'reduce'`에서 `document.getAnimations()` 0 | 전광판·캡처 회전·연결선 흐름이 멈추는지 |
34
+ | 상태 모양 | 기능 지도 범례(레이어 버튼) 표식 4종이 각각 동작 = 채운 원, 목업 = 반원 채움 + 실선, 계획 = 채움 없음 + 실선, 구상 = 채움 없음 + 점선. 기능이 없어 범례가 없으면 건너뛴다 | 다크 상태 색끼리는 휘도 차가 작아 색을 못 보는 독자에게 모양이 구분을 맡는다 |
35
+
36
+ 스크린샷은 모션 줄임 컨텍스트에서 `document.fonts.ready` 뒤에 찍는다. 새로 더한 숨은 스크롤·모션 줄임·상태 모양 검사는 엔진 화면 요소만 보므로, 프로젝트 자료가 달라서 새로 실패하지 않는다. 내부 용어·글자 대비·글자 크기 집합은 프로젝트 문구와 브라우저에 따라 달라져 예산 검사에 넣지 않는다.
22
37
 
23
38
  ## 개요 조각
24
39
 
25
- 개요는 `overview.json`만 읽는다. `derive.mjs`의 `overviewSlice()`가 경로·파일명·sha를 뺀 조각을 만들고, 검사는 개요 본문에서 `/api/`·`.tsx`·`.mjs`·`.sql`·`web/src`·7자 이상 16진수를 찾아 하나라도 있으면 실패한다. 개요에 무언가를 더할 때 이 조각을 거치지 않으면 식별자가 새어 들어온다.
40
+ 개요는 `overview.json`만 읽고 다시 계산하지 않는다. `derive.mjs`의 `overviewSlice(d, opts)`가 경로·파일명·sha를 뺀 조각을 만들고, 검사는 개요 본문에서 `/api/`·`.tsx`·`.mjs`·`.sql`·`web/src`·7자 이상 16진수를 찾아 하나라도 있으면 실패한다. 개요에 무언가를 더할 때 이 조각을 거치지 않으면 식별자가 새어 들어온다.
41
+
42
+ 프로젝트가 적은 문구도 개요에 나온다. 커밋 제목은 `overviewSlice`가 관례 접두어·내부 ID·식별자를 지우고 사람 커밋만 싣는다(작성자가 `[bot]`으로 끝나면 자동 커밋으로 따로 센다). 로드맵 항목·마일스톤의 목표와 결정 대기, 작업 제목, 기능·단계 이름은 엔진이 고치지 않는다. 여기에 경로나 7자 이상 16진수를 적으면 식별자 검사가 실패하므로 그 문구를 고친다.
43
+
44
+ `overview.json` 크기는 기능 수와 커밋 수에 따라 1.0.1보다 커진다. 엔진 픽스처는 3,697B이고, 기능 9개·14일 커밋 200여 건인 실제 프로젝트 하나에서 25,637B였다.
26
45
 
27
46
  ## 다시 보게 만드는 것
28
47
 
29
- - 마지막 방문 시각을 브라우저 안에 기억해(밖으로 보내지 않는다) 그 뒤 바뀐 커밋·장면에 빨간 점을 찍는다.
30
- - 상단 한 줄: 동작 장면 수·실데이터 화면 수·14일 커밋·미배포·경고·다음 걸음. 들어오자마자 읽을 문장.
48
+ - 마지막 방문 시각을 브라우저 안에 기억해(`localStorage` `map:lastVisit`, 밖으로 보내지 않는다) 그 뒤 생긴 커밋 행에 점을 찍는다.
49
+ - 전광판 한 줄: 동작 단계·완성 기능·로드맵 완료·14일 변경·규모 숫자·기능별 변경. 들어오자마자 읽는 줄이고, 정지 버튼과 모션 줄임으로 멈춘다.
31
50
  - 2주 뒤 실제로 연 뷰만 남긴다. 안 연 패널은 지운다.
32
51
 
33
52
  ## 패널을 바꾸는 절차
34
53
 
35
- 화면 파일은 엔진 소유다. 패널을 바꾸는 일은 엔진 저장소에서 하고 릴리스로 내보낸다.
54
+ 화면 원본은 엔진 저장소 `ui/`(React)이고 `site/`의 `index.html`·`map.css`·`map.js`는 그 번들 산출물이다. 패널을 바꾸는 일은 엔진 저장소에서 하고 릴리스로 내보낸다.
36
55
 
37
- 1. 답하려는 질문이 질문 중 무엇인지 적는다. 없으면 상세 층으로 간다.
56
+ 1. 답하려는 질문이 질문 중 무엇인지 적는다. 없으면 하위 화면으로 간다.
38
57
  2. 뺄 패널을 정한다.
39
- 3. 엔진 저장소 `site/map.js`의 `overview()`에서 `<section class="panel" data-budget="list|matrix">`로 만든다. 인라인 style 금지, 폭은 `data-w`.
40
- 4. 엔진 CI의 tarball 스모크(픽스처)와, 쓰는 프로젝트에 `npm install --no-save --install-links <엔진 저장소>`로 끼운 `npm run map:budget`으로 스크롤·행·식별자를 잰다. 스크린샷 `map/.out/overview-1440.png`을 사용자에게 보인다.
58
+ 3. `ui/components/panels.jsx`에 컴포넌트를 만들고 `ui/Overview.jsx` 격자에 놓는다. 틀은 `ui/components/primitives.jsx`의 `Panel`이고, 목록 패널은 `budget="list"`와 `.rows > .row`, 행 수는 `ui/lib/fit.js`의 `useFitRows`로 정한다. 마크업의 `style` 속성·`<style>` 주입은 쓰지 않고 크기·위치만 React `style` prop으로 준다. 프로젝트가 적은 문구를 담는 요소에는 `data-text="project"`, 커밋 제목에는 `data-text="commit"`을 단다.
59
+ 4. 자료가 필요하면 `overviewSlice`에 필드를 더한다(추가만 한다. 1.0.1 필드의 이름·형·값은 바꾸지 않는다).
60
+ 5. `node scripts/build-ui.mjs`로 `site/`를 다시 만들어 함께 커밋한다. CI는 다시 빌드한 결과가 커밋과 같은지 본다.
61
+ 6. 누르거나 이동하는 요소를 더하거나 바꿨으면 `scripts/click-targets.json`에 행을 맞춘다. 표에 없는 대화형 요소가 있으면 클릭 경로 크롤이 실패한다. 엔진 저장소에서 `node scripts/route-crawl.mjs --url http://127.0.0.1:<포트>/map/ --targets scripts/click-targets.json`(한 화면만 `--only <overview|journeys|roadmap|tasks|more>`, 이동 없이 개요만 `--overview-only`)로 확인한다. 크롤러는 팩에 없어 엔진 저장소에서만 돈다.
62
+ 7. 엔진 CI의 스모크(픽스처: 예산, 하위 화면 콘솔 0, 설정 경로를 바꾼 복사본의 화면 문구, 클릭 경로)와, 쓰는 프로젝트에 `npm install --no-save --install-links <엔진 저장소>`로 끼운 `npm run map:budget`으로 스크롤·숨은 스크롤·행·식별자를 잰다. 스크린샷 `map/.out/overview-1440.png`을 사용자에게 보인다.