Files
Aislo/B07_DesignDetail/B07_DesignDetail_UI_StandardSpec.ts
T
eomsangdonandClaude Opus 5 ca00ac278d feat(B07): 표준도를 「보여주기」에서 「고치기」로 — 제원 네 칸 입력
`phase: "detail"` 칸(돌 종류·조달·뒷길이·전면 기울기)을 **그리는 화면이 없었음**
(2026-09-09 실측: B06 구조물 폼은 b05 phase 열 칸만 그림. 화면 글자 전수에서 「조달」·
「뒷길이」·「돌 종류」 0건). 그 자리를 표준도가 맡음 — PLAN 4-5b 「표준도는 입력 화면이기도 함」.
B06 은 「어디에·몇 m」(배치), 표준도는 「어떤 제원」.

**장 하나 = 제원 조합 하나**라 한 번 고치면 그 조합의 개소 전부에 걸림.

지킨 것
  · **자동값을 저장에 박지 않음** — 빈 칸이 「정한 적 없음」의 뜻(확정 ⑨). 비우면 그 키를
    지움. 판정된 기울기는 칸이 아니라 **회색 도움말**로만 비춤.
  · **막지 않음** — 실무 도면에 1:0.7·1:0.8 이 실재(구조물도 53장). 품셈 범위(0.20~0.50)
    밖이어도 값은 받고 안내만.
  · **값을 여기서 셈하지 않음** — 정본에 적기만 하고 표·그림은 다음 조회에서 그 정본으로
    다시 섬. 표준도가 두 번째 정본이 되면 안 됨.

⚠ 만들다 잡은 것 셋
  ① `face_slope_ratio` 가 **구조물 등록부에 없어** 저장이 통째로 거절됐음
     (`ValueError: 돌쌓기(찰)에 정의되지 않은 옵션입니다`). 한 칸 때문에 전부 못 저장되는
     것은 나쁨 — 등록부에 없는 칸은 **빼고 이름으로 알림**(조용히 버리면 저장된 줄 앎).
     그 칸 신설은 랩탑 메인 몫.
  ② 저장 뒤 표·그림을 다시 받으면서 **폼이 새로 그려져 안내가 지워졌음.** 밖에서 들고
     있다가 다시 넣음.
  ③ **장 제목에 제원이 들어 있는데 좌측 단추 글자가 안 따라왔음.** 목록을 통째로 다시
     받지 않고 표준도 줄만 갈아 끼움.

실화면 확인 (프로젝트 936be972, 개발 우회로로 열어서)
  「표준도 2장」에서 돌 종류 견치돌·뒷길이 55 로 저장 →
    단추 글자가 「…뒷길이 35㎝ 야면석·호박돌」 → 「…뒷길이 55㎝ 견치돌」로 **즉시 바뀜**
  기울기 0.7 저장 → 안내 셋: 「1개소에 반영했습니다」 · 「1:0.7 는 품셈 표준경사 표
    범위(1:0.2~1:0.5) 밖입니다 — 값은 그대로 씁니다」 · 「「전면 기울기」 칸이 masonry_dry
    등록부에 아직 없어 저장하지 못했습니다」
  조달 「구입」 저장 → 그 장의 **2개소에만** 들어가고 다른 장은 그대로(정본 실측)
  확인 뒤 **지정받은 제원으로 되돌리고 우회로도 닫음**(목록 409 복귀).

700줄 제한으로 표준도 라우터를 `B07_DesignDetail_Router_Standard.py` 로 뗌(579 + 153).

자체검증 — 새 시험 7건 + 회귀 580 통과 · 0 실패, `tsc --noEmit` 0.

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

175 lines
6.6 KiB
TypeScript

/* =============================================================================
* B07_DesignDetail_UI_StandardSpec.ts
* 표준도 **제원 입력 칸** — 장 하나가 곧 제원 조합 하나이므로 여기서 고치면 그 조합 전부.
*
* 왜 여기인가 (PLAN 4-5b) — `phase: "detail"` 칸(돌 종류·조달·뒷길이·전면 기울기)을 그리는
* 화면이 없었다(2026-09-09 실측: B06 구조물 폼은 b05 phase 열 칸만 그림). B06 은
* 「어디에·몇 m」(배치), 표준도는 「어떤 제원」이다.
*
* ⚠ **빈 칸이 「자동」의 뜻**이다(확정 ⑨). 기울기를 비우면 품셈 표준경사 판정이 돌고,
* 채우면 그 값이 이긴다. 그래서 화면도 **빈 칸을 기본으로** 두고, 판정된 값은 회색
* 도움말로만 비춘다 — 칸에 미리 적어 두면 「정한 적 없음」이 사라진다.
* ⚠ **막지 않는다.** 실무 도면에 1:0.7·1:0.8 이 실재하므로 범위 밖 값도 받고 안내만 띄운다.
* ========================================================================== */
const STONE_KINDS = ["야면석·호박돌", "깬잡석", "깬돌", "견치돌"] as const;
const SUPPLIES = ["채집", "구입"] as const;
const BACK_LENGTHS = ["25", "30", "35", "45", "55", "60", "75"] as const;
/** 표준도 장 하나 — 서버가 낸 것 중 이 폼이 쓰는 것만. */
export interface StandardSheetSpec {
key: string;
title: string;
type_id: string;
member_count: number;
options: Record<string, unknown>;
}
export interface StandardSpecResult {
sheet_key: string;
stone_kind: string | null;
stone_supply: string | null;
back_len_cm: string | null;
face_slope_ratio: string | null;
}
/** 이 종류가 돌쌓기 계열인가 — 옹벽·집수정에는 이 칸들이 뜻이 없다. */
const STONE_TYPES = new Set(["masonry_wet", "masonry_dry", "boulder_masonry"]);
function field(label: string, control: HTMLElement, hint?: string): HTMLLabelElement {
const wrap = document.createElement("label");
wrap.className = "b07-spec__field";
const name = document.createElement("span");
name.className = "b07-spec__label";
name.textContent = label;
wrap.append(name, control);
if (hint) {
const help = document.createElement("small");
help.className = "b07-spec__hint";
help.textContent = hint;
wrap.append(help);
}
return wrap;
}
function select(
choices: readonly string[],
current: unknown,
autoLabel: string,
): HTMLSelectElement {
const el = document.createElement("select");
el.className = "b07-spec__input";
// 첫 보기가 **빈 값** — 「정한 적 없음」이 고를 수 있는 상태여야 한다.
const blank = document.createElement("option");
blank.value = "";
blank.textContent = autoLabel;
el.append(blank);
for (const choice of choices) {
const option = document.createElement("option");
option.value = choice;
option.textContent = choice;
el.append(option);
}
el.value = current == null ? "" : String(current);
return el;
}
/**
* 표준도 제원 폼. `onSave` 는 서버에 넘길 값을 받는다 — 빈 문자열은 **그 칸을 지우라**는 뜻.
*/
export function buildStandardSpecPanel(
sheet: StandardSheetSpec,
judgedSlope: string | null,
onSave: (result: StandardSpecResult) => Promise<string[]>,
initialNotes: string[] = [],
): HTMLDivElement {
const panel = document.createElement("div");
panel.className = "b07-spec ui-sidebar-section";
const title = document.createElement("h3");
title.className = "b07-spec__title";
title.textContent = `제원 — ${sheet.title}`;
const scope = document.createElement("p");
scope.className = "b07-spec__scope";
scope.textContent = `이 장의 ${sheet.member_count}개소에 함께 걸립니다.`;
panel.append(title, scope);
if (!STONE_TYPES.has(sheet.type_id)) {
const none = document.createElement("p");
none.className = "b07-spec__scope";
none.textContent = "이 종류는 표준도에서 받는 제원 칸이 아직 없습니다.";
panel.append(none);
return panel;
}
const options = sheet.options ?? {};
const kind = select(STONE_KINDS, options.stone_kind, "— 안 정함 —");
const supply = select(SUPPLIES, options.stone_supply, "— 안 정함(기본 채집) —");
const back = select(BACK_LENGTHS, options.back_len_cm, "— 안 정함 —");
const slope = document.createElement("input");
slope.className = "b07-spec__input";
slope.type = "text";
slope.inputMode = "decimal";
slope.placeholder = "비우면 자동";
slope.value = options.face_slope_ratio == null ? "" : String(options.face_slope_ratio);
panel.append(
field("돌 종류", kind),
field("조달", supply, "비우면 「캔다」로 봅니다."),
field("뒷길이 (㎝)", back, "품셈 일곱 규격 밖이면 물량이 서지 않습니다."),
field(
"전면 기울기 1:n",
slope,
judgedSlope ? `비우면 자동 — 지금 판정값 1:${judgedSlope}` : "비우면 자동으로 판정합니다.",
),
);
const notes = document.createElement("ul");
notes.className = "b07-spec__notes";
// ⚠ 저장하면 표·그림을 다시 받으면서 **이 폼이 통째로 새로 그려진다** — 그때 안내가
// 지워지지 않게 밖에서 들고 있다가 다시 넣는다(2026-09-09 실화면에서 잡음).
const showNotes = (messages: string[]): void => {
notes.replaceChildren(
...messages.map((text) => {
const item = document.createElement("li");
item.textContent = text;
return item;
}),
);
notes.hidden = messages.length === 0;
};
showNotes(initialNotes);
const save = document.createElement("button");
save.type = "button";
save.className = "b07-spec__save";
save.textContent = "제원 저장";
save.addEventListener("click", () => {
void (async () => {
save.disabled = true;
save.textContent = "저장 중…";
try {
showNotes(
await onSave({
sheet_key: sheet.key,
stone_kind: kind.value || null,
stone_supply: supply.value || null,
back_len_cm: back.value || null,
face_slope_ratio: slope.value.trim() || null,
}),
);
} catch (error) {
// 실패도 같은 자리에 적는다 — 조용히 끝나면 사용자는 저장된 줄 안다.
showNotes([error instanceof Error ? error.message : "제원을 저장하지 못했습니다."]);
} finally {
save.disabled = false;
save.textContent = "제원 저장";
}
})();
});
panel.append(save, notes);
return panel;
}