From 7d5f516b84fd2c66191df43fb64de83c44c3eabf Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 27 Oct 2026 19:58:44 +0900 Subject: [PATCH] =?UTF-8?q?feat(E03):=20=EC=B8=A1=EC=A0=90=20No.+=EA=B1=B0?= =?UTF-8?q?=EB=A6=AC=20=C2=B7=20=EC=9C=84=ED=97=98=20=EB=9D=A0=20=ED=99=95?= =?UTF-8?q?=EB=8C=80=20=C2=B7=20=EC=9D=B4=EB=8F=99=20=C2=B7=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EA=B8=80=20=C2=B7=20900=20=EB=86=92=EC=9D=B4=20?= =?UTF-8?q?=EA=B2=B9=EC=B9=A8=20=C2=B7=20=EB=8C=80=ED=91=9C=20=EB=8B=A8?= =?UTF-8?q?=EB=A9=B4=20=EB=B0=94=EB=A1=9C=20=EC=A0=80=EC=9E=A5=20(PLAN=205?= =?UTF-8?q?66-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 좌측 목록 · 횡단 카드 제목 = 공용 stationLabel(누가, 측점 목록 추정 간격, 소수 한 자리) - 평면 위험 띠 = 공용 attachSvgZoomPan(휠 · 가운데 끌기 · 두 번 누름 원배율) · 측점 글 솎음(logic labelStride · planLabelKeys) · 선 굵기 · 글 크기 고정 · 배율은 다시 그려도 유지 - .e03-page 행 셋(auto auto minmax) — 화면 높이 900 에서 띠 · 본문 겹침 고침 - 대표 단면 별 = 누르면 바로 PUT representative · 뒤에서 E05 보고서 갱신 길(GET report · POST refresh) · [대표 단면 저장] 단추 · 캐시 뺌 - 시험 532_3 logic TS 에 솎음 걸음 · 글 열쇠 Co-Authored-By: Claude Opus 5.5 --- E03_StabSection/E03_StabSection_UI_List.ts | 7 +- E03_StabSection/E03_StabSection_UI_Page.ts | 110 +++++++------ E03_StabSection/E03_StabSection_UI_Plan.ts | 148 +++++++++++++----- E03_StabSection/E03_StabSection_UI_Style.css | 9 +- .../logic/E03_StabSection_Logic.ts | 25 +++ resources/tester/test_532_3_e03_section.py | 7 + ui_template/ui_template_locale_e03.ts | 3 - 7 files changed, 224 insertions(+), 85 deletions(-) diff --git a/E03_StabSection/E03_StabSection_UI_List.ts b/E03_StabSection/E03_StabSection_UI_List.ts index 9cfcf82ed..db51f4c4d 100644 --- a/E03_StabSection/E03_StabSection_UI_List.ts +++ b/E03_StabSection/E03_StabSection_UI_List.ts @@ -9,6 +9,7 @@ import { createIconButton } from "@ui/ui_template_button"; import { el } from "@ui/ui_template_elements"; import { createEmpty } from "@ui/ui_template_empty"; import { t, type LocaleKey } from "@ui/ui_template_locale"; +import { stationLabel } from "../common_util/common_util_station"; import { e03Table } from "./E03_StabSection_UI_Table"; import { listRows, @@ -29,7 +30,9 @@ export interface ListDraw { sort: SortMode; ngOnly: boolean; active: string | null; - /** 사용자 켬/끔 — 저장 전 캐시 포함 · 없으면 서버 값 */ + /** 측점 간격(m) — 측점 글 「No.+거리」 의 분모 */ + interval: number; + /** 사용자 켬/끔 — 서버 값(별 누름 = 바로 저장) */ representative: (row: StationRow) => { on: boolean; user: boolean | null }; editable: boolean; } @@ -81,7 +84,7 @@ export function buildList( ); verdict.classList.add("e03-verdict", `e03-verdict--${v}`); const station = el("span", { - text: row.label?.replace(/^STA\./, "").replace(/\.000$/, "") ?? row.key, + text: stationLabel(row.chainage_m, draw.interval, 1), children: row.walls ? [el("span", { className: "e03-wall-tag", text: "▦", attrs: { title: t("E03_Walls") } })] : [], diff --git a/E03_StabSection/E03_StabSection_UI_Page.ts b/E03_StabSection/E03_StabSection_UI_Page.ts index b7baa1711..4d5621753 100644 --- a/E03_StabSection/E03_StabSection_UI_Page.ts +++ b/E03_StabSection/E03_StabSection_UI_Page.ts @@ -24,7 +24,8 @@ import { t, type LocaleKey } from "@ui/ui_template_locale"; import { mountGroupStep } from "../A00_Common/group_workflow"; import { buildMissingBanner } from "../E01_StabRegister/E01_StabRegister_UI_Missing"; import { buildList } from "./E03_StabSection_UI_List"; -import { buildPlan } from "./E03_StabSection_UI_Plan"; +import { buildPlan, type PlanView } from "./E03_StabSection_UI_Plan"; +import { inferStationInterval, stationLabel } from "../common_util/common_util_station"; import { card, check, emptyBox, muted } from "./E03_StabSection_UI_Parts"; import { buildResult } from "./E03_StabSection_UI_Result"; import { @@ -84,7 +85,6 @@ async function api(path: string, init?: RequestInit): Promise { return body; } -const repKey = (projectId: string): string => `e03_representative_cache:${projectId}`; const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms)); export function renderE03StabSection(root: HTMLElement): void { @@ -111,9 +111,9 @@ export function renderE03StabSection(root: HTMLElement): void { const slopeRecs = new Map(); // 측점 열쇠 → 마지막 가정 const wallRecs = new Map(); const supportRecs = new Map(); - let repCache: Record = JSON.parse( - sessionStorage.getItem(repKey(pid)) ?? "{}", - ); + let interval = 20; // 측점 간격 — 측점 목록에서 추정(요구 390) + const planView: PlanView = {}; + let repSaving: Promise = Promise.resolve(); // 별 누름마다 차례로 저장 // ---- 좌측 ---- const condField = createSelectField({ @@ -158,15 +158,10 @@ export function renderE03StabSection(root: HTMLElement): void { variant: "ghost", onClick: () => void show3d(), }); - const repButton = createButton({ - label: t("E03_RepSave"), - variant: "ghost", - onClick: () => void saveRep(), - }); const controls = el("div", { className: "e03-panel__row" }); controls.append(condField.root, sortField.root); const actions = el("div", { className: "ui-sidebar-actions" }); - actions.append(repButton, view3dButton, analyzeButton); + actions.append(view3dButton, analyzeButton); panel.append(controls, ngBox, status, listHost, actions); // ---- 본문 ---- @@ -187,11 +182,8 @@ export function renderE03StabSection(root: HTMLElement): void { const repOf = (row: StationRow): { on: boolean; user: boolean | null } => { const server = row.representative ?? { auto: [], user: null, on: false }; - if (!(row.key in repCache)) return { on: server.on, user: server.user }; - const user = repCache[row.key]; - return { on: user === null ? server.auto.length > 0 : user, user }; + return { on: server.on, user: server.user }; }; - const repDirty = (): boolean => Object.keys(repCache).length > 0; const currentRow = (): StationRow | undefined => result?.stations.find((r) => r.key === active); @@ -217,10 +209,8 @@ export function renderE03StabSection(root: HTMLElement): void { if (!result.fresh) lines.push(t("E03_Stale")); } if (result && !result.editable) lines.push(t("E03_ReadOnly")); - if (repDirty()) lines.push(t("E03_RepUnsaved")); status.textContent = lines.join("\n"); analyzeButton.disabled = !result?.editable; - repButton.disabled = !result?.editable || !repDirty(); view3dButton.disabled = result?.status !== "ok"; }; @@ -235,22 +225,12 @@ export function renderE03StabSection(root: HTMLElement): void { sort, ngOnly, active, + interval, representative: repOf, editable: result.editable, }, (key) => void select(key), - (key) => { - const row = result!.stations.find((r) => r.key === key)!; - const now = repOf(row).on; - const auto = (row.representative?.auto.length ?? 0) > 0; - // 자동과 같아지면 사용자 값 지움(null) — 서버 값과 같으면 캐시에서 뺌 - const next = !now === auto ? null : !now; - if (next === (row.representative?.user ?? null)) delete repCache[key]; - else repCache[key] = next; - sessionStorage.setItem(repKey(pid), JSON.stringify(repCache)); - paintList(); - paintStatus(); - }, + (key) => void toggleRep(key), ), ); }; @@ -286,7 +266,7 @@ export function renderE03StabSection(root: HTMLElement): void { }); const title = el("h3", { className: "e03-card__title", - text: `${view.label ?? view.key} · ${t(`E03_Cond_${condition}` as LocaleKey)}`, + text: `${stationLabel(view.chainage_m, interval, 1)} · ${t(`E03_Cond_${condition}` as LocaleKey)}`, }); const flags = el("div", { className: "e03-panel__row" }); flags.append( @@ -399,7 +379,16 @@ export function renderE03StabSection(root: HTMLElement): void { const paintPlan = (): void => { planHost.replaceChildren( - result ? buildPlan(result.stations, condition, active, (key) => void select(key)) : el("div"), + result + ? buildPlan( + result.stations, + condition, + active, + interval, + planView, + (key) => void select(key), + ) + : el("div"), ); }; @@ -438,7 +427,10 @@ export function renderE03StabSection(root: HTMLElement): void { condition, ); const keys = result.stations.map((r) => r.key); - repCache = Object.fromEntries(Object.entries(repCache).filter(([k]) => keys.includes(k))); + interval = inferStationInterval( + result.stations.slice().sort((a, b) => a.chainage_m - b.chainage_m), + ); + delete planView.zoom; // 다시 읽음 = 띠 원배율 if (!active || !keys.includes(active)) { // 처음 = 그 조건 가장 낮은 안전율 측점 const worst = result.stations @@ -488,20 +480,50 @@ export function renderE03StabSection(root: HTMLElement): void { } } - async function saveRep(): Promise { - try { + /** 별 누름 = 바로 저장(요구 390) — 자동과 같아지면 사용자 값 지움(null) · 저장 뒤 보고서 갱신 */ + function toggleRep(key: string): Promise { + repSaving = repSaving.then(async () => { + const row = result?.stations.find((r) => r.key === key); + if (!row) return; + const server = row.representative ?? { auto: [], user: null, on: false }; + const auto = server.auto.length > 0; + const next = !server.on === auto ? null : !server.on; const overrides: Record = {}; - for (const row of result?.stations ?? []) { - const user = row.key in repCache ? repCache[row.key] : (row.representative?.user ?? null); - if (user !== null) overrides[row.key] = user; + for (const r of result?.stations ?? []) { + const user = r.key === key ? next : (r.representative?.user ?? null); + if (user !== null) overrides[r.key] = user; } - await api(`${base}/representative`, { method: "PUT", body: JSON.stringify({ overrides }) }); - repCache = {}; - sessionStorage.removeItem(repKey(pid)); - showToast(t("E03_RepSaved"), "success"); - await load(); - } catch (error) { - showToast(error instanceof Error ? error.message : t("Common_Status_Error"), "error"); + try { + await api(`${base}/representative`, { method: "PUT", body: JSON.stringify({ overrides }) }); + row.representative = { ...server, user: next, on: next === null ? auto : next }; + paintList(); + void refreshReport(); + } catch (error) { + showToast(error instanceof Error ? error.message : t("Common_Status_Error"), "error"); + } + }); + return repSaving; + } + + /** 보고서 갱신 — E05 [보고서 갱신] 과 같은 길(GET 이 작업본 없으면 처음 만듦 · 뒤에서) */ + let reportRun = 0; + async function refreshReport(): Promise { + const run = ++reportRun; + await sleep(800); // 별을 잇달아 누르면 마지막 한 번만 + if (run !== reportRun) return; + try { + const state = await api<{ book: unknown; rev: string | null }>( + `/projects/${pid}/stab/report`, + ); + if (!state.book || run !== reportRun) return; + const body = await api<{ overwritten?: string[] }>(`/projects/${pid}/stab/report/refresh`, { + method: "POST", + body: JSON.stringify({ rev: state.rev }), + }); + const over = body.overwritten ?? []; + if (over.length) showToast(`${t("E05_Overwritten")} ${over.join(" · ")}`, "warning", 8000); + } catch { + /* 보고서는 E05 를 열 때 stale 로 알림 */ } } diff --git a/E03_StabSection/E03_StabSection_UI_Plan.ts b/E03_StabSection/E03_StabSection_UI_Plan.ts index cb38e360c..cb1bacaea 100644 --- a/E03_StabSection/E03_StabSection_UI_Plan.ts +++ b/E03_StabSection/E03_StabSection_UI_Plan.ts @@ -3,14 +3,20 @@ * E03 위 평면 위험 띠(PLAN 532-3) — 측점 중심을 이은 노선을 그 조건 판정 색으로(이웃 측점 사이 * 가운데서 끊음 · 북쪽 위 · 같은 축척) · 고른 측점 표시 · 누르면 가장 가까운 측점. * 범례 = 판정마다 아이콘 + 글 + 측점 수. 계산은 logic `bandSegments` · `planFit`. + * 확대 · 이동 = 공용 `attachSvgZoomPan`(휠 · 가운데 끌기 · 두 번 누름 원배율 · 요구 390) · 측점 글 솎음 + * = logic `labelStride` · `planLabelKeys` · 배율 · 폭은 바깥 `PlanView` 가 쥠(다시 지어도 살아남음). * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; +import { attachSvgZoomPan, type ZoomPanState } from "../A00_Common/b_svg_zoom_pan"; +import { stationLabel } from "../common_util/common_util_station"; import { el } from "@ui/ui_template_elements"; import { t, type LocaleKey } from "@ui/ui_template_locale"; import { bandSegments, + labelStride, planFit, + planLabelKeys, verdictOf, type Condition, type Point, @@ -21,11 +27,20 @@ import { svgEl } from "./E03_StabSection_UI_Section"; const H = 96; const PAD = 10; +const LABEL_GAP = 64; // 측점 글 사이 최소 화면 간격(px) + +/** 띠 배율 · 그때 폭 — 폭이 바뀌면 원배율로 */ +export interface PlanView { + zoom?: ZoomPanState; + width?: number; +} export function buildPlan( rows: StationRow[], condition: Condition, active: string | null, + interval: number, + state: PlanView, onSelect: (key: string) => void, ): HTMLElement { const box = card("e03-plan"); @@ -49,47 +64,110 @@ export function buildPlan( const host = el("div"); box.append(host); + const ro = new ResizeObserver(() => draw()); const draw = (): void => { + if (!host.isConnected) return void ro.disconnect(); // 다시 지은 띠 — 옛 것은 손 뗌 const W = Math.max(host.clientWidth, 300); const segs = bandSegments(rows, condition); const all: Point[] = segs.flatMap((s) => s.points); const svg = svgEl("svg", { viewBox: `0 0 ${W} ${H}`, "data-role": "plan" }); - if (all.length) { - const fit = planFit(all, W, H, PAD); - const scr = (p: Point): Point => [fit.k * p[0] + fit.tx, -fit.k * p[1] + fit.ty]; - for (const seg of segs) { - svg.append( - svgEl("polyline", { - points: seg.points.map((p) => scr(p).join(",")).join(" "), - class: `e03-plan__seg e03-plan__seg--${seg.verdict}`, - "data-verdict": seg.verdict, - }), - ); - } - const centers = rows.filter((r) => r.center && r.center[0] !== null && r.center[1] !== null); - const picked = centers.find((r) => r.key === active); - if (picked) { - const [x, y] = scr([picked.center![0]!, picked.center![1]!]); - svg.append( - svgEl("circle", { cx: x, cy: y, r: 6, class: "e03-plan__pick", "data-key": picked.key }), - ); - } - svg.addEventListener("click", (ev) => { - const box2 = svg.getBoundingClientRect(); - const px = ((ev.clientX - box2.left) / box2.width) * W; - const py = ((ev.clientY - box2.top) / box2.height) * H; - let best: StationRow | null = null; - let dist = Infinity; - for (const r of centers) { - const [x, y] = scr([r.center![0]!, r.center![1]!]); - const d = Math.hypot(x - px, y - py); - if (d < dist) [best, dist] = [r, d]; - } - if (best && dist < 30) onSelect(best.key); - }); - } host.replaceChildren(svg); + if (!all.length) return; + const fit = planFit(all, W, H, PAD); + const scr = (p: Point): Point => [fit.k * p[0] + fit.tx, -fit.k * p[1] + fit.ty]; + const layer = svgEl("g", { "data-role": "plan-layer" }) as SVGGElement; + for (const seg of segs) { + layer.append( + svgEl("polyline", { + points: seg.points.map((p) => scr(p).join(",")).join(" "), + class: `e03-plan__seg e03-plan__seg--${seg.verdict}`, + "data-verdict": seg.verdict, + }), + ); + } + const centers = rows + .filter((r) => r.center && r.center[0] !== null && r.center[1] !== null) + .sort((a, b) => a.chainage_m - b.chainage_m); + const at = (r: StationRow): Point => scr([r.center![0]!, r.center![1]!]); + // 측점 간격 하나의 화면 길이(원배율) — 중심 이은 길이 ÷ 누가 길이 × 간격 + const span = + centers.length > 1 ? centers[centers.length - 1].chainage_m - centers[0].chainage_m : 0; + let len = 0; + for (let i = 1; i < centers.length; i += 1) { + const [a, b] = [at(centers[i - 1]), at(centers[i])]; + len += Math.hypot(b[0] - a[0], b[1] - a[1]); + } + const intervalPx = span > 0 ? (len / span) * interval : 0; + const maxScale = Math.min(500, Math.max(8, Math.ceil(240 / Math.max(intervalPx, 1e-3)))); + const picked = centers.find((r) => r.key === active); + const pick = picked + ? svgEl("circle", { + cx: at(picked)[0], + cy: at(picked)[1], + r: 6, + class: "e03-plan__pick", + "data-key": picked.key, + }) + : null; + const labels = svgEl("g", { class: "e03-plan__labels" }) as SVGGElement; + layer.append(labels, ...(pick ? [pick] : [])); + svg.append(layer); + let stride = 0; + const apply = (s: ZoomPanState): void => { + state.zoom = s; + labels.style.setProperty("--e03-label-k", String(1 / s.scale)); // 글 · 표시는 확대 안 함 + pick?.setAttribute("r", String(6 / s.scale)); + const next = intervalPx > 0 ? labelStride(intervalPx * s.scale, LABEL_GAP) : 100; + if (next === stride) return; + stride = next; + const keep = new Set(planLabelKeys(centers, interval, stride, active)); + labels.replaceChildren( + ...centers + .filter((r) => keep.has(r.key)) + .map((r) => + svgEl( + "text", + { + x: at(r)[0], + y: at(r)[1], + dy: "-0.9em", + class: "e03-label e03-plan__label", + "data-key": r.key, + }, + stationLabel(r.chainage_m, interval, 1), + ), + ), + ); + }; + const initial = state.width === W ? state.zoom : undefined; + state.width = W; + attachSvgZoomPan({ + svg, + layer, + widthPx: W, + heightPx: H, + pad: { left: 0, right: 0, top: 0, bottom: 0 }, + wheelZoom: true, + maxScale, + initial, + onChange: apply, + }); + apply(initial ?? { scale: 1, tx: 0, ty: 0 }); + svg.addEventListener("click", (ev) => { + const box2 = svg.getBoundingClientRect(); + const px = ((ev.clientX - box2.left) / box2.width) * W; + const py = ((ev.clientY - box2.top) / box2.height) * H; + const s = state.zoom ?? { scale: 1, tx: 0, ty: 0 }; + let best: StationRow | null = null; + let dist = Infinity; + for (const r of centers) { + const [x, y] = at(r); + const d = Math.hypot(s.scale * x + s.tx - px, s.scale * y + s.ty - py); + if (d < dist) [best, dist] = [r, d]; + } + if (best && dist < 30) onSelect(best.key); + }); }; - new ResizeObserver(draw).observe(host); + ro.observe(host); return box; } diff --git a/E03_StabSection/E03_StabSection_UI_Style.css b/E03_StabSection/E03_StabSection_UI_Style.css index f0de0ff7b..e1225726f 100644 --- a/E03_StabSection/E03_StabSection_UI_Style.css +++ b/E03_StabSection/E03_StabSection_UI_Style.css @@ -78,7 +78,7 @@ /* ---- 틀 ---- */ .e03-page { display: grid; - grid-template-rows: auto minmax(0, 1fr); + grid-template-rows: auto auto minmax(0, 1fr); gap: var(--spacing-8); height: calc(100dvh - var(--app-header-h)); padding: var(--spacing-8) var(--spacing-12); @@ -207,6 +207,7 @@ .e03-plan__seg { fill: none; stroke-width: 7; + vector-effect: non-scaling-stroke; stroke-linecap: butt; stroke-linejoin: round; cursor: pointer; @@ -228,6 +229,12 @@ fill: var(--e03-plot); stroke: var(--e03-critical); stroke-width: 2; + vector-effect: non-scaling-stroke; +} + +.e03-plan__label { + text-anchor: middle; + pointer-events: none; } .e03-legend { diff --git a/E03_StabSection/logic/E03_StabSection_Logic.ts b/E03_StabSection/logic/E03_StabSection_Logic.ts index c869cb37a..969d97871 100644 --- a/E03_StabSection/logic/E03_StabSection_Logic.ts +++ b/E03_StabSection/logic/E03_StabSection_Logic.ts @@ -190,6 +190,31 @@ export function planFit( return { k, tx: width / 2 - (k * (x0 + x1)) / 2, ty: height / 2 + (k * (y0 + y1)) / 2 }; } +/** 평면 띠 측점 글 솎음 걸음 — 측점 간격 화면 길이(px) × 걸음이 minPx 넘는 가장 작은 것 */ +export const PLAN_LABEL_STRIDES = [1, 2, 5, 10, 25, 50, 100] as const; + +export function labelStride(intervalPx: number, minPx: number): number { + return PLAN_LABEL_STRIDES.find((s) => s * intervalPx >= minPx) ?? 100; +} + +/** 측점 글 붙일 열쇠 — 본 측점(간격 배수) 중 걸음 배수 · 걸음 1 이면 보조 측점도 · 고른 측점은 늘 */ +export function planLabelKeys( + rows: StationRow[], + interval: number, + stride: number, + active: string | null, +): string[] { + const step = interval > 0 ? interval : 1; + return rows + .filter((r) => r.center && r.center[0] !== null && r.center[1] !== null) + .filter((r) => { + if (r.key === active || stride === 1) return true; + const n = Math.round(r.chainage_m / step); + return Math.abs(r.chainage_m - n * step) < 0.05 && n % stride === 0; + }) + .map((r) => r.key); +} + /* ---- 안전율 색 ------------------------------------------------------------ */ /** 등안전율 띠 — 안전율 ÷ 기준안전율 경계(빨강 셋 · 회색 하나 · 파랑 셋 · 가운데 = 기준 통과 직후) */ diff --git a/resources/tester/test_532_3_e03_section.py b/resources/tester/test_532_3_e03_section.py index 7f1a08727..17d0cd9cd 100644 --- a/resources/tester/test_532_3_e03_section.py +++ b/resources/tester/test_532_3_e03_section.py @@ -411,6 +411,9 @@ process.stdout.write(JSON.stringify({ stress: m.sliceStress([input.slice]), ticks: m.niceTicks(-3, 47, 5), zAt: [m.zAt([[0, 0], [10, 10]], 2.5), m.zAt([[10, 10], [0, 0]], -5)], + stride: [m.labelStride(100, 64), m.labelStride(40, 64), m.labelStride(10, 64), m.labelStride(0.1, 64)], + labels: [1, 2, 5].map((k) => m.planLabelKeys(input.rows, 20, k, null)), + labelsActive: m.planLabelKeys(input.rows, 20, 5, "e"), })); """ @@ -541,6 +544,10 @@ def test_logic_TS(): ) assert out["ticks"][0] <= -3 and out["ticks"][-1] >= 47 and 0 in out["ticks"] assert out["zAt"] == [2.5, 0] + # 평면 띠 측점 글 솎음 — 간격 px × 걸음 ≥ 64 · 본 측점 중 걸음 배수 · 고른 측점은 늘 + assert out["stride"] == [1, 2, 10, 100] + assert out["labels"] == [["a", "b", "c", "d", "e"], ["a", "c", "e"], ["a"]] + assert out["labelsActive"] == ["a", "e"] def test_엔진_지문은_코드_내용(): diff --git a/ui_template/ui_template_locale_e03.ts b/ui_template/ui_template_locale_e03.ts index 2051255c8..29befffc6 100644 --- a/ui_template/ui_template_locale_e03.ts +++ b/ui_template/ui_template_locale_e03.ts @@ -25,9 +25,6 @@ export const ui_locales_e03 = { E03_Fresh: ["입력이 같아 다시 셀 것이 없습니다.", "Inputs unchanged — nothing to recompute."], E03_View3d: ["3D 보기", "3D View"], E03_View3d_Legend: ["최소 안전율 ÷ 기준안전율", "Min FS ÷ required FS"], - E03_RepSave: ["대표 단면 저장", "Save Representative"], - E03_RepSaved: ["대표 단면을 저장했습니다.", "Representative sections saved."], - E03_RepUnsaved: ["대표 단면 바뀜 — 저장 안 됨", "Representative changes not saved"], E03_Loading: ["불러오는 중…", "Loading…"], E03_NotYet: [ "아직 해석하지 않았습니다 — [해석]을 누르세요.",