Merge remote-tracking branch 'origin/dev' into sub_desktop_1
This commit is contained in:
@@ -42,14 +42,14 @@ export const ROUTE_LINE_WIDTH = 2.4;
|
||||
* 렌더 시 "화면 오차 < LOD_PX가 되는 정점"만 제외해 어느 줌에서도 시각적 무손실 LOD를 얻는다.
|
||||
* line 파트에만 존재하며 원본 GeoJSON은 변형하지 않는다.
|
||||
*/
|
||||
type PreparedPart = {
|
||||
export type PreparedPart = {
|
||||
coords: Float64Array;
|
||||
closed: boolean;
|
||||
weights: Float64Array | null;
|
||||
};
|
||||
|
||||
/** 사전 투영된 피처 1개. bbox는 정규화 좌표 기준이며 컬링에 사용한다. */
|
||||
type PreparedFeature = {
|
||||
export type PreparedFeature = {
|
||||
kind: "line" | "point";
|
||||
parts: PreparedPart[];
|
||||
minX: number;
|
||||
@@ -60,6 +60,8 @@ type PreparedFeature = {
|
||||
labelAnchorX: number;
|
||||
labelAnchorY: number;
|
||||
labelText: string | null;
|
||||
/** 그 라벨의 표고(m). 어느 줄을 실제로 낼지는 `drawPreparedLabels` 가 줌을 보고 고른다. */
|
||||
labelValue: number | null;
|
||||
};
|
||||
|
||||
export type PreparedLayer = {
|
||||
@@ -214,226 +216,81 @@ export function computeRouteView(
|
||||
};
|
||||
}
|
||||
|
||||
function isPoint(value: unknown): value is [number, number] {
|
||||
return Array.isArray(value) && typeof value[0] === "number" && typeof value[1] === "number";
|
||||
}
|
||||
|
||||
/** lon/lat 배열 → 정규화 좌표 Float64Array. 유효 정점이 없으면 null. */
|
||||
function projectRing(ring: unknown, normalizer: Normalizer): Float64Array | null {
|
||||
if (!Array.isArray(ring) || ring.length === 0) return null;
|
||||
const coords = new Float64Array(ring.length * 2);
|
||||
let count = 0;
|
||||
for (const point of ring) {
|
||||
if (!isPoint(point)) continue;
|
||||
coords[count * 2] = (point[0] - normalizer.lonMin) / normalizer.lonRange;
|
||||
coords[count * 2 + 1] = 1 - (point[1] - normalizer.latMin) / normalizer.latRange;
|
||||
count += 1;
|
||||
}
|
||||
if (count === 0) return null;
|
||||
return count * 2 === coords.length ? coords : coords.slice(0, count * 2);
|
||||
}
|
||||
|
||||
function collectParts(
|
||||
geometry: GeoJsonGeometry,
|
||||
normalizer: Normalizer,
|
||||
parts: PreparedPart[],
|
||||
): "line" | "point" {
|
||||
const coordinates = geometry.coordinates;
|
||||
if (!Array.isArray(coordinates)) return "line";
|
||||
const push = (ring: unknown, closed: boolean): void => {
|
||||
const projected = projectRing(ring, normalizer);
|
||||
if (projected) parts.push({ coords: projected, closed, weights: null });
|
||||
};
|
||||
switch (geometry.type) {
|
||||
case "Point":
|
||||
push([coordinates], false);
|
||||
return "point";
|
||||
case "MultiPoint":
|
||||
push(coordinates, false);
|
||||
return "point";
|
||||
case "LineString":
|
||||
push(coordinates, false);
|
||||
return "line";
|
||||
case "MultiLineString":
|
||||
for (const line of coordinates) push(line, false);
|
||||
return "line";
|
||||
case "Polygon":
|
||||
for (const ring of coordinates) push(ring, true);
|
||||
return "line";
|
||||
case "MultiPolygon":
|
||||
for (const polygon of coordinates) {
|
||||
if (!Array.isArray(polygon)) continue;
|
||||
for (const ring of polygon) push(ring, true);
|
||||
}
|
||||
return "line";
|
||||
default:
|
||||
return "line";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Douglas-Peucker 가중치 계산 (반복형, 스택 오버플로 방지).
|
||||
* weights[i] = "허용 오차가 이 값보다 크면 정점 i를 버려도 되는" 임계값.
|
||||
* 부모 구간의 오차로 상한을 걸어(cap) 어떤 허용 오차에서도 일관된 부분집합이 나오게 한다.
|
||||
* y축은 1/aspect로 보정해 화면 픽셀 거리와 비례하는 좌표계에서 계산한다.
|
||||
*/
|
||||
function computeDpWeights(coords: Float64Array, aspect: number): Float64Array {
|
||||
const n = coords.length / 2;
|
||||
const weights = new Float64Array(n);
|
||||
weights[0] = Infinity;
|
||||
weights[n - 1] = Infinity;
|
||||
if (n <= 2) return weights;
|
||||
const stack: number[] = [0, n - 1];
|
||||
const caps: number[] = [Infinity];
|
||||
while (stack.length) {
|
||||
const last = stack.pop()!;
|
||||
const first = stack.pop()!;
|
||||
const cap = caps.pop()!;
|
||||
if (last - first < 2) continue;
|
||||
const ax = coords[first * 2];
|
||||
const ay = coords[first * 2 + 1] / aspect;
|
||||
const bx = coords[last * 2];
|
||||
const by = coords[last * 2 + 1] / aspect;
|
||||
const dx = bx - ax;
|
||||
const dy = by - ay;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
let maxDist = -1;
|
||||
let maxIndex = -1;
|
||||
for (let i = first + 1; i < last; i += 1) {
|
||||
const px = coords[i * 2] - ax;
|
||||
const py = coords[i * 2 + 1] / aspect - ay;
|
||||
const dist = len === 0 ? Math.sqrt(px * px + py * py) : Math.abs(px * dy - py * dx) / len;
|
||||
if (dist > maxDist) {
|
||||
maxDist = dist;
|
||||
maxIndex = i;
|
||||
}
|
||||
}
|
||||
const weight = Math.min(maxDist, cap);
|
||||
weights[maxIndex] = weight;
|
||||
stack.push(first, maxIndex, maxIndex, last);
|
||||
caps.push(weight, weight);
|
||||
}
|
||||
return weights;
|
||||
}
|
||||
|
||||
/** 등고 라벨 앵커: LineString/MultiLineString 첫 파트의 중앙 정점 (기존 동작 유지). */
|
||||
function labelAnchorOf(geometry: GeoJsonGeometry, normalizer: Normalizer): [number, number] | null {
|
||||
const coords = geometry.coordinates;
|
||||
if (!Array.isArray(coords)) return null;
|
||||
const line =
|
||||
geometry.type === "LineString"
|
||||
? coords
|
||||
: geometry.type === "MultiLineString"
|
||||
? coords[0]
|
||||
: null;
|
||||
if (!Array.isArray(line) || line.length === 0) return null;
|
||||
const mid = line[Math.floor(line.length / 2)];
|
||||
if (!isPoint(mid)) return null;
|
||||
return [
|
||||
(mid[0] - normalizer.lonMin) / normalizer.lonRange,
|
||||
1 - (mid[1] - normalizer.latMin) / normalizer.latRange,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GeoJSON 컬렉션 1개를 사전 투영한다.
|
||||
* labelKeys가 주어지면 계곡선(25m 배수) 피처에만 라벨 텍스트·앵커를 계산해 둔다.
|
||||
*/
|
||||
export function prepareLayer(
|
||||
collection: GeoJsonCollection | undefined,
|
||||
normalizer: Normalizer,
|
||||
labelKeys?: string[],
|
||||
): PreparedLayer {
|
||||
const features: PreparedFeature[] = [];
|
||||
for (const feature of collection?.features ?? []) {
|
||||
if (!feature.geometry) continue;
|
||||
const parts: PreparedPart[] = [];
|
||||
const kind = collectParts(feature.geometry, normalizer, parts);
|
||||
if (parts.length === 0) continue;
|
||||
if (kind === "line") {
|
||||
for (const part of parts) {
|
||||
if (part.coords.length < 6) continue;
|
||||
part.weights = computeDpWeights(part.coords, normalizer.aspect);
|
||||
}
|
||||
}
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
for (const part of parts) {
|
||||
/** 화면 px 에 가장 가까운 선 피처의 자리. 그만큼 안에 없으면 -1(계획서 0-9 ⑦). */
|
||||
export function hitPreparedLayer(
|
||||
layer: PreparedLayer,
|
||||
view: ViewState,
|
||||
px: number,
|
||||
py: number,
|
||||
tolerancePx: number,
|
||||
everyM?: number,
|
||||
): number {
|
||||
const affine = affineOf(view);
|
||||
const step = everyM !== undefined && everyM > 0 ? everyM : 0;
|
||||
let best = -1;
|
||||
let bestDistance = tolerancePx;
|
||||
layer.features.forEach((feature, index) => {
|
||||
if (feature.kind !== "line") return;
|
||||
// **그리지 않은 줄은 집히지도 않는다** — 안 보이는 등고선이 골라지면 없던 선이 튀어나온다.
|
||||
if (step && feature.labelValue !== null && feature.labelValue % step !== 0) return;
|
||||
// 화면 밖·멀리 있는 피처는 바운딩박스에서 먼저 떨군다 — 도엽 등고선은 수천 가닥이다.
|
||||
const x0 = feature.minX * affine.ax + affine.bx - tolerancePx;
|
||||
const x1 = feature.maxX * affine.ax + affine.bx + tolerancePx;
|
||||
const y0 = feature.minY * affine.ay + affine.by - tolerancePx;
|
||||
const y1 = feature.maxY * affine.ay + affine.by + tolerancePx;
|
||||
if (px < x0 || px > x1 || py < y0 || py > y1) return;
|
||||
for (const part of feature.parts) {
|
||||
const coords = part.coords;
|
||||
let lastX = NaN;
|
||||
let lastY = NaN;
|
||||
// 그릴 때와 **같은 LOD** 로 훑는다 — 화면에 없는 정점에 걸리면 눈과 손이 어긋난다.
|
||||
const tolerance = LOD_PX / affine.ax;
|
||||
for (let i = 0; i < coords.length; i += 2) {
|
||||
const x = coords[i];
|
||||
const y = coords[i + 1];
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
}
|
||||
let labelText: string | null = null;
|
||||
let labelAnchorX = 0;
|
||||
let labelAnchorY = 0;
|
||||
if (labelKeys && labelKeys.length > 0) {
|
||||
const raw = labelKeys.map((key) => feature.properties?.[key]).find((value) => value != null);
|
||||
const elevation = typeof raw === "number" ? raw : Number(raw);
|
||||
// 계곡선(25m 배수)만 라벨 — 전체 표기 시 화면이 숫자로 뒤덮이는 것 방지
|
||||
if (Number.isFinite(elevation) && elevation % 25 === 0) {
|
||||
const anchor = labelAnchorOf(feature.geometry, normalizer);
|
||||
if (anchor) {
|
||||
labelText = String(elevation);
|
||||
labelAnchorX = anchor[0];
|
||||
labelAnchorY = anchor[1];
|
||||
if (part.weights && part.weights[i / 2] < tolerance) continue;
|
||||
const x = coords[i] * affine.ax + affine.bx;
|
||||
const y = coords[i + 1] * affine.ay + affine.by;
|
||||
if (Number.isFinite(lastX)) {
|
||||
const distance = pointSegmentDistance(px, py, lastX, lastY, x, y);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = index;
|
||||
}
|
||||
}
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
}
|
||||
}
|
||||
features.push({ kind, parts, minX, minY, maxX, maxY, labelAnchorX, labelAnchorY, labelText });
|
||||
}
|
||||
return { features };
|
||||
});
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사업지 좌표계(m) 폴리라인을 한 개 피처짜리 레이어로 사전 투영한다.
|
||||
* meta의 x/y 범위와 lon/lat 범위는 같은 사각형을 가리키므로, 미터 좌표도 GeoJSON과 동일한
|
||||
* 정규화 공간으로 들어간다 — 노선 선형을 도엽 레이어 위에 그대로 겹칠 수 있다.
|
||||
*/
|
||||
export function prepareMetricPolyline(
|
||||
points: ReadonlyArray<{ x: number; y: number }>,
|
||||
meta: VWorldMeta,
|
||||
): PreparedLayer {
|
||||
if (points.length < 2) return { features: [] };
|
||||
const widthMeters = meta.width_meters || 1;
|
||||
const heightMeters = meta.height_meters || 1;
|
||||
const coords = new Float64Array(points.length * 2);
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
points.forEach((point, index) => {
|
||||
const nx = (point.x - meta.x_min) / widthMeters;
|
||||
const ny = 1 - (point.y - meta.y_min) / heightMeters;
|
||||
coords[index * 2] = nx;
|
||||
coords[index * 2 + 1] = ny;
|
||||
if (nx < minX) minX = nx;
|
||||
if (nx > maxX) maxX = nx;
|
||||
if (ny < minY) minY = ny;
|
||||
if (ny > maxY) maxY = ny;
|
||||
});
|
||||
return {
|
||||
features: [
|
||||
{
|
||||
kind: "line",
|
||||
parts: [{ coords, closed: false, weights: null }],
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
labelAnchorX: 0,
|
||||
labelAnchorY: 0,
|
||||
labelText: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
/** 레이어 안의 피처 하나만 다시 그린다 — 고른 등고선을 도드라지게 할 때 쓴다. */
|
||||
export function drawPreparedFeature(
|
||||
context: CanvasRenderingContext2D,
|
||||
layer: PreparedLayer,
|
||||
index: number,
|
||||
view: ViewState,
|
||||
): void {
|
||||
const feature = layer.features[index];
|
||||
if (!feature || feature.kind !== "line") return;
|
||||
drawLineParts(context, feature, affineOf(view));
|
||||
}
|
||||
|
||||
/** 점과 선분 사이 거리(px). */
|
||||
function pointSegmentDistance(
|
||||
px: number,
|
||||
py: number,
|
||||
ax: number,
|
||||
ay: number,
|
||||
bx: number,
|
||||
by: number,
|
||||
): number {
|
||||
const dx = bx - ax;
|
||||
const dy = by - ay;
|
||||
const lengthSquared = dx * dx + dy * dy;
|
||||
if (lengthSquared <= 1e-9) return Math.hypot(px - ax, py - ay);
|
||||
const ratio = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / lengthSquared));
|
||||
return Math.hypot(px - (ax + dx * ratio), py - (ay + dy * ratio));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -563,6 +420,9 @@ function drawPointParts(
|
||||
|
||||
/** 컬링 여백: 선 굵기·X 마커 팔 길이·라벨 폭을 감안한 화면 밖 판정 마진(px). */
|
||||
const CULL_MARGIN = 32;
|
||||
/** 등고 라벨끼리 이만큼(px)은 떨어져야 둘 다 낸다 — 가로 여백과 줄 높이. */
|
||||
const LABEL_GAP_PX = 10;
|
||||
const LABEL_ROW_PX = 14;
|
||||
|
||||
function isVisible(feature: PreparedFeature, affine: Affine, view: ViewState): boolean {
|
||||
const margin = CULL_MARGIN;
|
||||
@@ -578,36 +438,100 @@ function isVisible(feature: PreparedFeature, affine: Affine, view: ViewState): b
|
||||
);
|
||||
}
|
||||
|
||||
/** 레이어 1개를 그린다. context의 lineWidth/strokeStyle은 호출부에서 설정한다. */
|
||||
/** 등고선을 몇 m 마다 낼지 고를 때 훑는 배수. 성긴 쪽으로 한 칸씩 물러난다. */
|
||||
const LEVEL_STEP_MULTIPLES = [1, 2, 5, 10, 20, 50, 100];
|
||||
/** 한 화면에 둘 등고선 가닥 수의 어림 상한 — 이보다 많으면 한 칸 성글게 간다. */
|
||||
const LEVEL_BUDGET = 350;
|
||||
|
||||
/**
|
||||
* 지금 화면에 **몇 m 간격**으로 등고선을 낼지 고른다.
|
||||
*
|
||||
* 간격을 줌으로만 정하면 가파른 데서는 여전히 선이 뭉개지고 완만한 데서는 너무 성기다.
|
||||
* 그래서 **지금 화면에 실제로 들어오는 가닥 수**를 세어 상한을 넘지 않는 가장 촘촘한 간격을
|
||||
* 고른다 — 확대하면 저절로 촘촘해지고 물러나면 성겨진다(2026-09-12 실화면: 1m LAS 등고선을
|
||||
* 다 그리면 지형이 선으로 덮였다).
|
||||
*/
|
||||
export function pickLevelStep(
|
||||
layer: PreparedLayer,
|
||||
view: ViewState,
|
||||
intervalM: number,
|
||||
budget = LEVEL_BUDGET,
|
||||
): number {
|
||||
const interval = intervalM > 0 ? intervalM : 1;
|
||||
const affine = affineOf(view);
|
||||
let step = interval * LEVEL_STEP_MULTIPLES[LEVEL_STEP_MULTIPLES.length - 1];
|
||||
for (const multiple of LEVEL_STEP_MULTIPLES) {
|
||||
const candidate = interval * multiple;
|
||||
let count = 0;
|
||||
for (const feature of layer.features) {
|
||||
if (feature.labelValue !== null && feature.labelValue % candidate !== 0) continue;
|
||||
if (!isVisible(feature, affine, view)) continue;
|
||||
count += 1;
|
||||
if (count > budget) break;
|
||||
}
|
||||
if (count <= budget) return candidate;
|
||||
step = candidate;
|
||||
}
|
||||
return step;
|
||||
}
|
||||
|
||||
/** 레이어 1개를 그린다. context의 lineWidth/strokeStyle은 호출부에서 설정한다.
|
||||
*
|
||||
* `everyM` 을 주면 **그 배수의 표고만** 그린다. 1m 간격 LAS 등고선을 멀리서 다 그리면 화면이
|
||||
* 선으로 뭉개져 지형이 안 읽힌다 — 확대에 따라 성긴 등고선부터 내보이려는 것이다. 안 주면
|
||||
* 전부 그리므로 기존 화면(B04 지도·배수유역도)의 표기는 그대로다. */
|
||||
export function drawPreparedLayer(
|
||||
context: CanvasRenderingContext2D,
|
||||
layer: PreparedLayer,
|
||||
view: ViewState,
|
||||
marker: MarkerKind,
|
||||
everyM?: number,
|
||||
): void {
|
||||
const affine = affineOf(view);
|
||||
const step = everyM !== undefined && everyM > 0 ? everyM : 0;
|
||||
for (const feature of layer.features) {
|
||||
if (step && feature.labelValue !== null && feature.labelValue % step !== 0) continue;
|
||||
if (!isVisible(feature, affine, view)) continue;
|
||||
if (feature.kind === "point") drawPointParts(context, feature, affine, marker);
|
||||
else drawLineParts(context, feature, affine);
|
||||
}
|
||||
}
|
||||
|
||||
/** 사전 계산된 계곡선 라벨을 그린다. 폰트·정렬은 호출부에서 설정한다. */
|
||||
/** 사전 계산된 등고 라벨을 그린다. 폰트·정렬은 호출부에서 설정한다.
|
||||
*
|
||||
* `everyM` 은 **몇 m 마다 한 줄을 라벨할지**다. 기본 25m(계곡선)는 B04 지도가 쓰던 값 그대로다
|
||||
* — 전부 내면 화면이 숫자로 뒤덮인다. 확대가 큰 화면은 더 작은 값을 넘겨 촘촘히 낸다. */
|
||||
export function drawPreparedLabels(
|
||||
context: CanvasRenderingContext2D,
|
||||
layer: PreparedLayer,
|
||||
view: ViewState,
|
||||
color: string,
|
||||
everyM = 25,
|
||||
): void {
|
||||
const affine = affineOf(view);
|
||||
const margin = CULL_MARGIN;
|
||||
const step = everyM > 0 ? everyM : 25;
|
||||
// 이미 찍은 라벨과 겹치면 건너뛴다 — LAS 등고선은 **한 표고가 여러 가닥**으로 끊겨 있어
|
||||
// 가닥마다 숫자를 내면 화면이 숫자로 덮인다(2026-09-12 실화면). 도엽 계곡선은 원래
|
||||
// 드물어 이 규칙에 걸리지 않으므로 B04 지도의 표기는 그대로다.
|
||||
const drawn: Array<{ x: number; y: number; half: number }> = [];
|
||||
for (const feature of layer.features) {
|
||||
if (feature.labelText === null) continue;
|
||||
// 표고를 못 읽은 라벨(값 없음)은 솎지 않고 그대로 낸다.
|
||||
if (feature.labelValue !== null && feature.labelValue % step !== 0) continue;
|
||||
const x = feature.labelAnchorX * affine.ax + affine.bx;
|
||||
const y = feature.labelAnchorY * affine.ay + affine.by;
|
||||
if (x < -margin || x > view.width + margin) continue;
|
||||
if (y < -margin || y > view.height + margin) continue;
|
||||
const half = context.measureText(feature.labelText).width / 2 + LABEL_GAP_PX;
|
||||
if (
|
||||
drawn.some(
|
||||
(item) => Math.abs(item.x - x) < item.half + half && Math.abs(item.y - y) < LABEL_ROW_PX,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
drawn.push({ x, y, half });
|
||||
context.lineWidth = 3;
|
||||
context.strokeStyle = haloColor();
|
||||
context.strokeText(feature.labelText, x, y);
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/* =============================================================================
|
||||
* B04_PreProcess_UI_MapRender_Prepare.ts
|
||||
* 지도 레이어 **사전 투영** — GeoJSON·사업지 좌표 폴리라인을 정규화 좌표로 펴고,
|
||||
* 줌 무손실 LOD 가중치(Douglas-Peucker)와 등고 라벨 앵커를 미리 잡아 둔다.
|
||||
*
|
||||
* `B04_PreProcess_UI_MapRender.ts` 가 700줄을 넘겨 떼어낸 조각이다(2026-09-12).
|
||||
* 본문 로직과 수치는 그대로다. 그리기는 그쪽, 준비는 이쪽 — 한 방향으로만 기대어
|
||||
* 순환 참조가 생기지 않는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { VWorldMeta } from "./B04_PreProcess_Api_Fetch";
|
||||
import type {
|
||||
GeoJsonCollection,
|
||||
GeoJsonGeometry,
|
||||
Normalizer,
|
||||
PreparedFeature,
|
||||
PreparedLayer,
|
||||
PreparedPart,
|
||||
} from "./B04_PreProcess_UI_MapRender";
|
||||
|
||||
function isPoint(value: unknown): value is [number, number] {
|
||||
return Array.isArray(value) && typeof value[0] === "number" && typeof value[1] === "number";
|
||||
}
|
||||
|
||||
/** lon/lat 배열 → 정규화 좌표 Float64Array. 유효 정점이 없으면 null. */
|
||||
function projectRing(ring: unknown, normalizer: Normalizer): Float64Array | null {
|
||||
if (!Array.isArray(ring) || ring.length === 0) return null;
|
||||
const coords = new Float64Array(ring.length * 2);
|
||||
let count = 0;
|
||||
for (const point of ring) {
|
||||
if (!isPoint(point)) continue;
|
||||
coords[count * 2] = (point[0] - normalizer.lonMin) / normalizer.lonRange;
|
||||
coords[count * 2 + 1] = 1 - (point[1] - normalizer.latMin) / normalizer.latRange;
|
||||
count += 1;
|
||||
}
|
||||
if (count === 0) return null;
|
||||
return count * 2 === coords.length ? coords : coords.slice(0, count * 2);
|
||||
}
|
||||
|
||||
function collectParts(
|
||||
geometry: GeoJsonGeometry,
|
||||
normalizer: Normalizer,
|
||||
parts: PreparedPart[],
|
||||
): "line" | "point" {
|
||||
const coordinates = geometry.coordinates;
|
||||
if (!Array.isArray(coordinates)) return "line";
|
||||
const push = (ring: unknown, closed: boolean): void => {
|
||||
const projected = projectRing(ring, normalizer);
|
||||
if (projected) parts.push({ coords: projected, closed, weights: null });
|
||||
};
|
||||
switch (geometry.type) {
|
||||
case "Point":
|
||||
push([coordinates], false);
|
||||
return "point";
|
||||
case "MultiPoint":
|
||||
push(coordinates, false);
|
||||
return "point";
|
||||
case "LineString":
|
||||
push(coordinates, false);
|
||||
return "line";
|
||||
case "MultiLineString":
|
||||
for (const line of coordinates) push(line, false);
|
||||
return "line";
|
||||
case "Polygon":
|
||||
for (const ring of coordinates) push(ring, true);
|
||||
return "line";
|
||||
case "MultiPolygon":
|
||||
for (const polygon of coordinates) {
|
||||
if (!Array.isArray(polygon)) continue;
|
||||
for (const ring of polygon) push(ring, true);
|
||||
}
|
||||
return "line";
|
||||
default:
|
||||
return "line";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Douglas-Peucker 가중치 계산 (반복형, 스택 오버플로 방지).
|
||||
* weights[i] = "허용 오차가 이 값보다 크면 정점 i를 버려도 되는" 임계값.
|
||||
* 부모 구간의 오차로 상한을 걸어(cap) 어떤 허용 오차에서도 일관된 부분집합이 나오게 한다.
|
||||
* y축은 1/aspect로 보정해 화면 픽셀 거리와 비례하는 좌표계에서 계산한다.
|
||||
*/
|
||||
function computeDpWeights(coords: Float64Array, aspect: number): Float64Array {
|
||||
const n = coords.length / 2;
|
||||
const weights = new Float64Array(n);
|
||||
weights[0] = Infinity;
|
||||
weights[n - 1] = Infinity;
|
||||
if (n <= 2) return weights;
|
||||
const stack: number[] = [0, n - 1];
|
||||
const caps: number[] = [Infinity];
|
||||
while (stack.length) {
|
||||
const last = stack.pop()!;
|
||||
const first = stack.pop()!;
|
||||
const cap = caps.pop()!;
|
||||
if (last - first < 2) continue;
|
||||
const ax = coords[first * 2];
|
||||
const ay = coords[first * 2 + 1] / aspect;
|
||||
const bx = coords[last * 2];
|
||||
const by = coords[last * 2 + 1] / aspect;
|
||||
const dx = bx - ax;
|
||||
const dy = by - ay;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
let maxDist = -1;
|
||||
let maxIndex = -1;
|
||||
for (let i = first + 1; i < last; i += 1) {
|
||||
const px = coords[i * 2] - ax;
|
||||
const py = coords[i * 2 + 1] / aspect - ay;
|
||||
const dist = len === 0 ? Math.sqrt(px * px + py * py) : Math.abs(px * dy - py * dx) / len;
|
||||
if (dist > maxDist) {
|
||||
maxDist = dist;
|
||||
maxIndex = i;
|
||||
}
|
||||
}
|
||||
const weight = Math.min(maxDist, cap);
|
||||
weights[maxIndex] = weight;
|
||||
stack.push(first, maxIndex, maxIndex, last);
|
||||
caps.push(weight, weight);
|
||||
}
|
||||
return weights;
|
||||
}
|
||||
|
||||
/** 등고 라벨 앵커: LineString/MultiLineString 첫 파트의 중앙 정점 (기존 동작 유지). */
|
||||
function labelAnchorOf(geometry: GeoJsonGeometry, normalizer: Normalizer): [number, number] | null {
|
||||
const coords = geometry.coordinates;
|
||||
if (!Array.isArray(coords)) return null;
|
||||
const line =
|
||||
geometry.type === "LineString"
|
||||
? coords
|
||||
: geometry.type === "MultiLineString"
|
||||
? coords[0]
|
||||
: null;
|
||||
if (!Array.isArray(line) || line.length === 0) return null;
|
||||
const mid = line[Math.floor(line.length / 2)];
|
||||
if (!isPoint(mid)) return null;
|
||||
return [
|
||||
(mid[0] - normalizer.lonMin) / normalizer.lonRange,
|
||||
1 - (mid[1] - normalizer.latMin) / normalizer.latRange,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GeoJSON 컬렉션 1개를 사전 투영한다.
|
||||
* labelKeys가 주어지면 계곡선(25m 배수) 피처에만 라벨 텍스트·앵커를 계산해 둔다.
|
||||
*/
|
||||
export function prepareLayer(
|
||||
collection: GeoJsonCollection | undefined,
|
||||
normalizer: Normalizer,
|
||||
labelKeys?: string[],
|
||||
): PreparedLayer {
|
||||
const features: PreparedFeature[] = [];
|
||||
for (const feature of collection?.features ?? []) {
|
||||
if (!feature.geometry) continue;
|
||||
const parts: PreparedPart[] = [];
|
||||
const kind = collectParts(feature.geometry, normalizer, parts);
|
||||
if (parts.length === 0) continue;
|
||||
if (kind === "line") {
|
||||
for (const part of parts) {
|
||||
if (part.coords.length < 6) continue;
|
||||
part.weights = computeDpWeights(part.coords, normalizer.aspect);
|
||||
}
|
||||
}
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
for (const part of parts) {
|
||||
const coords = part.coords;
|
||||
for (let i = 0; i < coords.length; i += 2) {
|
||||
const x = coords[i];
|
||||
const y = coords[i + 1];
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
}
|
||||
let labelText: string | null = null;
|
||||
let labelValue: number | null = null;
|
||||
let labelAnchorX = 0;
|
||||
let labelAnchorY = 0;
|
||||
if (labelKeys && labelKeys.length > 0) {
|
||||
const raw = labelKeys.map((key) => feature.properties?.[key]).find((value) => value != null);
|
||||
const elevation = typeof raw === "number" ? raw : Number(raw);
|
||||
// **모든 등고선**에 앵커를 잡아 둔다. 어느 줄을 실제로 낼지는 그릴 때 고른다 —
|
||||
// 화면마다 솎는 눈금이 다르기 때문이다(B04 지도는 계곡선만, 계획노선 편집 모달은
|
||||
// 확대에 따라 더 촘촘히). 준비 단계에서 걸러 버리면 확대해도 되살릴 수가 없다.
|
||||
if (Number.isFinite(elevation)) {
|
||||
const anchor = labelAnchorOf(feature.geometry, normalizer);
|
||||
if (anchor) {
|
||||
labelText = String(elevation);
|
||||
labelValue = elevation;
|
||||
labelAnchorX = anchor[0];
|
||||
labelAnchorY = anchor[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
features.push({
|
||||
kind,
|
||||
parts,
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
labelAnchorX,
|
||||
labelAnchorY,
|
||||
labelText,
|
||||
labelValue,
|
||||
});
|
||||
}
|
||||
return { features };
|
||||
}
|
||||
|
||||
/**
|
||||
* 사업지 좌표계(m) 폴리라인을 한 개 피처짜리 레이어로 사전 투영한다.
|
||||
* meta의 x/y 범위와 lon/lat 범위는 같은 사각형을 가리키므로, 미터 좌표도 GeoJSON과 동일한
|
||||
* 정규화 공간으로 들어간다 — 노선 선형을 도엽 레이어 위에 그대로 겹칠 수 있다.
|
||||
*/
|
||||
export function prepareMetricPolyline(
|
||||
points: ReadonlyArray<{ x: number; y: number }>,
|
||||
meta: VWorldMeta,
|
||||
): PreparedLayer {
|
||||
if (points.length < 2) return { features: [] };
|
||||
const widthMeters = meta.width_meters || 1;
|
||||
const heightMeters = meta.height_meters || 1;
|
||||
const coords = new Float64Array(points.length * 2);
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
points.forEach((point, index) => {
|
||||
const nx = (point.x - meta.x_min) / widthMeters;
|
||||
const ny = 1 - (point.y - meta.y_min) / heightMeters;
|
||||
coords[index * 2] = nx;
|
||||
coords[index * 2 + 1] = ny;
|
||||
if (nx < minX) minX = nx;
|
||||
if (nx > maxX) maxX = nx;
|
||||
if (ny < minY) minY = ny;
|
||||
if (ny > maxY) maxY = ny;
|
||||
});
|
||||
return {
|
||||
features: [
|
||||
{
|
||||
kind: "line",
|
||||
parts: [{ coords, closed: false, weights: null }],
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
labelAnchorX: 0,
|
||||
labelAnchorY: 0,
|
||||
labelText: null,
|
||||
labelValue: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 사업지 좌표계(m) 폴리라인 **여러 개**를 한 레이어로 사전 투영한다(LAS 등고선 등).
|
||||
*
|
||||
* `prepareMetricPolyline` 의 여러 줄 판이다. 줄마다 `label`(표고 m)을 주면 가운데 정점을
|
||||
* 앵커로 잡아 `drawPreparedLabels` 가 그대로 쓸 수 있다.
|
||||
*/
|
||||
export function prepareMetricPolylines(
|
||||
lines: ReadonlyArray<{ points: ReadonlyArray<readonly [number, number]>; label?: number }>,
|
||||
meta: VWorldMeta,
|
||||
): PreparedLayer {
|
||||
const widthMeters = meta.width_meters || 1;
|
||||
const heightMeters = meta.height_meters || 1;
|
||||
const features: PreparedFeature[] = [];
|
||||
for (const line of lines) {
|
||||
if (line.points.length < 2) continue;
|
||||
const coords = new Float64Array(line.points.length * 2);
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
line.points.forEach((point, index) => {
|
||||
const nx = (point[0] - meta.x_min) / widthMeters;
|
||||
const ny = 1 - (point[1] - meta.y_min) / heightMeters;
|
||||
coords[index * 2] = nx;
|
||||
coords[index * 2 + 1] = ny;
|
||||
if (nx < minX) minX = nx;
|
||||
if (nx > maxX) maxX = nx;
|
||||
if (ny < minY) minY = ny;
|
||||
if (ny > maxY) maxY = ny;
|
||||
});
|
||||
const middle = Math.floor(line.points.length / 2) * 2;
|
||||
features.push({
|
||||
kind: "line",
|
||||
parts: [
|
||||
{ coords, closed: false, weights: computeDpWeights(coords, widthMeters / heightMeters) },
|
||||
],
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
labelAnchorX: coords[middle],
|
||||
labelAnchorY: coords[middle + 1],
|
||||
labelText: line.label === undefined ? null : String(line.label),
|
||||
labelValue: line.label ?? null,
|
||||
});
|
||||
}
|
||||
return { features };
|
||||
}
|
||||
@@ -33,8 +33,6 @@ import {
|
||||
createNormalizer,
|
||||
drawPreparedLabels,
|
||||
drawPreparedLayer,
|
||||
prepareLayer,
|
||||
prepareMetricPolyline,
|
||||
routeLineColor,
|
||||
ROUTE_LINE_WIDTH,
|
||||
type GeoJsonCollection,
|
||||
@@ -44,6 +42,7 @@ import {
|
||||
type PreparedLayer,
|
||||
type ViewState,
|
||||
} from "./B04_PreProcess_UI_MapRender";
|
||||
import { prepareLayer, prepareMetricPolyline } from "./B04_PreProcess_UI_MapRender_Prepare";
|
||||
import { drawStationTicks } from "./B04_PreProcess_UI_MapOverlays";
|
||||
import type { WatershedAnalysis } from "./B04_PreProcess_Api_Fetch";
|
||||
|
||||
|
||||
@@ -56,8 +56,12 @@ export interface RoutePlanResponse {
|
||||
nodes: RoutePlanNode[];
|
||||
/** 직선·곡선 성분 — 곡선 시작·끝점과 반지름. 화면이 이것으로 손잡이를 그린다. */
|
||||
curves: RoutePlanCurve[];
|
||||
/** 이 프로젝트에 적용한 법정 최소곡선반지름(m). */
|
||||
/** 이 프로젝트에 적용한 법정 최소곡선반지름(m) — **기본값·위반 표시 기준**. */
|
||||
min_radius_m: number;
|
||||
/** **못 넘는** 곡선반지름 하한(m). 0이면 제한 없음(작업임도). 기본값과 다른 값이다. */
|
||||
limit_radius_m?: number;
|
||||
/** **못 넘는** 곡선 길이 하한(m). 0이면 제한 없음 — 지금은 전부 0(법에 값이 없음). */
|
||||
limit_curve_length_m?: number;
|
||||
curve_count: number;
|
||||
violation_count: number;
|
||||
/** 사용자가 고친 계획노선이 저장돼 있으면 true. */
|
||||
@@ -128,6 +132,60 @@ export async function replanRoute(
|
||||
);
|
||||
}
|
||||
|
||||
/** 점 묶음의 **지반고**를 묻는다(계획서 0-9 ⑤·⑧).
|
||||
*
|
||||
* 새 계산이 아니라 확정된 지표면을 **읽기만** 하므로 편집 중에 불러도 된다 — 다만 끄는 동안
|
||||
* 프레임마다 부르지는 않는다(찍는 순간에만). 지표면 밖은 `null` 로 온다. */
|
||||
export async function fetchRouteElevations(
|
||||
projectId: string,
|
||||
points: Array<[number, number]>,
|
||||
): Promise<Array<number | null>> {
|
||||
const payload = await requestJson<{ z: Array<number | null> }>(
|
||||
`/projects/${projectId}/route/elevations`,
|
||||
{ method: "POST", body: JSON.stringify({ points }) },
|
||||
60000,
|
||||
);
|
||||
return payload.z;
|
||||
}
|
||||
|
||||
/** 횡단 미리보기 한 장 — 고치던 노선 그대로 그 측점만 서버가 셈해 준다(계획서 0-9 ⑧). */
|
||||
export interface CrossPreviewResponse {
|
||||
status: string;
|
||||
chainage_m: number;
|
||||
label: string | null;
|
||||
uphill_side: string | null;
|
||||
plan_radius_m: number | null;
|
||||
curve_widening_m: number | null;
|
||||
/** 원지반 횡단 샘플. */
|
||||
samples: Array<{ offset_m?: number; elevation_m?: number | null; valid: boolean }>;
|
||||
/** 기본 계획 횡단 — B06 `compute_cross_design` 이 낸 것. 계획고를 못 세우면 null. */
|
||||
design: {
|
||||
design_line: Array<{ offset_m: number; elevation_m: number }>;
|
||||
cut_area_m2: number;
|
||||
fill_area_m2: number;
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface CrossPreviewRequest {
|
||||
vertices: Array<{ x: number; y: number; curve: boolean; radius_m: number | null }>;
|
||||
chainage_m: number;
|
||||
min_radius_m: number;
|
||||
station_interval_m: number;
|
||||
}
|
||||
|
||||
/** 한 측점 횡단을 묻는다. 종·횡단을 한 번 돌리므로 **한두 초** 걸린다(사용자 확정: 괜찮음). */
|
||||
export async function fetchCrossPreview(
|
||||
projectId: string,
|
||||
request: CrossPreviewRequest,
|
||||
): Promise<CrossPreviewResponse> {
|
||||
return requestJson<CrossPreviewResponse>(
|
||||
`/projects/${projectId}/route/cross-preview`,
|
||||
{ method: "POST", body: JSON.stringify(request) },
|
||||
120000,
|
||||
);
|
||||
}
|
||||
|
||||
/** 계획노선을 예상노선으로 되돌리고 같은 재계산을 돈다(노선 초기화). */
|
||||
export async function resetRoutePlan(projectId: string): Promise<RouteReplanResponse> {
|
||||
return requestJson<RouteReplanResponse>(
|
||||
|
||||
@@ -151,6 +151,37 @@ def legal_plan_radius_min_m(design_speed_kph: int, terrain_type: str = "normal")
|
||||
return float(speeds[terrain])
|
||||
|
||||
|
||||
def plan_radius_limit_m(
|
||||
grade_class: str,
|
||||
design_speed_kph: int | None = None,
|
||||
terrain_type: str = "normal",
|
||||
) -> float:
|
||||
"""계획노선 편집 화면이 **못 넘게 막을** 평면 곡선반지름 하한(m). 0이면 제한 없음.
|
||||
|
||||
위 `legal_plan_radius_min_m` 은 **기본값·위반 표시 기준**이고 이것은 **제한**이다
|
||||
(2026-09-12 사용자 확정: 「아예 못 넘게 막음」). 임도 종류별 칸이 비어 있으면(None)
|
||||
법정 표를 그대로 하한으로 쓰고, 값이 적혀 있으면 그 값을 쓴다 — 작업임도는 별표2에
|
||||
곡선반지름 규정이 없어 0(제한 없음)으로 열려 있다.
|
||||
"""
|
||||
table = FOREST_ROAD_PROFILE_CRITERIA["plan_radius_limit_by_grade_m"]
|
||||
override = table.get(grade_class)
|
||||
if override is not None:
|
||||
return float(override)
|
||||
return legal_plan_radius_min_m(
|
||||
resolve_design_speed(grade_class, design_speed_kph), terrain_type
|
||||
)
|
||||
|
||||
|
||||
def plan_curve_length_limit_m(grade_class: str) -> float:
|
||||
"""평면 **곡선 길이(L)** 하한(m). 0이면 제한 없음.
|
||||
|
||||
법령·교본에 값이 없어 지금은 임도 종류 전부 0이다 — 자리만 열어 둔 칸이라
|
||||
실무값이 정해지면 `config_system_design` 의 표만 고치면 된다(2026-09-12 사용자 확정).
|
||||
"""
|
||||
table = FOREST_ROAD_PROFILE_CRITERIA["plan_curve_length_limit_by_grade_m"]
|
||||
return float(table.get(grade_class) or 0.0)
|
||||
|
||||
|
||||
def _pick(*candidates: Any) -> Any:
|
||||
"""요청 → DB 저장값 → config 순으로 처음 나오는 유효값을 고른다."""
|
||||
for value in candidates:
|
||||
|
||||
@@ -133,7 +133,19 @@ def _ensure_expected_route(project_root: Path) -> str:
|
||||
|
||||
|
||||
async def _min_plan_radius_m(project_id: UUID) -> float:
|
||||
"""이 프로젝트에 적용할 법정 최소곡선반지름(m) — 임도 종류·설계속도·지형으로 고른다.
|
||||
"""기본 반지름만 필요한 자리 — 하한까지 필요하면 `_plan_criteria` 를 쓸 것."""
|
||||
criteria = await _plan_criteria(project_id)
|
||||
return criteria[0]
|
||||
|
||||
|
||||
async def _plan_criteria(project_id: UUID) -> tuple[float, float, float]:
|
||||
"""이 프로젝트의 **기본 반지름 · 반지름 하한 · 곡선 길이 하한**(m) 세 값.
|
||||
|
||||
기본 반지름은 곡선을 만들 때 쓰는 값이고, 하한 둘은 **화면이 못 넘게 막는** 값이다
|
||||
(2026-09-12 사용자 확정). 둘을 한 값으로 묶으면 하한 0 이 곧 반지름 0 이 되어 곡선이
|
||||
아예 안 그려지므로 반드시 갈라 둔다.
|
||||
|
||||
기본 반지름은 임도 종류·설계속도·지형으로 고른다.
|
||||
|
||||
값의 출처는 지식DB(`01_임도/02_상세설계/평면선형.md`, 별표2 Ⅰ.2.다)이고 산식은 이미
|
||||
`B05_Profile_Engine_Grade.legal_plan_radius_min_m` 에 있다 — 여기서 다시 짜지 않는다.
|
||||
@@ -145,7 +157,12 @@ async def _min_plan_radius_m(project_id: UUID) -> float:
|
||||
"""
|
||||
import aiomysql
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Grade import legal_plan_radius_min_m, resolve_design_speed
|
||||
from B05_Profile.B05_Profile_Engine_Grade import (
|
||||
legal_plan_radius_min_m,
|
||||
plan_curve_length_limit_m,
|
||||
plan_radius_limit_m,
|
||||
resolve_design_speed,
|
||||
)
|
||||
from common_util.common_util_workflow_state import get_workflow_state
|
||||
|
||||
grade_class, design_speed, terrain = "work", None, "special"
|
||||
@@ -172,7 +189,11 @@ async def _min_plan_radius_m(project_id: UUID) -> float:
|
||||
terrain = str(params["terrain_type"])
|
||||
except Exception: # noqa: BLE001 — 설정을 못 읽어도 폴리라인화는 이어 간다
|
||||
logger.exception("최소곡선반지름 설정을 못 읽어 기본값을 씁니다: %s", project_id)
|
||||
return legal_plan_radius_min_m(resolve_design_speed(grade_class, design_speed), terrain)
|
||||
return (
|
||||
legal_plan_radius_min_m(resolve_design_speed(grade_class, design_speed), terrain),
|
||||
plan_radius_limit_m(grade_class, design_speed, terrain),
|
||||
plan_curve_length_limit_m(grade_class),
|
||||
)
|
||||
|
||||
|
||||
def _nodes_path(path: Path) -> Path:
|
||||
@@ -404,7 +425,7 @@ async def read_route_plan(project_id: UUID) -> dict[str, Any] | JSONResponse:
|
||||
if not expected:
|
||||
expected = await asyncio.to_thread(_vertices_of, design_route_csv_path(project_root))
|
||||
|
||||
radius_m = await _min_plan_radius_m(project_id)
|
||||
radius_m, radius_limit_m, arc_limit_m = await _plan_criteria(project_id)
|
||||
await asyncio.to_thread(_ensure_planned_initial, project_root, radius_m)
|
||||
working = await asyncio.to_thread(_vertices_of, planned_route_working_path(project_root))
|
||||
initial = await asyncio.to_thread(_vertices_of, planned_route_initial_path(project_root))
|
||||
@@ -445,6 +466,10 @@ async def read_route_plan(project_id: UUID) -> dict[str, Any] | JSONResponse:
|
||||
# (2026-09-07 사용자 확정). 저장분이 있으면 그것을, 없으면 방금 뽑은 것을 준다.
|
||||
"curves": saved_curves or [curve.as_dict() for curve in outline.curves],
|
||||
"min_radius_m": round(radius_m, 2),
|
||||
# **못 넘는 하한** — 기본값(`min_radius_m`)과 다른 값이다. 0이면 제한 없음
|
||||
# (작업임도는 별표2에 곡선반지름 규정이 없어 0으로 열려 있다, 2026-09-12 확정).
|
||||
"limit_radius_m": round(radius_limit_m, 2),
|
||||
"limit_curve_length_m": round(arc_limit_m, 2),
|
||||
"curve_count": len(saved_curves) if saved_curves else outline.curve_count,
|
||||
"violation_count": outline.violation_count,
|
||||
"edited": bool(working),
|
||||
@@ -468,7 +493,7 @@ async def replan_route(
|
||||
|
||||
# 고치기 전에 예상노선(원본)·초기 폴리라인이 서 있는지 본다 — 초기화가 돌아갈 자리다.
|
||||
await asyncio.to_thread(_ensure_expected_route, project_root)
|
||||
radius_m = await _min_plan_radius_m(project_id)
|
||||
radius_m, radius_limit_m, arc_limit_m = await _plan_criteria(project_id)
|
||||
await asyncio.to_thread(_ensure_planned_initial, project_root, radius_m)
|
||||
# 화면이 보낸 것은 **노드(꺾임점)** 다 — 같은 R 규칙으로 다시 폴리라인을 만든다.
|
||||
# 노드만 옮기면 선이 저절로 규칙을 지키는 것이 이 구조의 목적이다(2026-09-06 사용자).
|
||||
@@ -527,7 +552,7 @@ async def reset_route_plan(project_id: UUID) -> dict[str, Any] | JSONResponse:
|
||||
return JSONResponse(status_code=404, content=_PROJECT_PATH_MISSING)
|
||||
project_root, stored_path = paths
|
||||
await asyncio.to_thread(_ensure_expected_route, project_root)
|
||||
radius_m = await _min_plan_radius_m(project_id)
|
||||
radius_m, radius_limit_m, arc_limit_m = await _plan_criteria(project_id)
|
||||
await asyncio.to_thread(_ensure_planned_initial, project_root, radius_m)
|
||||
working_path = planned_route_working_path(project_root)
|
||||
if working_path.is_file():
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""계획노선 편집 중 **지반고만** 묻는 가벼운 통로.
|
||||
|
||||
편집 모달은 [확인] 전까지 아무 계산도 내보내지 않는다(계획서 0-2 확정 7). 다만 두 점을 찍어
|
||||
**구간 길이와 종단기울기**를 볼 때(0-9 ⑤)와 한 측점의 **횡단도 미리보기**(⑧)는 지반고가
|
||||
있어야 한다. 새 계산이 아니라 **이미 확정된 지표면을 읽기만** 하는 통로라 그 규칙과 부딪히지
|
||||
않는다 — 노선을 갈아 끼우지도, 정본을 건드리지도 않는다.
|
||||
|
||||
표고 조회는 종·횡단 생성기가 쓰는 것과 **같은 sampler**(`build_surface_sampler`)를 연다.
|
||||
두 화면이 다른 표고를 보면 같은 자리의 기울기가 갈린다.
|
||||
|
||||
POST /api/projects/{id}/route/elevations → 점 묶음의 지반고
|
||||
POST /api/projects/{id}/route/cross-preview → 고치던 노선의 한 측점 횡단 미리보기
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
import numpy as np
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import (
|
||||
SectionGenerationOptions,
|
||||
generate_sections,
|
||||
)
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design, curve_widening_args
|
||||
from common_util.common_util_route_polyline import build_planned_polyline
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
||||
from common_util.common_util_surface_sampler import build_surface_sampler
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B05 Route Terrain"])
|
||||
|
||||
_MODELS_SUBDIR = Path("B04_PreProcess") / "models"
|
||||
#: 한 번에 물을 수 있는 점 수. 구간 재기는 수십 점, 횡단 한 장은 수백 점이면 넉넉하다 —
|
||||
#: 상한을 두어 실수로 노선 전체를 밀어 넣는 일을 막는다.
|
||||
MAX_POINTS = 4000
|
||||
|
||||
|
||||
class ElevationRequest(BaseModel):
|
||||
"""사업지 좌표계(m) 점 묶음 [[x, y], …]."""
|
||||
|
||||
points: list[tuple[float, float]] = Field(..., min_length=1, max_length=MAX_POINTS)
|
||||
|
||||
|
||||
def _sample(project_root: Path, params: dict, points: list[tuple[float, float]]):
|
||||
"""확정 지표면에서 표고를 읽는다. 모델을 못 열면 None."""
|
||||
try:
|
||||
sampler = build_surface_sampler(
|
||||
project_root / _MODELS_SUBDIR,
|
||||
str(params["source_filter"]),
|
||||
str(params["method"]),
|
||||
bool(params["smooth"]),
|
||||
)
|
||||
except (FileNotFoundError, KeyError, OSError, ValueError) as exc:
|
||||
logger.warning("계획노선 편집: 지표면을 열지 못했습니다 — %s", exc)
|
||||
return None
|
||||
z, valid = sampler.sample_xy(np.asarray(points, dtype=np.float64))
|
||||
return z, valid
|
||||
|
||||
|
||||
@router.post("/{project_id}/route/elevations", response_model=None)
|
||||
async def read_route_elevations(project_id: UUID, request: ElevationRequest) -> dict | JSONResponse:
|
||||
"""점 묶음의 지반고(m)와 유효 여부를 돌려준다.
|
||||
|
||||
지표면 밖이거나 자료가 없는 자리는 `valid=false` 로 나가고 표고는 `null` 이다 —
|
||||
**임의 표고로 메우지 않는다**(sampler 규칙 그대로). 화면은 그 자리를 「모름」으로 낸다.
|
||||
"""
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection:
|
||||
stored = await get_project_storage_relative_path(connection, project_id)
|
||||
if not stored:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"status": "error", "message": "프로젝트 저장 경로를 찾을 수 없습니다."},
|
||||
)
|
||||
params = await get_surface_confirmation_params(connection, str(project_id))
|
||||
|
||||
project_root = Path(resolve_stored_project_path(stored))
|
||||
sampled = await asyncio.to_thread(_sample, project_root, params, request.points)
|
||||
if sampled is None:
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={
|
||||
"status": "error",
|
||||
"message": "확정된 지표면이 없어 지반고를 읽을 수 없습니다.",
|
||||
},
|
||||
)
|
||||
z, valid = sampled
|
||||
return {
|
||||
"status": "success",
|
||||
"project_id": str(project_id),
|
||||
"z": [None if not ok else round(float(value), 3) for value, ok in zip(z, valid)],
|
||||
"valid": [bool(ok) for ok in valid],
|
||||
}
|
||||
|
||||
|
||||
class PreviewVertex(BaseModel):
|
||||
"""편집 중인 꺾임점 하나 — `RouteVertexInput` 과 같은 꼴."""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
curve: bool = True
|
||||
radius_m: float | None = None
|
||||
|
||||
|
||||
class CrossPreviewRequest(BaseModel):
|
||||
"""고치던 노선 그대로 한 측점의 횡단을 미리 본다."""
|
||||
|
||||
vertices: list[PreviewVertex] = Field(..., min_length=2)
|
||||
chainage_m: float = Field(..., ge=0)
|
||||
#: 법정 최소곡선반지름(m) — 화면이 `/route/plan` 에서 받은 값을 그대로 돌려준다.
|
||||
min_radius_m: float = Field(12.0, gt=0)
|
||||
station_interval_m: float | None = None
|
||||
|
||||
|
||||
def _cross_preview(
|
||||
project_root: Path,
|
||||
params: dict,
|
||||
request: CrossPreviewRequest,
|
||||
) -> dict | None:
|
||||
"""고치던 노선으로 종·횡단을 한 번 돌려 그 측점 한 장을 뽑는다.
|
||||
|
||||
**B05·B06 의 정본 로직을 그대로 재사용한다**(2026-09-12 사용자 확정 「기본 로직은 B06에
|
||||
존재함. 재사용」) — `generate_sections` 가 측점·접선·지반 샘플을, `compute_cross_design`
|
||||
이 설계선을 만든다. 여기서 기하를 새로 짜지 않는다.
|
||||
|
||||
⚠ **계획고는 아직 없다.** 계획고는 [확인] 뒤 전 체인이 낳는 값이라 편집 중에는 존재하지
|
||||
않는다. 그래서 그 측점의 **지반고를 그대로 계획고로 놓는다**(지반 추종) — 절·성토가 사면
|
||||
기울기만으로 서는 「기본 계획 횡단」이며, 사용자가 보기로 한 것도 그것이다.
|
||||
"""
|
||||
try:
|
||||
sampler = build_surface_sampler(
|
||||
project_root / _MODELS_SUBDIR,
|
||||
str(params["source_filter"]),
|
||||
str(params["method"]),
|
||||
bool(params["smooth"]),
|
||||
)
|
||||
except (FileNotFoundError, KeyError, OSError, ValueError) as exc:
|
||||
logger.warning("횡단 미리보기: 지표면을 열지 못했습니다 — %s", exc)
|
||||
return None
|
||||
|
||||
built = build_planned_polyline(
|
||||
[(vertex.x, vertex.y) for vertex in request.vertices],
|
||||
min_radius_m=request.min_radius_m,
|
||||
# 화면이 준 노드는 이미 꺾임점이다 — 다시 뽑으면 선이 깎인다(`_write_planned_polyline`).
|
||||
simplify=False,
|
||||
curve_flags=[vertex.curve for vertex in request.vertices],
|
||||
radii=[vertex.radius_m for vertex in request.vertices],
|
||||
)
|
||||
interval = request.station_interval_m
|
||||
options = (
|
||||
SectionGenerationOptions(station_interval_m=float(interval))
|
||||
if interval and interval > 0
|
||||
else SectionGenerationOptions()
|
||||
)
|
||||
result = generate_sections(built.vertices, sampler, options)
|
||||
sections = result["cross_sections"]
|
||||
if not sections:
|
||||
return None
|
||||
section = min(sections, key=lambda row: abs(float(row["chainage_m"]) - request.chainage_m))
|
||||
|
||||
design = None
|
||||
center_z = section.get("center_z")
|
||||
if center_z is not None:
|
||||
# 단면유형 기본값은 B06 화면과 같다 — 등고가 높은 쪽을 절토로 본다.
|
||||
section_mode = "right_cut" if section.get("uphill_side") == "right" else "left_cut"
|
||||
design = compute_cross_design(
|
||||
section["samples"],
|
||||
float(center_z),
|
||||
ground_type="soil",
|
||||
section_mode=section_mode,
|
||||
**curve_widening_args(section),
|
||||
)
|
||||
return {
|
||||
"chainage_m": round(float(section["chainage_m"]), 3),
|
||||
"label": section.get("label"),
|
||||
"uphill_side": section.get("uphill_side"),
|
||||
"plan_radius_m": section.get("plan_radius_m"),
|
||||
"curve_widening_m": section.get("curve_widening_m"),
|
||||
"samples": section["samples"],
|
||||
"design": design,
|
||||
"total_length_m": round(float(result["longitudinal"]["total_length_m"]), 3)
|
||||
if result.get("longitudinal", {}).get("total_length_m") is not None
|
||||
else None,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{project_id}/route/cross-preview", response_model=None)
|
||||
async def read_cross_preview(project_id: UUID, request: CrossPreviewRequest) -> dict | JSONResponse:
|
||||
"""고치던 계획노선의 **한 측점 횡단**을 돌려준다(계획서 0-9 ⑧).
|
||||
|
||||
정본을 건드리지 않는다 — 파일도 DB 도 쓰지 않고 그 자리에서 셈해 돌려주기만 한다.
|
||||
"""
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection:
|
||||
stored = await get_project_storage_relative_path(connection, project_id)
|
||||
if not stored:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"status": "error", "message": "프로젝트 저장 경로를 찾을 수 없습니다."},
|
||||
)
|
||||
params = await get_surface_confirmation_params(connection, str(project_id))
|
||||
|
||||
project_root = Path(resolve_stored_project_path(stored))
|
||||
preview = await asyncio.to_thread(_cross_preview, project_root, params, request)
|
||||
if preview is None:
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={
|
||||
"status": "error",
|
||||
"message": "확정된 지표면이 없어 횡단을 미리 볼 수 없습니다.",
|
||||
},
|
||||
)
|
||||
return {"status": "success", "project_id": str(project_id), **preview}
|
||||
@@ -15,13 +15,15 @@ import {
|
||||
computeMapRect,
|
||||
MAP_STATION_INTERVAL_M,
|
||||
createNormalizer,
|
||||
prepareLayer,
|
||||
prepareMetricPolyline,
|
||||
type MapRect,
|
||||
type Normalizer,
|
||||
type PreparedLayer,
|
||||
type ViewState,
|
||||
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||
import {
|
||||
prepareLayer,
|
||||
prepareMetricPolyline,
|
||||
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender_Prepare";
|
||||
import { type RoutePoint } from "./B05_Profile_Api_Fetch";
|
||||
import type { FlowArrow } from "../B04_PreProcess/B04_PreProcess_UI_FlowArrows";
|
||||
import { buildStrengthArray } from "../B04_PreProcess/B04_PreProcess_UI_FlowRamp";
|
||||
|
||||
@@ -281,9 +281,20 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
// 계획노선 편집 — 모달 [확인]에서 서버가 배수유역부터 다시 계산하므로, 끝나면
|
||||
// 옛 노선 기준 캐시를 버리고 페이지를 새로 세운다([초기화]와 같은 뒷정리).
|
||||
onEditPlannedRoute: () =>
|
||||
void openRouteEditModal(activeProjectId, () => {
|
||||
navigateTo(ROUTES.B05_PROFILE);
|
||||
}),
|
||||
void openRouteEditModal(
|
||||
activeProjectId,
|
||||
() => {
|
||||
navigateTo(ROUTES.B05_PROFILE);
|
||||
},
|
||||
{
|
||||
// 측점 눈금 간격은 좌측 패널이 쥔 값을 그대로 넘긴다 — 모달이 따로 굳히지 않는다.
|
||||
stationIntervalM: panel.values().stationInterval ?? undefined,
|
||||
// 바탕 등고선 — 확정 지표면이 있으면 모달이 LAS 등고선을 쓴다(계획서 0-9 ⑥).
|
||||
surfaceModelId: confirmedSurface?.model_id ?? null,
|
||||
contourIntervalM: latest?.surface_params.contour_interval_m,
|
||||
smooth: latest?.surface_params.smooth,
|
||||
},
|
||||
),
|
||||
onTempSave: () => void tempSaveAction(actionContext),
|
||||
onGoCross: () => {
|
||||
// 페이지 이동 = 코리도 영구저장 시점(2026-08-23 사용자 확정) — 이동은 막지 않는다.
|
||||
|
||||
@@ -16,36 +16,44 @@
|
||||
import {
|
||||
computeMapRect,
|
||||
computeRouteView,
|
||||
drawPreparedLayer,
|
||||
createNormalizer,
|
||||
hitPreparedLayer,
|
||||
metricToScreen,
|
||||
prepareLayer,
|
||||
pickLevelStep,
|
||||
type PreparedLayer,
|
||||
type ViewState,
|
||||
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||
import { prepareLayer } from "../B04_PreProcess/B04_PreProcess_UI_MapRender_Prepare";
|
||||
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { clearDrafts, clearResults } from "../A00_Common/b_page_state";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { loadRouteEditContours, type RouteEditContours } from "./B05_Profile_UI_RouteEdit_Contour";
|
||||
import { fetchDrainageLayers } from "./B05_Profile_UI_Drainage_Parts";
|
||||
import { fetchRoutePlan, replanRoute, resetRoutePlan } from "./B05_Profile_Api_Replan";
|
||||
import type { RoutePlanCurve } from "./B05_Profile_Api_Replan";
|
||||
import { buildEditedPolyline, dragHandleTo as curveDragTo } from "./B05_Profile_UI_RouteEdit_Curve";
|
||||
import { fetchRoutePlan } from "./B05_Profile_Api_Replan";
|
||||
import { bindRouteApply } from "./B05_Profile_UI_RouteEdit_Apply";
|
||||
import {
|
||||
buildEditedPolyline,
|
||||
dragHandleTo as curveDragTo,
|
||||
type EditedCurve,
|
||||
type EditedNode,
|
||||
} from "./B05_Profile_UI_RouteEdit_Curve";
|
||||
import { drawRouteEditScene, polylineLengthM } from "./B05_Profile_UI_RouteEdit_Render";
|
||||
import {
|
||||
bindRouteEditNavigation,
|
||||
contourBandRect,
|
||||
handleAtScreen,
|
||||
nodeAtScreen,
|
||||
segmentAtScreen,
|
||||
stationAtScreen,
|
||||
} from "./B05_Profile_UI_RouteEdit_Input";
|
||||
import {
|
||||
centerDirectionOf,
|
||||
createCurveLabel,
|
||||
deflectionRad,
|
||||
} from "./B05_Profile_UI_RouteEdit_Label";
|
||||
import { createCrossPreview } from "./B05_Profile_UI_RouteEdit_Cross";
|
||||
import { createMeasureTool } from "./B05_Profile_UI_RouteEdit_Measure";
|
||||
import { createCurveBar } from "./B05_Profile_UI_RouteEdit_CurveBar";
|
||||
import {
|
||||
applyArcLocks,
|
||||
applyCurveLimits,
|
||||
curveShortfalls,
|
||||
curveSummary,
|
||||
flattenServerPlan,
|
||||
shortfallCrossed,
|
||||
type CurveLock,
|
||||
} from "./B05_Profile_UI_RouteEdit_Edits";
|
||||
import {
|
||||
@@ -58,27 +66,32 @@ import "./B05_Profile_UI_Style_RouteEdit.css";
|
||||
|
||||
/** 노드를 잡았다고 볼 거리(px). 손가락·마우스 모두 무리 없는 크기. */
|
||||
const NODE_HIT_PX = 9;
|
||||
/** 노드 반지름(px). */
|
||||
const NODE_R = 4;
|
||||
/** 선을 두 번 눌러 노드를 끼울 때, 선에서 이만큼(px) 안쪽이면 그 선으로 본다. */
|
||||
const SEGMENT_HIT_PX = 12;
|
||||
/** 등고선을 보일 **노선 둘레 띠**(m) — 사용자 지시 ⑥(2026-09-07).
|
||||
*
|
||||
* 노선에서 이만큼 밖의 등고선은 안 그린다. **창 크기와 무관한 고정 띠**라 창을 늘리거나
|
||||
* 줄여도 띠가 흔들리지 않는다(사용자가 「다이나믹 창이라 조심」이라 한 자리). 화면 밖을
|
||||
* 걸러내는 일은 `drawPreparedLayer` 가 이미 하므로 여기서는 띠만 덧씌운다. */
|
||||
const CONTOUR_BAND_M = 300;
|
||||
/** 곡선 시작·끝점 손잡이 크기(px) — 노드 동그라미와 구별되게 **속 빈 네모**로 그린다.
|
||||
* 처음엔 3.5px 였는데 선과 색이 같아 눈에도 안 띄고 집기도 어려웠다(2026-09-07 실화면). */
|
||||
const CURVE_HANDLE_PX = 5;
|
||||
/** 등고선을 집었다고 볼 거리(px) — 노드·손잡이보다 **좁게** 둔다(노선 편집이 먼저). */
|
||||
const CONTOUR_HIT_PX = 6;
|
||||
/** 측점 눈금을 집었다고 볼 거리(px) — 눈금이 보이는 자리를 누르면 잡히게 넉넉히. */
|
||||
const STATION_HIT_PX = 11;
|
||||
|
||||
type Vertex = [number, number];
|
||||
|
||||
/** 모달을 연다. [확인]·[예상노선으로]가 끝나면 `onApplied`를 부른다(화면 다시 읽기). */
|
||||
export interface RouteEditOptions {
|
||||
/** 규칙 측점 간격(m) — 좌측 패널이 쥔 값을 그대로 받는다(코드에 굳히지 않는다). */
|
||||
stationIntervalM?: number;
|
||||
/** 확정 지표면 모델 id — 있으면 바탕 등고선을 **LAS 것**으로 쓴다(계획서 0-9 ⑥). */
|
||||
surfaceModelId?: number | null;
|
||||
/** 등고선 간격(m)·평활 여부 — 3D 뷰어가 쓰는 값 그대로. */
|
||||
contourIntervalM?: number;
|
||||
smooth?: boolean;
|
||||
}
|
||||
|
||||
export async function openRouteEditModal(
|
||||
projectId: string,
|
||||
onApplied: () => void | Promise<void>,
|
||||
options: RouteEditOptions = {},
|
||||
): Promise<void> {
|
||||
const stationIntervalM = options.stationIntervalM ?? 20;
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "b05-routeedit";
|
||||
overlay.innerHTML = `
|
||||
@@ -87,7 +100,9 @@ export async function openRouteEditModal(
|
||||
<strong>계획노선 편집</strong>
|
||||
<span class="b05-routeedit__hint">
|
||||
노드 끌기 = 옮기기 · 노드 클릭 = R 라벨 · 선 두 번 클릭 = 노드 추가 ·
|
||||
노드 오른쪽 클릭 = 삭제 · 가운데(휠) 버튼 끌기 = 지도 이동 · 휠 = 확대
|
||||
노드 오른쪽 클릭 = 삭제 · <b>측점 눈금 클릭 = 횡단 미리보기</b> ·
|
||||
<b>Shift+클릭 = 두 점 사이 거리·기울기</b> ·
|
||||
가운데(휠) 버튼 끌기 = 지도 이동 · 휠 = 확대
|
||||
</span>
|
||||
<button type="button" class="b05-routeedit__close" aria-label="닫기">✕</button>
|
||||
</div>
|
||||
@@ -123,14 +138,13 @@ export async function openRouteEditModal(
|
||||
/** 사용자가 잡아 옮기는 **노드**(꺾임점). 서버가 이 노드로 폴리라인을 다시 만든다. */
|
||||
let planned: Vertex[] = [];
|
||||
/** 노드마다의 반지름·내각·법정 위반 — 서버가 함께 내려 준다(표시용). */
|
||||
let nodeInfo: Array<{
|
||||
radius_m: number | null;
|
||||
inner_angle_deg: number | null;
|
||||
violations: string[];
|
||||
}> = [];
|
||||
let nodeInfo: EditedNode[] = [];
|
||||
let minRadiusM = 0;
|
||||
/** **못 넘는** 하한 — 0이면 제한 없음. 기본 반지름(`minRadiusM`)과 다른 값이다(계획서 0-9 ④). */
|
||||
let limitRadiusM = 0;
|
||||
let limitArcM = 0;
|
||||
/** 서버가 준 곡선 성분 — 손잡이(곡선 시작·끝점)를 그리는 재료. 편집하면 비운다. */
|
||||
let curveInfo: RoutePlanCurve[] = [];
|
||||
let curveInfo: EditedCurve[] = [];
|
||||
/** 꺾임점마다의 편집값 — 곡선을 둘지, 반지름을 못박을지(2026-09-07 사용자 지시). */
|
||||
let curveOn: boolean[] = [];
|
||||
let curveRadius: Array<number | null> = [];
|
||||
@@ -143,7 +157,40 @@ export async function openRouteEditModal(
|
||||
/** 되돌리기 사진첩 — 노선을 읽은 뒤에 선다(그전에는 되돌릴 것이 없다). */
|
||||
let history: RouteEditHistory | null = null;
|
||||
let meta: VWorldMeta | null = null;
|
||||
let sheets: PreparedLayer[] = [];
|
||||
/** 바탕 등고선 한 벌 — LAS 것이거나 도엽 것. 고르기는 `_Contour` 몫. */
|
||||
let contours: RouteEditContours | null = null;
|
||||
/** 등고선 말고 함께 깔 도엽 레이어(하천중심선). */
|
||||
let otherSheets: PreparedLayer[] = [];
|
||||
/** 고른 등고선 가닥 — 없으면 -1(계획서 0-9 ⑦). */
|
||||
let pickedContour = -1;
|
||||
/** 측점 횡단 미리보기 창 — 측점 눈금을 누르면 뜬다(계획서 0-9 ⑧). */
|
||||
const crossPreview = createCrossPreview({
|
||||
projectId,
|
||||
bounds: () => canvas.getBoundingClientRect(),
|
||||
request: () => ({
|
||||
vertices: planned.map(([x, y], index) => ({
|
||||
x,
|
||||
y,
|
||||
curve: curveOn[index] !== false,
|
||||
radius_m: curveRadius[index] ?? null,
|
||||
})),
|
||||
min_radius_m: minRadiusM || 12,
|
||||
station_interval_m: stationIntervalM,
|
||||
}),
|
||||
});
|
||||
/** 구간 재기 — Shift+클릭으로 두 점을 찍는다. 셈·서버 묻기는 `_Measure` 몫(계획서 0-9 ⑤). */
|
||||
const measure = createMeasureTool({
|
||||
projectId,
|
||||
stationIntervalM,
|
||||
line: () => (plannedLine.length ? plannedLine : planned),
|
||||
// 아래에 선언된 것을 감싸 넘긴다 — 부르는 시점은 늘 그 뒤다.
|
||||
toScreen: (vertex) => toScreen(vertex),
|
||||
isClosed: () => closed,
|
||||
onChange: () => {
|
||||
status.textContent = `${routeHead()} — ${measure.hint()}`;
|
||||
draw();
|
||||
},
|
||||
});
|
||||
let view: ViewState = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
@@ -159,6 +206,7 @@ export async function openRouteEditModal(
|
||||
window.removeEventListener("resize", resize);
|
||||
historyControls.dispose(); // 단축키는 창(window)에 달려 있어 안 떼면 닫힌 뒤에도 산다.
|
||||
curveLabelBox.destroy(); // 패널은 `document.body` 에 붙어 있어 스스로 안 사라진다.
|
||||
crossPreview.destroy();
|
||||
overlay.remove();
|
||||
};
|
||||
overlay.querySelector(".b05-routeedit__close")!.addEventListener("click", close);
|
||||
@@ -199,111 +247,40 @@ export async function openRouteEditModal(
|
||||
return [meta.x_min + (px - x0) / (sx || 1), meta.y_min + (py - y0) / (sy || 1)];
|
||||
}
|
||||
|
||||
function strokePolyline(points: Vertex[], dash: number[], color: string, width: number): void {
|
||||
if (points.length < 2) return;
|
||||
context.save();
|
||||
context.setLineDash(dash);
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = width;
|
||||
context.beginPath();
|
||||
points.forEach((vertex, index) => {
|
||||
const [x, y] = toScreen(vertex);
|
||||
if (index === 0) context.moveTo(x, y);
|
||||
else context.lineTo(x, y);
|
||||
});
|
||||
context.stroke();
|
||||
context.restore();
|
||||
/** 화면 1m 당 픽셀 — 측점 라벨 솎기 단계를 여기서 정한다. `metricToScreen` 이 선형이라
|
||||
* 100m 떨어진 두 점으로 잰다(1m 로 재면 반올림 오차가 그대로 비율에 실린다). */
|
||||
function pxPerMeter(): number {
|
||||
if (!meta) return 1;
|
||||
const [x0] = metricToScreen(meta, view, meta.x_min, meta.y_min);
|
||||
const [x1] = metricToScreen(meta, view, meta.x_min + 100, meta.y_min);
|
||||
return Math.abs(x1 - x0) / 100;
|
||||
}
|
||||
|
||||
/** 지금 화면에 낼 등고선 간격(m) — 그리기와 집기가 같은 값을 보게 한 자리에서 셈한다. */
|
||||
const contourStepM = (): number =>
|
||||
contours ? pickLevelStep(contours.layer, view, contours.intervalM) : 0;
|
||||
|
||||
function draw(): void {
|
||||
if (closed) return;
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
context.clearRect(0, 0, view.width, view.height);
|
||||
context.fillStyle = style.getPropertyValue("--color-surface") || "#111";
|
||||
context.fillRect(0, 0, view.width, view.height);
|
||||
|
||||
context.save();
|
||||
// 등고선은 **노선 둘레 300m 안**에서만 그린다 — 노선과 상관없는 산줄기까지 다 그리면
|
||||
// 화면이 등고선으로 덮여 노선이 안 보인다(2026-09-07 사용자 지시 ⑥).
|
||||
const band = meta
|
||||
? contourBandRect(plannedLine.length ? plannedLine : planned, toScreen, CONTOUR_BAND_M)
|
||||
: null;
|
||||
if (band) {
|
||||
context.beginPath();
|
||||
context.rect(band.x, band.y, band.width, band.height);
|
||||
context.clip();
|
||||
}
|
||||
context.strokeStyle = style.getPropertyValue("--map-sheet-contour") || "#a5b4fc";
|
||||
context.lineWidth = 0.8;
|
||||
for (const layer of sheets) drawPreparedLayer(context, layer, view, "dot");
|
||||
context.restore();
|
||||
|
||||
strokePolyline(
|
||||
drawRouteEditScene(context, {
|
||||
view,
|
||||
toScreen,
|
||||
pxPerMeter: pxPerMeter(),
|
||||
hasMeta: meta !== null,
|
||||
contours,
|
||||
otherSheets,
|
||||
pickedContour,
|
||||
contourStepM: contourStepM(),
|
||||
measure: measure.points(),
|
||||
expected,
|
||||
[6, 5],
|
||||
style.getPropertyValue("--color-text-secondary") || "#9ca3af",
|
||||
1.6,
|
||||
);
|
||||
// 선은 **폴리라인**(원호 포함)을 그리고, 잡는 동그라미는 **노드**에만 찍는다.
|
||||
// 노드를 옮기는 동안에는 폴리라인이 없으므로 노드를 곧바로 이어 미리 보인다.
|
||||
strokePolyline(
|
||||
plannedLine.length ? plannedLine : planned,
|
||||
[],
|
||||
style.getPropertyValue("--map-route") || "#f97316",
|
||||
2.4,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.fillStyle = style.getPropertyValue("--map-route") || "#f97316";
|
||||
context.strokeStyle = style.getPropertyValue("--map-halo") || "rgba(255,255,255,0.9)";
|
||||
context.lineWidth = 1;
|
||||
planned.forEach((vertex, index) => {
|
||||
const [x, y] = toScreen(vertex);
|
||||
// 법정 기준을 못 맞춘 자리는 붉게 — 막지는 않고 보이기만 한다(2026-09-06 사용자 확정).
|
||||
const bad = (nodeInfo[index]?.violations?.length ?? 0) > 0;
|
||||
context.fillStyle = bad
|
||||
? style.getPropertyValue("--color-danger") || "#dc2626"
|
||||
: style.getPropertyValue("--map-route") || "#f97316";
|
||||
context.beginPath();
|
||||
context.arc(x, y, index === picked ? NODE_R + 2 : NODE_R, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
context.stroke();
|
||||
// 곡선을 지운 자리는 가운데를 비워 「여기는 곡선이 없다」를 보인다.
|
||||
if (curveOn.length && !curveOn[index] && index > 0 && index < planned.length - 1) {
|
||||
context.save();
|
||||
context.fillStyle = style.getPropertyValue("--map-halo") || "rgba(255,255,255,0.9)";
|
||||
context.beginPath();
|
||||
context.arc(x, y, NODE_R - 2, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
context.restore();
|
||||
}
|
||||
plannedLine,
|
||||
planned,
|
||||
nodeInfo,
|
||||
curveInfo,
|
||||
curveOn,
|
||||
picked,
|
||||
stationIntervalM,
|
||||
});
|
||||
|
||||
// 곡선 시작·끝점 — 잡아서 직선 각도와 R 을 함께 바꾸는 손잡이(2026-09-07 사용자 지시).
|
||||
// **속을 비우고 테두리를 굵게** 그린다 — 선·노드와 색이 같으면 눈에도 안 띄고 집기도 어렵다.
|
||||
context.lineWidth = 2;
|
||||
curveInfo.forEach((curve) => {
|
||||
// **늘 보인다**(2026-09-07 사용자 지시) — 직선이 곡선에 닿는 자리는 손잡이이기 이전에
|
||||
// **읽을 정보**다. 한때 고른 곡선만 내보였더니 「표기가 다 사라졌다」는 지적을 받았다.
|
||||
// 노드를 못 집던 문제는 집기 우선순위(노드가 먼저)로 따로 풀었으므로 다 내놓아도 된다.
|
||||
const on = curveOn[curve.node_first] !== false;
|
||||
if (!on) return; // 곡선을 지운 자리에는 접선점도 없다.
|
||||
// 고른 곡선은 속을 채워 도드라지게 — 지금 끌 수 있는 것이 무엇인지 보이게.
|
||||
const isPicked = curve.node_first === picked;
|
||||
[curve.start, curve.end].forEach((point) => {
|
||||
const [x, y] = toScreen([point[0], point[1]]);
|
||||
context.beginPath();
|
||||
const size = isPicked ? CURVE_HANDLE_PX + 1 : CURVE_HANDLE_PX;
|
||||
context.rect(x - size, y - size, size * 2, size * 2);
|
||||
context.fillStyle = isPicked
|
||||
? style.getPropertyValue("--map-route") || "#f97316"
|
||||
: style.getPropertyValue("--map-halo") || "rgba(255,255,255,0.95)";
|
||||
context.fill();
|
||||
context.strokeStyle = style.getPropertyValue("--map-route") || "#f97316";
|
||||
context.stroke();
|
||||
});
|
||||
});
|
||||
context.restore();
|
||||
// 라벨은 **그린 뒤** 자리를 맞춘다 — 확대·이동·창 크기가 바뀌어도 고른 노드에 붙어 있게.
|
||||
syncCurveBar();
|
||||
}
|
||||
@@ -346,12 +323,27 @@ export async function openRouteEditModal(
|
||||
function markEdited(): void {
|
||||
// 길이를 붙든 자리는 교각이 바뀌었을 수 있다 — 그리기 전에 R 부터 다시 잡는다.
|
||||
applyArcLocks(planned, curveLock, curveArc, curveRadius);
|
||||
// 지정해 둔 값이 하한을 밑돌면 하한까지 끌어올린다(계획서 0-9 ④).
|
||||
applyCurveLimits(planned, curveOn, curveRadius, limitRadiusM, limitArcM);
|
||||
const built = buildEditedPolyline(planned, curveOn, curveRadius, minRadiusM);
|
||||
plannedLine = built.vertices;
|
||||
curveInfo = built.curves;
|
||||
nodeInfo = built.nodes;
|
||||
}
|
||||
|
||||
/** 상태줄 머리 — 지금 그려진 계획노선 길이와 노드 수(계획서 0-9 ①). 원호가 정점으로
|
||||
* 펴져 있어 브라우저에서 바로 잴 수 있다 — 서버에 묻지 않는다. */
|
||||
const routeHead = (): string =>
|
||||
`길이 ${polylineLengthM(plannedLine.length ? plannedLine : planned).toFixed(1)}m · ` +
|
||||
`노드 ${planned.length}개`;
|
||||
|
||||
/** 고른 등고선의 높이 — 못 읽었으면 높이 없이 「고른 등고선」만(계획서 0-9 ⑦). */
|
||||
const contourHint = (): string => {
|
||||
if (pickedContour < 0) return "등고선을 누르면 그 줄의 높이가 보입니다.";
|
||||
const level = contours?.layer.features[pickedContour]?.labelValue ?? null;
|
||||
return level === null ? "등고선 한 줄을 골랐습니다." : `고른 등고선 ${level}m.`;
|
||||
};
|
||||
|
||||
/** 상태줄 꼬리 — 셈은 `_Edits` 몫. */
|
||||
const curveHint = (): string =>
|
||||
curveSummary({
|
||||
@@ -365,84 +357,32 @@ export async function openRouteEditModal(
|
||||
fresh: nodeInfo.length === 0,
|
||||
});
|
||||
|
||||
// ── 곡선 라벨 — 고른 꺾임점 옆(곡선 중심 반대쪽)에 뜬다. 그리기는 `_Label` 몫 ──
|
||||
const curveLabelBox = createCurveLabel({
|
||||
onRadius: (value) => {
|
||||
if (picked < 0) return;
|
||||
curveRadius[picked] = value;
|
||||
// 반지름만 바꾼 것이라 노드 자리는 그대로지만, 그려진 선은 낡았다.
|
||||
applyEdit(value === null ? "반지름을 자동으로 되돌렸습니다." : "반지름을 바꿨습니다.");
|
||||
},
|
||||
onArcLength: (value) => {
|
||||
if (picked < 0) return;
|
||||
// 곡선 길이 L 과 반지름 R 은 L = R·Δ 로 묶여 있다(Δ = 교각, 앞뒤 직선이 정함).
|
||||
// 그래서 길이를 받으면 반지름으로 바꿔 **한 값만** 들고 간다 — 두 벌로 두면 어긋난다.
|
||||
const deflection = deflectionRad(nodeInfo[picked]?.inner_angle_deg);
|
||||
curveArc[picked] = value;
|
||||
curveRadius[picked] = value !== null && deflection > 1e-9 ? value / deflection : null;
|
||||
applyEdit(value === null ? "반지름을 자동으로 되돌렸습니다." : "곡선 길이를 바꿨습니다.");
|
||||
},
|
||||
onLock: (lock) => {
|
||||
if (picked < 0) return;
|
||||
curveLock[picked] = lock;
|
||||
const deflection = deflectionRad(nodeInfo[picked]?.inner_angle_deg);
|
||||
const shown = curveRadius[picked] ?? nodeInfo[picked]?.radius_m ?? null;
|
||||
// 길이를 붙들려면 지금 길이를 적어 둬야 한다 — 뒤에 교각이 바뀌면 이 값으로 R 을 다시 잡는다.
|
||||
if (lock === "arc") {
|
||||
curveArc[picked] = shown !== null && deflection > 1e-9 ? shown * deflection : null;
|
||||
}
|
||||
// R 을 붙들 때 칸이 비어 있으면 지금 그려진 R 을 적어 둔다(자동 상태를 그대로 못 박음).
|
||||
if (lock === "radius" && curveRadius[picked] === null) curveRadius[picked] = shown;
|
||||
applyEdit(
|
||||
lock === "radius"
|
||||
? "반지름을 고정했습니다."
|
||||
: lock === "arc"
|
||||
? "곡선 길이를 고정했습니다."
|
||||
: "고정을 풀었습니다.",
|
||||
);
|
||||
},
|
||||
onCurveOn: (on) => {
|
||||
if (picked < 0) return;
|
||||
curveOn[picked] = on;
|
||||
applyEdit(on ? "곡선을 넣었습니다." : "곡선을 지웠습니다.");
|
||||
},
|
||||
// ── 곡선 라벨 — 고른 꺾임점 옆에 뜨는 조작 패널. 배선은 `_CurveBar` 몫 ──
|
||||
const curveBar = createCurveBar({
|
||||
canvas,
|
||||
state: () => ({
|
||||
picked,
|
||||
planned,
|
||||
nodeInfo,
|
||||
curveInfo,
|
||||
curveOn,
|
||||
curveRadius,
|
||||
curveLock,
|
||||
curveArc,
|
||||
limitRadiusM,
|
||||
limitArcM,
|
||||
}),
|
||||
toScreen: (vertex) => toScreen(vertex),
|
||||
applyEdit: (message) => applyEdit(message),
|
||||
});
|
||||
|
||||
/** 고른 자리에 맞춰 라벨을 옮겨 그린다. 끝점은 곡선이 없으므로 라벨을 숨긴다. */
|
||||
function syncCurveBar(): void {
|
||||
if (!(picked > 0 && picked < planned.length - 1)) {
|
||||
curveLabelBox.hide();
|
||||
return;
|
||||
}
|
||||
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 rect = canvas.getBoundingClientRect();
|
||||
const [screenX, screenY] = toScreen(planned[picked]);
|
||||
curveLabelBox.show({
|
||||
seat: picked,
|
||||
// 패널은 `position: fixed` 라 **화면 좌표**로 넘긴다 — 모달 밖으로 넘어가도 안 잘린다.
|
||||
at: [screenX + rect.left, screenY + rect.top],
|
||||
centerDirection: pickedCurve
|
||||
? centerDirectionOf(
|
||||
toScreen([pickedCurve.apex[0], pickedCurve.apex[1]]),
|
||||
toScreen(pickedCurve.start),
|
||||
toScreen(pickedCurve.end),
|
||||
)
|
||||
: null,
|
||||
curveOn: curveOn[picked] !== false,
|
||||
radiusShown: shown,
|
||||
arcLengthShown: shown === null || deflection <= 1e-9 ? null : shown * deflection,
|
||||
lock: curveLock[picked] ?? null,
|
||||
innerAngleDeg: nodeInfo[picked]?.inner_angle_deg ?? null,
|
||||
});
|
||||
}
|
||||
const curveLabelBox = curveBar.label;
|
||||
const syncCurveBar = curveBar.sync;
|
||||
|
||||
/** 한 번의 편집을 마무리한다 — 다시 그리고, 라벨·상태줄을 맞추고, 되돌리기에 쌓는다. */
|
||||
function applyEdit(message: string, record = true): void {
|
||||
markEdited();
|
||||
syncCurveBar();
|
||||
status.textContent = `노드 ${planned.length}개 — ${message} ${curveHint()}`;
|
||||
status.textContent = `${routeHead()} — ${message} ${curveHint()}`;
|
||||
draw();
|
||||
if (record) history?.commit(snapshotNow());
|
||||
historyControls.sync();
|
||||
@@ -481,6 +421,11 @@ export async function openRouteEditModal(
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const px = event.clientX - rect.left;
|
||||
const py = event.clientY - rect.top;
|
||||
if (event.shiftKey) {
|
||||
// 구간 재기가 먼저다 — 노드 위에서도 재려는 뜻으로 본다(계획서 0-9 ⑤).
|
||||
void measure.pick(px, py);
|
||||
return;
|
||||
}
|
||||
// **노드가 손잡이보다 먼저다**(2026-09-07 사용자 지적 ④). 반대로 두었더니 헤어핀처럼
|
||||
// 곡선이 몰린 데서는 손잡이가 늘 먼저 잡혀 **노드를 아예 못 집었다**(실화면에서 격자로
|
||||
// 훑어 보니 잡히는 것이 전부 손잡이였음). 손잡이는 고른 곡선에만 나오므로 겹침도 적다.
|
||||
@@ -495,6 +440,32 @@ export async function openRouteEditModal(
|
||||
picked = dragHandle.node;
|
||||
syncCurveBar();
|
||||
draw();
|
||||
} else if (
|
||||
// 측점 눈금을 누르면 그 측점 횡단을 따로 띄운다(계획서 0-9 ⑧). 노드·손잡이 다음이다.
|
||||
(() => {
|
||||
const chainage = stationAtScreen(
|
||||
plannedLine.length ? plannedLine : planned,
|
||||
toScreen,
|
||||
stationIntervalM,
|
||||
px,
|
||||
py,
|
||||
STATION_HIT_PX,
|
||||
);
|
||||
if (chainage === null) return false;
|
||||
void crossPreview.open(chainage);
|
||||
return true;
|
||||
})()
|
||||
) {
|
||||
/* 횡단 창이 떴다 — 더 집지 않는다. */
|
||||
} else if (contours) {
|
||||
// 노드도 손잡이도 아니면 **등고선**을 집는다 — 노선 편집이 늘 먼저다(계획서 0-9 ⑦).
|
||||
// 빈 자리를 누르면 -1 이 되어 고른 것이 풀린다.
|
||||
const hit = hitPreparedLayer(contours.layer, view, px, py, CONTOUR_HIT_PX, contourStepM());
|
||||
if (hit !== pickedContour) {
|
||||
pickedContour = hit;
|
||||
status.textContent = `${routeHead()} — ${contourHint()} ${curveHint()}`;
|
||||
draw();
|
||||
}
|
||||
}
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
});
|
||||
@@ -509,7 +480,8 @@ export async function openRouteEditModal(
|
||||
const moved = dragHandleTo(node, dragHandle.end, toMetric(px, py));
|
||||
if (moved) {
|
||||
planned[node] = moved.apex;
|
||||
curveRadius[node] = Math.round(moved.radius * 100) / 100;
|
||||
// 손으로 끌어도 하한 아래로는 안 내려간다 — 거기서 멈춘다(계획서 0-9 ④).
|
||||
curveRadius[node] = Math.max(limitRadiusM, Math.round(moved.radius * 100) / 100);
|
||||
curveOn[node] = true;
|
||||
picked = node;
|
||||
// 손잡이 자리는 다시 셈한 곡선에서 나온다 — 접선 자리가 모자라 R 이 눌리면 손이
|
||||
@@ -517,18 +489,32 @@ export async function openRouteEditModal(
|
||||
dragMoved = true;
|
||||
markEdited();
|
||||
syncCurveBar();
|
||||
status.textContent = `노드 ${planned.length}개 — 곡선을 잡는 중. ${curveHint()}`;
|
||||
status.textContent = `${routeHead()} — 곡선을 잡는 중. ${curveHint()}`;
|
||||
draw();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (dragNode >= 0) {
|
||||
// 옮기기 **전**에 하한을 지키던 자리 — 이미 밑돌던 자리는 그대로 고칠 수 있어야 하므로
|
||||
// **지키던 자리가 넘어가는 것만** 막는다(계획서 0-9 ④).
|
||||
const before = curveShortfalls(nodeInfo, limitRadiusM, limitArcM);
|
||||
const previous = planned[dragNode];
|
||||
dragMoved = true;
|
||||
planned[dragNode] = toMetric(px, py);
|
||||
markEdited(); // 곡선을 그 자리에서 다시 그린다 — 나머지 곡선은 그대로 남는다.
|
||||
if (shortfallCrossed(before, curveShortfalls(nodeInfo, limitRadiusM, limitArcM))) {
|
||||
// 접선 자리가 모자라 R 이 하한 아래로 눌리는 자리다 — 그 걸음만 되돌린다.
|
||||
planned[dragNode] = previous;
|
||||
markEdited();
|
||||
status.textContent =
|
||||
`${routeHead()} — 하한에 걸려 더 못 옮깁니다` +
|
||||
`(곡선반지름 ${limitRadiusM}m${limitArcM > 0 ? ` · 곡선 길이 ${limitArcM}m` : ""}).`;
|
||||
draw();
|
||||
return;
|
||||
}
|
||||
// 끄는 동안에도 상태줄이 살아 있어야 한다 — 예전에는 여기서 아무 말이 없어
|
||||
// 「곡선이 사라졌다」는 인상만 남았다(2026-09-07 사용자 지적 ②).
|
||||
status.textContent = `노드 ${planned.length}개 — 옮기는 중. ${curveHint()}`;
|
||||
status.textContent = `${routeHead()} — 옮기는 중. ${curveHint()}`;
|
||||
draw();
|
||||
return;
|
||||
}
|
||||
@@ -595,54 +581,13 @@ export async function openRouteEditModal(
|
||||
draw,
|
||||
});
|
||||
|
||||
async function runHeavy(label: string, task: () => Promise<unknown>): Promise<void> {
|
||||
busy.hidden = false;
|
||||
// ⚠ 「몇 분」은 옛 값이었다 — 0-11 로 **약 90초**가 됐다(2026-09-09 실측 네 번:
|
||||
// 87.3 · 90.0 · 93.9 · 95.4초). 중간 취소를 안 만드는 대신, **얼마나 지났는지**를
|
||||
// 보여 사람이 멈춘 것인지 도는 것인지 알 수 있게 한다(계획서 0-2).
|
||||
const message = busy.querySelector("span")!;
|
||||
const started = Date.now();
|
||||
const tick = (): void => {
|
||||
const seconds = Math.round((Date.now() - started) / 1000);
|
||||
message.textContent = `${label} — 배수유역부터 다시 계산 중입니다. 1분 반쯤 걸립니다 (${seconds}초 지남).`;
|
||||
};
|
||||
tick();
|
||||
const timer = window.setInterval(tick, 1000);
|
||||
try {
|
||||
await task();
|
||||
// 노선이 바뀌면 세션 초안·조회 캐시는 옛 노선 것이라 남기지 않는다(PLAN 0-7 확정 5).
|
||||
clearDrafts(projectId);
|
||||
clearResults(projectId);
|
||||
showToast("노선을 다시 계산했습니다.", "success");
|
||||
close();
|
||||
await onApplied();
|
||||
} catch (error) {
|
||||
busy.hidden = true;
|
||||
showToast(error instanceof Error ? error.message : "노선 재계산에 실패했습니다.", "error");
|
||||
} finally {
|
||||
window.clearInterval(timer); // 성공·실패·닫힘 어느 쪽이든 멈춘다
|
||||
}
|
||||
}
|
||||
|
||||
overlay.querySelector('[data-act="apply"]')!.addEventListener("click", () => {
|
||||
if (planned.length < 2) {
|
||||
showToast("노선은 노드가 2개 이상이어야 합니다.", "error");
|
||||
return;
|
||||
}
|
||||
void runHeavy("계획노선 반영", () =>
|
||||
replanRoute(
|
||||
projectId,
|
||||
planned.map(([x, y], index) => ({
|
||||
x,
|
||||
y,
|
||||
curve: curveOn[index] !== false,
|
||||
radius_m: curveRadius[index] ?? null,
|
||||
})),
|
||||
),
|
||||
);
|
||||
});
|
||||
overlay.querySelector('[data-act="reset"]')!.addEventListener("click", () => {
|
||||
void runHeavy("예상노선으로 되돌리기", () => resetRoutePlan(projectId));
|
||||
bindRouteApply({
|
||||
overlay,
|
||||
busy,
|
||||
projectId,
|
||||
nodes: () => ({ planned, curveOn, curveRadius }),
|
||||
close,
|
||||
onApplied,
|
||||
});
|
||||
|
||||
// ── 자료 읽기 — 노선 두 벌 + 등고선 도엽(배수유역도와 같은 것) ──
|
||||
@@ -658,6 +603,8 @@ export async function openRouteEditModal(
|
||||
// (2026-09-06 사용자 지시: 노드를 제어해 계획노선을 고친다).
|
||||
const nodes = plan.nodes ?? [];
|
||||
minRadiusM = plan.min_radius_m ?? 0;
|
||||
limitRadiusM = plan.limit_radius_m ?? 0;
|
||||
limitArcM = plan.limit_curve_length_m ?? 0;
|
||||
// 곡선 성분을 편집할 수 있는 꼴로 편다 — 셈은 `_Edits` 몫(까닭도 그쪽에 적었다).
|
||||
const flat = flattenServerPlan(nodes, plan.curves ?? []);
|
||||
planned = flat.planned;
|
||||
@@ -675,9 +622,23 @@ export async function openRouteEditModal(
|
||||
if (!planned.length) planned = plannedLine.map((vertex) => [vertex[0], vertex[1]]);
|
||||
meta = drainage.meta;
|
||||
const normalizer = createNormalizer(drainage.meta);
|
||||
sheets = drainage.layers
|
||||
// 등고선은 따로 고른다(LAS 우선). 나머지 도엽 레이어(하천중심선)만 배경으로 깐다.
|
||||
otherSheets = drainage.layers
|
||||
.filter(([layer]) => layer !== "도엽_등고선")
|
||||
.map(([, collection]) => (collection ? prepareLayer(collection, normalizer) : null))
|
||||
.filter((layer): layer is PreparedLayer => layer !== null);
|
||||
contours = await loadRouteEditContours(
|
||||
projectId,
|
||||
drainage.meta,
|
||||
normalizer,
|
||||
drainage.layers.find(([layer]) => layer === "도엽_등고선")?.[1] ?? null,
|
||||
{
|
||||
surfaceModelId: options.surfaceModelId ?? null,
|
||||
intervalM: options.contourIntervalM ?? 1,
|
||||
smooth: options.smooth ?? false,
|
||||
},
|
||||
);
|
||||
if (closed) return;
|
||||
resize();
|
||||
const xs = planned.map((vertex) => vertex[0]);
|
||||
const ys = planned.map((vertex) => vertex[1]);
|
||||
@@ -694,7 +655,8 @@ export async function openRouteEditModal(
|
||||
);
|
||||
view = { ...view, ...fitted };
|
||||
status.textContent =
|
||||
`노드 ${planned.length}개 · ${plan.edited ? "고친 계획노선" : "초기 폴리라인"} · ` +
|
||||
`${routeHead()} · ${plan.edited ? "고친 계획노선" : "초기 폴리라인"} · ` +
|
||||
`${contours?.source === "las" ? "LAS 등고선" : "도엽 등고선"} · ` +
|
||||
curveHint();
|
||||
draw();
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_Apply.ts
|
||||
* 계획노선 편집 모달의 **[확인]·[예상노선으로]** — 무거운 재계산과 대기 표시.
|
||||
*
|
||||
* 누르면 서버가 배수유역부터 종·횡단·유토곡선까지 전 단계를 다시 돈다(약 90초). 중간 취소는
|
||||
* 만들지 않기로 했으므로(계획서 0-2, 2026-09-09) **얼마나 지났는지**를 초로 보여 사람이
|
||||
* 멈춘 것인지 도는 것인지 알 수 있게 한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { clearDrafts, clearResults } from "../A00_Common/b_page_state";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { replanRoute, resetRoutePlan } from "./B05_Profile_Api_Replan";
|
||||
|
||||
type Vertex = [number, number];
|
||||
|
||||
export interface RouteApplyParams {
|
||||
overlay: HTMLElement;
|
||||
/** 화면 전체를 덮는 대기 막. 안에 `<span>` 한 개가 글을 받는다. */
|
||||
busy: HTMLElement;
|
||||
projectId: string;
|
||||
/** 지금 편집값 — 누른 순간에 읽는다. */
|
||||
nodes: () => { planned: Vertex[]; curveOn: boolean[]; curveRadius: Array<number | null> };
|
||||
/** 성공하면 모달을 닫고 화면을 다시 읽는다. */
|
||||
close: () => void;
|
||||
onApplied: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
/** [확인]·[예상노선으로]를 붙인다. 리스너는 모달과 수명이 같다. */
|
||||
export function bindRouteApply(params: RouteApplyParams): void {
|
||||
const { overlay, busy, projectId } = params;
|
||||
|
||||
async function runHeavy(label: string, task: () => Promise<unknown>): Promise<void> {
|
||||
busy.hidden = false;
|
||||
// ⚠ 「몇 분」은 옛 값이었다 — 0-11 로 **약 90초**가 됐다(2026-09-09 실측 네 번:
|
||||
// 87.3 · 90.0 · 93.9 · 95.4초).
|
||||
const message = busy.querySelector("span")!;
|
||||
const started = Date.now();
|
||||
const tick = (): void => {
|
||||
const seconds = Math.round((Date.now() - started) / 1000);
|
||||
message.textContent = `${label} — 배수유역부터 다시 계산 중입니다. 1분 반쯤 걸립니다 (${seconds}초 지남).`;
|
||||
};
|
||||
tick();
|
||||
const timer = window.setInterval(tick, 1000);
|
||||
try {
|
||||
await task();
|
||||
// 노선이 바뀌면 세션 초안·조회 캐시는 옛 노선 것이라 남기지 않는다(PLAN 0-7 확정 5).
|
||||
clearDrafts(projectId);
|
||||
clearResults(projectId);
|
||||
showToast("노선을 다시 계산했습니다.", "success");
|
||||
params.close();
|
||||
await params.onApplied();
|
||||
} catch (error) {
|
||||
busy.hidden = true;
|
||||
showToast(error instanceof Error ? error.message : "노선 재계산에 실패했습니다.", "error");
|
||||
} finally {
|
||||
window.clearInterval(timer); // 성공·실패·닫힘 어느 쪽이든 멈춘다
|
||||
}
|
||||
}
|
||||
|
||||
overlay.querySelector('[data-act="apply"]')!.addEventListener("click", () => {
|
||||
const { planned, curveOn, curveRadius } = params.nodes();
|
||||
if (planned.length < 2) {
|
||||
showToast("노선은 노드가 2개 이상이어야 합니다.", "error");
|
||||
return;
|
||||
}
|
||||
void runHeavy("계획노선 반영", () =>
|
||||
replanRoute(
|
||||
projectId,
|
||||
planned.map(([x, y], index) => ({
|
||||
x,
|
||||
y,
|
||||
curve: curveOn[index] !== false,
|
||||
radius_m: curveRadius[index] ?? null,
|
||||
})),
|
||||
),
|
||||
);
|
||||
});
|
||||
overlay.querySelector('[data-act="reset"]')!.addEventListener("click", () => {
|
||||
void runHeavy("예상노선으로 되돌리기", () => resetRoutePlan(projectId));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_Contour.ts
|
||||
* 계획노선 편집 모달이 바탕에 깔 **등고선 한 벌**을 고른다.
|
||||
*
|
||||
* **어느 등고선을 쓰나**(2026-09-12 사용자 지시 ⑥) — 도엽 등고선과 LAS 로 만든 등고선은
|
||||
* 서로 어긋난다. 노선은 실제 지형 위에 놓여야 하므로 **확정 지표면 모델이 있으면 LAS 쪽**을
|
||||
* 쓰고, 없는 프로젝트에서만 지금까지처럼 도엽 등고선을 쓴다.
|
||||
*
|
||||
* 둘은 생김새가 다르다 — 도엽은 위경도 GeoJSON(표고는 `등고수치` 속성), LAS 는 사업지
|
||||
* 좌표(m) 점렬(표고는 `level`)이다. 여기서 **같은 `PreparedLayer` 한 꼴로 맞춰** 내보내
|
||||
* 그리기·라벨·집기가 출처를 안 가리게 한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { fetchCachedJson } from "../A00_Common/b_asset_cache";
|
||||
import {
|
||||
type GeoJsonCollection,
|
||||
type Normalizer,
|
||||
type PreparedLayer,
|
||||
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||
import {
|
||||
prepareLayer,
|
||||
prepareMetricPolylines,
|
||||
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender_Prepare";
|
||||
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
|
||||
/** 도엽 등고선의 표고 속성 이름 — B04 지도가 쓰는 것과 같은 키. */
|
||||
const SHEET_ELEVATION_KEYS = ["등고수치"];
|
||||
/** 표고를 못 읽었을 때 라벨 솎기에 쓸 간격(m). */
|
||||
const FALLBACK_INTERVAL_M = 5;
|
||||
|
||||
export interface RouteEditContours {
|
||||
layer: PreparedLayer;
|
||||
/** 등고선 간격(m) — 라벨을 몇 줄마다 낼지 정하는 기준. */
|
||||
intervalM: number;
|
||||
source: "las" | "sheet";
|
||||
}
|
||||
|
||||
interface ContourResponse {
|
||||
contours: Array<{ level: number; coordinates: Array<[number, number, number]> }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 바탕 등고선을 읽는다. 확정 지표면 모델이 있으면 LAS, 없으면 이미 받아 둔 도엽 컬렉션.
|
||||
*
|
||||
* LAS 쪽을 못 읽으면 **조용히 도엽으로 내려앉는다** — 등고선이 아예 없는 화면보다 낫고,
|
||||
* 어느 쪽을 쓰고 있는지는 `source` 로 나가 상태줄에 적힌다.
|
||||
*/
|
||||
export async function loadRouteEditContours(
|
||||
projectId: string,
|
||||
meta: VWorldMeta,
|
||||
normalizer: Normalizer,
|
||||
sheet: GeoJsonCollection | null,
|
||||
options: { surfaceModelId: number | null; intervalM: number; smooth: boolean },
|
||||
): Promise<RouteEditContours> {
|
||||
if (options.surfaceModelId !== null) {
|
||||
const interval = options.intervalM > 0 ? options.intervalM : 1;
|
||||
try {
|
||||
// 3D 뷰어가 쓰는 것과 **같은 파일**이다 — 보관함에 있으면 다시 내려받지 않는다.
|
||||
const data = await fetchCachedJson<ContourResponse>(
|
||||
projectId,
|
||||
`${API_BASE_URL}/projects/${projectId}/surface/models/${options.surfaceModelId}` +
|
||||
`/contour?interval=${interval}&smooth=${options.smooth}`,
|
||||
);
|
||||
const lines = (data.contours ?? [])
|
||||
.map((contour) => ({
|
||||
points: contour.coordinates.map(([x, y]) => [x, y] as const),
|
||||
label: contour.level,
|
||||
}))
|
||||
.filter((line) => line.points.length >= 2);
|
||||
if (lines.length > 0) {
|
||||
return { layer: prepareMetricPolylines(lines, meta), intervalM: interval, source: "las" };
|
||||
}
|
||||
} catch {
|
||||
/* 내려앉는다 — 아래 도엽 갈래로 이어 간다. */
|
||||
}
|
||||
}
|
||||
const layer = prepareLayer(sheet ?? undefined, normalizer, SHEET_ELEVATION_KEYS);
|
||||
return { layer, intervalM: inferIntervalM(layer), source: "sheet" };
|
||||
}
|
||||
|
||||
/** 도엽 등고선의 간격(m) — 표고 값들의 **가장 좁은 칸**을 간격으로 본다. */
|
||||
function inferIntervalM(layer: PreparedLayer): number {
|
||||
const levels = [
|
||||
...new Set(
|
||||
layer.features
|
||||
.map((feature) => feature.labelValue)
|
||||
.filter((value): value is number => value !== null),
|
||||
),
|
||||
].sort((a, b) => a - b);
|
||||
let smallest = Infinity;
|
||||
for (let index = 1; index < levels.length; index += 1) {
|
||||
const gap = levels[index] - levels[index - 1];
|
||||
if (gap > 0 && gap < smallest) smallest = gap;
|
||||
}
|
||||
return Number.isFinite(smallest) ? smallest : FALLBACK_INTERVAL_M;
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_Cross.ts
|
||||
* 계획노선 편집 중 **한 측점의 횡단 미리보기** — 따로 뜨는 작은 창(계획서 0-9 ⑧).
|
||||
*
|
||||
* 보이는 것은 셋뿐이다(2026-09-12 사용자 확정) — **원지반 횡단선 · 기본 계획 횡단선 ·
|
||||
* 계획 횡단의 성토사면 길이**. 구조물은 그리지 않는다.
|
||||
*
|
||||
* ⚠ **계획고는 편집 중에 없다** — [확인] 뒤 전 체인이 낳는 값이다. 그래서 서버가 그 측점의
|
||||
* 지반고를 그대로 계획고로 놓고(지반 추종) 사면만 세운 「기본 계획 횡단」을 낸다. 확정 뒤의
|
||||
* 횡단과 다를 수 있고, 창 머리에 그렇게 적어 둔다.
|
||||
*
|
||||
* 셈은 **B05·B06 정본을 그대로 재사용**한다 — 측점·지반 샘플은 `generate_sections`, 설계선은
|
||||
* `compute_cross_design`(서버), 성토사면 길이는 B06 화면이 쓰는 `fillSlopeLengths`(여기).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSection } from "./../B06_Section/B06_Section_Api_Fetch";
|
||||
import { fillSlopeLengths } from "./../B06_Section/B06_Section_UI_Cross_Fit";
|
||||
import { fetchCrossPreview, type CrossPreviewResponse } from "./B05_Profile_Api_Replan";
|
||||
|
||||
/** 그림 가장자리 여백(px). */
|
||||
const PAD = 28;
|
||||
|
||||
export interface CrossPreviewParams {
|
||||
projectId: string;
|
||||
/** 창을 처음 띄울 테두리(화면 좌표) — 보통 모달의 지도 칸. */
|
||||
bounds: () => DOMRect;
|
||||
/** 지금 편집값 — 누른 순간에 읽어 서버로 보낸다. */
|
||||
request: () => {
|
||||
vertices: Array<{ x: number; y: number; curve: boolean; radius_m: number | null }>;
|
||||
min_radius_m: number;
|
||||
station_interval_m: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CrossPreviewWindow {
|
||||
/** 그 측점의 횡단을 띄운다. 이미 떠 있으면 내용만 갈아 끼운다. */
|
||||
open: (chainageM: number) => Promise<void>;
|
||||
/** 모달을 닫을 때 — 몸통이 `document.body` 에 붙어 있어 스스로 안 사라진다. */
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export function createCrossPreview(params: CrossPreviewParams): CrossPreviewWindow {
|
||||
const root = document.createElement("div");
|
||||
root.className = "b05-routeedit__cross";
|
||||
root.hidden = true;
|
||||
root.innerHTML = `
|
||||
<div class="b05-routeedit__cross-head">
|
||||
<strong class="b05-routeedit__cross-title">횡단 미리보기</strong>
|
||||
<button type="button" class="b05-routeedit__cross-close" aria-label="닫기">✕</button>
|
||||
</div>
|
||||
<canvas class="b05-routeedit__cross-canvas" width="420" height="260"></canvas>
|
||||
<div class="b05-routeedit__cross-foot"></div>`;
|
||||
document.body.append(root);
|
||||
|
||||
const head = root.querySelector<HTMLElement>(".b05-routeedit__cross-head")!;
|
||||
const title = root.querySelector<HTMLElement>(".b05-routeedit__cross-title")!;
|
||||
const foot = root.querySelector<HTMLElement>(".b05-routeedit__cross-foot")!;
|
||||
const canvas = root.querySelector<HTMLCanvasElement>(".b05-routeedit__cross-canvas")!;
|
||||
const context = canvas.getContext("2d")!;
|
||||
|
||||
root.querySelector(".b05-routeedit__cross-close")!.addEventListener("click", () => {
|
||||
root.hidden = true;
|
||||
});
|
||||
// 창 위에서 누른 것이 지도로 새어 나가면 노드가 딸려 움직인다.
|
||||
for (const type of ["pointerdown", "dblclick", "contextmenu", "wheel"] as const) {
|
||||
root.addEventListener(type, (event) => event.stopPropagation());
|
||||
}
|
||||
|
||||
// ── 머리를 잡아 옮기기 — 노선을 가리면 손으로 치울 수 있어야 한다 ──
|
||||
let dragFrom: { x: number; y: number; left: number; top: number } | null = null;
|
||||
head.addEventListener("pointerdown", (event) => {
|
||||
if ((event.target as HTMLElement).closest("button")) return;
|
||||
dragFrom = { x: event.clientX, y: event.clientY, left: root.offsetLeft, top: root.offsetTop };
|
||||
head.setPointerCapture(event.pointerId);
|
||||
event.preventDefault();
|
||||
});
|
||||
head.addEventListener("pointermove", (event) => {
|
||||
if (!dragFrom) return;
|
||||
root.style.left = `${Math.round(dragFrom.left + event.clientX - dragFrom.x)}px`;
|
||||
root.style.top = `${Math.round(dragFrom.top + event.clientY - dragFrom.y)}px`;
|
||||
});
|
||||
const stopDrag = (event: PointerEvent): void => {
|
||||
if (head.hasPointerCapture(event.pointerId)) head.releasePointerCapture(event.pointerId);
|
||||
dragFrom = null;
|
||||
};
|
||||
head.addEventListener("pointerup", stopDrag);
|
||||
head.addEventListener("pointercancel", stopDrag);
|
||||
|
||||
/** 이번에 물은 측점 — 늦게 온 응답을 옛 자리에 적지 않으려고 들고 있는다. */
|
||||
let asked = -1;
|
||||
|
||||
return {
|
||||
async open(chainageM) {
|
||||
asked = chainageM;
|
||||
root.hidden = false;
|
||||
if (!root.style.left) {
|
||||
// 처음 열 때만 자리를 잡는다 — 그 뒤에는 사용자가 옮긴 자리를 지킨다.
|
||||
// 지도 칸 **오른쪽 아래**에 붙인다 — 모달 머리·하단 정보행을 가리지 않는 자리다.
|
||||
const box = params.bounds();
|
||||
root.style.left = `${Math.round(box.right - root.offsetWidth - 16)}px`;
|
||||
root.style.top = `${Math.round(box.bottom - root.offsetHeight - 16)}px`;
|
||||
}
|
||||
title.textContent = "횡단 미리보기 — 읽는 중…";
|
||||
foot.textContent = "";
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
let preview: CrossPreviewResponse;
|
||||
try {
|
||||
preview = await fetchCrossPreview(params.projectId, {
|
||||
...params.request(),
|
||||
chainage_m: chainageM,
|
||||
});
|
||||
} catch (error) {
|
||||
if (asked !== chainageM) return;
|
||||
title.textContent = "횡단 미리보기";
|
||||
foot.textContent = error instanceof Error ? error.message : "횡단을 읽지 못했습니다.";
|
||||
return;
|
||||
}
|
||||
if (asked !== chainageM || root.hidden) return;
|
||||
title.textContent = `횡단 미리보기 — ${preview.label ?? `${preview.chainage_m}m`}`;
|
||||
drawCross(context, canvas, preview);
|
||||
foot.textContent = summarize(preview);
|
||||
},
|
||||
destroy() {
|
||||
root.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 성토사면 길이·절성토 면적 한 줄. 계획고가 없다는 것도 여기 적는다. */
|
||||
function summarize(preview: CrossPreviewResponse): string {
|
||||
const design = preview.design;
|
||||
if (!design) return "계획고를 못 세워 계획 횡단을 그리지 못했습니다.";
|
||||
// 성토사면 길이는 **B06 화면이 쓰는 그 함수**를 그대로 부른다 — 두 화면이 다른 길이를
|
||||
// 말하면 안 된다. 필요한 것은 `samples` 와 `design` 둘뿐이라 그만 담아 넘긴다.
|
||||
const lengths = fillSlopeLengths({
|
||||
samples: preview.samples,
|
||||
design,
|
||||
} as unknown as CrossSection);
|
||||
const sides = (["left", "right"] as const)
|
||||
.filter((side) => lengths[side] !== null)
|
||||
.map((side) => {
|
||||
const value = lengths[side]!;
|
||||
const label = side === "left" ? "좌" : "우";
|
||||
// 계산 반폭 안에서 원지반을 못 만난 사면은 거기까지만 잰 하한값이라 「≥」로 구분한다.
|
||||
return `${label} ${value.open ? "≥" : ""}${value.lengthM.toFixed(2)}m`;
|
||||
});
|
||||
const slope = sides.length ? `성토사면 ${sides.join(" · ")}` : "성토측 없음";
|
||||
return (
|
||||
`${slope} · 절토 ${design.cut_area_m2.toFixed(2)}㎡ · 성토 ${design.fill_area_m2.toFixed(2)}㎡` +
|
||||
" · 계획고는 [확인] 뒤에 정해지므로 지반을 따라 세운 기본 계획임"
|
||||
);
|
||||
}
|
||||
|
||||
/** 원지반선과 기본 계획 횡단선을 한 판에 그린다. 좌(+)가 왼쪽에 오게 눕힌다. */
|
||||
function drawCross(
|
||||
context: CanvasRenderingContext2D,
|
||||
canvas: HTMLCanvasElement,
|
||||
preview: CrossPreviewResponse,
|
||||
): void {
|
||||
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],
|
||||
);
|
||||
const all = [...ground, ...design];
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
if (all.length < 2) return;
|
||||
|
||||
const offsets = all.map((point) => point[0]);
|
||||
const heights = all.map((point) => point[1]);
|
||||
const minOffset = Math.min(...offsets);
|
||||
const maxOffset = Math.max(...offsets);
|
||||
const minZ = Math.min(...heights);
|
||||
const maxZ = Math.max(...heights);
|
||||
const spanX = maxOffset - minOffset || 1;
|
||||
const spanZ = maxZ - minZ || 1;
|
||||
// **가로·세로를 같은 배율로** 둔다 — 따로 늘리면 사면 기울기가 거짓으로 보인다. 횡단도는
|
||||
// 기울기를 눈으로 읽는 그림이라 왜곡하면 안 된다(2026-09-12 실화면: 노면이 안 보였다).
|
||||
const scale = Math.min((canvas.width - PAD * 2) / spanX, (canvas.height - PAD * 2) / spanZ);
|
||||
const centerOffset = (minOffset + maxOffset) / 2;
|
||||
const centerZ = (minZ + maxZ) / 2;
|
||||
// 좌(+offset)가 화면 왼쪽 — 횡단도 규약(generate_sections cad_exchange)과 같은 방향이다.
|
||||
const toScreen = (point: [number, number]): [number, number] => [
|
||||
canvas.width / 2 + (centerOffset - point[0]) * scale,
|
||||
canvas.height / 2 + (centerZ - point[1]) * scale,
|
||||
];
|
||||
|
||||
const stroke = (points: Array<[number, number]>, color: string, width: number): 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();
|
||||
};
|
||||
|
||||
// 중심선 — 어디가 노선 가운데인지 먼저 보이게.
|
||||
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, 6);
|
||||
context.fillStyle = "#f97316";
|
||||
context.textAlign = "right";
|
||||
context.fillText("기본 계획 횡단", canvas.width - PAD, 6);
|
||||
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 - 4,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_CurveBar.ts
|
||||
* 곡선 조작 패널의 **배선** — 어느 꺾임점을 만질지 정하고, 칸에서 들어온 값을 편집값에
|
||||
* 옮겨 적는다. 패널을 그리고 자리를 잡는 일은 `_Label` 몫이다.
|
||||
*
|
||||
* `B05_Profile_UI_RouteEdit.ts` 가 700줄을 넘겨 떼어낸 조각이다(2026-09-12). 본문 로직과
|
||||
* 수치는 그대로이고, 모달 클로저가 쥐고 있던 값만 `state()` 로 받는다.
|
||||
*
|
||||
* **R 과 곡선 길이는 한 쌍**(L = R·Δ) — 어느 쪽으로 들어와도 **반지름 한 값**으로 바꿔
|
||||
* 들고 간다. 두 벌로 두면 교각이 바뀔 때 서로 어긋난다(`_Edits.ts` 설명 참고).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { EditedCurve, EditedNode, Vertex } from "./B05_Profile_UI_RouteEdit_Curve";
|
||||
import type { CurveLock } from "./B05_Profile_UI_RouteEdit_Edits";
|
||||
import {
|
||||
centerDirectionOf,
|
||||
createCurveLabel,
|
||||
deflectionRad,
|
||||
type CurveLabel,
|
||||
} from "./B05_Profile_UI_RouteEdit_Label";
|
||||
|
||||
/** 패널이 만지는 편집값 한 벌 — 모달이 쥔 배열을 그대로 건네받는다. */
|
||||
export interface CurveBarState {
|
||||
picked: number;
|
||||
planned: Vertex[];
|
||||
nodeInfo: EditedNode[];
|
||||
curveInfo: EditedCurve[];
|
||||
curveOn: boolean[];
|
||||
curveRadius: Array<number | null>;
|
||||
curveLock: CurveLock[];
|
||||
curveArc: Array<number | null>;
|
||||
/** 못 넘는 하한(m). 0이면 제한 없음(계획서 0-9 ④). */
|
||||
limitRadiusM: number;
|
||||
limitArcM: number;
|
||||
}
|
||||
|
||||
export interface CurveBarParams {
|
||||
canvas: HTMLCanvasElement;
|
||||
state: () => CurveBarState;
|
||||
toScreen: (vertex: Vertex) => [number, number];
|
||||
/** 한 번의 편집을 마무리한다 — 다시 그리고 되돌리기에 쌓는다. */
|
||||
applyEdit: (message: string) => void;
|
||||
}
|
||||
|
||||
export interface CurveBar {
|
||||
label: CurveLabel;
|
||||
/** 고른 자리에 맞춰 패널을 옮겨 그린다. */
|
||||
sync: () => void;
|
||||
}
|
||||
|
||||
export function createCurveBar(params: CurveBarParams): CurveBar {
|
||||
const label = createCurveLabel({
|
||||
onRadius: (value) => {
|
||||
const { picked, curveRadius } = params.state();
|
||||
if (picked < 0) return;
|
||||
curveRadius[picked] = value;
|
||||
// 반지름만 바꾼 것이라 노드 자리는 그대로지만, 그려진 선은 낡았다.
|
||||
params.applyEdit(value === null ? "반지름을 자동으로 되돌렸습니다." : "반지름을 바꿨습니다.");
|
||||
},
|
||||
onArcLength: (value) => {
|
||||
const { picked, nodeInfo, curveArc, curveRadius } = params.state();
|
||||
if (picked < 0) return;
|
||||
// 곡선 길이 L 과 반지름 R 은 L = R·Δ 로 묶여 있다(Δ = 교각, 앞뒤 직선이 정함).
|
||||
// 그래서 길이를 받으면 반지름으로 바꿔 **한 값만** 들고 간다 — 두 벌로 두면 어긋난다.
|
||||
const deflection = deflectionRad(nodeInfo[picked]?.inner_angle_deg);
|
||||
curveArc[picked] = value;
|
||||
curveRadius[picked] = value !== null && deflection > 1e-9 ? value / deflection : null;
|
||||
params.applyEdit(
|
||||
value === null ? "반지름을 자동으로 되돌렸습니다." : "곡선 길이를 바꿨습니다.",
|
||||
);
|
||||
},
|
||||
onLock: (lock) => {
|
||||
const { picked, nodeInfo, curveArc, curveRadius, curveLock } = params.state();
|
||||
if (picked < 0) return;
|
||||
curveLock[picked] = lock;
|
||||
const deflection = deflectionRad(nodeInfo[picked]?.inner_angle_deg);
|
||||
const shown = curveRadius[picked] ?? nodeInfo[picked]?.radius_m ?? null;
|
||||
// 길이를 붙들려면 지금 길이를 적어 둬야 한다 — 뒤에 교각이 바뀌면 이 값으로 R 을 다시 잡는다.
|
||||
if (lock === "arc") {
|
||||
curveArc[picked] = shown !== null && deflection > 1e-9 ? shown * deflection : null;
|
||||
}
|
||||
// R 을 붙들 때 칸이 비어 있으면 지금 그려진 R 을 적어 둔다(자동 상태를 그대로 못 박음).
|
||||
if (lock === "radius" && curveRadius[picked] === null) curveRadius[picked] = shown;
|
||||
params.applyEdit(
|
||||
lock === "radius"
|
||||
? "반지름을 고정했습니다."
|
||||
: lock === "arc"
|
||||
? "곡선 길이를 고정했습니다."
|
||||
: "고정을 풀었습니다.",
|
||||
);
|
||||
},
|
||||
onCurveOn: (on) => {
|
||||
const { picked, curveOn } = params.state();
|
||||
if (picked < 0) return;
|
||||
curveOn[picked] = on;
|
||||
params.applyEdit(on ? "곡선을 넣었습니다." : "곡선을 지웠습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
/** 고른 자리에 맞춰 라벨을 옮겨 그린다. 끝점은 곡선이 없으므로 라벨을 숨긴다. */
|
||||
function sync(): void {
|
||||
const {
|
||||
picked,
|
||||
planned,
|
||||
nodeInfo,
|
||||
curveInfo,
|
||||
curveOn,
|
||||
curveRadius,
|
||||
curveLock,
|
||||
limitRadiusM,
|
||||
limitArcM,
|
||||
} = params.state();
|
||||
if (!(picked > 0 && picked < planned.length - 1)) {
|
||||
label.hide();
|
||||
return;
|
||||
}
|
||||
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 rect = params.canvas.getBoundingClientRect();
|
||||
const [screenX, screenY] = params.toScreen(planned[picked]);
|
||||
label.show({
|
||||
seat: picked,
|
||||
// 패널은 `position: fixed` 라 **화면 좌표**로 넘긴다.
|
||||
at: [screenX + rect.left, screenY + rect.top],
|
||||
// 넘어가도 되는 테두리 = **지도 칸**(하단 정보행 위까지). 밖으로 나가면 지금 무엇을
|
||||
// 고치는지 모달 안에서 안 보인다(2026-09-12 사용자 지적 ⑨).
|
||||
bounds: {
|
||||
left: rect.left + 8,
|
||||
top: rect.top + 8,
|
||||
right: rect.right - 8,
|
||||
bottom: rect.bottom - 8,
|
||||
},
|
||||
centerDirection: pickedCurve
|
||||
? centerDirectionOf(
|
||||
params.toScreen([pickedCurve.apex[0], pickedCurve.apex[1]]),
|
||||
params.toScreen(pickedCurve.start),
|
||||
params.toScreen(pickedCurve.end),
|
||||
)
|
||||
: null,
|
||||
curveOn: curveOn[picked] !== false,
|
||||
radiusShown: shown,
|
||||
arcLengthShown: shown === null || deflection <= 1e-9 ? null : shown * deflection,
|
||||
lock: curveLock[picked] ?? null,
|
||||
innerAngleDeg: nodeInfo[picked]?.inner_angle_deg ?? null,
|
||||
limitRadiusM,
|
||||
limitArcM,
|
||||
});
|
||||
}
|
||||
|
||||
return { label, sync };
|
||||
}
|
||||
@@ -48,6 +48,75 @@ export function applyArcLocks(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* **하한을 지키도록 지정 반지름을 끌어올린다**(계획서 0-9 ④, 2026-09-12 사용자 확정).
|
||||
*
|
||||
* L = R·Δ 이므로 「곡선 길이 하한」은 그 자리에서 「반지름 하한 L/Δ」과 같은 말이다. 두 하한
|
||||
* 중 큰 쪽으로 올린다. **비워 둔(자동) 자리는 건드리지 않는다** — 자동은 이미 기본 반지름을
|
||||
* 쓰고 있고, 여기서 값을 적어 넣으면 아무것도 안 고쳤는데 「R 지정」이 늘어난다.
|
||||
*/
|
||||
export function applyCurveLimits(
|
||||
planned: Vertex[],
|
||||
curveOn: ReadonlyArray<boolean>,
|
||||
curveRadius: Array<number | null>,
|
||||
limitRadiusM: number,
|
||||
limitArcM: number,
|
||||
): void {
|
||||
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;
|
||||
const deflection = deflectionRad(
|
||||
innerAngleDeg(planned[seat - 1], planned[seat], planned[seat + 1]),
|
||||
);
|
||||
const byArc = limitArcM > 0 && deflection > 1e-9 ? limitArcM / deflection : 0;
|
||||
const floor = Math.max(limitRadiusM, byArc);
|
||||
if (floor > 0 && current < floor) curveRadius[seat] = floor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 노드마다 하한을 **얼마나 밑돌고 있나**(m). 다 지키고 있으면 0.
|
||||
*
|
||||
* 노드를 옮기면 접선 자리가 모자라 그리기 단계에서 R 이 눌릴 수 있다. 그 눌림까지 막으려면
|
||||
* 옮기기 자체를 되돌려야 하므로, **옮기기 전보다 나빠진 자리가 있는지**만 견준다 — 이미
|
||||
* 하한을 밑돌던 옛 노선도 그대로 고칠 수 있어야 하기 때문이다(2026-09-12).
|
||||
*
|
||||
* ⚠ 가장 큰 값 하나로 견주면 안 된다. 크게 밑도는 자리가 이미 있으면 **다른 자리가 새로
|
||||
* 무너져도 최댓값이 안 움직여** 그냥 통과한다(실화면에서 「기준 미달 1곳 → 2곳」이 그대로
|
||||
* 지나갔다). 자리마다 따로 견준다.
|
||||
*/
|
||||
export function curveShortfalls(
|
||||
nodes: ReadonlyArray<EditedNode>,
|
||||
limitRadiusM: number,
|
||||
limitArcM: number,
|
||||
): number[] {
|
||||
return nodes.map((node) => {
|
||||
if (node.radius_m === null || (limitRadiusM <= 0 && limitArcM <= 0)) return 0;
|
||||
let short = 0;
|
||||
if (limitRadiusM > 0) short = Math.max(short, limitRadiusM - node.radius_m);
|
||||
if (limitArcM > 0) {
|
||||
short = Math.max(short, limitArcM - node.radius_m * deflectionRad(node.inner_angle_deg));
|
||||
}
|
||||
return Math.max(0, short);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 하한을 지키던 자리가 **이번 걸음에 처음으로 무너졌나**. 자리 수가 달라지면(넣기·지우기)
|
||||
* 안 따진다.
|
||||
*
|
||||
* ⚠ 「조금이라도 나빠졌으면 막기」로 두면 **이미 하한을 밑돌던 옛 노선을 아예 못 고친다** —
|
||||
* 그 옆 노드를 1px 만 건드려도 밑돌던 값이 미세하게 더 내려가 첫 걸음부터 막혔다(2026-09-12
|
||||
* 실화면). 이미 무너진 자리는 그대로 두고(붉은 표시는 남는다), **지키고 있던 자리가 넘어가는
|
||||
* 것만** 막는다.
|
||||
*/
|
||||
export function shortfallCrossed(before: readonly number[], after: readonly number[]): boolean {
|
||||
if (before.length !== after.length) return false;
|
||||
return after.some((value, index) => value > 1e-6 && before[index] <= 1e-6);
|
||||
}
|
||||
|
||||
export interface CurveSummaryInput {
|
||||
nodeCount: number;
|
||||
curveOn: boolean[];
|
||||
|
||||
@@ -181,6 +181,98 @@ export function segmentAtScreen(
|
||||
return best;
|
||||
}
|
||||
|
||||
/** 노선 위 한 점 — 어디를 짚었나와 그 자리의 누가거리. */
|
||||
export interface RoutePointHit {
|
||||
/** 사업지 좌표(m). */
|
||||
point: [number, number];
|
||||
/** 시점에서 노선을 따라간 거리(m). */
|
||||
chainageM: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 노선(그려지는 폴리라인) 위에서 **클릭에 가장 가까운 점**과 그 누가거리. 멀면 null.
|
||||
*
|
||||
* 직선·곡선을 가리지 않는다(계획서 0-9 ⑤) — 원호도 이미 정점으로 펴져 있어 같은 선분 훑기로
|
||||
* 잡힌다. 누가거리는 선분 길이를 누적해 구하므로 노선 길이 표기와 같은 값을 본다.
|
||||
*/
|
||||
export function routePointAtScreen(
|
||||
line: Array<[number, number]>,
|
||||
toScreen: ScreenOf,
|
||||
px: number,
|
||||
py: number,
|
||||
maxPx: number,
|
||||
): RoutePointHit | null {
|
||||
let best: RoutePointHit | null = null;
|
||||
let bestDistance = maxPx;
|
||||
let travelled = 0;
|
||||
for (let index = 0; index < line.length - 1; index += 1) {
|
||||
const from = line[index];
|
||||
const to = line[index + 1];
|
||||
const segmentM = Math.hypot(to[0] - from[0], to[1] - from[1]);
|
||||
const [ax, ay] = toScreen(from);
|
||||
const [bx, by] = toScreen(to);
|
||||
const dx = bx - ax;
|
||||
const dy = by - ay;
|
||||
const lengthSquared = dx * dx + dy * dy || 1;
|
||||
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / lengthSquared));
|
||||
const distance = Math.hypot(ax + t * dx - px, ay + t * dy - py);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = {
|
||||
point: [from[0] + (to[0] - from[0]) * t, from[1] + (to[1] - from[1]) * t],
|
||||
chainageM: travelled + segmentM * t,
|
||||
};
|
||||
}
|
||||
travelled += segmentM;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* 클릭에 가장 가까운 **규칙 측점**의 누가거리(m). 그만큼 안에 없으면 null(계획서 0-9 ⑧).
|
||||
*
|
||||
* 눈금을 그리는 `drawStationTicks` 와 **같은 자리**를 짚는다 — 선분 길이를 누적해 측점 간격
|
||||
* 마다 한 점씩 보간한다. 눈금이 보이는 자리를 눌렀는데 안 잡히면 안 되기 때문이다.
|
||||
*/
|
||||
export function stationAtScreen(
|
||||
line: Array<[number, number]>,
|
||||
toScreen: ScreenOf,
|
||||
intervalM: number,
|
||||
px: number,
|
||||
py: number,
|
||||
maxPx: number,
|
||||
): number | null {
|
||||
if (line.length < 2 || !(intervalM > 0)) return null;
|
||||
const cumulative: number[] = [0];
|
||||
for (let index = 1; index < line.length; index += 1) {
|
||||
cumulative.push(
|
||||
cumulative[index - 1] +
|
||||
Math.hypot(line[index][0] - line[index - 1][0], line[index][1] - line[index - 1][1]),
|
||||
);
|
||||
}
|
||||
const total = cumulative[cumulative.length - 1];
|
||||
let best: number | null = null;
|
||||
let bestDistance = maxPx;
|
||||
let cursor = 1;
|
||||
for (let chainage = 0; chainage <= total; chainage += intervalM) {
|
||||
while (cursor < cumulative.length - 1 && cumulative[cursor] < chainage) cursor += 1;
|
||||
const back = line[cursor - 1];
|
||||
const front = line[cursor];
|
||||
const segment = cumulative[cursor] - cumulative[cursor - 1] || 1;
|
||||
const ratio = Math.min(1, Math.max(0, (chainage - cumulative[cursor - 1]) / segment));
|
||||
const [x, y] = toScreen([
|
||||
back[0] + (front[0] - back[0]) * ratio,
|
||||
back[1] + (front[1] - back[1]) * ratio,
|
||||
]);
|
||||
const distance = Math.hypot(x - px, y - py);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = chainage;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** 등고선을 보일 화면 사각형 — 노선 경계에 `bandM` 를 두른 것. 노선이 없으면 null.
|
||||
*
|
||||
* **매 프레임 다시 잰다** — 창 크기·배율·이동이 바뀌어도 띠가 노선을 따라간다. 띠 자체는
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
*
|
||||
* **자리**(2026-09-07 사용자 지시)
|
||||
* · 몸통은 `document.body` 에 `position: fixed` 로 띄운다 — 모달이 `overflow: hidden` 이라
|
||||
* 안에 두면 가장자리에서 **잘린다**. 화면 밖으로도 넘어갈 수 있어야 한다.
|
||||
* 안에 두면 가장자리에서 **잘린다**.
|
||||
* · 다만 **지도 칸 밖으로는 안 나간다**(2026-09-12 사용자 지적 ⑨) — 상자 밖이나 하단
|
||||
* 정보행 위로 넘어가면 지금 무엇을 고치는지 모달 안에서 안 보인다. 「잘리지 않게」와
|
||||
* 「상자 밖으로 나가게」는 다른 문제여서 자리 계산에서만 가둔다.
|
||||
* · 자동 자리는 **곡선 중심의 반대쪽**, **16방위**로 잡는다(4방위는 대각 자리에서 곡선을 물었다).
|
||||
* · 머리를 잡아 **손으로 옮길 수 있다**. 옮긴 자리는 그 꺾임점을 보는 동안 유지되고,
|
||||
* 다른 꺾임점을 고르면 자동 자리로 돌아간다.
|
||||
@@ -60,12 +63,17 @@ export interface CurveLabelState {
|
||||
at: [number, number];
|
||||
/** **곡선 중심이 있는 쪽**(화면 기준 방향벡터). 패널은 이 반대쪽에 붙는다. */
|
||||
centerDirection: [number, number] | null;
|
||||
/** 패널이 넘어가면 안 되는 테두리(화면 좌표) — 보통 모달의 지도 칸. 없으면 안 가둔다. */
|
||||
bounds?: { left: number; top: number; right: number; bottom: number };
|
||||
curveOn: boolean;
|
||||
radiusShown: number | null;
|
||||
/** 곡선 길이(m) = R·Δ. 곡선이 없으면 null. */
|
||||
arcLengthShown: number | null;
|
||||
lock: CurveLock;
|
||||
innerAngleDeg: number | null;
|
||||
/** **못 넘는** 반지름·곡선 길이 하한(m). 0이면 제한 없음(계획서 0-9 ④). */
|
||||
limitRadiusM?: number;
|
||||
limitArcM?: number;
|
||||
}
|
||||
|
||||
export interface CurveLabelHandlers {
|
||||
@@ -143,13 +151,25 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
|
||||
/** 손으로 옮긴 자리 — 꺾임점 기준 어긋남(px). 다른 꺾임점을 고르면 지운다. */
|
||||
let manual: [number, number] | null = null;
|
||||
let anchor: [number, number] = [0, 0];
|
||||
/** 지금 자리의 하한 — 칸이 여기서 멈춘다. 0이면 제한 없음. */
|
||||
let limitRadius = 0;
|
||||
let limitArc = 0;
|
||||
/** 마지막으로 받은 테두리 — 손으로 끌 때도 같은 자리를 지키려고 들고 있는다. */
|
||||
let limit: CurveLabelState["bounds"];
|
||||
|
||||
const numberOf = (input: HTMLInputElement): number | null => {
|
||||
/** 칸에서 읽은 값. **하한 아래는 하한에서 멈추고, 멈춘 값을 칸에 되적어 보인다**
|
||||
* (2026-09-12 사용자 확정 「아예 못 넘게 막음」) — 조용히 바꾸면 왜 안 먹었는지 모른다. */
|
||||
const numberOf = (input: HTMLInputElement, floor: number): number | null => {
|
||||
const value = Number(input.value);
|
||||
return input.value.trim() !== "" && Number.isFinite(value) && value > 0 ? value : null;
|
||||
if (input.value.trim() === "" || !Number.isFinite(value) || value <= 0) return null;
|
||||
if (floor > 0 && value < floor) {
|
||||
input.value = String(floor);
|
||||
return floor;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
radius.addEventListener("change", () => handlers.onRadius(numberOf(radius)));
|
||||
arc.addEventListener("change", () => handlers.onArcLength(numberOf(arc)));
|
||||
radius.addEventListener("change", () => handlers.onRadius(numberOf(radius, limitRadius)));
|
||||
arc.addEventListener("change", () => handlers.onArcLength(numberOf(arc, limitArc)));
|
||||
toggle.addEventListener("click", () => handlers.onCurveOn(!curveOn));
|
||||
lockRadius.addEventListener("click", () => handlers.onLock(lock === "radius" ? null : "radius"));
|
||||
lockArc.addEventListener("click", () => handlers.onLock(lock === "arc" ? null : "arc"));
|
||||
@@ -169,8 +189,14 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
|
||||
});
|
||||
head.addEventListener("pointermove", (event) => {
|
||||
if (!dragFrom) return;
|
||||
const left = dragFrom.left + event.clientX - dragFrom.x;
|
||||
const top = dragFrom.top + event.clientY - dragFrom.y;
|
||||
// 끄는 동안에도 테두리를 지킨다 — 놓은 뒤에만 가두면 손이 간 자리에서 패널이 튄다.
|
||||
const [left, top] = clamp(
|
||||
dragFrom.left + event.clientX - dragFrom.x,
|
||||
dragFrom.top + event.clientY - dragFrom.y,
|
||||
root.offsetWidth,
|
||||
root.offsetHeight,
|
||||
limit,
|
||||
);
|
||||
root.style.left = `${Math.round(left)}px`;
|
||||
root.style.top = `${Math.round(top)}px`;
|
||||
// 꺾임점 기준으로 기억한다 — 지도를 옮기거나 확대해도 같은 자리에 따라온다.
|
||||
@@ -183,7 +209,8 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
|
||||
head.addEventListener("pointerup", stopDrag);
|
||||
head.addEventListener("pointercancel", stopDrag);
|
||||
|
||||
/** 자동 자리 — 곡선 중심의 반대쪽, 16방위. 손으로 옮겼으면 그 어긋남을 얹는다. */
|
||||
/** 자동 자리 — 곡선 중심의 반대쪽, 16방위. 손으로 옮겼으면 그 어긋남을 얹는다.
|
||||
* 마지막에 **테두리 안으로 가둔다** — 자동 자리든 손으로 옮긴 자리든 같이 갇힌다. */
|
||||
function place(state: CurveLabelState): void {
|
||||
const width = root.offsetWidth;
|
||||
const height = root.offsetHeight;
|
||||
@@ -193,12 +220,32 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
|
||||
: ([1, 0] as [number, number]);
|
||||
const distance = GAP_PX + boxReach(away[0], away[1], width, height);
|
||||
anchor = [nx + away[0] * distance - width / 2, ny + away[1] * distance - height / 2];
|
||||
const left = anchor[0] + (manual ? manual[0] : 0);
|
||||
const top = anchor[1] + (manual ? manual[1] : 0);
|
||||
const [left, top] = clamp(
|
||||
anchor[0] + (manual ? manual[0] : 0),
|
||||
anchor[1] + (manual ? manual[1] : 0),
|
||||
width,
|
||||
height,
|
||||
state.bounds,
|
||||
);
|
||||
root.style.left = `${Math.round(left)}px`;
|
||||
root.style.top = `${Math.round(top)}px`;
|
||||
}
|
||||
|
||||
/** 테두리 안으로 민다. 패널이 테두리보다 크면 왼쪽·위를 맞춰 **머리가 먼저 보이게** 한다. */
|
||||
function clamp(
|
||||
left: number,
|
||||
top: number,
|
||||
width: number,
|
||||
height: number,
|
||||
bounds: CurveLabelState["bounds"],
|
||||
): [number, number] {
|
||||
if (!bounds) return [left, top];
|
||||
return [
|
||||
Math.max(bounds.left, Math.min(left, bounds.right - width)),
|
||||
Math.max(bounds.top, Math.min(top, bounds.bottom - height)),
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
show(state) {
|
||||
if (state.seat !== seat) {
|
||||
@@ -207,6 +254,12 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
|
||||
}
|
||||
curveOn = state.curveOn;
|
||||
lock = state.lock;
|
||||
limit = state.bounds;
|
||||
limitRadius = state.limitRadiusM ?? 0;
|
||||
limitArc = state.limitArcM ?? 0;
|
||||
// 칸 자체에도 하한을 박아 화살표·스피너가 그 아래로 안 내려가게 한다.
|
||||
radius.min = limitRadius > 0 ? String(limitRadius) : "1";
|
||||
arc.min = limitArc > 0 ? String(limitArc) : "1";
|
||||
root.hidden = false;
|
||||
seatText.textContent = `${state.seat + 1}번째 꺾임점`;
|
||||
toggle.textContent = state.curveOn ? "곡선 지우기" : "곡선 넣기";
|
||||
@@ -223,8 +276,14 @@ export function createCurveLabel(handlers: CurveLabelHandlers): CurveLabel {
|
||||
const inner = state.innerAngleDeg;
|
||||
const held =
|
||||
lock === "radius" ? "반지름 고정" : lock === "arc" ? "곡선 길이 고정" : "고정 없음";
|
||||
const floors = [
|
||||
limitRadius > 0 ? `R ≥ ${limitRadius}m` : "",
|
||||
limitArc > 0 ? `L ≥ ${limitArc}m` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
info.textContent = state.curveOn
|
||||
? `${held}${inner ? ` · 내각 ${Math.round(inner)}°` : ""}`
|
||||
? `${held}${inner ? ` · 내각 ${Math.round(inner)}°` : ""}${floors ? ` · ${floors}` : ""}`
|
||||
: "곡선 없음 — 직선이 그대로 꺾입니다";
|
||||
place(state);
|
||||
// 글자가 바뀌면 상자 높이가 한 박자 늦게 자란다 — 다음 그림 직전에 한 번 더 맞춘다.
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_Measure.ts
|
||||
* 계획노선 위 **두 점 사이 구간 재기** — 길이와 종단기울기(계획서 0-9 ⑤).
|
||||
*
|
||||
* Shift+클릭으로 a·b 를 찍는다. 직선·곡선을 가리지 않는다 — 그려지는 폴리라인 위라면 어디든
|
||||
* 짚을 수 있고, 누가거리는 노선 길이 표기와 같은 방식으로 잰다.
|
||||
*
|
||||
* 지반고는 **찍는 순간에만** 서버에 묻는다(`/route/elevations`). 확정된 지표면을 읽기만 하는
|
||||
* 통로라 「편집 중에는 계산이 안 나간다」(계획서 0-2 확정 7)와 부딪히지 않는다 — 다만 노드를
|
||||
* 끄는 동안에는 한 번도 부르지 않는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { fetchRouteElevations } from "./B05_Profile_Api_Replan";
|
||||
import { routePointAtScreen, type RoutePointHit } from "./B05_Profile_UI_RouteEdit_Input";
|
||||
import { formatStation } from "./B05_Profile_Util_Station";
|
||||
|
||||
type Vertex = [number, number];
|
||||
|
||||
/** 구간 재기로 노선을 짚었다고 볼 거리(px). */
|
||||
const MEASURE_HIT_PX = 14;
|
||||
|
||||
interface MeasurePoint extends RoutePointHit {
|
||||
/** 그 자리의 지반고(m). 아직 못 물었거나 지표면 밖이면 null. */
|
||||
z: number | null;
|
||||
}
|
||||
|
||||
export interface MeasureToolParams {
|
||||
projectId: string;
|
||||
/** 규칙 측점 간격(m) — 측점 표기에 쓴다. */
|
||||
stationIntervalM: number;
|
||||
/** 지금 그려지는 노선(원호 포함). 편집으로 바뀌므로 함수로 받는다. */
|
||||
line: () => Vertex[];
|
||||
toScreen: (vertex: Vertex) => [number, number];
|
||||
/** 창이 닫혔나 — 늦게 온 응답을 죽은 화면에 적지 않으려고. */
|
||||
isClosed: () => boolean;
|
||||
/** 상태가 바뀌었다 — 호출부가 상태줄을 다시 적고 다시 그린다. */
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export interface MeasureTool {
|
||||
/** 찍힌 자리(0~2개) — 그리기가 쓴다. */
|
||||
points: () => Vertex[];
|
||||
/** 상태줄에 낼 한 줄. */
|
||||
hint: () => string;
|
||||
/** Shift+클릭 한 번. 두 점이 차면 지반고를 한 번만 물어 온다. */
|
||||
pick: (px: number, py: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export function createMeasureTool(params: MeasureToolParams): MeasureTool {
|
||||
/** 찍은 두 점. 셋째를 찍으면 새 구간의 시작이 된다. */
|
||||
let picked: MeasurePoint[] = [];
|
||||
|
||||
const hint = (): string => {
|
||||
if (picked.length === 0) return "Shift+클릭으로 두 점을 찍으면 거리와 기울기가 보입니다.";
|
||||
const first = picked[0];
|
||||
if (picked.length === 1) {
|
||||
return `구간 재기 — 시작 ${formatStation(first.chainageM, params.stationIntervalM)}. 한 점 더.`;
|
||||
}
|
||||
const second = picked[1];
|
||||
const span = Math.abs(second.chainageM - first.chainageM);
|
||||
const head =
|
||||
`구간 ${formatStation(first.chainageM, params.stationIntervalM)} → ` +
|
||||
`${formatStation(second.chainageM, params.stationIntervalM)} · 길이 ${span.toFixed(1)}m`;
|
||||
if (first.z === null || second.z === null || span <= 1e-6) {
|
||||
return `${head} · 지반고를 못 읽어 기울기는 못 냅니다.`;
|
||||
}
|
||||
// 기울기는 **노선을 따라간 길이** 기준이다 — 직선거리로 나누면 곡선부에서 과대평가된다.
|
||||
const rise = second.z - first.z;
|
||||
return (
|
||||
`${head} · 지반고 ${first.z.toFixed(1)} → ${second.z.toFixed(1)}m` +
|
||||
` · 종단기울기 ${((rise / span) * 100).toFixed(1)}%`
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
points: () => picked.map((entry) => entry.point),
|
||||
hint,
|
||||
async pick(px, py) {
|
||||
const hit = routePointAtScreen(params.line(), params.toScreen, px, py, MEASURE_HIT_PX);
|
||||
if (!hit) {
|
||||
picked = []; // 노선을 빗나가면 재던 것을 접는다.
|
||||
params.onChange();
|
||||
return;
|
||||
}
|
||||
picked = picked.length >= 2 ? [{ ...hit, z: null }] : [...picked, { ...hit, z: null }];
|
||||
params.onChange();
|
||||
if (picked.length < 2) return;
|
||||
const asked = picked;
|
||||
try {
|
||||
const heights = await fetchRouteElevations(
|
||||
params.projectId,
|
||||
asked.map((entry) => entry.point),
|
||||
);
|
||||
if (params.isClosed() || picked !== asked) return; // 그 사이 다시 찍었으면 버린다.
|
||||
asked.forEach((entry, index) => {
|
||||
entry.z = heights[index] ?? null;
|
||||
});
|
||||
} catch {
|
||||
/* 지반고를 못 읽으면 길이만 낸다 — `hint` 가 그렇게 말한다. */
|
||||
}
|
||||
params.onChange();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_Render.ts
|
||||
* 계획노선 편집 모달의 **그리기** — 등고선·예상노선·계획노선·노드·곡선 손잡이,
|
||||
* 그 위에 시점·종점·규칙측점 눈금.
|
||||
*
|
||||
* `B05_Profile_UI_RouteEdit.ts` 가 700줄을 넘겨 떼어낸 조각이다(2026-09-12). 본문 로직과
|
||||
* 수치는 그대로이고, 모달 클로저가 쥐고 있던 값만 `scene` 으로 받는다.
|
||||
*
|
||||
* 측점 눈금은 **B04 지도·배수유역도와 같은 한 곳**(`drawStationTicks`)을 부른다 — 표기가
|
||||
* 화면마다 갈리면 같은 자리를 두 이름으로 부르게 된다(계획서 0-9 ②).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
drawPreparedFeature,
|
||||
drawPreparedLabels,
|
||||
drawPreparedLayer,
|
||||
type PreparedLayer,
|
||||
type ViewState,
|
||||
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||
import type { RouteEditContours } from "./B05_Profile_UI_RouteEdit_Contour";
|
||||
import { drawStationTicks } from "../B04_PreProcess/B04_PreProcess_UI_MapOverlays";
|
||||
import type { EditedCurve, EditedNode, Vertex } from "./B05_Profile_UI_RouteEdit_Curve";
|
||||
import { contourBandRect } from "./B05_Profile_UI_RouteEdit_Input";
|
||||
import { formatStation } from "./B05_Profile_Util_Station";
|
||||
|
||||
/** 노드 반지름(px). */
|
||||
const NODE_R = 4;
|
||||
/** 등고선을 보일 **노선 둘레 띠**(m) — 사용자 지시 ⑥(2026-09-07).
|
||||
*
|
||||
* 노선에서 이만큼 밖의 등고선은 안 그린다. **창 크기와 무관한 고정 띠**라 창을 늘리거나
|
||||
* 줄여도 띠가 흔들리지 않는다(사용자가 「다이나믹 창이라 조심」이라 한 자리). 화면 밖을
|
||||
* 걸러내는 일은 `drawPreparedLayer` 가 이미 하므로 여기서는 띠만 덧씌운다. */
|
||||
const CONTOUR_BAND_M = 300;
|
||||
/** 곡선 시작·끝점 손잡이 크기(px) — 노드 동그라미와 구별되게 **속 빈 네모**로 그린다.
|
||||
* 처음엔 3.5px 였는데 선과 색이 같아 눈에도 안 띄고 집기도 어려웠다(2026-09-07 실화면). */
|
||||
const CURVE_HANDLE_PX = 5;
|
||||
/** 시점·종점 이름표를 끝점에서 **노선 바깥으로** 밀어내는 거리(px). */
|
||||
const OUTWARD_PX = 26;
|
||||
/** 구간 재기 표시 색 — 노선(주황)·등고선(연보라)·고른 등고선(보라)과 겹치지 않는 초록. */
|
||||
const MEASURE_COLOR = "#22c55e";
|
||||
|
||||
/** 노선을 따라간 길이(m) — 원호가 이미 정점으로 펴져 있어 정점 간 거리의 합이 곧 길이다. */
|
||||
export function polylineLengthM(points: ReadonlyArray<Vertex>): number {
|
||||
let total = 0;
|
||||
for (let index = 1; index < points.length; index += 1) {
|
||||
total += Math.hypot(
|
||||
points[index][0] - points[index - 1][0],
|
||||
points[index][1] - points[index - 1][1],
|
||||
);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
export interface RouteEditScene {
|
||||
view: ViewState;
|
||||
/** 사업지 좌표(m) → 캔버스 px. */
|
||||
toScreen: (vertex: Vertex) => [number, number];
|
||||
/** 화면 1m 당 픽셀 — 측점 라벨 솎기 단계를 이 값으로 정한다. */
|
||||
pxPerMeter: number;
|
||||
/** 도엽 메타를 읽었나 — 못 읽었으면 등고선 띠를 씌우지 않는다. */
|
||||
hasMeta: boolean;
|
||||
/** 바탕 등고선 한 벌 — LAS 것이거나 도엽 것(`_Contour` 가 고른다). */
|
||||
contours: RouteEditContours | null;
|
||||
/** 등고선 말고 함께 깔 도엽 레이어(하천중심선). */
|
||||
otherSheets: ReadonlyArray<PreparedLayer>;
|
||||
/** 고른 등고선 가닥 — 없으면 -1(계획서 0-9 ⑦). */
|
||||
pickedContour: number;
|
||||
/** 지금 화면에 낼 등고선 간격(m) — 그리기와 집기가 **같은 값**을 봐야 한다. */
|
||||
contourStepM: number;
|
||||
expected: ReadonlyArray<Vertex>;
|
||||
/** 그려 보이는 계획노선(원호 포함). */
|
||||
plannedLine: ReadonlyArray<Vertex>;
|
||||
/** 잡아 옮기는 노드(꺾임점). */
|
||||
planned: ReadonlyArray<Vertex>;
|
||||
nodeInfo: ReadonlyArray<EditedNode>;
|
||||
curveInfo: ReadonlyArray<EditedCurve>;
|
||||
curveOn: ReadonlyArray<boolean>;
|
||||
/** 지금 고른 꺾임점. 없으면 -1. */
|
||||
picked: number;
|
||||
/** 규칙 측점 간격(m). */
|
||||
stationIntervalM: number;
|
||||
/** 구간 재기로 찍은 점(0~2개) — 노선 위 자리(계획서 0-9 ⑤). */
|
||||
measure: ReadonlyArray<Vertex>;
|
||||
}
|
||||
|
||||
export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: RouteEditScene): void {
|
||||
const { view, toScreen } = scene;
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
const line = scene.plannedLine.length ? scene.plannedLine : scene.planned;
|
||||
context.clearRect(0, 0, view.width, view.height);
|
||||
context.fillStyle = style.getPropertyValue("--color-surface") || "#111";
|
||||
context.fillRect(0, 0, view.width, view.height);
|
||||
|
||||
context.save();
|
||||
// 등고선은 **노선 둘레 300m 안**에서만 그린다 — 노선과 상관없는 산줄기까지 다 그리면
|
||||
// 화면이 등고선으로 덮여 노선이 안 보인다(2026-09-07 사용자 지시 ⑥).
|
||||
const band = scene.hasMeta ? contourBandRect(line as Vertex[], toScreen, CONTOUR_BAND_M) : null;
|
||||
if (band) {
|
||||
context.beginPath();
|
||||
context.rect(band.x, band.y, band.width, band.height);
|
||||
context.clip();
|
||||
}
|
||||
context.strokeStyle = style.getPropertyValue("--map-sheet-stream") || "#2563eb";
|
||||
context.lineWidth = 1.2;
|
||||
for (const layer of scene.otherSheets) drawPreparedLayer(context, layer, view, "dot");
|
||||
if (scene.contours) {
|
||||
// 그리는 줄과 라벨을 **같은 눈금**으로 솎는다 — 그린 줄에만 숫자가 붙어야 짝이 맞는다.
|
||||
const everyM = scene.contourStepM;
|
||||
context.strokeStyle = style.getPropertyValue("--map-sheet-contour") || "#a5b4fc";
|
||||
context.lineWidth = 0.8;
|
||||
drawPreparedLayer(context, scene.contours.layer, view, "dot", everyM);
|
||||
// 고른 가닥은 굵고 다른 색으로 덧그린다 — 지우고 다시 그리지 않고 위에 얹는다.
|
||||
if (scene.pickedContour >= 0) {
|
||||
context.strokeStyle = style.getPropertyValue("--map-flow-arrow") || "#7c3aed";
|
||||
context.lineWidth = 2.6;
|
||||
drawPreparedFeature(context, scene.contours.layer, scene.pickedContour, 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,
|
||||
);
|
||||
}
|
||||
context.restore();
|
||||
|
||||
strokePolyline(
|
||||
context,
|
||||
toScreen,
|
||||
scene.expected,
|
||||
[6, 5],
|
||||
style.getPropertyValue("--color-text-secondary") || "#9ca3af",
|
||||
1.6,
|
||||
);
|
||||
// 선은 **폴리라인**(원호 포함)을 그리고, 잡는 동그라미는 **노드**에만 찍는다.
|
||||
// 노드를 옮기는 동안에는 폴리라인이 없으므로 노드를 곧바로 이어 미리 보인다.
|
||||
strokePolyline(
|
||||
context,
|
||||
toScreen,
|
||||
line,
|
||||
[],
|
||||
style.getPropertyValue("--map-route") || "#f97316",
|
||||
2.4,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.fillStyle = style.getPropertyValue("--map-route") || "#f97316";
|
||||
context.strokeStyle = style.getPropertyValue("--map-halo") || "rgba(255,255,255,0.9)";
|
||||
context.lineWidth = 1;
|
||||
scene.planned.forEach((vertex, index) => {
|
||||
const [x, y] = toScreen(vertex);
|
||||
// 법정 기준을 못 맞춘 자리는 붉게 — 막지는 않고 보이기만 한다(2026-09-06 사용자 확정).
|
||||
const bad = (scene.nodeInfo[index]?.violations?.length ?? 0) > 0;
|
||||
context.fillStyle = bad
|
||||
? style.getPropertyValue("--color-danger") || "#dc2626"
|
||||
: style.getPropertyValue("--map-route") || "#f97316";
|
||||
context.beginPath();
|
||||
context.arc(x, y, index === scene.picked ? NODE_R + 2 : NODE_R, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
context.stroke();
|
||||
// 곡선을 지운 자리는 가운데를 비워 「여기는 곡선이 없다」를 보인다.
|
||||
if (
|
||||
scene.curveOn.length &&
|
||||
!scene.curveOn[index] &&
|
||||
index > 0 &&
|
||||
index < scene.planned.length - 1
|
||||
) {
|
||||
context.save();
|
||||
context.fillStyle = style.getPropertyValue("--map-halo") || "rgba(255,255,255,0.9)";
|
||||
context.beginPath();
|
||||
context.arc(x, y, NODE_R - 2, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
context.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// 곡선 시작·끝점 — 잡아서 직선 각도와 R 을 함께 바꾸는 손잡이(2026-09-07 사용자 지시).
|
||||
// **속을 비우고 테두리를 굵게** 그린다 — 선·노드와 색이 같으면 눈에도 안 띄고 집기도 어렵다.
|
||||
context.lineWidth = 2;
|
||||
scene.curveInfo.forEach((curve) => {
|
||||
// **늘 보인다**(2026-09-07 사용자 지시) — 직선이 곡선에 닿는 자리는 손잡이이기 이전에
|
||||
// **읽을 정보**다. 한때 고른 곡선만 내보였더니 「표기가 다 사라졌다」는 지적을 받았다.
|
||||
// 노드를 못 집던 문제는 집기 우선순위(노드가 먼저)로 따로 풀었으므로 다 내놓아도 된다.
|
||||
if (scene.curveOn[curve.node_first] === false) return; // 곡선을 지운 자리에는 접선점도 없다.
|
||||
// 고른 곡선은 속을 채워 도드라지게 — 지금 끌 수 있는 것이 무엇인지 보이게.
|
||||
const isPicked = curve.node_first === scene.picked;
|
||||
[curve.start, curve.end].forEach((point) => {
|
||||
const [x, y] = toScreen([point[0], point[1]]);
|
||||
context.beginPath();
|
||||
const size = isPicked ? CURVE_HANDLE_PX + 1 : CURVE_HANDLE_PX;
|
||||
context.rect(x - size, y - size, size * 2, size * 2);
|
||||
context.fillStyle = isPicked
|
||||
? style.getPropertyValue("--map-route") || "#f97316"
|
||||
: style.getPropertyValue("--map-halo") || "rgba(255,255,255,0.95)";
|
||||
context.fill();
|
||||
context.strokeStyle = style.getPropertyValue("--map-route") || "#f97316";
|
||||
context.stroke();
|
||||
});
|
||||
});
|
||||
context.restore();
|
||||
|
||||
drawStationMarks(context, scene, line);
|
||||
drawMeasureMarks(context, scene, line);
|
||||
}
|
||||
|
||||
/** 구간 재기로 찍은 자리 — a·b 를 동그라미로 찍고 그 사이 노선을 굵게 덧그린다(계획서 0-9 ⑤). */
|
||||
function drawMeasureMarks(
|
||||
context: CanvasRenderingContext2D,
|
||||
scene: RouteEditScene,
|
||||
line: ReadonlyArray<Vertex>,
|
||||
): void {
|
||||
if (scene.measure.length === 0) return;
|
||||
context.save();
|
||||
if (scene.measure.length >= 2) {
|
||||
const span = spanBetween(line, scene.measure[0], scene.measure[1]);
|
||||
if (span.length >= 2) {
|
||||
context.strokeStyle = MEASURE_COLOR;
|
||||
context.lineWidth = 4;
|
||||
context.beginPath();
|
||||
span.forEach((vertex, index) => {
|
||||
const [x, y] = scene.toScreen(vertex);
|
||||
if (index === 0) context.moveTo(x, y);
|
||||
else context.lineTo(x, y);
|
||||
});
|
||||
context.stroke();
|
||||
}
|
||||
}
|
||||
context.lineWidth = 2.4;
|
||||
context.strokeStyle = MEASURE_COLOR;
|
||||
context.font = "bold 11px system-ui, sans-serif";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
scene.measure.forEach((vertex, index) => {
|
||||
const [x, y] = scene.toScreen(vertex);
|
||||
context.fillStyle = "rgba(255,255,255,0.95)";
|
||||
context.beginPath();
|
||||
context.arc(x, y, 7, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
context.stroke();
|
||||
context.fillStyle = "#14532d";
|
||||
context.fillText(index === 0 ? "a" : "b", x, y);
|
||||
});
|
||||
context.restore();
|
||||
}
|
||||
|
||||
/** 두 점 사이의 노선 조각 — 가장 가까운 정점부터 정점까지. 어디를 쟀는지 보이기만 하면 된다. */
|
||||
function spanBetween(line: ReadonlyArray<Vertex>, from: Vertex, to: Vertex): Vertex[] {
|
||||
const nearest = (target: Vertex): number => {
|
||||
let best = 0;
|
||||
let bestDistance = Infinity;
|
||||
line.forEach((vertex, index) => {
|
||||
const distance = Math.hypot(vertex[0] - target[0], vertex[1] - target[1]);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = index;
|
||||
}
|
||||
});
|
||||
return best;
|
||||
};
|
||||
const start = nearest(from);
|
||||
const end = nearest(to);
|
||||
const [low, high] = start <= end ? [start, end] : [end, start];
|
||||
return [from, ...line.slice(low, high + 1), to];
|
||||
}
|
||||
|
||||
/** 규칙 측점 눈금·번호와 시점·종점 이름표(계획서 0-9 ②). */
|
||||
function drawStationMarks(
|
||||
context: CanvasRenderingContext2D,
|
||||
scene: RouteEditScene,
|
||||
line: ReadonlyArray<Vertex>,
|
||||
): void {
|
||||
if (line.length < 2) return;
|
||||
// 눈금은 B04 지도·배수유역도와 같은 한 곳이 그린다 — 표기가 화면마다 갈리지 않게.
|
||||
drawStationTicks(
|
||||
context,
|
||||
line.map(([x, y]) => ({ x, y })),
|
||||
{
|
||||
intervalM: scene.stationIntervalM,
|
||||
pxPerMeter: scene.pxPerMeter,
|
||||
toScreen: (x, y) => scene.toScreen([x, y]),
|
||||
},
|
||||
);
|
||||
const total = polylineLengthM(line);
|
||||
const last = line.length - 1;
|
||||
endLabel(context, scene, line[0], line[1], "시점 0+0.0");
|
||||
endLabel(
|
||||
context,
|
||||
scene,
|
||||
line[last],
|
||||
line[last - 1],
|
||||
`종점 ${formatStation(total, scene.stationIntervalM)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 시점·종점 이름표 — 측점 라벨보다 크고 짙게 찍어 양 끝을 한눈에 알게 한다.
|
||||
*
|
||||
* 자리는 **노선 바깥쪽**(끝점에서 노선을 등진 방향)이다. 위로만 띄웠더니 같은 자리의 측점
|
||||
* 라벨(0+0.0 · 50+0.0)과 겹쳐 두 글자가 포개졌다 — 측점 라벨은 노선에 **직각**으로 나가므로
|
||||
* 노선을 따라 밀면 서로 안 물린다(2026-09-12 실화면). */
|
||||
function endLabel(
|
||||
context: CanvasRenderingContext2D,
|
||||
scene: RouteEditScene,
|
||||
at: Vertex,
|
||||
inward: Vertex,
|
||||
text: string,
|
||||
): void {
|
||||
const [x0, y0] = scene.toScreen(at);
|
||||
const [x1, y1] = scene.toScreen(inward);
|
||||
const length = Math.hypot(x0 - x1, y0 - y1) || 1;
|
||||
const x = x0 + ((x0 - x1) / length) * OUTWARD_PX;
|
||||
const y = y0 + ((y0 - y1) / length) * OUTWARD_PX;
|
||||
context.save();
|
||||
context.font = "bold 12px system-ui, sans-serif";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
const width = context.measureText(text).width + 10;
|
||||
context.fillStyle = "rgba(255, 255, 255, 0.9)";
|
||||
context.fillRect(x - width / 2, y - 26, width, 17);
|
||||
context.strokeStyle = "#f97316";
|
||||
context.lineWidth = 1;
|
||||
context.strokeRect(x - width / 2, y - 26, width, 17);
|
||||
context.fillStyle = "#111111";
|
||||
context.fillText(text, x, y - 17.5);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function strokePolyline(
|
||||
context: CanvasRenderingContext2D,
|
||||
toScreen: (vertex: Vertex) => [number, number],
|
||||
points: ReadonlyArray<Vertex>,
|
||||
dash: number[],
|
||||
color: string,
|
||||
width: number,
|
||||
): void {
|
||||
if (points.length < 2) return;
|
||||
context.save();
|
||||
context.setLineDash(dash);
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = width;
|
||||
context.beginPath();
|
||||
points.forEach((vertex, index) => {
|
||||
const [x, y] = toScreen(vertex);
|
||||
if (index === 0) context.moveTo(x, y);
|
||||
else context.lineTo(x, y);
|
||||
});
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
@@ -241,3 +241,53 @@
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* ── 측점 횡단 미리보기 창 (계획서 0-9 ⑧) ────────────────────────────────
|
||||
곡선 조작 패널과 같은 까닭으로 `document.body` 에 띄운다 — 모달이 `overflow: hidden`
|
||||
이라 안에 두면 가장자리에서 잘린다. 머리를 잡아 옮길 수 있다. */
|
||||
.b05-routeedit__cross {
|
||||
position: fixed;
|
||||
z-index: calc(var(--z-modal, 1000) + 2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8, 8px);
|
||||
width: 452px;
|
||||
padding: var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-8, 6px);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: 0 8px 28px rgb(0 0 0 / 40%);
|
||||
}
|
||||
|
||||
.b05-routeedit__cross-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8, 8px);
|
||||
cursor: move;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.b05-routeedit__cross-close {
|
||||
padding: 0 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-4, 4px);
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b05-routeedit__cross-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-4, 4px);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.b05-routeedit__cross-foot {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -157,9 +157,10 @@ export function renderConversionFactorFields(
|
||||
if (!entries.length) return null;
|
||||
|
||||
const box = document.createElement("div");
|
||||
box.className = "b08-quantity__factors";
|
||||
// 제 제목을 제 안에 들고 있어 이 구획 자신이 공용 접기 컨테이너가 된다(B03~B07 과 같은 틀).
|
||||
box.className = "b08-quantity__factors ui-collapsible";
|
||||
const title = document.createElement("div");
|
||||
title.className = "b08-quantity__field";
|
||||
title.className = "b08-quantity__field ui-collapsible__title";
|
||||
const titleName = document.createElement("span");
|
||||
titleName.textContent = L("B08_Quantity_Side_Factors");
|
||||
title.append(titleName);
|
||||
|
||||
@@ -188,6 +188,33 @@ const CSS = `
|
||||
.b08-quantity__hint { margin: 0 0 6px; font-size: 11px; line-height: 1.4; color: var(--color-text-secondary); }
|
||||
/* 품셈 범위 밖을 고른 칸 — **막지 않고** 사유를 받는 자리라 경고 색만 준다. */
|
||||
.b08-quantity__hint--warn { color: var(--color-warning, #b45309); }
|
||||
/* 좌측 산출조건 패널 — B03~B07 과 같은 공통 틀을 쓴다(접히는 상자 ui-collapsible
|
||||
+ 공통 외곽선 ui-sidebar-section + 바닥 고정 액션 줄 ui-sidebar-actions).
|
||||
⚠ 테두리는 공용 .ui-sidebar-section(ui_template_overlay.css)이 전담한다 —
|
||||
여기서 border 를 다시 선언하면 로드 순서상 공통 색을 덮어 흐려진다(B04 주석과 같은 까닭). */
|
||||
.b08-quantity__panel { display: flex; flex-direction: column; gap: 8px; }
|
||||
.b08-quantity__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface-raised);
|
||||
}
|
||||
.b08-quantity__section-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
/* 개발 전용 줄 — ⚠ 단추 묶음에 공용 ui-sidebar-actions 를 쓰면 공용 코드가 **이 줄**을
|
||||
패널 바닥 액션으로 잘못 집어(첫 번째 것을 찾는다) 스크롤 영역이 이 줄 안에 갇히고
|
||||
아래 칸들이 통째로 잘린다(2026-09-12 사용자 보고). 생김새만 같게 두고 클래스는 따로 쓴다. */
|
||||
.b08-quantity__dev { display: flex; flex-direction: column; gap: 4px; }
|
||||
.b08-quantity__dev-actions { display: flex; gap: 8px; }
|
||||
.b08-quantity__dev-actions > * { flex: 1 1 0; min-width: 0; }
|
||||
.b08-quantity__note { margin: 0; font-size: 11px; line-height: 1.4; color: var(--color-text-secondary); }
|
||||
/* 토량환산계수 구획 — 갈래마다 (값 · 안내 · 사유)가 한 덩어리로 붙는다. */
|
||||
.b08-quantity__factors { margin: 4px 0 10px; }
|
||||
.b08-quantity__factor { margin-bottom: 6px; }
|
||||
|
||||
@@ -11,6 +11,8 @@ import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import { groupPanelSections } from "./B08_Quantity_UI_SidePanel_Sections";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import {
|
||||
fetchWorkflowState,
|
||||
@@ -353,7 +355,10 @@ function devUnlockRow(projectId: string, reload: () => void): HTMLElement {
|
||||
onClick: () => call("DELETE", relock),
|
||||
});
|
||||
const buttons = document.createElement("div");
|
||||
buttons.className = "b08-quantity__actions ui-sidebar-actions";
|
||||
// ⚠ 공용 `ui-sidebar-actions` 를 쓰지 않는다 — 공용 코드가 **첫 번째** 그 클래스를
|
||||
// 패널 바닥 액션 줄로 집어(`ui_template_overlay.ts` splitSidebarActions),
|
||||
// 스크롤 영역이 이 줄 안에 갇히고 아래 칸들이 통째로 잘린다(2026-09-12).
|
||||
buttons.className = "b08-quantity__dev-actions";
|
||||
buttons.append(unlock, relock);
|
||||
row.append(buttons);
|
||||
return row;
|
||||
@@ -640,6 +645,8 @@ function buildQuantitySidePanel(
|
||||
},
|
||||
),
|
||||
);
|
||||
// ⚠ `?.` 이 빠지면 표를 못 받은 때(`table === null`) 여기서 터져 **페이지가 통째로
|
||||
// 백지**가 된다 — 정작 보여야 할 「표를 못 불렀다」 안내까지 같이 사라진다(2026-09-12 실측).
|
||||
const placing = (
|
||||
table as unknown as {
|
||||
concrete_placing?: {
|
||||
@@ -647,8 +654,8 @@ function buildQuantitySidePanel(
|
||||
is_default: boolean;
|
||||
price_hint?: { basis?: string; values?: Record<string, number> };
|
||||
};
|
||||
}
|
||||
).concrete_placing;
|
||||
} | null
|
||||
)?.concrete_placing;
|
||||
if (placing) {
|
||||
// ⚠ 방식 이름을 **늘** 값 옆에 보인다 — 코드(`12-01-01`)만으로는 무엇을 쓰는지 모른다.
|
||||
const label = PLACING_LABELS[placing.method] ?? placing.method;
|
||||
@@ -743,6 +750,10 @@ function buildQuantitySidePanel(
|
||||
// TODO(미결) — 설정의 초기값을 무엇으로 볼지 사용자 확인 뒤에 붙인다.
|
||||
actions.append(saveButton, confirmButton);
|
||||
panel.append(actions);
|
||||
|
||||
// 조건 칸을 B03~B07 공통 상자로 묶고 제목 클릭으로 접히게 한다.
|
||||
groupPanelSections(panel);
|
||||
attachCollapsible(panel);
|
||||
return panel;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_UI_SidePanel_Sections.ts
|
||||
* 좌측 산출조건 패널을 **B03~B07 공통 상자**로 묶는 자리.
|
||||
*
|
||||
* 페이지 본문(`B08_Quantity_UI_Page.ts`)이 이미 700줄을 크게 넘어 새 코드를 그리로
|
||||
* 보내지 않고 이 파일로 뀜다(CLAUDE.md 4장 700줄 제한).
|
||||
* ========================================================================== */
|
||||
|
||||
/**
|
||||
* 다 쌓인 조건 칸을 「제목 + 그 뒤 칸들」 덩어리로 잘라 **B03~B07 공통 상자**에 담는다.
|
||||
*
|
||||
* 왜 다 쌓은 뒤에 한 번 묶나
|
||||
* 칸을 쌓는 코드가 400줄에 흩어져 있어 append 를 하나하나 고치면 손댈 자리가 너무 많다.
|
||||
* 제목은 `field(이름, "")` 이 낸 **값이 빈 줄**이고, 그것이 나올 때마다 새 상자가 열린다.
|
||||
* 상자 겉모습·접기는 공용 클래스가 전담한다 — B04·B06 과 같은 틀이다.
|
||||
*
|
||||
* ⚠ 개발용 줄과 맨 아래 액션 줄은 **상자 밖에 남긴다.** 액션 줄은 공용 코드가 패널 바닥에
|
||||
* 고정하는 줄이라 상자 안으로 들어가면 바닥에 안 붙는다.
|
||||
* ⚠ 첫 제목보다 앞에 오는 것(산출법 한 줄 · 토량환산계수 구획)은 **제목 없는 상자**에 담는다 —
|
||||
* 토량환산계수는 제 제목을 제 안에 이미 들고 있어 따로 붙이면 제목이 둘이 된다.
|
||||
*/
|
||||
export function groupPanelSections(panel: HTMLElement): void {
|
||||
const isHeading = (node: Element): boolean => {
|
||||
if (node.tagName !== "DIV" || !node.classList.contains("b08-quantity__field")) return false;
|
||||
const value = node.querySelector(".b08-quantity__field-value");
|
||||
return !value || !value.textContent;
|
||||
};
|
||||
const newSection = (title: string | null): HTMLElement => {
|
||||
const section = document.createElement("section");
|
||||
section.className = title
|
||||
? "b08-quantity__section ui-collapsible ui-sidebar-section"
|
||||
: "b08-quantity__section ui-sidebar-section";
|
||||
if (title) {
|
||||
const heading = document.createElement("p");
|
||||
heading.className = "b08-quantity__section-title ui-collapsible__title";
|
||||
heading.textContent = title;
|
||||
section.append(heading);
|
||||
}
|
||||
return section;
|
||||
};
|
||||
|
||||
let box: HTMLElement | null = null;
|
||||
for (const node of [...panel.children]) {
|
||||
// 개발용 줄·바닥 액션 줄은 건너뛰고 상자도 끊는다.
|
||||
if (
|
||||
node.classList.contains("b08-quantity__dev") ||
|
||||
node.classList.contains("ui-sidebar-actions")
|
||||
) {
|
||||
box = null;
|
||||
continue;
|
||||
}
|
||||
if (isHeading(node)) {
|
||||
box = newSection(node.firstElementChild?.textContent ?? "");
|
||||
panel.insertBefore(box, node);
|
||||
node.remove();
|
||||
continue;
|
||||
}
|
||||
if (!box) {
|
||||
box = newSection(null);
|
||||
panel.insertBefore(box, node);
|
||||
}
|
||||
box.append(node);
|
||||
}
|
||||
}
|
||||
@@ -516,6 +516,31 @@ FOREST_ROAD_PROFILE_CRITERIA = {
|
||||
# 배향곡선(Hair Pin) 중심선 반지름 하한(m, 별표2 Ⅰ.2.다.(2)). 이보다 급하면 **경고만**
|
||||
# 낸다 — 자동 보정·차단은 하지 않는다(2026-09-06 사용자 확정).
|
||||
"hairpin_min_radius_m": 10.0,
|
||||
# 임도 종류별 **못 넘는 하한**(m) — 계획노선 편집 화면이 값을 막는 기준이다
|
||||
# (2026-09-12 사용자 확정). 위 `min_plan_radius_m` 은 **기본값·위반 표시 기준**이고
|
||||
# 여기는 **제한**이라 서로 다르다. 둘을 한 값으로 묶으면 하한 0 이 곧 반지름 0 이 되어
|
||||
# 곡선이 아예 안 그려진다.
|
||||
# · None = 위 표(설계속도 × 지형)를 그대로 하한으로 쓴다.
|
||||
# · 0.0 = 제한 없음. 작업임도는 별표2에 곡선반지름 규정이 없어 열어 둔다 —
|
||||
# 값이 정해지면 **이 칸만** 고치면 서버·화면이 함께 따라간다.
|
||||
# ⚠ `projects.road_type` 은 main|fire|work 로 들어온다(B02 스키마) — 계획선 등급 코드
|
||||
# trunk 와 같은 뜻이라 둘 다 적어 둔다. 없는 키는 None 과 같게(법정 표) 다뤄진다.
|
||||
"plan_radius_limit_by_grade_m": {
|
||||
"main": None,
|
||||
"trunk": None,
|
||||
"fire": None,
|
||||
"work": 0.0,
|
||||
"branch": None,
|
||||
},
|
||||
# 평면 **곡선 길이(L)** 하한(m). 법령·교본에 값이 없어 지금은 전부 0(제한 없음)이다.
|
||||
# 자리만 만들어 두고, 실무값이 정해지면 여기에 적는다(2026-09-12 사용자 확정).
|
||||
"plan_curve_length_limit_by_grade_m": {
|
||||
"main": 0.0,
|
||||
"trunk": 0.0,
|
||||
"fire": 0.0,
|
||||
"work": 0.0,
|
||||
"branch": 0.0,
|
||||
},
|
||||
# 임도 종류 → **기본** 설계속도(km/h). 임도는 속도를 낼 수 없는 노선이라 20이
|
||||
# 기본이다(2026-08-19 사용자 확정). 별표2상 간선·산불진화는 20~40 범위에서
|
||||
# 설계자가 고르고, 작업임도는 20 이하이므로 20 고정이다. 사용자가 화면에서 고른
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
type: concept
|
||||
status: stable
|
||||
related_pages: ["[[multi_environment_safety]]", "[[design_data_lifecycle]]"]
|
||||
last_updated: 2026-09-12
|
||||
source: ["docs/raw/verification/2026-09-12_공용_브라우저_운용.md"]
|
||||
---
|
||||
|
||||
# 화면 검증 브라우저 운용
|
||||
|
||||
## 기본 선택
|
||||
|
||||
- 화면 검증 기본은 Orca 내장 브라우저다. 공용 브라우저는 사용자가 명시한 때만 쓴다.
|
||||
- 공용 창과 Orca 탭을 동시에 띄우면 `snapshot`·`screenshot`이 충돌하므로 함께 사용하지 않는다.
|
||||
- 검증은 직접 조작하고 `sessionStorage`, SVG 좌표, 툴팁, 스크린샷 등 수치 근거로 판정한다. 바꾼 값은 원래 상태로 복원한다.
|
||||
|
||||
## 공용 브라우저 시동과 조작
|
||||
|
||||
| 항목 | 위치·명령 | 역할 |
|
||||
|---|---|---|
|
||||
| 시동 | `./venv/Scripts/python.exe .claude/dev_up.py` | 서버와 공용 창을 시작하거나 살아 있는 프로세스 재사용 |
|
||||
| Orca용 시동 | `dev_up.py --no-browser` | 서버만 시작 |
|
||||
| 명령 큐 | `tmp/browser/cmd/NN_이름.py` | `page`, `log`, `shot`, `time` 전역으로 이름순 조작 |
|
||||
| 드라이버 로그 | `tmp/browser/driver.log` | RUN·OK·ERROR·SHOT 결과 기록 |
|
||||
| 스크린샷 | `tmp/browser/shots/` | 화면 검증 증거 저장 |
|
||||
|
||||
`tmp/`는 환경 사이에 전달되지 않는다. 남길 시험은 `resources/tester/`에 둔다. 브라우저 창 종료·재시작은 사용자 지시가 있을 때만 하며, 캐시는 CDP `Network.clearBrowserCache` 뒤 `page.reload()`로 비운다.
|
||||
|
||||
## 변경 종류별 반영
|
||||
|
||||
| 변경 | 공용 브라우저 | Orca |
|
||||
|---|---|---|
|
||||
| 페이지·공용 TypeScript | `page.reload()` | `orca reload` |
|
||||
| B07 WebCAD | `npm run build` 후 캐시 비우기 | 빌드 후 `orca reload`; 캐시 잔존 여부 미확인 |
|
||||
| Python·도면 템플릿 JSON | 백엔드만 재시작 | 백엔드만 재시작 |
|
||||
|
||||
공용 서버·브라우저의 포트와 주인은 현재 `OWNERS.md`를 따른다. `sessionStorage`는 포트별로 갈리므로 검증 포트에서 상태를 다시 세운다.
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
type: concept
|
||||
status: stable
|
||||
related_pages: ["[[architecture/shared_resources]]", "[[storage_paths]]", "[[workflow_state]]", "[[design_data_lifecycle]]"]
|
||||
last_updated: 2026-09-11
|
||||
source: ["docs/raw/verification/2026-09-09d_OWNERS_이력.md", "docs/raw/verification/2026-09-09e_계획서_완료근거_이관.md", "docs/raw/verification/2026-09-09f_계획서_공통기반과_참고_이관.md", "docs/raw/verification/2026-09-11_창환경_링크와_깃운영_조사.md", "docs/raw/plans/2026-09-11_plan_창환경_여섯워크트리_링크_깃운영.md"]
|
||||
related_pages: ["[[architecture/shared_resources]]", "[[storage_paths]]", "[[workflow_state]]", "[[design_data_lifecycle]]", "[[browser_verification_operations]]"]
|
||||
last_updated: 2026-09-12
|
||||
source: ["docs/raw/verification/2026-09-09d_OWNERS_이력.md", "docs/raw/verification/2026-09-09e_계획서_완료근거_이관.md", "docs/raw/verification/2026-09-09f_계획서_공통기반과_참고_이관.md", "docs/raw/verification/2026-09-11_창환경_링크와_깃운영_조사.md", "docs/raw/plans/2026-09-11_plan_창환경_여섯워크트리_링크_깃운영.md", "docs/raw/verification/2026-09-12_깃_합류점_dev_전환.md"]
|
||||
---
|
||||
|
||||
# 다중 환경 저장소·공용 DB 안전
|
||||
@@ -31,15 +31,26 @@ source: ["docs/raw/verification/2026-09-09d_OWNERS_이력.md", "docs/raw/verific
|
||||
| 구분 | 확정 운영 |
|
||||
|---|---|
|
||||
| 코드 환경 | `main_laptop_1`, `sub_laptop_1`, `main_desktop_1`, `sub_desktop_1` |
|
||||
| AI 환경 | 데스크탑의 `CODEX`, `안티그래비티`; AI 브랜치는 `docs/`만 전달 |
|
||||
| AI 환경 | 데스크탑의 `CODEX`, `안티그래비티`; 코드 작업은 사용자 지시가 있을 때만 수행 |
|
||||
| 메인 동기화 | 두 메인 폴더의 `.claude`, `.codex`, 루트 지침·장부는 Synology가 전달 |
|
||||
| 보조·AI 링크 | 폴더 `.claude`·`.codex`·`venv`는 정션, 루트 파일 넷은 심볼릭 링크 |
|
||||
| 링크 원본 | 각 PC의 `OWNERS.md`에 적힌 자기 PC 메인 경로 |
|
||||
| 신규 워크트리 | Orca가 생성·삭제, `worktree_setup.ps1`가 설치 후 `worktree_link.ps1` 실행 |
|
||||
| Git 동기화 | 기존 스크립트가 fetch·merge·자동 push 담당; 브랜치 이름 패턴은 넓히지 않음 |
|
||||
| Git 동기화 | `main`은 정본, `dev`는 합류점, 환경 브랜치 6개는 작업 자리 |
|
||||
|
||||
링크는 Synology 동기화 루트 밖에만 만들고, 지침·장부를 `.claude`나 `.codex` 안으로 옮기지 않는다. `worktree_link.ps1`는 살아 있는 하드링크만 심볼릭 링크로 갈아타며 갈라진 실물은 지우지 않는다.
|
||||
|
||||
## Git 한 바퀴
|
||||
|
||||
| 단계 | 규칙 |
|
||||
|---|---|
|
||||
| 받기 | 작업 시작 전 자기 워크트리에서 `.claude\git-sync.ps1`를 인자 없이 한 번 실행 |
|
||||
| 작업 | 자기 환경 브랜치에서 지시받은 범위만 변경 |
|
||||
| 밀기 | 경로를 지정해 커밋한 뒤 `git push origin HEAD`로 자기 브랜치에만 push |
|
||||
| 모으기 | 사용자가 Git 싱크를 지시한 때만 `git-sync.ps1 -Converge` 실행 |
|
||||
|
||||
`git-sync.ps1`는 `origin/dev`를 받고, `dev`가 아직 품지 않은 환경 브랜치를 한 번의 옥토퍼스 머지로 담은 뒤 `dev`와 현재 환경 브랜치를 빨리감기한다. `-Brief`는 작업 중 HEAD를 바꾸지 않고 미수신 커밋만 알린다. 미커밋이 있으면 받기를 통째로 건너뛰며, `main`과 `dev`에 손으로 push하지 않는다.
|
||||
|
||||
## 완료 판정
|
||||
|
||||
- 랩탑 AI 워크트리 둘 제거, 데스크탑 AI 워크트리 둘 생성, 랩탑·데스크탑 보조 및 AI 워크트리 링크 구성을 완료했다.
|
||||
|
||||
@@ -171,14 +171,21 @@
|
||||
"169": "B08_DesignDetail_Engine_Cad_Basin.py",
|
||||
"170": "B08_DesignDetail_Engine_Cad_MassHaul.py",
|
||||
"171": "B08 CAD·납품 도면 후속",
|
||||
"172": "2026-09-04 완료 항목",
|
||||
"173": "multi_environment_safety.md",
|
||||
"174": "Workflow 상태 관리",
|
||||
"175": "DB 스키마 개요",
|
||||
"176": "OpenWebCAD Core",
|
||||
"177": "common_util_mass_haul_settle.ts",
|
||||
"178": "Drainage Watershed (유역도)",
|
||||
"179": "Mass Haul Diagram (토적도)",
|
||||
"180": "다중 환경 저장소·공용 DB 안전",
|
||||
"181": "저장 경로 규칙 (Workflow-based Folder Structure)",
|
||||
"182": "2026-09-02 완료 항목",
|
||||
"183": "2026-09-03 완료 — 입력·배수·종횡단·CAD",
|
||||
"184": "2026-09-03 추가 완료 — 화면·종단·횡단",
|
||||
"185": "Query: 임도 집수정 형태정보",
|
||||
"186": "설계 데이터 생명주기",
|
||||
"187": "B03 File Input Backend",
|
||||
"188": "B03 File Input Frontend",
|
||||
"189": "B04 PreProcess Backend",
|
||||
|
||||
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
@@ -1,16 +1,16 @@
|
||||
# Graph Report - wiki (2026-09-11)
|
||||
# Graph Report - wiki (2026-09-12)
|
||||
|
||||
## Corpus Check
|
||||
- 206 files · ~57,686 words
|
||||
- 207 files · ~58,032 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 1170 nodes · 996 edges · 189 communities (155 shown, 34 thin omitted)
|
||||
- 1176 nodes · 1003 edges · 196 communities (162 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: `d6e45bb4`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -187,14 +187,21 @@
|
||||
- B08_DesignDetail_Engine_Cad_Basin.py
|
||||
- B08_DesignDetail_Engine_Cad_MassHaul.py
|
||||
- B08 CAD·납품 도면 후속
|
||||
- 2026-09-04 완료 항목
|
||||
- multi_environment_safety.md
|
||||
- Workflow 상태 관리
|
||||
- DB 스키마 개요
|
||||
- OpenWebCAD Core
|
||||
- common_util_mass_haul_settle.ts
|
||||
- Drainage Watershed (유역도)
|
||||
- Mass Haul Diagram (토적도)
|
||||
- 다중 환경 저장소·공용 DB 안전
|
||||
- 저장 경로 규칙 (Workflow-based Folder Structure)
|
||||
- 2026-09-02 완료 항목
|
||||
- 2026-09-03 완료 — 입력·배수·종횡단·CAD
|
||||
- 2026-09-03 추가 완료 — 화면·종단·횡단
|
||||
- Query: 임도 집수정 형태정보
|
||||
- 설계 데이터 생명주기
|
||||
- B03 File Input Backend
|
||||
- B03 File Input Frontend
|
||||
- B04 PreProcess Backend
|
||||
@@ -229,9 +236,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 +243,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 (196 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.10
|
||||
Nodes (18): 디자인 시스템 (Design System), 레이아웃 및 둥근 테두리 (Radius & Spacing), 비주얼 테마, 전역 스크롤바 디자인 (Scrollbars), 타이포그래피 (Typography), 핵심 색상 토큰 (Colors), theme.css (스타일 변수), ui_template_elements.ts (공통 엘리먼트 템플릿) (+10 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.11
|
||||
Nodes (17): OTP / 비밀번호 및 디바이스 신뢰, 권한 검증 헬퍼 (B01_Dashboard), 라우팅 가드 (frontend.md 5.2), 사용자 상태 생명주기, 사용처 (역참조), 세션 인증 (backend.md 6.3), 역할 (users.role), 인증 / RBAC (+9 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)
|
||||
Cohesion: 0.22
|
||||
Nodes (7): API 공통 (여러 페이지가 공유하는 엔드포인트), 공통 오류 응답 포맷 (전 라우터), 워크플로우 상태 조회, 폴링 패턴 (legacy workflow.json 설계; 현재 구현은 workflow-state API 사용), 검증 원칙 (backend.md 4절), 공통 스키마 (Pydantic 요청/응답 규칙), 명명 규칙
|
||||
|
||||
### Community 4 - "배수유역 해석 및 세부설계 (Drainage Watershed)"
|
||||
Cohesion: 0.25
|
||||
@@ -270,8 +274,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.06
|
||||
Nodes (31): 2026-08-29 완료 반영, B03 재업로드·B05 최신 조회, B05/B06 구조물 UI 통합, B07↔B08 순서, B07 CAD 고정 척도·횡단 장 배치, B07 CAD 테마, B07 CAD 확대·팬, 배수시설 추천 기준 (+23 more)
|
||||
|
||||
### Community 8 - "DB: 파일/지표면분석 테이블"
|
||||
Cohesion: 0.18
|
||||
@@ -849,6 +853,30 @@ Nodes (3): B06 횡단 계산 미러·카드 표기, 프론트 계산 미러, 횡
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B08 CAD·납품 도면 후속, CAD 편집·확정, 토적도·유역도
|
||||
|
||||
### Community 172 - "2026-09-04 완료 항목"
|
||||
Cohesion: 0.18
|
||||
Nodes (9): 공통 유틸 (common_util/), 리소스 모니터링 (common_util_resource_monitor.py), 이메일 발송 (common_util_email.py), 2026-09-04 완료 항목, 700줄 제한 분리, 보존된 미완료 범위, 상시계획서 추가 완료 범위, 완료 범위 (+1 more)
|
||||
|
||||
### Community 173 - "multi_environment_safety.md"
|
||||
Cohesion: 0.32
|
||||
Nodes (4): 공용 브라우저 시동과 조작, 기본 선택, 변경 종류별 반영, 화면 검증 브라우저 운용
|
||||
|
||||
### Community 174 - "Workflow 상태 관리"
|
||||
Cohesion: 0.25
|
||||
Nodes (8): R1 워크플로우 단계 재편 (2026-08-08 반영), SSOT: `project_workflow_stages` 테이블 (실 DB 확인), Workflow 상태 관리, 공통 유틸 `common_util/common_util_workflow_state.py`, 무효화의 실제 범위, 백그라운드 자동 계산 체인 및 사용자 설정 이월, 조회 API, 프론트엔드 게이팅 및 스텝바 연동
|
||||
|
||||
### Community 175 - "DB 스키마 개요"
|
||||
Cohesion: 0.29
|
||||
Nodes (5): DB 스키마 개요, 설계 원칙, 테이블 관계 (핵심 흐름), 테이블 그룹 (9개), 파일 경로 추적 컬럼 (DB에 경로만 기록, 실 파일은 파일시스템)
|
||||
|
||||
### Community 180 - "다중 환경 저장소·공용 DB 안전"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): Git 한 바퀴, 다중 환경 저장소·공용 DB 안전, 여섯 워크트리 운영 확정판, 완료 판정, 운영 규칙, 재계산 영향, 확인된 위험
|
||||
|
||||
### Community 181 - "저장 경로 규칙 (Workflow-based Folder Structure)"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): DB 컬럼 ↔ 실제 경로 매핑, 경로 패턴, 원칙 (backend.md 3절), 저장 경로 규칙 (Workflow-based Folder Structure), 코드 감사 주의사항, 파일명 규칙 (structure.md 1절)
|
||||
|
||||
### Community 182 - "2026-09-02 완료 항목"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): 2026-09-02 완료 항목, B05 구조물 3D, B05 종단 편집 후속, CAD, 노선·지표면, 작업 환경, 회귀 상태
|
||||
@@ -861,8 +889,12 @@ Nodes (3): 2026-09-03 완료 — 입력·배수·종횡단·CAD, 공통 결정,
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 2026-09-03 추가 완료 — 화면·종단·횡단, 완료 범위, 최신 결정
|
||||
|
||||
### Community 186 - "설계 데이터 생명주기"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): 계산 구현 원칙, 계획노선 규칙, 설계 데이터 생명주기, 정본 세 벌
|
||||
|
||||
## Knowledge Gaps
|
||||
- **756 isolated node(s):** `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어`, `반드시 유지할 구분`, `미결 설계` (+751 more)
|
||||
- **760 isolated node(s):** `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어`, `반드시 유지할 구분`, `미결 설계` (+755 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 +907,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._
|
||||
_760 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.1 - 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.10526315789473684 - 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._
|
||||
_Cohesion score 0.05555555555555555 - 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._
|
||||
File diff suppressed because one or more lines are too long
+294
-235
@@ -3,48 +3,6 @@
|
||||
"multigraph": false,
|
||||
"graph": {
|
||||
"hyperedges": [
|
||||
{
|
||||
"id": "workflow_b03_b08_integration",
|
||||
"label": "B03-B08 Workflow Data Flow",
|
||||
"nodes": [
|
||||
"b03_fileinput_route_snapshot_crs",
|
||||
"b04_preprocess_drainage_compass_crs",
|
||||
"b05_profile_frontend",
|
||||
"b06_section_cross_design_ui_2026_09",
|
||||
"b08_designdetail_frontend"
|
||||
],
|
||||
"relation": "participate_in",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.9,
|
||||
"source_file": "index.md"
|
||||
},
|
||||
{
|
||||
"id": "mass_haul_shared_logic",
|
||||
"label": "Shared Mass Haul Calculation and UI",
|
||||
"nodes": [
|
||||
"b05_profile_masshaul_structure_2026_09",
|
||||
"b06_section_masshaul_culvert_2026_09",
|
||||
"b08_designdetail_cad_delivery_2026_09"
|
||||
],
|
||||
"relation": "participate_in",
|
||||
"confidence": "EXTRACTED",
|
||||
"confidence_score": 0.85,
|
||||
"source_file": "pages/B05_Profile/B05_masshaul_structure_2026_09.md"
|
||||
},
|
||||
{
|
||||
"id": "cad_delivery_system",
|
||||
"label": "CAD Delivery and Usability Framework",
|
||||
"nodes": [
|
||||
"b08_designdetail_cad_interaction",
|
||||
"b08_designdetail_cad_title_block",
|
||||
"b08_designdetail_cad_usability_2026_09_01",
|
||||
"b08_designdetail_cad_delivery_2026_09"
|
||||
],
|
||||
"relation": "form",
|
||||
"confidence": "EXTRACTED",
|
||||
"confidence_score": 0.95,
|
||||
"source_file": "pages/B08_DesignDetail/B08_cad_delivery_2026_09.md"
|
||||
},
|
||||
{
|
||||
"id": "las_free_workflow_chain",
|
||||
"label": "LAS-Free Analysis Workflow",
|
||||
@@ -798,6 +756,61 @@
|
||||
"community_name": "B07 \uc678\ubd80 WebCAD \ube44\uad50 \uc2e4\ud589\ud658\uacbd",
|
||||
"norm_label": "\u1105\u1161\u110b\u1175\u1109\u1165\u11ab\u1109\u1173 \u110c\u116e\u110b\u1174"
|
||||
},
|
||||
{
|
||||
"label": "browser_verification_operations.md",
|
||||
"file_type": "document",
|
||||
"source_file": "concepts/browser_verification_operations.md",
|
||||
"source_location": "L1",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_browser_verification_operations",
|
||||
"community": 173,
|
||||
"community_name": "multi_environment_safety.md",
|
||||
"norm_label": "browser_verification_operations.md"
|
||||
},
|
||||
{
|
||||
"label": "\ud654\uba74 \uac80\uc99d \ube0c\ub77c\uc6b0\uc800 \uc6b4\uc6a9",
|
||||
"file_type": "document",
|
||||
"source_file": "concepts/browser_verification_operations.md",
|
||||
"source_location": "L9",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_browser_verification_operations_\ud654\uba74_\uac80\uc99d_\ube0c\ub77c\uc6b0\uc800_\uc6b4\uc6a9",
|
||||
"community": 173,
|
||||
"community_name": "multi_environment_safety.md",
|
||||
"norm_label": "\u1112\u116a\u1106\u1167\u11ab \u1100\u1165\u11b7\u110c\u1173\u11bc \u1107\u1173\u1105\u1161\u110b\u116e\u110c\u1165 \u110b\u116e\u11ab\u110b\u116d\u11bc"
|
||||
},
|
||||
{
|
||||
"label": "\uae30\ubcf8 \uc120\ud0dd",
|
||||
"file_type": "document",
|
||||
"source_file": "concepts/browser_verification_operations.md",
|
||||
"source_location": "L11",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_browser_verification_operations_\uae30\ubcf8_\uc120\ud0dd",
|
||||
"community": 173,
|
||||
"community_name": "multi_environment_safety.md",
|
||||
"norm_label": "\u1100\u1175\u1107\u1169\u11ab \u1109\u1165\u11ab\u1110\u1162\u11a8"
|
||||
},
|
||||
{
|
||||
"label": "\uacf5\uc6a9 \ube0c\ub77c\uc6b0\uc800 \uc2dc\ub3d9\uacfc \uc870\uc791",
|
||||
"file_type": "document",
|
||||
"source_file": "concepts/browser_verification_operations.md",
|
||||
"source_location": "L17",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_browser_verification_operations_\uacf5\uc6a9_\ube0c\ub77c\uc6b0\uc800_\uc2dc\ub3d9\uacfc_\uc870\uc791",
|
||||
"community": 173,
|
||||
"community_name": "multi_environment_safety.md",
|
||||
"norm_label": "\u1100\u1169\u11bc\u110b\u116d\u11bc \u1107\u1173\u1105\u1161\u110b\u116e\u110c\u1165 \u1109\u1175\u1103\u1169\u11bc\u1100\u116a \u110c\u1169\u110c\u1161\u11a8"
|
||||
},
|
||||
{
|
||||
"label": "\ubcc0\uacbd \uc885\ub958\ubcc4 \ubc18\uc601",
|
||||
"file_type": "document",
|
||||
"source_file": "concepts/browser_verification_operations.md",
|
||||
"source_location": "L29",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_browser_verification_operations_\ubcc0\uacbd_\uc885\ub958\ubcc4_\ubc18\uc601",
|
||||
"community": 173,
|
||||
"community_name": "multi_environment_safety.md",
|
||||
"norm_label": "\u1107\u1167\u11ab\u1100\u1167\u11bc \u110c\u1169\u11bc\u1105\u1172\u1107\u1167\u11af \u1107\u1161\u11ab\u110b\u1167\u11bc"
|
||||
},
|
||||
{
|
||||
"label": "common_util.md",
|
||||
"file_type": "document",
|
||||
@@ -805,8 +818,8 @@
|
||||
"source_location": "L1",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_common_util",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 172,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"norm_label": "common_util.md"
|
||||
},
|
||||
{
|
||||
@@ -816,8 +829,8 @@
|
||||
"source_location": "L9",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_common_util_\uacf5\ud1b5_\uc720\ud2f8_common_util",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 172,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"norm_label": "\u1100\u1169\u11bc\u1110\u1169\u11bc \u110b\u1172\u1110\u1175\u11af (common_util/)"
|
||||
},
|
||||
{
|
||||
@@ -827,8 +840,8 @@
|
||||
"source_location": "L39",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_common_util_\uc774\uba54\uc77c_\ubc1c\uc1a1_common_util_email_py",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 172,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"norm_label": "\u110b\u1175\u1106\u1166\u110b\u1175\u11af \u1107\u1161\u11af\u1109\u1169\u11bc (common_util_email.py)"
|
||||
},
|
||||
{
|
||||
@@ -838,8 +851,8 @@
|
||||
"source_location": "L43",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_common_util_\ub9ac\uc18c\uc2a4_\ubaa8\ub2c8\ud130\ub9c1_common_util_resource_monitor_py",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 172,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"norm_label": "\u1105\u1175\u1109\u1169\u1109\u1173 \u1106\u1169\u1102\u1175\u1110\u1165\u1105\u1175\u11bc (common_util_resource_monitor.py)"
|
||||
},
|
||||
{
|
||||
@@ -1377,8 +1390,8 @@
|
||||
"source_location": "L1",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_completed_2026_09_04",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 172,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"norm_label": "completed_2026-09-04.md"
|
||||
},
|
||||
{
|
||||
@@ -1388,8 +1401,8 @@
|
||||
"source_location": "L9",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_completed_2026_09_04_2026_09_04_\uc644\ub8cc_\ud56d\ubaa9",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 172,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"norm_label": "2026-09-04 \u110b\u116a\u11ab\u1105\u116d \u1112\u1161\u11bc\u1106\u1169\u11a8"
|
||||
},
|
||||
{
|
||||
@@ -1399,8 +1412,8 @@
|
||||
"source_location": "L13",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_completed_2026_09_04_\uc644\ub8cc_\ubc94\uc704",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 172,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"norm_label": "\u110b\u116a\u11ab\u1105\u116d \u1107\u1165\u11b7\u110b\u1171"
|
||||
},
|
||||
{
|
||||
@@ -1410,8 +1423,8 @@
|
||||
"source_location": "L38",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_completed_2026_09_04_\ucd94\uac00_\uc644\ub8cc_\ubc94\uc704",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 172,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"norm_label": "\u110e\u116e\u1100\u1161 \u110b\u116a\u11ab\u1105\u116d \u1107\u1165\u11b7\u110b\u1171"
|
||||
},
|
||||
{
|
||||
@@ -1421,8 +1434,8 @@
|
||||
"source_location": "L48",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_completed_2026_09_04_700\uc904_\uc81c\ud55c_\ubd84\ub9ac",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 172,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"norm_label": "700\u110c\u116e\u11af \u110c\u1166\u1112\u1161\u11ab \u1107\u116e\u11ab\u1105\u1175"
|
||||
},
|
||||
{
|
||||
@@ -1432,8 +1445,8 @@
|
||||
"source_location": "L54",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_completed_2026_09_04_\uc0c1\uc2dc\uacc4\ud68d\uc11c_\ucd94\uac00_\uc644\ub8cc_\ubc94\uc704",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 172,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"norm_label": "\u1109\u1161\u11bc\u1109\u1175\u1100\u1168\u1112\u116c\u11a8\u1109\u1165 \u110e\u116e\u1100\u1161 \u110b\u116a\u11ab\u1105\u116d \u1107\u1165\u11b7\u110b\u1171"
|
||||
},
|
||||
{
|
||||
@@ -1443,8 +1456,8 @@
|
||||
"source_location": "L61",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_completed_2026_09_04_\ubcf4\uc874\ub41c_\ubbf8\uc644\ub8cc_\ubc94\uc704",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 172,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"norm_label": "\u1107\u1169\u110c\u1169\u11ab\u1103\u116c\u11ab \u1106\u1175\u110b\u116a\u11ab\u1105\u116d \u1107\u1165\u11b7\u110b\u1171"
|
||||
},
|
||||
{
|
||||
@@ -1905,8 +1918,8 @@
|
||||
"source_location": "L1",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_db_schema_overview",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 175,
|
||||
"community_name": "DB \uc2a4\ud0a4\ub9c8 \uac1c\uc694",
|
||||
"norm_label": "overview.md"
|
||||
},
|
||||
{
|
||||
@@ -1916,8 +1929,8 @@
|
||||
"source_location": "L9",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_db_schema_overview_db_\uc2a4\ud0a4\ub9c8_\uac1c\uc694",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 175,
|
||||
"community_name": "DB \uc2a4\ud0a4\ub9c8 \uac1c\uc694",
|
||||
"norm_label": "db \u1109\u1173\u110f\u1175\u1106\u1161 \u1100\u1162\u110b\u116d"
|
||||
},
|
||||
{
|
||||
@@ -1927,8 +1940,8 @@
|
||||
"source_location": "L22",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_db_schema_overview_\ud14c\uc774\ube14_\uadf8\ub8f9_9\uac1c",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 175,
|
||||
"community_name": "DB \uc2a4\ud0a4\ub9c8 \uac1c\uc694",
|
||||
"norm_label": "\u1110\u1166\u110b\u1175\u1107\u1173\u11af \u1100\u1173\u1105\u116e\u11b8 (9\u1100\u1162)"
|
||||
},
|
||||
{
|
||||
@@ -1938,8 +1951,8 @@
|
||||
"source_location": "L34",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_db_schema_overview_\ud30c\uc77c_\uacbd\ub85c_\ucd94\uc801_\uceec\ub7fc_db\uc5d0_\uacbd\ub85c\ub9cc_\uae30\ub85d_\uc2e4_\ud30c\uc77c\uc740_\ud30c\uc77c\uc2dc\uc2a4\ud15c",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 175,
|
||||
"community_name": "DB \uc2a4\ud0a4\ub9c8 \uac1c\uc694",
|
||||
"norm_label": "\u1111\u1161\u110b\u1175\u11af \u1100\u1167\u11bc\u1105\u1169 \u110e\u116e\u110c\u1165\u11a8 \u110f\u1165\u11af\u1105\u1165\u11b7 (db\u110b\u1166 \u1100\u1167\u11bc\u1105\u1169\u1106\u1161\u11ab \u1100\u1175\u1105\u1169\u11a8, \u1109\u1175\u11af \u1111\u1161\u110b\u1175\u11af\u110b\u1173\u11ab \u1111\u1161\u110b\u1175\u11af\u1109\u1175\u1109\u1173\u1110\u1166\u11b7)"
|
||||
},
|
||||
{
|
||||
@@ -1949,8 +1962,8 @@
|
||||
"source_location": "L51",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_db_schema_overview_\uc124\uacc4_\uc6d0\uce59",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 175,
|
||||
"community_name": "DB \uc2a4\ud0a4\ub9c8 \uac1c\uc694",
|
||||
"norm_label": "\u1109\u1165\u11af\u1100\u1168 \u110b\u116f\u11ab\u110e\u1175\u11a8"
|
||||
},
|
||||
{
|
||||
@@ -1960,8 +1973,8 @@
|
||||
"source_location": "L56",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_db_schema_overview_\ud14c\uc774\ube14_\uad00\uacc4_\ud575\uc2ec_\ud750\ub984",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 175,
|
||||
"community_name": "DB \uc2a4\ud0a4\ub9c8 \uac1c\uc694",
|
||||
"norm_label": "\u1110\u1166\u110b\u1175\u1107\u1173\u11af \u1100\u116a\u11ab\u1100\u1168 (\u1112\u1162\u11a8\u1109\u1175\u11b7 \u1112\u1173\u1105\u1173\u11b7)"
|
||||
},
|
||||
{
|
||||
@@ -2521,8 +2534,8 @@
|
||||
"source_location": "L1",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_design_data_lifecycle",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 173,
|
||||
"community_name": "multi_environment_safety.md",
|
||||
"norm_label": "design_data_lifecycle.md"
|
||||
},
|
||||
{
|
||||
@@ -2532,8 +2545,8 @@
|
||||
"source_location": "L9",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_design_data_lifecycle_\uc124\uacc4_\ub370\uc774\ud130_\uc0dd\uba85\uc8fc\uae30",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 186,
|
||||
"community_name": "\uc124\uacc4 \ub370\uc774\ud130 \uc0dd\uba85\uc8fc\uae30",
|
||||
"norm_label": "\u1109\u1165\u11af\u1100\u1168 \u1103\u1166\u110b\u1175\u1110\u1165 \u1109\u1162\u11bc\u1106\u1167\u11bc\u110c\u116e\u1100\u1175"
|
||||
},
|
||||
{
|
||||
@@ -2543,8 +2556,8 @@
|
||||
"source_location": "L11",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_design_data_lifecycle_\uc815\ubcf8_\uc138_\ubc8c",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 186,
|
||||
"community_name": "\uc124\uacc4 \ub370\uc774\ud130 \uc0dd\uba85\uc8fc\uae30",
|
||||
"norm_label": "\u110c\u1165\u11bc\u1107\u1169\u11ab \u1109\u1166 \u1107\u1165\u11af"
|
||||
},
|
||||
{
|
||||
@@ -2554,8 +2567,8 @@
|
||||
"source_location": "L21",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_design_data_lifecycle_\uacc4\ud68d\ub178\uc120_\uaddc\uce59",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 186,
|
||||
"community_name": "\uc124\uacc4 \ub370\uc774\ud130 \uc0dd\uba85\uc8fc\uae30",
|
||||
"norm_label": "\u1100\u1168\u1112\u116c\u11a8\u1102\u1169\u1109\u1165\u11ab \u1100\u1172\u110e\u1175\u11a8"
|
||||
},
|
||||
{
|
||||
@@ -2565,8 +2578,8 @@
|
||||
"source_location": "L28",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_design_data_lifecycle_\uacc4\uc0b0_\uad6c\ud604_\uc6d0\uce59",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 186,
|
||||
"community_name": "\uc124\uacc4 \ub370\uc774\ud130 \uc0dd\uba85\uc8fc\uae30",
|
||||
"norm_label": "\u1100\u1168\u1109\u1161\u11ab \u1100\u116e\u1112\u1167\u11ab \u110b\u116f\u11ab\u110e\u1175\u11a8"
|
||||
},
|
||||
{
|
||||
@@ -2576,8 +2589,8 @@
|
||||
"source_location": "L1",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_drainage_watershed",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "drainage_watershed.md"
|
||||
},
|
||||
{
|
||||
@@ -2587,8 +2600,8 @@
|
||||
"source_location": "L7",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_drainage_watershed_\ubc30\uc218\uc720\uc5ed_\ud574\uc11d_\ubc0f_\uc138\ubd80\uc124\uacc4_drainage_watershed",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "\u1107\u1162\u1109\u116e\u110b\u1172\u110b\u1167\u11a8 \u1112\u1162\u1109\u1165\u11a8 \u1106\u1175\u11be \u1109\u1166\u1107\u116e\u1109\u1165\u11af\u1100\u1168 (drainage watershed)"
|
||||
},
|
||||
{
|
||||
@@ -2598,8 +2611,8 @@
|
||||
"source_location": "L13",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_drainage_watershed_1_b04_vs_b05_\uc5ed\ud560_\ubd84\ub2f4_\ubc0f_\uc77c\uc6d0\ud654",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "1. b04 vs b05 \u110b\u1167\u11a8\u1112\u1161\u11af \u1107\u116e\u11ab\u1103\u1161\u11b7 \u1106\u1175\u11be \u110b\u1175\u11af\u110b\u116f\u11ab\u1112\u116a"
|
||||
},
|
||||
{
|
||||
@@ -2609,8 +2622,8 @@
|
||||
"source_location": "L22",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_drainage_watershed_2_\uacf5\uc6a9_\ubc30\uc218_\uc5d4\uc9c4_\ubc0f_wamis_\uac15\uc6b0\ub7c9_\uc5f0\ub3d9_phase_1_2_2026_08_13_\uad00\uce21\uc18c_\uc804\ud658",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "2. \u1100\u1169\u11bc\u110b\u116d\u11bc \u1107\u1162\u1109\u116e \u110b\u1166\u11ab\u110c\u1175\u11ab \u1106\u1175\u11be wamis \u1100\u1161\u11bc\u110b\u116e\u1105\u1163\u11bc \u110b\u1167\u11ab\u1103\u1169\u11bc (phase 1~2, 2026-08-13 \u1100\u116a\u11ab\u110e\u1173\u11a8\u1109\u1169 \u110c\u1165\u11ab\u1112\u116a\u11ab)"
|
||||
},
|
||||
{
|
||||
@@ -2620,8 +2633,8 @@
|
||||
"source_location": "L30",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_drainage_watershed_3_\uad6c\uc870\ubb3c_3\ub2e8_\uc635\uc158_\uccb4\uacc4_ui_phase_3_5",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "3. \u1100\u116e\u110c\u1169\u1106\u116e\u11af 3\u1103\u1161\u11ab \u110b\u1169\u11b8\u1109\u1167\u11ab \u110e\u1166\u1100\u1168 & ui (phase 3~5)"
|
||||
},
|
||||
{
|
||||
@@ -2631,8 +2644,8 @@
|
||||
"source_location": "L36",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_drainage_watershed_4_\ud574\uc11d_\uc54c\uace0\ub9ac\uc998_\ub4f1\uace0\uc120_\ud558\uac15_contour_descent",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "4. \u1112\u1162\u1109\u1165\u11a8 \u110b\u1161\u11af\u1100\u1169\u1105\u1175\u110c\u1173\u11b7 \u2014 \u1103\u1173\u11bc\u1100\u1169\u1109\u1165\u11ab \u1112\u1161\u1100\u1161\u11bc (contour descent)"
|
||||
},
|
||||
{
|
||||
@@ -2642,8 +2655,8 @@
|
||||
"source_location": "L47",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_drainage_watershed_5_\uc801\uc0c9_\uccad\uc0c9_\ud310\uc815_\ubc0f_\uc720\uc5ed_\ud655\uc7a5",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "5. \u110c\u1165\u11a8\u1109\u1162\u11a8/\u110e\u1165\u11bc\u1109\u1162\u11a8 \u1111\u1161\u11ab\u110c\u1165\u11bc \u1106\u1175\u11be \u110b\u1172\u110b\u1167\u11a8 \u1112\u116a\u11a8\u110c\u1161\u11bc"
|
||||
},
|
||||
{
|
||||
@@ -2653,8 +2666,8 @@
|
||||
"source_location": "L56",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_drainage_watershed_6_\ud3c9\uade0_\ud750\ub984_\ud654\uc0b4\ud45c_flow_arrows_\ubc0f_\ud750\ub984\uac15\ub3c4_\ub7a8\ud504",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "6. \u1111\u1167\u11bc\u1100\u1172\u11ab \u1112\u1173\u1105\u1173\u11b7 \u1112\u116a\u1109\u1161\u11af\u1111\u116d (flow arrows) \u1106\u1175\u11be \u1112\u1173\u1105\u1173\u11b7\u1100\u1161\u11bc\u1103\u1169 \u1105\u1162\u11b7\u1111\u1173"
|
||||
},
|
||||
{
|
||||
@@ -2664,8 +2677,8 @@
|
||||
"source_location": "L60",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_drainage_watershed_7_\uc601\uad6c\uc800\uc7a5\uc18c_\uc0b0\ucd9c\ubb3c_\uad6c\uc870",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "7. \u110b\u1167\u11bc\u1100\u116e\u110c\u1165\u110c\u1161\u11bc\u1109\u1169 \u1109\u1161\u11ab\u110e\u116e\u11af\u1106\u116e\u11af \u1100\u116e\u110c\u1169"
|
||||
},
|
||||
{
|
||||
@@ -2675,8 +2688,8 @@
|
||||
"source_location": "L75",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_drainage_watershed_8_b08_\uc218\ub9ac\uc9d1\uc218\uba74\uc801\uc720\uc5ed\ub3c4",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "8. b08 \u1109\u116e\u1105\u1175\u110c\u1175\u11b8\u1109\u116e\u1106\u1167\u11ab\u110c\u1165\u11a8\u110b\u1172\u110b\u1167\u11a8\u1103\u1169"
|
||||
},
|
||||
{
|
||||
@@ -2686,8 +2699,8 @@
|
||||
"source_location": "L1",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_las_free_sheet_surface",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "las_free_sheet_surface.md"
|
||||
},
|
||||
{
|
||||
@@ -2697,8 +2710,8 @@
|
||||
"source_location": "L9",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_las_free_sheet_surface_las_\uc5c6\ub294_\ub3c4\uc5fd\ub4f1\uace0\uc120_\uc11c\ud53c\uc2a4",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "las \u110b\u1165\u11b9\u1102\u1173\u11ab \u1103\u1169\u110b\u1167\u11b8\u1103\u1173\u11bc\u1100\u1169\u1109\u1165\u11ab \u1109\u1165\u1111\u1175\u1109\u1173"
|
||||
},
|
||||
{
|
||||
@@ -2708,8 +2721,8 @@
|
||||
"source_location": "L11",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_las_free_sheet_surface_\uacc4\uc57d\uacfc_\ud655\uc815\uac12",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "\u1100\u1168\u110b\u1163\u11a8\u1100\u116a \u1112\u116a\u11a8\u110c\u1165\u11bc\u1100\u1161\u11b9"
|
||||
},
|
||||
{
|
||||
@@ -2719,8 +2732,8 @@
|
||||
"source_location": "L22",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_las_free_sheet_surface_\ud750\ub984",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "\u1112\u1173\u1105\u1173\u11b7"
|
||||
},
|
||||
{
|
||||
@@ -2730,8 +2743,8 @@
|
||||
"source_location": "L33",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_las_free_sheet_surface_e2e_\uacb0\uacfc",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "e2e \u1100\u1167\u11af\u1100\u116a"
|
||||
},
|
||||
{
|
||||
@@ -2741,8 +2754,8 @@
|
||||
"source_location": "L40",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_las_free_sheet_surface_\ubbf8\uacb0",
|
||||
"community": 0,
|
||||
"community_name": "UI Templates \u2014 Localization & Components",
|
||||
"community": 7,
|
||||
"community_name": "2026-08-29 \uc644\ub8cc \ubc18\uc601",
|
||||
"norm_label": "\u1106\u1175\u1100\u1167\u11af"
|
||||
},
|
||||
{
|
||||
@@ -2906,8 +2919,8 @@
|
||||
"source_location": "L1",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_multi_environment_safety",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 173,
|
||||
"community_name": "multi_environment_safety.md",
|
||||
"norm_label": "multi_environment_safety.md"
|
||||
},
|
||||
{
|
||||
@@ -2917,8 +2930,8 @@
|
||||
"source_location": "L9",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_multi_environment_safety_\ub2e4\uc911_\ud658\uacbd_\uc800\uc7a5\uc18c_\uacf5\uc6a9_db_\uc548\uc804",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 180,
|
||||
"community_name": "\ub2e4\uc911 \ud658\uacbd \uc800\uc7a5\uc18c\u00b7\uacf5\uc6a9 DB \uc548\uc804",
|
||||
"norm_label": "\u1103\u1161\u110c\u116e\u11bc \u1112\u116a\u11ab\u1100\u1167\u11bc \u110c\u1165\u110c\u1161\u11bc\u1109\u1169\u00b7\u1100\u1169\u11bc\u110b\u116d\u11bc db \u110b\u1161\u11ab\u110c\u1165\u11ab"
|
||||
},
|
||||
{
|
||||
@@ -2928,8 +2941,8 @@
|
||||
"source_location": "L11",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_multi_environment_safety_\ud655\uc778\ub41c_\uc704\ud5d8",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 180,
|
||||
"community_name": "\ub2e4\uc911 \ud658\uacbd \uc800\uc7a5\uc18c\u00b7\uacf5\uc6a9 DB \uc548\uc804",
|
||||
"norm_label": "\u1112\u116a\u11a8\u110b\u1175\u11ab\u1103\u116c\u11ab \u110b\u1171\u1112\u1165\u11b7"
|
||||
},
|
||||
{
|
||||
@@ -2939,8 +2952,8 @@
|
||||
"source_location": "L18",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_multi_environment_safety_\uc6b4\uc601_\uaddc\uce59",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 180,
|
||||
"community_name": "\ub2e4\uc911 \ud658\uacbd \uc800\uc7a5\uc18c\u00b7\uacf5\uc6a9 DB \uc548\uc804",
|
||||
"norm_label": "\u110b\u116e\u11ab\u110b\u1167\u11bc \u1100\u1172\u110e\u1175\u11a8"
|
||||
},
|
||||
{
|
||||
@@ -2950,30 +2963,41 @@
|
||||
"source_location": "L29",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_multi_environment_safety_\uc5ec\uc12f_\uc6cc\ud06c\ud2b8\ub9ac_\uc6b4\uc601_\ud655\uc815\ud310",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 180,
|
||||
"community_name": "\ub2e4\uc911 \ud658\uacbd \uc800\uc7a5\uc18c\u00b7\uacf5\uc6a9 DB \uc548\uc804",
|
||||
"norm_label": "\u110b\u1167\u1109\u1165\u11ba \u110b\u116f\u110f\u1173\u1110\u1173\u1105\u1175 \u110b\u116e\u11ab\u110b\u1167\u11bc \u1112\u116a\u11a8\u110c\u1165\u11bc\u1111\u1161\u11ab"
|
||||
},
|
||||
{
|
||||
"label": "Git \ud55c \ubc14\ud034",
|
||||
"file_type": "document",
|
||||
"source_file": "concepts/multi_environment_safety.md",
|
||||
"source_location": "L43",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_multi_environment_safety_git_\ud55c_\ubc14\ud034",
|
||||
"community": 180,
|
||||
"community_name": "\ub2e4\uc911 \ud658\uacbd \uc800\uc7a5\uc18c\u00b7\uacf5\uc6a9 DB \uc548\uc804",
|
||||
"norm_label": "git \u1112\u1161\u11ab \u1107\u1161\u110f\u1171"
|
||||
},
|
||||
{
|
||||
"label": "\uc644\ub8cc \ud310\uc815",
|
||||
"file_type": "document",
|
||||
"source_file": "concepts/multi_environment_safety.md",
|
||||
"source_location": "L43",
|
||||
"source_location": "L54",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_multi_environment_safety_\uc644\ub8cc_\ud310\uc815",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 180,
|
||||
"community_name": "\ub2e4\uc911 \ud658\uacbd \uc800\uc7a5\uc18c\u00b7\uacf5\uc6a9 DB \uc548\uc804",
|
||||
"norm_label": "\u110b\u116a\u11ab\u1105\u116d \u1111\u1161\u11ab\u110c\u1165\u11bc"
|
||||
},
|
||||
{
|
||||
"label": "\uc7ac\uacc4\uc0b0 \uc601\ud5a5",
|
||||
"file_type": "document",
|
||||
"source_file": "concepts/multi_environment_safety.md",
|
||||
"source_location": "L49",
|
||||
"source_location": "L60",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_multi_environment_safety_\uc7ac\uacc4\uc0b0_\uc601\ud5a5",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 180,
|
||||
"community_name": "\ub2e4\uc911 \ud658\uacbd \uc800\uc7a5\uc18c\u00b7\uacf5\uc6a9 DB \uc548\uc804",
|
||||
"norm_label": "\u110c\u1162\u1100\u1168\u1109\u1161\u11ab \u110b\u1167\u11bc\u1112\u1163\u11bc"
|
||||
},
|
||||
{
|
||||
@@ -3192,8 +3216,8 @@
|
||||
"source_location": "L1",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_storage_paths",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 175,
|
||||
"community_name": "DB \uc2a4\ud0a4\ub9c8 \uac1c\uc694",
|
||||
"norm_label": "storage_paths.md"
|
||||
},
|
||||
{
|
||||
@@ -3203,8 +3227,8 @@
|
||||
"source_location": "L9",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_storage_paths_\uc800\uc7a5_\uacbd\ub85c_\uaddc\uce59_workflow_based_folder_structure",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 181,
|
||||
"community_name": "\uc800\uc7a5 \uacbd\ub85c \uaddc\uce59 (Workflow-based Folder Structure)",
|
||||
"norm_label": "\u110c\u1165\u110c\u1161\u11bc \u1100\u1167\u11bc\u1105\u1169 \u1100\u1172\u110e\u1175\u11a8 (workflow-based folder structure)"
|
||||
},
|
||||
{
|
||||
@@ -3214,8 +3238,8 @@
|
||||
"source_location": "L11",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_storage_paths_\uacbd\ub85c_\ud328\ud134",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 181,
|
||||
"community_name": "\uc800\uc7a5 \uacbd\ub85c \uaddc\uce59 (Workflow-based Folder Structure)",
|
||||
"norm_label": "\u1100\u1167\u11bc\u1105\u1169 \u1111\u1162\u1110\u1165\u11ab"
|
||||
},
|
||||
{
|
||||
@@ -3225,8 +3249,8 @@
|
||||
"source_location": "L29",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_storage_paths_\uc6d0\uce59_backend_md_3\uc808",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 181,
|
||||
"community_name": "\uc800\uc7a5 \uacbd\ub85c \uaddc\uce59 (Workflow-based Folder Structure)",
|
||||
"norm_label": "\u110b\u116f\u11ab\u110e\u1175\u11a8 (backend.md 3\u110c\u1165\u11af)"
|
||||
},
|
||||
{
|
||||
@@ -3236,8 +3260,8 @@
|
||||
"source_location": "L36",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_storage_paths_db_\uceec\ub7fc_\uc2e4\uc81c_\uacbd\ub85c_\ub9e4\ud551",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 181,
|
||||
"community_name": "\uc800\uc7a5 \uacbd\ub85c \uaddc\uce59 (Workflow-based Folder Structure)",
|
||||
"norm_label": "db \u110f\u1165\u11af\u1105\u1165\u11b7 \u2194 \u1109\u1175\u11af\u110c\u1166 \u1100\u1167\u11bc\u1105\u1169 \u1106\u1162\u1111\u1175\u11bc"
|
||||
},
|
||||
{
|
||||
@@ -3247,8 +3271,8 @@
|
||||
"source_location": "L39",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_storage_paths_\ud30c\uc77c\uba85_\uaddc\uce59_structure_md_1\uc808",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 181,
|
||||
"community_name": "\uc800\uc7a5 \uacbd\ub85c \uaddc\uce59 (Workflow-based Folder Structure)",
|
||||
"norm_label": "\u1111\u1161\u110b\u1175\u11af\u1106\u1167\u11bc \u1100\u1172\u110e\u1175\u11a8 (structure.md 1\u110c\u1165\u11af)"
|
||||
},
|
||||
{
|
||||
@@ -3258,8 +3282,8 @@
|
||||
"source_location": "L44",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_storage_paths_\ucf54\ub4dc_\uac10\uc0ac_\uc8fc\uc758\uc0ac\ud56d",
|
||||
"community": 1,
|
||||
"community_name": "\uc778\uc99d / RBAC",
|
||||
"community": 181,
|
||||
"community_name": "\uc800\uc7a5 \uacbd\ub85c \uaddc\uce59 (Workflow-based Folder Structure)",
|
||||
"norm_label": "\u110f\u1169\u1103\u1173 \u1100\u1161\u11b7\u1109\u1161 \u110c\u116e\u110b\u1174\u1109\u1161\u1112\u1161\u11bc"
|
||||
},
|
||||
{
|
||||
@@ -3467,8 +3491,8 @@
|
||||
"source_location": "L1",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_workflow_state",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 173,
|
||||
"community_name": "multi_environment_safety.md",
|
||||
"norm_label": "workflow_state.md"
|
||||
},
|
||||
{
|
||||
@@ -3478,8 +3502,8 @@
|
||||
"source_location": "L9",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_workflow_state_workflow_\uc0c1\ud0dc_\uad00\ub9ac",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 174,
|
||||
"community_name": "Workflow \uc0c1\ud0dc \uad00\ub9ac",
|
||||
"norm_label": "workflow \u1109\u1161\u11bc\u1110\u1162 \u1100\u116a\u11ab\u1105\u1175"
|
||||
},
|
||||
{
|
||||
@@ -3489,8 +3513,8 @@
|
||||
"source_location": "L13",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_workflow_state_ssot_project_workflow_stages_\ud14c\uc774\ube14_\uc2e4_db_\ud655\uc778",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 174,
|
||||
"community_name": "Workflow \uc0c1\ud0dc \uad00\ub9ac",
|
||||
"norm_label": "ssot: `project_workflow_stages` \u1110\u1166\u110b\u1175\u1107\u1173\u11af (\u1109\u1175\u11af db \u1112\u116a\u11a8\u110b\u1175\u11ab)"
|
||||
},
|
||||
{
|
||||
@@ -3500,8 +3524,8 @@
|
||||
"source_location": "L27",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_workflow_state_r1_\uc6cc\ud06c\ud50c\ub85c\uc6b0_\ub2e8\uacc4_\uc7ac\ud3b8_2026_08_08_\ubc18\uc601",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 174,
|
||||
"community_name": "Workflow \uc0c1\ud0dc \uad00\ub9ac",
|
||||
"norm_label": "r1 \u110b\u116f\u110f\u1173\u1111\u1173\u11af\u1105\u1169\u110b\u116e \u1103\u1161\u11ab\u1100\u1168 \u110c\u1162\u1111\u1167\u11ab (2026-08-08 \u1107\u1161\u11ab\u110b\u1167\u11bc)"
|
||||
},
|
||||
{
|
||||
@@ -3511,8 +3535,8 @@
|
||||
"source_location": "L35",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_workflow_state_\ubc31\uadf8\ub77c\uc6b4\ub4dc_\uc790\ub3d9_\uacc4\uc0b0_\uccb4\uc778_\ubc0f_\uc0ac\uc6a9\uc790_\uc124\uc815_\uc774\uc6d4",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 174,
|
||||
"community_name": "Workflow \uc0c1\ud0dc \uad00\ub9ac",
|
||||
"norm_label": "\u1107\u1162\u11a8\u1100\u1173\u1105\u1161\u110b\u116e\u11ab\u1103\u1173 \u110c\u1161\u1103\u1169\u11bc \u1100\u1168\u1109\u1161\u11ab \u110e\u1166\u110b\u1175\u11ab \u1106\u1175\u11be \u1109\u1161\u110b\u116d\u11bc\u110c\u1161 \u1109\u1165\u11af\u110c\u1165\u11bc \u110b\u1175\u110b\u116f\u11af"
|
||||
},
|
||||
{
|
||||
@@ -3522,8 +3546,8 @@
|
||||
"source_location": "L40",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_workflow_state_\uacf5\ud1b5_\uc720\ud2f8_common_util_common_util_workflow_state_py",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 174,
|
||||
"community_name": "Workflow \uc0c1\ud0dc \uad00\ub9ac",
|
||||
"norm_label": "\u1100\u1169\u11bc\u1110\u1169\u11bc \u110b\u1172\u1110\u1175\u11af `common_util/common_util_workflow_state.py`"
|
||||
},
|
||||
{
|
||||
@@ -3533,8 +3557,8 @@
|
||||
"source_location": "L51",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_workflow_state_\ubb34\ud6a8\ud654\uc758_\uc2e4\uc81c_\ubc94\uc704",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 174,
|
||||
"community_name": "Workflow \uc0c1\ud0dc \uad00\ub9ac",
|
||||
"norm_label": "\u1106\u116e\u1112\u116d\u1112\u116a\u110b\u1174 \u1109\u1175\u11af\u110c\u1166 \u1107\u1165\u11b7\u110b\u1171"
|
||||
},
|
||||
{
|
||||
@@ -3544,8 +3568,8 @@
|
||||
"source_location": "L60",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_workflow_state_\uc870\ud68c_api",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 174,
|
||||
"community_name": "Workflow \uc0c1\ud0dc \uad00\ub9ac",
|
||||
"norm_label": "\u110c\u1169\u1112\u116c api"
|
||||
},
|
||||
{
|
||||
@@ -3555,8 +3579,8 @@
|
||||
"source_location": "L63",
|
||||
"_origin": "ast",
|
||||
"id": "concepts_workflow_state_\ud504\ub860\ud2b8\uc5d4\ub4dc_\uac8c\uc774\ud305_\ubc0f_\uc2a4\ud15d\ubc14_\uc5f0\ub3d9",
|
||||
"community": 3,
|
||||
"community_name": "2026-09-04 \uc644\ub8cc \ud56d\ubaa9",
|
||||
"community": 174,
|
||||
"community_name": "Workflow \uc0c1\ud0dc \uad00\ub9ac",
|
||||
"norm_label": "\u1111\u1173\u1105\u1169\u11ab\u1110\u1173\u110b\u1166\u11ab\u1103\u1173 \u1100\u1166\u110b\u1175\u1110\u1175\u11bc \u1106\u1175\u11be \u1109\u1173\u1110\u1166\u11b8\u1107\u1161 \u110b\u1167\u11ab\u1103\u1169\u11bc"
|
||||
},
|
||||
{
|
||||
@@ -3739,7 +3763,7 @@
|
||||
"label": "\ud604\uc7ac \uc8fc\uc758\uc0ac\ud56d",
|
||||
"file_type": "document",
|
||||
"source_file": "index.md",
|
||||
"source_location": "L75",
|
||||
"source_location": "L76",
|
||||
"_origin": "ast",
|
||||
"id": "index_\ud604\uc7ac_\uc8fc\uc758\uc0ac\ud56d",
|
||||
"community": 33,
|
||||
@@ -13695,6 +13719,72 @@
|
||||
"target": "concepts_b07_external_webcad_demos_\uc2e4\ud589\uacfc_\uc885\ub8cc",
|
||||
"confidence_score": 1.0
|
||||
},
|
||||
{
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "concepts/browser_verification_operations.md",
|
||||
"source_location": "L9",
|
||||
"weight": 1.0,
|
||||
"_origin": "ast",
|
||||
"source": "concepts_browser_verification_operations",
|
||||
"target": "concepts_browser_verification_operations_\ud654\uba74_\uac80\uc99d_\ube0c\ub77c\uc6b0\uc800_\uc6b4\uc6a9",
|
||||
"confidence_score": 1.0
|
||||
},
|
||||
{
|
||||
"relation": "references",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "concepts/browser_verification_operations.md",
|
||||
"source_location": "L4",
|
||||
"weight": 1.0,
|
||||
"_origin": "ast",
|
||||
"source": "concepts_browser_verification_operations",
|
||||
"target": "concepts_design_data_lifecycle",
|
||||
"confidence_score": 1.0
|
||||
},
|
||||
{
|
||||
"relation": "references",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "concepts/browser_verification_operations.md",
|
||||
"source_location": "L4",
|
||||
"weight": 1.0,
|
||||
"_origin": "ast",
|
||||
"source": "concepts_browser_verification_operations",
|
||||
"target": "concepts_multi_environment_safety",
|
||||
"confidence_score": 1.0
|
||||
},
|
||||
{
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "concepts/browser_verification_operations.md",
|
||||
"source_location": "L17",
|
||||
"weight": 1.0,
|
||||
"_origin": "ast",
|
||||
"source": "concepts_browser_verification_operations_\ud654\uba74_\uac80\uc99d_\ube0c\ub77c\uc6b0\uc800_\uc6b4\uc6a9",
|
||||
"target": "concepts_browser_verification_operations_\uacf5\uc6a9_\ube0c\ub77c\uc6b0\uc800_\uc2dc\ub3d9\uacfc_\uc870\uc791",
|
||||
"confidence_score": 1.0
|
||||
},
|
||||
{
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "concepts/browser_verification_operations.md",
|
||||
"source_location": "L11",
|
||||
"weight": 1.0,
|
||||
"_origin": "ast",
|
||||
"source": "concepts_browser_verification_operations_\ud654\uba74_\uac80\uc99d_\ube0c\ub77c\uc6b0\uc800_\uc6b4\uc6a9",
|
||||
"target": "concepts_browser_verification_operations_\uae30\ubcf8_\uc120\ud0dd",
|
||||
"confidence_score": 1.0
|
||||
},
|
||||
{
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "concepts/browser_verification_operations.md",
|
||||
"source_location": "L29",
|
||||
"weight": 1.0,
|
||||
"_origin": "ast",
|
||||
"source": "concepts_browser_verification_operations_\ud654\uba74_\uac80\uc99d_\ube0c\ub77c\uc6b0\uc800_\uc6b4\uc6a9",
|
||||
"target": "concepts_browser_verification_operations_\ubcc0\uacbd_\uc885\ub958\ubcc4_\ubc18\uc601",
|
||||
"confidence_score": 1.0
|
||||
},
|
||||
{
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
@@ -15752,6 +15842,17 @@
|
||||
"target": "concepts_workflow_state",
|
||||
"confidence_score": 1.0
|
||||
},
|
||||
{
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "concepts/multi_environment_safety.md",
|
||||
"source_location": "L43",
|
||||
"weight": 1.0,
|
||||
"_origin": "ast",
|
||||
"source": "concepts_multi_environment_safety_\ub2e4\uc911_\ud658\uacbd_\uc800\uc7a5\uc18c_\uacf5\uc6a9_db_\uc548\uc804",
|
||||
"target": "concepts_multi_environment_safety_git_\ud55c_\ubc14\ud034",
|
||||
"confidence_score": 1.0
|
||||
},
|
||||
{
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
@@ -15767,7 +15868,7 @@
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "concepts/multi_environment_safety.md",
|
||||
"source_location": "L43",
|
||||
"source_location": "L54",
|
||||
"weight": 1.0,
|
||||
"_origin": "ast",
|
||||
"source": "concepts_multi_environment_safety_\ub2e4\uc911_\ud658\uacbd_\uc800\uc7a5\uc18c_\uacf5\uc6a9_db_\uc548\uc804",
|
||||
@@ -15789,7 +15890,7 @@
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "concepts/multi_environment_safety.md",
|
||||
"source_location": "L49",
|
||||
"source_location": "L60",
|
||||
"weight": 1.0,
|
||||
"_origin": "ast",
|
||||
"source": "concepts_multi_environment_safety_\ub2e4\uc911_\ud658\uacbd_\uc800\uc7a5\uc18c_\uacf5\uc6a9_db_\uc548\uc804",
|
||||
@@ -16493,7 +16594,7 @@
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "index.md",
|
||||
"source_location": "L75",
|
||||
"source_location": "L76",
|
||||
"weight": 1.0,
|
||||
"_origin": "ast",
|
||||
"source": "index_wiki_index",
|
||||
@@ -22605,28 +22706,6 @@
|
||||
"target": "pages_b07_designdetail_b07_standard_drawings_2026_09_b07_\uad6c\uc870\ubb3c_\ud45c\uc900\ub3c4_\uc870\uc0ac_\ud569\uc758\uc640_\ud604\uc7ac_\ud1b5\ub85c",
|
||||
"confidence_score": 1.0
|
||||
},
|
||||
{
|
||||
"relation": "references",
|
||||
"confidence": "EXTRACTED",
|
||||
"confidence_score": 1.0,
|
||||
"weight": 1.0,
|
||||
"source_file": "pages/B07_DesignDetail/B07_standard_drawings_2026_09.md",
|
||||
"source_location": "wikilink",
|
||||
"_origin": "curated",
|
||||
"source": "pages_b07_designdetail_b07_standard_drawings_2026_09_b07_\uad6c\uc870\ubb3c_\ud45c\uc900\ub3c4_\uc870\uc0ac_\ud569\uc758\uc640_\ud604\uc7ac_\ud1b5\ub85c",
|
||||
"target": "pages_b08_quantity_b08_overview_2026_09_b08_quantity_2026_09_\uc218\ub7c9\uc0b0\ucd9c"
|
||||
},
|
||||
{
|
||||
"relation": "references",
|
||||
"confidence": "EXTRACTED",
|
||||
"confidence_score": 1.0,
|
||||
"weight": 1.0,
|
||||
"source_file": "pages/B07_DesignDetail/B07_standard_drawings_2026_09.md",
|
||||
"source_location": "wikilink",
|
||||
"_origin": "curated",
|
||||
"source": "pages_b07_designdetail_b07_standard_drawings_2026_09_b07_\uad6c\uc870\ubb3c_\ud45c\uc900\ub3c4_\uc870\uc0ac_\ud569\uc758\uc640_\ud604\uc7ac_\ud1b5\ub85c",
|
||||
"target": "pages_b09_estimation_b09_overview_2026_09_b09_estimation_2026_09_\uc6d0\uac00\uacc4\uc0b0"
|
||||
},
|
||||
{
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
@@ -22682,6 +22761,28 @@
|
||||
"target": "pages_b07_designdetail_b07_standard_drawings_2026_09_\uc870\uc0ac\ub85c_\ud655\uc778\ub41c_\uc124\uacc4_\uc6d0\uce59",
|
||||
"confidence_score": 1.0
|
||||
},
|
||||
{
|
||||
"relation": "references",
|
||||
"confidence": "EXTRACTED",
|
||||
"confidence_score": 1.0,
|
||||
"weight": 1.0,
|
||||
"source_file": "pages/B07_DesignDetail/B07_standard_drawings_2026_09.md",
|
||||
"source_location": "wikilink",
|
||||
"_origin": "curated",
|
||||
"source": "pages_b07_designdetail_b07_standard_drawings_2026_09_b07_\uad6c\uc870\ubb3c_\ud45c\uc900\ub3c4_\uc870\uc0ac_\ud569\uc758\uc640_\ud604\uc7ac_\ud1b5\ub85c",
|
||||
"target": "pages_b08_quantity_b08_overview_2026_09_b08_quantity_2026_09_\uc218\ub7c9\uc0b0\ucd9c"
|
||||
},
|
||||
{
|
||||
"relation": "references",
|
||||
"confidence": "EXTRACTED",
|
||||
"confidence_score": 1.0,
|
||||
"weight": 1.0,
|
||||
"source_file": "pages/B07_DesignDetail/B07_standard_drawings_2026_09.md",
|
||||
"source_location": "wikilink",
|
||||
"_origin": "curated",
|
||||
"source": "pages_b07_designdetail_b07_standard_drawings_2026_09_b07_\uad6c\uc870\ubb3c_\ud45c\uc900\ub3c4_\uc870\uc0ac_\ud569\uc758\uc640_\ud604\uc7ac_\ud1b5\ub85c",
|
||||
"target": "pages_b09_estimation_b09_overview_2026_09_b09_estimation_2026_09_\uc6d0\uac00\uacc4\uc0b0"
|
||||
},
|
||||
{
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
@@ -23927,48 +24028,6 @@
|
||||
}
|
||||
],
|
||||
"hyperedges": [
|
||||
{
|
||||
"id": "workflow_b03_b08_integration",
|
||||
"label": "B03-B08 Workflow Data Flow",
|
||||
"nodes": [
|
||||
"b03_fileinput_route_snapshot_crs",
|
||||
"b04_preprocess_drainage_compass_crs",
|
||||
"b05_profile_frontend",
|
||||
"b06_section_cross_design_ui_2026_09",
|
||||
"b08_designdetail_frontend"
|
||||
],
|
||||
"relation": "participate_in",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.9,
|
||||
"source_file": "index.md"
|
||||
},
|
||||
{
|
||||
"id": "mass_haul_shared_logic",
|
||||
"label": "Shared Mass Haul Calculation and UI",
|
||||
"nodes": [
|
||||
"b05_profile_masshaul_structure_2026_09",
|
||||
"b06_section_masshaul_culvert_2026_09",
|
||||
"b08_designdetail_cad_delivery_2026_09"
|
||||
],
|
||||
"relation": "participate_in",
|
||||
"confidence": "EXTRACTED",
|
||||
"confidence_score": 0.85,
|
||||
"source_file": "pages/B05_Profile/B05_masshaul_structure_2026_09.md"
|
||||
},
|
||||
{
|
||||
"id": "cad_delivery_system",
|
||||
"label": "CAD Delivery and Usability Framework",
|
||||
"nodes": [
|
||||
"b08_designdetail_cad_interaction",
|
||||
"b08_designdetail_cad_title_block",
|
||||
"b08_designdetail_cad_usability_2026_09_01",
|
||||
"b08_designdetail_cad_delivery_2026_09"
|
||||
],
|
||||
"relation": "form",
|
||||
"confidence": "EXTRACTED",
|
||||
"confidence_score": 0.95,
|
||||
"source_file": "pages/B08_DesignDetail/B08_cad_delivery_2026_09.md"
|
||||
},
|
||||
{
|
||||
"id": "las_free_workflow_chain",
|
||||
"label": "LAS-Free Analysis Workflow",
|
||||
@@ -24049,5 +24108,5 @@
|
||||
"confidence_score": 1.0
|
||||
}
|
||||
],
|
||||
"built_at_commit": "e49e4d8526c0c6fc08d488546cda8445dbea1a31"
|
||||
"built_at_commit": "d6e45bb431c88702c49e6cb7ea6e0414346064d7"
|
||||
}
|
||||
@@ -175,8 +175,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"index.md": {
|
||||
"mtime": 1789132135.2547836,
|
||||
"ast_hash": "07f8a8513e83d93de0bf992e1fc4e501",
|
||||
"mtime": 1789177295.6797397,
|
||||
"ast_hash": "b918b75614e641023689764d2dab781b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"ingest/index.md": {
|
||||
@@ -1025,8 +1025,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"concepts/multi_environment_safety.md": {
|
||||
"mtime": 1789132131.6764865,
|
||||
"ast_hash": "1667aa4c657a4f529de42bcaac3fcd44",
|
||||
"mtime": 1789177284.6051137,
|
||||
"ast_hash": "bd15bb9032668bee166d16eaf600dc14",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"concepts/quantity_cost_contract.md": {
|
||||
@@ -1073,5 +1073,10 @@
|
||||
"mtime": 1789128567.000224,
|
||||
"ast_hash": "0b98a093e67d58c91f57ab12971c3da0",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"concepts/browser_verification_operations.md": {
|
||||
"mtime": 1789177288.405969,
|
||||
"ast_hash": "23611cfc9c4dd173829d51130ba779b1",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: index
|
||||
status: stable
|
||||
last_updated: 2026-09-11
|
||||
last_updated: 2026-09-12
|
||||
---
|
||||
|
||||
# Wiki Index
|
||||
@@ -65,6 +65,7 @@ last_updated: 2026-09-11
|
||||
- 표준도·입력 미결: [[standard_drawing_cost_inputs]] — 자동 계산·사용자 입력·기준자료·미확보 경계.
|
||||
- 2026-09-09 미결 근거: [[standard_quantity_open_2026-09-09]] — 표준도 제원·수량·원가 후보와 확정 금지 경계.
|
||||
- 다중 환경 안전: [[multi_environment_safety]] — 여섯 워크트리·링크·Git 역할과 공용 DB 사고 방지 규칙.
|
||||
- 화면 검증 브라우저: [[browser_verification_operations]] — Orca 기본·공용 브라우저 예외 운용과 변경별 갱신 절차.
|
||||
- 인증·상태: [[auth_rbac]], [[workflow_state]], [[temp_upload]].
|
||||
- 데이터·경로: [[db_schema/overview]], [[storage_paths]], [[crs_metadata]].
|
||||
- API·스키마·유틸: [[api_common]], [[schema_common]], [[common_util]].
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: ingest-register
|
||||
status: stable
|
||||
related_pages: ["[[ingest/index]]", "[[completed_2026-09-01]]", "[[completed_2026-09-09]]", "[[design_data_lifecycle]]", "[[multi_environment_safety]]"]
|
||||
last_updated: 2026-09-11
|
||||
last_updated: 2026-09-12
|
||||
---
|
||||
|
||||
# 2026-09 검증 인제스트
|
||||
@@ -16,3 +16,5 @@ last_updated: 2026-09-11
|
||||
| `docs/raw/verification/2026-09-09g_계획서_0_3_4장_완료근거_이관.md` | `ingested` | [[completed_2026-09-09]], [[B07_DesignDetail/B07_standard_drawings_2026_09]] |
|
||||
| `docs/raw/verification/2026-09-10_계획서_e_f_g_이관_교차검증.md` | `verification` | [[completed_2026-09-09]], [[design_data_lifecycle]] |
|
||||
| `docs/raw/verification/2026-09-11_창환경_링크와_깃운영_조사.md` | `ingested` | [[multi_environment_safety]] |
|
||||
| `docs/raw/verification/2026-09-12_깃_합류점_dev_전환.md` | `ingested` | [[multi_environment_safety]] |
|
||||
| `docs/raw/verification/2026-09-12_공용_브라우저_운용.md` | `ingested` | [[browser_verification_operations]] |
|
||||
|
||||
+7
-1
@@ -1,9 +1,15 @@
|
||||
---
|
||||
type: log
|
||||
status: stable
|
||||
last_updated: 2026-09-11
|
||||
last_updated: 2026-09-12
|
||||
---
|
||||
|
||||
## [2026-09-12] ingest+lint+graphify | Git 합류점·화면 검증 브라우저 운용 반영
|
||||
- `main` 정본·`dev` 합류점·환경 브랜치 6개의 받기→작업→밀기와 사용자 지시형 `-Converge` 절차를 [[multi_environment_safety]]에 반영했다.
|
||||
- Orca 내장 브라우저 기본, 공용 브라우저 예외 사용, 명령 큐·캐시·재시작·수치 판정 규칙을 [[browser_verification_operations]]에 정리했다.
|
||||
- 두 검증 원본을 [[ingest/verification_2026_09]]에 등록했다.
|
||||
- 비로그 위키의 100줄·frontmatter·status·page_id 검사와 신규 링크 검사를 통과했다. Graphify는 1,176노드·1,003관계·196커뮤니티로 갱신했고 새 두 개념의 질의 연결을 확인했다.
|
||||
|
||||
## [2026-09-11] ingest+lint | 여섯 워크트리·링크·Git 운영 완료 반영
|
||||
- 상시계획서 10번 원문 전체를 `docs/raw/plans/2026-09-11_plan_창환경_여섯워크트리_링크_깃운영.md`로 이관했다.
|
||||
- 사용자 요청에 따라 소스코드 2차 검증과 별도 검증보고서는 생략하고, 기존 운영 실측 원문을 [[multi_environment_safety]]에 반영했다.
|
||||
|
||||
@@ -48,15 +48,16 @@ from B05_Profile.B05_Profile_Router import router as b05_route_router
|
||||
from B05_Profile.B05_Profile_Router_Corridor import router as b05_corridor_router
|
||||
from B05_Profile.B05_Profile_Router_Lifecycle import router as b05_route_lifecycle_router
|
||||
from B05_Profile.B05_Profile_Router_Replan import router as b05_route_replan_router
|
||||
from B05_Profile.B05_Profile_Router_Terrain import router as b05_route_terrain_router
|
||||
from B05_Profile.B05_Profile_Structures_Router import router as b05_structures_router
|
||||
from B06_Section.B06_Section_Router import router as b06_section_router
|
||||
from B06_Section.B06_Section_Router_Stations import router as b06_section_stations_router
|
||||
from B06_Section.B06_Section_Router_Confirm import (
|
||||
router as b06_section_confirm_router,
|
||||
)
|
||||
from B06_Section.B06_Section_Router_HaulPlan import (
|
||||
router as b06_section_haul_plan_router,
|
||||
)
|
||||
from B06_Section.B06_Section_Router_Stations import router as b06_section_stations_router
|
||||
from B07_DesignDetail.B07_DesignDetail_Router import router as b07_design_router
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Frame import router as b07_frame_router
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Standard import router as b07_standard_router
|
||||
@@ -537,6 +538,7 @@ app.include_router(b05_route_router, dependencies=protected_with_company)
|
||||
app.include_router(b05_route_lifecycle_router, dependencies=protected_with_company)
|
||||
app.include_router(b05_corridor_router, dependencies=protected_with_company)
|
||||
app.include_router(b05_route_replan_router, dependencies=protected_with_company)
|
||||
app.include_router(b05_route_terrain_router, dependencies=protected_with_company)
|
||||
app.include_router(b05_structures_router, dependencies=protected_with_company)
|
||||
app.include_router(b06_section_router, dependencies=protected_with_company)
|
||||
app.include_router(b06_section_stations_router, dependencies=protected_with_company)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""계획노선 곡선 **하한**(반지름·곡선 길이) 해석 시험.
|
||||
|
||||
기본값(`legal_plan_radius_min_m`)과 **못 넘는 하한**(`plan_radius_limit_m`)은 다른 값이다
|
||||
(2026-09-12 사용자 확정). 둘을 한 값으로 묶으면 작업임도의 하한 0 이 곧 반지름 0 이 되어
|
||||
곡선이 아예 안 그려지므로, 갈라져 있다는 것 자체를 시험으로 못박는다.
|
||||
"""
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Grade import (
|
||||
legal_plan_radius_min_m,
|
||||
plan_curve_length_limit_m,
|
||||
plan_radius_limit_m,
|
||||
resolve_design_speed,
|
||||
)
|
||||
|
||||
|
||||
def _default(grade_class: str, terrain: str) -> float:
|
||||
return legal_plan_radius_min_m(resolve_design_speed(grade_class, None), terrain)
|
||||
|
||||
|
||||
def test_작업임도는_하한이_없다():
|
||||
"""별표2에 작업임도 곡선반지름 규정이 없어 하한을 0(제한 없음)으로 열어 둔다."""
|
||||
assert plan_radius_limit_m("work", None, "normal") == 0.0
|
||||
assert plan_radius_limit_m("work", None, "special") == 0.0
|
||||
|
||||
|
||||
def test_작업임도도_기본_반지름은_그대로다():
|
||||
"""하한이 0이어도 **곡선을 만들 때 쓰는 기본값**은 살아 있어야 한다."""
|
||||
assert _default("work", "normal") > 0
|
||||
|
||||
|
||||
def test_간선_산불진화는_법정표가_곧_하한이다():
|
||||
for grade_class in ("main", "trunk", "fire"):
|
||||
for terrain in ("normal", "special"):
|
||||
assert plan_radius_limit_m(grade_class, None, terrain) == _default(grade_class, terrain)
|
||||
|
||||
|
||||
def test_모르는_임도종류는_법정표로_떨어진다():
|
||||
"""칸이 없는 값이 와도 막지 않고 법정표를 하한으로 쓴다(가장 보수적인 쪽)."""
|
||||
assert plan_radius_limit_m("알 수 없는 종류", None, "normal") == _default("work", "normal")
|
||||
|
||||
|
||||
def test_곡선길이_하한은_아직_전부_0이다():
|
||||
"""법령·교본에 평면 곡선 길이 하한 값이 없다 — 자리만 열어 둔 칸이다."""
|
||||
for grade_class in ("main", "trunk", "fire", "work", "branch", "없는종류"):
|
||||
assert plan_curve_length_limit_m(grade_class) == 0.0
|
||||
Reference in New Issue
Block a user