feat(B05/B02): 표시 정보 정비 + 설계속도 축 도입
- 종단 상단 표시줄을 법정 판정 3항목으로 축소: 최대 기울기/상한(별표2 Ⅰ.2.라),
절·성토 불균형(Ⅰ.1.나.(4)(다)), 종단곡선 필요 n곳(Ⅰ.2.마 — 대수차 5% 초과인데
곡선이 빠진 변화점). 변화점·종단곡선 개수·기본 곡선길이 L·중복 경고칩은 제거.
- [초기선 복원] → [편집 되돌리기] — 실제 동작은 화면 편집 델타 삭제이지 저장 지점
복귀가 아니다. 툴팁에 동작과 [초기화]와의 차이를 적었다.
- 3D 뷰포트 안내 라벨 삭제(로딩 완료 후 남던 조작법 문구).
- 배수유역 버튼 정비: 유역 다시 나누기 / 선택한 관 삭제 / 배관 배치 초기화 +
각 툴팁에 실제 동작 명시(선택 삭제는 툴팁 자체가 없었다).
- 유토곡선 요약 12칩 → 5칩(절토·성토·잉여|부족·운반·검산). 토질 3종 내역·다짐
환산·블록 수·장거리 운반·사토는 해당 칩 툴팁으로. B05·B06 공용 함수라 동시 반영.
- 설계속도 축 도입(임도는 속도를 낼 수 없는 노선 — 기본 20km/h):
· 임도 종류는 프로젝트 등록값(projects.road_type)을 읽어 B05가 읽기 전용 표시
(main→간선임도, fire→산불진화임도, 그 외→작업임도). 화면에서 고치지 않는다.
· 설계속도 선택(간선·산불진화 20/30/40, 작업 20 고정) 신설 — 종단기울기 상한과
종단곡선 반경이 등급이 아니라 이 값으로 정해진다(지식DB 설계제원_총괄 §6·§7).
· B02 임도 종류 선택지를 현행 3종으로 정리(지선 폐지·계류보전은 사방 구분).
· 백엔드: ROUTE_GRADE_CLASSES 3종+branch 호환, selectable_design_speeds 신설,
resolve_design_speed 신설, legal_grade_limit_pct·resolve_grade_options에
설계속도 인자, sections/context가 road_type 제공(B06도 저장값 승계).
· 계획선 정책은 그릴 때마다 현재 기준으로 동기화 — 저장분의 옛 상한이 남지 않는다.
- 검증: pytest 125 통과(설계속도 12건 신규), tsc, 헤드 브라우저 — 진입 시 상한 9%,
40km/h 전환 시 7% 즉시 반영, 원복 9%, 안내문 '간선임도 · 설계속도 20km/h ·
일반지형', 유토곡선 5칩, 유역 버튼 새 이름·툴팁, 3D 라벨 빈 문자열 확인.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,13 +57,15 @@ export function renderB02ProjRegister(root: HTMLElement): void {
|
|||||||
placeholder: L("B02_Proj_Field_Region_Placeholder"),
|
placeholder: L("B02_Proj_Field_Region_Placeholder"),
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
|
// 임도 종류 3종 — 현행 규칙 기준(지선임도 폐지, 계류보전은 사방사업 구분이라 제외).
|
||||||
|
// 이 값이 B05 계획선의 법정 기준(설계속도 선택지·종단기울기 상한)을 정한다
|
||||||
|
// (2026-08-19 사용자 확정).
|
||||||
const roadTypeField = createSelectField({
|
const roadTypeField = createSelectField({
|
||||||
label: L("B02_Proj_Field_RoadType"),
|
label: L("B02_Proj_Field_RoadType"),
|
||||||
options: [
|
options: [
|
||||||
{ value: "main", text: L("B02_Proj_RoadType_Main") },
|
{ value: "main", text: L("B02_Proj_RoadType_Main") },
|
||||||
{ value: "branch", text: L("B02_Proj_RoadType_Branch") },
|
|
||||||
{ value: "fire", text: L("B02_Proj_RoadType_Fire") },
|
{ value: "fire", text: L("B02_Proj_RoadType_Fire") },
|
||||||
{ value: "stream", text: L("B02_Proj_RoadType_Stream") },
|
{ value: "work", text: L("B02_Proj_RoadType_Work") },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export interface RouteSolveRequest {
|
|||||||
cross_sample_interval_m?: number | null;
|
cross_sample_interval_m?: number | null;
|
||||||
long_sample_interval_m?: number | null;
|
long_sample_interval_m?: number | null;
|
||||||
terrain_type?: string;
|
terrain_type?: string;
|
||||||
|
/** 설계속도(km/h) — 종단 법정 기준 축. 임도 기본 20(2026-08-19). */
|
||||||
|
design_speed_kph?: number | null;
|
||||||
main_direction?: string;
|
main_direction?: string;
|
||||||
max_grade_pct?: number | null;
|
max_grade_pct?: number | null;
|
||||||
min_vertical_radius_m?: number | null;
|
min_vertical_radius_m?: number | null;
|
||||||
|
|||||||
@@ -98,8 +98,25 @@ class GradeDesignOptions:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_design_speed(grade_class: str, design_speed_kph: int | None = None) -> int:
|
||||||
|
"""적용할 설계속도(km/h). 사용자가 고른 값이 있으면 그 값, 없으면 종류별 기본.
|
||||||
|
|
||||||
|
임도는 속도를 낼 수 없는 노선이라 기본은 20이고, 간선·산불진화만 30·40을 고를
|
||||||
|
수 있다(별표2, 2026-08-19 사용자 확정). 허용되지 않는 값이 오면 기본으로 돌린다.
|
||||||
|
"""
|
||||||
|
criteria = FOREST_ROAD_PROFILE_CRITERIA
|
||||||
|
default_speed = int(criteria["grade_to_design_speed"].get(grade_class, 20))
|
||||||
|
allowed = criteria["selectable_design_speeds"].get(grade_class, (20,))
|
||||||
|
if design_speed_kph in allowed:
|
||||||
|
return int(design_speed_kph)
|
||||||
|
return default_speed
|
||||||
|
|
||||||
|
|
||||||
def legal_grade_limit_pct(
|
def legal_grade_limit_pct(
|
||||||
grade_class: str, terrain_type: str = "normal", paved: bool = False
|
grade_class: str,
|
||||||
|
terrain_type: str = "normal",
|
||||||
|
paved: bool = False,
|
||||||
|
design_speed_kph: int | None = None,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""설계속도 × 지형 구분에 따른 법정 종단기울기 상한(%).
|
"""설계속도 × 지형 구분에 따른 법정 종단기울기 상한(%).
|
||||||
|
|
||||||
@@ -108,7 +125,7 @@ def legal_grade_limit_pct(
|
|||||||
"""
|
"""
|
||||||
criteria = FOREST_ROAD_PROFILE_CRITERIA
|
criteria = FOREST_ROAD_PROFILE_CRITERIA
|
||||||
terrain = terrain_type if terrain_type in GRADE_TERRAIN_TYPES else "normal"
|
terrain = terrain_type if terrain_type in GRADE_TERRAIN_TYPES else "normal"
|
||||||
speed = criteria["grade_to_design_speed"].get(grade_class, 20)
|
speed = resolve_design_speed(grade_class, design_speed_kph)
|
||||||
legal_max = float(criteria["design_speed"][speed]["max_grade_pct"][terrain])
|
legal_max = float(criteria["design_speed"][speed]["max_grade_pct"][terrain])
|
||||||
if paved and terrain == "special":
|
if paved and terrain == "special":
|
||||||
return float(criteria["paved_exception_grade_pct"])
|
return float(criteria["paved_exception_grade_pct"])
|
||||||
@@ -129,6 +146,7 @@ def resolve_grade_options(
|
|||||||
terrain_type: str = "normal",
|
terrain_type: str = "normal",
|
||||||
paved: bool = False,
|
paved: bool = False,
|
||||||
main_direction: str = "auto",
|
main_direction: str = "auto",
|
||||||
|
design_speed_kph: int | None = None,
|
||||||
requested: dict[str, Any] | None = None,
|
requested: dict[str, Any] | None = None,
|
||||||
stored: dict[str, Any] | None = None,
|
stored: dict[str, Any] | None = None,
|
||||||
) -> GradeDesignOptions:
|
) -> GradeDesignOptions:
|
||||||
@@ -142,10 +160,10 @@ def resolve_grade_options(
|
|||||||
stored = stored or {}
|
stored = stored or {}
|
||||||
criteria = FOREST_ROAD_PROFILE_CRITERIA
|
criteria = FOREST_ROAD_PROFILE_CRITERIA
|
||||||
terrain = terrain_type if terrain_type in GRADE_TERRAIN_TYPES else "normal"
|
terrain = terrain_type if terrain_type in GRADE_TERRAIN_TYPES else "normal"
|
||||||
speed = criteria["grade_to_design_speed"].get(grade_class, 20)
|
speed = resolve_design_speed(grade_class, design_speed_kph)
|
||||||
legal = criteria["design_speed"][speed]
|
legal = criteria["design_speed"][speed]
|
||||||
legal_max = float(legal["max_grade_pct"][terrain])
|
legal_max = float(legal["max_grade_pct"][terrain])
|
||||||
ceiling = legal_grade_limit_pct(grade_class, terrain, paved)
|
ceiling = legal_grade_limit_pct(grade_class, terrain, paved, design_speed_kph)
|
||||||
|
|
||||||
max_grade = float(_pick(requested.get("max_grade_pct"), stored.get("max_grade_pct"), legal_max))
|
max_grade = float(_pick(requested.get("max_grade_pct"), stored.get("max_grade_pct"), legal_max))
|
||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ def _grade_options(
|
|||||||
terrain_type=request.terrain_type,
|
terrain_type=request.terrain_type,
|
||||||
paved=request.paved,
|
paved=request.paved,
|
||||||
main_direction=request.main_direction,
|
main_direction=request.main_direction,
|
||||||
|
design_speed_kph=request.design_speed_kph,
|
||||||
requested=request.grade_options(),
|
requested=request.grade_options(),
|
||||||
stored=stored_grade_options,
|
stored=stored_grade_options,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ class RouteSolveRequest(BaseModel):
|
|||||||
|
|
||||||
# 종단 계획선(계획고) 설계 옵션. 빈 값은 null로 두어 config 기본값을 쓴다.
|
# 종단 계획선(계획고) 설계 옵션. 빈 값은 null로 두어 config 기본값을 쓴다.
|
||||||
terrain_type: str = Field(default="normal", description="지형 구분 (normal/special)")
|
terrain_type: str = Field(default="normal", description="지형 구분 (normal/special)")
|
||||||
|
# 설계속도(km/h) — 법정 종단 기준을 정하는 축이다. 임도는 속도를 낼 수 없어 20이
|
||||||
|
# 기본이며, 간선·산불진화만 30·40을 고를 수 있다(2026-08-19 사용자 확정).
|
||||||
|
# 미지정(None)이면 종류별 기본 설계속도(grade_to_design_speed)를 쓴다.
|
||||||
|
design_speed_kph: int | None = Field(default=None, description="설계속도 (20/30/40)")
|
||||||
main_direction: str = Field(
|
main_direction: str = Field(
|
||||||
default="auto", description="주 진행방향 (auto/ascending/descending/none)"
|
default="auto", description="주 진행방향 (auto/ascending/descending/none)"
|
||||||
)
|
)
|
||||||
@@ -108,6 +112,8 @@ class RouteSolveRequest(BaseModel):
|
|||||||
raise ValueError(f"임도 등급은 {ROUTE_GRADE_CLASSES} 중 하나여야 합니다.")
|
raise ValueError(f"임도 등급은 {ROUTE_GRADE_CLASSES} 중 하나여야 합니다.")
|
||||||
if self.algorithm not in ("dijkstra", "ridge_valley"):
|
if self.algorithm not in ("dijkstra", "ridge_valley"):
|
||||||
raise ValueError("경로 알고리즘은 dijkstra 또는 ridge_valley여야 합니다.")
|
raise ValueError("경로 알고리즘은 dijkstra 또는 ridge_valley여야 합니다.")
|
||||||
|
if self.design_speed_kph is not None and self.design_speed_kph not in (20, 30, 40):
|
||||||
|
raise ValueError("설계속도는 20/30/40 중 하나여야 합니다.")
|
||||||
if self.terrain_type not in GRADE_TERRAIN_TYPES:
|
if self.terrain_type not in GRADE_TERRAIN_TYPES:
|
||||||
raise ValueError(f"지형 구분은 {GRADE_TERRAIN_TYPES} 중 하나여야 합니다.")
|
raise ValueError(f"지형 구분은 {GRADE_TERRAIN_TYPES} 중 하나여야 합니다.")
|
||||||
if self.main_direction not in GRADE_MAIN_DIRECTIONS:
|
if self.main_direction not in GRADE_MAIN_DIRECTIONS:
|
||||||
@@ -142,6 +148,7 @@ class RouteSolveRequest(BaseModel):
|
|||||||
"grade_class": self.grade_class,
|
"grade_class": self.grade_class,
|
||||||
"paved": self.paved,
|
"paved": self.paved,
|
||||||
"terrain_type": self.terrain_type,
|
"terrain_type": self.terrain_type,
|
||||||
|
"design_speed_kph": self.design_speed_kph,
|
||||||
"main_direction": self.main_direction,
|
"main_direction": self.main_direction,
|
||||||
"min_curve_radius_m": self.min_curve_radius_m,
|
"min_curve_radius_m": self.min_curve_radius_m,
|
||||||
"max_uphill_grade": self.max_uphill_grade,
|
"max_uphill_grade": self.max_uphill_grade,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface DrainageChromeLabels {
|
|||||||
analyze: string;
|
analyze: string;
|
||||||
analyzeTip: string;
|
analyzeTip: string;
|
||||||
deleteSelected: string;
|
deleteSelected: string;
|
||||||
|
deleteSelectedTip: string;
|
||||||
auto: string;
|
auto: string;
|
||||||
autoTip: string;
|
autoTip: string;
|
||||||
imageAlt: string;
|
imageAlt: string;
|
||||||
@@ -85,6 +86,7 @@ export function createDrainageChrome(
|
|||||||
deleteButton.type = "button";
|
deleteButton.type = "button";
|
||||||
deleteButton.className = "b05-drainage__analyze b05-drainage__tool";
|
deleteButton.className = "b05-drainage__analyze b05-drainage__tool";
|
||||||
deleteButton.textContent = labels.deleteSelected;
|
deleteButton.textContent = labels.deleteSelected;
|
||||||
|
deleteButton.title = labels.deleteSelectedTip;
|
||||||
deleteButton.disabled = true;
|
deleteButton.disabled = true;
|
||||||
// 자동 제안으로 되돌리기 — 편집한 배관 배치를 버리고 백엔드 자동 제안으로 재산정.
|
// 자동 제안으로 되돌리기 — 편집한 배관 배치를 버리고 백엔드 자동 제안으로 재산정.
|
||||||
const autoButton = document.createElement("button");
|
const autoButton = document.createElement("button");
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
|||||||
analyze: L("B05_Drainage_Btn_Analyze"),
|
analyze: L("B05_Drainage_Btn_Analyze"),
|
||||||
analyzeTip: L("B05_Drainage_Btn_Analyze_Tip"),
|
analyzeTip: L("B05_Drainage_Btn_Analyze_Tip"),
|
||||||
deleteSelected: L("B05_Drainage_Btn_DeleteSelected"),
|
deleteSelected: L("B05_Drainage_Btn_DeleteSelected"),
|
||||||
|
deleteSelectedTip: L("B05_Drainage_Btn_DeleteSelected_Tip"),
|
||||||
auto: L("B05_Drainage_Btn_Auto"),
|
auto: L("B05_Drainage_Btn_Auto"),
|
||||||
autoTip: L("B05_Drainage_Btn_Auto_Tip"),
|
autoTip: L("B05_Drainage_Btn_Auto_Tip"),
|
||||||
imageAlt: L("B05_Drainage_ImageAlt"),
|
imageAlt: L("B05_Drainage_ImageAlt"),
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
restorePoints,
|
restorePoints,
|
||||||
routePoint,
|
routePoint,
|
||||||
toBounds,
|
toBounds,
|
||||||
|
toGradeClass,
|
||||||
} from "./B05_Profile_UI_Page_Helpers";
|
} from "./B05_Profile_UI_Page_Helpers";
|
||||||
|
|
||||||
function L(key: keyof typeof ui_locales): string {
|
function L(key: keyof typeof ui_locales): string {
|
||||||
@@ -341,6 +342,7 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
|||||||
crossSampleInterval: next.route_params?.cross_sample_interval_m ?? undefined,
|
crossSampleInterval: next.route_params?.cross_sample_interval_m ?? undefined,
|
||||||
longSampleInterval: next.route_params?.long_sample_interval_m ?? undefined,
|
longSampleInterval: next.route_params?.long_sample_interval_m ?? undefined,
|
||||||
terrainType: options.terrain_type as RoutePanelValues["terrainType"] | undefined,
|
terrainType: options.terrain_type as RoutePanelValues["terrainType"] | undefined,
|
||||||
|
designSpeed: options.design_speed_kph as RoutePanelValues["designSpeed"] | undefined,
|
||||||
maxGradePct: next.route_params?.max_grade_pct ?? undefined,
|
maxGradePct: next.route_params?.max_grade_pct ?? undefined,
|
||||||
minVerticalRadius: next.route_params?.min_vertical_radius_m ?? undefined,
|
minVerticalRadius: next.route_params?.min_vertical_radius_m ?? undefined,
|
||||||
minTangentLength: next.route_params?.min_tangent_length_m ?? undefined,
|
minTangentLength: next.route_params?.min_tangent_length_m ?? undefined,
|
||||||
@@ -387,6 +389,10 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
|||||||
function renderSections(detail: SectionDetailResponse, routeId?: number): void {
|
function renderSections(detail: SectionDetailResponse, routeId?: number): void {
|
||||||
currentSectionDetail = detail;
|
currentSectionDetail = detail;
|
||||||
profilePanel.render(detail, panel.values().stationInterval ?? undefined, routeId);
|
profilePanel.render(detail, panel.values().stationInterval ?? undefined, routeId);
|
||||||
|
// 저장된 계획선의 정책은 만들 당시 기준이라 지금 설정(임도 종류·설계속도·지형)과
|
||||||
|
// 다를 수 있다 — 그릴 때마다 현재 기준으로 맞춘다(2026-08-19). 이렇게 하지 않으면
|
||||||
|
// 진입 직후 상단 표시줄이 옛 상한을 그대로 보여 준다.
|
||||||
|
profilePanel.setGradeLimit(panel.gradeLimitPct());
|
||||||
profilePanel.setStationDisplay(panel.stationDisplayOffset());
|
profilePanel.setStationDisplay(panel.stationDisplayOffset());
|
||||||
profilePanel.setIrregularStations(bridge.irregularStations());
|
profilePanel.setIrregularStations(bridge.irregularStations());
|
||||||
renderStationLines(detail);
|
renderStationLines(detail);
|
||||||
@@ -514,6 +520,7 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
|||||||
cross_sample_interval_m: values.crossSampleInterval,
|
cross_sample_interval_m: values.crossSampleInterval,
|
||||||
long_sample_interval_m: values.longSampleInterval,
|
long_sample_interval_m: values.longSampleInterval,
|
||||||
terrain_type: values.terrainType,
|
terrain_type: values.terrainType,
|
||||||
|
design_speed_kph: values.designSpeed,
|
||||||
max_grade_pct: values.maxGradePct,
|
max_grade_pct: values.maxGradePct,
|
||||||
min_vertical_radius_m: values.minVerticalRadius,
|
min_vertical_radius_m: values.minVerticalRadius,
|
||||||
min_tangent_length_m: values.minTangentLength,
|
min_tangent_length_m: values.minTangentLength,
|
||||||
@@ -649,6 +656,10 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
|||||||
stationInterval: sectionContext.defaults.station_interval_m,
|
stationInterval: sectionContext.defaults.station_interval_m,
|
||||||
crossSampleInterval: sectionContext.defaults.cross_sample_interval_m,
|
crossSampleInterval: sectionContext.defaults.cross_sample_interval_m,
|
||||||
longSampleInterval: sectionContext.defaults.long_sample_interval_m,
|
longSampleInterval: sectionContext.defaults.long_sample_interval_m,
|
||||||
|
// 임도 종류는 프로젝트 등록(B02)이 정본이다 — 화면에서 고르지 않는다
|
||||||
|
// (2026-08-19 사용자 확정). B02의 main/branch/fire/stream을 계획선 기준
|
||||||
|
// 코드로 옮긴다: main=간선, fire=산불진화, 그 밖(구 지선·계류보전)은 작업.
|
||||||
|
gradeClass: toGradeClass(sectionContext.road_type),
|
||||||
});
|
});
|
||||||
// 유토곡선용 설정 — 토량환산계수·운반장비 경계·자연방토 판정 경사를 config_system
|
// 유토곡선용 설정 — 토량환산계수·운반장비 경계·자연방토 판정 경사를 config_system
|
||||||
// 정의 그대로 받아 B06과 같은 계산을 태운다(프론트 사본 금지).
|
// 정의 그대로 받아 B06과 같은 계산을 태운다(프론트 사본 금지).
|
||||||
|
|||||||
@@ -26,7 +26,19 @@ import {
|
|||||||
type GradeClass = RoutePanelValues["gradeClass"];
|
type GradeClass = RoutePanelValues["gradeClass"];
|
||||||
export type RoadWidths = Record<GradeClass, number>;
|
export type RoadWidths = Record<GradeClass, number>;
|
||||||
|
|
||||||
export const DEFAULT_ROAD_WIDTHS: RoadWidths = { trunk: 3, branch: 3, work: 2.5 };
|
/** 임도 종류별 유효너비(m) — 간선·산불진화 3m, 작업 2.5m(설계제원_총괄 §3).
|
||||||
|
* branch(지선)는 폐지됐지만 기존 저장분이 남아 있어 값은 유지한다(2026-08-19). */
|
||||||
|
export const DEFAULT_ROAD_WIDTHS: RoadWidths = { trunk: 3, fire: 3, branch: 3, work: 2.5 };
|
||||||
|
|
||||||
|
/** 프로젝트 등록(B02)의 임도 종류 값을 계획선 기준 코드로 옮긴다(2026-08-19).
|
||||||
|
* B02 값: main(간선)·fire(산불진화)·branch(구 지선)·stream(계류보전).
|
||||||
|
* 현행 규칙의 3종은 간선·산불진화·작업이고 지선은 폐지됐으므로, 간선·산불진화가
|
||||||
|
* 아닌 값은 모두 작업임도로 본다(별표2 부칙 경과조치와 같은 방향). */
|
||||||
|
export function toGradeClass(roadType: string | null | undefined): GradeClass {
|
||||||
|
if (roadType === "main" || roadType === "trunk") return "trunk";
|
||||||
|
if (roadType === "fire") return "fire";
|
||||||
|
return "work";
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchRoadWidths(projectId: string): Promise<RoadWidths> {
|
export async function fetchRoadWidths(projectId: string): Promise<RoadWidths> {
|
||||||
const response = await fetch(`/api/projects/${projectId}/sections/road-widths`);
|
const response = await fetch(`/api/projects/${projectId}/sections/road-widths`);
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
|||||||
export interface RoutePanelValues {
|
export interface RoutePanelValues {
|
||||||
contourInterval: number;
|
contourInterval: number;
|
||||||
algorithm: "dijkstra" | "ridge_valley";
|
algorithm: "dijkstra" | "ridge_valley";
|
||||||
gradeClass: "trunk" | "branch" | "work";
|
/** 임도 종류 — 현행 3종(간선·산불진화·작업). branch(지선)는 폐지됐으나 기존
|
||||||
|
* 저장분 호환을 위해 값으로는 받는다(2026-08-19). 값의 원천은 프로젝트 등록
|
||||||
|
* 정보(projects.road_type)이며 화면에서는 고르지 않는다. */
|
||||||
|
gradeClass: "trunk" | "fire" | "branch" | "work";
|
||||||
|
/** 설계속도(km/h) — 임도는 속도를 낼 수 없는 노선이라 기본 20이다. 간선·산불진화만
|
||||||
|
* 20~40에서 고를 수 있고 작업임도는 20 고정(별표2, 2026-08-19 사용자 확정). */
|
||||||
|
designSpeed: 20 | 30 | 40;
|
||||||
paved: boolean;
|
paved: boolean;
|
||||||
minCurveRadius: number | null;
|
minCurveRadius: number | null;
|
||||||
maxUphillGrade: number | null;
|
maxUphillGrade: number | null;
|
||||||
@@ -51,12 +57,28 @@ const STATION_INTERVAL_M = 20;
|
|||||||
* 평면 최소곡선반지름은 일반지형 60/30/15 (설계제원_총괄 §4).
|
* 평면 최소곡선반지름은 일반지형 60/30/15 (설계제원_총괄 §4).
|
||||||
* 값을 임의로 바꾸지 않고 남긴다 — 기본값 확정은 사용자 협의 영역이다. */
|
* 값을 임의로 바꾸지 않고 남긴다 — 기본값 확정은 사용자 협의 영역이다. */
|
||||||
const PROFILE_CRITERIA: Record<
|
const PROFILE_CRITERIA: Record<
|
||||||
RoutePanelValues["gradeClass"],
|
RoutePanelValues["designSpeed"],
|
||||||
{ speed: number; grade: Record<RoutePanelValues["terrainType"], number>; radius: number }
|
{ grade: Record<RoutePanelValues["terrainType"], number>; radius: number }
|
||||||
> = {
|
> = {
|
||||||
trunk: { speed: 40, grade: { normal: 7, special: 10 }, radius: 450 },
|
40: { grade: { normal: 7, special: 10 }, radius: 450 },
|
||||||
branch: { speed: 30, grade: { normal: 8, special: 12 }, radius: 250 },
|
30: { grade: { normal: 8, special: 12 }, radius: 250 },
|
||||||
work: { speed: 20, grade: { normal: 9, special: 14 }, radius: 100 },
|
20: { grade: { normal: 9, special: 14 }, radius: 100 },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 임도 종류별 표시 이름 — 프로젝트 등록(B02)과 같은 말을 쓴다(2026-08-19 확정). */
|
||||||
|
const ROAD_TYPE_LABELS: Record<string, string> = {
|
||||||
|
trunk: "간선임도",
|
||||||
|
fire: "산불진화임도",
|
||||||
|
work: "작업임도",
|
||||||
|
branch: "작업임도(구 지선)",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 종류별로 고를 수 있는 설계속도 — 작업임도는 20 이하라 20만 둔다(별표2 Ⅰ.3). */
|
||||||
|
const SPEED_CHOICES: Record<string, Array<RoutePanelValues["designSpeed"]>> = {
|
||||||
|
trunk: [20, 30, 40],
|
||||||
|
fire: [20, 30, 40],
|
||||||
|
work: [20],
|
||||||
|
branch: [20],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PanelCallbacks {
|
interface PanelCallbacks {
|
||||||
@@ -265,15 +287,30 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
const algorithm = algorithmField.select;
|
const algorithm = algorithmField.select;
|
||||||
|
// 임도 종류는 **프로젝트 등록(B02)에서 정한 값**을 그대로 쓴다 — 여기서 고치는
|
||||||
|
// 값이 아니다(2026-08-19 사용자 확정). 화면에는 읽기 전용으로 보여만 준다.
|
||||||
const gradeField = createSelectField({
|
const gradeField = createSelectField({
|
||||||
label: "임도 등급",
|
label: "임도 종류 (프로젝트 등록값)",
|
||||||
options: [
|
options: [
|
||||||
{ value: "trunk", text: "간선" },
|
{ value: "trunk", text: ROAD_TYPE_LABELS.trunk },
|
||||||
{ value: "branch", text: "지선" },
|
{ value: "fire", text: ROAD_TYPE_LABELS.fire },
|
||||||
{ value: "work", text: "작업" },
|
{ value: "work", text: ROAD_TYPE_LABELS.work },
|
||||||
|
{ value: "branch", text: ROAD_TYPE_LABELS.branch },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const gradeClass = gradeField.select;
|
const gradeClass = gradeField.select;
|
||||||
|
gradeClass.disabled = true;
|
||||||
|
gradeClass.title = "프로젝트 등록 정보의 임도 종류입니다. 변경은 대시보드에서 하세요.";
|
||||||
|
// 설계속도 — 임도는 속도를 낼 수 없어 기본 20km/h다. 종류에 따라 선택지가 갈린다.
|
||||||
|
const speedField = createSelectField({
|
||||||
|
label: "설계속도 (km/h)",
|
||||||
|
options: [20, 30, 40].map((value) => ({ value: String(value), text: `${value} km/h` })),
|
||||||
|
});
|
||||||
|
const designSpeed = speedField.select;
|
||||||
|
designSpeed.value = "20";
|
||||||
|
designSpeed.title =
|
||||||
|
"종단기울기·종단곡선의 법정 기준은 설계속도로 정해집니다(별표2).\n" +
|
||||||
|
"간선·산불진화임도는 20~40, 작업임도는 20만 쓸 수 있습니다.";
|
||||||
const minCurveRadius = numberField("최소 곡선반경 (m)");
|
const minCurveRadius = numberField("최소 곡선반경 (m)");
|
||||||
const maxUphillGrade = numberField("오르막 경사 상한 (%)");
|
const maxUphillGrade = numberField("오르막 경사 상한 (%)");
|
||||||
const maxDownhillGrade = numberField("내리막 경사 상한 (%)");
|
const maxDownhillGrade = numberField("내리막 경사 상한 (%)");
|
||||||
@@ -295,7 +332,6 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
|
|||||||
const solveButton = button("최적 경로 계산", callbacks.onSolve, "filled");
|
const solveButton = button("최적 경로 계산", callbacks.onSolve, "filled");
|
||||||
routeCalc.body.append(
|
routeCalc.body.append(
|
||||||
algorithmField.root,
|
algorithmField.root,
|
||||||
gradeField.root,
|
|
||||||
paved.wrapper,
|
paved.wrapper,
|
||||||
avoidPass.wrapper,
|
avoidPass.wrapper,
|
||||||
details,
|
details,
|
||||||
@@ -349,6 +385,10 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
|
|||||||
settingsDivider1,
|
settingsDivider1,
|
||||||
contourRow,
|
contourRow,
|
||||||
settingsDivider2,
|
settingsDivider2,
|
||||||
|
// 임도 종류(등록값) → 설계속도 → 지형 구분 순 — 법정 기준이 정해지는 순서다
|
||||||
|
// (2026-08-19 사용자 확정: 종류는 B02에서, 속도·지형은 여기서).
|
||||||
|
gradeField.root,
|
||||||
|
speedField.root,
|
||||||
terrainField.root,
|
terrainField.root,
|
||||||
criteriaNote,
|
criteriaNote,
|
||||||
gradeAdvanced,
|
gradeAdvanced,
|
||||||
@@ -393,30 +433,51 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
|
|||||||
/** 지금 적용되는 종단기울기 상한(%) — 사용자가 값을 넣었으면 그 값, 비웠으면
|
/** 지금 적용되는 종단기울기 상한(%) — 사용자가 값을 넣었으면 그 값, 비웠으면
|
||||||
* 등급·지형의 법정 기준. 종단 그래프의 위반 판정이 이 값을 즉시 따라온다
|
* 등급·지형의 법정 기준. 종단 그래프의 위반 판정이 이 값을 즉시 따라온다
|
||||||
* (2026-08-19 사용자 지시 6). 특수지형+포장 예외(18%)는 서버가 최종 판정한다. */
|
* (2026-08-19 사용자 지시 6). 특수지형+포장 예외(18%)는 서버가 최종 판정한다. */
|
||||||
|
function currentSpeed(): RoutePanelValues["designSpeed"] {
|
||||||
|
const value = Number(designSpeed.value);
|
||||||
|
return (value === 40 || value === 30 ? value : 20) as RoutePanelValues["designSpeed"];
|
||||||
|
}
|
||||||
|
|
||||||
function gradeLimitPct(): number {
|
function gradeLimitPct(): number {
|
||||||
const criteria = PROFILE_CRITERIA[gradeClass.value as RoutePanelValues["gradeClass"]];
|
const criteria = PROFILE_CRITERIA[currentSpeed()];
|
||||||
const terrain = terrainType.value as RoutePanelValues["terrainType"];
|
const terrain = terrainType.value as RoutePanelValues["terrainType"];
|
||||||
const typed = parseOptional(maxGradePct);
|
const typed = parseOptional(maxGradePct);
|
||||||
return typed != null && typed > 0 ? typed : criteria.grade[terrain];
|
return typed != null && typed > 0 ? typed : criteria.grade[terrain];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 등급·지형 선택에 맞춰 법정 기준값을 placeholder와 안내문에 반영한다. */
|
/** 임도 종류에 맞춰 설계속도 선택지를 갈아 끼운다 — 작업임도는 20만 쓸 수 있다. */
|
||||||
|
function syncSpeedChoices(): void {
|
||||||
|
const allowed = SPEED_CHOICES[gradeClass.value] ?? [20];
|
||||||
|
const keep = allowed.includes(currentSpeed()) ? currentSpeed() : allowed[0];
|
||||||
|
designSpeed.replaceChildren(
|
||||||
|
...allowed.map((value) => new Option(`${value} km/h`, String(value))),
|
||||||
|
);
|
||||||
|
designSpeed.value = String(keep);
|
||||||
|
// 고를 것이 하나뿐이면 잠근다 — 열어 두면 바꿀 수 있다는 오해를 준다.
|
||||||
|
designSpeed.disabled = allowed.length < 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 설계속도·지형 선택에 맞춰 법정 기준값을 placeholder와 안내문에 반영한다. */
|
||||||
function syncCriteria(): void {
|
function syncCriteria(): void {
|
||||||
const criteria = PROFILE_CRITERIA[gradeClass.value as RoutePanelValues["gradeClass"]];
|
const speed = currentSpeed();
|
||||||
|
const criteria = PROFILE_CRITERIA[speed];
|
||||||
const terrain = terrainType.value as RoutePanelValues["terrainType"];
|
const terrain = terrainType.value as RoutePanelValues["terrainType"];
|
||||||
maxGradePct.placeholder = String(criteria.grade[terrain]);
|
maxGradePct.placeholder = String(criteria.grade[terrain]);
|
||||||
minVerticalRadius.placeholder = String(criteria.radius);
|
minVerticalRadius.placeholder = String(criteria.radius);
|
||||||
minTangentLength.placeholder = "20";
|
minTangentLength.placeholder = "20";
|
||||||
|
const roadType = ROAD_TYPE_LABELS[gradeClass.value] ?? gradeClass.value;
|
||||||
criteriaNote.textContent =
|
criteriaNote.textContent =
|
||||||
`설계속도 ${criteria.speed}km/h 기준 — 종단기울기 ${criteria.grade[terrain]}% 이하, ` +
|
`${roadType} · 설계속도 ${speed}km/h · ${terrain === "special" ? "특수지형" : "일반지형"} 기준 — ` +
|
||||||
`종단곡선 반경 ${criteria.radius}m 이상. 비워두면 이 기준이 적용됩니다.`;
|
`종단기울기 ${criteria.grade[terrain]}% 이하, 종단곡선 반경 ${criteria.radius}m 이상. ` +
|
||||||
|
"비워두면 이 기준이 적용됩니다.";
|
||||||
}
|
}
|
||||||
// 지형·등급뿐 아니라 기울기 세부 입력까지 — 바꾸는 즉시 안내문과 위반 기준이
|
// 설계속도·지형뿐 아니라 기울기 세부 입력까지 — 바꾸는 즉시 안내문과 위반 기준이
|
||||||
// 따라온다(2026-08-19 사용자 지시 6). onInputChange가 그래프 쪽 갱신을 잇는다.
|
// 따라온다(2026-08-19 사용자 지시 6). onInputChange가 그래프 쪽 갱신을 잇는다.
|
||||||
[gradeClass, terrainType, maxGradePct].forEach((input) =>
|
[designSpeed, terrainType, maxGradePct].forEach((input) =>
|
||||||
input.addEventListener("change", syncCriteria),
|
input.addEventListener("change", syncCriteria),
|
||||||
);
|
);
|
||||||
maxGradePct.addEventListener("input", syncCriteria);
|
maxGradePct.addEventListener("input", syncCriteria);
|
||||||
|
syncSpeedChoices();
|
||||||
syncCriteria();
|
syncCriteria();
|
||||||
|
|
||||||
// 하단 고정 액션 행: [초기화][임시저장][횡단 이동] — 경로 확정 개념 폐지, 종·횡 통합
|
// 하단 고정 액션 행: [초기화][임시저장][횡단 이동] — 경로 확정 개념 폐지, 종·횡 통합
|
||||||
@@ -449,6 +510,7 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
|
|||||||
minDownhillGrade,
|
minDownhillGrade,
|
||||||
crossSampleInterval,
|
crossSampleInterval,
|
||||||
longSampleInterval,
|
longSampleInterval,
|
||||||
|
designSpeed,
|
||||||
terrainType,
|
terrainType,
|
||||||
maxGradePct,
|
maxGradePct,
|
||||||
minVerticalRadius,
|
minVerticalRadius,
|
||||||
@@ -496,6 +558,7 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
|
|||||||
stationInterval: STATION_INTERVAL_M,
|
stationInterval: STATION_INTERVAL_M,
|
||||||
crossSampleInterval: parseOptional(crossSampleInterval),
|
crossSampleInterval: parseOptional(crossSampleInterval),
|
||||||
longSampleInterval: parseOptional(longSampleInterval),
|
longSampleInterval: parseOptional(longSampleInterval),
|
||||||
|
designSpeed: currentSpeed(),
|
||||||
terrainType: terrainType.value as RoutePanelValues["terrainType"],
|
terrainType: terrainType.value as RoutePanelValues["terrainType"],
|
||||||
maxGradePct: parseOptional(maxGradePct),
|
maxGradePct: parseOptional(maxGradePct),
|
||||||
minVerticalRadius: parseOptional(minVerticalRadius),
|
minVerticalRadius: parseOptional(minVerticalRadius),
|
||||||
@@ -520,6 +583,11 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
|
|||||||
crossSampleInterval.value = String(values.crossSampleInterval);
|
crossSampleInterval.value = String(values.crossSampleInterval);
|
||||||
if (values.longSampleInterval != null)
|
if (values.longSampleInterval != null)
|
||||||
longSampleInterval.value = String(values.longSampleInterval);
|
longSampleInterval.value = String(values.longSampleInterval);
|
||||||
|
if (values.gradeClass) {
|
||||||
|
gradeClass.value = values.gradeClass;
|
||||||
|
syncSpeedChoices();
|
||||||
|
}
|
||||||
|
if (values.designSpeed) designSpeed.value = String(values.designSpeed);
|
||||||
if (values.terrainType) terrainType.value = values.terrainType;
|
if (values.terrainType) terrainType.value = values.terrainType;
|
||||||
if (values.maxGradePct != null) maxGradePct.value = String(values.maxGradePct);
|
if (values.maxGradePct != null) maxGradePct.value = String(values.maxGradePct);
|
||||||
if (values.minVerticalRadius != null)
|
if (values.minVerticalRadius != null)
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* B05_Profile_UI_Profile_Balance.ts
|
* B05_Profile_UI_Profile_Balance.ts
|
||||||
* 종단 패널 상단 균형 표시줄 — 절·성토 면적, 불균형률, 변화점·종단곡선 개수,
|
* 종단 패널 상단 표시줄 — **법정 판정에 쓰이는 값만** 남긴다(2026-08-19 재편).
|
||||||
* 종단기울기 위반 경고, [초기선 복원] 버튼, 미저장 배지.
|
*
|
||||||
|
* 표시 항목(별표2 근거):
|
||||||
|
* 최대 기울기 x.x% (상한 y%) — Ⅰ.2.라: 설계속도·지형별 상한 준수 판정
|
||||||
|
* 불균형 z% / 허용 w% — Ⅰ.1.나.(4)(다): 시공계획고 절·성토 균형
|
||||||
|
* 곡선 필요 n곳 — Ⅰ.2.마: 대수차 5% 초과 변화점의 종단곡선 삽입
|
||||||
|
* 절·성토 면적은 하단 유토곡선이 부피로 더 정확히 보여 주고, 변화점·종단곡선 개수와
|
||||||
|
* 기본 곡선길이 L은 판단 기준이 없어 뺐다(2026-08-19 사용자 지적 + 지식DB 대조).
|
||||||
|
* [저장선 복원]은 상단에서 내려 그래프 조작부로 옮겼다.
|
||||||
*
|
*
|
||||||
* 패널 본체(B05_Profile_UI_Profile_Panel)가 700줄 한계에 닿아 분리했다.
|
* 패널 본체(B05_Profile_UI_Profile_Panel)가 700줄 한계에 닿아 분리했다.
|
||||||
* 표시줄은 상태를 갖지 않는다 — 그릴 때마다 현재 선형·편집 상태를 인자로 받는다.
|
* 표시줄은 상태를 갖지 않는다 — 그릴 때마다 현재 선형·편집 상태를 인자로 받는다.
|
||||||
@@ -40,21 +47,30 @@ export function renderBalanceBar(params: BalanceBarParams): void {
|
|||||||
}
|
}
|
||||||
const { alignment } = params;
|
const { alignment } = params;
|
||||||
const { balance, policy, violations } = alignment;
|
const { balance, policy, violations } = alignment;
|
||||||
|
// 최대 종단기울기 — 법정 상한 대비가 이 화면의 첫 판정 항목이다(별표2 Ⅰ.2.라).
|
||||||
|
const steepest = alignment.segments.reduce(
|
||||||
|
(worst, segment) => Math.max(worst, Math.abs(segment.grade_percent)),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
// 종단곡선이 필요한데 빠진 변화점 — 생략은 비포장 & 대수차 5% 이하만 허용된다
|
||||||
|
// (별표2 Ⅰ.2.마 / 지식DB 종단선형 §3). skip_allowed가 그 판정 결과다.
|
||||||
|
const curvesNeeded = alignment.curves.filter(
|
||||||
|
(curve) => curve.omitted && !curve.skip_allowed,
|
||||||
|
).length;
|
||||||
const entries: Array<[string, string, string?]> = [
|
const entries: Array<[string, string, string?]> = [
|
||||||
["절토", `${balance.cut_area_m2.toFixed(1)} m²`, "cut"],
|
|
||||||
["성토", `${balance.fill_area_m2.toFixed(1)} m²`, "fill"],
|
|
||||||
[
|
[
|
||||||
"불균형",
|
"최대 기울기",
|
||||||
|
`${steepest.toFixed(1)} % / 상한 ${policy.max_grade_pct.toFixed(1)} %`,
|
||||||
|
violations.length ? "over" : undefined,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"절·성토 불균형",
|
||||||
`${balance.imbalance_percent.toFixed(1)} % / 허용 ${balance.tolerance_percent.toFixed(0)} %`,
|
`${balance.imbalance_percent.toFixed(1)} % / 허용 ${balance.tolerance_percent.toFixed(0)} %`,
|
||||||
balance.within_tolerance ? undefined : "over",
|
balance.within_tolerance ? undefined : "over",
|
||||||
],
|
],
|
||||||
["변화점", `${alignment.pvi.length} 개`],
|
|
||||||
["종단곡선", `${alignment.curves.filter((curve) => !curve.omitted).length} 개`],
|
|
||||||
// 기준은 길이 L이다. 옛 저장분(L 없음)만 그 시절 기준인 R을 그대로 밝혀 적는다.
|
|
||||||
Number.isFinite(policy.default_curve_length_m)
|
|
||||||
? ["기본 곡선길이 L", `${(policy.default_curve_length_m as number).toFixed(1)} m`]
|
|
||||||
: ["기본 R(옛 저장분)", `${policy.default_curve_radius_m.toFixed(1)} m`],
|
|
||||||
];
|
];
|
||||||
|
// 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다.
|
||||||
|
if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded} 곳`, "over"]);
|
||||||
const editedCount = Object.keys(alignment.edits.station_offsets).length;
|
const editedCount = Object.keys(alignment.edits.station_offsets).length;
|
||||||
if (editedCount) entries.push(["편집 측점", `${editedCount} 개`, "edited"]);
|
if (editedCount) entries.push(["편집 측점", `${editedCount} 개`, "edited"]);
|
||||||
entries.forEach(([label, value, tone]) => {
|
entries.forEach(([label, value, tone]) => {
|
||||||
@@ -63,27 +79,31 @@ export function renderBalanceBar(params: BalanceBarParams): void {
|
|||||||
const caption = document.createElement("em");
|
const caption = document.createElement("em");
|
||||||
caption.textContent = label;
|
caption.textContent = label;
|
||||||
item.append(caption, document.createTextNode(value));
|
item.append(caption, document.createTextNode(value));
|
||||||
|
// 상한 초과 구간의 내역은 별도 경고 칩 대신 이 항목의 툴팁으로 붙인다 —
|
||||||
|
// 같은 사실을 두 번 적지 않는다(2026-08-19 재편).
|
||||||
|
if (label === "최대 기울기" && violations.length) {
|
||||||
|
item.title = violations
|
||||||
|
.map(
|
||||||
|
(entry) =>
|
||||||
|
`구간 ${entry.segment_index + 1}: ${entry.value.toFixed(2)}% > ${entry.limit}%`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
params.balanceBar.append(item);
|
params.balanceBar.append(item);
|
||||||
});
|
});
|
||||||
if (violations.length) {
|
|
||||||
const warning = document.createElement("span");
|
|
||||||
warning.className = "b05-route-profile__balance-warning";
|
|
||||||
warning.textContent = `⚠ 종단기울기 초과 ${violations.length}개 구간`;
|
|
||||||
warning.title = violations
|
|
||||||
.map((item) => `구간 ${item.segment_index + 1}: ${item.value.toFixed(2)}% > ${item.limit}%`)
|
|
||||||
.join("\n");
|
|
||||||
params.balanceBar.append(warning);
|
|
||||||
}
|
|
||||||
if (params.edited || params.hasIrregularStations) {
|
if (params.edited || params.hasIrregularStations) {
|
||||||
const reset = document.createElement("button");
|
const reset = document.createElement("button");
|
||||||
reset.type = "button";
|
reset.type = "button";
|
||||||
reset.className = "b05-route-profile__balance-reset";
|
reset.className = "b05-route-profile__balance-reset";
|
||||||
// 복원 목표는 **마지막 저장된 계획선**이다 — 임시저장을 거치면 편집이 정본에
|
// 이 버튼이 지우는 것은 **화면에 쌓인 편집 델타**다(계획선 변화점 이동·비정규
|
||||||
// 반영되므로 최초 자동 계산선과 다를 수 있다(2026-08-19 사용자 확인). 최초
|
// 측점). 임시저장을 거치면 그 편집이 정본에 반영되므로 결과적으로 "마지막 저장
|
||||||
// 파이프라인 결과로의 완전 복귀는 좌측 하단 [초기화](B05·B06 재계산)가 맡는다.
|
// 상태"가 되는 것뿐, 저장 지점으로 되돌아가는 기능이 아니다. 최초 자동 계산
|
||||||
reset.textContent = "저장선 복원";
|
// 상태로의 복귀는 좌측 하단 [초기화](B05·B06 재계산)가 맡는다(2026-08-19 정정).
|
||||||
|
// 자리도 값 칩들 뒤 — 편집 상태(미저장 배지) 옆이 맥락에 맞다.
|
||||||
|
reset.textContent = "편집 되돌리기";
|
||||||
reset.title =
|
reset.title =
|
||||||
"저장 후의 편집과 추가한 비정규 측점을 지우고 마지막 저장된 계획선으로 되돌립니다.\n" +
|
"화면에서 수정한 계획선 변화점과 추가한 비정규 측점을 지웁니다.\n" +
|
||||||
|
"이미 임시저장한 내용은 정본에 반영돼 있어 그대로 남습니다.\n" +
|
||||||
"최초 자동 계산 상태로 되돌리려면 좌측 하단 [초기화]를 쓰세요.";
|
"최초 자동 계산 상태로 되돌리려면 좌측 하단 [초기화]를 쓰세요.";
|
||||||
reset.addEventListener("click", () => {
|
reset.addEventListener("click", () => {
|
||||||
params.onResetAll();
|
params.onResetAll();
|
||||||
|
|||||||
@@ -411,7 +411,9 @@ export function createRouteViewer(): RouteViewer {
|
|||||||
fit("top");
|
fit("top");
|
||||||
markers.renderMarkers();
|
markers.renderMarkers();
|
||||||
await reloadContours(interval);
|
await reloadContours(interval);
|
||||||
status.textContent = "지형을 클릭하거나 팔레트 포인트를 드래그해 배치하세요.";
|
// 로딩이 끝나면 안내문을 지운다 — 조작법 설명이 화면에 계속 떠 있을 이유가
|
||||||
|
// 없다(2026-08-19 사용자 지시). 로딩·이동 중 안내는 그대로 쓴다.
|
||||||
|
status.textContent = "";
|
||||||
},
|
},
|
||||||
reloadContours,
|
reloadContours,
|
||||||
setSurfaceVisible(visible) {
|
setSurfaceVisible(visible) {
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ export interface SectionContextResponse {
|
|||||||
method: string | null;
|
method: string | null;
|
||||||
smooth: boolean | null;
|
smooth: boolean | null;
|
||||||
crs_epsg: number | null;
|
crs_epsg: number | null;
|
||||||
|
/** 프로젝트 등록(B02)에서 정한 임도 종류 — B05 계획선 법정 기준의 출발점(2026-08-19). */
|
||||||
|
road_type: string | null;
|
||||||
defaults: SectionOptionDefaults;
|
defaults: SectionOptionDefaults;
|
||||||
/** 표준 횡단면 설정 패널(토사/암/포장) 기본값. */
|
/** 표준 횡단면 설정 패널(토사/암/포장) 기본값. */
|
||||||
standard_cross_section: StandardCrossSection;
|
standard_cross_section: StandardCrossSection;
|
||||||
|
|||||||
@@ -79,6 +79,13 @@ async def get_section_context(project_id: UUID) -> SectionContextResponse | JSON
|
|||||||
async with pool.acquire() as connection:
|
async with pool.acquire() as connection:
|
||||||
route_context = await get_confirmed_route_context(connection, project_id)
|
route_context = await get_confirmed_route_context(connection, project_id)
|
||||||
surface_params = await get_surface_confirmation_params(connection, str(project_id))
|
surface_params = await get_surface_confirmation_params(connection, str(project_id))
|
||||||
|
# 임도 종류(projects.road_type) — B05가 계획선 법정 기준을 정하는 데 쓴다.
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"SELECT road_type FROM projects WHERE id = %s", (str(project_id),)
|
||||||
|
)
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
road_type = row[0] if row else None
|
||||||
|
|
||||||
defaults = SectionGenerationOptions()
|
defaults = SectionGenerationOptions()
|
||||||
return SectionContextResponse(
|
return SectionContextResponse(
|
||||||
@@ -88,6 +95,7 @@ async def get_section_context(project_id: UUID) -> SectionContextResponse | JSON
|
|||||||
method=surface_params["method"] if route_context else None,
|
method=surface_params["method"] if route_context else None,
|
||||||
smooth=bool(surface_params["smooth"]) if route_context else None,
|
smooth=bool(surface_params["smooth"]) if route_context else None,
|
||||||
crs_epsg=route_context["crs_epsg"] if route_context else None,
|
crs_epsg=route_context["crs_epsg"] if route_context else None,
|
||||||
|
road_type=road_type,
|
||||||
defaults=SectionOptionDefaults(
|
defaults=SectionOptionDefaults(
|
||||||
station_interval_m=defaults.station_interval_m,
|
station_interval_m=defaults.station_interval_m,
|
||||||
cross_half_width_m=defaults.cross_half_width_m,
|
cross_half_width_m=defaults.cross_half_width_m,
|
||||||
@@ -327,10 +335,13 @@ def _regeneration_grade_options(stage_params: dict[str, Any] | None) -> GradeDes
|
|||||||
)
|
)
|
||||||
if stage_params.get(key) is not None
|
if stage_params.get(key) is not None
|
||||||
}
|
}
|
||||||
|
# 설계속도는 B05가 저장해 둔 값을 그대로 따른다 — 없으면 종류별 기본(20).
|
||||||
|
speed = options.get("design_speed_kph")
|
||||||
return resolve_grade_options(
|
return resolve_grade_options(
|
||||||
str(grade_class),
|
str(grade_class),
|
||||||
terrain_type=str(options.get("terrain_type") or "normal"),
|
terrain_type=str(options.get("terrain_type") or "normal"),
|
||||||
paved=bool(options.get("paved", False)),
|
paved=bool(options.get("paved", False)),
|
||||||
|
design_speed_kph=int(speed) if speed is not None else None,
|
||||||
stored=stored,
|
stored=stored,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ class SectionContextResponse(BaseModel):
|
|||||||
method: str | None = None
|
method: str | None = None
|
||||||
smooth: bool | None = None
|
smooth: bool | None = None
|
||||||
crs_epsg: int | None = None
|
crs_epsg: int | None = None
|
||||||
|
# 프로젝트 등록(B02)에서 정한 임도 종류 — B05 계획선 법정 기준의 출발점이다.
|
||||||
|
# 화면은 이 값을 읽기만 하고 고치지 않는다(2026-08-19 사용자 확정).
|
||||||
|
road_type: str | None = None
|
||||||
defaults: SectionOptionDefaults
|
defaults: SectionOptionDefaults
|
||||||
# 표준 횡단면 설정 패널(토사/암/포장) 기본값. config STANDARD_CROSS_SECTION 사본.
|
# 표준 횡단면 설정 패널(토사/암/포장) 기본값. config STANDARD_CROSS_SECTION 사본.
|
||||||
standard_cross_section: dict[str, Any] = Field(default_factory=dict)
|
standard_cross_section: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ export function appendBalanceLayer(svg: SVGSVGElement, plan: HaulPlan, box: Bala
|
|||||||
* 사토·토취는 요약줄이 이미 곡선 기준으로 적고 있고 계단 단차의 합이 그 값과 같으므로
|
* 사토·토취는 요약줄이 이미 곡선 기준으로 적고 있고 계단 단차의 합이 그 값과 같으므로
|
||||||
* 여기서 또 적지 않는다(같은 수치가 두 번 뜨면 서로 다른 값으로 오해한다).
|
* 여기서 또 적지 않는다(같은 수치가 두 번 뜨면 서로 다른 값으로 오해한다).
|
||||||
*/
|
*/
|
||||||
export function haulPlanChips(plan: HaulPlan | null): Array<[string, string]> {
|
export function haulPlanChips(plan: HaulPlan | null): Array<[string, string, string?]> {
|
||||||
if (!plan) return [];
|
if (!plan) return [];
|
||||||
const bands = plan.blocks.reduce((sum, block) => sum + block.bands.length, 0);
|
const bands = plan.blocks.reduce((sum, block) => sum + block.bands.length, 0);
|
||||||
const moved = plan.hauled_m3 + plan.transferred_m3;
|
const moved = plan.hauled_m3 + plan.transferred_m3;
|
||||||
@@ -500,16 +500,18 @@ export function haulPlanChips(plan: HaulPlan | null): Array<[string, string]> {
|
|||||||
// (그 노선은 절토 우세라 토취가 0이었다).
|
// (그 노선은 절토 우세라 토취가 0이었다).
|
||||||
const gap = moved + plan.borrow_m3 - plan.fill_total_m3;
|
const gap = moved + plan.borrow_m3 - plan.fill_total_m3;
|
||||||
const ratio = plan.fill_total_m3 > 0 ? Math.abs(gap) / plan.fill_total_m3 : 0;
|
const ratio = plan.fill_total_m3 > 0 ? Math.abs(gap) / plan.fill_total_m3 : 0;
|
||||||
const chips: Array<[string, string]> = [
|
/* 요약 줄 압축(2026-08-19) — 블록·띠 수와 장거리 운반·사토는 칩으로 두지 않고
|
||||||
[L("B06_MassHaul_HaulTotal"), `${compactVolume(moved)}㎥`],
|
* 운반 총량 칩의 툴팁으로 내린다. 판단에 쓰는 값은 "얼마나 옮기는가"와 검산이다. */
|
||||||
[L("B06_MassHaul_BlockCount"), `${plan.blocks.length} / ${bands}`],
|
const haulTip = [`${L("B06_MassHaul_BlockCount")} ${plan.blocks.length} / ${bands}`];
|
||||||
];
|
|
||||||
if (plan.transfers.length) {
|
if (plan.transfers.length) {
|
||||||
chips.push([L("B06_MassHaul_Transfer"), `${compactVolume(plan.transferred_m3)}㎥`]);
|
haulTip.push(`${L("B06_MassHaul_Transfer")} ${compactVolume(plan.transferred_m3)}㎥`);
|
||||||
}
|
}
|
||||||
if (plan.natural_spoil_m3 > 0.5) {
|
if (plan.natural_spoil_m3 > 0.5) {
|
||||||
chips.push([L("B06_MassHaul_NaturalSpoil"), `${compactVolume(plan.natural_spoil_m3)}㎥`]);
|
haulTip.push(`${L("B06_MassHaul_NaturalSpoil")} ${compactVolume(plan.natural_spoil_m3)}㎥`);
|
||||||
}
|
}
|
||||||
|
const chips: Array<[string, string, string?]> = [
|
||||||
|
[L("B06_MassHaul_HaulTotal"), `${compactVolume(moved)}㎥`, haulTip.join(" · ")],
|
||||||
|
];
|
||||||
chips.push([
|
chips.push([
|
||||||
L("B06_MassHaul_Check"),
|
L("B06_MassHaul_Check"),
|
||||||
ratio <= 0.005
|
ratio <= 0.005
|
||||||
|
|||||||
@@ -632,16 +632,29 @@ export function createMassHaulSummary(
|
|||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "b06-masshaul__summary";
|
row.className = "b06-masshaul__summary";
|
||||||
const cut = result.cut_natural_m3;
|
const cut = result.cut_natural_m3;
|
||||||
const chips: Array<[string, string]> = [
|
const cutTotal = cut.soil + cut.ripping_rock + cut.blasting_rock;
|
||||||
|
/* 요약 줄이 12칩까지 늘어 한눈에 안 읽혔다(2026-08-19 사용자 지적). 판단에 쓰는
|
||||||
|
* 값만 칩으로 남기고, 세부 내역은 그 칩의 툴팁으로 내린다.
|
||||||
|
* 절토(자연) — 토질 3종 내역은 툴팁
|
||||||
|
* 성토(다짐) — 절토 다짐 환산은 툴팁(토량환산계수 f 적용 결과)
|
||||||
|
* 잉여 또는 부족 — 둘은 동시에 성립하지 않는다
|
||||||
|
* 운반·검산 — 토량 분배를 켰을 때만(haulPlanChips)
|
||||||
|
* B05 종단 패널과 B06 횡단 화면이 같은 함수를 쓰므로 양쪽이 함께 바뀐다. */
|
||||||
|
const chips: Array<[string, string, string?]> = [
|
||||||
[
|
[
|
||||||
L("B06_MassHaul_CutNatural"),
|
L("B06_MassHaul_CutNatural"),
|
||||||
`${formatVolume(cut.soil + cut.ripping_rock + cut.blasting_rock)}㎥`,
|
`${formatVolume(cutTotal)}㎥`,
|
||||||
|
[
|
||||||
|
`${L("B06_Design_Ground_Soil")} ${formatVolume(cut.soil)}㎥`,
|
||||||
|
`${L("B06_Design_Ground_Ripping")} ${formatVolume(cut.ripping_rock)}㎥`,
|
||||||
|
`${L("B06_Design_Ground_Blasting")} ${formatVolume(cut.blasting_rock)}㎥`,
|
||||||
|
].join(" · "),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
L("B06_MassHaul_Fill"),
|
||||||
|
`${formatVolume(result.fill_compacted_m3)}㎥`,
|
||||||
|
`${L("B06_MassHaul_CutCompacted")} ${formatVolume(result.cut_compacted_m3)}㎥`,
|
||||||
],
|
],
|
||||||
[L("B06_Design_Ground_Soil"), `${formatVolume(cut.soil)}㎥`],
|
|
||||||
[L("B06_Design_Ground_Ripping"), `${formatVolume(cut.ripping_rock)}㎥`],
|
|
||||||
[L("B06_Design_Ground_Blasting"), `${formatVolume(cut.blasting_rock)}㎥`],
|
|
||||||
[L("B06_MassHaul_CutCompacted"), `${formatVolume(result.cut_compacted_m3)}㎥`],
|
|
||||||
[L("B06_MassHaul_Fill"), `${formatVolume(result.fill_compacted_m3)}㎥`],
|
|
||||||
];
|
];
|
||||||
// 잉여와 부족은 동시에 성립하지 않으므로 해당하는 쪽만 보여 준다.
|
// 잉여와 부족은 동시에 성립하지 않으므로 해당하는 쪽만 보여 준다.
|
||||||
chips.push(
|
chips.push(
|
||||||
@@ -649,11 +662,12 @@ export function createMassHaulSummary(
|
|||||||
? [L("B06_MassHaul_Shortage"), `${formatVolume(result.shortage_m3)}㎥`]
|
? [L("B06_MassHaul_Shortage"), `${formatVolume(result.shortage_m3)}㎥`]
|
||||||
: [L("B06_MassHaul_Surplus"), `${formatVolume(result.surplus_m3)}㎥`],
|
: [L("B06_MassHaul_Surplus"), `${formatVolume(result.surplus_m3)}㎥`],
|
||||||
);
|
);
|
||||||
// 토량 분배를 켜 두면 운반 총량·블록 수가 뒤에 붙는다(사토·토취는 발생 지점 기준 합계).
|
// 토량 분배를 켜 두면 운반 총량·검산이 뒤에 붙는다(사토·토취는 발생 지점 기준 합계).
|
||||||
chips.push(...haulPlanChips(haulPlan));
|
chips.push(...haulPlanChips(haulPlan));
|
||||||
for (const [label, value] of chips) {
|
for (const [label, value, tip] of chips) {
|
||||||
const chip = document.createElement("span");
|
const chip = document.createElement("span");
|
||||||
chip.className = "b06-masshaul__chip";
|
chip.className = "b06-masshaul__chip";
|
||||||
|
if (tip) chip.title = tip;
|
||||||
const name = document.createElement("em");
|
const name = document.createElement("em");
|
||||||
name.textContent = label;
|
name.textContent = label;
|
||||||
const amount = document.createElement("strong");
|
const amount = document.createElement("strong");
|
||||||
|
|||||||
+15
-6
@@ -243,11 +243,13 @@ ROUTE_AVOID_DEFAULT_RADIUS_M = float(os.getenv("ROUTE_AVOID_DEFAULT_RADIUS_M", "
|
|||||||
ROUTE_DEFAULT_GRADE_CLASS = os.getenv("ROUTE_DEFAULT_GRADE_CLASS", "trunk")
|
ROUTE_DEFAULT_GRADE_CLASS = os.getenv("ROUTE_DEFAULT_GRADE_CLASS", "trunk")
|
||||||
ROUTE_MAX_COST_CELLS = int(os.getenv("ROUTE_MAX_COST_CELLS", "4000000"))
|
ROUTE_MAX_COST_CELLS = int(os.getenv("ROUTE_MAX_COST_CELLS", "4000000"))
|
||||||
ROUTE_REQUIRED_POINT_TOLERANCE_M = float(os.getenv("ROUTE_REQUIRED_POINT_TOLERANCE_M", "1.0"))
|
ROUTE_REQUIRED_POINT_TOLERANCE_M = float(os.getenv("ROUTE_REQUIRED_POINT_TOLERANCE_M", "1.0"))
|
||||||
ROUTE_GRADE_CLASSES = ("trunk", "branch", "work")
|
# 임도 종류 — 현행 규칙(별표2)의 3종. `branch`(지선)는 규칙에서 폐지됐으나 기존
|
||||||
|
# 저장분이 남아 있어 값으로는 계속 받는다(화면 선택지에서는 뺀다, 2026-08-19).
|
||||||
|
ROUTE_GRADE_CLASSES = ("trunk", "fire", "work", "branch")
|
||||||
|
|
||||||
# 임도 등급별 종단경사 한계 및 최소 곡선반지름
|
# 임도 등급별 종단경사 한계 및 최소 곡선반지름 (경로탐색 제약 — 계획선 법정 기준과 별개)
|
||||||
FOREST_ROAD_MAX_GRADE = {"trunk": 0.26, "branch": 0.28, "work": 0.40}
|
FOREST_ROAD_MAX_GRADE = {"trunk": 0.26, "fire": 0.26, "branch": 0.28, "work": 0.40}
|
||||||
FOREST_ROAD_MIN_CURVE_R_M = {"trunk": 12.0, "branch": 10.0, "work": 6.0}
|
FOREST_ROAD_MIN_CURVE_R_M = {"trunk": 12.0, "fire": 12.0, "branch": 10.0, "work": 6.0}
|
||||||
|
|
||||||
# 대안(정속경사) 파라미터
|
# 대안(정속경사) 파라미터
|
||||||
ROUTE_ALT_MIN_GRADE = float(os.getenv("ROUTE_ALT_MIN_GRADE", "0.08"))
|
ROUTE_ALT_MIN_GRADE = float(os.getenv("ROUTE_ALT_MIN_GRADE", "0.08"))
|
||||||
@@ -640,8 +642,15 @@ FOREST_ROAD_PROFILE_CRITERIA = {
|
|||||||
"min_curve_length_m": 20.0,
|
"min_curve_length_m": 20.0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# 임도등급 → 기본 설계속도. 작업임도는 별표에 종단 기준이 없어 20km/h를 준용한다.
|
# 임도 종류 → **기본** 설계속도(km/h). 임도는 속도를 낼 수 없는 노선이라 20이
|
||||||
"grade_to_design_speed": {"trunk": 40, "branch": 30, "work": 20},
|
# 기본이다(2026-08-19 사용자 확정). 별표2상 간선·산불진화는 20~40 범위에서
|
||||||
|
# 설계자가 고르고, 작업임도는 20 이하이므로 20 고정이다. 사용자가 화면에서 고른
|
||||||
|
# 설계속도가 오면 그 값이 우선한다(design_speed_kph).
|
||||||
|
# trunk = 간선임도 / fire = 산불진화임도 / work = 작업임도
|
||||||
|
# branch(지선)는 현행 규칙에서 폐지 — 기존 데이터 호환용으로만 남긴다.
|
||||||
|
"grade_to_design_speed": {"trunk": 20, "fire": 20, "work": 20, "branch": 20},
|
||||||
|
# 화면에서 고를 수 있는 설계속도 — 작업임도는 20만 허용된다(별표2 Ⅰ.3).
|
||||||
|
"selectable_design_speeds": {"trunk": (20, 30, 40), "fire": (20, 30, 40), "work": (20,)},
|
||||||
# 특수지형에서 기준 적용이 어려운 경우 노면포장 시에 한하여 허용되는 상한
|
# 특수지형에서 기준 적용이 어려운 경우 노면포장 시에 한하여 허용되는 상한
|
||||||
"paved_exception_grade_pct": 18.0,
|
"paved_exception_grade_pct": 18.0,
|
||||||
# 비포장 도로이면서 종단기울기 대수차가 이 값 이하이면 종단곡선을 두지 않는다
|
# 비포장 도로이면서 종단기울기 대수차가 이 값 이하이면 종단곡선을 두지 않는다
|
||||||
|
|||||||
@@ -200,10 +200,11 @@ export const ui_locales_b1 = {
|
|||||||
B02_Proj_Field_Region: ["사업 지역", "Region"],
|
B02_Proj_Field_Region: ["사업 지역", "Region"],
|
||||||
B02_Proj_Field_Region_Placeholder: ["예: 울진군 금강송면", "e.g. Uljin-gun"],
|
B02_Proj_Field_Region_Placeholder: ["예: 울진군 금강송면", "e.g. Uljin-gun"],
|
||||||
B02_Proj_Field_RoadType: ["임도 종류", "Road type"],
|
B02_Proj_Field_RoadType: ["임도 종류", "Road type"],
|
||||||
B02_Proj_RoadType_Main: ["간선임도", "Main road"],
|
// 현행 「임도설치 및 관리 등에 관한 규정」의 3종 — 지선임도는 폐지(작업임도로
|
||||||
B02_Proj_RoadType_Branch: ["지선임도", "Branch road"],
|
// 간주), 계류보전은 임도 종류가 아니라 사방사업 구분이라 뺐다(2026-08-19 확정).
|
||||||
B02_Proj_RoadType_Fire: ["산불진화임도", "Fire-suppression road"],
|
B02_Proj_RoadType_Main: ["간선임도", "Main forest road"],
|
||||||
B02_Proj_RoadType_Stream: ["계류보전", "Stream conservation"],
|
B02_Proj_RoadType_Fire: ["산불진화임도", "Fire-suppression forest road"],
|
||||||
|
B02_Proj_RoadType_Work: ["작업임도", "Work forest road"],
|
||||||
B02_Proj_Field_Year: ["사업 연도", "Project year"],
|
B02_Proj_Field_Year: ["사업 연도", "Project year"],
|
||||||
B02_Proj_Field_Length: ["예상 연장 (m)", "Estimated length (m)"],
|
B02_Proj_Field_Length: ["예상 연장 (m)", "Estimated length (m)"],
|
||||||
B02_Proj_Field_Length_Placeholder: ["예상 노선 길이", "Estimated route length"],
|
B02_Proj_Field_Length_Placeholder: ["예상 노선 길이", "Estimated route length"],
|
||||||
|
|||||||
@@ -30,16 +30,22 @@ export const ui_locales_b2 = {
|
|||||||
"유역 안쪽 상류 세류망을 굵게 강조합니다.",
|
"유역 안쪽 상류 세류망을 굵게 강조합니다.",
|
||||||
"Emphasize the upstream stream network inside the basin.",
|
"Emphasize the upstream stream network inside the basin.",
|
||||||
],
|
],
|
||||||
B05_Drainage_Btn_Analyze: ["세부유역 산정", "Compute sub-basins"],
|
// 버튼 이름만으로 무엇이 바뀌는지 알기 어려워 동작을 이름·툴팁에 함께 적는다
|
||||||
|
// (2026-08-19 사용자 지시 4).
|
||||||
|
B05_Drainage_Btn_Analyze: ["유역 다시 나누기", "Recompute sub-basins"],
|
||||||
B05_Drainage_Btn_Analyze_Tip: [
|
B05_Drainage_Btn_Analyze_Tip: [
|
||||||
"B04에서 분석해 둔 배수유역을 불러와 관을 보충하고 세부유역을 나눕니다. 분석 결과가 없으면 B04에서 먼저 실행해야 합니다.",
|
"B04에서 분석해 둔 배수유역을 불러와, 지금 배치된 배관을 기준으로 세부유역(관이 담당하는 구역)을 다시 나눕니다. 관이 부족한 구간은 자동으로 보충합니다. B04 분석 결과가 없으면 B04에서 먼저 실행해야 합니다.",
|
||||||
"Loads the drainage analysis from B04, fills in culverts, and splits sub-basins. Run the analysis in B04 first if none exists.",
|
"Reloads the B04 drainage analysis and re-splits sub-basins around the current culverts, adding culverts where spacing requires. Run the analysis in B04 first if none exists.",
|
||||||
],
|
],
|
||||||
B05_Drainage_Btn_DeleteSelected: ["선택 삭제", "Delete selected"],
|
B05_Drainage_Btn_DeleteSelected: ["선택한 관 삭제", "Delete selected culvert"],
|
||||||
B05_Drainage_Btn_Auto: ["초기화", "Reset"],
|
B05_Drainage_Btn_DeleteSelected_Tip: [
|
||||||
|
"지도에서 고른 배관 한 개를 지웁니다. 관을 먼저 눌러 고른 뒤에 쓸 수 있습니다.",
|
||||||
|
"Removes the culvert selected on the map. Select a culvert marker first.",
|
||||||
|
],
|
||||||
|
B05_Drainage_Btn_Auto: ["배관 배치 초기화", "Reset culvert layout"],
|
||||||
B05_Drainage_Btn_Auto_Tip: [
|
B05_Drainage_Btn_Auto_Tip: [
|
||||||
"손으로 넣거나 옮긴 배관을 버리고 기본 관 + 자동 보충 배치로 되돌립니다.",
|
"손으로 넣거나 옮기거나 지운 배관을 모두 버리고, 계곡부·간격 기준으로 프로그램이 자동 배치한 상태로 되돌립니다. 유역 구획도 함께 다시 나뉩니다.",
|
||||||
"Discards manual culvert edits and restores the automatic placement.",
|
"Discards every manual culvert edit and restores the automatic placement (valley crossings + spacing rule); sub-basins are re-split as well.",
|
||||||
],
|
],
|
||||||
B05_Drainage_Menu_Add: ["배수관 추가", "Add culvert"],
|
B05_Drainage_Menu_Add: ["배수관 추가", "Add culvert"],
|
||||||
B05_Drainage_Menu_Delete: ["배수관 삭제", "Remove culvert"],
|
B05_Drainage_Menu_Delete: ["배수관 삭제", "Remove culvert"],
|
||||||
|
|||||||
Reference in New Issue
Block a user