feat(구조물표): 322-2 M02 표 화면 — 탭 셋(한눈 표 · 종류별 · 부품) · 반영 · 부품 · 알약 색 저장 · 목록 + enabled 함께 · 검사 짝(화면 = 서버 · 1-3 규칙 · 만든 뒤 고정) · 명세 폼 보임 따름 · 판 알림

- 한눈 표(M02_SO_Def_Table): 군 · 종류 ▲▼ · 이름 · 목록 · 자리꼴 · 정본(고정) · 횡단도 · 카드 이름 · 형상(+ 벽 기본 형태 · 물빼기) · 알약 색 · 3D · 검사 · 처음 값 = 흐림 「기본」 · 바꿈 = 진함 · [처음 값으로]
- 종류별: 반영 칸 + 명세 이름 나무 줄마다 보임(컨테이너 = 그 안 칸) · 못 숨김 · 미리보기 숨김 따름 · 「안 씀」 = 목록 B06 만
- 부품 탭(M02_SO_Def_Parts): 부품 · 읽는 칸 · 자리꼴 · 정본 · 쓰는 종류 · 켬/끔(정의 부품 = 끈 것만)
- 편집 손질(M02_SO_Def_Reflect): 처음 값이면 반영에서 지움 · [저장] 때 enabled = 목록 both 일 때만 참
- 검사 짝 M02_SO_Def_Check.ts ↔ M02_SO_Check.py(글까지 같음): 반영 · 부품 꼴 · 형상 none → 횡단도 none · 부품 자리꼴 · 정본 · 기본값 없는 읽는 칸 새로 숨김 금지(앞 판 숨김 그대로) · 종류 지우기 · 자리꼴 · 정본 · 군 글자 · 선택지 값 고정 · 서버 check(doc, 지금 판) · 앞 판 없으면 꼴만
- 명세 폼: 쓰는 옵션 키가 모두 숨긴 칸 · 상자는 안 그림(값 그대로) · B05 · B06 시설 · 구조물 명세 폼이 정의 「표시」를 따름
- 판 알림: GET 구조물 목록에 definition_version · structures.json 판 ≠ 지금 판이면 「구조물 정의가 판 N 으로 바뀜 — 저장하면 반영」 한 번
- 시험: test_322_2_check_mirror(거울 · 규칙 · 반영 손질 · 서버 저장 · 422) · 판 알림 · 옛 정의 화면 시험 = 선택지 값 대신 이름 표 · 못 숨김 아닌 칸

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-14 15:40:38 +09:00
co-authored by Claude Opus 5.5
parent bbd6254892
commit 3abd2bdfd4
26 changed files with 1635 additions and 34 deletions
+9 -1
View File
@@ -22,6 +22,8 @@ import {
linkOf,
readState,
solveSplit,
specContainerHidden,
specItemHidden,
TEXT_MAX,
writeOptions,
type SpecContainer,
@@ -82,6 +84,8 @@ export interface SpecForm {
groupNames?: Record<string, string>,
/** 정의 기본값(저장 키 → 값) — 필수 상자 빈 고르기 칸이 받는 값(54-43). */
defaults?: Record<string, string | number>,
/** 숨긴 옵션 키(정의 「표시」 · 322-2) — 그 키만 쓰는 칸 · 상자는 안 그림(값은 그대로). */
hidden?: ReadonlySet<string>,
) => void;
read: () => Record<string, string | number>;
state: () => SpecState;
@@ -104,6 +108,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
let names: SpecNames = {};
let groupNames: Record<string, string> = {};
let defaults: Record<string, string | number> = {};
let hidden: ReadonlySet<string> = new Set();
const recent = new Map<Instance, SplitRole[]>();
const slots = new Map<string, HTMLElement>();
const rows: Array<{ key: string; set: (item: SummaryItem | null) => void }> = [];
@@ -235,6 +240,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
}
const fields = c.items
.filter((item) => item.kind === "auto" || !item.by || branchOf(item, view))
.filter((item) => !specItemHidden(item, hidden))
.map((item) => {
const input = control(c, item, view, tags.get(item.id));
const wrap = labeled(itemLabel(c, item, names), input);
@@ -316,6 +322,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
const fitted: SpecContainer[] = [];
for (const c of spec?.containers ?? []) {
const list = (state[c.id] ??= c.required ? [{}] : []);
if (specContainerHidden(c, hidden)) continue; // 숨긴 상자 — 값은 그대로(322-2)
if (fit(c, list)) fitted.push(c);
shown(c, list).forEach((values, index) => boxes.push(instanceBox(c, values, index)));
if (c.required || (c.max && list.length >= c.max) || hooks.count?.(c) !== undefined) continue;
@@ -341,9 +348,10 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
return {
root,
write(next, options, nextNames = {}, nextGroups = {}, nextDefaults = {}) {
write(next, options, nextNames = {}, nextGroups = {}, nextDefaults = {}, nextHidden) {
spec = next;
defaults = nextDefaults;
hidden = nextHidden ?? new Set();
names = nextNames;
groupNames = nextGroups;
state = readState(next, options);
@@ -120,6 +120,23 @@ export const linkOf = (c: SpecContainer, n: number): string =>
const text = (value: unknown): string =>
value === undefined || value === null ? "" : String(value);
/** 항목이 쓰는 정의 옵션 키 — 필수 상자 저장 키 · 갈래 키(선택 상자 인스턴스 키는 옵션 밖 · 322-2 보임). */
export const specItemKeys = (item: SpecItem): string[] =>
[item.key, ...Object.values(item.by?.map ?? {}).map((b) => b.key)].filter(
(key): key is string => !!key,
);
/** 숨긴 항목인가 — 쓰는 키가 있고 모두 숨김(정의 「표시」 · 322-2). */
export const specItemHidden = (item: SpecItem, hidden: ReadonlySet<string>): boolean => {
const keys = specItemKeys(item);
return keys.length > 0 && keys.every((key) => hidden.has(key));
};
/** 숨긴 상자인가 — 키 있는 항목이 하나 이상이고 모두 숨김. */
export const specContainerHidden = (c: SpecContainer, hidden: ReadonlySet<string>): boolean =>
c.items.some((item) => specItemKeys(item).length > 0) &&
c.items.every((item) => !specItemKeys(item).length || specItemHidden(item, hidden));
/** 옵션 → 컨테이너 값. 필수는 한 벌(상위 항목 먼저 읽어 갈래 키를 앎) · 선택은 저장된 n 차례대로. */
export function readState(
spec: StructureSpec,