Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_laptop_5' into sub_laptop_2
This commit is contained in:
@@ -201,6 +201,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
const title = containerTitle(c, names, groupNames[c.link]);
|
||||
const box = group(c.required ? title : `${title} ${index + 1}`);
|
||||
box.root.dataset.specContainer = c.id;
|
||||
box.root.dataset.specIndex = String(index); // 칸을 고친 뒤 다시 그려도 몇째 상자인지(54-30)
|
||||
// 보이는 값 = 고른 값 + 그림이 쓰는 값(54-13) · 위 칸이 다른 갈래를 보이면 아래 칸은 빈 채로
|
||||
const view: Instance = { ...values };
|
||||
const tags = new Map<string, string>();
|
||||
@@ -309,6 +310,8 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
root.replaceChildren(...boxes);
|
||||
// 개수가 밖에서 바뀌었으면 값 변경으로 알림(불러오기 때는 안 함)
|
||||
if (!quiet) fitted.forEach((c) => hooks.onChange?.(c, c.items[0]));
|
||||
// 상자를 새로 세움 — 고른 상자 강조를 다시 얹을 곳(B06 · 54-30 · 시험 흉내 DOM 은 없음)
|
||||
root.dispatchEvent?.(new Event("specpaint", { bubbles: true }));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -152,15 +152,8 @@ export const PIPE_SPEC: StructureSpec = {
|
||||
required: false,
|
||||
prefix: "soil_add",
|
||||
link: "gsoiladd",
|
||||
items: [
|
||||
{
|
||||
id: "structure",
|
||||
label: "구조",
|
||||
kind: "select",
|
||||
choices: [{ value: SOIL, label: "흙막이" }],
|
||||
},
|
||||
...guardItems(),
|
||||
],
|
||||
// 사면안정 흙막이(표 09 · 54-26)와 같은 칸 — 형태 6 · 길이 · 높이 · 전 · 후 · 구조 고르기 없음(54-30)
|
||||
items: guardItems(),
|
||||
},
|
||||
{
|
||||
id: "chute_add",
|
||||
|
||||
@@ -122,3 +122,40 @@ export function guardHeightHint(diameterMm: string): string {
|
||||
const d = Number(diameterMm || "1000") / 1000;
|
||||
return `비우면 관경 기준 ${pipeWallMinHeightM(d)}`;
|
||||
}
|
||||
|
||||
/** 관 보호공 [연동] · [경사](54-30) — null = 그 쪽을 쓸 수 없음(벽이 서는 옆 측점 없음 · 구간 없음). */
|
||||
export interface PipeRevetChoice {
|
||||
linked: boolean | null;
|
||||
grade: boolean | null;
|
||||
}
|
||||
/** 읽고 쓰는 곳 — 횡단 보기의 연동 제어(`revetLink` · 옛 저장 자리)가 걸어 둠. */
|
||||
interface RevetChoiceBridge {
|
||||
read: (owner: CrossSection, sections: readonly CrossSection[]) => PipeRevetChoice;
|
||||
write: (
|
||||
owner: CrossSection,
|
||||
sections: readonly CrossSection[],
|
||||
kind: "link" | "grade",
|
||||
on: boolean,
|
||||
) => void;
|
||||
}
|
||||
let revetChoice: RevetChoiceBridge | null = null;
|
||||
export function setPipeRevetChoiceBridge(bridge: RevetChoiceBridge): void {
|
||||
revetChoice = bridge;
|
||||
}
|
||||
|
||||
/** 그 관의 [연동] · [경사] 지금 값 — 횡단 자료가 없으면 null(모름). */
|
||||
export function pipeRevetChoice(chainageM: number | null): PipeRevetChoice | null {
|
||||
const found = ownerSection(chainageM);
|
||||
return found && revetChoice ? revetChoice.read(found.owner, found.sections) : null;
|
||||
}
|
||||
|
||||
/** 좌측 [연동] · [경사] → 횡단 연동 제어(종단 보기는 횡단 보기를 숨긴 채 세워 씀 · 54-27 과 같은 길). */
|
||||
export async function setPipeRevetChoice(
|
||||
chainageM: number | null,
|
||||
kind: "link" | "grade",
|
||||
on: boolean,
|
||||
): Promise<void> {
|
||||
if (!revetChoice) await crossOpener?.();
|
||||
const found = ownerSection(chainageM);
|
||||
if (found) revetChoice?.write(found.owner, found.sections, kind, on);
|
||||
}
|
||||
|
||||
@@ -78,8 +78,10 @@ import {
|
||||
setPipeExtraCountSink,
|
||||
setPipeExtraCountSource,
|
||||
setPipeInletSource,
|
||||
setPipeRevetChoiceBridge,
|
||||
setPipeSectionsSource,
|
||||
} from "./design_structure_pipe_calc";
|
||||
import { pipeRevetChoiceBridge } from "../../B06_Section/B06_Section_UI_Page_Link_Session";
|
||||
import {
|
||||
bindDesignView,
|
||||
bindPipeOptions,
|
||||
@@ -313,6 +315,8 @@ export async function mountCrossView(
|
||||
});
|
||||
// 폼이 보이는 유입 그림 값(54-13) — 세션 형식 우선
|
||||
setPipeInletSource((section) => stationControls.inletStructure.valueFor(section));
|
||||
// 좌측 관 보호공 [연동] · [경사] = 옛 연동 제어 그대로(54-30)
|
||||
setPipeRevetChoiceBridge(pipeRevetChoiceBridge(stationControls.revetLink));
|
||||
|
||||
const sectionView = createSectionView(
|
||||
(chainageM, change) => {
|
||||
@@ -372,6 +376,7 @@ export async function mountCrossView(
|
||||
partKey: key,
|
||||
from: "b06",
|
||||
}),
|
||||
(chainageM) => sectionView.refreshCard(chainageM),
|
||||
);
|
||||
// 횡단도(카드) 자체를 골라도 그 측점 구조물 정보를 폼에 올린다(2026-08-29 사용자).
|
||||
// 연동으로 옆에서 넘어온 카드는 **소유 측점** 시설을 보여 준다.
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
WING_IN_KEYS,
|
||||
WING_OUT_KEYS,
|
||||
} from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import { revetChoiceRow } from "./B05_Profile_UI_Drainage_Revet_Choice";
|
||||
import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form";
|
||||
import { branchOf } from "../A00_Common/design/design_structure_spec";
|
||||
import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe";
|
||||
@@ -321,8 +322,22 @@ export function createFacilityOptionsForm(
|
||||
// 지형이 못 세운 단은 횡단 기하가 자름 — 끝난 뒤 세운 만큼으로 다시 그림
|
||||
void setPipeExtraCount(chainage, count).then(() => pipeForm.recount());
|
||||
});
|
||||
const inletChoice = revetChoiceRow(
|
||||
() => chainage,
|
||||
() => pipeForm.refresh(),
|
||||
);
|
||||
const outletChoice = revetChoiceRow(
|
||||
() => chainage,
|
||||
() => pipeForm.refresh(),
|
||||
);
|
||||
const pipeForm = createSpecForm(specLinks, {
|
||||
fields: (c, values) => (c.id === "outlet" ? [paintTiers(values)] : []),
|
||||
// 관 보호공 [연동] · [경사](54-30) — 유입 · 유출 상자 각각 한 줄(같은 값) · 집수정이면 숨김
|
||||
fields: (c, values) =>
|
||||
c.id === "outlet"
|
||||
? [paintTiers(values), outletChoice.paint(false)]
|
||||
: c.id === "inlet"
|
||||
? [inletChoice.paint(values.structure === "집수정")]
|
||||
: [],
|
||||
shown: (c, item, values, index) => {
|
||||
if (c.id === "inlet" && item.id === "structure" && (!values.structure || inletIsAuto())) {
|
||||
const drawn = drawnNow();
|
||||
@@ -331,8 +346,6 @@ export function createFacilityOptionsForm(
|
||||
: { value: "", tag: "자동(지형 규칙)" };
|
||||
}
|
||||
if (values[item.id]) return null;
|
||||
// 흙막이 상자 구조 = 표 01 흙막이 하나(54-27)
|
||||
if (c.id === "soil_add" && item.id === "structure") return { value: "기슭막이", tag: "기본" };
|
||||
if (item.id !== "form") return byDefault(branchOf(item, values)?.key);
|
||||
const basin = c.id === "inlet" && values.structure === "집수정";
|
||||
const drawn = drawnNow();
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Drainage_Revet_Choice.ts
|
||||
* 관 보호공 상자 안 [연동] · [경사] 한 줄(54-30) — 반 폭 단추 둘 · 쓸 수 있는 쪽만 누를 수 있음.
|
||||
* 값 = 횡단 연동 제어 옛 자리(`revet_link_detached` · `revet_follow_grade` · `design_structure_pipe_calc`) ·
|
||||
* 고르면 횡단 그림이 바로 따라감. 경사를 끄면(수평) 연동은 풀 수 없음(옛 규칙 그대로).
|
||||
* ========================================================================== */
|
||||
|
||||
import { toggleButton } from "./B05_Profile_UI_Panel_Fields";
|
||||
import {
|
||||
pipeRevetChoice,
|
||||
setPipeRevetChoice,
|
||||
} from "../A00_Common/design/design_structure_pipe_calc";
|
||||
|
||||
const LINK_TITLE = "옆 측점 흙막이와 횡단 자리를 함께 씀 — 끄면 측점마다 따로 잡음";
|
||||
const LINK_LOCKED = "경사를 끈(수평) 동안은 연동을 풀 수 없음";
|
||||
const GRADE_TITLE = "종단경사 반영 — 끄면 연장 구간 내내 같은 높이(수평)";
|
||||
|
||||
/** 한 줄 — `paint` 가 지금 값 · 쓸 수 있는지를 다시 칠하고 줄을 돌려줌(폼 다시 그림마다). */
|
||||
export function revetChoiceRow(
|
||||
chainageOf: () => number | null,
|
||||
/** 쓴 뒤 — 폼을 다시 그려 유입 · 유출 두 줄을 함께 칠함. */
|
||||
written: () => void,
|
||||
): {
|
||||
paint: (hidden: boolean) => HTMLElement;
|
||||
} {
|
||||
const row = document.createElement("div"); // label 이 아님 — 이름표를 눌러 첫 단추가 눌리지 않게
|
||||
row.className = "b05-route__field";
|
||||
const caption = document.createElement("span");
|
||||
caption.textContent = "옵션";
|
||||
const pair = document.createElement("div");
|
||||
pair.className = "b05-structure__half-pair";
|
||||
const write = (kind: "link" | "grade", on: boolean): void =>
|
||||
void setPipeRevetChoice(chainageOf(), kind, on).then(written);
|
||||
const link = toggleButton("연동", true, (on) => write("link", on));
|
||||
const grade = toggleButton("경사", true, (on) => write("grade", on));
|
||||
grade.title = GRADE_TITLE;
|
||||
pair.append(link, grade);
|
||||
row.append(caption, pair);
|
||||
|
||||
const set = (button: HTMLButtonElement, value: boolean | null, locked: boolean): void => {
|
||||
button.disabled = value === null || locked;
|
||||
button.classList.toggle("is-active", value === true);
|
||||
button.setAttribute("aria-pressed", String(value === true));
|
||||
};
|
||||
function paint(hidden: boolean): HTMLElement {
|
||||
const now = pipeRevetChoice(chainageOf());
|
||||
const locked = now?.grade === false;
|
||||
set(link, now?.linked ?? null, locked);
|
||||
link.title = locked ? LINK_LOCKED : LINK_TITLE;
|
||||
set(grade, now?.grade ?? null, false);
|
||||
row.hidden = hidden;
|
||||
return row;
|
||||
}
|
||||
return { paint };
|
||||
}
|
||||
@@ -547,3 +547,10 @@
|
||||
.b05-structure__spec-add {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* 반 폭 단추 둘 한 줄 — 관 보호공 [연동] · [경사](54-30). */
|
||||
.b05-structure__half-pair {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ function moveRows(panelRoot: HTMLElement, selector: string, target: HTMLElement)
|
||||
* 조정창 사본의 단 수·옵션 행을 고른 벽 쪽(유입/유출) 그룹으로 옮긴다. 행 DOM을
|
||||
* 그대로 옮기므로 배선·값 동기는 손대지 않는다. 자리가 없으면 사본에 그대로 둔다.
|
||||
*/
|
||||
function relocateSideRows(panelRoot: HTMLElement): void {
|
||||
function relocateSideRows(panelRoot: HTMLElement, scroll = true): void {
|
||||
clearSideSlots();
|
||||
const side = panelRoot.dataset.side;
|
||||
if (!sideSlots || (side !== "inlet" && side !== "outlet" && side !== "extra")) return;
|
||||
@@ -173,7 +173,7 @@ function relocateSideRows(panelRoot: HTMLElement): void {
|
||||
moveRows(panelRoot, PIPE_OUTLET_ROWS, ownerSlot);
|
||||
setGroupLabel(soil, "");
|
||||
soil.classList.add("is-active");
|
||||
soil.scrollIntoView({ block: "nearest" });
|
||||
if (scroll) soil.scrollIntoView({ block: "nearest" });
|
||||
return;
|
||||
}
|
||||
const owner = ownerSlot.closest("fieldset");
|
||||
@@ -197,7 +197,7 @@ function relocateSideRows(panelRoot: HTMLElement): void {
|
||||
const pipe = target === sideSlots.inlet || target === sideSlots.outlet;
|
||||
setGroupLabel(group, pipe ? "" : (panelRoot.dataset.panelTitle ?? ""));
|
||||
// 고른 칸이 화면 밖일 수 있다 — 그 자리로 스크롤한다(지시 2 · 유입 · 유출도 53-1).
|
||||
group.scrollIntoView({ block: "nearest" });
|
||||
if (scroll) group.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
function ensureDock(): void {
|
||||
@@ -248,9 +248,9 @@ export function adoptAdjustPanel(panelRoot: HTMLElement): void {
|
||||
* 칸(배관 유입구·유출구 / 독립 좌·우)이 아직 이전 시설 것이다(2026-08-30 사용자:
|
||||
* 다른 기슭막이를 고른 뒤 배관 유출을 고르면 강조가 없다).
|
||||
*/
|
||||
export function refreshAdjustSlots(): void {
|
||||
export function refreshAdjustSlots(scroll = true): void {
|
||||
const panel = slot?.firstElementChild as HTMLElement | null;
|
||||
if (panel) relocateSideRows(panel);
|
||||
if (panel) relocateSideRows(panel, scroll);
|
||||
}
|
||||
|
||||
/** 이 사본이 슬롯에 서 있으면 내린다 — 다른 사본이 이미 대신 서 있으면 손대지 않는다. */
|
||||
|
||||
@@ -9,6 +9,15 @@
|
||||
* 검증 → 담기 / 쓰기 → 통째 직렬화)이라 제어기마다 짝을 두던 것을 하나로 모았다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
culvertOwnerFor,
|
||||
culvertReach,
|
||||
culvertWallsStandAt,
|
||||
type RevetLinkControl,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import type { PipeRevetChoice } from "../A00_Common/design/design_structure_pipe_calc";
|
||||
|
||||
/** 세션에 담기는 측점별 조작값 한 겹 맵. */
|
||||
export interface SessionMap<V> {
|
||||
values: Map<string, V>;
|
||||
@@ -101,3 +110,31 @@ export function createLinkFlagSession(sessionKey: () => string | null): LinkFlag
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 좌측 관 보호공 [연동] · [경사](54-30) — 옛 연동 제어 그대로 읽고 씀. 연동 = 벽이 서는 옆 측점이
|
||||
* 있을 때만 · 켬 = 그 측점이 모두 연동 · 끄면 모두 따로. 경사 = 보호공 구간이 있을 때만.
|
||||
* ponytail: 연동을 풀 때 카드 단추가 하던 높이 굳히기는 안 함 — 필요하면 카드 높이 셈을 여기로.
|
||||
*/
|
||||
export function pipeRevetChoiceBridge(revetLink: RevetLinkControl) {
|
||||
const linkCards = (owner: CrossSection, sections: readonly CrossSection[]) =>
|
||||
sections.filter((s) => culvertOwnerFor(s, sections) === owner && culvertWallsStandAt(owner, s));
|
||||
return {
|
||||
read: (owner: CrossSection, sections: readonly CrossSection[]): PipeRevetChoice => {
|
||||
const cards = linkCards(owner, sections);
|
||||
return {
|
||||
linked: cards.length ? cards.every((s) => revetLink.linkedFor(s)) : null,
|
||||
grade: culvertReach(owner) ? revetLink.followGradeFor(owner) : null,
|
||||
};
|
||||
},
|
||||
write: (
|
||||
owner: CrossSection,
|
||||
sections: readonly CrossSection[],
|
||||
kind: "link" | "grade",
|
||||
on: boolean,
|
||||
): void => {
|
||||
if (kind === "grade") revetLink.setFollowGrade(owner, on);
|
||||
else linkCards(owner, sections).forEach((s) => revetLink.setLinked(s.chainage_m, on));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,6 +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 { withSpecDefaults } from "./B06_Section_UI_Page_Structures_Spec";
|
||||
|
||||
const PIPE_ADD_GUIDE = "계곡 통과 시설을 넣고 빼려면 프로젝트를 먼저 여세요.";
|
||||
@@ -317,6 +318,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
resetAdjustDock();
|
||||
section.root.querySelector(".b05-route__panel-body")?.append(adjustDockRoot());
|
||||
// 단 수·옵션 행은 유입구·유출구 그룹으로 옮겨 붙는다(2026-08-29 지시 4).
|
||||
// 상자를 새로 세우면(단 수 · 유입 형식 · 벽 조작 · 저장 · 넘김) 고른 상자 강조를 다시 얹음(54-30)
|
||||
section.facility.root.addEventListener("specpaint", () => refreshAdjustSlots(false));
|
||||
setAdjustSideSlots({
|
||||
inlet: section.facility.inletSlot,
|
||||
outlet: section.facility.outletSlot,
|
||||
@@ -496,8 +499,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
facility: {
|
||||
setInletStructure: (value) => section.facility.setInletStructure(value),
|
||||
onInletStructureChange: (handler) => section.facility.onInletStructureChange(handler),
|
||||
// 다시 그린 상자에 강조를 다시 얹음(흙막이 상자 · 54-29)
|
||||
recount: () => (section.facility.recount(), refreshAdjustSlots()),
|
||||
recount: () => section.facility.recount(),
|
||||
},
|
||||
overrideOptions: (chainageM, patch) => {
|
||||
const hit = pipeFacilities.find(
|
||||
@@ -567,6 +569,8 @@ export function wireStructureSelection(
|
||||
/** 고른 부재를 세션에 남긴다(2026-09-04). 해제(null)는 부르지 않는다 — 조정창 닫기가
|
||||
* 카드(측점) 선택까지 지우면 안 된다. 측점 해제는 카드 선택 리스너가 따로 적는다. */
|
||||
remember?: (chainageM: number, key: string) => void,
|
||||
/** 폼에서 고른 벽을 카드 조정창에도 세움(54-30 · 목록 넘김 `applyStructurePick` 과 같은 차례). */
|
||||
refreshCard?: (chainageM: number) => void,
|
||||
): { syncInletStructure: () => void } {
|
||||
const ownerChainageOf = (chainageM: number): number => {
|
||||
const sectionAt = detail()?.cross_sections.find(
|
||||
@@ -588,9 +592,7 @@ export function wireStructureSelection(
|
||||
const syncInletStructure = (): void => {
|
||||
const chainageM = panel.currentChainage();
|
||||
const target = chainageM === null ? undefined : sectionAt(chainageM);
|
||||
if (!target) return;
|
||||
panel.facility.setInletStructure(stationControls.inletStructure.valueFor(target));
|
||||
refreshAdjustSlots(); // 폼을 다시 그림 — 고른 상자 강조를 다시 얹음(54-29)
|
||||
if (target) panel.facility.setInletStructure(stationControls.inletStructure.valueFor(target));
|
||||
};
|
||||
|
||||
const origRevet = stationControls.revetOffset.select;
|
||||
@@ -611,6 +613,26 @@ export function wireStructureSelection(
|
||||
refreshAdjustSlots();
|
||||
};
|
||||
|
||||
// 관 상자 칸을 고치거나 고르면 그 부분을 고름 — 횡단에서 누른 것과 같은 길(54-30)
|
||||
panel.root.addEventListener("change", (event) => {
|
||||
// 단 수 칸은 고른 단을 지킴(단 수 바꿈 뒤 강조 유지)
|
||||
if ((event.target as HTMLElement).closest('[data-spec-item="outlet.tiers"]')) return;
|
||||
const box = (event.target as HTMLElement).closest<HTMLElement>("fieldset[data-spec-container]");
|
||||
const chainageM = panel.currentChainage();
|
||||
const target = chainageM === null ? undefined : sectionAt(chainageM);
|
||||
if (!box || !target?.culvert) return;
|
||||
const id = box.dataset.specContainer;
|
||||
const key: RevetKey | null =
|
||||
id === "inlet" || id === "outlet"
|
||||
? id
|
||||
: id === "soil_add"
|
||||
? `extra${Number(box.dataset.specIndex)}`
|
||||
: null;
|
||||
if (!key || stationControls.revetOffset.highlightFor(target) === key) return;
|
||||
stationControls.revetOffset.select(target.chainage_m, key);
|
||||
refreshCard?.(target.chainage_m);
|
||||
});
|
||||
|
||||
const origFord = stationControls.ford.select;
|
||||
stationControls.ford.select = (chainageM, role) => {
|
||||
const nested = wallSelecting;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
54-33 끝 조사 · ① 갈래 3(추천 B: 열 수 자동 + 카드 최소·최대 폭) · ② 됨(같은 창 코드로 화면 조각만 팝업에 옮기는 길 · 키보드·메뉴 연결 손봐야 함)
|
||||
|
||||
# ① 횡단 카드 크기
|
||||
|
||||
## 지금 크기가 정해지는 곳
|
||||
- 열 수 — `B06_Section_UI_Section_View.ts:319-326` : 열 수 = 칸 폭 ÷ (최소 폭 560 + 간격 16) 내림. 카드 폭 = 칸 폭을 열 수로 나눈 값. 최대 폭 없음 (`_Section_Common.ts:39-40` 상수 560·16).
|
||||
- 높이 — `_Cross_View_Metrics.ts:57-64` : 1m 당 픽셀 = (카드 폭 − 좌우 여백 78) ÷ 가로 범위(m). 높이 = 고도 범위 × 1m 당 픽셀 + 위아래 여백 36 + 면적 표 머리방 58. 바닥 250 (`_Section_Common.ts:37`).
|
||||
- 줄 높이 — `_Section_View.ts:344-349` : 같은 줄 카드 중 가장 큰 높이로 통일 (낮은 카드는 위아래 빈 자리).
|
||||
- 그림 — `_Cross_View.ts:310-314` : SVG 가로세로 = 카드 px 그대로, viewBox 도 같은 값. 즉 CSS 확대·축소 없음 · 가로세로 같은 1m 당 픽셀 · 글자 크기 고정.
|
||||
- 카드 틀 — `_Style_Cross.css:42-63`(머리·꼬리 줄) · `:119-123`(바둑판 · 열 수는 JS 가 직접 덮어씀).
|
||||
|
||||
## 원인
|
||||
- 카드 높이 = 폭에 비례 (그림 모양 고정이므로). 칸 폭 1500 이면 2열 · 카드 약 740 폭 · 그림 높이 약 480 + 머리·꼬리 줄 → 한 줄이 칸 높이를 넘음.
|
||||
- 폭을 줄이면 그림 전체가 같은 비율로 작아짐 = 축척(가로세로 비) 유지. 높이만 자르면 위아래 고도가 잘리거나 빈 자리가 늚.
|
||||
- 줄여도 안 줄어드는 고정분 : 여백 36 · 면적 표 머리방 58 · 카드 머리·꼬리 · 바닥 250 → 아주 좁은 카드에서 높이가 폭을 따라 안 줄어듦.
|
||||
|
||||
## 갈래
|
||||
- A. 최소 폭만 낮추기 (560 → 약 400)
|
||||
- 고칠 곳 : 상수 한 줄.
|
||||
- 장 : 가장 작음 · 축척 그대로.
|
||||
- 단 : 1.5줄 보장 없음 (칸 높이를 안 봄) · 칸 폭마다 우연히 맞음.
|
||||
- 크기 : 1줄.
|
||||
- B. 열 수 자동 (추천)
|
||||
- 고칠 곳 : `_Section_View.ts:319-326` 열 수 결정부 + 칸 높이도 보기(ResizeObserver 가 지금 폭만 봄 `:402-412`).
|
||||
- 방식 : 열 수 2·3·4… 를 차례로 시험 → 첫 줄 높이 + 간격 + 둘째 줄 절반이 칸 높이 안에 드는 가장 적은 열 수 채택. 카드 폭이 최소 폭(예 360~400) 아래로 내려가면 거기서 멈춤. 최대 폭(예 800~900)을 넘는 1열은 열을 늘리거나 가운데 정렬로 폭 상한.
|
||||
- 장 : 칸 높이가 달라져도(하단 패널 끌기·창 크기) 늘 1.5줄 근처 · 축척 유지(그림이 통째로 작아질 뿐 · 잘림 없음) · 계산이 기존 `crossPlotBase` 재사용.
|
||||
- 단 : 하단 패널을 끌 때 열 수가 바뀌면 카드 전체 다시 그림 (열 수가 바뀔 때만 다시 그리게 막아야 함 · 줌·팬 상태는 측점별 캐시라 유지) · 좁아진 카드는 글자 대비 그림이 작아 읽기 힘든 한계(바닥 폭이 정함) · 바닥 250 · 머리방 58 같은 고정분이 1.5줄에 못 미치게 할 수 있음 → 이 값도 열 수 계산에 넣어야 함.
|
||||
- 크기 : 약 40~60줄 + 시험 (`_Section_View.ts` 700줄 한계 확인 필요 · 열 수 계산을 별 파일로).
|
||||
- C. 높이 상한 + 그림 맞춤 (CSS 만)
|
||||
- 방식 : 카드 높이를 상한으로 막고 SVG 를 `preserveAspectRatio` 로 맞춤.
|
||||
- 장 : 고치는 곳 적음.
|
||||
- 단 : 그림이 줄어 양옆이 비고(폭 낭비) 카드 안 모든 클릭 좌표·끌기(`data-px-per-m`·팬)·글자 크기가 화면 배율과 어긋나 축척 신뢰 훼손 → 비추천.
|
||||
|
||||
## 한계·확인 필요
|
||||
- 1.5줄이 실제로 되는 폭은 칸 높이와 단면 모양(고도 범위 · 가로 범위 · 세로 과장 배율)에 따라 다름 — 실측값 아님 · 식으로만 추정 : 칸 높이 500, 가로 40m, 고도 범위 23m 이면 카드 폭 약 300 까지 내려가야 1.5줄 → 바닥 폭 360~400 이면 칸 높이가 작은 화면에서는 1.5줄에 못 미칠 수 있음. 바닥 폭은 사용자 협의 대상(임의 확정 안 함).
|
||||
- 줄 높이가 줄 최댓값이라 줄마다 열 수 시험 값이 다름 → 시험은 첫 줄과 둘째 줄을 실제 데이터로 계산.
|
||||
|
||||
# ② 종단 하단 패널을 별도 브라우저 창으로
|
||||
|
||||
## 지금 하단 패널이 기대는 것
|
||||
- 한 벌만 존재 — `A00_Common/design/design_page.ts:116-118,125-138` (`profileSlot` · `shared.profile`) · 종단 보기·횡단 보기가 같은 인스턴스를 씀. 만드는 곳 `design_profile.ts:128-239`.
|
||||
- 자료 — 같은 창 메모리(모듈 변수) + `sessionStorage`(초안·결과 · `b_page_state.ts:46-199,249-250`) + 설계 가게(`design_store.ts` : 구독 방식이 같은 창 안에서만 동작). 화면 취향(패널 높이·접힘)은 `localStorage` (+ 계정 서버 저장) → 창끼리 이미 공유.
|
||||
- 페이지 쪽 연결 — 콜백 약 12 종 (`B05_Profile_UI_Profile_Panel_Types.ts:17-57` : 관 변경 · 구조물 이동·추가·삭제 · 측점 선택 · 횡단 재계산 알림 등) + `onSelectStation` · `onResetAll` + 횡단 카드 갱신 `refreshCards`.
|
||||
- 창 전역에 거는 리스너 — `window` 포인터 놓기·취소·blur·keydown·keyup (`_Profile_Drag.ts:82-83,201-202` · `_Profile_Edit.ts:60-62` · `_Profile_Panel_Tools.ts:268-269,313-317` · `_Profile_Wheel`) · 우클릭 메뉴가 `document` 에 리스너 (`ui_template_context_menu.ts:57,67`) · 화면 크기 `window.innerHeight` (`_Profile_Heights.ts:26` · `_MassHaul.ts:231` · `_TableOverlay.ts:84`) · 토스트·모달은 `document.body` (`ui_template_elements.ts`, `ui_template_modal.ts`).
|
||||
- 위치 — 종단 보기에서는 3D 위에 뜨는 판(`B05_Profile_UI_Style.css:42-54` absolute) · 횡단 보기는 카드 아래 흐름(`design_page.css:38-41`).
|
||||
|
||||
## 갈래
|
||||
- A. 같은 창 코드로 화면 조각만 팝업에 옮기기 (추천)
|
||||
- 방식 : [분리] 단추 → `window.open`(같은 출처 · 사용자 클릭 안에서) → 팝업 문서에 스타일 복사 → `profileSlot` 안 종단도 DOM 을 팝업 본문으로 옮김. 코드는 메인 창에서 계속 실행.
|
||||
- 자료 공유 : 필요 없음 — 같은 메모리 · 같은 `sessionStorage` · 같은 가게. 편집 → 횡단 카드·3D 갱신도 지금 경로 그대로.
|
||||
- 손볼 곳 : ① 창 전역 리스너를 요소의 소속 창(`ownerDocument.defaultView`)에 걸도록 약 10곳 ② 우클릭 메뉴 `document` 리스너와 화면 밖 보정 `window.innerWidth/Height` ③ 팝업 닫힘(`pagehide`) 때 DOM 을 원래 자리로 되돌림 · 메인 창 새로고침·이동 때 팝업 닫기 ④ 메인 쪽 빈 자리 처리(종단 보기 = 3D 가 전면 · 횡단 보기 = 카드 칸이 아래까지 늘어남) ⑤ 스타일 복사 (`link`·`style` 통째 복제 · 개발 서버 핫리로드 스타일 포함) ⑥ 토스트·모달은 메인 창에 뜸 (팝업 쪽에서 보이려면 별도 처리).
|
||||
- 크기 : 신규 약 200~300줄 + 기존 약 8 파일 소폭 수정. 위험 중간 — 키보드(▲▼ 방향키)·끌기·우클릭 전부 다시 시험해야 함.
|
||||
- B. 팝업이 자체 앱을 띄우고 BroadcastChannel 로 동기화
|
||||
- 방식 : 별도 라우트로 종단도만 띄우고 메인과 메시지 주고받기.
|
||||
- 막히는 것 : `sessionStorage` 는 탭마다 따로(팝업 열릴 때 복사본만 받고 이후 갈라짐 · `storage` 이벤트도 세션 저장소는 창 사이 안 옴) → 초안 5종(계획고·관·구조물·선택·횡단 상세) 양방향 동기화 직접 구현 · 콜백 12종을 메시지로 바꿔야 함 · 자료 정본이 둘로 갈려 `CLAUDE.md` 5장(캐시 한 곳)과 충돌 · 횡단 상세 응답이 커서 메시지 비용 큼.
|
||||
- 크기 : 큼 (수백 줄 + 충돌 처리). 비추천.
|
||||
- C. 읽기 전용 거울 (팝업은 종단도 그림 + 선택 표시만, 편집은 메인)
|
||||
- 장 : 단순(BroadcastChannel 로 선택 측점·종단 자료 복사).
|
||||
- 단 : 편집 도구(▲▼·직선화·유토곡선 표)를 쓰려는 목적이면 의미 없음.
|
||||
|
||||
## 한계·확인 필요
|
||||
- ORCA 내장 브라우저에서 `window.open` 팝업이 뜨는지 확인 안 함 — 되는지 먼저 한 번 시험 필요(안 되면 공용 브라우저에서만 검증).
|
||||
- 팝업 차단 : 반드시 단추 클릭 안에서 열어야 함.
|
||||
- 메인 창 닫기·이동 시 팝업 정리, 팝업 위치·크기 기억(취향 저장소 `pref` 에 한 칸)은 부가 작업.
|
||||
- 두 창을 동시에 켜는 쓰기 충돌은 A 에서는 없음 (한 인스턴스).
|
||||
|
||||
# 요약 다섯 줄
|
||||
1. ① 카드 높이는 폭에 비례(그림이 CSS 확대 없이 px 그대로) → 폭을 줄이면 축척 유지한 채 높이가 줄어듦.
|
||||
2. 추천 B : 열 수를 칸 높이에 맞춰 자동 선택(1.5줄) + 카드 최소·최대 폭 상한 · 약 50줄 · 최소 폭 값은 협의 필요.
|
||||
3. 한계 : 여백 36·머리방 58·바닥 250 은 안 줄어서 칸이 낮은 화면에서는 1.5줄에 못 미칠 수 있음.
|
||||
4. ② 종단 패널은 지금 한 벌 + 같은 창 메모리·세션 저장소에 기댐 → 팝업에 별도 앱을 띄우면 동기화가 크게 어려움.
|
||||
5. 추천 A : 코드는 메인에 두고 화면 조각만 팝업으로 옮김(자료 공유 불필요) · 창 전역 리스너 약 10곳과 메뉴 손봐야 함 · ORCA 에서 팝업 되는지 먼저 확인.
|
||||
@@ -3331,8 +3331,6 @@
|
||||
"outlet.before": "기준측점 전 (m)",
|
||||
"outlet.after": "기준측점 후 (m)",
|
||||
"soil_add": "흙막이 추가",
|
||||
"soil_add.structure": "구조",
|
||||
"soil_add.structure=기슭막이": "흙막이",
|
||||
"soil_add.form": "형태",
|
||||
"soil_add.form=돌쌓기(찰)": "돌 쌓기(찰)",
|
||||
"soil_add.form=돌쌓기(메)": "돌 쌓기(메)",
|
||||
|
||||
@@ -127,6 +127,10 @@ const PIPE_CALC = {
|
||||
const STUBS = {
|
||||
"@config/config_frontend": { FORD_MANNING_N: 0.03, FORD_SLOPE: 0.02 },
|
||||
"../A00_Common/design/design_structure_pipe_calc": PIPE_CALC,
|
||||
// 관 보호공 [연동] · [경사] 줄(54-30) — 여기서는 자리만
|
||||
"./B05_Profile_UI_Drainage_Revet_Choice": {
|
||||
revetChoiceRow: () => ({ paint: () => new FakeNode("div") }),
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_UI_Structures_Summary_Row": {
|
||||
buildSummaryRow: () => {
|
||||
const root = new FakeNode("div");
|
||||
@@ -174,7 +178,10 @@ const tierInput = () => tierRow()?.querySelector("input");
|
||||
const snap = () => ({
|
||||
value: tierInput()?.value ?? null,
|
||||
boxes: boxesOf("soil_add").length,
|
||||
structures: boxesOf("soil_add").map((b) => b.querySelector("select")?.children[0]?.textContent),
|
||||
// 상자 첫 칸 이름 — 구조 고르기 없이 형태부터(54-30 · 사면안정 흙막이와 같은 칸)
|
||||
structures: boxesOf("soil_add").map(
|
||||
(b) => b.querySelectorAll("label")[0]?.children[0]?.textContent,
|
||||
),
|
||||
});
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -66,9 +66,10 @@ def test_옛_키로_읽고_같은_키로_씀_옛_칸은_안_씀(out):
|
||||
|
||||
|
||||
def test_선택_컨테이너는_인스턴스마다_차례대로_다시_매김(out):
|
||||
assert [i["structure"] for i in out["state"]["soil_add"]] == ["기슭막이", ""]
|
||||
# 흙막이 상자 = 구조 고르기 없음(54-30) — 옛 구조 키만 든 상자도 자리는 지킴
|
||||
assert [i["form"] for i in out["state"]["soil_add"]] == ["바자", ""]
|
||||
assert out["back"]["soil_add_1_form"] == "바자"
|
||||
assert "soil_add_2_form" not in out["back"]
|
||||
assert out["back"]["soil_add_2_form"] == "" # 빈 상자 자리 표식 = 첫 칸(형태) 빈 글
|
||||
assert out["back"]["chute_add_1_structure"] == "도수로"
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ def test_배관_컨테이너마다_첫_값_한_줄(out):
|
||||
"관: 관경 1200",
|
||||
"관 보호공(유입): 구조 집수정",
|
||||
"관 보호공(유출): 구조 흙막이",
|
||||
"흙막이 1: 구조 흙막이",
|
||||
"흙막이 1: 형태 돌망태", # 구조 칸 없음(54-30)
|
||||
"도수로/돌붙임 1: 구조 도수로",
|
||||
]
|
||||
|
||||
@@ -56,7 +56,7 @@ def test_빈_값은_등록부_기본값에_기본_표시(out):
|
||||
|
||||
|
||||
def test_흙막이_상자_수는_밖에서_정한_단_수(out):
|
||||
assert out["pipeCount"][-2:] == ["흙막이 1: 구조 흙막이", "흙막이 2: 구조 —"]
|
||||
assert out["pipeCount"][-2:] == ["흙막이 1: 형태 —", "흙막이 2: 형태 —"]
|
||||
assert len(out["pipeCount"]) == 2 + 3 + 2
|
||||
|
||||
|
||||
|
||||
@@ -52,8 +52,9 @@ def test_횡단에서_바꾸면_좌측_칸이_따라감(out) -> None:
|
||||
assert (out["fromCross"]["value"], out["fromCross"]["boxes"]) == ("2", 2)
|
||||
|
||||
|
||||
def test_늘어난_흙막이_상자_구조는_흙막이_기본(out) -> None:
|
||||
assert out["set2"]["structures"] == ["흙막이 (기본)", "흙막이 (기본)"]
|
||||
def test_늘어난_흙막이_상자는_구조_칸_없이_형태부터(out) -> None:
|
||||
"""54-30 — 사면안정 흙막이와 같은 칸(형태 · 길이 · 높이 · 전 · 후) · 구조 고르기 없음."""
|
||||
assert out["set2"]["structures"] == ["형태", "형태"]
|
||||
|
||||
|
||||
def test_흙막이_아닌_구조면_칸을_가림(out) -> None:
|
||||
|
||||
@@ -40,7 +40,31 @@ def test_배관_유입_단_수_줄은_선_단이_있을_때만() -> None:
|
||||
|
||||
|
||||
def test_폼을_다시_그리면_강조_다시() -> None:
|
||||
"""54-30 — 상자를 새로 세울 때마다(단 수 · 유입 형식 · 벽 조작 · 저장 · 넘김) 한 곳에서 강조를 다시 얹음."""
|
||||
form = _src("A00_Common/design/design_structure_form.ts")
|
||||
assert 'root.dispatchEvent?.(new Event("specpaint", { bubbles: true }));' in form
|
||||
page = _src("B06_Section/B06_Section_UI_Page_Structures_Panel.ts")
|
||||
assert "recount: () => (section.facility.recount(), refreshAdjustSlots())" in page
|
||||
sync = page[page.index("const syncInletStructure") :]
|
||||
assert sync.index("refreshAdjustSlots()") < sync.index("};")
|
||||
assert 'addEventListener("specpaint", () => refreshAdjustSlots(false))' in page
|
||||
|
||||
|
||||
def test_상자_칸을_고치면_그_부분을_고름() -> None:
|
||||
"""54-30 — 칸 고침 = 횡단에서 그 벽을 누른 것과 같은 길 · 단 수 칸은 고른 단을 지킴."""
|
||||
page = _src("B06_Section/B06_Section_UI_Page_Structures_Panel.ts")
|
||||
assert '[data-spec-item="outlet.tiers"]' in page
|
||||
assert "`extra${Number(box.dataset.specIndex)}`" in page
|
||||
assert "refreshCard?.(target.chainage_m);" in page
|
||||
|
||||
|
||||
def test_흙막이_상자는_사면안정_흙막이와_같은_칸() -> None:
|
||||
pipe = _src("A00_Common/design/design_structure_pipe.ts")
|
||||
soil = pipe[pipe.index('id: "soil_add"') :]
|
||||
assert soil[: soil.index("},")].count("items: guardItems(),") == 1
|
||||
|
||||
|
||||
def test_관_보호공_연동_경사_단추() -> None:
|
||||
row = _src("B05_Profile/B05_Profile_UI_Drainage_Revet_Choice.ts")
|
||||
assert 'toggleButton("연동"' in row and 'toggleButton("경사"' in row
|
||||
assert "b05-structure__half-pair" in row
|
||||
session = _src("B06_Section/B06_Section_UI_Page_Link_Session.ts")
|
||||
assert "revetLink.setFollowGrade(owner, on)" in session
|
||||
assert "revetLink.setLinked(s.chainage_m, on)" in session
|
||||
|
||||
Reference in New Issue
Block a user