chore: establish TechLog migration baseline
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# TechLog UI migration baseline
|
||||
|
||||
## Source provenance
|
||||
|
||||
- Source path: `/home/donghyeon/workspace/techlog-studio-frontend`
|
||||
- Inspected: 2026-08-15 (Asia/Seoul)
|
||||
- Git revision/status: unavailable. The supplied source directory is an exported
|
||||
checkout with no `.git` metadata; both `git rev-parse HEAD` and
|
||||
`git status --short` report that it is not a Git repository. The source path
|
||||
and the byte checksums below are the reproducible provenance available for
|
||||
this baseline.
|
||||
|
||||
## Approved copied assets
|
||||
|
||||
| Source and target-relative path | SHA-256 |
|
||||
| --- | --- |
|
||||
| `public/favicon.svg` | `e6d2e59b7b5bbb0342e0fb496dfc262decbfe4426bbb7b047aec8d467d1dc6f7` |
|
||||
| `public/media/fetch-strategy-boundary.svg` | `b07926823ed77fc200f962a1f64a440e130cefa6bed31144b611612af9b02606` |
|
||||
|
||||
Only these two SVGs are approved for this baseline. They must remain exact
|
||||
byte-for-byte copies of the source assets. The evidence key
|
||||
`fetch-strategy-boundary` resolves to `/media/fetch-strategy-boundary.svg`
|
||||
with dimensions `1080 × 420`, trigger label `Fetch Join과 Batch Fetch 비교
|
||||
다이어그램 크게 보기`, and dialog label `Fetch Join과 Batch Fetch의 페이징 경계
|
||||
확대`.
|
||||
|
||||
## Required dependency pins
|
||||
|
||||
| Package | Exact version |
|
||||
| --- | --- |
|
||||
| `pretendard` | `1.3.9` |
|
||||
| `@fontsource/ibm-plex-mono` | `5.3.0` |
|
||||
| `unified` | `11.0.5` |
|
||||
| `remark-parse` | `11.0.0` |
|
||||
| `remark-gfm` | `4.0.1` |
|
||||
| `remark-directive` | `4.0.0` |
|
||||
|
||||
No Next.js, Vinext, or Cloudflare package is part of the migration baseline.
|
||||
|
||||
## Expected route inventory
|
||||
|
||||
| Layout | Route ID | Path |
|
||||
| --- | --- | --- |
|
||||
| PUBLIC | `TECH_LOG_HOME` | `/` |
|
||||
| PUBLIC | `TECH_LOG_EXPLORE` | `/explore` |
|
||||
| PUBLIC | `TECH_LOG_EXPLORE_KIND` | `/explore/:kind` |
|
||||
| PUBLIC | `TECH_LOG_CASE` | `/cases/:slug` |
|
||||
| PUBLIC | `TECH_LOG_REFERENCE` | `/references/:slug` |
|
||||
| PUBLIC | `TECH_LOG_QUESTION` | `/questions/:slug` |
|
||||
| PUBLIC | `TECH_LOG_TOPIC` | `/topics/:slug` |
|
||||
| PUBLIC | `TECH_LOG_PROJECTS` | `/projects` |
|
||||
| PUBLIC | `TECH_LOG_PROJECT` | `/projects/:slug` |
|
||||
| PUBLIC | `TECH_LOG_PROJECT_RECORDS` | `/projects/:slug/records` |
|
||||
| PUBLIC | `TECH_LOG_PROJECT_DECISIONS` | `/projects/:slug/decisions` |
|
||||
| PUBLIC | `TECH_LOG_PROJECT_ACTIVITY` | `/projects/:slug/activity` |
|
||||
| PUBLIC | `TECH_LOG_RELEASES` | `/releases` |
|
||||
| PUBLIC | `TECH_LOG_RELEASE` | `/releases/:version` |
|
||||
| PUBLIC | `TECH_LOG_PROFILE` | `/profile` |
|
||||
| PUBLIC | `TECH_LOG_SEARCH` | `/search` |
|
||||
| STUDIO | `TECH_LOG_STUDIO_HOME` | `/studio` |
|
||||
| STUDIO | `TECH_LOG_STUDIO_DOCUMENTS` | `/studio/documents` |
|
||||
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_NEW` | `/studio/documents/new` |
|
||||
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_EDIT` | `/studio/documents/:id/edit` |
|
||||
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_VALIDATION` | `/studio/documents/:id/validation` |
|
||||
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_PREVIEW` | `/studio/documents/:id/preview` |
|
||||
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_PUBLISH` | `/studio/documents/:id/publish` |
|
||||
| STUDIO | `TECH_LOG_STUDIO_PUBLICATIONS` | `/studio/publications` |
|
||||
| STUDIO | `TECH_LOG_STUDIO_PUBLICATION_PREVIEW` | `/studio/publications/:publicationEventId/preview` |
|
||||
| STUDIO | `TECH_LOG_STUDIO_NOT_FOUND` | `/studio/*` |
|
||||
| PUBLIC | `NOT_FOUND` | `*` |
|
||||
|
||||
## Source stylesheet inventory
|
||||
|
||||
- `app/globals.css`
|
||||
- `app/studio.css`
|
||||
- `app/studio-editor.css`
|
||||
- `components/studio/workflow.module.css`
|
||||
- `components/studio/publication-flow.module.css`
|
||||
|
||||
The inventories in this document are human review baselines. Automated
|
||||
coverage is intentionally limited to the two SVG byte contracts, evidence
|
||||
asset lookup behavior, and the package manager's frozen-lockfile verification.
|
||||
@@ -123,11 +123,17 @@
|
||||
"check:types:service-worker": "tsc --project tsconfig.service-worker.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/ibm-plex-mono": "5.3.0",
|
||||
"@tanstack/react-query": "5.101.4",
|
||||
"lucide-react": "1.25.0",
|
||||
"pretendard": "1.3.9",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8",
|
||||
"react-router-dom": "7.18.1",
|
||||
"remark-directive": "4.0.0",
|
||||
"remark-gfm": "4.0.1",
|
||||
"remark-parse": "11.0.0",
|
||||
"unified": "11.0.5",
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+729
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 19.2727C22 20.779 20.779 22 19.2727 22H14.7273C13.221 22 12 20.779 12 19.2727V12H19.2727C20.779 12 22 13.221 22 14.7273V19.2727Z" fill="#68C4FF"/>
|
||||
<path d="M20 2C21.1046 2 22 2.89543 22 4V7C22 8.10457 21.1046 9 20 9H17C15.8954 9 15 8.10457 15 7V4C15 2.89543 15.8954 2 17 2H20Z" fill="#0C79D8"/>
|
||||
<path d="M7 15C8.10457 15 9 15.8954 9 17V20C9 21.1046 8.10457 22 7 22H4C2.89543 22 2 21.1046 2 20V17C2 15.8954 2.89543 15 4 15H7Z" fill="#0C79D8"/>
|
||||
<path d="M12 12H4.72727C3.22104 12 2 10.779 2 9.27273V4.72727C2 3.22104 3.22104 2 4.72727 2H9.27273C10.779 2 12 3.22104 12 4.72727V12Z" fill="#2E9EFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 712 B |
@@ -0,0 +1,57 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="420" viewBox="0 0 1080 420">
|
||||
<title>Fetch Join과 Batch Fetch의 페이징 경계</title>
|
||||
<desc>Fetch Join은 전체 조인 결과를 읽은 뒤 메모리에서 20개를 고른다. Batch Fetch는 부모 20개를 먼저 고른 뒤 해당 ID의 컬렉션만 조회한다.</desc>
|
||||
<rect width="1080" height="420" rx="18" fill="#FCFCFB"/>
|
||||
<style>
|
||||
.label { font: 600 18px Pretendard, sans-serif; fill: #17181B; }
|
||||
.text { font: 500 15px Pretendard, sans-serif; fill: #3F4249; }
|
||||
.muted { font: 500 13px Pretendard, sans-serif; fill: #686B72; }
|
||||
.box { fill: #F7F7F5; stroke: #D9DBDE; stroke-width: 1.5; }
|
||||
.bad { fill: #FFF4E8; stroke: #D99A4E; stroke-width: 1.5; }
|
||||
.good { fill: #ECF6F2; stroke: #4F927F; stroke-width: 1.5; }
|
||||
.arrow { stroke: #A5A8AE; stroke-width: 2; fill: none; marker-end: url(#arrow); }
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
|
||||
<path d="M0 0 L8 4 L0 8 Z" fill="#A5A8AE"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<text class="label" x="40" y="54">Collection Fetch Join</text>
|
||||
<rect class="box" x="40" y="80" width="190" height="76" rx="10"/>
|
||||
<text class="text" x="66" y="112">목록 + 컬렉션 JOIN</text>
|
||||
<text class="muted" x="66" y="136">부모 × 자식 행</text>
|
||||
<path class="arrow" d="M230 118 H286"/>
|
||||
<rect class="bad" x="286" y="80" width="230" height="76" rx="10"/>
|
||||
<text class="text" x="312" y="112">전체 Join 결과 로드</text>
|
||||
<text class="muted" x="312" y="136">DB LIMIT 없음 · 1,961행</text>
|
||||
<path class="arrow" d="M516 118 H572"/>
|
||||
<rect class="box" x="572" y="80" width="220" height="76" rx="10"/>
|
||||
<text class="text" x="598" y="112">부모 엔티티 복원</text>
|
||||
<text class="muted" x="598" y="136">중복 부모 정리</text>
|
||||
<path class="arrow" d="M792 118 H848"/>
|
||||
<rect class="bad" x="848" y="80" width="192" height="76" rx="10"/>
|
||||
<text class="text" x="874" y="112">메모리에서 20개</text>
|
||||
<text class="muted" x="874" y="136">페이지 경계가 뒤에 있음</text>
|
||||
|
||||
<line x1="40" y1="210" x2="1040" y2="210" stroke="#E2E3E1"/>
|
||||
|
||||
<text class="label" x="40" y="258">Parent Paging + Batch Fetch</text>
|
||||
<rect class="good" x="40" y="284" width="230" height="76" rx="10"/>
|
||||
<text class="text" x="66" y="316">부모 목록 LIMIT 20</text>
|
||||
<text class="muted" x="66" y="340">정렬과 페이지 경계 확정</text>
|
||||
<path class="arrow" d="M270 322 H342"/>
|
||||
<rect class="box" x="342" y="284" width="210" height="76" rx="10"/>
|
||||
<text class="text" x="368" y="316">부모 ID 20개</text>
|
||||
<text class="muted" x="368" y="340">현재 페이지 집합</text>
|
||||
<path class="arrow" d="M552 322 H624"/>
|
||||
<rect class="good" x="624" y="284" width="220" height="76" rx="10"/>
|
||||
<text class="text" x="650" y="316">컬렉션 IN 조회</text>
|
||||
<text class="muted" x="650" y="340">현재 부모만 로드</text>
|
||||
<path class="arrow" d="M844 322 H900"/>
|
||||
<rect class="box" x="900" y="284" width="140" height="76" rx="10"/>
|
||||
<text class="text" x="926" y="316">화면 조립</text>
|
||||
<text class="muted" x="926" y="340">비용 상한 설명 가능</text>
|
||||
|
||||
<text class="muted" x="40" y="396">관찰: 페이징이 적용되는 지점이 부모 조회 앞으로 이동한다.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,71 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const publicDirectory = resolve(process.cwd(), "public");
|
||||
|
||||
const approvedSvgAssets = [
|
||||
{
|
||||
relativePath: "favicon.svg",
|
||||
sha256: "e6d2e59b7b5bbb0342e0fb496dfc262decbfe4426bbb7b047aec8d467d1dc6f7",
|
||||
},
|
||||
{
|
||||
relativePath: "media/fetch-strategy-boundary.svg",
|
||||
sha256: "b07926823ed77fc200f962a1f64a440e130cefa6bed31144b611612af9b02606",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const localEvidenceAssets = {
|
||||
"fetch-strategy-boundary": {
|
||||
src: "/media/fetch-strategy-boundary.svg",
|
||||
width: 1080,
|
||||
height: 420,
|
||||
triggerLabel: "Fetch Join과 Batch Fetch 비교 다이어그램 크게 보기",
|
||||
dialogLabel: "Fetch Join과 Batch Fetch의 페이징 경계 확대",
|
||||
},
|
||||
} as const;
|
||||
|
||||
function getLocalEvidenceAsset(key: string) {
|
||||
const asset = localEvidenceAssets[key as keyof typeof localEvidenceAssets];
|
||||
if (!asset) {
|
||||
throw new Error(`Unknown local evidence asset: ${key}`);
|
||||
}
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
function sha256(filePath: string) {
|
||||
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
||||
}
|
||||
|
||||
describe("TechLog migration baseline", () => {
|
||||
it("preserves each approved SVG asset byte-for-byte", () => {
|
||||
for (const asset of approvedSvgAssets) {
|
||||
const assetPath = resolve(publicDirectory, asset.relativePath);
|
||||
|
||||
expect(existsSync(assetPath), asset.relativePath).toBe(true);
|
||||
expect(sha256(assetPath), asset.relativePath).toBe(asset.sha256);
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves the fetch strategy evidence to its published asset contract", () => {
|
||||
expect(getLocalEvidenceAsset("fetch-strategy-boundary")).toEqual({
|
||||
src: "/media/fetch-strategy-boundary.svg",
|
||||
width: 1080,
|
||||
height: 420,
|
||||
triggerLabel: "Fetch Join과 Batch Fetch 비교 다이어그램 크게 보기",
|
||||
dialogLabel: "Fetch Join과 Batch Fetch의 페이징 경계 확대",
|
||||
});
|
||||
|
||||
const evidencePath = resolve(
|
||||
publicDirectory,
|
||||
getLocalEvidenceAsset("fetch-strategy-boundary").src.slice(1),
|
||||
);
|
||||
expect(existsSync(evidencePath)).toBe(true);
|
||||
expect(sha256(evidencePath)).toBe(
|
||||
"b07926823ed77fc200f962a1f64a440e130cefa6bed31144b611612af9b02606",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user