From 4cd7444ce7997cc06bd0c9a91ad4c3060a5c20f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:36:06 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=EB=B0=94?= =?UTF-8?q?=ED=80=B4=20105?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A4ghKpw3GNw4TCz4sKdufm From af025dc6c9c2771c772375d483cd8e76f32d470d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 11:30:08 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=EB=B0=94=ED=80=B4=20105=20=E2=80=94=20?= =?UTF-8?q?=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=EB=A1=9C=20=E3=80=8C?= =?UTF-8?q?=ED=9B=84=EB=B3=B4=E3=80=8D=20=ED=91=9C=EB=A5=BC=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=B1=84=EC=9A=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지시 10: 빌드한 커뮤니티를 재서 바퀴 106~132 가 고를 35줄을 둔다. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A4ghKpw3GNw4TCz4sKdufm --- incoming/ROUND.md | 60 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/incoming/ROUND.md b/incoming/ROUND.md index 1bfff92bc..73f022398 100644 --- a/incoming/ROUND.md +++ b/incoming/ROUND.md @@ -151,31 +151,51 @@ await browser.close(); ## 후보 -10월 25일 포크 `dev`(`902f5ea`)를 재서 적었어요. 바퀴 91 부터 102 까지 이 표에서 골라요. 그 가운데 두 바퀴는 위 「창작자 지시」 6 과 7 이 먼저 써요. 창작자가 10월 25일 「정리부분을 10사이클정도 더 돌려볼래? 패턴랩 정리.」 라고 해서 정리 줄만 두었고, 1~7위가 Pattern Lab 이에요. 아무도 읽지 않는 코드와 CSS, 두 번 적힌 도우미, 타입 구멍, 지금 Lab 을 보이지 않는 문서와 사진을 재서 적었어요. 화면을 바꾸는 줄은 없어요. 6위만 문서와 가이드 속 사진을 바꿔요. 끝 칸은 그 바퀴가 무엇으로 확인하는지예요. +10월 26일 포크 `dev`(`39944bd`)를 재서 적었어요. 바퀴 106 부터 132 까지 이 표에서 골라요. 창작자 지시 10 대로 범위는 커뮤니티(`app/community/`, `components/community/`, `lib/community/`, `app/api/community/`)예요. 쓰는 사람이 바로 느끼는 줄을 위에 두고 근간 줄을 사이사이에 섞었어요. 1위와 2위는 근간이지만 맨 위에 두었어요. 한 회원이 다른 사람 이름으로 보이고, 남의 포크에 코드 한 줄이 따라가는 일이라서예요. 끝 칸은 그 바퀴가 무엇으로 확인하는지예요. -knip 5 와 jscpd 4 는 스크래치에 깔아 돌렸고 `package.json` 은 그대로예요. 지난 표에서 남은 UI 줄 열하나(26~36위)는 정리가 아니라서 뺐어요. 그 줄은 `902f5ea` 의 이 파일에 그대로 있어요. 남은 정리 줄 넷은 오늘 다시 재서 8, 13~15위에 두었어요. 고칠 곳은 `web/src/` 아래면 그 앞을 줄였어요. +사이트는 Pi 와 같은 `NEXT_PUBLIC_COMMUNITY_URL` 과 `NEXT_PUBLIC_BUILD_ENABLED` 로 빌드했어요. 그 빌드를 두 번 띄웠어요. 하나는 스크래치 DB 에 가입 일곱과 공개 패턴 스물여섯, 덱 넷을 넣은 서버이고, 하나는 빈 DB 서버예요. playwright-core 1.56.1, axe-core 4.10.3, knip 5, jscpd 4 는 스크래치에 깔았고 `package.json` 은 그대로예요. 지난 표에서 남은 정리 줄 다섯(11~15위)은 지시 10 대로 뺐어요. 그 줄은 `39944bd` 의 이 파일에 그대로 있어요. 그 가운데 커뮤니티 몫(11위의 `BuildStatus`, 12위의 `CommunityFeedClient.tsx:5`)은 다시 재서 34위에 넣었어요. `902f5ea` 표에서 하지 않고 빠진 커뮤니티 UI 줄 셋(29, 30, 36위)도 오늘 다시 쟀어요. 셋 다 아직 맞아서 5위, 23위, 25위에 두었어요. 로드맵의 커뮤니티 노드 가운데 남은 것은 `biz-market` 하나예요. 그 노드가 바탕으로 삼는 라이선스 머리를 바로잡는 줄이 2위예요. 고칠 곳은 `web/src/` 아래면 그 앞을 줄였어요. | 순위 | 갈래 | 무엇 | 까닭 | 고칠 곳 | 확인 | |---|---|---|---|---|---| -| 1 | 근간 | Pattern Lab 코드에서 아무도 읽지 않는 이름 둘이 빠지고, 다른 파일이 읽지 않는 export 57개가 파일 안으로 들어간다 | knip 5 를 스크래치에 깔아 `web/` 에서 돌리니, `lib/lab/` 와 `app/pattern-lab/` 의 32개 파일에 다른 파일이 하나도 읽지 않는 export 가 59개(값 29개, 타입 30개) 있었어요. 셈에는 시험 파일, `web/scripts`, `tools/virtual-panel`, `docs` 가 그 이름을 부르는지까지 넣었어요. 많은 곳은 `lib/lab/capture/probe.ts`(8개)와 `lib/lab/textFonts.ts`(5개)예요. 스크래치 worktree 에서 그 `export` 를 다 떼고 `tsc --noEmit` 을 돌리니(`noUnusedLocals` 는 이미 켜져 있어요) 오류가 둘만 났어요. `lib/lab/capture/shaderSource.ts:71` 의 `SHADER_PREAMBLE_LINES` 와 `app/pattern-lab/panels/director/useTimelineZoom.ts:79` 의 `TimelineZoom` 은 정의 말고 부르는 곳이 없어요. 이 둘은 지우고 나머지 57개는 `export` 만 떼요. | `lib/lab/` 28개 파일, `app/pattern-lab/panels/` 4개 파일 | knip 을 다시 돌려 Lab 경로가 0개, `npm run typecheck`, `npm run check:panels` | -| 2 | 근간 | Pattern Lab 에 두세 번 적힌 도우미 다섯이 한 곳에서 온다 | jscpd 4 와 같은 이름 함수 찾기로 Lab 을 보니 몸이 같은 함수가 이래요. FNV-1a 해시가 셋(`lib/lab/capture/probe.ts:358`, `lib/lab/capture/shaderStore.ts:29`, `lib/lab/hardwarePastes.ts:24` 의 `promptKey` 안에 풀어 씀)이고, 파일 내려받기가 셋(`lib/lab/capture/download.ts:30` 의 `downloadBlob`, `app/pattern-lab/panels/director/showFiles.ts:12` 의 같은 이름 함수, `app/pattern-lab/HardwareModal.tsx:199` 의 `downloadH`)이에요. `commentSafe`(`lib/lab/flatten.ts:92`, `lib/lab/pixelToCode.ts:126`), `roundRangeValue`(`app/pattern-lab/panels/KnobsPanel.tsx:42`, `lib/lab/cppPrompt.ts:17`), `sortedLane`(`app/pattern-lab/panels/director/useShowEditing.ts:32`, `lib/lab/director/bake.ts:72`)은 둘씩이에요. 내려받기 셋 가운데 `capture/download.ts` 만 링크를 문서에 붙였다 떼요. 나머지 둘은 붙이지 않고 `click()` 해요. 하나로 모으면 Graphic Export 와 Director 의 `.pfs` 와 `.mid`, To hardware 의 `.h` 가 같은 길로 내려받아요. | `lib/lab/capture/download.ts`, `lib/lab/capture/probe.ts`, `lib/lab/capture/shaderStore.ts`, `lib/lab/hardwarePastes.ts`, `lib/lab/flatten.ts`, `lib/lab/pixelToCode.ts`, `lib/lab/cppPrompt.ts`, `lib/lab/director/bake.ts`, `app/pattern-lab/HardwareModal.tsx`, `app/pattern-lab/panels/KnobsPanel.tsx`, `app/pattern-lab/panels/director/showFiles.ts`, `app/pattern-lab/panels/director/useShowEditing.ts` | `npm run typecheck`, `npm run check:panels`(`hardwarePastes` 와 `shaderStore` 의 키가 바뀌지 않는지 시험 하나), 같은 이름으로 `git grep` 해 정의가 하나씩 | -| 3 | 근간 | Layers 패널의 규칙 24개가 dock 틀 CSS 에서 Layers 패널의 CSS 로 간다 | `web/ARCHITECTURE.md:78` 은 패널마다 제 CSS(`panels/.module.css`)를 갖고 `LabPanels.module.css` 는 dock 틀만 둔다고 해요. 그런데 `app/pattern-lab/LabPanels.module.css` 의 클래스 41개를 읽는 파일을 세니, `:314`~`:501` 의 규칙 24개(클래스 15개, `layerList`, `layerRow`, `layerEye` 들)는 `app/pattern-lab/panels/LayersPanel.tsx` 하나만 읽어요. `LayersPanel.module.css` 는 아직 없어요. `:477` 의 `@media (hover: none)` 은 Layers 규칙 뒤에 Pixel 규칙(5위)을 같이 안고 있어서 그 덩어리는 둘로 나눠요. 파일 머리 주석(`:1`~`:3`)도 「knob/ramp/gallery/modal styles are shared from PatternLab.module.css」 라고 해서, 10월 23일에 걷은 Gallery 를 말해요. | `app/pattern-lab/LabPanels.module.css`, 새 `app/pattern-lab/panels/LayersPanel.module.css`, `app/pattern-lab/panels/LayersPanel.tsx`, `app/pattern-lab/labCss.test.ts` | 바퀴 89(#223) 처럼 빌드한 `/pattern-lab` 을 390px 와 1440px 에서 열어 옮기기 앞뒤의 `getComputedStyle` 이 0 차이, `npm run check:panels` | -| 4 | 근간 | Lab 머리줄 규칙 24개가 여럿이 쓰는 `PatternLab.module.css` 에서 셸의 CSS 로 간다 | `web/ARCHITECTURE.md:78` 은 `PatternLab.module.css` 가 두 파일 넘게 쓰는 것만 둔다고 해요. 그 파일의 규칙을 읽는 파일로 나누니, 24개(153줄, `:1`~`:89`, `:129`~`:168`, `:177`~`:193`, `:367`~`:393`)는 `app/pattern-lab/PatternLabClient.tsx` 하나만 읽어요. `labBrand`, `projectIdentity`, `saveStatus`, `storageNotice` 같은 머리줄 클래스 11개예요. `darkButton`(`:210`~`:214`)은 `HardwareModal.tsx` 하나만 읽는데 그 창은 이미 `HardwareModal.module.css` 가 있어요. 옮기고 나면 `labCss.test.ts` 에 「`PatternLab.module.css` 의 클래스는 두 파일 넘게 읽는다」를 더해 다시 모이지 않게 해요. | `app/pattern-lab/PatternLab.module.css`, `app/pattern-lab/LabPanels.module.css`, `app/pattern-lab/HardwareModal.module.css`, `app/pattern-lab/PatternLabClient.tsx`, `app/pattern-lab/HardwareModal.tsx`, `app/pattern-lab/labCss.test.ts` | 3위와 같은 `getComputedStyle` 비교, `npm run check:panels` | -| 5 | 근간 | Preview, Pixel, Color Ramp, Knobs 패널만 쓰는 규칙 18개가 dock 틀 CSS 에서 그 패널의 CSS 로 간다 | 3위와 같은 셈이에요. `app/pattern-lab/LabPanels.module.css` 에서 `PreviewPanel.tsx` 만 읽는 규칙이 `:266`~`:312`, `PixelPanel.tsx` 만 읽는 규칙이 `:503`~`:553`, `pixel/PixelToolbars.tsx` 만 읽는 규칙이 `:564`~`:591`, `RampPanel.tsx` 만 읽는 규칙이 `:593`~`:652`, `KnobsPanel.tsx` 만 읽는 규칙이 `:624`~`:627` 이에요(규칙 18개, 142줄). Preview, Ramp, Knobs 는 제 CSS 가 이미 있고 Pixel 은 없어요. 3위와 이 줄이 끝나면 `LabPanels.module.css` 에는 dock 틀(`:1`~`:146`)과 패널 여럿이 쓰는 `panel`, `panelBar`, `panelHint`, `panelBody`, `colorWell` 만 남아요. | `app/pattern-lab/LabPanels.module.css`, `app/pattern-lab/panels/PreviewPanel.module.css`, `app/pattern-lab/panels/RampPanel.module.css`, `app/pattern-lab/panels/KnobsPanel.module.css`, 새 `app/pattern-lab/panels/PixelPanel.module.css`, 그 CSS 를 읽는 패널 다섯 | 3위와 같은 `getComputedStyle` 비교, `npm run check:panels` | -| 6 | 근간 | Make 가이드와 Pattern Guide 의 Lab 사진이 걷어 낸 Gallery 탭 대신 지금 Lab 을 보인다 | 세 장이 10월 23일에 걷은 Gallery 탭을 보여요. Make 가이드의 Lab 창(`public/guide/lab-window/lab.webp`, `components/guide/LabWindow.tsx:17` 와 `components/guide/desk/DeskStage.tsx:35` 가 읽어요)은 Code 줄에 「Gallery」 탭이 있고 Layers 에 「+ Text」 가 없어요. `PATTERN_GUIDE.md:215` 의 `docs/images/pattern-guide/11-pattern-lab.png` 와 `:264` 의 `13-color-ramp.png` 도 「GALLERY」 탭이 있고 Text 탭이 없어요. `lab.webp` 는 Lab 을 1024×680 CSS px 로 연 2× 캡처예요(`LabWindow.tsx:19`~`:20`). 새로 찍는 크기를 같게 두면 그 카드의 틀은 그대로예요. | `web/public/guide/lab-window/lab.webp`, `docs/images/pattern-guide/11-pattern-lab.png`, `docs/images/pattern-guide/13-color-ramp.png` | 빌드한 `/pattern-lab` 을 Playwright 로 찍고 세 장을 PR 에 앞뒤로, `/guide/make` 를 390px 와 1440px 로 찍어 카드가 같은 틀인지 | -| 7 | 근간 | Pattern Lab 의 `!` 단언 가운데 타입으로 좁힐 수 있는 일곱이 빠진다 | 시험 아닌 Lab 코드에 `!` 단언이 17곳이에요(TypeScript AST 의 `NonNullExpression` 으로 셌어요). 일곱은 타입 쪽 구멍이에요. `lib/pattern/matrix.ts:79` 의 `isValidMatrix` 가 `boolean` 을 돌려줘서, 그걸 거친 뒤에도 `lib/lab/legacyDraft.ts:82` 와 `lib/lab/serialize.ts:442` 가 `candidate!.width`, `candidate!.height` 를 써요(넷). 저장된 값은 `"128"` 같은 글자일 수도 있어서 `size is MatrixSize` 가드는 맞지 않아요. 맞는 크기를 `MatrixSize | null` 로 돌려주는 함수 하나면 넷이 빠져요. `isValidMatrix` 를 부르는 곳은 이 두 파일뿐이에요. 텍스트 효과의 `layer.source!` 셋(`lib/lab/flatten.ts:106`, `lib/lab/hExport.ts:291`, `lib/lab/textEffects.ts:137`)은 `TextSource | null`(`lib/lab/types.ts:165`)을 효과가 켜졌으니 있다고 믿고 써요. `lib/lab/textEffects.ts:44` 의 `sourceFits` 가 이미 타입 가드라 그걸 거치면 단언이 필요 없어요. 나머지 열(`stack.pop()!`, `getContext("2d")!`, 닫힘 안의 `project!` 들)은 그대로 둬요. | `lib/pattern/matrix.ts`, `lib/lab/legacyDraft.ts`, `lib/lab/serialize.ts`, `lib/lab/flatten.ts`, `lib/lab/hExport.ts`, `lib/lab/textEffects.ts` | `npm run typecheck`, `npm run check:panels`, `source` 가 `null` 인 효과 레이어를 내보내는 시험 하나 | -| 8 | 근간 | 모든 쪽이 받는 `globals.css` 에서 아무도 쓰지 않는 옛 Inside 패널 규칙 377줄이 빠진다 | 레포의 postcss 로 `app/globals.css` 를 읽어 클래스 191개를 `web/src`, `web/content`, `web/public` 의 html 과 js 에서 찾으니 60개가 어디에도 없었어요. 가장 큰 덩어리가 `:1117`~`:1493`(「"Inside" panel list」 주석부터 그 `@media (max-width: 900px)` 의 닫는 괄호까지, Sponsor 앞)이에요. 규칙 62개, 클래스 27개(`inside-*`, `reddit-comment-*`, `reddit-upvotes`)에 산 규칙이 하나도 없어요. 8,387바이트(gzip 1,854)이고 모든 쪽이 받는 CSS 에 그대로 들어가요. `InsidePanel.tsx` 는 이제 `InsidePanel.module.css` 를 써요. `inside-` 나 `reddit-` 이름을 템플릿이나 `+` 로 만드는 곳도 없어요. `reddit-comment` 는 저널 MDX 에 사진 파일 이름(`/journal/v1-30-days/reddit-comment-1.png`)으로만 나와요. 이 덩어리 안의 `--inside-wire`(`:1133`)도 같이 빠져요. | `app/globals.css` | 빌드한 사이트의 `.next/static/chunks/*.css` 크기 앞뒤, `/` 와 `/inside` 를 390px 와 1440px 로 찍어 앞뒤가 같은지, `npm run check:ci`(`fonts.test.ts`, `hoverUnderline.test.ts`, `panelNotes.test.ts` 가 이 파일을 읽어요) | -| 9 | 근간 | Lab 밖 CSS 모듈 다섯에서 아무 컴포넌트도 읽지 않는 클래스 23개가 빠지고, 시험이 사이트 전체의 CSS 모듈을 본다 | 바퀴 89 의 셈(모듈을 불러오는 파일이 `styles.x` 나 `styles["x"]` 로 읽는 이름과 `composes`)을 `app/pattern-lab/` 밖의 CSS 모듈 36개에 돌렸어요. 계산된 이름으로 읽는 모듈은 없었어요. 죽은 클래스는 `app/roadmap/Roadmap.module.css` 가 11개(`legend` 들과 `swatch*` 셋, `zoom*` 셋, `mapCaption`, `gridLine`, `:106`~`:152`, `:185`~`:221`, `:512`~`:528`, 그리고 `:252` 의 묶인 셀렉터 하나)이고, `components/community/Community.module.css` 가 9개(`gatePanel` 들, `sectionLink`, `btnLink`, `freshStrip`, `introRow`, `composerCount`, `:2769`~`:2796` 「Sign-in gate」 부터 일곱 덩어리)예요. 하나씩은 `components/guide/desk/Desk.module.css:249`~`:257` 의 `appEmpty`, `components/community/Atlas.module.css:364`~`:371` 의 `panelWell`, `components/sections/BuildPanel.module.css:12`~`:18` 의 `note` 예요(`CasePicker.tsx` 는 제 모듈의 `.note` 를 읽어요). 규칙 30개, 빈 줄과 주석까지 221줄이에요. 같은 셈에서 `components/guide/Guide.module.css:254`~`:258` 의 `@keyframes draw` 와 아무도 `var()` 로 읽지 않는 변수 셋(`components/guide/ConsoleWindow.module.css:10` 의 `--cw-hair`, `Desk.module.css:17` 의 `--d-hair`, `components/guide/LabWindow.module.css:15` 의 `--lw-chrome-2`)도 나왔어요. 걷고 나서 `app/pattern-lab/labCss.test.ts` 의 「define no class that no component uses」 를 `src/` 의 모든 `*.module.css` 로 넓혀요. | 위 CSS 모듈 여덟, 새 `src/cssModules.test.ts`(또는 `labCss.test.ts` 를 넓혀서) | 바퀴 89 처럼 고친 쪽(`/roadmap`, `/community`, `/community/atlas`, `/guide/make`, `/build`)의 `getComputedStyle` 앞뒤 0 차이, 시험에 `.zzDead {}` 를 넣어 빨개지는지, `npm run check:panels` | -| 10 | 근간 | `globals.css` 에 남은 안 쓰는 규칙 70개와 그것만 쓰던 애니메이션 둘, 변수 넷이 빠진다 | 8위와 같은 셈에서 Inside 덩어리 밖의 죽은 규칙이 24덩어리, 70개, 292줄이에요. 큰 것은 `pf-marquee`, `pf-track`, `pf-comment` 들(`:992`~`:1067`, 73줄), `version-tag`(`:670`~`:705`, 36줄), `spec-row`, `stack-layer`, `track-dot` 들(`:716`~`:794`, 53줄), `ph-*` 와 `key-points`(`:808`~`:845`, 36줄), `business-*`(`:1770`~`:1811`, `:2597`~`:2619`, 46줄)예요. 묶인 셀렉터에서 죽은 이름만 빼면 되는 규칙이 일곱 더 있어요(`:911`, `:1716`, `:1724`, `:1762`, `:1779`, `:2582`, `:2611`). 이 규칙만 쓰던 `@keyframes slideUpFadeIn`(`:639`~`:642`)과 `pf-slide`(`:1013`~`:1016`), 변수 `--led-hex`(`:122`), `--pf-fs-card`(`:174`), `--pf-fs-ghost`(`:175`, `:1071`), `--pf-marquee-dur`(`:183`)도 읽는 곳이 없어져요. 8위와 합치면 이 파일이 75,019바이트(gzip 14,965)에서 6만 바이트 밑으로 내려가요. 저널 쪽 죽은 규칙(`journal-list-number`, `journal-figure` 들, `:2068`, `:1891`, `:2377`~`:2452`, `:2740`~`:2745`)은 저널 MDX 가 앞으로 쓸 수도 있어 이 줄에 넣지 않았어요. `.spec`, `.stack`, `.track`, `.placeholder` 넷은 낱말이 글과 데이터에만 나와서 그 바퀴가 다시 봐요. | `app/globals.css` | 8위와 같은 크기와 사진 비교, `npm run check:ci` | -| 11 | 근간 | Lab 밖에서 아무도 읽지 않는 이름 다섯이 빠지고, 가이드 화면 그리기가 글자 폭과 줄 높이를 이름으로 읽는다 | 1위와 같은 방법(knip 으로 찾은 export 를 스크래치에서 다 떼고 `tsc --noEmit`)으로 Lab 밖에서 정의 말고 부르는 곳이 없는 이름이 일곱 나왔어요. `lib/community/server/builds.ts:14` 의 `BuildStatus`, `lib/community/workshop.ts:36` 의 `FLOOR_COLUMNS`, `lib/journal.ts:117` 의 `getJournalPost`, `lib/pattern/controls.ts:97` 의 `KnobValues`, `lib/pattern/packs.ts:82` 의 `DEFAULT_PACK` 은 지워요. `DEFAULT_PACK` 은 `BASICS_PACK` 의 다른 이름인데(`:76`~`:81` 주석) 부르는 곳이 없어요. 남은 둘은 지우지 않아요. `lib/guide/glcdFont.ts:310` 의 `GLCD_ADVANCE`(6)와 `:312` 의 `GLCD_LINE_HEIGHT`(8)는 Adafruit 값을 적어 둔 이름인데, `lib/guide/panelScreens.ts:562`~`:601` 이 같은 값을 `s * 6` 다섯 곳과 `s * 8` 다섯 곳으로 숫자로 써요. 그 열 곳이 두 이름을 읽게 해요. Lab 밖에서 파일 밖으로 안 나가는 export 133개(78개 파일)는 이 줄에 넣지 않았어요. | `lib/community/server/builds.ts`, `lib/community/workshop.ts`, `lib/journal.ts`, `lib/pattern/controls.ts`, `lib/pattern/packs.ts`, `lib/guide/panelScreens.ts` | `npm run typecheck`, `npm run check:panels`(`panelScreens.test.ts` 가 화면 픽셀을 봐요), `npm run check:virtual-panel` | -| 12 | 근간 | 타입 검사가 타입만 쓰는 import 에 `import type` 을 바라고, 닿지 않는 코드를 잡는다 | `web/tsconfig.json` 에 꺼진 규칙마다 `npx tsc --noEmit <규칙>` 을 돌려 오류를 셌어요. `--verbatimModuleSyntax` 는 10개이고 다 타입만 쓰는 import 예요(`components/3d/HeroScene.tsx:8`, `components/3d/patterns/index.ts:3`, `patternFlowOriginal.ts:1`, `patternWaveSaw.ts:1`, `components/community/CommunityFeedClient.tsx:5`, `components/sections/BuildPanel.tsx:3`, `:4`, `InsidePanel.tsx:4`, `PatternPanel.tsx:5`, `RightPanel.tsx:11`). `--allowUnreachableCode false` 는 하나예요. `lib/guide/deviceSim.ts:767` 의 `return none();` 가 모든 갈래에서 돌아오는 `switch` 뒤에 있어요. `tools/virtual-panel/tsconfig.json` 이 이 파일을 이어받는데 그 프로젝트에선 둘 다 더 나는 오류가 0개예요. `--noImplicitReturns` 는 오류가 둘인데 하나가 `tools/virtual-panel/transports/mqtt.ts:256` 이라 이 줄에 넣지 않았어요. | `web/tsconfig.json` 과 위 열한 곳 | `npm run typecheck`(두 프로젝트), `npm run lint`, `npm run build`, 타입 import 하나를 값 import 로 되돌린 파일로 빨개지는지 | -| 13 | 근간 | 타입 검사에서 `PatternPanel.tsx` 한 파일이 쓰는 3초가 0.3초 아래로 준다 | `npx tsc --noEmit --generateTrace` 로 재니 이 파일이 2.97초로 사이트에서 가장 느려요(검사 17.7초의 16%). 그 안에서 `components/sections/PatternPanel.tsx:22` 가 1.42초, `` 을 그리는 `:338`~`:353` 이 1.46초예요. `:22` 가 그 요소를 `React.ElementType` 로 바꿔 쓰는데, 이 타입은 JSX 의 내장 요소마다 props 가 맞는지 따져요. `@react-three/fiber` 9 가 three.js 요소를 다 그 목록에 더해서 커요. 파일 하나로 따로 재니 `React.ElementType

` 는 2.42초(인스턴스 639,270개), `React.ComponentType

` 는 0.23초(9,827개)였어요. 같은 버튼을 `components/guide/Extras.tsx:38` 은 이미 `React.FC` 로 써요. 10월 21일에 잰 9초는 그 뒤 다른 파일이 같은 타입을 먼저 계산하게 되면서 이 파일 몫이 준 거예요. | `components/sections/PatternPanel.tsx` | `npx tsc --noEmit --generateTrace <폴더>` 앞뒤로 이 파일의 `checkSourceFile` 시간, `npm run typecheck`, `npm run build` | -| 14 | 근간 | `web/ARCHITECTURE.md` 의 첫 화면 구성도와 캠페인 주소가 지금 파일을 말한다 | 10월 21일 표의 줄을 오늘 다시 봤어요. 틀린 말이 다섯 곳이에요. `:43` 은 패턴 목록이 `common.ts` 에 있다지만 `components/3d/patterns/index.ts:6` 에 있어요. `:46` 은 `BuildPanel.tsx` 에 플래셔 입구가 있다지만 그 파일엔 `esp-web-install-button` 이 없고 `PatternPanel.tsx:22` 에 있어요(같은 문서 `:21` 도 그렇게 말해요). `:47` 은 Pattern 패널의 단추가 다 /pattern-lab 로 간다지만 `PatternPanel.tsx:310`, `:325`, `:374` 는 커뮤니티, 펌웨어 README, Play 가이드로 가요. 지구본은 `components/3d/ViewerPanel.tsx:11`, `:93` 이 그리는데 그림(`:39`~`:48`)엔 InsidePanel 밑에 있어요. `:31` 은 캠페인 링크를 `/r/*` 로 적지만 8개 가운데 4개(`/hn`, `/ig`, `/pcbway`, `/x`)는 맨 위 주소예요(`lib/campaignRoutes.ts:60`, `:70`, `:80`, `:90`). Pattern Lab 절(`:66`~`:80`)은 패널 아홉과 표가 `panels/registry.tsx:78`~`:88` 과 맞아서 고칠 게 없어요. | `web/ARCHITECTURE.md` | `npm run check:human-lock`, 저장소 뿌리에서 `python3 .github/scripts/check_links.py` | -| 15 | 준비 | 가이드 시뮬레이터 주석이 펌웨어를 줄 번호 대신 함수 이름으로 가리킨다 | `lib/guide/` 에 `patternflow.ino:NNN` 참조가 30곳이에요. 함수 이름이 같이 적힌 12곳은 오늘 하나도 맞지 않아요. `drawScreenHeader` 는 646 이라 적혔지만 697 에 있고(`lib/guide/panelScreens.ts:718`), `drawKnobMap` 은 973 이 아니라 1022(`:899`), `asciiFold` 는 577 이 아니라 599(`:247`)예요. 펌웨어를 고칠 때마다 또 어긋나니, `lib/guide/deviceSim.ts` 의 「patternflow.ino contentNoticeTimer」 꼴의 이름으로 바꿔요. `lib/guide/firmwareConstants.test.ts` 가 펌웨어 파일을 읽으니, 주석이 부르는 이름이 `.ino` 에 있는지도 거기서 볼 수 있어요. | `lib/guide/panelScreens.ts`, `lib/guide/deviceSim.ts`, `lib/guide/tomThumbFont.ts`, `lib/guide/panelScreens.test.ts`, `lib/guide/firmwareConstants.test.ts` | `npm run check:panels`, `git grep -n 'patternflow\.ino:[0-9]' -- web/src/lib/guide` 가 0줄 | +| 1 | 근간 | 회원이 표시 이름을 남의 이름으로 바꿔 카드와 라이선스 머리에 그 사람으로 나오지 못한다 | `lib/community/server/auth.ts:58`~`:62` 의 `username()` 플러그인에 `displayUsernameValidator` 가 없어요. 스크래치 DB 에서 회원 eve 가 진짜 `app/api/auth/[...all]` 경로로 `/api/auth/update-user` 에 `displayUsername` 을 보내니, 「ada」 도 「ADA (moderator)」 도 줄바꿈이 든 글도 200 이었어요. 그 뒤 eve 의 카드는 `@ada` 로 나와요. `components/community/PatternCard.tsx:591` 과 `app/community/p/[id]/PatternDetailClient.tsx:585` 가 `displayUsername` 을 먼저 찍어서예요. 그 뒤 eve 가 올린 패턴의 저장된 코드 머리도 `// Author: @ada` 예요. 가입할 때도 이 칸을 아무 글로 받아요. | `lib/community/server/auth.ts:57`~`:62`(가입과 update-user 에서 `displayUsername` 이 대소문자만 다른 제 `username` 일 때만 받기) | 새 `lib/community/server/auth.test.ts` 가 auth 경로를 진짜로 돌려, eve 의 「ada」 는 400, 「EVE」 는 200, 다른 이름으로 가입하면 400 인지 | +| 2 | 근간 | 제목에 든 줄바꿈이 패턴 코드의 한 줄이 되지 않고, 포크한 사람의 코드에도 남의 줄이 따라가지 않는다 | `lib/community/validate.ts:13`~`:18` 의 `cleanTitle` 은 앞뒤 공백만 깎아서 `\n`, NUL, U+202E 를 그대로 받아요. 진짜 POST `/api/community/patterns` 에 줄바꿈 뒤 JS 한 줄을 단 제목을 보내니 201 이었어요. 저장된 코드의 셋째 줄이 주석이 아닌 그 JS 였어요. 라이선스 머리의 `// Title:` 줄이 둘로 갈려서예요. 다른 회원이 그 패턴을 포크하니(201) 그 사람 이름 아래 `// Based on:` 줄에 같은 줄이 실렸고, 두 코드를 돌리니 둘 다 그 줄을 실행했어요. U+2028 과 U+2029 도 JS 에선 줄을 끊어요. 같은 머리가 `.h` 내려받기(`buildSharedHeaderFile`)에도 들어가요. | `lib/community/validate.ts:13`~`:18`(제어 문자와 U+2028, U+2029 거절), 이미 저장된 줄을 위해 `lib/community/license.ts:62`~`:72` 의 `shareMeta` 와 `lib/community/download.ts:27`~`:40` 의 `shareMetaFor` 에서 제목과 이름, 원작 줄의 줄바꿈 접기 | `cleanTitle` 시험, 올리고 포크한 뒤 머리 줄 수를 세는 경로 시험 | +| 3 | UI | 커뮤니티의 흐린 회색 글자가 어두운 바탕에서 읽힌다 | 「창작자가 열어야 하는 것」의 흐린 글자색 줄 가운데 커뮤니티 몫이에요. 창작자가 지시 10 으로 열었어요. axe-core 로 커뮤니티 11쪽을 390px 와 1440px, 로그아웃과 로그인으로 40번 보니 color-contrast(serious)가 426곳이었어요. 426곳 모두 글자색이 `--pfc-ink-faint` #6B6558 이었어요. 바닥 링크, 카드의 LIK 와 FRK, 덱 카드의 수, 「Report」, 댓글 날짜 같은 글이에요. 이 색은 `--pfc-ground` 에서 3.40:1, `--pfc-panel` 에서 3.28:1, `--pfc-well` 에서 3.44:1, `--pfc-well-2` 에서 3.18:1 이에요. `:root` 에 #847E70 을 덮어 다시 돌리니 40번 모두 0곳이었어요(4.87, 4.71, 4.93, 4.56:1). 가이드 연습 커뮤니티(`components/guide/desk/community/Practice.module.css`)도 같은 토큰을 읽어서 `/guide/make` 의 19곳이 같이 바뀌어요. | `app/globals.css:152` | globals.css 를 읽어 그 값이 네 바탕과 4.5:1 넘는지 셈하는 시험, axe 다시 돌려 0곳 | +| 4 | UI | 폰에서 커뮤니티 벽이 여섯 칸으로 떴다가 세 칸으로 다시 놓이지 않는다 | `components/community/CommunityFeedClient.tsx:217` 의 `useState(6)` 때문에 서버 HTML 이 `repeat(6, minmax(0,1fr))` 이에요. 390px 에선 46×136px 카드 여섯이 `data-view="large"` 로 떴다가, 하이드레이션 뒤 114×267px 세 칸으로 바뀌어요. 390×844 에서 레이아웃 이동(CLS)이 `/community/patterns` 세 번 0.652, 0.660, 1.284 였고 `/community` 세 번 0.646 이었어요. 0.25 를 넘으면 「나쁨」이에요. 폰 미디어 쿼리에 `repeat(auto-fill, minmax(100px,1fr))` 과 작은 카드 규칙을 넣어 보니 `/community/patterns` 가 0.000 이었어요. `/community` 는 넣은 규칙이 하이드레이션에 날아가서 못 쟀어요. | `components/community/CommunityFeedClient.tsx:217`, `:306`~`:309`, `:545`~`:549`, `components/community/Community.module.css:484`~`:490`(`.feedGrid`), `:858`~`:891`(`[data-view="small"]`), 폰 덩어리 `:4149` | 서버 렌더의 인라인 스타일에 `repeat(6` 이 없는 시험, `communityCss.test.ts` 의 폰 규칙 검사, 두 쪽 CLS 다시 재기 | +| 5 | UI | 폰에서 긴 덱 이름 하나가 덱 쪽과 프로필을 옆으로 밀고 YOU 탭을 화면 밖으로 내보내지 않는다 | `902f5ea` 표 29위를 다시 쟀어요. 62자 이름의 공개 덱을 두니 `/community/decks` 와 `/community/u/ada` 의 scrollWidth 가 390px 에서도 360px 에서도 578 이었어요. 그 칸의 카드가 다 562px 가 되고, 탭 바도 578px 로 늘어 YOU 가 화면 밖이에요. 폰 규칙 `.deckGrid { grid-template-columns: 1fr }` 가 `minmax(auto, 1fr)` 이라 `nowrap` 제목보다 좁아지지 않아서예요. `minmax(0, 1fr)` 로 바꿔 보니 scrollWidth 가 390 과 360, 카드가 358 과 328px 였고 제목은 말줄임표로 끝났어요. 1440px 은 그대로예요. | `components/community/Community.module.css:4253`~`:4255`(제목은 `.deckCardTitle` `:2919`, `components/community/DeckCard.tsx:47`~`:57`) | `communityCss.test.ts` 의 폰 규칙에 `minmax(0` 검사, 두 쪽을 390px 와 360px 로 다시 재기 | +| 6 | UI | 커뮤니티 창 안에서 글을 끌어 고르다 창 밖에서 놓아도 창이 닫히지 않고 쓴 글이 남는다 | 편집 창(Edit details)의 설명 칸에 글을 쓰고 칸 안에서 눌러 바깥 판 위에서 놓으니 창이 닫혔고, 다시 여니 설명이 `""` 였어요. 브라우저가 누른 곳과 놓은 곳이 같이 든 바깥 판에 click 을 보내서, 카드의 `stopPropagation` 이 돌 틈이 없어요. Lab 의 창 셋은 바퀴 78(#204)이 만든 `lib/hooks/useBackdropClose.ts` 로 이미 막았는데, 커뮤니티 창 열둘은 그 훅을 쓰지 않아요. | `components/community/PublishModal.tsx:266`, `EditDetailsModal.tsx:116`, `ReportModal.tsx:72`, `AddHeaderModal.tsx:129`, `AddPerformanceModal.tsx:82`, `ShareDeckModal.tsx:86`, `ShareDeckPackModal.tsx:65`, `AuthModal.tsx:143`, `SendModuleModal.tsx:52`, `DeckPanel.tsx:114`, `app/community/d/[id]/DeckDetailClient.tsx:646`, `:748` 의 `onClick={onClose}` 를 `useBackdropClose` 로 | 칸에서 누르고 바깥 판에서 놓으면 `onClose` 가 안 불리고 바깥 판에서 누르고 놓으면 불리는 jsdom 시험(`dialogKeys.test.tsx` 옆) | +| 7 | UI | 폰에서 댓글의 Post 단추와 덱 패널의 순서 단추, 내 프로필의 단추가 오른쪽 제자리에 선다 | 폰 규칙 `.sectionNav, .headerSpacer { display: none }` 는 쪽 머리의 빈 칸을 숨기려던 건데, `.headerSpacer` 는 다른 여덟 곳에서도 오른쪽으로 미는 빈 칸이에요. 390px 에서 댓글 「Post」 가 x 316~374 대신 16~74 에 있어요. 덱 패널의 ↑ 단추 다섯은 다 x 213 이어야 하는데 115, 144, 158, 213, 158 로 들쭉날쭉해요. 내 프로필의 「Make & publish」 도 오른쪽(234) 대신 왼쪽(16)이에요. 덱 패널 창이 `

` 안에 그려져서 범위는 `.pageHeader > .headerSpacer` 여야 해요. 두 칸 그리드인 폰 `.actionRow` 안에선 계속 숨겨야 「Empty deck」 이 줄을 넘지 않아요. | `components/community/Community.module.css:4158`~`:4161`(쓰는 곳 `DeckPanel.tsx:187`, `:242`, `:302`, `CommentSection.tsx:249`, `BodyComposer.tsx:61`, `app/community/u/[username]/page.tsx:87`, `FeaturedEditor.tsx:251`, `ReportQueue.tsx:125`) | 셀렉터 범위를 보는 CSS 시험, 390px 에서 세 곳의 x 다시 재기 | +| 8 | UI | 지워졌거나 비공개인 패턴, 덱, 프로필 링크가 커뮤니티 안에서 까닭을 말한다 | `/community/p/0000000000000000`, `/community/u/nobody404`, `/community/d/0000000000000000`, 남의 비공개 덱이 모두 사이트 뿌리의 404 쪽을 그려요. 「404」 와 「RETURN TO MAIN」 뿐이고 커뮤니티 머리와 메뉴가 사라지며, 탭 제목은 「Community / Patternflow」 그대로이고 링크는 첫 화면 `/` 로 가요. `app/community/` 아래 `not-found.tsx` 가 없어서예요. 지워진 패턴의 옛 주소는 지우기 뒤에 바로 이 쪽이 돼요. | 새 `app/community/not-found.tsx`(커뮤니티 레이아웃 안에서 그려지고, 지워졌거나 비공개라는 글과 `/community/patterns` 링크) | 렌더 시험, 네 주소를 curl 해 404 와 `href="/community/patterns"` 를 보기 | +| 9 | UI | 머리줄의 알림 수가 알림 쪽을 열면 사라지고, 쪽에 보이지 않는 알림은 세지 않는다 | 레이아웃이 수를 한 번 읽고(`app/community/layout.tsx:70`), 쪽을 옮겨도 레이아웃은 다시 그려지지 않아요. 읽음 POST 는 답을 기다리지 않아요(`components/community/NotificationsList.tsx:71`~`:77`). 그래서 「Alerts 1」 이 알림 쪽에서도, Patterns 와 Decks 를 오가도 남고 새로 고침해야 사라져요. 세는 쪽도 어긋나요. `lib/community/server/queries/notifications.ts:12`~`:18` 의 `countUnreadNotifications` 는 그냥 COUNT 인데, `listNotifications`(`:86`~`:99`)는 비공개가 된 대상과 워크숍 `post` 알림을 걸러요. bea 가 공개 패턴에 댓글을 단 뒤 주인이 그 패턴을 비공개로 돌리고 댓글을 달면, bea 의 배지는 1 이고 알림 쪽은 0개예요. 남은 워크숍 알림 하나를 더하면 배지 2, 목록 0이에요. | `components/community/NotificationsList.tsx:71`~`:77`(POST 가 끝나면 `router.refresh()`), `lib/community/server/queries/notifications.ts:12`~`:18`(목록과 같은 조인과 거르기로 세기) | 읽음 POST 뒤 refresh 를 부르는 시험, 두 경우에 배지 수와 목록 길이가 같은 시험 | +| 10 | UI | 커뮤니티 쪽이 Pattern Lab 코드 218 KB 를 뒤에서 받지 않는다 | `/community` 는 스크립트 20개, 404.8 KB 를 받아요. 그 가운데 224 KB 가 `load` 뒤에 와요. 218 KB 는 Pattern Lab 덩이 여덟(풀면 811,475바이트, dockview 와 편집기, 프리셋 48개)이고, 머리의 `/pattern-lab` Link 가 미리 받아서예요. 11.5 KB 는 FirmwareNotice 의 `/editions` 링크 몫이에요. 390px 에서도, 로그인 여부와 상관없이 커뮤니티 모든 쪽에서 그래요. 쪽 제 덩이는 다섯, 131 KB(gzip 44 KB)뿐이에요. #156 이 머리의 `/` 링크에서 3D 600 KB 를 걷은 것과 같은 꼴이에요. | `app/community/layout.tsx:99`~`:100`, `:107`, `app/community/u/[username]/page.tsx:90`, `:107`, `components/community/NotificationsList.tsx:88`, `components/community/FirmwareNotice.tsx:68` 에 `prefetch={false}` | `app/homeLinkPrefetch.test.ts` 꼴의 시험, 브라우저로 `load` 뒤 스크립트가 0 KB 가까운지 | +| 11 | 근간 | 빌드 경로 둘이 이상한 몸과 이름에 500 이나 빈 200 대신 400 과 404 를 돌려주고, 처음 시험을 얻는다 | 쓰는 경로 13개에 몸 칸 하나씩을 null, 0, true, `{}`, `[]`, `[null]`, `[0]` 으로 바꿔 360번 보내니 한 번 던졌어요. `POST /api/community/builds` 에 `{patterns:[null]}` 을 보내면 `TypeError: Cannot read properties of null (reading 'code')` 로 CORS 없는 500 이 돼요(#147 과 같은 꼴). `GET /api/community/builds//modules?file=__proto__` 는 200 에 `Content-Length: undefined`, 0~1바이트를 돌려줘요. `constructor`, `toString`, `hasOwnProperty` 도 같아요. 없는 이름은 바르게 404 예요. 두 GET 경로를 부르는 시험은 없어요. | `app/api/community/builds/route.ts:112`~`:114`(칸이 객체인지), `app/api/community/builds/[id]/modules/route.ts:98`(`Object.hasOwn(entries, wantedFile)`) | 새 `app/api/community/builds/route.test.ts` 가 `[null]`, 목록, 파일, 없는 이름, 프로토타입 이름 넷, 산출물 경로 막기를 돌리기 | +| 12 | UI | 로그인한 사람의 머리줄에 쪽을 열 때마다 「Sign in」 이 잠깐 뜨지 않는다 | `components/community/AuthStatus.tsx:16`~`:43` 은 브라우저가 세션을 받아올 때까지 「Sign in」 을 그려요. ada 로 연 폰 서버 HTML 의 머리가 `Make… / ▦ 0/20 / Alerts / Sign in` 이고, 두 줄(104px)이었다가 0.6~0.93초 뒤 이름이 오면 64px 로 줄어요. 390px CLS 가 로그인한 패턴 쪽과 덱 쪽마다 0.049, 알림 쪽 0.038 이에요. 부하가 걸린 한 번은 2.5초 내내 「Sign in」 이었어요. 레이아웃은 이미 세션과 이름을 갖고 있어요(`app/community/layout.tsx:65`~`:73`). `MobileTabBar` 는 그 이름을 `:143` 에서 받아요. | `components/community/AuthStatus.tsx:16`~`:43`(기다리는 동안 서버가 준 이름), `app/community/layout.tsx:122` | `useSession` 을 `{data:null,isPending:true}` 로 흉내 낸 `AuthStatus.test.tsx` 가 「Sign in」 대신 「@ada」 를 보는지 | +| 13 | UI | 펌웨어 안내 띠가 첫 그림부터 쪽에 있어서, 하이드레이션 뒤에 쪽 전체를 아래로 밀지 않는다 | `components/community/FirmwareNotice.tsx:44` 의 서버 쪽 값이 「닫음」(`() => true`)이라 띠가 하이드레이션 뒤에야 나와요. 닫지 않은 사람에게 폰에선 본문이 119px(y 66 에서 185 로), 데스크톱에선 55px 밀려요. 390px CLS 가 패턴 쪽과 `/community/decks` 에서 매번 0.141 이었어요. 로그인하면 0.145~0.183, 1440px 은 0.038~0.051 이에요. | `components/community/FirmwareNotice.tsx:31`~`:45`, `:52`~`:58`(닫음을 localStorage 와 함께 쿠키에도 적기), `app/community/layout.tsx:80`(이미 `headers()` 를 부르니 쿠키를 읽어 서버 값으로 넘기기) | 쿠키가 없으면 서버 HTML 에 띠가 있고 있으면 없는 `renderToString` 시험, CLS 다시 재기 | +| 14 | UI | 아이폰에서 커뮤니티 글 칸을 눌러도 쪽이 확대되지 않는다 | 폰에서 글 칸의 계산된 글자 크기가 모두 16px 아래예요. 벽 검색, 댓글 칸, 로그인 칸, 신고 내용, 편집 칸은 14px, 공유 링크는 12px, 공개 범위 고르개는 12.5px 예요. 아이폰 Safari 는 16px 아래 칸에 초점이 가면 쪽을 확대하고, viewport 메타엔 `maximum-scale` 이 없어요. 확대 자체는 Chromium 에서 재현되지 않아서 잰 것은 글자 크기예요. | `components/community/Community.module.css` 폰 덩어리 `:4149` 에 `.textInput`, `.commentForm textarea`, `.field select`(바탕 `:2460`~`:2472`), `.postBodyInput`(`:3635`), `.shareLinkField`(`:3073`), `.deckVisControl select`(`:3343`)를 16px 로 | CSS 시험, 390px 에서 다시 재기(칸 높이는 그대로, 댓글 칸 84px) | +| 15 | UI | 폰에서 커뮤니티 머리줄과 벽의 정렬 칸을 엄지로 누를 수 있다 | 폰으로 30번 열어 쟀어요. 위아래로 붙은 「Patternflow」(90.9×18.4)와 「COMMUNITY」(90.9×11.5)는 24px 원이 겹쳐 WCAG 2.5.8 에 걸려요. Atlas 밖에선 이것 하나예요. 로그인하면 Alerts 가 35.8×18.8, @ada 가 28.8×17.4, 덱 칩 ▦ 가 64.9×29.2 예요. 폰에서 덱으로 가는 길은 그 칩뿐이에요. 로그아웃이면 「Pattern Lab ↗」 가 86.1×18.8, 펌웨어 띠의 ✕ 는 18.9×21 이에요. 벽의 정렬 링크 5~6개는 25.6px 높이이고 「Flashable now」 는 117.6×18.1 이에요. 머리 칸에 min-height 44, 브랜드 줄 24px, ✕ 44×44 를 넣어 보니 모두 44 가 되고 머리는 65.8 에서 77px 이 됐어요. 정렬 칸을 44px 로 하면 첫 카드가 y 240 에서 284 로 내려가요. | `components/community/Community.module.css` 폰 덩어리 `:4149`~`:4205`(`.headerNav` `:4168`, `.makeCta` `:4180`, `.userChip` `:4192`), `.brandBlock` 과 `.pageTitle` `:53`~`:79`, `.deckChip` `:174`, `.fwNoticeClose` `:3031`~`:3042`, `.sortTabs` `:529`~`:575` 와 폰 `:4230`~`:4245` | `communityCss.test.ts` 검사, 390px 에서 다시 재기 | +| 16 | UI | 서버가 HTML 오류 쪽을 돌려줘도 커뮤니티 글 칸이 「Network error.」 대신 서버가 몇 번으로 답했는지 말한다 | `(await response.json()) as …` 로 읽는 28곳이 JSON 이 아닌 답에 던져서 catch-all 로 떨어져요. 댓글 POST 를 502 HTML 쪽으로 돌리니 CommentSection 이 끊긴 때와 똑같이 「Network error.」 였고, 신고 창은 「Network error — is the community server reachable?」 이었어요. EditDetailsModal 은 502 와 413 HTML 쪽 모두 「Network error.」 였어요. PublishModal 은 이미 「Publishing failed (HTTP 502).」 라고 해요(`components/community/PublishModal.tsx:221`~`:229`). 이 줄은 도우미와 글 칸 몫이고, 쪽과 훅의 나머지 19곳(`PatternDetailClient.tsx`, `DeckDetailClient.tsx`, `useModuleBuild.ts`, `PatternCard.tsx`, `DeckDock.tsx`, `FeaturedEditor.tsx`, `ReportQueue.tsx`, `CommunityFeedClient.tsx`)은 그 바퀴가 「가정」에 남겨요. | `lib/community/apiBase.ts`(이미 `COMMUNITY_FETCH_INIT` 이 있는 곳)에 `readApiError(response, fallback)` 하나, 쓰는 곳 `components/community/CommentSection.tsx:76`, `:98`, `:127`, `EditDetailsModal.tsx:100`, `ReportModal.tsx:56`, `AddHeaderModal.tsx:94`, `AddPerformanceModal.tsx:65`, `ShareDeckModal.tsx:65`, `LikeButton.tsx:52` | 도우미 시험, 502 HTML 이면 그 번호를 보이는 CommentSection 시험 | +| 17 | UI | 「Liked」 탭이 비면 아무도 올리지 않았다는 글 대신 아직 좋아한 패턴이 없다고 한다 | 벽에 패턴이 30개 있는 회원이 「Liked」 를 누르면 「0 PATTERNS · PATTERNS · EMPTY · Nobody has hung anything here yet. · Open Pattern Lab, make something, publish it here…」 가 떠요. 빈 서버의 새 회원도 같아요. 빈 갈래(`components/community/CommunityFeedClient.tsx:525`~`:538`)가 `sort === "liked"` 를 보지 않아서예요. | `components/community/CommunityFeedClient.tsx:525`~`:538`(liked 갈래, 하트를 누르면 여기 남는다는 글과 벽으로 가는 링크) | `CommunityFeedClient.test.tsx` 에 `sort="liked"`, `total=0` 경우 | +| 18 | UI | 서버가 거절한 좋아요가 말없이 되돌아가지 않고 까닭을 보인다 | 좋아요를 429 와 `{"error":"Too many likes — slow down a moment."}` 로 돌려보내니, 패턴 쪽 단추는 켜졌다가 150ms 안에 「LIK 00」 으로 돌아왔고 쪽 글은 한 글자도 바뀌지 않았어요. 벽 카드도 `aria-pressed` 가 false 로 돌아갈 뿐 어디에도 글이 없어요. 카드엔 이미 잠깐 뜨는 쪽지(`deckNote`, `components/community/PatternCard.tsx:149`)가 있어요. | `components/community/LikeButton.tsx:47`~`:51`, `:55`~`:58`, `components/community/PatternCard.tsx:190`~`:194`, `:198`~`:201` | fetch 를 429 로 흉내 내 서버 글이 보이는 시험 | +| 19 | UI | Preview 에서 댓글을 올리면 칸이 Write 로 돌아온다 | 글을 쓰고 Preview 를 누른 뒤 Post 하니, 그 뒤엔 글 칸이 없고 Preview 탭이 고른 채 꺼져 있었어요. 다시 쓰려면 「Write」 를 찾아 눌러야 해요. `components/community/BodyComposer.tsx:34` 의 `previewing` 이 글이 비어도 돌아가지 않아서예요. | `components/community/BodyComposer.tsx:34`, `:56`, `:67`(글이 있을 때만 미리보기) | 새 `BodyComposer.test.tsx` 가 `value=""` 로 다시 그려 글 칸이 돌아오는지 | +| 20 | 근간 | 댓글 마크다운이 머리 주석이 말한 시험을 얻고, 같은 쪽 링크가 새 탭으로 열리지 않는다 | `lib/community/markdown.ts:5` 는 URL 규칙을 smoke 시험이 지킨다고 하는데 `safeHref` 를 부르는 시험은 0곳이고, 커버리지로 재도 한 번도 돌지 않아요. 20가지를 넣으니 17가지가 맞았어요. `/\evil.example`, `\\evil.example`, `\/evil.example` 은 지나가고, 브라우저는 셋 다 `https://evil.example/` 로 풀어요. 지금은 micromark 가 역슬래시를 퍼센트로 바꿔서 PostBody 는 안전해요. 각주 링크 `#user-content-fn-1` 은 `target="_blank"` 로 그려져 누르면 같은 쪽이 새 탭으로 열려요. 각주 든 댓글 둘이 한 쪽에 있으면 `id="footnote-label"` 이 겹쳐요. | `lib/community/markdown.ts:27`(앞 역슬래시를 슬래시처럼), `components/community/PostBody.tsx:56`~`:70`(`#` 와 `/` 링크엔 target 없이), `:48`~`:54`(댓글마다 `clobberPrefix`, 또는 각주 빼기) | 새 `lib/community/markdown.test.ts` 와 `components/community/PostBody.test.tsx` 에 그 20가지 | +| 21 | UI | 덱과 보내기 창 여섯이 Esc 로 닫히고, 열리면 초점이 창 안으로, 닫히면 연 단추로 간다 | #154 는 `components/community/dialogKeys.test.tsx` 가 부르는 창 여섯만 고쳤어요. 1440px 에서 키보드로 재니 덱 쪽의 Edit details 창(EditDeckModal)과 「Share deck」(ShareDeckModal)은 Esc 가 아무것도 하지 않아요. 덱 공연 창(DeckPerformanceModal)은 초점도 연 단추에 남아요. ShareDeckPackModal 은 초점이 안으로 들어가지 않고, Esc 뒤 초점이 BODY 에 떨어져요. 390px 의 DeckPanel 은 초점이 들어가지 않고 Esc 도 안 돼요. SendModuleModal 은 jsdom 에서 Esc 로 닫히지 않고 초점이 body 에 남았어요. 여섯 다 `lib/hooks/useDialog.ts` 를 쓰지 않아요. | `app/community/d/[id]/DeckDetailClient.tsx:646`, `:748`, `components/community/DeckPanel.tsx:114`, `ShareDeckModal.tsx:86`, `ShareDeckPackModal.tsx:43`~`:50`, `:65`, `SendModuleModal.tsx:52` 에 `useDialog`(DeckPanel 은 안에 AuthModal 을 품어서 창마다 Esc 한 번) | 여섯을 `dialogKeys.test.tsx` 의 `WINDOWS` 에 더해 dev 에서 빨갛고 고친 뒤 초록 | +| 22 | UI | 패턴 쪽에서 화살표 키 한 번이 노브를 0.001 대신 기기 한 칸만큼 돌린다 | `app/community/p/[id]/PatternDetailClient.tsx:332`~`:345` 의 범위 칸은 `step="0.001"` 이라 ArrowRight 열 번에 Knob 1 이 0.5 에서 0.51 이 돼요. 끝에서 끝까지 Knob 1(0..1)은 1,000번, Knob 2(0.1..10)는 9,900번, Knob 3 은 4,900번이에요. Lab 은 바퀴 81(#208)이 `knobArrowValue`(`lib/pattern/controls.ts:92`)로 48번이 되게 고쳤어요(`app/pattern-lab/panels/KnobsPanel.tsx:289`). 커뮤니티 쪽은 그 함수를 부르지 않아요. | `app/community/p/[id]/PatternDetailClient.tsx:332`~`:345`(onKeyDown 에서 `knobArrowValue`) | 그 칸에 ArrowRight 를 쏴 `knobDetentStep` 만큼 움직이는 시험, 브라우저로 끝까지 48번 | +| 23 | UI | 기기 주소 칸이 제 글자를 눌러도 초점을 받고, 폰에서 누르기 쉬운 크기다 | `902f5ea` 표 30위를 다시 쟀어요. 이름은 #142 가 붙였고 남은 건 `