Merge remote-tracking branches 'origin/sub_laptop_2', 'origin/sub_laptop_5' and 'origin/sub_laptop_7' into sub_laptop_6
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""B08 4탭 홍수량 계산서 — 기본값 · 선택지 한 곳 (PLAN 62-3 · 73-11 · 73-12).
|
||||
"""B08 4탭 홍수량 계산서 — 기본값 · 선택지 · B05 연결값 한 곳 (PLAN 62-3 · 73-11 · 73-12 · 96).
|
||||
|
||||
임의로 정한 값 — 사용자가 나중에 고침. B05/B06 · 배수유역 값과 잇지 않음(자기 기본값만).
|
||||
표준값(유출계수 · 할증 · 재현기간 · 최소 도달시간)은 B05 배수 설정(config DRAINAGE_*)과 같은 값.
|
||||
측점 고유 값은 B05 배수유역 저장본의 연결값(linked · 96-1) · 하상경사 · 관 종류 · n 은 안 이음.
|
||||
근거 후보: docs/raw/조사/2026-10-06_62-1_홍수량_방법.md (별표2 2.0배 · KDS 표 2.7-1 · 실무 견본).
|
||||
"""
|
||||
|
||||
@@ -8,7 +9,14 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_drainage_pipes import carry_indices
|
||||
from config.config_system import FORD_MANNING_N, FORD_SLOPE
|
||||
from config.config_system_design import (
|
||||
DRAINAGE_DESIGN_FLOW_FACTOR,
|
||||
DRAINAGE_DESIGN_RETURN_PERIOD_YR,
|
||||
DRAINAGE_RUNOFF_COEFFICIENT,
|
||||
DRAINAGE_TC_MIN_MINUTES,
|
||||
)
|
||||
|
||||
#: 할증(Qd = Q × 할증) — 2.0 별표2 법정 · 1.5 견본(울진 2공구) · 1.2 구기준
|
||||
FACTORS = (2.0, 1.5, 1.2)
|
||||
@@ -22,15 +30,15 @@ PIPE_KINDS = {"파형강관": 0.024, "흄관": 0.013, "콘크리트관": 0.015}
|
||||
#: 처음 값 = 표준 값만(69-6) · 측점 고유 값(유역면적 · L · H · D · S)은 None(빈칸)
|
||||
#: R24 · 지역은 공사지 자료(B08_Quantity_Hydro_Site) — 자료가 없을 때만 화면 입력
|
||||
DEFAULTS: dict[str, Any] = {
|
||||
"factor": 2.0,
|
||||
"runoff": 0.80, # 유출계수 f — KDS 가파른 산지
|
||||
"factor": DRAINAGE_DESIGN_FLOW_FACTOR,
|
||||
"runoff": DRAINAGE_RUNOFF_COEFFICIENT, # 유출계수 f — KDS 가파른 산지
|
||||
"area_km2": None,
|
||||
"length_m": None, # 산정에서 하천시점까지 거리
|
||||
"height_m": None, # 산정에서 하천시점까지 표고차
|
||||
"tc_min_minutes": 5.0, # 최소 도달시간(KDS 2.4)
|
||||
"tc_min_minutes": DRAINAGE_TC_MIN_MINUTES, # 최소 도달시간(KDS 2.4)
|
||||
"r24_mm": None,
|
||||
"region": "",
|
||||
"return_period": 100,
|
||||
"return_period": DRAINAGE_DESIGN_RETURN_PERIOD_YR,
|
||||
"pipe_kind": "파형강관",
|
||||
"n": None, # None = 관 종류 값
|
||||
"diameter_mm": None,
|
||||
@@ -58,15 +66,62 @@ SHEET_DEFAULTS: dict[str, dict[str, Any]] = {
|
||||
}
|
||||
|
||||
|
||||
def station_rows(points: list[Any]) -> list[dict[str, Any]]:
|
||||
"""관 지점 → 보조 탭 줄(측점 차례) · 장 종류(pipe · box · ford)."""
|
||||
def _positive(value: Any) -> float | None:
|
||||
"""양수만 — 글 '1000' 도 숫자로 · 0 · 빈 값 · 글자는 None(빈칸)."""
|
||||
try:
|
||||
number = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return number if number > 0 else None
|
||||
|
||||
|
||||
def _linked(sheet: str, options: dict[str, Any], basin: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""B05 값 → 계산서 칸(96-1) — 값이 있는 칸만 · A · L · H 는 유역 · 치수는 시설 옵션."""
|
||||
values: dict[str, Any] = {}
|
||||
if basin is not None:
|
||||
area_m2 = _positive(basin.get("area_m2"))
|
||||
values["area_km2"] = area_m2 / 1e6 if area_m2 else None
|
||||
values["length_m"] = _positive(basin.get("flow_length_m"))
|
||||
values["height_m"] = _positive(basin.get("relief_m"))
|
||||
if sheet == "pipe":
|
||||
values["diameter_mm"] = _positive(options.get("pipe_diameter_mm"))
|
||||
elif sheet == "box":
|
||||
values["box_width_m"] = _positive(options.get("body_width_m"))
|
||||
values["box_height_m"] = _positive(options.get("body_height_m"))
|
||||
else:
|
||||
values["overflow_width_m"] = _positive(options.get("ford_width_m"))
|
||||
return {k: v for k, v in values.items() if v is not None}
|
||||
|
||||
|
||||
def station_rows(
|
||||
points: list[Any], basins: list[dict[str, Any]] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""관 지점 → 보조 탭 줄(측점 차례) · 장 종류(pipe · box · ford).
|
||||
|
||||
basins(B05 세부유역 속성) 를 주면 측점마다 연결값 `linked` · 비교용 `b05`(Qd · T) — 측점 짝은
|
||||
`carry_indices`(±0.05 m) · 안 주면 둘 다 None.
|
||||
"""
|
||||
points = sorted(points, key=lambda p: p.chainage_m)
|
||||
picks = (
|
||||
carry_indices([p.chainage_m for p in points], [float(b["chainage_m"]) for b in basins])
|
||||
if basins is not None
|
||||
else [None] * len(points)
|
||||
)
|
||||
rows = []
|
||||
for p in points:
|
||||
for p, pick in zip(points, picks):
|
||||
sheet, kind = SHEETS.get(p.facility, ("pipe", ""))
|
||||
rows.append(
|
||||
{"chainage_m": p.chainage_m, "facility": p.facility, "kind": kind, "sheet": sheet}
|
||||
)
|
||||
return sorted(rows, key=lambda s: s["chainage_m"])
|
||||
row = {"chainage_m": p.chainage_m, "facility": p.facility, "kind": kind, "sheet": sheet}
|
||||
row["linked"] = row["b05"] = None
|
||||
if basins is not None:
|
||||
basin = basins[pick] if pick is not None else None
|
||||
row["linked"] = _linked(sheet, getattr(p, "options", None) or {}, basin)
|
||||
if basin is not None:
|
||||
row["b05"] = {
|
||||
"design_flow_m3s": basin.get("design_flow_m3s"),
|
||||
"tc_minutes": basin.get("tc_minutes"),
|
||||
}
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def options() -> dict[str, Any]:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""B08 4탭 홍수량 계산서 — 공사지 지역 · 확률강우량 · 저장 파일 (PLAN 69-5 · 69-6).
|
||||
|
||||
읽기만: 전처리 강우량표(`B04_PreProcess/drainage/rainfall_table.json` · 관측소 확률강우량)
|
||||
· 행정구역(`B04_PreProcess/processed/행정구역_*_bounds.geojson` · WGS84) · 프로젝트 등록 지역(DB).
|
||||
· 행정구역(`B04_PreProcess/processed/행정구역_*_bounds.geojson` · WGS84) · 프로젝트 등록 지역(DB)
|
||||
· B05 세부유역(`B04_PreProcess/drainage/04_detailed_basins.geojson` · B05 [저장] 때 씀 · 96-1).
|
||||
배수유역 계산 코드는 부르지 않음 — 이미 저장된 파일만 읽음.
|
||||
|
||||
쓰기: B08 자기 파일 `B08_Quantity/quantities/hydro_inputs.json` 하나([저장] 때만).
|
||||
@@ -16,7 +17,10 @@ from typing import Any
|
||||
|
||||
from shapely.geometry import Point, shape
|
||||
|
||||
RAINFALL_FILE = Path("B04_PreProcess") / "drainage" / "rainfall_table.json"
|
||||
from config.config_system_design import DRAINAGE_CACHE_DIRNAME, DRAINAGE_DETAIL_FILENAME
|
||||
|
||||
RAINFALL_FILE = Path("B04_PreProcess") / DRAINAGE_CACHE_DIRNAME / "rainfall_table.json"
|
||||
BASINS_FILE = Path("B04_PreProcess") / DRAINAGE_CACHE_DIRNAME / DRAINAGE_DETAIL_FILENAME
|
||||
PROCESSED_DIR = Path("B04_PreProcess") / "processed"
|
||||
EMD_FILE = "행정구역_읍면동_bounds.geojson"
|
||||
SGG_FILE = "행정구역_시군구_bounds.geojson"
|
||||
@@ -105,6 +109,18 @@ def site_r24(root: Path, return_period: float) -> float | None:
|
||||
return _r24_table(_read_json(root / RAINFALL_FILE)).get(int(return_period))
|
||||
|
||||
|
||||
def read_basins(root: Path) -> list[dict[str, Any]]:
|
||||
"""B05 세부유역 속성(kind detail_basin) — 파일이 없으면 빈 목록(연결값은 시설 옵션만)."""
|
||||
basins = []
|
||||
for feature in _features(root / BASINS_FILE):
|
||||
props = feature.get("properties") or {}
|
||||
if props.get("kind") == "detail_basin" and isinstance(
|
||||
props.get("chainage_m"), (int, float)
|
||||
):
|
||||
basins.append(props)
|
||||
return basins
|
||||
|
||||
|
||||
def read_saved(root: Path) -> dict[str, dict[str, Any]]:
|
||||
data = _read_json(root / SAVED_FILE) or {}
|
||||
stations = data.get("stations")
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""B08 4탭 홍수량 계산서 길 (PLAN 62-2 · 62-3 · 69-5 · 69-6 · 73-11 · 73-12).
|
||||
|
||||
GET /api/projects/{id}/quantity/hydro 배수지점 측점(저장된 pipe_points) + 기본값 · 선택지
|
||||
+ 공사지(지역 · 재현기간별 R24) + 저장한 입력
|
||||
GET /api/projects/{id}/quantity/hydro 배수지점 측점(저장된 pipe_points) + B05 연결값
|
||||
+ 기본값 · 선택지 + 공사지(지역 · R24) + 저장본
|
||||
POST /api/projects/{id}/quantity/hydro/calc 화면 입력 → 계산서 값(B08_Quantity_Engine_Hydro)
|
||||
PUT /api/projects/{id}/quantity/hydro [저장] — 측점별 입력을 B08 자기 파일에만 씀
|
||||
|
||||
측점 목록만 관 지점 정본에서 읽음 — 관경 · 관 종류 등 B05/B06 값은 잇지 않음(자기 기본값).
|
||||
B05/B06 · 배수유역 파일에는 쓰지 않음.
|
||||
측점 목록 · 시설 치수는 관 지점 정본 · A · L · H 는 B05 세부유역 저장본에서 연결값(96-1) —
|
||||
화면이 빈칸만 채움 · 하상경사 · 관 종류 · n 은 잇지 않음. B05/B06 · 배수유역 파일에는 쓰지 않음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,7 +22,13 @@ from B02_ProjRegister.B02_ProjRegister_Repository import get_project_by_id
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B08_Quantity.B08_Quantity_Engine_Hydro import compute
|
||||
from B08_Quantity.B08_Quantity_Hydro_Defaults import FACTORS, PIPE_KINDS, options, station_rows
|
||||
from B08_Quantity.B08_Quantity_Hydro_Site import read_saved, site_info, site_r24, write_saved
|
||||
from B08_Quantity.B08_Quantity_Hydro_Site import (
|
||||
read_basins,
|
||||
read_saved,
|
||||
site_info,
|
||||
site_r24,
|
||||
write_saved,
|
||||
)
|
||||
from common_util.common_util_drainage_pipes import pipe_points_path_in, read_pipe_points_file
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool
|
||||
@@ -75,7 +81,7 @@ async def _project_root(project_id: UUID) -> Path:
|
||||
@router.get("/{project_id}/quantity/hydro")
|
||||
async def hydro_sheet(project_id: UUID) -> dict[str, Any]:
|
||||
root = await _project_root(project_id)
|
||||
stations = station_rows(read_pipe_points_file(pipe_points_path_in(root)))
|
||||
stations = station_rows(read_pipe_points_file(pipe_points_path_in(root)), read_basins(root))
|
||||
project = await get_project_by_id(str(project_id)) or {}
|
||||
return {
|
||||
"stations": stations,
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
|
||||
/** 구조물 · 관 지점 한 건 — 필요한 칸만. 구간형은 기준점(`chainage_m`) · 없으면 시점. */
|
||||
export interface PlacedItem {
|
||||
/** 복사본 폴더를 찾는 구조물 id(관 지점은 `pipe@측점`) — 없으면 복사본 없이 원본 */
|
||||
structure_id?: string | null;
|
||||
chainage_m?: number | null;
|
||||
start_m?: number | null;
|
||||
summary_item?: unknown;
|
||||
@@ -82,6 +84,37 @@ export function summaryColumns(
|
||||
);
|
||||
}
|
||||
|
||||
/** 한 측점에 놓인 조합 한 건 — 어느 구조물 것인지(복사본 찾기용)까지. */
|
||||
export interface StationCombo {
|
||||
id: string;
|
||||
item: SummaryItem;
|
||||
structureId: string | null;
|
||||
}
|
||||
|
||||
/** 측점(1cm 안 같은 측점)에 놓인 조합들 — 구조물마다 본체 → 그룹 차례 · 같은 구조물의 같은 조합은 한 건. */
|
||||
export function combosAt(things: readonly PlacedItem[], station: number): StationCombo[] {
|
||||
const seen = new Set<string>();
|
||||
const found: StationCombo[] = [];
|
||||
for (const thing of things) {
|
||||
const at = stationOf(thing);
|
||||
if (at === null || Math.round(at * 100) !== Math.round(station * 100)) continue;
|
||||
const own = readSummaryItem(thing.summary_item);
|
||||
for (const item of [
|
||||
...(own ? [own] : []),
|
||||
...Object.values(readGroupItems(thing.group_items)),
|
||||
]) {
|
||||
const structureId = thing.structure_id ?? null;
|
||||
const key = `${structureId}
|
||||
${comboId(item)}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
found.push({ id: comboId(item), item, structureId });
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/** 머리 첫 줄 — 이어진 같은 구분을 한 칸으로 `[구분, 열 수]`. */
|
||||
export function groupSpans(columns: readonly ComboColumn[]): Array<[string, number]> {
|
||||
const spans: Array<[string, number]> = [];
|
||||
|
||||
@@ -1,17 +1,48 @@
|
||||
/* B08 2탭 구조물도(PLAN 61-2) — 보조 탭 · 카드 둘(도면 · 면적산출). 색은 테마 변수만. */
|
||||
/* B08 구조물도 모달(PLAN 95-3) — 조합마다 카드 둘(도면 · 면적산출). 색은 테마 변수만. */
|
||||
|
||||
/* M02 에 없는 조합 — 빨간 글은 비어 있는 단추만(고른 단추 바탕 위에선 안 읽힘 · 아래 안내 글이 알림) */
|
||||
.b08-drawing__tabs .is-missing:not(.ui-btn--filled) {
|
||||
/* 큰 창 — 공용 모달 css 는 그대로 두고 자기 class 로 */
|
||||
.ui-modal.b08-sheets {
|
||||
width: min(1320px, 96vw);
|
||||
--modal-max: 94vh;
|
||||
}
|
||||
|
||||
.b08-sheets .ui-modal__body {
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
|
||||
.b08-sheets__combo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b08-sheets__title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--spacing-8);
|
||||
margin: 0;
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.b08-sheets__from {
|
||||
font-size: var(--text-caption);
|
||||
font-weight: var(--font-weight-regular);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* M02 에 없는 조합 안내 — 빨간 글 */
|
||||
.b08-sheets__combo > .ui-general-block__empty {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
/* M02 카드 칸은 셋(도면 · 일위대가 · 면적산출) — 여기는 둘 · 도면을 넓게 */
|
||||
.b08-drawing__cards .m02-struct {
|
||||
.b08-sheets .m02-struct {
|
||||
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.b08-drawing__cards .m02-struct {
|
||||
.b08-sheets .m02-struct {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_UI_Drawing.ts
|
||||
* 2탭 구조물도(PLAN 61-2) — 집계표 열(저장된 구조물 조합)마다 보조 탭 · 한 장 = 그 조합의
|
||||
* M02 도면(도각 포함 · 보기 전용 CAD) + 면적산출 — M02 항목 카드(`M02_MasterTemplete_ItemCards`) 그대로.
|
||||
* 구조물 집계표 행 → 구조물도 모달(PLAN 95-3) — 그 측점에 놓인 조합 전부를 세로로 쌓음.
|
||||
* 조합 한 개 = M02 항목 카드(`M02_MasterTemplete_ItemCards`) 그대로 — 도면(도각 포함 · 보기 전용 CAD ·
|
||||
* 여러 장이면 장 탭) + 면적산출.
|
||||
*
|
||||
* 조합 목록 · 차례 = 집계표와 같음(`B08_Quantity_Summary_Data`)
|
||||
* 읽기만 — 조합 원본 층(`readTemplate`)을 읽고 카드는 [저장] · [수정] 없음 · B05/B06 자료도 읽기만
|
||||
* 카드 · CAD · 스프레드시트는 이 탭을 열 때만 `import()`
|
||||
* 원천 = 그 구조물의 프로젝트 복사본(같은 항목일 때) · 없으면 M02 원본 층 — 읽기만
|
||||
* 조합 목록 = 집계표와 같음(`B08_Quantity_Summary_Data` `combosAt`)
|
||||
* 카드 · CAD · 스프레드시트는 모달을 열 때만 `import()` · 닫을 때 카드를 모두 정리
|
||||
* ========================================================================== */
|
||||
|
||||
import "./B08_Quantity_UI_Drawing.css";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { loadRows as loadPickerRows } from "@ui/ui_template_item_picker";
|
||||
import { layerOf, pathOf, rowId, type PickerRow } from "@ui/ui_template_item_picker_data";
|
||||
import { readTemplate } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import type { ItemSource } from "../M02_MasterTemplete/M02_MasterTemplete_ItemSource";
|
||||
import { fetchStructures } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { summaryTooltip } from "../common_util/common_util_summary_item";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { layerOf, type PickerRow } from "@ui/ui_template_item_picker_data";
|
||||
import {
|
||||
comboId,
|
||||
summaryColumns,
|
||||
type ComboColumn,
|
||||
type ComboLabel,
|
||||
} from "./B08_Quantity_Summary_Data";
|
||||
PART_OF,
|
||||
projectItems,
|
||||
sameItem,
|
||||
structureFolder,
|
||||
type ProjectItemInfo,
|
||||
} from "@ui/ui_template_item_picker_project";
|
||||
import { readTemplate } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import type { ItemCardsHandle } from "../M02_MasterTemplete/M02_MasterTemplete_ItemCards";
|
||||
import type { ItemSource } from "../M02_MasterTemplete/M02_MasterTemplete_ItemSource";
|
||||
import { summaryTooltip, type SummaryItem } from "../common_util/common_util_summary_item";
|
||||
import type { ComboLabel, StationCombo } from "./B08_Quantity_Summary_Data";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
@@ -36,94 +37,136 @@ function note(text: string): HTMLElement {
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 집계표와 같은 열 + 열마다 M02 줄(없으면 M02 에 없는 조합). */
|
||||
async function loadColumns(
|
||||
projectId: string,
|
||||
): Promise<{ columns: ComboColumn[]; rows: Map<string, PickerRow> }> {
|
||||
const [structures, pickerRows, pipes] = await Promise.all([
|
||||
fetchStructures(projectId),
|
||||
loadPickerRows(),
|
||||
// 배수 분석 전이면 관이 없음 — 구조물만으로
|
||||
fetchDetailPipePoints(projectId).then(
|
||||
(response) => response.pipe_points,
|
||||
() => [],
|
||||
),
|
||||
]);
|
||||
const rows = new Map(pickerRows.map((row) => [rowId(row), row]));
|
||||
const describe = (item: Parameters<typeof comboId>[0]): ComboLabel | null => {
|
||||
const row = rows.get(comboId(item));
|
||||
if (!row) return null;
|
||||
const [group = "", ...rest] = pathOf(row);
|
||||
return { group, name: rest.join(" ") || group || row.key };
|
||||
};
|
||||
return { columns: summaryColumns([...structures.structures, ...pipes], describe), rows };
|
||||
export interface SheetsRequest {
|
||||
projectId: string;
|
||||
/** 모달 제목에 붙는 측점 글 */
|
||||
station: string;
|
||||
combos: StationCombo[];
|
||||
/** `comboId` → M02 줄 */
|
||||
rows: Map<string, PickerRow>;
|
||||
describe: (item: SummaryItem) => ComboLabel | null;
|
||||
}
|
||||
|
||||
/** 탭 몸통을 그림 — 불러오는 동안 한 줄 · 실패하면 까닭 한 줄. */
|
||||
export async function renderStructureSheets(host: HTMLElement, projectId: string): Promise<void> {
|
||||
host.replaceChildren(note(L("B08_Quantity_Drawing_Loading")));
|
||||
try {
|
||||
const [{ columns, rows }, { createItemCards }] = await Promise.all([
|
||||
loadColumns(projectId),
|
||||
import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards"),
|
||||
]);
|
||||
if (!host.isConnected) return; // 그새 다른 탭으로 옮김
|
||||
if (!columns.length) return void host.replaceChildren(note(L("B08_Quantity_Summary_NoCombo")));
|
||||
/** 그 구조물의 복사본 목록 — 프로젝트 복사본을 못 읽으면 원본만(모달은 그대로 열림). */
|
||||
const loadCopies = (projectId: string): Promise<ProjectItemInfo[]> =>
|
||||
projectItems(projectId)
|
||||
.list()
|
||||
.catch(() => []);
|
||||
|
||||
let row: PickerRow | null = null;
|
||||
const source = (): ItemSource => {
|
||||
const layer = row ? layerOf(row) : "system";
|
||||
return {
|
||||
layer,
|
||||
projectId: null,
|
||||
read: (part, column) => readTemplate(layer, part, column, null),
|
||||
};
|
||||
function sourceOf(
|
||||
request: SheetsRequest,
|
||||
combo: StationCombo,
|
||||
row: PickerRow,
|
||||
copies: ProjectItemInfo[],
|
||||
): { source: ItemSource; copy: boolean } {
|
||||
const own = combo.structureId
|
||||
? copies.find((info) => info.폴더 === structureFolder(combo.structureId!))
|
||||
: undefined;
|
||||
if (own && sameItem(own, row)) {
|
||||
const items = projectItems(request.projectId);
|
||||
return {
|
||||
copy: true,
|
||||
source: {
|
||||
layer: "project",
|
||||
projectId: request.projectId,
|
||||
read: async (part) => {
|
||||
const doc = await items.read(own.폴더, PART_OF[part]);
|
||||
if (doc == null) throw new Error(L("B08_Quantity_Sheets_NoCopy"));
|
||||
return { 문서: doc };
|
||||
},
|
||||
},
|
||||
};
|
||||
const cards = createItemCards({
|
||||
source,
|
||||
editable: () => false,
|
||||
onEdit: () => undefined,
|
||||
warnOnFail: () => false, // 못 읽으면 카드에 「없음」
|
||||
});
|
||||
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "b08-quantity__tabs b08-drawing__tabs";
|
||||
tabs.setAttribute("role", "tablist");
|
||||
const notice = document.createElement("div");
|
||||
const sheet = document.createElement("div");
|
||||
sheet.className = "b08-drawing__cards";
|
||||
sheet.append(cards.root);
|
||||
|
||||
const select = (picked: ComboColumn): void => {
|
||||
tabs.replaceChildren(
|
||||
...columns.map((column) => {
|
||||
const button = createButton({
|
||||
label: column.group ? `${column.group} ${column.name}` : column.name,
|
||||
variant: column === picked ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => column !== picked && select(column),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(column === picked));
|
||||
button.dataset.b08Combo = column.id;
|
||||
button.title = summaryTooltip(column.item);
|
||||
if (column.missing) button.classList.add("is-missing");
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
row = rows.get(picked.id) ?? null;
|
||||
if (!row?.열) {
|
||||
notice.replaceChildren(note(L("B08_Quantity_Drawing_Missing")));
|
||||
return void cards.load(null);
|
||||
}
|
||||
notice.replaceChildren(...(row.도번 ? [] : [note(L("B08_Quantity_Drawing_NoDrawing"))]));
|
||||
void cards.load({ id: row.열, label: picked.name });
|
||||
};
|
||||
|
||||
host.replaceChildren(tabs, notice, sheet);
|
||||
select(columns[0]);
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
host.replaceChildren(note(`${L("B08_Quantity_Drawing_Failed")} — ${reason}`));
|
||||
}
|
||||
const layer = layerOf(row);
|
||||
return {
|
||||
copy: false,
|
||||
source: {
|
||||
layer,
|
||||
projectId: null,
|
||||
read: (part, column) => readTemplate(layer, part, column, null),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 조합 한 칸 — 제목줄(이름 · 원천) + 안내 + 카드. */
|
||||
function comboSection(
|
||||
request: SheetsRequest,
|
||||
combo: StationCombo,
|
||||
copies: ProjectItemInfo[],
|
||||
createItemCards: typeof import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards").createItemCards,
|
||||
handles: ItemCardsHandle[],
|
||||
): HTMLElement {
|
||||
const label = request.describe(combo.item);
|
||||
const name = label
|
||||
? [label.group, label.name].filter(Boolean).join(" ")
|
||||
: combo.item.name || combo.item.key;
|
||||
const row = request.rows.get(combo.id) ?? null;
|
||||
const section = document.createElement("section");
|
||||
section.className = "b08-sheets__combo";
|
||||
section.dataset.b08Combo = combo.id;
|
||||
const title = document.createElement("h4");
|
||||
title.className = "b08-sheets__title";
|
||||
title.textContent = name;
|
||||
title.title = summaryTooltip(combo.item);
|
||||
section.append(title);
|
||||
if (!row?.열) {
|
||||
section.append(note(L("B08_Quantity_Drawing_Missing")));
|
||||
return section;
|
||||
}
|
||||
const picked = sourceOf(request, combo, row, copies);
|
||||
const from = document.createElement("span");
|
||||
from.className = "b08-sheets__from";
|
||||
from.textContent = L(
|
||||
picked.copy ? "B08_Quantity_Sheets_FromCopy" : "B08_Quantity_Sheets_FromMaster",
|
||||
);
|
||||
title.append(from);
|
||||
if (!row.도번) section.append(note(L("B08_Quantity_Drawing_NoDrawing")));
|
||||
const cards = createItemCards({
|
||||
source: () => picked.source,
|
||||
editable: () => false,
|
||||
onEdit: () => undefined,
|
||||
warnOnFail: () => false, // 못 읽으면 카드에 「없음」
|
||||
});
|
||||
handles.push(cards);
|
||||
section.append(cards.root);
|
||||
void cards.load({ id: row.열, label: name });
|
||||
return section;
|
||||
}
|
||||
|
||||
/** 모달을 엶 — 조합이 없으면 안내 한 줄 · 불러오다 실패하면 까닭 한 줄. */
|
||||
export function openStructureSheets(request: SheetsRequest): void {
|
||||
const handles: ItemCardsHandle[] = [];
|
||||
let closed = false;
|
||||
openModal({
|
||||
title: `${request.station} ${L("B08_Quantity_Sheets_Title")}`,
|
||||
closeLabel: L("Common_Btn_Close"),
|
||||
dialogClass: "b08-sheets",
|
||||
beforeClose: () => {
|
||||
closed = true; // 아직 불러오는 중이면 뒤늦게 카드를 만들지 않음
|
||||
handles.forEach((cards) => cards.destroy());
|
||||
return true;
|
||||
},
|
||||
mount: (body) => {
|
||||
if (!request.combos.length) return void body.append(note(L("B08_Quantity_Summary_NoCombo")));
|
||||
body.append(note(L("B08_Quantity_Drawing_Loading")));
|
||||
void (async () => {
|
||||
try {
|
||||
const [{ createItemCards }, copies] = await Promise.all([
|
||||
import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards"),
|
||||
loadCopies(request.projectId),
|
||||
]);
|
||||
if (closed) return;
|
||||
body.replaceChildren(
|
||||
...request.combos.map((combo) =>
|
||||
comboSection(request, combo, copies, createItemCards, handles),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (closed) return;
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
body.replaceChildren(note(`${L("B08_Quantity_Drawing_Failed")} — ${reason}`));
|
||||
}
|
||||
})();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
색은 테마 토큰만(밝음 · 어두움 같이) · 테두리는 입력 · 선택 칸(공용 ui-input · ui-select)만
|
||||
· 굵게 + 붉은 계열은 중요 결과(.is-key)만. */
|
||||
|
||||
/* 오른쪽 「진행단계」 떠 있는 판에 안 가리게 */
|
||||
.b08-hydro__toolbar,
|
||||
.b08-hydro__sheet {
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
.b08-hydro__toolbar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -27,6 +21,7 @@
|
||||
}
|
||||
|
||||
.b08-hydro__sheet {
|
||||
container-type: inline-size; /* 2열 ↔ 1열 = 창이 아닌 이 칸 폭 기준 */
|
||||
color: var(--color-text-body);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
@@ -38,12 +33,24 @@
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
@container (max-width: 800px) {
|
||||
.b08-hydro__body {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* 카드 높이 = 화면 따라(B08 다른 표와 같은 calc(100vh − N)) · 넘치면 카드 몸만 스크롤 */
|
||||
.b08-hydro__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 240px);
|
||||
}
|
||||
|
||||
.b08-hydro__card .ui-card__body {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.b08-hydro__card .ui-card__title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* B08_Quantity_UI_Hydro.ts
|
||||
* 4탭 수리집수면적표 — 배수지점 측점마다 보조 탭 · 한 장 = 홍수량 계산서(PLAN 62-2 · 69-4~6).
|
||||
*
|
||||
* 측점 목록만 저장된 관 지점(pipe_points)에서 · 입력은 이 탭 자기 값(B05/B06 값과 안 이음)
|
||||
* 측점 목록은 저장된 관 지점(pipe_points) · B05 연결값(A · L · H · 관경 · 내공 · 월류 폭 · 96)이 빈칸을 채움
|
||||
* 값 차례 = 기본 < 장 기본 < 연결 < 저장 < 초안 · 연결값과 같은 칸은 null 로 저장(연동 유지)
|
||||
* 박스암거 = 사각 단면 장(73-11) · 세월교 · 물넘이포장 = 월류 높이 장(73-12) · 그 밖 = 관 장
|
||||
* 처음 값 = 표준 값 · 측점 고유 값은 빈칸 · 지역 · R24 = 공사지 자료(서버)
|
||||
* 조작 = 캐시(sessionStorage) · [저장] = B08 자기 파일(PUT …/quantity/hydro) · 뒤 캐시 비움
|
||||
@@ -23,6 +24,7 @@ import { stationInterval } from "./B08_Quantity_UI_Summary";
|
||||
import {
|
||||
buildHydroSheet,
|
||||
type HydroInputs,
|
||||
type HydroLink,
|
||||
type HydroOptions,
|
||||
type HydroResult,
|
||||
} from "./B08_Quantity_UI_Hydro_Sheet";
|
||||
@@ -34,6 +36,9 @@ interface HydroStation {
|
||||
kind: string;
|
||||
/** 계산서 장 — 시설이 정함(서버 station_rows) */
|
||||
sheet: HydroInputs["sheet"];
|
||||
/** B05 연결값(값이 있는 칸만) · 비교용 B05 Qd · T — 서버 station_rows */
|
||||
linked: HydroLink["values"] | null;
|
||||
b05: HydroLink["b05"];
|
||||
}
|
||||
|
||||
type Drafts = Record<string, Partial<HydroInputs>>;
|
||||
@@ -83,14 +88,26 @@ export async function renderHydro(host: HTMLElement, projectId: string): Promise
|
||||
}
|
||||
|
||||
let drafts: Drafts = readState<Drafts>("b08-hydro", projectId) ?? {};
|
||||
const linkedOf = (station: HydroStation): Record<string, unknown> => station.linked ?? {};
|
||||
/** 저장본 · 초안 한 겹 — 연결값이 있는 칸의 null 은 건너뜀(연결값을 덮지 않게) */
|
||||
const layer = (station: HydroStation, values: Partial<HydroInputs> = {}) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(values).filter(([k, v]) => v != null || !(k in linkedOf(station))),
|
||||
);
|
||||
const inputsOf = (station: HydroStation): HydroInputs =>
|
||||
({
|
||||
...sheet.defaults,
|
||||
...(station.sheet === "pipe" ? {} : sheet.sheet_defaults[station.sheet]),
|
||||
...sheet.saved[keyOf(station)],
|
||||
...drafts[keyOf(station)],
|
||||
...station.linked,
|
||||
// 초안은 측점 입력 통째 — 있으면 저장본 대신(초안의 null = 연결값으로 되돌림)
|
||||
...layer(station, drafts[keyOf(station)] ?? sheet.saved[keyOf(station)]),
|
||||
sheet: station.sheet,
|
||||
}) as HydroInputs;
|
||||
/** 연결값과 같은 칸은 null — 저장 · 초안이 연동을 유지(B05 값이 바뀌면 따라옴) · 다르면 손 값 */
|
||||
const unlinked = (station: HydroStation, inputs: HydroInputs): HydroInputs =>
|
||||
Object.fromEntries(
|
||||
Object.entries(inputs).map(([k, v]) => [k, linkedOf(station)[k] === v ? null : v]),
|
||||
) as unknown as HydroInputs;
|
||||
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "b08-quantity__tabs b08-hydro__tabs";
|
||||
@@ -134,13 +151,14 @@ export async function renderHydro(host: HTMLElement, projectId: string): Promise
|
||||
inputs,
|
||||
sheet,
|
||||
(next) => {
|
||||
drafts[keyOf(station)] = next;
|
||||
drafts[keyOf(station)] = unlinked(station, next);
|
||||
writeState("b08-hydro", drafts, projectId);
|
||||
// 단면 가정 · 관 종류는 그림 · 식이 바뀜 — 장을 다시 그림 · 그 밖은 값만
|
||||
if (next.section !== inputs.section || next.pipe_kind !== inputs.pipe_kind) draw(next);
|
||||
else void calc(next, view.show);
|
||||
},
|
||||
station.kind,
|
||||
{ values: station.linked ?? {}, b05: station.b05 },
|
||||
);
|
||||
page.replaceChildren(view.root);
|
||||
void calc(inputs, view.show);
|
||||
@@ -166,7 +184,9 @@ export async function renderHydro(host: HTMLElement, projectId: string): Promise
|
||||
|
||||
/** [저장] — 측점 전부의 지금 입력을 B08 자기 파일로 · 성공하면 캐시를 비우고 저장본으로 다시 엶 */
|
||||
const saveAll = async (): Promise<void> => {
|
||||
const stations = Object.fromEntries(sheet.stations.map((s) => [keyOf(s), inputsOf(s)]));
|
||||
const stations = Object.fromEntries(
|
||||
sheet.stations.map((s) => [keyOf(s), unlinked(s, inputsOf(s))]),
|
||||
);
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const body = await request<{ saved: Drafts }>(`/projects/${projectId}/quantity/hydro`, {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 장(sheet) 갈래 — 박스 = 사각 내공 B · H · 수심비(73-11) · 세월교 · 물넘이 = 월류 폭 → 월류 높이(73-12)
|
||||
* 테두리는 입력 · 선택 칸만(공용 ui-input · ui-select) · 계산 칸은 글자만
|
||||
* 굵게 + 붉은 계열은 중요 결과(Qd · 유출량 · 판정 · 유출율)만 · 빈칸에 기대는 계산 칸은 「-」.
|
||||
* B05 연결값과 같은 칸은 이름 옆 작은 「B05」(풍선 「B05 분석」) · Qd · T 옆에 B05 값 참고(96-2).
|
||||
*
|
||||
* 입력 칸은 한 번 만들고 계산 결과만 제자리에 바꿈 — 고치는 중 칸 포커스를 잃지 않게.
|
||||
* ========================================================================== */
|
||||
@@ -77,6 +78,12 @@ export interface HydroOptions {
|
||||
site: HydroSite;
|
||||
}
|
||||
|
||||
/** B05 배수유역 연결(96) — 칸 값 · 비교용 설계유량 · 도달시간(분) */
|
||||
export interface HydroLink {
|
||||
values: Partial<Record<keyof HydroInputs, number>>;
|
||||
b05: { design_flow_m3s: number | null; tc_minutes: number | null } | null;
|
||||
}
|
||||
|
||||
export interface HydroSheet {
|
||||
root: HTMLElement;
|
||||
/** 계산값 칸을 바꿈 — null 이면 비움 + 까닭 한 줄 */
|
||||
@@ -103,6 +110,14 @@ const DASH = "-";
|
||||
const fmt = (value: number | null | undefined, digits: number, unit = ""): string =>
|
||||
value == null || !Number.isFinite(value) ? DASH : `${value.toFixed(digits)}${unit}`;
|
||||
|
||||
/** 입력 칸이 놓인 줄의 이름 칸 */
|
||||
function sheetLabel(left: HTMLElement, right: HTMLElement, key: string): HTMLElement | null {
|
||||
const input = [left, right]
|
||||
.map((card) => card.querySelector(`input[data-b08-hydro="${key}"]`))
|
||||
.find(Boolean);
|
||||
return input?.closest(".b08-hydro__row")?.querySelector(".b08-hydro__label") ?? null;
|
||||
}
|
||||
|
||||
function node<K extends keyof HTMLElementTagNameMap>(
|
||||
tag: K,
|
||||
className = "",
|
||||
@@ -218,8 +233,15 @@ export function buildHydroSheet(
|
||||
onChange: (next: HydroInputs) => void,
|
||||
/** 시설 이름(박스암거 · 세월교 · 물넘이포장) — 관이면 "" */
|
||||
kind = "",
|
||||
link: HydroLink = { values: {}, b05: null },
|
||||
): HydroSheet {
|
||||
const site = options.site;
|
||||
/** 연결된 칸의 「B05」 표 — 값이 연결값과 같을 때만 보임 */
|
||||
const marks = new Map<Key, HTMLElement>();
|
||||
const markLinked = (key: Key): void => {
|
||||
const mark = marks.get(key);
|
||||
if (mark) mark.hidden = current[key] !== link.values[key];
|
||||
};
|
||||
const out = new Map<string, HTMLElement>();
|
||||
const outCell = (id: string, key = false): HTMLElement => {
|
||||
const cell = node("span", `b08-hydro__out${key ? " is-key" : ""}`, DASH);
|
||||
@@ -229,6 +251,7 @@ export function buildHydroSheet(
|
||||
let current = inputs;
|
||||
const commit = (key: Key, value: unknown): void => {
|
||||
current = { ...current, [key]: value } as HydroInputs;
|
||||
markLinked(key);
|
||||
onChange(current);
|
||||
};
|
||||
const numberInput = (key: Key, placeholder = ""): HTMLInputElement => {
|
||||
@@ -238,6 +261,14 @@ export function buildHydroSheet(
|
||||
input.dataset.b08Hydro = key;
|
||||
const value = inputs[key];
|
||||
input.value = value == null ? "" : String(value);
|
||||
const linked = link.values[key];
|
||||
if (linked != null) {
|
||||
input.title = `B05 분석 ${linked}`;
|
||||
const mark = node("span", "b08-hydro__hint", "B05");
|
||||
mark.title = "B05 분석";
|
||||
mark.dataset.b08HydroLinked = key;
|
||||
marks.set(key, mark);
|
||||
}
|
||||
input.addEventListener("change", () => {
|
||||
// 빈칸은 NULLABLE 만 · 최소 도달시간은 0 까지 · 그 밖은 0 보다 커야 함 — 아니면 되돌림
|
||||
const parsed = input.value === "" ? null : Number(input.value);
|
||||
@@ -283,6 +314,18 @@ export function buildHydroSheet(
|
||||
};
|
||||
const sub = (text: string): HTMLElement => node("h4", "b08-hydro__h2", text);
|
||||
const hint = (text: string): HTMLElement => node("span", "b08-hydro__hint", text);
|
||||
/** B05 분석 값 참고(비교만) — 값이 없으면 칸 없음 */
|
||||
const b05Hint = (
|
||||
key: "design_flow_m3s" | "tc_minutes",
|
||||
text: (value: number) => string,
|
||||
): HTMLElement | undefined => {
|
||||
const value = link.b05?.[key];
|
||||
if (value == null) return undefined;
|
||||
const el = hint(text(value));
|
||||
el.title = "B05 분석";
|
||||
el.dataset.b08HydroB05 = key;
|
||||
return el;
|
||||
};
|
||||
|
||||
// ── 왼쪽 1. 홍수량 계산 ──
|
||||
const factor = select(
|
||||
@@ -332,6 +375,7 @@ export function buildHydroSheet(
|
||||
return label;
|
||||
})(),
|
||||
outCell("design_flow", true),
|
||||
b05Hint("design_flow_m3s", (v) => `B05 ${fmt(v, 2)}`),
|
||||
),
|
||||
row("Q : 홍수량 (㎥/s)", outCell("flow")),
|
||||
row("f : 유출계수", numberInput("runoff")),
|
||||
@@ -339,7 +383,16 @@ export function buildHydroSheet(
|
||||
row("A : 유역면적 (㎢)", numberInput("area_km2", "입력"), outCell("area_ha")),
|
||||
sub("1-2. 홍수 도달시간 — 산지부 (Kirpich)"),
|
||||
formula("T = 0.0663 × L^0.77^ × s^-0.385^ (L : ㎞ · T : hr)"),
|
||||
row("T : 홍수 도달시간 (hr)", outCell("tc_hr"), outCell("tc_note")),
|
||||
row(
|
||||
"T : 홍수 도달시간 (hr)",
|
||||
outCell("tc_hr"),
|
||||
(() => {
|
||||
const note = node("span");
|
||||
const b05 = b05Hint("tc_minutes", (v) => ` B05 ${fmt(v, 1)}분`);
|
||||
note.append(outCell("tc_note"), ...(b05 ? [b05] : []));
|
||||
return note;
|
||||
})(),
|
||||
),
|
||||
row("L : 산정에서 하천시점까지 거리 (m)", numberInput("length_m", "입력")),
|
||||
row("H : 산정에서 하천시점까지 표고차 (m)", numberInput("height_m", "입력")),
|
||||
row("s : 지표면 경사 (H / L)", outCell("slope_ground")),
|
||||
@@ -443,6 +496,13 @@ export function buildHydroSheet(
|
||||
});
|
||||
right.classList.add("b08-hydro__card");
|
||||
|
||||
// 「B05」 표는 그 칸 이름 옆에
|
||||
for (const [key, mark] of marks) {
|
||||
const label = sheetLabel(left, right, key);
|
||||
label?.append(" ", mark);
|
||||
markLinked(key);
|
||||
}
|
||||
|
||||
const status = node("p", "b08-hydro__status");
|
||||
const sheet = node("div", "b08-hydro__sheet");
|
||||
sheet.dataset.b08HydroSheet = stationText;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.b08-quantity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
gap: var(--spacing-8); /* 보조 탭 줄 위 = 아래(.b08-quantity__tabs padding-bottom) */
|
||||
padding: var(--spacing-16) var(--spacing-24); /* M02 본문(.m02-main)과 같은 값 */
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* B08_Quantity_UI_Page.ts
|
||||
* 로그인 후 08: 5차 워크플로우 (수량 산출) — 위 탭 화면 (PLAN 61)
|
||||
*
|
||||
* 탭 = 구조물 집계표 · 구조물도 · 토공 · 수리집수면적표 — 차례대로 만듦.
|
||||
* 위 탭 단추 = 공용 기본 크기(md) · 보조 탭 단추 = sm(PLAN 69-1).
|
||||
* 1탭(구조물 집계표 · 61-1) · 4탭(수리집수면적표 · 홍수량 계산서 62-2) 내용 · 나머지는 빈 탭.
|
||||
* 탭 = 구조물 · 토공 · 수리집수면적표 (PLAN 95-1). 위 탭 단추 = 공용 기본 크기(md) · 보조 탭 단추 = sm(PLAN 69-1).
|
||||
* 구조물 아래 보조 탭 = 구조물 집계표(61-1) · 총괄자재수량집계표 · 콘크리트소요자재집계표 ·
|
||||
* 자재수량집계표(셋은 아직 빈 탭) — 구조물도는 집계표 행을 눌러 여는 모달(95-3).
|
||||
* 옛 수량 산출 코드(`old_code/B08_Quantity/`)는 가져오지 않음 — 새로 씀.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -18,7 +18,6 @@ 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";
|
||||
import { renderHydro } from "./B08_Quantity_UI_Hydro";
|
||||
import { renderStructureSheets } from "./B08_Quantity_UI_Drawing";
|
||||
import { renderEarthwork } from "./B08_Quantity_UI_Earthwork";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
@@ -26,32 +25,68 @@ function L(key: keyof typeof ui_locales): string {
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
{ id: "summary", label: "B08_Quantity_Tab_StructureSummary" },
|
||||
{ id: "sheet", label: "B08_Quantity_Tab_StructureSheet" },
|
||||
{ id: "structure", label: "B08_Quantity_Tab_Structure" },
|
||||
{ id: "earthwork", label: "B08_Quantity_Tab_Earthwork" },
|
||||
{ id: "hydrology", label: "B08_Quantity_Tab_Hydrology" },
|
||||
] as const;
|
||||
|
||||
type TabId = (typeof TABS)[number]["id"];
|
||||
|
||||
/** 구조물 탭 보조 탭 — 첫째만 내용이 있음. */
|
||||
const SHEETS = [
|
||||
{ id: "summary", label: "B08_Quantity_Tab_StructureSummary" },
|
||||
{ id: "material", label: "B08_Quantity_Tab_MaterialTotal" },
|
||||
{ id: "concrete", label: "B08_Quantity_Tab_ConcreteMaterial" },
|
||||
{ id: "quantity", label: "B08_Quantity_Tab_MaterialQuantity" },
|
||||
] as const;
|
||||
|
||||
type SheetId = (typeof SHEETS)[number]["id"];
|
||||
|
||||
function pendingNote(): HTMLElement {
|
||||
const pending = document.createElement("p");
|
||||
pending.className = "ui-general-block__empty";
|
||||
pending.textContent = L("B08_Quantity_Tab_Pending");
|
||||
return pending;
|
||||
}
|
||||
|
||||
/** 구조물 탭 — 보조 탭 줄 + 몸통(보조 탭마다 새 몸통 · 늦은 응답이 다른 보조 탭을 덮지 않게). */
|
||||
function renderStructure(host: HTMLElement, projectId: string | null): void {
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "b08-quantity__tabs b08-structure__tabs";
|
||||
tabs.setAttribute("role", "tablist");
|
||||
const view = document.createElement("div");
|
||||
const select = (picked: SheetId): void => {
|
||||
tabs.replaceChildren(
|
||||
...SHEETS.map(({ id, label }) => {
|
||||
const button = createButton({
|
||||
label: L(label),
|
||||
variant: id === picked ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => id !== picked && select(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === picked));
|
||||
button.dataset.b08StructureSheet = id;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
const body = document.createElement("div");
|
||||
view.replaceChildren(body);
|
||||
if (picked === "summary" && projectId) void renderStructureSummary(body, projectId);
|
||||
else body.append(pendingNote());
|
||||
};
|
||||
host.replaceChildren(tabs, view);
|
||||
select("summary");
|
||||
}
|
||||
|
||||
/** 탭마다 새 몸통 — 늦게 온 응답이 다른 탭 화면을 덮지 않게(떨어진 몸통에만 그림). */
|
||||
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;
|
||||
}
|
||||
if (tab === "hydrology" && projectId) {
|
||||
void renderHydro(view, projectId);
|
||||
return;
|
||||
}
|
||||
if (tab === "sheet" && projectId) return void renderStructureSheets(view, projectId);
|
||||
if (tab === "structure") return renderStructure(view, projectId);
|
||||
if (tab === "hydrology" && projectId) return void renderHydro(view, projectId);
|
||||
if (tab === "earthwork" && projectId) return void renderEarthwork(view, projectId);
|
||||
const pending = document.createElement("p");
|
||||
pending.className = "ui-general-block__empty";
|
||||
pending.textContent = L("B08_Quantity_Tab_Pending");
|
||||
view.append(pending);
|
||||
view.append(pendingNote());
|
||||
}
|
||||
|
||||
export function renderB08Quantity(root: HTMLElement): void {
|
||||
@@ -84,7 +119,7 @@ export function renderB08Quantity(root: HTMLElement): void {
|
||||
const body = document.createElement("div");
|
||||
body.className = "b08-quantity";
|
||||
body.append(tabBar, panel);
|
||||
select("summary");
|
||||
select("structure");
|
||||
|
||||
const layout = createWorkflowLayout({
|
||||
title: L("B08_Quantity_Title"),
|
||||
|
||||
@@ -5,23 +5,29 @@
|
||||
*
|
||||
* 구조물 = GET …/route/structures · 배관 = GET …/drainage/pipe-points (읽기만)
|
||||
* 머리 두 줄 = 조합 구분(M02 경로 첫 칸) · 조합 이름(나머지 경로) — `GET /api/m02/items?all=1`
|
||||
* 줄을 누르면 그 측점의 구조물도 모달(PLAN 95-3 · `B08_Quantity_UI_Drawing` 은 누를 때 `import()`)
|
||||
* ========================================================================== */
|
||||
|
||||
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 { pathOf, rowId, type PickerRow } from "@ui/ui_template_item_picker_data";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
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 { createDataTable } from "@ui/ui_template_data_table";
|
||||
import { pipeStructureId } from "@ui/ui_template_item_picker_project";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
import { summaryTooltip } from "../common_util/common_util_summary_item";
|
||||
import { summaryTooltip, type SummaryItem } from "../common_util/common_util_summary_item";
|
||||
import {
|
||||
combosAt,
|
||||
comboId,
|
||||
summaryColumns,
|
||||
summaryStations,
|
||||
type ComboColumn,
|
||||
type ComboLabel,
|
||||
type PlacedItem,
|
||||
type StationCombo,
|
||||
} from "./B08_Quantity_Summary_Data";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
@@ -44,7 +50,12 @@ function note(text: string): HTMLElement {
|
||||
/** 머리 글 길이에 맞춘 열 폭 — 칸은 비어 있어 글이 폭을 정함. */
|
||||
const widthOf = (label: string): number => Math.min(200, Math.max(96, label.length * 13 + 24));
|
||||
|
||||
function buildTable(stations: number[], columns: ComboColumn[], interval: number): HTMLElement {
|
||||
function buildTable(
|
||||
stations: number[],
|
||||
columns: ComboColumn[],
|
||||
interval: number,
|
||||
onRowClick: (station: number, label: string) => void,
|
||||
): HTMLElement {
|
||||
const table = createDataTable({
|
||||
columns: [
|
||||
{ key: "station", label: L("B08_Quantity_Summary_Station"), width: 96 },
|
||||
@@ -59,6 +70,7 @@ function buildTable(stations: number[], columns: ComboColumn[], interval: number
|
||||
],
|
||||
// 칸은 아직 비움 — 사용자가 채울 자리라 「-」 대신 빈칸
|
||||
rows: stations.map((station) => ({ station: stationLabel(station, interval) })),
|
||||
onRowClick: (i) => onRowClick(stations[i], stationLabel(stations[i], interval)),
|
||||
frozen: 1,
|
||||
empty: "",
|
||||
caption: L("B08_Quantity_Tab_StructureSummary"),
|
||||
@@ -67,6 +79,22 @@ function buildTable(stations: number[], columns: ComboColumn[], interval: number
|
||||
return table.root;
|
||||
}
|
||||
|
||||
/** 행 누름 — 카드 · CAD 묶음은 이때 처음 받음. */
|
||||
async function openSheets(
|
||||
projectId: string,
|
||||
station: string,
|
||||
combos: StationCombo[],
|
||||
rows: Map<string, PickerRow>,
|
||||
describe: (item: SummaryItem) => ComboLabel | null,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { openStructureSheets } = await import("./B08_Quantity_UI_Drawing");
|
||||
openStructureSheets({ projectId, station, combos, rows, describe });
|
||||
} catch (error) {
|
||||
showToast(`${L("B08_Quantity_Drawing_Failed")} — ${String(error)}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
/** 탭 몸통을 그림 — 불러오는 동안 한 줄 · 실패하면 까닭 한 줄. */
|
||||
export async function renderStructureSummary(host: HTMLElement, projectId: string): Promise<void> {
|
||||
host.replaceChildren(note(L("B08_Quantity_Summary_Loading")));
|
||||
@@ -82,20 +110,25 @@ export async function renderStructureSummary(host: HTMLElement, projectId: strin
|
||||
stationInterval(projectId).catch(() => 20),
|
||||
]);
|
||||
const m02 = new Map(pickerRows.map((row) => [rowId(row), row]));
|
||||
const describe = (item: Parameters<typeof comboId>[0]): ComboLabel | null => {
|
||||
const describe = (item: SummaryItem): 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 things: PlacedItem[] = [
|
||||
...structures.structures,
|
||||
...pipes.map((pipe) => ({ ...pipe, structure_id: pipeStructureId(pipe.chainage_m) })),
|
||||
];
|
||||
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)
|
||||
? buildTable(stations, columns, interval, (station, label) =>
|
||||
openSheets(projectId, label, combosAt(things, station), m02, describe),
|
||||
)
|
||||
: note(L("B08_Quantity_Summary_NoStation")),
|
||||
);
|
||||
host.replaceChildren(...parts);
|
||||
|
||||
Reference in New Issue
Block a user