Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -73,13 +73,13 @@ from B06_Section.B06_Section_Schema import (
|
||||
SectionRegenerateRequest,
|
||||
SectionSummaryResponse,
|
||||
)
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import conversion_factors_for
|
||||
from common_util.common_util_auth import verify_session
|
||||
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_workflow_state import get_workflow_state
|
||||
from config.config_db import get_db_pool, run_with_connection
|
||||
from config.config_system import (
|
||||
EARTHWORK_CONVERSION_FACTORS,
|
||||
EARTHWORK_HAUL_EQUIPMENT_LIMITS_M,
|
||||
FOREST_ROAD_MIN_WIDTH_M,
|
||||
NATURAL_SPOIL_MIN_GROUND_SLOPE,
|
||||
@@ -144,7 +144,9 @@ async def get_section_context(project_id: UUID) -> SectionContextResponse | JSON
|
||||
stored_standard_cross_section=stored_standard,
|
||||
rock_boundary_default_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||
rock_boundary_step_m=STANDARD_ROCK_BOUNDARY_STEP_M,
|
||||
earthwork_conversion=EARTHWORK_CONVERSION_FACTORS,
|
||||
# ⚠ 상수를 직접 들지 않는다 — 프로젝트가 고른 계수가 있으면 화면도 그 값으로
|
||||
# 그려야 서버가 뒤에 다시 셈한 값과 갈리지 않는다(CLAUDE.md 5장).
|
||||
earthwork_conversion=await conversion_factors_for(project_id),
|
||||
haul_equipment_limits=[
|
||||
HaulEquipmentLimit(key=key, max_distance_m=limit)
|
||||
for key, limit in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M
|
||||
|
||||
@@ -25,6 +25,7 @@ from fastapi.responses import JSONResponse
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import (
|
||||
BUNDLE,
|
||||
_mass_haul_context,
|
||||
conversion_factors_for,
|
||||
haul_inputs_for,
|
||||
)
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
@@ -63,6 +64,8 @@ async def compute_haul_plan(
|
||||
# 구조물 몫(공제·잔토)을 **넘겨야** 사토가 줄고 는다 — 인자 없이 부르면 늘 `None` 이라
|
||||
# 통로만 있고 값이 안 흐른다(2026-09-09 실측으로 드러난 자리).
|
||||
haul_inputs = await haul_inputs_for(project_id)
|
||||
# 곡선이 쓰는 계수도 프로젝트가 고른 값으로 — 토적표·운반표와 같은 값이어야 한다.
|
||||
factors = await conversion_factors_for(project_id)
|
||||
try:
|
||||
output = await asyncio.to_thread(
|
||||
run_bundle_json,
|
||||
@@ -70,7 +73,7 @@ async def compute_haul_plan(
|
||||
_NPM_SCRIPT,
|
||||
{
|
||||
"haul_plan_for": result,
|
||||
"context": _mass_haul_context(haul_inputs),
|
||||
"context": _mass_haul_context(haul_inputs, factors),
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -38,6 +38,10 @@ from B06_Section.B06_Section_Repository import (
|
||||
)
|
||||
from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
from common_util.common_util_project_settings import (
|
||||
earthwork_conversion_factors,
|
||||
quantity_settings,
|
||||
)
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool, run_with_connection
|
||||
from config.config_system import (
|
||||
@@ -79,7 +83,25 @@ async def haul_inputs_for(project_id: Any) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
def _mass_haul_context(haul_inputs: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
async def conversion_factors_for(project_id: Any) -> dict[str, dict[str, float]]:
|
||||
"""이 프로젝트가 쓸 토량환산계수. 못 읽으면 정본 기본값 — 화면은 그대로 선다.
|
||||
|
||||
⚠ 곡선·운반·토적표가 **같은 계수**로 서야 한다. 그래서 상수를 직접 들지 않고 이 함수를
|
||||
거친다(고른 값은 프로젝트 설정 `conversion_factors_override` 에 산다).
|
||||
"""
|
||||
try:
|
||||
stored_path = await run_with_connection(get_project_storage_relative_path, project_id)
|
||||
root = resolve_stored_project_path(stored_path)
|
||||
except Exception:
|
||||
logger.warning("B06 프로젝트 경로를 못 찾음 — 기본 계수로 진행: project_id=%s", project_id)
|
||||
return {kind: dict(entry) for kind, entry in EARTHWORK_CONVERSION_FACTORS.items()}
|
||||
return earthwork_conversion_factors(quantity_settings(root))
|
||||
|
||||
|
||||
def _mass_haul_context(
|
||||
haul_inputs: dict[str, Any] | None = None,
|
||||
factors: dict[str, dict[str, float]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다.
|
||||
|
||||
⚠ 채집석 공제(`collected_stone_deduction_m3`)만 상수가 아니라 **B08 이 내는 값**이다.
|
||||
@@ -93,7 +115,8 @@ def _mass_haul_context(haul_inputs: dict[str, Any] | None = None) -> dict[str, A
|
||||
"""
|
||||
inputs = haul_inputs or {}
|
||||
return {
|
||||
"earthwork_conversion": EARTHWORK_CONVERSION_FACTORS,
|
||||
# 프로젝트가 고른 계수가 있으면 그것, 없으면 정본 기본값.
|
||||
"earthwork_conversion": factors or EARTHWORK_CONVERSION_FACTORS,
|
||||
"natural_spoil_min_ground_slope": NATURAL_SPOIL_MIN_GROUND_SLOPE,
|
||||
"haul_equipment_limits": [
|
||||
{"key": key, "max_distance_m": limit}
|
||||
@@ -196,7 +219,9 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int:
|
||||
_NPM_SCRIPT,
|
||||
{
|
||||
"detail": detail,
|
||||
"context": _mass_haul_context(haul_inputs),
|
||||
"context": _mass_haul_context(
|
||||
haul_inputs, earthwork_conversion_factors(quantity_settings(project_root))
|
||||
),
|
||||
},
|
||||
)
|
||||
marks.append(("Node 번들(면적·유토곡선)", time.perf_counter()))
|
||||
|
||||
@@ -35,7 +35,17 @@ import {
|
||||
type SectionPersistContext,
|
||||
} from "./B06_Section_UI_Page_Persist";
|
||||
import { maxToeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
|
||||
import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import {
|
||||
createProfileEditStore,
|
||||
readAlignmentDraft,
|
||||
type ProfileEditStore,
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { readAlignment } from "../B05_Profile/B05_Profile_UI_Profile_Data";
|
||||
import {
|
||||
adjustStation,
|
||||
buildAlignment,
|
||||
toAlignmentBase,
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Alignment";
|
||||
import {
|
||||
readStructurePick,
|
||||
writeStructurePick,
|
||||
@@ -481,6 +491,41 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
);
|
||||
// 좌측 목록이 넘겨 준 구조물을 종단 알약 레인으로 보낸다(표시 통일).
|
||||
structureMarksSink = (structures, types) => sectionView.setStructureMarks(structures, types);
|
||||
/**
|
||||
* 계획선 편집(▲/▼) — B05 와 **같은 세션 초안**에 쌓는다(2026-09-12 사용자: B06 에만
|
||||
* 버튼이 없었다). 누르면 그 자리에서 계획선·전 측점 횡단을 다시 풀고(`reconcileStale…`),
|
||||
* 영구저장은 [저장]·[확정]에서만 한다(CLAUDE.md 5장).
|
||||
*/
|
||||
let gradeStore: ProfileEditStore | null = null;
|
||||
let gradeRouteId: number | null = null;
|
||||
const gradeEditFor = (): ReturnType<
|
||||
NonNullable<Parameters<typeof sectionView.setGradeEdit>[0]>
|
||||
> => {
|
||||
const detail = sectionDetail;
|
||||
if (!detail || currentRouteId === null) return null;
|
||||
const stored = readAlignment(detail.longitudinal);
|
||||
if (!stored) return null; // 선형 저장분이 없는 옛 노선 — 편집할 기준선이 없다.
|
||||
if (!gradeStore || gradeRouteId !== currentRouteId) {
|
||||
gradeRouteId = currentRouteId;
|
||||
gradeStore = createProfileEditStore(currentRouteId, stored.edits, () => undefined);
|
||||
}
|
||||
const store = gradeStore;
|
||||
const base = toAlignmentBase(stored);
|
||||
const alignment = buildAlignment(base, store.edits());
|
||||
return {
|
||||
alignment,
|
||||
stepM: alignment.policy.edit_step_m,
|
||||
onStation: (chainageM, delta) => {
|
||||
store.replace(adjustStation(base, store.edits(), chainageM, delta));
|
||||
// 편집분은 세션 초안에 있으므로 재계산이 그것을 그대로 읽는다(강제로 한 번).
|
||||
void reconcileStaleDesigns({ force: true }).then(() =>
|
||||
sectionView.setGradeEdit(gradeEditFor),
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
sectionView.setGradeEdit(gradeEditFor);
|
||||
|
||||
// 종단 그래프 우클릭 — B05 와 같은 메뉴로 넣고 뺀다(2026-09-12 사용자: B05·B06 은 한
|
||||
// 페이지라 같은 자리에서 되어야 한다). 어느 길로 들어와도 좌측 「구조물 배치」와
|
||||
// 같은 함수를 타므로 목록·폼·알약이 함께 선다.
|
||||
|
||||
@@ -18,6 +18,11 @@ import {
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import { invalidateSectionDetail } from "./B06_Section_Section_Store";
|
||||
import { flushPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft";
|
||||
import { saveProfileAlignment } from "../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import {
|
||||
clearAlignmentDrafts,
|
||||
readAlignmentDraft,
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { flushPendingStructures } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { flushUphillOverrides } from "../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import { buildCrossPatches, type CrossPatchSources } from "./B06_Section_UI_Page_Patches";
|
||||
@@ -418,6 +423,15 @@ export function collectSectionEdits(ctx: SectionPersistContext): {
|
||||
};
|
||||
}
|
||||
|
||||
/** 계획선 편집 초안이 있으면 종단 정본에 쓰고 초안을 지운다. 없으면 아무 일도 하지 않는다. */
|
||||
async function flushAlignmentDraft(projectId: string, routeId: number | null): Promise<void> {
|
||||
if (routeId === null) return;
|
||||
const draft = readAlignmentDraft(routeId);
|
||||
if (!draft) return;
|
||||
await saveProfileAlignment(projectId, routeId, draft);
|
||||
clearAlignmentDrafts();
|
||||
}
|
||||
|
||||
/** 세션에 쌓인 조정창·구조물 조작을 정본으로 내보낸다 — [저장]·[확정] 공통 앞단. */
|
||||
async function flushPendingEdits(ctx: SectionPersistContext, projectId: string): Promise<void> {
|
||||
// ⚠ 순서는 **B05 [임시저장]과 같아야 한다**(2026-09-12 사용자: 어느 페이지에서 저장해도
|
||||
@@ -439,6 +453,13 @@ async function flushPendingEdits(ctx: SectionPersistContext, projectId: string):
|
||||
// B05에서 만지고 넘어온 구조물 조작분도 여기서 정본에 남긴다. 실패해도 횡단
|
||||
// 저장까지 막지는 않는다 — 미저장분은 세션에 남으므로 다시 시도할 수 있다
|
||||
// (2026-08-29 실측: 타입이 거절되자 sections/save가 아예 나가지 않았다).
|
||||
// 계획선 편집(▲/▼)은 세션 초안에만 있다 — B06 에서 고쳤든 B05 에서 고쳤든 여기서
|
||||
// 종단 정본으로 내보낸다(2026-09-12). 종전에는 B06 이 초안을 **읽기만** 해서, B06 에서
|
||||
// 저장하면 계획선 편집이 다음 진입 때 사라졌다.
|
||||
await flushAlignmentDraft(projectId, ctx.routeId()).catch((error) => {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`계획선 저장에 실패했습니다.${detail}`, "error");
|
||||
});
|
||||
await flushPendingStructures(projectId).catch((error) => {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`구조물 저장에 실패했습니다.${detail}`, "error");
|
||||
|
||||
@@ -45,7 +45,11 @@ import {
|
||||
import { CROSS_HEIGHT } from "./B06_Section_UI_Section_Common";
|
||||
import { STRUCTURE_LANE_HEIGHT_PX } from "../B05_Profile/B05_Profile_UI_Structures_Marks";
|
||||
import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu";
|
||||
import { attachWindowScroll, drawLongitudinalPanel } from "./B06_Section_UI_Section_View_Draw";
|
||||
import {
|
||||
attachWindowScroll,
|
||||
drawLongitudinalPanel,
|
||||
type LongitudinalPanelInput,
|
||||
} from "./B06_Section_UI_Section_View_Draw";
|
||||
import {
|
||||
type StructureInstance,
|
||||
type StructureType,
|
||||
@@ -118,6 +122,8 @@ export interface SectionViewController {
|
||||
/** 종단 그래프 우클릭으로 구조물을 넣고 빼는 길 — 없으면 메뉴가 안 뜬다
|
||||
* (2026-09-12 B05·B06 일원화). */
|
||||
setStructureEdit: (edit: SectionStructureEdit | null) => void;
|
||||
/** 계획선 편집 ▲/▼ — 그릴 때마다 불러 지금 선형·편집 함수를 받는다(null = 버튼 없음). */
|
||||
setGradeEdit: (provider: (() => LongitudinalPanelInput["grade"]) | null) => void;
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
@@ -151,6 +157,7 @@ export function createSectionView(
|
||||
let markStructures: ReadonlyArray<StructureInstance> = [];
|
||||
let markTypes: ReadonlyArray<StructureType> = [];
|
||||
let structureEdit: SectionStructureEdit | null = null;
|
||||
let gradeEditProvider: (() => LongitudinalPanelInput["grade"]) | null = null;
|
||||
let renderWidth = 0;
|
||||
let resizeTimer = 0;
|
||||
let panelResizeTimer = 0;
|
||||
@@ -508,6 +515,7 @@ export function createSectionView(
|
||||
naturalSpoilSlope: currentNaturalSpoilSlope,
|
||||
selectStation: (stationId) => selectStation(stationId, true),
|
||||
setMassBadge: (values) => massBadge.set(values),
|
||||
grade: gradeEditProvider?.() ?? null,
|
||||
});
|
||||
// 상단 패널이 sticky라 선택 카드가 그 아래로 숨는다 — 패널 높이만큼 스크롤 여백을 잡아 준다.
|
||||
syncScrollMargin();
|
||||
@@ -525,11 +533,7 @@ export function createSectionView(
|
||||
}
|
||||
}
|
||||
|
||||
attachWindowScroll(
|
||||
chartWrap,
|
||||
() => updateChartWindow,
|
||||
() => drawPanel(),
|
||||
);
|
||||
attachWindowScroll(chartWrap, () => updateChartWindow, drawPanel);
|
||||
|
||||
const draw = (): void => {
|
||||
if (!currentDetail || !Number.isFinite(renderWidth) || renderWidth <= 0) return;
|
||||
@@ -663,6 +667,10 @@ export function createSectionView(
|
||||
structureEdit = edit;
|
||||
drawPanel();
|
||||
},
|
||||
setGradeEdit(provider) {
|
||||
gradeEditProvider = provider;
|
||||
drawPanel();
|
||||
},
|
||||
setStructureMarks(structures, types) {
|
||||
markStructures = structures;
|
||||
markTypes = types;
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface LongitudinalChartInput {
|
||||
viewportWidth: number;
|
||||
/** 구조물(비정규) 측점선을 끌어 옮겼다. 안 넘기면 그 선은 못 잡는다. */
|
||||
onDragStation?: (stationId: string, toChainageM: number) => void;
|
||||
/** X축·측점 라벨을 바닥에서 이만큼(px) 올린다 — 계획고 편집 ▼ 버튼과 겹치지 않게. */
|
||||
bottomInsetPx?: number;
|
||||
}
|
||||
|
||||
export interface LongitudinalChartResult {
|
||||
@@ -114,7 +116,7 @@ export function buildLongitudinalChart(input: LongitudinalChartInput): Longitudi
|
||||
undefined,
|
||||
// 구조물(비정규) 측점선 끌어 옮기기 — B05 와 같은 조작이다(2026-09-12 일원화).
|
||||
input.onDragStation,
|
||||
0,
|
||||
input.bottomInsetPx ?? 0,
|
||||
0,
|
||||
visibleElevationRange(detail, fromM, toM) ?? undefined,
|
||||
),
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
moveMarkById,
|
||||
type SectionStructureEdit,
|
||||
} from "./B06_Section_UI_Section_View_Menu";
|
||||
import { createEditOverlay } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import type { ProfileAlignment } from "../B05_Profile/B05_Profile_UI_Profile_Alignment";
|
||||
|
||||
/** 그래프 한 벌을 세우는 데 필요한 값 — 본체가 재고 고른 것을 그대로 넘긴다. */
|
||||
export interface LongitudinalPanelInput {
|
||||
@@ -53,6 +55,14 @@ export interface LongitudinalPanelInput {
|
||||
conversion?: EarthworkConversion;
|
||||
naturalSpoilSlope?: number;
|
||||
selectStation: (stationId: string) => void;
|
||||
/** 계획선 편집 — 넘기면 종단 그래프 위에 ▲/▼ 버튼층이 선다(B05 와 같은 부품).
|
||||
* 안 넘기면 버튼이 없고 그래프만 보인다(옛 저장분처럼 선형이 없는 노선). */
|
||||
grade?: {
|
||||
alignment: ProfileAlignment;
|
||||
/** 한 번 누를 때 오르내리는 양(m) — 선형 정책값. */
|
||||
stepM: number;
|
||||
onStation: (chainageM: number, delta: number) => void;
|
||||
} | null;
|
||||
/** 좌측 상단 누가토량 배지 — 값이 없으면 null 로 지운다. */
|
||||
setMassBadge: (values: ReturnType<typeof badgeValuesFrom> | null) => void;
|
||||
}
|
||||
@@ -91,6 +101,9 @@ export function drawLongitudinalPanel(
|
||||
minWidth: chartWidth,
|
||||
scrollLeft: input.keepScrollLeft,
|
||||
viewportWidth: chartWrap.clientWidth || chartWidth,
|
||||
// 계획고 편집 ▼ 버튼이 바닥에 붙으므로 X축·측점 라벨을 그만큼 밀어 올린다
|
||||
// (B05 와 같은 값 15px). 버튼이 없으면 0 — 종전 여백 그대로다.
|
||||
bottomInsetPx: input.grade ? 15 : 0,
|
||||
// 측점선을 끌면 그 구조물이 옮겨 간다 — 관은 예약 이동, 구조물은 정본 이동.
|
||||
onDragStation: edit ? moveMark : undefined,
|
||||
});
|
||||
@@ -130,6 +143,20 @@ export function drawLongitudinalPanel(
|
||||
}
|
||||
|
||||
chartWrap.replaceChildren(...nodes);
|
||||
// 계획선 편집 버튼층 — B05 와 **같은 부품**(`createEditOverlay`)이다(2026-09-12 사용자:
|
||||
// B05·B06 은 한 페이지인데 B06 에만 버튼이 없었다). 누른 값은 B05 와 같은 세션 초안에
|
||||
// 쌓이고 [저장]·[확정]에서 종단 정본으로 나간다.
|
||||
if (input.grade) {
|
||||
chartWrap.append(
|
||||
createEditOverlay({
|
||||
alignment: input.grade.alignment,
|
||||
width: chartWidth,
|
||||
x: chart.toX,
|
||||
step: input.grade.stepM,
|
||||
onStation: input.grade.onStation,
|
||||
}),
|
||||
);
|
||||
}
|
||||
// 종단 그래프 우클릭 — B05 와 같은 메뉴다(가까운 구조물이 있으면 삭제, 없으면 구조물군
|
||||
// → 종류 2단 추가). 그래프를 다시 그릴 때마다 붙인다(상태를 안 남긴다).
|
||||
if (edit) {
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
보정량 = 체적 × 토량환산계수(다짐)
|
||||
절취한 흙이 다져지면 줄거나 부푼다. 성토에 쓸 수 있는 양으로 환산한 것이 보정량이다.
|
||||
계수의 유일한 정의처는 `config.config_system_design.EARTHWORK_CONVERSION_FACTORS` 이며
|
||||
여기서 값을 다시 적지 않는다.
|
||||
여기서 값을 다시 적지 않는다. 프로젝트가 고른 값이 있으면 라우터가
|
||||
`earthwork_conversion_factors(settings)` 로 풀어 `factors` 로 넘긴다 — 기본값은 그대로다.
|
||||
|
||||
측구터파기 토사·암 — 설계가 가른 값을 그대로 읽는다
|
||||
B06 이 지반 유형 + 암반 경계선으로 이미 갈라 냈다(`ditch_soil_area_m2`·
|
||||
@@ -49,9 +50,13 @@ _FALLBACK_BASIS = "cut_area_ratio_fallback"
|
||||
_FALLBACK_NOTE = "측구 가름값이 설계에 없어 절토 토사:암 면적비로 안분함"
|
||||
|
||||
|
||||
def _factor(kind: str) -> float:
|
||||
#: 계수 묶음의 모양 — `{지반유형: {"compacted": C}}`.
|
||||
Factors = dict[str, dict[str, float]]
|
||||
|
||||
|
||||
def _factor(kind: str, factors: Factors) -> float:
|
||||
"""지반유형 → 다짐 환산계수. 모르는 유형이면 토사로 본다."""
|
||||
entry = EARTHWORK_CONVERSION_FACTORS.get(kind) or EARTHWORK_CONVERSION_FACTORS["soil"]
|
||||
entry = factors.get(kind) or factors["soil"]
|
||||
return float(entry["compacted"])
|
||||
|
||||
|
||||
@@ -155,8 +160,15 @@ def _split_ditch(area: StationArea) -> tuple[float, float, str]:
|
||||
return ditch * soil / total, ditch * rock / total, _FALLBACK_BASIS
|
||||
|
||||
|
||||
def build_rows(stations: Iterable[StationArea]) -> list[EarthworkRow]:
|
||||
"""측점 목록 → 토적표 줄 목록. 측점은 이정 순으로 정렬해 받는다."""
|
||||
def build_rows(
|
||||
stations: Iterable[StationArea], factors: Factors | None = None
|
||||
) -> list[EarthworkRow]:
|
||||
"""측점 목록 → 토적표 줄 목록. 측점은 이정 순으로 정렬해 받는다.
|
||||
|
||||
`factors` 는 프로젝트가 고른 토량환산계수다(`earthwork_conversion_factors`).
|
||||
안 주면 정본 기본값이 선다 — 설정을 안 읽는 자리(시험·되짚기)를 위한 것이다.
|
||||
"""
|
||||
factors = factors or EARTHWORK_CONVERSION_FACTORS
|
||||
ordered = sorted(stations, key=lambda s: s.chainage_m)
|
||||
rows: list[EarthworkRow] = []
|
||||
previous: StationArea | None = None
|
||||
@@ -165,8 +177,8 @@ def build_rows(stations: Iterable[StationArea]) -> list[EarthworkRow]:
|
||||
|
||||
for station in ordered:
|
||||
ditch_soil, ditch_rock, ditch_basis = _split_ditch(station)
|
||||
soil_factor = _factor("soil")
|
||||
rock_factor = _factor(station.cut_rock_kind or _DEFAULT_ROCK_KIND)
|
||||
soil_factor = _factor("soil", factors)
|
||||
rock_factor = _factor(station.cut_rock_kind or _DEFAULT_ROCK_KIND, factors)
|
||||
row = EarthworkRow(
|
||||
chainage_m=station.chainage_m,
|
||||
cut_soil_area_m2=station.cut_soil_area_m2,
|
||||
@@ -238,12 +250,17 @@ def totals(rows: list[EarthworkRow]) -> dict[str, float]:
|
||||
return {key: sum(getattr(row, key) for row in rows) for key in keys}
|
||||
|
||||
|
||||
def build_table(stations: Iterable[StationArea]) -> dict[str, Any]:
|
||||
"""화면·API 가 그대로 쓰는 모양. 값은 자르지 않는다(PLAN 8-16)."""
|
||||
rows = build_rows(stations)
|
||||
def build_table(stations: Iterable[StationArea], factors: Factors | None = None) -> dict[str, Any]:
|
||||
"""화면·API 가 그대로 쓰는 모양. 값은 자르지 않는다(PLAN 8-16).
|
||||
|
||||
`conversion_factors` 로 **실제로 쓴 계수**를 되싣는다 — 프로젝트가 고른 값이면
|
||||
그것이 나가야 화면이 「무엇으로 셌나」를 그대로 보인다.
|
||||
"""
|
||||
factors = factors or EARTHWORK_CONVERSION_FACTORS
|
||||
rows = build_rows(stations, factors)
|
||||
return {
|
||||
"method": "average_end_area",
|
||||
"conversion_factors": EARTHWORK_CONVERSION_FACTORS,
|
||||
"conversion_factors": factors,
|
||||
"rows": [row.__dict__ if not hasattr(row, "__slots__") else _as_dict(row) for row in rows],
|
||||
"totals": totals(rows),
|
||||
"station_count": len(rows),
|
||||
|
||||
@@ -43,14 +43,24 @@ GROUND_LABELS = {"ea_m3": "토사", "rr_m3": "리핑암", "br_m3": "발파암"}
|
||||
GROUND_KIND_OF = {"토사": "soil", "리핑암": "ripping_rock", "발파암": "blasting_rock"}
|
||||
|
||||
|
||||
def _factor_of(ground: str) -> float | None:
|
||||
"""그 갈래의 다짐 환산계수 `C`. 모르면 `None`(받는 쪽이 환산했는지 되짚는 데 쓴다)."""
|
||||
#: 계수 묶음의 모양 — `{지반유형: {"compacted": C}}`. 안 주면 정본 기본값이 선다.
|
||||
Factors = dict[str, dict[str, float]]
|
||||
|
||||
|
||||
def _factor_of(ground: str, factors: Factors | None = None) -> float | None:
|
||||
"""그 갈래의 다짐 환산계수 `C`. 모르면 `None`(받는 쪽이 환산했는지 되짚는 데 쓴다).
|
||||
|
||||
`factors` 는 프로젝트가 고른 계수다(`earthwork_conversion_factors`) — 안 주면 정본.
|
||||
"""
|
||||
kind = GROUND_KIND_OF.get(ground)
|
||||
entry = EARTHWORK_CONVERSION_FACTORS.get(kind) if kind else None
|
||||
table = factors or EARTHWORK_CONVERSION_FACTORS
|
||||
entry = table.get(kind) if kind else None
|
||||
return float(entry["compacted"]) if entry else None
|
||||
|
||||
|
||||
def natural_m3(compacted_volume_m3: float, ground: str) -> float | None:
|
||||
def natural_m3(
|
||||
compacted_volume_m3: float, ground: str, factors: Factors | None = None
|
||||
) -> float | None:
|
||||
"""**다짐상태 → 자연상태**(÷ C). 내역서에 오르는 수량은 자연상태다.
|
||||
|
||||
근거 — `config_system_design` 5-4-3 에 이미 적혀 있던 문장이다.
|
||||
@@ -69,7 +79,8 @@ def natural_m3(compacted_volume_m3: float, ground: str) -> float | None:
|
||||
⚠ 갈래를 모르면 `None` 이다 — 토사 계수로 눅이면 근거 없이 금액이 움직인다.
|
||||
"""
|
||||
kind = GROUND_KIND_OF.get(ground)
|
||||
entry = EARTHWORK_CONVERSION_FACTORS.get(kind) if kind else None
|
||||
table = factors or EARTHWORK_CONVERSION_FACTORS
|
||||
entry = table.get(kind) if kind else None
|
||||
if not entry:
|
||||
return None
|
||||
factor = float(entry["compacted"])
|
||||
@@ -189,8 +200,11 @@ def summarize(legs: Iterable[HaulLeg]) -> list[HaulSummaryRow]:
|
||||
)
|
||||
|
||||
|
||||
def build_table(plan: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""화면·API 가 그대로 쓰는 모양. 내역 줄과 근거 줄을 함께 낸다."""
|
||||
def build_table(plan: dict[str, Any] | None, factors: Factors | None = None) -> dict[str, Any]:
|
||||
"""화면·API 가 그대로 쓰는 모양. 내역 줄과 근거 줄을 함께 낸다.
|
||||
|
||||
`factors` 는 프로젝트가 고른 토량환산계수다 — 다짐 → 자연 되돌리기가 이 값에 걸린다.
|
||||
"""
|
||||
legs = _legs_of(plan or {})
|
||||
rows = summarize(legs)
|
||||
return {
|
||||
@@ -203,9 +217,9 @@ def build_table(plan: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"volume_m3": row.volume_m3,
|
||||
"volume_basis": "compacted",
|
||||
# 내역서에 오르는 수량 = **자연상태**(÷C). 갈래를 모르면 `None`.
|
||||
"natural_m3": natural_m3(row.volume_m3, row.ground),
|
||||
"natural_m3": natural_m3(row.volume_m3, row.ground, factors),
|
||||
"natural_volume_basis": "natural",
|
||||
"conversion_c": _factor_of(row.ground),
|
||||
"conversion_c": _factor_of(row.ground, factors),
|
||||
"average_distance_m": row.average_distance_m,
|
||||
"work_m3m": row.work_m3m,
|
||||
"legs": row.legs,
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
"""B08 수량 화면의 **근거 사전** — 어느 숫자가 어디서 와서 어떻게 나왔나 (PLAN 8-36 ④).
|
||||
|
||||
⚠⚠ **개발 전용.** 사전은 `provenance_payload()` 를 거쳐 나가고, 개발환경이 아니면 `None`
|
||||
이라 응답에 칸 자체가 안 생긴다. 화면에서 숨기는 것이 아니라 **안 보내는 것**이다.
|
||||
|
||||
왜 이 파일인가
|
||||
「식」과 「원천」의 정답은 값을 낳는 엔진이 안다. 화면 TS 에 손으로 적어 두면 엔진을
|
||||
고칠 때 설명만 옛것으로 남는다. 엔진 옆(같은 폴더)에 두어 같이 눈에 들어오게 한다.
|
||||
|
||||
⚠ **열 단위로 적는다.** 토적표 한 장이 30열 × 200줄 = 6천 칸이라 칸마다 지으면 응답이
|
||||
붐는다. 줄마다 갈리는 것(측구 안분 폴백 사유 등)은 줄이 이미 `notes` 로 들고 있고,
|
||||
화면이 그것을 카드에 덧붙인다.
|
||||
|
||||
⚠ **토적표에는 `final`(최종) 열이 없다 — 억지로 붙이지 않았다.**
|
||||
이 표는 중간 장부다. 내역서로 나가는 값은 **토공집계표**에서 선다. 여섯 등급을
|
||||
한 장에 다 채우려고 아무 열에나 `final` 을 붙이면 「분류가 있다」는 거짓만 남는다.
|
||||
이 어긋남은 등급을 고칠 근거이므로 PLAN 8-36 ① 에 그대로 남긴다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_provenance import (
|
||||
TIER_CALC,
|
||||
TIER_FINAL,
|
||||
TIER_INPUT,
|
||||
TIER_STANDARD,
|
||||
TIER_SURVEY,
|
||||
ColumnProvenance,
|
||||
provenance_payload,
|
||||
sheet_provenance,
|
||||
)
|
||||
|
||||
#: 토량환산계수가 어디서 오는지 — 여러 열이 같은 문장을 쓰므로 한 벌로 둔다.
|
||||
_FACTOR_SOURCE = (
|
||||
"토량환산계수(다짐) — 기본값 `config_system_design.EARTHWORK_CONVERSION_FACTORS`, "
|
||||
"프로젝트가 고른 값이 있으면 산출 조건 패널의 값"
|
||||
)
|
||||
|
||||
#: 단면적 넷의 공통 원천. B06 이 낸 설계 단면을 **그대로** 읽는다(여기서 다시 안 짓는다).
|
||||
_SECTION_SOURCE = "B06 횡단 설계가 낸 측점별 단면적"
|
||||
|
||||
#: 평균단면적법 한 줄. 신규 문서 5장 「다. 공사수량의 산출」.
|
||||
_MEAN_AREA = "(앞 측점 단면적 + 이 측점 단면적) ÷ 2 × 두 측점 사이 거리"
|
||||
|
||||
|
||||
def _area(key: str, label: str, extra: str = "") -> ColumnProvenance:
|
||||
"""단면적 열 — B06 설계값을 그대로 옮긴 자리라 식이 없다."""
|
||||
return ColumnProvenance(
|
||||
key=key,
|
||||
label=label,
|
||||
tier=TIER_SURVEY,
|
||||
formula="설계가 낸 값을 그대로 읽음 (여기서 다시 계산하지 않음)",
|
||||
source=_SECTION_SOURCE + (f" · {extra}" if extra else ""),
|
||||
code="B08_Quantity_Engine_EarthworkTable.py:StationArea.from_design",
|
||||
)
|
||||
|
||||
|
||||
def _volume(key: str, label: str, area_label: str) -> ColumnProvenance:
|
||||
return ColumnProvenance(
|
||||
key=key,
|
||||
label=label,
|
||||
tier=TIER_CALC,
|
||||
formula=_MEAN_AREA.replace("단면적", area_label),
|
||||
source="첫 측점은 앞이 없어 비어 있음 (실무 토적표도 첫 줄 체적이 빈칸)",
|
||||
code="B08_Quantity_Engine_EarthworkTable.py:200 mean_volume",
|
||||
)
|
||||
|
||||
|
||||
def _adjusted(key: str, label: str, volume_label: str) -> ColumnProvenance:
|
||||
return ColumnProvenance(
|
||||
key=key,
|
||||
label=label,
|
||||
tier=TIER_CALC,
|
||||
formula=f"{volume_label} × 토량환산계수(다짐)",
|
||||
source=_FACTOR_SOURCE,
|
||||
code="B08_Quantity_Engine_EarthworkTable.py:210",
|
||||
)
|
||||
|
||||
|
||||
def earthwork_sheet() -> dict[str, Any]:
|
||||
"""토적표 한 장의 사전. 열 키는 화면 `EarthworkRow` 와 같은 낱말이라야 한다."""
|
||||
return sheet_provenance(
|
||||
[
|
||||
ColumnProvenance(
|
||||
key="chainage_m",
|
||||
label="측점",
|
||||
tier=TIER_SURVEY,
|
||||
formula="노선 시점에서 잰 이정(m). 화면은 NO.n+m 으로 적음",
|
||||
source="B05 종단이 놓은 측점 배치",
|
||||
code="B08_Quantity_Engine_EarthworkTable.py:StationArea.chainage_m",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="distance_m",
|
||||
label="거리",
|
||||
tier=TIER_CALC,
|
||||
formula="이 측점 이정 − 앞 측점 이정",
|
||||
source="B05 종단 측점 배치. 첫 줄은 앞이 없어 0",
|
||||
code="B08_Quantity_Engine_EarthworkTable.py:195",
|
||||
),
|
||||
_area("cut_soil_area_m2", "절토 토사 단면적"),
|
||||
_volume("cut_soil_volume_m3", "절토 토사 입적", "절토 토사 단면적"),
|
||||
_adjusted("cut_soil_adjusted_m3", "절토 토사 보정량", "절토 토사 입적"),
|
||||
_area("cut_rock_area_m2", "절토 암석 단면적", "암 갈래는 측점의 `cut_rock_kind`"),
|
||||
_volume("cut_rock_volume_m3", "절토 암석 입적", "절토 암석 단면적"),
|
||||
_adjusted("cut_rock_adjusted_m3", "절토 암석 보정량", "절토 암석 입적"),
|
||||
_area(
|
||||
"ditch_soil_area_m2",
|
||||
"측구터파기 토사 단면적",
|
||||
"지반 유형·암반 경계선으로 B06 이 가른 값. 가름이 없는 옛 저장분만 "
|
||||
"절토 토사:암 면적비로 안분하고 그 줄에 사유가 남음",
|
||||
),
|
||||
_volume("ditch_soil_volume_m3", "측구터파기 토사 입적", "측구 토사 단면적"),
|
||||
_adjusted("ditch_soil_adjusted_m3", "측구터파기 토사 보정량", "측구 토사 입적"),
|
||||
_area(
|
||||
"ditch_rock_area_m2",
|
||||
"측구터파기 암석 단면적",
|
||||
"위와 같은 가름값. 0.0 은 설계가 낸 「없음」이고 값 없음과 다름",
|
||||
),
|
||||
_volume("ditch_rock_volume_m3", "측구터파기 암석 입적", "측구 암석 단면적"),
|
||||
_adjusted("ditch_rock_adjusted_m3", "측구터파기 암석 보정량", "측구 암석 입적"),
|
||||
ColumnProvenance(
|
||||
key="adjusted_total_m3",
|
||||
label="보정량계",
|
||||
tier=TIER_CALC,
|
||||
formula="절토 토사 보정량 + 절토 암석 보정량 + 측구 토사 보정량 + 측구 암석 보정량",
|
||||
source="네 보정량의 합. 성토에 쓸 수 있는 양으로 환산한 뒤의 값",
|
||||
code="B08_Quantity_Engine_EarthworkTable.py:215",
|
||||
),
|
||||
_area("fill_area_m2", "성토 단면적"),
|
||||
_volume("fill_volume_m3", "성토 입적", "성토 단면적"),
|
||||
ColumnProvenance(
|
||||
key="diverted_m3",
|
||||
label="유용토",
|
||||
tier=TIER_CALC,
|
||||
formula="min(보정량계, 성토 입적)",
|
||||
source="그 측점에서 절취분과 성토분이 서로 만나는 몫",
|
||||
code="B08_Quantity_Engine_EarthworkTable.py:222",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="balance_m3",
|
||||
label="차인토량",
|
||||
tier=TIER_CALC,
|
||||
formula="보정량계 − 성토 입적",
|
||||
source="양수면 남는 흙(사토), 음수면 모자란 흙(객토)",
|
||||
code="B08_Quantity_Engine_EarthworkTable.py:223",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="cumulative_m3",
|
||||
label="누가토량",
|
||||
tier=TIER_CALC,
|
||||
formula="첫 줄부터 이 줄까지 차인토량을 더해 온 값",
|
||||
source="유토곡선(mass haul)의 세로축이 되는 값",
|
||||
code="B08_Quantity_Engine_EarthworkTable.py:225",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def summary_sheet() -> dict[str, Any]:
|
||||
"""토공집계표 — 토적표·사면표를 공종별 총량으로 모은 장.
|
||||
|
||||
⚠ **B08 에서 `final`(최종)이 처음 서는 자리다.** 토적표는 중간 장부였고, 내역서로
|
||||
나가는 값은 여기 「계」다. 다만 무대(소운반 20m)처럼 **집계에는 오르되 내역 줄이
|
||||
되지 않는** 줄이 있어, 그 줄의 「계」는 칸 등급 `excluded` 로 덮어쓴다(화면 배선).
|
||||
"""
|
||||
return sheet_provenance(
|
||||
[
|
||||
ColumnProvenance(
|
||||
key="group",
|
||||
label="구분",
|
||||
tier=TIER_STANDARD,
|
||||
formula="품셈 공종 갈래 이름을 그대로 씀 (흙깎기·성토·측구터파기…)",
|
||||
source="거창 실무 토공집계표 시트의 열 문구를 그대로 옮김",
|
||||
code="B08_Quantity_Engine_EarthworkSummary.py:SummaryRow",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="item",
|
||||
label="공종",
|
||||
tier=TIER_STANDARD,
|
||||
formula="지반 갈래 이름 (토사·연암·발파암…)",
|
||||
source="갈래 수는 프로젝트 설정의 암 갈래 세트가 정함 — 코드에 안 박음",
|
||||
rule="암 총량을 설계자가 넣은 갈래 비율(%)로 나눠 줄을 만듦",
|
||||
code="B08_Quantity_Engine_EarthworkSummary.py:_rock_split",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="spec",
|
||||
label="규격",
|
||||
tier=TIER_STANDARD,
|
||||
formula="시공 방법 표기 (기계(굴삭기)·백호우…)",
|
||||
source="품셈 공종이 요구하는 규격. 암은 시공법(긁어내기/터뜨리기)이 갈림",
|
||||
code="B08_Quantity_Engine_EarthworkSummary.py:SummaryRow",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="unit",
|
||||
label="단위",
|
||||
tier=TIER_STANDARD,
|
||||
formula="품셈 공종이 정한 단위 (㎥·㎡·주…)",
|
||||
source="단위가 다르면 내역 단가와 안 맞음 — 여기서 정하지 않고 품셈을 따름",
|
||||
code="B08_Quantity_Engine_EarthworkSummary.py:SummaryRow",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="amount",
|
||||
label="계",
|
||||
tier=TIER_FINAL,
|
||||
formula="토적표·사면표 총량 × 반영률(%)",
|
||||
source=(
|
||||
"토공은 토적표 합계, 사면은 사면표 합계. ⚠ 반영률은 법정값이 아니라 "
|
||||
"설계자가 넣는 값이고 기본 100 %"
|
||||
),
|
||||
rule="무대(소운반 20m)는 집계에는 오르되 내역 줄이 아님 — 그 줄은 「제외」로 섬",
|
||||
code="B08_Quantity_Engine_EarthworkSummary.py:build_table",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def haul_sheet() -> dict[str, Any]:
|
||||
"""운반거리 — (운반수단 × 지반유형)별 가중평균 줄.
|
||||
|
||||
⚠⚠ **상태가 둘이다.** 거리는 다짐상태로 재고, 내역에 오르는 수량만 자연상태(÷C)로 낸다
|
||||
(설계실무 요령 5-4-3). 이 표의 「토량」은 **다짐상태**이므로 내역서 수량과 숫자가 다르다 —
|
||||
그 어긋남이 정상이라는 것을 카드가 말해 주어야 헛걸음을 안 한다.
|
||||
"""
|
||||
return sheet_provenance(
|
||||
[
|
||||
ColumnProvenance(
|
||||
key="equipment",
|
||||
label="운반수단",
|
||||
tier=TIER_CALC,
|
||||
formula="유토곡선이 띠마다 고른 수단 (무대·도자운반·덤프운반)",
|
||||
source="B06 운반계획(HaulPlan)의 띠. 여기서 다시 고르지 않음",
|
||||
code="B08_Quantity_Engine_HaulSummary.py:_legs_of",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="ground",
|
||||
label="지반유형",
|
||||
tier=TIER_CALC,
|
||||
formula="띠의 토량을 절토 구간 구성비로 안분한 세 갈래 (토사·리핑암·발파암)",
|
||||
source="B06 운반계획이 이미 안분해 둔 값",
|
||||
code="B08_Quantity_Engine_HaulSummary.py:_legs_of",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="volume_m3",
|
||||
label="토량",
|
||||
tier=TIER_CALC,
|
||||
formula="그 갈래에 속한 근거 구간들의 토량 합",
|
||||
source=(
|
||||
"⚠ **다짐상태**임. 내역서에 오르는 수량은 자연상태(÷토량환산계수)라 "
|
||||
"숫자가 다름 — 어긋난 것이 아님"
|
||||
),
|
||||
code="B08_Quantity_Engine_HaulSummary.py:summarize",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="average_distance_m",
|
||||
label="평균운반거리",
|
||||
tier=TIER_CALC,
|
||||
formula="Σ(토량 × 거리) ÷ Σ(토량) — 단순평균이 아님",
|
||||
source="실무 산출서가 「토량 × 거리」를 쌓아 나누는 그 식",
|
||||
code="B08_Quantity_Engine_HaulSummary.py:119 average_distance_m",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="legs",
|
||||
label="근거 구간",
|
||||
tier=TIER_CALC,
|
||||
formula="이 평균을 만든 구간의 개수",
|
||||
source="구간 줄은 버리지 않고 표 아래 근거로 함께 냄 — 되짚을 수 있어야 함",
|
||||
code="B08_Quantity_Engine_HaulSummary.py:190",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def preparation_sheet() -> dict[str, Any]:
|
||||
"""준비공·사방공 — **못 서는 줄도 서는 장.**
|
||||
|
||||
빈 표를 내면 「빠뜨린 것」과 「원래 없는 것」이 구별되지 않는다. 그래서 값이 없는 줄도
|
||||
상태와 사유를 달아 그대로 세운다. 값이 비어 있는 줄의 「수량」은 칸 등급 `blocked` 로
|
||||
덮어쓴다 — **근거가 오면 채워질 자리**이지 일부러 비운 자리가 아니다.
|
||||
"""
|
||||
return sheet_provenance(
|
||||
[
|
||||
ColumnProvenance(
|
||||
key="group",
|
||||
label="구분",
|
||||
tier=TIER_STANDARD,
|
||||
formula="준비공·사방공의 갈래 이름",
|
||||
source="품셈 9장(준비공)과 배치된 구조물 종류가 줄을 만듦",
|
||||
code="B08_Quantity_Engine_Preparation.py:build_table",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="item",
|
||||
label="공종",
|
||||
tier=TIER_STANDARD,
|
||||
formula="품셈 공종 이름 (표토제거·제근·임목파쇄…)",
|
||||
source="공종이 없으면 줄도 없음 — 화면에서 이름을 짓지 않음",
|
||||
code="B08_Quantity_Engine_Preparation.py:build_table",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="unit",
|
||||
label="단위",
|
||||
tier=TIER_STANDARD,
|
||||
formula="품셈 공종이 정한 단위",
|
||||
source="단위가 다르면 내역 단가와 안 맞음",
|
||||
code="B08_Quantity_Engine_Preparation.py:build_table",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="amount",
|
||||
label="수량",
|
||||
tier=TIER_CALC,
|
||||
formula="공종마다 다름 — 표토제거는 면적 × 표토 두께(T), 제근은 임목축적 등급",
|
||||
source=(
|
||||
"밑수는 사면표·구조물 목록이 내고, 두께·등급·개소는 산출 조건 패널에서 "
|
||||
"설계자가 넣음"
|
||||
),
|
||||
rule="넣어야 할 값이 비면 줄은 서되 수량이 「-」로 남고 사유가 붙음",
|
||||
code="B08_Quantity_Engine_Preparation.py:build_table",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="status",
|
||||
label="상태",
|
||||
tier=TIER_CALC,
|
||||
formula="값을 세웠나 못 세웠나",
|
||||
source="못 세운 줄은 옆 칸에 사유가 붙음 — 사유가 곧 무엇을 넣어야 하는지임",
|
||||
code="B08_Quantity_Engine_Preparation_Status.py",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def material_sheet() -> dict[str, Any]:
|
||||
"""자재총괄 — 구조물 원단위에서 자재별로 모은 장. 관급/사급을 줄마다 고른다."""
|
||||
return sheet_provenance(
|
||||
[
|
||||
ColumnProvenance(
|
||||
key="name",
|
||||
label="자재",
|
||||
tier=TIER_STANDARD,
|
||||
formula="품셈·카탈로그의 자재 이름",
|
||||
source="구조물 원단위의 성분 이름을 그대로 모음 — 여기서 이름을 짓지 않음",
|
||||
code="B08_Quantity_Engine_MaterialSummary.py",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="unit",
|
||||
label="단위",
|
||||
tier=TIER_STANDARD,
|
||||
formula="자재가 팔리는 단위 (㎥·본·kg…)",
|
||||
source="단가가 붙는 단위와 같아야 함",
|
||||
code="B08_Quantity_Engine_MaterialSummary.py",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="net_amount",
|
||||
label="순수량",
|
||||
tier=TIER_CALC,
|
||||
formula="구조물마다 낸 성분 수량의 합 (할증 전)",
|
||||
source="구조물 원단위 표의 「수량」을 자재 이름으로 모은 값",
|
||||
code="B08_Quantity_Engine_MaterialSummary.py",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="surcharge_pct",
|
||||
label="할증률",
|
||||
tier=TIER_STANDARD,
|
||||
formula="자재마다 정해진 할증률(%)",
|
||||
source="할증 판(dataset)이 정함. 판에 없는 자재는 「-」로 두고 지어내지 않음",
|
||||
code="B08_Quantity_Engine_MaterialSummary.py",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="total_amount",
|
||||
label="총수량",
|
||||
tier=TIER_FINAL,
|
||||
formula="순수량 × (1 + 할증률)",
|
||||
source="내역서·자재대로 나가는 값. 할증률이 없으면 순수량 그대로",
|
||||
code="B08_Quantity_Engine_MaterialSummary.py",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="supply",
|
||||
label="관급/사급",
|
||||
tier=TIER_INPUT,
|
||||
formula="설계자가 줄마다 고름",
|
||||
source="자재마다 갈리는 발주 결정이라 표 안에서 고름 (2026-09-07 확정)",
|
||||
code="B08_Quantity_UI_MaterialGrid.ts",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="install_by",
|
||||
label="설치 주체",
|
||||
tier=TIER_INPUT,
|
||||
formula="설계자가 줄마다 고름",
|
||||
source="⚠ **관급 줄에만 뜻이 있음** — 사급으로 되돌리면 값이 비워짐",
|
||||
code="B08_Quantity_UI_MaterialGrid.ts",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def unit_quantity_sheet() -> dict[str, Any]:
|
||||
"""구조물 원단위 — 치수에서 성분까지. 성분마다 갈 곳을 적는다.
|
||||
|
||||
⚠ 이 장은 **근거·출처 열을 이미 화면에 들고 있다**(2026-09-09 부터). 사전은 그 열이
|
||||
무엇을 뜻하는지 설명하는 자리이지, 있는 값을 다시 만드는 자리가 아니다.
|
||||
"""
|
||||
return sheet_provenance(
|
||||
[
|
||||
ColumnProvenance(
|
||||
key="structure",
|
||||
label="구조물",
|
||||
tier=TIER_SURVEY,
|
||||
formula="B05 노선에 놓인 구조물의 이름과 놓인 측점",
|
||||
source="측점 표기(NO.4 ~ NO.4+10)는 화면이 만듦 — 서버는 이정만 냄",
|
||||
code="B08_Quantity_Engine_Handoff_Rows_Prep.py:182",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="spec",
|
||||
label="규격",
|
||||
tier=TIER_SURVEY,
|
||||
formula="구조물 제원 (길이 × 높이)",
|
||||
source="B05·B06 이 배치할 때 정한 치수. 여기서 다시 정하지 않음",
|
||||
code="B08_Quantity_Engine_UnitQuantity.py",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="component",
|
||||
label="성분",
|
||||
tier=TIER_STANDARD,
|
||||
formula="그 구조물이 쓰는 재료·공종 이름",
|
||||
source="품셈 표 또는 실무 관측 원단위표가 정함",
|
||||
code="B08_Quantity_Engine_UnitQuantity.py",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="unit",
|
||||
label="단위",
|
||||
tier=TIER_STANDARD,
|
||||
formula="성분이 세어지는 단위",
|
||||
source="단가가 붙는 단위와 같아야 함",
|
||||
code="B08_Quantity_Engine_UnitQuantity.py",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="amount",
|
||||
label="수량",
|
||||
tier=TIER_CALC,
|
||||
formula="치수 전개(길이·높이로 편 식) 또는 실무 관측 원단위 × 개소",
|
||||
source="어느 쪽인지는 같은 줄의 「출처」 칸이 말해 줌 (치수 전개 / 실무 관측)",
|
||||
rule="치수 전개는 식이 있고, 실무 관측은 관측값이라 식이 없음 — 둘을 섞지 않음",
|
||||
code="B08_Quantity_Engine_UnitQuantity.py",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="destination",
|
||||
label="갈 곳",
|
||||
tier=TIER_STANDARD,
|
||||
formula="이 성분이 어느 표로 가는가 (자재총괄·공종 내역·양쪽)",
|
||||
source=(
|
||||
"⚠ 갈 곳이 겹치면 이중계상임 — 그것을 막으려고 성분마다 갈 곳을 적음 (PLAN 8-7)"
|
||||
),
|
||||
code="B08_Quantity_Engine_Handoff_Mapping.py",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def quantity_provenance() -> dict[str, Any] | None:
|
||||
"""B08 응답에 실을 사전 — **개발환경이 아니면 `None`.**
|
||||
|
||||
시트를 늘릴 때는 여기 한 줄만 더한다. 화면은 시트 이름으로 찾아 쓴다.
|
||||
"""
|
||||
return provenance_payload(
|
||||
{
|
||||
"earthwork": earthwork_sheet(),
|
||||
"summary": summary_sheet(),
|
||||
"haul": haul_sheet(),
|
||||
"preparation": preparation_sheet(),
|
||||
"material": material_sheet(),
|
||||
"unit_quantity": unit_quantity_sheet(),
|
||||
}
|
||||
)
|
||||
@@ -40,18 +40,24 @@ from B08_Quantity.B08_Quantity_Engine_Preparation import build_table as build_pr
|
||||
from B08_Quantity.B08_Quantity_Engine_HaulSummary import summary_input_rows
|
||||
from B08_Quantity.B08_Quantity_Engine_SlopeArea import build_table as build_slope_table
|
||||
from B08_Quantity.B08_Quantity_Engine_SlopeLength import station_slopes
|
||||
from B08_Quantity.B08_Quantity_Provenance import quantity_provenance
|
||||
from common_util.common_util_project_settings import (
|
||||
CONCRETE_PLACING_METHODS,
|
||||
ROCK_METHODS,
|
||||
application_ratio,
|
||||
concrete_placing_method,
|
||||
earthwork_conversion_choices,
|
||||
earthwork_conversion_factors,
|
||||
quantity_settings,
|
||||
rock_classes,
|
||||
save_section,
|
||||
)
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import run_with_connection
|
||||
from config.config_system_design import EARTHWORK_CONVERSION_FACTORS
|
||||
from config.config_system_design import (
|
||||
EARTHWORK_CONVERSION_FACTORS,
|
||||
EARTHWORK_CONVERSION_PUMSEM_C_RANGES,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B08 Quantity"])
|
||||
@@ -78,14 +84,24 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "토적표를 만들지 못했습니다."},
|
||||
)
|
||||
table = build_table(_stations(designs))
|
||||
# ⚠ 설정을 **먼저** 읽는다 — 토량환산계수를 프로젝트가 골랐으면 표가 그 값으로 서야 한다.
|
||||
settings, project_root = await _project_settings(project_id)
|
||||
factors = earthwork_conversion_factors(settings)
|
||||
table = build_table(_stations(designs), factors)
|
||||
# 화면이 「무엇을 골랐나 · 품셈 범위 안인가」를 보이는 데 쓴다. 계산에는 안 들어간다.
|
||||
table["conversion_factor_choices"] = earthwork_conversion_choices(settings)
|
||||
# 품셈 암종별 범위 — **화면 안내용**이다. 정의처가 서버 한 곳이라 내려보내 쓴다
|
||||
# (프론트에 다시 적으면 두 벌이 되어 갈린다).
|
||||
table["conversion_factor_pumsem_ranges"] = [
|
||||
{"name": name, "min": low, "max": high}
|
||||
for name, low, high in EARTHWORK_CONVERSION_PUMSEM_C_RANGES
|
||||
]
|
||||
# 사면 계열은 저장된 설계선에서 유도한다.
|
||||
slope = build_slope_table(station_slopes(designs))
|
||||
table["slope"] = slope
|
||||
|
||||
settings, project_root = await _project_settings(project_id)
|
||||
plan = await _stored_haul_plan(project_id, route_id)
|
||||
haul = build_haul_table(plan)
|
||||
haul = build_haul_table(plan, factors)
|
||||
# 사토 — **운반 줄이 되는 값**인데 유토곡선의 띠·이동에는 안 들어 있다(잔량으로 남는다).
|
||||
# 여기서 그 값을 운반표에 실어 인계가 「사토 운반」 한 줄을 세우게 한다.
|
||||
# ⚠ 거리는 품셈이 정하지 않는다 — 설계 입력(`spoil_site_distance_m`)이고 없으면 막힌다.
|
||||
@@ -161,15 +177,22 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
||||
table["settings"] = settings
|
||||
table["project_root_known"] = project_root is not None
|
||||
table["route_id"] = route_id
|
||||
# 근거 사전(PLAN 8-36 ④) — ⚠ **개발환경에서만** 실린다. 운영에서는 `None` 이라
|
||||
# 칸 자체가 안 생긴다 — 화면에서 숨기는 것이 아니라 안 보내는 것이 요점이다.
|
||||
provenance = quantity_provenance()
|
||||
if provenance is not None:
|
||||
table["provenance"] = provenance
|
||||
return JSONResponse(content=table)
|
||||
|
||||
|
||||
#: 갈래 칸 ↔ 다짐 환산계수 `C`. 정의처는 `config_system_design` 한 곳뿐이다.
|
||||
_COMPACTED_FACTOR = {
|
||||
"ea_m3": float(EARTHWORK_CONVERSION_FACTORS["soil"]["compacted"]),
|
||||
"rr_m3": float(EARTHWORK_CONVERSION_FACTORS["ripping_rock"]["compacted"]),
|
||||
"br_m3": float(EARTHWORK_CONVERSION_FACTORS["blasting_rock"]["compacted"]),
|
||||
}
|
||||
#: 갈래 칸 ↔ 지반유형 이름. 계수의 정의처는 `config_system_design` 한 곳뿐이다.
|
||||
_GROUND_KIND_OF = {"ea_m3": "soil", "rr_m3": "ripping_rock", "br_m3": "blasting_rock"}
|
||||
|
||||
|
||||
def _compacted_factor(settings: dict[str, Any]) -> dict[str, float]:
|
||||
"""갈래 칸 ↔ 다짐 환산계수 `C` — 프로젝트가 고른 값이 있으면 그것이 선다."""
|
||||
factors = earthwork_conversion_factors(settings)
|
||||
return {key: float(factors[kind]["compacted"]) for key, kind in _GROUND_KIND_OF.items()}
|
||||
|
||||
|
||||
def _spoil_sites(designs: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
@@ -295,10 +318,11 @@ def _spoil_of(
|
||||
# 여기서 ÷C 한 값을 함께 내 받는 쪽이 **또 환산하지 않게** 한다.
|
||||
# ⚠ 갈래를 못 붙인 몫은 계수가 없어 **환산하지 않는다** — 토사 계수로 눅이면 근거 없이
|
||||
# 금액이 움직인다. 그 사실을 사유로 낸다.
|
||||
compacted_factor = _compacted_factor(settings)
|
||||
natural_by_ground = {
|
||||
key: round(value / _COMPACTED_FACTOR[key], 3)
|
||||
key: round(value / compacted_factor[key], 3)
|
||||
for key, value in grounds.items()
|
||||
if key in _COMPACTED_FACTOR
|
||||
if key in compacted_factor
|
||||
}
|
||||
if unknown > 0:
|
||||
note_parts.append(f"⚠ 갈래를 못 붙인 {unknown:,.2f}㎥ 는 상태도 못 되돌림")
|
||||
@@ -417,6 +441,10 @@ class QuantitySettingsBody(BaseModel):
|
||||
# 임목파쇄 — 기본 꺼짐(확정 5차 5번). 켜면 줄이 서고, 부피를 넣으면 값이 선다.
|
||||
wood_chipping_enabled: bool | None = None
|
||||
wood_chipping_volume_m3: float | None = None
|
||||
# 토량환산계수(다짐) — `{갈래: {"compacted": C, "reason": 사유}}`.
|
||||
# ⚠ **기본값을 복사해 넣지 않는다** — 안 고른 갈래는 키가 없어야 정본이 선다.
|
||||
# 빈 dict 는 「전부 기본값으로 되돌림」이라 통째로 갈아 끼운다.
|
||||
conversion_factors_override: dict[str, Any] | None = None
|
||||
|
||||
|
||||
#: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**.
|
||||
@@ -458,6 +486,21 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -
|
||||
method = values["concrete_placing_method"]
|
||||
# 「안 정함」으로 되돌릴 수 있어야 한다 — 빈 값이면 지운다(8-22 ② 와 같은 자리).
|
||||
values["concrete_placing_method"] = method if method in CONCRETE_PLACING_METHODS else None
|
||||
if "conversion_factors_override" in values:
|
||||
# 아는 갈래·양수만 남긴다. 사유는 값이 있을 때만 따라간다(계산에는 안 쓴다).
|
||||
cleaned: dict[str, Any] = {}
|
||||
for kind, entry in (values["conversion_factors_override"] or {}).items():
|
||||
if kind not in EARTHWORK_CONVERSION_FACTORS or not isinstance(entry, dict):
|
||||
continue
|
||||
value = entry.get("compacted")
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool) or float(value) <= 0:
|
||||
continue
|
||||
kept: dict[str, Any] = {"compacted": float(value)}
|
||||
reason = entry.get("reason")
|
||||
if isinstance(reason, str) and reason.strip():
|
||||
kept["reason"] = reason.strip()
|
||||
cleaned[kind] = kept
|
||||
values["conversion_factors_override"] = cleaned
|
||||
if "rock_methods" in values:
|
||||
# 「안 정함」(빈 값)은 저장하지 않는다 — 정한 것과 구별이 안 된다. 통째로 갈아 끼우므로
|
||||
# 여기서 버리면 그 갈래는 미지정으로 돌아간다.
|
||||
@@ -473,7 +516,14 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -
|
||||
_save_quantity,
|
||||
root,
|
||||
values,
|
||||
("rock_methods", "material_supply", "concrete_placing_method", "ancillary_counts")
|
||||
(
|
||||
"rock_methods",
|
||||
"material_supply",
|
||||
"concrete_placing_method",
|
||||
"ancillary_counts",
|
||||
# 고른 계수를 **기본값으로 되돌릴 길**이 있어야 한다 — 병합이면 못 지운다.
|
||||
"conversion_factors_override",
|
||||
)
|
||||
+ NULLABLE_SETTING_KEYS,
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -31,6 +31,7 @@ from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
||||
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff, load_mapping, summarize
|
||||
from B08_Quantity.B08_Quantity_Engine_MaterialSummary import build_table as build_material_table
|
||||
from B08_Quantity.B08_Quantity_Provenance import quantity_provenance
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table as build_unit_table
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import (
|
||||
ground_types_from_designs,
|
||||
@@ -173,15 +174,19 @@ async def get_material_summary(project_id: UUID) -> JSONResponse:
|
||||
# 조각이 없어도(원단위 자체가 없어 못 세운 경우) 사유는 보여야 한다.
|
||||
if row.get("composite_parts") or row.get("composite_not_ready")
|
||||
]
|
||||
return JSONResponse(
|
||||
content={
|
||||
"unit_quantity": unit_table,
|
||||
"material": material_table,
|
||||
"composite": composite,
|
||||
"skipped_structures": skipped,
|
||||
"structure_count": len(structures),
|
||||
}
|
||||
)
|
||||
body: dict[str, Any] = {
|
||||
"unit_quantity": unit_table,
|
||||
"material": material_table,
|
||||
"composite": composite,
|
||||
"skipped_structures": skipped,
|
||||
"structure_count": len(structures),
|
||||
}
|
||||
# 근거 사전(PLAN 8-36 ④) — ⚠ **개발환경에서만** 실린다. 운영에서는 `None` 이라
|
||||
# 칸 자체가 안 생긴다 — 화면에서 숨기는 것이 아니라 안 보내는 것이 요점이다.
|
||||
provenance = quantity_provenance()
|
||||
if provenance is not None:
|
||||
body["provenance"] = provenance
|
||||
return JSONResponse(content=body)
|
||||
|
||||
|
||||
async def project_haul_inputs(project_id: UUID) -> dict[str, Any]:
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_UI_ConversionFactors.ts
|
||||
* 산출 조건 패널의 「토량환산계수(다짐)」 칸 — 고를 수 있게 열어 둔 자리.
|
||||
*
|
||||
* 왜 고르게 하나 (오솔길 대조 06절 3번)
|
||||
* 품셈 체적변화율표가 암종마다 **범위**를 주고 「토질 시험하여 적용함을 원칙」이라 한다.
|
||||
* 즉 정답 숫자가 하나가 아니다. 경쟁사(오솔길)가 전 구간 1.0 을 쓰는 것도 풍화암·연암
|
||||
* 범위의 하한이라 틀린 값이 아니다. 그래서 **값을 못 박지 않고 범위를 보이며 고르게** 한다.
|
||||
*
|
||||
* ⚠ 기본값은 건드리지 않는다
|
||||
* 정의처는 서버 `config_system_design.EARTHWORK_CONVERSION_FACTORS` 한 곳이다. 화면은
|
||||
* 고른 값만 `conversion_factors_override` 로 보내고, 안 고른 갈래는 **키 자체를 안 보낸다** —
|
||||
* 그래야 나중에 정본이 바뀌어도 옛 프로젝트가 따라온다.
|
||||
*
|
||||
* ⚠ 범위 밖을 막지 않는다
|
||||
* 토질시험 값일 수 있다. 막는 대신 **사유를 적게** 하고, 그 사유가 정본에 함께 남는다.
|
||||
*
|
||||
* ⚠ 이 계수는 토적표만 쓰는 것이 아니다
|
||||
* 유토곡선(B06) · 운반표 · 기초단가가 같은 값을 읽는다. 패널에 그 사실을 한 줄 보인다 —
|
||||
* 안 보이면 「토적표만 바뀌겠지」로 읽힌다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
|
||||
import type { ConversionFactorChoice, PumsemRange } from "./B08_Quantity_UI_EarthworkGrid";
|
||||
|
||||
/** locale 헬퍼 — 페이지 쪽과 같은 모양으로 둔다(문구는 `ui_template_locale_b2`). */
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
/** 화면이 들고 있는 고른 값 — `compacted` 가 `null` 이면 「안 고름」이라 저장에서 빠진다. */
|
||||
export interface FactorDraft {
|
||||
compacted: number | null;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** 서버 키 → 사람이 읽는 이름. 모르는 키는 **지어내지 않고** 그대로 보인다. */
|
||||
const GROUND_LABELS: Record<string, string> = {
|
||||
soil: "토사",
|
||||
ripping_rock: "리핑암",
|
||||
blasting_rock: "발파암",
|
||||
};
|
||||
|
||||
function groundLabel(kind: string): string {
|
||||
return GROUND_LABELS[kind] ?? kind;
|
||||
}
|
||||
|
||||
function hint(text: string): HTMLElement {
|
||||
const row = document.createElement("p");
|
||||
row.className = "b08-quantity__hint";
|
||||
row.textContent = text;
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 범위 밖인가 — 서버가 준 범위로 판정한다. 범위를 모르면 **밖이라고 하지 않는다.** */
|
||||
function outOfRange(value: number, range: [number, number] | null): boolean {
|
||||
if (!range) return false;
|
||||
return value < range[0] || value > range[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 갈래 한 줄 — 값 칸 + 기본값·품셈 범위 안내 + (범위 밖일 때만) 사유 칸.
|
||||
*
|
||||
* 값을 비우면 「안 고름」으로 돌아가 기본값이 선다. 그 되돌리는 길이 있어야
|
||||
* 한 번 넣은 값이 영영 남지 않는다.
|
||||
*/
|
||||
function factorRow(
|
||||
kind: string,
|
||||
choice: ConversionFactorChoice,
|
||||
draft: Record<string, FactorDraft>,
|
||||
onChange: () => void,
|
||||
): HTMLElement {
|
||||
const box = document.createElement("div");
|
||||
box.className = "b08-quantity__factor";
|
||||
|
||||
const row = document.createElement("label");
|
||||
row.className = "b08-quantity__field";
|
||||
const name = document.createElement("span");
|
||||
name.textContent = groundLabel(kind);
|
||||
const input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.className = "b08-quantity__input";
|
||||
input.min = "0";
|
||||
input.step = "0.01";
|
||||
input.placeholder = String(choice.default);
|
||||
const current = draft[kind]?.compacted;
|
||||
input.value = current === null || current === undefined ? "" : String(current);
|
||||
row.append(name, input);
|
||||
box.append(row);
|
||||
|
||||
const range = (choice.range ?? null) as [number, number] | null;
|
||||
box.append(
|
||||
hint(
|
||||
`${L("B08_Quantity_Factor_Default")} ${choice.default}` +
|
||||
(range
|
||||
? ` · ${L("B08_Quantity_Factor_Range")} ${range[0].toFixed(2)}~${range[1].toFixed(2)}`
|
||||
: ""),
|
||||
),
|
||||
);
|
||||
|
||||
// 사유 칸은 **범위 밖일 때만** 선다 — 늘 띄우면 채우지 않아도 되는 칸으로 읽힌다.
|
||||
const reasonRow = document.createElement("label");
|
||||
reasonRow.className = "b08-quantity__field";
|
||||
const reasonName = document.createElement("span");
|
||||
reasonName.textContent = L("B08_Quantity_Factor_Reason");
|
||||
const reasonInput = document.createElement("input");
|
||||
reasonInput.type = "text";
|
||||
reasonInput.className = "b08-quantity__input";
|
||||
reasonInput.value = draft[kind]?.reason ?? "";
|
||||
reasonRow.append(reasonName, reasonInput);
|
||||
const warning = hint(L("B08_Quantity_Factor_OutOfRange"));
|
||||
warning.classList.add("b08-quantity__hint--warn");
|
||||
|
||||
const sync = (): void => {
|
||||
const value = input.value.trim() === "" ? null : Number(input.value);
|
||||
const outside = value !== null && Number.isFinite(value) && outOfRange(value, range);
|
||||
warning.hidden = !outside;
|
||||
reasonRow.hidden = !outside;
|
||||
};
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const raw = input.value.trim();
|
||||
const value = raw === "" ? null : Number(raw);
|
||||
draft[kind] = {
|
||||
compacted: value !== null && Number.isFinite(value) ? value : null,
|
||||
reason: draft[kind]?.reason ?? "",
|
||||
};
|
||||
sync();
|
||||
onChange();
|
||||
});
|
||||
reasonInput.addEventListener("input", () => {
|
||||
draft[kind] = {
|
||||
compacted: draft[kind]?.compacted ?? null,
|
||||
reason: reasonInput.value,
|
||||
};
|
||||
onChange();
|
||||
});
|
||||
|
||||
box.append(warning, reasonRow);
|
||||
sync();
|
||||
return box;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「토량환산계수(다짐)」 구획 전체. 서버가 준 갈래만 그린다 — 갈래 수를 화면에 안 박는다.
|
||||
*
|
||||
* `choices` 가 없으면(옛 응답) **아무것도 그리지 않는다** — 빈 칸을 지어내지 않는다.
|
||||
*/
|
||||
export function renderConversionFactorFields(
|
||||
choices: Record<string, ConversionFactorChoice> | undefined,
|
||||
pumsem: PumsemRange[] | undefined,
|
||||
draft: Record<string, FactorDraft>,
|
||||
onChange: () => void,
|
||||
): HTMLElement | null {
|
||||
const entries = Object.entries(choices ?? {});
|
||||
if (!entries.length) return null;
|
||||
|
||||
const box = document.createElement("div");
|
||||
// 제 제목을 제 안에 들고 있어 이 구획 자신이 공용 접기 컨테이너가 된다(B03~B07 과 같은 틀).
|
||||
box.className = "b08-quantity__factors ui-collapsible";
|
||||
const title = document.createElement("div");
|
||||
title.className = "b08-quantity__field ui-collapsible__title";
|
||||
const titleName = document.createElement("span");
|
||||
titleName.textContent = L("B08_Quantity_Side_Factors");
|
||||
title.append(titleName);
|
||||
box.append(title);
|
||||
// 어디까지 닿는 값인지 먼저 보인다 — 토적표만 바뀌는 줄 알면 함부로 고친다.
|
||||
box.append(hint(L("B08_Quantity_Factor_Reach")));
|
||||
|
||||
for (const [kind, choice] of entries) {
|
||||
box.append(factorRow(kind, choice, draft, onChange));
|
||||
}
|
||||
|
||||
// 품셈 암종별 범위 — 서버가 내려 준 값을 그대로 보인다(화면에 다시 적지 않는다).
|
||||
if (pumsem?.length) {
|
||||
box.append(
|
||||
hint(
|
||||
`${L("B08_Quantity_Factor_Pumsem")} — ` +
|
||||
pumsem
|
||||
.map((item) => `${item.name} ${item.min.toFixed(2)}~${item.max.toFixed(2)}`)
|
||||
.join(" · "),
|
||||
),
|
||||
);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 저장 몸통에 실을 모양 — **고른 갈래만** 담는다. 빈 dict 는 「전부 기본값」이다. */
|
||||
export function conversionOverridePayload(
|
||||
draft: Record<string, FactorDraft>,
|
||||
): Record<string, { compacted: number; reason?: string }> {
|
||||
const payload: Record<string, { compacted: number; reason?: string }> = {};
|
||||
for (const [kind, entry] of Object.entries(draft)) {
|
||||
if (entry.compacted === null || !Number.isFinite(entry.compacted) || entry.compacted <= 0) {
|
||||
continue;
|
||||
}
|
||||
payload[kind] = entry.reason.trim()
|
||||
? { compacted: entry.compacted, reason: entry.reason.trim() }
|
||||
: { compacted: entry.compacted };
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
@@ -14,6 +14,13 @@
|
||||
* 원가 쪽(줄마다 원 단위 절사)과 규칙이 반대이므로 그 코드를 여기로 옮기지 말 것.
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
attachProvenance,
|
||||
markProvenanceCell,
|
||||
type ProvenancePayload,
|
||||
type ProvenanceSheet,
|
||||
} from "@ui/ui_template_provenance";
|
||||
|
||||
/** 서버가 주는 토적표 한 줄. 이름은 엔진(`B08_Quantity_Engine_EarthworkTable.py`)과 같다. */
|
||||
export interface EarthworkRow {
|
||||
chainage_m: number;
|
||||
@@ -61,6 +68,8 @@ export interface SlopeTable {
|
||||
|
||||
/** 산출 조건 — `project_settings.json` 의 `quantity` 구획. */
|
||||
export interface QuantitySettings {
|
||||
/** 고른 토량환산계수 — `{갈래: {compacted, reason?}}`. 안 고르면 키가 없다. */
|
||||
conversion_factors_override?: Record<string, { compacted?: number; reason?: string }> | null;
|
||||
rock_class_set?: string;
|
||||
rock_classes?: string[];
|
||||
rock_ratios_pct?: Record<string, number>;
|
||||
@@ -95,6 +104,25 @@ export interface QuantitySettings {
|
||||
wood_chipping_volume_m3?: number | null;
|
||||
}
|
||||
|
||||
/** 갈래 하나의 「무엇을 골랐나」. 서버 `earthwork_conversion_choices` 와 짝이다. */
|
||||
export interface ConversionFactorChoice {
|
||||
compacted: number;
|
||||
default: number;
|
||||
/** 기본값과 다른 값을 골랐나. */
|
||||
chosen: boolean;
|
||||
/** 품셈 범위 안인가. 밖이어도 **막지 않고** 사유를 받는다. */
|
||||
in_range: boolean;
|
||||
range: [number, number] | null;
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
/** 품셈 암종별 체적변화율 범위 — **화면 안내용**이고 계산에 안 쓴다. */
|
||||
export interface PumsemRange {
|
||||
name: string;
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface EarthworkTable {
|
||||
method: string;
|
||||
station_count: number;
|
||||
@@ -109,6 +137,12 @@ export interface EarthworkTable {
|
||||
/** 운반계획은 [저장]·[확정]에서 정본에 남는 값 — 아직 없으면 false. */
|
||||
haul_available?: boolean;
|
||||
settings?: QuantitySettings;
|
||||
/** 갈래별 토량환산계수 선택 상태 — 산출 조건 패널이 그린다. */
|
||||
conversion_factor_choices?: Record<string, ConversionFactorChoice>;
|
||||
/** 품셈 암종별 범위(안내용). 정의처가 서버라 내려받아 보인다. */
|
||||
conversion_factor_pumsem_ranges?: PumsemRange[];
|
||||
/** 근거 사전 — ⚠ **개발환경에서만** 실려 온다. 운영에서는 칸 자체가 없다. */
|
||||
provenance?: ProvenancePayload;
|
||||
}
|
||||
|
||||
/** 표 칸에 들어갈 수 있는 열 — 숫자 칸만 고른다(사유·주기는 표 밖이다). */
|
||||
@@ -309,7 +343,11 @@ function buildHead(): HTMLTableSectionElement {
|
||||
return head;
|
||||
}
|
||||
|
||||
function buildBody(rows: EarthworkRow[], slope?: SlopeTable): HTMLTableSectionElement {
|
||||
function buildBody(
|
||||
rows: EarthworkRow[],
|
||||
slope?: SlopeTable,
|
||||
sheet?: ProvenanceSheet,
|
||||
): HTMLTableSectionElement {
|
||||
const body = document.createElement("tbody");
|
||||
const columns = flatColumns();
|
||||
const slopeByChainage = new Map((slope?.rows ?? []).map((row) => [row.chainage_m, row]));
|
||||
@@ -321,9 +359,15 @@ function buildBody(rows: EarthworkRow[], slope?: SlopeTable): HTMLTableSectionEl
|
||||
td.textContent =
|
||||
index === 0 ? stationLabel(row.chainage_m) : cell(row[column.key], column.digits);
|
||||
if (index === 0) td.className = "b08-grid__station";
|
||||
// 근거 호버·등급색은 **사전이 왔을 때만** 붙는다(개발환경).
|
||||
const columnProvenance = sheet?.columns[column.key];
|
||||
if (columnProvenance) markProvenanceCell(td, column.key, columnProvenance.tier);
|
||||
tr.append(td);
|
||||
});
|
||||
|
||||
// 줄마다 갈리는 사유(측구 안분 폴백 등)는 열 사전이 못 든다 — 줄에 실어 카드가 덧붙게 한다.
|
||||
if (row.notes?.length) tr.dataset.provNotes = row.notes.join("\n");
|
||||
|
||||
const slopeRow = slopeByChainage.get(row.chainage_m);
|
||||
// 사면이 원지반을 못 만난 측점은 값이 잘려 있다 — 줄에 표시를 남긴다(PLAN 8-4b).
|
||||
if (slopeRow?.unclosed) tr.classList.add("is-unclosed");
|
||||
@@ -433,11 +477,21 @@ export function renderEarthworkGrid(table: EarthworkTable): HTMLElement {
|
||||
scroller.className = "b08-grid__scroll";
|
||||
const element = document.createElement("table");
|
||||
element.className = "b08-grid__table";
|
||||
const sheet = table.provenance?.sheets?.earthwork;
|
||||
element.append(
|
||||
buildHead(),
|
||||
buildBody(table.rows, table.slope),
|
||||
buildBody(table.rows, table.slope, sheet),
|
||||
buildFoot(table.totals, table.slope),
|
||||
);
|
||||
// 사전이 없으면 아무 일도 안 한다 — 빈 카드를 띄우면 「설명이 있다」는 거짓만 남는다.
|
||||
// 줄 사유는 **그 사유가 닿는 열에만** 붙인다. 줄에 달렸다고 십몇 칸에 다 띄우면
|
||||
// 「절토 보정량」 카드에 「측구 가름값…」 이 떠서 읽는 사람을 속인다(2026-09-12 실측).
|
||||
// ⚠ 지금 줄 사유는 **측구 안분 폴백 하나뿐**이라 여기서 열 이름으로 가른다.
|
||||
// 사유가 늘면 엔진이 「어느 열에 닿는 사유인가」를 같이 내는 쪽이 맞다.
|
||||
attachProvenance(element, sheet, (cell, columnKey) => {
|
||||
if (!columnKey.startsWith("ditch_")) return [];
|
||||
return (cell.closest("tr")?.dataset.provNotes ?? "").split("\n").filter(Boolean);
|
||||
});
|
||||
|
||||
if (table.slope) {
|
||||
const notice = buildUnclosedNotice(table.slope, element);
|
||||
|
||||
@@ -186,6 +186,38 @@ const CSS = `
|
||||
.b08-quantity__field-value { color: var(--color-text-secondary); font-variant-numeric: tabular-nums; }
|
||||
/* 칸 밑 근거 한 줄 — 왜 그 값인지 화면에서 보이게 한다(2026-09-09 사용자 지시). */
|
||||
.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; }
|
||||
`;
|
||||
|
||||
/** 스타일을 한 번만 넣는다 — 페이지를 다시 그려도 중복되지 않는다. */
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { stationLabel } from "./B08_Quantity_UI_EarthworkGrid";
|
||||
import {
|
||||
attachProvenance,
|
||||
markProvenanceCell,
|
||||
type ProvenancePayload,
|
||||
type ProvenanceSheet,
|
||||
} from "@ui/ui_template_provenance";
|
||||
|
||||
export interface MaterialRow {
|
||||
name: string;
|
||||
@@ -27,6 +33,38 @@ export interface MaterialRow {
|
||||
sources: string[];
|
||||
}
|
||||
|
||||
/** 줄 하나의 칸에 열 키를 차례대로 심는다 (집계표 쪽과 같은 틀). */
|
||||
function markRow(tr: HTMLTableRowElement, keys: readonly (string | null)[]): void {
|
||||
[...tr.children].forEach((cell, index) => {
|
||||
const key = keys[index];
|
||||
if (key) markProvenanceCell(cell as HTMLElement, key);
|
||||
});
|
||||
}
|
||||
|
||||
/** 자재총괄 열 차례 — 비고는 사전을 안 붙인다. */
|
||||
const MATERIAL_KEYS = [
|
||||
"name",
|
||||
"unit",
|
||||
"net_amount",
|
||||
"surcharge_pct",
|
||||
"total_amount",
|
||||
"supply",
|
||||
"install_by",
|
||||
null,
|
||||
] as const;
|
||||
|
||||
/** 구조물 원단위 열 차례 — 근거·출처는 **이미 설명 글**이라 카드를 거듭 안 띄운다. */
|
||||
const UNIT_QUANTITY_KEYS = [
|
||||
"structure",
|
||||
"spec",
|
||||
"component",
|
||||
"unit",
|
||||
"amount",
|
||||
"destination",
|
||||
null,
|
||||
null,
|
||||
] as const;
|
||||
|
||||
export interface MaterialTable {
|
||||
columns: string[];
|
||||
rows: MaterialRow[];
|
||||
@@ -95,6 +133,8 @@ export interface MaterialResponse {
|
||||
skipped_structures: string[];
|
||||
structure_count: number;
|
||||
/** 인계에서 온 묶음 조각 — 화면이 「무엇으로 나뉘어 서는지」를 보인다. */
|
||||
/** 근거 사전 — ⚠ **개발환경에서만** 실려 온다. 운영에서는 칸 자체가 없다. */
|
||||
provenance?: ProvenancePayload;
|
||||
composite?: {
|
||||
name: string;
|
||||
parts: CompositePart[];
|
||||
@@ -244,6 +284,7 @@ function spreadLine(spread: MaterialTable["amount_spread"], title: string): HTML
|
||||
export function renderMaterialGrid(
|
||||
table: MaterialTable,
|
||||
options?: MaterialGridOptions,
|
||||
sheet?: ProvenanceSheet,
|
||||
): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b08-grid";
|
||||
@@ -330,10 +371,12 @@ export function renderMaterialGrid(
|
||||
tr.append(textCell(row.install_by_label));
|
||||
}
|
||||
tr.append(textCell(row.note, "b08-grid__note"));
|
||||
markRow(tr, MATERIAL_KEYS);
|
||||
body.append(tr);
|
||||
}
|
||||
|
||||
element.append(body);
|
||||
attachProvenance(element, sheet);
|
||||
scroller.append(element);
|
||||
wrap.append(scroller);
|
||||
return wrap;
|
||||
@@ -465,11 +508,15 @@ export function renderUnitQuantityGrid(response: MaterialResponse): HTMLElement
|
||||
// 나중에 「이 값이 왜 이런가」를 되짚을 수 있다.
|
||||
const kind = component.basis_kind === "observed" ? "실무 관측" : "치수 전개";
|
||||
tr.append(textCell(component.reuse_count ? `${kind} · ${component.reuse_count}회` : kind));
|
||||
markRow(tr, UNIT_QUANTITY_KEYS);
|
||||
body.append(tr);
|
||||
}
|
||||
}
|
||||
|
||||
element.append(body);
|
||||
// 성분이 없는 줄은 칸을 붙여 쌀으므로(`colSpan`) 짚지 않았다 — 차례가 어긋나면
|
||||
// 엉뚱한 열의 설명이 뜼다.
|
||||
attachProvenance(element, response.provenance?.sheets?.unit_quantity);
|
||||
scroller.append(element);
|
||||
wrap.append(scroller);
|
||||
return wrap;
|
||||
|
||||
@@ -11,6 +11,9 @@ 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 { createProvenanceToggle } from "@ui/ui_template_provenance";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import {
|
||||
fetchWorkflowState,
|
||||
@@ -18,6 +21,11 @@ import {
|
||||
WORKFLOW_STEP_ROUTES,
|
||||
} from "../A00_Common/b_workflow_nav";
|
||||
import { renderEarthworkGrid, type EarthworkTable } from "./B08_Quantity_UI_EarthworkGrid";
|
||||
import {
|
||||
conversionOverridePayload,
|
||||
renderConversionFactorFields,
|
||||
type FactorDraft,
|
||||
} from "./B08_Quantity_UI_ConversionFactors";
|
||||
import { injectEarthworkGridStyles } from "./B08_Quantity_UI_EarthworkGrid_Style";
|
||||
import {
|
||||
renderHaulGrid,
|
||||
@@ -98,6 +106,8 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr
|
||||
wood_chipping_volume_m3: draft.wood_chipping_volume_m3,
|
||||
// 개소는 **통째로** 보낸다 — 지운 항목까지 그대로 가야 되돌릴 길이 있다.
|
||||
ancillary_counts: draft.ancillary_counts,
|
||||
// 토량환산계수 — 고른 갈래만 담긴다. 빈 dict 는 「전부 기본값으로 되돌림」이다.
|
||||
conversion_factors_override: conversionOverridePayload(draft.conversion_factors),
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -243,16 +253,6 @@ function selectField(
|
||||
}
|
||||
|
||||
/** 지반 종류 표기 — 서버 키가 화면에 새지 않게. 모르는 키는 그대로 보인다. */
|
||||
const GROUND_LABELS: Record<string, string> = {
|
||||
soil: "토사",
|
||||
ripping_rock: "리핑암",
|
||||
blasting_rock: "발파암",
|
||||
};
|
||||
|
||||
function groundLabel(kind: string): string {
|
||||
return GROUND_LABELS[kind] ?? kind;
|
||||
}
|
||||
|
||||
/** 자재 한 줄의 관급/사급. `install_by` 는 **관급 줄에만** 뜻이 있다. */
|
||||
export interface SupplyChoice {
|
||||
supply: string;
|
||||
@@ -291,6 +291,8 @@ interface DraftSettings {
|
||||
wood_chipping_volume_m3: number | null;
|
||||
// 자재별 관급/사급 — 표 안에서 줄마다 고른 값.
|
||||
material_supply: Record<string, SupplyChoice>;
|
||||
// 갈래별 토량환산계수(다짐) — `compacted` 가 `null` 이면 「안 고름」이라 기본값이 선다.
|
||||
conversion_factors: Record<string, FactorDraft>;
|
||||
dirty: boolean;
|
||||
}
|
||||
|
||||
@@ -354,7 +356,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;
|
||||
@@ -383,18 +388,18 @@ function buildQuantitySidePanel(
|
||||
panel.append(devUnlockRow(projectId, reload));
|
||||
}
|
||||
|
||||
// 계수는 서버 상수가 유일한 정의처다 — 화면은 보여 주기만 하고 값을 다시 적지 않는다.
|
||||
panel.append(field(L("B08_Quantity_Side_Method"), L("B08_Quantity_Side_Method_Value")));
|
||||
const entries = Object.entries(table?.conversion_factors ?? {});
|
||||
if (entries.length) {
|
||||
panel.append(field(L("B08_Quantity_Side_Factors"), ""));
|
||||
for (const [kind, value] of entries) {
|
||||
// ⚠ 서버 키(`soil`·`ripping_rock`·`blasting_rock`)를 그대로 내보내지 않는다 —
|
||||
// 2026-09-08 ㉕ 화면 통과에서 좌측 세 줄이 개발자 키로 떠 있었다(`soil_guard` 와 같은 병).
|
||||
// 모르는 키는 **지어내지 않고** 그대로 보인다.
|
||||
panel.append(field(groundLabel(kind), String((value as { compacted: number }).compacted)));
|
||||
}
|
||||
}
|
||||
// 토량환산계수 — **고를 수 있는 값**이다(오솔길 대조 06절 3번). 기본값 정의처는 서버 한 곳이고,
|
||||
// 화면은 고른 값만 보낸다. 유토곡선·운반표·기초단가가 같이 읽는다는 안내도 그 칸이 낸다.
|
||||
const factorFields = renderConversionFactorFields(
|
||||
table?.conversion_factor_choices,
|
||||
table?.conversion_factor_pumsem_ranges,
|
||||
draft.conversion_factors,
|
||||
() => {
|
||||
draft.dirty = true;
|
||||
},
|
||||
);
|
||||
if (factorFields) panel.append(factorFields);
|
||||
|
||||
// ── 지반 구성비 — 갈래 수는 프로젝트 세트가 정한다(코드에 안 박음, PLAN 8-13) ──
|
||||
const classes = [...(table?.summary?.rock_classes ?? [])];
|
||||
@@ -641,6 +646,8 @@ function buildQuantitySidePanel(
|
||||
},
|
||||
),
|
||||
);
|
||||
// ⚠ `?.` 이 빠지면 표를 못 받은 때(`table === null`) 여기서 터져 **페이지가 통째로
|
||||
// 백지**가 된다 — 정작 보여야 할 「표를 못 불렀다」 안내까지 같이 사라진다(2026-09-12 실측).
|
||||
const placing = (
|
||||
table as unknown as {
|
||||
concrete_placing?: {
|
||||
@@ -648,8 +655,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;
|
||||
@@ -744,6 +751,10 @@ function buildQuantitySidePanel(
|
||||
// TODO(미결) — 설정의 초기값을 무엇으로 볼지 사용자 확인 뒤에 붙인다.
|
||||
actions.append(saveButton, confirmButton);
|
||||
panel.append(actions);
|
||||
|
||||
// 조건 칸을 B03~B07 공통 상자로 묶고 제목 클릭으로 접히게 한다.
|
||||
groupPanelSections(panel);
|
||||
attachCollapsible(panel);
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -769,6 +780,14 @@ function buildQuantityBody(
|
||||
return element;
|
||||
};
|
||||
|
||||
// 등급색 토글 — ⚠ **사전이 왔을 때만** 만든다(개발환경). 배포 빌드에서는 단추 자체가 없다.
|
||||
if (
|
||||
(table as unknown as { provenance?: unknown } | null)?.provenance ||
|
||||
(material as unknown as { provenance?: unknown } | null)?.provenance
|
||||
) {
|
||||
tabs.append(createProvenanceToggle(body));
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
body.append(tabs, message(L("B08_Quantity_Grid_Failed")));
|
||||
return body;
|
||||
@@ -783,13 +802,19 @@ function buildQuantityBody(
|
||||
{
|
||||
label: L("B08_Quantity_Tab_Summary"),
|
||||
build: () =>
|
||||
table.summary ? renderSummaryGrid(table.summary) : message(L("B08_Quantity_Grid_Empty")),
|
||||
table.summary
|
||||
? renderSummaryGrid(table.summary, table.provenance?.sheets?.summary)
|
||||
: message(L("B08_Quantity_Grid_Empty")),
|
||||
},
|
||||
{
|
||||
label: L("B08_Quantity_Tab_Haul"),
|
||||
build: () =>
|
||||
table.haul
|
||||
? renderHaulGrid(table.haul, Boolean(table.haul_available))
|
||||
? renderHaulGrid(
|
||||
table.haul,
|
||||
Boolean(table.haul_available),
|
||||
table.provenance?.sheets?.haul,
|
||||
)
|
||||
: message(L("B08_Quantity_Haul_Missing")),
|
||||
},
|
||||
{
|
||||
@@ -797,7 +822,7 @@ function buildQuantityBody(
|
||||
build: () => {
|
||||
const preparation = (table as unknown as { preparation?: PreparationTable }).preparation;
|
||||
return preparation
|
||||
? renderPreparationGrid(preparation)
|
||||
? renderPreparationGrid(preparation, table.provenance?.sheets?.preparation)
|
||||
: message(L("B08_Quantity_Grid_Empty"));
|
||||
},
|
||||
},
|
||||
@@ -810,12 +835,16 @@ function buildQuantityBody(
|
||||
label: L("B08_Quantity_Tab_Material"),
|
||||
build: () =>
|
||||
material
|
||||
? renderMaterialGrid(material.material, {
|
||||
choices: draft.material_supply,
|
||||
onChange: () => {
|
||||
draft.dirty = true;
|
||||
? renderMaterialGrid(
|
||||
material.material,
|
||||
{
|
||||
choices: draft.material_supply,
|
||||
onChange: () => {
|
||||
draft.dirty = true;
|
||||
},
|
||||
},
|
||||
})
|
||||
material.provenance?.sheets?.material,
|
||||
)
|
||||
: message(L("B08_Quantity_Material_Failed")),
|
||||
},
|
||||
];
|
||||
@@ -887,6 +916,16 @@ export async function renderB08Quantity(root: HTMLElement): Promise<void> {
|
||||
...((stored.ancillary_counts ?? {}) as Record<string, number | null>),
|
||||
},
|
||||
material_supply: { ...((stored.material_supply ?? {}) as Record<string, SupplyChoice>) },
|
||||
// ⚠ 저장분에 있는 갈래만 담는다 — 기본값을 복사해 넣으면 「안 고름」이 사라진다.
|
||||
conversion_factors: Object.fromEntries(
|
||||
Object.entries(stored.conversion_factors_override ?? {}).map(([kind, entry]) => [
|
||||
kind,
|
||||
{
|
||||
compacted: typeof entry?.compacted === "number" ? entry.compacted : null,
|
||||
reason: typeof entry?.reason === "string" ? entry.reason : "",
|
||||
},
|
||||
]),
|
||||
),
|
||||
dirty: false,
|
||||
};
|
||||
const reload = (): void => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,38 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import {
|
||||
attachProvenance,
|
||||
markProvenanceCell,
|
||||
type ProvenanceSheet,
|
||||
} from "@ui/ui_template_provenance";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
/** 줄 하나의 칸에 열 키를 차례대로 심는다.
|
||||
|
||||
* 이 세 표는 칸을 `textCell()` 로 줄지어 붙이므로 **다 지은 뒤에 차례로 짚는 것**이
|
||||
* 가장 적게 고치는 길이다. `override` 는 **그 칸만 열 등급을 이기는** 자리다 —
|
||||
* 같은 열이라도 줄마다 성격이 갈리는 것(내역 제외 줄·값을 못 세운 줄)을 위한 것이다.
|
||||
*/
|
||||
function markRow(
|
||||
tr: HTMLTableRowElement,
|
||||
keys: readonly (string | null)[],
|
||||
override?: Record<number, string | undefined>,
|
||||
): void {
|
||||
[...tr.children].forEach((cell, index) => {
|
||||
const key = keys[index];
|
||||
if (key) markProvenanceCell(cell as HTMLElement, key, override?.[index]);
|
||||
});
|
||||
}
|
||||
|
||||
/** 열 차례 — 비고는 사유 글이라 사전을 안 붙인다(`null`). */
|
||||
const SUMMARY_KEYS = ["group", "item", "spec", "unit", "amount", null] as const;
|
||||
const HAUL_KEYS = ["equipment", "ground", "volume_m3", "average_distance_m", "legs", null] as const;
|
||||
const PREPARATION_KEYS = ["group", "item", "unit", "amount", "status", null] as const;
|
||||
|
||||
export interface SummaryRow {
|
||||
group: string;
|
||||
item: string;
|
||||
@@ -77,7 +104,7 @@ function textCell(text: string, className?: string): HTMLTableCellElement {
|
||||
}
|
||||
|
||||
/** 토공집계표 — 실무 시트와 같은 여섯 열. */
|
||||
export function renderSummaryGrid(table: SummaryTable): HTMLElement {
|
||||
export function renderSummaryGrid(table: SummaryTable, sheet?: ProvenanceSheet): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b08-grid";
|
||||
|
||||
@@ -114,17 +141,25 @@ export function renderSummaryGrid(table: SummaryTable): HTMLElement {
|
||||
note.prepend(tag);
|
||||
}
|
||||
tr.append(note);
|
||||
// ⚠ 무대(소운반 20m)처럼 **집계에는 오르되 내역 줄이 아닌** 줄은 「계」가
|
||||
// 최종이 아니라 **제외**임(품셀 1-2-7). 못 세운 것과 뜻이 정반대라 칸 등급을 갈라 준다.
|
||||
markRow(tr, SUMMARY_KEYS, row.in_bill ? undefined : { 4: "excluded" });
|
||||
body.append(tr);
|
||||
}
|
||||
|
||||
element.append(head, body);
|
||||
attachProvenance(element, sheet);
|
||||
scroller.append(element);
|
||||
wrap.append(scroller);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/** 운반거리 — 내역 줄(가중평균)과 근거 줄을 나눠 보인다. */
|
||||
export function renderHaulGrid(table: HaulTable, available: boolean): HTMLElement {
|
||||
export function renderHaulGrid(
|
||||
table: HaulTable,
|
||||
available: boolean,
|
||||
sheet?: ProvenanceSheet,
|
||||
): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b08-grid";
|
||||
|
||||
@@ -180,10 +215,13 @@ export function renderHaulGrid(table: HaulTable, available: boolean): HTMLElemen
|
||||
note.append(document.createTextNode(" 품셈 1-2-7 소운반 20m 이내는 품에 포함"));
|
||||
}
|
||||
tr.append(note);
|
||||
// 내역 줄이 안 되는 줄은 토량·거리 둘 다 「제외」임 — 값은 검산에만 쓴다.
|
||||
markRow(tr, HAUL_KEYS, row.in_bill ? undefined : { 2: "excluded", 3: "excluded" });
|
||||
body.append(tr);
|
||||
}
|
||||
|
||||
element.append(head, body);
|
||||
attachProvenance(element, sheet);
|
||||
scroller.append(element);
|
||||
wrap.append(scroller);
|
||||
return wrap;
|
||||
@@ -212,7 +250,10 @@ export interface PreparationTable {
|
||||
* 빈 표를 내면 「빠뜨린 것」과 「원래 없는 것」이 구별되지 않는다. 그래서 값이 없는 줄도
|
||||
* 상태와 사유를 달아 그대로 세운다.
|
||||
*/
|
||||
export function renderPreparationGrid(table: PreparationTable): HTMLElement {
|
||||
export function renderPreparationGrid(
|
||||
table: PreparationTable,
|
||||
sheet?: ProvenanceSheet,
|
||||
): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b08-grid";
|
||||
|
||||
@@ -251,10 +292,13 @@ export function renderPreparationGrid(table: PreparationTable): HTMLElement {
|
||||
note.append(document.createTextNode(` (참고 면적 ${num(row.reference_amount, 1)})`));
|
||||
}
|
||||
tr.append(note);
|
||||
// 값을 못 세운 줄의 「수량」은 **막힘** — 근거가 오면 채워질 자리라 제외과 갈라 보인다.
|
||||
markRow(tr, PREPARATION_KEYS, row.amount === null ? { 3: "blocked" } : undefined);
|
||||
body.append(tr);
|
||||
}
|
||||
|
||||
element.append(head, body);
|
||||
attachProvenance(element, sheet);
|
||||
scroller.append(element);
|
||||
wrap.append(scroller);
|
||||
return wrap;
|
||||
|
||||
@@ -152,7 +152,22 @@ class BillRow:
|
||||
expense_krw: Decimal = _ZERO
|
||||
is_group: bool = False
|
||||
in_bill: bool = True
|
||||
note: str = ""
|
||||
#: 줄 사유 **조각** — `(닿는 열 키, 글)`. 화면 「비고」는 이것을 이어 붙인 것이고,
|
||||
#: 근거 호버는 열 키로 걸러 **그 사유가 닿는 칸에만** 띄운다(PLAN 8-36 ㉮).
|
||||
#: ⚠ 종전엔 `note` 한 칸에 덮어썼다 — 한 줄에 사유가 둘이면 **하나가 조용히 사라졌다**
|
||||
#: (막힘 사유가 먼저 적힌 반영률 문구를 지웠다, 2026-09-12 데스크탑 보조 조사 ㉮).
|
||||
#: 그래서 **덮지 않고 쌓는다.** 열을 못 짚는 줄 전체 사유는 키를 빈 글로 둔다.
|
||||
notes: list[tuple[str, str]] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def note(self) -> str:
|
||||
"""화면 「비고」 칸 — 조각을 종전과 **같은 꼴**로 이어 붙인다."""
|
||||
return " / ".join(text for _, text in self.notes if text)
|
||||
|
||||
def add_note(self, column: str, text: str) -> None:
|
||||
"""사유 한 조각을 **쌓는다**. `column` 은 그 사유가 닿는 열 키(줄 전체면 빈 글)."""
|
||||
if text:
|
||||
self.notes.append((column, text))
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
def money(value: Decimal | None) -> str | None:
|
||||
@@ -185,6 +200,9 @@ class BillRow:
|
||||
"is_group": self.is_group,
|
||||
"in_bill": self.in_bill,
|
||||
"note": self.note,
|
||||
#: 근거 호버용 — 어느 사유가 **어느 열**에 닿는지까지 실어 보낸다.
|
||||
#: 화면 「비고」 칸은 위 `note` 그대로라 토글을 끈 사용자도 사유를 그대로 본다.
|
||||
"notes": [{"column": column, "text": text} for column, text in self.notes],
|
||||
}
|
||||
|
||||
|
||||
@@ -514,7 +532,8 @@ def build_bill(
|
||||
continue
|
||||
entry = sheet.by_unit_price(f"B-{row.code}")
|
||||
if entry is not None:
|
||||
row.note = " / ".join(part for part in (entry.label, row.note) if part)
|
||||
# 종전처럼 **맨 앞**에 놓는다 — 실무 참조번호(「단산 46」)가 먼저 읽혀야 한다.
|
||||
row.notes.insert(0, ("unit_price_krw", entry.label))
|
||||
result.price_basis = sheet
|
||||
|
||||
if any(m.surcharge_pct is None for m in materials):
|
||||
|
||||
@@ -83,7 +83,7 @@ def _composite_row(
|
||||
|
||||
if missing_parts or money is None:
|
||||
detail_text = "; ".join(reasons[:3]) or ", ".join(missing_parts[:4])
|
||||
row.note = f"묶음 조각이 덜 찼습니다 — {detail_text}"
|
||||
row.add_note("quantity", f"묶음 조각이 덜 찼습니다 — {detail_text}")
|
||||
result.missing.append(
|
||||
{
|
||||
"name": row.name,
|
||||
@@ -103,7 +103,7 @@ def _composite_row(
|
||||
row.material_krw = line.material
|
||||
row.labor_krw = line.labor
|
||||
row.expense_krw = line.expense
|
||||
row.note = f"묶음 {len(item.composite_parts)}조각 합계"
|
||||
row.add_note("quantity", f"묶음 {len(item.composite_parts)}조각 합계")
|
||||
return row
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ def _excluded_row(item: HandoffWorkItem) -> BillRow:
|
||||
· ⚠ **여기서 세지 않는 줄**(`blocked_kind` 없음) — 「다른 표에서 이미 섬」·
|
||||
「이 노선엔 없음」. **이것을 할 일 목록에 얹으면 결국 이중계상이 된다.**
|
||||
"""
|
||||
return BillRow(
|
||||
row = BillRow(
|
||||
item_no="",
|
||||
level=1,
|
||||
code=item.work_item_code,
|
||||
@@ -125,10 +125,13 @@ def _excluded_row(item: HandoffWorkItem) -> BillRow:
|
||||
unit=item.unit,
|
||||
quantity=item.quantity,
|
||||
in_bill=False,
|
||||
note=item.blocked_reason
|
||||
or item.in_bill_reason
|
||||
or "합계 검산용 줄 — 금액을 매기지 않습니다.",
|
||||
)
|
||||
# 줄 하나가 통째로 빠지는 사유라 닿는 열이 없다 — 키를 비워 **모든 칸**에 따라붙게 둔다.
|
||||
row.add_note(
|
||||
"",
|
||||
item.blocked_reason or item.in_bill_reason or "합계 검산용 줄 — 금액을 매기지 않습니다.",
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
def _leaf_row(
|
||||
@@ -151,10 +154,10 @@ def _leaf_row(
|
||||
)
|
||||
if item.spec_class_basis:
|
||||
# 갈래 판정 근거는 **B08 문구를 그대로** 쓴다(두 벌로 짜지 않는다).
|
||||
row.note = " / ".join(part for part in (row.note, item.spec_class_basis) if part)
|
||||
row.add_note("spec", item.spec_class_basis)
|
||||
if item.application_ratio_pct is not None:
|
||||
# ⚠ 곱하지 않는다 — B08 이 이미 곱한 값이다. 산출근거로만 적는다.
|
||||
row.note = f"반영률 {item.application_ratio_pct}% 적용 후 수량"
|
||||
row.add_note("quantity", f"반영률 {item.application_ratio_pct}% 적용 후 수량")
|
||||
elif item.application_ratio_breakdown:
|
||||
# ⚠ **「율 없음」이 아니라 「갈래마다 다름」이다.** 율이 갈리는 줄은 B08 이 `pct` 를
|
||||
# 비우고 갈래로만 보낸다. 그 사실을 안 적으면 **값은 맞는데 왜 그 수량인지**를
|
||||
@@ -162,12 +165,12 @@ def _leaf_row(
|
||||
parts = ", ".join(
|
||||
f"{name} {value}%" for name, value in item.application_ratio_breakdown.items()
|
||||
)
|
||||
row.note = f"반영률이 갈래마다 다릅니다 — {parts} (적용 후 수량)"
|
||||
row.add_note("quantity", f"반영률이 갈래마다 다릅니다 — {parts} (적용 후 수량)")
|
||||
|
||||
if not item.in_bill:
|
||||
# 검산용 줄 — 수량은 보이되 **단가를 안 붙인다**(PLAN 8-7 ㉡ 와 같은 성격).
|
||||
row.quantity = item.quantity
|
||||
row.note = item.in_bill_reason or "합계 검산용 줄 — 금액을 매기지 않습니다."
|
||||
row.add_note("", item.in_bill_reason or "합계 검산용 줄 — 금액을 매기지 않습니다.")
|
||||
result.excluded.append(row)
|
||||
return row
|
||||
|
||||
@@ -177,13 +180,16 @@ def _leaf_row(
|
||||
# **주의 문구**였다 — 「관종을 안 정해 기본값(파형강관)으로 섰습니다」.
|
||||
# ⇒ 사유만 온 줄은 **금액을 세우고 그 문구를 곁말로** 단다.
|
||||
if item.blocked_reason and not item.blocked_kind:
|
||||
row.note = " / ".join(part for part in (row.note, f"ⓘ {item.blocked_reason}") if part)
|
||||
row.add_note("spec", f"ⓘ {item.blocked_reason}")
|
||||
|
||||
if item.blocked_reason and item.blocked_kind:
|
||||
# B08 이 「왜 못 골랐는지」를 적어 보냈다 — **그 문구를 그대로** 보인다.
|
||||
# 사용자가 입력하면 풀리는 것(`input_missing`)과 우리가 만들어야 하는 것을
|
||||
# 가르지 않으면, 사용자가 「후보를 고르면 되나」로 잘못 읽는다.
|
||||
row.note = f"{_BLOCKED_LABELS.get(item.blocked_kind, '막힘')} — {item.blocked_reason}"
|
||||
row.add_note(
|
||||
"unit_price_krw",
|
||||
f"{_BLOCKED_LABELS.get(item.blocked_kind, '막힘')} — {item.blocked_reason}",
|
||||
)
|
||||
result.missing.append(
|
||||
{
|
||||
"name": row.name,
|
||||
@@ -221,11 +227,14 @@ def _leaf_row(
|
||||
)
|
||||
if children:
|
||||
names = ", ".join(f"{c[2:]} {unit_prices.book.title(c).name}" for c in children)
|
||||
row.note = f"이 공종엔 일위대가가 없고 한 층 아래에 있습니다 — 후보: {names}"
|
||||
row.add_note(
|
||||
"unit_price_krw",
|
||||
f"이 공종엔 일위대가가 없고 한 층 아래에 있습니다 — 후보: {names}",
|
||||
)
|
||||
# 관경이 표 밖이면 **무엇을 정해야 하는지**까지 가리킨다.
|
||||
diameter_note = pipe_diameter_note(node.code, item.variant_value)
|
||||
if diameter_note:
|
||||
row.note = f"{row.note} / {diameter_note}"
|
||||
row.add_note("spec", diameter_note)
|
||||
reason = f"일위대가가 하위 공종에 있음(후보 {len(children)}건)"
|
||||
else:
|
||||
# ⚠ 「아직 안 만든 것」과 「성분이 빠져 못 세운 것」은 **할 일이 다르다**.
|
||||
@@ -233,10 +242,12 @@ def _leaf_row(
|
||||
# 표에 수량 칸이 비어 있고 「철근가공조립(간단)의 30 %」처럼 참조로만 적힌 자리).
|
||||
gap = unit_prices.component_gaps.get(node.code)
|
||||
if gap:
|
||||
row.note = f"성분이 빠져 단가를 못 세웠습니다 — {gap}"
|
||||
row.add_note("unit_price_krw", f"성분이 빠져 단가를 못 세웠습니다 — {gap}")
|
||||
reason = f"성분 미확보 — {gap}"
|
||||
else:
|
||||
row.note = "일위대가가 아직 없습니다 — 금액을 0 으로 때우지 않습니다."
|
||||
row.add_note(
|
||||
"unit_price_krw", "일위대가가 아직 없습니다 — 금액을 0 으로 때우지 않습니다."
|
||||
)
|
||||
reason = "일위대가 없음"
|
||||
result.missing.append(
|
||||
{
|
||||
@@ -254,7 +265,10 @@ def _leaf_row(
|
||||
if missing_basis:
|
||||
# ⚠ 밑수를 모르는 표다 — 「10㎡당」인지 「1㎡당」인지 모른 채 곱하면 10배·100배
|
||||
# 틀린다(떼채취가 실제로 100배였다). **곱하지 않고 드러낸다.**
|
||||
row.note = f"밑수(기준 수량)를 못 찾은 표입니다 — 곱하지 않았습니다. 원문 {missing_basis}"
|
||||
row.add_note(
|
||||
"unit_price_krw",
|
||||
f"밑수(기준 수량)를 못 찾은 표입니다 — 곱하지 않았습니다. 원문 {missing_basis}",
|
||||
)
|
||||
result.missing.append(
|
||||
{
|
||||
"name": row.name,
|
||||
@@ -275,8 +289,9 @@ def _leaf_row(
|
||||
missing_rows = unit_prices.unattached.get(node.code) or []
|
||||
if not why and missing_rows:
|
||||
why = f"{', '.join(missing_rows[:3])} 줄이 아직 안 붙었습니다"
|
||||
row.note = (
|
||||
f"단가가 일부만 섰습니다 — 붙은 몫 {covered}%" + (f" · {why}" if why else "") + "."
|
||||
row.add_note(
|
||||
"unit_price_krw",
|
||||
f"단가가 일부만 섰습니다 — 붙은 몫 {covered}%" + (f" · {why}" if why else "") + ".",
|
||||
)
|
||||
result.missing.append(
|
||||
{
|
||||
@@ -294,14 +309,10 @@ def _leaf_row(
|
||||
# ⚠ 품셈 표가 기준 단위를 안 준 단가다 — 「10㎡당」 같은 묶음 기준일 수 있다.
|
||||
# 값을 막지는 않되(막으면 대부분이 멈춘다) **모르는 채 곱했다는 사실을 적는다**.
|
||||
# 비고를 **덮지 않고 잇는다** — 반영률 문구가 먼저 적혀 있을 수 있다.
|
||||
row.note = " / ".join(
|
||||
part
|
||||
for part in (
|
||||
row.note,
|
||||
f"단가의 기준 단위가 표에 없습니다 — B08 수량 단위({row.unit})와 같다고 "
|
||||
"보고 곱했습니다. 확인 필요.",
|
||||
)
|
||||
if part
|
||||
row.add_note(
|
||||
"unit_price_krw",
|
||||
f"단가의 기준 단위가 표에 없습니다 — B08 수량 단위({row.unit})와 같다고 "
|
||||
"보고 곱했습니다. 확인 필요.",
|
||||
)
|
||||
|
||||
if title.unit and row.unit and not _same_unit(title.unit, row.unit):
|
||||
@@ -311,9 +322,10 @@ def _leaf_row(
|
||||
# 52,938.9 = 1,381,753원이라 **2.6 배 적은 금액**이 내역서에 든 셈이다.
|
||||
# 어느 쪽이 맞는지는 우리가 정할 일이 아니다 — **B08 이 면적을 보내거나 묶음
|
||||
# 조각으로 보내야** 풀린다. 그때까지 **금액을 만들지 않고 드러낸다.**
|
||||
row.note = (
|
||||
row.add_note(
|
||||
"amount_krw",
|
||||
f"단위가 안 맞습니다 — 수량은 {row.unit}, 단가는 {title.unit}당입니다. "
|
||||
"곱하면 금액이 틀리므로 비워 둡니다."
|
||||
"곱하면 금액이 틀리므로 비워 둡니다.",
|
||||
)
|
||||
result.missing.append(
|
||||
{
|
||||
@@ -332,13 +344,13 @@ def _leaf_row(
|
||||
# 그 밑수가 사용자 확정을 기다리고 있다(계획서 4-12 3단계).
|
||||
pending = pending_formula_note(node.code)
|
||||
if pending:
|
||||
row.note = " / ".join(part for part in (row.note, pending) if part)
|
||||
row.add_note("quantity", pending)
|
||||
|
||||
# ⚠ **원문에는 있는데 단가에 못 실린 몫**도 같은 자리에서 말한다. 금액이 서 있는 줄이라
|
||||
# 표시가 없으면 완성된 값으로 읽힌다(규준틀 둘이 인력만으로 492만원이었다).
|
||||
gap = known_gap_note(node.code)
|
||||
if gap:
|
||||
row.note = " / ".join(part for part in (row.note, gap) if part)
|
||||
row.add_note("unit_price_krw", gap)
|
||||
|
||||
# 쓰인 차례를 기억한다 — 실무 참조번호(「단산 46」)가 그 차례다.
|
||||
if price_code not in result.used_unit_prices:
|
||||
@@ -392,10 +404,13 @@ def _material_row(material: HandoffMaterial, result: BillResult) -> BillRow:
|
||||
spec=material.spec,
|
||||
unit=material.unit,
|
||||
quantity=material.total_amount,
|
||||
note=material.surcharge_note,
|
||||
)
|
||||
# 할증 사유는 **수량**에 닿는다 — 할증이 곱해진 뒤의 수량이기 때문이다.
|
||||
row.add_note("quantity", material.surcharge_note)
|
||||
if material.supply_type == SUPPLY_UNKNOWN:
|
||||
row.note = "관급·사급이 안 갈렸습니다 — 관급자재대에도, 도급 재료비에도 넣지 않습니다."
|
||||
row.add_note(
|
||||
"", "관급·사급이 안 갈렸습니다 — 관급자재대에도, 도급 재료비에도 넣지 않습니다."
|
||||
)
|
||||
result.missing.append(
|
||||
{
|
||||
"name": material.display_name,
|
||||
@@ -409,14 +424,16 @@ def _material_row(material: HandoffMaterial, result: BillResult) -> BillRow:
|
||||
# `owner_supplied` 인데 「사급 자재 단가 미확보」로 뜨고 있었다). 갈래마다 **가는 자리도
|
||||
# 원천도 다르다** — 관급은 총원가 밖 관급자재대(나라장터), 사급은 도급 재료비(물가지).
|
||||
if material.supply_type == SUPPLY_OWNER:
|
||||
row.note = (
|
||||
row.note
|
||||
or "관급 자재 단가 미확보 — 나라장터 목록에 이 품목이 없습니다. "
|
||||
"관급자재대(총원가 밖 별도 표기)로 갑니다."
|
||||
row.add_note(
|
||||
"unit_price_krw",
|
||||
"관급 자재 단가 미확보 — 나라장터 목록에 이 품목이 없습니다. "
|
||||
"관급자재대(총원가 밖 별도 표기)로 갑니다.",
|
||||
)
|
||||
reason = "관급 자재 단가 없음"
|
||||
else:
|
||||
row.note = row.note or "사급 자재 단가 미확보 — 6번 슬롯(적용 단가) 수동 입력 대기."
|
||||
row.add_note(
|
||||
"unit_price_krw", "사급 자재 단가 미확보 — 6번 슬롯(적용 단가) 수동 입력 대기."
|
||||
)
|
||||
reason = "사급 자재 단가 없음(미결 No.18)"
|
||||
|
||||
result.missing.append(
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
"""B09 원가 화면의 **근거 사전** — 어느 숫자가 어디서 와서 어떻게 나왔나 (PLAN 8-36 ④).
|
||||
|
||||
⚠⚠ **개발 전용.** 사전은 `provenance_payload()` 를 거쳐 나가고, 개발환경이 아니면 `None`
|
||||
이라 응답에 칸 자체가 안 생긴다. 화면에서 숨기는 것이 아니라 **안 보내는 것**이다.
|
||||
|
||||
왜 이 파일인가
|
||||
「식」과 「원천」의 정답은 값을 낳는 엔진이 안다. 화면 TS 에 손으로 적어 두면 엔진을
|
||||
고칠 때 설명만 옛것으로 남는다. 엔진 옆에 두어 같이 눈에 들어오게 한다.
|
||||
⚠ `B09_Estimation_UI_Page.ts`(1415줄)·`B09_Estimation_UnitPrice.py`(1173줄) 가 이미
|
||||
700줄을 크게 넘어 **새 파일로 뺐다**(PLAN 8-36 끝 ⚠).
|
||||
|
||||
⚠ **열 단위로 적는다.** 줄마다 갈리는 사유는 줄이 `notes` 로 들고 오고(내역서는 그 사유가
|
||||
**닿는 열 키**까지 함께 들고 온다 — `BillRow.notes`), 화면이 그 열의 칸에만 덧붙인다.
|
||||
|
||||
토적표와 맞대 본 것 (데스크탑 메인 요청)
|
||||
· B08 토적표에는 `final` 이 **한 열도 없었다** — 중간 장부이기 때문이다.
|
||||
· B09 는 반대로 `final` 이 분명히 있다 — 내역서 금액·자재대 금액이 그 자리다.
|
||||
⇒ 등급 여섯은 **한 장이 아니라 두 장을 합쳐야** 다 쓰인다.
|
||||
· 그래도 **원가계산서 「금액」은 열 단위로 `final` 을 못 붙였다.** 같은 열 안에서
|
||||
중간줄(간접노무비 따위)과 마지막줄(총원가·도급금액·총계)의 성격이 갈리는데 등급은
|
||||
**열에 하나**뿐이라서다. `calc` 로 두고 `rule` 에 어느 줄이 `final` 인지 적었다.
|
||||
⇒ 이 어긋남은 PLAN 8-36 ① 에 남긴다(칸 단위 등급이 필요한 첫 자리).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_provenance import (
|
||||
TIER_CALC,
|
||||
TIER_EXCLUDED,
|
||||
TIER_FINAL,
|
||||
TIER_STANDARD,
|
||||
TIER_SURVEY,
|
||||
ColumnProvenance,
|
||||
provenance_payload,
|
||||
sheet_provenance,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_Provenance_Common import _label, _note_column
|
||||
from B09_Estimation.B09_Estimation_Provenance_Sources import (
|
||||
base_reference_fuel_sheet,
|
||||
base_reference_labor_sheet,
|
||||
basis_sheet_tables,
|
||||
material_comparison_sheet,
|
||||
price_basis_sheet,
|
||||
)
|
||||
|
||||
#: 요율이 어디서 오는지 — 원가계산서 여러 열이 같은 문장을 쓴다.
|
||||
_RATE_SOURCE = (
|
||||
"요율 판 `resources/data_cost_input_value/rates_2026.json` — "
|
||||
"공사금액·공사기간 구간으로 골라 씀(`B09_Estimation_Rates.py:180 select_bracket`). "
|
||||
"어느 판으로 섰는지는 좌측 패널 「요율 판」과 산출기초 ① 에 지문까지 남음"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ① 공사원가계산서
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def cost_sheet() -> dict[str, Any]:
|
||||
"""열 키는 화면 `buildCostSheetTable` 이 심는 낱말과 같아야 한다."""
|
||||
return sheet_provenance(
|
||||
[
|
||||
ColumnProvenance(
|
||||
key="name",
|
||||
label="비목",
|
||||
tier=TIER_STANDARD,
|
||||
formula="법이 정한 비목 이름 (차례도 법이 정함)",
|
||||
source="법정경비 14 비목은 `B09_Estimation_Statutory.py:58 STATUTORY_ITEMS` "
|
||||
"— 그 차례가 곧 원가계산서 줄 차례",
|
||||
code="B09_Estimation_Engine_Cost.py:115 CostLine.name",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="amount_krw",
|
||||
label="금액",
|
||||
tier=TIER_CALC,
|
||||
formula="밑수 × 요율% (+ 정액) 을 원 단위로 버림",
|
||||
source="밑수는 비목마다 다름 — 「산출근거」 칸에 그 줄의 실제 밑수가 적힘. "
|
||||
"버림은 `B09_Estimation_Engine_Cost.py:44 floor_won`",
|
||||
rule="⚠ **총원가·도급금액·총계 줄은 `final`** — 계약으로 나가는 값이다. "
|
||||
"등급이 열에 하나뿐이라 그 셋을 따로 못 적었다(PLAN 8-36 ①). "
|
||||
"도급금액만 천원 단위 **올림**(`:49 ceil_thousand`)이라 끝자리가 다르다",
|
||||
code="B09_Estimation_Engine_Cost.py:175 _emitter",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="rate_percent",
|
||||
label="요율",
|
||||
tier=TIER_STANDARD,
|
||||
formula="공사금액·공사기간이 든 구간의 요율을 그대로 씀",
|
||||
source=_RATE_SOURCE,
|
||||
code="B09_Estimation_Rates.py:180 select_bracket",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="formula_text",
|
||||
label="산출근거",
|
||||
tier=TIER_CALC,
|
||||
formula="그 줄이 실제로 쓴 밑수와 요율을 사람이 읽게 적은 한 줄",
|
||||
source="엔진이 셈하면서 같이 지음 — 화면이 따로 짓지 않는다",
|
||||
rule="⚠ **산업안전보건관리비만 「× %」 꼴이 아니다** — A(요율식)·B(대상액×1.2) "
|
||||
"중 **작은 쪽**을 쓰므로 값 안에 고름이 숨어 있다"
|
||||
"(`B09_Estimation_Statutory.py:154 safety_management_cost`)",
|
||||
code="B09_Estimation_Engine_Cost.py:128 CostLine.formula_text",
|
||||
),
|
||||
_note_column("B09_Estimation_Engine_Cost.py:125 CostLine.note"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ② 설계내역서
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def boq_sheet() -> dict[str, Any]:
|
||||
return sheet_provenance(
|
||||
[
|
||||
_label("item_no", "No.", "마스터 목차가 매긴 번호"),
|
||||
_label("name", "공종", "B08 이 보낸 이름, 없으면 마스터 이름"),
|
||||
ColumnProvenance(
|
||||
key="spec",
|
||||
label="규격",
|
||||
tier=TIER_STANDARD,
|
||||
formula="마스터 규격 (갈래가 정해진 줄은 갈래 이름을 뒤에 이음)",
|
||||
source="갈래를 어떻게 골랐는지는 그 줄의 사유에 적힘 — B08 문구를 그대로 옮김",
|
||||
code="B09_Estimation_BillOfQuantities_Rows.py:149",
|
||||
),
|
||||
_label("unit", "단위"),
|
||||
ColumnProvenance(
|
||||
key="quantity",
|
||||
label="수량",
|
||||
tier=TIER_SURVEY,
|
||||
formula="B08 이 보낸 값을 그대로 씀 (여기서 다시 곱하지 않음)",
|
||||
source="⚠ **반영률은 B08 이 이미 곱했다** — 여기서 또 곱하면 두 번 곱해진다. "
|
||||
"찍는 자리수는 품셈 1-2-2 종목별(`B09_Estimation_QuantityDigits.py`)이고 "
|
||||
"값 자체는 전정밀로 남는다",
|
||||
code="B09_Estimation_BillOfQuantities_Rows.py:151",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="unit_price_krw",
|
||||
label="단가",
|
||||
tier=TIER_CALC,
|
||||
formula="그 공종의 일위대가 본표 합계 (1단위 값)",
|
||||
source="일위대가 탭에서 같은 표를 그대로 봄. 묶음 줄은 조각들의 "
|
||||
"`단가 × 조각수량` 을 더한 값",
|
||||
rule="⚠ **못 세우면 0 으로 때우지 않고 비운다.** 일위대가가 없음·성분이 빠짐·"
|
||||
"밑수를 모름·일부만 섬·단위가 안 맞음 — 사유는 그 줄의 사유에 적히고 "
|
||||
"「금액을 못 세운 줄」 목록에도 오른다",
|
||||
code="B09_Estimation_BillOfQuantities_Rows.py:134 _leaf_row",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="amount_krw",
|
||||
label="금액",
|
||||
tier=TIER_FINAL,
|
||||
formula="수량 × 단가",
|
||||
source="이 값들의 합이 공사원가계산서의 직접비로 나간다 — 화면 밖으로 나가는 값",
|
||||
rule="단가가 안 선 줄은 **금액도 안 세운다**. 단위가 안 맞는 줄도 비운다 — "
|
||||
"곱하면 조용히 틀린 금액이 내역서에 든다",
|
||||
code="B09_Estimation_BillOfQuantities_Rows.py:134 _leaf_row",
|
||||
),
|
||||
_note_column("B09_Estimation_BillOfQuantities.py:BillRow.note"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ③ 일위대가
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def unit_price_list_sheet() -> dict[str, Any]:
|
||||
"""목록표 — 「무엇이 있나」."""
|
||||
common = "단가판(`PriceBook`)이 성분을 풀어 낸 값 — 본표를 열면 줄마다 보인다"
|
||||
return sheet_provenance(
|
||||
[
|
||||
_label("name", "명칭", "단가판 제목"),
|
||||
_label("unit", "단위", "단가판 기준 단위"),
|
||||
ColumnProvenance(
|
||||
key="material",
|
||||
label="재료비",
|
||||
tier=TIER_CALC,
|
||||
formula="본표 재료비 줄의 합",
|
||||
source=common,
|
||||
code="B09_Estimation_UnitPrice_View.py:149 list_unit_prices",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="labor",
|
||||
label="노무비",
|
||||
tier=TIER_CALC,
|
||||
formula="본표 노무비 줄의 합",
|
||||
source=common,
|
||||
code="B09_Estimation_UnitPrice_View.py:149 list_unit_prices",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="expense",
|
||||
label="경비",
|
||||
tier=TIER_CALC,
|
||||
formula="본표 경비 줄의 합",
|
||||
source=common,
|
||||
code="B09_Estimation_UnitPrice_View.py:149 list_unit_prices",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="total",
|
||||
label="합계",
|
||||
tier=TIER_CALC,
|
||||
formula="재료비 + 노무비 + 경비",
|
||||
source=common,
|
||||
code="B09_Estimation_UnitPrice_View.py:149 list_unit_prices",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def unit_price_detail_sheet() -> dict[str, Any]:
|
||||
"""본표 — 「무엇으로 이루어졌나」."""
|
||||
money = (
|
||||
"성분 단위값 × 수량을 성분별로 자른 값. ⚠ 행마다 자르므로 **전정밀 합과 끝자리가 "
|
||||
"어긋난다 — 정상이다.** 숨기면 나중에 「합계가 안 맞는다」며 계산을 고치려 든다"
|
||||
)
|
||||
return sheet_provenance(
|
||||
[
|
||||
_label("name", "명칭", "성분 이름. 제잡비·공구손료는 품셈 [주]가 만든 줄"),
|
||||
_label("spec", "규격", "성분 규격. 비율 줄은 「노무비의 N%」 꼴"),
|
||||
ColumnProvenance(
|
||||
key="source",
|
||||
label="원천",
|
||||
tier=TIER_STANDARD,
|
||||
formula="그 성분이 어느 판에서 왔는지 + 그 판에서의 순번",
|
||||
source="자재 · 노임 · 기계경비 · 일위대가 · 단가산출 · 일식견적 여섯 중 하나"
|
||||
"(`B09_Estimation_UnitPrice.py:1026 SOURCE_LABEL`)",
|
||||
rule="기계경비·일위대가·단가산출 줄은 **눌러서 한 층 아래로 내려갈 수 있다**"
|
||||
"(`:1037 DRILLABLE_KINDS`)",
|
||||
code="B09_Estimation_UnitPrice_View.py:257",
|
||||
),
|
||||
_label("unit", "단위"),
|
||||
ColumnProvenance(
|
||||
key="quantity",
|
||||
label="수량",
|
||||
tier=TIER_STANDARD,
|
||||
formula="품셈 표가 정한 1단위당 소요량",
|
||||
source="비율 줄(제잡비·공구손료)은 수량 칸에 **퍼센트**가 들어간다",
|
||||
code="B09_Estimation_UnitPrice_View.py:259",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="material",
|
||||
label="재료비",
|
||||
tier=TIER_CALC,
|
||||
formula="성분 단위 재료비 × 수량",
|
||||
source=money,
|
||||
code="B09_Estimation_UnitPrice_View.py:264",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="labor",
|
||||
label="노무비",
|
||||
tier=TIER_CALC,
|
||||
formula="성분 단위 노무비 × 수량",
|
||||
source=money,
|
||||
code="B09_Estimation_UnitPrice_View.py:265",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="expense",
|
||||
label="경비",
|
||||
tier=TIER_CALC,
|
||||
formula="성분 단위 경비 × 수량",
|
||||
source=money,
|
||||
code="B09_Estimation_UnitPrice_View.py:266",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="total",
|
||||
label="합계",
|
||||
tier=TIER_CALC,
|
||||
formula="자른 성분 셋을 더한 값 (표에서 합계 = 재료비+노무비+경비 가 서게)",
|
||||
source=money,
|
||||
code="B09_Estimation_UnitPrice_View.py:267",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ④ 관급·사급 자재대
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _material_columns(*, excluded: bool) -> list[ColumnProvenance]:
|
||||
"""자재대 열 일곱. 「안 갈린 것」 표만 통째로 `excluded` 로 선다."""
|
||||
if excluded:
|
||||
why = (
|
||||
"⚠ **관급·사급이 안 갈린 줄** — 어느 합계에도 넣지 않는다. 못 세운 것이 아니라 "
|
||||
"**세면 안 되는** 자리다. 관급자재대에도 도급 재료비에도 넣으면 이중계상이 된다"
|
||||
)
|
||||
return [
|
||||
ColumnProvenance(
|
||||
key=key,
|
||||
label=label,
|
||||
tier=TIER_EXCLUDED,
|
||||
source=why,
|
||||
code="B09_Estimation_BillOfQuantities_Rows.py:398",
|
||||
)
|
||||
for key, label in (
|
||||
("name", "자재"),
|
||||
("spec", "규격"),
|
||||
("unit", "단위"),
|
||||
("total_amount", "수량"),
|
||||
("unit_price_krw", "단가"),
|
||||
("amount_krw", "금액"),
|
||||
("note", "비고"),
|
||||
)
|
||||
]
|
||||
return [
|
||||
_label("name", "자재"),
|
||||
_label("spec", "규격"),
|
||||
_label("unit", "단위"),
|
||||
ColumnProvenance(
|
||||
key="total_amount",
|
||||
label="수량",
|
||||
tier=TIER_SURVEY,
|
||||
formula="B08 이 낸 자재 수량 × 할증률",
|
||||
source="할증 사유는 그 줄의 사유에 적힘. 할증률이 아직 없는 자재는 "
|
||||
"**할증 전 값**으로 서고 그 사실이 표 밑에 뜬다",
|
||||
code="B09_Estimation_MaterialSheet.py:47 MaterialSheetRow",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="unit_price_krw",
|
||||
label="단가",
|
||||
tier=TIER_STANDARD,
|
||||
formula="단가판에서 찾은 값",
|
||||
source="⚠ **관급과 사급은 원천이 다르다** — 관급은 나라장터, 사급은 물가지·견적. "
|
||||
"관급을 「사급 단가 없음」으로 적으면 안 된다",
|
||||
rule="못 찾으면 **0 으로 때우지 않고 비운다** — 사유가 그 줄에 적힌다",
|
||||
code="B09_Estimation_MaterialSheet.py:117 build_material_sheet",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="amount_krw",
|
||||
label="금액",
|
||||
tier=TIER_FINAL,
|
||||
formula="수량 × 단가",
|
||||
source="⚠ **사급만 도급 재료비로 든다.** 관급은 총원가 **밖** 별도 표기라 "
|
||||
"여기 합계가 원가계산서 재료비와 같지 않다",
|
||||
code="B09_Estimation_MaterialSheet.py:117 build_material_sheet",
|
||||
),
|
||||
_note_column("B09_Estimation_BillOfQuantities.py:BillRow.note"),
|
||||
]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ⑤ 중기목록표 · 기초자료 목록표
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def machine_sheet() -> dict[str, Any]:
|
||||
hourly = "시간당 사용료 — 「각종 중기경비계산서」에 셈 과정이 그대로 펼쳐진다"
|
||||
return sheet_provenance(
|
||||
[
|
||||
_label("code", "코드번호"),
|
||||
_label("name", "명 칭"),
|
||||
_label("spec", "규 격"),
|
||||
_label("unit", "단위"),
|
||||
ColumnProvenance(
|
||||
key="total_krw",
|
||||
label="합 계",
|
||||
tier=TIER_CALC,
|
||||
formula="노무비 + 재료비 + 경비",
|
||||
source=hourly,
|
||||
code="B09_Estimation_Lists.py:105 machine_list",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="labor_krw",
|
||||
label="노 무 비",
|
||||
tier=TIER_CALC,
|
||||
formula="조종원 노임 ÷ 8시간 × 16/12 × 25/20 (약 1.667배)",
|
||||
source="공표 노임은 기본급여액뿐이라 제수당·상여금·퇴직급여충당금을 따로 "
|
||||
"계상함(건협 임금적용요령 4-나 · 기재부 집행기준 제76조의3). "
|
||||
"⚠ 계수 자체의 예규 원문은 아직 못 봐 실무 관행을 따름",
|
||||
code="B09_Estimation_MachineCost.py:74 OPERATOR_ALLOWANCE_FACTOR",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="material_krw",
|
||||
label="재 료 비",
|
||||
tier=TIER_CALC,
|
||||
formula="주연료(L/hr) × 유가 + 잡재료(주연료의 %)",
|
||||
source="유가는 전국 또는 고른 시도의 공시가 — 기초자료 탭에서 고른다. "
|
||||
"잡재료는 연료 소요량에 포함되어 있어 따로 세지 않는다",
|
||||
rule="같은 기종이라도 **조합 사용이면 잡재료가 16% 로 줄어** 재료비가 달라진다 "
|
||||
"— 그래서 층이 따로 선다(건설품셈 제8장 [주]⑤)",
|
||||
code="B09_Estimation_MachineExpenseSheet.py:81 machine_expense_sheets",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="expense_krw",
|
||||
label="경 비",
|
||||
tier=TIER_CALC,
|
||||
formula="취득가격 × 손료계수(상각비 + 정비비 + 관리비, 10⁻⁷)",
|
||||
source="취득가격·내용시간·연간표준가동시간·계수 셋은 모두 품셈 표 값",
|
||||
code="B09_Estimation_MachineExpenseSheet.py:81 machine_expense_sheets",
|
||||
),
|
||||
_note_column("B09_Estimation_Lists.py:105 machine_list"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def catalog_sheet() -> dict[str, Any]:
|
||||
"""기초자료 탭의 목록표 셋(노무비·재료비·경비)이 같이 쓰는 사전."""
|
||||
return sheet_provenance(
|
||||
[
|
||||
_label("code", "코드번호"),
|
||||
_label("name", "명 칭"),
|
||||
_label("spec", "규 격"),
|
||||
_label("unit", "단위"),
|
||||
ColumnProvenance(
|
||||
key="unit_price_krw",
|
||||
label="단 가",
|
||||
tier=TIER_STANDARD,
|
||||
formula="단가판 값을 그대로 옮김 (여기서 셈하지 않음)",
|
||||
source="어느 판·어느 기준일인지는 산출기초 ① 에 지문까지 남음. "
|
||||
"⚠ 경비목록표의 값은 **기계 취득가격(천원)** 이고 시간당 사용료가 아니다",
|
||||
rule="자재단가대비표에서 **원천 다섯 중 하나를 골라** 적용 단가가 선다 — "
|
||||
"값 안에 고름이 숨은 자리(PLAN 8-36 ㉯)",
|
||||
code="B09_Estimation_Lists.py:50 catalog_list",
|
||||
),
|
||||
_note_column("B09_Estimation_Lists.py:50 catalog_list"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 응답에 싣기
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def estimation_provenance() -> dict[str, Any] | None:
|
||||
"""B09 응답에 실을 사전 — **개발환경이 아니면 `None`.**
|
||||
|
||||
시트를 늘릴 때는 여기 한 줄만 더한다. 화면은 시트 이름으로 찾아 쓴다.
|
||||
|
||||
⚠ **일부러 안 붙인 둘** (2026-09-12 두 창 합의)
|
||||
· **설계서 구성표** — 프로젝트가 낳은 값이 아니라 「무슨 문서를 낼 것인가」 목록이라
|
||||
원천도 식도 없다. 없는 것을 지어 붙이면 「분류가 있다」는 거짓만 남는다(조사표 ㉴).
|
||||
· **산출 조건(좌측 패널)** — 표가 아니라 입력 칸이고, 칸 밑 근거 한 줄이 이미 같은
|
||||
일을 한다. 두 벌이 되면 어긋난다.
|
||||
"""
|
||||
return provenance_payload(
|
||||
{
|
||||
"cost_sheet": cost_sheet(),
|
||||
"boq": boq_sheet(),
|
||||
"unit_price_list": unit_price_list_sheet(),
|
||||
"unit_price_detail": unit_price_detail_sheet(),
|
||||
"material": sheet_provenance(_material_columns(excluded=False)),
|
||||
"material_unknown": sheet_provenance(_material_columns(excluded=True)),
|
||||
"machine": machine_sheet(),
|
||||
"catalog": catalog_sheet(),
|
||||
"material_comparison": material_comparison_sheet(),
|
||||
"base_reference_labor": base_reference_labor_sheet(),
|
||||
"base_reference_fuel": base_reference_fuel_sheet(),
|
||||
"price_basis": price_basis_sheet(),
|
||||
**basis_sheet_tables(),
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
"""B09 근거 사전이 **여러 장에서 같이 쓰는 조각** (PLAN 8-36 ④).
|
||||
|
||||
왜 따로 두나
|
||||
사전이 두 파일로 갈리면서(값을 낳는 장 / 바깥에서 오거나 모으는 장) 두 쪽이 같은
|
||||
이름표 열과 같은 「비고」 설명을 쓴다. 한쪽에 두고 다른 쪽이 가져가면 **고리가 생겨**
|
||||
(`Provenance` → `Provenance_Sources` → `Provenance`) 불러들이지 못한다.
|
||||
그래서 두 쪽이 함께 바라보는 자리를 따로 뒀다.
|
||||
|
||||
⚠ 여기에는 **여러 장이 실제로 같이 쓰는 것만** 둔다. 한 장만 쓰는 문장을 여기로 올리면
|
||||
사전을 읽을 때 그 장에서 눈이 떠나 버린다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from common_util.common_util_provenance import (
|
||||
TIER_STANDARD,
|
||||
TIER_UNCLASSIFIED,
|
||||
ColumnProvenance,
|
||||
)
|
||||
|
||||
#: 이름표 열(코드·명칭·규격·단위)이 공통으로 쓰는 문장.
|
||||
CATALOG_SOURCE = "단가판·품셈 표의 이름을 그대로 옮긴 자리 — 여기서 짓지 않음"
|
||||
|
||||
#: 「비고」가 왜 미분류인지 — 표마다 같은 말을 쓴다.
|
||||
NOTE_RULE = (
|
||||
"이 칸은 등급을 붙일 열이 아니라 **다른 열의 사유를 담는 그릇**이다. "
|
||||
"안에 든 조각마다 닿는 열이 다르므로(갈래 근거는 규격, 반영률은 수량, "
|
||||
"막힘 사유는 단가) 호버는 조각을 그 열의 칸에만 띄운다"
|
||||
)
|
||||
|
||||
|
||||
def _label(key: str, label: str, extra: str = "") -> ColumnProvenance:
|
||||
"""이름표 열 — 값을 낳은 것이 아니라 옮겨 적은 자리."""
|
||||
return ColumnProvenance(
|
||||
key=key,
|
||||
label=label,
|
||||
tier=TIER_STANDARD,
|
||||
formula="옮겨 적은 값 (여기서 계산하지 않음)",
|
||||
source=CATALOG_SOURCE + (f" · {extra}" if extra else ""),
|
||||
)
|
||||
|
||||
|
||||
def _note_column(code: str) -> ColumnProvenance:
|
||||
"""「비고」 열 — 여덟 어디에도 안 맞아 `unclassified` 로 둔다(PLAN 8-36 ㉮)."""
|
||||
return ColumnProvenance(
|
||||
key="note",
|
||||
label="비고",
|
||||
tier=TIER_UNCLASSIFIED,
|
||||
formula="줄에 달린 사유 조각을 차례로 이어 붙인 글",
|
||||
source="조각마다 원천이 다름 — 조각별 원천은 그 조각이 닿는 열의 카드에 뜸",
|
||||
rule=NOTE_RULE,
|
||||
code=code,
|
||||
)
|
||||
@@ -0,0 +1,268 @@
|
||||
"""B09 **바깥에서 온 값**과 **모으는 표**의 근거 사전 (PLAN 8-36 ④).
|
||||
|
||||
왜 파일을 갈랐나
|
||||
`B09_Estimation_Provenance.py` 가 700줄을 넘었다(CLAUDE.md 4장). 가르는 금은
|
||||
**값을 낳는 장**과 **값이 바깥에서 오거나 모으기만 하는 장**이다 —
|
||||
· 저쪽: 원가계산서·내역서·일위대가·자재대·중기·기초자료 목록표
|
||||
· 이쪽: 자재단가대비표·환율및기초자료·단가산출근거·산출기초
|
||||
이쪽 넷은 식이 얇거나 아예 없고, 대신 **「어느 판에서 왔나」**가 알맹이다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_provenance import (
|
||||
TIER_BLOCKED,
|
||||
TIER_CALC,
|
||||
TIER_INPUT,
|
||||
TIER_STANDARD,
|
||||
TIER_UNCLASSIFIED,
|
||||
ColumnProvenance,
|
||||
sheet_provenance,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_Provenance_Common import _label, _note_column
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ⑥ 자재단가대비표 — **원천 다섯 중 하나를 고르는** 자리
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def material_comparison_sheet() -> dict[str, Any]:
|
||||
"""실무 시트의 「기.가 · 유.물 · 견적」 약호가 곧 채택 근거다.
|
||||
|
||||
⚠ 원천 칸이 **여럿이라도 뜻은 하나**다(어느 판이 얼마라 했나). 슬롯마다 열 키를
|
||||
따로 두면 사전이 다섯 벌이 되고 판이 늘 때마다 어긋난다 — 한 키를 나눠 쓴다.
|
||||
"""
|
||||
return sheet_provenance(
|
||||
[
|
||||
_label("code", "코드번호"),
|
||||
_label("name", "명 칭"),
|
||||
_label("spec", "규 격"),
|
||||
_label("unit", "단위"),
|
||||
ColumnProvenance(
|
||||
key="slot_price",
|
||||
label="원천 단가",
|
||||
tier=TIER_STANDARD,
|
||||
formula="그 판이 적어 놓은 값을 그대로 옮김",
|
||||
source="물가지·거래가격·업체 견적 등 판마다 다름 — 오른쪽 「페이지」 칸이 "
|
||||
"어느 쪽에서 왔는지 가리킨다",
|
||||
rule="⚠ **빈 칸은 「0원」이 아니라 「그 판에 그 품목이 없다」** — "
|
||||
"0 으로 때우지 않는다",
|
||||
code="B09_Estimation_Lists_Sources.py:79 material_price_comparison",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="slot_page",
|
||||
label="페이지",
|
||||
tier=TIER_STANDARD,
|
||||
formula="그 값이 실린 쪽수·출처 표기",
|
||||
source="실무 시트의 「기.가 1,024」 같은 약호 — 사람이 원문을 찾아갈 열쇠",
|
||||
code="B09_Estimation_Lists_Sources.py:79 material_price_comparison",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="adopted_price_krw",
|
||||
label="적 용",
|
||||
tier=TIER_CALC,
|
||||
formula="원천 다섯 중 **채택한 하나**의 값",
|
||||
source="채택한 칸은 표에서 굵게 선다 — 표가 스스로 「어느 값을 썼나」를 밝힌다",
|
||||
rule="⚠ **값 안에 고름이 숨은 열**(PLAN 8-36 ㉯). 실무 약호가 곧 채택 규칙이다 "
|
||||
"— 「기.가」(정부 기준가격) · 「유.물」(물가정보) · 「견적」(업체). "
|
||||
"슬롯 6 이 곧 적용 단가라 그 자리에 값이 서면 「적용」 칸을 따로 안 세운다",
|
||||
code="B09_Estimation_Lists_Sources.py:79 material_price_comparison",
|
||||
),
|
||||
_note_column("B09_Estimation_Lists_Sources.py:79"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ⑦ 환율및기초자료 — **바깥에서 온 값**과 **우리가 고른 것**이 갈리는 장
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def base_reference_labor_sheet() -> dict[str, Any]:
|
||||
"""② 인건비 — 공표 노임을 시간당으로 푼 표."""
|
||||
return sheet_provenance(
|
||||
[
|
||||
_label("code", "코드번호"),
|
||||
_label("name", "직 종"),
|
||||
ColumnProvenance(
|
||||
key="day_wage_krw",
|
||||
label="일 당",
|
||||
tier=TIER_STANDARD,
|
||||
formula="공표 노임을 그대로 옮김",
|
||||
source="대한건설협회 시중노임 공표값. ⚠ **기본급여액뿐**이라 제수당·상여금·"
|
||||
"퇴직급여충당금은 따로 계상해야 한다",
|
||||
rule="표본이 얇은 직종(조사현장 5곳 미만 `*` · 미조사 `**`)은 금액은 서되 "
|
||||
"그 사실을 화면이 따로 알린다",
|
||||
code="B09_Estimation_Lists_Sources.py:137 base_reference_data",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="hourly_krw",
|
||||
label="시간당",
|
||||
tier=TIER_CALC,
|
||||
formula="일당 ÷ 8시간",
|
||||
source="⚠ **나눈 값을 자르지 않고 그대로 둔다** — 여기서 원 단위로 자르면 "
|
||||
"기계 시간당 사용료가 조금씩 어긋난다. 자르는 자리는 일위대가·내역서 쪽",
|
||||
code="B09_Estimation_Lists_Sources.py:137 base_reference_data",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="formula",
|
||||
label="산 식",
|
||||
tier=TIER_CALC,
|
||||
formula="그 줄이 실제로 쓴 셈을 사람이 읽게 적은 한 줄",
|
||||
source="서버가 값과 같이 지음 — 화면이 따로 짓지 않는다",
|
||||
code="B09_Estimation_Lists_Sources.py:137 base_reference_data",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def base_reference_fuel_sheet() -> dict[str, Any]:
|
||||
"""③ 단가 및 재료비 — 유가 한 줄. 지역을 고르면 기계 연료비가 다시 선다."""
|
||||
return sheet_provenance(
|
||||
[
|
||||
_label("item", "항 목"),
|
||||
ColumnProvenance(
|
||||
key="price_krw",
|
||||
label="단 가",
|
||||
tier=TIER_STANDARD,
|
||||
formula="공시가를 그대로 옮김 (전국 또는 고른 시도)",
|
||||
source="한국석유공사 공시가. 이 값이 바뀌면 **기계 연료비가 통째로 다시 선다**",
|
||||
code="B09_Estimation_Lists_Sources.py:137 base_reference_data",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="scope",
|
||||
label="적용 범위",
|
||||
tier=TIER_INPUT,
|
||||
formula="사용자가 고른 유가 범위 (전국 공시가 또는 현장 시도)",
|
||||
source="안 고르면 전국 공시가로 선다 — 고른 적이 없다는 뜻이지 "
|
||||
"「전국이 맞다」는 뜻이 아니다",
|
||||
rule="⚠ **자료가 없는 시도는 고를 수 없게 둔다.** 고르게만 해 두고 값이 없으면 "
|
||||
"조용히 틀린 값이 선다",
|
||||
code="B09_Estimation_Lists_Sources.py:44 fuel_scopes",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="effective_date",
|
||||
label="기준일",
|
||||
tier=TIER_STANDARD,
|
||||
formula="그 공시가의 기준일",
|
||||
source="어느 판으로 섰는지는 산출기초 ① 에 지문까지 남는다",
|
||||
code="B09_Estimation_Lists_Sources.py:137 base_reference_data",
|
||||
),
|
||||
_label("dataset_id", "자료", "판 이름 — 산출기초 ① 의 같은 낱말"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ⑧ 모으는 표 둘 — **값을 낳지 않는다.** 식은 비우고 「어디서 왔나」만 적는다
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def price_basis_sheet() -> dict[str, Any]:
|
||||
"""단가산출근거 목록 — 내역서 줄의 단가가 어느 일위대가에서 왔는지.
|
||||
|
||||
⚠ **여기서 값을 다시 셈하지 않는다.** 그래서 `formula` 를 비웠다 — 없는 식을
|
||||
지어 적으면 읽는 사람이 「여기서 계산한다」고 잘못 읽는다.
|
||||
"""
|
||||
return sheet_provenance(
|
||||
[
|
||||
ColumnProvenance(
|
||||
key="number",
|
||||
label="번호",
|
||||
tier=TIER_STANDARD,
|
||||
source="내역서에 **쓰인 차례**대로 매긴 번호 — 실무 참조번호(「단산 46」)가 "
|
||||
"곧 이 번호다",
|
||||
code="B09_Estimation_PriceBasis.py:74 build_price_basis",
|
||||
),
|
||||
_label("name", "공종", "공종 이름 + 규격"),
|
||||
_label("unit", "단위"),
|
||||
ColumnProvenance(
|
||||
key="unit_price_krw",
|
||||
label="단가",
|
||||
tier=TIER_CALC,
|
||||
source="한 층 아래 일위대가 본표의 합계를 **그대로 옮긴 값** — 그 표는 "
|
||||
"일위대가 탭에서 본다(본문 「참조」가 그 코드를 가리킨다)",
|
||||
code="B09_Estimation_PriceBasis.py:74 build_price_basis",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def basis_sheet_tables() -> dict[str, dict[str, Any]]:
|
||||
"""산출기초 네 표. **모으기만 하는 장**이라 식이 없다.
|
||||
|
||||
⚠ 이 장이 답하는 물음은 「이 값이 얼마인가」가 아니라 **「어느 판·어느 근거로
|
||||
섰나」** 다. 그래서 `source` 와 등급만 채운다.
|
||||
"""
|
||||
versions = sheet_provenance(
|
||||
[
|
||||
_label("dataset_id", "자료", "판 이름"),
|
||||
_label("file", "파일", "그 판의 파일 이름"),
|
||||
ColumnProvenance(
|
||||
key="effective_date",
|
||||
label="기준일",
|
||||
tier=TIER_STANDARD,
|
||||
source="그 판이 언제 것인가 — 값이 달라졌을 때 가장 먼저 보는 자리",
|
||||
code="B09_Estimation_BasisSheet.py:44 dataset_versions",
|
||||
),
|
||||
ColumnProvenance(
|
||||
key="sha256",
|
||||
label="지문(앞 12)",
|
||||
tier=TIER_UNCLASSIFIED,
|
||||
source="파일 해시. 값도 근거도 아니고 **재현성 표식**이라 여섯 어디에도 "
|
||||
"안 맞는다(PLAN 8-36 ㉰). 「같은 판으로 다시 세웠나」를 가르는 데만 쓴다",
|
||||
code="B09_Estimation_BasisSheet.py:44 dataset_versions",
|
||||
),
|
||||
]
|
||||
)
|
||||
chosen = sheet_provenance(
|
||||
[
|
||||
_label("item", "항 목", "고를 수 있는 자리의 이름"),
|
||||
ColumnProvenance(
|
||||
key="value",
|
||||
label="고른 값",
|
||||
tier=TIER_INPUT,
|
||||
source="사용자가 산출 조건에서 고른 값 — 비어 있으면 **고른 적이 없어 "
|
||||
"확정 기본값으로 돌고 있다**는 뜻이다",
|
||||
code="B09_Estimation_BasisSheet.py:77 chosen_conditions",
|
||||
),
|
||||
]
|
||||
)
|
||||
items = sheet_provenance(
|
||||
[
|
||||
_label("code", "코드"),
|
||||
_label("name", "공 종"),
|
||||
_label("unit", "단위"),
|
||||
ColumnProvenance(
|
||||
key="notes",
|
||||
label="근 거",
|
||||
tier=TIER_STANDARD,
|
||||
source="그 공종의 단가가 **어느 품셈 표·어느 [주]** 를 따랐는지. 줄에 달린 "
|
||||
"근거를 모아 온 것이라 여기서 새로 짓지 않는다",
|
||||
code="B09_Estimation_BasisSheet.py:106 work_item_basis",
|
||||
),
|
||||
]
|
||||
)
|
||||
gaps = sheet_provenance(
|
||||
[
|
||||
_label("kind", "갈 래", "못 채운 사유의 갈래"),
|
||||
_label("code", "코드"),
|
||||
ColumnProvenance(
|
||||
key="reason",
|
||||
label="사 유",
|
||||
tier=TIER_BLOCKED,
|
||||
source="⚠ **0 으로 때우지 않고 남겨 둔 자리.** 근거가 오면 채워질 자리이지 "
|
||||
"「세면 안 되는 자리」가 아니다",
|
||||
code="B09_Estimation_BasisSheet.py:134 open_gaps",
|
||||
),
|
||||
]
|
||||
)
|
||||
return {
|
||||
"basis_versions": versions,
|
||||
"basis_chosen": chosen,
|
||||
"basis_items": items,
|
||||
"basis_gaps": gaps,
|
||||
}
|
||||
@@ -28,6 +28,7 @@ from B09_Estimation.B09_Estimation_Engine_Cost import (
|
||||
proposed_profit_adjustment,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceBookError
|
||||
from B09_Estimation.B09_Estimation_Provenance import estimation_provenance
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities import bill_summary, build_bill
|
||||
from B09_Estimation.B09_Estimation_Guards import DoubleCountError
|
||||
from B09_Estimation.B09_Estimation_Rates import RateLookupError
|
||||
@@ -47,6 +48,18 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B09 Estimation"])
|
||||
|
||||
|
||||
def _with_provenance(body: dict[str, Any]) -> dict[str, Any]:
|
||||
"""근거 사전을 응답에 얹는다 — **개발환경이 아니면 칸 자체를 안 만든다.**
|
||||
|
||||
⚠ 빈 dict 를 실으면 화면이 「사전이 있는데 비었다」로 읽어 빈 카드를 띄운다.
|
||||
그래서 `None` 이면 **키를 넣지 않는다**(로직 보안의 문은 서버 쪽 하나뿐이다).
|
||||
"""
|
||||
provenance = estimation_provenance()
|
||||
if provenance is not None:
|
||||
body["provenance"] = provenance
|
||||
return body
|
||||
|
||||
|
||||
class CostRequest(BaseModel):
|
||||
"""원가계산 입력 — 금액은 원 단위."""
|
||||
|
||||
@@ -176,7 +189,7 @@ async def compute_cost(project_id: UUID, payload: CostRequest) -> JSONResponse:
|
||||
body["suggested_profit_adjustment_krw"] = str(
|
||||
proposed_profit_adjustment(result, payload.target_contract_amount_krw)
|
||||
)
|
||||
return JSONResponse(content={"status": "success", **body})
|
||||
return JSONResponse(content=_with_provenance({"status": "success", **body}))
|
||||
|
||||
|
||||
@router.get("/{project_id}/estimation/items")
|
||||
@@ -203,11 +216,13 @@ async def list_unit_price_titles(project_id: UUID) -> JSONResponse:
|
||||
try:
|
||||
build = await _build_for(project_id)
|
||||
return JSONResponse(
|
||||
content={
|
||||
"status": "success",
|
||||
"summary": build_summary(build),
|
||||
"rows": list_unit_prices(build),
|
||||
}
|
||||
content=_with_provenance(
|
||||
{
|
||||
"status": "success",
|
||||
"summary": build_summary(build),
|
||||
"rows": list_unit_prices(build),
|
||||
}
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B09 일위대가 목록 실패: project_id=%s", project_id)
|
||||
@@ -274,7 +289,9 @@ async def get_base_data_lists(project_id: UUID) -> JSONResponse:
|
||||
|
||||
try:
|
||||
return JSONResponse(
|
||||
content={"status": "success", **all_lists(await _build_for(project_id))}
|
||||
content=_with_provenance(
|
||||
{"status": "success", **all_lists(await _build_for(project_id))}
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B09 기초자료 목록 실패: project_id=%s", project_id)
|
||||
@@ -303,13 +320,15 @@ async def get_price_sources(project_id: UUID) -> JSONResponse:
|
||||
root = await _project_root_of(project_id)
|
||||
settings = estimation_settings(root) if root else {}
|
||||
return JSONResponse(
|
||||
content={
|
||||
"status": "success",
|
||||
"material_comparison": material_price_comparison(build),
|
||||
"base_reference": base_reference_data(
|
||||
build, str(settings.get("fuel_region") or "") or None
|
||||
),
|
||||
}
|
||||
content=_with_provenance(
|
||||
{
|
||||
"status": "success",
|
||||
"material_comparison": material_price_comparison(build),
|
||||
"base_reference": base_reference_data(
|
||||
build, str(settings.get("fuel_region") or "") or None
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B09 단가 원천 표 실패: project_id=%s", project_id)
|
||||
@@ -332,7 +351,9 @@ async def get_basis_sheet(project_id: UUID) -> JSONResponse:
|
||||
build = await _build_for(project_id)
|
||||
root = await _project_root_of(project_id)
|
||||
settings = estimation_settings(root) if root else {}
|
||||
return JSONResponse(content={"status": "success", **basis_sheet(build, settings)})
|
||||
return JSONResponse(
|
||||
content=_with_provenance({"status": "success", **basis_sheet(build, settings)})
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B09 산출기초 실패: project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
@@ -689,7 +710,9 @@ async def get_unit_price_detail(project_id: UUID, code: str) -> JSONResponse:
|
||||
"""일위대가 **본표** — 「무엇으로 이루어졌나」. 줄마다 원천·파고들기 표시가 붙는다."""
|
||||
try:
|
||||
return JSONResponse(
|
||||
content={"status": "success", **detail_of(await _build_for(project_id), code)}
|
||||
content=_with_provenance(
|
||||
{"status": "success", **detail_of(await _build_for(project_id), code)}
|
||||
)
|
||||
)
|
||||
except PriceBookError as error:
|
||||
return JSONResponse(status_code=404, content={"status": "error", "message": str(error)})
|
||||
@@ -759,14 +782,18 @@ async def get_bill(project_id: UUID) -> JSONResponse:
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"status": "success",
|
||||
"rows": [row.as_dict() for row in result.rows],
|
||||
"excluded": [row.as_dict() for row in result.excluded],
|
||||
"materials": [row.as_dict() for row in result.material_rows],
|
||||
"summary": bill_summary(result),
|
||||
"price_basis": result.price_basis.as_dict() if result.price_basis else {"entries": []},
|
||||
}
|
||||
content=_with_provenance(
|
||||
{
|
||||
"status": "success",
|
||||
"rows": [row.as_dict() for row in result.rows],
|
||||
"excluded": [row.as_dict() for row in result.excluded],
|
||||
"materials": [row.as_dict() for row in result.material_rows],
|
||||
"summary": bill_summary(result),
|
||||
"price_basis": (
|
||||
result.price_basis.as_dict() if result.price_basis else {"entries": []}
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import {
|
||||
attachProvenance,
|
||||
markProvenanceCell,
|
||||
type ProvenancePayload,
|
||||
type ProvenanceSheet,
|
||||
} from "@ui/ui_template_provenance";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
@@ -48,6 +54,8 @@ export interface BaseDataDto {
|
||||
material: BaseDataRow[];
|
||||
expense: BaseDataRow[];
|
||||
machine: MachineRow[];
|
||||
/** 근거 사전 — **개발환경에서만** 온다. 없으면 호버·등급색이 통째로 안 붙는다. */
|
||||
provenance?: ProvenancePayload;
|
||||
}
|
||||
|
||||
export async function fetchBaseData(projectId: string): Promise<BaseDataDto> {
|
||||
@@ -80,7 +88,19 @@ function note(text: string): HTMLElement {
|
||||
return el;
|
||||
}
|
||||
|
||||
function table(headers: string[], rows: string[][], leftCols: number[]): HTMLElement {
|
||||
/**
|
||||
* 표 한 장.
|
||||
*
|
||||
* `keys`·`sheet` 를 함께 주면 칸마다 근거 호버가 붙는다 — **사전이 없으면 아무 일도
|
||||
* 안 한다**(빈 카드를 띄우면 「설명이 있다」는 거짓만 남는다). 안 주는 표는 종전 그대로다.
|
||||
*/
|
||||
function table(
|
||||
headers: string[],
|
||||
rows: string[][],
|
||||
leftCols: number[],
|
||||
keys?: string[],
|
||||
sheet?: ProvenanceSheet,
|
||||
): HTMLElement {
|
||||
const el = document.createElement("table");
|
||||
el.className = "b09-sheet";
|
||||
const thead = document.createElement("thead");
|
||||
@@ -99,16 +119,20 @@ function table(headers: string[], rows: string[][], leftCols: number[]): HTMLEle
|
||||
const td = document.createElement("td");
|
||||
td.textContent = text;
|
||||
if (leftCols.includes(index)) td.className = "b09-left";
|
||||
const key = keys?.[index];
|
||||
const column = key ? sheet?.columns[key] : undefined;
|
||||
if (key && column) markProvenanceCell(td, key, column.tier);
|
||||
tr.append(td);
|
||||
});
|
||||
tbody.append(tr);
|
||||
}
|
||||
el.append(thead, tbody);
|
||||
attachProvenance(el, sheet);
|
||||
return el;
|
||||
}
|
||||
|
||||
/** 목록표 한 장 — 코드·명칭·규격·단위·단가·비고 (실무 시트와 같은 칸). */
|
||||
function catalogTable(rows: BaseDataRow[]): HTMLElement {
|
||||
function catalogTable(rows: BaseDataRow[], sheet?: ProvenanceSheet): HTMLElement {
|
||||
return table(
|
||||
["코드번호", "명 칭", "규 격", "단위", "단 가", "비 고"],
|
||||
rows.map((row) => [
|
||||
@@ -120,6 +144,8 @@ function catalogTable(rows: BaseDataRow[]): HTMLElement {
|
||||
row.note,
|
||||
]),
|
||||
[0, 1, 2, 5],
|
||||
["code", "name", "spec", "unit", "unit_price_krw", "note"],
|
||||
sheet,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,7 +174,7 @@ export function drawBaseDataTab(body: HTMLElement, data: BaseDataDto): void {
|
||||
body.append(note("아직 선 줄이 없습니다 — 값을 0 으로 채우지 않습니다."));
|
||||
continue;
|
||||
}
|
||||
body.append(catalogTable(rows));
|
||||
body.append(catalogTable(rows, data.provenance?.sheets?.catalog));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +200,18 @@ export function drawMachineTab(body: HTMLElement, data: BaseDataDto): void {
|
||||
row.note,
|
||||
]),
|
||||
[0, 1, 2, 8],
|
||||
[
|
||||
"code",
|
||||
"name",
|
||||
"spec",
|
||||
"unit",
|
||||
"total_krw",
|
||||
"labor_krw",
|
||||
"material_krw",
|
||||
"expense_krw",
|
||||
"note",
|
||||
],
|
||||
data.provenance?.sheets?.machine,
|
||||
),
|
||||
);
|
||||
// ⚠ 계산 과정을 감추지 않는다(PLAN 8-13). 조종원 환산이 실무와 다른 것을 여기서 밝힌다.
|
||||
@@ -364,6 +402,7 @@ export function drawMachineExpense(body: HTMLElement, data: MachineExpenseDto):
|
||||
|
||||
export interface BasisSheetDto {
|
||||
status: string;
|
||||
provenance?: ProvenancePayload;
|
||||
note: string;
|
||||
summary: string;
|
||||
dataset_versions: Array<{
|
||||
@@ -402,6 +441,8 @@ export function drawBasisSheet(body: HTMLElement, data: BasisSheetDto): void {
|
||||
row.sha256 || "—",
|
||||
]),
|
||||
[0, 1, 2, 3],
|
||||
["dataset_id", "file", "effective_date", "sha256"],
|
||||
data.provenance?.sheets?.basis_versions,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -414,6 +455,8 @@ export function drawBasisSheet(body: HTMLElement, data: BasisSheetDto): void {
|
||||
["항 목", "고른 값"],
|
||||
data.chosen_conditions.map((row) => [row.item, row.value]),
|
||||
[0, 1],
|
||||
["item", "value"],
|
||||
data.provenance?.sheets?.basis_chosen,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -424,6 +467,8 @@ export function drawBasisSheet(body: HTMLElement, data: BasisSheetDto): void {
|
||||
["코드", "공 종", "단위", "근 거"],
|
||||
data.work_items.map((row) => [row.code, row.name, row.unit, row.notes.join(" · ")]),
|
||||
[0, 1, 2, 3],
|
||||
["code", "name", "unit", "notes"],
|
||||
data.provenance?.sheets?.basis_items,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -436,6 +481,8 @@ export function drawBasisSheet(body: HTMLElement, data: BasisSheetDto): void {
|
||||
["갈 래", "코드", "사 유"],
|
||||
data.gaps.map((row) => [row.kind, row.code, row.reason]),
|
||||
[0, 1, 2],
|
||||
["kind", "code", "reason"],
|
||||
data.provenance?.sheets?.basis_gaps,
|
||||
),
|
||||
);
|
||||
body.append(note("⚠ 여기 있는 것은 0 으로 때우지 않고 남겨 둔 자리입니다."));
|
||||
@@ -484,6 +531,7 @@ export interface FuelScope {
|
||||
|
||||
export interface PriceSourcesDto {
|
||||
status: string;
|
||||
provenance?: ProvenancePayload;
|
||||
material_comparison: {
|
||||
slot_names: string[];
|
||||
rows: MaterialComparisonRow[];
|
||||
@@ -530,7 +578,11 @@ export async function fetchPriceSources(projectId: string): Promise<PriceSources
|
||||
* 자재단가대비표 — 원천마다 **단가·페이지** 두 칸이라 머리글이 두 줄이다.
|
||||
* 채택한 원천 칸에 표시를 넣어 「어느 것을 썼나」가 한눈에 보이게 한다.
|
||||
*/
|
||||
function comparisonTable(slotNames: string[], rows: MaterialComparisonRow[]): HTMLElement {
|
||||
function comparisonTable(
|
||||
slotNames: string[],
|
||||
rows: MaterialComparisonRow[],
|
||||
sheet?: ProvenanceSheet,
|
||||
): HTMLElement {
|
||||
const el = document.createElement("table");
|
||||
el.className = "b09-sheet";
|
||||
|
||||
@@ -571,35 +623,45 @@ function comparisonTable(slotNames: string[], rows: MaterialComparisonRow[]): HT
|
||||
const tbody = document.createElement("tbody");
|
||||
for (const row of rows) {
|
||||
const tr = document.createElement("tr");
|
||||
const put = (text: string, left = false): void => {
|
||||
/** 칸 하나 — `key` 를 주면 근거 호버가 붙는다(사전에 없는 열은 아무 일도 안 한다). */
|
||||
const put2 = (td: HTMLElement, key: string, tier?: string): void => {
|
||||
const column = sheet?.columns[key];
|
||||
if (column) markProvenanceCell(td, key, tier ?? column.tier);
|
||||
};
|
||||
const put = (text: string, left = false, key?: string): void => {
|
||||
const td = document.createElement("td");
|
||||
td.textContent = text;
|
||||
if (left) td.className = "b09-left";
|
||||
if (key) put2(td, key);
|
||||
tr.append(td);
|
||||
};
|
||||
put(row.code, true);
|
||||
put(row.name, true);
|
||||
put(row.spec, true);
|
||||
put(row.unit);
|
||||
put(row.code, true, "code");
|
||||
put(row.name, true, "name");
|
||||
put(row.spec, true, "spec");
|
||||
put(row.unit, false, "unit");
|
||||
for (const slot of row.slots) {
|
||||
const td = document.createElement("td");
|
||||
td.textContent = money(slot.price_krw);
|
||||
// 채택한 원천을 굵게 — 「어느 값을 썼나」를 표가 스스로 밝힌다.
|
||||
if (slot.adopted) td.style.fontWeight = "700";
|
||||
// ⚠ **빈 칸은 「0원」이 아니라 「그 판에 그 품목이 없다」** — 막힌 자리로 표시한다.
|
||||
put2(td, "slot_price", slot.price_krw === null ? "blocked" : undefined);
|
||||
tr.append(td);
|
||||
const page = document.createElement("td");
|
||||
page.textContent = slot.source_note;
|
||||
page.className = "b09-left";
|
||||
put2(page, "slot_page");
|
||||
tr.append(page);
|
||||
}
|
||||
if (!appliedIsLastSlot) {
|
||||
put(money(row.adopted_price_krw));
|
||||
put(money(row.adopted_price_krw), false, "adopted_price_krw");
|
||||
put(row.adopted_slot ? (row.slots[row.adopted_slot - 1]?.name ?? "") : "", true);
|
||||
}
|
||||
put(row.note, true);
|
||||
put(row.note, true, "note");
|
||||
tbody.append(tr);
|
||||
}
|
||||
el.append(thead, tbody);
|
||||
attachProvenance(el, sheet);
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -609,6 +671,8 @@ function baseReferenceSections(
|
||||
data: PriceSourcesDto["base_reference"],
|
||||
projectId: string,
|
||||
reload: () => void,
|
||||
laborSheet?: ProvenanceSheet,
|
||||
fuelSheet?: ProvenanceSheet,
|
||||
): void {
|
||||
body.append(head("환율및기초자료 — ① 환율"));
|
||||
body.append(note(data.exchange.note));
|
||||
@@ -628,6 +692,8 @@ function baseReferenceSections(
|
||||
row.formula,
|
||||
]),
|
||||
[0, 1, 4],
|
||||
["code", "name", "day_wage_krw", "hourly_krw", "formula"],
|
||||
laborSheet,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -655,6 +721,8 @@ function baseReferenceSections(
|
||||
],
|
||||
],
|
||||
[0, 2, 3, 4],
|
||||
["item", "price_krw", "scope", "effective_date", "dataset_id"],
|
||||
fuelSheet,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -727,11 +795,24 @@ export function drawPriceSourcesSections(
|
||||
if (comparison.rows.length === 0) {
|
||||
body.append(note("아직 선 줄이 없습니다 — 값을 0 으로 채우지 않습니다."));
|
||||
} else {
|
||||
body.append(comparisonTable(comparison.slot_names, comparison.rows));
|
||||
body.append(
|
||||
comparisonTable(
|
||||
comparison.slot_names,
|
||||
comparison.rows,
|
||||
data.provenance?.sheets?.material_comparison,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const text of comparison.notes) body.append(note(text));
|
||||
|
||||
baseReferenceSections(body, data.base_reference, projectId, reload);
|
||||
baseReferenceSections(
|
||||
body,
|
||||
data.base_reference,
|
||||
projectId,
|
||||
reload,
|
||||
data.provenance?.sheets?.base_reference_labor,
|
||||
data.provenance?.sheets?.base_reference_fuel,
|
||||
);
|
||||
}
|
||||
|
||||
/** 두 표를 아직 못 받아왔을 때 — 화면을 비우지 않는다. */
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, createInputField, showToast } from "@ui/ui_template_elements";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import {
|
||||
attachProvenance,
|
||||
createProvenanceToggle,
|
||||
markProvenanceCell,
|
||||
type ProvenancePayload,
|
||||
type ProvenanceSheet,
|
||||
} from "@ui/ui_template_provenance";
|
||||
import {
|
||||
drawBaseDataTab,
|
||||
drawFactorChoices,
|
||||
@@ -71,6 +79,8 @@ interface CostSheetDto {
|
||||
rate_version: { dataset_id: string; effective_date: string; sha256: string };
|
||||
notes: string[];
|
||||
suggested_profit_adjustment_krw?: string;
|
||||
/** 근거 사전 — **개발환경에서만** 온다. 없으면 호버·등급색이 통째로 안 붙는다. */
|
||||
provenance?: ProvenancePayload;
|
||||
}
|
||||
|
||||
interface UnitPriceRow {
|
||||
@@ -95,6 +105,7 @@ interface UnitPriceListDto {
|
||||
labor_reliability: Array<{ code: string; name: string; flag: string; why: string }>;
|
||||
};
|
||||
rows: UnitPriceRow[];
|
||||
provenance?: ProvenancePayload;
|
||||
}
|
||||
|
||||
interface UnitPriceDetailRow extends UnitPriceRow {
|
||||
@@ -119,6 +130,7 @@ interface UnitPriceDetailDto {
|
||||
expense: string;
|
||||
total: string;
|
||||
sum_matches: boolean;
|
||||
provenance?: ProvenancePayload;
|
||||
/** 품셈 표에 있는데 아직 안 붙은 줄 — 있으면 이 단가는 **붙은 줄만의 값**이다. */
|
||||
unattached: string[];
|
||||
unattached_note: string;
|
||||
@@ -175,6 +187,14 @@ function injectStyles(): void {
|
||||
style.textContent = `
|
||||
.b09-panel { display: flex; flex-direction: column; gap: var(--space-md, 12px); }
|
||||
.b09-panel__group { display: flex; flex-direction: column; gap: var(--space-xs, 4px); }
|
||||
/* 좌측 패널 상자 — B04~B07 과 같은 꼴(테두리는 공용 .ui-sidebar-section 이 전담).
|
||||
.ui-sidebar-section 이 붙은 것만 집어 본문 기초자료 표의 같은 클래스는 안 건드린다. */
|
||||
.b09-panel__group.ui-sidebar-section {
|
||||
margin: 0;
|
||||
padding: calc(var(--spacing-8) + var(--spacing-4));
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface-raised);
|
||||
}
|
||||
.b09-panel__legend {
|
||||
font-size: var(--font-size-xs, 12px); letter-spacing: .06em;
|
||||
color: var(--color-text-secondary); text-transform: uppercase;
|
||||
@@ -184,7 +204,6 @@ function injectStyles(): void {
|
||||
display: flex; justify-content: space-between; gap: var(--space-sm, 8px);
|
||||
border-bottom: 1px solid var(--color-border); padding: 2px 0;
|
||||
}
|
||||
.b09-panel__actions { display: flex; gap: var(--space-xs, 4px); margin-top: var(--space-sm, 8px); }
|
||||
.b09-hint { font-size: var(--font-size-xs, 12px); color: var(--color-text-secondary); }
|
||||
/* 표본이 얇은 노임 — 막는 것이 아니라 눈에 띄기만 하면 된다. */
|
||||
.b09-hint--warn { color: var(--color-warning-text, #8a5a00); }
|
||||
@@ -239,7 +258,37 @@ function formatWon(value: string): string {
|
||||
return n.toLocaleString("ko-KR");
|
||||
}
|
||||
|
||||
/**
|
||||
* 줄 사유 조각을 줄에 실어 둔다 — 카드가 꺼내 쓴다.
|
||||
*
|
||||
* ⚠ 조각마다 **닿는 열**이 함께 온다. 줄에 달렸다고 모든 칸에 띄우면
|
||||
* 「금액」 카드에 「갈래 근거…」 가 떠서 읽는 사람을 속인다(2026-09-12 B08 실측).
|
||||
*/
|
||||
function stashRowNotes(tr: HTMLElement, notes?: Array<{ column: string; text: string }>): void {
|
||||
if (notes?.length) tr.dataset.provNotes = JSON.stringify(notes);
|
||||
}
|
||||
|
||||
/** 그 칸에 **닿는** 줄 사유만 돌려준다. 열 키가 빈 조각은 줄 전체에 걸리는 사유다. */
|
||||
function rowNotesFor(cell: HTMLElement, columnKey: string): string[] {
|
||||
const raw = cell.closest("tr")?.dataset.provNotes;
|
||||
if (!raw) return [];
|
||||
try {
|
||||
return (JSON.parse(raw) as Array<{ column: string; text: string }>)
|
||||
.filter((note) => note.column === "" || note.column === columnKey)
|
||||
.map((note) => note.text);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 칸에 열 키·등급을 심는다 — **사전에 없는 열은 아무 일도 안 한다**(빈 카드 방지). */
|
||||
function mark(cell: HTMLElement, sheet: ProvenanceSheet | undefined, columnKey: string): void {
|
||||
const column = sheet?.columns[columnKey];
|
||||
if (column) markProvenanceCell(cell, columnKey, column.tier);
|
||||
}
|
||||
|
||||
function buildCostSheetTable(sheet: CostSheetDto): HTMLElement {
|
||||
const prov = sheet.provenance?.sheets?.cost_sheet;
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b09-sheet";
|
||||
|
||||
@@ -287,11 +336,26 @@ function buildCostSheetTable(sheet: CostSheetDto): HTMLElement {
|
||||
note.className = "b09-left";
|
||||
note.textContent = line.note;
|
||||
|
||||
mark(name, prov, "name");
|
||||
// ⚠ **같은 열 안에서 줄마다 등급이 갈리는 첫 자리.** 중간줄(간접노무비 따위)은
|
||||
// `calc` 인데 마지막줄 셋은 계약으로 나가는 `final` 이다. 열 사전은 등급이 하나뿐이라
|
||||
// 여기서 칸에 덮어 심는다 — 나머지 칸은 생략해 열 등급을 그대로 물려받는다.
|
||||
const isFinalLine =
|
||||
line.key === "total_cost" || line.key === "contract_amount" || line.key === "grand_total";
|
||||
const amountColumn = prov?.columns.amount_krw;
|
||||
// 등급을 빼면 공용 쪽이 열 등급으로 채워 주지만, **여기서 명시**해 두면 그 채움이
|
||||
// 없는 판에서도 띠 색이 제대로 붙는다.
|
||||
if (amountColumn)
|
||||
markProvenanceCell(amount, "amount_krw", isFinalLine ? "final" : amountColumn.tier);
|
||||
mark(rate, prov, "rate_percent");
|
||||
mark(basis, prov, "formula_text");
|
||||
mark(note, prov, "note");
|
||||
tr.append(name, amount, rate, basis, note);
|
||||
tbody.append(tr);
|
||||
}
|
||||
table.append(tbody);
|
||||
wrap.append(table);
|
||||
attachProvenance(wrap, prov);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
@@ -303,6 +367,7 @@ function buildUnitPriceList(
|
||||
): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b09-sheet b09-up-list";
|
||||
const prov = list.provenance?.sheets?.unit_price_list;
|
||||
|
||||
const caption = document.createElement("div");
|
||||
caption.className = "b09-hint";
|
||||
@@ -341,16 +406,21 @@ function buildUnitPriceList(
|
||||
const unit = document.createElement("td");
|
||||
unit.className = "b09-left";
|
||||
unit.textContent = row.unit;
|
||||
mark(name, prov, "name");
|
||||
mark(unit, prov, "unit");
|
||||
tr.append(name, unit);
|
||||
for (const value of [row.material, row.labor, row.expense, row.total]) {
|
||||
const moneyKeys = ["material", "labor", "expense", "total"];
|
||||
[row.material, row.labor, row.expense, row.total].forEach((value, index) => {
|
||||
const cell = document.createElement("td");
|
||||
cell.textContent = formatWon(value);
|
||||
mark(cell, prov, moneyKeys[index]);
|
||||
tr.append(cell);
|
||||
}
|
||||
});
|
||||
body.append(tr);
|
||||
}
|
||||
table.append(body);
|
||||
wrap.append(table);
|
||||
attachProvenance(wrap, prov);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
@@ -361,6 +431,7 @@ function buildUnitPriceDetail(
|
||||
): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b09-sheet b09-up-detail";
|
||||
const prov = detail.provenance?.sheets?.unit_price_detail;
|
||||
|
||||
const caption = document.createElement("div");
|
||||
caption.className = "b09-hint";
|
||||
@@ -433,12 +504,18 @@ function buildUnitPriceDetail(
|
||||
const unit = document.createElement("td");
|
||||
unit.className = "b09-left";
|
||||
unit.textContent = row.unit;
|
||||
mark(name, prov, "name");
|
||||
mark(spec, prov, "spec");
|
||||
mark(source, prov, "source");
|
||||
mark(unit, prov, "unit");
|
||||
tr.append(name, spec, source, unit);
|
||||
for (const value of [row.quantity, row.material, row.labor, row.expense, row.total]) {
|
||||
const detailKeys = ["quantity", "material", "labor", "expense", "total"];
|
||||
[row.quantity, row.material, row.labor, row.expense, row.total].forEach((value, index) => {
|
||||
const cell = document.createElement("td");
|
||||
cell.textContent = formatWon(value);
|
||||
mark(cell, prov, detailKeys[index]);
|
||||
tr.append(cell);
|
||||
}
|
||||
});
|
||||
body.append(tr);
|
||||
}
|
||||
|
||||
@@ -458,6 +535,7 @@ function buildUnitPriceDetail(
|
||||
|
||||
table.append(body);
|
||||
wrap.append(table);
|
||||
attachProvenance(wrap, prov);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
@@ -483,10 +561,10 @@ function buildSidePanel(
|
||||
legendKey: keyof typeof ui_locales,
|
||||
fields: Array<[keyof CostFormState, keyof typeof ui_locales]>,
|
||||
): void => {
|
||||
const group = document.createElement("div");
|
||||
group.className = "b09-panel__group";
|
||||
const group = document.createElement("section");
|
||||
group.className = "b09-panel__group ui-collapsible ui-sidebar-section";
|
||||
const legend = document.createElement("span");
|
||||
legend.className = "b09-panel__legend";
|
||||
legend.className = "b09-panel__legend ui-collapsible__title";
|
||||
legend.textContent = L(legendKey);
|
||||
group.append(legend);
|
||||
for (const [field, labelKey] of fields) {
|
||||
@@ -512,10 +590,10 @@ function buildSidePanel(
|
||||
]);
|
||||
|
||||
// 요율 판 — 읽기 전용. 「어느 판으로 계산했나」가 화면에 남아야 재현성이 선다.
|
||||
const rateGroup = document.createElement("div");
|
||||
rateGroup.className = "b09-panel__group";
|
||||
const rateGroup = document.createElement("section");
|
||||
rateGroup.className = "b09-panel__group ui-collapsible ui-sidebar-section";
|
||||
const rateLegend = document.createElement("span");
|
||||
rateLegend.className = "b09-panel__legend";
|
||||
rateLegend.className = "b09-panel__legend ui-collapsible__title";
|
||||
rateLegend.textContent = L("B09_Estimation_Group_RateVersion");
|
||||
const rateVersionBox = document.createElement("div");
|
||||
rateGroup.append(rateLegend, rateVersionBox);
|
||||
@@ -532,10 +610,10 @@ function buildSidePanel(
|
||||
]);
|
||||
|
||||
// 수량 — 여러 줄이라 텍스트 영역으로. 비어 있으면 위 직접비 3칸을 그대로 쓴다.
|
||||
const quantityGroup = document.createElement("div");
|
||||
quantityGroup.className = "b09-panel__group";
|
||||
const quantityGroup = document.createElement("section");
|
||||
quantityGroup.className = "b09-panel__group ui-collapsible ui-sidebar-section";
|
||||
const quantityLegend = document.createElement("span");
|
||||
quantityLegend.className = "b09-panel__legend";
|
||||
quantityLegend.className = "b09-panel__legend ui-collapsible__title";
|
||||
quantityLegend.textContent = L("B09_Estimation_Group_Quantity");
|
||||
const quantityLabel = document.createElement("label");
|
||||
quantityLabel.className = "ui-field__label";
|
||||
@@ -555,7 +633,8 @@ function buildSidePanel(
|
||||
root.append(hintBox);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "b09-panel__actions";
|
||||
// 바닥 고정 액션 줄(공용) — ui_template_overlay 가 이 줄을 스크롤 밖으로 빼낸다.
|
||||
actions.className = "ui-sidebar-actions";
|
||||
actions.append(
|
||||
createButton({
|
||||
label: L("B09_Estimation_Btn_Recalc"),
|
||||
@@ -569,6 +648,9 @@ function buildSidePanel(
|
||||
);
|
||||
root.append(actions);
|
||||
|
||||
// 그룹 제목 행 클릭 시 접기/펼치기(B04~B07 공통). 액션 줄은 collapsible 이 아니다.
|
||||
attachCollapsible(root);
|
||||
|
||||
return { root, rateVersionBox, hintBox };
|
||||
}
|
||||
|
||||
@@ -713,6 +795,12 @@ interface BillRowDto {
|
||||
is_group: boolean;
|
||||
in_bill: boolean;
|
||||
note: string;
|
||||
/**
|
||||
* 줄 사유 **조각** — 어느 사유가 어느 열에 닿는지까지 서버가 갈라 보낸다.
|
||||
* ⚠ 줄에 달렸다고 모든 칸에 띄우면 「금액」 카드에 「갈래 근거…」 가 떠서 읽는 사람을
|
||||
* 속인다(2026-09-12 B08 실측). `column` 이 빈 글인 것만 줄 전체에 붙는다.
|
||||
*/
|
||||
notes?: Array<{ column: string; text: string }>;
|
||||
}
|
||||
|
||||
interface PriceBasisEntryDto {
|
||||
@@ -745,6 +833,7 @@ interface BillDto {
|
||||
material_sheet: MaterialSheetDto | null;
|
||||
};
|
||||
price_basis: { entries: PriceBasisEntryDto[] };
|
||||
provenance?: ProvenancePayload;
|
||||
}
|
||||
|
||||
interface MaterialSheetRowDto {
|
||||
@@ -755,6 +844,7 @@ interface MaterialSheetRowDto {
|
||||
unit_price_krw: string | null;
|
||||
amount_krw: string | null;
|
||||
note: string;
|
||||
notes?: Array<{ column: string; text: string }>;
|
||||
}
|
||||
|
||||
interface MaterialSheetDto {
|
||||
@@ -827,6 +917,9 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
let selectedUnitPrice: string | null = null;
|
||||
let bill: BillDto | null = null;
|
||||
let priceBasis: string | null = null;
|
||||
// 근거 사전이 **한 번이라도** 왔는지 — 개발환경에서만 온다. 안 오면 토글도 안 세운다
|
||||
// (없는 기능의 단추가 떠 있으면 눌러 보고 「고장났다」고 읽는다).
|
||||
let hasProvenance = false;
|
||||
|
||||
const main = document.createElement("div");
|
||||
main.className = "b09-main";
|
||||
@@ -840,11 +933,19 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
body.style.display = "flex";
|
||||
body.style.flexDirection = "column";
|
||||
|
||||
/** 사전이 **처음 온 순간에만** 탭 줄을 다시 세운다 — 토글이 그때 생긴다. */
|
||||
const noteProvenance = (payload?: ProvenancePayload): void => {
|
||||
if (!payload || hasProvenance) return;
|
||||
hasProvenance = true;
|
||||
drawTabs();
|
||||
};
|
||||
|
||||
/** 일위대가 본표를 불러 다시 그린다 — 기계 줄을 누르면 그 층으로 파고든다. */
|
||||
const openUnitPrice = async (code: string): Promise<void> => {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
unitPriceDetail = await fetchUnitPriceDetail(projectId, code);
|
||||
noteProvenance(unitPriceDetail.provenance);
|
||||
selectedUnitPrice = code;
|
||||
drawBody();
|
||||
} catch {
|
||||
@@ -913,6 +1014,7 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
void (async () => {
|
||||
try {
|
||||
bill = await fetchBill(projectId);
|
||||
noteProvenance(bill.provenance);
|
||||
} catch {
|
||||
bill = null;
|
||||
window.alert(L("B09_Estimation_Boq_Failed"));
|
||||
@@ -930,6 +1032,18 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
head.innerHTML =
|
||||
"<tr><th>No.</th><th>공종</th><th>규격</th><th>단위</th>" +
|
||||
"<th>수량</th><th>단가</th><th>금액</th><th>비고</th></tr>";
|
||||
// 열 키는 서버 `BillRow.as_dict()` 낱말과 같아야 사전이 붙는다.
|
||||
const boqKeys = [
|
||||
"item_no",
|
||||
"name",
|
||||
"spec",
|
||||
"unit",
|
||||
"quantity",
|
||||
"unit_price_krw",
|
||||
"amount_krw",
|
||||
"note",
|
||||
];
|
||||
const prov = bill.provenance?.sheets?.boq;
|
||||
const tbody = document.createElement("tbody");
|
||||
for (const row of bill.rows) {
|
||||
const tr = document.createElement("tr");
|
||||
@@ -947,16 +1061,20 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
row.amount_krw ?? "",
|
||||
row.note,
|
||||
];
|
||||
for (const text of cells) {
|
||||
cells.forEach((text, index) => {
|
||||
const td = document.createElement("td");
|
||||
td.textContent = text;
|
||||
// 머리(그룹)줄은 값이 없다 — 빈 칸에 카드를 띄우면 「설명이 있다」는 거짓이 남는다.
|
||||
if (!row.is_group) mark(td, prov, boqKeys[index]);
|
||||
tr.append(td);
|
||||
}
|
||||
});
|
||||
if (row.is_group) tr.style.fontWeight = "600";
|
||||
else stashRowNotes(tr, row.notes);
|
||||
tbody.append(tr);
|
||||
}
|
||||
table.append(head, tbody);
|
||||
body.append(table);
|
||||
attachProvenance(table, prov, rowNotesFor);
|
||||
|
||||
const total = document.createElement("div");
|
||||
total.className = "b09-hint";
|
||||
@@ -1054,19 +1172,23 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
const list = document.createElement("table");
|
||||
list.className = "b09-sheet b09-up-list";
|
||||
list.innerHTML = "<thead><tr><th>번호</th><th>공종</th><th>단위</th><th>단가</th></tr></thead>";
|
||||
// 모으기만 하는 표라 사전에 **식이 없다** — 「어느 표에서 왔나」만 카드에 뜬다.
|
||||
const prov = bill.provenance?.sheets?.price_basis;
|
||||
const pbKeys = ["number", "name", "unit", "unit_price_krw"];
|
||||
const tbody = document.createElement("tbody");
|
||||
for (const entry of entries) {
|
||||
const tr = document.createElement("tr");
|
||||
for (const text of [
|
||||
[
|
||||
String(entry.number),
|
||||
`${entry.name} ${entry.spec}`.trim(),
|
||||
entry.unit,
|
||||
entry.unit_price_krw,
|
||||
]) {
|
||||
].forEach((text, index) => {
|
||||
const td = document.createElement("td");
|
||||
td.textContent = text;
|
||||
mark(td, prov, pbKeys[index]);
|
||||
tr.append(td);
|
||||
}
|
||||
});
|
||||
tr.style.cursor = "pointer";
|
||||
if (entry.code === priceBasis) tr.style.fontWeight = "600";
|
||||
tr.addEventListener("click", () => {
|
||||
@@ -1076,6 +1198,7 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
tbody.append(tr);
|
||||
}
|
||||
list.append(tbody);
|
||||
attachProvenance(list, prov);
|
||||
split.append(list);
|
||||
|
||||
const picked = entries.find((entry) => entry.code === priceBasis) ?? null;
|
||||
@@ -1118,11 +1241,13 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [labelKey, rows, total] of [
|
||||
["B09_Estimation_Mat_Contractor", sheet.contractor, sheet.contractor_total_krw],
|
||||
["B09_Estimation_Mat_Owner", sheet.owner, sheet.owner_total_krw],
|
||||
["B09_Estimation_Mat_Unknown", sheet.unknown, null],
|
||||
] as Array<[keyof typeof ui_locales, MaterialSheetRowDto[], string | null]>) {
|
||||
// ⚠ 「안 갈린 것」은 **못 세운 것이 아니라 세면 안 되는 것**이라 사전을 따로 쓴다
|
||||
// (`excluded` — 채우면 이중계상, PLAN 8-36 ㉱).
|
||||
for (const [labelKey, rows, total, sheetName] of [
|
||||
["B09_Estimation_Mat_Contractor", sheet.contractor, sheet.contractor_total_krw, "material"],
|
||||
["B09_Estimation_Mat_Owner", sheet.owner, sheet.owner_total_krw, "material"],
|
||||
["B09_Estimation_Mat_Unknown", sheet.unknown, null, "material_unknown"],
|
||||
] as Array<[keyof typeof ui_locales, MaterialSheetRowDto[], string | null, string]>) {
|
||||
const head = document.createElement("div");
|
||||
head.className = "b09-hint";
|
||||
head.textContent = `${L(labelKey)} (${rows.length})` + (total === null ? "" : ` — ${total}`);
|
||||
@@ -1134,10 +1259,20 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
table.innerHTML =
|
||||
"<thead><tr><th>자재</th><th>규격</th><th>단위</th><th>수량</th>" +
|
||||
"<th>단가</th><th>금액</th><th>비고</th></tr></thead>";
|
||||
const matKeys = [
|
||||
"name",
|
||||
"spec",
|
||||
"unit",
|
||||
"total_amount",
|
||||
"unit_price_krw",
|
||||
"amount_krw",
|
||||
"note",
|
||||
];
|
||||
const prov = bill?.provenance?.sheets?.[sheetName];
|
||||
const tbody = document.createElement("tbody");
|
||||
for (const row of rows) {
|
||||
const tr = document.createElement("tr");
|
||||
for (const text of [
|
||||
[
|
||||
row.name,
|
||||
row.spec,
|
||||
row.unit,
|
||||
@@ -1145,15 +1280,18 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
row.unit_price_krw ?? "",
|
||||
row.amount_krw ?? "",
|
||||
row.note,
|
||||
]) {
|
||||
].forEach((text, index) => {
|
||||
const td = document.createElement("td");
|
||||
td.textContent = text;
|
||||
mark(td, prov, matKeys[index]);
|
||||
tr.append(td);
|
||||
}
|
||||
});
|
||||
stashRowNotes(tr, row.notes);
|
||||
tbody.append(tr);
|
||||
}
|
||||
table.append(tbody);
|
||||
body.append(table);
|
||||
attachProvenance(table, prov, rowNotesFor);
|
||||
}
|
||||
|
||||
for (const note of sheet.notes) {
|
||||
@@ -1196,6 +1334,7 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
void fetchBasisSheet(projectId)
|
||||
.then((data) => {
|
||||
basisSheet = data;
|
||||
noteProvenance(data.provenance);
|
||||
drawBody();
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -1237,6 +1376,7 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
void fetchBaseData(projectId)
|
||||
.then((data) => {
|
||||
baseData = data;
|
||||
noteProvenance(data.provenance);
|
||||
drawBody();
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -1301,6 +1441,7 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
void fetchPriceSources(projectId)
|
||||
.then((data) => {
|
||||
priceSources = data;
|
||||
noteProvenance(data.provenance);
|
||||
drawBody();
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -1358,11 +1499,15 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
void fetchUnitPriceList(projectId)
|
||||
.then((data) => {
|
||||
unitPriceList = data;
|
||||
noteProvenance(data.provenance);
|
||||
drawBody();
|
||||
})
|
||||
.catch(() => showToast(L("B09_Estimation_UP_Load_Failed"), "error"));
|
||||
}
|
||||
});
|
||||
// ⚠ 등급색은 **평소엔 꺼 둔다** — 여덟 색이 늘 켜져 있으면 표가 알록달록해
|
||||
// 실무 시트와 눈으로 대조를 못 한다(PLAN 8-36 ②). 단추는 탭 줄 끝에 둔다.
|
||||
if (hasProvenance) bar.append(createProvenanceToggle(root));
|
||||
const old = main.querySelector(".b09-tabs");
|
||||
if (old) old.replaceWith(bar);
|
||||
else main.prepend(bar);
|
||||
@@ -1374,6 +1519,7 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
sheet = await fetchCostSheet(projectId, form);
|
||||
noteProvenance(sheet.provenance);
|
||||
renderRateVersion(panel.rateVersionBox, sheet);
|
||||
panel.hintBox.textContent =
|
||||
sheet.suggested_profit_adjustment_krw && sheet.suggested_profit_adjustment_krw !== "0"
|
||||
|
||||
@@ -36,6 +36,10 @@ from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from config.config_system_design import (
|
||||
EARTHWORK_CONVERSION_C_RANGES,
|
||||
EARTHWORK_CONVERSION_FACTORS,
|
||||
)
|
||||
|
||||
SETTINGS_FILENAME = "project_settings.json"
|
||||
SCHEMA_VERSION = 1
|
||||
@@ -258,6 +262,62 @@ def concrete_placing_method(settings: dict[str, Any]) -> tuple[str, bool]:
|
||||
return DEFAULT_CONCRETE_PLACING_METHOD, True
|
||||
|
||||
|
||||
def earthwork_conversion_factors(settings: dict[str, Any]) -> dict[str, dict[str, float]]:
|
||||
"""이 프로젝트가 쓸 토량환산계수 — **기본값 위에 고른 값만 얹는다.**
|
||||
|
||||
⚠ 정의처는 여전히 `config_system_design.EARTHWORK_CONVERSION_FACTORS` 한 곳이다.
|
||||
여기서 값을 새로 적지 않고, 설계자가 고른 갈래만 갈아 끼운다. 안 고른 갈래는
|
||||
키 자체가 없어 정본이 그대로 선다 — 기본값을 복사해 넣지 않는 까닭은 이 파일
|
||||
머리글 `*_override` 규칙과 같다.
|
||||
|
||||
⚠ 이 값은 토적표만 쓰는 것이 아니다 — 유토곡선(B06)·운반표·기초단가가 같이 읽는다.
|
||||
그래서 읽는 자리마다 상수를 직접 들지 말고 **이 함수를 거친다.**
|
||||
|
||||
고른 값의 모양 — `conversion_factors_override`
|
||||
`{"ripping_rock": {"compacted": 1.0, "reason": "토질시험 값"}}`
|
||||
`reason` 은 품셈 범위 밖을 골랐을 때 남기는 사유이고 계산에 안 쓴다.
|
||||
"""
|
||||
resolved = {kind: dict(entry) for kind, entry in EARTHWORK_CONVERSION_FACTORS.items()}
|
||||
override = settings.get("conversion_factors_override")
|
||||
if not isinstance(override, dict):
|
||||
return resolved
|
||||
for kind, entry in override.items():
|
||||
if kind not in resolved or not isinstance(entry, dict):
|
||||
continue
|
||||
value = entry.get("compacted")
|
||||
if isinstance(value, (int, float)) and not isinstance(value, bool) and float(value) > 0:
|
||||
resolved[kind]["compacted"] = float(value)
|
||||
return resolved
|
||||
|
||||
|
||||
def earthwork_conversion_choices(settings: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
"""갈래별 「무엇을 골랐나」 — 화면이 기본값과 고른 값을 갈라 보이는 데 쓴다.
|
||||
|
||||
`{갈래: {"compacted", "default", "chosen", "in_range", "range", "reason"}}`.
|
||||
`chosen` 이 거짓이면 기본값이 선 것이고, `in_range` 가 거짓이면 품셈 범위 밖이라
|
||||
사유가 있어야 하는 자리다. **범위 밖이라고 막지 않는다**(품셈 원칙이 토질시험이다).
|
||||
"""
|
||||
override = settings.get("conversion_factors_override")
|
||||
override = override if isinstance(override, dict) else {}
|
||||
resolved = earthwork_conversion_factors(settings)
|
||||
choices: dict[str, dict[str, Any]] = {}
|
||||
for kind, entry in resolved.items():
|
||||
default = float(EARTHWORK_CONVERSION_FACTORS[kind]["compacted"])
|
||||
value = float(entry["compacted"])
|
||||
low, high = EARTHWORK_CONVERSION_C_RANGES.get(kind, (None, None))
|
||||
entry_override = override.get(kind)
|
||||
reason = entry_override.get("reason") if isinstance(entry_override, dict) else None
|
||||
choices[kind] = {
|
||||
"compacted": value,
|
||||
"default": default,
|
||||
"chosen": value != default,
|
||||
"in_range": low is None or low <= value <= high,
|
||||
"range": [low, high] if low is not None else None,
|
||||
"reason": str(reason) if reason else None,
|
||||
}
|
||||
return choices
|
||||
|
||||
|
||||
def application_ratio(settings: dict[str, Any], key: str) -> float:
|
||||
"""반영률을 0~1 로. 없으면 100 %(=1.0) — 실무 관측치를 기본값으로 쓰지 않는다."""
|
||||
raw = (settings.get("application_ratios_pct") or {}).get(key, 100)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""화면에 뜬 숫자가 **어디서 와서 어떻게 계산됐는지**를 적어 두는 한 벌.
|
||||
|
||||
왜 서버가 드나 (CLAUDE.md 5장 · PLAN 8-36 ④)
|
||||
「어디서 와서 어떻게 계산됐나」의 정답은 **엔진이 안다.** 이 설명을 화면 TS 에 손으로
|
||||
적어 두면 엔진을 고칠 때 설명만 옛것으로 남아, 맞는 값 옆에 틀린 근거가 붙는다.
|
||||
그래서 사전은 값을 낳는 쪽(서버)이 들고, 화면은 **그리기만** 한다.
|
||||
|
||||
⚠ **칸마다 만들지 않는다 — 열 단위다.**
|
||||
토적표 한 장이 30열 × 200줄 = 6천 칸이다. 칸마다 설명을 지으면 응답이 수십 배로 붐는데,
|
||||
정작 설명이 갈리는 것은 **열**이지 칸이 아니다. 줄마다 갈리는 것(폴백 안분 사유 등)은
|
||||
이미 줄이 `notes` 로 들고 있으니 화면이 그것만 덧붙인다. 보는 사람 눈에는 그대로
|
||||
**칸 단위**로 뜬다.
|
||||
|
||||
⚠⚠ **로직 보안 — 배포에서는 아예 안 실어 보낸다.**
|
||||
화면에서 숨기는 것만으로는 막히지 않는다. API 를 직접 부르면 그대로 나온다.
|
||||
그래서 `provenance_payload()` 가 **개발환경이 아니면 `None`** 을 돌려주고, 라우터는
|
||||
그 `None` 을 응답에서 통째로 뺀다. 화면 쪽 `import.meta.env.DEV` 는 보조일 뿐이다.
|
||||
문의 정본은 `common_util_dev_unlock.is_dev_environment()` 하나로 통일한다 —
|
||||
개발용 문이 두 벌이 되면 한쪽만 닫히는 날이 온다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Iterable, Mapping
|
||||
|
||||
from common_util.common_util_dev_unlock import is_dev_environment
|
||||
|
||||
#: 출처 등급 (PLAN 8-36 ①) — 사람이 고르는 여섯 + 사전이 쓰는 둘(`excluded`·`unclassified`). **키는 영문 고정** — 화면·서버가 같은 낱말을 써야 하고,
|
||||
#: 사람이 읽는 이름은 화면 locale 이 맡는다(번역이 서버 값을 흔들면 안 된다).
|
||||
#:
|
||||
#: ⚠ 등급에 **안 맞는 열이 나오면 억지로 끼우지 말 것.** 그 어긋남이 등급을 고칠 근거다.
|
||||
#: 맞는 등급이 없으면 `UNCLASSIFIED` 로 두고 계획서에 남긴다 — 조용히 아무 등급이나
|
||||
#: 붙이면 「분류가 있다」는 거짓만 남는다.
|
||||
TIER_INPUT = "input" # 사용자가 화면에 직접 넣은 값
|
||||
TIER_SURVEY = "survey" # 앞 단계(B05 종단·B06 횡단)가 낳은 값
|
||||
TIER_STANDARD = "standard" # 법·품셈·단가판이 정한 고정값
|
||||
TIER_CALC = "calc" # 위 셋으로 만든 중간값
|
||||
TIER_FINAL = "final" # 내역서·원가계산서로 나가는 값
|
||||
TIER_BLOCKED = "blocked" # 근거가 없어 값을 **못** 세운 자리 — 근거가 오면 채워질 자리
|
||||
#: ⚠ `EXCLUDED` 는 `BLOCKED` 와 **뜻이 정반대**다(2026-09-12 데스크탑 보조 B09 조사 ㉱).
|
||||
#: 내역서의 「우리 줄이 아닌 것」·검산용 제외 줄·이중계상이 되는 자리는 **못 세운 것이 아니라
|
||||
#: 세면 안 되는 것**이다. 둘을 같은 등급으로 두면 사용자가 「빈 칸을 채워야 겠다」고 움직이고,
|
||||
#: 그것이 곧 이중계상이다(PLAN 8-7).
|
||||
TIER_EXCLUDED = "excluded" # 일부러 안 세는 자리 — 채우면 이중계상
|
||||
TIER_UNCLASSIFIED = "unclassified" # 어느 등급에도 안 맞아 **판단을 미룬** 자리
|
||||
|
||||
TIERS: tuple[str, ...] = (
|
||||
TIER_INPUT,
|
||||
TIER_SURVEY,
|
||||
TIER_STANDARD,
|
||||
TIER_CALC,
|
||||
TIER_FINAL,
|
||||
TIER_BLOCKED,
|
||||
TIER_EXCLUDED,
|
||||
TIER_UNCLASSIFIED,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ColumnProvenance:
|
||||
"""열 하나의 「무엇이고 · 어디서 왔고 · 어떻게 나왔나」.
|
||||
|
||||
`formula` 는 **사람이 읽는 한 줄**이지 실행되는 식이 아니다 — 코드를 그대로 베끼면
|
||||
읽는 사람이 못 읽고, 코드가 바뀌면 또 어긋난다. 「입적 × 토량환산계수」처럼 적는다.
|
||||
|
||||
`code` 는 `파일:줄` 이고 **개발환경에서만 화면에 뜬다.** 줄 번호는 쉽게 밀리므로
|
||||
함수 이름을 같이 적어 두면 밀려도 찾을 수 있다.
|
||||
"""
|
||||
|
||||
key: str
|
||||
label: str
|
||||
tier: str
|
||||
formula: str = ""
|
||||
source: str = ""
|
||||
#: 고르는 자리의 **채택 규칙**. 안전관리비 A·B 중 작은 쪽·자재단가 다섯 중 적용처럼
|
||||
#: **값 안에 선택이 숨은** 열이 있다(2026-09-12 B09 조사 ㉰). 그 열은 `calc` 로만 적으면
|
||||
#: 「왜 그것을 골랐나」가 사라진다. 후보값은 줄마다 달라지므로 여기엔 **규칙만** 적고
|
||||
#: 실제 후보값은 줄 쪽으로 내려보낸다.
|
||||
rule: str = ""
|
||||
code: str = ""
|
||||
|
||||
def as_dict(self) -> dict[str, str]:
|
||||
body: dict[str, str] = {"label": self.label, "tier": self.tier}
|
||||
if self.formula:
|
||||
body["formula"] = self.formula
|
||||
if self.source:
|
||||
body["source"] = self.source
|
||||
if self.rule:
|
||||
body["rule"] = self.rule
|
||||
if self.code:
|
||||
body["code"] = self.code
|
||||
return body
|
||||
|
||||
|
||||
def sheet_provenance(columns: Iterable[ColumnProvenance]) -> dict[str, Any]:
|
||||
"""한 장(시트)의 사전. 열 키로 찾아 쓰게 dict 로 편다.
|
||||
|
||||
⚠ 같은 키를 두 번 적으면 **뒤엣것이 앞엣것을 조용히 덮는다.** 열이 늘 때 실수하기
|
||||
쉬운 자리라 여기서 막고 이름을 알려 준다.
|
||||
"""
|
||||
body: dict[str, dict[str, str]] = {}
|
||||
for column in columns:
|
||||
if column.key in body:
|
||||
raise ValueError(f"사전에 같은 열 키가 둘 있습니다: {column.key}")
|
||||
if column.tier not in TIERS:
|
||||
raise ValueError(f"모르는 등급입니다: {column.key} → {column.tier}")
|
||||
body[column.key] = column.as_dict()
|
||||
return {"columns": body}
|
||||
|
||||
|
||||
def provenance_payload(sheets: Mapping[str, dict[str, Any]]) -> dict[str, Any] | None:
|
||||
"""응답에 실을 사전 — **개발환경이 아니면 `None`.**
|
||||
|
||||
라우터는 `None` 이면 그 칸을 응답에서 아예 뺀다(빈 dict 를 실으면 「사전이 있는데
|
||||
비었다」로 읽혀 화면이 빈 카드를 띄운다).
|
||||
"""
|
||||
if not is_dev_environment():
|
||||
return None
|
||||
return {"sheets": dict(sheets)}
|
||||
@@ -0,0 +1,364 @@
|
||||
# 2026-09-12 체크리스트 전체 완료 항목 이관
|
||||
|
||||
> 원본: 저장소 루트 PLAN.md
|
||||
> 처리: 사용자가 2차 교차검증 생략을 지시하여 기존 체크 상태와 자체검증 기록을 그대로 보존함.
|
||||
> 주의: 「검증 대기」·「사용자 확인 대기」·「보류 확정」 표기는 완료로 재해석하지 않고 원문대로 유지함.
|
||||
|
||||
### 0-9. 계획노선 편집 모달 — 기능 개선 (2026-09-12 사용자 지시) — ⭐ 앞 아홉은 만듦 · **검증 대기**
|
||||
|
||||
**선행:** 0-2 (모달 본체는 이미 섬). 여기는 그 위에 얹는 개선임. 손대는 자리는 `B05_Profile_UI_RouteEdit.ts` 와 조각 다섯(`_Curve`·`_Edits`·`_History`·`_Input`·`_Label`)임.
|
||||
|
||||
**⭐ 지금 상태 — 조사로 확인한 것 (2026-09-12)**
|
||||
|
||||
1. **등고선은 도엽 것 한 벌뿐임.** 모달은 배수유역도와 같은 `도엽_등고선.geojson` 을 노선 둘레 300m 띠 안에서만 그림. LAS 로 만든 등고선은 이미 있으나(`GET /projects/{id}/surface/models/{model_id}/contour?interval=`, 응답에 `level`(표고)과 사업지 좌표 점렬) **B05 3D 뷰어만** 씀. 확정 지표면 모델 id 는 B05 페이지가 이미 쥐고 있음(`confirmedSurface.model_id`).
|
||||
2. **최소곡선반지름은 제한이 아니라 표시임**(2026-09-06 확정). 서버가 `projects.road_type` + 설계속도 + 지형으로 골라 `min_radius_m` 로 내려 주고, 화면은 못 미치는 노드를 **붉게만** 칠함. 값 표는 `config_system_design.FOREST_ROAD_PROFILE_CRITERIA["min_plan_radius_m"]`(설계속도 40/30/20 × 일반·특수)이고 근거는 지식DB `01_임도/02_상세설계/평면선형.md` 별표2 Ⅰ.2.다.(1)임. **작업임도는 곡선반지름 규정 자체가 없음**(같은 문서 §1).
|
||||
3. **노선 길이·시점·종점·측점 표기가 모달에 없음.** 측점 간격 기본값은 20m(`SECTION_STATION_INTERVAL_M`), 「3+18.0」 표기 유틸은 `B05_Profile_Util_Station.ts` 에 이미 있음.
|
||||
4. **점을 주고 지반고를 받는 통로가 없음.** 서버 `common_util_surface_sampler.build_surface_sampler` 는 종·횡단 생성 안에서만 불림 — ⑤·⑧ 은 이 통로를 새로 내야 함.
|
||||
5. **곡선 조작 패널이 모달을 벗어남**(사용자 지적 ⑨). `document.body` 에 `position: fixed` 로 띄우고 곡선 중심 반대쪽 16방위로 자동배치라 **모달 상자 밖**과 **하단 정보행 위**로 넘어감. 모달이 `overflow: hidden` 이라 밖에 띄운 것이었으나, 「잘리지 않게」와 「상자 밖으로 나가게」는 다른 문제임.
|
||||
|
||||
**개선 방향 — 아홉**
|
||||
|
||||
- [x] **① 계획노선 길이 실시간 라벨** — 그려지는 폴리라인(`plannedLine`)의 정점 간 거리 합을 브라우저에서 바로 내 상태줄에 냄. 원호가 이미 정점으로 펴져 있어 이 합이 곧 실제 노선 길이임. 서버 왕복 없음.
|
||||
- [x] **② 시점·종점·규칙측점 실시간 표기** — 양 끝에 시점(BP)·종점(EP) 이름표, 규칙 간격마다 노선에 직교 눈금과 측점번호를 냄. 누가거리 → 「3+18.0」 변환은 `B05_Profile_Util_Station.formatStation` 재사용. ⚠ 측점 간격은 프로젝트 설정을 따라야 하므로 20m 를 코드에 굳히지 않음. 확대를 줄이면 눈금이 붙어 뭉개지므로 화면 간격이 좁아지면 5측점마다만 이름표를 냄.
|
||||
**자체검증 (2026-09-12, ORCA 내장 브라우저 · 프로젝트 `5cff3920`)** — ①②⑨ 셋 다 화면에서 확인함.
|
||||
· **①** 상태줄이 「길이 1017.5m · 노드 26개 …」로 뜸. 저장된 `planned_route.csv`(정점 170개)를 따로 재어 본 값도 **1017.5m**, 노드 26개·곡선 24곳으로 **정확히 같음**.
|
||||
· **②** 시점·종점 이름표와 5측점마다의 눈금(0+0.0 · 5+0.0 … 50+0.0)이 노선 위에 뜸. 종점 표기 **50+17.5** 는 50×20+17.5 = 1017.5m 로 길이와 맞아떨어짐. 이름표를 위로만 띄웠더니 측점 라벨과 포개져 **노선 바깥 방향으로 밀어** 풀었음(측점 라벨은 노선에 직각으로 나감).
|
||||
· **⑨** 패널 머리를 지도 밖 2000px 까지 끌어도 오른쪽 1401.2px · 아래 863.1px 에서 멈춤(테두리 1401.5 · 863.2). 하단 정보행은 지도 칸 아래라 **가려지지 않음**.
|
||||
· 그리기가 `B05_Profile_UI_RouteEdit_Render.ts` 로 떨어져 나가 본체가 **703줄 → 627줄**이 됨(700줄 규정 회복). 측점 눈금은 B04 지도와 **같은 `drawStationTicks`** 를 부름.
|
||||
|
||||
- [x] **③ 등고선 한 줄당 높이값 라벨** — 도엽은 `등고수치` 속성, LAS 는 `level` 값이 이미 실려 있어 새로 셀 것이 없음. 가닥마다 화면 안쪽 한 자리에 숫자를 찍되, 겹치면 주곡선(간격의 5배)만 냄.
|
||||
- [x] **④ R·L 최소값 제한 — 하한에서 막음** (2026-09-12 확정). 지금의 「붉게만 칠함」을 **하한 아래로 아예 못 가게 막는 쪽**으로 바꿈 — 0-2 확정의 「막지 않고 표시만」은 이 자리에서 뒤집힘. 막는 값은 둘임.
|
||||
· **R 하한** — 서버가 이미 내려 주는 `min_radius_m`(임도 종류·설계속도·지형이 정함)을 그대로 씀. **작업임도는 규정이 없어 0** — 수치를 지어내지 않고 **설정값 한 자리**로 두어 나중에 값만 바꾸면 바로 걸리게 함.
|
||||
· **L 하한(곡선 길이)** — 법령·교본에 평면 곡선 길이 하한이 **없음**. R 과 같은 꼴로 **임도 종류별 설정값**을 새로 두되 지금은 **전부 0**(제한 없음)으로 열어 둠.
|
||||
· 막는 자리는 반지름 칸 · 곡선 길이 칸 · 곡선 손잡이 끌기 셋. 하한에 닿으면 거기서 멈추고 왜 멈췄는지 상태줄에 냄.
|
||||
⚠ 노드를 옮겨 교각이 작아지면 L = R·Δ 라 길이가 저절로 줄어듦 — 이때는 노드 이동을 막지 않고 **R 을 늘려 L 을 지킴**(길이 고정과 같은 셈)이며, R 까지 하한에 닿으면 그때 멈춤.
|
||||
**자체검증 (2026-09-12, ORCA 내장 브라우저 · 프로젝트 `5cff3920` · 하한 R 12m)**
|
||||
· **막는 자리 셋 다 확인** — 반지름 칸에 5·11.9 를 넣으면 **12 로 멈추고 그 값이 칸에 되적힘**(30 은 그대로 통과), 곡선 손잡이를 교각점 쪽으로 44px 당겨도 R 칸이 **12 에서 안 내려감**, 노드를 끌어 접선 자리가 모자라지는 구간에서는 「하한에 걸려 더 못 옮깁니다(곡선반지름 12m)」가 뜨며 **그 걸음이 되돌려짐**. 패널에도 「R ≥ 12m」이 적힘.
|
||||
· **기본값과 하한을 갈라 둠** — 한 값으로 묶으면 작업임도의 하한 0 이 곧 반지름 0 이 되어 곡선이 아예 안 그려짐. 응답에 `min_radius_m`(기본값)과 `limit_radius_m`·`limit_curve_length_m`(하한)이 따로 나감(실측 12 · 12 · 0).
|
||||
· ⚠ **이미 하한을 밑돌던 자리는 그대로 둠** — 「조금이라도 나빠지면 막기」로 두었더니 옛 노선의 미달 노드 옆을 1px 만 건드려도 첫 걸음부터 막혀 **아무것도 못 고쳤음**. 지키고 있던 자리가 넘어가는 것만 막게 고침(붉은 표시는 그대로 남음).
|
||||
· 시험 `resources/tester/test_plan_curve_limits.py` 5건 통과. 전체 시험 **1300 통과 · 22 건너뜀**(설정 표를 건드려 전부 돌림).
|
||||
|
||||
- [x] **⑤ 두 지점 사이 거리·종단기울기** — 노선 위 아무 자리(직선·곡선 가림 없음)를 두 번 찍으면 a·b 사이 **노선을 따라간 길이**와 **(b 지반고 − a 지반고) ÷ 길이** 를 냄. 지반고는 확정 지표면에서 와야 하므로 **점 목록을 받아 표고를 돌려주는 가벼운 API 를 새로 냄**(`build_surface_sampler` 재사용, 새 계산 아님). ⚠ 0-2 확정 7(편집 중 계산 금지)과 부딪히지 않게 **두 번째 점을 찍는 순간에만** 부르고 노드를 끄는 동안에는 안 부름.
|
||||
**자체검증 (2026-09-12, ORCA 내장 브라우저 · 프로젝트 `5cff3920`)** — Shift+클릭 두 번으로 잼.
|
||||
· 상태줄에 「구간 0+8.8 → 32+9.2 · 길이 640.3m · 지반고 876.2 → 869.9m · 종단기울기 -1.0%」가 뜸. 측점 표기로 되짚으면 32×20+9.2 − 8.8 = **640.4m** 로 표시 길이와 맞고, (869.9−876.2)÷640.3 = **−0.98%** 로 표시 기울기와 맞음.
|
||||
· 지반고는 새 통로 `POST /route/elevations` 가 냄. 저장된 종단 측점(`center_x/y` 208407.437464 · 367751.645256, `center_z` 878.30352)을 그대로 물어 **878.304** 가 돌아옴 — 종·횡단 생성기와 **같은 sampler** 를 보고 있음.
|
||||
· 찍은 자리는 a·b 동그라미와 초록 굵은 선으로 노선 위에 보임. **끄는 동안에는 서버에 안 부름** — 두 번째 점을 찍는 순간 한 번만 부름(0-2 확정 7).
|
||||
· 700줄 규정을 지키려 모달에서 구간 재기(`_Measure.ts`)와 [확인]·[예상노선으로](`_Apply.ts`)를 떼어냄 — 본체 693줄.
|
||||
|
||||
- [x] **⑥ 바탕 등고선을 LAS 것으로** — 확정 지표면 모델이 있으면 LAS 등고선을 그리고, **없으면 지금처럼 도엽 등고선**을 씀(사용자 지시 그대로). 좌표계는 둘 다 사업지 m 라 변환이 필요 없고, 간격은 프로젝트 등고선 간격 설정을 따름. 노선 둘레 300m 띠로 거르는 것은 그대로 둠.
|
||||
- [x] **⑦ 등고선 선택 활성화** — 등고선 한 가닥을 찍으면 그 가닥만 다른 색·굵기로 살아나고 높이값을 크게 냄. 집는 차례는 **노드 · 곡선 손잡이 · 등고선** 순 — 노선 편집이 먼저임.
|
||||
**자체검증 (2026-09-12, ORCA 내장 브라우저 · 프로젝트 `5cff3920`)** — ③⑥⑦ 셋 다 화면에서 확인함.
|
||||
· **⑥** 상태줄이 「… · LAS 등고선 · …」으로 바뀜. 3D 뷰어가 쓰던 등고선 파일(`contour_…_1.0m.json`, 가닥 2277·표고 187단)을 그대로 읽음. 확정 지표면이 없으면 도엽으로 내려앉는 갈래도 함께 넣었음.
|
||||
· **③** 가닥마다 높이값이 붙음. 처음엔 **화면이 숫자로 덮였음** — LAS 등고선은 한 표고가 여러 가닥으로 끊겨 있어서임. 겹치는 라벨을 건너뛰게 고쳤고, B04 지도(계곡선만 라벨)는 원래 드물어 표기가 안 바뀜.
|
||||
· 그래도 1m 간격을 다 그리니 지형이 선으로 뭉개졌음 → **화면에 실제로 들어오는 가닥 수**를 세어 간격을 고르게 함(상한 350가닥). 이 노선에서는 10m 로 잡혀 750·760·…·920 이 읽힘. 확대하면 저절로 촘촘해짐.
|
||||
· **⑦** 등고선을 누르면 그 가닥만 보라색으로 굵어지고 상태줄에 「고른 등고선 860m」이 뜸. 집히는 것은 **그려진 줄뿐**임(고른 값 850·840·860 이 모두 10m 배수) — 안 그린 줄이 골라져 없던 선이 튀어나오던 것을 막았음. 빈 자리를 누르면 풀림. 집는 차례는 노드 · 곡선 손잡이 · 등고선 순.
|
||||
· `B04_PreProcess_UI_MapRender.ts` 가 700줄을 넘어 **사전 투영 쪽을 `_Prepare.ts` 로 분리**함(619 → 479 + 306).
|
||||
|
||||
- [x] **⑧ 측점 횡단도 미리보기 — 별도 창** (2026-09-12 확정). 측점을 찍으면 **따로 뜨는 작은 창**에 그 측점 횡단을 보임. **시간이 조금 걸려도 됨** — 찍은 측점 하나만 서버가 셈함. 보일 것은 셋뿐임 — **원지반 횡단선 · 기본 계획 횡단선 · 계획 횡단의 성토사면 길이**. **구조물은 안 그림.** 셈은 **B06 로직을 그대로 재사용**(`B06_Section_Engine_Design*`) — 새로 짜지 않음. ⚠ 계획고는 편집 중에 없으므로 B06 이 쓰는 기본 계획 규칙(지반 추종)으로 그 측점 하나만 세움.
|
||||
**자체검증 (2026-09-12, ORCA 내장 브라우저 · 프로젝트 `5cff3920`)** — 측점 눈금을 누르면 창이 뜸.
|
||||
· 창 제목 「횡단 미리보기 — STA.0+100.000」, 그림에 **원지반(회색)·기본 계획 횡단(주황)** 두 줄, 아래 한 줄에 「성토사면 좌 2.86m · 절토 71.43㎡ · 성토 1.97㎡ · 계획고는 [확인] 뒤에 정해지므로 지반을 따라 세운 기본 계획임」.
|
||||
· **정본과 같은 값인지 대조함** — 저장된 `cross_00100m.json` 의 지반 샘플과 견주니 offset −20.0m 에서 **889.910743 vs 889.910847**(0.1mm 차), 상단측도 둘 다 `right`. 계획고로 쓴 값은 그 측점 `center_z` 873.219755 그대로임.
|
||||
· 성토사면 길이는 **B06 화면이 쓰는 `fillSlopeLengths`** 를 그대로 부름 — 두 화면이 다른 길이를 말하지 않음. 설계선은 서버가 B06 `compute_cross_design` 으로 냄.
|
||||
· 한 장에 **0.7초**쯤 걸림(종·횡단을 한 번 돌림). 처음엔 가로·세로를 따로 늘려 사면 기울기가 거짓으로 보였음 — **같은 배율**로 고침.
|
||||
· 전체 시험 **1300 통과 · 22 건너뜀**.
|
||||
|
||||
- [x] **⑨ 곡선 조작 패널이 모달을 벗어남** — 자동 자리를 **모달 상자 안**(하단 정보행 위까지)으로 가둠. 손으로 옮기는 것도 같은 테두리 안에서만. `position: fixed` 로 띄우는 것은 그대로 두고 자리 계산에서만 가둠 — 상자가 작아 패널이 안 들어가면 캔버스 오른쪽 위에 붙임.
|
||||
|
||||
**개선 둘째 묶음 — 화면 정리와 회전 (2026-09-12 사용자 지시)**
|
||||
|
||||
앞 아홉을 화면에서 보고 이어서 내려온 지시임. **모달 아래 정보행을 없애고 지도 위 오버레이로 옮기는 것**이 줄기임 — 지도가 세로로 넓어짐.
|
||||
|
||||
- [x] **⑩ 조작 설명을 지도 좌상단 오버레이로** — 지금은 제목행 옆에 한 줄로 길게 늘어서 창이 좁으면 두 줄로 접힘. **2열**로 묶어 지도 왼쪽 위에 띄움.
|
||||
- [x] **⑪ 단추를 제목행 오른쪽으로** — [되돌리기]·[다시하기]·[초기화]·[예상노선으로]·[취소]·[확인]을 제목행 **오른쪽 맞춤**으로 옮김. 크기·모양은 **공용 규격**(`ui_template` 의 `.ui-btn`)을 그대로 씀 — 모달이 따로 만든 `b05-routeedit__btn` 을 걷어냄.
|
||||
- [x] **⑫ 상태·범례를 지도 좌하단 오버레이로** — 길이·노드 수·곡선 요약과 예상노선·계획노선 범례를 지도 왼쪽 아래에 띄움.
|
||||
- [x] **⑬ 하단 정보행 삭제** — ⑩~⑫ 를 옮긴 뒤 그 줄을 통째로 없앰. 지도 칸이 그만큼 내려감.
|
||||
- [x] **⑭ LAS 등고선도 5m 단위로** — 지금 LAS 자료는 1m 간격이라 확대하면 1m·2m 짜리까지 나옴. 도엽 등고선(5m)과 **같은 눈금**으로 맞춰 5m 아래로는 안 내려가게 함. ⚠ 서버에 5m 로 다시 뽑아 달라고 하면 첫 한 번이 오래 걸리므로, **받아 둔 1m 자료에서 5의 배수만 골라** 그림.
|
||||
- [x] **⑮ LAS 일 때 세류선도 등고 범위 안쪽만** — 도엽 하천중심선은 도엽 전체를 덮어 LAS 등고선 바깥까지 뻗음. 바탕이 LAS 일 때는 **등고선이 있는 범위 안**에서만 그림.
|
||||
- [x] **⑯ 지도 회전** — 지도 오른쪽 위에 **반시계·시계** 아이콘 둘. 한 번 누를 때마다 **그림 전체**가 한 칸씩 돎(글자도 함께 돎 — CAD 도면과 같은 방식). 집기(노드·손잡이·등고선·측점)는 돌린 자리를 그대로 따라감.
|
||||
- [x] **⑰ R·L 하한 5m 로 채움** (2026-09-12 사용자 확정) — 실무값도 없는 자리이고 **자유도가 높은 공사**라 0 으로 열어 두었으나, **표기상 값이 필요하면 5m** 로 하라는 지시임. 작은 값으로 고칠 때 걸리적거리지 않는 수준으로 보신 값임. 설정 표 한 칸이라 뒤에 언제든 바꿈.
|
||||
|
||||
**자체검증 (2026-09-12, ORCA 내장 브라우저 · 프로젝트 `5cff3920`)** — ⑩~⑯ 다 화면에서 확인함.
|
||||
· **⑩⑫⑬** 하단 정보행이 사라지고 지도가 그만큼 세로로 넓어짐. 조작 설명은 왼쪽 위 **2열**, 길이·노드·곡선 요약과 범례는 왼쪽 아래. 두 판 모두 **클릭을 통과시켜** 지도 조작을 안 가림.
|
||||
· **⑪** 단추 여섯이 제목행 **오른쪽 끝**에 붙음. 공용 `.ui-btn`(`--ghost`, [확인]만 `--filled`)을 그대로 써서 다른 화면 단추와 크기·모양이 같아짐 — 모달이 따로 쓰던 `b05-routeedit__btn` 은 지움.
|
||||
· **⑭** 확대하면 **855·865·875·885·895·905·915·925** 가 뜸 — 5m 단위가 맞음(1m·2m 짜리가 안 나옴). 받아 둔 1m 자료에서 5의 배수만 골라 그리므로 서버에 다시 묻지 않음.
|
||||
· **⑮** 세류선이 LAS 등고선 범위 밖으로 뻗던 긴 파란 선이 사라짐.
|
||||
· **⑯** 시계 단추 셋(45°)에 지도가 통째로 돌고 글자도 함께 돎. 돌린 상태에서 눈에 보이는 노드·손잡이를 눌러도 그대로 잡힘(「옮기는 중」·「곡선을 잡는 중」), 곡선 패널도 돌아간 노드 옆(105px)에 뜨고 지도 칸 안에 머묾. 반시계 셋으로 제자리.
|
||||
· ⚠ **⑰ 을 고치며 하나 알아냄** — L 하한을 기하에도 걸었더니 **아무것도 안 고쳤는데 노선이 바뀌었음**(길이 1017.5 → 1017.2m). 내각 179° 처럼 거의 곧은 자리는 L 5m 를 채우려면 R 이 286m 로 부풀기 때문임(L = R·Δ). 별표2 도 내각 155° 이상은 곡선을 안 둘 수 있다고 함. 그래서 **L 하한은 칸에 적을 때만 막고**(2 를 넣으면 5 로 멈춤) 패널에 「L ≥ 5m」으로 보이기만 함 — 노선은 손대지 않음.
|
||||
· 전체 시험 **1310 통과 · 22 건너뜀**.
|
||||
|
||||
**개선 셋째 묶음 — 창 배치와 손맛 (2026-09-12 사용자 지시, 화면을 보고 내려온 것)**
|
||||
|
||||
**⑱~⑳ 이 줄기임** — 횡단을 **메인 창 오른쪽에 붙박이로** 두고 그 아래에 **전후 비교창**을 새로 냄. 나머지는 잔손질.
|
||||
|
||||
- [x] **⑱ 창 배치를 바꿈** — 메인 모달을 **왼쪽으로** 밀고, 오른쪽에 폭이 같은 **세로 칸** 하나를 세움. 위쪽 절반이 **횡단 미리보기**(지금 떠다니던 창을 붙박이로), 아래 절반이 **전후 비교창**(새로 만듦). 떠다니며 끌던 것은 그만둠 — 자리가 정해지면 끌 까닭이 없음.
|
||||
- [x] **⑲ 전후 비교** — 비교창은 평소 **빈 화면**. 노선을 고쳐 **노드를 놓는 순간**(2026-09-12 사용자 확정) 그 측점 횡단을 다시 셈해 **새것은 위, 직전 것은 아래**로 내림. 끄는 동안에는 안 부름(한 장에 0.7초).
|
||||
- [x] **⑳ 횡단 창 손질** — 제목의 측점을 **누가거리가 아니라 측점 표기**로(⓫ 참고). 하단 정보에서 **「계획고는 [확인] 뒤에 정해지므로…」 설명을 지움**(2026-09-12 사용자 확정) — 성토사면 길이·절토·성토 면적은 그대로 둠.
|
||||
- [x] **㉑ 측점 표기를 한 규칙으로** — B05 왼쪽 아래 구조물 목록이 쓰는 **「측점번호+잔여거리」**(`4+9.8` · `17+4.5`) 그대로. 서버가 주는 `STA.0+100.000` 을 그대로 쓰지 않음 — 화면마다 표기가 갈림.
|
||||
- [x] **㉒ 좌하단에 길이 둘** — **예상노선 길이**와 **계획노선 길이**를 나란히. 지금은 계획노선 하나뿐이라 얼마나 달라졌는지 안 보임.
|
||||
- [x] **㉓ 거리 재기를 단추로** — [되돌리기] **왼쪽에 구분선**을 두고 그 왼쪽에 **[거리 재기]** 단추. 켜면 그냥 눌러도 재짐(Shift 는 지름길로 남김).
|
||||
- [x] **㉔ 잰 값을 작은 창으로** — 상태줄에 섞지 않고 **따로 뜨는 작은 오버레이**에 냄. **닫기 단추**가 있고 닫으면 **잰 것이 지워짐**. **곡선 패널과 같이 뜨지 않음** — 하나가 뜨면 다른 하나는 숨음.
|
||||
- [x] **㉕ 잰 구간이 꼬임** — 화면 사진에서 초록 띠가 노선을 벗어나 **삼각형으로 얽힘**. 두 점 사이를 **가장 가까운 정점**으로 잘라서 생긴 일임(노선이 되꺾이는 자리에서 엉뚱한 정점을 고름). **누가거리로 잘라** 고침.
|
||||
- [x] **㉖ 곡선 패널 여닫기** — 고른 꺾임점을 **벗어나 누르면 선택이 풀리고** 패널이 닫힘. 패널 **오른쪽 위에 닫기 단추**도 붙임.
|
||||
- [x] **㉗ 곡선 패널 하단 정보 줄이기** — **내각만 남김**(2026-09-12 사용자 지시). 「고정 없음」·「R ≥ 12m · L ≥ 5m」·「칸을 비우면 자동」은 지움 — 막는 일은 칸이 이미 함.
|
||||
- [x] **㉘ 고른 등고선에 높이 라벨** — 등고선 한 줄을 고르면 그 줄에 **높이값을 크게** 붙임. 지금은 색만 바뀌고 숫자는 상태줄에만 있음.
|
||||
- [x] **㉙ 회전 아이콘을 반원으로** — `↺`·`↻` 대신 **반만 도는 화살표** 모양으로 바꿈.
|
||||
- [x] **㉚ 돌려도 글자는 바로 세움** — 180° 로 돌리면 글자가 뒤집혀 읽히지 않음(사용자 지적). 라벨마다 **거꾸로 한 번 더 돌려** 눈높이로 세움. ⓘ **비용은 거의 없음** — 화면에 나오는 라벨이 수십 개뿐이고 라벨 하나에 변환 한 번 더 얹는 정도라 그림 전체를 다시 그리는 값에 묻힘. 취소할 까닭이 없어 **넣기로 함**.
|
||||
|
||||
**자체검증 (2026-09-12, ORCA 내장 브라우저 · 프로젝트 `5cff3920`)** — 열셋 다 화면에서 확인함.
|
||||
· **⑱** 메인 창이 왼쪽으로 밀리고 오른쪽 세로 칸에 「횡단」·「이전 횡단」 두 판이 반씩 앉음. 화면이 좁으면(1500px 미만) 오른쪽 칸이 접히고 지도가 폭을 다 가짐.
|
||||
· **⑲** 측점을 눌러 `2+0.0` 횡단(성토사면 좌 12.50m · 절토 24.80㎡)을 본 뒤 노드를 옮겨 놓으니 위 판이 **새 값**(좌 5.86m · 절토 41.21㎡)으로 바뀌고 아래 판에 **옛 값이 그대로** 남음 — 전후 비교가 됨. 끄는 동안에는 안 부름.
|
||||
· **⑳㉑** 제목이 「횡단 2+0.0」 — 누가거리가 아니라 구조물 목록과 같은 측점 표기임. 「계획고는 [확인] 뒤에…」 안내는 지워짐.
|
||||
· **㉒** 좌하단이 「예상노선 1070.4m · 계획노선 1017.5m · 노드 26개」 — 서버 로그의 예상노선 연장 1070m 과 맞음.
|
||||
· **㉓㉔** [거리 재기]가 [되돌리기] 왼쪽에 구분선과 함께 섬. 두 점을 찍으면 지도 오른쪽 아래 작은 창에 값이 뜨고 **곡선 패널은 숨음**. 닫으면 잰 것이 지워지고 모드도 꺼짐.
|
||||
· **㉕** 꼬였던 초록 띠가 풀림 — 누가거리로 자르니 a 에서 b 까지 노선을 그대로 따라감.
|
||||
· **㉖㉗** 노드를 고르면 패널이 뜨고 **빈 곳을 누르면 닫힘**, 닫기 단추도 붙음. 하단 정보는 **「내각 162°」 한 줄**만 남음(「칸을 비우면 자동」까지 지움).
|
||||
· **㉘** 고른 등고선 위에 높이값(`890m`)이 크게 붙음.
|
||||
· **㉙㉚** 회전 아이콘이 반원 화살표로 바뀜. **180° 로 돌려도 글자가 모두 바로 섬**(시점·종점·측점 번호·등고 높이) — 그림만 돌고 숫자는 눈높이. 되돌리려면 `_Rotate.ts` 의 `UPRIGHT_LABELS` 를 `false` 로만 바꾸면 됨.
|
||||
· 700줄 규정에 맞춰 모달 뼈대를 `_Chrome.ts`, 횡단 그리기를 `_Cross_Draw.ts` 로 떼어냄. 전체 시험 **1318 통과 · 22 건너뜀**.
|
||||
|
||||
**⭐ 사용자 확정 (2026-09-12)**
|
||||
|
||||
1. 「최소 길이」 = **곡선 길이 L**.
|
||||
2. 하한은 **아예 못 넘게 막음** — 0-2 의 「표시만」을 이 자리에서 뒤집음.
|
||||
3. **작업임도는 규정이 없어 하한 0** — 다만 **설정값**으로 두어 나중에 값만 바꾸면 바로 걸리게 함.
|
||||
4. ⑧ 은 **별도 창** — 원지반 횡단선 · 기본 계획 횡단선 · 성토사면 길이 셋만. 구조물 없음. B06 로직 재사용.
|
||||
|
||||
**⚠ 남은 가정 하나** — 평면 곡선 길이 L 의 하한은 법령·교본에 **없음**. 임도 종류별 설정값 자리만 만들고 **전부 0** 으로 열어 둠(작업임도와 같은 방식). 값이 정해지면 그 자리에 적기만 하면 됨.
|
||||
|
||||
### 0-8. ⚠⚠ 같은 프로젝트를 둘이 만지면 **앞사람 설계가 통째로 지워짐** (2026-09-09 사용자 지시로 조사)
|
||||
|
||||
**다른 프로젝트끼리는 안전함.** 위험은 **한 프로젝트를 둘이 만질 때**만 생기고 그때는 크게 생김.
|
||||
업로드 완료 판정이 `clear_designing()` · `discard_initial_snapshot()` · `purge_project_outputs()` 를
|
||||
부르는데 **누가 올렸는지 안 따짐** — B 가 파일 하나 올리면 A 가 며칠 만든 설계가 **스냅샷까지**
|
||||
사라짐. 막는 가드(분석 중 차단 · 같은 이름 옛 행 내림)가 **직행 `/files` 한 갈래에만 없음.**
|
||||
|
||||
**고칠 순서 (사용자 확인 대기)**
|
||||
|
||||
- [x] **직행 `/files` 가드 — 이미 붙어 있었음**(2026-09-09 확인) — 세 갈래가 같아졌음.
|
||||
- [x] **purge 전 확인 단계 — 이미 있었음**(2026-09-09 확인) — 409 로 묻고 무엇이 지워지는지 알림.
|
||||
- [x] **프로젝트 단위 업로드 잠금 표시**(2026-09-09, `018ebe7f`) — 「○○○ 님이 올린 자료를
|
||||
|
||||
분석하는 중입니다」가 B03 에 뜸. 올리는 경로 넷 모두 시작한 사람을 실음.
|
||||
⚠ 실제로 도는 분석 화면은 못 봤음 — 7장 V-21 로 넘겼음.
|
||||
|
||||
### 3-6. B06 구조물 추가 — 남은 것 (2026-09-05 조사 → 2026-09-08 정리)
|
||||
|
||||
**선 것** — B06 에서 구조물 전 군(A~G) 배치 가능. B군(종단배수 6종)은 **연장(m)** 으로
|
||||
`common_util_structure_lengths.py` 가 냄(겹친 구간 합침, 원래 합도 함께 냄). 종단·평면에
|
||||
구간 띠 표시. 측구(옆도랑)는 **횡단 설계가 `ditch_area_m2` 로 이미 세므로 구조물 수량에서 제외**
|
||||
(`design_owner: 횡단 설계`) — 또 세면 이중 계상.
|
||||
|
||||
- [x] **도수로·절토사면 배수로 — 보류 확정**(2026-09-09) — 품셈에 그 공종이 없어
|
||||
|
||||
**배치만 받고** 단가·일위대가를 안 만듦. 사유로 드러내 두는 것이 확정된 동작임.
|
||||
|
||||
### 3-16. 측구 토글 화면 실측 — 남겨 둔 것 (2026-09-09)
|
||||
|
||||
- [x] **측구 토글이 화면에서 도나**(2026-09-09, `5907de85`·`2eb2c972`) — 안 돌던 까닭은
|
||||
|
||||
선택(`ditch_choice`)이 캐시·재계산·저장 patch·서버 스키마 **넷 다에 없어** 버려진 것.
|
||||
이은 뒤 실측 — 끄면 절토 계 3.33 → 1.78㎡, 다시 켜면 3.90㎡. 저장이 옛 면적을 싣던
|
||||
자리도 함께 고쳤음(저장 직전 전 측점 재계산).
|
||||
|
||||
#### 확정 ② 벽 두께 — 식이 나왔음 (뒷길이 기반)
|
||||
|
||||
```
|
||||
상부 두께 = ℓ3 + 0.30 (ℓ3 = 뒷길이, m)
|
||||
하부 두께 = 상부 + 0.30 × (H − 1.0)
|
||||
```
|
||||
|
||||
⚠ 돌조공(H=0.5)만 이 규칙 밖 — 다른 시설이라 따로 둘 것.
|
||||
|
||||
- [x] **표준도에 상부·하부 두께 칸**(2026-09-09, `3a64bb5b`) — 비우면 실무 구조물도 식,
|
||||
|
||||
넣으면 그 값이 이김. 실측 — 상부 0.60 저장에 입적 24.375 → 20.625㎥, 비우니 복귀.
|
||||
|
||||
### 4-14. 시험 예외 목록 — 잔손질 하나
|
||||
|
||||
- [x] `**spoil_bank` 예외 두 줄 걷음**(2026-09-09, `0ff26d43`) — 정본 사유가 덮고 있어
|
||||
|
||||
목록 없이도 통과. ⚠ 같은 상태가 13 개 더 있음 — 그 파일을 손댈 때 함께 걷을 자리.
|
||||
|
||||
### 8-36. ⭐ 수량·원가 화면 — 좌측 패널 일원화 + 데이터 분류·근거 호버 (2026-09-12 사용자 지시) — 모두 끝, 검증 대기
|
||||
|
||||
까닭 — 메인 화면에 뜨는 수많은 숫자가 **어디서 왔고 어떻게 계산됐는지** 화면에서 가릴 길이 없어 개발·검산이 막힘. 나아가 **어느 값을 사용자가 고치게 열어 줄지**도 분류가 없어 못 정함. 실측으로 드러난 것 셋 — B03~B07 은 좌측 그룹마다 `ui-collapsible ui-sidebar-section`(접히는 상자+공통 외곽선)에 바닥 고정 `ui-sidebar-actions` 를 쓰는데 **B08 은 상자가 없고 B09 는 공통 클래스를 하나도 안 씀**, B08 은 개발용 「확정 없이 다음으로」 줄에도 `ui-sidebar-actions` 를 붙여 공용 코드가 **첫 번째 것**을 집는 바람에(`ui_template_overlay.ts:115`) 스크롤 영역이 그 줄 안에 갇혀 **패널 내용이 잘림**, 근거 표기는 B08 구조물 원단위 표(「근거」·「출처」 열)와 B09 원가계산서(`formula_text`)에만 있고 토적표·집계표·운반표·일위대가는 **맨 숫자뿐**임.
|
||||
|
||||
- [x] **⓪-가. B09 좌측 패널 공통 틀로 교체** `[데스크탑 보조]` — `b09-panel__group` → `ui-collapsible ui-sidebar-section`, `b09-panel__actions` → `ui-sidebar-actions`, 그룹 제목에 `ui-collapsible__title`. 만지는 파일은 `B09_Estimation_UI_Page.ts` 한 개뿐이고 ⛔ `B08_*` 은 데스크탑 메인이 잡고 있어 손대지 말 것. 화면 판정 = 상자 제목 눌러 접힘 · [계산]·[확정] 이 바닥에 붙어 늘 보임 · 본문만 구름.
|
||||
|
||||
**자체검증** — ORCA 내장 브라우저(5174)에서 B09 화면을 통째로 띄워 실좌표로 잼.
|
||||
상자 다섯(공사 조건·요율 판·관급자재·이윤 조정·수량) 모두 `b09-panel__group ui-collapsible
|
||||
ui-sidebar-section`, 제목 다섯에 `ui-collapsible__title`. ① 제목 클릭 = 상자 높이 332px → 44px,
|
||||
캐럿 ▾ → ▸, 다시 눌러 332px 복귀. ② 액션 줄이 창 바닥(920px)에 붙음 — 스크롤 영역을 끝까지
|
||||
내려도(226/226) 액션 줄 좌표 [863, 920] 그대로. ③ 스크롤은 안쪽 래퍼만(scrollHeight 964 /
|
||||
clientHeight 738), 패널 본체 scrollTop 0 — 잘리는 데 없음. 상자 꼴은 테두리 1px(공통
|
||||
`ui-sidebar-section` 색) · 패딩 12px · 모서리 8px 로 B04~B07 과 같음.
|
||||
⚠ `b09-panel__group` 은 기초자료 본문 표도 쓰므로 상자 CSS 는 `.ui-sidebar-section` 이 함께
|
||||
붙은 것만 집음. 공용 파일(`ui_template/`)·`B08_*` 은 안 건드림. `tsc --noEmit` 통과.
|
||||
- [x] **⓪-나. B08 좌측 패널 공통 틀로 교체** `[데스크탑 메인]` — 조건 칸을 접히는 상자 다섯으로 묶음(토량환산계수 / 지반 구성비·시공법 / 현장값(표토·층따기·사토장·용수·운반거리) / 규준틀·임목 / 부대시설). 개발용 줄은 `ui-sidebar-actions` 를 떼고 자체 클래스로 바꿔 **짤림의 원인을 없앰**. 같은 실수 재발을 막게 공용 `splitSidebarActions` 가 **마지막** 액션 줄을 집게 고칠지는 B08 이 통과한 뒤 판단(B04~B08 전부에 닿음).
|
||||
|
||||
**자체검증**(2026-09-12, 커밋 `648bc528`) — 상자는 다섯이 아니라 **일곱**이 됨(제목 여섯 + 무제목 하나). 제목을 따로 지어내지 않고 **이미 있던 제목 줄을 그대로 상자 제목으로 옮긴** 까닭임 — 표토제거 · 구조물·사토 · 부대시설 개소 · 규준틀 개소당 재료 · 임목파쇄 · 콘크리트 타설 여섯과, 산출법 한 줄·토량환산계수 구획을 담는 무제목 상자 하나임(토량환산계수는 제 제목을 제 안에 들고 있어 제목을 둘로 만들지 않음). 지반 구성비·반영률 상자는 표가 있어야 서는 칸이라 이번 측정에는 안 뜸 — 표가 붙으면 같은 규칙으로 상자가 둘 더 섬.
|
||||
|
||||
측정(ORCA 내장 브라우저 5173, 모듈을 직접 불러 패널을 그림) — 스크롤 래퍼가 패널 직계로 돌아옴(`scrollParent = b08-quantity__panel ui-sidebar-fill`, 전에는 개발용 줄 안) · `.ui-sidebar-actions` 가 **1개**만 남음(전 2개) · [저장]·[확정] 줄 바닥 920px = 패널 바닥 920px = 창 높이 920px 로 **잘림 없음** · 스크롤은 안쪽 래퍼만 · 「구조물·사토」 제목 클릭에 상자 높이 511px → 42px → 511px 로 접힘·펼침. `tsc --noEmit` 통과 · `pytest -q` **1294 passed**.
|
||||
|
||||
곁들이로 잡은 것 — 표를 못 받은 경우(`table === null`) `concrete_placing` 접근에서 터져 **페이지가 통째로 백지**가 되던 것을 `?.` 로 막음. 정작 보여야 할 「표를 못 불렀다」 안내까지 같이 사라지던 자리임.
|
||||
|
||||
남은 것 — 토량환산계수 구획의 접힘은 **표가 있어야** 그 구획이 서므로 로그인한 화면에서 한 번 더 볼 것. 공용 `splitSidebarActions` 를 「마지막 액션 줄」로 바꿀지는 아직 안 건드렸음(B04~B08 전부에 닿음).
|
||||
- [x] **①. 출처 등급 여섯 못 박기** — `입력`(사용자가 넣음) · `측량`(앞 단계 B05·B06 이 낳음) · `기준`(법·품셈·단가판, 고정) · `계산`(위 셋으로 만든 중간값) · `최종`(내역서·원가로 나가는 값) · `막힘`(근거가 없어 못 세움). ⚠ 이 여섯이 뒤에 오는 모든 사전의 밑동이라 **B08 토적표로 먼저 검증한 뒤** B09 로 넘김.
|
||||
|
||||
**자체검증**(2026-09-12) — 등급을 **여덟**으로 못 박음. 사람이 고르는 여섯(`input`·`survey`·`standard`·`calc`·`final`·`blocked`)에, 사전이 쓰는 둘을 더함. 정의처는 `common_util/common_util_provenance.py` 한 곳뿐임.
|
||||
|
||||
**여섯으로 모자랐던 자리 둘 — 양쪽 조사에서 같이 나옴**
|
||||
㉠ **`excluded`(제외) 신설** — 데스크탑 보조의 B09 조사 ㉱. 내역서의 「우리 줄이 아닌 것」(`not_our_row`)·검산용 제외 줄·자재대의 안 갈린 것은 **못 세운 것이 아니라 세면 안 되는 것**이라 `blocked` 와 뜻이 정반대임. 둘을 한 등급에 두면 사용자가 빈 칸을 채우려 들고 그것이 곧 이중계상임(8-7). 색도 다르게 줌 — `blocked` 는 붉은 띠, `excluded` 는 회색 빗금.
|
||||
㉡ **`rule`(채택 규칙) 칸 신설** — 같은 조사 ㉯. 안전관리비 A·B 중 작은 쪽, 자재단가 다섯 중 적용처럼 **값 안에 선택이 숨은** 열은 `calc` 로만 적으면 「왜 그것을 골랐나」가 사라짐. 열 사전에는 **규칙만** 적고 후보값은 줄 쪽으로 내려보냄.
|
||||
|
||||
**토적표에는 `final`(최종) 열이 없음 — 억지로 안 붙임.** 이 표는 중간 장부이고 내역서로 나가는 값은 토공집계표에서 섬. 실제로 붙은 것은 `survey` 여섯 열(단면적 넷 + 측점) · `calc` 열넷뿐임. 여섯을 한 장에 다 채우려고 아무 열에나 `final` 을 붙이면 「분류가 있다」는 거짓만 남음.
|
||||
|
||||
측정(ORCA 내장 브라우저 5173, 토적표 모듈에 실제 사전을 물려 그림) — 사전 **20열** · 표시된 칸 40개(2줄 × 20열) · 등급 갈래 `survey` 12 / `calc` 28 로 열 성격대로 갈림. 호버 카드에 「절토 토사 보정량 · 계산 · 값 45.00 · 식 절토 토사 입적 × 토량환산계수(다짐) · 원천 기본값 정의처 · 자리 `EarthworkTable.py:210`」 이 그대로 뜸. 토글은 켜면 `is-prov-tinted` 가 붙고 칸 배경이 투명 → 초록 7 %, 왼쪽 띠 `inset 3px`, 다시 누르면 꺼짐.
|
||||
|
||||
**고쳐 잡은 것** — 줄 사유(측구 안분 폴백)가 **그 줄의 모든 칸**에 뜨던 것을 그 사유가 닿는 열(`ditch_*`)에만 뜨게 막음. 「절토 보정량」 카드에 「측구 가름값이…」 가 떠서 읽는 사람을 속이던 자리임. 사유가 늘면 엔진이 「어느 열에 닿는 사유인가」를 같이 내는 쪽이 맞음.
|
||||
|
||||
시험 `resources/tester/test_b08_provenance.py` **8개 추가** — 값어치는 첫 번째에 있음: **사전 열 이름이 전부 실제 `EarthworkRow` 에 있는가.** 엔진이 열 이름을 갈면 사전만 옛것으로 남는데 화면에서는 카드가 그냥 안 떠서 눈에 안 띔. 배포환경에서 `None` 이 나오는 것도 시험으로 박음.
|
||||
**등급 여섯은 한 페이지가 아니라 두 페이지를 합쳐야 다 쓰임** — 토적표엔 `final` 이 하나도 없고(중간 장부), B09 에는 원가계산서의 총원가·도급금액·총계·내역 금액·자재대 금액으로 분명히 있음(데스크탑 보조 확인). 한 장에서 여섯이 다 안 보인다고 등급을 줄이지 말 것.
|
||||
- [x] **②. 등급 색칠 토글** — 셀 왼쪽 얇은 띠 + 옅은 배경. 여섯 색이 늘 켜져 있으면 표가 알록달록해 실무 시트 대조를 방해하므로 **평소엔 꺼 두고 토글로 켬**. 토글 단추는 시트 탭 줄에 둠(좌측 패널이 아님 — 패널 작업과 파일이 겹치지 않게).
|
||||
- [x] **③. 칸 단위 근거 호버** — 마우스가 **그 숫자 칸**에 올라가면 카드가 뜸. 카드에 등급 · 값 · 식 · 원천 · (개발 빌드만) 파일:줄. 줄마다 갈리는 것(막힌 사유 · 폴백 안분 같은 것)은 그 칸에 따로 얹힘.
|
||||
- [x] **④. 사전은 서버가 듦 + 배포 빌드에서 끊음** `[로직 보안]` — 「어디서 와서 어떻게 계산됐나」의 정답은 엔진이 아는 것이라 화면 TS 에 손으로 적으면 엔진을 고칠 때 설명만 옛것으로 남음. 새 파일 `B08_Quantity_Provenance.py` 에 **열 단위 사전**을 두고 응답에 실음(칸마다 만들면 토적표 한 장이 30열×200줄=6천 칸이라 응답이 붐 — 겉보기는 칸 단위 그대로임). ⚠ **화면에서 숨기는 것으로는 보안이 안 됨** — API 로 그대로 뚫림. 배포 빌드에서는 **서버가 사전을 아예 안 실어 보냄**, 문은 이미 있는 `ENVIRONMENT`(`common_util_dev_unlock` 과 같은 방식)를 그대로 씀. 토글 단추도 개발 빌드에만 뜸.
|
||||
- [x] **⑤. 원천은 글로만 적음 — 자동 이동은 보류** — 카드에 「원천: B06 횡단 NO.12+5」라고 **적기만** 함. 눌러서 그 화면·그 측점으로 **이동하는 것은 지금 못 함** — B05·B06 이 「어느 측점을 보여 달라」를 밖에서 받는 문이 없고(측점 선택이 화면 안 변수로만 있음), 노선을 다시 탐색하면 측점 번호가 어긋나 엉뚱한 데로 감. 뒤에 붙일 때는 측점 번호가 아니라 **측점 id** 로 짚을 것.
|
||||
|
||||
**자체검증 ②③④⑤** (2026-09-12, 커밋 `6e8de279`·`c94e7773`·`e1927423`) — 뼈대는 공용 두 파일에 한 벌로 섬. `common_util/common_util_provenance.py`(등급 여덟 · `ColumnProvenance` · `provenance_payload`)와 `ui_template/ui_template_provenance.ts`(호버 카드 · 등급색 · 토글). B08·B09 가 같은 것을 씀.
|
||||
|
||||
**② 토글** — 시트 탭 줄 끝. 평소 꺼짐, 켜면 칸 왼쪽 3px 띠 + 옅은 배경. 측정: 켬 `is-prov-tinted` 붙고 배경 투명 → 초록 `color(srgb 0.12 0.54 0.30 / 0.07)`, 다시 끔 복귀. ⚠ **사전이 안 오면 단추 자체를 안 세움** — 배포 빌드에 빈 단추가 남지 않음.
|
||||
|
||||
**③ 칸 단위 호버** — 카드에 등급·값·식·원천·채택·자리. 겉보기는 칸 단위지만 사전은 열 단위라 응답이 안 붐(토적표 한 장이 6천 칸). **같은 열이라도 줄마다 성격이 갈리는 자리는 칸 등급이 열 등급을 이김** — 원가계산서 「금액」 26칸 중 셋(총원가·도급공사비·총공사비)만 `final`, 토공집계 「계」의 무대 줄만 `excluded`, 준비공 「수량」의 못 세운 줄만 `blocked`. 배지와 띠가 같은 말을 하게 `fill()` 이 칸 등급을 먼저 봄(`c94e7773`).
|
||||
|
||||
**④ 로직 보안** — 문은 `provenance_payload()` 하나. 개발환경이 아니면 `None` 이고 라우터가 응답에서 칸을 통째로 뺌. ⚠ **화면에서 숨기는 것이 아니라 안 보내는 것** — API 를 직접 불러도 안 나옴. 문의 정본은 `is_dev_environment()` 한 곳으로 통일(개발용 문이 두 벌이면 한쪽만 닫히는 날이 옴). 배포환경 `None` 은 시험으로 박음.
|
||||
|
||||
**⑤ 원천은 글로만** — 카드 「원천」 줄에 어느 표·어느 단계가 낳았는지 적음. 자동 이동은 안 붙임(보류 근거는 항목 본문).
|
||||
|
||||
- [x] **⑥. 사전 채우기 — B08 여섯 장 · B09 열여섯 장** — **양쪽 다 닫힘.**
|
||||
|
||||
**B08**(데스크탑 메인, `e1927423`) — 여섯 장 48열: 토적표 20 · 토공집계 5 · 운반 5 · 준비공 5 · 자재총괄 7 · 구조물 원단위 6. 화면 판정(ORCA 5173, 실제 사전을 물려 다섯 표를 그림): 집계 정상줄 `final` / 무대줄 `excluded` · 운반 정상 `calc` / 무대 `excluded` · 준비공 정상 `calc` / 값 없는 줄 `blocked` · 자재 총수량 `final` · 관급사급 `input` · 원단위 수량 `calc`. 시험 12개(`test_b08_provenance.py`) · `pytest -q` **1317 passed**.
|
||||
|
||||
**B09**(데스크탑 보조, `d1a25080`·`aadb4f67`) — 열여섯 장 92열. 등급 여덟이 **전부** 쓰임(standard 52 · calc 19 · excluded 7 · unclassified 7 · final 2 · input 2 · survey 2 · blocked 1). 화면 판정에서 나온 것: 자재단가대비표 `slot_price` 6칸 중 값 없는 5칸이 `blocked` 로 서서 「0원」과 「그 판에 그 품목이 없음」이 색으로 갈림 · 산출기초는 **모으는 표라 카드에 「식」 줄이 아예 없음**(값·원천·자리만) · 기초자료 실데이터 4,392칸 표시.
|
||||
|
||||
**사전 대상에서 뺀 둘** — 설계서 구성표(프로젝트가 낳은 값이 아니라 문서 목록) · 산출 조건 패널(표가 아니라 입력 칸이고 칸 밑 `hint` 가 이미 같은 일을 함). 없는 것을 지어내면 거짓만 남음.
|
||||
|
||||
**등급 여섯은 한 장이 아니라 여러 장을 합쳐야 다 쓰임** — 토적표엔 `final` 이 하나도 없고(중간 장부) B09 원가계산서·내역서·자재대에 몰림. 한 장에서 여섯이 다 안 보인다고 등급을 줄이지 말 것. 이 갈림은 시험으로 박음.
|
||||
|
||||
- [x] **⑦. B09 내역서 「비고」 사유 유실 — 고침** `[데스크탑 보조]`
|
||||
|
||||
덮어쓰던 열세 곳 + 이어 붙이던 일곱 곳을 조각 배열 `notes` 로 옮기고 조각마다 **닿는 열 키**를 함께 실음. 화면 「비고」 칸은 조각을 종전 꼴(`/` 이음)로 되뽑는 property 라 **눈에 보이는 것은 안 바뀜** — 토글 끈 사용자도 그대로 봄. 판정: 같은 줄에서 규격 카드엔 「갈래: 토사」만, 수량 카드엔 반영률 문구만, 단가 카드엔 단산 번호만, 금액 카드엔 줄 사유 없음. 줄 전체에 걸리는 사유(열 키 빈 것)만 세 칸 모두에 붙음.
|
||||
|
||||
⚠ **줄 사유는 그 사유가 닿는 열에만 붙일 것** — B08 에서 줄 사유를 그 줄 모든 칸에 띄웠다가 「절토 보정량」 카드에 「측구 가름값이…」 가 떠서 읽는 사람을 속인 자리가 있었음(2026-09-12 실측 후 고침).
|
||||
|
||||
**앞작업 — B09 원천 조사표 완료** (2026-09-12, 데스크탑 보조) · [조사표](docs/raw/verification/2026-09-12_B09_데이터_원천_조사.md)
|
||||
탭 열 개(살아 있는 아홉)의 화면 열마다 이름·원천(`파일:줄`)·식·등급 후보를 적음.
|
||||
이미 있어 그대로 쓸 것 넷 — `formula_text`(원가계산서 식) · `source_label`(일위대가 원천) ·
|
||||
`base_label`(법정경비 밑수) · 산출기초 ③ `work_item_basis`. 나머지 표는 식·원천·등급이 통째로 없음.
|
||||
⚠ **등급 여섯에 안 맞는 것 일곱 갈래**를 조사표 6장에 모음. 가장 확실한 어긋남은 ㉱ —
|
||||
「여기서 안 세는 줄」(`not_our_row` · 검산용 제외 줄 · 관급사급 안 갈린 것)이 `막힘` 과 뜻이
|
||||
정반대라 일곱째 등급 「제외」가 필요해 보임. 이 결정이 나머지보다 앞섬 — ① 을 굳힐 때 함께 볼 것.
|
||||
|
||||
**B09 배선 완료** (2026-09-12, 데스크탑 보조). 데스크탑 메인이 낸 공용 한 벌
|
||||
(`common_util_provenance.py` · `ui_template_provenance.ts`) 위에 B09 쪽만 얹음.
|
||||
⛔ 공용 두 파일은 안 건드림 — 그 창 소관.
|
||||
|
||||
**① 비고 덮어쓰기 버그 먼저 고침** (사전보다 앞세우라는 지시). `BillRow.note` 한 칸에
|
||||
덮어쓰던 열세 곳 + 이어 붙이던 일곱 곳을 **조각 배열 `notes`** 로 옮김.
|
||||
조각마다 **닿는 열 키**를 함께 실음(규격·수량·단가·금액·줄전체). 화면 「비고」 칸은
|
||||
조각을 종전과 **같은 꼴**(`/` 이음)로 되뽑는 property 라 눈에 보이는 것은 안 바뀜.
|
||||
⇒ 한 줄에 사유가 둘이면 하나가 사라지던 것이 멈춤.
|
||||
|
||||
**② 열 사전** `B09_Estimation_Provenance.py` (새 파일 — 기존 B09 파일이 700줄 초과).
|
||||
8장 57열 — 원가계산서 5 · 내역서 8 · 일위대가 목록 6/본표 9 · 자재대 7 ·
|
||||
자재대(안 갈린 것) 7 · 중기 9 · 기초자료 목록표 6. 이미 있던 `formula_text` ·
|
||||
`source_label` · `base_label` 은 그대로 쓰고 없는 것만 새로 적음.
|
||||
**③ 배선** 라우터 5개 응답에 `_with_provenance()` (개발환경 아니면 칸 자체를 안 만듦) ·
|
||||
표 여섯에 `markProvenanceCell` + `attachProvenance` · **④ 등급색 토글은 탭 줄 끝**.
|
||||
|
||||
**자체검증** — ORCA 내장 브라우저(5174)에서 진짜 사전 JSON(12.7KB)을 서버에서 뽑아
|
||||
페이지 fetch 에 물리고 실제로 눌러 봄.
|
||||
· 원가계산서 3줄×5열 = **15칸 표시**, 등급 `standard`·`calc`·`unclassified` 셋.
|
||||
· 내역서 그룹줄 **0칸**(값이 없으니 카드도 없음) · 잎줄 각 8칸 · 사유 실린 줄 2개.
|
||||
· **닿는 열 판정 통과** — 같은 줄에서 규격 카드엔 「갈래: 토사」만, 수량 카드엔
|
||||
「반영률 80%…」만, 단가 카드엔 「단산 46」만, 금액 카드엔 **줄 사유 없음**.
|
||||
· 자재대 「안 갈린 것」 표는 일곱 열이 모두 `excluded`, 줄 전체 사유는 세 칸 모두에 붙음.
|
||||
· 토글 = 끔 `box-shadow:none` → 켬 `excluded` 회색 `rgb(156,163,175)` ·
|
||||
`final` 금색 `rgb(184,134,11)` 3px inset → 다시 끔 `none`. sessionStorage 에 저장됨.
|
||||
· 사전 안 온 탭은 표시 0칸·오류 없음. 카드는 mouseout 에 닫힘.
|
||||
· `tsc --noEmit` 통과 · pytest 187 passed(b09·bill·boq·haul).
|
||||
|
||||
⚠ **남긴 어긋남 — 등급이 열에 하나뿐이라 못 적은 자리**
|
||||
원가계산서 「금액」은 같은 열 안에서 중간줄(간접노무비 등)과 마지막줄(총원가·도급금액·
|
||||
총계)의 성격이 갈리는데 등급은 열에 하나임. `calc` 로 두고 `rule` 에 어느 줄이 `final`
|
||||
인지 적어 둠. **칸 단위 등급이 필요한 첫 자리** — 공용 `fill()` 이 배지를 `column.tier`
|
||||
로만 그려서 칸에 다른 등급을 심어도 배지가 안 따라옴(공용 파일이라 안 고침, 메인 창에 알림).
|
||||
|
||||
⚠ **아직 사전이 없는 장** — 단가산출근거·설계서 구성·산출기초·환율및기초자료·
|
||||
자재단가대비표·산출 조건. 앞 넷은 **값을 낳는 표가 아니라 모으는 표**라 열 사전보다
|
||||
「사전 대상인가」를 먼저 정해야 함(조사표 6장 ㉴).
|
||||
|
||||
**2차 — 남은 여섯 장 가름 완료** (2026-09-12, 두 창 합의). **넷 붙이고 둘은 뺌.**
|
||||
· **붙임** 자재단가대비표(「적용」이 원천 다섯 중 하나를 고르는 자리라 `calc`+`rule`,
|
||||
값 빈 칸은 `blocked`) · 환율및기초자료(`standard` 와 `input` 이 갈리는 장) ·
|
||||
단가산출근거·산출기초(**모으는 표**라 식은 비우고 `source`·등급만).
|
||||
· **뺌** 설계서 구성표(프로젝트 값이 아니라 개발 진척표 — 조사표 ㉴) ·
|
||||
산출 조건 패널(표가 아니라 입력 칸이고 칸 밑 근거 한 줄이 이미 같은 일을 함).
|
||||
⇒ 사전 **16장 92열**. 등급 **여덟이 전부 쓰임** — standard 52 · calc 19 ·
|
||||
excluded 7 · unclassified 7 · final 2 · input 2 · survey 2 · blocked 1.
|
||||
|
||||
**칸 단위 `final` 심음** — 원가계산서 총원가·도급금액·총계 세 줄. 메인 창이 공용
|
||||
`fill()` 을 고쳐 배지도 칸 등급을 따라오게 함. 나머지 칸은 열 등급을 그대로 넘김
|
||||
(공용 채움이 없는 판에서도 띠가 붙게 **여기서 명시**).
|
||||
|
||||
**700줄 넘어 셋으로 가름** — `B09_Estimation_Provenance.py`(457줄, 값을 낳는 장 여덟) ·
|
||||
`_Sources.py`(268줄, 바깥에서 오거나 모으는 장) · `_Common.py`(53줄, 두 쪽이 같이 쓰는
|
||||
이름표·비고 조각 — 한쪽에 두면 불러들이기가 고리를 이룸).
|
||||
|
||||
**자체검증 2차** — 백엔드를 새로 띄우고(`stale:false`) **진짜 서버 응답**으로 잼.
|
||||
· `/estimation/base-data` 가 **16장**을 그대로 실어 보냄.
|
||||
· 기초자료 탭 실데이터에서 **4,392칸** 표시(목록표 셋), 등급 `standard`·`unclassified`.
|
||||
· `tsc --noEmit` 통과 · pytest 187 passed.
|
||||
· **자재단가대비표** `slot_price` 6칸 중 **값 없는 5칸이 `blocked`** — 「0원」이 아니라
|
||||
「그 판에 그 품목이 없다」가 색으로 드러남. 「적용」 칸은 0 — 슬롯 6 이 곧 적용 단가라
|
||||
그 자리에 값이 서면 칸을 따로 안 세움(정상).
|
||||
· **환율및기초자료** 시간당 3칸 `calc` · 유가 적용 범위 1칸 `input` · 일당·산식·단가·
|
||||
기준일·자료 모두 붙음. 한 탭에 등급 다섯(standard·unclassified·blocked·calc·input).
|
||||
· **산출기초** 지문 12칸 `unclassified` · 못 채운 사유 105칸 `blocked` · 근거 65칸
|
||||
`standard`. **모으는 표 카드에 「식」 줄이 아예 없음**(값·원천·자리만) — 없는 식을
|
||||
지어내지 않은 것이 화면에서 확인됨.
|
||||
· **칸 단위 `final`** 원가계산서 금액 26칸 중 **정확히 셋**(총원가·도급공사비·총공사비)이
|
||||
`final`, 나머지 23칸 `calc`. 띠 색 최종 `rgb(184,134,11)` · 계산 `rgb(31,138,76)`.
|
||||
· **배지 어긋남 해소 확인** — 메인 창의 공용 `fill()` 고침(`c94e7773`)을 받은 뒤 다시 잼.
|
||||
총원가 칸 카드 「금액**최종**」 · 재료비 칸 카드 「금액**계산**」 — 띠와 배지가 같은 말을 함.
|
||||
금액 26칸이 `final` 3 · `calc` 23 으로 갈리고, 등급을 안 심은 「비목」 칸은 공용이
|
||||
열 등급(`standard`)으로 채워 띠가 붙음(`rgb(97,94,110)`).
|
||||
⚠ 서버는 uvicorn StatReload 가 멈춰(main.py 가 리로드마다 프론트 빌드를 다시 돌림) 옛
|
||||
코드로 굳어 있어, 라우터 두 곳은 **함수를 직접 불러** 응답을 받아 그 실제 응답으로 잼
|
||||
(둘 다 사전 16장 실림 확인). 프로세스 종료가 창 권한에 막혀 재시작은 사용자 몫.
|
||||
|
||||
⚠ **새 코드를 옛 파일에 넣지 말 것** — `B08_Quantity_Engine_UnitQuantity.py`(1606줄) · `B09_Estimation_UI_Page.ts`(1415줄) · `B09_Estimation_UnitPrice.py`(1173줄) · `B09_Estimation_UI_BaseData.ts`(1080줄) 가 이미 700줄을 크게 넘김. 사전·호버는 **새 파일**로 뺄 것.
|
||||
|
||||
### 8-36 ⑧. ⭐ 실제 화면 통합 검증 (2026-09-12, 데스크탑 메인) — 끝
|
||||
|
||||
지금까지의 판정은 **모듈을 직접 띄워 꾸민 자료로** 잰 것이었음. 사용자 지시로 **로그인한 진짜 화면·진짜 서버 응답**으로 한 바퀴 다시 돌았음. 백엔드를 새로 띄워(`stale:false`, `code_mtime == source_mtime`) 옛 코드로 헛검증하는 일을 막았음.
|
||||
|
||||
- [x] **B08 좌측 패널** — 상자 **아홉**(제목 여덟 + 무제목 하나). 제목: 암 갈래 구성비 · 반영률 · 표토제거 · 구조물·사토 · 부대시설 개소 · 규준틀 개소당 재료 · 임목파쇄 · 콘크리트 타설. ⚠ 앞서 모듈 검증에서 일곱이던 것이 아홉이 된 까닭은 **표가 있어야 서는 상자 둘**(암 갈래 구성비·반영률)이 실데이터에서 섰기 때문임 — 예상대로임. 「암 갈래 구성비」 제목 클릭에 258px → 42px → 258px. 스크롤 래퍼는 패널 직계(`b08-quantity__panel ui-sidebar-fill`)이고 실제로 구름. [저장]·[확정] 바닥 920px = 패널 바닥 920px = 창 920px — **잘림 없음.** 토량환산계수 구획도 제 제목으로 접힘(실데이터에서만 서는 자리라 이번에 처음 확인).
|
||||
- [x] **B08 여섯 장 전부에 등급이 붙음 — 모두 1,914칸** · 토적표 1,240(측량 372 · 계산 868) · 토공집계 85(기준 68 · **최종 15** · **제외 2**) · 운반거리 30(계산 26 · **제외 4**) · 준비공 70(기준 42 · 계산 23 · **막힘 5**) · 구조물 원단위 426(측량 142 · 기준 213 · 계산 71) · 자재총괄 63(기준 27 · 계산 9 · **최종 9** · **입력 18**). 탭마다 카드가 실제로 떴고 식·원천·자리가 그 장의 엔진을 가리킴.
|
||||
- [x] **칸 등급이 열 등급을 이기는 자리가 실데이터에서 확인됨** — 토공집계 「계」 열 안에서 정상 줄은 `최종`, 무대(소운반 20m) 줄만 `제외` 2칸. 운반표도 같은 줄의 토량·거리 둘이 `제외`. 준비공은 값을 못 세운 줄 5칸이 `막힘`.
|
||||
- [x] **B09 좌측 패널** — 상자 다섯(공사 조건 · 요율 판 · 관급자재 · 이윤 조정 · 수량) 모두 접힘(332px → 44px → 332px). [재계산]·[확정] 바닥 920px. 스크롤 래퍼 `b09-panel ui-sidebar-fill`.
|
||||
- [x] **B09 사전이 서버 응답에 실림** — `unit-prices`·`base-data`·`price-sources`·`basis-sheet`·`bill` 다섯 응답 모두 `provenance.sheets` **16장**. 화면: 일위대가 1,740칸(기준 580 · 계산 1,160) · 산출기초 623칸 · 원가계산서 130칸(기준 52 · 계산 49 · 미분류 26 · **최종 3**).
|
||||
- [x] **배지와 띠가 같은 말을 함**(`c94e7773` 확인) — 원가계산서 같은 「금액」 열에서 **총원가 = 배지 「최종」 · 띠 금색 `rgb(184,134,11)`**, **재료비 = 배지 「계산」 · 띠 초록 `rgb(31,138,76)`**.
|
||||
|
||||
**남긴 것 · 되돌린 것**
|
||||
- 원가계산서·내역서는 이 프로젝트에 원가가 안 돌아 있어 빈 표였음 → **[재계산]을 눌러 26줄을 세운 뒤 쟀음.** 데스크탑 메인 검증 프로젝트(`79da822d…`)라 남의 자리가 아님.
|
||||
- 검증 뒤 브라우저가 쓰던 값을 그대로 되돌림 — `frd_current_project_id` 를 원래 값(`26275834…`)으로, 등급색 토글(`aislo.provenance.tint`)은 지움.
|
||||
- ⚠ **백엔드(8000)를 다시 띄웠음.** 옛 코드로 굳어 있었음(`stale:true`, `started_at` 이 이틀 전). 이 PC 의 로그인 세션이 끊겼을 수 있음.
|
||||
|
||||
### 9-11. 사용자 확정 5차 — 반영 상태 (2026-09-09 밤, 중간 점검 시점)
|
||||
|
||||
- [x] `[반나절]` **돌 무게 인자표** — 이미 들어가 있었음(`252e6b54`). 계산식이 기본, 야면석만 관측표.
|
||||
|
||||
⚠ **야면석은 뒷길이 25·30·60·75 에 원본 값이 없어** 그 규격에서 돌 줄이 안 서고 사유만 남음.
|
||||
- [x] `[잔손질]` **공구손료 칸** — 세웠음(`83c842e3`). 기본은 빔 = 안 붙음.
|
||||
|
||||
⚠ **지금은 넣어도 붙을 밑수가 없음** — 일위대가에 주재료비가 선 공종이 0개(사급 자재 단가 대기).
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
type: completion-summary
|
||||
status: draft
|
||||
related_pages: ["[[B05_Profile/B05_route_edit_2026_09]]", "[[B06_Section/B06_profile_cross_unification_2026_09]]", "[[B07_DesignDetail/B07_standard_drawings_2026_09]]", "[[B08_Quantity/B08_provenance_ui_2026_09]]", "[[B09_Estimation/B09_provenance_ui_2026_09]]", "[[multi_environment_safety]]"]
|
||||
last_updated: 2026-09-12
|
||||
source: ["docs/raw/plans/2026-09-12_plan_checked_complete_items.md"]
|
||||
---
|
||||
|
||||
# 2026-09-12 체크 완료 항목
|
||||
|
||||
## 이관 범위
|
||||
|
||||
- 계획노선 편집 모달 기능 30개 체크와 B05·B06 공용 편집 기반.
|
||||
- 프로젝트 단위 업로드 잠금 표시와 삭제 전 확인 경로.
|
||||
- B06 구조물 보류 결정, 측구 토글 실측, B07 벽 두께 자동식, 사토장 시험 예외 정리.
|
||||
- B08·B09 좌측 패널 일원화와 값 출처·계산 근거 표시.
|
||||
- 사용자 확정 5차 중 돌 무게 인자표와 공구손료 입력칸.
|
||||
|
||||
## 상태 경계
|
||||
|
||||
- 사용자가 위키 관리자의 2차 교차검증을 생략하도록 지시했다. 이 페이지는 체크 상태와 계획서의 자체검증 기록을 옮긴 `draft`다.
|
||||
- 계획서 제목의 `검증 대기`, 본문의 `사용자 확인 대기`, `보류 확정`을 완료 검증으로 바꾸지 않았다.
|
||||
- 미완료 체크가 남은 `3-17 횡단도 손질` 등은 아카이브하지 않고 상시 계획서에 유지했다.
|
||||
|
||||
## 자체검증 기록 요약
|
||||
|
||||
| 범위 | 계획서 기록 |
|
||||
|---|---|
|
||||
| 계획노선 편집 | ORCA 실제 화면, API 표고 측정, TypeScript·Ruff·Prettier와 전체 회귀 기록 |
|
||||
| B08·B09 근거 표시 | 로그인한 실제 화면·실제 서버 응답에서 B08 1,914칸, B09 사전 16장과 패널 동작 확인 |
|
||||
| 기타 완료 체크 | 각 항목의 커밋·시험·화면 실측 또는 보류 결정은 원문 아카이브에 보존 |
|
||||
|
||||
@@ -171,6 +171,10 @@
|
||||
"169": "B08_DesignDetail_Engine_Cad_Basin.py",
|
||||
"170": "B08_DesignDetail_Engine_Cad_MassHaul.py",
|
||||
"171": "B08 CAD·납품 도면 후속",
|
||||
"172": "B05 계획노선 편집 — 2026-09-12",
|
||||
"173": "B06 종횡단 공용화 — 2026-09-12",
|
||||
"174": "B08 수량 근거 표시 — 2026-09-12",
|
||||
"175": "B09 원가 근거 표시 — 2026-09-12",
|
||||
"176": "OpenWebCAD Core",
|
||||
"177": "common_util_mass_haul_settle.ts",
|
||||
"178": "Drainage Watershed (유역도)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"0": "UI Templates — Localization & Components",
|
||||
"1": "인증 / RBAC",
|
||||
"2": "A00_Common — App Shell Framework",
|
||||
"3": "2026-09-04 완료 항목",
|
||||
"4": "배수유역 해석 및 세부설계 (Drainage Watershed)",
|
||||
"5": "DB: 로그/모니터링 테이블",
|
||||
"6": "A09_Security — Backend",
|
||||
"7": "2026-08-29 완료 반영",
|
||||
"8": "DB: 파일/지표면분석 테이블",
|
||||
"9": "DB: 경로/종횡단 테이블",
|
||||
"10": "A04_NewsHistory — Frontend",
|
||||
"11": "A07_Register — Frontend",
|
||||
"12": "A08_Support — Frontend",
|
||||
"13": "B02_ProjRegister — Frontend",
|
||||
"14": "A01_Home — Frontend",
|
||||
"15": "A05_EduDetail — Frontend",
|
||||
"16": "A06_Login — Frontend",
|
||||
"17": "A08_Support — Backend",
|
||||
"18": "A09_Security — Frontend",
|
||||
"19": "B02_ProjRegister — Backend",
|
||||
"20": "B04_PreProcess — Backend",
|
||||
"21": "B04_PreProcess — Frontend",
|
||||
"22": "B05 구조물 정본·통합 편집",
|
||||
"23": "B06_Section — Frontend",
|
||||
"24": "A03_CompDetail — Frontend",
|
||||
"25": "A06_Login — Backend",
|
||||
"26": "유토곡선 (Mass Haul Diagram) 계산 명세",
|
||||
"27": "A07_Register — Backend",
|
||||
"28": "B06 배수관 구조물 조작·표시",
|
||||
"29": "B06 기슭막이 연동·경사·단별 제어",
|
||||
"30": "B08_DesignDetail — Backend",
|
||||
"31": "B08 토적도·수리집수면적유역도",
|
||||
"32": "B09_Estimation — Frontend",
|
||||
"33": "현재 구현 현황 — 소스 읽기 감사",
|
||||
"34": "DB: 구조물/수량/산출물 테이블",
|
||||
"35": "B01_Dashboard — Backend",
|
||||
"36": "B01_Dashboard — Frontend",
|
||||
"37": "B03_FileInput — Backend",
|
||||
"38": "B05 급선회 3D 국부 보정 계획",
|
||||
"39": "B06 집수정·다단 기슭막이",
|
||||
"40": "B06 배수관 구조물 기하·조작 체계",
|
||||
"41": "B06 배수관 횡단도 세트",
|
||||
"42": "B10_Payment — Frontend",
|
||||
"43": "B11_Status — Frontend",
|
||||
"44": "저장 경로 규칙 (Workflow-based Folder Structure)",
|
||||
"45": "DB: 프로젝트 관리 테이블",
|
||||
"46": "임도기술교본 원문 md 추출 품질 결함",
|
||||
"47": "A01_Home — 세부 구현",
|
||||
"48": "A02_ProgDetail — 세부 구현",
|
||||
"49": "A02_ProgDetail — Frontend",
|
||||
"50": "B01_Dashboard — API",
|
||||
"51": "B03_FileInput — Frontend",
|
||||
"52": "B05 계획노선 코리도 삼각망 서피스",
|
||||
"53": "B05 구조물·UI 현재 계획",
|
||||
"54": "B05 구조물 입력·종단 표시 정비 — 2026-08-19",
|
||||
"55": "B07 구조물 표준도 — 조사·합의와 현재 통로",
|
||||
"56": "B08 횡단도 구조물·장 배치",
|
||||
"57": "2026-09-07 완료·보류 요약",
|
||||
"58": "B07 외부 WebCAD 비교 실행환경",
|
||||
"59": "Temp Upload (프로젝트 생성 전 임시 보관함)",
|
||||
"60": "Q: 배수관 매설시 각도의 제약조건이 있는지 확인해줘. 임도에서",
|
||||
"61": "Q: 임도 기술정보DB에서 집수정의 형태정보는 어떤게 있는지 확인해줘.",
|
||||
"62": "B02_ProjRegister — DB",
|
||||
"63": "B03_FileInput — API",
|
||||
"64": "B04_PreProcess — DB",
|
||||
"65": "B05_Profile — API",
|
||||
"66": "B05 변형 성토면 마감·날개 패치",
|
||||
"67": "B05_Profile — Profile Alignment & Table",
|
||||
"68": "B05 구조물 비정규 측점 공급",
|
||||
"69": "B05_Profile_UI_Drainage_Parts — 배수유역 공용 UI 파츠",
|
||||
"70": "_UI_Drainage_Render — 배수유역도 Canvas 렌더러",
|
||||
"71": "_UI_Profile_Structures — 종단 구조물 렌더링 및 인터랙션",
|
||||
"72": "_UI_Selection — 배수/구조물 3자 선택 동기화",
|
||||
"73": "B06 인접 측점 구조물 트림 정리",
|
||||
"74": "B06 물넘이포장·콘크리트 포장·독립 기슭막이",
|
||||
"75": "B06_Section_Engine_Areas — 횡단 면적 적분 연산 엔진",
|
||||
"76": "B06_Section_Router_Confirm — 임시 저장 및 확정 라우터",
|
||||
"77": "B06_Section_UI_Cross_Areas — 횡단 단면적 표기 및 밴드 하이라이트",
|
||||
"78": "B06_Section_UI_Cross_Design — 횡단 측점별 세부 설계 컨트롤",
|
||||
"79": "B06_Section_UI_MassHaul — 유토곡선 적분 계산 엔진",
|
||||
"80": "B06_Section_UI_MassHaul_Balance — 평형선 및 장비 띠 분할 엔진",
|
||||
"81": "B06_Section_UI_MassHaul_Balance_View — 운반 띠 시각화 렌더러",
|
||||
"82": "B06_Section_UI_MassHaul_Balloon — 물량 말풍선 배치 및 조작",
|
||||
"83": "B06_Section_UI_MassHaul_Curve — 유토곡선 궤적 보간 및 렌더링",
|
||||
"84": "B06_Section_UI_MassHaul_Settle — 토량 정산 및 장거리 상쇄 엔진",
|
||||
"85": "B06_Section_UI_MassHaul_View — 유토곡선 시각화 렌더러",
|
||||
"86": "B06_Section_UI_Page — B06 메인 페이지 오케스트레이터",
|
||||
"87": "B06_Section_UI_Section_View — 종/횡단 및 유토곡선 뷰 조립",
|
||||
"88": "B07 Quantity — Backend",
|
||||
"89": "B08 CAD 블록 라이브러리·사진",
|
||||
"90": "B08 OpenWebCAD 명령 체계",
|
||||
"91": "B08_CAD_table_entity.md",
|
||||
"92": "공개·인증·관리 영역 지도",
|
||||
"93": "공유 자원 영향 지도",
|
||||
"94": "A00_Common — 공통 프레임워크 & 유틸",
|
||||
"95": "app_shell.ts",
|
||||
"96": "router.ts",
|
||||
"97": "B04_PreProcess — API",
|
||||
"98": "B04_PreProcess — Dependencies",
|
||||
"99": "B05_Profile — Backend",
|
||||
"100": "B05_Profile — DB 사용 관계",
|
||||
"101": "B05_Profile — 3D Viewer & Interaction",
|
||||
"102": "B05_Profile_Engine_Sections.md",
|
||||
"103": "B05_Profile_Api_Fetch.ts",
|
||||
"104": "B05_Profile_UI_Drainage_Panel",
|
||||
"105": "B05_Profile_UI_Drainage_Pipes",
|
||||
"106": "B05_Profile_UI_Profile_Panel.md",
|
||||
"107": "B06_Section — DB 사용 관계",
|
||||
"108": "B06_Section_Api_Fetch — B06 프론트엔드 API 클라이언트",
|
||||
"109": "B06_Section_UI_Standard_Diagram — 표준단면 모식도 컴포넌트",
|
||||
"110": "B06_Section_UI_Standard_Panel — 표준단면 입력 및 제어 패널",
|
||||
"111": "B08_DesignDetail — Dependencies",
|
||||
"112": "common_util_project_delete.md",
|
||||
"113": "common_util_storage.md",
|
||||
"114": "common_util_workflow_state.md",
|
||||
"115": "미확정 테이블 보관소 (Unconfirmed DB Schemas)",
|
||||
"116": "A01_Home_UI_Page.md",
|
||||
"117": "A02_ProgDetail_UI_Page.md",
|
||||
"118": "A06_Login_Router.md",
|
||||
"119": "A07_Register_Router.md",
|
||||
"120": "A08_Support_Router.md",
|
||||
"121": "A09_Security_Router.md",
|
||||
"122": "B01_Dashboard — DB 사용 관계",
|
||||
"123": "B01_Dashboard — Dependencies",
|
||||
"124": "B01_Dashboard_Router.md",
|
||||
"125": "B01_Dashboard_UI_Page.md",
|
||||
"126": "B02_ProjRegister_Router.md",
|
||||
"127": "B03_FileInput — DB 사용 관계",
|
||||
"128": "B03_FileInput — Dependencies",
|
||||
"129": "B03_FileInput_Router.md",
|
||||
"130": "B04_PreProcess_Router.md",
|
||||
"131": "B05_Profile — Dependencies",
|
||||
"132": "B05_Profile_Engine_Grade.md",
|
||||
"133": "B05_Profile_Engine_Solver.md",
|
||||
"134": "B05_Profile_Repository.md",
|
||||
"135": "B05_Profile_Router.md",
|
||||
"136": "B05_Profile_Router_Confirm.md",
|
||||
"137": "B05_Profile_Schema.md",
|
||||
"138": "B05_Profile_UI_IrregularStations.md",
|
||||
"139": "B05_Profile_UI_Page.md",
|
||||
"140": "B05_Profile_UI_Panel.md",
|
||||
"141": "B05_Profile_UI_Profile_Alignment.md",
|
||||
"142": "B05_Profile_UI_Profile_Table.md",
|
||||
"143": "B05_Profile_UI_Viewer.md",
|
||||
"144": "B06_Section — Dependencies",
|
||||
"145": "B06_Section_Router.md",
|
||||
"146": "B01~B09 Workflow 데이터 흐름",
|
||||
"147": "B08_DesignDetail — API",
|
||||
"148": "longitudinal_alignment.md",
|
||||
"149": "B03_FileInput_plan_lidar_multi_file.md",
|
||||
"150": "B05_completed_followups.md",
|
||||
"151": "B05_corridor_followup_decisions.md",
|
||||
"152": "B06_api.md",
|
||||
"153": "B07_db.md",
|
||||
"154": "B08_DesignDetail_Router.md",
|
||||
"155": "B09_Estimation_UI_Page.md",
|
||||
"156": "B10_Payment_UI_Page.md",
|
||||
"157": "B11_Status_UI_Page.md",
|
||||
"158": "B03 계획노선 정본·좌표계 후속",
|
||||
"159": "B03 파일 입력 화면 정리",
|
||||
"160": "B04 3D 방위·좌표계 최신 결정",
|
||||
"161": "B04 세부유역·방위·좌표계",
|
||||
"162": "B05 구조물 구간 절취·측벽·성토 패치",
|
||||
"163": "B05 구조물 3D 투영 커브",
|
||||
"164": "B05_Profile — Frontend",
|
||||
"165": "B05 유토곡선·구조물 후속",
|
||||
"166": "B05 종단곡선·실시간 횡단 연동",
|
||||
"167": "B06 횡단 계산 미러·카드 표기",
|
||||
"168": "B06 횡단 관 형상·유토곡선 후속",
|
||||
"169": "B08_DesignDetail_Engine_Cad_Basin.py",
|
||||
"170": "B08_DesignDetail_Engine_Cad_MassHaul.py",
|
||||
"171": "B08 CAD·납품 도면 후속",
|
||||
"176": "OpenWebCAD Core",
|
||||
"177": "common_util_mass_haul_settle.ts",
|
||||
"178": "Drainage Watershed (유역도)",
|
||||
"179": "Mass Haul Diagram (토적도)",
|
||||
"182": "2026-09-02 완료 항목",
|
||||
"183": "2026-09-03 완료 — 입력·배수·종횡단·CAD",
|
||||
"184": "2026-09-03 추가 완료 — 화면·종단·횡단",
|
||||
"185": "Query: 임도 집수정 형태정보",
|
||||
"187": "B03 File Input Backend",
|
||||
"188": "B03 File Input Frontend",
|
||||
"189": "B04 PreProcess Backend",
|
||||
"190": "B04 PreProcess Frontend",
|
||||
"191": "B07_Quantity_Router.md",
|
||||
"192": "B08 CAD Blocks Library",
|
||||
"193": "B08 CAD Table Entity",
|
||||
"194": "B08 Cross Section Structure Sheets",
|
||||
"195": "Query: 배수관 매설 각도 제약조건 (임도)"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"output_tokens": 2385}
|
||||
@@ -0,0 +1,891 @@
|
||||
# Graph Report - wiki (2026-09-11)
|
||||
|
||||
## Corpus Check
|
||||
- 206 files · ~57,686 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 1170 nodes · 996 edges · 189 communities (155 shown, 34 thin omitted)
|
||||
- Extraction: 100% EXTRACTED · 0% INFERRED · 0% AMBIGUOUS
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `e49e4d85`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
- UI Templates — Localization & Components
|
||||
- 인증 / RBAC
|
||||
- A00_Common — App Shell Framework
|
||||
- 2026-09-04 완료 항목
|
||||
- 배수유역 해석 및 세부설계 (Drainage Watershed)
|
||||
- DB: 로그/모니터링 테이블
|
||||
- A09_Security — Backend
|
||||
- 2026-08-29 완료 반영
|
||||
- DB: 파일/지표면분석 테이블
|
||||
- DB: 경로/종횡단 테이블
|
||||
- A04_NewsHistory — Frontend
|
||||
- A07_Register — Frontend
|
||||
- A08_Support — Frontend
|
||||
- B02_ProjRegister — Frontend
|
||||
- A01_Home — Frontend
|
||||
- A05_EduDetail — Frontend
|
||||
- A06_Login — Frontend
|
||||
- A08_Support — Backend
|
||||
- A09_Security — Frontend
|
||||
- B02_ProjRegister — Backend
|
||||
- B04_PreProcess — Backend
|
||||
- B04_PreProcess — Frontend
|
||||
- B05 구조물 정본·통합 편집
|
||||
- B06_Section — Frontend
|
||||
- A03_CompDetail — Frontend
|
||||
- A06_Login — Backend
|
||||
- 유토곡선 (Mass Haul Diagram) 계산 명세
|
||||
- A07_Register — Backend
|
||||
- B06 배수관 구조물 조작·표시
|
||||
- B06 기슭막이 연동·경사·단별 제어
|
||||
- B08_DesignDetail — Backend
|
||||
- B08 토적도·수리집수면적유역도
|
||||
- B09_Estimation — Frontend
|
||||
- 현재 구현 현황 — 소스 읽기 감사
|
||||
- DB: 구조물/수량/산출물 테이블
|
||||
- B01_Dashboard — Backend
|
||||
- B01_Dashboard — Frontend
|
||||
- B03_FileInput — Backend
|
||||
- B05 급선회 3D 국부 보정 계획
|
||||
- B06 집수정·다단 기슭막이
|
||||
- B06 배수관 구조물 기하·조작 체계
|
||||
- B06 배수관 횡단도 세트
|
||||
- B10_Payment — Frontend
|
||||
- B11_Status — Frontend
|
||||
- 저장 경로 규칙 (Workflow-based Folder Structure)
|
||||
- DB: 프로젝트 관리 테이블
|
||||
- 임도기술교본 원문 md 추출 품질 결함
|
||||
- A01_Home — 세부 구현
|
||||
- A02_ProgDetail — 세부 구현
|
||||
- A02_ProgDetail — Frontend
|
||||
- B01_Dashboard — API
|
||||
- B03_FileInput — Frontend
|
||||
- B05 계획노선 코리도 삼각망 서피스
|
||||
- B05 구조물·UI 현재 계획
|
||||
- B05 구조물 입력·종단 표시 정비 — 2026-08-19
|
||||
- B07 구조물 표준도 — 조사·합의와 현재 통로
|
||||
- B08 횡단도 구조물·장 배치
|
||||
- 2026-09-07 완료·보류 요약
|
||||
- B07 외부 WebCAD 비교 실행환경
|
||||
- Temp Upload (프로젝트 생성 전 임시 보관함)
|
||||
- Q: 배수관 매설시 각도의 제약조건이 있는지 확인해줘. 임도에서
|
||||
- Q: 임도 기술정보DB에서 집수정의 형태정보는 어떤게 있는지 확인해줘.
|
||||
- B02_ProjRegister — DB
|
||||
- B03_FileInput — API
|
||||
- B04_PreProcess — DB
|
||||
- B05_Profile — API
|
||||
- B05 변형 성토면 마감·날개 패치
|
||||
- B05_Profile — Profile Alignment & Table
|
||||
- B05 구조물 비정규 측점 공급
|
||||
- B05_Profile_UI_Drainage_Parts — 배수유역 공용 UI 파츠
|
||||
- _UI_Drainage_Render — 배수유역도 Canvas 렌더러
|
||||
- _UI_Profile_Structures — 종단 구조물 렌더링 및 인터랙션
|
||||
- _UI_Selection — 배수/구조물 3자 선택 동기화
|
||||
- B06 인접 측점 구조물 트림 정리
|
||||
- B06 물넘이포장·콘크리트 포장·독립 기슭막이
|
||||
- B06_Section_Engine_Areas — 횡단 면적 적분 연산 엔진
|
||||
- B06_Section_Router_Confirm — 임시 저장 및 확정 라우터
|
||||
- B06_Section_UI_Cross_Areas — 횡단 단면적 표기 및 밴드 하이라이트
|
||||
- B06_Section_UI_Cross_Design — 횡단 측점별 세부 설계 컨트롤
|
||||
- B06_Section_UI_MassHaul — 유토곡선 적분 계산 엔진
|
||||
- B06_Section_UI_MassHaul_Balance — 평형선 및 장비 띠 분할 엔진
|
||||
- B06_Section_UI_MassHaul_Balance_View — 운반 띠 시각화 렌더러
|
||||
- B06_Section_UI_MassHaul_Balloon — 물량 말풍선 배치 및 조작
|
||||
- B06_Section_UI_MassHaul_Curve — 유토곡선 궤적 보간 및 렌더링
|
||||
- B06_Section_UI_MassHaul_Settle — 토량 정산 및 장거리 상쇄 엔진
|
||||
- B06_Section_UI_MassHaul_View — 유토곡선 시각화 렌더러
|
||||
- B06_Section_UI_Page — B06 메인 페이지 오케스트레이터
|
||||
- B06_Section_UI_Section_View — 종/횡단 및 유토곡선 뷰 조립
|
||||
- B07 Quantity — Backend
|
||||
- B08 CAD 블록 라이브러리·사진
|
||||
- B08 OpenWebCAD 명령 체계
|
||||
- B08_CAD_table_entity.md
|
||||
- 공개·인증·관리 영역 지도
|
||||
- 공유 자원 영향 지도
|
||||
- A00_Common — 공통 프레임워크 & 유틸
|
||||
- app_shell.ts
|
||||
- router.ts
|
||||
- B04_PreProcess — API
|
||||
- B04_PreProcess — Dependencies
|
||||
- B05_Profile — Backend
|
||||
- B05_Profile — DB 사용 관계
|
||||
- B05_Profile — 3D Viewer & Interaction
|
||||
- B05_Profile_Engine_Sections.md
|
||||
- B05_Profile_Api_Fetch.ts
|
||||
- B05_Profile_UI_Drainage_Panel
|
||||
- B05_Profile_UI_Drainage_Pipes
|
||||
- B05_Profile_UI_Profile_Panel.md
|
||||
- B06_Section — DB 사용 관계
|
||||
- B06_Section_Api_Fetch — B06 프론트엔드 API 클라이언트
|
||||
- B06_Section_UI_Standard_Diagram — 표준단면 모식도 컴포넌트
|
||||
- B06_Section_UI_Standard_Panel — 표준단면 입력 및 제어 패널
|
||||
- B08_DesignDetail — Dependencies
|
||||
- common_util_project_delete.md
|
||||
- common_util_storage.md
|
||||
- common_util_workflow_state.md
|
||||
- 미확정 테이블 보관소 (Unconfirmed DB Schemas)
|
||||
- A01_Home_UI_Page.md
|
||||
- A02_ProgDetail_UI_Page.md
|
||||
- A06_Login_Router.md
|
||||
- A07_Register_Router.md
|
||||
- A08_Support_Router.md
|
||||
- A09_Security_Router.md
|
||||
- B01_Dashboard — DB 사용 관계
|
||||
- B01_Dashboard — Dependencies
|
||||
- B01_Dashboard_Router.md
|
||||
- B01_Dashboard_UI_Page.md
|
||||
- B02_ProjRegister_Router.md
|
||||
- B03_FileInput — DB 사용 관계
|
||||
- B03_FileInput — Dependencies
|
||||
- B03_FileInput_Router.md
|
||||
- B04_PreProcess_Router.md
|
||||
- B05_Profile — Dependencies
|
||||
- B05_Profile_Engine_Grade.md
|
||||
- B05_Profile_Engine_Solver.md
|
||||
- B05_Profile_Repository.md
|
||||
- B05_Profile_Router.md
|
||||
- B05_Profile_Router_Confirm.md
|
||||
- B05_Profile_Schema.md
|
||||
- B05_Profile_UI_IrregularStations.md
|
||||
- B05_Profile_UI_Page.md
|
||||
- B05_Profile_UI_Panel.md
|
||||
- B05_Profile_UI_Profile_Alignment.md
|
||||
- B05_Profile_UI_Profile_Table.md
|
||||
- B05_Profile_UI_Viewer.md
|
||||
- B06_Section — Dependencies
|
||||
- B06_Section_Router.md
|
||||
- B01~B09 Workflow 데이터 흐름
|
||||
- B08_DesignDetail — API
|
||||
- longitudinal_alignment.md
|
||||
- B03_FileInput_plan_lidar_multi_file.md
|
||||
- B05_completed_followups.md
|
||||
- B05_corridor_followup_decisions.md
|
||||
- B06_api.md
|
||||
- B07_db.md
|
||||
- B08_DesignDetail_Router.md
|
||||
- B09_Estimation_UI_Page.md
|
||||
- B10_Payment_UI_Page.md
|
||||
- B11_Status_UI_Page.md
|
||||
- B03 계획노선 정본·좌표계 후속
|
||||
- B03 파일 입력 화면 정리
|
||||
- B04 3D 방위·좌표계 최신 결정
|
||||
- B04 세부유역·방위·좌표계
|
||||
- B05 구조물 구간 절취·측벽·성토 패치
|
||||
- B05 구조물 3D 투영 커브
|
||||
- B05_Profile — Frontend
|
||||
- B05 유토곡선·구조물 후속
|
||||
- B05 종단곡선·실시간 횡단 연동
|
||||
- B06 횡단 계산 미러·카드 표기
|
||||
- B06 횡단 관 형상·유토곡선 후속
|
||||
- B08_DesignDetail_Engine_Cad_Basin.py
|
||||
- B08_DesignDetail_Engine_Cad_MassHaul.py
|
||||
- B08 CAD·납품 도면 후속
|
||||
- OpenWebCAD Core
|
||||
- common_util_mass_haul_settle.ts
|
||||
- Drainage Watershed (유역도)
|
||||
- Mass Haul Diagram (토적도)
|
||||
- 2026-09-02 완료 항목
|
||||
- 2026-09-03 완료 — 입력·배수·종횡단·CAD
|
||||
- 2026-09-03 추가 완료 — 화면·종단·횡단
|
||||
- Query: 임도 집수정 형태정보
|
||||
- B03 File Input Backend
|
||||
- B03 File Input Frontend
|
||||
- B04 PreProcess Backend
|
||||
- B04 PreProcess Frontend
|
||||
- B07_Quantity_Router.md
|
||||
- B08 CAD Blocks Library
|
||||
- B08 CAD Table Entity
|
||||
- B08 Cross Section Structure Sheets
|
||||
- Query: 배수관 매설 각도 제약조건 (임도)
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `UI Templates — Localization & Components` - 12 edges
|
||||
2. `DB: 로그/모니터링 테이블` - 11 edges
|
||||
3. `B07 구조물 표준도 — 조사·합의와 현재 통로` - 11 edges
|
||||
4. `2026-08-29 완료 반영` - 10 edges
|
||||
5. `DB: 파일/지표면분석 테이블` - 10 edges
|
||||
6. `A07_Register — Frontend` - 10 edges
|
||||
7. `A08_Support — Frontend` - 10 edges
|
||||
8. `B02_ProjRegister — Frontend` - 10 edges
|
||||
9. `인증 / RBAC` - 9 edges
|
||||
10. `배수유역 해석 및 세부설계 (Drainage Watershed)` - 9 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `B07 구조물 표준도 — 조사·합의와 현재 통로` --references--> `B07 DesignDetail — 현재 책임` [EXTRACTED]
|
||||
pages/B07_DesignDetail/B07_standard_drawings_2026_09.md → pages/B07_DesignDetail/B07_frontend.md
|
||||
- `B07 구조물 표준도 — 조사·합의와 현재 통로` --references--> `B08 Quantity — 2026-09 수량산출` [EXTRACTED]
|
||||
pages/B07_DesignDetail/B07_standard_drawings_2026_09.md → pages/B08_Quantity/B08_overview_2026_09.md
|
||||
- `B07 구조물 표준도 — 조사·합의와 현재 통로` --references--> `B09 Estimation — 2026-09 원가계산` [EXTRACTED]
|
||||
pages/B07_DesignDetail/B07_standard_drawings_2026_09.md → pages/B09_Estimation/B09_overview_2026_09.md
|
||||
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Hyperedges (group relationships)
|
||||
- **B03-B08 Workflow Data Flow** — b03_fileinput_route_snapshot_crs, b04_preprocess_drainage_compass_crs, b05_profile_frontend, b06_section_cross_design_ui_2026_09, b08_designdetail_frontend [INFERRED 0.90]
|
||||
- **Shared Mass Haul Calculation and UI** — b05_profile_masshaul_structure_2026_09, b06_section_masshaul_culvert_2026_09, b08_designdetail_cad_delivery_2026_09 [EXTRACTED 0.85]
|
||||
- **CAD Delivery and Usability Framework** — b08_designdetail_cad_interaction, b08_designdetail_cad_title_block, b08_designdetail_cad_usability_2026_09_01, b08_designdetail_cad_delivery_2026_09 [EXTRACTED 0.95]
|
||||
- **LAS-Free Analysis Workflow** — concepts_las_free_sheet_surface, pages_b03_fileinput_backend, pages_b04_preprocess_backend [EXTRACTED 1.00]
|
||||
- **B08 Drawing Generation Flow** — b08_designdetail_b08_designdetail_engine_cad_masshaul_py, b08_designdetail_b08_designdetail_engine_cad_basin_py, common_util_common_util_mass_haul_settle_ts [EXTRACTED 0.90]
|
||||
- **Drainage System Workflow** — concepts_drainage_watershed, pages_b05_profile_b05_structures, pages_b06_section_b06_culvert_set, pages_b06_section_b06_culvert_geometry_redesign [EXTRACTED 0.95]
|
||||
- **Mass Haul Diagram System** — concepts_mass_haul_diagram, pages_b06_section_b06_frontend, pages_b08_designdetail_b08_drawing_masshaul_watershed [EXTRACTED 0.90]
|
||||
- **3D Corridor Generation Flow** — pages_b05_profile_b05_corridor_surface, pages_b05_profile_b05_corridor_plan_curves, pages_b05_profile_b05_corridor_cut_fill, pages_b05_profile_b05_corridor_patch_finish [EXTRACTED 0.90]
|
||||
- **Late Workflow Stages (B07-B09)** — pages_b07_quantity_b07_frontend, pages_b08_designdetail_b08_frontend, pages_b09_estimation_b09_frontend [EXTRACTED 1.00]
|
||||
|
||||
## Communities (189 total, 34 thin omitted)
|
||||
|
||||
### Community 0 - "UI Templates — Localization & Components"
|
||||
Cohesion: 0.05
|
||||
Nodes (38): 2026-09-04 완료 항목, 700줄 제한 분리, 보존된 미완료 범위, 상시계획서 추가 완료 범위, 완료 범위, 추가 완료 범위, 디자인 시스템 (Design System), 레이아웃 및 둥근 테두리 (Radius & Spacing) (+30 more)
|
||||
|
||||
### Community 1 - "인증 / RBAC"
|
||||
Cohesion: 0.06
|
||||
Nodes (31): OTP / 비밀번호 및 디바이스 신뢰, 권한 검증 헬퍼 (B01_Dashboard), 라우팅 가드 (frontend.md 5.2), 사용자 상태 생명주기, 사용처 (역참조), 세션 인증 (backend.md 6.3), 역할 (users.role), 인증 / RBAC (+23 more)
|
||||
|
||||
### Community 2 - "A00_Common — App Shell Framework"
|
||||
Cohesion: 0.08
|
||||
Nodes (22): A00_Common — App Shell Framework, app_shell 구성요소, router 라우팅 테이블, A00_Common — 스캐폴드·CSS·종속성, b_page_scaffold, CSS 인젝션, 사용처, 종속성 (+14 more)
|
||||
|
||||
### Community 3 - "2026-09-04 완료 항목"
|
||||
Cohesion: 0.07
|
||||
Nodes (25): API 공통 (여러 페이지가 공유하는 엔드포인트), 공통 오류 응답 포맷 (전 라우터), 워크플로우 상태 조회, 폴링 패턴 (legacy workflow.json 설계; 현재 구현은 workflow-state API 사용), 계산 구현 원칙, 계획노선 규칙, 설계 데이터 생명주기, 정본 세 벌 (+17 more)
|
||||
|
||||
### Community 4 - "배수유역 해석 및 세부설계 (Drainage Watershed)"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): B08_DesignDetail — Frontend, CAD 계획선 레이어 연동 및 편집 지원 (2026-07-22), openwebcad 단위 정합, 선 특성/폰트 UI 및 Fit-in-all (2026-07-20), 독립형 CAD 임베드 및 데이터 연동 아키텍처, 의존성, 주요 컴포넌트 / 함수, 파일 구조
|
||||
|
||||
### Community 5 - "DB: 로그/모니터링 테이블"
|
||||
Cohesion: 0.17
|
||||
Nodes (11): activity_logs (사용자 활동 로그), audit_logs (감사 로그), change_logs (설계 변경 이력), DB: 로그/모니터링 테이블, login_logs (로그인 시도 로그), support_requests (기술 지원 요청), system_admin_logs (시스템 관리자 행위 로그), system_audit_logs — 사용처 (B01, B02) (+3 more)
|
||||
|
||||
### Community 6 - "A09_Security — Backend"
|
||||
Cohesion: 0.17
|
||||
Nodes (11): A09_Security — Backend, API 엔드포인트, DB 저장 (activity_logs), 권한 헬퍼, 마스터(회사 관리자) 전용 — `require_master`, 시스템 관리자 전용 — `require_system_admin`, 요청 스키마 (Pydantic), 의존성 (공통 유틸) (+3 more)
|
||||
|
||||
### Community 7 - "2026-08-29 완료 반영"
|
||||
Cohesion: 0.10
|
||||
Nodes (17): 2026-08-29 완료 반영, B03 재업로드·B05 최신 조회, B05/B06 구조물 UI 통합, B07↔B08 순서, B07 CAD 고정 척도·횡단 장 배치, B07 CAD 테마, B07 CAD 확대·팬, 배수시설 추천 기준 (+9 more)
|
||||
|
||||
### Community 8 - "DB: 파일/지표면분석 테이블"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): DB: 파일/지표면분석 테이블, input_files.status 값, input_files (입력 원본 파일), processed_point_cloud.status 값, processed_point_cloud (필터/변환 포인트클라우드), surface_models.status 값, surface_models (지표면 모델 및 등고선), terrain_layers (지형 레이어) (+2 more)
|
||||
|
||||
### Community 9 - "DB: 경로/종횡단 테이블"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): cross_sections.data.structures, cross_sections (횡단면 설계), `data` 컬럼 내 `options` 스냅샷 구조 (2026-07-19 도입), `data` 컬럼 내 `profile_alignment` 구조 (2026-07-23 도입), DB: 경로/종횡단 테이블, longitudinal_sections (종단면 설계), route_points (경로 좌표점), route_statistics (노선 통계) (+2 more)
|
||||
|
||||
### Community 10 - "A04_NewsHistory — Frontend"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): A04_NewsHistory — Frontend, CSS 클래스 구조, Mock 데이터 구조, 로컬라이제이션, 미해결 사항, 반응형, 스타일 (CSS), 의존성 (+2 more)
|
||||
|
||||
### Community 11 - "A07_Register — Frontend"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): A07_Register — Frontend, API 클라이언트 함수, 로컬라이제이션, 스타일 (CSS), 약관 동의 (아코디언), 의존성, 이벤트 핸들러, 제출 로직 (2단계 폼 전환) (+2 more)
|
||||
|
||||
### Community 12 - "A08_Support — Frontend"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): A08_Support — Frontend, 로컬라이제이션, 세션 자동 채움, 스타일 (CSS), 의존성, 이벤트 핸들러, 제출 로직, 참고 (+2 more)
|
||||
|
||||
### Community 13 - "B02_ProjRegister — Frontend"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): B02_ProjRegister — Frontend, 로컬라이제이션, 스타일 (CSS), 의존성, 이벤트 핸들러, 입력 필드, 제출 로직, 참고 (+2 more)
|
||||
|
||||
### Community 14 - "A01_Home — Frontend"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): A01_Home — Frontend, Hero 섹션, 로컬라이제이션, 세부 구현, 이벤트 핸들러, 주요 기능 섹션, 최신 소식 섹션, 컴포넌트 (+1 more)
|
||||
|
||||
### Community 15 - "A05_EduDetail — Frontend"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): A05_EduDetail — Frontend, CSS 클래스 구조, 로컬라이제이션, 반응형, 스타일 (CSS), 의존성, 이벤트 핸들러, 컴포넌트 (섹션 빌더) (+1 more)
|
||||
|
||||
### Community 16 - "A06_Login — Frontend"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): A06_Login — Frontend, API 클라이언트 함수, 로컬라이제이션, 스타일 (CSS), 의존성, 이벤트 핸들러, 제출 및 OTP 제어 로직 (2단계 폼 전환), 컴포넌트 / 함수 (+1 more)
|
||||
|
||||
### Community 17 - "A08_Support — Backend"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): A08_Support — Backend, API 엔드포인트, DB 저장 컬럼 (실 코드 INSERT 기준), 내부 헬퍼, 요청 스키마 (Pydantic), 의존성 (공통 유틸), 접수 로직, 특징 (+1 more)
|
||||
|
||||
### Community 18 - "A09_Security — Frontend"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): A09_Security — Frontend, API 클라이언트 함수 (⚠️ 미사용, 정의만), 로컬라이제이션, 스타일 (CSS), 약관 데이터 (A09_Security_Terms.ts), ⚠️ 약관 텍스트와 실 코드 불일치, 의존성, 컴포넌트 / 함수 (+1 more)
|
||||
|
||||
### Community 19 - "B02_ProjRegister — Backend"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): API 엔드포인트, B02_ProjRegister — Backend, DB 저장 컬럼 (projects INSERT), 생성 로직 (create_project 트랜잭션), 요청/응답 스키마 (Pydantic), 의존성 (공통 유틸), 참고, 파일 구조 (+1 more)
|
||||
|
||||
### Community 20 - "B04_PreProcess — Backend"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): 2D/GIS 미표시 원인 분석 및 조치, B04_PreProcess — Backend, 📋 구현 예외 처리 검토 항목 (PLAN), ⚙️ 설정 및 환경 파일 정합성, 수치지형도 도엽 오버레이 아키텍처 (2026-07-26, 2026-08-01 S8 개편), 엔진 서브모듈, 워크플로우 상태 전이 및 자동 확정, 주요 함수 (Router / Repository / Engine / Utility) (+1 more)
|
||||
|
||||
### Community 21 - "B04_PreProcess — Frontend"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): 3D 뷰어 DOM 마운트 버그 수리, 3D 뷰어 테마 연동 및 가독성 개선, 3D 카메라 커서 피봇 & 2D 오버레이 UI 개선 (2026-08-01~02 일원화), B04_PreProcess — Frontend, ⚠️ 계획서와 실 코드 불일치 (3D 뷰어), 의존성, 처리 흐름, 컴포넌트 & API 함수 (+1 more)
|
||||
|
||||
### Community 22 - "B05 구조물 정본·통합 편집"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): API, B05 구조물 정본·통합 편집, B06 경계와 남은 범위, 검증, 배수관·시설 옵션, 백엔드 파일, 유역 추천·개략 단면, 정본과 타입 레지스트리 (+1 more)
|
||||
|
||||
### Community 23 - "B06_Section — Frontend"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): 2026-08-22 파일 한계 정리, API 클라이언트, B06_Section — Frontend, SVG 렌더러 및 유틸리티, UI 패널 크기 및 리사이저 규칙 (2026-08-02 신설), 기술부채 (해결됨), 입력 옵션 (표시 옵션 및 횡단 반폭 제어), 파일 구성 (+1 more)
|
||||
|
||||
### Community 24 - "A03_CompDetail — Frontend"
|
||||
Cohesion: 0.22
|
||||
Nodes (8): A03_CompDetail — Frontend, CSS 클래스 구조, 로컬라이제이션, 반응형, 스타일 (CSS), 의존성, 컴포넌트 (섹션 빌더), 파일 구조
|
||||
|
||||
### Community 25 - "A06_Login — Backend"
|
||||
Cohesion: 0.22
|
||||
Nodes (8): A06_Login — Backend, API 엔드포인트, 내부 헬퍼, 로그인 로직 흐름, 보안 정책 (실 코드 기준), 요청 스키마 (Pydantic), 의존성 (공통 유틸), 파일 구조
|
||||
|
||||
### Community 26 - "유토곡선 (Mass Haul Diagram) 계산 명세"
|
||||
Cohesion: 0.15
|
||||
Nodes (11): 1. 개요 및 분석 목적, 2. 주요 계산 수식 및 원리 (실무 관례 반영), 3. 지반유형별 토량환산계수 기본값 (`config_system.py`), 4. 토공 운반장비 선정거리 및 분배 기준 (`config_system.py`), 5. 유토곡선 곡선 사양 (B06 구현 v2), 6. 웹앱 연동 및 시각화 명세 (2026-08-02 확정), 유토곡선 (Mass Haul Diagram) 계산 명세, B08 Quantity — 2026-09 수량산출 (+3 more)
|
||||
|
||||
### Community 27 - "A07_Register — Backend"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): A07_Register — Backend, API 엔드포인트, 가입 로직 흐름, 요청 스키마 (Pydantic), 의존성 (공통 유틸), 참고, 파일 구조
|
||||
|
||||
### Community 28 - "B06 배수관 구조물 조작·표시"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): 4축 조작과 재질, B06 배수관 구조물 조작·표시, 계산·보기 분리, 구현 파일, 자체검증 기록, 조정창, 집수정 9키 조작
|
||||
|
||||
### Community 29 - "B06 기슭막이 연동·경사·단별 제어"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): B06 기슭막이 연동·경사·단별 제어, 검증, 단별 구간값, 선택과 하이라이트, 연동과 경사, 정본과 공용 모델, 형태와 조정창
|
||||
|
||||
### Community 30 - "B08_DesignDetail — Backend"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): B08_DesignDetail — Backend, 도각 템플릿 변환 및 종단도 A1 도각 병합 (2026-07-26), 도면 관리, 종단 30측점 분할, CAD 수량산출표 및 도면 템플릿 파이프라인, 워크플로우 게이팅 연동, 종단도 30측점 N분할 및 납품 양식 측점 테이블 (2026-07-25 N-1-1), 현재 책임 경계, 횡단도 4개 선별 레이어 및 CAD 수량산출표 (2026-07-25 N-1-2/N-1-3)
|
||||
|
||||
### Community 31 - "B08 토적도·수리집수면적유역도"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): B08 토적도·수리집수면적유역도, 검증 한계, 구현·검증 완료, 남은 결정, 데이터 흐름, 도면 기준, 유역 정보표
|
||||
|
||||
### Community 32 - "B09_Estimation — Frontend"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): B09_Estimation — Frontend, 로컬라이제이션, 백엔드/DB — 미착수, 의존성, 참고, 컴포넌트 / 함수, 파일 구조
|
||||
|
||||
### Community 33 - "현재 구현 현황 — 소스 읽기 감사"
|
||||
Cohesion: 0.06
|
||||
Nodes (28): 구현 상태 용어, 단계별 판정, 미결 설계, 반드시 유지할 구분, 비워크플로 영역 판정, 현재 구현 현황 — 소스 읽기 감사, Aislo 프로젝트 지도, 명칭 판정 (+20 more)
|
||||
|
||||
### Community 34 - "DB: 구조물/수량/산출물 테이블"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): DB: 구조물/수량/산출물 테이블, output_files (개별 산출 파일 리스트), outputs (최종 견적/도면 산출 세션), quantity_items (수량 산출 항목), quantity_items 총비용 계산 예, structures (배치 구조물)
|
||||
|
||||
### Community 35 - "B01_Dashboard — Backend"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B01_Dashboard — Backend, 기술부채, 라우터 권한 헬퍼, 세분화 백엔드 위키 명세, 요청 스키마, 저장소 및 삭제 함수
|
||||
|
||||
### Community 36 - "B01_Dashboard — Frontend"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B01_Dashboard — Frontend, UI 권한 헬퍼, 공유 자원 연결, 모달, 분할된 UI 컴포넌트 파일, 파일과 진입점
|
||||
|
||||
### Community 37 - "B03_FileInput — Backend"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B03_FileInput — Backend, workflow·알림, 메타데이터 분석 및 파일 지문, 임시 보관함 (R2 Temp Upload), 입력 검증·파일 처리, 저장소 및 초기화
|
||||
|
||||
### Community 38 - "B05 급선회 3D 국부 보정 계획"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B05 급선회 3D 국부 보정 계획, 구조물 연동, 국부 패치 절차, 목적과 경계, 완료 조건, 확인된 노견 확장 회귀
|
||||
|
||||
### Community 39 - "B06 집수정·다단 기슭막이"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B06 집수정·다단 기슭막이, 미결, 유입 구조물, 유출 성토부·다단, 자체검증 기록, 집수정 계류측 성토부
|
||||
|
||||
### Community 40 - "B06 배수관 구조물 기하·조작 체계"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B06 배수관 구조물 기하·조작 체계, 검증 상태, 구현 파일, 기슭막이·관 핵심 규칙, 설계선 트림, 접속선
|
||||
|
||||
### Community 41 - "B06 배수관 횡단도 세트"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B06 배수관 횡단도 세트, 검증 근거와 후속, 계획선 규칙, 구현 항목, 입력·판정, 형상·표시 순서 (2026-08-20 스냅샷)
|
||||
|
||||
### Community 42 - "B10_Payment — Frontend"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B10_Payment — Frontend, 로컬라이제이션, 비즈니스 로직 전제 (목업), 의존성, 주요 컴포넌트 및 함수 (Mockup), 파일 구조
|
||||
|
||||
### Community 43 - "B11_Status — Frontend"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B11_Status — Frontend, 결재 상태 흐름 (Payment Flow Status), 로컬라이제이션, 의존성, 주요 컴포넌트 및 기능 (Mockup), 파일 구조
|
||||
|
||||
### Community 44 - "저장 경로 규칙 (Workflow-based Folder Structure)"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B05 구조물 3D 투영 커브, 검증, 구조물 날개·바닥 연결, 비탈 투영·성토면 절단, 저장·호환성, 커브 생성·렌더
|
||||
|
||||
### Community 45 - "DB: 프로젝트 관리 테이블"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): DB: 프로젝트 관리 테이블, project_automations (프로젝트 자동화 정책), project_versions (프로젝트 버전 스냅샷), project_workflow_stages (단계별 상세 상태), projects (프로젝트)
|
||||
|
||||
### Community 46 - "임도기술교본 원문 md 추출 품질 결함"
|
||||
Cohesion: 0.05
|
||||
Nodes (33): 2026-09-09 완료 구현·검증, B05·B06 종횡단, B07 표준도, B08 수량산출, B09 원가계산, 검증 경계, 결함 유형 (예시 = 위 파일 기준 줄번호), 원인 추정 (+25 more)
|
||||
|
||||
### Community 47 - "A01_Home — 세부 구현"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): A01_Home — 세부 구현, 데이터 흐름, 미해결, 스타일, 의존성
|
||||
|
||||
### Community 48 - "A02_ProgDetail — 세부 구현"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): A02_ProgDetail — 세부 구현, 데이터 흐름, 미해결 / 특이사항, 스타일 (CSS), 의존성
|
||||
|
||||
### Community 49 - "A02_ProgDetail — Frontend"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): A02_ProgDetail — Frontend, 구조, 세부 구현, 제약 준수, 컴포넌트 분석
|
||||
|
||||
### Community 50 - "B01_Dashboard — API"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): B01_Dashboard — API, 사용자·회사, 시스템 관리자, 프로젝트·자동화, 회사 관리자
|
||||
|
||||
### Community 51 - "B03_FileInput — Frontend"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): API 클라이언트, B03_FileInput — Frontend, UI 지원 유틸리티 (분할 완료), 브라우저 상태·오프라인 보조, 페이지·업로드 흐름
|
||||
|
||||
### Community 52 - "B05 계획노선 코리도 삼각망 서피스"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): B05 계획노선 코리도 삼각망 서피스, 검증, 저장·호환성, 진행 중 계획, 프론트엔드 구성
|
||||
|
||||
### Community 53 - "B05 구조물·UI 현재 계획"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): 2026-08-18 B05 페이지 개선 2차 계획 기록, 2026-08-18 사이드 패널·입력 로직 계획 기록, B05/B06 구조물 적용 범위, B05 구조물·UI 현재 계획, 후속 결정 대기
|
||||
|
||||
### Community 54 - "B05 구조물 입력·종단 표시 정비 — 2026-08-19"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): B05 구조물 입력·종단 표시 정비 — 2026-08-19, 기록된 검증, 기준 해석 보류, 완료 범위, 주요 항목
|
||||
|
||||
### Community 55 - "B07 구조물 표준도 — 조사·합의와 현재 통로"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B05_Profile — Frontend, 남은 파일 한계, 📂 소스코드 1:1 세분화 위키 파일 목록, 종단 편집 안전장치, 종단테이블 표시 보정, 📋 핵심 프론트엔드 아키텍처 개요
|
||||
|
||||
### Community 56 - "B08 횡단도 구조물·장 배치"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): B08 횡단도 구조물·장 배치, 결과, 데이터·구현 흐름, 문제와 결정, 한계
|
||||
|
||||
### Community 57 - "2026-09-07 완료·보류 요약"
|
||||
Cohesion: 0.14
|
||||
Nodes (11): 2026-09-07 완료·보류 요약, 성능·운영 결정, 완료 범위, 주의, B06_Section — Backend, 계산 엔진, 데이터 영구 저장 및 환경설정, 라우터·workflow (조회, 확정, 타 프로젝트 불러오기 및 재생성) (+3 more)
|
||||
|
||||
### Community 58 - "B07 외부 WebCAD 비교 실행환경"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B07 외부 WebCAD 비교 실행환경, 검증 상태, 라이선스 주의, 실행과 종료
|
||||
|
||||
### Community 59 - "Temp Upload (프로젝트 생성 전 임시 보관함)"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): Temp Upload (프로젝트 생성 전 임시 보관함), 사용처, 주요 개념 및 스펙, 주요 구성 요소
|
||||
|
||||
### Community 60 - "Q: 배수관 매설시 각도의 제약조건이 있는지 확인해줘. 임도에서"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): Answer, Outcome, Q: 배수관 매설시 각도의 제약조건이 있는지 확인해줘. 임도에서, Source Nodes
|
||||
|
||||
### Community 61 - "Q: 임도 기술정보DB에서 집수정의 형태정보는 어떤게 있는지 확인해줘."
|
||||
Cohesion: 0.40
|
||||
Nodes (4): Answer, Outcome, Q: 임도 기술정보DB에서 집수정의 형태정보는 어떤게 있는지 확인해줘., Source Nodes
|
||||
|
||||
### Community 62 - "B02_ProjRegister — DB"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B02_ProjRegister — DB, 쓰는 테이블, 저장소(파일시스템), 참고 (계획 당시 의도)
|
||||
|
||||
### Community 63 - "B03_FileInput — API"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B03_FileInput — API, workflow 조회, 일반 업로드, 청크 업로드
|
||||
|
||||
### Community 64 - "B04_PreProcess — DB"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B04_PreProcess — DB, Repository 함수, 쓰는 테이블, 참고
|
||||
|
||||
### Community 65 - "B05_Profile — API"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): API 스키마 및 반환 필드, B05_Profile — API, `POST /{project_id}/route/confirm` 요청 (`RouteConfirmRequest`), 엔드포인트
|
||||
|
||||
### Community 66 - "B05 변형 성토면 마감·날개 패치"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B05 변형 성토면 마감·날개 패치, 세월교 날개 패치, 저장·검증, 패치 마감
|
||||
|
||||
### Community 67 - "B05_Profile — Profile Alignment & Table"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 12행 도면 테이블 및 가로 스크롤 정렬 개편 (`_UI_Profile_Table.ts`, `_UI_Profile_Panel.ts`), B05_Profile — Profile Alignment & Table, 비정규 측점(구조물) 테이블 오버레이 & 런타임 검증 (`_UI_Profile_Table.ts`, `_UI_IrregularStations.ts`), 종단 계획고 편집 인터랙션 (`_UI_Profile_Edit.ts`, `_UI_Profile_Panel.ts`, `_UI_Page.ts`)
|
||||
|
||||
### Community 68 - "B05 구조물 비정규 측점 공급"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B05 구조물 비정규 측점 공급, 검증, 공급 경로, 정본 규칙
|
||||
|
||||
### Community 69 - "B05_Profile_UI_Drainage_Parts — 배수유역 공용 UI 파츠"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B05_Profile_UI_Drainage_Parts — 배수유역 공용 UI 파츠
|
||||
|
||||
### Community 70 - "_UI_Drainage_Render — 배수유역도 Canvas 렌더러"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, _UI_Drainage_Render — 배수유역도 Canvas 렌더러
|
||||
|
||||
### Community 71 - "_UI_Profile_Structures — 종단 구조물 렌더링 및 인터랙션"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, _UI_Profile_Structures — 종단 구조물 렌더링 및 인터랙션
|
||||
|
||||
### Community 72 - "_UI_Selection — 배수/구조물 3자 선택 동기화"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, _UI_Selection — 배수/구조물 3자 선택 동기화
|
||||
|
||||
### Community 73 - "B06 인접 측점 구조물 트림 정리"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B06 인접 측점 구조물 트림 정리, 검증, 원인과 경계, 처리 항목
|
||||
|
||||
### Community 74 - "B06 물넘이포장·콘크리트 포장·독립 기슭막이"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B06 물넘이포장·콘크리트 포장·독립 기슭막이, 검증, 독립 기슭막이, 포장과 물넘이
|
||||
|
||||
### Community 75 - "B06_Section_Engine_Areas — 횡단 면적 적분 연산 엔진"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_Engine_Areas — 횡단 면적 적분 연산 엔진
|
||||
|
||||
### Community 76 - "B06_Section_Router_Confirm — 임시 저장 및 확정 라우터"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 엔드포인트, 3. 의존성, B06_Section_Router_Confirm — 임시 저장 및 확정 라우터
|
||||
|
||||
### Community 77 - "B06_Section_UI_Cross_Areas — 횡단 단면적 표기 및 밴드 하이라이트"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_Cross_Areas — 횡단 단면적 표기 및 밴드 하이라이트
|
||||
|
||||
### Community 78 - "B06_Section_UI_Cross_Design — 횡단 측점별 세부 설계 컨트롤"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_Cross_Design — 횡단 측점별 세부 설계 컨트롤
|
||||
|
||||
### Community 79 - "B06_Section_UI_MassHaul — 유토곡선 적분 계산 엔진"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_MassHaul — 유토곡선 적분 계산 엔진
|
||||
|
||||
### Community 80 - "B06_Section_UI_MassHaul_Balance — 평형선 및 장비 띠 분할 엔진"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 알고리즘, 3. 의존성, B06_Section_UI_MassHaul_Balance — 평형선 및 장비 띠 분할 엔진
|
||||
|
||||
### Community 81 - "B06_Section_UI_MassHaul_Balance_View — 운반 띠 시각화 렌더러"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 렌더링, 3. 의존성, B06_Section_UI_MassHaul_Balance_View — 운반 띠 시각화 렌더러
|
||||
|
||||
### Community 82 - "B06_Section_UI_MassHaul_Balloon — 물량 말풍선 배치 및 조작"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 알고리즘, 3. 의존성, B06_Section_UI_MassHaul_Balloon — 물량 말풍선 배치 및 조작
|
||||
|
||||
### Community 83 - "B06_Section_UI_MassHaul_Curve — 유토곡선 궤적 보간 및 렌더링"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 기하 수학, 3. 의존성, B06_Section_UI_MassHaul_Curve — 유토곡선 궤적 보간 및 렌더링
|
||||
|
||||
### Community 84 - "B06_Section_UI_MassHaul_Settle — 토량 정산 및 장거리 상쇄 엔진"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 로직, 3. 의존성, B06_Section_UI_MassHaul_Settle — 토량 정산 및 장거리 상쇄 엔진
|
||||
|
||||
### Community 85 - "B06_Section_UI_MassHaul_View — 유토곡선 시각화 렌더러"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_MassHaul_View — 유토곡선 시각화 렌더러
|
||||
|
||||
### Community 86 - "B06_Section_UI_Page — B06 메인 페이지 오케스트레이터"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_Page — B06 메인 페이지 오케스트레이터
|
||||
|
||||
### Community 87 - "B06_Section_UI_Section_View — 종/횡단 및 유토곡선 뷰 조립"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1. 개요 및 역할, 2. 주요 기능 및 함수, 3. 의존성, B06_Section_UI_Section_View — 종/횡단 및 유토곡선 뷰 조립
|
||||
|
||||
### Community 88 - "B07 Quantity — Backend"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B07 Quantity — Backend, 구현되지 않은 항목, 구현된 항목, 책임 경계 미결
|
||||
|
||||
### Community 89 - "B08 CAD 블록 라이브러리·사진"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B08 CAD 블록 라이브러리·사진, 검증, 구현, 범위 결정
|
||||
|
||||
### Community 90 - "B08 OpenWebCAD 명령 체계"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B08 OpenWebCAD 명령 체계, 검증·제한, 구현 범위, 핵심 구성
|
||||
|
||||
### Community 91 - "B08_CAD_table_entity.md"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): B08 CAD TableEntity, 검증·후속, 기능, 모델, 이관 범위
|
||||
|
||||
### Community 92 - "공개·인증·관리 영역 지도"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): B09 원가계산 — 2026-09-09 완료 근거, 가격·조건 입력, 검증 기록, 기계·제비율·산출물, 남은 경계, 단가·밑수 완성
|
||||
|
||||
### Community 93 - "공유 자원 영향 지도"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): B08 수량산출 — 2026-09-09 완료 근거, 검증 기록, 구조물·수량 표시, 남은 경계, 밑수와 인계
|
||||
|
||||
### Community 94 - "A00_Common — 공통 프레임워크 & 유틸"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): A00_Common — 공통 프레임워크 & 유틸, 📋 개요, 📂 세분화 마크다운 문서 목록
|
||||
|
||||
### Community 95 - "app_shell.ts"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): app_shell.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 96 - "router.ts"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): router.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 97 - "B04_PreProcess — API"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B04_PreProcess — API, 엔드포인트, 요청/응답 스키마 (Pydantic)
|
||||
|
||||
### Community 98 - "B04_PreProcess — Dependencies"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B04_PreProcess — Dependencies, 백엔드 (Python), 프론트엔드 (TypeScript)
|
||||
|
||||
### Community 99 - "B05_Profile — Backend"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile — Backend, 📂 소스코드 1:1 세분화 위키 파일 목록, 📋 핵심 백엔드 아키텍처 개요
|
||||
|
||||
### Community 100 - "B05_Profile — DB 사용 관계"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile — DB 사용 관계, Repository 함수, 저장 경로
|
||||
|
||||
### Community 101 - "B05_Profile — 3D Viewer & Interaction"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 3D 마커 직접 드래그 이동 (0_old I-401 이식), 3D 지형 뷰포트 시각화 (`_UI_Viewer.ts`, `_UI_Markers.ts`), B05_Profile — 3D Viewer & Interaction
|
||||
|
||||
### Community 102 - "B05_Profile_Engine_Sections.md"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B05_Profile_Engine_Sections.py, ⚠️ 런타임 검증 주의사항 (2026-07-24 검증 보고서 기준), 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 103 - "B05_Profile_Api_Fetch.ts"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_Api_Fetch.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 API 함수 목록
|
||||
|
||||
### Community 104 - "B05_Profile_UI_Drainage_Panel"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_UI_Drainage_Panel, 📋 개요 및 특징, 🛠️ 주요 함수 / 심볼 목록
|
||||
|
||||
### Community 105 - "B05_Profile_UI_Drainage_Pipes"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_UI_Drainage_Pipes, 📋 개요 및 특징, 🛠️ 주요 함수 / 심볼 목록
|
||||
|
||||
### Community 106 - "B05_Profile_UI_Profile_Panel.md"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B05_Profile_UI_Profile_Panel.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 기능 및 개선사항 (2026-08-06), 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 107 - "B06_Section — DB 사용 관계"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B06_Section — DB 사용 관계, Repository 함수, 파일 경로
|
||||
|
||||
### Community 108 - "B06_Section_Api_Fetch — B06 프론트엔드 API 클라이언트"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 1. 개요 및 역할, 2. 주요 연동 API 함수, B06_Section_Api_Fetch — B06 프론트엔드 API 클라이언트
|
||||
|
||||
### Community 109 - "B06_Section_UI_Standard_Diagram — 표준단면 모식도 컴포넌트"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 1. 개요 및 역할, 2. 주요 기능, B06_Section_UI_Standard_Diagram — 표준단면 모식도 컴포넌트
|
||||
|
||||
### Community 110 - "B06_Section_UI_Standard_Panel — 표준단면 입력 및 제어 패널"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 1. 개요 및 역할, 2. 주요 기능, B06_Section_UI_Standard_Panel — 표준단면 입력 및 제어 패널
|
||||
|
||||
### Community 111 - "B08_DesignDetail — Dependencies"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B08_DesignDetail — Dependencies, Backend (requirements.txt), Frontend (package.json / tsconfig.json)
|
||||
|
||||
### Community 112 - "common_util_project_delete.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): common_util_project_delete.py, 🔗 역참조 (사용처), 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 113 - "common_util_storage.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): common_util_storage.py, 🔗 역참조 (사용처), 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 114 - "common_util_workflow_state.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): common_util_workflow_state.py, 🔗 역참조 (사용처), 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 116 - "A01_Home_UI_Page.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): A01_Home_UI_Page.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 및 인터페이스 목록
|
||||
|
||||
### Community 117 - "A02_ProgDetail_UI_Page.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): A02_ProgDetail_UI_Page.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 118 - "A06_Login_Router.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): A06_Login_Router.py, 🛠️ 라우터 API 및 주요 헬퍼 함수 목록, 🔗 연관 개념 및 의존성
|
||||
|
||||
### Community 119 - "A07_Register_Router.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): A07_Register_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 개념 및 의존성
|
||||
|
||||
### Community 120 - "A08_Support_Router.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): A08_Support_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 개념 및 의존성
|
||||
|
||||
### Community 121 - "A09_Security_Router.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): A09_Security_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 개념 및 의존성
|
||||
|
||||
### Community 124 - "B01_Dashboard_Router.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B01_Dashboard_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 개념 및 의존성
|
||||
|
||||
### Community 125 - "B01_Dashboard_UI_Page.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B01_Dashboard_UI_Page.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 126 - "B02_ProjRegister_Router.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B02_ProjRegister_Router.py, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 129 - "B03_FileInput_Router.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B03_FileInput_Router.py, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 130 - "B04_PreProcess_Router.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B04_PreProcess_Router.py, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 132 - "B05_Profile_Engine_Grade.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_Engine_Grade.py, 🔗 연관 개념 및 의존성, 🛠️ 주요 클래스 및 함수 목록
|
||||
|
||||
### Community 133 - "B05_Profile_Engine_Solver.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_Engine_Solver.py, 🛠️ 엔진 핵심 함수 목록, 🔗 연관 개념 및 의존성
|
||||
|
||||
### Community 134 - "B05_Profile_Repository.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_Repository.py, 🛠️ DB 접근 함수 목록, 🔗 연관 개념 및 의존성
|
||||
|
||||
### Community 135 - "B05_Profile_Router.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 모듈 및 의존성
|
||||
|
||||
### Community 136 - "B05_Profile_Router_Confirm.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_Router_Confirm.py, 🔗 연관 모듈, 🛠️ 주요 헬퍼 함수 목록
|
||||
|
||||
### Community 137 - "B05_Profile_Schema.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_Schema.py, 🛠️ Pydantic 모델 및 검증 헬퍼 목록, 🔗 연관 개념 및 의존성
|
||||
|
||||
### Community 138 - "B05_Profile_UI_IrregularStations.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_UI_IrregularStations.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 인터페이스 및 함수 목록
|
||||
|
||||
### Community 139 - "B05_Profile_UI_Page.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_UI_Page.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 컴포넌트 및 함수 목록
|
||||
|
||||
### Community 140 - "B05_Profile_UI_Panel.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_UI_Panel.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 141 - "B05_Profile_UI_Profile_Alignment.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_UI_Profile_Alignment.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 142 - "B05_Profile_UI_Profile_Table.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_UI_Profile_Table.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 143 - "B05_Profile_UI_Viewer.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05_Profile_UI_Viewer.ts, 🔗 연관 개념 및 의존성, 🛠️ 주요 함수 목록
|
||||
|
||||
### Community 145 - "B06_Section_Router.md"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B06_Section_Router.py, 🛠️ 라우터 API 및 주요 함수 목록, 🔗 연관 개념 및 의존성
|
||||
|
||||
### Community 146 - "B01~B09 Workflow 데이터 흐름"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 3D 방위와 좌표계, B04 세부유역·방위·좌표계, 세부유역 형상 보존, 종단 높낮이 기반 배정
|
||||
|
||||
### Community 158 - "B03 계획노선 정본·좌표계 후속"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B05 구조물 구간 절취·측벽·성토 패치, B06 변형 성토선 패치, 저장·검증, 절취와 경계
|
||||
|
||||
### Community 159 - "B03 파일 입력 화면 정리"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B06 횡단 관 형상·유토곡선 후속, I형 집수정 관 형상, 계산 상태와 경고, 파일 책임 분리
|
||||
|
||||
### Community 160 - "B04 3D 방위·좌표계 최신 결정"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B08 CAD 기본 조작, 검증·제외, 선택·편집, 입력·상태
|
||||
|
||||
### Community 161 - "B04 세부유역·방위·좌표계"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B08 CAD 도각·표제란, 값 공급, 도각 편집·보존, 회사 자산과 담당자
|
||||
|
||||
### Community 162 - "B05 구조물 구간 절취·측벽·성토 패치"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): B08 CAD 사용자 편의성 정리, 검증 상태, 구현 기록, 사용자 흐름
|
||||
|
||||
### Community 163 - "B05 구조물 3D 투영 커브"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B03 계획노선 정본·좌표계 후속, LAS 없는 설계와 업로드 상태, 계획노선 정본
|
||||
|
||||
### Community 164 - "B05_Profile — Frontend"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B03 파일 입력 화면 정리, 입력 규칙, 화면 구성
|
||||
|
||||
### Community 165 - "B05 유토곡선·구조물 후속"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 3D 방위 위젯, B04 3D 방위·좌표계 최신 결정, 작업 좌표계
|
||||
|
||||
### Community 166 - "B05 종단곡선·실시간 횡단 연동"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05 유토곡선·구조물 후속, 유지·판정 사항, 유토곡선 공용화
|
||||
|
||||
### Community 167 - "B06 횡단 계산 미러·카드 표기"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05 종단곡선·실시간 횡단 연동, 실시간 횡단·유토곡선, 초기 종단곡선
|
||||
|
||||
### Community 168 - "B06 횡단 관 형상·유토곡선 후속"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B06 횡단 계산 미러·카드 표기, 프론트 계산 미러, 횡단 카드 표기
|
||||
|
||||
### Community 171 - "B08 CAD·납품 도면 후속"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B08 CAD·납품 도면 후속, CAD 편집·확정, 토적도·유역도
|
||||
|
||||
### Community 182 - "2026-09-02 완료 항목"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): 2026-09-02 완료 항목, B05 구조물 3D, B05 종단 편집 후속, CAD, 노선·지표면, 작업 환경, 회귀 상태
|
||||
|
||||
### Community 183 - "2026-09-03 완료 — 입력·배수·종횡단·CAD"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 2026-09-03 완료 — 입력·배수·종횡단·CAD, 공통 결정, 완료 범위
|
||||
|
||||
### Community 184 - "2026-09-03 추가 완료 — 화면·종단·횡단"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 2026-09-03 추가 완료 — 화면·종단·횡단, 완료 범위, 최신 결정
|
||||
|
||||
## Knowledge Gaps
|
||||
- **756 isolated node(s):** `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어`, `반드시 유지할 구분`, `미결 설계` (+751 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **34 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
## Work-memory lessons
|
||||
|
||||
**Known dead ends** — questions that led nowhere; don't re-derive.
|
||||
- "배수관 매설시 각도의 제약조건이 있는지 확인해줘. 임도에서" -> `배수유역 해석 및 세부설계`
|
||||
- "임도 기술정보DB에서 집수정의 형태정보는 어떤게 있는지 확인해줘." -> `B06 배수관 횡단도 세트`, `유입 구조물 판정`
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **What connects `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어` to the rest of the system?**
|
||||
_756 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `UI Templates — Localization & Components` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.046511627906976744 - nodes in this community are weakly interconnected._
|
||||
- **Should `인증 / RBAC` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.05555555555555555 - nodes in this community are weakly interconnected._
|
||||
- **Should `A00_Common — App Shell Framework` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.07692307692307693 - nodes in this community are weakly interconnected._
|
||||
- **Should `2026-09-04 완료 항목` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.06896551724137931 - nodes in this community are weakly interconnected._
|
||||
- **Should `2026-08-29 완료 반영` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.1 - nodes in this community are weakly interconnected._
|
||||
- **Should `현재 구현 현황 — 소스 읽기 감사` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.058823529411764705 - nodes in this community are weakly interconnected._
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"runs": [
|
||||
{
|
||||
"date": "2026-08-16T00:00:00+09:00",
|
||||
"input_tokens": 265124,
|
||||
"output_tokens": 14991,
|
||||
"files": 146
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16T12:43:20.106802+00:00",
|
||||
"input_tokens": 27985,
|
||||
"output_tokens": 2184,
|
||||
"files": 22
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16T12:46:45.166477+00:00",
|
||||
"input_tokens": 5694,
|
||||
"output_tokens": 1138,
|
||||
"files": 6
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16T12:48:56.020124+00:00",
|
||||
"input_tokens": 14784,
|
||||
"output_tokens": 2835,
|
||||
"files": 12
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16T14:33:37.811664+00:00",
|
||||
"input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
"files": 12,
|
||||
"usage_note": "semantic subagent token usage unavailable from collaboration runtime; graph content extracted and validated"
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20T11:01:22.299635+00:00",
|
||||
"input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
"files": 10,
|
||||
"usage_note": "Gemini backend dependency unavailable; host semantic extraction used and collaboration runtime token usage unavailable; 27 nodes/59 edges validated"
|
||||
},
|
||||
{
|
||||
"date": "2026-08-21T09:54:12.581221+00:00",
|
||||
"input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
"files": 3,
|
||||
"note": "collaboration runtime did not expose semantic extraction token usage"
|
||||
}
|
||||
],
|
||||
"total_input_tokens": 313587,
|
||||
"total_output_tokens": 21148
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,16 @@
|
||||
# Graph Report - wiki (2026-09-11)
|
||||
# Graph Report - wiki (2026-09-12)
|
||||
|
||||
## Corpus Check
|
||||
- 206 files · ~57,686 words
|
||||
- 211 files · ~58,469 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 1170 nodes · 996 edges · 189 communities (155 shown, 34 thin omitted)
|
||||
- 1191 nodes · 1013 edges · 193 communities (159 shown, 34 thin omitted)
|
||||
- Extraction: 100% EXTRACTED · 0% INFERRED · 0% AMBIGUOUS
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `e49e4d85`
|
||||
- Built from commit: `9a9062f0`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -187,6 +187,10 @@
|
||||
- B08_DesignDetail_Engine_Cad_Basin.py
|
||||
- B08_DesignDetail_Engine_Cad_MassHaul.py
|
||||
- B08 CAD·납품 도면 후속
|
||||
- B05 계획노선 편집 — 2026-09-12
|
||||
- B06 종횡단 공용화 — 2026-09-12
|
||||
- B08 수량 근거 표시 — 2026-09-12
|
||||
- B09 원가 근거 표시 — 2026-09-12
|
||||
- OpenWebCAD Core
|
||||
- common_util_mass_haul_settle.ts
|
||||
- Drainage Watershed (유역도)
|
||||
@@ -229,9 +233,6 @@
|
||||
- None detected.
|
||||
|
||||
## Hyperedges (group relationships)
|
||||
- **B03-B08 Workflow Data Flow** — b03_fileinput_route_snapshot_crs, b04_preprocess_drainage_compass_crs, b05_profile_frontend, b06_section_cross_design_ui_2026_09, b08_designdetail_frontend [INFERRED 0.90]
|
||||
- **Shared Mass Haul Calculation and UI** — b05_profile_masshaul_structure_2026_09, b06_section_masshaul_culvert_2026_09, b08_designdetail_cad_delivery_2026_09 [EXTRACTED 0.85]
|
||||
- **CAD Delivery and Usability Framework** — b08_designdetail_cad_interaction, b08_designdetail_cad_title_block, b08_designdetail_cad_usability_2026_09_01, b08_designdetail_cad_delivery_2026_09 [EXTRACTED 0.95]
|
||||
- **LAS-Free Analysis Workflow** — concepts_las_free_sheet_surface, pages_b03_fileinput_backend, pages_b04_preprocess_backend [EXTRACTED 1.00]
|
||||
- **B08 Drawing Generation Flow** — b08_designdetail_b08_designdetail_engine_cad_masshaul_py, b08_designdetail_b08_designdetail_engine_cad_basin_py, common_util_common_util_mass_haul_settle_ts [EXTRACTED 0.90]
|
||||
- **Drainage System Workflow** — concepts_drainage_watershed, pages_b05_profile_b05_structures, pages_b06_section_b06_culvert_set, pages_b06_section_b06_culvert_geometry_redesign [EXTRACTED 0.95]
|
||||
@@ -239,23 +240,23 @@
|
||||
- **3D Corridor Generation Flow** — pages_b05_profile_b05_corridor_surface, pages_b05_profile_b05_corridor_plan_curves, pages_b05_profile_b05_corridor_cut_fill, pages_b05_profile_b05_corridor_patch_finish [EXTRACTED 0.90]
|
||||
- **Late Workflow Stages (B07-B09)** — pages_b07_quantity_b07_frontend, pages_b08_designdetail_b08_frontend, pages_b09_estimation_b09_frontend [EXTRACTED 1.00]
|
||||
|
||||
## Communities (189 total, 34 thin omitted)
|
||||
## Communities (193 total, 34 thin omitted)
|
||||
|
||||
### Community 0 - "UI Templates — Localization & Components"
|
||||
Cohesion: 0.05
|
||||
Nodes (38): 2026-09-04 완료 항목, 700줄 제한 분리, 보존된 미완료 범위, 상시계획서 추가 완료 범위, 완료 범위, 추가 완료 범위, 디자인 시스템 (Design System), 레이아웃 및 둥근 테두리 (Radius & Spacing) (+30 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (20): 2026-09-04 완료 항목, 700줄 제한 분리, 보존된 미완료 범위, 상시계획서 추가 완료 범위, 완료 범위, 추가 완료 범위, 1. B04 vs B05 역할 분담 및 일원화, 2. 공용 배수 엔진 및 WAMIS 강우량 연동 (Phase 1~2, 2026-08-13 관측소 전환) (+12 more)
|
||||
|
||||
### Community 1 - "인증 / RBAC"
|
||||
Cohesion: 0.06
|
||||
Nodes (31): OTP / 비밀번호 및 디바이스 신뢰, 권한 검증 헬퍼 (B01_Dashboard), 라우팅 가드 (frontend.md 5.2), 사용자 상태 생명주기, 사용처 (역참조), 세션 인증 (backend.md 6.3), 역할 (users.role), 인증 / RBAC (+23 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (20): OTP / 비밀번호 및 디바이스 신뢰, 권한 검증 헬퍼 (B01_Dashboard), 라우팅 가드 (frontend.md 5.2), 사용자 상태 생명주기, 사용처 (역참조), 세션 인증 (backend.md 6.3), 역할 (users.role), 인증 / RBAC (+12 more)
|
||||
|
||||
### Community 2 - "A00_Common — App Shell Framework"
|
||||
Cohesion: 0.08
|
||||
Nodes (22): A00_Common — App Shell Framework, app_shell 구성요소, router 라우팅 테이블, A00_Common — 스캐폴드·CSS·종속성, b_page_scaffold, CSS 인젝션, 사용처, 종속성 (+14 more)
|
||||
Cohesion: 0.05
|
||||
Nodes (35): A00_Common — App Shell Framework, app_shell 구성요소, router 라우팅 테이블, A00_Common — 스캐폴드·CSS·종속성, b_page_scaffold, CSS 인젝션, 사용처, 종속성 (+27 more)
|
||||
|
||||
### Community 3 - "2026-09-04 완료 항목"
|
||||
Cohesion: 0.07
|
||||
Nodes (25): API 공통 (여러 페이지가 공유하는 엔드포인트), 공통 오류 응답 포맷 (전 라우터), 워크플로우 상태 조회, 폴링 패턴 (legacy workflow.json 설계; 현재 구현은 workflow-state API 사용), 계산 구현 원칙, 계획노선 규칙, 설계 데이터 생명주기, 정본 세 벌 (+17 more)
|
||||
Cohesion: 0.04
|
||||
Nodes (40): API 공통 (여러 페이지가 공유하는 엔드포인트), 공통 오류 응답 포맷 (전 라우터), 워크플로우 상태 조회, 폴링 패턴 (legacy workflow.json 설계; 현재 구현은 workflow-state API 사용), 2026-09-12 체크 완료 항목, 상태 경계, 이관 범위, 자체검증 기록 요약 (+32 more)
|
||||
|
||||
### Community 4 - "배수유역 해석 및 세부설계 (Drainage Watershed)"
|
||||
Cohesion: 0.25
|
||||
@@ -270,8 +271,8 @@ Cohesion: 0.17
|
||||
Nodes (11): A09_Security — Backend, API 엔드포인트, DB 저장 (activity_logs), 권한 헬퍼, 마스터(회사 관리자) 전용 — `require_master`, 시스템 관리자 전용 — `require_system_admin`, 요청 스키마 (Pydantic), 의존성 (공통 유틸) (+3 more)
|
||||
|
||||
### Community 7 - "2026-08-29 완료 반영"
|
||||
Cohesion: 0.10
|
||||
Nodes (17): 2026-08-29 완료 반영, B03 재업로드·B05 최신 조회, B05/B06 구조물 UI 통합, B07↔B08 순서, B07 CAD 고정 척도·횡단 장 배치, B07 CAD 테마, B07 CAD 확대·팬, 배수시설 추천 기준 (+9 more)
|
||||
Cohesion: 0.08
|
||||
Nodes (22): 2026-08-29 완료 반영, B03 재업로드·B05 최신 조회, B05/B06 구조물 UI 통합, B07↔B08 순서, B07 CAD 고정 척도·횡단 장 배치, B07 CAD 테마, B07 CAD 확대·팬, 배수시설 추천 기준 (+14 more)
|
||||
|
||||
### Community 8 - "DB: 파일/지표면분석 테이블"
|
||||
Cohesion: 0.18
|
||||
@@ -849,6 +850,22 @@ Nodes (3): B06 횡단 계산 미러·카드 표기, 프론트 계산 미러, 횡
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B08 CAD·납품 도면 후속, CAD 편집·확정, 토적도·유역도
|
||||
|
||||
### Community 172 - "B05 계획노선 편집 — 2026-09-12"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B05 계획노선 편집 — 2026-09-12, 저장·공용화, 편집 화면
|
||||
|
||||
### Community 173 - "B06 종횡단 공용화 — 2026-09-12"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B06 종횡단 공용화 — 2026-09-12, 완료 체크에서 확인된 구조, 횡단 표시
|
||||
|
||||
### Community 174 - "B08 수량 근거 표시 — 2026-09-12"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B08 수량 근거 표시 — 2026-09-12, 구현 진입점, 출처 등급과 표시
|
||||
|
||||
### Community 175 - "B09 원가 근거 표시 — 2026-09-12"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): B09 원가 근거 표시 — 2026-09-12, 공용 근거 계약, 좌측 패널
|
||||
|
||||
### Community 182 - "2026-09-02 완료 항목"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): 2026-09-02 완료 항목, B05 구조물 3D, B05 종단 편집 후속, CAD, 노선·지표면, 작업 환경, 회귀 상태
|
||||
@@ -862,7 +879,7 @@ Cohesion: 0.50
|
||||
Nodes (3): 2026-09-03 추가 완료 — 화면·종단·횡단, 완료 범위, 최신 결정
|
||||
|
||||
## Knowledge Gaps
|
||||
- **756 isolated node(s):** `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어`, `반드시 유지할 구분`, `미결 설계` (+751 more)
|
||||
- **767 isolated node(s):** `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어`, `반드시 유지할 구분`, `미결 설계` (+762 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **34 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
@@ -875,17 +892,17 @@ Nodes (3): 2026-09-03 추가 완료 — 화면·종단·횡단, 완료 범위,
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `B07 구조물 표준도 — 조사·합의와 현재 통로` connect `임도기술교본 원문 md 추출 품질 결함` to `현재 구현 현황 — 소스 읽기 감사`, `유토곡선 (Mass Haul Diagram) 계산 명세`?**
|
||||
_High betweenness centrality (0.003) - this node is a cross-community bridge._
|
||||
- **What connects `단계별 판정`, `비워크플로 영역 판정`, `구현 상태 용어` to the rest of the system?**
|
||||
_756 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_767 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `UI Templates — Localization & Components` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.046511627906976744 - nodes in this community are weakly interconnected._
|
||||
_Cohesion score 0.08695652173913043 - nodes in this community are weakly interconnected._
|
||||
- **Should `인증 / RBAC` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.05555555555555555 - nodes in this community are weakly interconnected._
|
||||
_Cohesion score 0.08695652173913043 - nodes in this community are weakly interconnected._
|
||||
- **Should `A00_Common — App Shell Framework` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.07692307692307693 - nodes in this community are weakly interconnected._
|
||||
_Cohesion score 0.05 - nodes in this community are weakly interconnected._
|
||||
- **Should `2026-09-04 완료 항목` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.06896551724137931 - nodes in this community are weakly interconnected._
|
||||
_Cohesion score 0.0425531914893617 - nodes in this community are weakly interconnected._
|
||||
- **Should `2026-08-29 완료 반영` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.1 - nodes in this community are weakly interconnected._
|
||||
- **Should `현재 구현 현황 — 소스 읽기 감사` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.058823529411764705 - nodes in this community are weakly interconnected._
|
||||
_Cohesion score 0.07692307692307693 - nodes in this community are weakly interconnected._
|
||||
File diff suppressed because one or more lines are too long
+525
-191
File diff suppressed because it is too large
Load Diff
@@ -175,8 +175,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"index.md": {
|
||||
"mtime": 1789132135.2547836,
|
||||
"ast_hash": "07f8a8513e83d93de0bf992e1fc4e501",
|
||||
"mtime": 1789206522.9744911,
|
||||
"ast_hash": "43fc670a3fdfc5430a47b8721e40dd4b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"ingest/index.md": {
|
||||
@@ -1025,8 +1025,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"concepts/multi_environment_safety.md": {
|
||||
"mtime": 1789132131.6764865,
|
||||
"ast_hash": "1667aa4c657a4f529de42bcaac3fcd44",
|
||||
"mtime": 1789206280.4225435,
|
||||
"ast_hash": "78368c25f2a4c124d7fe1958a66bad8a",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"concepts/quantity_cost_contract.md": {
|
||||
@@ -1073,5 +1073,30 @@
|
||||
"mtime": 1789128567.000224,
|
||||
"ast_hash": "0b98a093e67d58c91f57ab12971c3da0",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"concepts/completed_2026-09-12.md": {
|
||||
"mtime": 1789206522.9701393,
|
||||
"ast_hash": "d5c838a31f14c52bf60971e917886eaa",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"pages/B05_Profile/B05_route_edit_2026_09.md": {
|
||||
"mtime": 1789206522.9712057,
|
||||
"ast_hash": "f793738fe66f94f47762c6828ee44fa5",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"pages/B06_Section/B06_profile_cross_unification_2026_09.md": {
|
||||
"mtime": 1789206522.9724846,
|
||||
"ast_hash": "486d417448cda087432b3039be112737",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"pages/B08_Quantity/B08_provenance_ui_2026_09.md": {
|
||||
"mtime": 1789206522.9724846,
|
||||
"ast_hash": "10821b6f0c7f6c19aaf1026e4950c06c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"pages/B09_Estimation/B09_provenance_ui_2026_09.md": {
|
||||
"mtime": 1789206522.9734895,
|
||||
"ast_hash": "a9537ea5efd23d063508c01721cd7591",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: index
|
||||
status: stable
|
||||
last_updated: 2026-09-11
|
||||
last_updated: 2026-09-12
|
||||
---
|
||||
|
||||
# Wiki Index
|
||||
@@ -29,15 +29,19 @@ last_updated: 2026-09-11
|
||||
- [[B05_Profile/B05_corridor_plan_curves|B05 구조물 3D 투영 커브]] — 평면·비탈 투영, 날개벽 연결과 성토면 절단.
|
||||
- [[B05_Profile/B05_corridor_cut_fill|B05 구조물 절취·성토 패치]] — 합집합 절취, 측벽, B06 구간별 성토 리본.
|
||||
- [[B05_Profile/B05_corridor_patch_finish|B05 변형 성토면 마감]] — 패치 스커트, 원지반 재노출, 세월교 날개 역클리핑.
|
||||
- [[B05_Profile/B05_route_edit_2026_09|B05 계획노선 편집]] — 실시간 길이·측점·등고선·R/L 하한과 B05/B06 공용 편집.
|
||||
- [[B06_Section/B06_backend|B06 Section]] — 횡단·유토곡선·확정; [[B06_Section/B06_pavement_revetment|물넘이포장]], [[B06_Section/B06_revetment_link_controls|기슭막이 연동·경사·단별 제어]], [[B06_Section/B06_culvert_link_trim|인접 측점 트림]].
|
||||
- [[B06_Section/B06_profile_cross_unification_2026_09|B06 종횡단 공용화]] — B05 공용 종단 편집·관 스냅샷·횡단 확폭 표시.
|
||||
- [[B07_DesignDetail/B07_frontend|B07 DesignDetail]] — 상세설계·CAD·도각·설계도면과 [[B07_DesignDetail/B07_standard_drawings_2026_09|구조물 표준도]] 관리.
|
||||
- [[B08_Quantity/B08_overview_2026_09|B08 Quantity]] — 토공·구조물·자재 수량산출과 B09 인계.
|
||||
- [[B08_Quantity/B08_2026_09_09_completion|B08 9월 9일 완료]] — 밑수·관 연장·돌 무게와 미확보 경계.
|
||||
- [[B08_Quantity/B08_provenance_ui_2026_09|B08 수량 근거 표시]] — 출처 등급·칸 호버·개발 환경 색칠과 배포 차단.
|
||||
- [[B08_DesignDetail/B08_CAD_commands|B08 CAD 명령]] · [[B08_DesignDetail/B08_CAD_blocks|블록]] · [[B08_DesignDetail/B08_CAD_table_entity|표]] · [[B08_DesignDetail/B08_CAD_title_block|도각·표제란]].
|
||||
- [[B08_DesignDetail/B08_CAD_usability_2026-09-01|B08 CAD 편의성]] — 편집 손실·확정 잠금·도면별 복구 구현 기록과 재검증 대기 상태.
|
||||
- [[B08_DesignDetail/B08_cross_structure_sheets|B08 횡단 장]] · [[B08_DesignDetail/B08_drawing_masshaul_watershed|토적·유역도]].
|
||||
- [[B09_Estimation/B09_overview_2026_09|B09 Estimation]] — 단가산출·예산내역·공사원가계산 5표 구현.
|
||||
- [[B09_Estimation/B09_2026_09_09_completion|B09 9월 9일 완료]] — 단가·수송비·유가·할증·제비율·설계서 구성.
|
||||
- [[B09_Estimation/B09_provenance_ui_2026_09|B09 원가 근거 표시]] — B08 공용 사전·배지·호버와 접힘 패널.
|
||||
- [[B10_Payment/B10_frontend|B10 Payment]] · [[B11_Status/B11_frontend|B11 Status]] — 결제·진행상태 목업.
|
||||
|
||||
## 로그인 전·관리
|
||||
@@ -60,6 +64,7 @@ last_updated: 2026-09-11
|
||||
- 2026-09-04 완료: [[completed_2026-09-04]] — B01 모달·로고, B03 미리보기, 공용 진행단계 패널, B05 3D·종단, B06 횡단, B08 도면, 700줄 제한 분리.
|
||||
- 2026-09-07 완료: [[completed_2026-09-07]] — 노선·캐시·B01~B07 기능, 성능 종료 기준과 보류 갈래.
|
||||
- 2026-09-09 완료: [[completed_2026-09-09]] — B05·B06 종횡단, B07 표준도, B08 수량, B09 원가 구현·검증.
|
||||
- 2026-09-12 체크 완료: [[completed_2026-09-12]] — 계획노선 편집, 종횡단 공용화, 수량·원가 근거 표시; 2차 검증 생략 상태.
|
||||
- 수량·원가 계약: [[quantity_cost_contract]] — B08 물량과 B09 금액의 책임·저장·금지 규칙.
|
||||
- 설계 데이터 생명주기: [[design_data_lifecycle]] — 초기값·작업본·브라우저 캐시와 계획노선 재설계 규칙.
|
||||
- 표준도·입력 미결: [[standard_drawing_cost_inputs]] — 자동 계산·사용자 입력·기준자료·미확보 경계.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
type: ingest-register
|
||||
status: stable
|
||||
related_pages: ["[[ingest/index]]", "[[completed_2026-09-01]]", "[[completed_2026-09-03]]", "[[completed_2026-09-03_additional]]", "[[completed_2026-09-04]]", "[[multi_environment_safety]]"]
|
||||
last_updated: 2026-09-11
|
||||
related_pages: ["[[ingest/index]]", "[[completed_2026-09-01]]", "[[completed_2026-09-03]]", "[[completed_2026-09-03_additional]]", "[[completed_2026-09-04]]", "[[completed_2026-09-12]]", "[[multi_environment_safety]]"]
|
||||
last_updated: 2026-09-12
|
||||
---
|
||||
|
||||
# 2026-09 계획 인제스트
|
||||
@@ -19,3 +19,4 @@ last_updated: 2026-09-11
|
||||
| `docs/raw/plans/2026-09-04_plan_dead_code_cleanup_parked.md` | `parked_not_started` | 위키 완료 범위 제외 |
|
||||
| `docs/raw/plans/2026-09-04_plan_snapshot_before_cleanup.md` | `referenced_completed_with_open_items_carried_forward` | [[completed_2026-09-04]]; 미완료 21건은 `docs/raw/PLAN.md` 유지 |
|
||||
| `docs/raw/plans/2026-09-11_plan_창환경_여섯워크트리_링크_깃운영.md` | `referenced_user_verification_waived` | [[multi_environment_safety]] |
|
||||
| `docs/raw/plans/2026-09-12_plan_checked_complete_items.md` | `referenced_secondary_verification_waived` | [[completed_2026-09-12]] 및 B05·B06·B08·B09 상세 페이지 |
|
||||
|
||||
+9
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: log
|
||||
status: stable
|
||||
last_updated: 2026-09-11
|
||||
last_updated: 2026-09-12
|
||||
---
|
||||
|
||||
## [2026-09-11] ingest+lint | 여섯 워크트리·링크·Git 운영 완료 반영
|
||||
@@ -1445,3 +1445,11 @@ last_updated: 2026-09-11
|
||||
- 위키 본문 215개에서 비로그 100줄 초과·frontmatter·status·page_id 오류는 모두 0건이며 세 원문은 각각 5·4·3곳에 연결됐다.
|
||||
- `graphify update .`가 Markdown 9개를 코드로 오인해 기존 992노드 그래프를 1,168노드로 팽창시켰으므로 산출물을 직전 정제본으로 복구했다.
|
||||
- 신규 `design_data_lifecycle` 의미 노드 반영은 Graphify Markdown 증분 결함 해소 후 다시 수행해야 한다.
|
||||
## [2026-09-12] ingest+plan-cleanup | 체크리스트 전체 완료 항목 위키화
|
||||
|
||||
- 상시 계획서의 체크 완료 상위 8개와 완료 하위 항목 1개를 `2026-09-12_plan_checked_complete_items.md`로 원문 이관했다.
|
||||
- 계획노선 편집, B05·B06 종횡단 공용화, B08·B09 근거 표시를 [[completed_2026-09-12]]와 페이지별 문서에 반영했다.
|
||||
- 미완료 체크가 섞인 `3-17 횡단도 손질` 등은 상시 계획서에 유지했다.
|
||||
- 사용자 지시에 따라 위키 관리자의 2차 교차검증은 생략하고, `검증 대기`·`보류 확정` 경계를 그대로 기록했다.
|
||||
- 린트에서 이번 변경분의 frontmatter·status·page_id·신규 링크·원본 연결은 정상이다. 기존 `completed_2026-08-29.md`와 `B04_backend.md`의 100줄 초과는 이번 범위 밖이라 남겼다.
|
||||
- `graphify update .` 후 그래프는 1,170노드에서 1,191노드·1,013관계·193커뮤니티로 갱신됐고, 신규 완료·B05·B06 페이지가 질의되는 것을 확인했다.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
type: page-feature
|
||||
status: draft
|
||||
page_id: B05_Profile
|
||||
related_pages: ["[[B05_Profile/B05_frontend]]", "[[B06_Section/B06_profile_cross_unification_2026_09]]", "[[completed_2026-09-12]]", "[[design_data_lifecycle]]"]
|
||||
last_updated: 2026-09-12
|
||||
source: ["docs/raw/plans/2026-09-12_plan_checked_complete_items.md"]
|
||||
---
|
||||
|
||||
# B05 계획노선 편집 — 2026-09-12
|
||||
|
||||
## 편집 화면
|
||||
|
||||
- 폴리라인 길이, BP·EP·규칙 측점, 등고선 높이와 선택 강조를 편집 중 표시한다.
|
||||
- 곡선 R과 직선 L은 설계 기준 하한에서 막고, 부족 노드는 붉은 표시와 부족량을 함께 낸다.
|
||||
- 두 노선 지점의 노선상 거리와 종단기울기는 두 번째 점을 찍을 때만 서버 표고 샘플러를 호출한다.
|
||||
- 확정 지표면 모델이 있으면 LAS 등고선을, 없으면 도엽 등고선을 사용한다.
|
||||
- 클로소이드·접선장은 값이 없으면 숨기며, 현재 선형은 단순 원곡선이다.
|
||||
|
||||
## 저장·공용화
|
||||
|
||||
| 항목 | 위치 | 역할 |
|
||||
|---|---|---|
|
||||
| `formatStation` | `B05_Profile/B05_Profile_Util_Station.ts` | 실시간 측점 라벨 공용 변환 |
|
||||
| `build_surface_sampler` | B04 공용 지표면 샘플러 | 두 점의 지반고 조회에 재사용 |
|
||||
| `B05_Profile_UI_Route_Edit.ts` | `B05_Profile/` | 편집 모달 조립; 700줄 제한 아래로 분리 |
|
||||
|
||||
B05와 B06의 관·구조물 편집은 같은 부품과 스냅샷을 사용한다. 2차 교차검증은 사용자 지시로 생략되어 현재 상태는 `draft`다.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
type: page-feature
|
||||
status: draft
|
||||
page_id: B06_Section
|
||||
related_pages: ["[[B06_Section/B06_frontend]]", "[[B06_Section/B06_cross_design_ui_2026_09]]", "[[B05_Profile/B05_route_edit_2026_09]]", "[[completed_2026-09-12]]"]
|
||||
last_updated: 2026-09-12
|
||||
source: ["docs/raw/plans/2026-09-12_plan_checked_complete_items.md"]
|
||||
---
|
||||
|
||||
# B06 종횡단 공용화 — 2026-09-12
|
||||
|
||||
## 완료 체크에서 확인된 구조
|
||||
|
||||
- 종단 그래프는 B06의 `createLongitudinalProfile()`을 B05가 함께 사용한다.
|
||||
- 구조물 우클릭 추가·삭제, 측점선·알약 이동, 좌측 구조물 폼은 공용 경로를 탄다.
|
||||
- 관 추가·삭제·이동은 B05와 같은 `pipes` 스냅샷에 저장하며 옛 이동 예약만 일회 배출한다.
|
||||
- 어느 페이지에서 저장해도 같은 결과가 되도록 스냅샷과 델타의 저장 순서를 맞췄다.
|
||||
- `B06_Section_UI_Section_View.ts`는 그리기 책임을 분리해 693줄로 기록됐다.
|
||||
|
||||
## 횡단 표시
|
||||
|
||||
- 노면 끝에 차도·노견 틱과 노견 폭 라벨을 표시한다.
|
||||
- 확폭은 계획 곡선표의 반경을 쓰고 곡선 앞뒤 10m에서 0으로 잇는다. 새 측점은 만들지 않는다.
|
||||
- 중심선은 계획고 0점에 유지하고, 조정창은 면적표 아래에 둔다.
|
||||
|
||||
미완료 `B06_Section_UI_Page.ts` 700줄 초과 항목은 상시 계획서에 남아 있다. 2차 교차검증 생략으로 이 페이지는 `draft`다.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
type: page-feature
|
||||
status: draft
|
||||
page_id: B08_Quantity
|
||||
related_pages: ["[[B08_Quantity/B08_overview_2026_09]]", "[[B09_Estimation/B09_provenance_ui_2026_09]]", "[[quantity_cost_contract]]", "[[completed_2026-09-12]]"]
|
||||
last_updated: 2026-09-12
|
||||
source: ["docs/raw/plans/2026-09-12_plan_checked_complete_items.md"]
|
||||
---
|
||||
|
||||
# B08 수량 근거 표시 — 2026-09-12
|
||||
|
||||
## 출처 등급과 표시
|
||||
|
||||
- 등급은 `input`, `survey`, `standard`, `calc`, `final`, `blocked`, `excluded`, `unclassified` 여덟 가지다.
|
||||
- 평소에는 색칠을 끄고 개발 환경의 토글로 셀 왼쪽 띠와 배경을 표시한다.
|
||||
- 숫자 칸 호버 카드에 등급·값·식·원천과 개발 빌드의 파일 위치를 낸다.
|
||||
- 열 단위 사전은 서버가 제공하고 칸별 예외가 열 기본값을 덮는다. 배포 환경은 사전 자체를 보내지 않는다.
|
||||
|
||||
## 구현 진입점
|
||||
|
||||
| 항목 | 위치 | 역할 |
|
||||
|---|---|---|
|
||||
| `B08_Quantity_Provenance.py` | `B08_Quantity/` | 시트·열·칸의 근거 사전 조립 |
|
||||
| `common_util_provenance.py` | `common_util/` | 공통 등급 정의와 유효성 검사 |
|
||||
| `B08_Quantity_UI_Provenance.ts` | `B08_Quantity/` | 배지·호버 카드·색칠 토글 |
|
||||
|
||||
실화면 자체검증은 여섯 장 1,914칸에 등급이 붙은 것으로 기록됐다. 위키 관리자의 2차 교차검증은 생략됐다.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
type: page-feature
|
||||
status: draft
|
||||
page_id: B09_Estimation
|
||||
related_pages: ["[[B09_Estimation/B09_overview_2026_09]]", "[[B08_Quantity/B08_provenance_ui_2026_09]]", "[[quantity_cost_contract]]", "[[completed_2026-09-12]]"]
|
||||
last_updated: 2026-09-12
|
||||
source: ["docs/raw/plans/2026-09-12_plan_checked_complete_items.md"]
|
||||
---
|
||||
|
||||
# B09 원가 근거 표시 — 2026-09-12
|
||||
|
||||
## 공용 근거 계약
|
||||
|
||||
- B09는 B08과 같은 배지·띠·호버 카드 UI를 사용한다.
|
||||
- `unit-prices`, `base-data`, `price-sources`, `basis-sheet`, `bill` 응답은 같은 근거 사전을 공유한다.
|
||||
- 실제 화면 기록에서 사전 16장, 일위대가 1,740칸, 산출기초 623칸, 원가계산서 130칸을 확인했다.
|
||||
- 같은 금액 열에서도 총원가는 `final`, 재료비는 `calc`처럼 칸 의미에 따라 등급이 갈린다.
|
||||
|
||||
## 좌측 패널
|
||||
|
||||
- 공사 조건·요율 판·관급자재·이윤 조정·수량 상자는 공용 접힘 UI를 쓴다.
|
||||
- 본문만 스크롤하고 재계산·확정 버튼은 패널 바닥에 고정한다.
|
||||
|
||||
계획서의 완료 체크와 실제 화면 자체검증을 반영했으며, 사용자의 지시에 따라 별도 2차 교차검증은 하지 않았다.
|
||||
|
||||
@@ -758,6 +758,18 @@ export const ui_locales_b2 = {
|
||||
B08_Quantity_Side_Method: ["산출법", "Method"],
|
||||
B08_Quantity_Side_Method_Value: ["평균단면적법", "Average end area"],
|
||||
B08_Quantity_Side_Factors: ["토량환산계수(다짐)", "Conversion factors (compacted)"],
|
||||
B08_Quantity_Factor_Reach: [
|
||||
"이 계수는 유토곡선·운반표·기초단가에도 같이 닿습니다.",
|
||||
"These factors also feed the mass-haul curve, haul table and basis units.",
|
||||
],
|
||||
B08_Quantity_Factor_Default: ["기본값", "Default"],
|
||||
B08_Quantity_Factor_Range: ["품셈 범위", "Standard range"],
|
||||
B08_Quantity_Factor_OutOfRange: [
|
||||
"품셈 범위 밖입니다 — 사유를 적어 주세요(토질시험 값일 수 있습니다).",
|
||||
"Outside the standard range — please note why (it may be a soil-test value).",
|
||||
],
|
||||
B08_Quantity_Factor_Reason: ["사유", "Reason"],
|
||||
B08_Quantity_Factor_Pumsem: ["품셈 체적변화율(C)", "Standard volume-change (C)"],
|
||||
|
||||
/* --- B09_Estimation 원가계산 --- */
|
||||
B09_Estimation_Title: ["원가계산", "Cost Estimate"],
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
/* =============================================================================
|
||||
* ui_template_provenance.ts
|
||||
* 표 칸에 마우스를 올리면 **그 숫자가 어디서 와서 어떻게 나왔는지**를 띄우는 한 벌.
|
||||
* B08 수량·B09 원가가 같이 쓴다 (PLAN 8-36 ②③).
|
||||
*
|
||||
* ⚠⚠ **개발 전용 — 사용자에게 보이지 않는다 (로직 보안).**
|
||||
* 사전은 서버가 개발환경에서만 실어 보낸다(`common_util_provenance.provenance_payload`).
|
||||
* 사전이 안 오면 이 모듈은 **아무것도 하지 않는다** — 화면에서 숨기는 것이 아니라
|
||||
* 애초에 들고 있지 않은 것이 요점이다. 그래서 낱말도 번역하지 않는다(안 나간다).
|
||||
*
|
||||
* ⚠ 겉보기는 **칸 단위**지만 사전은 **열 단위**다. 칸마다 사전을 만들면 토적표 한 장이
|
||||
* 6천 칸이라 응답이 붐는다. 줄마다 갈리는 것(폴백 사유 등)은 부르는 쪽이
|
||||
* `resolveExtra` 로 얹는다.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 열 하나의 사전. 서버 `ColumnProvenance.as_dict()` 와 1:1. */
|
||||
export interface ProvenanceColumn {
|
||||
label: string;
|
||||
tier: string;
|
||||
formula?: string;
|
||||
source?: string;
|
||||
/** 고르는 자리의 채택 규칙(「안전관리비 A·B 중 작은 쪽」 따위). 없으면 칸이 안 뜨다. */
|
||||
rule?: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/** 한 장(시트)의 사전. */
|
||||
export interface ProvenanceSheet {
|
||||
columns: Record<string, ProvenanceColumn>;
|
||||
}
|
||||
|
||||
/** 응답에 실려 오는 사전 전체. 개발환경이 아니면 **칸 자체가 없다**(`undefined`). */
|
||||
export interface ProvenancePayload {
|
||||
sheets: Record<string, ProvenanceSheet>;
|
||||
}
|
||||
|
||||
/** 등급 여섯(+미분류). 키는 서버와 같은 낱말이라야 한다 — 어긋나면 색도 카드도 빈다. */
|
||||
const TIER_LABELS: Record<string, string> = {
|
||||
input: "입력",
|
||||
survey: "측량",
|
||||
standard: "기준",
|
||||
calc: "계산",
|
||||
final: "최종",
|
||||
blocked: "막힘",
|
||||
excluded: "제외",
|
||||
unclassified: "미분류",
|
||||
};
|
||||
|
||||
/** 색칠을 켤지 — **포트별로 갈리는 sessionStorage** 에 둔다(창마다 취향이 다르다). */
|
||||
const TINT_KEY = "aislo.provenance.tint";
|
||||
|
||||
const STYLE_ID = "ui-provenance-style";
|
||||
const CARD_ID = "ui-provenance-card";
|
||||
|
||||
/**
|
||||
* 칸에 심는 표시 — 열 키와 등급. 표를 그리는 쪽이 칸마다 한 번 부른다.
|
||||
*
|
||||
* `tier` 를 주면 **그 칸만 열 등급을 이긴다.** 같은 열이라도 줄마다 성격이 갈리는
|
||||
* 자리가 있기 때문이다 — 원가계산서 「금액」은 중간줄(간접노무비 따위)이 `calc` 인데
|
||||
* 마지막줄(총원가·도급금액·총계)은 `final` 이다(2026-09-12 데스크탑 보조 B09 배선).
|
||||
* 칸 등급을 심지 않으면 열 등급이 그대로 선다.
|
||||
*/
|
||||
export function markProvenanceCell(cell: HTMLElement, columnKey: string, tier?: string): void {
|
||||
cell.dataset.provCol = columnKey;
|
||||
if (tier) cell.dataset.provTier = tier;
|
||||
}
|
||||
|
||||
export function isProvenanceTinted(): boolean {
|
||||
try {
|
||||
return sessionStorage.getItem(TINT_KEY) === "on";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function setTinted(root: HTMLElement, on: boolean): void {
|
||||
root.classList.toggle("is-prov-tinted", on);
|
||||
try {
|
||||
sessionStorage.setItem(TINT_KEY, on ? "on" : "off");
|
||||
} catch {
|
||||
/* 저장이 막힌 창에서도 화면은 돌아야 한다 — 이번 화면에서만 켜진다. */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 색칠 토글 단추. **사전이 없으면 만들지 않는다**(부르는 쪽이 `payload` 를 보고 거른다).
|
||||
*
|
||||
* ⚠ 여섯 색이 늘 켜져 있으면 표가 알록달록해 실무 시트와 눈으로 대조를 못 한다.
|
||||
* 그래서 **평소엔 꺼 두고** 이 단추로만 켠다.
|
||||
*/
|
||||
export function createProvenanceToggle(root: HTMLElement): HTMLElement {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "ui-prov-toggle";
|
||||
const paint = (): void => {
|
||||
const on = root.classList.contains("is-prov-tinted");
|
||||
button.textContent = on ? "등급색 끄기" : "등급색 켜기";
|
||||
button.classList.toggle("is-on", on);
|
||||
};
|
||||
setTinted(root, isProvenanceTinted());
|
||||
paint();
|
||||
button.addEventListener("click", () => {
|
||||
setTinted(root, !root.classList.contains("is-prov-tinted"));
|
||||
paint();
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
function line(card: HTMLElement, name: string, value: string): void {
|
||||
if (!value) return;
|
||||
const row = document.createElement("div");
|
||||
row.className = "ui-prov-card__row";
|
||||
const key = document.createElement("span");
|
||||
key.className = "ui-prov-card__key";
|
||||
key.textContent = name;
|
||||
const body = document.createElement("span");
|
||||
body.className = "ui-prov-card__value";
|
||||
body.textContent = value;
|
||||
row.append(key, body);
|
||||
card.append(row);
|
||||
}
|
||||
|
||||
function card(): HTMLElement {
|
||||
let element = document.getElementById(CARD_ID);
|
||||
if (!element) {
|
||||
element = document.createElement("div");
|
||||
element.id = CARD_ID;
|
||||
element.className = "ui-prov-card";
|
||||
document.body.append(element);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
/** 카드를 마우스 옆에 둔다 — 화면 밖으로 나가면 반대쪽으로 접는다. */
|
||||
function place(element: HTMLElement, x: number, y: number): void {
|
||||
element.style.visibility = "hidden";
|
||||
element.style.display = "block";
|
||||
const box = element.getBoundingClientRect();
|
||||
const left = x + 16 + box.width > window.innerWidth ? x - 16 - box.width : x + 16;
|
||||
const top = y + 16 + box.height > window.innerHeight ? y - 16 - box.height : y + 16;
|
||||
element.style.left = `${Math.max(4, left)}px`;
|
||||
element.style.top = `${Math.max(4, top)}px`;
|
||||
element.style.visibility = "visible";
|
||||
}
|
||||
|
||||
/** 카드 한 장을 채운다. 값은 **화면에 적힌 글자 그대로** 보인다 — 자리수까지 같은 것이 요점. */
|
||||
/** 카드 한 장을 채운다. 배지는 **칸 등급**을 먼저 본다 — 띄는 띄었는데 배지가 다른 말을
|
||||
* 하면 읽는 사람이 둘 중 어느 것을 믿을지 모른다. */
|
||||
function fill(
|
||||
target: HTMLElement,
|
||||
cell: HTMLElement,
|
||||
column: ProvenanceColumn,
|
||||
value: string,
|
||||
extra: string[],
|
||||
): void {
|
||||
target.replaceChildren();
|
||||
const head = document.createElement("div");
|
||||
head.className = "ui-prov-card__head";
|
||||
const title = document.createElement("span");
|
||||
title.textContent = column.label;
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "ui-prov-card__badge";
|
||||
const tier = cell.dataset.provTier || column.tier;
|
||||
badge.dataset.provTier = tier;
|
||||
badge.textContent = TIER_LABELS[tier] ?? tier;
|
||||
head.append(title, badge);
|
||||
target.append(head);
|
||||
|
||||
line(target, "값", value);
|
||||
line(target, "식", column.formula ?? "");
|
||||
line(target, "원천", column.source ?? "");
|
||||
line(target, "채택", column.rule ?? "");
|
||||
line(target, "자리", column.code ?? "");
|
||||
for (const note of extra) line(target, "줄 사유", note);
|
||||
}
|
||||
|
||||
/**
|
||||
* 표에 호버를 붙인다. 칸에 심어 둔 `data-prov-col` 로 사전을 찾는다.
|
||||
*
|
||||
* `resolveExtra` — 줄마다 갈리는 사유(폴백 안분 등)를 얹고 싶을 때 부르는 쪽이 준다.
|
||||
* 사전에 없는 열은 **아무 일도 안 한다** — 빈 카드를 띄우면 「설명이 있다」는 거짓이 남는다.
|
||||
*/
|
||||
export function attachProvenance(
|
||||
root: HTMLElement,
|
||||
sheet: ProvenanceSheet | undefined,
|
||||
resolveExtra?: (cell: HTMLElement, columnKey: string) => string[],
|
||||
): void {
|
||||
if (!sheet) return;
|
||||
injectProvenanceStyles();
|
||||
// 등급을 안 심은 칸은 **여기서 열 등급으로 채운다.**
|
||||
// 색칠은 CSS 가 `data-prov-tier` 를 보고 하므로, 그 칸은 배지만 띄고 띄는 안 붙어
|
||||
// 「색이 안 붙는 칸」이 생긴다. 부르는 쪽이 등급을 빼먹는 것은 흔한 일이라 여기서 맞춘다.
|
||||
for (const cell of root.querySelectorAll<HTMLElement>("[data-prov-col]")) {
|
||||
if (cell.dataset.provTier) continue;
|
||||
const tier = sheet.columns[cell.dataset.provCol ?? ""]?.tier;
|
||||
if (tier) cell.dataset.provTier = tier;
|
||||
}
|
||||
const hide = (): void => {
|
||||
const element = document.getElementById(CARD_ID);
|
||||
if (element) element.style.display = "none";
|
||||
};
|
||||
root.addEventListener("mouseover", (event) => {
|
||||
const cell = (event.target as HTMLElement).closest<HTMLElement>("[data-prov-col]");
|
||||
if (!cell || !root.contains(cell)) return;
|
||||
const column = sheet.columns[cell.dataset.provCol ?? ""];
|
||||
if (!column) return hide();
|
||||
const target = card();
|
||||
fill(
|
||||
target,
|
||||
cell,
|
||||
column,
|
||||
cell.textContent?.trim() ?? "",
|
||||
resolveExtra?.(cell, cell.dataset.provCol ?? "") ?? [],
|
||||
);
|
||||
place(target, (event as MouseEvent).clientX, (event as MouseEvent).clientY);
|
||||
});
|
||||
root.addEventListener("mousemove", (event) => {
|
||||
const element = document.getElementById(CARD_ID);
|
||||
if (!element || element.style.display === "none") return;
|
||||
place(element, (event as MouseEvent).clientX, (event as MouseEvent).clientY);
|
||||
});
|
||||
root.addEventListener("mouseleave", hide);
|
||||
root.addEventListener("mouseout", (event) => {
|
||||
const next = (event as MouseEvent).relatedTarget as HTMLElement | null;
|
||||
if (!next || !next.closest?.("[data-prov-col]")) hide();
|
||||
});
|
||||
}
|
||||
|
||||
const CSS = `
|
||||
/* 등급색 — 평소엔 꺼져 있고 토글로만 켠다. 칸 왼쪽 얇은 띠 + 아주 옅은 배경이라
|
||||
숫자 읽기를 방해하지 않는다. 색은 테마 변수를 섞어 어두운 테마에서도 맞는다. */
|
||||
.is-prov-tinted [data-prov-tier="input"] { box-shadow: inset 3px 0 0 var(--color-accent); background: color-mix(in srgb, var(--color-accent) 7%, transparent); }
|
||||
.is-prov-tinted [data-prov-tier="survey"] { box-shadow: inset 3px 0 0 var(--color-info, #3b82f6); background: color-mix(in srgb, var(--color-info, #3b82f6) 7%, transparent); }
|
||||
.is-prov-tinted [data-prov-tier="standard"] { box-shadow: inset 3px 0 0 var(--color-text-secondary); background: color-mix(in srgb, var(--color-text-secondary) 7%, transparent); }
|
||||
.is-prov-tinted [data-prov-tier="calc"] { box-shadow: inset 3px 0 0 var(--color-success, #16a34a); background: color-mix(in srgb, var(--color-success, #16a34a) 7%, transparent); }
|
||||
.is-prov-tinted [data-prov-tier="final"] { box-shadow: inset 3px 0 0 var(--color-warning, #c08a3e); background: color-mix(in srgb, var(--color-warning, #c08a3e) 10%, transparent); }
|
||||
.is-prov-tinted [data-prov-tier="blocked"] { box-shadow: inset 3px 0 0 var(--color-danger, #dc2626); background: color-mix(in srgb, var(--color-danger, #dc2626) 8%, transparent); }
|
||||
.is-prov-tinted [data-prov-tier="excluded"] { box-shadow: inset 3px 0 0 var(--color-muted, #9ca3af); background: repeating-linear-gradient(135deg, transparent, transparent 5px, color-mix(in srgb, var(--color-muted, #9ca3af) 12%, transparent) 5px, color-mix(in srgb, var(--color-muted, #9ca3af) 12%, transparent) 10px); }
|
||||
.is-prov-tinted [data-prov-tier="unclassified"] { box-shadow: inset 3px 0 0 var(--color-border); }
|
||||
|
||||
.ui-prov-toggle {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-pills, 999px);
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-prov-toggle.is-on { color: var(--color-accent); border-color: var(--color-accent); }
|
||||
|
||||
.ui-prov-card {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
max-width: 26rem;
|
||||
padding: 8px 10px;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface-raised);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards, 8px);
|
||||
box-shadow: 0 6px 18px rgb(0 0 0 / 18%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.ui-prov-card__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 4px; font-weight: 600; }
|
||||
.ui-prov-card__badge { flex: 0 0 auto; padding: 0 6px; font-size: 10.5px; font-weight: 500; border-radius: var(--radius-pills, 999px); border: 1px solid currentColor; }
|
||||
.ui-prov-card__badge[data-prov-tier="input"] { color: var(--color-accent); }
|
||||
.ui-prov-card__badge[data-prov-tier="survey"] { color: var(--color-info, #3b82f6); }
|
||||
.ui-prov-card__badge[data-prov-tier="standard"] { color: var(--color-text-secondary); }
|
||||
.ui-prov-card__badge[data-prov-tier="calc"] { color: var(--color-success, #16a34a); }
|
||||
.ui-prov-card__badge[data-prov-tier="final"] { color: var(--color-warning, #c08a3e); }
|
||||
.ui-prov-card__badge[data-prov-tier="blocked"] { color: var(--color-danger, #dc2626); }
|
||||
.ui-prov-card__badge[data-prov-tier="excluded"] { color: var(--color-muted, #9ca3af); }
|
||||
.ui-prov-card__row { display: flex; gap: 8px; }
|
||||
.ui-prov-card__key { flex: 0 0 2.4rem; color: var(--color-text-secondary); }
|
||||
.ui-prov-card__value { min-width: 0; white-space: pre-wrap; }
|
||||
`;
|
||||
|
||||
export function injectProvenanceStyles(): void {
|
||||
if (document.getElementById(STYLE_ID)) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = STYLE_ID;
|
||||
style.textContent = CSS;
|
||||
document.head.append(style);
|
||||
}
|
||||
Reference in New Issue
Block a user