Merge remote-tracking branch 'origin/dev' into sub_desktop_1

This commit is contained in:
2026-09-13 08:50:24 +09:00
37 changed files with 29724 additions and 531 deletions
+219
View File
@@ -0,0 +1,219 @@
/* =============================================================================
* b_svg_zoom_pan.ts
* SVG 그래프 한 장의 **확대·축소·팬** — 여러 화면이 한 코드를 쓴다.
*
* B06 횡단 카드(`B06_Section_UI_Cross_View_Zoom.ts`)에 있던 것을 옮겨 온 것이다
* (2026-09-12 사용자 지시 「공용화해줘」). 동작 규칙은 그때 정해진 것을 그대로 지킨다.
*
* · **팬은 가운데(휠) 버튼 전용** — 왼쪽 버튼은 그 화면의 고르기·조작 몫이다.
* · **더블클릭이면 원배율**로 돌아간다.
* · **휠은 화면마다 다르다** — 카드가 수십 장 깔리는 B06 은 휠을 줌에 묶지 않는다
* (묶으면 목록을 훑을 수가 없다, 2026-08-02 사용자 확정). 판이 둘뿐인 B05 계획노선
* 횡단은 휠 = 줌이다(2026-09-12 사용자 지시 ③). `wheelZoom` 한 칸으로 가른다.
*
* 그리는 일은 안 한다 — 넘겨받은 `layer` 에 `transform` 만 적는다.
* ========================================================================== */
/** 줌·팬 상태 — 다시 그릴 때 배율을 되살리는 데 쓴다(2026-08-22 사용자 ③). */
export interface ZoomPanState {
scale: number;
tx: number;
ty: number;
}
/**
* 그려 둔 도형이 실제로 차지하는 범위(원배율 SVG 좌표). 표시 범위 밖에 있어 잘려 있는
* **캐시 보유분**까지 포함한다 — 확대 상태의 팬은 이 범위까지 갈 수 있다
* (2026-08-23 사용자 지시).
*/
export interface ContentBounds {
x: number;
y: number;
width: number;
height: number;
}
export interface ZoomPanHandle {
/** 1보다 크면 확대, 작으면 축소. 플롯 영역의 중앙을 붙잡는다. */
zoom: (factor: number) => void;
reset: () => void;
/** 현재 배율 — 원배율(1)일 때는 부르는 쪽 버튼이 배율 대신 표시 폭을 조절한다. */
scale: () => number;
}
/** 축 안쪽 여백(px) — 확대·축소의 중심이자 이동 한계의 기준이 된다. */
export interface ZoomPanPad {
left: number;
right: number;
top: number;
bottom: number;
}
export interface SvgZoomPanOptions {
svg: SVGSVGElement;
/** 밀고 키울 도형 묶음 — 여기에만 `transform` 이 적힌다. */
layer: SVGGElement;
/** `viewBox` 의 폭·높이(원배율 SVG 좌표). */
widthPx: number;
heightPx: number;
pad: ZoomPanPad;
/** 배율 상한. 안 주면 8. */
maxScale?: number;
/** 이전 상태 — 다시 그려도 배율이 살아남게. */
initial?: ZoomPanState;
/** 상태가 바뀔 때마다 부른다 — 바깥이 담아 뒀다가 다음 그림에 되돌린다. */
onChange?: (state: ZoomPanState) => void;
/** 도형이 실제로 그려진 범위. 확대 상태에서 팬 한계가 여기까지 늘어난다. */
content?: ContentBounds;
/** 휠을 줌에 묶을지(기본 안 묶음). 묶으면 **커서 아래 지점**을 붙잡고 확대한다. */
wheelZoom?: boolean;
}
/** 휠 한 칸에 얼마나 — 노선 편집 지도와 같은 계수(`_RouteEdit_Input`). */
const WHEEL_IN = 1.15;
const WHEEL_OUT = 0.87;
export function attachSvgZoomPan(options: SvgZoomPanOptions): ZoomPanHandle {
const { svg, layer, widthPx, heightPx, pad, initial, onChange, content } = options;
// 플롯 영역(축 안쪽) — 확대·축소의 중심이자 이동 한계의 기준이다.
const plot = {
x: pad.left,
y: pad.top,
width: Math.max(widthPx - pad.left - pad.right, 1),
height: Math.max(heightPx - pad.top - pad.bottom, 1),
};
const maxScale = options.maxScale ?? 8;
let scale = initial?.scale ?? 1;
let tx = initial?.tx ?? 0;
let ty = initial?.ty ?? 0;
const applyTransform = (): void => {
layer.setAttribute("transform", `translate(${tx} ${ty}) scale(${scale})`);
onChange?.({ scale, tx, ty });
};
// 확대한 도형이 플롯 영역을 항상 덮게 이동량을 가둔다 — 원배율에서는 이동량이 0으로 묶인다.
// **확대 상태**에서는 한계가 그려진 도형 전체(표시 범위 밖 캐시 보유분 포함)까지 늘어나,
// 가운데 버튼 팬으로 잘려 있던 지반·설계선을 끌어다 볼 수 있다(2026-08-23 사용자 지시).
const clampAxis = (
value: number,
start: number,
size: number,
from: number,
to: number,
): number =>
Math.min(
Math.max(value, Math.min(start + size - scale * to, start - scale * from)),
Math.max(start + size - scale * to, start - scale * from),
);
const clampPan = (): void => {
// 도형 범위를 모르면 플롯 영역 자신이 한계다(기존 규칙). 알면 **원배율에서도** 그
// 범위까지 열어 둔다 — 캐시 보유분이 표시 폭보다 넓으면 1배에서도 끌어다 봐야 한다
// (2026-08-23 사용자 재보고: 확대해야만 움직이는 줄 모르고 안 된다고 판단).
const bounds = content
? {
x: Math.min(content.x, plot.x),
y: Math.min(content.y, plot.y),
right: Math.max(content.x + content.width, plot.x + plot.width),
bottom: Math.max(content.y + content.height, plot.y + plot.height),
}
: { x: plot.x, y: plot.y, right: plot.x + plot.width, bottom: plot.y + plot.height };
tx = clampAxis(tx, plot.x, plot.width, bounds.x, bounds.right);
ty = clampAxis(ty, plot.y, plot.height, bounds.y, bounds.bottom);
};
/** 그 자리를 붙잡고 확대·축소한다 — 버튼은 플롯 중앙을, 휠은 커서 자리를 준다. */
const zoomAt = (factor: number, cx: number, cy: number): void => {
const next = Math.min(maxScale, Math.max(1, scale * factor));
tx = cx - ((cx - tx) / scale) * next;
ty = cy - ((cy - ty) / scale) * next;
scale = next;
clampPan();
applyTransform();
};
// 버튼에는 마우스 자리가 없다 — 보이는 **플롯 영역의 중앙**을 붙잡는다.
const zoom = (factor: number): void =>
zoomAt(factor, plot.x + plot.width / 2, plot.y + plot.height / 2);
let panning = false;
let moved = false;
let lastX = 0;
let lastY = 0;
// 가운데 버튼을 누르면 브라우저가 자동 스크롤(가운데 클릭 스크롤)을 켠다 — `mousedown`
// 기본동작이라 `pointerdown`에서는 못 막는다. 여기서 막아야 팬만 남는다(2026-08-02 사용자 지시).
svg.addEventListener("mousedown", (event) => {
if (event.button === 1) event.preventDefault();
});
svg.addEventListener("auxclick", (event) => {
if (event.button === 1) event.preventDefault();
});
svg.addEventListener("pointerdown", (event) => {
// 팬은 **가운데 버튼**만. 좌클릭은 그 화면의 고르기 몫이다.
if (event.button !== 1) return;
event.preventDefault();
panning = true;
moved = false;
lastX = event.clientX;
lastY = event.clientY;
svg.classList.add("is-panning");
svg.setPointerCapture(event.pointerId);
});
svg.addEventListener("pointermove", (event) => {
if (!panning) return;
if (Math.abs(event.clientX - lastX) + Math.abs(event.clientY - lastY) > 2) moved = true;
// 도형을 직접 미는 방식이라 커서를 따라간다(viewBox를 밀던 때와 부호가 반대다).
const rect = svg.getBoundingClientRect();
tx += ((event.clientX - lastX) / rect.width) * widthPx;
ty += ((event.clientY - lastY) / rect.height) * heightPx;
lastX = event.clientX;
lastY = event.clientY;
clampPan();
applyTransform();
});
const endPan = (event: PointerEvent): void => {
if (!panning) return;
panning = false;
svg.classList.remove("is-panning");
try {
svg.releasePointerCapture(event.pointerId);
} catch {
/* 이미 해제됨 */
}
};
if (initial && (scale !== 1 || tx !== 0 || ty !== 0)) {
clampPan();
applyTransform();
}
svg.addEventListener("pointerup", endPan);
svg.addEventListener("pointercancel", endPan);
// 드래그(팬)로 끝난 클릭은 바깥(카드 고르기 등)으로 전파하지 않는다.
svg.addEventListener("click", (event) => {
if (moved) event.stopPropagation();
});
if (options.wheelZoom) {
svg.addEventListener(
"wheel",
(event) => {
event.preventDefault();
// 커서 아래 지점이 제자리에 남게 **그 자리**를 붙잡는다.
const rect = svg.getBoundingClientRect();
const cx = ((event.clientX - rect.left) / Math.max(rect.width, 1)) * widthPx;
const cy = ((event.clientY - rect.top) / Math.max(rect.height, 1)) * heightPx;
// 노선 편집 지도와 같은 방향 — **당기면 확대**(`deltaY > 0`).
zoomAt(event.deltaY > 0 ? WHEEL_IN : WHEEL_OUT, cx, cy);
},
{ passive: false },
);
}
const currentScale = (): number => scale;
const reset = (): void => {
scale = 1;
tx = 0;
ty = 0;
applyTransform();
};
// 더블클릭 원복.
svg.addEventListener("dblclick", (event) => {
event.stopPropagation();
reset();
});
return { zoom, reset, scale: currentScale };
}
+45 -12
View File
@@ -53,6 +53,7 @@ import {
applyArcLocks,
applyCurveLimits,
curveShortfalls,
radiusFloorM,
curveSummary,
flattenServerPlan,
shortfallCrossed,
@@ -74,6 +75,10 @@ const SEGMENT_HIT_PX = 12;
const CONTOUR_HIT_PX = 6;
/** 측점 눈금을 집었다고 볼 거리(px) — 눈금이 보이는 자리를 누르면 잡히게 넉넉히. */
const STATION_HIT_PX = 11;
/** 등고선 눈금을 고를 때 쓸 라벨 예산 — B04 지도 기본값(350)보다 **넉넉히** 둔다
* (2026-09-12 사용자 지시 ⑦ 「좀더 촘촘히」). 편집 창은 노선 둘레 300m 띠만 보이므로
* 같은 예산으로도 화면에 드는 줄이 적어 눈금이 필요 이상으로 성겼다. */
const CONTOUR_LEVEL_BUDGET = 1200;
type Vertex = [number, number];
@@ -118,6 +123,9 @@ export async function openRouteEditModal(
* 노드를 옮기면 교각이 바뀌어 R 과 길이 중 하나는 반드시 따라 움직인다(`_Edits.ts`). */
let curveLock: CurveLock[] = [];
let curveArc: Array<number | null> = [];
/** 교각점 자리를 못 박은 꺾임점(2026-09-12 사용자 지시 ①) — 켜진 자리는 노드도 손잡이도
* 안 끌리고 반지름·곡선 길이 칸으로만 바뀐다. */
let apexLock: boolean[] = [];
/** 지금 고른 꺾임점 — 곡선 편집줄이 이 자리를 만진다. 없으면 -1. */
let picked = -1;
/** 되돌리기 사진첩 — 노선을 읽은 뒤에 선다(그전에는 되돌릴 것이 없다). */
@@ -258,7 +266,7 @@ export async function openRouteEditModal(
/** 지금 화면에 낼 등고선 간격(m) — 그리기와 집기가 같은 값을 보게 한 자리에서 셈한다. */
const contourStepM = (): number =>
contours ? pickLevelStep(contours.layer, view, contours.intervalM) : 0;
contours ? pickLevelStep(contours.layer, view, contours.intervalM, CONTOUR_LEVEL_BUDGET) : 0;
function draw(): void {
if (closed) return;
@@ -291,7 +299,10 @@ export async function openRouteEditModal(
const handleAt = (px: number, py: number): { node: number; end: "start" | "end" } | null =>
handleAtScreen(
// 붙들어 둔 곡선은 손잡이로도 안 바뀐다 — 끌면 R 이 바뀌기 때문(사용자 지시 5).
curveInfo.filter((entry) => (curveLock[entry.node_first] ?? null) === null),
curveInfo.filter(
(entry) =>
(curveLock[entry.node_first] ?? null) === null && apexLock[entry.node_first] !== true,
),
toScreen,
px,
py,
@@ -326,7 +337,8 @@ export async function openRouteEditModal(
// 길이를 붙든 자리는 교각이 바뀌었을 수 있다 — 그리기 전에 R 부터 다시 잡는다.
applyArcLocks(planned, curveLock, curveArc, curveRadius);
// 지정해 둔 값이 하한을 밑돌면 하한까지 끌어올린다(계획서 0-9 ④).
applyCurveLimits(planned, curveOn, curveRadius, limitRadiusM);
// 하한은 **R 과 L 을 함께 본 값**이다 — 내각 155° 이상은 R 만 본다(2026-09-12 확정).
applyCurveLimits(planned, curveOn, curveRadius, limitRadiusM, limitArcM);
const built = buildEditedPolyline(planned, curveOn, curveRadius, minRadiusM);
plannedLine = built.vertices;
curveInfo = built.curves;
@@ -354,6 +366,7 @@ export async function openRouteEditModal(
curveOn,
curveRadius,
curveLock,
apexLock,
curveCount: curveInfo.length,
violationCount: nodeInfo.filter((node) => node.violations.length).length,
minRadiusM,
@@ -372,6 +385,7 @@ export async function openRouteEditModal(
curveRadius,
curveLock,
curveArc,
apexLock,
limitRadiusM,
limitArcM,
}),
@@ -398,7 +412,7 @@ export async function openRouteEditModal(
/** 지금 편집값을 사진 한 벌로 담는다 — 되돌리기가 쌓아 두는 것. */
function snapshotNow(): RouteEditSnapshot {
return { planned, curveOn, curveRadius, curveLock, curveArc, picked };
return { planned, curveOn, curveRadius, curveLock, curveArc, apexLock, picked };
}
const historyControls = bindHistoryControls({
@@ -410,6 +424,7 @@ export async function openRouteEditModal(
curveRadius = snapshot.curveRadius;
curveLock = snapshot.curveLock;
curveArc = snapshot.curveArc;
apexLock = snapshot.apexLock;
picked = snapshot.picked;
applyEdit(message, false); // 되살리는 것은 새 걸음이 아니다.
},
@@ -436,11 +451,13 @@ export async function openRouteEditModal(
// **노드가 손잡이보다 먼저다**(2026-09-07 사용자 지적 ④). 반대로 두었더니 헤어핀처럼
// 곡선이 몰린 데서는 손잡이가 늘 먼저 잡혀 **노드를 아예 못 집었다**(실화면에서 격자로
// 훑어 보니 잡히는 것이 전부 손잡이였음). 손잡이는 고른 곡선에만 나오므로 겹침도 적다.
dragNode = nodeAt(px, py);
dragHandle = dragNode >= 0 ? null : handleAt(px, py);
const hitNode = nodeAt(px, py);
// 교각점을 고정한 자리는 **고르기만** 되고 안 끌린다(2026-09-12 사용자 지시 ①).
dragNode = hitNode >= 0 && apexLock[hitNode] !== true ? hitNode : -1;
dragHandle = hitNode >= 0 ? null : handleAt(px, py);
dragMoved = false;
if (dragNode >= 0) {
picked = dragNode; // 누른 자리를 고른다 — R 라벨이 그 곡선을 만진다.
if (hitNode >= 0) {
picked = hitNode; // 누른 자리를 고른다 — R 라벨이 그 곡선을 만진다.
measure.clear(); // 잰 창과 곡선 패널은 같이 뜨지 않는다(㉔).
syncCurveBar();
draw();
@@ -501,7 +518,11 @@ export async function openRouteEditModal(
if (moved) {
planned[node] = moved.apex;
// 손으로 끌어도 하한 아래로는 안 내려간다 — 거기서 멈춘다(계획서 0-9 ④).
curveRadius[node] = Math.max(limitRadiusM, Math.round(moved.radius * 100) / 100);
// 하한은 그 자리의 교각까지 본 값이라 L 하한도 함께 지켜진다.
curveRadius[node] = Math.max(
radiusFloorM(nodeInfo[node]?.inner_angle_deg, limitRadiusM, limitArcM),
Math.round(moved.radius * 100) / 100,
);
curveOn[node] = true;
picked = node;
// 손잡이 자리는 다시 셈한 곡선에서 나온다 — 접선 자리가 모자라 R 이 눌리면 손이
@@ -517,12 +538,12 @@ export async function openRouteEditModal(
if (dragNode >= 0) {
// 옮기기 **전**에 하한을 지키던 자리 — 이미 밑돌던 자리는 그대로 고칠 수 있어야 하므로
// **지키던 자리가 넘어가는 것만** 막는다(계획서 0-9 ④).
const before = curveShortfalls(nodeInfo, limitRadiusM);
const before = curveShortfalls(nodeInfo, limitRadiusM, limitArcM);
const previous = planned[dragNode];
dragMoved = true;
planned[dragNode] = toMetric(px, py);
markEdited(); // 곡선을 그 자리에서 다시 그린다 — 나머지 곡선은 그대로 남는다.
if (shortfallCrossed(before, curveShortfalls(nodeInfo, limitRadiusM))) {
if (shortfallCrossed(before, curveShortfalls(nodeInfo, limitRadiusM, limitArcM))) {
// 접선 자리가 모자라 R 이 하한 아래로 눌리는 자리다 — 그 걸음만 되돌린다.
planned[dragNode] = previous;
markEdited();
@@ -538,7 +559,16 @@ export async function openRouteEditModal(
draw();
return;
}
canvas.style.cursor = nodeAt(px, py) >= 0 || handleAt(px, py) ? "grab" : "default";
const overNode = nodeAt(px, py);
// 고정한 교각점 위에서는 **못 끈다**고 커서로 먼저 알린다.
canvas.style.cursor =
overNode >= 0
? apexLock[overNode]
? "not-allowed"
: "grab"
: handleAt(px, py)
? "grab"
: "default";
});
const endDrag = (event: PointerEvent): void => {
@@ -570,6 +600,7 @@ export async function openRouteEditModal(
curveRadius.splice(segment + 1, 0, null);
curveLock.splice(segment + 1, 0, null);
curveArc.splice(segment + 1, 0, null);
apexLock.splice(segment + 1, 0, false);
picked = segment + 1;
applyEdit("새 노드를 넣었습니다(직선 추가).");
});
@@ -588,6 +619,7 @@ export async function openRouteEditModal(
curveRadius.splice(index, 1);
curveLock.splice(index, 1);
curveArc.splice(index, 1);
apexLock.splice(index, 1);
picked = -1;
applyEdit("노드를 지웠습니다(직선 삭제).");
});
@@ -638,6 +670,7 @@ export async function openRouteEditModal(
curveRadius = flat.curveRadius;
curveLock = flat.curveLock;
curveArc = flat.curveArc;
apexLock = flat.apexLock;
picked = -1;
// 여기가 [초기화]가 돌아갈 자리다 — 창을 연 그대로.
history = createRouteEditHistory(snapshotNow());
+35 -12
View File
@@ -16,8 +16,13 @@
* `compute_cross_design`(서버), 성토사면 길이는 B06 화면이 쓰는 `fillSlopeLengths`.
* ========================================================================== */
import { attachSvgZoomPan } from "../A00_Common/b_svg_zoom_pan";
import { fetchCrossPreview, type CrossPreviewResponse } from "./B05_Profile_Api_Replan";
import { drawCross, summarizeCross } from "./B05_Profile_UI_RouteEdit_Cross_Draw";
import {
buildCrossSvg,
CROSS_PLOT_PAD,
summarizeCross,
} from "./B05_Profile_UI_RouteEdit_Cross_Draw";
import { formatStation } from "./B05_Profile_Util_Station";
export interface CrossPreviewParams {
@@ -33,7 +38,7 @@ export interface CrossPreviewParams {
}
export interface CrossPreviewWindow {
/** 그 측점의 횡단을 위 판에 낸다. 같은 측점을 다시 누르면 보던 것을 아래로 내린다. */
/** 그 측점의 횡단을 위 판에 낸다. 아래 판은 비운다 — 전후 비교는 노선을 고쳤을 때만. */
open: (chainageM: number) => Promise<void>;
/** 노선을 고쳤다 — 보던 측점을 **다시 셈해** 전후로 늘어놓는다. 보던 것이 없으면 아무 일도 없다. */
refresh: () => Promise<void>;
@@ -55,16 +60,15 @@ function createPane(title: string, empty: string): CrossPane {
<strong class="b05-routeedit__cross-title">${title}</strong>
<span class="b05-routeedit__cross-station"></span>
</div>
<canvas class="b05-routeedit__cross-canvas" width="420" height="240"></canvas>
<div class="b05-routeedit__cross-box"></div>
<div class="b05-routeedit__cross-foot">${empty}</div>`;
const station = root.querySelector<HTMLElement>(".b05-routeedit__cross-station")!;
const foot = root.querySelector<HTMLElement>(".b05-routeedit__cross-foot")!;
const canvas = root.querySelector<HTMLCanvasElement>(".b05-routeedit__cross-canvas")!;
const context = canvas.getContext("2d")!;
const box = root.querySelector<HTMLElement>(".b05-routeedit__cross-box")!;
return {
root,
show(preview, intervalM) {
context.clearRect(0, 0, canvas.width, canvas.height);
box.replaceChildren();
if (!preview) {
station.textContent = "";
foot.textContent = empty;
@@ -73,11 +77,30 @@ function createPane(title: string, empty: string): CrossPane {
// 측점은 **누가거리가 아니라 측점 표기**로 낸다(계획서 0-9 ㉑) — B05 왼쪽 아래 구조물
// 목록이 쓰는 그 규칙이다. 서버가 주는 `STA.0+100.000` 을 그대로 쓰면 표기가 갈린다.
station.textContent = formatStation(preview.chainage_m, intervalM);
drawCross(context, canvas, preview);
// 판의 실제 크기로 짓는다 — `viewBox` 가 픽셀과 1:1 이라야 휠·팬이 커서를 따라간다.
// 칸이 접혀 있으면(1500px 미만) 0 이 나오므로 최소치를 깐다.
const rect = box.getBoundingClientRect();
const plot = buildCrossSvg(
preview,
Math.max(Math.round(rect.width), 240),
Math.max(Math.round(rect.height), 150),
);
box.append(plot.svg);
// 확대·이동은 **공용 조각**이 맡는다 — B06 횡단 카드와 한 코드다(2026-09-12 지시 ③).
// 판이 둘뿐이라 여기서는 휠을 줌에 묶는다(카드 목록인 B06 은 안 묶는다).
attachSvgZoomPan({
svg: plot.svg,
layer: plot.layer,
widthPx: plot.width,
heightPx: plot.height,
pad: CROSS_PLOT_PAD,
content: plot.content,
wheelZoom: true,
});
foot.textContent = summarizeCross(preview);
},
wait(text) {
context.clearRect(0, 0, canvas.width, canvas.height);
box.replaceChildren();
foot.textContent = text;
},
};
@@ -117,11 +140,11 @@ export function createCrossPreview(params: CrossPreviewParams): CrossPreviewWind
return {
async open(chainageM) {
// 다른 측점을 고른 것이라면 전후 비교가 아니다 — 아래 판을 비운다.
const sameStation = watching !== null && Math.abs(watching - chainageM) < 1e-6;
if (!sameStation) previous.show(null, params.request().station_interval_m);
// 아래 판은 **노선을 고쳤을 때만** 채운다(2026-09-12 사용자 지시 ⑤). 예전에는 같은
// 측점을 두 번 누르면 위·아래에 **같은 그림 두 장**이 떠 전후 비교가 되지 않았다.
previous.show(null, params.request().station_interval_m);
watching = chainageM;
await load(chainageM, sameStation);
await load(chainageM, false);
},
async refresh() {
if (watching === null) return;
@@ -1,17 +1,39 @@
/* =============================================================================
* B05_Profile_UI_RouteEdit_Cross_Draw.ts
* 횡단 한 장을 캔버스에 그린다 — **원지반선·기본 계획 횡단선**과 아래 한 줄 요약.
* 횡단 한 장을 **SVG 선**으로 짓는다 — 원지반선·기본 계획 횡단선과 아래 한 줄 요약.
*
* `B05_Profile_UI_RouteEdit_Cross.ts` 에서 떼어낸 조각이다(2026-09-12, 700줄 규정).
* 값은 서버가 B05·B06 정본으로 낸 것을 그대로 그린다 — 여기서 기하를 만들지 않는다.
*
* **왜 캔버스가 아니라 SVG 인가**(2026-09-12 사용자 지시 ③) — 캔버스는 한 번 찍으면 그림이라
* 확대하면 뭉개진다. SVG 는 선이 그대로 남아 휠로 키워도 또렷하다. 확대·이동은 **공용
* 조각**(`A00_Common/b_svg_zoom_pan.ts`)이 맡는다 — B06 횡단 카드와 한 코드다.
*
* **계획 횡단선은 절·성토 교차점까지만 그린다**(2026-09-12 사용자 지시 ②). 서버가 주는
* `design_line` 은 계산 반폭 끝까지 이어지는데 **사면 끝 바깥은 손대지 않는 원지반**이라,
* 그대로 그으면 지반선과 겹쳐 같은 지형이 두 번 그려진다. B06 횡단 카드는 2026-08-20 에
* 같은 지적을 받아 이미 이렇게 끊고 있다(`appendCrossDesignOverlay`) — 교차점 찾기도
* 그쪽이 쓰는 `toeOffsets` 를 그대로 부른다.
* ========================================================================== */
import { svgElement, svgText } from "@util/common_util_svg";
import type { ContentBounds } from "../A00_Common/b_svg_zoom_pan";
import type { CrossSection } from "./../B06_Section/B06_Section_Api_Fetch";
import { fillSlopeLengths } from "./../B06_Section/B06_Section_UI_Cross_Fit";
import { fillSlopeLengths, toeOffsets } from "./../B06_Section/B06_Section_UI_Cross_Fit";
import type { CrossPreviewResponse } from "./B05_Profile_Api_Replan";
/** 그림 가장자리 여백(px). */
const PAD = 24;
/** 그림 가장자리 여백(px) — 위는 범례 한 줄, 아래는 눈금 안내 한 줄 몫이다. */
export const CROSS_PLOT_PAD = { left: 10, right: 10, top: 20, bottom: 20 };
/** 지은 한 판 — 확대·이동은 `layer` 에만 걸린다(범례·안내 글자는 제자리). */
export interface CrossPlot {
svg: SVGSVGElement;
layer: SVGGElement;
width: number;
height: number;
/** 실제로 선이 놓인 범위 — 확대했을 때 팬이 갈 수 있는 데까지. */
content: ContentBounds;
}
/** 성토사면 길이·절성토 면적 한 줄. */
export function summarizeCross(preview: CrossPreviewResponse): string {
@@ -19,10 +41,7 @@ export function summarizeCross(preview: CrossPreviewResponse): string {
if (!design) return "계획고를 못 세워 계획 횡단을 그리지 못했습니다.";
// 성토사면 길이는 **B06 화면이 쓰는 그 함수**를 그대로 부른다 — 두 화면이 다른 길이를
// 말하면 안 된다. 필요한 것은 `samples` 와 `design` 둘뿐이라 그만 담아 넘긴다.
const lengths = fillSlopeLengths({
samples: preview.samples,
design,
} as unknown as CrossSection);
const lengths = fillSlopeLengths(asSection(preview));
const sides = (["left", "right"] as const)
.filter((side) => lengths[side] !== null)
.map((side) => {
@@ -34,27 +53,81 @@ export function summarizeCross(preview: CrossPreviewResponse): string {
return `${slope} · 절토 ${design.cut_area_m2.toFixed(2)}㎡ · 성토 ${design.fill_area_m2.toFixed(2)}`;
}
/** B06 셈 함수가 보는 꼴로 맞춘다 — 그 둘은 `samples` 와 `design` 만 읽는다. */
function asSection(preview: CrossPreviewResponse): CrossSection {
return { samples: preview.samples, design: preview.design } as unknown as CrossSection;
}
type Point = [number, number];
/**
* 원지반선과 기본 계획 횡단선을 한 판에 그린다. 좌(+offset)가 화면 왼쪽이다
* 선을 **절·성토 교차점 사이만** 남긴다(2026-09-12 사용자 지시 ②).
*
* 교차점이 꼭짓점 사이에 떨어지면 그 자리에 점을 하나 만들어 **딱 거기서 끊는다** — 가까운
* 꼭짓점에서 끊으면 사면 끝이 지반에 닿기 전에 멈춘 것처럼 보인다.
* 그 측에서 교차점을 못 찾으면(사면이 지반을 못 만난 측점) 그쪽은 안 자른다.
*/
function clipToToes(line: Point[], low: number, high: number): Point[] {
if (line.length < 2) return line;
const inside = (point: Point): boolean => point[0] >= low - 1e-9 && point[0] <= high + 1e-9;
const cutAt = (from: Point, to: Point, edge: number): Point => {
const span = to[0] - from[0];
const ratio = Math.abs(span) <= 1e-9 ? 0 : (edge - from[0]) / span;
return [edge, from[1] + (to[1] - from[1]) * ratio];
};
const out: Point[] = [];
line.forEach((point, index) => {
if (inside(point)) out.push(point);
const next = line[index + 1];
if (!next) return;
// 경계를 넘어가는 구간은 경계 자리에 점을 하나 세운다. 가까운 경계부터 넣어야 순서가 산다.
[low, high]
.filter((edge) => (point[0] - edge) * (next[0] - edge) < 0)
.sort((a, b) => Math.abs(a - point[0]) - Math.abs(b - point[0]))
.forEach((edge) => out.push(cutAt(point, next, edge)));
});
return out;
}
/**
* 원지반선과 기본 계획 횡단선을 한 판에 짓는다. 좌(+offset)가 화면 왼쪽이다
* (`generate_sections` cad_exchange 규약과 같은 방향).
*
* **가로·세로를 같은 배율로** 둔다 — 따로 늘리면 사면 기울기가 거짓으로 보인다. 횡단도는
* 기울기를 눈으로 읽는 그림이라 왜곡하면 안 된다(2026-09-12 실화면: 노면이 안 보였다).
*/
export function drawCross(
context: CanvasRenderingContext2D,
canvas: HTMLCanvasElement,
export function buildCrossSvg(
preview: CrossPreviewResponse,
): void {
width: number,
height: number,
): CrossPlot {
const svg = svgElement("svg", {
viewBox: `0 0 ${width} ${height}`,
width: "100%",
height: "100%",
preserveAspectRatio: "none",
class: "b05-routeedit__cross-svg",
});
const layer = svgElement("g", { class: "b05-routeedit__cross-plot" });
const ground = preview.samples
.filter((sample) => sample.valid && sample.elevation_m !== null)
.map((sample) => [Number(sample.offset_m), Number(sample.elevation_m)] as [number, number]);
const design = (preview.design?.design_line ?? []).map(
(point) => [point.offset_m, point.elevation_m] as [number, number],
.map((sample) => [Number(sample.offset_m), Number(sample.elevation_m)] as Point);
const toes = preview.design ? toeOffsets(asSection(preview)) : { left: null, right: null };
// 계획 횡단선은 사면 끝에서 끊는다 — 그 바깥은 원지반이고 지반선이 이미 그린다.
const design = clipToToes(
(preview.design?.design_line ?? []).map(
(point) => [point.offset_m, point.elevation_m] as Point,
),
toes.right ?? -Infinity, // offset = 우
toes.left ?? Infinity, // +offset = 좌
);
const all = [...ground, ...design];
context.clearRect(0, 0, canvas.width, canvas.height);
if (all.length < 2) return;
if (all.length < 2) {
svg.append(layer);
return { svg, layer, width, height, content: { x: 0, y: 0, width, height } };
}
const offsets = all.map((point) => point[0]);
const heights = all.map((point) => point[1]);
@@ -64,57 +137,80 @@ export function drawCross(
const maxZ = Math.max(...heights);
const spanX = maxOffset - minOffset || 1;
const spanZ = maxZ - minZ || 1;
const scale = Math.min((canvas.width - PAD * 2) / spanX, (canvas.height - PAD * 2) / spanZ);
const plotWidth = Math.max(width - CROSS_PLOT_PAD.left - CROSS_PLOT_PAD.right, 1);
const plotHeight = Math.max(height - CROSS_PLOT_PAD.top - CROSS_PLOT_PAD.bottom, 1);
const scale = Math.min(plotWidth / spanX, plotHeight / spanZ);
const centerOffset = (minOffset + maxOffset) / 2;
const centerZ = (minZ + maxZ) / 2;
const toScreen = (point: [number, number]): [number, number] => [
canvas.width / 2 + (centerOffset - point[0]) * scale,
canvas.height / 2 + (centerZ - point[1]) * scale,
const toScreen = (point: Point): Point => [
width / 2 + (centerOffset - point[0]) * scale,
height / 2 + (centerZ - point[1]) * scale,
];
const stroke = (points: Array<[number, number]>, color: string, width: number): void => {
const path = (points: Point[], className: string): void => {
if (points.length < 2) return;
context.beginPath();
points.forEach((point, index) => {
const [x, y] = toScreen(point);
if (index === 0) context.moveTo(x, y);
else context.lineTo(x, y);
});
context.strokeStyle = color;
context.lineWidth = width;
context.stroke();
layer.append(
svgElement("polyline", {
class: className,
points: points.map((point) => toScreen(point).join(",")).join(" "),
}),
);
};
// 중심선 — 어디가 노선 가운데인지 먼저 보이게.
const [centerX] = toScreen([0, centerZ]);
context.save();
context.setLineDash([4, 4]);
context.strokeStyle = "rgba(148,163,184,0.7)";
context.lineWidth = 1;
context.beginPath();
context.moveTo(centerX, PAD / 2);
context.lineTo(centerX, canvas.height - PAD / 2);
context.stroke();
context.restore();
stroke(ground, "#94a3b8", 1.6); // 원지반
stroke(design, "#f97316", 2.2); // 기본 계획 횡단
context.font = "11px system-ui, sans-serif";
context.textBaseline = "top";
context.fillStyle = "#94a3b8";
context.textAlign = "left";
context.fillText("원지반", PAD, 4);
context.fillStyle = "#f97316";
context.textAlign = "right";
context.fillText("기본 계획 횡단", canvas.width - PAD, 4);
context.fillStyle = "#94a3b8";
context.textAlign = "center";
context.textBaseline = "bottom";
context.fillText(
`${maxOffset.toFixed(0)}m ← 중심 → 우 ${Math.abs(minOffset).toFixed(0)}m` +
` · 표고 ${minZ.toFixed(1)}~${maxZ.toFixed(1)}m`,
canvas.width / 2,
canvas.height - 2,
layer.append(
svgElement("line", {
class: "b05-routeedit__cross-axis",
x1: centerX,
y1: CROSS_PLOT_PAD.top / 2,
x2: centerX,
y2: height - CROSS_PLOT_PAD.bottom / 2,
}),
);
path(ground, "b05-routeedit__cross-ground");
path(design, "b05-routeedit__cross-design");
// 범례·눈금 안내는 **확대해도 제자리**다 — 확대 묶음 밖에 둔다.
svg.append(layer);
svg.append(
svgText("원지반", {
class: "b05-routeedit__cross-key is-ground",
x: CROSS_PLOT_PAD.left,
y: 13,
"text-anchor": "start",
}),
svgText("기본 계획 횡단", {
class: "b05-routeedit__cross-key is-design",
x: width - CROSS_PLOT_PAD.right,
y: 13,
"text-anchor": "end",
}),
svgText(
`${maxOffset.toFixed(0)}m ← 중심 → 우 ${Math.abs(minOffset).toFixed(0)}m` +
` · 표고 ${minZ.toFixed(1)}~${maxZ.toFixed(1)}m`,
{
class: "b05-routeedit__cross-key",
x: width / 2,
y: height - 6,
"text-anchor": "middle",
},
),
);
const screens = all.map(toScreen);
const xs = screens.map((point) => point[0]);
const ys = screens.map((point) => point[1]);
return {
svg,
layer,
width,
height,
content: {
x: Math.min(...xs),
y: Math.min(...ys),
width: Math.max(...xs) - Math.min(...xs),
height: Math.max(...ys) - Math.min(...ys),
},
};
}
@@ -11,11 +11,15 @@
* ========================================================================== */
import type { EditedCurve, EditedNode, Vertex } from "./B05_Profile_UI_RouteEdit_Curve";
import type { CurveLock } from "./B05_Profile_UI_RouteEdit_Edits";
import {
curveOptionalAt,
deflectionRad,
radiusFloorM,
type CurveLock,
} from "./B05_Profile_UI_RouteEdit_Edits";
import {
centerDirectionOf,
createCurveLabel,
deflectionRad,
type CurveLabel,
} from "./B05_Profile_UI_RouteEdit_Label";
@@ -29,6 +33,8 @@ export interface CurveBarState {
curveRadius: Array<number | null>;
curveLock: CurveLock[];
curveArc: Array<number | null>;
/** 교각점 자리를 못 박은 꺾임점(2026-09-12 사용자 지시 ①). */
apexLock: boolean[];
/** 못 넘는 하한(m). 0이면 제한 없음(계획서 0-9 ④). */
limitRadiusM: number;
limitArcM: number;
@@ -93,6 +99,13 @@ export function createCurveBar(params: CurveBarParams): CurveBar {
: "고정을 풀었습니다.",
);
},
onApexLock: (locked) => {
const { picked, apexLock } = params.state();
if (picked < 0) return;
apexLock[picked] = locked;
// 자리는 안 바뀌었지만 잡히는 것이 달라졌다 — 상태줄과 되돌리기에 한 걸음으로 남긴다.
params.applyEdit(locked ? "교각점을 고정했습니다." : "교각점 고정을 풀었습니다.");
},
onCurveOn: (on) => {
const { picked, curveOn } = params.state();
if (picked < 0) return;
@@ -111,6 +124,7 @@ export function createCurveBar(params: CurveBarParams): CurveBar {
curveOn,
curveRadius,
curveLock,
apexLock,
limitRadiusM,
limitArcM,
} = params.state();
@@ -120,7 +134,9 @@ export function createCurveBar(params: CurveBarParams): CurveBar {
}
const pickedCurve = curveInfo.find((entry) => entry.node_first === picked);
const shown = curveRadius[picked] ?? pickedCurve?.radius_m ?? null;
const deflection = deflectionRad(nodeInfo[picked]?.inner_angle_deg);
const innerAngle = nodeInfo[picked]?.inner_angle_deg ?? null;
const optional = curveOptionalAt(innerAngle);
const deflection = deflectionRad(innerAngle);
const rect = params.canvas.getBoundingClientRect();
const [screenX, screenY] = params.toScreen(planned[picked]);
label.show({
@@ -146,9 +162,13 @@ export function createCurveBar(params: CurveBarParams): CurveBar {
radiusShown: shown,
arcLengthShown: shown === null || deflection <= 1e-9 ? null : shown * deflection,
lock: curveLock[picked] ?? null,
apexLocked: apexLock[picked] === true,
innerAngleDeg: nodeInfo[picked]?.inner_angle_deg ?? null,
limitRadiusM,
limitArcM,
curveOptional: optional,
// 칸이 막는 하한은 **그 자리 값**이다 — R 칸은 L 하한까지 환산해 함께 보고,
// 곡선을 생략해도 되는 자리(내각 155° 이상)는 L 하한을 안 건다(2026-09-12 확정).
limitRadiusM: radiusFloorM(innerAngle, limitRadiusM, limitArcM),
limitArcM: optional ? 0 : limitArcM,
});
}
+66 -15
View File
@@ -12,6 +12,12 @@
*
* ⚠ 접선 자리가 모자라면 그리기 단계에서 R 을 줄이는 것은 그대로다(`buildEditedPolyline`).
* 그것은 **그려지는 값**만 줄이고 잠가 둔 값은 안 건드린다 — 자리를 넓히면 되돌아온다.
*
* **하한은 R 과 L 둘 다 본다**(2026-09-12 사용자 확정 「둘 다 지켜야 함」). L = R·Δ 라
* 한 자리의 하한은 결국 **반지름 하나**로 모인다 — `radiusFloorM` 이 그 값을 낸다.
* 다만 **내각 155° 이상은 뺀다**(별표2 .2.다.(1) 「내각 155도 이상 되는 장소는 곡선을
* 설치하지 아니할 수 있다」). 거기서 L 을 채우라고 하면 교각이 0 에 가까워 R 이 수십 배로
* 부풀고, **아무것도 안 고쳤는데 노선이 휜다**(실측: 내각 174.8° 자리가 R 12m → 55m).
* ========================================================================== */
import {
@@ -20,11 +26,43 @@ import {
type EditedNode,
type Vertex,
} from "./B05_Profile_UI_RouteEdit_Curve";
import { deflectionRad } from "./B05_Profile_UI_RouteEdit_Label";
/** 무엇을 붙들고 있나. */
export type CurveLock = "radius" | "arc" | null;
/** 그 꺾임점의 **교각 Δ**(라디안) — 내각의 나머지. 곡선 길이 L = R·Δ 에 쓴다. */
export function deflectionRad(innerAngleDeg: number | null | undefined): number {
if (innerAngleDeg === null || innerAngleDeg === undefined) return 0;
return ((180 - innerAngleDeg) * Math.PI) / 180;
}
/** 곡선을 **안 둘 수 있는** 내각(도) — 별표2 .2.다.(1) · .3.라.(1). */
export const CURVE_OPTIONAL_INNER_ANGLE_DEG = 155;
/** 그 자리에서 곡선을 생략해도 되나 — 내각이 155° 이상이면 그렇다. */
export function curveOptionalAt(innerAngleDeg: number | null | undefined): boolean {
return innerAngleDeg !== null && innerAngleDeg !== undefined
? innerAngleDeg >= CURVE_OPTIONAL_INNER_ANGLE_DEG
: false;
}
/**
* 그 꺾임점의 **못 넘는 반지름 하한**(m) — R 하한과 L 하한을 함께 본 값.
*
* L = R·Δ 이므로 「L ≥ 하한」은 「R ≥ 하한/Δ」와 같다. 둘 중 큰 쪽이 그 자리의 하한이다.
* 내각 155° 이상(곡선 생략 가능)이거나 교각을 모르면 **R 하한만** 본다.
*/
export function radiusFloorM(
innerAngleDeg: number | null | undefined,
limitRadiusM: number,
limitArcM: number,
): number {
if (!(limitArcM > 0) || curveOptionalAt(innerAngleDeg)) return limitRadiusM;
const deflection = deflectionRad(innerAngleDeg);
if (!(deflection > 1e-9)) return limitRadiusM;
return Math.max(limitRadiusM, limitArcM / deflection);
}
/**
* **곡선 길이를 잠근 자리**의 반지름을 지금 교각에 맞춰 다시 잡는다(`curveRadius` 를 고침).
*
@@ -49,36 +87,37 @@ export function applyArcLocks(
}
/**
* **반지름 하한을 지키도록 지정값을 끌어올린다**(계획서 0-9 ④, 2026-09-12 사용자 확정).
* **하한(R·L)을 지키도록 지정값을 끌어올린다**(계획서 0-9 ④, 2026-09-12 사용자 확정).
*
* **비워 둔(자동) 자리는 건드리지 않는다** — 자동은 이미 기본 반지름을 쓰고 있고, 여기서
* 값을 적어 넣으면 아무것도 안 고쳤는데 「R 지정」이 늘어난다.
*
* ⚠ **곡선 길이(L) 하한은 여기서 안 건다.** L = R·Δ 라, 내각 179° 처럼 거의 곧은 자리는
* L 5m 를 채우려면 R 이 286m 로 부풀어 **아무것도 안 고쳤는데 노선이 바뀐다**(2026-09-12
* 실화면: 길이 1017.5m → 1017.2m). 별표2 도 내각 155° 이상은 곡선을 안 둘 수 있다고 한다.
* L 하한은 **사용자가 칸에 적을 때만** 막고(`_Label`), 패널에 「L ≥ 5m」으로 보이기만 한다.
* 하한은 `radiusFloorM` 이 R·L 을 함께 본 값이다 — 내각 155° 이상은 R 하한만 본다.
*/
export function applyCurveLimits(
planned: Vertex[],
curveOn: ReadonlyArray<boolean>,
curveRadius: Array<number | null>,
limitRadiusM: number,
limitArcM = 0,
): void {
if (limitRadiusM <= 0) return;
if (limitRadiusM <= 0 && limitArcM <= 0) return;
for (let seat = 1; seat < planned.length - 1; seat += 1) {
if (curveOn[seat] === false) continue;
const current = curveRadius[seat];
if (current === null || current === undefined) continue;
if (current < limitRadiusM) curveRadius[seat] = limitRadiusM;
const inner = innerAngleDeg(planned[seat - 1], planned[seat], planned[seat + 1]);
const floor = radiusFloorM(inner, limitRadiusM, limitArcM);
if (current < floor) curveRadius[seat] = floor;
}
}
/**
* 노드마다 **반지름 하한**을 얼마나 밑돌고 있나(m). 다 지키고 있으면 0.
* 노드마다 **하한(R·L)** 을 얼마나 밑돌고 있나(m). 다 지키고 있으면 0.
*
* 곡선 길이(L) 하한은 여기서도 안 본다 — 까닭은 `applyCurveLimits` 설명에 있다. 거의 곧은
* 자리에서 L 을 채우라고 막으면 **노드를 한 뼘도 못 옮긴다**.
* 재는 자는 `radiusFloorM` 이다 — L 하한도 반지름으로 환산해 함께 본다. 거의 곧은 자리
* (내각 155° 이상)는 곡선을 안 둬도 되는 자리라 R 하한만 본다. 그 예외가 없으면 그런
* 자리에서 **노드를 한 뼘도 못 옮긴다**.
*
* 노드를 옮기면 접선 자리가 모자라 그리기 단계에서 R 이 눌릴 수 있다. 그 눌림까지 막으려면
* 옮기기 자체를 되돌려야 하므로, **옮기기 전보다 나빠진 자리가 있는지**만 견준다 — 이미
@@ -88,10 +127,16 @@ export function applyCurveLimits(
* 무너져도 최댓값이 안 움직여** 그냥 통과한다(실화면에서 「기준 미달 1곳 → 2곳」이 그대로
* 지나갔다). 자리마다 따로 견준다.
*/
export function curveShortfalls(nodes: ReadonlyArray<EditedNode>, limitRadiusM: number): number[] {
export function curveShortfalls(
nodes: ReadonlyArray<EditedNode>,
limitRadiusM: number,
limitArcM = 0,
): number[] {
return nodes.map((node) => {
if (node.radius_m === null || limitRadiusM <= 0) return 0;
return Math.max(0, limitRadiusM - node.radius_m);
if (node.radius_m === null) return 0;
const floor = radiusFloorM(node.inner_angle_deg, limitRadiusM, limitArcM);
if (floor <= 0) return 0;
return Math.max(0, floor - node.radius_m);
});
}
@@ -114,6 +159,8 @@ export interface CurveSummaryInput {
curveOn: boolean[];
curveRadius: Array<number | null>;
curveLock: CurveLock[];
/** 교각점을 못 박은 자리 — 「고정 N곳」에 함께 센다(2026-09-12 사용자 지시 ①). */
apexLock: boolean[];
/** 그려 낸 곡선 수 — 아직 안 그렸으면 0. */
curveCount: number;
/** 법정 기준을 못 맞춘 자리 수. */
@@ -129,7 +176,8 @@ export function curveSummary(input: CurveSummaryInput): string {
(on, index) => !on && index > 0 && index < input.nodeCount - 1,
).length;
const forced = input.curveRadius.filter((value) => value !== null).length;
const locked = input.curveLock.filter((lock) => lock !== null).length;
const locked =
input.curveLock.filter((lock) => lock !== null).length + input.apexLock.filter(Boolean).length;
const edits = [
off ? `곡선 지움 ${off}` : "",
forced ? `R 지정 ${forced}` : "",
@@ -157,6 +205,7 @@ export interface FlattenedPlan {
curveRadius: Array<number | null>;
curveLock: CurveLock[];
curveArc: Array<number | null>;
apexLock: boolean[];
}
interface ServerNode {
@@ -198,6 +247,7 @@ export function flattenServerPlan(nodes: ServerNode[], curves: EditedCurve[]): F
curveRadius: [],
curveLock: [],
curveArc: [],
apexLock: [],
};
nodes.forEach((node, index) => {
if (dropped.has(index)) return;
@@ -215,6 +265,7 @@ export function flattenServerPlan(nodes: ServerNode[], curves: EditedCurve[]): F
out.curveRadius.push(curve ? Math.round(curve.radius_m * 100) / 100 : null);
out.curveLock.push(null);
out.curveArc.push(null);
out.apexLock.push(false);
if (curve) out.curves.push({ ...curve, node_first: seat, node_last: seat });
});
return out;
@@ -21,6 +21,8 @@ export interface RouteEditSnapshot {
curveLock: Array<"radius" | "arc" | null>;
/** 길이를 붙들었을 때의 그 길이(m). */
curveArc: Array<number | null>;
/** 교각점(꺾임점) 자리를 못 박았나 — 켜진 자리는 끌어도 안 움직인다(2026-09-12 지시 ①). */
apexLock: boolean[];
picked: number;
}
@@ -50,6 +52,7 @@ function clone(snapshot: RouteEditSnapshot): RouteEditSnapshot {
curveRadius: [...snapshot.curveRadius],
curveLock: [...snapshot.curveLock],
curveArc: [...snapshot.curveArc],
apexLock: [...snapshot.apexLock],
picked: snapshot.picked,
};
}
@@ -64,6 +67,7 @@ function sameRoute(a: RouteEditSnapshot, b: RouteEditSnapshot): boolean {
if (a.curveRadius[index] !== b.curveRadius[index]) return false;
if (a.curveLock[index] !== b.curveLock[index]) return false;
if (a.curveArc[index] !== b.curveArc[index]) return false;
if (a.apexLock[index] !== b.apexLock[index]) return false;
}
return true;
}
+26 -7
View File
@@ -20,13 +20,11 @@
* 다른 꺾임점을 고르면 자동 자리로 돌아간다.
* ========================================================================== */
import type { CurveLock } from "./B05_Profile_UI_RouteEdit_Edits";
import { CURVE_OPTIONAL_INNER_ANGLE_DEG, type CurveLock } from "./B05_Profile_UI_RouteEdit_Edits";
/** 그 꺾임점의 **교각 Δ**(라디안) — 내각의 나머지. 곡선 길이 L = R·Δ 에 쓴다. */
export function deflectionRad(innerAngleDeg: number | null | undefined): number {
if (innerAngleDeg === null || innerAngleDeg === undefined) return 0;
return ((180 - innerAngleDeg) * Math.PI) / 180;
}
// `deflectionRad` 는 화면을 안 만지는 셈이라 `_Edits.ts` 로 옮겼다(2026-09-12) — 순수
// 함수가 DOM 파일에 얹혀 있으면 시험이 화면째 들여와야 한다. 재수출로 부르던 자리를 지킨다.
export { deflectionRad } from "./B05_Profile_UI_RouteEdit_Edits";
/** 곡선 중심이 있는 **화면 방향**(단위벡터) — 패널을 그 반대쪽에 붙이는 데 쓴다.
*
@@ -70,6 +68,10 @@ export interface CurveLabelState {
/** 곡선 길이(m) = R·Δ. 곡선이 없으면 null. */
arcLengthShown: number | null;
lock: CurveLock;
/** 교각점(이 꺾임점) 자리를 못 박았나 — 켜면 끌어서 못 옮긴다(2026-09-12 사용자 지시 ①). */
apexLocked: boolean;
/** 내각 155° 이상이라 **곡선을 안 둬도 되는** 자리인가(별표2 .2.다.(1)). */
curveOptional: boolean;
innerAngleDeg: number | null;
/** **못 넘는** 반지름·곡선 길이 하한(m). 0이면 제한 없음(계획서 0-9 ④). */
limitRadiusM?: number;
@@ -84,6 +86,8 @@ export interface CurveLabelHandlers {
onCurveOn: (on: boolean) => void;
/** 무엇을 붙들지 바꿨다 — 같은 것을 다시 누르면 null(품). */
onLock: (lock: CurveLock) => void;
/** 교각점 자리를 못 박거나 푼다. */
onApexLock: (locked: boolean) => void;
}
export interface CurveLabel {
@@ -131,6 +135,10 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
<button type="button" class="b05-routeedit__lock" data-act="lock-arc"
title="곡선 길이 고정 — 노드를 옮겨도 안 바뀝니다">고정</button>
</label>
<div class="b05-routeedit__curve-field">교각점
<button type="button" class="b05-routeedit__lock" data-act="lock-apex"
title="교각점 고정 — 이 꺾임점의 자리를 못 박습니다. 노드도 손잡이도 안 끌리고 반지름·곡선 길이 칸으로만 바뀝니다">고정</button>
</div>
<span class="b05-routeedit__curve-info"></span>`;
document.body.append(root);
@@ -141,6 +149,7 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
const arc = root.querySelector<HTMLInputElement>(".b05-routeedit__curve-arc")!;
const lockRadius = root.querySelector<HTMLButtonElement>('[data-act="lock-radius"]')!;
const lockArc = root.querySelector<HTMLButtonElement>('[data-act="lock-arc"]')!;
const lockApex = root.querySelector<HTMLButtonElement>('[data-act="lock-apex"]')!;
const info = root.querySelector<HTMLElement>(".b05-routeedit__curve-info")!;
// 패널 위에서 누른 것이 캔버스로 새어 나가면 노드가 딸려 움직인다.
@@ -150,6 +159,7 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
let curveOn = true;
let lock: CurveLock = null;
let apexLocked = false;
let seat = -1;
/** 손으로 옮긴 자리 — 꺾임점 기준 어긋남(px). 다른 꺾임점을 고르면 지운다. */
let manual: [number, number] | null = null;
@@ -179,6 +189,8 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
.addEventListener("click", () => handlers.onClose());
lockRadius.addEventListener("click", () => handlers.onLock(lock === "radius" ? null : "radius"));
lockArc.addEventListener("click", () => handlers.onLock(lock === "arc" ? null : "arc"));
// 교각점 고정은 **곡선 유무와 무관**하다 — 곡선을 지운 자리도 꺾임점 자리는 못 박을 수 있다.
lockApex.addEventListener("click", () => handlers.onApexLock(!apexLocked));
// ── 머리를 잡아 옮기기 — 곡선을 가리면 손으로 치울 수 있어야 한다 ──
let dragFrom: { x: number; y: number; left: number; top: number } | null = null;
@@ -260,6 +272,7 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
}
curveOn = state.curveOn;
lock = state.lock;
apexLocked = state.apexLocked;
limit = state.bounds;
limitRadius = state.limitRadiusM ?? 0;
limitArc = state.limitArcM ?? 0;
@@ -275,6 +288,7 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
lockArc.disabled = !state.curveOn;
lockRadius.classList.toggle("is-on", lock === "radius");
lockArc.classList.toggle("is-on", lock === "arc");
lockApex.classList.toggle("is-on", apexLocked);
radius.value =
state.radiusShown === null ? "" : String(Math.round(state.radiusShown * 10) / 10);
arc.value =
@@ -282,9 +296,14 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
const inner = state.innerAngleDeg;
// 하단에는 **내각만** 남긴다(2026-09-12 사용자 지시 ㉗) — 고정 여부는 단추 색으로,
// 하한은 칸이 이미 막으므로 글로 또 적을 까닭이 없다.
// 내각 155° 이상은 법이 곡선을 안 둬도 된다고 한 자리다 — L 하한을 안 거는 까닭이
// 여기서 보여야 한다(2026-09-12 사용자 확정).
info.textContent = state.curveOn
? inner
? `내각 ${Math.round(inner)}°`
? `내각 ${Math.round(inner)}°` +
(state.curveOptional
? ` · ${CURVE_OPTIONAL_INNER_ANGLE_DEG}° 이상 — 곡선 생략 가능`
: "")
: ""
: "곡선 없음 — 직선이 그대로 꺾입니다";
place(state);
+36 -13
View File
@@ -160,20 +160,8 @@ export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: Rou
context.strokeStyle = style.getPropertyValue("--map-flow-arrow") || "#7c3aed";
context.lineWidth = 2.6;
drawPreparedFeature(context, scene.contours.layer, scene.pickedContour, view);
drawPickedContourLabel(context, scene, view);
}
// 등고 높이값 — 확대가 클수록 촘촘히 낸다(계획서 0-9 ③).
context.font = "10px system-ui, sans-serif";
context.textAlign = "center";
context.textBaseline = "middle";
drawPreparedLabels(
context,
scene.contours.layer,
view,
style.getPropertyValue("--map-sheet-contour") || "#a5b4fc",
everyM,
scene.uprightRad,
);
// 높이값 라벨은 여기서 안 낸다 — 노선·눈금 **뒤에** 그려야 안 묻힌다(맨 아래 참고).
}
context.restore();
@@ -254,9 +242,44 @@ export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: Rou
drawStationMarks(context, scene, line);
drawMeasureMarks(context, scene, line);
// 등고 높이값은 **맨 나중에** 얹는다(2026-09-12 사용자 지시 ⑦) — 노선·측점 눈금보다 먼저
// 그리면 숫자가 그 아래 깔려 안 읽힌다. 띠(300m)는 다시 씌워 그리는 범위는 그대로 둔다.
drawContourLabels(context, scene, band, style);
context.restore(); // 회전 끝
}
/** 등고 높이값 라벨 — **그림 맨 위**에 얹는다(2026-09-12 사용자 지시 ⑦).
*
* 고른 가닥의 큰 이름표도 여기서 낸다. 줄과 라벨은 **같은 눈금**(`contourStepM`)으로 솎아
* 그린 줄에만 숫자가 붙는다 — 촘촘함은 부르는 쪽이 그 눈금으로 정한다. */
function drawContourLabels(
context: CanvasRenderingContext2D,
scene: RouteEditScene,
band: { x: number; y: number; width: number; height: number } | null,
style: CSSStyleDeclaration,
): void {
if (!scene.contours) return;
context.save();
if (band) {
context.beginPath();
context.rect(band.x, band.y, band.width, band.height);
context.clip();
}
context.font = "10px system-ui, sans-serif";
context.textAlign = "center";
context.textBaseline = "middle";
drawPreparedLabels(
context,
scene.contours.layer,
scene.view,
style.getPropertyValue("--map-sheet-contour") || "#a5b4fc",
scene.contourStepM,
scene.uprightRad,
);
if (scene.pickedContour >= 0) drawPickedContourLabel(context, scene, scene.view);
context.restore();
}
/** 구간 재기로 찍은 자리 — a·b 를 동그라미로 찍고 그 사이 노선을 굵게 덧그린다(계획서 0-9 ⑤). */
function drawMeasureMarks(
context: CanvasRenderingContext2D,
+53 -2
View File
@@ -190,15 +190,58 @@
font-size: var(--text-caption);
}
.b05-routeedit__cross-canvas {
/* 횡단 판 — 캔버스 그림이 아니라 **SVG 선**이다(2026-09-12 사용자 지시 ③).
휠 = 확대·축소, 가운데 버튼 끌기 = 이동, 더블클릭 = 원복(공용 조각이 맡음). */
.b05-routeedit__cross-box {
flex: 1 1 auto;
min-height: 0;
min-height: 150px;
overflow: hidden;
width: 100%;
border: 1px solid var(--color-border);
border-radius: var(--radius-8, 6px);
background: var(--color-surface);
}
.b05-routeedit__cross-svg {
display: block;
width: 100%;
height: 100%;
cursor: default;
}
.b05-routeedit__cross-svg.is-panning {
cursor: grabbing;
}
.b05-routeedit__cross-axis {
stroke: rgb(148 163 184 / 70%);
stroke-width: 1;
stroke-dasharray: 4 4;
}
.b05-routeedit__cross-ground {
fill: none;
stroke: var(--color-text-secondary, #94a3b8);
stroke-width: 1.6;
vector-effect: non-scaling-stroke;
}
.b05-routeedit__cross-design {
fill: none;
stroke: var(--map-route, #f97316);
stroke-width: 2.2;
vector-effect: non-scaling-stroke;
}
.b05-routeedit__cross-key {
fill: var(--color-text-secondary, #94a3b8);
font-size: 11px;
}
.b05-routeedit__cross-key.is-design {
fill: var(--map-route, #f97316);
}
.b05-routeedit__cross-foot {
flex: none;
color: var(--color-text-secondary);
@@ -381,3 +424,11 @@
color: var(--color-text-secondary);
line-height: 1.35;
}
/* 「거리 재기」가 켜진 동안 — 단추가 눌린 꼴로 남는다(2026-09-12 사용자 지시 ⑥).
`is-active` 는 예전에도 붙고 있었으나 꼴이 없어 겉모습이 그대로였다. */
.b05-routeedit__actions .ui-btn.is-active {
border-color: transparent;
background: var(--color-primary, #7c3aed);
color: #fff;
}
+32
View File
@@ -58,6 +58,38 @@ export function toeFitHalfWidth(section: CrossSection): number | null {
return Math.min(Math.ceil(extent + 1), MAX_FIT_HALF_WIDTH_M);
}
/**
* 절·성토 사면이 원지반과 **실제로 만나는** 좌·우 offset(m). 그 측에서 못 만나면 null.
*
* `toeFitHalfWidth` 와 같은 교차점을 쓰되 **좌우를 따로** 낸다(2026-09-12 사용자 지시 ②) —
* 반폭 하나로 자르면 한쪽만 성토인 측점에서 반대쪽 원지반이 필요보다 넓게 남는다.
* **외삽으로 짚은 자리는 안 쓴다**(null) — 지반을 모르는 자리라 거기까지 그으면 없는 지반을
* 그리는 셈이 된다.
*/
export function toeOffsets(section: CrossSection): { left: number | null; right: number | null } {
const out: { left: number | null; right: number | null } = { left: null, right: null };
const design = section.design;
if (!design) return out;
const groundAt = groundInterpolator(section.samples);
const designAt = designInterpolator(design.design_line);
const edges = design.road_edges;
if (!groundAt || !designAt || !edges) return out;
const lineOffsets = design.design_line.map((point) => point.offset_m);
if (lineOffsets.length < 2) return out;
const { protectMax, protectMin } = protectedSpan(design, edges);
for (const side of ["left", "right"] as const) {
const outward = side === "left" ? 1 : -1;
const start = side === "left" ? protectMax : protectMin;
const limit = side === "left" ? Math.max(...lineOffsets) : Math.min(...lineOffsets);
const meet = meetOffset(designAt, groundAt, start, limit, outward);
// 설계선 끝 밖으로 나간 값은 외삽이다 — 만난 것으로 치지 않는다.
if (Math.abs(meet) > Math.abs(limit) + 1e-9) continue;
if (Math.abs(designAt(meet) - groundAt(meet)) > MEET_TOLERANCE_M) continue;
out[side] = meet;
}
return out;
}
/**
* 성토측 사면의 **본래** 경사길이(m) — 성토가 아닌 측은 null (2026-09-03 사용자 지시).
*
+26 -159
View File
@@ -1,24 +1,23 @@
/* =============================================================================
* B06_Section_UI_Cross_View_Zoom.ts
* 횡단 카드 그래프의 **확대·축소·팬** 조작구. 카드 렌더러(`_UI_Cross_View.ts`)에서
* 700줄 제한으로 분리했다. 동작 규칙(휠은 줌이 아님)은 아래 주석 그대로다.
* 횡단 카드 그래프의 **확대·축소·팬** 조작구.
*
* ⚠ 줌·팬 **본문은 공용 조각**(`A00_Common/b_svg_zoom_pan.ts`)으로 옮겼다
* (2026-09-12 사용자 지시 「공용화해줘」) — 계획노선 편집 횡단이 같은 코드를 쓴다.
* 여기에는 **B06 것**만 남는다: 카드 여백(`CROSS_PAD`)을 씌운 감싸개, 측점별 배율 기억,
* 우측 상단 버튼 묶음. 동작 규칙(휠은 줌이 아님)은 그대로다 — 카드가 수십 장 깔리는
* 화면에서 휠을 줌에 묶으면 목록을 훑을 수가 없다(2026-08-02 사용자 확정).
* ========================================================================== */
import {
attachSvgZoomPan,
type ContentBounds,
type ZoomPanHandle,
type ZoomPanState,
} from "../A00_Common/b_svg_zoom_pan";
import { CROSS_PAD, L } from "./B06_Section_UI_Section_Common";
/**
* 횡단 카드 줌·팬 제어. 반환한 `zoom()`을 그래프 우측 상단 버튼이 부른다.
*
* **휠은 줌이 아니다**(2026-08-02 사용자 확정). 카드가 수십 장 깔리는 화면에서 휠을 줌에
* 묶으면 목록을 훑을 수가 없다. 확대·축소는 버튼, 팬은 가운데 버튼 드래그, 원복은 더블클릭.
*/
export interface ZoomPanHandle {
/** 1보다 크면 확대, 작으면 축소. 플롯 영역의 중앙을 붙잡는다. */
zoom: (factor: number) => void;
reset: () => void;
/** 현재 배율 — 원배율(1)일 때는 버튼이 배율 대신 **표시 반폭**을 조절한다. */
scale: () => number;
}
export type { ContentBounds, ZoomPanHandle, ZoomPanState };
/**
* 카드 표시 반폭 조작(2026-08-23 사용자 지시). 카드 하단 ◀/▶/↺을 없애고 우측 상단
@@ -34,28 +33,10 @@ export interface CrossWidthActions {
fit: () => void;
}
/** 줌·팬 상태 — 카드가 다시 그려질 때 배율을 되살리는 데 쓴다(2026-08-22 사용자 ③). */
export interface ZoomPanState {
scale: number;
tx: number;
ty: number;
}
/**
* 그려 둔 도형이 실제로 차지하는 범위(원배율 SVG 좌표). 표시 반폭 밖에 있어 잘려 있는
* **캐시 보유분**까지 포함한다 — 확대 상태의 팬은 이 범위까지 갈 수 있다
* (2026-08-23 사용자 지시).
*/
export interface ContentBounds {
x: number;
y: number;
width: number;
height: number;
}
/** 측점별 줌·팬 상태 — 카드를 다시 만들어도 배율이 살아남는다(2026-08-22 사용자 ③). */
export const cardZoomStates = new Map<string, ZoomPanState>();
/** 카드 SVG 에 줌·팬을 붙인다 — 여백만 씌워 공용 조각에 넘긴다. 부르는 꼴은 그대로다. */
export function attachZoomPan(
svg: SVGSVGElement,
plotLayer: SVGGElement,
@@ -68,132 +49,18 @@ export function attachZoomPan(
/** 도형이 실제로 그려진 범위(캐시 보유 폭). 확대 상태에서 팬 한계가 여기까지 늘어난다. */
content?: ContentBounds,
): ZoomPanHandle {
// 플롯 영역(축 안쪽) — 확대·축소의 중심이자 이동 한계의 기준이다.
const plot = {
x: CROSS_PAD.left,
y: CROSS_PAD.top,
width: Math.max(widthPx - CROSS_PAD.left - CROSS_PAD.right, 1),
height: Math.max(heightPx - CROSS_PAD.top - CROSS_PAD.bottom, 1),
};
const maxScale = 8;
let scale = initial?.scale ?? 1;
let tx = initial?.tx ?? 0;
let ty = initial?.ty ?? 0;
const applyTransform = (): void => {
plotLayer.setAttribute("transform", `translate(${tx} ${ty}) scale(${scale})`);
onChange?.({ scale, tx, ty });
};
// 확대한 도형이 플롯 영역을 항상 덮게 이동량을 가둔다 — 원배율에서는 이동량이 0으로 묶인다.
// **확대 상태**에서는 한계가 그려진 도형 전체(표시 반폭 밖 캐시 보유분 포함)까지 늘어나,
// 가운데 버튼 팬으로 잘려 있던 지반·설계선을 끌어다 볼 수 있다(2026-08-23 사용자 지시).
const clampAxis = (
value: number,
start: number,
size: number,
from: number,
to: number,
): number =>
Math.min(
Math.max(value, Math.min(start + size - scale * to, start - scale * from)),
Math.max(start + size - scale * to, start - scale * from),
);
const clampPan = (): void => {
// 도형 범위를 모르면 플롯 영역 자신이 한계다(기존 규칙). 알면 **원배율에서도** 그
// 범위까지 열어 둔다 — 캐시 보유분이 표시 반폭보다 넓으면 1배에서도 끌어다 봐야 한다
// (2026-08-23 사용자 재보고: 확대해야만 움직이는 줄 모르고 안 된다고 판단).
// 캐시가 표시 폭과 같으면 범위가 플롯과 같아져 종전처럼 이동량 0으로 묶인다.
const bounds = content
? {
x: Math.min(content.x, plot.x),
y: Math.min(content.y, plot.y),
right: Math.max(content.x + content.width, plot.x + plot.width),
bottom: Math.max(content.y + content.height, plot.y + plot.height),
}
: { x: plot.x, y: plot.y, right: plot.x + plot.width, bottom: plot.y + plot.height };
tx = clampAxis(tx, plot.x, plot.width, bounds.x, bounds.right);
ty = clampAxis(ty, plot.y, plot.height, bounds.y, bounds.bottom);
};
// 보이는 **플롯 영역의 중앙**을 붙잡고 확대·축소한다 — 버튼에는 마우스 위치가 없다.
const zoom = (factor: number): void => {
const next = Math.min(maxScale, Math.max(1, scale * factor));
const cx = plot.x + plot.width / 2;
const cy = plot.y + plot.height / 2;
tx = cx - ((cx - tx) / scale) * next;
ty = cy - ((cy - ty) / scale) * next;
scale = next;
clampPan();
applyTransform();
};
let panning = false;
let moved = false;
let lastX = 0;
let lastY = 0;
// 가운데 버튼을 누르면 브라우저가 자동 스크롤(가운데 클릭 스크롤)을 켠다 — `mousedown`
// 기본동작이라 `pointerdown`에서는 못 막는다. 여기서 막아야 팬만 남는다(2026-08-02 사용자 지시).
svg.addEventListener("mousedown", (event) => {
if (event.button === 1) event.preventDefault();
return attachSvgZoomPan({
svg,
layer: plotLayer,
widthPx,
heightPx,
pad: CROSS_PAD,
initial,
onChange,
content,
// 휠은 카드 목록을 훑는 몫이다 — 줌에 안 묶는다.
wheelZoom: false,
});
svg.addEventListener("auxclick", (event) => {
if (event.button === 1) event.preventDefault();
});
svg.addEventListener("pointerdown", (event) => {
// 팬은 **가운데 버튼**만. 좌클릭은 측점 선택·면적 강조 몫이다.
if (event.button !== 1) return;
event.preventDefault();
panning = true;
moved = false;
lastX = event.clientX;
lastY = event.clientY;
svg.classList.add("is-panning");
svg.setPointerCapture(event.pointerId);
});
svg.addEventListener("pointermove", (event) => {
if (!panning) return;
if (Math.abs(event.clientX - lastX) + Math.abs(event.clientY - lastY) > 2) moved = true;
// 도형을 직접 미는 방식이라 커서를 따라간다(viewBox를 밀던 때와 부호가 반대다).
const rect = svg.getBoundingClientRect();
tx += ((event.clientX - lastX) / rect.width) * widthPx;
ty += ((event.clientY - lastY) / rect.height) * heightPx;
lastX = event.clientX;
lastY = event.clientY;
clampPan();
applyTransform();
});
const endPan = (event: PointerEvent): void => {
if (!panning) return;
panning = false;
svg.classList.remove("is-panning");
try {
svg.releasePointerCapture(event.pointerId);
} catch {
/* 이미 해제됨 */
}
};
if (initial && (scale !== 1 || tx !== 0 || ty !== 0)) {
clampPan();
applyTransform();
}
svg.addEventListener("pointerup", endPan);
svg.addEventListener("pointercancel", endPan);
// 드래그(팬)로 끝난 클릭은 카드 선택으로 전파하지 않는다.
svg.addEventListener("click", (event) => {
if (moved) event.stopPropagation();
});
const currentScale = (): number => scale;
const reset = (): void => {
scale = 1;
tx = 0;
ty = 0;
applyTransform();
};
// 더블클릭 원복.
svg.addEventListener("dblclick", (event) => {
event.stopPropagation();
reset();
});
return { zoom, reset, scale: currentScale };
}
/**
+31 -6
View File
@@ -40,7 +40,7 @@ import {
readAlignmentDraft,
type ProfileEditStore,
} from "../B05_Profile/B05_Profile_UI_Profile_Edit";
import { readAlignment } from "../B05_Profile/B05_Profile_UI_Profile_Data";
import { readAlignment, toDesignProfile } from "../B05_Profile/B05_Profile_UI_Profile_Data";
import {
adjustStation,
buildAlignment,
@@ -340,7 +340,9 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
detail: sectionDetail,
edits,
});
for (const chainageM of updated) sectionView.refreshCard(chainageM);
// 카드는 한꺼번에 갈아 끼운다 — 측점마다 `refreshCard` 를 부르면 그때마다 종단
// 그래프·유토곡선까지 다시 그려 측점 수만큼 화면이 멈췄다(2026-09-12).
sectionView.refreshCards(updated);
} catch (error) {
const detail = error instanceof Error ? ` ${error.message}` : "";
showToast(`${L("B06_Design_Failed")}${detail}`, "error");
@@ -498,6 +500,18 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
*/
let gradeStore: ProfileEditStore | null = null;
let gradeRouteId: number | null = null;
/** ▲▼ 길게 누르기는 초당 10회(`HOLD_INTERVAL_MS`) 들어온다 — 그보다 길게 잡아
* 누르는 동안은 선만 움직이고, 손을 뗀 뒤 재계산이 한 번 돈다. */
const GRADE_RECONCILE_DEBOUNCE_MS = 120;
let gradeReconcileTimer = 0;
const scheduleGradeReconcile = (): void => {
window.clearTimeout(gradeReconcileTimer);
gradeReconcileTimer = window.setTimeout(() => {
void reconcileStaleDesigns({ force: true }).then(() =>
sectionView.setGradeEdit(gradeEditFor),
);
}, GRADE_RECONCILE_DEBOUNCE_MS);
};
const gradeEditFor = (): ReturnType<
NonNullable<Parameters<typeof sectionView.setGradeEdit>[0]>
> => {
@@ -517,10 +531,21 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
stepM: alignment.policy.edit_step_m,
onStation: (chainageM, delta) => {
store.replace(adjustStation(base, store.edits(), chainageM, delta));
// 편집분은 세션 초안에 있으므로 재계산이 그것을 그대로 읽는다(강제로 한 번).
void reconcileStaleDesigns({ force: true }).then(() =>
sectionView.setGradeEdit(gradeEditFor),
);
// ① 선은 **그 자리에서** 움직인다 — 그래프가 읽는 계획선(`design_profiles`)만
// 갈아 끼우고 다시 그린다(재계산을 기다리면 누른 뒤 한참 뒤에 움직였다).
const detail = sectionDetail;
if (detail) {
detail.longitudinal.design_profiles = [
toDesignProfile(
buildAlignment(base, store.edits()),
detail.longitudinal.design_profiles?.[0],
),
];
}
sectionView.setGradeEdit(gradeEditFor);
// ② 전 측점 횡단 재계산·카드 갱신은 무겁다 — 마지막 한 번만(B05 프리뷰와 같은 규칙).
// 편집분은 세션 초안에 있으므로 재계산이 그것을 그대로 읽는다.
scheduleGradeReconcile();
},
};
};
+24 -4
View File
@@ -107,6 +107,8 @@ export interface SectionViewController {
) => void;
/** 측점 하나의 카드만 새로 만들어 교체한다 (전체 재렌더 없이 설계 변경 반영). */
refreshCard: (chainageM: number) => void;
/** 여러 측점을 한꺼번에 교체한다 — 상단 패널은 **마지막에 한 번만** 다시 그린다. */
refreshCards: (chainages: ReadonlyArray<number>) => void;
/** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음
* (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). */
focusStation: (stationId: string) => void;
@@ -601,18 +603,35 @@ export function createSectionView(
chartWrap.scrollLeft = keepScrollLeft;
};
const refreshCard = (chainageM: number): void => {
if (!currentDetail) return;
/** 카드 한 장만 갈아 끼운다(상단 패널은 안 건드린다). 못 찾으면 false. */
const rebuildCard = (chainageM: number): boolean => {
if (!currentDetail) return false;
const section = currentDetail.cross_sections.find(
(candidate) => Math.abs(candidate.chainage_m - chainageM) < 0.01,
);
if (!section) return;
if (!section) return false;
const existing = document.getElementById(`cross-${section.station_id}`);
// 단건 갱신은 draw에서 정해둔 행 높이를 재사용해 같은 행 카드와 높이를 유지한다.
if (existing)
existing.replaceWith(buildCrossCard(section, cachedRowHeight.get(section.station_id)));
return true;
};
const refreshCard = (chainageM: number): void => {
// 단면적이 바뀌면 유토곡선도 함께 흔들리므로 상단 패널만 다시 그린다(카드 전체 재렌더 없음).
drawPanel();
if (rebuildCard(chainageM)) drawPanel();
};
/**
* 여러 측점을 한 번에 갈아 끼운다 — **상단 패널은 끝에 한 번만** 다시 그린다.
*
* 측점마다 `refreshCard` 를 부르면 측점 수만큼 종단 그래프·유토곡선을 다시 그려,
* 계획선 ▲▼ 한 번에 화면이 몇 백 ms 씩 멈췄다(2026-09-12 B05 대비 느림 원인).
*/
const refreshCards = (chainages: ReadonlyArray<number>): void => {
let touched = false;
for (const chainageM of chainages) if (rebuildCard(chainageM)) touched = true;
if (touched) drawPanel();
};
const resizeObserver = new ResizeObserver(() => {
@@ -658,6 +677,7 @@ export function createSectionView(
if (renderWidth <= 0) requestAnimationFrame(() => resizeObserver.observe(root));
},
refreshCard,
refreshCards,
focusStation(stationId) {
if (selectedStationId !== stationId) selectStation(stationId, true);
else revealCard(stationId, "smooth");
+7 -4
View File
@@ -781,12 +781,13 @@ function buildQuantityBody(
};
// 등급색 토글 — ⚠ **사전이 왔을 때만** 만든다(개발환경). 배포 빌드에서는 단추 자체가 없다.
if (
// ⚠ 만들기만 하고 **붙이는 것은 탭 단추를 다 세운 뒤**이다 — 여기서 바로 붙이면
// 탭 줄 **맨 앞**에 서서 「토적표」 앞에 탭 하나가 더 있는 것처럼 보인다(2026-09-12 실측: x=8).
const provenanceToggle =
(table as unknown as { provenance?: unknown } | null)?.provenance ||
(material as unknown as { provenance?: unknown } | null)?.provenance
) {
tabs.append(createProvenanceToggle(body));
}
? createProvenanceToggle(body)
: null;
if (failed) {
body.append(tabs, message(L("B08_Quantity_Grid_Failed")));
@@ -863,6 +864,8 @@ function buildQuantityBody(
buttons.push(button);
tabs.append(button);
});
// 토글은 탭 줄 **끝**에 둔다 — 표를 고르는 단추가 아니라 보기 방식을 바꾸는 단추라 섞이면 안 된다.
if (provenanceToggle) tabs.append(provenanceToggle);
body.append(tabs, pane);
show(0);
@@ -0,0 +1,31 @@
---
type: completion-summary
status: draft
related_pages: ["[[B05_Profile/B05_route_edit_2026_09]]", "[[B06_Section/B06_profile_cross_unification_2026_09]]", "[[B07_DesignDetail/B07_standard_drawings_2026_09]]", "[[B08_Quantity/B08_provenance_ui_2026_09]]", "[[B09_Estimation/B09_provenance_ui_2026_09]]", "[[multi_environment_safety]]"]
last_updated: 2026-09-12
source: ["docs/raw/plans/2026-09-12_plan_checked_complete_items.md", "docs/raw/plans/2026-09-12b_plan_snapshot_before_checked_cleanup.md"]
---
# 2026-09-12 체크 완료 항목
## 이관 범위
- 계획노선 편집 모달 기능 30개 체크와 B05·B06 공용 편집 기반.
- 프로젝트 단위 업로드 잠금 표시와 삭제 전 확인 경로.
- B06 구조물 보류 결정, 측구 토글 실측, B07 벽 두께 자동식, 사토장 시험 예외 정리.
- B08·B09 좌측 패널 일원화와 값 출처·계산 근거 표시.
- 사용자 확정 5차 중 돌 무게 인자표와 공구손료 입력칸.
## 상태 경계
- 사용자가 위키 관리자의 2차 교차검증을 생략하도록 지시했다. 이 페이지는 체크 상태와 계획서의 자체검증 기록을 옮긴 `draft`다.
- 계획서 제목의 `검증 대기`, 본문의 `사용자 확인 대기`, `보류 확정`을 완료 검증으로 바꾸지 않았다.
- 혼합 섹션의 완료 체크 44개와 근거는 정리 전 스냅숏에 보존했고, 미완료 체크 39개와 후속 문맥만 상시 계획서에 유지했다.
## 자체검증 기록 요약
| 범위 | 계획서 기록 |
|---|---|
| 계획노선 편집 | ORCA 실제 화면, API 표고 측정, TypeScript·Ruff·Prettier와 전체 회귀 기록 |
| B08·B09 근거 표시 | 로그인한 실제 화면·실제 서버 응답에서 B08 1,914칸, B09 사전 16장과 패널 동작 확인 |
| 기타 완료 체크 | 각 항목의 커밋·시험·화면 실측 또는 보류 결정은 원문 아카이브에 보존 |
@@ -171,6 +171,10 @@
"169": "B08_DesignDetail_Engine_Cad_Basin.py",
"170": "B08_DesignDetail_Engine_Cad_MassHaul.py",
"171": "B08 CAD·납품 도면 후속",
"172": "B05 계획노선 편집 — 2026-09-12",
"173": "B06 종횡단 공용화 — 2026-09-12",
"174": "B08 수량 근거 표시 — 2026-09-12",
"175": "B09 원가 근거 표시 — 2026-09-12",
"176": "OpenWebCAD Core",
"177": "common_util_mass_haul_settle.ts",
"178": "Drainage Watershed (유역도)",
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,191 @@
{
"0": "UI Templates — Localization & Components",
"1": "인증 / RBAC",
"2": "A00_Common — App Shell Framework",
"3": "2026-09-04 완료 항목",
"4": "배수유역 해석 및 세부설계 (Drainage Watershed)",
"5": "DB: 로그/모니터링 테이블",
"6": "A09_Security — Backend",
"7": "2026-08-29 완료 반영",
"8": "DB: 파일/지표면분석 테이블",
"9": "DB: 경로/종횡단 테이블",
"10": "A04_NewsHistory — Frontend",
"11": "A07_Register — Frontend",
"12": "A08_Support — Frontend",
"13": "B02_ProjRegister — Frontend",
"14": "A01_Home — Frontend",
"15": "A05_EduDetail — Frontend",
"16": "A06_Login — Frontend",
"17": "A08_Support — Backend",
"18": "A09_Security — Frontend",
"19": "B02_ProjRegister — Backend",
"20": "B04_PreProcess — Backend",
"21": "B04_PreProcess — Frontend",
"22": "B05 구조물 정본·통합 편집",
"23": "B06_Section — Frontend",
"24": "A03_CompDetail — Frontend",
"25": "A06_Login — Backend",
"26": "유토곡선 (Mass Haul Diagram) 계산 명세",
"27": "A07_Register — Backend",
"28": "B06 배수관 구조물 조작·표시",
"29": "B06 기슭막이 연동·경사·단별 제어",
"30": "B08_DesignDetail — Backend",
"31": "B08 토적도·수리집수면적유역도",
"32": "B09_Estimation — Frontend",
"33": "현재 구현 현황 — 소스 읽기 감사",
"34": "DB: 구조물/수량/산출물 테이블",
"35": "B01_Dashboard — Backend",
"36": "B01_Dashboard — Frontend",
"37": "B03_FileInput — Backend",
"38": "B05 급선회 3D 국부 보정 계획",
"39": "B06 집수정·다단 기슭막이",
"40": "B06 배수관 구조물 기하·조작 체계",
"41": "B06 배수관 횡단도 세트",
"42": "B10_Payment — Frontend",
"43": "B11_Status — Frontend",
"44": "저장 경로 규칙 (Workflow-based Folder Structure)",
"45": "DB: 프로젝트 관리 테이블",
"46": "임도기술교본 원문 md 추출 품질 결함",
"47": "A01_Home — 세부 구현",
"48": "A02_ProgDetail — 세부 구현",
"49": "A02_ProgDetail — Frontend",
"50": "B01_Dashboard — API",
"51": "B03_FileInput — Frontend",
"52": "B05 계획노선 코리도 삼각망 서피스",
"53": "B05 구조물·UI 현재 계획",
"54": "B05 구조물 입력·종단 표시 정비 — 2026-08-19",
"55": "B07 구조물 표준도 — 조사·합의와 현재 통로",
"56": "B08 횡단도 구조물·장 배치",
"57": "2026-09-07 완료·보류 요약",
"58": "B07 외부 WebCAD 비교 실행환경",
"59": "Temp Upload (프로젝트 생성 전 임시 보관함)",
"60": "Q: 배수관 매설시 각도의 제약조건이 있는지 확인해줘. 임도에서",
"61": "Q: 임도 기술정보DB에서 집수정의 형태정보는 어떤게 있는지 확인해줘.",
"62": "B02_ProjRegister — DB",
"63": "B03_FileInput — API",
"64": "B04_PreProcess — DB",
"65": "B05_Profile — API",
"66": "B05 변형 성토면 마감·날개 패치",
"67": "B05_Profile — Profile Alignment & Table",
"68": "B05 구조물 비정규 측점 공급",
"69": "B05_Profile_UI_Drainage_Parts — 배수유역 공용 UI 파츠",
"70": "_UI_Drainage_Render — 배수유역도 Canvas 렌더러",
"71": "_UI_Profile_Structures — 종단 구조물 렌더링 및 인터랙션",
"72": "_UI_Selection — 배수/구조물 3자 선택 동기화",
"73": "B06 인접 측점 구조물 트림 정리",
"74": "B06 물넘이포장·콘크리트 포장·독립 기슭막이",
"75": "B06_Section_Engine_Areas — 횡단 면적 적분 연산 엔진",
"76": "B06_Section_Router_Confirm — 임시 저장 및 확정 라우터",
"77": "B06_Section_UI_Cross_Areas — 횡단 단면적 표기 및 밴드 하이라이트",
"78": "B06_Section_UI_Cross_Design — 횡단 측점별 세부 설계 컨트롤",
"79": "B06_Section_UI_MassHaul — 유토곡선 적분 계산 엔진",
"80": "B06_Section_UI_MassHaul_Balance — 평형선 및 장비 띠 분할 엔진",
"81": "B06_Section_UI_MassHaul_Balance_View — 운반 띠 시각화 렌더러",
"82": "B06_Section_UI_MassHaul_Balloon — 물량 말풍선 배치 및 조작",
"83": "B06_Section_UI_MassHaul_Curve — 유토곡선 궤적 보간 및 렌더링",
"84": "B06_Section_UI_MassHaul_Settle — 토량 정산 및 장거리 상쇄 엔진",
"85": "B06_Section_UI_MassHaul_View — 유토곡선 시각화 렌더러",
"86": "B06_Section_UI_Page — B06 메인 페이지 오케스트레이터",
"87": "B06_Section_UI_Section_View — 종/횡단 및 유토곡선 뷰 조립",
"88": "B07 Quantity — Backend",
"89": "B08 CAD 블록 라이브러리·사진",
"90": "B08 OpenWebCAD 명령 체계",
"91": "B08_CAD_table_entity.md",
"92": "공개·인증·관리 영역 지도",
"93": "공유 자원 영향 지도",
"94": "A00_Common — 공통 프레임워크 & 유틸",
"95": "app_shell.ts",
"96": "router.ts",
"97": "B04_PreProcess — API",
"98": "B04_PreProcess — Dependencies",
"99": "B05_Profile — Backend",
"100": "B05_Profile — DB 사용 관계",
"101": "B05_Profile — 3D Viewer & Interaction",
"102": "B05_Profile_Engine_Sections.md",
"103": "B05_Profile_Api_Fetch.ts",
"104": "B05_Profile_UI_Drainage_Panel",
"105": "B05_Profile_UI_Drainage_Pipes",
"106": "B05_Profile_UI_Profile_Panel.md",
"107": "B06_Section — DB 사용 관계",
"108": "B06_Section_Api_Fetch — B06 프론트엔드 API 클라이언트",
"109": "B06_Section_UI_Standard_Diagram — 표준단면 모식도 컴포넌트",
"110": "B06_Section_UI_Standard_Panel — 표준단면 입력 및 제어 패널",
"111": "B08_DesignDetail — Dependencies",
"112": "common_util_project_delete.md",
"113": "common_util_storage.md",
"114": "common_util_workflow_state.md",
"115": "미확정 테이블 보관소 (Unconfirmed DB Schemas)",
"116": "A01_Home_UI_Page.md",
"117": "A02_ProgDetail_UI_Page.md",
"118": "A06_Login_Router.md",
"119": "A07_Register_Router.md",
"120": "A08_Support_Router.md",
"121": "A09_Security_Router.md",
"122": "B01_Dashboard — DB 사용 관계",
"123": "B01_Dashboard — Dependencies",
"124": "B01_Dashboard_Router.md",
"125": "B01_Dashboard_UI_Page.md",
"126": "B02_ProjRegister_Router.md",
"127": "B03_FileInput — DB 사용 관계",
"128": "B03_FileInput — Dependencies",
"129": "B03_FileInput_Router.md",
"130": "B04_PreProcess_Router.md",
"131": "B05_Profile — Dependencies",
"132": "B05_Profile_Engine_Grade.md",
"133": "B05_Profile_Engine_Solver.md",
"134": "B05_Profile_Repository.md",
"135": "B05_Profile_Router.md",
"136": "B05_Profile_Router_Confirm.md",
"137": "B05_Profile_Schema.md",
"138": "B05_Profile_UI_IrregularStations.md",
"139": "B05_Profile_UI_Page.md",
"140": "B05_Profile_UI_Panel.md",
"141": "B05_Profile_UI_Profile_Alignment.md",
"142": "B05_Profile_UI_Profile_Table.md",
"143": "B05_Profile_UI_Viewer.md",
"144": "B06_Section — Dependencies",
"145": "B06_Section_Router.md",
"146": "B01~B09 Workflow 데이터 흐름",
"147": "B08_DesignDetail — API",
"148": "longitudinal_alignment.md",
"149": "B03_FileInput_plan_lidar_multi_file.md",
"150": "B05_completed_followups.md",
"151": "B05_corridor_followup_decisions.md",
"152": "B06_api.md",
"153": "B07_db.md",
"154": "B08_DesignDetail_Router.md",
"155": "B09_Estimation_UI_Page.md",
"156": "B10_Payment_UI_Page.md",
"157": "B11_Status_UI_Page.md",
"158": "B03 계획노선 정본·좌표계 후속",
"159": "B03 파일 입력 화면 정리",
"160": "B04 3D 방위·좌표계 최신 결정",
"161": "B04 세부유역·방위·좌표계",
"162": "B05 구조물 구간 절취·측벽·성토 패치",
"163": "B05 구조물 3D 투영 커브",
"164": "B05_Profile — Frontend",
"165": "B05 유토곡선·구조물 후속",
"166": "B05 종단곡선·실시간 횡단 연동",
"167": "B06 횡단 계산 미러·카드 표기",
"168": "B06 횡단 관 형상·유토곡선 후속",
"169": "B08_DesignDetail_Engine_Cad_Basin.py",
"170": "B08_DesignDetail_Engine_Cad_MassHaul.py",
"171": "B08 CAD·납품 도면 후속",
"176": "OpenWebCAD Core",
"177": "common_util_mass_haul_settle.ts",
"178": "Drainage Watershed (유역도)",
"179": "Mass Haul Diagram (토적도)",
"182": "2026-09-02 완료 항목",
"183": "2026-09-03 완료 — 입력·배수·종횡단·CAD",
"184": "2026-09-03 추가 완료 — 화면·종단·횡단",
"185": "Query: 임도 집수정 형태정보",
"187": "B03 File Input Backend",
"188": "B03 File Input Frontend",
"189": "B04 PreProcess Backend",
"190": "B04 PreProcess Frontend",
"191": "B07_Quantity_Router.md",
"192": "B08 CAD Blocks Library",
"193": "B08 CAD Table Entity",
"194": "B08 Cross Section Structure Sheets",
"195": "Query: 배수관 매설 각도 제약조건 (임도)"
}
@@ -0,0 +1 @@
{"output_tokens": 2385}
@@ -0,0 +1,891 @@
# Graph Report - wiki (2026-09-11)
## Corpus Check
- 206 files · ~57,686 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 1170 nodes · 996 edges · 189 communities (155 shown, 34 thin omitted)
- Extraction: 100% EXTRACTED · 0% INFERRED · 0% AMBIGUOUS
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `e49e4d85`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
## Community Hubs (Navigation)
- UI Templates — Localization & Components
- 인증 / RBAC
- A00_Common — App Shell Framework
- 2026-09-04 완료 항목
- 배수유역 해석 및 세부설계 (Drainage Watershed)
- DB: 로그/모니터링 테이블
- A09_Security — Backend
- 2026-08-29 완료 반영
- DB: 파일/지표면분석 테이블
- DB: 경로/종횡단 테이블
- A04_NewsHistory — Frontend
- A07_Register — Frontend
- A08_Support — Frontend
- B02_ProjRegister — Frontend
- A01_Home — Frontend
- A05_EduDetail — Frontend
- A06_Login — Frontend
- A08_Support — Backend
- A09_Security — Frontend
- B02_ProjRegister — Backend
- B04_PreProcess — Backend
- B04_PreProcess — Frontend
- B05 구조물 정본·통합 편집
- B06_Section — Frontend
- A03_CompDetail — Frontend
- A06_Login — Backend
- 유토곡선 (Mass Haul Diagram) 계산 명세
- A07_Register — Backend
- B06 배수관 구조물 조작·표시
- B06 기슭막이 연동·경사·단별 제어
- B08_DesignDetail — Backend
- B08 토적도·수리집수면적유역도
- B09_Estimation — Frontend
- 현재 구현 현황 — 소스 읽기 감사
- DB: 구조물/수량/산출물 테이블
- B01_Dashboard — Backend
- B01_Dashboard — Frontend
- B03_FileInput — Backend
- B05 급선회 3D 국부 보정 계획
- B06 집수정·다단 기슭막이
- B06 배수관 구조물 기하·조작 체계
- B06 배수관 횡단도 세트
- B10_Payment — Frontend
- B11_Status — Frontend
- 저장 경로 규칙 (Workflow-based Folder Structure)
- DB: 프로젝트 관리 테이블
- 임도기술교본 원문 md 추출 품질 결함
- A01_Home — 세부 구현
- A02_ProgDetail — 세부 구현
- A02_ProgDetail — Frontend
- B01_Dashboard — API
- B03_FileInput — Frontend
- B05 계획노선 코리도 삼각망 서피스
- B05 구조물·UI 현재 계획
- B05 구조물 입력·종단 표시 정비 — 2026-08-19
- B07 구조물 표준도 — 조사·합의와 현재 통로
- B08 횡단도 구조물·장 배치
- 2026-09-07 완료·보류 요약
- B07 외부 WebCAD 비교 실행환경
- Temp Upload (프로젝트 생성 전 임시 보관함)
- Q: 배수관 매설시 각도의 제약조건이 있는지 확인해줘. 임도에서
- Q: 임도 기술정보DB에서 집수정의 형태정보는 어떤게 있는지 확인해줘.
- B02_ProjRegister — DB
- B03_FileInput — API
- B04_PreProcess — DB
- B05_Profile — API
- B05 변형 성토면 마감·날개 패치
- B05_Profile — Profile Alignment & Table
- B05 구조물 비정규 측점 공급
- B05_Profile_UI_Drainage_Parts — 배수유역 공용 UI 파츠
- _UI_Drainage_Render — 배수유역도 Canvas 렌더러
- _UI_Profile_Structures — 종단 구조물 렌더링 및 인터랙션
- _UI_Selection — 배수/구조물 3자 선택 동기화
- B06 인접 측점 구조물 트림 정리
- B06 물넘이포장·콘크리트 포장·독립 기슭막이
- B06_Section_Engine_Areas — 횡단 면적 적분 연산 엔진
- B06_Section_Router_Confirm — 임시 저장 및 확정 라우터
- B06_Section_UI_Cross_Areas — 횡단 단면적 표기 및 밴드 하이라이트
- B06_Section_UI_Cross_Design — 횡단 측점별 세부 설계 컨트롤
- B06_Section_UI_MassHaul — 유토곡선 적분 계산 엔진
- B06_Section_UI_MassHaul_Balance — 평형선 및 장비 띠 분할 엔진
- B06_Section_UI_MassHaul_Balance_View — 운반 띠 시각화 렌더러
- B06_Section_UI_MassHaul_Balloon — 물량 말풍선 배치 및 조작
- B06_Section_UI_MassHaul_Curve — 유토곡선 궤적 보간 및 렌더링
- B06_Section_UI_MassHaul_Settle — 토량 정산 및 장거리 상쇄 엔진
- B06_Section_UI_MassHaul_View — 유토곡선 시각화 렌더러
- B06_Section_UI_Page — B06 메인 페이지 오케스트레이터
- B06_Section_UI_Section_View — 종/횡단 및 유토곡선 뷰 조립
- B07 Quantity — Backend
- B08 CAD 블록 라이브러리·사진
- B08 OpenWebCAD 명령 체계
- B08_CAD_table_entity.md
- 공개·인증·관리 영역 지도
- 공유 자원 영향 지도
- A00_Common — 공통 프레임워크 & 유틸
- app_shell.ts
- router.ts
- B04_PreProcess — API
- B04_PreProcess — Dependencies
- B05_Profile — Backend
- B05_Profile — DB 사용 관계
- B05_Profile — 3D Viewer & Interaction
- B05_Profile_Engine_Sections.md
- B05_Profile_Api_Fetch.ts
- B05_Profile_UI_Drainage_Panel
- B05_Profile_UI_Drainage_Pipes
- B05_Profile_UI_Profile_Panel.md
- B06_Section — DB 사용 관계
- B06_Section_Api_Fetch — B06 프론트엔드 API 클라이언트
- B06_Section_UI_Standard_Diagram — 표준단면 모식도 컴포넌트
- B06_Section_UI_Standard_Panel — 표준단면 입력 및 제어 패널
- B08_DesignDetail — Dependencies
- common_util_project_delete.md
- common_util_storage.md
- common_util_workflow_state.md
- 미확정 테이블 보관소 (Unconfirmed DB Schemas)
- A01_Home_UI_Page.md
- A02_ProgDetail_UI_Page.md
- A06_Login_Router.md
- A07_Register_Router.md
- A08_Support_Router.md
- A09_Security_Router.md
- B01_Dashboard — DB 사용 관계
- B01_Dashboard — Dependencies
- B01_Dashboard_Router.md
- B01_Dashboard_UI_Page.md
- B02_ProjRegister_Router.md
- B03_FileInput — DB 사용 관계
- B03_FileInput — Dependencies
- B03_FileInput_Router.md
- B04_PreProcess_Router.md
- B05_Profile — Dependencies
- B05_Profile_Engine_Grade.md
- B05_Profile_Engine_Solver.md
- B05_Profile_Repository.md
- B05_Profile_Router.md
- B05_Profile_Router_Confirm.md
- B05_Profile_Schema.md
- B05_Profile_UI_IrregularStations.md
- B05_Profile_UI_Page.md
- B05_Profile_UI_Panel.md
- B05_Profile_UI_Profile_Alignment.md
- B05_Profile_UI_Profile_Table.md
- B05_Profile_UI_Viewer.md
- B06_Section — Dependencies
- B06_Section_Router.md
- B01~B09 Workflow 데이터 흐름
- B08_DesignDetail — API
- longitudinal_alignment.md
- B03_FileInput_plan_lidar_multi_file.md
- B05_completed_followups.md
- B05_corridor_followup_decisions.md
- B06_api.md
- B07_db.md
- B08_DesignDetail_Router.md
- B09_Estimation_UI_Page.md
- B10_Payment_UI_Page.md
- B11_Status_UI_Page.md
- B03 계획노선 정본·좌표계 후속
- B03 파일 입력 화면 정리
- B04 3D 방위·좌표계 최신 결정
- B04 세부유역·방위·좌표계
- B05 구조물 구간 절취·측벽·성토 패치
- B05 구조물 3D 투영 커브
- B05_Profile — Frontend
- B05 유토곡선·구조물 후속
- B05 종단곡선·실시간 횡단 연동
- B06 횡단 계산 미러·카드 표기
- B06 횡단 관 형상·유토곡선 후속
- B08_DesignDetail_Engine_Cad_Basin.py
- B08_DesignDetail_Engine_Cad_MassHaul.py
- B08 CAD·납품 도면 후속
- OpenWebCAD Core
- common_util_mass_haul_settle.ts
- Drainage Watershed (유역도)
- Mass Haul Diagram (토적도)
- 2026-09-02 완료 항목
- 2026-09-03 완료 — 입력·배수·종횡단·CAD
- 2026-09-03 추가 완료 — 화면·종단·횡단
- Query: 임도 집수정 형태정보
- B03 File Input Backend
- B03 File Input Frontend
- B04 PreProcess Backend
- B04 PreProcess Frontend
- B07_Quantity_Router.md
- B08 CAD Blocks Library
- B08 CAD Table Entity
- B08 Cross Section Structure Sheets
- Query: 배수관 매설 각도 제약조건 (임도)
## God Nodes (most connected - your core abstractions)
1. `UI Templates — Localization & Components` - 12 edges
2. `DB: 로그/모니터링 테이블` - 11 edges
3. `B07 구조물 표준도 — 조사·합의와 현재 통로` - 11 edges
4. `2026-08-29 완료 반영` - 10 edges
5. `DB: 파일/지표면분석 테이블` - 10 edges
6. `A07_Register — Frontend` - 10 edges
7. `A08_Support — Frontend` - 10 edges
8. `B02_ProjRegister — Frontend` - 10 edges
9. `인증 / RBAC` - 9 edges
10. `배수유역 해석 및 세부설계 (Drainage Watershed)` - 9 edges
## Surprising Connections (you probably didn't know these)
- `B07 구조물 표준도 — 조사·합의와 현재 통로` --references--> `B07 DesignDetail — 현재 책임` [EXTRACTED]
pages/B07_DesignDetail/B07_standard_drawings_2026_09.md → pages/B07_DesignDetail/B07_frontend.md
- `B07 구조물 표준도 — 조사·합의와 현재 통로` --references--> `B08 Quantity — 2026-09 수량산출` [EXTRACTED]
pages/B07_DesignDetail/B07_standard_drawings_2026_09.md → pages/B08_Quantity/B08_overview_2026_09.md
- `B07 구조물 표준도 — 조사·합의와 현재 통로` --references--> `B09 Estimation — 2026-09 원가계산` [EXTRACTED]
pages/B07_DesignDetail/B07_standard_drawings_2026_09.md → pages/B09_Estimation/B09_overview_2026_09.md
## Import Cycles
- None detected.
## Hyperedges (group relationships)
- **B03-B08 Workflow Data Flow** — b03_fileinput_route_snapshot_crs, b04_preprocess_drainage_compass_crs, b05_profile_frontend, b06_section_cross_design_ui_2026_09, b08_designdetail_frontend [INFERRED 0.90]
- **Shared Mass Haul Calculation and UI** — b05_profile_masshaul_structure_2026_09, b06_section_masshaul_culvert_2026_09, b08_designdetail_cad_delivery_2026_09 [EXTRACTED 0.85]
- **CAD Delivery and Usability Framework** — b08_designdetail_cad_interaction, b08_designdetail_cad_title_block, b08_designdetail_cad_usability_2026_09_01, b08_designdetail_cad_delivery_2026_09 [EXTRACTED 0.95]
- **LAS-Free Analysis Workflow** — concepts_las_free_sheet_surface, pages_b03_fileinput_backend, pages_b04_preprocess_backend [EXTRACTED 1.00]
- **B08 Drawing Generation Flow** — b08_designdetail_b08_designdetail_engine_cad_masshaul_py, b08_designdetail_b08_designdetail_engine_cad_basin_py, common_util_common_util_mass_haul_settle_ts [EXTRACTED 0.90]
- **Drainage System Workflow** — concepts_drainage_watershed, pages_b05_profile_b05_structures, pages_b06_section_b06_culvert_set, pages_b06_section_b06_culvert_geometry_redesign [EXTRACTED 0.95]
- **Mass Haul Diagram System** — concepts_mass_haul_diagram, pages_b06_section_b06_frontend, pages_b08_designdetail_b08_drawing_masshaul_watershed [EXTRACTED 0.90]
- **3D Corridor Generation Flow** — pages_b05_profile_b05_corridor_surface, pages_b05_profile_b05_corridor_plan_curves, pages_b05_profile_b05_corridor_cut_fill, pages_b05_profile_b05_corridor_patch_finish [EXTRACTED 0.90]
- **Late Workflow Stages (B07-B09)** — pages_b07_quantity_b07_frontend, pages_b08_designdetail_b08_frontend, pages_b09_estimation_b09_frontend [EXTRACTED 1.00]
## Communities (189 total, 34 thin omitted)
### Community 0 - "UI Templates — Localization & Components"
Cohesion: 0.05
Nodes (38): 2026-09-04 완료 항목, 700줄 제한 분리, 보존된 미완료 범위, 상시계획서 추가 완료 범위, 완료 범위, 추가 완료 범위, 디자인 시스템 (Design System), 레이아웃 및 둥근 테두리 (Radius & Spacing) (+30 more)
### Community 1 - "인증 / RBAC"
Cohesion: 0.06
Nodes (31): OTP / 비밀번호 및 디바이스 신뢰, 권한 검증 헬퍼 (B01_Dashboard), 라우팅 가드 (frontend.md 5.2), 사용자 상태 생명주기, 사용처 (역참조), 세션 인증 (backend.md 6.3), 역할 (users.role), 인증 / RBAC (+23 more)
### Community 2 - "A00_Common — App Shell Framework"
Cohesion: 0.08
Nodes (22): A00_Common — App Shell Framework, app_shell 구성요소, router 라우팅 테이블, A00_Common — 스캐폴드·CSS·종속성, b_page_scaffold, CSS 인젝션, 사용처, 종속성 (+14 more)
### Community 3 - "2026-09-04 완료 항목"
Cohesion: 0.07
Nodes (25): API 공통 (여러 페이지가 공유하는 엔드포인트), 공통 오류 응답 포맷 (전 라우터), 워크플로우 상태 조회, 폴링 패턴 (legacy workflow.json 설계; 현재 구현은 workflow-state API 사용), 계산 구현 원칙, 계획노선 규칙, 설계 데이터 생명주기, 정본 세 벌 (+17 more)
### Community 4 - "배수유역 해석 및 세부설계 (Drainage Watershed)"
Cohesion: 0.25
Nodes (7): B08_DesignDetail — Frontend, CAD 계획선 레이어 연동 및 편집 지원 (2026-07-22), openwebcad 단위 정합, 선 특성/폰트 UI 및 Fit-in-all (2026-07-20), 독립형 CAD 임베드 및 데이터 연동 아키텍처, 의존성, 주요 컴포넌트 / 함수, 파일 구조
### Community 5 - "DB: 로그/모니터링 테이블"
Cohesion: 0.17
Nodes (11): activity_logs (사용자 활동 로그), audit_logs (감사 로그), change_logs (설계 변경 이력), DB: 로그/모니터링 테이블, login_logs (로그인 시도 로그), support_requests (기술 지원 요청), system_admin_logs (시스템 관리자 행위 로그), system_audit_logs — 사용처 (B01, B02) (+3 more)
### Community 6 - "A09_Security — Backend"
Cohesion: 0.17
Nodes (11): A09_Security — Backend, API 엔드포인트, DB 저장 (activity_logs), 권한 헬퍼, 마스터(회사 관리자) 전용 — `require_master`, 시스템 관리자 전용 — `require_system_admin`, 요청 스키마 (Pydantic), 의존성 (공통 유틸) (+3 more)
### Community 7 - "2026-08-29 완료 반영"
Cohesion: 0.10
Nodes (17): 2026-08-29 완료 반영, B03 재업로드·B05 최신 조회, B05/B06 구조물 UI 통합, B07↔B08 순서, B07 CAD 고정 척도·횡단 장 배치, B07 CAD 테마, B07 CAD 확대·팬, 배수시설 추천 기준 (+9 more)
### Community 8 - "DB: 파일/지표면분석 테이블"
Cohesion: 0.18
Nodes (10): DB: 파일/지표면분석 테이블, input_files.status 값, input_files (입력 원본 파일), processed_point_cloud.status 값, processed_point_cloud (필터/변환 포인트클라우드), surface_models.status 값, surface_models (지표면 모델 및 등고선), terrain_layers (지형 레이어) (+2 more)
### Community 9 - "DB: 경로/종횡단 테이블"
Cohesion: 0.18
Nodes (10): cross_sections.data.structures, cross_sections (횡단면 설계), `data` 컬럼 내 `options` 스냅샷 구조 (2026-07-19 도입), `data` 컬럼 내 `profile_alignment` 구조 (2026-07-23 도입), DB: 경로/종횡단 테이블, longitudinal_sections (종단면 설계), route_points (경로 좌표점), route_statistics (노선 통계) (+2 more)
### Community 10 - "A04_NewsHistory — Frontend"
Cohesion: 0.18
Nodes (10): A04_NewsHistory — Frontend, CSS 클래스 구조, Mock 데이터 구조, 로컬라이제이션, 미해결 사항, 반응형, 스타일 (CSS), 의존성 (+2 more)
### Community 11 - "A07_Register — Frontend"
Cohesion: 0.18
Nodes (10): A07_Register — Frontend, API 클라이언트 함수, 로컬라이제이션, 스타일 (CSS), 약관 동의 (아코디언), 의존성, 이벤트 핸들러, 제출 로직 (2단계 폼 전환) (+2 more)
### Community 12 - "A08_Support — Frontend"
Cohesion: 0.18
Nodes (10): A08_Support — Frontend, 로컬라이제이션, 세션 자동 채움, 스타일 (CSS), 의존성, 이벤트 핸들러, 제출 로직, 참고 (+2 more)
### Community 13 - "B02_ProjRegister — Frontend"
Cohesion: 0.18
Nodes (10): B02_ProjRegister — Frontend, 로컬라이제이션, 스타일 (CSS), 의존성, 이벤트 핸들러, 입력 필드, 제출 로직, 참고 (+2 more)
### Community 14 - "A01_Home — Frontend"
Cohesion: 0.20
Nodes (9): A01_Home — Frontend, Hero 섹션, 로컬라이제이션, 세부 구현, 이벤트 핸들러, 주요 기능 섹션, 최신 소식 섹션, 컴포넌트 (+1 more)
### Community 15 - "A05_EduDetail — Frontend"
Cohesion: 0.20
Nodes (9): A05_EduDetail — Frontend, CSS 클래스 구조, 로컬라이제이션, 반응형, 스타일 (CSS), 의존성, 이벤트 핸들러, 컴포넌트 (섹션 빌더) (+1 more)
### Community 16 - "A06_Login — Frontend"
Cohesion: 0.20
Nodes (9): A06_Login — Frontend, API 클라이언트 함수, 로컬라이제이션, 스타일 (CSS), 의존성, 이벤트 핸들러, 제출 및 OTP 제어 로직 (2단계 폼 전환), 컴포넌트 / 함수 (+1 more)
### Community 17 - "A08_Support — Backend"
Cohesion: 0.20
Nodes (9): A08_Support — Backend, API 엔드포인트, DB 저장 컬럼 (실 코드 INSERT 기준), 내부 헬퍼, 요청 스키마 (Pydantic), 의존성 (공통 유틸), 접수 로직, 특징 (+1 more)
### Community 18 - "A09_Security — Frontend"
Cohesion: 0.20
Nodes (9): A09_Security — Frontend, API 클라이언트 함수 (⚠️ 미사용, 정의만), 로컬라이제이션, 스타일 (CSS), 약관 데이터 (A09_Security_Terms.ts), ⚠️ 약관 텍스트와 실 코드 불일치, 의존성, 컴포넌트 / 함수 (+1 more)
### Community 19 - "B02_ProjRegister — Backend"
Cohesion: 0.20
Nodes (9): API 엔드포인트, B02_ProjRegister — Backend, DB 저장 컬럼 (projects INSERT), 생성 로직 (create_project 트랜잭션), 요청/응답 스키마 (Pydantic), 의존성 (공통 유틸), 참고, 파일 구조 (+1 more)
### Community 20 - "B04_PreProcess — Backend"
Cohesion: 0.20
Nodes (9): 2D/GIS 미표시 원인 분석 및 조치, B04_PreProcess — Backend, 📋 구현 예외 처리 검토 항목 (PLAN), ⚙️ 설정 및 환경 파일 정합성, 수치지형도 도엽 오버레이 아키텍처 (2026-07-26, 2026-08-01 S8 개편), 엔진 서브모듈, 워크플로우 상태 전이 및 자동 확정, 주요 함수 (Router / Repository / Engine / Utility) (+1 more)
### Community 21 - "B04_PreProcess — Frontend"
Cohesion: 0.20
Nodes (9): 3D 뷰어 DOM 마운트 버그 수리, 3D 뷰어 테마 연동 및 가독성 개선, 3D 카메라 커서 피봇 & 2D 오버레이 UI 개선 (2026-08-01~02 일원화), B04_PreProcess — Frontend, ⚠️ 계획서와 실 코드 불일치 (3D 뷰어), 의존성, 처리 흐름, 컴포넌트 & API 함수 (+1 more)
### Community 22 - "B05 구조물 정본·통합 편집"
Cohesion: 0.20
Nodes (9): API, B05 구조물 정본·통합 편집, B06 경계와 남은 범위, 검증, 배수관·시설 옵션, 백엔드 파일, 유역 추천·개략 단면, 정본과 타입 레지스트리 (+1 more)
### Community 23 - "B06_Section — Frontend"
Cohesion: 0.20
Nodes (9): 2026-08-22 파일 한계 정리, API 클라이언트, B06_Section — Frontend, SVG 렌더러 및 유틸리티, UI 패널 크기 및 리사이저 규칙 (2026-08-02 신설), 기술부채 (해결됨), 입력 옵션 (표시 옵션 및 횡단 반폭 제어), 파일 구성 (+1 more)
### Community 24 - "A03_CompDetail — Frontend"
Cohesion: 0.22
Nodes (8): A03_CompDetail — Frontend, CSS 클래스 구조, 로컬라이제이션, 반응형, 스타일 (CSS), 의존성, 컴포넌트 (섹션 빌더), 파일 구조
### Community 25 - "A06_Login — Backend"
Cohesion: 0.22
Nodes (8): A06_Login — Backend, API 엔드포인트, 내부 헬퍼, 로그인 로직 흐름, 보안 정책 (실 코드 기준), 요청 스키마 (Pydantic), 의존성 (공통 유틸), 파일 구조
### Community 26 - "유토곡선 (Mass Haul Diagram) 계산 명세"
Cohesion: 0.15
Nodes (11): 1. 개요 및 분석 목적, 2. 주요 계산 수식 및 원리 (실무 관례 반영), 3. 지반유형별 토량환산계수 기본값 (`config_system.py`), 4. 토공 운반장비 선정거리 및 분배 기준 (`config_system.py`), 5. 유토곡선 곡선 사양 (B06 구현 v2), 6. 웹앱 연동 및 시각화 명세 (2026-08-02 확정), 유토곡선 (Mass Haul Diagram) 계산 명세, B08 Quantity — 2026-09 수량산출 (+3 more)
### Community 27 - "A07_Register — Backend"
Cohesion: 0.25
Nodes (7): A07_Register — Backend, API 엔드포인트, 가입 로직 흐름, 요청 스키마 (Pydantic), 의존성 (공통 유틸), 참고, 파일 구조
### Community 28 - "B06 배수관 구조물 조작·표시"
Cohesion: 0.25
Nodes (7): 4축 조작과 재질, B06 배수관 구조물 조작·표시, 계산·보기 분리, 구현 파일, 자체검증 기록, 조정창, 집수정 9키 조작
### Community 29 - "B06 기슭막이 연동·경사·단별 제어"
Cohesion: 0.25
Nodes (7): B06 기슭막이 연동·경사·단별 제어, 검증, 단별 구간값, 선택과 하이라이트, 연동과 경사, 정본과 공용 모델, 형태와 조정창
### Community 30 - "B08_DesignDetail — Backend"
Cohesion: 0.25
Nodes (7): B08_DesignDetail — Backend, 도각 템플릿 변환 및 종단도 A1 도각 병합 (2026-07-26), 도면 관리, 종단 30측점 분할, CAD 수량산출표 및 도면 템플릿 파이프라인, 워크플로우 게이팅 연동, 종단도 30측점 N분할 및 납품 양식 측점 테이블 (2026-07-25 N-1-1), 현재 책임 경계, 횡단도 4개 선별 레이어 및 CAD 수량산출표 (2026-07-25 N-1-2/N-1-3)
### Community 31 - "B08 토적도·수리집수면적유역도"
Cohesion: 0.25
Nodes (7): B08 토적도·수리집수면적유역도, 검증 한계, 구현·검증 완료, 남은 결정, 데이터 흐름, 도면 기준, 유역 정보표
### Community 32 - "B09_Estimation — Frontend"
Cohesion: 0.25
Nodes (7): B09_Estimation — Frontend, 로컬라이제이션, 백엔드/DB — 미착수, 의존성, 참고, 컴포넌트 / 함수, 파일 구조
### Community 33 - "현재 구현 현황 — 소스 읽기 감사"
Cohesion: 0.06
Nodes (28): 구현 상태 용어, 단계별 판정, 미결 설계, 반드시 유지할 구분, 비워크플로 영역 판정, 현재 구현 현황 — 소스 읽기 감사, Aislo 프로젝트 지도, 명칭 판정 (+20 more)
### Community 34 - "DB: 구조물/수량/산출물 테이블"
Cohesion: 0.29
Nodes (6): DB: 구조물/수량/산출물 테이블, output_files (개별 산출 파일 리스트), outputs (최종 견적/도면 산출 세션), quantity_items (수량 산출 항목), quantity_items 총비용 계산 예, structures (배치 구조물)
### Community 35 - "B01_Dashboard — Backend"
Cohesion: 0.29
Nodes (6): B01_Dashboard — Backend, 기술부채, 라우터 권한 헬퍼, 세분화 백엔드 위키 명세, 요청 스키마, 저장소 및 삭제 함수
### Community 36 - "B01_Dashboard — Frontend"
Cohesion: 0.29
Nodes (6): B01_Dashboard — Frontend, UI 권한 헬퍼, 공유 자원 연결, 모달, 분할된 UI 컴포넌트 파일, 파일과 진입점
### Community 37 - "B03_FileInput — Backend"
Cohesion: 0.29
Nodes (6): B03_FileInput — Backend, workflow·알림, 메타데이터 분석 및 파일 지문, 임시 보관함 (R2 Temp Upload), 입력 검증·파일 처리, 저장소 및 초기화
### Community 38 - "B05 급선회 3D 국부 보정 계획"
Cohesion: 0.29
Nodes (6): B05 급선회 3D 국부 보정 계획, 구조물 연동, 국부 패치 절차, 목적과 경계, 완료 조건, 확인된 노견 확장 회귀
### Community 39 - "B06 집수정·다단 기슭막이"
Cohesion: 0.29
Nodes (6): B06 집수정·다단 기슭막이, 미결, 유입 구조물, 유출 성토부·다단, 자체검증 기록, 집수정 계류측 성토부
### Community 40 - "B06 배수관 구조물 기하·조작 체계"
Cohesion: 0.29
Nodes (6): B06 배수관 구조물 기하·조작 체계, 검증 상태, 구현 파일, 기슭막이·관 핵심 규칙, 설계선 트림, 접속선
### Community 41 - "B06 배수관 횡단도 세트"
Cohesion: 0.29
Nodes (6): B06 배수관 횡단도 세트, 검증 근거와 후속, 계획선 규칙, 구현 항목, 입력·판정, 형상·표시 순서 (2026-08-20 스냅샷)
### Community 42 - "B10_Payment — Frontend"
Cohesion: 0.29
Nodes (6): B10_Payment — Frontend, 로컬라이제이션, 비즈니스 로직 전제 (목업), 의존성, 주요 컴포넌트 및 함수 (Mockup), 파일 구조
### Community 43 - "B11_Status — Frontend"
Cohesion: 0.29
Nodes (6): B11_Status — Frontend, 결재 상태 흐름 (Payment Flow Status), 로컬라이제이션, 의존성, 주요 컴포넌트 및 기능 (Mockup), 파일 구조
### Community 44 - "저장 경로 규칙 (Workflow-based Folder Structure)"
Cohesion: 0.29
Nodes (6): B05 구조물 3D 투영 커브, 검증, 구조물 날개·바닥 연결, 비탈 투영·성토면 절단, 저장·호환성, 커브 생성·렌더
### Community 45 - "DB: 프로젝트 관리 테이블"
Cohesion: 0.33
Nodes (5): DB: 프로젝트 관리 테이블, project_automations (프로젝트 자동화 정책), project_versions (프로젝트 버전 스냅샷), project_workflow_stages (단계별 상세 상태), projects (프로젝트)
### Community 46 - "임도기술교본 원문 md 추출 품질 결함"
Cohesion: 0.05
Nodes (33): 2026-09-09 완료 구현·검증, B05·B06 종횡단, B07 표준도, B08 수량산출, B09 원가계산, 검증 경계, 결함 유형 (예시 = 위 파일 기준 줄번호), 원인 추정 (+25 more)
### Community 47 - "A01_Home — 세부 구현"
Cohesion: 0.33
Nodes (5): A01_Home — 세부 구현, 데이터 흐름, 미해결, 스타일, 의존성
### Community 48 - "A02_ProgDetail — 세부 구현"
Cohesion: 0.33
Nodes (5): A02_ProgDetail — 세부 구현, 데이터 흐름, 미해결 / 특이사항, 스타일 (CSS), 의존성
### Community 49 - "A02_ProgDetail — Frontend"
Cohesion: 0.33
Nodes (5): A02_ProgDetail — Frontend, 구조, 세부 구현, 제약 준수, 컴포넌트 분석
### Community 50 - "B01_Dashboard — API"
Cohesion: 0.33
Nodes (5): B01_Dashboard — API, 사용자·회사, 시스템 관리자, 프로젝트·자동화, 회사 관리자
### Community 51 - "B03_FileInput — Frontend"
Cohesion: 0.33
Nodes (5): API 클라이언트, B03_FileInput — Frontend, UI 지원 유틸리티 (분할 완료), 브라우저 상태·오프라인 보조, 페이지·업로드 흐름
### Community 52 - "B05 계획노선 코리도 삼각망 서피스"
Cohesion: 0.33
Nodes (5): B05 계획노선 코리도 삼각망 서피스, 검증, 저장·호환성, 진행 중 계획, 프론트엔드 구성
### Community 53 - "B05 구조물·UI 현재 계획"
Cohesion: 0.33
Nodes (5): 2026-08-18 B05 페이지 개선 2차 계획 기록, 2026-08-18 사이드 패널·입력 로직 계획 기록, B05/B06 구조물 적용 범위, B05 구조물·UI 현재 계획, 후속 결정 대기
### Community 54 - "B05 구조물 입력·종단 표시 정비 — 2026-08-19"
Cohesion: 0.33
Nodes (5): B05 구조물 입력·종단 표시 정비 — 2026-08-19, 기록된 검증, 기준 해석 보류, 완료 범위, 주요 항목
### Community 55 - "B07 구조물 표준도 — 조사·합의와 현재 통로"
Cohesion: 0.29
Nodes (6): B05_Profile — Frontend, 남은 파일 한계, 📂 소스코드 1:1 세분화 위키 파일 목록, 종단 편집 안전장치, 종단테이블 표시 보정, 📋 핵심 프론트엔드 아키텍처 개요
### Community 56 - "B08 횡단도 구조물·장 배치"
Cohesion: 0.33
Nodes (5): B08 횡단도 구조물·장 배치, 결과, 데이터·구현 흐름, 문제와 결정, 한계
### Community 57 - "2026-09-07 완료·보류 요약"
Cohesion: 0.14
Nodes (11): 2026-09-07 완료·보류 요약, 성능·운영 결정, 완료 범위, 주의, B06_Section — Backend, 계산 엔진, 데이터 영구 저장 및 환경설정, 라우터·workflow (조회, 확정, 타 프로젝트 불러오기 및 재생성) (+3 more)
### Community 58 - "B07 외부 WebCAD 비교 실행환경"
Cohesion: 0.40
Nodes (4): B07 외부 WebCAD 비교 실행환경, 검증 상태, 라이선스 주의, 실행과 종료
### Community 59 - "Temp Upload (프로젝트 생성 전 임시 보관함)"
Cohesion: 0.40
Nodes (4): Temp Upload (프로젝트 생성 전 임시 보관함), 사용처, 주요 개념 및 스펙, 주요 구성 요소
### Community 60 - "Q: 배수관 매설시 각도의 제약조건이 있는지 확인해줘. 임도에서"
Cohesion: 0.40
Nodes (4): Answer, Outcome, Q: 배수관 매설시 각도의 제약조건이 있는지 확인해줘. 임도에서, Source Nodes
### Community 61 - "Q: 임도 기술정보DB에서 집수정의 형태정보는 어떤게 있는지 확인해줘."
Cohesion: 0.40
Nodes (4): Answer, Outcome, Q: 임도 기술정보DB에서 집수정의 형태정보는 어떤게 있는지 확인해줘., Source Nodes
### Community 62 - "B02_ProjRegister — DB"
Cohesion: 0.40
Nodes (4): B02_ProjRegister — DB, 쓰는 테이블, 저장소(파일시스템), 참고 (계획 당시 의도)
### Community 63 - "B03_FileInput — API"
Cohesion: 0.40
Nodes (4): B03_FileInput — API, workflow 조회, 일반 업로드, 청크 업로드
### Community 64 - "B04_PreProcess — DB"
Cohesion: 0.40
Nodes (4): B04_PreProcess — DB, Repository 함수, 쓰는 테이블, 참고
### Community 65 - "B05_Profile — API"
Cohesion: 0.40
Nodes (4): API 스키마 및 반환 필드, B05_Profile — API, `POST /{project_id}/route/confirm` 요청 (`RouteConfirmRequest`), 엔드포인트
### Community 66 - "B05 변형 성토면 마감·날개 패치"
Cohesion: 0.40
Nodes (4): B05 변형 성토면 마감·날개 패치, 세월교 날개 패치, 저장·검증, 패치 마감
### Community 67 - "B05_Profile — Profile Alignment & Table"
Cohesion: 0.40
Nodes (4): 12행 도면 테이블 및 가로 스크롤 정렬 개편 (`_UI_Profile_Table.ts`, `_UI_Profile_Panel.ts`), B05_Profile — Profile Alignment & Table, 비정규 측점(구조물) 테이블 오버레이 & 런타임 검증 (`_UI_Profile_Table.ts`, `_UI_IrregularStations.ts`), 종단 계획고 편집 인터랙션 (`_UI_Profile_Edit.ts`, `_UI_Profile_Panel.ts`, `_UI_Page.ts`)
### Community 68 - "B05 구조물 비정규 측점 공급"
Cohesion: 0.40
Nodes (4): B05 구조물 비정규 측점 공급, 검증, 공급 경로, 정본 규칙
### Community 69 - "B05_Profile_UI_Drainage_Parts — 배수유역 공용 UI 파츠"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B05_Profile_UI_Drainage_Parts — 배수유역 공용 UI 파츠
### Community 70 - "_UI_Drainage_Render — 배수유역도 Canvas 렌더러"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, _UI_Drainage_Render — 배수유역도 Canvas 렌더러
### Community 71 - "_UI_Profile_Structures — 종단 구조물 렌더링 및 인터랙션"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, _UI_Profile_Structures — 종단 구조물 렌더링 및 인터랙션
### Community 72 - "_UI_Selection — 배수/구조물 3자 선택 동기화"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, _UI_Selection — 배수/구조물 3자 선택 동기화
### Community 73 - "B06 인접 측점 구조물 트림 정리"
Cohesion: 0.40
Nodes (4): B06 인접 측점 구조물 트림 정리, 검증, 원인과 경계, 처리 항목
### Community 74 - "B06 물넘이포장·콘크리트 포장·독립 기슭막이"
Cohesion: 0.40
Nodes (4): B06 물넘이포장·콘크리트 포장·독립 기슭막이, 검증, 독립 기슭막이, 포장과 물넘이
### Community 75 - "B06_Section_Engine_Areas — 횡단 면적 적분 연산 엔진"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_Engine_Areas — 횡단 면적 적분 연산 엔진
### Community 76 - "B06_Section_Router_Confirm — 임시 저장 및 확정 라우터"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 엔드포인트, 3. 의존성, B06_Section_Router_Confirm — 임시 저장 및 확정 라우터
### Community 77 - "B06_Section_UI_Cross_Areas — 횡단 단면적 표기 및 밴드 하이라이트"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_Cross_Areas — 횡단 단면적 표기 및 밴드 하이라이트
### Community 78 - "B06_Section_UI_Cross_Design — 횡단 측점별 세부 설계 컨트롤"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_Cross_Design — 횡단 측점별 세부 설계 컨트롤
### Community 79 - "B06_Section_UI_MassHaul — 유토곡선 적분 계산 엔진"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_MassHaul — 유토곡선 적분 계산 엔진
### Community 80 - "B06_Section_UI_MassHaul_Balance — 평형선 및 장비 띠 분할 엔진"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 알고리즘, 3. 의존성, B06_Section_UI_MassHaul_Balance — 평형선 및 장비 띠 분할 엔진
### Community 81 - "B06_Section_UI_MassHaul_Balance_View — 운반 띠 시각화 렌더러"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 렌더링, 3. 의존성, B06_Section_UI_MassHaul_Balance_View — 운반 띠 시각화 렌더러
### Community 82 - "B06_Section_UI_MassHaul_Balloon — 물량 말풍선 배치 및 조작"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 알고리즘, 3. 의존성, B06_Section_UI_MassHaul_Balloon — 물량 말풍선 배치 및 조작
### Community 83 - "B06_Section_UI_MassHaul_Curve — 유토곡선 궤적 보간 및 렌더링"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 기하 수학, 3. 의존성, B06_Section_UI_MassHaul_Curve — 유토곡선 궤적 보간 및 렌더링
### Community 84 - "B06_Section_UI_MassHaul_Settle — 토량 정산 및 장거리 상쇄 엔진"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 로직, 3. 의존성, B06_Section_UI_MassHaul_Settle — 토량 정산 및 장거리 상쇄 엔진
### Community 85 - "B06_Section_UI_MassHaul_View — 유토곡선 시각화 렌더러"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_MassHaul_View — 유토곡선 시각화 렌더러
### Community 86 - "B06_Section_UI_Page — B06 메인 페이지 오케스트레이터"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_Page — B06 메인 페이지 오케스트레이터
### Community 87 - "B06_Section_UI_Section_View — 종/횡단 및 유토곡선 뷰 조립"
Cohesion: 0.40
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_Section_View — 종/횡단 및 유토곡선 뷰 조립
### Community 88 - "B07 Quantity — Backend"
Cohesion: 0.40
Nodes (4): B07 Quantity — Backend, 구현되지 않은 항목, 구현된 항목, 책임 경계 미결
### Community 89 - "B08 CAD 블록 라이브러리·사진"
Cohesion: 0.40
Nodes (4): B08 CAD 블록 라이브러리·사진, 검증, 구현, 범위 결정
### Community 90 - "B08 OpenWebCAD 명령 체계"
Cohesion: 0.40
Nodes (4): B08 OpenWebCAD 명령 체계, 검증·제한, 구현 범위, 핵심 구성
### Community 91 - "B08_CAD_table_entity.md"
Cohesion: 0.33
Nodes (5): B08 CAD TableEntity, 검증·후속, 기능, 모델, 이관 범위
### Community 92 - "공개·인증·관리 영역 지도"
Cohesion: 0.29
Nodes (6): B09 원가계산 — 2026-09-09 완료 근거, 가격·조건 입력, 검증 기록, 기계·제비율·산출물, 남은 경계, 단가·밑수 완성
### Community 93 - "공유 자원 영향 지도"
Cohesion: 0.33
Nodes (5): B08 수량산출 — 2026-09-09 완료 근거, 검증 기록, 구조물·수량 표시, 남은 경계, 밑수와 인계
### Community 94 - "A00_Common — 공통 프레임워크 & 유틸"
Cohesion: 0.50
Nodes (3): A00_Common — 공통 프레임워크 & 유틸, 📋 개요, 📂 세분화 마크다운 문서 목록
### Community 95 - "app_shell.ts"
Cohesion: 0.50
Nodes (3): app_shell.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 96 - "router.ts"
Cohesion: 0.50
Nodes (3): router.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 97 - "B04_PreProcess — API"
Cohesion: 0.50
Nodes (3): B04_PreProcess — API, 엔드포인트, 요청/응답 스키마 (Pydantic)
### Community 98 - "B04_PreProcess — Dependencies"
Cohesion: 0.50
Nodes (3): B04_PreProcess — Dependencies, 백엔드 (Python), 프론트엔드 (TypeScript)
### Community 99 - "B05_Profile — Backend"
Cohesion: 0.50
Nodes (3): B05_Profile — Backend, 📂 소스코드 1:1 세분화 위키 파일 목록, 📋 핵심 백엔드 아키텍처 개요
### Community 100 - "B05_Profile — DB 사용 관계"
Cohesion: 0.50
Nodes (3): B05_Profile — DB 사용 관계, Repository 함수, 저장 경로
### Community 101 - "B05_Profile — 3D Viewer & Interaction"
Cohesion: 0.50
Nodes (3): 3D 마커 직접 드래그 이동 (0_old I-401 이식), 3D 지형 뷰포트 시각화 (`_UI_Viewer.ts`, `_UI_Markers.ts`), B05_Profile — 3D Viewer & Interaction
### Community 102 - "B05_Profile_Engine_Sections.md"
Cohesion: 0.40
Nodes (4): B05_Profile_Engine_Sections.py, ⚠️ 런타임 검증 주의사항 (2026-07-24 검증 보고서 기준), 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 103 - "B05_Profile_Api_Fetch.ts"
Cohesion: 0.50
Nodes (3): B05_Profile_Api_Fetch.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 API 함수 목록
### Community 104 - "B05_Profile_UI_Drainage_Panel"
Cohesion: 0.50
Nodes (3): B05_Profile_UI_Drainage_Panel, 📋 개요 및 특징, 🛠️ 주요 함수 / 심볼 목록
### Community 105 - "B05_Profile_UI_Drainage_Pipes"
Cohesion: 0.50
Nodes (3): B05_Profile_UI_Drainage_Pipes, 📋 개요 및 특징, 🛠️ 주요 함수 / 심볼 목록
### Community 106 - "B05_Profile_UI_Profile_Panel.md"
Cohesion: 0.40
Nodes (4): B05_Profile_UI_Profile_Panel.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 기능 및 개선사항 (2026-08-06), 🛠️ 주요 함수 목록
### Community 107 - "B06_Section — DB 사용 관계"
Cohesion: 0.50
Nodes (3): B06_Section — DB 사용 관계, Repository 함수, 파일 경로
### Community 108 - "B06_Section_Api_Fetch — B06 프론트엔드 API 클라이언트"
Cohesion: 0.50
Nodes (3): 1. 개요 및 역할, 2. 주요 연동 API 함수, B06_Section_Api_Fetch — B06 프론트엔드 API 클라이언트
### Community 109 - "B06_Section_UI_Standard_Diagram — 표준단면 모식도 컴포넌트"
Cohesion: 0.50
Nodes (3): 1. 개요 및 역할, 2. 주요 기능, B06_Section_UI_Standard_Diagram — 표준단면 모식도 컴포넌트
### Community 110 - "B06_Section_UI_Standard_Panel — 표준단면 입력 및 제어 패널"
Cohesion: 0.50
Nodes (3): 1. 개요 및 역할, 2. 주요 기능, B06_Section_UI_Standard_Panel — 표준단면 입력 및 제어 패널
### Community 111 - "B08_DesignDetail — Dependencies"
Cohesion: 0.50
Nodes (3): B08_DesignDetail — Dependencies, Backend (requirements.txt), Frontend (package.json / tsconfig.json)
### Community 112 - "common_util_project_delete.md"
Cohesion: 0.50
Nodes (3): common_util_project_delete.py, 🔗 역참조 (사용처), 🛠️ 주요 함수 목록
### Community 113 - "common_util_storage.md"
Cohesion: 0.50
Nodes (3): common_util_storage.py, 🔗 역참조 (사용처), 🛠️ 주요 함수 목록
### Community 114 - "common_util_workflow_state.md"
Cohesion: 0.50
Nodes (3): common_util_workflow_state.py, 🔗 역참조 (사용처), 🛠️ 주요 함수 목록
### Community 116 - "A01_Home_UI_Page.md"
Cohesion: 0.50
Nodes (3): A01_Home_UI_Page.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 및 인터페이스 목록
### Community 117 - "A02_ProgDetail_UI_Page.md"
Cohesion: 0.50
Nodes (3): A02_ProgDetail_UI_Page.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 118 - "A06_Login_Router.md"
Cohesion: 0.50
Nodes (3): A06_Login_Router.py, 🛠️ 라우터 API 및 주요 헬퍼 함수 목록, 🔗 연관 개념 및 의존성
### Community 119 - "A07_Register_Router.md"
Cohesion: 0.50
Nodes (3): A07_Register_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 개념 및 의존성
### Community 120 - "A08_Support_Router.md"
Cohesion: 0.50
Nodes (3): A08_Support_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 개념 및 의존성
### Community 121 - "A09_Security_Router.md"
Cohesion: 0.50
Nodes (3): A09_Security_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 개념 및 의존성
### Community 124 - "B01_Dashboard_Router.md"
Cohesion: 0.50
Nodes (3): B01_Dashboard_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 개념 및 의존성
### Community 125 - "B01_Dashboard_UI_Page.md"
Cohesion: 0.50
Nodes (3): B01_Dashboard_UI_Page.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 126 - "B02_ProjRegister_Router.md"
Cohesion: 0.50
Nodes (3): B02_ProjRegister_Router.py, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 129 - "B03_FileInput_Router.md"
Cohesion: 0.50
Nodes (3): B03_FileInput_Router.py, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 130 - "B04_PreProcess_Router.md"
Cohesion: 0.50
Nodes (3): B04_PreProcess_Router.py, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 132 - "B05_Profile_Engine_Grade.md"
Cohesion: 0.50
Nodes (3): B05_Profile_Engine_Grade.py, 🔗 연관 개념 및 의존성, 🛠️ 주요 클래스 및 함수 목록
### Community 133 - "B05_Profile_Engine_Solver.md"
Cohesion: 0.50
Nodes (3): B05_Profile_Engine_Solver.py, 🛠️ 엔진 핵심 함수 목록, 🔗 연관 개념 및 의존성
### Community 134 - "B05_Profile_Repository.md"
Cohesion: 0.50
Nodes (3): B05_Profile_Repository.py, 🛠️ DB 접근 함수 목록, 🔗 연관 개념 및 의존성
### Community 135 - "B05_Profile_Router.md"
Cohesion: 0.50
Nodes (3): B05_Profile_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 모듈 및 의존성
### Community 136 - "B05_Profile_Router_Confirm.md"
Cohesion: 0.50
Nodes (3): B05_Profile_Router_Confirm.py, 🔗 연관 모듈, 🛠️ 주요 헬퍼 함수 목록
### Community 137 - "B05_Profile_Schema.md"
Cohesion: 0.50
Nodes (3): B05_Profile_Schema.py, 🛠️ Pydantic 모델 및 검증 헬퍼 목록, 🔗 연관 개념 및 의존성
### Community 138 - "B05_Profile_UI_IrregularStations.md"
Cohesion: 0.50
Nodes (3): B05_Profile_UI_IrregularStations.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 인터페이스 및 함수 목록
### Community 139 - "B05_Profile_UI_Page.md"
Cohesion: 0.50
Nodes (3): B05_Profile_UI_Page.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 컴포넌트 및 함수 목록
### Community 140 - "B05_Profile_UI_Panel.md"
Cohesion: 0.50
Nodes (3): B05_Profile_UI_Panel.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 141 - "B05_Profile_UI_Profile_Alignment.md"
Cohesion: 0.50
Nodes (3): B05_Profile_UI_Profile_Alignment.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 142 - "B05_Profile_UI_Profile_Table.md"
Cohesion: 0.50
Nodes (3): B05_Profile_UI_Profile_Table.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 143 - "B05_Profile_UI_Viewer.md"
Cohesion: 0.50
Nodes (3): B05_Profile_UI_Viewer.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
### Community 145 - "B06_Section_Router.md"
Cohesion: 0.50
Nodes (3): B06_Section_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 개념 및 의존성
### Community 146 - "B01~B09 Workflow 데이터 흐름"
Cohesion: 0.40
Nodes (4): 3D 방위와 좌표계, B04 세부유역·방위·좌표계, 세부유역 형상 보존, 종단 높낮이 기반 배정
### Community 158 - "B03 계획노선 정본·좌표계 후속"
Cohesion: 0.40
Nodes (4): B05 구조물 구간 절취·측벽·성토 패치, B06 변형 성토선 패치, 저장·검증, 절취와 경계
### Community 159 - "B03 파일 입력 화면 정리"
Cohesion: 0.40
Nodes (4): B06 횡단 관 형상·유토곡선 후속, I형 집수정 관 형상, 계산 상태와 경고, 파일 책임 분리
### Community 160 - "B04 3D 방위·좌표계 최신 결정"
Cohesion: 0.40
Nodes (4): B08 CAD 기본 조작, 검증·제외, 선택·편집, 입력·상태
### Community 161 - "B04 세부유역·방위·좌표계"
Cohesion: 0.40
Nodes (4): B08 CAD 도각·표제란, 값 공급, 도각 편집·보존, 회사 자산과 담당자
### Community 162 - "B05 구조물 구간 절취·측벽·성토 패치"
Cohesion: 0.40
Nodes (4): B08 CAD 사용자 편의성 정리, 검증 상태, 구현 기록, 사용자 흐름
### Community 163 - "B05 구조물 3D 투영 커브"
Cohesion: 0.50
Nodes (3): B03 계획노선 정본·좌표계 후속, LAS 없는 설계와 업로드 상태, 계획노선 정본
### Community 164 - "B05_Profile — Frontend"
Cohesion: 0.50
Nodes (3): B03 파일 입력 화면 정리, 입력 규칙, 화면 구성
### Community 165 - "B05 유토곡선·구조물 후속"
Cohesion: 0.50
Nodes (3): 3D 방위 위젯, B04 3D 방위·좌표계 최신 결정, 작업 좌표계
### Community 166 - "B05 종단곡선·실시간 횡단 연동"
Cohesion: 0.50
Nodes (3): B05 유토곡선·구조물 후속, 유지·판정 사항, 유토곡선 공용화
### Community 167 - "B06 횡단 계산 미러·카드 표기"
Cohesion: 0.50
Nodes (3): B05 종단곡선·실시간 횡단 연동, 실시간 횡단·유토곡선, 초기 종단곡선
### Community 168 - "B06 횡단 관 형상·유토곡선 후속"
Cohesion: 0.50
Nodes (3): B06 횡단 계산 미러·카드 표기, 프론트 계산 미러, 횡단 카드 표기
### Community 171 - "B08 CAD·납품 도면 후속"
Cohesion: 0.50
Nodes (3): B08 CAD·납품 도면 후속, CAD 편집·확정, 토적도·유역도
### Community 182 - "2026-09-02 완료 항목"
Cohesion: 0.25
Nodes (7): 2026-09-02 완료 항목, B05 구조물 3D, B05 종단 편집 후속, CAD, 노선·지표면, 작업 환경, 회귀 상태
### Community 183 - "2026-09-03 완료 — 입력·배수·종횡단·CAD"
Cohesion: 0.50
Nodes (3): 2026-09-03 완료 — 입력·배수·종횡단·CAD, 공통 결정, 완료 범위
### Community 184 - "2026-09-03 추가 완료 — 화면·종단·횡단"
Cohesion: 0.50
Nodes (3): 2026-09-03 추가 완료 — 화면·종단·횡단, 완료 범위, 최신 결정
## Knowledge Gaps
- **756 isolated node(s):** `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어`, `반드시 유지할 구분`, `미결 설계` (+751 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **34 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Work-memory lessons
**Known dead ends** — questions that led nowhere; don't re-derive.
- "배수관 매설시 각도의 제약조건이 있는지 확인해줘. 임도에서" -> `배수유역 해석 및 세부설계`
- "임도 기술정보DB에서 집수정의 형태정보는 어떤게 있는지 확인해줘." -> `B06 배수관 횡단도 세트`, `유입 구조물 판정`
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **What connects `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어` to the rest of the system?**
_756 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `UI Templates — Localization & Components` be split into smaller, more focused modules?**
_Cohesion score 0.046511627906976744 - nodes in this community are weakly interconnected._
- **Should `인증 / RBAC` be split into smaller, more focused modules?**
_Cohesion score 0.05555555555555555 - nodes in this community are weakly interconnected._
- **Should `A00_Common — App Shell Framework` be split into smaller, more focused modules?**
_Cohesion score 0.07692307692307693 - nodes in this community are weakly interconnected._
- **Should `2026-09-04 완료 항목` be split into smaller, more focused modules?**
_Cohesion score 0.06896551724137931 - nodes in this community are weakly interconnected._
- **Should `2026-08-29 완료 반영` be split into smaller, more focused modules?**
_Cohesion score 0.1 - nodes in this community are weakly interconnected._
- **Should `현재 구현 현황 — 소스 읽기 감사` be split into smaller, more focused modules?**
_Cohesion score 0.058823529411764705 - nodes in this community are weakly interconnected._
@@ -0,0 +1,51 @@
{
"runs": [
{
"date": "2026-08-16T00:00:00+09:00",
"input_tokens": 265124,
"output_tokens": 14991,
"files": 146
},
{
"date": "2026-08-16T12:43:20.106802+00:00",
"input_tokens": 27985,
"output_tokens": 2184,
"files": 22
},
{
"date": "2026-08-16T12:46:45.166477+00:00",
"input_tokens": 5694,
"output_tokens": 1138,
"files": 6
},
{
"date": "2026-08-16T12:48:56.020124+00:00",
"input_tokens": 14784,
"output_tokens": 2835,
"files": 12
},
{
"date": "2026-08-16T14:33:37.811664+00:00",
"input_tokens": 0,
"output_tokens": 0,
"files": 12,
"usage_note": "semantic subagent token usage unavailable from collaboration runtime; graph content extracted and validated"
},
{
"date": "2026-08-20T11:01:22.299635+00:00",
"input_tokens": 0,
"output_tokens": 0,
"files": 10,
"usage_note": "Gemini backend dependency unavailable; host semantic extraction used and collaboration runtime token usage unavailable; 27 nodes/59 edges validated"
},
{
"date": "2026-08-21T09:54:12.581221+00:00",
"input_tokens": 0,
"output_tokens": 0,
"files": 3,
"note": "collaboration runtime did not expose semantic extraction token usage"
}
],
"total_input_tokens": 313587,
"total_output_tokens": 21148
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+44 -27
View File
@@ -1,16 +1,16 @@
# Graph Report - wiki (2026-09-11)
# Graph Report - wiki (2026-09-12)
## Corpus Check
- 206 files · ~57,686 words
- 211 files · ~58,469 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 1170 nodes · 996 edges · 189 communities (155 shown, 34 thin omitted)
- 1191 nodes · 1013 edges · 193 communities (159 shown, 34 thin omitted)
- Extraction: 100% EXTRACTED · 0% INFERRED · 0% AMBIGUOUS
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `e49e4d85`
- Built from commit: `9a9062f0`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -187,6 +187,10 @@
- B08_DesignDetail_Engine_Cad_Basin.py
- B08_DesignDetail_Engine_Cad_MassHaul.py
- B08 CAD·납품 도면 후속
- B05 계획노선 편집 — 2026-09-12
- B06 종횡단 공용화 — 2026-09-12
- B08 수량 근거 표시 — 2026-09-12
- B09 원가 근거 표시 — 2026-09-12
- OpenWebCAD Core
- common_util_mass_haul_settle.ts
- Drainage Watershed (유역도)
@@ -229,9 +233,6 @@
- None detected.
## Hyperedges (group relationships)
- **B03-B08 Workflow Data Flow** — b03_fileinput_route_snapshot_crs, b04_preprocess_drainage_compass_crs, b05_profile_frontend, b06_section_cross_design_ui_2026_09, b08_designdetail_frontend [INFERRED 0.90]
- **Shared Mass Haul Calculation and UI** — b05_profile_masshaul_structure_2026_09, b06_section_masshaul_culvert_2026_09, b08_designdetail_cad_delivery_2026_09 [EXTRACTED 0.85]
- **CAD Delivery and Usability Framework** — b08_designdetail_cad_interaction, b08_designdetail_cad_title_block, b08_designdetail_cad_usability_2026_09_01, b08_designdetail_cad_delivery_2026_09 [EXTRACTED 0.95]
- **LAS-Free Analysis Workflow** — concepts_las_free_sheet_surface, pages_b03_fileinput_backend, pages_b04_preprocess_backend [EXTRACTED 1.00]
- **B08 Drawing Generation Flow** — b08_designdetail_b08_designdetail_engine_cad_masshaul_py, b08_designdetail_b08_designdetail_engine_cad_basin_py, common_util_common_util_mass_haul_settle_ts [EXTRACTED 0.90]
- **Drainage System Workflow** — concepts_drainage_watershed, pages_b05_profile_b05_structures, pages_b06_section_b06_culvert_set, pages_b06_section_b06_culvert_geometry_redesign [EXTRACTED 0.95]
@@ -239,23 +240,23 @@
- **3D Corridor Generation Flow** — pages_b05_profile_b05_corridor_surface, pages_b05_profile_b05_corridor_plan_curves, pages_b05_profile_b05_corridor_cut_fill, pages_b05_profile_b05_corridor_patch_finish [EXTRACTED 0.90]
- **Late Workflow Stages (B07-B09)** — pages_b07_quantity_b07_frontend, pages_b08_designdetail_b08_frontend, pages_b09_estimation_b09_frontend [EXTRACTED 1.00]
## Communities (189 total, 34 thin omitted)
## Communities (193 total, 34 thin omitted)
### Community 0 - "UI Templates — Localization & Components"
Cohesion: 0.05
Nodes (38): 2026-09-04 완료 항목, 700줄 제한 분리, 보존된 미완료 범위, 상시계획서 추가 완료 범위, 완료 범위, 추가 완료 범위, 디자인 시스템 (Design System), 레이아웃 및 둥근 테두리 (Radius & Spacing) (+30 more)
Cohesion: 0.09
Nodes (20): 2026-09-04 완료 항목, 700줄 제한 분리, 보존된 미완료 범위, 상시계획서 추가 완료 범위, 완료 범위, 추가 완료 범위, 1. B04 vs B05 역할 분담 및 일원화, 2. 공용 배수 엔진 및 WAMIS 강우량 연동 (Phase 1~2, 2026-08-13 관측소 전환) (+12 more)
### Community 1 - "인증 / RBAC"
Cohesion: 0.06
Nodes (31): OTP / 비밀번호 및 디바이스 신뢰, 권한 검증 헬퍼 (B01_Dashboard), 라우팅 가드 (frontend.md 5.2), 사용자 상태 생명주기, 사용처 (역참조), 세션 인증 (backend.md 6.3), 역할 (users.role), 인증 / RBAC (+23 more)
Cohesion: 0.09
Nodes (20): OTP / 비밀번호 및 디바이스 신뢰, 권한 검증 헬퍼 (B01_Dashboard), 라우팅 가드 (frontend.md 5.2), 사용자 상태 생명주기, 사용처 (역참조), 세션 인증 (backend.md 6.3), 역할 (users.role), 인증 / RBAC (+12 more)
### Community 2 - "A00_Common — App Shell Framework"
Cohesion: 0.08
Nodes (22): A00_Common — App Shell Framework, app_shell 구성요소, router 라우팅 테이블, A00_Common — 스캐폴드·CSS·종속성, b_page_scaffold, CSS 인젝션, 사용처, 종속성 (+14 more)
Cohesion: 0.05
Nodes (35): A00_Common — App Shell Framework, app_shell 구성요소, router 라우팅 테이블, A00_Common — 스캐폴드·CSS·종속성, b_page_scaffold, CSS 인젝션, 사용처, 종속성 (+27 more)
### Community 3 - "2026-09-04 완료 항목"
Cohesion: 0.07
Nodes (25): API 공통 (여러 페이지가 공유하는 엔드포인트), 공통 오류 응답 포맷 (전 라우터), 워크플로우 상태 조회, 폴링 패턴 (legacy workflow.json 설계; 현재 구현은 workflow-state API 사용), 계산 구현 원칙, 계획노선 규칙, 설계 데이터 생명주기, 정본 세 벌 (+17 more)
Cohesion: 0.04
Nodes (40): API 공통 (여러 페이지가 공유하는 엔드포인트), 공통 오류 응답 포맷 (전 라우터), 워크플로우 상태 조회, 폴링 패턴 (legacy workflow.json 설계; 현재 구현은 workflow-state API 사용), 2026-09-12 체크 완료 항목, 상태 경계, 이관 범위, 자체검증 기록 요약 (+32 more)
### Community 4 - "배수유역 해석 및 세부설계 (Drainage Watershed)"
Cohesion: 0.25
@@ -270,8 +271,8 @@ Cohesion: 0.17
Nodes (11): A09_Security — Backend, API 엔드포인트, DB 저장 (activity_logs), 권한 헬퍼, 마스터(회사 관리자) 전용 — `require_master`, 시스템 관리자 전용 — `require_system_admin`, 요청 스키마 (Pydantic), 의존성 (공통 유틸) (+3 more)
### Community 7 - "2026-08-29 완료 반영"
Cohesion: 0.10
Nodes (17): 2026-08-29 완료 반영, B03 재업로드·B05 최신 조회, B05/B06 구조물 UI 통합, B07↔B08 순서, B07 CAD 고정 척도·횡단 장 배치, B07 CAD 테마, B07 CAD 확대·팬, 배수시설 추천 기준 (+9 more)
Cohesion: 0.08
Nodes (22): 2026-08-29 완료 반영, B03 재업로드·B05 최신 조회, B05/B06 구조물 UI 통합, B07↔B08 순서, B07 CAD 고정 척도·횡단 장 배치, B07 CAD 테마, B07 CAD 확대·팬, 배수시설 추천 기준 (+14 more)
### Community 8 - "DB: 파일/지표면분석 테이블"
Cohesion: 0.18
@@ -849,6 +850,22 @@ Nodes (3): B06 횡단 계산 미러·카드 표기, 프론트 계산 미러, 횡
Cohesion: 0.50
Nodes (3): B08 CAD·납품 도면 후속, CAD 편집·확정, 토적도·유역도
### Community 172 - "B05 계획노선 편집 — 2026-09-12"
Cohesion: 0.50
Nodes (3): B05 계획노선 편집 — 2026-09-12, 저장·공용화, 편집 화면
### Community 173 - "B06 종횡단 공용화 — 2026-09-12"
Cohesion: 0.50
Nodes (3): B06 종횡단 공용화 — 2026-09-12, 완료 체크에서 확인된 구조, 횡단 표시
### Community 174 - "B08 수량 근거 표시 — 2026-09-12"
Cohesion: 0.50
Nodes (3): B08 수량 근거 표시 — 2026-09-12, 구현 진입점, 출처 등급과 표시
### Community 175 - "B09 원가 근거 표시 — 2026-09-12"
Cohesion: 0.50
Nodes (3): B09 원가 근거 표시 — 2026-09-12, 공용 근거 계약, 좌측 패널
### Community 182 - "2026-09-02 완료 항목"
Cohesion: 0.25
Nodes (7): 2026-09-02 완료 항목, B05 구조물 3D, B05 종단 편집 후속, CAD, 노선·지표면, 작업 환경, 회귀 상태
@@ -862,7 +879,7 @@ Cohesion: 0.50
Nodes (3): 2026-09-03 추가 완료 — 화면·종단·횡단, 완료 범위, 최신 결정
## Knowledge Gaps
- **756 isolated node(s):** `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어`, `반드시 유지할 구분`, `미결 설계` (+751 more)
- **767 isolated node(s):** `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어`, `반드시 유지할 구분`, `미결 설계` (+762 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **34 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
@@ -875,17 +892,17 @@ Nodes (3): 2026-09-03 추가 완료 — 화면·종단·횡단, 완료 범위,
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `B07 구조물 표준도 — 조사·합의와 현재 통로` connect `임도기술교본 원문 md 추출 품질 결함` to `현재 구현 현황 — 소스 읽기 감사`, `유토곡선 (Mass Haul Diagram) 계산 명세`?**
_High betweenness centrality (0.003) - this node is a cross-community bridge._
- **What connects `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어` to the rest of the system?**
_756 weakly-connected nodes found - possible documentation gaps or missing edges._
_767 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `UI Templates — Localization & Components` be split into smaller, more focused modules?**
_Cohesion score 0.046511627906976744 - nodes in this community are weakly interconnected._
_Cohesion score 0.08695652173913043 - nodes in this community are weakly interconnected._
- **Should `인증 / RBAC` be split into smaller, more focused modules?**
_Cohesion score 0.05555555555555555 - nodes in this community are weakly interconnected._
_Cohesion score 0.08695652173913043 - nodes in this community are weakly interconnected._
- **Should `A00_Common — App Shell Framework` be split into smaller, more focused modules?**
_Cohesion score 0.07692307692307693 - nodes in this community are weakly interconnected._
_Cohesion score 0.05 - nodes in this community are weakly interconnected._
- **Should `2026-09-04 완료 항목` be split into smaller, more focused modules?**
_Cohesion score 0.06896551724137931 - nodes in this community are weakly interconnected._
_Cohesion score 0.0425531914893617 - nodes in this community are weakly interconnected._
- **Should `2026-08-29 완료 반영` be split into smaller, more focused modules?**
_Cohesion score 0.1 - nodes in this community are weakly interconnected._
- **Should `현재 구현 현황 — 소스 읽기 감사` be split into smaller, more focused modules?**
_Cohesion score 0.058823529411764705 - nodes in this community are weakly interconnected._
_Cohesion score 0.07692307692307693 - nodes in this community are weakly interconnected._
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+29 -4
View File
@@ -175,8 +175,8 @@
"semantic_hash": ""
},
"index.md": {
"mtime": 1789132135.2547836,
"ast_hash": "07f8a8513e83d93de0bf992e1fc4e501",
"mtime": 1789206522.9744911,
"ast_hash": "43fc670a3fdfc5430a47b8721e40dd4b",
"semantic_hash": ""
},
"ingest/index.md": {
@@ -1025,8 +1025,8 @@
"semantic_hash": ""
},
"concepts/multi_environment_safety.md": {
"mtime": 1789132131.6764865,
"ast_hash": "1667aa4c657a4f529de42bcaac3fcd44",
"mtime": 1789206280.4225435,
"ast_hash": "78368c25f2a4c124d7fe1958a66bad8a",
"semantic_hash": ""
},
"concepts/quantity_cost_contract.md": {
@@ -1073,5 +1073,30 @@
"mtime": 1789128567.000224,
"ast_hash": "0b98a093e67d58c91f57ab12971c3da0",
"semantic_hash": ""
},
"concepts/completed_2026-09-12.md": {
"mtime": 1789206851.709459,
"ast_hash": "b4656197391ba87cf54a14fa3945975f",
"semantic_hash": ""
},
"pages/B05_Profile/B05_route_edit_2026_09.md": {
"mtime": 1789206522.9712057,
"ast_hash": "f793738fe66f94f47762c6828ee44fa5",
"semantic_hash": ""
},
"pages/B06_Section/B06_profile_cross_unification_2026_09.md": {
"mtime": 1789206522.9724846,
"ast_hash": "486d417448cda087432b3039be112737",
"semantic_hash": ""
},
"pages/B08_Quantity/B08_provenance_ui_2026_09.md": {
"mtime": 1789206522.9724846,
"ast_hash": "10821b6f0c7f6c19aaf1026e4950c06c",
"semantic_hash": ""
},
"pages/B09_Estimation/B09_provenance_ui_2026_09.md": {
"mtime": 1789206522.9734895,
"ast_hash": "a9537ea5efd23d063508c01721cd7591",
"semantic_hash": ""
}
}
+6 -1
View File
@@ -1,7 +1,7 @@
---
type: index
status: stable
last_updated: 2026-09-11
last_updated: 2026-09-12
---
# Wiki Index
@@ -29,15 +29,19 @@ last_updated: 2026-09-11
- [[B05_Profile/B05_corridor_plan_curves|B05 구조물 3D 투영 커브]] — 평면·비탈 투영, 날개벽 연결과 성토면 절단.
- [[B05_Profile/B05_corridor_cut_fill|B05 구조물 절취·성토 패치]] — 합집합 절취, 측벽, B06 구간별 성토 리본.
- [[B05_Profile/B05_corridor_patch_finish|B05 변형 성토면 마감]] — 패치 스커트, 원지반 재노출, 세월교 날개 역클리핑.
- [[B05_Profile/B05_route_edit_2026_09|B05 계획노선 편집]] — 실시간 길이·측점·등고선·R/L 하한과 B05/B06 공용 편집.
- [[B06_Section/B06_backend|B06 Section]] — 횡단·유토곡선·확정; [[B06_Section/B06_pavement_revetment|물넘이포장]], [[B06_Section/B06_revetment_link_controls|기슭막이 연동·경사·단별 제어]], [[B06_Section/B06_culvert_link_trim|인접 측점 트림]].
- [[B06_Section/B06_profile_cross_unification_2026_09|B06 종횡단 공용화]] — B05 공용 종단 편집·관 스냅샷·횡단 확폭 표시.
- [[B07_DesignDetail/B07_frontend|B07 DesignDetail]] — 상세설계·CAD·도각·설계도면과 [[B07_DesignDetail/B07_standard_drawings_2026_09|구조물 표준도]] 관리.
- [[B08_Quantity/B08_overview_2026_09|B08 Quantity]] — 토공·구조물·자재 수량산출과 B09 인계.
- [[B08_Quantity/B08_2026_09_09_completion|B08 9월 9일 완료]] — 밑수·관 연장·돌 무게와 미확보 경계.
- [[B08_Quantity/B08_provenance_ui_2026_09|B08 수량 근거 표시]] — 출처 등급·칸 호버·개발 환경 색칠과 배포 차단.
- [[B08_DesignDetail/B08_CAD_commands|B08 CAD 명령]] · [[B08_DesignDetail/B08_CAD_blocks|블록]] · [[B08_DesignDetail/B08_CAD_table_entity|표]] · [[B08_DesignDetail/B08_CAD_title_block|도각·표제란]].
- [[B08_DesignDetail/B08_CAD_usability_2026-09-01|B08 CAD 편의성]] — 편집 손실·확정 잠금·도면별 복구 구현 기록과 재검증 대기 상태.
- [[B08_DesignDetail/B08_cross_structure_sheets|B08 횡단 장]] · [[B08_DesignDetail/B08_drawing_masshaul_watershed|토적·유역도]].
- [[B09_Estimation/B09_overview_2026_09|B09 Estimation]] — 단가산출·예산내역·공사원가계산 5표 구현.
- [[B09_Estimation/B09_2026_09_09_completion|B09 9월 9일 완료]] — 단가·수송비·유가·할증·제비율·설계서 구성.
- [[B09_Estimation/B09_provenance_ui_2026_09|B09 원가 근거 표시]] — B08 공용 사전·배지·호버와 접힘 패널.
- [[B10_Payment/B10_frontend|B10 Payment]] · [[B11_Status/B11_frontend|B11 Status]] — 결제·진행상태 목업.
## 로그인 전·관리
@@ -60,6 +64,7 @@ last_updated: 2026-09-11
- 2026-09-04 완료: [[completed_2026-09-04]] — B01 모달·로고, B03 미리보기, 공용 진행단계 패널, B05 3D·종단, B06 횡단, B08 도면, 700줄 제한 분리.
- 2026-09-07 완료: [[completed_2026-09-07]] — 노선·캐시·B01~B07 기능, 성능 종료 기준과 보류 갈래.
- 2026-09-09 완료: [[completed_2026-09-09]] — B05·B06 종횡단, B07 표준도, B08 수량, B09 원가 구현·검증.
- 2026-09-12 체크 완료: [[completed_2026-09-12]] — 계획노선 편집, 종횡단 공용화, 수량·원가 근거 표시; 2차 검증 생략 상태.
- 수량·원가 계약: [[quantity_cost_contract]] — B08 물량과 B09 금액의 책임·저장·금지 규칙.
- 설계 데이터 생명주기: [[design_data_lifecycle]] — 초기값·작업본·브라우저 캐시와 계획노선 재설계 규칙.
- 표준도·입력 미결: [[standard_drawing_cost_inputs]] — 자동 계산·사용자 입력·기준자료·미확보 경계.
+4 -2
View File
@@ -1,8 +1,8 @@
---
type: ingest-register
status: stable
related_pages: ["[[ingest/index]]", "[[completed_2026-09-01]]", "[[completed_2026-09-03]]", "[[completed_2026-09-03_additional]]", "[[completed_2026-09-04]]", "[[multi_environment_safety]]"]
last_updated: 2026-09-11
related_pages: ["[[ingest/index]]", "[[completed_2026-09-01]]", "[[completed_2026-09-03]]", "[[completed_2026-09-03_additional]]", "[[completed_2026-09-04]]", "[[completed_2026-09-12]]", "[[multi_environment_safety]]"]
last_updated: 2026-09-12
---
# 2026-09 계획 인제스트
@@ -19,3 +19,5 @@ last_updated: 2026-09-11
| `docs/raw/plans/2026-09-04_plan_dead_code_cleanup_parked.md` | `parked_not_started` | 위키 완료 범위 제외 |
| `docs/raw/plans/2026-09-04_plan_snapshot_before_cleanup.md` | `referenced_completed_with_open_items_carried_forward` | [[completed_2026-09-04]]; 미완료 21건은 `docs/raw/PLAN.md` 유지 |
| `docs/raw/plans/2026-09-11_plan_창환경_여섯워크트리_링크_깃운영.md` | `referenced_user_verification_waived` | [[multi_environment_safety]] |
| `docs/raw/plans/2026-09-12_plan_checked_complete_items.md` | `referenced_secondary_verification_waived` | [[completed_2026-09-12]] 및 B05·B06·B08·B09 상세 페이지 |
| `docs/raw/plans/2026-09-12b_plan_snapshot_before_checked_cleanup.md` | `referenced_completed_checks_with_open_items_carried_forward` | [[completed_2026-09-12]]; 미완료 체크 39개는 상시 계획서 유지 |
+11 -1
View File
@@ -1,7 +1,7 @@
---
type: log
status: stable
last_updated: 2026-09-11
last_updated: 2026-09-12
---
## [2026-09-11] ingest+lint | 여섯 워크트리·링크·Git 운영 완료 반영
@@ -1445,3 +1445,13 @@ last_updated: 2026-09-11
- 위키 본문 215개에서 비로그 100줄 초과·frontmatter·status·page_id 오류는 모두 0건이며 세 원문은 각각 5·4·3곳에 연결됐다.
- `graphify update .`가 Markdown 9개를 코드로 오인해 기존 992노드 그래프를 1,168노드로 팽창시켰으므로 산출물을 직전 정제본으로 복구했다.
- 신규 `design_data_lifecycle` 의미 노드 반영은 Graphify Markdown 증분 결함 해소 후 다시 수행해야 한다.
## [2026-09-12] ingest+plan-cleanup | 체크리스트 전체 완료 항목 위키화
- 상시 계획서의 체크 완료 상위 8개와 완료 하위 항목 1개를 `2026-09-12_plan_checked_complete_items.md`로 원문 이관했다.
- 계획노선 편집, B05·B06 종횡단 공용화, B08·B09 근거 표시를 [[completed_2026-09-12]]와 페이지별 문서에 반영했다.
- 미완료 체크가 섞인 `3-17 횡단도 손질` 등은 상시 계획서에 유지했다.
- 사용자 지시에 따라 위키 관리자의 2차 교차검증은 생략하고, `검증 대기`·`보류 확정` 경계를 그대로 기록했다.
- 린트에서 이번 변경분의 frontmatter·status·page_id·신규 링크·원본 연결은 정상이다. 기존 `completed_2026-08-29.md``B04_backend.md`의 100줄 초과는 이번 범위 밖이라 남겼다.
- `graphify update .` 후 그래프는 1,170노드에서 1,191노드·1,013관계·193커뮤니티로 갱신됐고, 신규 완료·B05·B06 페이지가 질의되는 것을 확인했다.
- 후속 정리에서 혼합 섹션에 남아 있던 완료 체크 44개를 `2026-09-12b_plan_snapshot_before_checked_cleanup.md` 전체 원문으로 보존하고 상시 계획서에서 제거했다.
- 정리 후 상시 계획서는 완료 체크 0개, 미완료 체크 39개다. 체크박스 없는 사용자 확정·설계 배경과 보류 문맥은 임의로 삭제하지 않았다.
@@ -0,0 +1,29 @@
---
type: page-feature
status: draft
page_id: B05_Profile
related_pages: ["[[B05_Profile/B05_frontend]]", "[[B06_Section/B06_profile_cross_unification_2026_09]]", "[[completed_2026-09-12]]", "[[design_data_lifecycle]]"]
last_updated: 2026-09-12
source: ["docs/raw/plans/2026-09-12_plan_checked_complete_items.md"]
---
# B05 계획노선 편집 — 2026-09-12
## 편집 화면
- 폴리라인 길이, BP·EP·규칙 측점, 등고선 높이와 선택 강조를 편집 중 표시한다.
- 곡선 R과 직선 L은 설계 기준 하한에서 막고, 부족 노드는 붉은 표시와 부족량을 함께 낸다.
- 두 노선 지점의 노선상 거리와 종단기울기는 두 번째 점을 찍을 때만 서버 표고 샘플러를 호출한다.
- 확정 지표면 모델이 있으면 LAS 등고선을, 없으면 도엽 등고선을 사용한다.
- 클로소이드·접선장은 값이 없으면 숨기며, 현재 선형은 단순 원곡선이다.
## 저장·공용화
| 항목 | 위치 | 역할 |
|---|---|---|
| `formatStation` | `B05_Profile/B05_Profile_Util_Station.ts` | 실시간 측점 라벨 공용 변환 |
| `build_surface_sampler` | B04 공용 지표면 샘플러 | 두 점의 지반고 조회에 재사용 |
| `B05_Profile_UI_Route_Edit.ts` | `B05_Profile/` | 편집 모달 조립; 700줄 제한 아래로 분리 |
B05와 B06의 관·구조물 편집은 같은 부품과 스냅샷을 사용한다. 2차 교차검증은 사용자 지시로 생략되어 현재 상태는 `draft`다.
@@ -0,0 +1,27 @@
---
type: page-feature
status: draft
page_id: B06_Section
related_pages: ["[[B06_Section/B06_frontend]]", "[[B06_Section/B06_cross_design_ui_2026_09]]", "[[B05_Profile/B05_route_edit_2026_09]]", "[[completed_2026-09-12]]"]
last_updated: 2026-09-12
source: ["docs/raw/plans/2026-09-12_plan_checked_complete_items.md"]
---
# B06 종횡단 공용화 — 2026-09-12
## 완료 체크에서 확인된 구조
- 종단 그래프는 B06의 `createLongitudinalProfile()`을 B05가 함께 사용한다.
- 구조물 우클릭 추가·삭제, 측점선·알약 이동, 좌측 구조물 폼은 공용 경로를 탄다.
- 관 추가·삭제·이동은 B05와 같은 `pipes` 스냅샷에 저장하며 옛 이동 예약만 일회 배출한다.
- 어느 페이지에서 저장해도 같은 결과가 되도록 스냅샷과 델타의 저장 순서를 맞췄다.
- `B06_Section_UI_Section_View.ts`는 그리기 책임을 분리해 693줄로 기록됐다.
## 횡단 표시
- 노면 끝에 차도·노견 틱과 노견 폭 라벨을 표시한다.
- 확폭은 계획 곡선표의 반경을 쓰고 곡선 앞뒤 10m에서 0으로 잇는다. 새 측점은 만들지 않는다.
- 중심선은 계획고 0점에 유지하고, 조정창은 면적표 아래에 둔다.
미완료 `B06_Section_UI_Page.ts` 700줄 초과 항목은 상시 계획서에 남아 있다. 2차 교차검증 생략으로 이 페이지는 `draft`다.
@@ -0,0 +1,28 @@
---
type: page-feature
status: draft
page_id: B08_Quantity
related_pages: ["[[B08_Quantity/B08_overview_2026_09]]", "[[B09_Estimation/B09_provenance_ui_2026_09]]", "[[quantity_cost_contract]]", "[[completed_2026-09-12]]"]
last_updated: 2026-09-12
source: ["docs/raw/plans/2026-09-12_plan_checked_complete_items.md"]
---
# B08 수량 근거 표시 — 2026-09-12
## 출처 등급과 표시
- 등급은 `input`, `survey`, `standard`, `calc`, `final`, `blocked`, `excluded`, `unclassified` 여덟 가지다.
- 평소에는 색칠을 끄고 개발 환경의 토글로 셀 왼쪽 띠와 배경을 표시한다.
- 숫자 칸 호버 카드에 등급·값·식·원천과 개발 빌드의 파일 위치를 낸다.
- 열 단위 사전은 서버가 제공하고 칸별 예외가 열 기본값을 덮는다. 배포 환경은 사전 자체를 보내지 않는다.
## 구현 진입점
| 항목 | 위치 | 역할 |
|---|---|---|
| `B08_Quantity_Provenance.py` | `B08_Quantity/` | 시트·열·칸의 근거 사전 조립 |
| `common_util_provenance.py` | `common_util/` | 공통 등급 정의와 유효성 검사 |
| `B08_Quantity_UI_Provenance.ts` | `B08_Quantity/` | 배지·호버 카드·색칠 토글 |
실화면 자체검증은 여섯 장 1,914칸에 등급이 붙은 것으로 기록됐다. 위키 관리자의 2차 교차검증은 생략됐다.
@@ -0,0 +1,25 @@
---
type: page-feature
status: draft
page_id: B09_Estimation
related_pages: ["[[B09_Estimation/B09_overview_2026_09]]", "[[B08_Quantity/B08_provenance_ui_2026_09]]", "[[quantity_cost_contract]]", "[[completed_2026-09-12]]"]
last_updated: 2026-09-12
source: ["docs/raw/plans/2026-09-12_plan_checked_complete_items.md"]
---
# B09 원가 근거 표시 — 2026-09-12
## 공용 근거 계약
- B09는 B08과 같은 배지·띠·호버 카드 UI를 사용한다.
- `unit-prices`, `base-data`, `price-sources`, `basis-sheet`, `bill` 응답은 같은 근거 사전을 공유한다.
- 실제 화면 기록에서 사전 16장, 일위대가 1,740칸, 산출기초 623칸, 원가계산서 130칸을 확인했다.
- 같은 금액 열에서도 총원가는 `final`, 재료비는 `calc`처럼 칸 의미에 따라 등급이 갈린다.
## 좌측 패널
- 공사 조건·요율 판·관급자재·이윤 조정·수량 상자는 공용 접힘 UI를 쓴다.
- 본문만 스크롤하고 재계산·확정 버튼은 패널 바닥에 고정한다.
계획서의 완료 체크와 실제 화면 자체검증을 반영했으며, 사용자의 지시에 따라 별도 2차 교차검증은 하지 않았다.
@@ -0,0 +1,95 @@
/* **(R·L)** `B05_Profile_UI_RouteEdit_Edits.ts`
* 자리에서 트랜스파일해 Node 돌린다. 2026-09-12
*
* 확장자가 `.cjs` 까닭은 `helper_b05_cut_merged_loop.cjs` 머리에 적힌 것과 같다
* 루트 `package.json` `"type": "module"` 때문에 `.js` 환경에 따라 ESM 으로 읽힌다.
*/
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();
/** TS 한 장을 CommonJS 로 옮겨 실행한다. 상대 import 는 같은 길로 다시 들어온다. */
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) =>
request.startsWith(".")
? loadTs(path.join(path.dirname(full), `${request}.ts`))
: require(request);
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
const edits = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Edits.ts"));
const { applyCurveLimits, curveShortfalls, radiusFloorM } = edits;
const LIMIT_R = 12;
const LIMIT_L = 5;
/** 내각(도)을 이루는 세 점 — 가운데가 꺾임점이고 앞뒤 팔 길이는 넉넉히 200m 다. */
function cornerAt(innerDeg) {
const half = ((180 - innerDeg) / 2) * (Math.PI / 180);
const arm = 200;
return [
[-arm * Math.cos(half), arm * Math.sin(half)],
[0, 0],
[arm * Math.cos(half), arm * Math.sin(half)],
];
}
const floors = {
// 내각 140° (교각 40°) — L 5m 은 R 7.16m 이면 차므로 R 하한 12m 이 이긴다.
bend140: radiusFloorM(140, LIMIT_R, LIMIT_L),
// 내각 150° 에 L 하한을 20m 로 키우면 L 쪽이 이긴다(20 / 0.5236 = 38.2m).
bend150LongArc: radiusFloorM(150, LIMIT_R, 20),
// 내각 170° — 별표2 상 곡선 생략 가능 자리라 L 하한을 안 건다(안 걸면 28.6m 로 부푼다).
bend170: radiusFloorM(170, LIMIT_R, LIMIT_L),
bend170NoExempt: LIMIT_L / ((180 - 170) * (Math.PI / 180)),
// 경계값 — 155° 는 「이상」이라 생략 가능 쪽이다.
bend155: radiusFloorM(155, LIMIT_R, LIMIT_L),
bend154: radiusFloorM(154, LIMIT_R, 20),
// 내각을 모르면 R 하한만 본다.
unknown: radiusFloorM(null, LIMIT_R, LIMIT_L),
};
// 지정값 끌어올리기 — 내각 150°(L 하한 20m) 자리는 하한까지 오르고, 비워 둔 자리는 그대로.
const planned = cornerAt(150);
const radiusSpecified = [null, 12, null];
applyCurveLimits(planned, [true, true, true], radiusSpecified, LIMIT_R, 20);
const radiusAuto = [null, null, null];
applyCurveLimits(planned, [true, true, true], radiusAuto, LIMIT_R, 20);
// 곡선을 지운 자리는 안 건드린다.
const radiusCurveOff = [null, 12, null];
applyCurveLimits(planned, [true, false, true], radiusCurveOff, LIMIT_R, 20);
// 모자란 양 — 내각 150° 는 L 하한(20m)까지 재고, 170° 는 R 하한만 잰다.
const shortfalls = curveShortfalls(
[
{ inner_angle_deg: 150, radius_m: 12, tangent_m: null, violations: [] },
{ inner_angle_deg: 170, radius_m: 12, tangent_m: null, violations: [] },
{ inner_angle_deg: 150, radius_m: 40, tangent_m: null, violations: [] },
],
LIMIT_R,
20,
);
process.stdout.write(
JSON.stringify({
floors,
applied: {
specified: radiusSpecified[1],
auto: radiusAuto[1],
curveOff: radiusCurveOff[1],
},
shortfalls,
}),
);
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
"""계획노선 편집의 **곡선 하한(R·L)** 회귀검증 — 2026-09-12.
사용자 확정: 반경과 길이가 트레이드오프기는 하지만 ** 지켜야** 한다.
L = R·Δ 자리의 하한은 반지름 하나로 모이고, 값을 `radiusFloorM` 낸다.
예외 하나 **내각 155° 이상은 곡선을 설치하지 않을 있다**(별표2 .2..(1)).
거기서 L 채우라고 하면 교각이 0 가까워 R 수십 배로 부풀고, 아무것도 고쳤는데
노선이 휜다(실측: 내각 174.8° 자리가 R 12m 55m). 그래서 자리는 R 하한만 본다.
Node 헬퍼(`helper_b05_curve_radius_floor.cjs`) TS 자리에서 트랜스파일해 돌리고,
여기서 값을 판정한다.
"""
import json
import os
import subprocess
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
@pytest.fixture(scope="module")
def results():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_b05_curve_radius_floor.cjs")],
capture_output=True,
text=True,
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_R_하한이_이기는_자리는_R_하한_그대로(results):
"""내각 140° 면 R 12m 만 지켜도 L 이 8.4m 라 L 하한(5m)이 안 건다."""
assert results["floors"]["bend140"] == pytest.approx(12.0)
def test_L_하한이_이기면_R_을_끌어올린다(results):
"""내각 150°(교각 30°)에 L 하한 20m 면 R 은 38.2m 이어야 한다 — 여기가 「둘 다」의 핵심."""
assert results["floors"]["bend150LongArc"] == pytest.approx(20 / (30 * 3.141592653589793 / 180))
assert results["floors"]["bend150LongArc"] > 12.0
def test_내각_155도_이상은_L_하한을_안_건다(results):
"""곡선 생략이 되는 자리다 — 걸었다면 R 이 28.6m 로 부풀어 노선이 조용히 휜다."""
assert results["floors"]["bend170"] == pytest.approx(12.0)
assert results["floors"]["bend170NoExempt"] > 28.0 # 예외가 없을 때의 값(참고)
assert results["floors"]["bend155"] == pytest.approx(12.0)
def test_155도_바로_아래는_L_하한을_건다(results):
"""경계는 「155° 이상」이라 154° 는 지켜야 하는 쪽이다."""
assert results["floors"]["bend154"] > 12.0
def test_내각을_모르면_R_하한만_본다(results):
assert results["floors"]["unknown"] == pytest.approx(12.0)
def test_지정한_반지름만_끌어올린다(results):
"""비워 둔(자동) 자리는 안 건드린다 — 건드리면 안 고쳤는데 「R 지정」이 늘어난다."""
applied = results["applied"]
assert applied["specified"] == pytest.approx(20 / (30 * 3.141592653589793 / 180))
assert applied["auto"] is None
assert applied["curveOff"] == 12 # 곡선을 지운 자리는 그대로
def test_모자란_양도_R과_L을_함께_잰다(results):
"""내각 150°(하한 38.2m)에서 R 12m 는 26.2m 모자라고, 170° 는 곡선 생략 자리라 0 이다."""
short = results["shortfalls"]
assert short[0] == pytest.approx(20 / (30 * 3.141592653589793 / 180) - 12)
assert short[1] == 0
assert short[2] == 0 # R 40m 는 하한을 넘겼다