Merge remote-tracking branches 'origin/sub_laptop_2' and 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -154,11 +154,14 @@ function relocateSideRows(panelRoot: HTMLElement, scroll = true): void {
|
||||
clearSideSlots();
|
||||
const side = panelRoot.dataset.side;
|
||||
// 세월교 · BOX 본체 = 옮길 줄 없이 그 명세의 본체 상자만 강조(66-1) — 날개벽 칸이 든 명세 폼에서 찾음
|
||||
if (side === "body") {
|
||||
const body = sideSlots?.wingInlet
|
||||
.closest(".b05-structure__spec")
|
||||
// 관 = 배관 명세 폼의 「관」 상자만 강조(118-1)
|
||||
if (side === "body" || side === "pipe") {
|
||||
const body = (side === "pipe" ? sideSlots?.inlet : sideSlots?.wingInlet)
|
||||
?.closest(".b05-structure__spec")
|
||||
?.querySelector<HTMLElement>(
|
||||
'fieldset[data-spec-container="ford"], fieldset[data-spec-container="body"]',
|
||||
side === "pipe"
|
||||
? 'fieldset[data-spec-container="pipe"]'
|
||||
: 'fieldset[data-spec-container="ford"], fieldset[data-spec-container="body"]',
|
||||
);
|
||||
if (!body || body.closest("[hidden]")) return;
|
||||
setGroupLabel(body, "");
|
||||
|
||||
@@ -56,9 +56,11 @@ function polygon(
|
||||
// "own" = 배관과 무관한 **독립 기슭막이**(구조물 정본 D군). 조정창·조작값 저장은
|
||||
// 배관 벽과 같은 체계를 그대로 쓴다(2026-08-28 사용자: 배관측점처럼 이동).
|
||||
export type RevetKey = "inlet" | "outlet" | "own" | `extra${number}` | `bextra${number}`;
|
||||
/** 고름 전용 키 — 벽 키 + 관(118-1 · 조정창 없음 · 강조만). 조정 함수에는 `RevetKey` 만. */
|
||||
export type RevetPick = RevetKey | "pipe";
|
||||
|
||||
/** 벽 강조를 카드 재생성 없이 갈아 끼우는 setter. */
|
||||
export type RevetHighlightSetter = (key: RevetKey | null) => void;
|
||||
export type RevetHighlightSetter = (key: RevetPick | null) => void;
|
||||
|
||||
/**
|
||||
* 배수관 세트 오버레이 — computeCulvertLayout 결과를 그린다.
|
||||
@@ -72,7 +74,7 @@ export function appendCulvertOverlay(
|
||||
layout: CulvertLayout,
|
||||
x: (offset: number) => number,
|
||||
toDisplayY: (elevation: number) => number,
|
||||
onSelectRevet?: (key: RevetKey) => void,
|
||||
onSelectRevet?: (key: RevetPick) => void,
|
||||
/** 관을 그리지 않는다 — 관은 한 측점에만 있고, 연장이 넘어온 옆 측점 카드에는
|
||||
* 기슭막이만 링크로 보인다(2026-08-24 사용자). */
|
||||
hidePipe?: boolean,
|
||||
@@ -311,6 +313,8 @@ export function appendCulvertOverlay(
|
||||
elevation: corner.top.elevation + normal.elevation * delta,
|
||||
},
|
||||
});
|
||||
const pipeShapes: SVGPolygonElement[] = []; // 관 두 겹 — 관 고름 강조(118-1)
|
||||
let pipeHitPoints: Array<[number, number]> = [];
|
||||
const pipeLayers = hidePipe
|
||||
? []
|
||||
: ([
|
||||
@@ -320,18 +324,16 @@ export function appendCulvertOverlay(
|
||||
for (const [delta, cls] of pipeLayers) {
|
||||
const a = shifted(inletFinal, delta);
|
||||
const b = shifted(outletFinal, delta);
|
||||
layer.append(
|
||||
polygon(
|
||||
[
|
||||
[x(a.bottom.offset), toDisplayY(a.bottom.elevation)],
|
||||
[x(b.bottom.offset), toDisplayY(b.bottom.elevation)],
|
||||
[x(b.top.offset), toDisplayY(b.top.elevation)],
|
||||
[x(a.top.offset), toDisplayY(a.top.elevation)],
|
||||
],
|
||||
cls,
|
||||
tip,
|
||||
),
|
||||
);
|
||||
const points: Array<[number, number]> = [
|
||||
[x(a.bottom.offset), toDisplayY(a.bottom.elevation)],
|
||||
[x(b.bottom.offset), toDisplayY(b.bottom.elevation)],
|
||||
[x(b.top.offset), toDisplayY(b.top.elevation)],
|
||||
[x(a.top.offset), toDisplayY(a.top.elevation)],
|
||||
];
|
||||
if (!pipeHitPoints.length) pipeHitPoints = points; // 외경 = 판정면
|
||||
const shape = polygon(points, cls, tip);
|
||||
pipeShapes.push(shape);
|
||||
layer.append(shape);
|
||||
}
|
||||
drawOutletFill();
|
||||
|
||||
@@ -340,6 +342,15 @@ export function appendCulvertOverlay(
|
||||
// 먼저 먹는다(2026-08-21 화면 확인). 그리기 순서(벽 → 관 → 보호공)는 그대로 두고
|
||||
// 판정면만 위로 올린다. 관 자체의 툴팁은 벽 밖 구간에서 그대로 뜬다.
|
||||
if (onSelectRevet) {
|
||||
// 관 판정면은 벽 판정면 **아래** — 겹친 자리는 벽이 이김(118-1).
|
||||
if (pipeHitPoints.length) {
|
||||
const hit = polygon(pipeHitPoints, "b06-chart__culvert-revet-hit", `${tip} — 관 선택`);
|
||||
hit.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
onSelectRevet("pipe");
|
||||
});
|
||||
layer.append(hit);
|
||||
}
|
||||
for (const { wall, key: wallKey } of wallsInOrder) {
|
||||
const hit = polygon(
|
||||
wall.points.map((p) => [x(p.offset), toDisplayY(p.elevation)] as [number, number]),
|
||||
@@ -375,5 +386,6 @@ export function appendCulvertOverlay(
|
||||
for (const [role, shape] of revetShapes) shape.classList.toggle("is-active", role === key);
|
||||
// 유입이 집수정이면 부재 전체를 함께 강조한다(2026-08-22 사용자 ③).
|
||||
for (const shape of basinShapes) shape.classList.toggle("is-active", key === "inlet");
|
||||
for (const shape of pipeShapes) shape.classList.toggle("is-active", key === "pipe");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
import { bodyAbsolute } from "./B06_Section_UI_Cross_Body_Guard";
|
||||
import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
|
||||
import { computeCulvertLayout } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { CulvertLayout, InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type {
|
||||
CulvertLayout,
|
||||
InletStructureChoice,
|
||||
RevetKey,
|
||||
RevetPick,
|
||||
} from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { BasinAdjust, CulvertDesignTrim } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import {
|
||||
basinSpanOfSpec,
|
||||
@@ -31,12 +36,12 @@ export interface RevetOffsetControl {
|
||||
adjustFor: (section: CrossSection, role: RevetKey) => WallAdjust;
|
||||
/** 이 측점이 **실제로 손댄** 값만 — 없으면 null(자동·상속과 구분해야 한다). */
|
||||
storedAdjustFor: (section: CrossSection, role: RevetKey) => WallAdjust | null;
|
||||
selectedFor: (section: CrossSection) => RevetKey | null;
|
||||
selectedFor: (section: CrossSection) => RevetPick | null;
|
||||
/** 이 카드에서 **강조**할 벽 — 연동으로 이어진 옆 측점이 고른 벽도 켠다
|
||||
* (2026-08-30 사용자: 한 기슭막이는 그 연장 안 모든 횡단도에서 같이 강조된다).
|
||||
* 조정창은 고른 카드 하나에만 뜨므로 `selectedFor`와 따로 둔다. */
|
||||
highlightFor: (section: CrossSection) => RevetKey | null;
|
||||
select: (chainageM: number, key: RevetKey | null) => void;
|
||||
highlightFor: (section: CrossSection) => RevetPick | null;
|
||||
select: (chainageM: number, key: RevetPick | null) => void;
|
||||
/** 선택이 바뀌면 화면이 연동 카드 강조를 다시 칠한다 — 목록 쪽이 꽂는다. */
|
||||
onSelectionChange?: () => void;
|
||||
/** 기하가 실제로 적용한 조작값을 되받아 담는다(한계에 걸린 요청값을 잘라 낸다). */
|
||||
@@ -382,7 +387,7 @@ export function restrictToSide(layout: CulvertLayout, side: string | undefined):
|
||||
export function computeCardCulvert(
|
||||
section: CrossSection,
|
||||
sourceSamples: SectionSample[],
|
||||
activeRevet: RevetKey | null,
|
||||
activeRevet: RevetPick | null,
|
||||
revetOffset?: RevetOffsetControl,
|
||||
inletStructure?: InletStructureControl,
|
||||
extraWalls?: ExtraWallControl,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* 폼 칸에 옮겨 붙인다(`_Adjust_Dock`). 폼과 같은 값(형태 · 높이 · 길이 · 전 · 후)은 만들지 않는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { InletStructureChoice, RevetKey, RevetPick } from "./B06_Section_UI_Cross_Culvert";
|
||||
import { REVET_FORMS } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { BasinAdjust, WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { SpanValues } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
@@ -97,7 +97,7 @@ export interface StructurePanelDeps {
|
||||
export interface StructurePanelHandle {
|
||||
root: HTMLElement;
|
||||
/** null이면 숨긴다. 값이 오면 그 구조물 기준으로 다시 그린다. */
|
||||
show: (key: RevetKey | null) => void;
|
||||
show: (key: RevetPick | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -494,9 +494,12 @@ export function buildStructurePanel(
|
||||
|
||||
return {
|
||||
root,
|
||||
show: (key) => {
|
||||
show: (pick) => {
|
||||
// 관 = 조정창 없음 · 좌측 「관」 상자만 강조(118-1 · 세월교 본체 꼴)
|
||||
const key = pick === "pipe" ? null : pick;
|
||||
current = key;
|
||||
root.classList.toggle("is-hidden", key === null);
|
||||
root.dataset.side = pick === "pipe" ? "pipe" : "";
|
||||
document.removeEventListener("keydown", onArrowKey);
|
||||
drag.detach();
|
||||
if (!key) return;
|
||||
|
||||
@@ -55,7 +55,7 @@ import { attachZoomPan, buildZoomControls, cardZoomStates } from "./B06_Section_
|
||||
import type { CrossWidthActions } from "./B06_Section_UI_Cross_View_Zoom";
|
||||
import type { CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
|
||||
import { createCrossAxes, IDENTITY_VIEW } from "./B06_Section_UI_Cross_Axes";
|
||||
import type { RevetHighlightSetter, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { RevetHighlightSetter, RevetKey, RevetPick } from "./B06_Section_UI_Cross_Culvert";
|
||||
import { REVET_FORM_DEFAULT } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import {
|
||||
@@ -82,7 +82,7 @@ export interface CrossCardElement extends HTMLElement {
|
||||
applySelection?: (selected: boolean, areaKey: CrossAreaKey | null) => void;
|
||||
/** 벽 강조만 갈아 끼운다 — 연동으로 이어진 카드도 같이 켜진다(2026-08-30 사용자).
|
||||
* 카드를 다시 만들지 않으므로 줌·팬이 살아남는다. */
|
||||
applyRevetHighlight?: (key: RevetKey | null) => void;
|
||||
applyRevetHighlight?: (key: RevetPick | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,10 +180,10 @@ export function createCrossSectionCard(
|
||||
let activeArea: CrossAreaKey | null = selected ? (initialAreaKey ?? null) : null;
|
||||
let setBandActive: AreaHighlightSetter = () => undefined;
|
||||
let setChipActive: AreaHighlightSetter = () => undefined;
|
||||
let activeRevet: RevetKey | null = revetOffset?.selectedFor(section) ?? null;
|
||||
let activeRevet: RevetPick | null = revetOffset?.selectedFor(section) ?? null;
|
||||
/** 이 카드에서 **강조**할 벽. 연동으로 이어진 옆 측점이 고른 벽도 켠다 —
|
||||
* 조정창(`activeRevet`)은 고른 카드 하나에만 뜬다(2026-08-30 사용자). */
|
||||
let highlightRevet: RevetKey | null = revetOffset?.highlightFor(section) ?? activeRevet;
|
||||
let highlightRevet: RevetPick | null = revetOffset?.highlightFor(section) ?? activeRevet;
|
||||
let setRevetActive: RevetHighlightSetter = () => undefined;
|
||||
let showRevetControl: (visible: boolean) => void = () => undefined;
|
||||
let culvertInletIsBasin = false; // 유입이 집수정인가 — 조정창 표시·제목 분기
|
||||
@@ -222,7 +222,7 @@ export function createCrossSectionCard(
|
||||
},
|
||||
selectCard: () => onSelect(section.station_id),
|
||||
});
|
||||
const toggleRevet = (key: RevetKey): void => {
|
||||
const toggleRevet = (key: RevetPick): void => {
|
||||
const wasSelected = isSelected;
|
||||
activeRevet = activeRevet === key ? null : key;
|
||||
showRevetControl(activeRevet !== null);
|
||||
@@ -654,7 +654,7 @@ export function createCrossSectionCard(
|
||||
extraState: () => culvertExtraState,
|
||||
basinExtraState: () => culvertBasinExtraState,
|
||||
activeRevet: () => activeRevet,
|
||||
setActiveRevet: (key: RevetKey | null) => {
|
||||
setActiveRevet: (key: RevetPick | null) => {
|
||||
activeRevet = key;
|
||||
},
|
||||
toggleRevet,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { RevetKey, RevetPick } from "./B06_Section_UI_Cross_Culvert";
|
||||
import {
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
PIPE_WALL_DEFAULT_RUN_M,
|
||||
@@ -81,9 +81,9 @@ export interface StructurePanelContext {
|
||||
inletOptions: () => { revetAllowed: boolean; basinLUAllowed: boolean };
|
||||
extraState: () => { canAdd: boolean; count: number };
|
||||
basinExtraState: () => { canAdd: boolean; count: number };
|
||||
activeRevet: () => RevetKey | null;
|
||||
setActiveRevet: (key: RevetKey | null) => void;
|
||||
toggleRevet: (key: RevetKey) => void;
|
||||
activeRevet: () => RevetPick | null;
|
||||
setActiveRevet: (key: RevetPick | null) => void;
|
||||
toggleRevet: (key: RevetPick) => void;
|
||||
/** 창 스크롤 자리를 기억할 카드 키(측점 id). */
|
||||
scrollKey: string;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
SectionDetailResponse,
|
||||
StoredWallSpan,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import type { InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { InletStructureChoice, RevetKey, RevetPick } from "./B06_Section_UI_Cross_Culvert";
|
||||
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { BasinAdjust, WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type {
|
||||
@@ -180,7 +180,7 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
const revetShifts = revetSession.values;
|
||||
/** 지금 고른 벽 **하나** — 조정창이 뜬 측점(누가거리 키)과 벽 키(2026-08-30 개편:
|
||||
* 강조는 연동으로 이어진 카드 전부, 조정창은 고른 카드 하나). */
|
||||
let revetSelection: { at: string; key: RevetKey } | null = null;
|
||||
let revetSelection: { at: string; key: RevetPick } | null = null;
|
||||
const revetKey = (chainageM: number, role: RevetKey): string => `${chainageM.toFixed(2)}:${role}`;
|
||||
|
||||
function loadRevetShifts(): void {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { RevetPick } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { StructurePickHandoff } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session";
|
||||
import {
|
||||
coversChainage,
|
||||
@@ -23,13 +23,13 @@ import {
|
||||
type SelectableStructure,
|
||||
} from "../A00_Common/design/design_selection";
|
||||
|
||||
/** 기슭막이 조정창이 받는 부재키 — 집수정·배관·본체는 여기서 빠진다. */
|
||||
const REVET_KEYS = /^(inlet|outlet|own|extra\d+|bextra\d+)$/;
|
||||
/** 벽 고름이 받는 부재키 — 관은 강조만(118-1) · 집수정·본체는 여기서 빠진다. */
|
||||
const REVET_KEYS = /^(inlet|outlet|own|extra\d+|bextra\d+|pipe)$/;
|
||||
|
||||
interface PickTargets {
|
||||
focusStation: (stationId: string) => void;
|
||||
refreshCard: (chainageM: number) => void;
|
||||
revetSelect: (chainageM: number, key: RevetKey) => void;
|
||||
revetSelect: (chainageM: number, key: RevetPick) => void;
|
||||
fordSelect: (chainageM: number, role: "inlet" | "outlet" | "body") => void;
|
||||
boxSelect: (chainageM: number, role: "left" | "right" | "body") => void;
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export function applyStructurePick(
|
||||
targets.boxSelect(target.chainage_m, key);
|
||||
} else {
|
||||
if (!REVET_KEYS.test(key)) return;
|
||||
targets.revetSelect(target.chainage_m, key as RevetKey);
|
||||
targets.revetSelect(target.chainage_m, key as RevetPick);
|
||||
}
|
||||
targets.refreshCard(target.chainage_m);
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ import {
|
||||
} from "./B06_Section_UI_Adjust_Dock";
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import type { StationControls } from "./B06_Section_UI_Page_Station_Controls";
|
||||
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { RevetPick } from "./B06_Section_UI_Cross_Culvert";
|
||||
import { withSpecDefaults } from "./B06_Section_UI_Page_Structures_Spec";
|
||||
|
||||
const PIPE_ADD_GUIDE = "계곡 통과 시설을 넣고 빼려면 프로젝트를 먼저 여세요.";
|
||||
@@ -616,9 +616,9 @@ export function wireStructureSelection(
|
||||
};
|
||||
|
||||
// 관 상자 칸을 고치거나 고르면 그 부분을 고름 — 횡단에서 누른 것과 같은 길(54-30)
|
||||
const keyOf = (box: HTMLElement): RevetKey | null => {
|
||||
const keyOf = (box: HTMLElement): RevetPick | null => {
|
||||
const id = box.dataset.specContainer;
|
||||
if (id === "inlet" || id === "outlet") return id;
|
||||
if (id === "inlet" || id === "outlet" || id === "pipe") return id;
|
||||
return id === "soil_add" ? `extra${Number(box.dataset.specIndex)}` : null;
|
||||
};
|
||||
const pickBox = (box: HTMLElement): void => {
|
||||
|
||||
@@ -25,6 +25,18 @@ DROPPED = "지운연결"
|
||||
CANCELS = "보내기취소" # 면적 [저장] 문서의 임시 칸 — 저장 안 함(112-2)
|
||||
# spreadsheet_area_table.ts `NAME_GONE` 과 같은 글자 — 이름이 지워졌을 때 까닭
|
||||
NAME_GONE = "이름없음"
|
||||
NAME = "도면_" # 보낸 범위 이름 앞머리 — AreaToDrawing.ts `NAME` 과 같은 글자
|
||||
|
||||
|
||||
def _book_names(book: Any) -> set[str]:
|
||||
"""책 모든 시트 이름정의 이름(작은 글자 · 화면 `resolveName` 과 같이 대소문자 무시)."""
|
||||
return {
|
||||
str(n.get("이름", "")).lower()
|
||||
for s in (book.get("시트") or [] if isinstance(book, dict) else [])
|
||||
if isinstance(s, dict)
|
||||
for n in s.get("이름정의") or []
|
||||
if isinstance(n, dict)
|
||||
}
|
||||
|
||||
|
||||
def to_table(
|
||||
@@ -119,6 +131,7 @@ def refresh_drawing(column: str, unlink: Any = ()) -> list[str]:
|
||||
"""지금 층 그 열 도면(모든 장)의 연결표마다 칸 글 다시 · 마지막 값 갱신 → 칸을 간 표의 `연결` 목록.
|
||||
`unlink` = 면적 [저장] 때 [보내기 취소]한 연결 — 그 줄을 연결표에서 뺌
|
||||
(도면 표는 남음 · 알림 없음 · 112-2).
|
||||
이름 없는 옛 줄은 책에 `도면_{연결}` 이름이 있으면 그 이름을 줄에 적음(114-1 · 면적 화면이 열 때 둠).
|
||||
이름(104-1)이 있으면 그 이름이 지금 가리키는 범위로 `범위` 도 갱신 — 이름이 지워졌으면(범위가
|
||||
통째로 지워짐) 그 연결을 끊고(도면 표는 남음) `지운연결` 목록에 적어 다음에 열 때 알림."""
|
||||
try:
|
||||
@@ -137,6 +150,12 @@ def refresh_drawing(column: str, unlink: Any = ()) -> list[str]:
|
||||
atomic_write_json(drawing, doc)
|
||||
return []
|
||||
book = json.loads(area.read_bytes())
|
||||
names = _book_names(book)
|
||||
for (
|
||||
x
|
||||
) in links: # 이름 없는 옛 연결(114-1) — 면적 화면이 책에 둔 `도면_{연결}` 이 있으면 줄에 적음
|
||||
if not x.get("이름") and f"{NAME}{x['연결']}".lower() in names:
|
||||
x["이름"] = f"{NAME}{x['연결']}"
|
||||
asks = [
|
||||
{
|
||||
"book": book,
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
type StructureDoc,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
|
||||
/** 표 칸 하나(CAD `TableCell` 꼴에서 쓰는 것만) */
|
||||
@@ -141,18 +142,41 @@ export const sameValues = (
|
||||
return ka.length === Object.keys(b).length && ka.every((k) => a[k] === b[k]);
|
||||
};
|
||||
|
||||
/** 면적 시트에 겹쳐 보일 「도면으로 보낸 범위」 — 같은 열 도면 `연결표` 에서(받기 실패 = 없음 · 조용히) */
|
||||
/** 이름 없는 옛 연결(114-1) — 책에 `도면_{연결}` 이름정의를 적힌 범위로 둠(이미 있으면 그대로) → 그 이름.
|
||||
* 책을 바로 고침(화면에 싣기 전 · 「저장 안 한 고침」 아님) · 서버는 면적 [저장] 때 그 이름을 연결표 줄에 적음 */
|
||||
async function nameOld(book: Workbook, row: TableLink): Promise<string | undefined> {
|
||||
const name = `${NAME}${row.연결}`;
|
||||
const low = name.toLowerCase();
|
||||
if (book.시트.some((s) => s.이름정의?.some((n) => n.이름.toLowerCase() === low))) return name;
|
||||
const { parseRange } = await import("../A00_Common/spreadsheet/spreadsheet_address");
|
||||
const sheet = book.시트.find((s) => s.id === row.시트);
|
||||
const g = parseRange(row.범위);
|
||||
if (!sheet || !g) return undefined; // 못 풂 — 적힌 범위로(옛 그대로)
|
||||
sheet.이름정의 = [...(sheet.이름정의 ?? []), { 이름: name, 범위: g }];
|
||||
return name;
|
||||
}
|
||||
|
||||
/** 면적 시트에 겹쳐 보일 「도면으로 보낸 범위」 — 같은 열 도면 `연결표` 에서(받기 실패 = 없음 · 조용히) ·
|
||||
* `book`(면적 책 · 화면에 싣기 전) 있으면 이름 없는 옛 연결에 이름을 둠(114-1 · 행열 넣기를 따라감) */
|
||||
export async function sentRanges(
|
||||
at: Selection,
|
||||
onCancel: (연결: string) => void,
|
||||
book?: Workbook,
|
||||
): Promise<NonNullable<LinkOptions["표시범위"]>> {
|
||||
try {
|
||||
const got = await readTemplate(at.layer, "structure", at.name, at.projectId);
|
||||
const rows = (got.문서 as StructureDoc | undefined)?.연결표 ?? [];
|
||||
return rows.map((row) => ({
|
||||
const names = await Promise.all(
|
||||
rows.map((row) =>
|
||||
row.이름 || !book || !Array.isArray(book.시트)
|
||||
? row.이름
|
||||
: nameOld(book, row).catch(() => undefined),
|
||||
),
|
||||
);
|
||||
return rows.map((row, i) => ({
|
||||
시트: row.시트,
|
||||
범위: row.범위,
|
||||
범위이름: row.이름,
|
||||
범위이름: names[i],
|
||||
이름: fill(TEXT.tag, "id", row.연결),
|
||||
설명: fill(fill(fill(TEXT.tip, "id", row.연결), "range", row.범위), "size", row.글자크기),
|
||||
취소: async () => (onCancel(row.연결), true), // 화면에서만 — 서버는 면적 [저장] 때
|
||||
|
||||
@@ -299,7 +299,8 @@ export function createMain(
|
||||
const [{ createSpreadsheet }, pills, sent] = await Promise.all([
|
||||
import("../A00_Common/spreadsheet/spreadsheet"),
|
||||
pillsFor(kind, at.name),
|
||||
sentRanges(at, toDrawing.cancel), // 도면으로 보낸 범위 — 시트 위 점선 테두리
|
||||
// 도면으로 보낸 범위 — 시트 위 점선 테두리 · 이름 없는 옛 연결은 책에 이름을 둠(114-1)
|
||||
sentRanges(at, toDrawing.cancel, doc as Parameters<typeof sentRanges>[2]),
|
||||
]);
|
||||
if (mine !== seq) return;
|
||||
if (pills.notice) showNotice([pills.notice]);
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
/* PLAN 118-1 단위검증 헬퍼 — B06 배관 횡단도 관 고름 · 강조 · 조정창 숨김 · 3D pipe 키.
|
||||
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 결과 = 표준출력 JSON 한 벌.
|
||||
* 입력 = `fixtures/b07_cross_overlay_sections.json` 배관 측점 85.052(관 Φ1.0). 짝 = `test_118_pipe_pick.py`.
|
||||
*/
|
||||
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();
|
||||
let STUBS = {
|
||||
"@ui/ui_template_toast": { showToast: () => undefined },
|
||||
"@ui/ui_template_elements": { showToast: () => undefined },
|
||||
};
|
||||
|
||||
// ── 화면 그릇 흉내 — 오버레이 · 조정창이 쓰는 만큼만 ─────────────────────────
|
||||
class FakeNode {
|
||||
constructor(tag) {
|
||||
this.tagName = String(tag).toUpperCase();
|
||||
this.children = [];
|
||||
this.attrs = {};
|
||||
this.dataset = {};
|
||||
this.listeners = {};
|
||||
this._text = "";
|
||||
this._classes = new Set();
|
||||
const self = this;
|
||||
this.classList = {
|
||||
add: (...names) => names.forEach((n) => self._classes.add(n)),
|
||||
remove: (...names) => names.forEach((n) => self._classes.delete(n)),
|
||||
toggle: (name, force) => {
|
||||
const on = force === undefined ? !self._classes.has(name) : !!force;
|
||||
on ? self._classes.add(name) : self._classes.delete(name);
|
||||
return on;
|
||||
},
|
||||
contains: (name) => self._classes.has(name),
|
||||
};
|
||||
this.style = { setProperty: () => {} };
|
||||
}
|
||||
set className(value) {
|
||||
this._classes = new Set(String(value).split(/\s+/).filter(Boolean));
|
||||
}
|
||||
get className() {
|
||||
return [...this._classes].join(" ");
|
||||
}
|
||||
set textContent(value) {
|
||||
this._text = String(value);
|
||||
this.children = [];
|
||||
}
|
||||
get textContent() {
|
||||
return this._text + this.children.map((c) => c.textContent).join("");
|
||||
}
|
||||
append(...kids) {
|
||||
kids.forEach((k) => this.children.push(k));
|
||||
}
|
||||
appendChild(kid) {
|
||||
this.children.push(kid);
|
||||
return kid;
|
||||
}
|
||||
replaceChildren(...kids) {
|
||||
this.children = kids;
|
||||
}
|
||||
setAttribute(name, value) {
|
||||
this.attrs[name] = String(value);
|
||||
if (name === "class") this.className = value;
|
||||
}
|
||||
getAttribute(name) {
|
||||
return name === "class" ? this.className : (this.attrs[name] ?? null);
|
||||
}
|
||||
addEventListener(type, fn) {
|
||||
(this.listeners[type] ??= []).push(fn);
|
||||
}
|
||||
removeEventListener() {}
|
||||
click() {
|
||||
let stopped = false;
|
||||
const event = { stopPropagation: () => (stopped = true), preventDefault: () => {} };
|
||||
(this.listeners.click ?? []).forEach((fn) => fn(event));
|
||||
return stopped;
|
||||
}
|
||||
querySelectorAll(selector) {
|
||||
const found = [];
|
||||
const walk = (node) =>
|
||||
node.children.forEach((kid) => {
|
||||
if (kid.classList && kid.classList.contains(selector.slice(1))) found.push(kid);
|
||||
walk(kid);
|
||||
});
|
||||
walk(this);
|
||||
return found;
|
||||
}
|
||||
querySelector(selector) {
|
||||
return this.querySelectorAll(selector)[0] ?? null;
|
||||
}
|
||||
getClientRects() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
globalThis.document = {
|
||||
createElement: (tag) => new FakeNode(tag),
|
||||
createElementNS: (_ns, tag) => new FakeNode(tag),
|
||||
createComment: () => new FakeNode("#comment"),
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
};
|
||||
globalThis.window = { addEventListener: () => {}, removeEventListener: () => {} };
|
||||
globalThis.requestAnimationFrame = (fn) => fn();
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
if (full.endsWith(".json")) {
|
||||
const data = JSON.parse(fs.readFileSync(full, "utf8"));
|
||||
cache.set(full, data);
|
||||
return data;
|
||||
}
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
||||
for (const [prefix, dir] of Object.entries(alias))
|
||||
if (request.startsWith(prefix))
|
||||
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
||||
return loadTs(resolve(path.join(path.dirname(full), request)));
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
const resolve = (base) =>
|
||||
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
) ?? `${base}.ts`;
|
||||
|
||||
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
|
||||
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
|
||||
const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert"));
|
||||
const { wallStandsAt } = loadTs(B06("B06_Section_UI_Cross_Culvert_Wire"));
|
||||
const { applyStructurePick } = loadTs(B06("B06_Section_UI_Page_Structure_Pick"));
|
||||
|
||||
const fixture = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "fixtures", "b07_cross_overlay_sections.json"), "utf8"),
|
||||
);
|
||||
const sections = fixture.cross_sections;
|
||||
const pipeSection = sections.find((s) => s.culvert && !s.culvert.hidden_pipe);
|
||||
const layout = computeStoredLayouts(pipeSection, sections).culvert;
|
||||
const out = {};
|
||||
|
||||
// ① 오버레이 — 판정면 차례 · 관 누름 · 강조
|
||||
function overlay(hidePipe) {
|
||||
const layer = new FakeNode("g");
|
||||
const picked = [];
|
||||
const setter = appendCulvertOverlay(
|
||||
layer,
|
||||
layout,
|
||||
(o) => -o * 10,
|
||||
(e) => -e * 10,
|
||||
(key) => picked.push(key),
|
||||
hidePipe,
|
||||
);
|
||||
const hits = layer.children.filter((n) => n.classList.contains("b06-chart__culvert-revet-hit"));
|
||||
const pipes = layer.children.filter((n) => n.classList.contains("b06-chart__culvert-pipe"));
|
||||
const walls = layer.children.filter((n) => n.classList.contains("b06-chart__culvert-revet"));
|
||||
return { layer, picked, setter, hits, pipes, walls };
|
||||
}
|
||||
const drawn = overlay(false);
|
||||
const firstHit = drawn.hits[0];
|
||||
const stopped = firstHit.click();
|
||||
const active = () => ({
|
||||
pipes: drawn.pipes.map((p) => p.classList.contains("is-active")),
|
||||
walls: drawn.walls.map((w) => w.classList.contains("is-active")),
|
||||
});
|
||||
drawn.setter("pipe");
|
||||
const onPipe = active();
|
||||
drawn.setter("outlet");
|
||||
const onOutlet = active();
|
||||
drawn.setter(null);
|
||||
const onNone = active();
|
||||
// 판정면 차례 — 관 판정면이 벽 판정면들보다 먼저(아래) 얹힘
|
||||
const order = drawn.layer.children
|
||||
.filter((n) => n.classList.contains("b06-chart__culvert-revet-hit"))
|
||||
.map((n) => (n === firstHit ? "pipe" : "wall"));
|
||||
// 관 판정면 = 외경 겹 꼭짓점과 같음
|
||||
const outer = drawn.pipes.find((p) => p.classList.contains("b06-chart__culvert-pipe--outer"));
|
||||
out.overlay = {
|
||||
pipeCount: drawn.pipes.length,
|
||||
wallCount: drawn.walls.length,
|
||||
hitOrder: order,
|
||||
firstPicked: drawn.picked,
|
||||
stopped,
|
||||
hitMatchesOuter: firstHit.attrs.points === outer.attrs.points,
|
||||
onPipe,
|
||||
onOutlet,
|
||||
onNone,
|
||||
};
|
||||
// 관을 숨긴 카드(링크 · 독립 흙막이 · 세월교 · 박스) — 관 판정면 없음
|
||||
const hidden = overlay(true);
|
||||
hidden.hits.forEach((h) => h.click());
|
||||
out.hiddenPipe = { pipes: hidden.pipes.length, picked: hidden.picked };
|
||||
|
||||
// ② 연동 카드 — 관은 주인 측점 카드에만
|
||||
const neighbour = { ...pipeSection, chainage_m: pipeSection.chainage_m + 2 };
|
||||
out.stands = {
|
||||
self: wallStandsAt(pipeSection, pipeSection, "pipe"),
|
||||
neighbour: wallStandsAt(pipeSection, neighbour, "pipe"),
|
||||
outletNeighbour: wallStandsAt(pipeSection, neighbour, "outlet"),
|
||||
};
|
||||
|
||||
// ③ B05 3D 관 고름(pipe 키) → B06 벽 고름 길로
|
||||
function pick(key, extra = {}) {
|
||||
const calls = [];
|
||||
const target = { ...pipeSection, ...extra };
|
||||
applyStructurePick(
|
||||
{ at: target.chainage_m, key },
|
||||
{ cross_sections: [target] },
|
||||
{
|
||||
focusStation: (id) => calls.push(["focus", id]),
|
||||
refreshCard: (c) => calls.push(["refresh", c]),
|
||||
revetSelect: (c, k) => calls.push(["revet", k]),
|
||||
fordSelect: (c, k) => calls.push(["ford", k]),
|
||||
boxSelect: (c, k) => calls.push(["box", k]),
|
||||
},
|
||||
);
|
||||
return calls.filter(([kind]) => kind !== "focus" && kind !== "refresh");
|
||||
}
|
||||
out.pick = {
|
||||
pipe: pick("pipe"),
|
||||
basin: pick("basin"),
|
||||
fordPipe: pick("pipe", { ford: {} }),
|
||||
};
|
||||
|
||||
// ④ 조정창 — show("pipe") = 숨김 · side "pipe" · 조정 함수 안 불림
|
||||
const base = loadTs(B06("B06_Section_UI_Cross_Panel_Base"));
|
||||
STUBS = {
|
||||
...STUBS,
|
||||
"./B06_Section_UI_Section_Common": { L: (key) => key },
|
||||
"./B06_Section_UI_Cross_Panel_Base": {
|
||||
...base,
|
||||
bindDragHandle: () => ({ attach: () => {}, detach: () => {} }),
|
||||
},
|
||||
};
|
||||
const { buildStructurePanel } = loadTs(B06("B06_Section_UI_Cross_Structure_Panel"));
|
||||
const touched = [];
|
||||
const spy = (name, value) => (key) => {
|
||||
touched.push([name, key]);
|
||||
return value;
|
||||
};
|
||||
const panelDeps = {
|
||||
scrollKey: "t118",
|
||||
adjustFor: spy("adjustFor", { x: 0, d: null, h: null, m: null }),
|
||||
nudge: spy("nudge"),
|
||||
nudgeSlope: spy("nudgeSlope"),
|
||||
nudgeHeight: spy("nudgeHeight"),
|
||||
heightFor: spy("heightFor", 2),
|
||||
formFor: spy("formFor", "돌쌓기(메)"),
|
||||
setForm: spy("setForm"),
|
||||
reset: spy("reset"),
|
||||
close: () => {},
|
||||
structureFor: () => "revet",
|
||||
basinAdjustFor: () => ({ innerWidthM: 1, innerHeightM: 1.2, lateralM: 0, slopeM: 0 }),
|
||||
updateBasin: () => {},
|
||||
nudgeBasinLateral: () => {},
|
||||
resetBasin: () => {},
|
||||
canNudge: spy("canNudge", true),
|
||||
fineMove: spy("fineMove", false),
|
||||
dragAxes: spy("dragAxes", null),
|
||||
extraState: () => ({ canAdd: false, count: 0 }),
|
||||
isLinkedCard: () => false,
|
||||
basinExtraState: () => ({ canAdd: false, count: 0 }),
|
||||
hiddenPipe: () => false,
|
||||
bodyGuard: () => false,
|
||||
setExtraCount: () => {},
|
||||
setBasinExtraCount: () => {},
|
||||
equalizeExtras: () => {},
|
||||
spanFor: spy("spanFor", null),
|
||||
setSpan: () => {},
|
||||
linkState: () => null,
|
||||
setLinked: () => {},
|
||||
followGrade: () => true,
|
||||
setFollowGrade: () => {},
|
||||
};
|
||||
const panel = buildStructurePanel(panelDeps);
|
||||
panel.show("outlet");
|
||||
const outletState = {
|
||||
hidden: panel.root.classList.contains("is-hidden"),
|
||||
side: panel.root.dataset.side,
|
||||
};
|
||||
touched.length = 0;
|
||||
panel.show("pipe");
|
||||
out.panel = {
|
||||
outlet: outletState,
|
||||
pipe: {
|
||||
hidden: panel.root.classList.contains("is-hidden"),
|
||||
side: panel.root.dataset.side,
|
||||
touched: touched.map(([name]) => name),
|
||||
},
|
||||
};
|
||||
panel.show(null);
|
||||
out.panel.none = {
|
||||
hidden: panel.root.classList.contains("is-hidden"),
|
||||
side: panel.root.dataset.side,
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,116 @@
|
||||
"""PLAN 118-1 — B06 배관 횡단도 관 고름 → 관 두 겹 강조 · 좌측 「관」 상자 강조 · 조정창 없음.
|
||||
|
||||
짝: `B06_Section/B06_Section_UI_Cross_Culvert.ts`(관 판정면 · 강조) · `_Cross_Structure_Panel.ts`(show "pipe") ·
|
||||
`_Adjust_Dock.ts`(side "pipe") · `_Page_Structures_Panel.ts`(`keyOf`) · `_Page_Structure_Pick.ts`(3D pipe 키).
|
||||
실제 TS 를 `helper_118_pipe_pick.cjs` 가 Node 로 돌린다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
ROOT = os.path.join(HERE, "..", "..")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.path.isdir(os.path.join(ROOT, "config", "node_modules")),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", os.path.join(HERE, "helper_118_pipe_pick.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _src(rel: str) -> str:
|
||||
with open(os.path.join(ROOT, rel), encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def test_관을_누르면_관을_고름(out) -> None:
|
||||
overlay = out["overlay"]
|
||||
assert overlay["firstPicked"] == ["pipe"]
|
||||
assert overlay["stopped"] is True # 카드 고름으로 안 번짐
|
||||
assert overlay["hitMatchesOuter"] is True # 판정면 = 외경
|
||||
|
||||
|
||||
def test_관_판정면은_벽_아래(out) -> None:
|
||||
"""겹친 자리는 벽이 이김 — 관 판정면이 벽 · 집수정 판정면보다 먼저 얹힘."""
|
||||
order = out["overlay"]["hitOrder"]
|
||||
assert order[0] == "pipe"
|
||||
assert order.count("pipe") == 1 and len(order) > 1
|
||||
|
||||
|
||||
def test_관_고름은_관_두_겹만_강조(out) -> None:
|
||||
overlay = out["overlay"]
|
||||
assert overlay["pipeCount"] == 2
|
||||
assert overlay["onPipe"]["pipes"] == [True, True]
|
||||
assert not any(overlay["onPipe"]["walls"])
|
||||
# 벽을 고르면 관 강조는 꺼짐 — 동시 강조 없음
|
||||
assert not any(overlay["onOutlet"]["pipes"]) and any(overlay["onOutlet"]["walls"])
|
||||
assert not any(overlay["onNone"]["pipes"] + overlay["onNone"]["walls"])
|
||||
|
||||
|
||||
def test_관을_숨긴_카드엔_관_판정면_없음(out) -> None:
|
||||
"""링크 · 독립 흙막이 · 세월교 · 박스 측점(hidden_pipe) — 관은 세월교 오버레이 몫."""
|
||||
assert out["hiddenPipe"]["pipes"] == 0
|
||||
assert "pipe" not in out["hiddenPipe"]["picked"]
|
||||
|
||||
|
||||
def test_관_강조는_주인_측점_카드만(out) -> None:
|
||||
stands = out["stands"]
|
||||
assert stands["self"] is True
|
||||
assert stands["neighbour"] is False
|
||||
assert stands["outletNeighbour"] is True # 벽 연동은 그대로
|
||||
|
||||
|
||||
def test_3D_관_고름이_B06_으로_이어짐(out) -> None:
|
||||
assert out["pick"]["pipe"] == [["revet", "pipe"]]
|
||||
assert out["pick"]["basin"] == []
|
||||
assert out["pick"]["fordPipe"] == [] # 세월교 측점 = 세월교 창 몫
|
||||
|
||||
|
||||
def test_관은_조정창_없음(out) -> None:
|
||||
panel = out["panel"]
|
||||
assert panel["outlet"] == {"hidden": False, "side": "outlet"}
|
||||
assert panel["pipe"] == {"hidden": True, "side": "pipe", "touched": []}
|
||||
assert panel["none"] == {"hidden": True, "side": ""}
|
||||
|
||||
|
||||
def test_좌측_관_상자_강조_스크롤() -> None:
|
||||
dock = _src("B06_Section/B06_Section_UI_Adjust_Dock.ts")
|
||||
assert 'if (side === "body" || side === "pipe") {' in dock
|
||||
assert "'fieldset[data-spec-container=\"pipe\"]'" in dock
|
||||
assert 'body.classList.add("is-active");' in dock
|
||||
assert 'if (scroll) body.scrollIntoView({ block: "nearest" });' in dock
|
||||
# 관 상자 칸을 고치면 관을 고름 — 횡단에서 누른 것과 같은 길
|
||||
page = _src("B06_Section/B06_Section_UI_Page_Structures_Panel.ts")
|
||||
assert 'if (id === "inlet" || id === "outlet" || id === "pipe") return id;' in page
|
||||
css = _src("B06_Section/B06_Section_UI_Style_Cross_Areas.css")
|
||||
assert ".b06-chart__culvert-pipe.is-active {" in css
|
||||
|
||||
|
||||
def test_벽_본체_동시_고름_금지_그대로() -> None:
|
||||
"""107-1 — 관도 같은 고름 저장소라 세월교 · 박스 고름과 함께 서지 않음."""
|
||||
station = _src("B06_Section/B06_Section_UI_Page_Station_Controls.ts")
|
||||
assert "let revetSelection: { at: string; key: RevetPick } | null = null;" in station
|
||||
assert (
|
||||
"if (key && ford.control.selectedFor(chainageM)) ford.control.select(chainageM, null);"
|
||||
in station
|
||||
)
|
||||
view = _src("B06_Section/B06_Section_UI_Cross_View.ts")
|
||||
assert "const toggleRevet = (key: RevetPick): void => {" in view
|
||||
assert "if (activeRevet !== null) toggleRevet(activeRevet);" in view
|
||||
@@ -0,0 +1,136 @@
|
||||
"""114 — 이름 없는 옛 보낸 범위 연결이 행 넣기를 따라감(화면 · 서버 한 바퀴).
|
||||
|
||||
- 114-1 면적 상세를 열 때 화면(`sentRanges` + 책)이 `도면_{연결}` 이름정의를 둠 → 행 넣기 → 면적 [저장]
|
||||
→ 서버 `refresh_drawing` 이 그 이름을 연결표 줄에 적고 도면 표 칸 글 = 옮겨진 칸.
|
||||
TS 는 Node 로(`AreaToDrawing` 은 그 자리 변환 · 스프레드시트 모듈은 `helper_spreadsheet_d_bundle.cjs`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from common_util import common_util_spreadsheet as sheet_node
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from resources.tester.test_m02_area_table import world # noqa: F401 — 같은 임시 층
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = r"""
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = process.argv[1];
|
||||
const { load, SS } = require(path.join(ROOT, "resources/tester/spreadsheet/helper_spreadsheet_d_bundle.cjs"));
|
||||
const ts = require(path.join(ROOT, "config/node_modules/typescript"));
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const ss = (name) => load(path.join(SS, name + ".ts"));
|
||||
const tsLoad = (file, stubs) => {
|
||||
const src = fs.readFileSync(path.join(ROOT, file), "utf8");
|
||||
const js = ts.transpileModule(src, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (n) => {
|
||||
if (!(n in stubs)) throw new Error("모르는 가져오기 " + n);
|
||||
return stubs[n];
|
||||
});
|
||||
return box.exports;
|
||||
};
|
||||
const toDrawing = tsLoad("M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts", {
|
||||
"@config/config_frontend": { API_BASE_URL: "/api" },
|
||||
"@ui/ui_template_elements": {},
|
||||
"@ui/ui_template_modal": {},
|
||||
"./M02_MasterTemplete_Api_Fetch": {
|
||||
getActiveOwner: () => null,
|
||||
readTemplate: async () => ({ 문서: input.drawing }),
|
||||
},
|
||||
"../A00_Common/spreadsheet/spreadsheet_address": ss("spreadsheet_address"),
|
||||
"../A00_Common/spreadsheet/spreadsheet_area_table": ss("spreadsheet_area_table"),
|
||||
});
|
||||
const { applyCommand } = ss("spreadsheet_commands");
|
||||
|
||||
(async () => {
|
||||
const book = input.book;
|
||||
const area = { layer: "system", projectId: null, kind: "area", name: "rv", label: "x" };
|
||||
const before = JSON.stringify(book);
|
||||
const noBook = await toDrawing.sentRanges(area, () => {});
|
||||
const unchanged = JSON.stringify(book) === before;
|
||||
const marks = await toDrawing.sentRanges(area, () => {}, book);
|
||||
const opened = {
|
||||
marks: marks.map((m) => [m.이름, m.범위이름 ?? null]),
|
||||
noBook: noBook.map((m) => m.범위이름 ?? null),
|
||||
unchanged,
|
||||
names: book.시트[0].이름정의,
|
||||
};
|
||||
applyCommand(book, { 종류: "행넣기", 시트: "s1", at: 0, 수: 1 }); // 1행 위에 한 행
|
||||
console.log(JSON.stringify({ opened, book }));
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
"""
|
||||
|
||||
T1_TABLE = {
|
||||
"type": "Table",
|
||||
"shapeData": {
|
||||
"origin": {"x": 0, "y": 0},
|
||||
"columnWidths": [9, 9],
|
||||
"rowHeights": [3, 3],
|
||||
"cells": [[{"text": "낡음"}, {"text": "낡음"}], [None, {"text": "낡음"}]],
|
||||
"style": {"fontSize": 2.5, "연결": "T1"},
|
||||
},
|
||||
}
|
||||
OLD_ROW = {"연결": "T1", "시트": "s1", "범위": "A1:B2", "글자크기": 2.5, "값": {}}
|
||||
|
||||
|
||||
def _node(payload: dict) -> dict:
|
||||
if shutil.which("node") is None:
|
||||
pytest.skip("node 가 없음")
|
||||
proc = subprocess.run(
|
||||
["node", "-e", SCRIPT, str(ROOT)],
|
||||
input=json.dumps(payload, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout.strip().splitlines()[-1])
|
||||
|
||||
|
||||
def _texts(cells: list) -> list:
|
||||
return [[c and c.get("text") for c in row] for row in cells]
|
||||
|
||||
|
||||
def test_옛_연결_열기_행_넣기_저장_도면_표_값_따라감_114_1(world): # noqa: F811
|
||||
system = world["system"]
|
||||
with store.at(system):
|
||||
got = store.read("structure", "rv")
|
||||
drawing = {**got["문서"], "연결표": [OLD_ROW]}
|
||||
drawing["도면"] = {**drawing["도면"], "entities": [T1_TABLE]}
|
||||
store.write("structure", "rv", got["판"], drawing)
|
||||
area = store.read("area", "rv")
|
||||
want = sheet_node.area_tables([{"book": area["문서"], "시트": "s1", "범위": "A1:B2"}])[0]
|
||||
out = _node({"book": area["문서"], "drawing": drawing})
|
||||
opened = out["opened"]
|
||||
assert opened["noBook"] == [None] and opened["unchanged"] # 책 없으면 안 고침(옛 길)
|
||||
assert opened["marks"] == [["T1 도면", "도면_T1"]] # 그 이름으로 점선
|
||||
assert opened["names"] == [{"이름": "도면_T1", "범위": {"r0": 0, "c0": 0, "r1": 1, "c1": 1}}]
|
||||
book = out["book"]
|
||||
assert book["시트"][0]["이름정의"][0]["범위"] == {
|
||||
"r0": 1,
|
||||
"c0": 0,
|
||||
"r1": 2,
|
||||
"c1": 1,
|
||||
} # 행 넣기 따라감
|
||||
with store.at(system):
|
||||
store.write("area", "rv", area["판"], book) # 면적 [저장] → refresh_drawing
|
||||
doc = store.read("structure", "rv")["문서"]
|
||||
row = doc["연결표"][0]
|
||||
assert (row["이름"], row["범위"]) == ("도면_T1", "A2:B3")
|
||||
cells = doc["도면"]["entities"][0]["shapeData"]["cells"]
|
||||
assert _texts(cells) == _texts(want["표"]["shapeData"]["cells"]) # 옮겨진 칸 = 보낸 그 칸
|
||||
assert _texts(cells)[0] == ["제목", "1,234.50"]
|
||||
@@ -300,11 +300,20 @@ def test_판_충돌로_도면_저장이_막히면_이름도_안_생김_112_2(wor
|
||||
assert (system / "area" / "rv.json").read_bytes() == before
|
||||
|
||||
|
||||
def test_이름_없는_옛_연결_줄은_이름을_안_둠_112_2(world):
|
||||
def test_이름_없는_옛_연결_줄은_면적_저장_때_책에_이름_있으면_줄에_적음_114_1(world):
|
||||
system = world["system"]
|
||||
with store.at(system):
|
||||
_save_links([{"연결": "T1", "시트": "s1", "범위": "A1:B2", "값": {}}])
|
||||
assert _area_names(system) == []
|
||||
assert _area_names(system) == [] # 도면 저장은 옛 줄 이름을 안 만듦(112-2 그대로)
|
||||
drawing = store.read("structure", "rv")["문서"]
|
||||
assert "이름" not in drawing["연결표"][0] # 책에 이름 없으면 적힌 범위 그대로
|
||||
got = store.read(
|
||||
"area", "rv"
|
||||
) # 면적 화면이 열 때 둔 이름(114-1) — 이름이 가리키는 범위는 B1:C2
|
||||
got["문서"]["시트"][0]["이름정의"] = [T1_NAME]
|
||||
store.write("area", "rv", got["판"], got["문서"])
|
||||
row = store.read("structure", "rv")["문서"]["연결표"][0]
|
||||
assert (row["이름"], row["범위"]) == ("도면_T1", "B1:C2")
|
||||
|
||||
|
||||
def test_면적_저장_보내기취소_줄과_이름을_뺌_도면_표는_남음_112_2(world):
|
||||
|
||||
Reference in New Issue
Block a user