Merge remote-tracking branch 'origin/dev' into sub_laptop_7
This commit is contained in:
@@ -52,8 +52,6 @@ export interface SpecLinks {
|
||||
export interface SpecFormHooks {
|
||||
/** 자동 항목 값 글(빈 글 = 「—」). */
|
||||
auto?: (c: SpecContainer, item: SpecItem, state: SpecState) => string;
|
||||
/** 숫자 칸 빈 칸 안내. */
|
||||
hint?: (c: SpecContainer, item: SpecItem, state: SpecState) => string;
|
||||
onChange?: (c: SpecContainer, item: SpecItem) => void;
|
||||
/** 선택 컨테이너 개수를 밖에서 정함(54-1 배관 흙막이 = 추가 기슭막이(단)) — 수면 그만큼 늘 보임 ·
|
||||
* [추가] · [빼기] 없음 · 줄면 끝에서부터 가림(값은 남음 · 54-10) · null = 모름(저장된 것 그대로) ·
|
||||
@@ -61,7 +59,7 @@ export interface SpecFormHooks {
|
||||
count?: (c: SpecContainer) => number | null | undefined;
|
||||
/** 컨테이너 칸 끝에 덧붙이는 칸(54-27 배관 유출 「추가 흙막이(단)」 · 값은 저장 옵션 밖). */
|
||||
fields?: (c: SpecContainer, values: Record<string, string>) => HTMLElement[];
|
||||
/** 그림이 쓰는 값(54-13) — 고르기 칸이 비었거나 자동 규칙일 때 그 값 · 꼬리(「기본」 · 「자동」) ·
|
||||
/** 그림이 쓰는 값(54-13) — 자동 규칙일 때 그 값 · 꼬리(「자동」) ·
|
||||
* null = 없음. 보여 주기만(값 · 저장은 사용자가 고를 때만). `values` = 보이는 값(위 칸 반영). */
|
||||
shown?: (
|
||||
c: SpecContainer,
|
||||
@@ -79,6 +77,8 @@ export interface SpecForm {
|
||||
options: Record<string, string | number>,
|
||||
names?: SpecNames,
|
||||
groupNames?: Record<string, string>,
|
||||
/** 정의 기본값(저장 키 → 값) — 필수 상자 빈 고르기 칸이 받는 값(54-43). */
|
||||
defaults?: Record<string, string | number>,
|
||||
) => void;
|
||||
read: () => Record<string, string | number>;
|
||||
state: () => SpecState;
|
||||
@@ -100,6 +100,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
let state: SpecState = {};
|
||||
let names: SpecNames = {};
|
||||
let groupNames: Record<string, string> = {};
|
||||
let defaults: Record<string, string | number> = {};
|
||||
const recent = new Map<Instance, SplitRole[]>();
|
||||
const slots = new Map<string, HTMLElement>();
|
||||
const rows: Array<{ key: string; set: (item: SummaryItem | null) => void }> = [];
|
||||
@@ -128,9 +129,10 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
const drawn = choices.find((ch) => ch.value === now);
|
||||
// 그림이 쓰는 값은 빈 보기 자리에 꼬리를 달아 보임 — 고른 값과 구별(54-13)
|
||||
const name = drawn ? choiceLabel(c, item, drawn, names) : now;
|
||||
const blank = !tag ? "선택 안 함" : name ? `${name} (${tag})` : tag;
|
||||
const blank = !tag ? "" : name ? `${name} (${tag})` : tag;
|
||||
// 필수 상자 = 빈 보기 없음(54-43) · 선택 상자 빈 보기 = 빈 글
|
||||
select.replaceChildren(
|
||||
new Option(blank, ""),
|
||||
...(c.required && !tag ? [] : [new Option(blank, "")]),
|
||||
...choices.map((ch) => new Option(choiceLabel(c, item, ch, names), ch.value)),
|
||||
);
|
||||
// 선택지 밖 옛 값은 그 값만 남겨 보임(지운 값을 몰래 바꾸지 않음)
|
||||
@@ -145,8 +147,8 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
input.value = values[item.id] ?? "";
|
||||
return input;
|
||||
}
|
||||
// 기본값은 새 상자 칸에 실제 값으로만(54-39) — 빈 칸 「(기본)」 안내 없음
|
||||
const input = numberInput(String(item.step ?? 0.1), "0", hooks.hint?.(c, item, state) ?? "");
|
||||
// 빈 칸 = 그냥 빈 칸(54-43 · 자리표시 글 없음)
|
||||
const input = numberInput(String(item.step ?? 0.1), "0");
|
||||
if (item.kind === "auto") {
|
||||
// 보여 주는 칸(54-1) — 손 입력 칸 아님
|
||||
const shown = document.createElement("output");
|
||||
@@ -203,6 +205,16 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
const box = group(c.required ? title : `${title} ${index + 1}`);
|
||||
box.root.dataset.specContainer = c.id;
|
||||
box.root.dataset.specIndex = String(index); // 칸을 고친 뒤 다시 그려도 몇째 상자인지(54-30)
|
||||
// 필수 상자 빈 고르기 칸 = 정의 기본값(없으면 첫 보기)을 실제 값으로 — 저장도 그 값(54-43)
|
||||
if (c.required)
|
||||
for (const item of c.items) {
|
||||
if (item.kind !== "select" || values[item.id] || (item.by && !branchOf(item, values)))
|
||||
continue;
|
||||
const choices = choicesOf(item, values);
|
||||
const key = branchOf(item, values)?.key;
|
||||
const wanted = String((key ? defaults[key] : undefined) ?? item.default ?? "");
|
||||
values[item.id] = (choices.find((ch) => ch.value === wanted) ?? choices[0])?.value ?? "";
|
||||
}
|
||||
// 보이는 값 = 고른 값 + 그림이 쓰는 값(54-13) · 위 칸이 다른 갈래를 보이면 아래 칸은 빈 채로
|
||||
const view: Instance = { ...values };
|
||||
const tags = new Map<string, string>();
|
||||
@@ -320,8 +332,9 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
|
||||
return {
|
||||
root,
|
||||
write(next, options, nextNames = {}, nextGroups = {}) {
|
||||
write(next, options, nextNames = {}, nextGroups = {}, nextDefaults = {}) {
|
||||
spec = next;
|
||||
defaults = nextDefaults;
|
||||
names = nextNames;
|
||||
groupNames = nextGroups;
|
||||
state = readState(next, options);
|
||||
|
||||
@@ -41,13 +41,13 @@ import {
|
||||
type InletStructureKind,
|
||||
labeled,
|
||||
numberInput,
|
||||
optionalSelect,
|
||||
OUTLET_REVET_KEYS,
|
||||
stepper,
|
||||
WING_IN_KEYS,
|
||||
WING_OUT_KEYS,
|
||||
} from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import { revetChoiceRow } from "./B05_Profile_UI_Drainage_Revet_Choice";
|
||||
import { optionDefaults } from "./B05_Profile_UI_Structures_Fields";
|
||||
import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form";
|
||||
import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe";
|
||||
import {
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
boxOptionsIn,
|
||||
} from "../A00_Common/design/design_structure_box";
|
||||
import { FORD_SPEC, withLegacyGuards } from "../A00_Common/design/design_structure_ford";
|
||||
import { fordHeightHint, fordHeightM } from "../A00_Common/design/design_structure_ford_calc";
|
||||
import { fordHeightM } from "../A00_Common/design/design_structure_ford_calc";
|
||||
import {
|
||||
pipeDrawn,
|
||||
pipeExtraCount,
|
||||
@@ -216,8 +216,8 @@ export interface FacilityOptionsForm {
|
||||
definition?: {
|
||||
spec_names?: Record<string, string>;
|
||||
groups?: Array<{ id: string; name: string }>;
|
||||
/** 옵션 정의 기본값 — 빈 고르기 칸이 그림 값을 보임(54-13). */
|
||||
options?: Array<{ key: string; default: unknown }>;
|
||||
/** 옵션 정의 기본값 — 필수 상자 빈 고르기 칸이 받는 값(54-43). */
|
||||
options?: Array<{ key: string; default: string | number | null }>;
|
||||
} | null,
|
||||
) => void;
|
||||
/** 배관 명세 컨테이너 「구조물 조합」 잇기 자리 — 폼이 패널 그룹 값에 이음(PLAN 52-1). */
|
||||
@@ -289,7 +289,7 @@ export function createFacilityOptionsForm(
|
||||
return drawnMemo;
|
||||
};
|
||||
// 유출 「추가 흙막이(단)」(54-27) — 값 = 횡단 조정 단 수(같은 저장 자리) · 흙막이 상자 수가 따라감
|
||||
const tiersInput = numberInput("1", "0", "횡단 설계 뒤");
|
||||
const tiersInput = numberInput("1", "0");
|
||||
tiersInput.max = "9";
|
||||
const tiersRow = labeled("추가 흙막이(단)", stepper(tiersInput, 1, 0));
|
||||
tiersRow.dataset.specItem = "outlet.tiers";
|
||||
@@ -378,10 +378,6 @@ export function createFacilityOptionsForm(
|
||||
const fordNeed = (): number | null =>
|
||||
fordHeightM(fordFlow, Number(fordForm.state().ford?.[0]?.width ?? ""));
|
||||
const fordForm = createSpecForm(specLinks, {
|
||||
hint: (c, item, state) =>
|
||||
c.id === "ford" && item.id === "height"
|
||||
? fordHeightHint(fordFlow, Number(state.ford?.[0]?.width ?? ""))
|
||||
: "",
|
||||
onChange: (c, item) => {
|
||||
const need = c.id === "ford" ? fordNeed() : null;
|
||||
const height = fordForm.state().ford?.[0]?.height ?? "";
|
||||
@@ -406,7 +402,9 @@ export function createFacilityOptionsForm(
|
||||
// 나눈다(2026-08-30 사용자). 설치 측이 한쪽이면 그 칸만 뜬다.
|
||||
// 좌·우가 저장 채널(유입/유출) 중 어느 쪽인지는 측점 지형이 정하므로 이름표만
|
||||
// 바꿔 단다(`setRevetSideLabels`) — 값은 언제나 그 채널 키로 오간다.
|
||||
const revetSide = optionalSelect("— 안 정함 —", ["양쪽", "좌", "우"]);
|
||||
// 설치 측 = 필수 칸 — 빈 보기 없음(54-43 · 빈 옛 값 = 양쪽)
|
||||
const revetSide = document.createElement("select");
|
||||
revetSide.replaceChildren(...["양쪽", "좌", "우"].map((v) => new Option(v, v)));
|
||||
const revetTopRow = grid(labeled("설치 측", revetSide, "side"));
|
||||
const revetInlet = createRevetSideGroup(INLET_REVET_KEYS);
|
||||
const revetOutlet = createRevetSideGroup(OUTLET_REVET_KEYS);
|
||||
@@ -582,6 +580,8 @@ export function createFacilityOptionsForm(
|
||||
// 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7).
|
||||
const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name]));
|
||||
const names = definition?.spec_names ?? {};
|
||||
// 필수 상자 빈 고르기 칸이 받는 정의 기본값(54-43) — 옛 키는 열 때와 같은 길로 바꿈
|
||||
const defaults = optionDefaults(definition?.options ?? []);
|
||||
if (facility === "pipe") {
|
||||
// 정의 파일의 옛 이름 「흙막이 추가」 — 명세 이름 「흙막이」가 앞섬(54-1)
|
||||
const { soil_add: _old, ...pipeNames } = names;
|
||||
@@ -591,21 +591,31 @@ export function createFacilityOptionsForm(
|
||||
!["revet", "I", "L", "U"].includes(String(kind ?? ""))
|
||||
? { structure: kind }
|
||||
: null;
|
||||
pipeForm.write(PIPE_SPEC, options, pipeNames, groups);
|
||||
pipeForm.write(PIPE_SPEC, options, pipeNames, groups, defaults);
|
||||
}
|
||||
if (facility === "box_culvert") {
|
||||
// 본체 규격은 저장 안 함 — 폭 · 높이로 되찾음 · 옛 설치 「있음」 = 날개벽
|
||||
boxForm.write(BOX_SPEC, boxOptionsIn(options), names, groups);
|
||||
boxForm.write(BOX_SPEC, boxOptionsIn(options), names, groups, boxOptionsIn(defaults));
|
||||
for (const [id, value] of Object.entries(bodyOf(options)))
|
||||
boxForm.setValue("body", id, value);
|
||||
}
|
||||
if (facility === "ford_bridge")
|
||||
fordForm.write(FORD_SPEC, withLegacyGuards(options), names, groups);
|
||||
if (facility === "ford_pavement") paveForm.write(FORD_PAVEMENT_SPEC, options, names, groups);
|
||||
fordForm.write(
|
||||
FORD_SPEC,
|
||||
withLegacyGuards(options),
|
||||
names,
|
||||
groups,
|
||||
withLegacyGuards(defaults),
|
||||
);
|
||||
if (facility === "ford_pavement")
|
||||
paveForm.write(FORD_PAVEMENT_SPEC, options, names, groups, defaults);
|
||||
wingInFields.write(options);
|
||||
wingOutFields.write(options);
|
||||
revetSide.value = text("side");
|
||||
revetSide.value = text("side") || "양쪽";
|
||||
const spread = legacyRevetOptions(options);
|
||||
// 빈 형태 = 정의 기본값(54-43 · 필수 칸)
|
||||
for (const key of [INLET_REVET_KEYS.form, OUTLET_REVET_KEYS.form])
|
||||
if (!spread[key] && defaults.form) spread[key] = defaults.form;
|
||||
revetInlet.fields.write(spread);
|
||||
revetOutlet.fields.write(spread);
|
||||
syncVisibility();
|
||||
|
||||
@@ -159,16 +159,13 @@ export function putNumber(target: Record<string, string | number>, key: string,
|
||||
if (Number.isFinite(value) && value > 0) target[key] = Number(value.toFixed(1));
|
||||
}
|
||||
|
||||
/** 선택지 셀렉트 — **맨 앞에 빈 보기(「안 정함」)** 를 둠. 첫 보기를 미리 고르지 않음
|
||||
/** 선택지 셀렉트 — **맨 앞에 빈 보기(빈 글 · 54-43)** 를 둠. 첫 보기를 미리 고르지 않음
|
||||
* (2026-09-14 브레인 판정 · 옛 2026-08-17 「빈 항목 두지 않음」 걷음) — 고른 적 없는 첫 값이 저장돼
|
||||
* 확정으로 굳지 않게. 기본값이 있는 칸은 부르는 쪽이 값을 넣음(그것은 따로 차례). */
|
||||
export function optionalSelect(
|
||||
blankLabel: string,
|
||||
choices: ReadonlyArray<string>,
|
||||
): HTMLSelectElement {
|
||||
export function optionalSelect(choices: ReadonlyArray<string>): HTMLSelectElement {
|
||||
const element = document.createElement("select");
|
||||
element.replaceChildren(
|
||||
new Option(blankLabel, ""),
|
||||
new Option("", ""),
|
||||
...choices.map((choice) => new Option(choice, choice)),
|
||||
);
|
||||
return element;
|
||||
@@ -211,13 +208,6 @@ export interface RevetmentKeys {
|
||||
after?: string;
|
||||
}
|
||||
|
||||
/** 높이 칸 안내 — 관 벽은 비우면 관경 기준 최소 높이(엔진 `pipe_wall_min_height_m` · 함수로 받아
|
||||
* 볼 때마다 셈) · 빈 글이면 「설계자 입력」. 형태 · 길이 · 전후 기본값은 등록부가 시설이 생길 때
|
||||
* 칸에 실제 값으로 넣음(PLAN 46-7 · 제안값 단추 없음). */
|
||||
export interface RevetmentDefaults {
|
||||
height: string | (() => string);
|
||||
}
|
||||
|
||||
export const INLET_REVET_KEYS: RevetmentKeys = {
|
||||
form: "inlet_revet_form",
|
||||
length: "inlet_revet_length_m",
|
||||
@@ -249,7 +239,7 @@ export function createRevetSideGroup(keys: RevetmentKeys): RevetSideGroup {
|
||||
const kind = document.createElement("select");
|
||||
kind.replaceChildren(new Option("기슭막이", "기슭막이"));
|
||||
// 높이는 기본값 없음 — 설계자 입력(계획홍수위 + 여유고 · 2026-09-14 브레인 판정).
|
||||
const fields = createRevetmentFields(keys, { height: "" });
|
||||
const fields = createRevetmentFields(keys);
|
||||
// 치수 근거 글(사방기술교본)은 [?] 설명서 `구조물_기슭막이.md` 로 옮김(54-2)
|
||||
const slot = document.createElement("div");
|
||||
slot.className = "b05-structure__adjust-slot";
|
||||
@@ -282,24 +272,13 @@ export interface RevetmentFields {
|
||||
* (2026-08-17 사용자 지시 9·11). 재질과 표면처리는 "형태" 한 축(지시 1). 바닥
|
||||
* 보호공(돌붙임) 한 벌은 뺐다 — 기슭막이는 사면 공정이라 그 아래 돌붙임이 붙을
|
||||
* 자리가 아니다(2026-08-19 사용자 지시 1). */
|
||||
export function createRevetmentFields(
|
||||
keys: RevetmentKeys,
|
||||
defaults: RevetmentDefaults,
|
||||
): RevetmentFields {
|
||||
const wallFloor = (): string =>
|
||||
typeof defaults.height === "function" ? defaults.height() : defaults.height;
|
||||
export function createRevetmentFields(keys: RevetmentKeys): RevetmentFields {
|
||||
// 형태 = 필수 칸 — 빈 보기 없음(54-43 · 값은 부르는 쪽이 정의 기본값으로 채움)
|
||||
const form = document.createElement("select");
|
||||
form.replaceChildren(
|
||||
new Option("— 안 정함 —", ""),
|
||||
...REVET_FORMS.map((value) => new Option(value, value)),
|
||||
);
|
||||
form.replaceChildren(...REVET_FORMS.map((value) => new Option(value, value)));
|
||||
const length = keys.length ? numberInput("0.1", "0") : null;
|
||||
// 관 벽은 비우면 관경 기준 최소 높이로 섬(엔진) · 독립 기슭막이 높이는 「설계자 입력」.
|
||||
const heightHint = (): string =>
|
||||
wallFloor() ? `비우면 관경 기준 ${wallFloor()}` : "설계자 입력";
|
||||
const height = numberInput("0.1", "0", heightHint());
|
||||
// 관 벽 기준 높이는 관경을 따라감 — 칸을 볼 때마다 다시 셈.
|
||||
height.addEventListener("focus", () => (height.placeholder = heightHint()));
|
||||
// 빈 칸 = 그냥 빈 칸(54-43 · 자리표시 글 없음)
|
||||
const height = numberInput("0.1", "0");
|
||||
// 기준측점 전/후(2026-08-23 사용자) — 구조물 패널(D군)과 같은 연동:
|
||||
// 전 + 후 = 길이, 나중에 고친 쪽이 살아남고 반대쪽이 재배분된다.
|
||||
const before = keys.before && length ? numberInput("0.1", "0") : null;
|
||||
@@ -342,9 +321,9 @@ export function createRevetmentFields(
|
||||
const text = (key: string): string =>
|
||||
options[key] !== undefined ? String(options[key]) : "";
|
||||
form.value = text(keys.form);
|
||||
if (form.selectedIndex < 0) form.selectedIndex = 0;
|
||||
if (length && keys.length) length.value = text(keys.length);
|
||||
height.value = text(keys.height);
|
||||
height.placeholder = heightHint();
|
||||
if (before && keys.before) before.value = text(keys.before);
|
||||
if (after && keys.after) after.value = text(keys.after);
|
||||
syncSplit("length");
|
||||
@@ -476,7 +455,7 @@ interface WingFields {
|
||||
* 설치 "없음"이면 제원 칸을 접는다(기슭막이 보호공 치수와 같은 패턴).
|
||||
* 기본 제원(있음·1·2·45, 사용자 확정)은 등록부 기본값 — 시설이 생길 때 칸에 실제 값(PLAN 46-7). */
|
||||
export function createWingFields(title: string, keys: WingKeys): WingFields {
|
||||
const install = optionalSelect("— 안 정함 —", ["있음", "없음"]);
|
||||
const install = optionalSelect(["있음", "없음"]);
|
||||
const height = numberInput("0.1", "0");
|
||||
const length = numberInput("0.1", "0");
|
||||
const angle = numberInput("1", "0");
|
||||
|
||||
@@ -54,7 +54,8 @@ interface OptionShape {
|
||||
/**
|
||||
* 등록부 옵션 입력 한 칸 — **받은 값만 칸에 넣음**. 새 구조물은 부르는 쪽이 등록부 기본값을 값으로
|
||||
* 실어 줌(PLAN 46-7 · 제안값 단추 없음) · 옛 기록의 빈 칸은 빈 칸 그대로(미확정).
|
||||
* 고르기 칸은 늘 빈 보기를 둠 — 첫 보기를 미리 고르지 않음.
|
||||
* 필수 · 기본값 있는 고르기 칸 = 빈 보기 없음(54-43 · 빈 옛 값은 기본값 · 없으면 첫 보기) ·
|
||||
* 그 밖 고르기 칸 = 빈 글 빈 보기.
|
||||
*/
|
||||
export function optionControl(
|
||||
option: OptionShape,
|
||||
@@ -66,15 +67,16 @@ export function optionControl(
|
||||
// 툴팁 — 표가 아직 안 읽는 칸이면 그 사유가 먼저(넣어도 안 바뀜을 알아야 함), 아니면 비울 때의 뜻.
|
||||
const tip = option.not_in_table ?? option.empty_means ?? "";
|
||||
if (option.input === "select") {
|
||||
// 빈 보기 = 값 없음(54-39) — 기본값은 새 구조물 칸에 실제 값으로만(「(기본)」 꼬리 없음)
|
||||
const blank = option.required ? "— 선택 —" : "선택 안 함";
|
||||
const fallback = option.default === null || option.default === "" ? "" : String(option.default);
|
||||
const must = !!option.required || fallback !== "";
|
||||
// 정의에서 뺀 선택지(48)를 고른 옛 값 — 그 값만 남겨 보임(값을 잃지 않음)
|
||||
const kept = value && !option.choices.includes(value) ? [value] : [];
|
||||
const element = select([
|
||||
["", blank],
|
||||
...(must ? [] : [["", ""] as [string, string]]),
|
||||
...[...option.choices, ...kept].map((c) => [c, label(c)] as [string, string]),
|
||||
]);
|
||||
element.value = value;
|
||||
element.value = value || fallback;
|
||||
if (element.selectedIndex < 0) element.selectedIndex = 0;
|
||||
if (tip) element.title = tip;
|
||||
return element;
|
||||
}
|
||||
@@ -82,7 +84,6 @@ export function optionControl(
|
||||
option.input === "number" ? numberInput("0.1", "0") : document.createElement("input");
|
||||
if (option.input !== "number") element.type = "text";
|
||||
element.value = value;
|
||||
element.placeholder = option.required ? "필수 입력" : "";
|
||||
if (tip) element.title = tip;
|
||||
const limit = option.warn_above;
|
||||
if (typeof limit === "number") {
|
||||
|
||||
@@ -239,9 +239,7 @@ export function createStructuresSection(
|
||||
root.dataset.helpTopic = type ? `구조물_${type.name}` : "";
|
||||
// 정의가 기준 측점을 끈 종류(48-3) — 기준 측점이 곧 위치인 종류(점형 · 범위 계산)는 늘 보임
|
||||
anchorFields.wrap.hidden = isInterval && !showsAnchor(type!);
|
||||
anchorFields.wrap.querySelector("span")!.textContent = isInterval
|
||||
? "기준 측점 (비우면 시작)"
|
||||
: "기준 측점";
|
||||
anchorFields.wrap.querySelector("span")!.textContent = "기준 측점"; // 빈 칸 안내 없음(54-43)
|
||||
syncRangeDisplay();
|
||||
}
|
||||
|
||||
|
||||
@@ -250,14 +250,16 @@ function syncChoices(
|
||||
choices: string[],
|
||||
label: (choice: string) => string,
|
||||
): void {
|
||||
const now = [...element.children].slice(1).map((o) => (o as HTMLOptionElement).value);
|
||||
// 빈 보기는 선택 칸에만(54-43 · 필수 칸은 없음)
|
||||
const blank = [...element.options].filter((o) => o.value === "").slice(0, 1);
|
||||
const now = [...element.options].filter((o) => o.value !== "").map((o) => o.value);
|
||||
const keep = element.value && !choices.includes(element.value) ? [element.value] : [];
|
||||
const want = [...choices, ...keep];
|
||||
if (now.length === want.length && now.every((v, i) => v === want[i])) return;
|
||||
const value = element.value;
|
||||
const blank = element.children[0];
|
||||
element.replaceChildren(blank, ...want.map((c) => new Option(label(c), c)));
|
||||
element.replaceChildren(...blank, ...want.map((c) => new Option(label(c), c)));
|
||||
element.value = value;
|
||||
if (element.selectedIndex < 0) element.selectedIndex = 0;
|
||||
}
|
||||
|
||||
/** 「지난 정의 값」 상자(48-3) — 지금 정의에 없는 키의 값을 읽기만 칸으로 보임(오류 없음 · 저장은
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "../A00_Common/design/design_structure_form";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { specKeys, type StructureSpec } from "../A00_Common/design/design_structure_spec";
|
||||
import { optionDefaults } from "./B05_Profile_UI_Structures_Fields";
|
||||
|
||||
const forms = new WeakMap<HTMLElement, SpecForm>();
|
||||
|
||||
@@ -63,7 +64,7 @@ export function paintStructureSpec(
|
||||
form.root.hidden = !spec;
|
||||
if (!spec) return;
|
||||
const groups = Object.fromEntries((type?.groups ?? []).map((g) => [g.id, g.name]));
|
||||
form.write(spec, values, type?.spec_names ?? {}, groups);
|
||||
form.write(spec, values, type?.spec_names ?? {}, groups, optionDefaults(type?.options ?? []));
|
||||
}
|
||||
|
||||
/** 명세 폼 값(저장 키 그대로) — 명세 없는 종류면 빈 묶음. */
|
||||
|
||||
@@ -289,3 +289,9 @@ B05에서 3D 영역의 상단 버튼들 중 원근으로, 직교로 버튼의
|
||||
|
||||
(10-6 사용자)
|
||||
빈칸은 그냥 빈칸이야. 별도의 안내는 필요없어. 기본 값 XX 라 흐린 글씨로 작성하지 말고 기본값을 사용자가 넣은 것 처럼. 기본값을 입력하면 된다는 말이야. 그리고 필수로 집수정이낭 흙막이처럼 필수로 필요한 정보에는 선택안함 선택지 자체가 없어야해.
|
||||
|
||||
(10-6 사용자)
|
||||
기존 프로젝트에서 존재하는 예전 데이터 때문에 검증이 어려운것같은데 쉽게 데이터를 초기화하고 신규로직으로 적용되게 하는 방법은 없어?
|
||||
B 방향이 맞겠네. 새 프로젝트를 만드는건 문제없어보여. 이때 개별 워크트리들이 검증할수 있는 프로젝트를 각각지정해주는게 좋겠어. 그리고 추가로 엄상준과 박동철 프로젝트도 같이 만들어줘. 같은회사 검증자 들이야. 이 사람들도 본인의 프로젝트로 검증할거야.
|
||||
이전 프로젝트는 그냥 삭제해
|
||||
전부 다 손대도 상관없음 프로젝트 전역 초기
|
||||
|
||||
@@ -42,6 +42,12 @@ def test_TS_가_쓰는_칸이_파일에_있다() -> None:
|
||||
|
||||
|
||||
def test_B05_높이_칸은_제안_없이_설계자_입력() -> None:
|
||||
# 독립 기슭막이 높이 = 기본값 없음 · 「설계자 입력」 안내(제안값 단추 걷음 · PLAN 46-7)
|
||||
assert 'createRevetmentFields(keys, { height: "" })' in FIELDS and '"설계자 입력"' in FIELDS
|
||||
# 독립 기슭막이 높이 = 기본값 없음 · 빈 칸 그대로(54-43 · 자리표시 글 없음)
|
||||
assert (
|
||||
"createRevetmentFields(keys)" in FIELDS
|
||||
and "placeholder"
|
||||
not in FIELDS.split("createRevetmentFields(keys: RevetmentKeys)")[1].split(
|
||||
"createBasinSplitFields"
|
||||
)[0]
|
||||
)
|
||||
assert "기슭막이 높이 없음 — 근입만 그림" in CHROME
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* 54-43 — 구조물 배치 상자: 빈 칸 = 그냥 빈 칸 · 필수 칸 빈 보기 없음 · 빈 옛 값 = 기본값
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) 또는 B06 횡단설계(#/b06-section) · 검증 프로젝트(sub5_검증) · 로그인
|
||||
* 부르는 법: 한 번 — 결과 `sessionStorage.getItem("__5443_out")`
|
||||
* 목록의 구조물 전부를 차례로 열어 「구조물 배치」 상자 글 · 보기 글 · 자리표시 글을 셈
|
||||
* 쓰는 것: 없음(열기만 · 저장 없음)
|
||||
* 기대 수치: words = 0 · requiredBlank = 0 · blankText = 0 · requiredEmpty = 0 · optionalBlank > 0(선택 칸 빈 보기 그대로)
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const txt = (el) => el.innerText.replace(/\s+/g, " ").trim();
|
||||
const WORDS = /선택 안 함|안 정함|비우면|기본/;
|
||||
const OPTIONAL = new Set(["soil_add", "chute_add", "chute"]);
|
||||
const out = { rows: 0, words: [], requiredBlank: [], blankText: [], requiredEmpty: [], optionalBlank: 0, seen: {} };
|
||||
for (let i = 0; i < 50 && !document.querySelector(".b05-route__irregular-list li"); i++) await w(200);
|
||||
const rows = [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
for (const row of rows) {
|
||||
const head = txt(row).slice(0, 24);
|
||||
row.click();
|
||||
await w(1200);
|
||||
const panel = document.querySelector(".b05-structure-section");
|
||||
if (!panel) continue;
|
||||
out.rows += 1;
|
||||
const shown = (el) => !!el.offsetParent;
|
||||
// 보이는 글 · 고르기 보기 글 · 자리표시 글
|
||||
const texts = [
|
||||
txt(panel),
|
||||
...[...panel.querySelectorAll("select")].filter(shown).flatMap((s) => [...s.options].map((o) => o.text)),
|
||||
...[...panel.querySelectorAll("input")].filter(shown).map((i) => i.placeholder),
|
||||
];
|
||||
for (const t of texts) {
|
||||
const hit = t.match(WORDS);
|
||||
if (hit) out.words.push(`${head}: …${t.slice(Math.max(0, hit.index - 12), hit.index + 14)}…`);
|
||||
}
|
||||
for (const sel of [...panel.querySelectorAll("select")].filter(shown)) {
|
||||
const wrap = sel.closest("[data-spec-item]");
|
||||
const box = sel.closest("[data-spec-container]");
|
||||
const blank = [...sel.options].find((o) => o.value === "");
|
||||
const name = wrap?.dataset.specItem ?? sel.closest("[data-option-keys]")?.dataset.optionKeys ?? "?";
|
||||
out.seen[name] = (out.seen[name] ?? 0) + 1;
|
||||
if (blank && blank.text !== "" && !wrap?.dataset.specShown) out.blankText.push(`${head}: ${name} 「${blank.text}」`);
|
||||
if (box && !OPTIONAL.has(box.dataset.specContainer)) {
|
||||
if (blank && !wrap?.dataset.specShown) out.requiredBlank.push(`${head}: ${name}`);
|
||||
if (!sel.value && !wrap?.dataset.specShown) out.requiredEmpty.push(`${head}: ${name}`);
|
||||
} else if (blank) out.optionalBlank += 1;
|
||||
}
|
||||
}
|
||||
sessionStorage.setItem("__5443_out", JSON.stringify(out));
|
||||
})();
|
||||
Reference in New Issue
Block a user