feat(b08): 수량산출 위 탭 틀 + 1탭 구조물 집계표(줄 측점 · 열 저장 조합 · 칸 비움)
- 탭 넷(구조물 집계표 · 구조물도 · 토적표 · 수리집수면적표) — 1탭만 내용 · 나머지 빈 탭 - 줄 = 구조물 · 배관이 놓인 측점(같은 측점 한 줄 · 정렬) · 열 = 저장된 summary_item + group_items(같은 층 · 주인 · 키 한 열) - 머리 두 줄 = 조합 구분(M02 경로 첫 칸) · 조합 이름(나머지 경로) · M02 에 없으면 빨강 「M02 에 없음」 - 읽기만 — structures.json · pipe_points.json GET · 옛 B08 코드 안 가져옴 - 항목 고르기 목록 읽기(loadRows) 내보냄 · 시험 test_61_1_b08_summary_table.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_Summary_Data.ts
|
||||
* 구조물 집계표의 줄 · 열 고르기(화면 없음 · PLAN 61-1).
|
||||
*
|
||||
* 줄 = 구조물 · 배관이 놓인 측점(같은 측점은 한 줄 · 오름차순)
|
||||
* 열 = 저장된 구조물 조합(본체 `summary_item` + 그룹 `group_items`) — 같은 층 · 주인 · 키는 한 열
|
||||
*
|
||||
* 읽기만 — B05/B06 저장 자료(structures.json · pipe_points.json)를 고치지 않는다.
|
||||
* 런타임 import 는 `common_util_summary_item` 하나 — 시험이 두 파일을 이어 붙여 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
readGroupItems,
|
||||
readSummaryItem,
|
||||
type SummaryItem,
|
||||
} from "../common_util/common_util_summary_item";
|
||||
|
||||
/** 구조물 · 관 지점 한 건 — 필요한 칸만. 구간형은 기준점(`chainage_m`) · 없으면 시점. */
|
||||
export interface PlacedItem {
|
||||
chainage_m?: number | null;
|
||||
start_m?: number | null;
|
||||
summary_item?: unknown;
|
||||
group_items?: unknown;
|
||||
}
|
||||
|
||||
/** M02 조합 정보에서 읽은 구분 · 이름 — 못 찾으면 null(저장된 이름으로 보임). */
|
||||
export interface ComboLabel {
|
||||
group: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ComboColumn {
|
||||
/** `{층}|{주인}|{키}` — 항목 고르기 `rowId` 와 같은 꼴 */
|
||||
id: string;
|
||||
item: SummaryItem;
|
||||
group: string;
|
||||
name: string;
|
||||
/** M02 에 그 조합이 없음 */
|
||||
missing: boolean;
|
||||
}
|
||||
|
||||
export const comboId = (item: SummaryItem): string =>
|
||||
`${item.layer}|${item.owner ?? ""}|${item.key}`;
|
||||
|
||||
function stationOf(thing: PlacedItem): number | null {
|
||||
const value = thing.chainage_m ?? thing.start_m;
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
/** 놓인 측점 — 1cm 안이면 같은 측점 · 오름차순. */
|
||||
export function summaryStations(things: readonly PlacedItem[]): number[] {
|
||||
const seen = new Map<number, number>();
|
||||
for (const thing of things) {
|
||||
const station = stationOf(thing);
|
||||
if (station !== null) seen.set(Math.round(station * 100), station);
|
||||
}
|
||||
return [...seen.values()].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/** 저장된 조합 열 — 구분 · 이름 차례(같은 구분은 붙어 머리 칸을 합침). */
|
||||
export function summaryColumns(
|
||||
things: readonly PlacedItem[],
|
||||
describe: (item: SummaryItem) => ComboLabel | null,
|
||||
): ComboColumn[] {
|
||||
const found = new Map<string, SummaryItem>();
|
||||
for (const thing of things) {
|
||||
const own = readSummaryItem(thing.summary_item);
|
||||
for (const item of [...(own ? [own] : []), ...Object.values(readGroupItems(thing.group_items))])
|
||||
if (!found.has(comboId(item))) found.set(comboId(item), item);
|
||||
}
|
||||
const columns = [...found].map(([id, item]): ComboColumn => {
|
||||
const label = describe(item);
|
||||
return label
|
||||
? { id, item, ...label, missing: false }
|
||||
: { id, item, group: "", name: item.name || item.key, missing: true };
|
||||
});
|
||||
return columns.sort(
|
||||
(a, b) =>
|
||||
a.group.localeCompare(b.group, "ko") ||
|
||||
a.name.localeCompare(b.name, "ko") ||
|
||||
a.id.localeCompare(b.id),
|
||||
);
|
||||
}
|
||||
|
||||
/** 머리 첫 줄 — 이어진 같은 구분을 한 칸으로 `[구분, 열 수]`. */
|
||||
export function groupSpans(columns: readonly ComboColumn[]): Array<[string, number]> {
|
||||
const spans: Array<[string, number]> = [];
|
||||
for (const column of columns) {
|
||||
const last = spans[spans.length - 1];
|
||||
if (last && last[0] === column.group) last[1] += 1;
|
||||
else spans.push([column.group, 1]);
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/* B08 수량산출 — 위 탭 · 구조물 집계표(PLAN 61-1). 색은 테마 변수만(밝음 · 어두움 같이). */
|
||||
|
||||
.b08-quantity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
.b08-quantity__tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8);
|
||||
padding-bottom: var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* 집계표 — 칸마다 테두리 · 머리 가운데 · 측점 열은 가로로 밀어도 붙어 있음 */
|
||||
.b08-summary__wrap {
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.b08-summary__table {
|
||||
width: auto;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.b08-summary__table th,
|
||||
.b08-summary__table td {
|
||||
padding: var(--spacing-4, 4px) var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b08-summary__table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
background: var(--color-surface-raised, var(--color-surface));
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* 머리 둘째 줄은 첫 줄 아래에 붙음 */
|
||||
.b08-summary__table thead tr + tr th {
|
||||
top: calc(1lh + 2 * var(--spacing-4, 4px) + 1px);
|
||||
}
|
||||
|
||||
.b08-summary__table .b08-summary__station {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
background: var(--color-surface-raised, var(--color-surface));
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.b08-summary__table thead .b08-summary__station {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.b08-summary__table td {
|
||||
min-width: 80px;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.b08-summary__name.is-missing {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
@@ -1,29 +1,81 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_UI_Page.ts
|
||||
* 로그인 후 08: 5차 워크플로우 (수량 산출) — **빈 화면** (PLAN 7-3)
|
||||
* 로그인 후 08: 5차 워크플로우 (수량 산출) — 위 탭 화면 (PLAN 61)
|
||||
*
|
||||
* - 옛 수량 산출 코드는 통째로 `old_code/B08_Quantity/` 에 있음(지우지 않음).
|
||||
* - 다시 짤 때까지 메뉴와 주소만 남김 — 제목·단계 막대만 뜨고 표·계산은 없음.
|
||||
* 탭 = 구조물 집계표 · 구조물도 · 토적표 · 수리집수면적표 — 차례대로 만듦.
|
||||
* 지금은 1탭(구조물 집계표 · 61-1)만 내용 · 나머지는 빈 탭.
|
||||
* 옛 수량 산출 코드(`old_code/B08_Quantity/`)는 가져오지 않음 — 새로 씀.
|
||||
* ========================================================================== */
|
||||
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
import "./B08_Quantity_UI_Page.css";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
|
||||
import { renderStructureSummary } from "./B08_Quantity_UI_Summary";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
{ id: "summary", label: "B08_Quantity_Tab_StructureSummary" },
|
||||
{ id: "sheet", label: "B08_Quantity_Tab_StructureSheet" },
|
||||
{ id: "earthwork", label: "B08_Quantity_Tab_Earthwork" },
|
||||
{ id: "hydrology", label: "B08_Quantity_Tab_Hydrology" },
|
||||
] as const;
|
||||
|
||||
type TabId = (typeof TABS)[number]["id"];
|
||||
|
||||
/** 탭마다 새 몸통 — 늦게 온 응답이 다른 탭 화면을 덮지 않게(떨어진 몸통에만 그림). */
|
||||
function renderTab(host: HTMLElement, tab: TabId, projectId: string | null): void {
|
||||
const view = document.createElement("div");
|
||||
host.replaceChildren(view);
|
||||
if (tab === "summary" && projectId) {
|
||||
void renderStructureSummary(view, projectId);
|
||||
return;
|
||||
}
|
||||
const pending = document.createElement("p");
|
||||
pending.className = "ui-general-block__empty";
|
||||
pending.textContent = L("B08_Quantity_Tab_Pending");
|
||||
view.append(pending);
|
||||
}
|
||||
|
||||
export function renderB08Quantity(root: HTMLElement): void {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
|
||||
const note = document.createElement("p");
|
||||
note.className = "b08-quantity__rebuilding";
|
||||
note.textContent = L("B08_Quantity_Rebuilding");
|
||||
const tabBar = document.createElement("div");
|
||||
tabBar.className = "b08-quantity__tabs";
|
||||
tabBar.setAttribute("role", "tablist");
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "b08-quantity__panel";
|
||||
panel.setAttribute("role", "tabpanel");
|
||||
|
||||
const select = (tab: TabId): void => {
|
||||
tabBar.replaceChildren(
|
||||
...TABS.map(({ id, label }) => {
|
||||
const button = createButton({
|
||||
label: L(label),
|
||||
variant: id === tab ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => id !== tab && select(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === tab));
|
||||
button.dataset.b08Tab = id;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
renderTab(panel, tab, projectId);
|
||||
};
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.append(note);
|
||||
body.className = "b08-quantity";
|
||||
body.append(tabBar, panel);
|
||||
select("summary");
|
||||
|
||||
const layout = createWorkflowLayout({
|
||||
title: L("B08_Quantity_Title"),
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_UI_Summary.ts
|
||||
* 1탭 구조물 집계표(PLAN 61-1) — 줄 = 측점 · 열 = 저장된 구조물 조합 · 칸은 아직 비움.
|
||||
*
|
||||
* 구조물 = GET …/route/structures · 배관 = GET …/drainage/pipe-points (읽기만)
|
||||
* 머리 두 줄 = 조합 구분(M02 경로 첫 칸) · 조합 이름(나머지 경로) — `GET /api/m02/items?all=1`
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { loadRows as loadPickerRows } from "@ui/ui_template_item_picker";
|
||||
import { pathOf, rowId } from "@ui/ui_template_item_picker_data";
|
||||
import { fetchStructures } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { fetchLatestRoute, readRouteLatestCache } from "../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
import { summaryTooltip } from "../common_util/common_util_summary_item";
|
||||
import {
|
||||
comboId,
|
||||
groupSpans,
|
||||
summaryColumns,
|
||||
summaryStations,
|
||||
type ComboColumn,
|
||||
type ComboLabel,
|
||||
} from "./B08_Quantity_Summary_Data";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
/** 측점 간격 — 노선 설정값 · 못 읽으면 20m. */
|
||||
async function stationInterval(projectId: string): Promise<number> {
|
||||
const latest = readRouteLatestCache(projectId) ?? (await fetchLatestRoute(projectId));
|
||||
return latest.route_params?.station_interval_m || 20;
|
||||
}
|
||||
|
||||
function cell(tag: "th" | "td", text = "", className = ""): HTMLTableCellElement {
|
||||
const node = document.createElement(tag);
|
||||
node.textContent = text;
|
||||
if (className) node.className = className;
|
||||
return node;
|
||||
}
|
||||
|
||||
function note(text: string): HTMLElement {
|
||||
const node = document.createElement("p");
|
||||
node.className = "ui-general-block__empty";
|
||||
node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function buildTable(stations: number[], columns: ComboColumn[], interval: number): HTMLElement {
|
||||
const head = cell("th", L("B08_Quantity_Summary_Station"), "b08-summary__station");
|
||||
head.rowSpan = 2;
|
||||
const groups = document.createElement("tr");
|
||||
groups.append(head);
|
||||
for (const [group, span] of groupSpans(columns)) {
|
||||
const th = cell("th", group || L("B08_Quantity_Summary_MissingGroup"), "b08-summary__group");
|
||||
th.colSpan = span;
|
||||
groups.append(th);
|
||||
}
|
||||
const names = document.createElement("tr");
|
||||
for (const column of columns) {
|
||||
const th = cell("th", column.name, "b08-summary__name");
|
||||
th.dataset.b08Combo = column.id;
|
||||
th.title = summaryTooltip(column.item);
|
||||
if (column.missing) th.classList.add("is-missing");
|
||||
names.append(th);
|
||||
}
|
||||
const thead = document.createElement("thead");
|
||||
thead.append(groups, names);
|
||||
|
||||
const tbody = document.createElement("tbody");
|
||||
for (const station of stations) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.dataset.b08Station = String(station);
|
||||
tr.append(cell("th", stationLabel(station, interval), "b08-summary__station"));
|
||||
for (const _ of columns) tr.append(cell("td"));
|
||||
tbody.append(tr);
|
||||
}
|
||||
const table = document.createElement("table");
|
||||
table.className = "ui-general-block__table b08-summary__table";
|
||||
table.dataset.b08Summary = "table";
|
||||
table.append(thead, tbody);
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "ui-general-block__table-wrap b08-summary__wrap";
|
||||
wrap.append(table);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/** 탭 몸통을 그림 — 불러오는 동안 한 줄 · 실패하면 까닭 한 줄. */
|
||||
export async function renderStructureSummary(host: HTMLElement, projectId: string): Promise<void> {
|
||||
host.replaceChildren(note(L("B08_Quantity_Summary_Loading")));
|
||||
try {
|
||||
const [structures, pickerRows, pipes, interval] = await Promise.all([
|
||||
fetchStructures(projectId),
|
||||
loadPickerRows(),
|
||||
// 배수 분석 전이면 관이 없음 — 구조물만으로 그림
|
||||
fetchDetailPipePoints(projectId).then(
|
||||
(response) => response.pipe_points,
|
||||
() => [],
|
||||
),
|
||||
stationInterval(projectId).catch(() => 20),
|
||||
]);
|
||||
const m02 = new Map(pickerRows.map((row) => [rowId(row), row]));
|
||||
const describe = (item: Parameters<typeof comboId>[0]): ComboLabel | null => {
|
||||
const row = m02.get(comboId(item));
|
||||
if (!row) return null;
|
||||
const [group = "", ...rest] = pathOf(row);
|
||||
return { group, name: rest.join(" ") || group || row.key };
|
||||
};
|
||||
const things = [...structures.structures, ...pipes];
|
||||
const stations = summaryStations(things);
|
||||
const columns = summaryColumns(things, describe);
|
||||
const parts: HTMLElement[] = [];
|
||||
if (!columns.length) parts.push(note(L("B08_Quantity_Summary_NoCombo")));
|
||||
parts.push(
|
||||
stations.length
|
||||
? buildTable(stations, columns, interval)
|
||||
: note(L("B08_Quantity_Summary_NoStation")),
|
||||
);
|
||||
host.replaceChildren(...parts);
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
host.replaceChildren(note(`${L("B08_Quantity_Summary_Failed")} — ${reason}`));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
"""B08 1탭 구조물 집계표 줄 · 열 고르기 (PLAN 61-1).
|
||||
|
||||
`B08_Quantity_Summary_Data.ts` 를 `common_util_summary_item.ts` 와 이어 붙여 node 로 돌림.
|
||||
줄 = 놓인 측점(같은 측점 한 줄 · 정렬) · 열 = 저장된 조합(본체 + 그룹 · 같은 키 한 열 · 구분 차례).
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
|
||||
|
||||
def _run(body: str):
|
||||
sources = [
|
||||
ROOT / "common_util" / "common_util_summary_item.ts",
|
||||
ROOT / "B08_Quantity" / "B08_Quantity_Summary_Data.ts",
|
||||
]
|
||||
script = "\n".join(IMPORTS.sub("", p.read_text(encoding="utf-8")) for p in sources) + body
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", script],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def _item(key: str, layer: str = "system", owner=None) -> dict:
|
||||
return {"layer": layer, "owner": owner, "key": key, "name": f"저장 {key}"}
|
||||
|
||||
|
||||
THINGS = [
|
||||
{"chainage_m": 120.004, "summary_item": _item("SS2")},
|
||||
{"chainage_m": 40.0, "summary_item": _item("SS1"), "group_items": {"g1": _item("SS3")}},
|
||||
{"chainage_m": 120.0, "summary_item": _item("SS1")}, # 같은 측점 · 같은 조합
|
||||
{"chainage_m": None, "start_m": 80.0}, # 구간형 옛 저장분 = 시점
|
||||
{"chainage_m": 10.0, "summary_item": {"key": "깨짐"}}, # 모양 틀림 = 미지정
|
||||
{"chainage_m": 60.0, "summary_item": _item("SS9", "company", "3")}, # M02 에 없음
|
||||
]
|
||||
M02 = {"SS1": ("기슭막이", "메쌓기"), "SS2": ("배수관", "파형관"), "SS3": ("기슭막이", "찰쌓기")}
|
||||
|
||||
|
||||
def test_줄_열_머리():
|
||||
out = _run(
|
||||
f"\nconst things = {json.dumps(THINGS, ensure_ascii=False)};"
|
||||
f"\nconst m02 = {json.dumps(M02, ensure_ascii=False)};"
|
||||
"\nconst describe = (item) => item.layer === 'system' && m02[item.key]"
|
||||
" ? { group: m02[item.key][0], name: m02[item.key][1] } : null;"
|
||||
"\nconst columns = summaryColumns(things, describe);"
|
||||
"\nconsole.log(JSON.stringify({ stations: summaryStations(things),"
|
||||
" columns: columns.map((c) => [c.id, c.group, c.name, c.missing]),"
|
||||
" spans: groupSpans(columns) }));"
|
||||
)
|
||||
assert out["stations"] == [10.0, 40.0, 60.0, 80.0, 120.0]
|
||||
assert out["columns"] == [
|
||||
["company|3|SS9", "", "저장 SS9", True],
|
||||
["system||SS1", "기슭막이", "메쌓기", False],
|
||||
["system||SS3", "기슭막이", "찰쌓기", False],
|
||||
["system||SS2", "배수관", "파형관", False],
|
||||
]
|
||||
assert out["spans"] == [["", 1], ["기슭막이", 2], ["배수관", 1]]
|
||||
|
||||
|
||||
def test_빈_목록():
|
||||
out = _run(
|
||||
"\nconsole.log(JSON.stringify([summaryStations([]),"
|
||||
" summaryColumns([{ chainage_m: 5 }], () => null), groupSpans([])]));"
|
||||
)
|
||||
assert out == [[], [], []]
|
||||
@@ -71,7 +71,7 @@ function reasonOf(body: unknown, status: number): string {
|
||||
}
|
||||
|
||||
/** `GET /api/m02/items?all=1` = `[{layer, owner, items}]` — 줄마다 `layer` · `owner` 를 붙여 하나로 폄 */
|
||||
async function loadRows(): Promise<PickerRow[]> {
|
||||
export async function loadRows(): Promise<PickerRow[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/m02/items?all=1`, { credentials: "include" });
|
||||
const body = (await response.json().catch(() => null)) as
|
||||
| { layer: PickerLayer; owner: string | null; items: PickerRow[] }[]
|
||||
|
||||
@@ -17,4 +17,21 @@ export const ui_locales_b4 = {
|
||||
"제안값 넣기 — KDS 표준치수 토사 0.5 m",
|
||||
"Insert suggested value — KDS standard soil 0.5 m",
|
||||
],
|
||||
B08_Quantity_Tab_Hydrology: ["수리집수면적표", "Hydraulic Catchment Table"],
|
||||
B08_Quantity_Tab_Pending: ["차례대로 만들 탭입니다.", "This tab comes later, in order."],
|
||||
B08_Quantity_Summary_Station: ["측점", "Station"],
|
||||
B08_Quantity_Summary_MissingGroup: ["M02 에 없음", "Not in M02"],
|
||||
B08_Quantity_Summary_Loading: ["구조물 집계표를 불러오는 중…", "Loading the structure summary…"],
|
||||
B08_Quantity_Summary_Failed: [
|
||||
"구조물 집계표를 불러오지 못했습니다",
|
||||
"Failed to load the structure summary",
|
||||
],
|
||||
B08_Quantity_Summary_NoCombo: [
|
||||
"저장된 구조물 조합이 없습니다 — B05 · B06 패널에서 조합을 지정 · 저장하세요.",
|
||||
"No saved structure combinations — assign and save them in the B05 · B06 panel.",
|
||||
],
|
||||
B08_Quantity_Summary_NoStation: [
|
||||
"놓인 구조물 · 배관이 없습니다.",
|
||||
"No placed structures or pipes.",
|
||||
],
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user