Merge remote-tracking branch 'origin/dev' into sub_laptop_5
This commit is contained in:
@@ -190,6 +190,7 @@ export function sharedStructures(shared: DesignPageShared): StructuresSection {
|
||||
onPipeUpdate: (from, to, attributes) => on().onPipeUpdate?.(from, to, attributes),
|
||||
onPipeRemove: (chainageM) => on().onPipeRemove?.(chainageM),
|
||||
onPipeSelect: (chainageM) => on().onPipeSelect?.(chainageM),
|
||||
onListRelease: () => on().onListRelease?.(),
|
||||
onReveal: () => shared.revealLeft(),
|
||||
},
|
||||
{ includeDetail: () => shared.view() === "cross", projectId: shared.projectId },
|
||||
|
||||
@@ -185,6 +185,7 @@ export async function mountCrossView(
|
||||
focusChainage: (chainageM) => {
|
||||
if (!isWallSelecting()) focusStationAt(chainageM);
|
||||
},
|
||||
releaseCard: () => sectionView.focusStation(null),
|
||||
markStructure: (structureId) => sectionView.selectMark(structureId),
|
||||
applyPipeOptions: (chainageM, patch) =>
|
||||
applyPipeOptionsToCache(pipeOptionsContext, chainageM, patch),
|
||||
|
||||
@@ -201,6 +201,24 @@ class StructureInstance(BaseModel):
|
||||
def validate_group_items(cls, value: Any) -> dict[str, Any] | None:
|
||||
return parse_group_items(value)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def follow_site_definition(cls, data: Any) -> Any:
|
||||
"""정의가 부지형인데 옛 구간형으로 적힌 저장분(사토장 · PLAN 54-7) — 기준 측점 하나로 읽음.
|
||||
|
||||
기준 측점이 없으면 옛 기준 → 시작 차례. 파일의 옛 값은 [저장] 전까지 그대로.
|
||||
"""
|
||||
if not isinstance(data, dict) or data.get("placement") != "interval":
|
||||
return data
|
||||
definition = structure_type_map().get(str(data.get("type_id")))
|
||||
if definition is None or definition.placement != "site":
|
||||
return data
|
||||
chainage = data.get("chainage_m")
|
||||
if chainage is None:
|
||||
chainage = data.get("start_m")
|
||||
rest = {key: value for key, value in data.items() if key not in ("start_m", "end_m")}
|
||||
return {**rest, "placement": "site", "chainage_m": chainage}
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_placement_fields(self) -> "StructureInstance":
|
||||
if self.placement == "interval":
|
||||
|
||||
@@ -38,6 +38,8 @@ const ROW_GAP_PX = 2;
|
||||
const LANE_PADDING_PX = 2;
|
||||
/** 두 알약이 겹친다고 볼 가로 거리(px). 3자 라벨 폭 기준. */
|
||||
const OVERLAP_PX = 52;
|
||||
/** 기타 알약 머리 글자 수 — 약호(3자 이하)와 같은 폭(54-9). */
|
||||
const ETC_PILL_CHARS = 3;
|
||||
|
||||
/** 레인 전체 높이(px) — 2행 구조를 항상 확보한다(항목이 하나여도 높이는 같다).
|
||||
* 큰(혼자) 알약도 이 안에 들어간다. 아래 서브패널 손잡이와의 사이는 CSS 여백을
|
||||
@@ -115,6 +117,53 @@ function rowAssignments(
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 이름마다 남과 갈리는 가장 짧은 머리 — 빈칸 뺌 · 적어도 3글자 · 이름보다 길지 않음. */
|
||||
function distinctHeads(names: ReadonlyArray<string>): Map<string, string> {
|
||||
const letters = names.map((name) => [...name.replace(/\s+/g, "")]);
|
||||
return new Map(
|
||||
names.map((name, index) => {
|
||||
const own = letters[index];
|
||||
let length = ETC_PILL_CHARS;
|
||||
letters.forEach((other, otherIndex) => {
|
||||
if (otherIndex === index) return;
|
||||
let common = 0;
|
||||
while (common < own.length && own[common] === other[common]) common += 1;
|
||||
length = Math.max(length, common + 1);
|
||||
});
|
||||
return [name, own.slice(0, length).join("")];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** 표시 이름 · 알약 글(구조물 id 별). 표시 이름 = 좌측 목록과 같은 중복 번호(시점에 가까운 순
|
||||
* "이름 1/2…", 2026-08-19) — 툴팁 · 벌룬 제목이 씀. 알약 = 약호 + 그 번호. 기타는 약호가 모두
|
||||
* 「기타」라 손 이름 머리(distinctHeads) + 번호 · 빈 이름 = 「기타 N」(54-9). */
|
||||
export function structurePillTexts(
|
||||
structures: ReadonlyArray<StructureInstance>,
|
||||
typeMap: ReadonlyMap<string, StructureType>,
|
||||
): Map<string, { name: string; pill: string }> {
|
||||
const sorted = [...structures].sort(
|
||||
(left, right) => structureAnchorM(left) - structureAnchorM(right),
|
||||
);
|
||||
const rawNames = sorted.map(
|
||||
(entry) => etcName(entry) ?? typeMap.get(entry.type_id)?.name ?? entry.type_id,
|
||||
);
|
||||
const numbered = numberDuplicateNames(rawNames);
|
||||
const heads = distinctHeads([
|
||||
...new Set(rawNames.filter((_name, index) => sorted[index].type_id === "etc")),
|
||||
]);
|
||||
return new Map(
|
||||
sorted.map((entry, index) => {
|
||||
const name = numbered[index];
|
||||
const suffix = name.slice(rawNames[index].length); // " 1" 또는 ""
|
||||
const head = etcName(entry)
|
||||
? heads.get(rawNames[index])
|
||||
: typeMap.get(entry.type_id)?.style?.abbr;
|
||||
return [entry.structure_id ?? "", { name, pill: `${head ?? "?"}${suffix}` }];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 그래프 아래에 구조물 알약 레인을 만들어 돌려준다(호출부가 원하는 위치에 붙인다).
|
||||
*/
|
||||
@@ -127,28 +176,15 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement
|
||||
const typeMap = new Map(options.types.map((type) => [type.type_id, type]));
|
||||
const rows = rowAssignments(options.structures, options.x);
|
||||
|
||||
// 좌측 목록과 같은 중복 이름 번호(2026-08-19 사용자 지시) — 시점에 가까운 순
|
||||
// "이름 1/2…". 알약 라벨(약호)·툴팁·벌룬 제목이 모두 이 번호를 따른다.
|
||||
const anchorSorted = [...options.structures].sort(
|
||||
(left, right) => structureAnchorM(left) - structureAnchorM(right),
|
||||
);
|
||||
const numberedNames = numberDuplicateNames(
|
||||
anchorSorted.map(
|
||||
(entry) => etcName(entry) ?? typeMap.get(entry.type_id)?.name ?? entry.type_id,
|
||||
),
|
||||
);
|
||||
const displayNames = new Map(
|
||||
anchorSorted.map((entry, index) => [entry.structure_id ?? "", numberedNames[index]]),
|
||||
);
|
||||
// 좌측 목록과 같은 중복 이름 번호(2026-08-19 사용자 지시) — 알약 글·툴팁·벌룬 제목이
|
||||
// 모두 이 번호를 따른다.
|
||||
const texts = structurePillTexts(options.structures, typeMap);
|
||||
|
||||
options.structures.forEach((structure) => {
|
||||
const id = structure.structure_id;
|
||||
if (!id) return;
|
||||
const type = typeMap.get(structure.type_id);
|
||||
const rawName = etcName(structure) ?? type?.name ?? structure.type_id; // 기타 = 손 이름(목록과 같게)
|
||||
const displayName = displayNames.get(id) ?? rawName;
|
||||
// "옹벽 1"에서 붙은 번호만 떼어 약호 뒤에 잇는다(알약은 약호 표기 유지).
|
||||
const numberSuffix = displayName.startsWith(rawName) ? displayName.slice(rawName.length) : "";
|
||||
const { name: displayName, pill } = texts.get(id) ?? { name: structure.type_id, pill: "?" };
|
||||
const anchorPx = options.x(structureAnchorM(structure));
|
||||
const selected = options.selectedId === id;
|
||||
// 이웃과 겹치는 알약만 위·아래 두 줄로 가르고, 혼자면 레인 세로 가운데에 크게 둔다.
|
||||
@@ -185,8 +221,9 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement
|
||||
mark.style.top = `${top}px`;
|
||||
mark.style.borderColor = type?.style?.color ?? "#888";
|
||||
mark.style.color = type?.style?.color ?? "#888";
|
||||
// 라벨은 3자 이하 짧은 이름(레지스트리 style.abbr, 2026-08-17 지시 4) + 중복 번호.
|
||||
mark.textContent = `${type?.style?.abbr ?? "?"}${numberSuffix}`;
|
||||
// 라벨은 3자 이하 짧은 이름(레지스트리 style.abbr, 2026-08-17 지시 4) + 중복 번호 ·
|
||||
// 기타는 손 이름 머리(54-9).
|
||||
mark.textContent = pill;
|
||||
mark.title = `${displayName} ${positionText(structure, options.stationIntervalM)}`;
|
||||
mark.dataset.structureId = id;
|
||||
|
||||
|
||||
@@ -433,8 +433,10 @@ export function createStructuresSection(
|
||||
selectedPipeChainage,
|
||||
// 재선택 = 해제(2026-08-18 복구) — 해제는 리셋과 같은 전체 초기화다.
|
||||
onSelectStructure: (structure) => {
|
||||
if (structure.structure_id !== null && structure.structure_id === editingId) resetForm();
|
||||
else loadForm(structure);
|
||||
if (structure.structure_id !== null && structure.structure_id === editingId) {
|
||||
resetForm();
|
||||
callbacks.onListRelease?.();
|
||||
} else loadForm(structure);
|
||||
},
|
||||
onSelectPipe: (pipe) => {
|
||||
const isSame =
|
||||
@@ -442,6 +444,7 @@ export function createStructuresSection(
|
||||
Math.abs(selectedPipeChainage - pipe.chainage_m) < PIPE_MATCH_M;
|
||||
if (isSame) {
|
||||
resetForm();
|
||||
callbacks.onListRelease?.();
|
||||
} else {
|
||||
loadPipeForm(pipe);
|
||||
callbacks.onPipeSelect(pipe.chainage_m);
|
||||
|
||||
@@ -164,6 +164,8 @@ export interface StructuresCallbacks {
|
||||
* 목록에서 직접 고른 경우엔 이미 펼쳐져 있어 아무 일도 안 한다.
|
||||
*/
|
||||
onReveal?: () => void;
|
||||
/** 목록에서 고른 줄을 다시 눌러 풂 — 다른 화면의 고름(횡단 카드)도 함께 끔(54-8). */
|
||||
onListRelease?: () => void;
|
||||
}
|
||||
|
||||
type OptionEntry = { key: string; read: () => string | number; isEmpty: () => boolean };
|
||||
|
||||
@@ -37,32 +37,35 @@ _SIDE_AUTO = "자동(성토 쪽)"
|
||||
_SIDE_WORDS = {"좌": "left", "우": "right"}
|
||||
|
||||
|
||||
def _sections_in(cross_sections: list[dict[str, Any]], start_m: float, end_m: float) -> list[dict]:
|
||||
"""구간 안에 든 측점만. **새 측점을 만들지 않는다**(2026-09-09 사용자 확정 ③)."""
|
||||
picked = []
|
||||
for section in cross_sections:
|
||||
chainage = section.get("chainage_m")
|
||||
if chainage is None:
|
||||
continue
|
||||
value = float(chainage)
|
||||
if start_m - 1e-6 <= value <= end_m + 1e-6:
|
||||
picked.append(section)
|
||||
return sorted(picked, key=lambda item: float(item["chainage_m"]))
|
||||
def _section_at(cross_sections: list[dict[str, Any]], chainage_m: float) -> list[dict]:
|
||||
"""기준 측점에 가장 가까운 측점 하나(PLAN 54-7 · 가운데면 앞 측점).
|
||||
|
||||
|
||||
def _spans(sections: list[dict[str, Any]], start_m: float, end_m: float) -> list[float]:
|
||||
"""측점마다 대표 길이(m) — 앞뒤 측점과의 절반씩. 구간 끝은 경계까지만."""
|
||||
spans: list[float] = []
|
||||
for index, section in enumerate(sections):
|
||||
chainage = float(section["chainage_m"])
|
||||
left = float(sections[index - 1]["chainage_m"]) if index else max(start_m, chainage)
|
||||
right = (
|
||||
float(sections[index + 1]["chainage_m"])
|
||||
if index + 1 < len(sections)
|
||||
else min(end_m, chainage)
|
||||
**새 측점을 만들지 않는다**(확정 ③).
|
||||
"""
|
||||
placed = [section for section in cross_sections if section.get("chainage_m") is not None]
|
||||
if not placed:
|
||||
return []
|
||||
return [
|
||||
min(
|
||||
placed,
|
||||
key=lambda item: (
|
||||
abs(float(item["chainage_m"]) - chainage_m),
|
||||
float(item["chainage_m"]),
|
||||
),
|
||||
)
|
||||
spans.append(max((chainage - left) / 2 + (right - chainage) / 2, 0.0))
|
||||
return spans
|
||||
]
|
||||
|
||||
|
||||
def _span_of(cross_sections: list[dict[str, Any]], section: dict[str, Any]) -> float:
|
||||
"""그 측점의 대표 길이(m) — 앞뒤 측점과의 절반씩. 끝 측점은 한쪽만."""
|
||||
chainages = sorted(
|
||||
{float(item["chainage_m"]) for item in cross_sections if item.get("chainage_m") is not None}
|
||||
)
|
||||
here = float(section["chainage_m"])
|
||||
index = chainages.index(here)
|
||||
left = chainages[index - 1] if index else here
|
||||
right = chainages[index + 1] if index + 1 < len(chainages) else here
|
||||
return (right - left) / 2
|
||||
|
||||
|
||||
def _has_spoil_value(design: dict[str, Any]) -> bool:
|
||||
@@ -92,7 +95,10 @@ def _side_of(design: dict[str, Any], option: Any) -> str | None:
|
||||
|
||||
|
||||
def spoil_sites(structures: list[Any]) -> list[dict[str, Any]]:
|
||||
"""배치된 사토장만 골라 쓰기 좋은 모양으로. 구간·용량이 없으면 뺀다."""
|
||||
"""배치된 사토장만 골라 쓰기 좋은 모양으로. 기준 측점·용량이 없으면 뺀다.
|
||||
|
||||
기준 측점이 없는 옛 구간 저장분은 시작 측점으로 읽음(PLAN 54-7).
|
||||
"""
|
||||
sites: list[dict[str, Any]] = []
|
||||
for item in structures:
|
||||
type_id = getattr(item, "type_id", None) or (
|
||||
@@ -104,13 +110,13 @@ def spoil_sites(structures: list[Any]) -> list[dict[str, Any]]:
|
||||
if options is None and isinstance(item, dict):
|
||||
options = item.get("options")
|
||||
options = options or {}
|
||||
start = getattr(item, "start_m", None)
|
||||
end = getattr(item, "end_m", None)
|
||||
chainage = getattr(item, "chainage_m", None)
|
||||
if isinstance(item, dict):
|
||||
start = item.get("start_m")
|
||||
end = item.get("end_m")
|
||||
chainage = item.get("chainage_m")
|
||||
if chainage is None:
|
||||
chainage = item.get("start_m")
|
||||
capacity = options.get("capacity_m3")
|
||||
if start is None or end is None or capacity in (None, ""):
|
||||
if chainage is None or capacity in (None, ""):
|
||||
continue
|
||||
try:
|
||||
capacity_value = float(capacity)
|
||||
@@ -122,8 +128,7 @@ def spoil_sites(structures: list[Any]) -> list[dict[str, Any]]:
|
||||
{
|
||||
"structure_id": getattr(item, "structure_id", None)
|
||||
or (item.get("structure_id") if isinstance(item, dict) else None),
|
||||
"start_m": min(float(start), float(end)),
|
||||
"end_m": max(float(start), float(end)),
|
||||
"chainage_m": float(chainage),
|
||||
"capacity_m3": capacity_value,
|
||||
"side_option": options.get("side"),
|
||||
"slope_ratio_n": options.get("fill_slope_ratio"),
|
||||
@@ -223,7 +228,7 @@ def enforce_spoil_fills(
|
||||
# (노선 성토도 그때 원래 값으로 돌아온다 — 사토장 몫을 빼 두었기 때문이다.)
|
||||
covered: set[float] = set()
|
||||
for site in sites:
|
||||
for section in _sections_in(cross_sections, site["start_m"], site["end_m"]):
|
||||
for section in _section_at(cross_sections, site["chainage_m"]):
|
||||
covered.add(float(section["chainage_m"]))
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
@@ -256,10 +261,10 @@ def enforce_spoil_fills(
|
||||
if not sites:
|
||||
return changed
|
||||
for site in sites:
|
||||
sections = _sections_in(cross_sections, site["start_m"], site["end_m"])
|
||||
sections = _section_at(cross_sections, site["chainage_m"])
|
||||
if not sections:
|
||||
continue
|
||||
spans = _spans(sections, site["start_m"], site["end_m"])
|
||||
spans = [_span_of(cross_sections, section) for section in sections]
|
||||
sides = [_side_of(section.get("design") or {}, site["side_option"]) for section in sections]
|
||||
ratio = site["slope_ratio_n"]
|
||||
|
||||
|
||||
@@ -87,6 +87,8 @@ export interface B06StructuresPanelDeps {
|
||||
stationInterval: () => number;
|
||||
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */
|
||||
focusChainage: (chainageM: number) => void;
|
||||
/** 목록에서 고른 줄을 다시 눌러 풂 — 횡단 카드 선택도 끔(54-8). */
|
||||
releaseCard?: () => void;
|
||||
/** 종단 알약 강조를 목록에서 고른 것에 맞춘다(null = 관 · 해제 — 측점선 짝을 따름). */
|
||||
markStructure?: (structureId: string | null) => void;
|
||||
/** 목록이 바뀔 때마다 종단 알약 레인에 같은 목록을 넘긴다(2026-09-07 표시 통일). */
|
||||
@@ -298,6 +300,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
// 알약 = 그 관 알약(같은 측점 구조물 말고 · 측점선 없는 물넘이포장도) — 카드 뒤에(53-2)
|
||||
deps.markStructure?.(chainageM === null ? null : `pipe-${chainageM.toFixed(2)}`);
|
||||
},
|
||||
onListRelease: () => deps.releaseCard?.(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -445,7 +445,8 @@ export function createSectionView(
|
||||
refreshCard,
|
||||
refreshCards,
|
||||
focusStation(stationId) {
|
||||
if (selectedStationId !== stationId) selectStation(stationId, true);
|
||||
if (stationId === null) clearSelection();
|
||||
else if (selectedStationId !== stationId) selectStation(stationId, true);
|
||||
else {
|
||||
revealCard(stationId, "smooth");
|
||||
// 같은 카드라도 알약 강조는 다시 맞춘다 — 목록에서 같은 측점 다른 항목을 고른 경우(52-15).
|
||||
|
||||
@@ -38,8 +38,8 @@ export interface SectionViewController {
|
||||
/** 여러 측점을 한꺼번에 교체한다 — 상단 패널은 **마지막에 한 번만** 다시 그린다. */
|
||||
refreshCards: (chainages: ReadonlyArray<number>) => void;
|
||||
/** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음
|
||||
* (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). */
|
||||
focusStation: (stationId: string) => void;
|
||||
* (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). null = 카드 선택 풂(54-8). */
|
||||
focusStation: (stationId: string | null) => void;
|
||||
/** 종단 알약 강조를 그 구조물로(null = 풀기 — 카드 측점선 짝을 따름). */
|
||||
selectMark: (structureId: string | null) => void;
|
||||
/** 카드(측점) 선택이 바뀔 때 알림 — 좌측 「구조물 배치」 폼이 그 측점 구조물을
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# 54-5 B05 · B06 실시간 조작 속도 · 보안 재확인 (조사 · 보고만)
|
||||
|
||||
- 기준 커밋: `6233daab1` (sub_laptop_8 = origin/main_laptop_1)
|
||||
- 검증 프로젝트: `sub8_검증` (`c81b21f8…`) · 노선 224 · 저장 · 확정 누르지 않음(캐시만 · 브라우저 문맥 버림)
|
||||
- 측정 도구: 헤드리스 Chrome(실 GPU — ANGLE D3D11 Intel Iris Xe) · 1509×1069 · 페이지 안 계측(Event Timing · Long Task · fetch/XHR 감쌈 · sessionStorage 쓰기 셈 · DOM 변화 셈 · WebGL clear 셈) + CDP 메인 스레드 시간
|
||||
- ⚠ ORCA 내장 탭이 10초마다 강제로 내려감(다른 창 탭 여럿 · 숨은 탭 10초 뒤 버림 · `tab switch --focus` 로도 같음) → 10초 넘는 측정 불가 → 측정 전용 헤드리스 Chrome 으로 대체(공용 브라우저 아님 · 화면 안 띄움 · ORCA 탭은 닫음)
|
||||
- 통합 전 판 실행 대조는 안 함(워크트리 금지) → 통합 전 실측값은 커밋 글에 남은 수치로 대조(`9da48d5e7` 9-12 · `1abb456ef` 9-06)
|
||||
|
||||
## ① 기준 커밋
|
||||
|
||||
속도 · 실시간 · 보안 작업
|
||||
|
||||
| sha | 날짜 | 내용 |
|
||||
|---|---|---|
|
||||
| 7fdbb343a | 08-23 | 코리도 편집 즉시 반영 · 프리즈 제거(프레임 갭 3603→100ms) |
|
||||
| 08c0dc322 | 08-29 | 자동저장 제거 · 초기값 스냅샷 |
|
||||
| a35c8ff35 | 09-03 | 계획선 편집 중 횡단 재계산을 브라우저로 — 서버 왕복 제거 |
|
||||
| c2f681f9d · 8dca4bbfb | 09-04 | 휠 · 세로 맞춤 실시간(다시 그리기 대신 변환) |
|
||||
| 3cd717d8f | 09-06 | [저장]·[확정] 정본 서버 재계산 |
|
||||
| 9da3bce1b | 09-06 | 카드 버튼 즉시 저장 → 캐시 · `designRequestSeq` 제거(최신만 채택은 `B05_Profile_UI_Profile_Preview.ts:35-57` seq 가 맡음) |
|
||||
| c0bfcba34 | 09-06 | 유토 배분을 서버 Node 로(사용자 확정 · 400ms 잠잠해지면 · 최신만) |
|
||||
| df2dedcbc · 8e3c1f91f | 09-06 | 진입 중복 호출 제거 |
|
||||
| 0b30f5b03 · 1abb456ef · 4df5d0a7e | 09-06 | 지형 색인 · 카드 강제 레이아웃 67→1회 · B06 진입 2.3~2.9s → 2.0s |
|
||||
| 674777626 | 09-06 | 세션 확인 활동시각 쓰기 1분 1회 |
|
||||
| **9da48d5e7** | 09-12 | **B06 ▲▼ — `refreshCards` 묶음 + 120ms 디바운스 · 한 번 누름 920→17~23ms · 최장 멈춤 180~229ms · 1.5초 누름 누적 멈춤 2,577→473ms** |
|
||||
|
||||
좌측 패널 통합(통합 직전 기준 `cd26699d2` 09-29)
|
||||
|
||||
| sha | 날짜 | 내용 |
|
||||
|---|---|---|
|
||||
| 439f605ba · 7760dcd77 · 2c520537b | 09-29 | 좌측 칸 등록부 · 설계 가게 한 곳 · 저장 한 줄기 |
|
||||
| 38aae89b2 | 09-29 | 종단도 한 벌(`design_profile.ts`) — B06 위 종단도도 B05 것으로 |
|
||||
| 8a9456579 | 09-29 | 고름 한 곳 |
|
||||
| **dd8066275** | 09-29 | **옛 B06 계획선 편집 껍데기 삭제 — 120ms 디바운스 같이 사라짐** · 유토 뱃지 한 계산(전 측점 `structureAreaRows`) |
|
||||
| 90a2cb9a1 | 09-29 | 한 페이지 뼈대 · 숨은 3D 그리기 멈춤 |
|
||||
| **473d959d6** | 09-29 | **가운데 보기 전환 — 폼 한 벌 · `pipeReloadTimer` 추가** |
|
||||
| 7f04bf961 · 767adc4a4 | 09-29~30 | 관 · 옹벽 칸 한 곳 |
|
||||
| fc54a385d | 10-05 | 구조물 배치 칸을 정의대로 |
|
||||
| 0a095c14b … 1a9055444 | 10-05~06 | 52장 명세 공용 틀 + 구조물별 폼 |
|
||||
|
||||
## ② 수치 (지금 판)
|
||||
|
||||
열: 입력→그림 = Event Timing 최대(ms · 괄호 = 그중 JS) · 긴 작업 = 50ms 넘는 멈춤(ms) · 요청 = 조작 하나에 나간 요청(올림/받음) · 캐시 쓰기 = sessionStorage setItem 수 · 좌측 = 좌측 패널 DOM 갈아 끼운 노드 수 · DOM = 전체 · 오버레이 = 잠금 오버레이 뜸
|
||||
|
||||
### B05 (종단 보기)
|
||||
|
||||
| 조작 | 입력→그림 | 긴 작업 | 요청 | 캐시 쓰기 | 좌측 | DOM | 오버레이 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 쉼 3초(3D 켜짐) | — | 0 | 0 | 0 | 0 | 1 | 0 |
|
||||
| 종단 패널 펼침 | 16~24 | 0 | 0~1(설정 PUT) | 0 | 0 | 0~17 | 0 |
|
||||
| 구조물 고르기(목록 클릭) | 72~136 (38~54) | 0~57 | 0 | 1 | 95~114 | 106~147 | 0 |
|
||||
| 좌측 칸 — 돌쌓기 높이 2→1.5 | 16 | 53 · 88 | 0 | 1 (2KB) | 68 | 84 | 0 |
|
||||
| 좌측 칸 — 돌쌓기 측점 +m | 80~128 | 52~76 · 88 | 0 | 1 (2KB) | 72 | 88 | 0 |
|
||||
| 좌측 칸 — 메모 6자 타자 | ≤24 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
| 좌측 칸 — 배관 관경 바꿈 | — | 59~64 · 68~93 | **1 · POST detail-basins 1KB/96KB · 211~478ms** | 1~2 | 52~119 | 143~245 | 0 |
|
||||
| 측점 끌기(구조물 알약 40px) | **160~240 (57~87)** | 59~95 · 62~95 | 0 | 1~2 | 136~158 | 172~182 | 0 |
|
||||
| 종단 점 ▲ 한 번 | 56~72 (12~22) | 53~78 | 1 · haul-plan 22KB/0.6KB(400ms 뒤) | 2 | 0 | 36 | 0 |
|
||||
| 종단 점 ▲ 2.5초 길게(약 20회) | 32~56 | 최대 64 | 2 · haul-plan | **42** | 0 | 558 | 0 |
|
||||
|
||||
- 쉼 3초 메인 스레드 시간 **1,484~1,600ms (≈50%)** · WebGL clear 700회(초당 ≈230) — 3D 가 가만히 있어도 매 프레임 그림 → 위 모든 B05 조작에 깔리는 바닥 부하
|
||||
- B05 진입: 5.9초 · 요청 21 · 받음 등고선 geojson **36.7MB** + 지형 미리보기 7.8MB + 등고선 7.0MB + 횡단 상세 2.6MB · 진입 때 `POST route/structures/migrate`(쓰기) · 긴 작업 합 832ms
|
||||
|
||||
### B05 ↔ B06 넘김
|
||||
|
||||
| 조작 | 긴 작업 | 요청 | 캐시 쓰기 | 좌측 | DOM |
|
||||
|---|---|---|---|---|---|
|
||||
| B05 → B06 (첫 번째) | **752** · 214 · 403 · 72 · 57 · 154 (합 1,652) | 6 · pipe-points 96KB 다시 받음(1,036ms) · missing-stations 541ms · haul-plan | 10~11 | 392~525 | 2,402~2,560 |
|
||||
| B06 → B05 | 198~231 · 104~114 | 1 (structures) | 2 | 207~390 | 863~1,094 |
|
||||
| B05 → B06 (두 번째) | **1,231~1,242** · 76~98 · 81~125 · 137~148 | 2 (structures · pipe-points — 메인 스레드 막혀 1,308~1,341ms 대기) | 1 | 260~371 | 1,474~1,611 |
|
||||
|
||||
- B06 쉼 3초 메인 스레드 5ms · WebGL 0 (숨은 3D 멈춤은 정상)
|
||||
- 통합 전 B06 페이지 진입 긴 작업 합 1,604~1,728ms(1abb456ef) → 지금은 **같은 페이지 안 보기 바꿈인데도** 1.5~1.7초를 매번 다시 씀
|
||||
|
||||
### B06 (횡단 보기)
|
||||
|
||||
| 조작 | 입력→그림 | 긴 작업 | 요청 | 캐시 쓰기 | 좌측 | DOM |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 종단 점 ▲ 한 번 | 40 | **587** | 1 · haul-plan | 2 | 0 | 900 |
|
||||
| 종단 점 ▲ 2.5초 길게 | pointerup **344** | **663 · 558 · 545 · 500** · 95 · 162 · 161 (합 2,684) | 4 · haul-plan(325~851ms) | 8 | 0 | 3,564 |
|
||||
| 횡단 조정 줄 — 카드 암 경계 ▲ | 96 (63) | 68 | 0 | 1 | 67 | 118 |
|
||||
| 구조물 고르기(돌쌓기) | 128 (74) | 80 | 0 | 2 | **247** | 279 |
|
||||
| 구조물 고르기(배관) | 120 (75) | 82 | 0 | 2 | **286** | 342 |
|
||||
| 좌측 칸 — 배관 관경 바꿈 | — | 109 · 54 | **2 · POST detail-basins + haul-plan** | 3 | 52 | 717 |
|
||||
| 좌측 칸 — 돌쌓기 높이 2→1.5 | 16 | 0 | 0 | **0** | **0** | **0** |
|
||||
|
||||
- **B06 ▲ 통합 전 대비(9da48d5e7 실측)**: 한 번 누름 최장 멈춤 180~229 → **587ms** · 길게 누름 누적 멈춤 473ms(1.5초) → **2,684ms(2.5초)** · 9-12 에 고친 것이 9-29 통합에서 되돌아감
|
||||
- B06 보기에서 돌쌓기 높이 칸을 바꿔도 캐시 쓰기 · 화면 변화 0 (B05 보기에선 즉시 반영) — 속도와 별개 흠 의심 · 확인 필요
|
||||
- 잠금 오버레이: 모든 조작에서 0 (규칙 맞음)
|
||||
|
||||
## ③ 느린 원인 후보 (6)
|
||||
|
||||
| # | 자리 | 왜 | 실측 근거 | 생긴 때 |
|
||||
|---|---|---|---|---|
|
||||
| S1 | `A00_Common/design/design_profile.ts:125` (`CROSS_PREVIEW_DEBOUNCE_MS = 16`) · `:367-392` · `B06_Section/B06_Section_UI_Section_View.ts:393-397` · `:287-291` · `B06_Section/B06_Section_Cross_Refresh.ts:129-140` | B06 ▲ 마다(초당 10회) 전 측점 재계산 → 바뀐 측점 카드 전부 다시 만들기 → 종단도 두 번 그림(`draw()` + `refreshCards`→`redrawProfile`) → 전 측점 `structureAreaRows` · 옛 B06 120ms 디바운스 사라짐 | B06 ▲ 587ms · 길게 500ms+ ×4 | dd8066275 · 38aae89b2 (통합) |
|
||||
| S2 | `A00_Common/design/design_view_cross.ts:493-497` (`host.onShow`) | 횡단 보기 보일 때마다 구조물 다시 읽기 · 상세 다시 받기 · `reconcileStaleDesigns` 전 측점 재계산 · 카드 전부 다시 — 바뀐 것 없어도 | B05→B06 1,231ms 한 덩어리 · pipe-points 96KB 매번 | 473d959d6 (통합) |
|
||||
| S3 | `B05_Profile/B05_Profile_UI_Viewer.ts:357-376` (`animate`) | 3D 가 쉬는 중에도 매 프레임 `renderer.render` — 메인 스레드 ≈50% 상시 점유 → 모든 B05 조작이 그 위에 얹힘 | 쉼 3초 CPU 1.5초 · clear 230/초 | 통합 전부터(90a2cb9a1 은 숨길 때만 멈춤) |
|
||||
| S4 | `A00_Common/design/design_view_cross.ts:140-143` (`pipeReloadTimer`) → `B05_Profile/B05_Profile_UI_Drainage_Panel.ts:551-553` · `:402-407` · B05 쪽 `:612-613` | 관 옵션 하나 바꿀 때마다 서버 `detail-basins` 왕복(96KB 받음) → 배수유역 목록 다시 그림 → 관 변경 알림 다시 퍼뜨림 — 5장 「조작 중 네트워크 왕복 금지」 어긋남 | 관경 바꿈 POST 163~478ms · B06 DOM 717 | B05 쪽 통합 전부터 · B06 쪽 473d959d6 |
|
||||
| S5 | `B06_Section/B06_Section_UI_Page_Pipe_Options.ts:144-148` | 관 옵션 바꿀 때 덮는 범위 측점마다 `refreshCard` → 측점마다 공용 종단도 전체 `draw()` — 9-12 에 고친 꼴(측점 수만큼 다시 그림)이 다시 생김 | 코드 읽기 · 관경 바꿈 긴 작업 109ms | 통합 때 커짐(무거운 공용 종단도로 바뀜) |
|
||||
| S6 | `B05_Profile/B05_Profile_UI_Structures_Panel.ts:393-400` (`liveCommit`) → `B05_Profile_UI_Structures_Panel_Commit.ts:216-219` · `B06_Section/B06_Section_UI_Page_Structures_Panel.ts:167-172` · `A00_Common/design/design_structure_form.ts:158-159` · `:214-234` · B06 쪽 `design_view_cross.ts:207-224` · `:447-462` | 고르기 · 칸 하나 · 알약 끌기마다 좌측 목록 통째 + 명세 폼 다시 만들기 · B06 은 목록을 두 번 다시 그리고 카드 전체 · 공용 종단도까지 | 고름 B05 95~114 · B06 247~286 노드 · 알약 끌기 160~240ms | 통합 · 52장 폼으로 무거워짐 |
|
||||
|
||||
덧(조작 아님): B05 진입 등고선 geojson 36.7MB · 진입 때 `structures/migrate` 쓰기 · 유토 배분 `haul-plan` 은 조작마다 22KB 올림이나 사용자 확정 설계(서버 Node · 400ms 잠잠 · 최신만)
|
||||
|
||||
## ④ 보안 (흠 11)
|
||||
|
||||
인증 · 권한 공통: 모든 B04/B05/B06 라우터가 `protected_with_company`(`main.py:613-637`) → 세션 + 회사 + 프로젝트가 같은 회사인지(`common_util/common_util_auth.py:105` · `:179` · `:187`) · `project_id` UUID 형 · `route_id` 는 프로젝트와 대조 · 조작 중 자동저장 PUT 없음
|
||||
|
||||
| # | 무게 | 자리 | 흠 |
|
||||
|---|---|---|---|
|
||||
| H1 | **치명** | `B05_Profile/B05_Profile_Structures_Schema.py:175` · `B05_Profile/B05_Profile_Structures_Repository.py:123` · `M02_MasterTemplete/M02_Item_Snapshot.py:67-69` · `:155-165` · `:212-223` | 구조물 저장 때 `structure_id` 아무 글자 그대로 저장 → 저장 뒤 `safe_sync` 가 `base / structure_id` 폴더를 `shutil.rmtree` · 그 자리에 씀 → `"../../.."` · 절대경로로 서버 디스크 폴더 지우기 가능(회사 사용자 누구나) · 안전 이름 `FOLDER_PATTERN`(`:53`) 이 있는데 여기선 안 씀 (코드로 확인함) |
|
||||
| H2 | 높음 | `B05_Profile/B05_Profile_Schema.py:66-67` · `:272` → `B05_Profile/B05_Profile_Engine_Solver.py:49-155` · `common_util/common_util_surface_sampler.py:134` · `B05_Profile/B05_Profile_Engine_Skeleton.py:284` | `filter_key` · `method` 자유 글자가 파일 이름에 들어감 → 경로 넘기(다른 프로젝트 지형 읽기 · 캐시 파일 쓰기) · confirm 은 값을 저장까지 |
|
||||
| H3 | 중간 | `B05_Profile/B05_Profile_Structures_Repository.py:200-202` · `common_util/common_util_drainage_pipes.py:345-361` | **52장 명세 칸 서버 검사 없음** — 정의에 없는 키(동적 `{머리}_{n}_{항목}` 포함)는 `continue` 로 통과 · 관 `options` 그대로 저장 · 명세 규칙은 브라우저(`A00_Common/design/design_structure_spec.ts`)에만 · 파이썬 짝 없음 · 자유 글자 길이 제한 없음 |
|
||||
| H4 | 중간 | `A00_Common/design/design_structure_ford_calc.ts:10` | 세월교 폭→최소 높이 규칙이 브라우저에만 · 서버는 받은 `ford_height_m` 그대로 — 5장 ⛔(브라우저만 계산) |
|
||||
| H5 | 중간 | `B05_Profile/B05_Profile_Router_Corridor.py:131-160` · 호출 `B05_Profile_UI_Corridor.ts:208` · `:228` | 코리도 PUT 이 브라우저가 만든 기하 · hash 를 검사 없이 저장 · 첫 빌드 때 자동 PUT — 5장 ⛔ · 서버가 만들 수 있음(`B05_Profile_Corridor_Prebuild.py`) |
|
||||
| H6 | 중간 | `B06_Section/B06_Section_Schema.py:198` · `B06_Section/B06_Section_Router_Confirm.py:124-169` | B06 저장 · 확정이 브라우저 `mass_haul` · 면적 · 관 길이를 먼저 적고 서버 재계산이 덮음 — 재계산 실패하면 로그만 · 브라우저 값 남음 · `standard_cross_section` 검사 없는 dict |
|
||||
| H7 | 낮음 | `B06_Section/B06_Section_Schema.py:174-177` (대 `:44-47`) | `CrossSectionPatch` 의 `ground_type` 등 자유 글자 · 사전 키 · 목록 길이 제한 없음 |
|
||||
| H8 | 낮음 | `B06_Section/B06_Section_Router.py:599` | 안 쓰는 `POST cross-design` 이 DB 에 바로 씀(저장 밖 쓰기 길) |
|
||||
| H9 | 낮음 | `design_view_profile.ts:503` · `:459` · `B05_Profile_UI_Corridor.ts:208` | [저장] 밖 쓰기 — 등고선 간격 PUT · 진입 때 structures/migrate · 코리도 첫 PUT — 5장 어긋남 |
|
||||
| H10 | 낮음 | `B06_Section_Router.py:431` · replan · cross-preview | `cross_half_width_m` 상한 없음 · `vertices` 길이 제한 없음 · 코리도 크기 검사가 전체 파싱 뒤 |
|
||||
| H11 | 낮음 | `station_offsets` · `curve_radii` · 관 `chainage_m` · `B05_Profile_Repository.py:256` | NaN/Infinity 통과 · 관 측점이 노선 길이 검사 없음 · `surface_model_id` 조회에 프로젝트 조건 없음(좌표계 코드만 샘) |
|
||||
|
||||
- 참고: 프로젝트 권한이 회사 단위뿐(멤버 · 읽기 전용 역할 없음) — 정책 결정 사항
|
||||
- 서버 정본 재계산: 계획선 저장(`B05_Profile_Router.py:387-402`) · B06 저장(`recompute_server_side`) 맞음 · 구조물은 등록부 대조만(재계산 아님)
|
||||
|
||||
## ⑤ 고칠 갈래와 차례 (8 · 작은 것부터)
|
||||
|
||||
| 차례 | 갈래 | 크기 | 예상 효과 |
|
||||
|---|---|---|---|
|
||||
| 1 | H1 — `structure_id` 를 `FOLDER_PATTERN` 으로 스키마 검사 + `sync_snapshots` 에서 한 번 더 | 몇 줄 | 서버 폴더 지우기 막음(최우선) |
|
||||
| 2 | H2 — `filter_key` · `method` 를 Literal 화이트리스트 | 몇 줄 | 경로 넘기 막음 |
|
||||
| 3 | S1 — 횡단 쪽 재계산 120ms 디바운스 되살림 · `onCrossDesignsUpdated` 받는 쪽에서 종단도 두 번째 그리기 뺌 · 숨은 보기 건너뜀 | 작음 | B06 ▲ 587ms → 9-12 수준(최장 ≈200ms · 길게 누적 ≈0.5초) |
|
||||
| 4 | S5 · S4 — 관 옵션 측점 루프를 `refreshCards` 한 번으로 · `detail-basins` 서버 왕복을 [저장] 때로(조작 중은 로컬) | 작음~중간 | 관 옵션 조작 왕복 0 · 다시 그림 1회 |
|
||||
| 5 | S2 — 횡단 보기 다시 보일 때 가게 판번호가 같으면 다시 읽기 · 전 측점 재계산 건너뜀 | 중간 | 보기 넘김 1.2~1.7초 → 바뀐 것만 |
|
||||
| 6 | S3 — 3D 그리기를 「바뀔 때만」(카메라 움직임 · 장면 변경 때 render 요청) | 중간 | 쉬는 중 메인 스레드 ≈50% → ≈0 · B05 조작 전반 체감 |
|
||||
| 7 | S6 — 고름 · 칸 바꿈 때 좌측 목록 통째 대신 바뀐 줄만 · 폼 다시 만들기 줄임 · B06 목록 두 번 그리기 없앰 | 중간 | 고름 B06 120ms → B05 수준 이하 |
|
||||
| 8 | H3~H11 — 52장 명세 서버 clean(파이썬 짝 + 거울 시험) · 세월교 높이 서버 검산 · 코리도 서버 생성 · B06 재계산 실패 시 거절 · 안 쓰는 POST 삭제 · [저장] 밖 쓰기 정리 · 상한 · NaN 막기 | 큼 | 5장 「서버 정본」 · 입력 검사 채움 |
|
||||
|
||||
## 남은 확인
|
||||
|
||||
- B06 보기 돌쌓기 높이 칸 바꿈 반영 0 (캐시 · 화면 모두) — 별도 흠 의심
|
||||
- B05 배관 관경 바꿈 뒤 고름이 풀린 일 1회(재현 불확실)
|
||||
- 통합 전 판 실행 대조가 필요하면 브레인 결정(워크트리 필요)
|
||||
- ORCA 내장 탭 10초 강제 내림 — 다른 창 탭 정리 또는 탭 보이게 두면 풀릴 수 있음
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"판": 13,
|
||||
"판": 14,
|
||||
"schema_version": 1,
|
||||
"comment": "B05 구조물 타입 레지스트리 정본. 근거·옵션 상세는 PLAN.md 구조물 리스트와 resources/knowledge/technical_info/01_임도 참조. 2026-08-17 선택지 재편: B군(종단배수)·F군(생태/녹화)·G군(노면공)은 enabled:false 보존 — B05 선택지에서 빠지고 B06 개별 횡단도 옵션으로 재사용한다. 바닥막이(bed_sill)·생태연못(eco_pond)은 임도 미사용 확정으로 삭제. 배관·BOX암거·물넘이·세월교는 pipe_points.json 정본 관리(managed_by) — structures.json에 저장하지 않으며, 배관 부속(유형/집수정/기슭막이/돌붙임)·날개벽 옵션 정의는 B06/B07 필수 승격의 선언 근거다. ★기본값 원칙(2026-08-16 크로스체크 2차): default를 두는 것은 ①법정이 등급과 무관하게 못박은 단일 수치 ②사용자가 개발 단계에서 확정한 값 ③도메인 수치가 아닌 표시용 문자열, 이 셋뿐이다. 재료·형식 같은 설계자 판단 선택지와 미확정 제원은 default 없이 required로 두어 사용자가 직접 고르게 한다 (pipe_diameter_mm 1000 = 별표2 원칙값+사용자 확정 — 화이트리스트 등재). BOX암거 본체 2.0×2.0·날개벽 유입/유출 있음·짧은쪽 높이 1m·길이 2m·각도 45°는 2026-08-17 사용자 확정값이다 — 본체 규격은 실무 관측 규격(2.0×2.0·3.0×3.0)을 폼에서 프리셋 select로 받고 필요 시 사용자 지정으로 자유 입력하되, 저장 키는 body_width_m·body_height_m 숫자 그대로다(프리셋 전용 키를 만들지 않는다). 이 값들은 B05 서브폼이 실제로 받으므로 phase를 detail로 두지 않는다. ★보호공 개편(2026-08-17 사용자 지시): 구 `*_pitching`(있음/없음) + `*_pitching_finish`(찰/메) 두 축을 `*_protection` 한 축(돌붙임(찰)·돌붙임(메)·도수로)으로 합쳤다 — 도수로·산비탈수로(B4)는 유출부에서 돌붙임 대신 쓰는 공종이라 별도 구조물이 아니라 보호공 선택지로 받는다. \"없음\"은 삭제(물이 흐르는 자리라 보호공 필수). 치수는 배타 — 돌붙임이면 `*_protection_area_m2`(10㎡), 도수로면 `*_protection_width_m`(1m, 지식DB 수치 근거 없음·사용자 확정, 실무 확인 후 개선). 구 저장분은 `common_util_drainage_pipes.parse_pipe_points`가 새 키로 옮긴다.",
|
||||
"구조물군": {
|
||||
@@ -2917,7 +2917,7 @@
|
||||
"type_id": "spoil_bank",
|
||||
"group": "E",
|
||||
"name": "유용토운반작업장(사토장)",
|
||||
"placement": "interval",
|
||||
"placement": "site",
|
||||
"style": {
|
||||
"color": "#8c9a2e",
|
||||
"abbr": "사토장"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
/* 54-9 종단 알약 글 단위검증 헬퍼 — TS 를 그 자리에서 트랜스파일해 Node 로 돌린다.
|
||||
* 결과는 표준출력에 JSON 한 벌. 화면 그릇은 쓰지 않는다(세션 저장소만 흉내).
|
||||
*
|
||||
* ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_structures_form_summary.cjs` 머리와 같다.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
|
||||
const session = new Map();
|
||||
const fakeStorage = {
|
||||
getItem: (k) => session.get(k) ?? null,
|
||||
setItem: (k, v) => session.set(k, String(v)),
|
||||
removeItem: (k) => session.delete(k),
|
||||
};
|
||||
globalThis.window = { sessionStorage: fakeStorage, localStorage: fakeStorage };
|
||||
globalThis.sessionStorage = fakeStorage;
|
||||
globalThis.localStorage = fakeStorage;
|
||||
|
||||
const STUBS = {
|
||||
"@config/config_frontend": { API_BASE_URL: "/api", API_TIMEOUT_MS: 1000 },
|
||||
};
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
if (request.startsWith("@")) return new Proxy({}, { get: () => () => {} });
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const marks = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Structures_Marks.ts"));
|
||||
|
||||
const typeMap = new Map([
|
||||
["etc", { type_id: "etc", name: "기타", style: { abbr: "기타" }, options: [] }],
|
||||
[
|
||||
"retaining_wall",
|
||||
{ type_id: "retaining_wall", name: "옹벽", style: { abbr: "옹" }, options: [] },
|
||||
],
|
||||
]);
|
||||
const etc = (id, chainage, name) => ({
|
||||
structure_id: id,
|
||||
type_id: "etc",
|
||||
placement: "point",
|
||||
chainage_m: chainage,
|
||||
options: name === undefined ? {} : { name },
|
||||
});
|
||||
const wall = (id, chainage) => ({
|
||||
structure_id: id,
|
||||
type_id: "retaining_wall",
|
||||
placement: "point",
|
||||
chainage_m: chainage,
|
||||
options: {},
|
||||
});
|
||||
const run = (structures) =>
|
||||
Object.fromEntries(
|
||||
[...marks.structurePillTexts(structures, typeMap)].map(([id, text]) => [id, text]),
|
||||
);
|
||||
|
||||
const out = {
|
||||
// 53-2 시나리오 — 같은 이름 둘 · 다른 이름 · 빈 이름 둘(빈칸 이름 = 빈 이름)
|
||||
scenario: run([
|
||||
etc("e1", 40, "배수로정비"),
|
||||
etc("e2", 60, "울타리"),
|
||||
etc("e3", 80, ""),
|
||||
etc("e4", 100, "배수로정비"),
|
||||
etc("e5", 120, " "),
|
||||
]),
|
||||
// 머리가 같은 다른 이름 — 갈릴 때까지 늘림 · 이름이 남의 머리면 그 이름 그대로
|
||||
prefix: run([
|
||||
etc("p1", 10, "콘크리트 블록"),
|
||||
etc("p2", 20, "콘크리트 옹벽"),
|
||||
etc("p3", 30, "배수로"),
|
||||
etc("p4", 40, "배수로공"),
|
||||
etc("p5", 50, "기타 시설"),
|
||||
etc("p6", 60),
|
||||
]),
|
||||
// 다른 종류 규칙은 그대로 — 약호 + 중복 번호 · 기타 하나 빈 이름 = 「기타」
|
||||
others: run([wall("w1", 10), wall("w2", 30), etc("x1", 20, "가로등"), etc("x2", 40)]),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -72,3 +72,19 @@ def test_B06_사토_배분은_부피를_그대로_읽음_옛_값도():
|
||||
assert [s["capacity_m3"] for s in sites] == [150.0, 90.0]
|
||||
assert sites[0]["side_option"] is None and sites[1]["side_option"] == "좌"
|
||||
assert sites[1]["extra_distance_m"] == 5
|
||||
|
||||
|
||||
def test_54_7_사토장_기준_측점_꼴은_토취장과_같음_옛_구간은_기준_측점으로_읽음():
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
|
||||
|
||||
by_id = {t.type_id: t for t in load_structure_types()}
|
||||
assert by_id["spoil_bank"].placement == by_id["borrow_pit"].placement == "site"
|
||||
old = {"type_id": "spoil_bank", "placement": "interval", "start_m": 60, "end_m": 80}
|
||||
for extra, station in (({"chainage_m": 70}, 70.0), ({}, 60.0)):
|
||||
item = StructureInstance.model_validate({**old, **extra})
|
||||
assert (item.placement, item.chainage_m, item.start_m, item.end_m) == (
|
||||
"site",
|
||||
station,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""54-9 종단 알약 글 — 기타가 여럿이어도 알약끼리 구별됨.
|
||||
|
||||
• 손 이름 앞 3글자 · 같은 이름이면 목록과 같은 번호 · 빈 이름 = 「기타 N」
|
||||
• 머리가 같은 다른 이름은 갈릴 때까지 늘림
|
||||
• 다른 종류 알약(약호 + 중복 번호) · 툴팁 · 목록 이름(손 이름 그대로)은 그대로
|
||||
화면 그릇 없이 Node 헬퍼가 TS 를 트랜스파일해 돌린다(`helper_54_9_etc_pill.cjs`).
|
||||
B05 · B06 은 같은 알약 레인(`buildStructureLane`)을 씀 — 실제 화면은 ORCA 로 따로 본다.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_54_9_etc_pill.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_53_2_기타_다섯_알약이_서로_다르다(out):
|
||||
pills = {key: value["pill"] for key, value in out["scenario"].items()}
|
||||
assert pills == {
|
||||
"e1": "배수로 1",
|
||||
"e2": "울타리",
|
||||
"e3": "기타 1",
|
||||
"e4": "배수로 2",
|
||||
"e5": "기타 2",
|
||||
}
|
||||
assert len(set(pills.values())) == 5
|
||||
|
||||
|
||||
def test_툴팁_이름은_손_이름_그대로(out):
|
||||
names = [value["name"] for value in out["scenario"].values()]
|
||||
assert names == ["배수로정비 1", "울타리", "기타 1", "배수로정비 2", "기타 2"]
|
||||
|
||||
|
||||
def test_머리가_같은_다른_이름은_갈릴_때까지_늘린다(out):
|
||||
pills = {key: value["pill"] for key, value in out["prefix"].items()}
|
||||
assert pills == {
|
||||
"p1": "콘크리트블",
|
||||
"p2": "콘크리트옹",
|
||||
"p3": "배수로",
|
||||
"p4": "배수로공",
|
||||
"p5": "기타시",
|
||||
"p6": "기타",
|
||||
}
|
||||
|
||||
|
||||
def test_다른_종류_알약은_약호와_번호_그대로(out):
|
||||
pills = {key: value["pill"] for key, value in out["others"].items()}
|
||||
assert pills == {"w1": "옹 1", "w2": "옹 2", "x1": "가로등", "x2": "기타"}
|
||||
|
||||
|
||||
def test_알약_레인이_글을_한_곳에서_받는다():
|
||||
with open(
|
||||
os.path.join(ROOT, "B05_Profile", "B05_Profile_UI_Structures_Marks.ts"), encoding="utf-8"
|
||||
) as handle:
|
||||
text = handle.read()
|
||||
assert "mark.textContent = pill;" in text
|
||||
assert text.count("structurePillTexts(") == 2 # 정의 하나 · 레인 하나
|
||||
@@ -4,7 +4,8 @@
|
||||
① 용량이 커질수록 폭이 넓어지고, 상한에서 멈추면 못 담은 몫이 값으로 남는다.
|
||||
② 사토장이 선 측점은 **노선 성토가 줄고** 그만큼이 사토장 몫으로 간다(두 번 세지 않기).
|
||||
③ 「자동」이면 그 측점의 **성토 쪽**에 선다.
|
||||
④ 구간 밖 측점은 안 건드린다 — **새 측점을 만들지 않는다**.
|
||||
④ 기준 측점 밖 측점은 안 건드린다 — **새 측점을 만들지 않는다**.
|
||||
⑤ 사토장 = 기준 측점 한 점(PLAN 54-7) — 가장 가까운 측점 하나 · 옛 구간 저장분은 시작 측점.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -65,9 +66,8 @@ def _write_site(root: Path, capacity_m3: float, **options) -> None:
|
||||
{
|
||||
"structure_id": "sp-1",
|
||||
"type_id": "spoil_bank",
|
||||
"placement": "interval",
|
||||
"start_m": 100.0,
|
||||
"end_m": 160.0,
|
||||
"placement": "site",
|
||||
"chainage_m": 120.0,
|
||||
"options": {"capacity_m3": capacity_m3, **options},
|
||||
}
|
||||
],
|
||||
@@ -86,15 +86,15 @@ def _run(root: Path, capacity_m3: float, **options) -> list[dict]:
|
||||
|
||||
|
||||
def _placed(sections: list[dict]) -> float:
|
||||
inside = [s for s in sections if 100.0 <= s["chainage_m"] <= 160.0]
|
||||
inside = [s for s in sections if s["chainage_m"] == 120.0]
|
||||
return float(inside[0]["design"].get("spoil_fill_placed_m3") or 0.0)
|
||||
|
||||
|
||||
def test_용량이_클수록_폭이_넓어진다(tmp_path: Path) -> None:
|
||||
small = _run(tmp_path, 200.0)
|
||||
large = _run(tmp_path, 800.0)
|
||||
narrow = small[1]["design"]["spoil_fill_width_m"]
|
||||
wide = large[1]["design"]["spoil_fill_width_m"]
|
||||
narrow = small[2]["design"]["spoil_fill_width_m"]
|
||||
wide = large[2]["design"]["spoil_fill_width_m"]
|
||||
assert 0 < narrow < wide
|
||||
assert _placed(small) == pytest.approx(200.0, rel=0.02)
|
||||
assert _placed(large) == pytest.approx(800.0, rel=0.02)
|
||||
@@ -103,7 +103,7 @@ def test_용량이_클수록_폭이_넓어진다(tmp_path: Path) -> None:
|
||||
def test_담을_수_없으면_남은_몫이_값으로_남는다(tmp_path: Path) -> None:
|
||||
"""상한에서 멈춘다 — 임의로 더 넓히지 않는다."""
|
||||
sections = _run(tmp_path, 1_000_000.0)
|
||||
design = sections[1]["design"]
|
||||
design = sections[2]["design"]
|
||||
assert design["spoil_fill_unplaced_m3"] > 0
|
||||
assert design["spoil_fill_width_m"] == pytest.approx(design["spoil_fill_max_width_m"])
|
||||
|
||||
@@ -123,16 +123,16 @@ def test_노선_성토가_그만큼_줄어든다(tmp_path: Path) -> None:
|
||||
def test_자동이면_성토_쪽에_선다(tmp_path: Path) -> None:
|
||||
sections = _run(tmp_path, 400.0, side="자동(성토 쪽)")
|
||||
# right_cut = 우측 절토 ⇒ 성토는 좌측(+offset).
|
||||
assert sections[1]["design"]["spoil_fill_side"] == "left"
|
||||
assert sections[2]["design"]["spoil_fill_side"] == "left"
|
||||
|
||||
|
||||
def test_구간_밖_측점은_안_건드린다(tmp_path: Path) -> None:
|
||||
"""새 측점을 만들지 않고, 구간 밖은 종전 그대로다(확정 ③)."""
|
||||
def test_기준_측점_밖_측점은_안_건드린다(tmp_path: Path) -> None:
|
||||
"""새 측점을 만들지 않고, 기준 측점 밖은 종전 그대로다(확정 ③)."""
|
||||
sections = _run(tmp_path, 400.0)
|
||||
assert len(sections) == 6
|
||||
for section in sections:
|
||||
design = section["design"]
|
||||
if 100.0 <= section["chainage_m"] <= 160.0:
|
||||
if section["chainage_m"] == 120.0:
|
||||
assert design["spoil_fill_area_m2"] > 0
|
||||
else:
|
||||
assert design.get("spoil_fill_area_m2", 0) == 0
|
||||
@@ -186,3 +186,62 @@ def test_사토장을_지우면_값도_사라진다(tmp_path: Path) -> None:
|
||||
assert design.get("spoil_fill_capacity_m3") in (None, 0), section["chainage_m"]
|
||||
# 노선 성토도 원래 값으로 돌아온다 — 사토장 몫을 빼 두었던 것이 복구된다.
|
||||
assert sections[2]["design"]["fill_area_m2"] == pytest.approx(plain_fill, abs=1e-6)
|
||||
|
||||
|
||||
def _write_raw(root: Path, item: dict) -> None:
|
||||
(root / "B05_Profile" / "route").mkdir(parents=True, exist_ok=True)
|
||||
(root / "B05_Profile" / "route" / "structures.json").write_text(
|
||||
json.dumps({"revision": 1, "structures": [item]}, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def _spoiled(sections: list[dict]) -> list[float]:
|
||||
return [
|
||||
s["chainage_m"] for s in sections if float(s["design"].get("spoil_fill_area_m2") or 0) > 0
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("legacy", "station"),
|
||||
[
|
||||
({"chainage_m": 140.0}, 140.0), # 옛 기준이 있으면 옛 기준
|
||||
({}, 100.0), # 없으면 시작
|
||||
],
|
||||
)
|
||||
def test_옛_구간_저장분은_옛_기준_시작_차례로_읽는다(
|
||||
tmp_path: Path, legacy: dict, station: float
|
||||
) -> None:
|
||||
_write_raw(
|
||||
tmp_path,
|
||||
{
|
||||
"structure_id": "sp-1",
|
||||
"type_id": "spoil_bank",
|
||||
"placement": "interval",
|
||||
"start_m": 100.0,
|
||||
"end_m": 160.0,
|
||||
"options": {"capacity_m3": 200.0},
|
||||
**legacy,
|
||||
},
|
||||
)
|
||||
sections = _sections([80.0, 100.0, 120.0, 140.0, 160.0, 180.0])
|
||||
enforce_spoil_fills(_LONGITUDINAL, sections, tmp_path)
|
||||
assert _spoiled(sections) == [station]
|
||||
|
||||
|
||||
def test_기준_측점_플러스_거리는_가장_가까운_측점에_선다(tmp_path: Path) -> None:
|
||||
_write_raw(
|
||||
tmp_path,
|
||||
{
|
||||
"structure_id": "sp-1",
|
||||
"type_id": "spoil_bank",
|
||||
"placement": "site",
|
||||
"chainage_m": 127.0,
|
||||
"options": {"capacity_m3": 200.0},
|
||||
},
|
||||
)
|
||||
sections = _sections([80.0, 100.0, 120.0, 140.0, 160.0, 180.0])
|
||||
enforce_spoil_fills(_LONGITUDINAL, sections, tmp_path)
|
||||
assert _spoiled(sections) == [120.0]
|
||||
# 대표 길이 = 앞뒤 측점 절반씩 20 m — 부피 = 면적 × 20
|
||||
design = sections[2]["design"]
|
||||
assert design["spoil_fill_placed_m3"] == pytest.approx(design["spoil_fill_area_m2"] * 20.0)
|
||||
|
||||
@@ -153,8 +153,14 @@
|
||||
}
|
||||
items()[i].click(); // 다시 누름 = 풂
|
||||
await w(900);
|
||||
if (selRows().length || litPills().length || (!isB06 && lit3D().length))
|
||||
fail(`${r.text}: 풂 뒤 남음 ${selRows().length}/${litPills()}/${lit3D()}`);
|
||||
// 풂 = 카드 선택도 풀림(54-8)
|
||||
if (
|
||||
selRows().length ||
|
||||
litPills().length ||
|
||||
(!isB06 && lit3D().length) ||
|
||||
(isB06 && cardSel().length)
|
||||
)
|
||||
fail(`${r.text}: 풂 뒤 남음 ${selRows().length}/${litPills()}/${lit3D()}/${cardSel()}`);
|
||||
}
|
||||
out.checked = rows.length;
|
||||
} else if (MODE === "pill") {
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/* 54-7 — B05 · B06 좌측 「구조물 배치」 사토장 기준 측점 = 토취장과 같은 꼴(52-9 시나리오 고침)
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub7_검증)
|
||||
* 앞 준비: 검증 프로젝트 structures.json 에 옛 꼴(구간 18+0~19+0 · 기준 18+10 · 부피 90) 사토장을
|
||||
* 파일로 직접 심음(API 로 보내면 서버가 새 꼴로 바꿔 적음) · 구조물 초안 비움 · orca reload
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m547 = "단계";` 를 붙여
|
||||
* edit → 옛 사토장 · 새 사토장(25+10 · 150) · 토취장(28+0 · 80) 위치 줄 꼴 · 잇기 · [저장] · 그 뒤 orca reload
|
||||
* verify → 다시 연 값 — B05 에서 한 번 · B06(#/b06-section) 에서 한 번(B05 = B06)
|
||||
* 되돌림: 앞 준비 때 떠 둔 structures.json 을 다시 씀 · 초안 비움
|
||||
* 기대 수치: 위치 줄 = 「기준 측점」 칸 하나(No + 거리 입력 둘) — 사토장 = 토취장 · 시작 · 종료 칸 숨김 ·
|
||||
* 옛 사토장 = 18+10(옛 기준) · 부피 90 · 저장 뒤 정본 = site · chainage 370 / 510 / 560
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m547 || "edit";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 20000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
if (fn()) return true;
|
||||
await w(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const projectId = localStorage.getItem("frd_current_project_id");
|
||||
const modUrl = (part) =>
|
||||
performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.filter((n) => n.includes(part))
|
||||
.pop();
|
||||
const api = await import(modUrl("/B05_Profile/B05_Profile_Api_Structures.ts"));
|
||||
const GID = { spoil_bank: "gspoildetail", borrow_pit: "gborrowdetail" };
|
||||
const section = () => document.querySelector(".b05-structure-section");
|
||||
const selects = () => section().querySelectorAll(".b05-structure__grid select");
|
||||
const change = async (el, v) => {
|
||||
el.value = v;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(250);
|
||||
};
|
||||
const row = () => section().querySelector(".b05-structure__position-row");
|
||||
const station = async (index, no, plus) => {
|
||||
const [s, r] = row().children[index].querySelectorAll("input");
|
||||
s.value = String(no);
|
||||
s.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
r.value = String(plus);
|
||||
r.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(250);
|
||||
};
|
||||
const box = (type) => section().querySelector(`[data-b05-group="${GID[type]}"]`);
|
||||
const volume = (type) => box(type)?.querySelector('[data-option-keys="capacity_m3"] input');
|
||||
const linked = (type) =>
|
||||
!!box(type)?.querySelector(".b05-structure__summary-name")?.classList.contains("is-assigned");
|
||||
// 위치 줄 — 보이는 칸마다 「이름 · 입력 수 · 값」
|
||||
const position = () =>
|
||||
[...row().children]
|
||||
.filter((c) => !c.hidden && c.offsetParent !== null)
|
||||
.map((c) => ({
|
||||
label: c.querySelector("span")?.textContent ?? "",
|
||||
inputs: c.querySelectorAll("input").length,
|
||||
value: [...c.querySelectorAll("input")].map((i) => i.value).join("+"),
|
||||
}));
|
||||
const shape = (type) => ({
|
||||
position: position(),
|
||||
boxes: [...section().querySelectorAll("[data-b05-group]")].map((b) => b.dataset.b05Group),
|
||||
volume: volume(type)?.value,
|
||||
linked: linked(type),
|
||||
});
|
||||
const listItem = (re) =>
|
||||
[...document.querySelectorAll(".b05-route__irregular-list li")].find((l) =>
|
||||
re.test(l.textContent.trim()),
|
||||
);
|
||||
const open = async (re) => {
|
||||
await until(() => listItem(re));
|
||||
const li = listItem(re);
|
||||
if (!li) return false;
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
if (!selects()[1].value) (li.querySelector("button") ?? li).click();
|
||||
await until(() => !!selects()[1].value);
|
||||
await w(400);
|
||||
return true;
|
||||
};
|
||||
const pickLink = async (type) => {
|
||||
box(type).querySelector('[data-b05-summary="pick"]').click();
|
||||
await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row"));
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")];
|
||||
const leaf = rows.find((r) => !r.classList.contains("is-branch"));
|
||||
if (leaf) {
|
||||
leaf.click();
|
||||
break;
|
||||
}
|
||||
rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click();
|
||||
await w(300);
|
||||
}
|
||||
await w(300);
|
||||
[...document.querySelectorAll(".ui-item-picker__actions button")]
|
||||
.find((b) => b.textContent.includes("적용"))
|
||||
.click();
|
||||
await w(600);
|
||||
};
|
||||
const save = async () => {
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(500);
|
||||
return until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
|
||||
};
|
||||
const mine = (list) =>
|
||||
list
|
||||
.filter((s) => s.type_id in GID)
|
||||
.map((s) => ({
|
||||
type: s.type_id,
|
||||
placement: s.placement,
|
||||
at: [s.start_m, s.chainage_m, s.end_m],
|
||||
capacity: s.options?.capacity_m3,
|
||||
group_items: Object.keys(s.group_items ?? {}),
|
||||
}));
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "edit") {
|
||||
out.listOld = listItem(/사토장/)?.textContent.trim();
|
||||
// ① 옛 꼴 사토장 — 기준 측점 하나(옛 기준 18+10)
|
||||
out.openedOld = await open(/^18\+10.*사토장/);
|
||||
out.old = shape("spoil_bank");
|
||||
// ② 새 사토장 기준 25+10 · 부피 150
|
||||
section().querySelector(".b05-route__irregular-actions button:last-child").click(); // 리셋
|
||||
await w(400);
|
||||
await change(selects()[0], "E");
|
||||
await change(selects()[1], "spoil_bank");
|
||||
out.newSpoilForm = shape("spoil_bank");
|
||||
const anchor = [...row().children].findIndex((c) => !c.hidden && c.offsetParent !== null);
|
||||
await station(anchor, 25, 10);
|
||||
await change(volume("spoil_bank"), "150");
|
||||
section().querySelector(".b05-route__irregular-actions .is-primary").click();
|
||||
await w(800);
|
||||
// ③ 새 토취장 28+0 · 부피 80
|
||||
section().querySelector(".b05-route__irregular-actions button:last-child").click();
|
||||
await w(400);
|
||||
await change(selects()[0], "E");
|
||||
await change(selects()[1], "borrow_pit");
|
||||
out.newBorrowForm = shape("borrow_pit");
|
||||
await station(anchor, 28, 0);
|
||||
await change(volume("borrow_pit"), "80");
|
||||
section().querySelector(".b05-route__irregular-actions .is-primary").click();
|
||||
await w(800);
|
||||
out.sameShape =
|
||||
JSON.stringify(out.newSpoilForm.position.map((p) => [p.label, p.inputs])) ===
|
||||
JSON.stringify(out.newBorrowForm.position.map((p) => [p.label, p.inputs]));
|
||||
// ④ 잇기
|
||||
await open(/^25\+10.*사토장/);
|
||||
await pickLink("spoil_bank");
|
||||
out.linkSpoil = linked("spoil_bank");
|
||||
await open(/^28\+0.*토취장/);
|
||||
await pickLink("borrow_pit");
|
||||
out.linkBorrow = linked("borrow_pit");
|
||||
out.saved = await save();
|
||||
out.file = mine((await api.fetchStructures(projectId)).structures);
|
||||
}
|
||||
|
||||
if (MODE === "verify") {
|
||||
await until(() => document.querySelectorAll(".b05-route__irregular-list li").length > 0);
|
||||
const r = {};
|
||||
await open(/^18\+10.*사토장/);
|
||||
r.old = shape("spoil_bank");
|
||||
await open(/^25\+10.*사토장/);
|
||||
r.spoil = shape("spoil_bank");
|
||||
await open(/^28\+0.*토취장/);
|
||||
r.borrow = shape("borrow_pit");
|
||||
section().querySelector(".b05-route__irregular-actions button:last-child").click(); // 리셋
|
||||
out.page = location.hash;
|
||||
out.read = r;
|
||||
if (location.hash.includes("b05")) sessionStorage.setItem("__547_b05", JSON.stringify(r));
|
||||
else out.sameDrawing = sessionStorage.getItem("__547_b05") === JSON.stringify(r);
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user