@pghoya2956/livemap 1.0.0
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 +15 -0
- package/LICENSE +24 -0
- package/README.md +70 -0
- package/bin/livemap.mjs +6 -0
- package/budget/playwright.config.mjs +24 -0
- package/budget/view-budget.spec.mjs +70 -0
- package/docs/adapter-contract.md +96 -0
- package/docs/hosting-and-csp.md +98 -0
- package/docs/migrate.md +17 -0
- package/docs/semantic-authoring.md +77 -0
- package/docs/semantic-schema.md +44 -0
- package/docs/view-budget.md +40 -0
- package/package.json +19 -0
- package/site/fonts/LICENSE.txt +104 -0
- package/site/fonts/PretendardVariable.woff2 +0 -0
- package/site/index.html +25 -0
- package/site/map.css +167 -0
- package/site/map.js +204 -0
- package/src/adapters/bff.mjs +39 -0
- package/src/adapters/deploy.mjs +24 -0
- package/src/adapters/git.mjs +30 -0
- package/src/adapters/migrations.mjs +32 -0
- package/src/adapters/roadmap.mjs +33 -0
- package/src/adapters/router.mjs +57 -0
- package/src/adapters/tasks.mjs +51 -0
- package/src/adapters/testreport.mjs +24 -0
- package/src/adapters/tests.mjs +38 -0
- package/src/adapters/wiki.mjs +13 -0
- package/src/check.mjs +38 -0
- package/src/cli.mjs +156 -0
- package/src/derive.mjs +161 -0
- package/src/init.mjs +70 -0
- package/src/lib/graph.mjs +48 -0
- package/src/lib/util.mjs +36 -0
- package/src/serve.mjs +106 -0
- package/src/test-report.mjs +22 -0
- package/templates/README.md +29 -0
- package/templates/captures-README.md +1 -0
- package/templates/config.json +18 -0
- package/templates/journeys.json +15 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
버전마다 `## [X.Y.Z] - YYYY-MM-DD` 절을 둔다. 릴리스 워크플로가 태그 버전의 절이 있는지 확인한다.
|
|
4
|
+
|
|
5
|
+
## [1.0.0] - 2026-09-17
|
|
6
|
+
|
|
7
|
+
첫 공개 판. 한 프로젝트 저장소 안에 있던 상황판 엔진을 패키지로 옮겼다.
|
|
8
|
+
|
|
9
|
+
- 명령 `livemap build | check | serve | export | init | test-report | --version`. npm bin 심링크로 불러도 실행된다.
|
|
10
|
+
- 기본 루트는 명령을 부른 폴더(`process.cwd()`), 설정은 `map/config.json`, 캡처는 `captures.site`(기본 `map/captures`).
|
|
11
|
+
- `config.json`의 `engine`(major)이 엔진과 다르면 exit 2. 키가 없으면 1로 본다.
|
|
12
|
+
- `export <dir>`: 화면·서체·캡처·생성물을 `/map/` 주소 배치 그대로 한 폴더에 모은다. `serve --static <dir>`이 같은 배치를 준다.
|
|
13
|
+
- 서체(Pretendard Variable, SIL OFL)를 `site/fonts/`에 번들하고 CSS는 상대 경로로 부른다.
|
|
14
|
+
- 화면 예산 검사 설정 `budget/playwright.config.mjs`는 프로젝트 루트 기준으로 산출물을 쓴다. `@playwright/test`는 선택적 peer다.
|
|
15
|
+
- 프로젝트 어댑터가 같은 이름의 참조 어댑터를 가리면 한 줄로 알린다.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pghoya2956
|
|
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.
|
|
22
|
+
|
|
23
|
+
The bundled font in site/fonts/ is licensed separately under the SIL Open Font
|
|
24
|
+
License 1.1; see site/fonts/LICENSE.txt.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# livemap
|
|
2
|
+
|
|
3
|
+
Project status board engine. It scans a repository (routes, API handlers, migrations, tests, task docs, git history, deploy manifests) into a graph and serves a one-screen board of journeys, screens, APIs, tests and work in progress. No runtime dependencies, Node 22.
|
|
4
|
+
|
|
5
|
+
저장소를 스캔해 "어디까지 실제로 동작하나, 지금 무엇을 하나, 무엇이 바뀌었나"를 한 화면에 보여주는 상황판 엔진이다. 프로젝트 코드를 실행하지 않고 파일과 git만 읽는다.
|
|
6
|
+
|
|
7
|
+
## 설치
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -D -E @pghoya2956/livemap
|
|
11
|
+
npm i -D -E @playwright/test@1.63.0 # 화면 예산 검사를 쓸 때만
|
|
12
|
+
npx --no livemap init # map/ 초안·.gitignore·npm 스크립트
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`init`이 만드는 것: `map/config.json`, `map/semantic/journeys.json`, `map/README.md`, `map/captures/README.md`, `.gitignore`의 `map/.out/`, npm 스크립트 `map`·`map:check`·`map:serve`·`map:export`·`test:report`(Playwright가 있으면 `map:budget`). 다시 실행하면 아무것도 바꾸지 않는다.
|
|
16
|
+
|
|
17
|
+
## 명령
|
|
18
|
+
|
|
19
|
+
| 명령 | 하는 일 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `livemap build [--out map/.out]` | 스캔 → `graph.json`·`data.json`·`overview.json` |
|
|
22
|
+
| `livemap check` | 정합 검사. 오류가 있으면 exit 1 |
|
|
23
|
+
| `livemap serve [--port 4180]` | `http://127.0.0.1:4180/map/`, 요청마다 재빌드(5초 캐시) |
|
|
24
|
+
| `livemap serve --static <dir>` | export 폴더를 재빌드 없이 같은 배치로 |
|
|
25
|
+
| `livemap export <dir>` | 화면·서체·캡처·생성물을 `/map/` 배치 그대로 한 폴더에 |
|
|
26
|
+
| `livemap test-report` | 단위 검사를 JUnit으로(`config.tests`) |
|
|
27
|
+
| `livemap --version` | 버전 |
|
|
28
|
+
| 화면 예산 | `npx --no playwright test --config node_modules/@pghoya2956/livemap/budget/playwright.config.mjs` |
|
|
29
|
+
|
|
30
|
+
명령은 프로젝트 루트에서 부른다. CI에서는 npm 스크립트나 `npx --no livemap`을 쓴다.
|
|
31
|
+
|
|
32
|
+
## 설정
|
|
33
|
+
|
|
34
|
+
`map/config.json` 하나가 프로젝트별이다. 참조 어댑터는 React Router + Node BFF + SQL migration + Markdown 작업 문서 관례를 읽는다. 스택이 다르면 `map/adapters/<이름>.mjs`에 프로젝트 어댑터를 둔다.
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"engine": 1,
|
|
39
|
+
"project": { "name": "ReefDesk", "host": "https://reefdesk.example.invalid" },
|
|
40
|
+
"adapters": ["router", "bff", "migrations", "tests", "wiki", "tasks", "roadmap", "git", "deploy", "testreport"],
|
|
41
|
+
"router": { "app": "web/src/App.tsx", "pagesDir": "web/src/pages", "localDirs": ["web/src/pages"], "mockPattern": "/mock'", "livePattern": "lib/queries", "hookApi": { "Bookings": "/api/bookings" } },
|
|
42
|
+
"bff": { "server": "app/server.mjs" },
|
|
43
|
+
"migrations": { "dir": "db/migrations" },
|
|
44
|
+
"tests": { "dir": "tests", "gatePattern": "ALLOW_DESTRUCTIVE", "report": "map/.out/junit.xml" },
|
|
45
|
+
"git": { "branch": "main", "sinceDays": 14, "areas": [["web/", "화면"], ["app/", "서버"]], "runtimePaths": ["web", "app"] },
|
|
46
|
+
"semantic": "map/semantic/journeys.json",
|
|
47
|
+
"captures": { "site": "map/captures" },
|
|
48
|
+
"floors": { "screen": 5, "api": 3 },
|
|
49
|
+
"budget": { "viewport": [1440, 900], "maxPanels": 8, "maxRowsPerPanel": 6, "navItems": 5 }
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 문서
|
|
54
|
+
|
|
55
|
+
| 문서 | 내용 |
|
|
56
|
+
|---|---|
|
|
57
|
+
| [docs/adapter-contract.md](docs/adapter-contract.md) | 어댑터 시그니처·노드·엣지·프로젝트 어댑터 |
|
|
58
|
+
| [docs/semantic-authoring.md](docs/semantic-authoring.md) | 여정 파일 작성 |
|
|
59
|
+
| [docs/semantic-schema.md](docs/semantic-schema.md) | 시맨틱 레이어 모델 |
|
|
60
|
+
| [docs/hosting-and-csp.md](docs/hosting-and-csp.md) | export·정적 서빙·CSP·CI |
|
|
61
|
+
| [docs/view-budget.md](docs/view-budget.md) | 화면 예산 규칙 |
|
|
62
|
+
| [docs/migrate.md](docs/migrate.md) | major 이행 |
|
|
63
|
+
|
|
64
|
+
## 버전
|
|
65
|
+
|
|
66
|
+
semver. 프로젝트는 정확한 버전으로 고정한다(`-E`). `config.json` 키, 여정 형식, 어댑터 계약, 명령·종료 코드, 생성물 파일 이름, export 배치, 예산 설정 경로를 바꾸면 major다. 변경 기록은 [CHANGELOG.md](CHANGELOG.md).
|
|
67
|
+
|
|
68
|
+
## 라이선스
|
|
69
|
+
|
|
70
|
+
MIT. `site/fonts/`의 Pretendard Variable은 SIL Open Font License 1.1이다.
|
package/bin/livemap.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// 상황판 화면 예산 검사 설정. 프로젝트 루트(cwd)에서 부른다:
|
|
2
|
+
// npx --no playwright test --config node_modules/@pghoya2956/livemap/budget/playwright.config.mjs
|
|
3
|
+
// 엔진 serve를 4181 포트(loopback)에 띄우고 개요를 잰다. LIVEMAP_BUDGET_STATIC=<export 폴더>면 serve --static을 잰다.
|
|
4
|
+
import { defineConfig } from '@playwright/test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const bin = resolve(here, '..', 'bin', 'livemap.mjs');
|
|
10
|
+
const root = process.cwd();
|
|
11
|
+
const port = Number(process.env.MAP_PORT || 4181);
|
|
12
|
+
const staticDir = process.env.LIVEMAP_BUDGET_STATIC;
|
|
13
|
+
const target = staticDir ? ` --static ${JSON.stringify(resolve(root, staticDir))}` : '';
|
|
14
|
+
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
testDir: here,
|
|
17
|
+
testMatch: /view-budget\.spec\.mjs/,
|
|
18
|
+
outputDir: resolve(root, 'map/.out/budget-results'),
|
|
19
|
+
timeout: 30_000,
|
|
20
|
+
retries: 0,
|
|
21
|
+
reporter: 'list',
|
|
22
|
+
use: { baseURL: `http://127.0.0.1:${port}/map/`, viewport: { width: 1440, height: 900 } },
|
|
23
|
+
webServer: { command: `node ${JSON.stringify(bin)} serve${target} --port ${port}`, url: `http://127.0.0.1:${port}/map/data/overview.json`, reuseExistingServer: false, timeout: 60_000, cwd: root },
|
|
24
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 화면 예산 검사: 첫 화면(개요)이 단순함 규칙을 지키는지 잰다. 어기면 CI가 실패한다.
|
|
2
|
+
// 1440×900에서 스크롤 없음 · 패널 8 이하 · 목록 패널 6행 이하 · 여정 매트릭스 12행 이하 · 시스템 식별자 0 · 내비 5
|
|
3
|
+
import { test, expect } from '@playwright/test';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// 설정·스크린샷은 프로젝트 루트(cwd) 기준이다. 패키지 폴더 기준으로 쓰면 산출물이 node_modules 안에 생긴다.
|
|
8
|
+
const cfg = JSON.parse(readFileSync(resolve(process.cwd(), 'map/config.json'), 'utf8')).budget;
|
|
9
|
+
const IDENT = /\/api\/|\.tsx\b|\.mjs\b|\.sql\b|\bweb\/src\b|\b[0-9a-f]{7,40}\b/;
|
|
10
|
+
|
|
11
|
+
test('CSP 아래서 오류 없이 렌더된다', async ({ page }) => {
|
|
12
|
+
const errors = [];
|
|
13
|
+
page.on('pageerror', (e) => errors.push(String(e.message)));
|
|
14
|
+
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 });
|
|
17
|
+
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)');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('개요는 한 화면에 들어간다', async ({ page }) => {
|
|
23
|
+
await page.goto('/map/#/overview');
|
|
24
|
+
await page.waitForSelector('.panel');
|
|
25
|
+
const [scrollH, clientH] = await page.evaluate(() => [document.documentElement.scrollHeight, document.documentElement.clientHeight]);
|
|
26
|
+
expect(scrollH, `스크롤 높이 ${scrollH} > 뷰포트 ${clientH}`).toBeLessThanOrEqual(clientH);
|
|
27
|
+
const bodyW = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
28
|
+
expect(bodyW).toBeLessThanOrEqual(cfg.viewport[0]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('패널·행·내비 수가 예산 안이다', async ({ page }) => {
|
|
32
|
+
await page.goto('/map/#/overview');
|
|
33
|
+
await page.waitForSelector('.panel');
|
|
34
|
+
expect(await page.locator('.panel').count()).toBeLessThanOrEqual(cfg.maxPanels);
|
|
35
|
+
expect(await page.locator('.nav a').count()).toBeLessThanOrEqual(cfg.navItems);
|
|
36
|
+
const lists = page.locator('.panel[data-budget="list"]');
|
|
37
|
+
for (let i = 0; i < await lists.count(); i += 1) {
|
|
38
|
+
const rows = await lists.nth(i).locator('.row, .bar').count();
|
|
39
|
+
// 한 패널에 목록이 둘이면 각각 6행 이하로 본다(작업 6 + 다음 한 걸음 4, 변화 6 + 3).
|
|
40
|
+
const groups = await lists.nth(i).locator('.rows, .bars').count();
|
|
41
|
+
expect(rows, `패널 ${i} 행 ${rows}`).toBeLessThanOrEqual(cfg.maxRowsPerPanel * Math.max(1, groups));
|
|
42
|
+
}
|
|
43
|
+
const matrix = page.locator('.panel[data-budget="matrix"] .jrow');
|
|
44
|
+
expect(await matrix.count()).toBeLessThanOrEqual(12);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('첫 화면에는 시스템 식별자가 없다', async ({ page }) => {
|
|
48
|
+
await page.goto('/map/#/overview');
|
|
49
|
+
await page.waitForSelector('.panel');
|
|
50
|
+
const text = await page.locator('.main').innerText();
|
|
51
|
+
const hit = text.split('\n').find((l) => IDENT.test(l));
|
|
52
|
+
expect(hit, `식별자 노출: ${hit}`).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('여정 장면 상세까지 3번 안에 닿는다', async ({ page }) => {
|
|
56
|
+
await page.goto('/map/#/overview');
|
|
57
|
+
await page.waitForSelector('.jrow');
|
|
58
|
+
await page.locator('.jrow').first().click();
|
|
59
|
+
await page.waitForSelector('.scene');
|
|
60
|
+
await page.locator('.scene').first().click();
|
|
61
|
+
await page.waitForSelector('.detail');
|
|
62
|
+
expect(await page.locator('.detail .node').count()).toBeGreaterThan(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
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') });
|
|
70
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# 어댑터 계약
|
|
2
|
+
|
|
3
|
+
어댑터는 저장소의 한 종류 사실을 읽어 그래프에 노드·엣지로 넣는 함수다. 화면은 어댑터를 모르고 파생 뷰만 읽으므로, 스택이 달라지면 어댑터만 바꾸면 된다.
|
|
4
|
+
|
|
5
|
+
## 시그니처
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// map/adapters/<name>.mjs ← 프로젝트 소유 어댑터. 엔진 업그레이드가 건드리지 않는다
|
|
9
|
+
export default function name(g, fs, cfg) {
|
|
10
|
+
// ... 노드·엣지 추가
|
|
11
|
+
return null; // 정상
|
|
12
|
+
// return '설명'; // partial: 일부만 읽음(예: 파일 없음). 생성은 계속되고 상단에 주황 점
|
|
13
|
+
// throw new Error('…'); // failed: 빨간 점 + check 오류. 그래도 다른 어댑터는 돈다
|
|
14
|
+
}
|
|
15
|
+
```
|
|
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`.
|
|
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
|
+
- `cfg` — `map/config.json` 전체. 자기 키(`cfg.<name>`)만 읽고, 다른 어댑터의 키는 `?.`로 방어한다.
|
|
20
|
+
|
|
21
|
+
## 어디에 두나
|
|
22
|
+
|
|
23
|
+
엔진은 `config.adapters`의 이름마다 프로젝트 `map/adapters/<name>.mjs`를 먼저 찾고, 없으면 패키지에 딸린 참조 어댑터(`src/adapters/<name>.mjs`)를 쓴다. 프로젝트 파일이 참조 어댑터와 이름이 같으면 `build`·`check`가 "프로젝트 어댑터가 참조 어댑터를 가림: <name>" 한 줄을 알린다. 참조 어댑터의 결함은 엔진 저장소에서 고치고, 프로젝트만의 스택은 다른 이름의 프로젝트 어댑터로 둔다.
|
|
24
|
+
|
|
25
|
+
같은 `(kind, id)`를 두 번 `add`하면 props가 병합되고 첫 `src`가 남는다. 그래서 router가 만든 `api` 노드에 bff가 method·calls를 덧붙일 수 있다.
|
|
26
|
+
|
|
27
|
+
## 출처(src)를 반드시 남긴다
|
|
28
|
+
|
|
29
|
+
`src: { file, line, rule }`. 상세 화면이 "이 값은 어느 파일 몇 번째 줄을 어떤 규칙으로 읽었나"를 보여주는 근거다. 출처 없는 값은 사용자가 믿을 수 없고, 어댑터가 잘못 읽었을 때 어디를 고칠지 알 수 없다. `rule`은 `'router:<Route path>'`처럼 짧게.
|
|
30
|
+
|
|
31
|
+
## 노드 종류와 엣지
|
|
32
|
+
|
|
33
|
+
| kind | id | 만드는 어댑터 |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| screen | 라우트 경로 | router |
|
|
36
|
+
| api | 경로(`/api/x`, `:id` 허용) | router(호출 측), bff(선언 측) |
|
|
37
|
+
| function | DB 함수 짧은 이름 | bff, migrations |
|
|
38
|
+
| table | `schema.table` | migrations |
|
|
39
|
+
| migration | 파일명 | migrations |
|
|
40
|
+
| test | 파일 경로 | tests |
|
|
41
|
+
| commit | 짧은 sha | git |
|
|
42
|
+
| decision | 위키 slug 또는 `DEC-nn`·`PN-nn` | wiki, tasks |
|
|
43
|
+
| task | 폴더명 | tasks |
|
|
44
|
+
| ledger | `running-i`·`waiting-i` | tasks |
|
|
45
|
+
| deploy | `head`·`homelab` | git, deploy |
|
|
46
|
+
| testreport | `last` | testreport |
|
|
47
|
+
|
|
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`도 손봐야 하므로, 먼저 기존 종류로 표현할 수 없는지 본다.
|
|
49
|
+
|
|
50
|
+
## 순서
|
|
51
|
+
|
|
52
|
+
`config.adapters` 순서로 돈다. `tests`·`git`은 `screen`·`api`가 있어야 covers·changes를 잇고, `testreport`는 `git`이 만든 `deploy:head`로 "최신 커밋" 여부를 판정한다. 새 어댑터가 다른 어댑터의 노드에 기대면 그 뒤에 둔다.
|
|
53
|
+
|
|
54
|
+
## 골격
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
// map/adapters/openapi.mjs
|
|
58
|
+
// OpenAPI 어댑터: openapi.json 의 paths 에서 api 노드를 만든다. 언어와 무관하다.
|
|
59
|
+
export default function openapi(g, fs, cfg) {
|
|
60
|
+
const c = cfg.openapi; // { "file": "docs/openapi.json" }
|
|
61
|
+
if (!fs.has(c.file)) return `OpenAPI 문서 없음: ${c.file}`;
|
|
62
|
+
const doc = JSON.parse(fs.read(c.file));
|
|
63
|
+
let n = 0;
|
|
64
|
+
for (const [path, ops] of Object.entries(doc.paths || {})) {
|
|
65
|
+
for (const method of Object.keys(ops)) {
|
|
66
|
+
const p = path.replace(/\{(\w+)\}/g, ':$1');
|
|
67
|
+
g.add('api', p, `${method.toUpperCase()} ${p}`, { method: method.toUpperCase(), calls: [], operationId: ops[method].operationId }, { file: c.file, line: null, rule: 'openapi:paths' });
|
|
68
|
+
n += 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return n ? null : 'paths 0건';
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 단위 검사
|
|
76
|
+
|
|
77
|
+
엔진 저장소의 `test/fixtures/mini/`는 참조 어댑터가 읽는 최소 저장소이고 `test/adapters.test.mjs`가 기대값을 고정한다. 참조 어댑터를 새로 쓰면 픽스처에 그 스택의 최소 파일을 더하고 기대값을 한 줄 추가한다. 프로젝트 소유 어댑터는 같은 방식의 검사를 프로젝트 안에 둔다(엔진의 `buildGraph(root)`를 import해 작은 픽스처 폴더를 읽힌다). 정규식이 깨졌을 때 빈 표 대신 여기서 먼저 실패해야 한다. 바닥값(`config.floors`)은 두 번째 방어선이다.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
test('openapi: paths → api 노드', () => {
|
|
81
|
+
assert.deepEqual(g.of('api').map((a) => a.id).sort(), ['/items', '/items/:id']);
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 스택별 대안
|
|
86
|
+
|
|
87
|
+
| 스택 | 우선 | 대안 |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| React Router 선언형 | 참조 router | Next.js는 `app/**/page.tsx` 파일 트리 → 경로; Remix는 `routes/` 파일명 |
|
|
90
|
+
| Express·Fastify·Hono | OpenAPI 문서(있으면) | 라우터 등록 호출 `app.get('/x'` 정규식 |
|
|
91
|
+
| Rails·Django·Spring | `rails routes`/`manage.py show_urls`/Actuator 덤프를 CI에서 파일로 저장 → 파일 어댑터 | 소스 파싱은 tree-sitter |
|
|
92
|
+
| Prisma·Django ORM·Alembic | migration 폴더 관례 | 스키마 파일(`schema.prisma`) |
|
|
93
|
+
| Jest·pytest·Go test | JUnit XML(거의 모든 러너가 낸다) → testreport | 소스에서 라우트 문자열 grep |
|
|
94
|
+
| 작업 문서가 tasks/가 아님 | tasks 어댑터의 `dir`·절 이름만 바꿈 | Linear·GitHub Issues는 CI에서 JSON 덤프 → 파일 어댑터 |
|
|
95
|
+
|
|
96
|
+
원칙은 "스택이 이미 내놓는 산출물을 읽는다"이다. 산출물은 형식이라 언어를 넘어 재사용되고, 소스 정규식은 그 프로젝트에서만 산다.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# 호스팅과 CSP
|
|
2
|
+
|
|
3
|
+
## 로컬
|
|
4
|
+
|
|
5
|
+
`npm run map:serve`(`livemap serve`) → `http://127.0.0.1:4180/map/`. 요청마다 재빌드(5초 캐시)라 병합 전 작업 중 상태가 그대로 보인다. loopback에만 바인드한다. 배포와 같은 CSP를 걸어 인라인 의존을 로컬에서 먼저 잡는다.
|
|
6
|
+
|
|
7
|
+
## 배포 폴더 만들기: export
|
|
8
|
+
|
|
9
|
+
`livemap export <dir>`이 화면 파일·서체·프로젝트 캡처·생성물을 브라우저 주소 배치 그대로 한 폴더에 모은다. 먼저 `livemap build`로 생성물을 만든다. 주소→파일 배치는 `serve`와 같은 함수가 정하므로, 로컬에서 본 배치가 그대로 배포된다.
|
|
10
|
+
|
|
11
|
+
| 브라우저 주소 | export 폴더 안 | 가져오는 곳 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `/map/`·`/map/map.css`·`/map/map.js` | `index.html`·`map.css`·`map.js` | 패키지 `site/` |
|
|
14
|
+
| 서체(`/map/` 아래 fonts 폴더) | fonts 폴더의 `PretendardVariable.woff2`·`LICENSE.txt` | 패키지 `site` 폴더의 fonts |
|
|
15
|
+
| `/map/captures/<id>.jpg` | `captures/<id>.jpg` | 프로젝트 `config.captures.site`의 `*.jpg` |
|
|
16
|
+
| `/map/data/*.json` | `data/data.json`·`data/overview.json`·`data/graph.json` | `--out`(기본 `map/.out`)의 build 산출 |
|
|
17
|
+
|
|
18
|
+
- 생성물 셋 중 하나라도 없으면 exit 2. 빈 상황판이 이미지에 실리지 않는다.
|
|
19
|
+
- 쓰기 전에 대상 폴더를 비운다. 비우는 대상은 없는 폴더, 빈 폴더, 이전 export 표시 파일(`.livemap-export`)이 있는 폴더뿐이다. 그 밖의 폴더는 지우지 않고 exit 2.
|
|
20
|
+
- `livemap serve --static <dir>`이 export 폴더를 재빌드 없이 같은 배치·같은 CSP로 준다. 배포 서버에 넣기 전 확인용이다.
|
|
21
|
+
|
|
22
|
+
## 정적 서빙 붙이기
|
|
23
|
+
|
|
24
|
+
원칙은 "이미 있는 정적 서버에 `/map/` 경로 하나". 새 인프라 객체가 없고 제품 배포 주기로 갱신되며 상황판이 자기 뒤처짐을 표시한다. 인증 없는 공개 표면을 만들지 않는다(비공개 저장소의 계획·커밋 메시지가 보인다).
|
|
25
|
+
|
|
26
|
+
서빙 계약은 export 폴더 하나다.
|
|
27
|
+
|
|
28
|
+
- `/map/*` → export 폴더. `/map/data/*`에만 `cache-control: no-store`, 나머지는 `no-cache`.
|
|
29
|
+
- `/map` → `/map/` 302. SPA 폴백 없이 없는 파일은 404. 경로 탈출(`..`)·숨김 파일 거부.
|
|
30
|
+
|
|
31
|
+
Node 예(앱 서버에 넣는 형태). 이미지에는 CI가 만든 export 폴더만 넣는다(`COPY map/.out/site/ ./map/site/`).
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
const mapDir = resolve(fileURLToPath(new URL('../map/site/', import.meta.url)));
|
|
35
|
+
async function serveMap(res, sub) {
|
|
36
|
+
let decoded; try { decoded = decodeURIComponent(sub); } catch { fail(404, 'not_found'); }
|
|
37
|
+
if (decoded === '' || decoded === '/') decoded = '/index.html';
|
|
38
|
+
if (decoded.includes('\0') || decoded.split('/').some(p => p.startsWith('.'))) fail(404, 'not_found');
|
|
39
|
+
const target = resolve(mapDir, '.' + decoded);
|
|
40
|
+
const type = staticTypes.get(extname(target));
|
|
41
|
+
if (!target.startsWith(mapDir + sep) || !type) fail(404, 'not_found');
|
|
42
|
+
const info = await stat(target).catch(() => null);
|
|
43
|
+
if (!info || !info.isFile()) fail(404, 'not_found');
|
|
44
|
+
res.writeHead(200, { 'content-type': type, 'cache-control': decoded.startsWith('/data/') ? 'no-store' : 'no-cache' });
|
|
45
|
+
res.end(await readFile(target));
|
|
46
|
+
}
|
|
47
|
+
// handle(): if (url.pathname === '/map') 302 → '/map/'; if (url.pathname.startsWith('/map/')) serveMap(res, url.pathname.slice(4))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`staticTypes`에는 `.html`·`.css`·`.js`·`.json`·`.jpg`·`.woff2`가 있어야 한다. GitHub Pages·Cloudflare Pages는 공개 표면이라 기본으로 권하지 않는다.
|
|
51
|
+
|
|
52
|
+
## CSP
|
|
53
|
+
|
|
54
|
+
`default-src 'self'; style-src 'self'; script-src 'self'`가 걸린 서버가 흔하다. 이 정책은 다음을 막는다.
|
|
55
|
+
|
|
56
|
+
- `<style>`·`<script>` 인라인 블록
|
|
57
|
+
- `style="…"` 속성(요소 인라인 스타일도 style-src 위반이다)
|
|
58
|
+
- Google Fonts 등 외부 스타일·폰트
|
|
59
|
+
- `data:` 이미지
|
|
60
|
+
|
|
61
|
+
엔진 화면은 그래서 `index.html`(마크업만) + `map.css` + `map.js`이고, 동적 폭은 `data-w` 속성을 붙인 뒤 JS에서 `el.style.width = …`로 적용한다(CSSOM 조작은 허용된다). 화면을 고칠 때 이 셋을 지키면 어느 CSP에서도 뜬다. 로컬 serve와 예산 검사가 같은 CSP를 걸어 회귀를 잡는다.
|
|
62
|
+
|
|
63
|
+
## CI
|
|
64
|
+
|
|
65
|
+
CI·문서는 엔진을 npm 스크립트로 부른다. 워크플로 `run:`에는 `node_modules/.bin`이 PATH에 없고, `npx livemap`은 CI에서 `--yes`를 가정해 로컬 설치가 없으면 레지스트리의 다른 패키지를 받을 수 있다. 직접 부를 때는 `npx --no livemap …`을 쓴다.
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
map:
|
|
69
|
+
name: 상황판 검사 (정합·화면 예산)
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v5
|
|
73
|
+
with: { fetch-depth: 0 } # 14일 로그·배포 sha 대비에 이력이 필요하다
|
|
74
|
+
- uses: actions/setup-node@v5
|
|
75
|
+
with: { node-version: 22, cache: npm, cache-dependency-path: package-lock.json }
|
|
76
|
+
- run: npm ci --no-audit --no-fund
|
|
77
|
+
- run: npx playwright install --with-deps chromium
|
|
78
|
+
- run: npm run map:check
|
|
79
|
+
- run: npm run map:budget
|
|
80
|
+
- uses: actions/upload-artifact@v4
|
|
81
|
+
if: always()
|
|
82
|
+
with: { name: map-overview, path: map/.out/overview-1440.png, if-no-files-found: ignore }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
이미지·사이트 빌드 잡에서는 빌드 직전에 생성물과 export 폴더를 만든다. 이때 매니페스트 태그는 아직 이전 배포를 가리키므로, 지금 커밋이 곧 배포본임을 알려준다.
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
- run: npm ci --no-audit --no-fund
|
|
89
|
+
- run: npm run map
|
|
90
|
+
env: { MAP_ASSUME_DEPLOYED_SHA: ${{ github.sha }} }
|
|
91
|
+
- run: npm run map:export
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Dockerfile에는 `COPY map/.out/site/ ./map/site/` 한 줄. 생성기·어댑터·검사는 이미지에 넣지 않는다.
|
|
95
|
+
|
|
96
|
+
## 배포 뒤 검증
|
|
97
|
+
|
|
98
|
+
상태 코드 200은 렌더를 증명하지 않는다. Chromium으로 열어 `.panel` 개수, 사이드바 배경색, 콘솔 오류 0을 본다. 포트포워드 뒤에서 프로덕션 호스트 헤더가 필요하면 `page.route('**/*', …)`로 요청을 로컬 포트로 바꿔 태운다(Chromium은 `extraHTTPHeaders`의 Host를 거부한다).
|
package/docs/migrate.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 이행 문서
|
|
2
|
+
|
|
3
|
+
`map/config.json`의 `engine`은 그 설정이 따르는 엔진 major다. 엔진은 자기 major와 다르면 멈추고 이 문서를 가리킨다. major가 바뀌는 릴리스마다 아래에 절을 더한다.
|
|
4
|
+
|
|
5
|
+
## 옛 설치 방식 → 1.x
|
|
6
|
+
|
|
7
|
+
프로젝트 `map/` 안에 엔진 파일을 복사해 쓰던 설치에서 옮길 때.
|
|
8
|
+
|
|
9
|
+
1. `npm i -D -E @pghoya2956/livemap@<버전>`(예산 검사를 쓰면 `@playwright/test`도 프로젝트 devDependency로 둔다).
|
|
10
|
+
2. `map/`에서 엔진 파일을 지운다: `cli.mjs`·`check.mjs`·`derive.mjs`·`serve.mjs`·`lib/`·`site/`의 `index.html`·`map.css`·`map.js`·`tests/`·`scripts/`·`semantic/schema.md`, 그리고 참조 어댑터 사본(`map/adapters/`에서 프로젝트가 직접 쓴 어댑터만 남긴다).
|
|
11
|
+
3. 캡처를 옛 화면 폴더(`map/site`) 아래 captures 폴더에서 `map/captures/`로 옮기고 `config.json`의 `captures.site`를 `map/captures`로 바꾼다.
|
|
12
|
+
4. `config.json`에 `"engine": 1`을 더한다.
|
|
13
|
+
5. npm 스크립트를 `livemap` 명령으로 바꾼다(`livemap init`이 없는 스크립트만 넣고 값이 다른 스크립트는 보고한다). 어댑터 단위 검사 스크립트는 지운다. 참조 어댑터 검사는 엔진 저장소 CI가 돈다.
|
|
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`이 통과하는지 본다.
|
|
16
|
+
|
|
17
|
+
로컬에서 고친 엔진을 끼워 볼 때는 `npm install --no-save --install-links <엔진 저장소 경로>`를 쓴다. `--install-links` 없이 폴더를 설치하면 심링크가 되어 예산 설정이 `@playwright/test`를 엔진 저장소 쪽에서 찾다 실패한다.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# 여정 파일 작성
|
|
2
|
+
|
|
3
|
+
`map/semantic/journeys.json`은 상황판에서 유일하게 손으로 유지하는 층이다. 스토리 맵의 backbone(배우가 하는 활동을 순서대로)과 같다. 코드가 아니라 제품의 뜻을 적는 자리이므로 사용자 어휘만 쓰고 라우트·파일명은 `screens`·`capture` 필드에만 둔다.
|
|
4
|
+
|
|
5
|
+
## 스키마
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"project": { "name": "ReefDesk", "tagline": "…", "host": "https://…" },
|
|
10
|
+
"actors": { "diver": "다이버", "resort": "리조트 직원" },
|
|
11
|
+
"statusLegend": { "live": "실데이터로 동작", "mock": "화면만(목업 데이터)", "planned": "미착수(스펙 있음)", "next": "다음 스펙" },
|
|
12
|
+
"journeys": [
|
|
13
|
+
{
|
|
14
|
+
"id": "booking", "title": "예약 요청부터 확정까지", "actor": "instructor", "lane": "다이버·강사 여정",
|
|
15
|
+
"goal": "견적을 예약으로 바꾸고 예약금까지 내서 확정한다",
|
|
16
|
+
"steps": [
|
|
17
|
+
{
|
|
18
|
+
"id": "request", "label": "예약 요청",
|
|
19
|
+
"intent": "견적을 예약 요청으로 보낸다",
|
|
20
|
+
"actor": "instructor",
|
|
21
|
+
"status": "mock",
|
|
22
|
+
"screens": ["/me/bookings"],
|
|
23
|
+
"apis": ["/api/bookings"],
|
|
24
|
+
"capture": "my-bookings",
|
|
25
|
+
"note": "공급 확인 대기 상태 모델",
|
|
26
|
+
"refs": ["PN-18", "DEC-35", "trust-boundary"],
|
|
27
|
+
"reviewedAt": "2026-09-14"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"captures": { "dir": "map/captures", "note": "출처와 갱신 규칙" }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
필드 의미:
|
|
37
|
+
|
|
38
|
+
- `status` — `live`(화면이 실데이터로 동작) / `mock`(화면은 있으나 고정 데이터) / `planned`(스펙은 있고 이 장면의 화면이 없음) / `next`(다음 스펙). 네 값만 쓴다. 판정 순서는 화면부터다: 그 장면을 보여주는 라우트가 코드에 있으면 `live` 아니면 `mock`이고, 라우트가 없을 때만 `planned`(스펙 있음)·`next`(스펙 없음)다. "흐름은 미착수지만 화면은 목업"은 `mock`에 note로 적는다. `planned`에 `screens`를 두면 check가 경고한다. 여정 상태는 장면에서 유도된다(전부 live면 live, 하나라도 live면 partial).
|
|
39
|
+
- `screens` — 코드에 실제로 있는 라우트만. 없는 라우트는 `check`가 오류로 막는다. 여러 라우트를 한 장면에 둘 수 있다.
|
|
40
|
+
- `apis` — 화면에서 유도되지 않는 API를 명시할 때만.
|
|
41
|
+
- `capture` — `<config.captures.site>/<id>.jpg`(기본 `map/captures/<id>.jpg`). 없으면 장면 카드가 빈 칸으로 그려지고 `check`가 경고한다.
|
|
42
|
+
- `refs` — `DEC-nn`·`PN-nn`(tasks 스펙 문서에서 정의된 것으로 자동 연결), 위키 결정 slug(`decisions/<slug>.md`). 해결되지 않는 DEC/PN 참조는 오류다.
|
|
43
|
+
- `reviewedAt` — 사람이 이 장면을 마지막으로 확인한 날. 그 뒤 화면 파일이 바뀌면 "확인 필요" 경고가 뜬다. 상태를 바꿀 때 같이 갱신한다.
|
|
44
|
+
- `actor` — 장면 단위 배우가 여정 배우와 다를 때만(예: 예약 여정 안의 "리조트 접수함").
|
|
45
|
+
|
|
46
|
+
## 인터뷰 질문
|
|
47
|
+
|
|
48
|
+
여정 파일을 처음 쓸 때 사용자에게 한 번에 묻는다. 답이 문서(스펙·위키)에 이미 있으면 거기서 가져오고 묻지 않는다.
|
|
49
|
+
|
|
50
|
+
1. 배우가 누구인가. 돈을 내는 사람, 운영하는 사람, 관리하는 사람으로 나뉘는가.
|
|
51
|
+
2. 각 배우가 이루려는 목표를 한 문장씩. 목표 하나가 여정 하나다.
|
|
52
|
+
3. 목표까지 가는 장면을 순서대로. 장면은 "사용자가 무엇을 원해서 무엇을 하는 순간"이다. 화면 단위가 아니라 의도 단위다(한 화면에 두 장면이 있을 수 있고, 화면 없는 장면도 있다).
|
|
53
|
+
4. 지금 각 장면이 어디까지 되어 있나. 실데이터로 동작 / 화면만 / 스펙만 / 다음.
|
|
54
|
+
5. 어느 스펙·결정이 그 장면을 정했나(DEC·PN·위키).
|
|
55
|
+
6. 확정된 화면 스크린샷이 있나. 있으면 장면과 짝을 맞춘다.
|
|
56
|
+
7. 첫 번째 walking skeleton(끝까지 한 번 통과하는 최소 슬라이스)은 어디까지인가. 이것이 "다음 한 걸음"의 기준이 된다.
|
|
57
|
+
|
|
58
|
+
## 규칙
|
|
59
|
+
|
|
60
|
+
- 장면 id는 여정 안에서 유일해야 한다(check 오류). intent는 `next`가 아니면 비우지 않는다(check 경고).
|
|
61
|
+
- 여정은 12개 이하로 둔다. 개요 매트릭스가 한 화면에 들어가는 한계이며, 그보다 많으면 레인을 합칠 때다.
|
|
62
|
+
- 장면 이름은 명사구 2~5어절, intent는 한 문장. 시스템 말투("데이터를 조회한다")가 아니라 사용자 말투("오늘 누가 오는지 본다").
|
|
63
|
+
- 여정에 없는 화면은 "미분류"로 센다. 화면을 어딘가 억지로 넣기보다, 정말 어느 여정에도 안 속하면 그 화면이 필요한지 묻는다.
|
|
64
|
+
- `next`는 스펙도 없는 것이다. 스펙이 생기면 `planned`, 화면이 생기면 `mock`, 실데이터가 붙으면 `live`. 각 전이는 병합과 같은 커밋에서 적는다.
|
|
65
|
+
|
|
66
|
+
## 등급과 상태의 관계
|
|
67
|
+
|
|
68
|
+
상태는 사람이 적는 주장이고 등급은 코드가 뒷받침하는 정도다. `live` 장면에만 붙는다.
|
|
69
|
+
|
|
70
|
+
| 등급 | 조건 |
|
|
71
|
+
|---|---|
|
|
72
|
+
| D | 여정 파일의 주장뿐. 화면이 실데이터가 아님 → 경고 |
|
|
73
|
+
| C | 모든 화면이 실데이터(스캔 관측) |
|
|
74
|
+
| B | 그 화면·API·함수를 다루는 검사 파일이 있음 |
|
|
75
|
+
| A | 그 검사가 main 최신 커밋에서 통과(`npm run test:report`) |
|
|
76
|
+
|
|
77
|
+
`live`라고 적었는데 D가 나오면 주장이 틀린 것이다. 상태를 `mock`으로 내리거나 화면을 고친다. 상황판은 둘 중 무엇이 맞는지 판단하지 않고 어긋남만 보여준다.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 프로젝트 상황판의 시맨틱 레이어
|
|
2
|
+
|
|
3
|
+
상황판은 두 종류의 사실을 하나의 그래프로 잇는다. 사람이 뜻을 붙이는 노드(여정·단계·결정)와 코드에서 긁어내는 노드(화면·API·함수·테이블·검사·커밋)다. 손으로 유지하는 것은 여정 파일 하나이고, 나머지는 생성기가 저장소를 스캔해 만든다. 둘이 어긋나면(단계가 가리키는 라우트가 코드에 없음) 화면에 경고로 드러난다.
|
|
4
|
+
|
|
5
|
+
## 노드
|
|
6
|
+
|
|
7
|
+
| 종류 | 출처 | 키 | 뜻 |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| journey | 손(journeys.json) | id | 한 배우가 한 목표를 이루는 흐름. 스토리 맵의 backbone 한 칸 |
|
|
10
|
+
| step | 손 | journey/step | 여정 안의 한 장면. intent(사용자가 원하는 것)·status·capture |
|
|
11
|
+
| screen | 생성(라우터) | route path | 화면 하나. 페이지 파일, 데이터 출처(live/mock/mixed), 호출 API, 검사, 마지막 변경 |
|
|
12
|
+
| api | 생성(BFF) | method+path | 서버 진입점. 호출하는 DB 함수·Auth |
|
|
13
|
+
| function | 생성(migration) | name | DB 함수. 읽고 쓰는 테이블, BFF 사용 여부 |
|
|
14
|
+
| table | 생성(migration) | schema.name | 저장 구조 |
|
|
15
|
+
| test | 생성(tests/) | file | 검사 파일. 다루는 라우트·API |
|
|
16
|
+
| commit | 생성(git) | sha | 최근 변경. 건드린 파일 → 영향받는 화면·여정 |
|
|
17
|
+
| decision | 생성(위키 index) | file | 결정 페이지와 상태(current/proposed/superseded) |
|
|
18
|
+
| plan | 생성(task 문서) | file | PN 체크 진척과 열린 질문 수 |
|
|
19
|
+
| ledger | 생성(tasks/index.md) | 행 | 지금 실행 중·대기 중인 작업 |
|
|
20
|
+
| milestone | 생성(tasks/roadmap.md) | id | 로드맵 항목. 순서·상태·진행 방식·장면·작업·선행·결정 대기·완료 기준 |
|
|
21
|
+
|
|
22
|
+
## 엣지
|
|
23
|
+
|
|
24
|
+
- journey → step (순서)
|
|
25
|
+
- step → screen (shows): `screens: [route]`
|
|
26
|
+
- step → api (uses): 명시(`apis`) 또는 screen을 거쳐 유도
|
|
27
|
+
- screen → api (calls): 페이지와 그 로컬 import 닫힘에서 query hook 스캔
|
|
28
|
+
- api → function (invokes): BFF 핸들러 블록의 `rpc/<name>`
|
|
29
|
+
- function → table (touches): 함수 본문에서 알려진 테이블 이름 스캔
|
|
30
|
+
- test → screen | api (covers): 검사 파일의 `goto('/…')`·`'/api/…'` 문자열
|
|
31
|
+
- commit → screen | api | function (touches): 파일 경로 → 노드(페이지 파일·닫힘·server.mjs·migration)
|
|
32
|
+
- milestone → task (tracks): 로드맵 항목의 `작업`. 장면·선행은 derive에서 해석하고 없으면 check 오류
|
|
33
|
+
- step → decision | plan (refs): `refs: ["DEC-57", "PN-15", "trust-boundary"]` 문자열 매칭
|
|
34
|
+
|
|
35
|
+
## 상태 규칙
|
|
36
|
+
|
|
37
|
+
- screen.source: 페이지(및 로컬 import 닫힘)가 `mock/`을 쓰면 mock, `lib/queries`를 쓰면 live, 둘 다면 mixed.
|
|
38
|
+
- screen.fixedVia: 닫힘이 `fixedPattern`(코드에 고정된 표시값)을 읽는 파일. source는 바꾸지 않고 "하드코딩 표시"로 따로 센다.
|
|
39
|
+
- step.status는 손으로 적되 screens의 source와 대조해 어긋나면 경고(live 단계인데 mock 화면 등).
|
|
40
|
+
- journey.status는 단계에서 유도: 전부 live면 live, 하나라도 live면 partial, 아니면 단계 다수 상태.
|
|
41
|
+
|
|
42
|
+
## 다른 프로젝트에 옮길 때 바꾸는 것
|
|
43
|
+
|
|
44
|
+
스캐너(라우터·서버·migration·검사 형식)만 어댑터로 갈아끼운다. journeys.json 형식·화면·상태 규칙은 그대로 둔다. 어댑터가 없는 종류는 비워도 화면이 깨지지 않는다.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 화면 예산
|
|
2
|
+
|
|
3
|
+
상황판이 복잡해지면 안 보게 된다. 그래서 단순함을 의지가 아니라 검사로 묶는다. 패키지의 `budget/view-budget.spec.mjs`가 개요를 열어 잰다. 프로젝트 루트에서 `npx --no playwright test --config node_modules/@pghoya2956/livemap/budget/playwright.config.mjs`(보통 `npm run map:budget`)로 부른다. `@playwright/test`는 프로젝트 의존성이다(엔진의 선택적 peer).
|
|
4
|
+
|
|
5
|
+
- 설정은 프로젝트 `map/config.json`의 `budget`을 읽는다.
|
|
6
|
+
- 산출물은 프로젝트 `map/.out/overview-1440.png`와 `map/.out/budget-results/`다.
|
|
7
|
+
- 대상은 엔진 `serve`(포트 `MAP_PORT`, 기본 4181)다. `LIVEMAP_BUDGET_STATIC=<export 폴더>`면 `serve --static`을 잰다.
|
|
8
|
+
|
|
9
|
+
## 규칙과 이유
|
|
10
|
+
|
|
11
|
+
| 규칙 | 값(`config.budget`) | 이유 |
|
|
12
|
+
|---|---|---|
|
|
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, 사이드바 배경 적용 | 인라인 의존 회귀 방지 |
|
|
22
|
+
|
|
23
|
+
## 개요 조각
|
|
24
|
+
|
|
25
|
+
개요는 `overview.json`만 읽는다. `derive.mjs`의 `overviewSlice()`가 경로·파일명·sha를 뺀 조각을 만들고, 검사는 개요 본문에서 `/api/`·`.tsx`·`.mjs`·`.sql`·`web/src`·7자 이상 16진수를 찾아 하나라도 있으면 실패한다. 개요에 무언가를 더할 때 이 조각을 거치지 않으면 식별자가 새어 들어온다.
|
|
26
|
+
|
|
27
|
+
## 다시 보게 만드는 것
|
|
28
|
+
|
|
29
|
+
- 마지막 방문 시각을 브라우저 안에 기억해(밖으로 보내지 않는다) 그 뒤 바뀐 커밋·장면에 빨간 점을 찍는다.
|
|
30
|
+
- 상단 띠 한 줄: 동작 장면 수·실데이터 화면 수·14일 커밋·미배포·경고·다음 한 걸음. 들어오자마자 읽을 한 문장.
|
|
31
|
+
- 2주 뒤 실제로 연 뷰만 남긴다. 안 연 패널은 지운다.
|
|
32
|
+
|
|
33
|
+
## 패널을 바꾸는 절차
|
|
34
|
+
|
|
35
|
+
화면 파일은 엔진 소유다. 패널을 바꾸는 일은 엔진 저장소에서 하고 릴리스로 내보낸다.
|
|
36
|
+
|
|
37
|
+
1. 답하려는 질문이 세 질문 중 무엇인지 적는다. 없으면 상세 층으로 간다.
|
|
38
|
+
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`을 사용자에게 보인다.
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pghoya2956/livemap",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Project status board engine: scans a repository into a graph and serves a one-screen map of journeys, screens, APIs, tests and work.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "livemap": "bin/livemap.mjs" },
|
|
8
|
+
"files": ["bin/", "src/", "site/", "budget/", "templates/", "docs/", "CHANGELOG.md"],
|
|
9
|
+
"engines": { "node": ">=22" },
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/*.test.mjs"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": { "@playwright/test": ">=1.63.0 <2" },
|
|
14
|
+
"peerDependenciesMeta": { "@playwright/test": { "optional": true } },
|
|
15
|
+
"devDependencies": { "@playwright/test": "1.63.0" },
|
|
16
|
+
"repository": { "type": "git", "url": "git+https://github.com/pghoya2956/livemap.git" },
|
|
17
|
+
"homepage": "https://github.com/pghoya2956/livemap#readme",
|
|
18
|
+
"publishConfig": { "access": "public" }
|
|
19
|
+
}
|