Files
clean-architecture-frontend…/docs/superpowers/specs/2026-09-20-memento-frontend-template-bootstrap-design.md
T
DongHyeonkaandClaude Opus 5 8dd7cbaea3 docs: design deriving memento-frontend from this template
Records the approved approach for standing up memento-frontend: merge
this template's history into it and keep the template as an `upstream`
remote so later improvements can be merged in.

Two decisions drive the rest of the design. The derived project tracks
`upstream/main`, so this repository's `main` has to be fast-forwarded to
`develop` first. And the derived project keeps the reference feature and
the example routes, so upstream merges stay conflict-free.

The design also covers one change to this repository that is not about
memento: the supply-chain builder and verifier IDs currently hardcode the
repository name in four files, which would make all four permanent
conflict points for every derived project. Deriving them from the
`name` in `package.json` reduces that surface to one line. This
repository's own name does not change, so the emitted ID strings stay
byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:44:40 +09:00

266 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# memento-frontend를 이 템플릿에서 파생시키기 — 설계서
- 작성일: 2026-09-20
- 대상 레포 (둘)
- 템플릿: `/home/donghyeon/workspace/desktop-server-git/clean-architecture-frontend-template` (`develop`, 기준 커밋 `ec7f20e`)
- 파생: `/home/donghyeon/workspace/desktop-server-git/memento-frontend` (`main`, 기준 커밋 `b35e17c`)
- 원격: 둘 다 같은 Gitea (`https://git.learn.hyeonworks.com/donghyeon.kang/...`)
- 이 문서는 설계만 한다. 소스 수정·빌드·테스트 실행 없음.
---
## 0. 요약 (먼저 읽을 것)
| 항목 | 판정 |
|---|---|
| 반영 방식 | **upstream 추적 부트스트랩**. 템플릿 히스토리를 memento에 머지하고 `upstream` 원격으로 남겨 계속 받아온다 |
| 추적 브랜치 | `upstream/main`. 그러려면 템플릿의 `main`을 먼저 `develop` 지점까지 올려야 한다 |
| `develop``main` | **fast-forward**. `main``Initial commit` 1개뿐이고 `develop`의 조상이라 머지 커밋이 안 생긴다 |
| memento 초기 상태 | 템플릿 그대로 유지 + 식별자만 리브랜딩. `reference-feature``/examples/*`**남긴다** |
| 템플릿 선수정 | builder/verifier ID를 `package.json``name`에서 파생. 템플릿 자신의 산출물 문자열은 **불변** |
| 템플릿에 memento 흔적 | **없다.** 템플릿에 "memento"라는 문자열은 들어가지 않는다 |
| 최대 위험 | `pnpm install` 후 게이트가 memento에서도 통과하는지. 특히 `verify:release` 계열이 새 `name`으로 재계산된 ID를 일관되게 쓰는지 |
실행 순서는 되돌리기 어려운 것이 뒤에 오도록 잡았다: 템플릿 수정 → 게이트 → push → memento 머지 → 리브랜딩 → 게이트.
---
## 1. 확정된 결정 (사용자 승인 완료)
1. **반영 = upstream 추적 부트스트랩.** 1회성 스냅샷 복사안과 "동기화 스크립트만 만들기"안은 기각.
근거: 템플릿이 계속 개선되는 레포이므로, 파생 시점에 끊기면 개선분을 수동 diff로 옮겨야 한다.
2. **추적 대상은 `upstream/main`.** (`develop`이 아니다.)
따라서 템플릿의 `main``develop` 지점까지 올리는 것이 선행 작업이다.
3. **memento 초기 상태는 "템플릿 그대로 + 리브랜딩만".**
`reference-feature``/examples/*`를 남긴다. 근거: (a) 살아있는 참조 구현이 남아 팀이 패턴을 보고 따라갈 수 있다, (b) 삭제하면 `INVENTORY.md`·테스트 레지스트리·e2e·a11y까지 연쇄로 손봐야 한다, (c) 삭제한 파일은 upstream 머지마다 충돌한다.
4. **builder/verifier ID는 템플릿 쪽에서 `package.json`의 `name` 파생으로 바꾼다.**
memento에서 문자열만 교체하는 안은 기각. 근거: 그 경우 4개 파일이 영구 충돌 후보가 된다.
5. **앱 표시 이름은 `Memento`.** ko/en 카탈로그 모두 동일 표기.
6. **템플릿 `main`의 origin push 승인됨.**
---
## 2. Part A — 템플릿 선수정: 프로젝트 이름 파생
### A.1 문제
레포 이름이 4개 파일에 하드코딩되어 있다.
| 파일 | 줄 | 값 |
|---|---|---|
| `scripts/lib/provider-evidence.ts` | 1213 | `PROMOTION_VERIFIER_ID = "clean-architecture-frontend-template/promotion-verifier"` |
| `scripts/lib/local-release-evidence.ts` | 306307 | `LOCAL_EVIDENCE_VERIFIER_ID = "clean-architecture-frontend-template/local-evidence-verifier"` |
| `scripts/generate-supply-chain.ts` | 273 | `builder: { id: "local:clean-architecture-frontend-template" }` |
| `tests/unit/security-followup-fixture.ts` | 38, 183, 499 | 위 두 ID의 리터럴 복사본 |
파생 프로젝트는 이 4개 파일을 고쳐야 하고, 그 순간 4개 파일 모두가 upstream 머지의 영구 충돌 지점이 된다.
### A.2 해법: `scripts/lib/supply-chain.ts`에 `PROJECT_NAME`을 둔다
새 파일을 만들지 않는 것이 핵심이다. `scripts/lib/supply-chain.ts`
- 세 소비자가 **이미 전부 임포트**하고 있고
(`provider-evidence.ts:5-8`, `local-release-evidence.ts:70`, `generate-supply-chain.ts:26`)
- `LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS`(`scripts/lib/release-candidate.ts:38-58`)에 **이미 등재**되어 있다.
새 파일을 만들면 이렇게 된다:
- `tests/unit/security-followup.test.ts:79-92``local-release-evidence.ts`의 모든 상대 `.ts` import를
뽑아 `LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS`의 부분집합인지 검사한다. 새 파일을 임포트하면 **이 테스트가 깨진다.**
- 목록을 늘리면 `release-candidate.ts:71`을 통해 릴리스 증적의 대상 파일 집합이 함께 바뀐다.
`supply-chain.ts`는 이미 임포트되어 있고 이미 목록에 있으므로 둘 다 발생하지 않는다.
### A.3 변경 내용
`scripts/lib/supply-chain.ts`에 추가:
- 리포지토리 루트의 `package.json`에서 `name`을 읽어 `PROJECT_NAME`으로 노출한다.
- 읽기는 모듈 로드 시점 1회. `node:fs``readFileSync`를 쓴다
(선례: `scripts/check-adapter-inventory.ts`). 기존 `export const` 소비자들이
동기 상수를 기대하므로 비동기 `readFile`로는 시그니처가 바뀐다.
- 루트 해석은 `new URL("../../package.json", import.meta.url)` 로 고정한다. 레포의
기존 관용구가 이것이다(`scripts/run-vitest.ts:26`, `scripts/check-architecture.ts:74`).
`import.meta.dirname`은 이 레포에서 쓰인 적이 없으므로 도입하지 않는다.
`process.cwd()`에 의존하면 다른 디렉터리에서 스크립트를 호출했을 때 값이 달라진다.
- `name`이 없거나 빈 문자열이면 **던진다**. 조용한 폴백은 증적(provenance)에
잘못된 identity를 박아 넣는다.
파생 상수:
| 상수 | 정의 | 템플릿에서의 값 |
|---|---|---|
| `PROMOTION_VERIFIER_ID` | `` `${PROJECT_NAME}/promotion-verifier` `` | `clean-architecture-frontend-template/promotion-verifier` |
| `LOCAL_EVIDENCE_VERIFIER_ID` | `` `${PROJECT_NAME}/local-evidence-verifier` `` | `clean-architecture-frontend-template/local-evidence-verifier` |
| supply-chain builder id | `` `local:${PROJECT_NAME}` `` | `local:clean-architecture-frontend-template` |
**템플릿의 `package.json` name은 안 바꾼다.** 따라서 세 값 모두 현재와 문자 단위로 동일하다.
이 변경은 템플릿의 산출물을 바꾸지 않는다. 바뀌는 것은 값의 출처뿐이다.
### A.4 테스트 픽스처
`tests/unit/security-followup-fixture.ts`의 리터럴 3곳(38, 183, 499)을 위 상수 import로 교체한다.
**조사 결과 부정 케이스는 없다.** 세 곳 모두 `status: "PASS"` 픽스처이고, 의도적으로 틀린
verifier ID를 넣어 거부를 확인하는 테스트는 `tests/` 어디에도 없다. 따라서 세 곳 전부
상수로 바꿔도 테스트가 무력화되지 않는다.
참고로 불일치를 거부하는 코드는 `scripts/lib/exact-promotion-bundle.ts:93`과
`scripts/lib/local-release-evidence.ts:785`다. 나중에 부정 케이스를 추가한다면
그쪽은 의도적으로 **틀린 리터럴**을 써야 한다. 상수를 쓰면 자기 자신과 비교하게 된다.
### A.5 Part A 완료 조건
```bash
corepack pnpm lint
corepack pnpm check:types
corepack pnpm test:all
corepack pnpm build
corepack pnpm verify:release
```
추가로, 이 변경이 산출물을 안 바꿨다는 것을 확인한다: 변경 전후로
`artifacts/release/provenance.json`의 `predicate.runDetails.builder.id`가 동일해야 한다.
---
## 3. Part B — 템플릿 `develop` → `main`
현재 상태:
```
main 91ae42a Initial commit (커밋 1개)
develop ec7f20e refactor: 프론트엔드 리펙토링 (커밋 234개)
```
`main`은 `develop`의 조상이다(`git merge-base --is-ancestor main develop` 통과). 따라서:
```bash
git checkout main
git merge --ff-only develop
git push origin main
```
`--ff-only`를 쓰는 이유: `main`이 고유 커밋을 하나도 갖고 있지 않아 `--no-ff` 머지 커밋은
아무 정보도 담지 못한다. `--ff-only`는 "조상이 아니면 실패"라는 안전장치 역할도 한다.
Part A의 커밋이 `develop`에 먼저 올라가야 하므로 **Part A → Part B 순서를 지킨다.**
(Part A가 `develop`에 커밋되어도 `main`은 여전히 조상이므로 ff는 유지된다.)
---
## 4. Part C — memento-frontend 부트스트랩
현재 상태: `main`에 커밋 2개 (`bce7c4c Initial commit`, `b35e17c README.md 업데이트`), 파일은 `README.md` 하나.
```bash
cd /home/donghyeon/workspace/desktop-server-git/memento-frontend
git remote add upstream https://git.learn.hyeonworks.com/donghyeon.kang/clean-architecture-frontend-template
git fetch upstream
git merge upstream/main --allow-unrelated-histories
```
- `--allow-unrelated-histories`가 필요한 이유: 두 레포는 공통 조상이 없다.
- 예상 충돌: **`README.md` 1건**(양쪽에 존재하는 유일한 경로). 템플릿 쪽을 채택한 뒤 Part D에서 교체한다.
- 결과: memento의 커밋 2개가 first-parent 선상에 보존되고, 템플릿 234개가 머지로 들어온다.
이후 동기화:
```bash
git fetch upstream && git merge upstream/main
```
---
## 5. Part D — 리브랜딩
### D.1 바꾸는 곳
| 파일 | 위치 | 현재 | 변경 후 |
|---|---|---|---|
| `package.json` | `name` | `clean-architecture-frontend-template` | `memento-frontend` |
| `index.html` | 7 | `Clean Architecture Frontend` | `Memento` |
| `src/presentation/i18n/catalog.ts` | 6, 165 (`common.appName`) | `Frontend Skeleton` | `Memento` |
| `src/presentation/i18n/catalog.ts` | 44, 203 (`route.APP_HOME.title`) | `Clean Architecture Frontend` | `Memento` |
| `src/presentation/pages/home-page.tsx` | 43 | `Clean Architecture Frontend` | `Memento` |
| `src/contracts/storage-keys.ts` | 1 (`APP_NAMESPACE`) | `ca-frontend` | `memento` |
| `README.md` | 전체 | 템플릿 설명 | memento 설명 + upstream 동기화 절차 |
`package.json`의 `name` 변경은 Part A 덕분에 자동으로 verifier/builder ID까지 memento 값으로 전파된다.
`APP_NAMESPACE` 변경 시 주의: 이 값은 `storage-keys.ts:140`에서 `${APP_NAMESPACE}:${scope}:v${schemaVersion}:${name}` 형태로 브라우저 저장소 키를 만든다. 템플릿이 이미 쓰던 키가 남아 있는 브라우저에서는 기존 데이터가 고아가 된다. 신규 프로젝트라 실제 영향은 없지만, 키 형식을 검사하는 테스트가 있으면 같이 갱신해야 한다.
### D.2 바꾸지 않는 곳 (의도적)
- **`docs/superpowers/specs/*`, `docs/reviews/*`, `docs/architecture/*` 안의 템플릿 이름**
이들은 템플릿에서 실제로 일어난 작업의 기록이다. 이름을 바꾸면 기록이 거짓이 되고,
upstream 머지 충돌만 늘어난다. (해당 파일 7개)
- **`config/runtime/*.json`의 `API_BASE_URL`**
`memento-backend`가 아직 빈 레포라 넣을 값이 없다. `http://localhost:8080/` 유지.
- **`public/config.json`의 `FEATURE_OVERRIDES["reference-feature"]`**
결정 3에 따라 `reference-feature`를 남기므로 그대로 둔다.
### D.3 memento `README.md`
최소한 다음을 담는다. memento의 도메인이 아직 정해지지 않았으므로 제품 설명은 쓰지 않는다.
1. 이 레포가 `clean-architecture-frontend-template`에서 파생되었다는 사실
2. upstream 동기화 절차 (`git fetch upstream && git merge upstream/main`)
3. 로컬 실행 절차 (템플릿 README에서 승계)
4. 리브랜딩된 지점 목록 — 다음 사람이 upstream 충돌을 만났을 때 볼 곳
---
## 6. 검증
memento-frontend에서, Part D 완료 후:
```bash
corepack pnpm install --frozen-lockfile
corepack pnpm lint
corepack pnpm check:types
corepack pnpm check:architecture
corepack pnpm check:adapter-inventory
corepack pnpm test:all
corepack pnpm build
corepack pnpm verify:release
```
`verify:release`를 포함하는 이유: Part A가 손댄 경로가 정확히 이 게이트의 대상이고,
`package.json`의 `name`이 바뀐 상태에서 ID 파생이 일관되게 동작하는지는 여기서만 드러난다.
`.nvmrc`의 Node 24.14.0과 Corepack이 필요하다.
---
## 7. 비범위 (이번에 하지 않는 것)
- memento의 도메인 기능 설계·구현. 이번 작업의 산출물은 "빌드되고 게이트를 통과하는 빈 스캐폴드"까지다.
- `memento-backend` 연동. 해당 레포도 아직 비어 있다.
- `reference-feature` 제거. 결정 3에 따라 남긴다. 나중에 제거할 때는 별도 작업으로 다룬다.
- `.gitea/workflows/quality-gates.yml`의 CI 활성화 검증. 파일은 그대로 넘어가지만 Gitea Actions 러너 설정은 이 작업 범위 밖이다.
- 템플릿의 `develop` 브랜치 정리나 `refactor/platform-consumer-migration` 브랜치 처리.
---
## 8. 리스크
| 리스크 | 영향 | 완화 |
|---|---|---|
| Part A에서 `readFileSync` 경로가 호출 컨텍스트에 따라 달라짐 | 증적에 잘못된 identity | `new URL(..., import.meta.url)` 기준 고정, `process.cwd()` 사용 금지 |
| Part A가 `local-release-evidence.ts`에 새 import를 추가함 | `security-followup.test.ts:79-92` 실패 | 이미 임포트 중인 `supply-chain.ts`만 쓴다. 새 파일을 만들지 않는다 |
| memento에서 `pnpm install` 후 게이트 실패 | 부트스트랩 미완 | Part B push 전에 템플릿에서 전 게이트를 먼저 통과시킨다 |
| `--allow-unrelated-histories` 머지에서 예상 밖 충돌 | 수작업 증가 | 충돌 파일이 `README.md` 외에 나오면 중단하고 보고 |
| `main` ff 머지 실패 | Part B 중단 | `--ff-only`가 알아서 실패시킨다. 그 경우 `main`에 예상 못한 커밋이 생긴 것이므로 재조사 |
---
## 9. 실행 순서 (되돌리기 어려운 것이 뒤)
1. Part A — 템플릿 `develop`에서 ID 파생으로 변경, 게이트 통과, 커밋
2. Part B — `develop` → `main` ff 머지, `origin/main` push ← 첫 외부 반영
3. Part C — memento에 `upstream` 추가, `upstream/main` 머지, `README.md` 충돌 해소
4. Part D — 리브랜딩
5. 검증 — memento에서 전 게이트 실행
6. memento 커밋 (push 여부는 별도 확인)