법정 위반 표시가 없던 유일한 자리였음(평면 곡선반경·성토사면 길이는 이미 있음). 절토 경사비는 표준 횡단면 설정에서 오는 입력값일 뿐이라 범위를 벗어나도 아무 표시가 없었음. - 기준 — 별표2(지식DB `01_임도/02_상세설계/절토_비탈면.md` §1): 경암 1:0.3~0.8 · 연암 1:0.5~1.2 · 토사 1:0.8~1.5. **작업임도는 규정 없음 → 검사 제외.** - 판정은 **구간별 경사**(`cut_slope_segments`)로 함. 소단이 서면 실효 경사가 완만해져 **위반이 사라진 것처럼** 보임(폭 1.0·간격 2 이면 1:1 이 1:1.71). 그 함정을 시험으로 못박음. - 표시는 **기존 방식 그대로** — 성토사면·미폐합 경고와 같은 자리·같은 클래스. 새 방식 안 만듦. - 지반유형(리핑암·발파암) → 별표2 줄 매핑은 법령 근거가 아니라 **프로그램 설정**이라 표준 횡단면 설정에서 **사용자가 고르게** 함(2026-09-07 사용자 확정). 기본값 리핑암 → 연암 · 발파암 → 경암. 표준단면과 함께 저장돼 이미 사용자 값 계통임. - 기준값은 서버가 컨텍스트로 내려보냄 — 화면에 상수를 복제하지 않음. 시험 7건 — 별표2 값 · 작업임도 제외 · 매핑이 기본값일 뿐 · 구간별 판정 · **소단이 있어도 위반이 안 사라짐** · 경계값 통과 · 카드에 같은 방식으로 붙는지. ⚠ 실화면 — 사용자 선택 칸은 섰음(리핑암 연암 · 발파암 경암). 다만 이 프로젝트 저장분 63측점에 `cut_slope_segments` 가 아직 없어(오늘 새로 생긴 키) 경고가 뜨는 것은 못 봄. 재계산이 한 번 돌면 채워짐 — 지금 경고 0건은 맞는 동작임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
526 lines
20 KiB
TypeScript
526 lines
20 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Standard_Panel.ts
|
|
* 좌측 사이드 "표준 횡단면 설정" 패널 — **「표준횡단면 상세값」 한 컨테이너**(2026-09-04
|
|
* 사용자 지시). 토사·암·포장 3그룹이 같은 필드를 각각 갖던 화면을 공통 한 벌 +
|
|
* 구간별로 다른 값만 남겼다. 공통 = 노폭·노견·측구·절토/성토 경사·횡단 경사,
|
|
* 암 = 절토 경사·L형 측구, 포장 = 횡단 경사.
|
|
*
|
|
* **저장 구조는 그대로 3그룹**(`standard_cross_section`) — 화면만 합치고 저장할 때
|
|
* 공통값을 세 그룹에 펼쳐 넣는다. 백엔드·기존 프로젝트가 그대로 동작한다.
|
|
*
|
|
* 각 그룹의 노폭·노견·측구 규격·경사값을 편집한다. 기본값은 백엔드 config
|
|
* (STANDARD_CROSS_SECTION, context.standard_cross_section)에서 내려오고, 사용자가
|
|
* 편집한 값은 프론트 세션(sessionStorage)에 프로젝트 단위로 보관한다. 종·횡단
|
|
* 확정 시 이 값을 백엔드/DB에 저장하기 위해 getValues()를 노출한다.
|
|
*
|
|
* 암 그룹은 일반 측구 + L형 측구 두 세트를 보관하며, 둘 중 선택은 각 횡단면도
|
|
* 카드에서 이뤄진다(여기서는 값만 보관).
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
clearState,
|
|
readState,
|
|
readStateRaw,
|
|
writeState,
|
|
writeStateRaw,
|
|
} from "../A00_Common/b_page_state";
|
|
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
|
import { createButton, createInputField, createSelectField } from "@ui/ui_template_elements";
|
|
import { applyCutSlopeClassChoice } from "./B06_Section_Cut_Slope_Check";
|
|
import { buildStandardDiagram } from "./B06_Section_UI_Standard_Diagram";
|
|
import {
|
|
getCompanyStandard,
|
|
listCompanyStandards,
|
|
type StandardCrossGroup,
|
|
type StandardCrossKey,
|
|
type StandardCrossSection,
|
|
} from "./B06_Section_Api_Fetch";
|
|
|
|
function L(key: keyof typeof ui_locales): string {
|
|
return ui_locales[key][currentLanguageIndex];
|
|
}
|
|
|
|
/**
|
|
* 서버가 내려 준 표준횡단 **config 기본값**을 세션에 둔다(`sections/context` 응답).
|
|
*
|
|
* 횡단 계산이 브라우저에서 돌므로 config 수치가 프론트에도 있어야 하는데, 상수를
|
|
* 복제하면 정본이 둘이 된다. 그래서 복제 대신 **서버가 준 값을 그대로 기억**한다.
|
|
* 패널을 열지 않는 B05도 이 값으로 계산해야 두 화면 결과가 같다(2026-09-03 로컬 전환).
|
|
*/
|
|
export function rememberStandardDefaults(projectId: string, defaults: StandardCrossSection): void {
|
|
writeState("std-cross-default", defaults, projectId);
|
|
}
|
|
|
|
/** 기억해 둔 config 기본값. 아직 컨텍스트를 못 받았으면 null. */
|
|
export function readStandardDefaults(projectId: string): StandardCrossSection | null {
|
|
return readState<StandardCrossSection>("std-cross-default", projectId);
|
|
}
|
|
|
|
/**
|
|
* 암반 경계선 기본 오프셋(config `STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M`)을 기억한다.
|
|
* 저장분에 경계 오프셋이 없는 옛 암 측점을 서버와 **같은 기본값**으로 다시 계산하려면
|
|
* 브라우저에도 이 값이 있어야 한다 — 상수 복제 대신 컨텍스트 응답을 기억하는 방식이다.
|
|
*/
|
|
export function rememberRockBoundaryDefault(projectId: string, offsetM: number): void {
|
|
writeStateRaw("rock-boundary-default", String(offsetM), projectId);
|
|
}
|
|
|
|
/** 기억해 둔 암반 경계 기본 오프셋(m). 없으면 null. */
|
|
export function readRockBoundaryDefault(projectId: string): number | null {
|
|
const raw = readStateRaw("rock-boundary-default", projectId);
|
|
const parsed = raw === null ? Number.NaN : Number(raw);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
|
|
/**
|
|
* 횡단 계산에 넣을 표준단면 한 벌 — **세션 편집값이 있으면 그쪽, 없으면 config 기본값**.
|
|
* 서버 `_resolve_group(preset, standard)`의 "요청값 → config" 우선순위와 같은 뜻이다.
|
|
*/
|
|
export function effectiveStandardCross(projectId: string): StandardCrossSection | null {
|
|
return readStandardCrossSession(projectId) ?? readStandardDefaults(projectId);
|
|
}
|
|
|
|
/** 표준횡단 세션 편집값을 버린다 — B05 [초기화]가 부른다(정의처를 여기 하나로 둔다). */
|
|
export function clearStandardCrossSession(projectId: string): void {
|
|
clearState("std-cross", projectId);
|
|
}
|
|
|
|
/** config 기본값을 깊은 복사해 편집용 초기 상태로 만든다. */
|
|
function cloneDefaults(defaults: StandardCrossSection): StandardCrossSection {
|
|
return JSON.parse(JSON.stringify(defaults)) as StandardCrossSection;
|
|
}
|
|
|
|
/**
|
|
* 세션에 저장된 편집값을 읽는다. 없거나 손상 시 null.
|
|
*
|
|
* 패널 밖에서도 필요하다 — 횡단 재계산 단일 창구(`B06_Section_Cross_Refresh`)가 B05처럼
|
|
* 패널이 없는 화면에서도 **같은 표준 단면값**을 서버로 보내야 두 화면 결과가 같다.
|
|
*/
|
|
export function readStandardCrossSession(projectId: string): StandardCrossSection | null {
|
|
return readState<StandardCrossSection>("std-cross", projectId);
|
|
}
|
|
|
|
function writeSession(projectId: string, value: StandardCrossSection): void {
|
|
writeState("std-cross", value, projectId);
|
|
}
|
|
|
|
export interface StandardPanelController {
|
|
root: HTMLElement;
|
|
/** 확정 시 백엔드/DB 저장에 쓰는 현재 편집값. */
|
|
getValues: () => StandardCrossSection;
|
|
/**
|
|
* DB에 확정 저장된 값으로 복원한다. 단, 세션에 미확정 편집값이 있으면
|
|
* 그쪽을 우선하고 무시한다(진행 중 편집 보호).
|
|
*/
|
|
applyStored: (stored: StandardCrossSection) => void;
|
|
}
|
|
|
|
/** 값을 어느 그룹에 쓸 것인가. `common` 은 세 그룹에 함께 펼쳐 넣는다. */
|
|
type FieldScope = "common" | "rock" | "paved";
|
|
|
|
interface NumberFieldSpec {
|
|
label: keyof typeof ui_locales;
|
|
scope: FieldScope;
|
|
/** 화면에 보일 값을 읽는다 — 공통은 **토사 값이 기준**(2026-09-04 사용자 확정). */
|
|
get: (group: StandardCrossGroup) => number;
|
|
set: (group: StandardCrossGroup, value: number) => void;
|
|
/**
|
|
* 공통값이지만 이 그룹은 따로 값을 갖는다 — 공통을 펼칠 때 건너뛴다.
|
|
* (절토 경사는 암이, 횡단 경사는 포장이 자기 값을 쓴다)
|
|
*/
|
|
exclude?: StandardCrossKey;
|
|
}
|
|
|
|
/** 한 컨테이너에 늘어놓을 편집 필드. 순서 = 화면 표기 순서. */
|
|
const FIELD_SPECS: NumberFieldSpec[] = [
|
|
{
|
|
label: "B06_Std_Field_RoadWidth",
|
|
scope: "common",
|
|
get: (g) => g.road_width_m,
|
|
set: (g, v) => (g.road_width_m = v),
|
|
},
|
|
{
|
|
label: "B06_Std_Field_ShoulderLeft",
|
|
scope: "common",
|
|
get: (g) => g.shoulder_left_m,
|
|
set: (g, v) => (g.shoulder_left_m = v),
|
|
},
|
|
{
|
|
label: "B06_Std_Field_ShoulderRight",
|
|
scope: "common",
|
|
get: (g) => g.shoulder_right_m,
|
|
set: (g, v) => (g.shoulder_right_m = v),
|
|
},
|
|
{
|
|
label: "B06_Std_Field_DitchTop",
|
|
scope: "common",
|
|
get: (g) => g.ditch.top_width_m,
|
|
set: (g, v) => (g.ditch.top_width_m = v),
|
|
},
|
|
{
|
|
label: "B06_Std_Field_DitchBottom",
|
|
scope: "common",
|
|
get: (g) => g.ditch.bottom_width_m,
|
|
set: (g, v) => (g.ditch.bottom_width_m = v),
|
|
},
|
|
{
|
|
label: "B06_Std_Field_DitchDepth",
|
|
scope: "common",
|
|
get: (g) => g.ditch.depth_m,
|
|
set: (g, v) => (g.ditch.depth_m = v),
|
|
},
|
|
{
|
|
// 암은 아래에서 자기 절토 경사를 따로 가진다 — 공통은 토사·포장 몫이다.
|
|
label: "B06_Std_Field_CutSlope",
|
|
scope: "common",
|
|
exclude: "rock",
|
|
get: (g) => g.cut_slope_ratio,
|
|
set: (g, v) => (g.cut_slope_ratio = v),
|
|
},
|
|
{
|
|
label: "B06_Std_Field_FillSlope",
|
|
scope: "common",
|
|
get: (g) => g.fill_slope_ratio,
|
|
set: (g, v) => (g.fill_slope_ratio = v),
|
|
},
|
|
{
|
|
// 포장은 아래에서 자기 횡단 경사를 따로 가진다.
|
|
label: "B06_Std_Field_CrossSlopeMin",
|
|
scope: "common",
|
|
exclude: "paved",
|
|
get: (g) => g.cross_slope_pct.min,
|
|
set: (g, v) => (g.cross_slope_pct.min = v),
|
|
},
|
|
{
|
|
label: "B06_Std_Field_CrossSlopeMax",
|
|
scope: "common",
|
|
exclude: "paved",
|
|
get: (g) => g.cross_slope_pct.max,
|
|
set: (g, v) => (g.cross_slope_pct.max = v),
|
|
},
|
|
{
|
|
label: "B06_Std_Field_CutSlope",
|
|
scope: "rock",
|
|
get: (g) => g.cut_slope_ratio,
|
|
set: (g, v) => (g.cut_slope_ratio = v),
|
|
},
|
|
{
|
|
label: "B06_Std_Field_LDitchWidth",
|
|
scope: "rock",
|
|
get: (g) => g.ditch_l_type?.width_m ?? 0,
|
|
set: (g, v) => {
|
|
g.ditch_l_type = { width_m: v, depth_m: g.ditch_l_type?.depth_m ?? 0 };
|
|
},
|
|
},
|
|
{
|
|
label: "B06_Std_Field_LDitchDepth",
|
|
scope: "rock",
|
|
get: (g) => g.ditch_l_type?.depth_m ?? 0,
|
|
set: (g, v) => {
|
|
g.ditch_l_type = { width_m: g.ditch_l_type?.width_m ?? 0, depth_m: v };
|
|
},
|
|
},
|
|
{
|
|
label: "B06_Std_Field_CrossSlopeMin",
|
|
scope: "paved",
|
|
get: (g) => g.cross_slope_pct.min,
|
|
set: (g, v) => (g.cross_slope_pct.min = v),
|
|
},
|
|
{
|
|
label: "B06_Std_Field_CrossSlopeMax",
|
|
scope: "paved",
|
|
get: (g) => g.cross_slope_pct.max,
|
|
set: (g, v) => (g.cross_slope_pct.max = v),
|
|
},
|
|
];
|
|
|
|
/** 공통 필드 하나를 그룹들에 펼쳐 넣는다(자기 값을 갖는 그룹은 건너뛴다). */
|
|
function spread(state: StandardCrossSection, spec: NumberFieldSpec, value: number): void {
|
|
for (const key of ["soil", "rock", "paved"] as StandardCrossKey[]) {
|
|
if (spec.exclude === key) continue;
|
|
const group = state[key];
|
|
if (group) spec.set(group, value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 그룹마다 공통값이 다르게 저장돼 있을 수 있다(옛 프로젝트) — **토사 값을 기준**으로
|
|
* 한 벌로 맞춘다(2026-09-04 사용자 확정). 화면은 값 하나를 보이는데 저장분이 셋으로
|
|
* 갈려 있으면 어느 값이 나갔는지 알 수 없기 때문이다.
|
|
*/
|
|
function unifyCommon(state: StandardCrossSection): void {
|
|
const soil = state.soil;
|
|
if (!soil) return;
|
|
for (const spec of FIELD_SPECS) {
|
|
if (spec.scope !== "common") continue;
|
|
spread(state, spec, spec.get(soil));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 표준 횡단면 설정 패널을 만든다.
|
|
* @param projectId 세션 캐시 스코프.
|
|
* @param defaults config에서 내려온 기본값(복원 기준).
|
|
*/
|
|
export function createStandardPanel(
|
|
projectId: string,
|
|
defaults: StandardCrossSection,
|
|
onApplyAll?: () => void | Promise<void>,
|
|
): StandardPanelController {
|
|
// 브라우저 횡단 계산이 패널 없이도 config 기본값을 쓸 수 있게 먼저 기억해 둔다.
|
|
rememberStandardDefaults(projectId, defaults);
|
|
// 세션값 우선, 없으면 config 기본값. defaults는 복원 기준으로 보존한다.
|
|
const sessionValue = readStandardCrossSession(projectId);
|
|
const hadSession = sessionValue !== null;
|
|
const state: StandardCrossSection = sessionValue ?? cloneDefaults(defaults);
|
|
|
|
const root = document.createElement("div");
|
|
root.className = "b06-std";
|
|
|
|
// 변수 위치 안내 모식도(고정 도형): 각 설정값이 횡단면 어느 위치인지 표시(작업 C-5).
|
|
root.append(buildStandardDiagram());
|
|
|
|
// 그룹 재구성(리셋 시) 편의를 위해 본문 컨테이너를 분리한다.
|
|
const body = document.createElement("div");
|
|
body.className = "b06-std__body";
|
|
|
|
const persist = (): void => writeSession(projectId, state);
|
|
|
|
/** 필드 한 칸. 공통은 토사 값을 보이고, 고치면 세 그룹에 함께 펼친다. */
|
|
const buildField = (spec: NumberFieldSpec, grid: HTMLElement): void => {
|
|
const source = spec.scope === "common" ? state.soil : state[spec.scope];
|
|
if (!source) return;
|
|
const field = createInputField({
|
|
label: L(spec.label),
|
|
type: "number",
|
|
value: String(spec.get(source)),
|
|
onInput: (raw) => {
|
|
const parsed = Number(raw);
|
|
if (!Number.isFinite(parsed)) return;
|
|
if (spec.scope === "common") spread(state, spec, parsed);
|
|
else spec.set(source, parsed);
|
|
persist();
|
|
},
|
|
});
|
|
field.input.step = "0.1";
|
|
field.input.min = "0";
|
|
grid.append(field.root);
|
|
};
|
|
|
|
/** 구분선 + 구간 이름 — 아래 값들이 그 구간에서만 쓰인다는 표시. */
|
|
const buildDivider = (labelKey: keyof typeof ui_locales): HTMLElement => {
|
|
const divider = document.createElement("p");
|
|
divider.className = "b06-std__divider";
|
|
divider.textContent = L(labelKey);
|
|
return divider;
|
|
};
|
|
|
|
const buildScope = (scope: FieldScope): HTMLElement => {
|
|
const grid = document.createElement("div");
|
|
grid.className = "b06-std__grid";
|
|
for (const spec of FIELD_SPECS) {
|
|
if (spec.scope === scope) buildField(spec, grid);
|
|
}
|
|
return grid;
|
|
};
|
|
|
|
/** 「표준횡단면 상세값」 한 컨테이너 — 공통 → 암 → 포장 순, 구분선으로 나눈다. */
|
|
const buildDetails = (): HTMLElement => {
|
|
const fieldset = document.createElement("details");
|
|
fieldset.className = "b06-std__group";
|
|
fieldset.open = true;
|
|
const legend = document.createElement("summary");
|
|
legend.className = "b06-std__legend";
|
|
legend.textContent = L("B06_Std_Detail_Title");
|
|
const note = document.createElement("p");
|
|
note.className = "b06-std__note";
|
|
note.textContent = L("B06_Std_LType_Note");
|
|
fieldset.append(
|
|
legend,
|
|
buildScope("common"),
|
|
buildDivider("B06_Std_Section_RockOnly"),
|
|
buildScope("rock"),
|
|
buildDivider("B06_Std_Section_PavedOnly"),
|
|
buildScope("paved"),
|
|
note,
|
|
);
|
|
return fieldset;
|
|
};
|
|
|
|
const renderBody = (): void => {
|
|
body.replaceChildren(buildDetails());
|
|
};
|
|
// 옛 저장분이 그룹마다 다른 공통값을 갖고 있으면 토사 기준으로 한 벌로 맞춘다.
|
|
unifyCommon(state);
|
|
persist();
|
|
renderBody();
|
|
|
|
/** 소스 표준값을 현재 상태에 전부 덮어쓴다(사용자가 "적용"을 눌렀을 때만 호출). */
|
|
const applyStandard = (source: StandardCrossSection): void => {
|
|
(Object.keys(source) as StandardCrossKey[]).forEach((key) => {
|
|
if (source[key]) state[key] = JSON.parse(JSON.stringify(source[key])) as StandardCrossGroup;
|
|
});
|
|
unifyCommon(state);
|
|
persist();
|
|
renderBody();
|
|
};
|
|
|
|
// 다른 프로젝트에서 설계값 불러오기: 선택·미리보기만으로는 현재 값이 바뀌지 않고,
|
|
// "적용" 버튼을 눌러야만 반영된다(작업 C-6).
|
|
const loader = buildProjectLoader(projectId, applyStandard);
|
|
|
|
const resetButton = createButton({
|
|
label: L("B06_Std_Reset"),
|
|
variant: "ghost",
|
|
onClick: () => {
|
|
const fresh = cloneDefaults(defaults);
|
|
(Object.keys(fresh) as StandardCrossKey[]).forEach((key) => {
|
|
state[key] = fresh[key];
|
|
});
|
|
unifyCommon(state);
|
|
persist();
|
|
renderBody();
|
|
},
|
|
});
|
|
const actions = document.createElement("div");
|
|
actions.className = "b06-std__actions";
|
|
// [전체 반영](N-2-1): 패널 수치를 design 보유 전 측점에 서버 재계산으로 일괄 반영한다.
|
|
// 측점별 버튼 선택값(지반/단면 등)은 보존하고 표준단면 수치만 갱신한다.
|
|
if (onApplyAll) {
|
|
actions.append(
|
|
createButton({
|
|
label: L("B06_Std_ApplyAll"),
|
|
variant: "filled",
|
|
onClick: () => void onApplyAll(),
|
|
}),
|
|
);
|
|
}
|
|
actions.append(resetButton);
|
|
|
|
// ── 절토 법정 기울기 판정 기준 (2026-09-07 사용자 확정) ──────────────────
|
|
// 별표2 는 암을 **경암·연암**으로 가르고 프로그램은 **리핑암·발파암**으로 가른다. 둘을
|
|
// 잇는 문장이 법령·교본에 없어 **프로그램 설정**으로 두고 여기서 고르게 한다. 기본값은
|
|
// 리핑암 → 연암 · 발파암 → 경암(자연스러운 읽기). 절토 경사비가 오는 자리 옆이라 여기 둔다.
|
|
const cutClassWrap = document.createElement("div");
|
|
cutClassWrap.className = "b06-std__cutclass";
|
|
const cutClassTitle = document.createElement("p");
|
|
cutClassTitle.className = "b06-std__cutclass-title";
|
|
cutClassTitle.textContent = "절토 법정 기울기 판정 기준 (별표2)";
|
|
cutClassTitle.title =
|
|
"별표2: 경암 1:0.3~0.8 · 연암 1:0.5~1.2 · 토사 1:0.8~1.5. 작업임도는 규정 없음.";
|
|
cutClassWrap.append(cutClassTitle);
|
|
const CUT_CLASS_OPTIONS = [
|
|
{ value: "soft_rock", text: "연암 (1:0.5~1.2)" },
|
|
{ value: "hard_rock", text: "경암 (1:0.3~0.8)" },
|
|
];
|
|
for (const [kind, label] of [
|
|
["ripping_rock", "리핑암"],
|
|
["blasting_rock", "발파암"],
|
|
] as const) {
|
|
const current =
|
|
state.cut_slope_class?.[kind] ?? (kind === "ripping_rock" ? "soft_rock" : "hard_rock");
|
|
const field = createSelectField({
|
|
label,
|
|
options: CUT_CLASS_OPTIONS,
|
|
value: current,
|
|
onChange: (value) => {
|
|
state.cut_slope_class = { ...(state.cut_slope_class ?? {}), [kind]: value };
|
|
persist();
|
|
applyCutSlopeClassChoice(state.cut_slope_class);
|
|
onApplyAll?.();
|
|
},
|
|
});
|
|
cutClassWrap.append(field.root);
|
|
}
|
|
// 처음 세울 때도 고른 값을 검사에 반영한다(세션에 남아 있던 선택 포함).
|
|
applyCutSlopeClassChoice(state.cut_slope_class);
|
|
|
|
// 횡단 반폭은 사이드의 **별도 컨테이너**로 뺐다(2026-08-23) — 여기는 표준단면 설정만.
|
|
root.append(body, cutClassWrap, loader, actions);
|
|
|
|
return {
|
|
root,
|
|
getValues: () => state,
|
|
applyStored: (stored) => {
|
|
if (hadSession) return; // 진행 중 세션 편집값이 우선.
|
|
(Object.keys(stored) as StandardCrossKey[]).forEach((key) => {
|
|
if (stored[key]) state[key] = JSON.parse(JSON.stringify(stored[key])) as StandardCrossGroup;
|
|
});
|
|
unifyCommon(state);
|
|
renderBody();
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* "다른 프로젝트에서 불러오기" 컨트롤. 같은 회사의 최근 프로젝트 5개를 보여주고,
|
|
* 사용자가 선택하면 그 프로젝트의 저장 설계값을 즉시 현재 설정(그룹 값)에 적용한다.
|
|
* 각 횡단면도 카드에서 사용자가 고른 버튼 옵션(지반유형·단면유형 등)은 건드리지 않는다
|
|
* — 이 값들은 측점별 design으로 별도 보관되며 패널 값과 독립이다.
|
|
*/
|
|
function buildProjectLoader(
|
|
projectId: string,
|
|
onApply: (source: StandardCrossSection) => void,
|
|
): HTMLElement {
|
|
const wrap = document.createElement("details");
|
|
wrap.className = "b06-std__loader";
|
|
const summary = document.createElement("summary");
|
|
summary.className = "b06-std__loader-summary";
|
|
summary.textContent = L("B06_Std_Load_Title");
|
|
wrap.append(summary);
|
|
|
|
const status = document.createElement("p");
|
|
status.className = "b06-std__loader-status";
|
|
status.textContent = L("B06_Std_Load_Loading");
|
|
|
|
const field = createSelectField({
|
|
label: L("B06_Std_Load_Select"),
|
|
options: [{ value: "", text: L("B06_Std_Load_Placeholder") }],
|
|
onChange: (value) => void onSelect(value),
|
|
});
|
|
field.root.hidden = true;
|
|
|
|
async function onSelect(sourceId: string): Promise<void> {
|
|
if (!sourceId) {
|
|
status.textContent = "";
|
|
return;
|
|
}
|
|
status.textContent = L("B06_Std_Load_Loading");
|
|
try {
|
|
const response = await getCompanyStandard(projectId, sourceId);
|
|
onApply(response.standard_cross_section);
|
|
status.textContent = L("B06_Std_Load_Applied");
|
|
} catch (error) {
|
|
// 404 = 해당 프로젝트에 저장된 설계값 없음(목록은 보유 여부 무관 최근 5개).
|
|
const message = error instanceof Error ? error.message : "";
|
|
status.textContent = message.includes("찾을 수 없")
|
|
? L("B06_Std_Load_None")
|
|
: L("B06_Std_Load_Failed");
|
|
}
|
|
}
|
|
|
|
// 같은 회사 최근 프로젝트 5개 비동기 로드 → 셀렉트 채우기.
|
|
void (async () => {
|
|
try {
|
|
const response = await listCompanyStandards(projectId);
|
|
if (!response.projects.length) {
|
|
status.textContent = L("B06_Std_Load_Empty");
|
|
return;
|
|
}
|
|
for (const project of response.projects) {
|
|
const option = document.createElement("option");
|
|
option.value = project.project_id;
|
|
option.textContent = project.name;
|
|
field.select.append(option);
|
|
}
|
|
field.root.hidden = false;
|
|
status.textContent = "";
|
|
} catch {
|
|
status.textContent = L("B06_Std_Load_Failed");
|
|
}
|
|
})();
|
|
|
|
wrap.append(status, field.root);
|
|
return wrap;
|
|
}
|