Files
Aislo/B07_DesignDetail/B07_DesignDetail_UI_StandardSpec.ts
T
eomsangdonandClaude Opus 5 af139d65cd feat(b07): 버림 콘크리트 끄는 칸 · 물빼기 주석 구조물 값 · 표준도 [여기서 고치기]
확정 8-2·14 — 버림 콘크리트 「넣음/안 넣음」 칸을 등록부(돌쌓기 찰·메·큰돌쌓기·기슭막이)와
표준도 입력(EDITABLE_KEYS)·제원 폼에 세움. 기본은 넣음(KDS 44 90 00 100㎜).
셈은 이미 wants_blinding 으로 서 있었고 끄는 자리만 없던 것임.

확정 5차 작은 것 2 — 표준도 그림 물빼기 주석이 기본 상수를 적던 것을
구조물 제원(weep_hole_*)에서 읽게 바꿈. 수량표와 같은 함수(weep_hole_spec)를 씀.

확정 6 — 표준도 장에서만 뜨는 [여기서 고치기] 단추를 고정 액션 줄에 둠.
표는 CAD 안이라 단추를 못 달고, 제원 폼은 도면 목록 아래라 밀림.
값 복사본은 만들지 않음 — 폼 한 벌이 그대로 정본 창구임.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
2026-09-13 09:49:17 +09:00

229 lines
9.6 KiB
TypeScript
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.
/* =============================================================================
* 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;
/** 정본 xls 탭 제목 그대로 — 터파기 기초 몫 0.45 대 0.07 을 가르는 축. */
const FOUNDATIONS = ["기초유", "기초버림"] as const;
/** 확정 ⑨ — 품셈 열이 기본, 실무 관행(깬돌 열)으로 바꿀 수 있게. */
const COEFF_BASES = ["품셈", "실무 관행"] as const;
/** 확정 2차 ⑩ — 기본 210. 180 은 국가기준 하한(돌쌓기 전용). */
const FILL_MPA = ["180", "210"] as const;
/** 확정 8-2·14 — 기본은 넣음(KDS 100㎜), 빼고 싶으면 「안 넣음」. 문구가 곧 서버가 읽는 말이다. */
const BLINDINGS = ["넣음", "안 넣음"] 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;
foundation: string | null;
stone_coeff_basis: string | null;
fill_concrete_mpa: string | null;
thickness_top_m: string | null;
thickness_bottom_m: string | null;
blinding_concrete: string | null;
}
/** 이 종류가 돌쌓기 계열인가 — 옹벽·집수정에는 이 칸들이 뜻이 없다. */
const STONE_TYPES = new Set(["masonry_wet", "masonry_dry", "boulder_masonry"]);
/** 벽 두께 칸을 받는 종류 — **큰돌쌓기는 두께 식이 달라** 등록부에 칸이 없다(2026-09-09). */
const THICKNESS_TYPES = new Set(["masonry_wet", "masonry_dry"]);
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 foundation = select(FOUNDATIONS, options.foundation, "— 안 정함 —");
const coeff = select(COEFF_BASES, options.stone_coeff_basis, "— 안 정함(품셈) —");
const mpa = select(FILL_MPA, options.fill_concrete_mpa, "— 안 정함(210) —");
const blinding = select(BLINDINGS, options.blinding_concrete, "— 안 정함(넣음) —");
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);
// 벽 두께 — 비우면 실무 구조물도 식(상부 = 뒷길이 + 0.30 · 하부 = 상부 + 0.30×(H1)).
const thickness = (key: "thickness_top_m" | "thickness_bottom_m"): HTMLInputElement => {
const input = document.createElement("input");
input.className = "b07-spec__input";
input.type = "text";
input.inputMode = "decimal";
input.placeholder = "비우면 식";
input.value = options[key] == null ? "" : String(options[key]);
return input;
};
const topThickness = thickness("thickness_top_m");
const bottomThickness = thickness("thickness_bottom_m");
panel.append(
field("돌 종류", kind),
field("조달", supply, "비우면 「캔다」로 봅니다."),
field("뒷길이 (㎝)", back, "품셈 일곱 규격 밖이면 물량이 서지 않습니다."),
field("기초", foundation, "터파기 몫이 갈립니다 — 기초유 0.45 · 기초버림 0.07 ㎥/m."),
field(
"전면 기울기 1:n",
slope,
judgedSlope ? `비우면 자동 — 지금 판정값 1:${judgedSlope}` : "비우면 자동으로 판정합니다.",
),
field("야면석 계수", coeff, "비우면 품셈 열을 씁니다."),
field("채움 강도 (MPa)", mpa, "비우면 210. 180 은 국가기준 하한입니다."),
field("버림 콘크리트", blinding, "비우면 넣습니다(두께 100㎜) — 빼려면 「안 넣음」."),
);
if (THICKNESS_TYPES.has(sheet.type_id)) {
panel.append(
field("상부 두께 (m)", topThickness, "비우면 뒷길이 + 0.30 으로 섭니다."),
field("하부 두께 (m)", bottomThickness, "비우면 상부 + 0.30×(H1) 로 섭니다."),
);
}
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,
foundation: foundation.value || null,
stone_coeff_basis: coeff.value || null,
fill_concrete_mpa: mpa.value || null,
blinding_concrete: blinding.value || null,
thickness_top_m: THICKNESS_TYPES.has(sheet.type_id)
? topThickness.value.trim() || null
: null,
thickness_bottom_m: THICKNESS_TYPES.has(sheet.type_id)
? bottomThickness.value.trim() || null
: null,
}),
);
} catch (error) {
// 실패도 같은 자리에 적는다 — 조용히 끝나면 사용자는 저장된 줄 안다.
showNotes([error instanceof Error ? error.message : "제원을 저장하지 못했습니다."]);
} finally {
save.disabled = false;
save.textContent = "제원 저장";
}
})();
});
panel.append(save, notes);
return panel;
}