Compare commits
3
Commits
main_laptop_1
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d33f8d61c | ||
|
|
cd6c7f3fed | ||
|
|
b34eb546f9 |
@@ -0,0 +1,49 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_FillSlope_Notice.ts
|
||||
* 성토사면 5m 초과 **경고 줄** — 횡단 카드 목록 위 요약 한 줄 + 펼치면 측점 목록
|
||||
* (2026-09-14 브레인 승인 (나)). 판정은 `_Cross_FillSlope_Warn`, 여기는 그리기만.
|
||||
* 측점을 누르면 그 카드로 간다. 경고만 — 구조물을 세우거나 값을 바꾸지 않는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import { fillSlopeLengths } from "./B06_Section_UI_Cross_Fit";
|
||||
import { FILL_SLOPE_WARN_TEXT, fillSlopeWarnings } from "./B06_Section_UI_Cross_FillSlope_Warn";
|
||||
import { L, stationLabel } from "./B06_Section_UI_Section_Common";
|
||||
|
||||
export interface FillSlopeNotice {
|
||||
root: HTMLDetailsElement;
|
||||
/** 측점 설계가 바뀔 때마다 부른다 — 펼침 상태는 그대로 둔다. */
|
||||
update: (sections: ReadonlyArray<CrossSection>, stationInterval: number) => void;
|
||||
}
|
||||
|
||||
export function createFillSlopeNotice(onPick: (stationId: string) => void): FillSlopeNotice {
|
||||
const root = document.createElement("details");
|
||||
root.className = "b06-section__notice";
|
||||
root.hidden = true;
|
||||
const summary = document.createElement("summary");
|
||||
const list = document.createElement("div");
|
||||
list.className = "b06-section__notice-list";
|
||||
root.append(summary, list);
|
||||
|
||||
return {
|
||||
root,
|
||||
update(sections, stationInterval) {
|
||||
const warnings = fillSlopeWarnings(sections, fillSlopeLengths);
|
||||
root.hidden = !warnings.length;
|
||||
summary.textContent = `⚠ ${FILL_SLOPE_WARN_TEXT} · ${warnings.length}측점`;
|
||||
list.replaceChildren(
|
||||
...warnings.map(({ section, sides }) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
const parts = sides.map(({ side, lengthM, open }) => {
|
||||
const label = L(side === "left" ? "B06_Design_Ditch_Left" : "B06_Design_Ditch_Right");
|
||||
return `${label} ${open ? "≥" : ""}${lengthM.toFixed(2)}m`;
|
||||
});
|
||||
button.textContent = `${stationLabel(section.chainage_m, stationInterval)} ${parts.join(" · ")}`;
|
||||
button.addEventListener("click", () => onPick(section.station_id));
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_FillSlope_Warn.ts
|
||||
* 성토사면 길이 5m 초과 **경고** 판정(2026-09-14 브레인 승인 (나)) — 값만 가리고 그리지 않는다.
|
||||
*
|
||||
* 근거 — 산림자원법 시행규칙 별표2 Ⅰ.2.차.(3).(나) · 임도설치 규정 별표7 2.차.(3).(나)
|
||||
* 「성토사면 길이 5m 초과 시 옹벽·석축」. 실무 표본(오솔길 W열 성토면 거리)에서도 흔해
|
||||
* (영월 63% · 봉화 49%) **경고까지만** — 구조물을 자동으로 세우지 않는다(설계자 판단).
|
||||
*
|
||||
* 벽이 선 쪽은 뺀다 — 기슭막이·옹벽이 사면을 끊은 쪽은 이미 조치된 자리다. **좌·우를 갈라**
|
||||
* 한쪽에만 벽이 서면 반대쪽은 그대로 경고한다.
|
||||
* 길이는 `fillSlopeLengths`(카드 머리 「성토사면」 칸과 같은 값)를 받아 쓴다 — 여기서 다시 재지 않는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import { FILL_SLOPE_MAX_LENGTH_M } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
|
||||
/** 브레인 승인 문구 그대로(2026-09-14) — 고치면 승인을 다시 받을 것. */
|
||||
export const FILL_SLOPE_WARN_TEXT =
|
||||
"성토사면 길이 5m 초과 — 법령상 옹벽·석축 설치 대상 (산림자원법 시행규칙 별표2 Ⅰ.2.차.(3).(나) · 임도설치 규정 별표7 2.차.(3).(나)) ※ 실무 표본에서도 흔함(영월 63% · 봉화 49%) — 설치 여부는 설계자 판단";
|
||||
|
||||
export type FillSlopeSideName = "left" | "right";
|
||||
|
||||
export interface FillSlopeSideLength {
|
||||
lengthM: number;
|
||||
/** 원지반을 못 만나 거기까지만 잰 하한값. */
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export interface FillSlopeWarning {
|
||||
section: CrossSection;
|
||||
sides: Array<{ side: FillSlopeSideName } & FillSlopeSideLength>;
|
||||
}
|
||||
|
||||
/** 벽이 서서 성토사면을 끊는 쪽 — 배관 기슭막이 · 독립 기슭막이 · 세월교·BOX암거 측벽. */
|
||||
export function wallSides(section: CrossSection): Set<FillSlopeSideName> {
|
||||
const sides = new Set<FillSlopeSideName>();
|
||||
if (section.ford || section.box) return new Set(["left", "right"]);
|
||||
const culvert = section.culvert;
|
||||
if (culvert?.hidden_pipe) {
|
||||
// 독립 기슭막이 설치 측 — 좌 = +offset(`restrictToSide`) · 양쪽·미지정은 둘 다.
|
||||
if (culvert.side !== "우") sides.add("left");
|
||||
if (culvert.side !== "좌") sides.add("right");
|
||||
} else if (culvert) {
|
||||
// 유입 = 상단측(미상이면 좌) · 집수정은 벽이 아니다.
|
||||
const inlet: FillSlopeSideName = (section.uphill_side ?? "left") === "left" ? "left" : "right";
|
||||
if (culvert.inlet.structure !== "집수정") sides.add(inlet);
|
||||
if (culvert.outlet.structure !== "집수정") sides.add(inlet === "left" ? "right" : "left");
|
||||
}
|
||||
const revetment = section.revetment;
|
||||
if (revetment) {
|
||||
// 설치 측이 비면 성토가 나는 쪽(`computeRevetmentLayout` 과 같은 규칙).
|
||||
const mode = section.design?.section_mode;
|
||||
const side =
|
||||
revetment.side === "우"
|
||||
? "right"
|
||||
: revetment.side === "좌"
|
||||
? "left"
|
||||
: mode === "left_cut"
|
||||
? "right"
|
||||
: mode === "right_cut" || mode === "both_fill"
|
||||
? "left"
|
||||
: null;
|
||||
if (side) sides.add(side);
|
||||
}
|
||||
return sides;
|
||||
}
|
||||
|
||||
/** 5m 를 **넘는** 성토사면(벽 선 쪽 뺌)이 있는 측점만 — 측점 순서 그대로. */
|
||||
export function fillSlopeWarnings(
|
||||
sections: ReadonlyArray<CrossSection>,
|
||||
lengthsOf: (section: CrossSection) => Record<FillSlopeSideName, FillSlopeSideLength | null>,
|
||||
): FillSlopeWarning[] {
|
||||
const warnings: FillSlopeWarning[] = [];
|
||||
for (const section of sections) {
|
||||
const lengths = lengthsOf(section);
|
||||
const walls = wallSides(section);
|
||||
const sides = (["left", "right"] as const)
|
||||
.filter((side) => !walls.has(side))
|
||||
.flatMap((side) => {
|
||||
const length = lengths[side];
|
||||
return length && length.lengthM > FILL_SLOPE_MAX_LENGTH_M + 1e-6
|
||||
? [{ side, ...length }]
|
||||
: [];
|
||||
});
|
||||
if (sides.length) warnings.push({ section, sides });
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import type {
|
||||
CrossSection,
|
||||
EarthworkConversion,
|
||||
HaulEquipmentLimit,
|
||||
SectionDetailResponse,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import type { RockBoundaryControl } from "./B06_Section_UI_Cross_Design";
|
||||
@@ -81,53 +80,10 @@ import {
|
||||
inferStationInterval,
|
||||
L,
|
||||
} from "./B06_Section_UI_Section_Common";
|
||||
import type { SectionViewController } from "./B06_Section_UI_Section_View_Types";
|
||||
import { createFillSlopeNotice } from "./B06_Section_UI_Cross_FillSlope_Notice";
|
||||
|
||||
export type { CrossDesignChange, DesignChangeHandler, RockBoundaryControl };
|
||||
|
||||
/**
|
||||
* 상태 행·요약줄·하단 접기 손잡이·테두리가 먹는 세로 공간의 **어림값**.
|
||||
*
|
||||
* 평소에는 쓰지 않는다 — 그래프 몫은 `chartWrap`을 직접 재서 정한다(어림값이 실제보다 크면
|
||||
* 유토곡선 아래에 빈 공간이 남는다). 화면에 붙기 전이라 잴 수 없는 첫 렌더에서만 쓰는
|
||||
* 출발값이고, 최소 패널 높이 계산의 기준이기도 하다.
|
||||
*/
|
||||
export interface SectionViewController {
|
||||
root: HTMLElement;
|
||||
render: (
|
||||
detail: SectionDetailResponse,
|
||||
verticalExaggeration: number,
|
||||
crossHalfWidth?: number,
|
||||
stationInterval?: number,
|
||||
earthworkConversion?: EarthworkConversion,
|
||||
haulEquipmentLimits?: HaulEquipmentLimit[],
|
||||
/** balloon 위치 캐시를 가르는 키(프로젝트+경로). 노선이 다르면 위치가 섞이면 안 된다. */
|
||||
balloonScope?: string,
|
||||
/** 자연방토 판정 경사(config). 못 받으면 자연방토 없음으로 본다. */
|
||||
naturalSpoilMinSlope?: number,
|
||||
) => void;
|
||||
/** 측점 하나의 카드만 새로 만들어 교체한다 (전체 재렌더 없이 설계 변경 반영). */
|
||||
refreshCard: (chainageM: number) => void;
|
||||
/** 여러 측점을 한꺼번에 교체한다 — 상단 패널은 **마지막에 한 번만** 다시 그린다. */
|
||||
refreshCards: (chainages: ReadonlyArray<number>) => void;
|
||||
/** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음
|
||||
* (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). */
|
||||
focusStation: (stationId: string) => void;
|
||||
/** 카드(측점) 선택이 바뀔 때 알림 — 좌측 「구조물 배치」 폼이 그 측점 구조물을
|
||||
* 올린다(2026-08-29 일원화). null = 선택 해제. */
|
||||
setStationSelectListener: (listener: (stationId: string | null) => void) => void;
|
||||
clear: () => void;
|
||||
/** 종단 아래 구조물 알약 레인에 쓸 목록 — B05 와 같은 부품·같은 표기(2026-09-07 사용자
|
||||
* 지시 4 「구조물 표시 통일」). 좌측 「구조물 배치」가 목록을 받을 때마다 넘겨준다. */
|
||||
setStructureMarks: (
|
||||
structures: ReadonlyArray<StructureInstance>,
|
||||
types: ReadonlyArray<StructureType>,
|
||||
) => void;
|
||||
/** 종단 그래프 우클릭으로 구조물을 넣고 빼는 길(2026-09-12 B05·B06 일원화). */
|
||||
setStructureEdit: (edit: SectionStructureEdit | null) => void;
|
||||
/** 계획선 편집 ▲/▼ — 그릴 때마다 불러 선형·편집 함수를 받는다(null = 버튼 없음). */
|
||||
setGradeEdit: (provider: (() => LongitudinalPanelInput["grade"]) | null) => void;
|
||||
dispose: () => void;
|
||||
}
|
||||
export type { CrossDesignChange, DesignChangeHandler, RockBoundaryControl, SectionViewController };
|
||||
|
||||
export function createSectionView(
|
||||
onDesignChange?: DesignChangeHandler,
|
||||
@@ -236,6 +192,10 @@ export function createSectionView(
|
||||
},
|
||||
});
|
||||
panel.append(panelResizer.root);
|
||||
// 성토사면 5m 초과 경고 줄(2026-09-14 브레인 (나)) — 패널과 카드 사이 · 측점 누르면 그 카드로.
|
||||
const fillSlopeNotice = createFillSlopeNotice((id) =>
|
||||
selectedStationId === id ? revealCard(id, "smooth") : selectStation(id, true),
|
||||
);
|
||||
|
||||
/**
|
||||
* 패널 **어디에서든** 굴린 휠은 페이지가 아니라 그래프를 좌우로 민다(2026-08-02 사용자 지시).
|
||||
@@ -489,6 +449,7 @@ export function createSectionView(
|
||||
// 보던 자리가 맨 앞으로 튀면 못 쓴다. 위치를 잡아 뒀다 되돌린다.
|
||||
const keepScrollLeft = chartWrap.scrollLeft;
|
||||
const detail = currentDetail;
|
||||
fillSlopeNotice.update(detail.cross_sections, cachedStationInterval); // 카드 갱신도 여기를 거침
|
||||
// 그래프 몫은 **추정하지 않고 잰다**. `chartWrap`은 `flex: 1 / min-height: 0`이라 높이가
|
||||
// 내용이 아니라 패널에서 정해지므로, 재서 쓰면 되먹임 없이 한 번에 수렴한다.
|
||||
// 화면에 붙기 전(detached)에는 잴 수 없으니 그때만 `PANEL_CHROME_PX` 추정치로 시작한다.
|
||||
@@ -599,7 +560,7 @@ export function createSectionView(
|
||||
}
|
||||
// panel은 이미 root의 자식이라 replaceChildren이 떼었다 붙이면서 스크롤을 잃는다.
|
||||
const keepScrollLeft = chartWrap.scrollLeft;
|
||||
root.replaceChildren(panel, grid);
|
||||
root.replaceChildren(panel, fillSlopeNotice.root, grid);
|
||||
chartWrap.scrollLeft = keepScrollLeft;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Section_View_Types.ts
|
||||
* `createSectionView` 가 돌려주는 조종기 모양 — 700줄 제한으로 뷰 본체에서 떼어 냄(2026-09-14).
|
||||
* 부르는 쪽은 종전대로 `_UI_Section_View` 에서 가져간다(거기서 다시 내보냄).
|
||||
* ========================================================================== */
|
||||
|
||||
import type {
|
||||
EarthworkConversion,
|
||||
HaulEquipmentLimit,
|
||||
SectionDetailResponse,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu";
|
||||
import type { LongitudinalPanelInput } from "./B06_Section_UI_Section_View_Draw";
|
||||
import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
|
||||
/**
|
||||
* 상태 행·요약줄·하단 접기 손잡이·테두리가 먹는 세로 공간의 **어림값**.
|
||||
*
|
||||
* 평소에는 쓰지 않는다 — 그래프 몫은 `chartWrap`을 직접 재서 정한다(어림값이 실제보다 크면
|
||||
* 유토곡선 아래에 빈 공간이 남는다). 화면에 붙기 전이라 잴 수 없는 첫 렌더에서만 쓰는
|
||||
* 출발값이고, 최소 패널 높이 계산의 기준이기도 하다.
|
||||
*/
|
||||
export interface SectionViewController {
|
||||
root: HTMLElement;
|
||||
render: (
|
||||
detail: SectionDetailResponse,
|
||||
verticalExaggeration: number,
|
||||
crossHalfWidth?: number,
|
||||
stationInterval?: number,
|
||||
earthworkConversion?: EarthworkConversion,
|
||||
haulEquipmentLimits?: HaulEquipmentLimit[],
|
||||
/** balloon 위치 캐시를 가르는 키(프로젝트+경로). 노선이 다르면 위치가 섞이면 안 된다. */
|
||||
balloonScope?: string,
|
||||
/** 자연방토 판정 경사(config). 못 받으면 자연방토 없음으로 본다. */
|
||||
naturalSpoilMinSlope?: number,
|
||||
) => void;
|
||||
/** 측점 하나의 카드만 새로 만들어 교체한다 (전체 재렌더 없이 설계 변경 반영). */
|
||||
refreshCard: (chainageM: number) => void;
|
||||
/** 여러 측점을 한꺼번에 교체한다 — 상단 패널은 **마지막에 한 번만** 다시 그린다. */
|
||||
refreshCards: (chainages: ReadonlyArray<number>) => void;
|
||||
/** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음
|
||||
* (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). */
|
||||
focusStation: (stationId: string) => void;
|
||||
/** 카드(측점) 선택이 바뀔 때 알림 — 좌측 「구조물 배치」 폼이 그 측점 구조물을
|
||||
* 올린다(2026-08-29 일원화). null = 선택 해제. */
|
||||
setStationSelectListener: (listener: (stationId: string | null) => void) => void;
|
||||
clear: () => void;
|
||||
/** 종단 아래 구조물 알약 레인에 쓸 목록 — B05 와 같은 부품·같은 표기(2026-09-07 사용자
|
||||
* 지시 4 「구조물 표시 통일」). 좌측 「구조물 배치」가 목록을 받을 때마다 넘겨준다. */
|
||||
setStructureMarks: (
|
||||
structures: ReadonlyArray<StructureInstance>,
|
||||
types: ReadonlyArray<StructureType>,
|
||||
) => void;
|
||||
/** 종단 그래프 우클릭으로 구조물을 넣고 빼는 길(2026-09-12 B05·B06 일원화). */
|
||||
setStructureEdit: (edit: SectionStructureEdit | null) => void;
|
||||
/** 계획선 편집 ▲/▼ — 그릴 때마다 불러 선형·편집 함수를 받는다(null = 버튼 없음). */
|
||||
setGradeEdit: (provider: (() => LongitudinalPanelInput["grade"]) | null) => void;
|
||||
dispose: () => void;
|
||||
}
|
||||
@@ -195,6 +195,29 @@
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
/* 성토사면 5m 초과 경고 줄(2026-09-14) — 요약 한 줄, 펼치면 측점 단추 목록. */
|
||||
.b06-section__notice {
|
||||
margin-bottom: var(--spacing-8);
|
||||
color: var(--color-warning);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.b06-section__notice > summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-section__notice-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
padding-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.b06-section__notice-list > button {
|
||||
font-size: var(--text-caption);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b06-cross-card {
|
||||
cursor: pointer;
|
||||
transition:
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
"""성토사면 길이 5m 초과 경고 — 벽 선 쪽만 빼고(좌·우 갈라) 경고만 (2026-09-14 브레인 (나)).
|
||||
|
||||
① 5m 를 **넘는** 쪽만 — 5.00 은 아님 · 원지반을 못 만난 하한값(≥)도 5m 이하면 아님
|
||||
② 벽이 선 쪽은 뺀다 — 배관 유입(상단측)·유출(반대측) 기슭막이, 집수정은 벽 아님
|
||||
③ 한쪽에만 벽 → 반대쪽은 그대로 경고 · 독립 기슭막이 설치 측(좌/우/양쪽)
|
||||
④ 세월교·BOX암거는 양쪽 측벽
|
||||
⑤ 문구는 브레인 승인 그대로
|
||||
|
||||
TS 를 실제로 돌린다(파이썬 짝이 없는 화면 판정).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
SOURCE = PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Cross_FillSlope_Warn.ts"
|
||||
|
||||
_RUNNER = """
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { FILL_SLOPE_WARN_TEXT, fillSlopeWarnings } from "./B06_Section_UI_Cross_FillSlope_Warn.js";
|
||||
|
||||
const [inputPath, outputPath] = process.argv.slice(2);
|
||||
const cases = JSON.parse(readFileSync(inputPath, "utf8"));
|
||||
const warnings = fillSlopeWarnings(cases.map((c) => c.section), (section) =>
|
||||
cases.find((c) => c.section.station_id === section.station_id).lengths,
|
||||
);
|
||||
writeFileSync(outputPath, JSON.stringify({
|
||||
text: FILL_SLOPE_WARN_TEXT,
|
||||
warnings: warnings.map((w) => [w.section.station_id, w.sides.map((s) => s.side)]),
|
||||
}));
|
||||
"""
|
||||
|
||||
LONG = {"lengthM": 7.0, "open": False}
|
||||
BOTH = {"left": LONG, "right": LONG}
|
||||
|
||||
|
||||
def _culvert(inlet: str = "기슭막이", outlet: str = "기슭막이", **extra: object) -> dict:
|
||||
return {"inlet": {"structure": inlet}, "outlet": {"structure": outlet}, **extra}
|
||||
|
||||
|
||||
CASES = [
|
||||
{"section": {"station_id": "plain"}, "lengths": BOTH},
|
||||
{
|
||||
"section": {"station_id": "edge"},
|
||||
"lengths": {
|
||||
"left": {"lengthM": 5.0, "open": False},
|
||||
"right": {"lengthM": 4.2, "open": True},
|
||||
},
|
||||
},
|
||||
# 상단측 좌 → 유입(좌) 기슭막이 · 유출(우) 기슭막이 — 양쪽 다 벽.
|
||||
{
|
||||
"section": {"station_id": "pipe", "uphill_side": "left", "culvert": _culvert()},
|
||||
"lengths": BOTH,
|
||||
},
|
||||
# 유입이 집수정 → 좌(유입측)는 벽 없음 → 좌만 경고.
|
||||
{
|
||||
"section": {"station_id": "basin", "uphill_side": "left", "culvert": _culvert("집수정")},
|
||||
"lengths": BOTH,
|
||||
},
|
||||
# 상단측 우 → 유출은 좌 · 유출이 집수정이면 좌만 경고.
|
||||
{
|
||||
"section": {
|
||||
"station_id": "right_up",
|
||||
"uphill_side": "right",
|
||||
"culvert": _culvert(outlet="집수정"),
|
||||
},
|
||||
"lengths": BOTH,
|
||||
},
|
||||
{
|
||||
"section": {"station_id": "own_left", "culvert": _culvert(hidden_pipe=True, side="좌")},
|
||||
"lengths": BOTH,
|
||||
},
|
||||
{
|
||||
"section": {
|
||||
"station_id": "revet_auto",
|
||||
"revetment": {"side": None},
|
||||
"design": {"section_mode": "left_cut"},
|
||||
},
|
||||
"lengths": {"left": None, "right": LONG},
|
||||
},
|
||||
{"section": {"station_id": "ford", "ford": {}}, "lengths": BOTH},
|
||||
]
|
||||
|
||||
|
||||
def _run(tmp_path: Path) -> dict:
|
||||
out = tmp_path / "js"
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(TSC),
|
||||
str(SOURCE),
|
||||
# 실행에 드는 import 는 이것 하나 — 나머지는 타입 import 라 지워진다.
|
||||
str(SOURCE.with_name("B06_Section_UI_Cross_Culvert_Const.ts")),
|
||||
"--outDir",
|
||||
str(out),
|
||||
"--module",
|
||||
"esnext",
|
||||
"--target",
|
||||
"es2022",
|
||||
"--moduleResolution",
|
||||
"bundler",
|
||||
"--ignoreConfig",
|
||||
# 타입 줄기가 별칭(@util 등)으로 번져 단독 컴파일로는 못 푼다 — 검사는 typecheck 몫.
|
||||
"--noCheck",
|
||||
"--noResolve",
|
||||
],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
for emitted in out.glob("*.js"):
|
||||
text = emitted.read_text(encoding="utf-8")
|
||||
emitted.write_text(
|
||||
re.sub(r'(from "\./[^"]+?)(")', lambda m: m.group(1) + ".js" + m.group(2), text),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
||||
payload, result = tmp_path / "input.json", tmp_path / "output.json"
|
||||
payload.write_text(json.dumps(CASES, ensure_ascii=False), encoding="utf-8")
|
||||
subprocess.run( # noqa: S603
|
||||
["node", str(out / "runner.mjs"), str(payload), str(result)],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return json.loads(result.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
|
||||
def test_벽_선_쪽만_빼고_5m_넘는_쪽을_경고한다(tmp_path: Path) -> None:
|
||||
got = dict(_run(tmp_path)["warnings"])
|
||||
assert got == {
|
||||
"plain": ["left", "right"],
|
||||
"basin": ["left"],
|
||||
"right_up": ["left"],
|
||||
"own_left": ["right"],
|
||||
}
|
||||
# 5.00 · 하한값 4.2(≥) · 양쪽 벽 · 자동 설치 측 벽 · 세월교는 경고 없음.
|
||||
assert not {"edge", "pipe", "revet_auto", "ford"} & set(got)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
|
||||
def test_문구는_승인된_그대로(tmp_path: Path) -> None:
|
||||
assert _run(tmp_path)["text"] == (
|
||||
"성토사면 길이 5m 초과 — 법령상 옹벽·석축 설치 대상 "
|
||||
"(산림자원법 시행규칙 별표2 Ⅰ.2.차.(3).(나) · 임도설치 규정 별표7 2.차.(3).(나)) "
|
||||
"※ 실무 표본에서도 흔함(영월 63% · 봉화 49%) — 설치 여부는 설계자 판단"
|
||||
)
|
||||
Reference in New Issue
Block a user