Merge remote-tracking branches 'origin/main_laptop_1', 'origin/sub_laptop_4' and 'origin/sub_laptop_8' into sub_laptop_3
This commit is contained in:
@@ -55,7 +55,7 @@ export function forgetPageData(projectId: string, leaving = false): void {
|
||||
/** 횡단 context(세션 캐시 · 새 요청 없음) — 노선 id 칸을 채움 · 두 보기가 이것으로만 받음. 서버 창구는 부를 때
|
||||
* 불러옴(묶음 · 초안 측점이 끄는 이 파일이 화면 모듈을 안 끌게). */
|
||||
export async function pageSectionContext(projectId: string): Promise<SectionContextResponse> {
|
||||
const { fetchSectionContext } = await import("../../B06_Section/B06_Section_Api_Fetch");
|
||||
const { fetchSectionContext } = await import("../../B06_Section/B06_Section_Api_Context");
|
||||
const context = await fetchSectionContext(projectId);
|
||||
setPageRouteId(projectId, context.route_id); // 노선 id 한 칸(design_page_route)
|
||||
return context;
|
||||
|
||||
@@ -19,7 +19,7 @@ import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../b_workflow_nav";
|
||||
import { readState, writeState } from "../b_page_state";
|
||||
import { currentRoute, navigateTo, remountRoute } from "../router";
|
||||
import { clearRouteLatestCache } from "../../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import { fetchCurrentRouteId } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
import { fetchCurrentRouteId } from "../../B06_Section/B06_Section_Api_Context";
|
||||
import { createStructuresSection, type StructuresSection } from "../b_structures_section";
|
||||
import type { StructuresCallbacks } from "../../B05_Profile/B05_Profile_UI_Structures_Panel_Types";
|
||||
import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_UI_Profile_Panel_Types";
|
||||
|
||||
@@ -16,6 +16,8 @@ scipy에 의존하지 않는 순수 기하 계산만 두어 프론트엔드 구
|
||||
|
||||
직선 판정 짝: `is_straight_bend` · `STRAIGHT_HEIGHT_M` ↔ TS `logic/B05_Profile_Profile_Straight.ts`
|
||||
(거울 시험 `test_426_profile_straight.py`).
|
||||
경고 글 짝: `build_alignment` warnings ↔ TS `buildAlignment` warnings — 같은 목록
|
||||
(거울 시험 `test_472_save_warnings.py`).
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
@@ -369,7 +371,7 @@ def build_curves(
|
||||
continue
|
||||
if length / 2.0 - half > 1e-6:
|
||||
warnings.append(
|
||||
f"chainage {chainage:.1f}m: 인접 직선이 짧아 종단곡선 길이를 "
|
||||
f"{chainage:.1f}m: 인접 직선이 짧아 종단곡선 길이를 "
|
||||
f"{half * 2.0:.1f}m(반경 {half * 2.0 / abs(delta):.0f}m)로 줄였습니다."
|
||||
)
|
||||
length = half * 2.0
|
||||
@@ -592,18 +594,9 @@ def build_alignment(
|
||||
for segment in segments
|
||||
if abs(segment["grade_percent"]) > policy.max_grade_pct + 1e-6
|
||||
]
|
||||
if violations:
|
||||
worst = max(abs(item["value"]) for item in violations)
|
||||
warnings.append(
|
||||
f"종단기울기 {worst:.2f}%가 기준 {policy.max_grade_pct:.2f}%를 초과하는 구간이 "
|
||||
f"{len(violations)}개 있습니다."
|
||||
)
|
||||
# 경고 글 = TS `buildAlignment` 와 한 벌(472 덧 · 거울 `test_472_save_warnings`) — 기울기 초과 ·
|
||||
# 불균형은 글로 안 남김(`violations` · `balance` 칸이 정본 · 화면 칩이 그 칸으로 셈)
|
||||
within_tolerance = imbalance <= policy.balance_tolerance_percent + 1e-9
|
||||
if not within_tolerance:
|
||||
warnings.append(
|
||||
f"절·성토 불균형 {imbalance:.1f}%가 허용치 "
|
||||
f"{policy.balance_tolerance_percent:.1f}%를 초과합니다."
|
||||
)
|
||||
|
||||
return {
|
||||
"schema_version": ALIGNMENT_SCHEMA_VERSION,
|
||||
|
||||
@@ -163,6 +163,18 @@ def valid_cross_names(cross_dir: Path, stations: list[Any]) -> set[str]:
|
||||
}
|
||||
|
||||
|
||||
def cross_files_in_order(cross_dir: Path, stations: list[Any]) -> list[Path]:
|
||||
"""잔재를 걸러 읽을 cross_*.json — **측점 차례**(이름 차례면 `01080.16` 이 `01080` 앞)."""
|
||||
valid = valid_cross_names(cross_dir, stations)
|
||||
|
||||
def chainage(path: Path) -> tuple[float, str]:
|
||||
match = re.fullmatch(rf"cross_{CROSS_KEY_PATTERN}m", path.stem)
|
||||
return (float(match.group(1)) if match else float("inf"), path.name)
|
||||
|
||||
files = [path for path in cross_dir.glob("cross_*.json") if not valid or path.name in valid]
|
||||
return sorted(files, key=chainage)
|
||||
|
||||
|
||||
def prune_stale_cross_files(cross_dir: Path, stations: list[Any]) -> set[str]:
|
||||
"""stations에 없는 잔재 cross_*.json을 삭제하고 유효 파일명 집합을 반환한다.
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { stationPartsLabel } from "../common_util/common_util_station";
|
||||
import { PIPE_DISPLAY_NAME } from "../common_util/common_util_structure_catalog";
|
||||
import {
|
||||
ESCAPE_ROUTE_DEFAULT_WIDTH_M,
|
||||
ESCAPE_ROUTE_WIDTHS_M,
|
||||
@@ -54,8 +55,6 @@ export interface IrregularStation {
|
||||
/** 관 매설 지점이 구조물 목록에 실체화될 때 쓰는 종류 값(구 데이터 판정 키라 유지 —
|
||||
* 화면 표시 이름은 "배수관"이다, 2026-08-17 사용자 지시). */
|
||||
export const PIPE_STRUCTURE_NAME = "배관";
|
||||
/** 화면에 보이는 이름. */
|
||||
export const PIPE_DISPLAY_NAME = "배수관";
|
||||
|
||||
/** 배수관으로 볼 항목인가. 드롭다운 종류가 "배관"이거나 손으로 이름을 "배관"·"배수관"이라
|
||||
* 적은 것도 관 지점 정본을 따르게 한다 — 아니면 [초기화]로 지워지지 않고 배수유역도와
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_Api_Context.ts
|
||||
* 종횡단 설정값(`GET …/sections/context`)의 **세션 캐시 창구** — 화면 쪽.
|
||||
*
|
||||
* `B06_Section_Api_Fetch.ts` 에서 갈라냄(472-2) — 서버 Node 묶음이 Api_Fetch 를 끌어도
|
||||
* 세션(`b_page_state`) · 되돌리기 사진첩(`Profile_History`)을 함께 끌지 않게.
|
||||
* ========================================================================== */
|
||||
|
||||
import { clearState, readState, stateKey, writeState } from "../A00_Common/b_page_state";
|
||||
import { seedPageHistory } from "../B05_Profile/B05_Profile_UI_Profile_History";
|
||||
import { requestSectionContext } from "./B06_Section_Api_Fetch";
|
||||
import type { SectionContextResponse } from "./B06_Section_Api_Types";
|
||||
|
||||
/** 최신 확정 경로와 B04 확정값, config 기반 생성 기본값을 조회한다. */
|
||||
/** 종횡단 설정값 — ④ 계산 결과라 세션에 담아 두고 화면을 오갈 때마다 다시 묻지 않는다
|
||||
* (2026-09-06 호출 정리). 노선이 바뀌면 `clearSectionContextCache` 로 버린다. */
|
||||
export async function fetchSectionContext(projectId: string): Promise<SectionContextResponse> {
|
||||
const cached = readState<SectionContextResponse>("section-context", projectId);
|
||||
if (cached) return seedStandardCross(projectId, cached);
|
||||
const fresh = await requestSectionContext(projectId);
|
||||
writeState("section-context", fresh, projectId);
|
||||
return seedStandardCross(projectId, fresh);
|
||||
}
|
||||
|
||||
/** 서버가 지금 보는 노선 id — 세션 캐시를 건너뛰고 묻는다. [저장] · [확정] 앞에서 화면이 쥔
|
||||
* 노선과 대조한다(PLAN 76-1). 받은 설정값으로 캐시도 새로 둔다. */
|
||||
export async function fetchCurrentRouteId(projectId: string): Promise<number | null> {
|
||||
const fresh = await requestSectionContext(projectId);
|
||||
writeState("section-context", fresh, projectId);
|
||||
return fresh.route_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장된 표준 횡단면을 **세션이 비어 있을 때만** 채운다(2026-09-07).
|
||||
*
|
||||
* 브라우저 횡단 계산은 `세션 ?? config 기본값` 으로 서는데, 표준단면 세션값은 그 탭에서만
|
||||
* 산다. 그래서 **탭을 새로 열면** 화면은 config 기본값으로, 서버는 저장분으로 계산해
|
||||
* 같은 측점이 갈렸다(실측 — 용화 route 169 저장분은 암반 횡단경사 **5%**·측구 상단폭
|
||||
* **0.9m**, config 기본값은 **3%**·**0.69m**).
|
||||
*
|
||||
* 여기가 두 화면(B05·B06)이 함께 지나는 유일한 자리라 이 한 곳에서 채운다. 사용자가
|
||||
* 그 탭에서 고친 값이 있으면 **건드리지 않는다** — 초안이 언제나 우선이다.
|
||||
*/
|
||||
function seedStandardCross(
|
||||
projectId: string,
|
||||
context: SectionContextResponse,
|
||||
): SectionContextResponse {
|
||||
const stored = context.stored_standard_cross_section;
|
||||
if (stored && readState<unknown>("std-cross", projectId) === null) {
|
||||
writeState("std-cross", stored, projectId);
|
||||
seedPageHistory(stateKey("std-cross", projectId)); // 씨앗 — 되돌리기 걸음 아님(364-1)
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function clearSectionContextCache(projectId: string): void {
|
||||
clearState("section-context", projectId);
|
||||
}
|
||||
@@ -16,15 +16,11 @@
|
||||
* 기존 호출 코드가 깨지지 않도록 여기서 그대로 다시 내보낸다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { clearState, readState, stateKey, writeState } from "../A00_Common/b_page_state";
|
||||
import { seedPageHistory } from "../B05_Profile/B05_Profile_UI_Profile_History";
|
||||
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import type { CrossDesignChoice } from "./B06_Section_Cross_Design_Session";
|
||||
import type {
|
||||
CrossDesign,
|
||||
CrossDesignRequest,
|
||||
CrossDesignResponse,
|
||||
CrossSection,
|
||||
CrossSectionPatch,
|
||||
SectionConfirmResponse,
|
||||
@@ -62,56 +58,11 @@ async function requestJson<T>(path: string, init: RequestInit): Promise<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 최신 확정 경로와 B04 확정값, config 기반 생성 기본값을 조회한다. */
|
||||
/** 종횡단 설정값 — ④ 계산 결과라 세션에 담아 두고 화면을 오갈 때마다 다시 묻지 않는다
|
||||
* (2026-09-06 호출 정리). 노선이 바뀌면 `clearSectionContextCache` 로 버린다. */
|
||||
export async function fetchSectionContext(projectId: string): Promise<SectionContextResponse> {
|
||||
const cached = readState<SectionContextResponse>("section-context", projectId);
|
||||
if (cached) return seedStandardCross(projectId, cached);
|
||||
const fresh = await requestJson<SectionContextResponse>(
|
||||
`/projects/${projectId}/sections/context`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
writeState("section-context", fresh, projectId);
|
||||
return seedStandardCross(projectId, fresh);
|
||||
}
|
||||
|
||||
/** 서버가 지금 보는 노선 id — 세션 캐시를 건너뛰고 묻는다. [저장] · [확정] 앞에서 화면이 쥔
|
||||
* 노선과 대조한다(PLAN 76-1). 받은 설정값으로 캐시도 새로 둔다. */
|
||||
export async function fetchCurrentRouteId(projectId: string): Promise<number | null> {
|
||||
const fresh = await requestJson<SectionContextResponse>(
|
||||
`/projects/${projectId}/sections/context`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
writeState("section-context", fresh, projectId);
|
||||
return fresh.route_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장된 표준 횡단면을 **세션이 비어 있을 때만** 채운다(2026-09-07).
|
||||
*
|
||||
* 브라우저 횡단 계산은 `세션 ?? config 기본값` 으로 서는데, 표준단면 세션값은 그 탭에서만
|
||||
* 산다. 그래서 **탭을 새로 열면** 화면은 config 기본값으로, 서버는 저장분으로 계산해
|
||||
* 같은 측점이 갈렸다(실측 — 용화 route 169 저장분은 암반 횡단경사 **5%**·측구 상단폭
|
||||
* **0.9m**, config 기본값은 **3%**·**0.69m**).
|
||||
*
|
||||
* 여기가 두 화면(B05·B06)이 함께 지나는 유일한 자리라 이 한 곳에서 채운다. 사용자가
|
||||
* 그 탭에서 고친 값이 있으면 **건드리지 않는다** — 초안이 언제나 우선이다.
|
||||
*/
|
||||
function seedStandardCross(
|
||||
projectId: string,
|
||||
context: SectionContextResponse,
|
||||
): SectionContextResponse {
|
||||
const stored = context.stored_standard_cross_section;
|
||||
if (stored && readState<unknown>("std-cross", projectId) === null) {
|
||||
writeState("std-cross", stored, projectId);
|
||||
seedPageHistory(stateKey("std-cross", projectId)); // 씨앗 — 되돌리기 걸음 아님(364-1)
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function clearSectionContextCache(projectId: string): void {
|
||||
clearState("section-context", projectId);
|
||||
/** 종횡단 설정값 한 번 묻기(캐시 없음) — 세션 캐시 창구는 `B06_Section_Api_Context.ts`(472-2). */
|
||||
export function requestSectionContext(projectId: string): Promise<SectionContextResponse> {
|
||||
return requestJson<SectionContextResponse>(`/projects/${projectId}/sections/context`, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/** 경로의 종단면 요약을 조회한다. */
|
||||
@@ -161,18 +112,6 @@ export async function regenerateSections(
|
||||
);
|
||||
}
|
||||
|
||||
/** 측점 표준횡단 설계(지반유형·단면유형)를 즉시 계산·저장하고 잠정 결과를 반환한다. */
|
||||
export async function computeCrossDesign(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
request: CrossDesignRequest,
|
||||
): Promise<CrossDesignResponse> {
|
||||
return requestJson<CrossDesignResponse>(
|
||||
`/projects/${projectId}/sections/${routeId}/cross-design`,
|
||||
{ method: "POST", body: JSON.stringify(request) },
|
||||
);
|
||||
}
|
||||
|
||||
/** 경로의 종·횡단면을 확정한다. 표준 횡단면 설정값·측점별 세션 보관값을 함께 저장할 수 있다. */
|
||||
export async function confirmSections(
|
||||
projectId: string,
|
||||
|
||||
@@ -550,35 +550,6 @@ export interface CrossDesign {
|
||||
pipe_length_m?: number;
|
||||
}
|
||||
|
||||
export interface CrossDesignResponse {
|
||||
status: string;
|
||||
chainage_m: number;
|
||||
design: CrossDesign;
|
||||
}
|
||||
|
||||
export interface CrossDesignRequest {
|
||||
chainage_m: number;
|
||||
ground_type: GroundType;
|
||||
section_mode: SectionMode;
|
||||
ditch_side?: DitchSide | null;
|
||||
/** 측구 형식(일반/L형). L형은 암 지반에서만 허용된다. */
|
||||
ditch_type?: DitchType;
|
||||
/** 포장 중첩 여부 — 횡단경사·포장층만 포장 그룹 값으로 계산된다. */
|
||||
paved?: boolean;
|
||||
/** 암 경계선 오프셋(m, 지면선 기준 하향 음수). 암 지반 2단계 절토 무릎 계산용. */
|
||||
rock_boundary_offset_m?: number | null;
|
||||
/** 암 지반 2단계 경사 적용 여부(기본 true, 토글로 해제). */
|
||||
two_stage_slope?: boolean;
|
||||
/** 이 측점만 쓰는 암 절토 경사비(1:n 의 n). 없으면 표준값(2026-09-07). */
|
||||
cut_slope_ratio?: number | null;
|
||||
/** 옛 이름 — 결과값이 그대로 실려 오던 자리(호환). 새 요청은 `ditch_choice` 를 쓴다. */
|
||||
ditch_enabled?: boolean | null;
|
||||
/** 측구를 둘지 **사용자가 정한 선택**. `null`/미지정 = 자동 판정(2026-09-09 정리). */
|
||||
ditch_choice?: boolean | null;
|
||||
/** 설정 패널 편집값. 요청값 → config 기본값 순으로 우선한다. */
|
||||
standard_cross_section?: StandardCrossSection;
|
||||
}
|
||||
|
||||
/** 확정 시 측점별 data.design에 병합할 프론트 세션 보관값. */
|
||||
export interface CrossSectionPatch {
|
||||
chainage_m: number;
|
||||
|
||||
@@ -31,15 +31,6 @@ export interface CrossDesignChoice {
|
||||
const readAll = (projectId: string, routeId: number) =>
|
||||
readKeyed<CrossDesignChoice>("crossdesign", projectId, routeId);
|
||||
|
||||
/** 이 측점의 선택값(없으면 null). */
|
||||
export function readCrossDesignChoice(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
chainageM: number,
|
||||
): CrossDesignChoice | null {
|
||||
return readAll(projectId, routeId).get(chainageKey(chainageM)) ?? null;
|
||||
}
|
||||
|
||||
/** 선택값을 세션에 담는다. 같은 측점의 앞선 값은 덮어쓴다. */
|
||||
export function writeCrossDesignChoice(
|
||||
projectId: string,
|
||||
|
||||
@@ -16,18 +16,12 @@ from fastapi.responses import JSONResponse, Response
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile
|
||||
from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
cross_files_in_order,
|
||||
relabel_pipe_stations,
|
||||
run_section_generation,
|
||||
valid_cross_names,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import SectionGenerationOptions
|
||||
from B06_Section.B06_Section_Engine_Attach import attach_saved_shapes
|
||||
from B06_Section.B06_Section_Engine_Design import (
|
||||
compute_cross_design,
|
||||
curve_widening_args,
|
||||
stored_shoulder_extend,
|
||||
)
|
||||
from B06_Section.B06_Section_Engine_Tilt import tilt_args, tilt_ranges
|
||||
from B06_Section.B06_Section_Repository import (
|
||||
count_cross_sections,
|
||||
create_longitudinal_section,
|
||||
@@ -40,26 +34,13 @@ from B06_Section.B06_Section_Repository import (
|
||||
get_workflow_route_context,
|
||||
insert_cross_sections,
|
||||
list_recent_company_projects,
|
||||
update_cross_section_design,
|
||||
)
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
PREVIEW_DESIGN_FIELDS as _PREVIEW_DESIGN_FIELDS,
|
||||
)
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
USER_TOUCHED_KEYS,
|
||||
ford_drop_at,
|
||||
ford_surface_drops,
|
||||
paved_at,
|
||||
pavement_ranges,
|
||||
stored_berm,
|
||||
stored_cut_slope,
|
||||
)
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
attach_default_designs as _attach_default_designs,
|
||||
)
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
read_cross_design_inputs as _read_cross_design_inputs,
|
||||
)
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
recompute_designs_for_alignment as _recompute_designs_for_alignment,
|
||||
)
|
||||
@@ -76,8 +57,6 @@ from B06_Section.B06_Section_Schema import (
|
||||
CompanyStandardResponse,
|
||||
CrossDesignPreviewRequest,
|
||||
CrossDesignPreviewResponse,
|
||||
CrossDesignRequest,
|
||||
CrossDesignResponse,
|
||||
HaulEquipmentLimit,
|
||||
SectionContextResponse,
|
||||
SectionDetailResponse,
|
||||
@@ -269,12 +248,10 @@ def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dic
|
||||
|
||||
longitudinal = json.loads(longitudinal_path.read_text(encoding="utf-8"))
|
||||
stations = longitudinal.get("stations") if isinstance(longitudinal, dict) else None
|
||||
# 조회는 지우지 않음 — 잔재는 걸러 읽기만(지우기는 측점을 쓰는 쪽 몫)
|
||||
valid_names = valid_cross_names(cross_dir, stations if isinstance(stations, list) else [])
|
||||
# 조회는 지우지 않음 — 잔재는 걸러 읽기만(지우기는 측점을 쓰는 쪽 몫) · 측점 차례(472-2)
|
||||
cross_sections = [
|
||||
json.loads(path.read_text(encoding="utf-8"))
|
||||
for path in sorted(cross_dir.glob("cross_*.json"))
|
||||
if not valid_names or path.name in valid_names
|
||||
for path in cross_files_in_order(cross_dir, stations if isinstance(stations, list) else [])
|
||||
]
|
||||
if not isinstance(longitudinal, dict) or not all(
|
||||
isinstance(section, dict) for section in cross_sections
|
||||
@@ -597,102 +574,3 @@ async def preview_cross_designs(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "횡단 설계 프리뷰 계산 중 오류가 발생했습니다."},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{project_id}/sections/{route_id}/cross-design", response_model=CrossDesignResponse)
|
||||
async def compute_cross_section_design(
|
||||
project_id: UUID, route_id: int, request: CrossDesignRequest
|
||||
) -> CrossDesignResponse | JSONResponse:
|
||||
"""측점 표준횡단 설계(지반유형·단면유형)를 즉시 계산해 잠정치로 저장한다."""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
longitudinal = await get_longitudinal_section(connection, project_id, route_id)
|
||||
if not longitudinal:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"status": "error", "message": "종횡단 상세 결과가 없습니다."},
|
||||
)
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
# 저장분을 **계산 전에** 읽는다 — 소단은 사용자 조작값이면서 **기하 입력**이라,
|
||||
# 나중에 키만 베껴 붙이면 설계선·면적은 계단 없이 나오고 `berm` 값만 남아
|
||||
# 서로 어긋난다(2026-09-07). 값을 나르는 다른 사용자 키와 다른 점이다.
|
||||
stored_designs = await get_cross_section_designs(connection, route_id)
|
||||
stored_design: dict[str, Any] | None = None
|
||||
for record in stored_designs:
|
||||
if abs(float(record["chainage_m"]) - request.chainage_m) < 0.01:
|
||||
candidate = record.get("design")
|
||||
stored_design = candidate if isinstance(candidate, dict) else None
|
||||
break
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
samples, design_elevation, pavement_suggested, cross_record = await asyncio.to_thread(
|
||||
_read_cross_design_inputs,
|
||||
project_root,
|
||||
str(longitudinal["longitudinal_file_path"]),
|
||||
request.chainage_m,
|
||||
)
|
||||
design = compute_cross_design(
|
||||
samples,
|
||||
design_elevation,
|
||||
ground_type=request.ground_type,
|
||||
section_mode=request.section_mode,
|
||||
ditch_side=request.ditch_side,
|
||||
ditch_type=request.ditch_type,
|
||||
paved=paved_at(request.chainage_m, pavement_ranges(project_root)),
|
||||
standard=request.standard_cross_section,
|
||||
rock_boundary_offset_m=request.rock_boundary_offset_m,
|
||||
two_stage_slope=request.two_stage_slope,
|
||||
# 측점별 암 절토 경사 — 요청값이 없으면 저장분에서 잇는다(2026-09-07).
|
||||
cut_slope_ratio=request.cut_slope_ratio or stored_cut_slope(stored_design or {}),
|
||||
ditch_enabled=request.ditch_enabled,
|
||||
ditch_choice=request.ditch_choice,
|
||||
surface_drop_m=ford_drop_at(request.chainage_m, ford_surface_drops(project_root)),
|
||||
berm=stored_berm(stored_design or {}),
|
||||
**curve_widening_args(cross_record),
|
||||
**stored_shoulder_extend(stored_design or {}), # 노견 확장(139-6)
|
||||
**tilt_args(request.chainage_m, tilt_ranges(project_root)),
|
||||
)
|
||||
# B06 지정 시점은 잠정치. B07 도면 확정 시 동일 엔진으로 재계산해 confirmed로 승격한다.
|
||||
design["status"] = "provisional"
|
||||
# 법정 근거 문구 표기용 — 사용자가 포장을 바꿔도 제안 여부는 그대로 남긴다.
|
||||
design["pavement_suggested"] = pavement_suggested
|
||||
# 표시 설정(측점 개별 반폭)은 계산 입력이 아니다 — 저장분에서 이월해 재계산이
|
||||
# 지우지 않게 한다(2026-08-06). 목록을 여기 다시 적지 않는다 — 서버의 한 벌은
|
||||
# `B06_Section_Router_Design.USER_TOUCHED_KEYS` 다(2026-09-07). 예전에는 여기에
|
||||
# 따로 적어 두어 `extra_spans` 가 빠져 있었다.
|
||||
if stored_design is not None:
|
||||
for key in USER_TOUCHED_KEYS:
|
||||
if stored_design.get(key) is not None:
|
||||
design[key] = stored_design[key]
|
||||
async with pool.acquire() as connection:
|
||||
await connection.begin()
|
||||
try:
|
||||
updated = await update_cross_section_design(
|
||||
connection,
|
||||
route_id=route_id,
|
||||
chainage_m=request.chainage_m,
|
||||
design=design,
|
||||
project_id=project_id,
|
||||
)
|
||||
await connection.commit()
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
raise
|
||||
if not updated:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"status": "error", "message": "해당 측점 횡단 레코드가 없습니다."},
|
||||
)
|
||||
return CrossDesignResponse(chainage_m=request.chainage_m, design=design)
|
||||
except FileNotFoundError as exc:
|
||||
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
|
||||
except ValueError as exc:
|
||||
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"B06 측점 횡단 설계 계산 실패: project_id=%s route_id=%s", project_id, route_id
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "횡단 설계 계산 중 오류가 발생했습니다."},
|
||||
)
|
||||
|
||||
@@ -63,14 +63,6 @@ class CrossDesignRequest(BaseModel):
|
||||
standard_cross_section: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class CrossDesignResponse(BaseModel):
|
||||
"""측점 표준횡단 설계 계산 결과(잠정치)."""
|
||||
|
||||
status: str = "success"
|
||||
chainage_m: float
|
||||
design: dict[str, Any]
|
||||
|
||||
|
||||
class BasinAdjustPatch(BaseModel):
|
||||
"""집수정 내부 치수와 기본 위치 대비 이동량."""
|
||||
|
||||
|
||||
@@ -229,6 +229,42 @@ def _enforce_stored_designs(
|
||||
enforce_ford_surface_drops(longitudinal, sections, project_root, standard)
|
||||
|
||||
|
||||
def refresh_alignment(longitudinal: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""종단 경고를 저장 입력(base_pvi · 편집 · 정책 · 기준)으로 지금 코드가 다시 셈 — 옛 코드가
|
||||
쓴 경고가 남지 않게(472 덧 · 요구 272). 기하(계획고 · 곡선 · 표본 · 위반)는 저장값 그대로 —
|
||||
저장값이 6 자리 반올림이라 통째로 다시 쓰면 끝자리가 흔들림(계획고 1e-6 m → 면적 1e-4 ㎡).
|
||||
반환 = 요약(`grade_summary`) · 선형이 없는 옛 종단이면 None(그대로)."""
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile
|
||||
|
||||
stored = longitudinal.get("profile_alignment")
|
||||
if not isinstance(stored, dict) or not stored.get("base_pvi"):
|
||||
return None
|
||||
fresh, profile = rebuild_alignment_profile(longitudinal, stored.get("edits"))
|
||||
stored["warnings"] = fresh["warnings"]
|
||||
profiles = longitudinal.get("design_profiles")
|
||||
if isinstance(profiles, list) and profiles and isinstance(profiles[0], dict):
|
||||
profile = profiles[0]
|
||||
profile.setdefault("summary", {})["warnings"] = fresh["warnings"]
|
||||
else:
|
||||
longitudinal["design_profiles"] = [profile]
|
||||
return {"id": profile.get("id"), **profile["summary"]}
|
||||
|
||||
|
||||
def _refresh_alignment_file(project_root: Path, relative: str) -> dict[str, Any] | None:
|
||||
"""종단 정본 파일의 선형을 다시 셈 · 바뀐 때만 씀. 파일 · 선형이 없으면 None."""
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
|
||||
path = project_root / relative
|
||||
if not path.is_file():
|
||||
return None
|
||||
longitudinal = json.loads(path.read_text(encoding="utf-8"))
|
||||
before = json.dumps(longitudinal, sort_keys=True, default=str)
|
||||
summary = refresh_alignment(longitudinal)
|
||||
if summary is not None and json.dumps(longitudinal, sort_keys=True, default=str) != before:
|
||||
atomic_write_json(path, longitudinal)
|
||||
return summary
|
||||
|
||||
|
||||
async def recompute_server_side(project_id: UUID | str, route_id: int) -> int:
|
||||
"""포장 구간·세월교 노면 하강 보정에 더해 구조물 면적·유토곡선까지 Node 로 만들어 저장한다.
|
||||
|
||||
@@ -240,19 +276,40 @@ async def recompute_server_side(project_id: UUID | str, route_id: int) -> int:
|
||||
|
||||
|
||||
async def _recompute(project_id: UUID | str, route_id: int) -> int:
|
||||
from B05_Profile.B05_Profile_Repository import update_longitudinal_grade_summary
|
||||
from B06_Section.B06_Section_Router import get_section_detail
|
||||
|
||||
project_uuid = UUID(str(project_id))
|
||||
marks = [("시작", time.perf_counter())]
|
||||
# 상세 만들기(파일 읽기 위주)와 DB 두 건은 서로 기다릴 이유가 없다 — 같이 보낸다.
|
||||
# 원격 DB 라 순차로 내면 왕복이 그대로 더해진다(질의 하나 약 12ms, 2026-09-06 실측).
|
||||
# 원격 DB 라 순차로 내면 왕복이 그대로 더해진다(질의 하나 약 12ms, 2026-09-06 실측) — 같이 보냄.
|
||||
pool = get_db_pool()
|
||||
# 구조물 몫(채집석 공제·구조물 잔토)도 함께 받아 온다 — **B08 이 낸 값**이고, 안 넘기면
|
||||
# 통로만 있고 값이 안 흐른다(2026-09-09 실측: 공제가 늘 `None` 이라 사토가 안 줄었다).
|
||||
response, stored_path, longitudinal_row, haul_inputs, surface = await asyncio.gather(
|
||||
get_section_detail(project_uuid, route_id),
|
||||
stored_path, longitudinal_row = await asyncio.gather(
|
||||
run_with_connection(get_project_storage_relative_path, project_uuid),
|
||||
run_with_connection(get_longitudinal_section, project_uuid, route_id),
|
||||
)
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
# 종단 경고 먼저 — 저장 입력으로 다시 셈(472 덧) · 아래 상세가 새 경고를 읽음
|
||||
summary = await asyncio.to_thread(
|
||||
_refresh_alignment_file,
|
||||
project_root,
|
||||
str((longitudinal_row or {}).get("longitudinal_file_path") or ""),
|
||||
)
|
||||
if summary is not None:
|
||||
async with pool.acquire() as connection:
|
||||
await connection.begin()
|
||||
try:
|
||||
await update_longitudinal_grade_summary(
|
||||
connection, route_id=route_id, grade_summary=summary
|
||||
)
|
||||
await connection.commit()
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
raise
|
||||
marks.append(("종단 선형 다시 셈", time.perf_counter()))
|
||||
# 구조물 몫(채집석 공제·구조물 잔토)도 함께 받아 온다 — **B08 이 낸 값**이고, 안 넘기면
|
||||
# 통로만 있고 값이 안 흐른다(2026-09-09 실측: 공제가 늘 `None` 이라 사토가 안 줄었다).
|
||||
response, haul_inputs, surface = await asyncio.gather(
|
||||
get_section_detail(project_uuid, route_id),
|
||||
haul_inputs_for(project_uuid),
|
||||
run_with_connection(get_surface_confirmation_params, str(project_uuid)),
|
||||
)
|
||||
@@ -262,7 +319,6 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int:
|
||||
raise RuntimeError(f"서버 재계산: 종횡단 상세를 못 받음 (route_id={route_id})")
|
||||
detail = payload(mode="json")
|
||||
sections = detail.get("cross_sections") or []
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
from B06_Section.B06_Section_Router_Design import stored_standard_cross_section
|
||||
|
||||
standard = stored_standard_cross_section(longitudinal_row)
|
||||
|
||||
@@ -232,9 +232,6 @@ export function registeredKeys(
|
||||
return sections.map((s, i) => (list && nearest(list, s.chainage_m)?.key) ?? computed[i].key);
|
||||
}
|
||||
|
||||
/** 지금 노선(마지막으로 세운 목록) — 세션 열쇠만 아는 저장소(암 경계 · 암 경사)가 씀. */
|
||||
export const currentStationScope = () => current;
|
||||
|
||||
function nearest(list: Keyed[], chainageM: number): Keyed | undefined {
|
||||
let best: Keyed | undefined;
|
||||
for (const entry of list) {
|
||||
|
||||
@@ -22,11 +22,6 @@ import {
|
||||
import { EXTRA_SPAN_DEFAULT } from "./logic/B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { StructureSpan } from "./logic/B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import {
|
||||
designInterpolator,
|
||||
groundInterpolator,
|
||||
slopeToeOffset,
|
||||
} from "./logic/B06_Section_UI_Cross_Culvert_Solve";
|
||||
/**
|
||||
* 기슭막이 4축 조작 제어(2026-08-22 확정) — 벽을 눌러 고르고 조정창에서
|
||||
* 좌우(x)·상하(d, 성토선 대각)·높이(h)·재질(m)을 만진다. 고른 벽은 여기 담아 두어
|
||||
@@ -648,26 +643,3 @@ function adjustsInput(
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* **구조물 없는 상태**의 설계 절·성토선이 원지반과 만나는 지점이 들어오는 데
|
||||
* 필요한 표시 반폭(m, 여유 0.5 포함) — 2026-08-22 사용자 확정 줌 기준.
|
||||
* 구조물(기슭막이·집수정)이 어디로 가든 화면 기준은 지형·설계선이 정한다.
|
||||
*/
|
||||
export function culvertRequiredHalfWidth(section: CrossSection): number | null {
|
||||
const design = section.design;
|
||||
if (!design) return null;
|
||||
const groundAt = groundInterpolator(section.samples);
|
||||
const designAt = designInterpolator(design.design_line);
|
||||
const edges = design.road_edges;
|
||||
if (!groundAt || !designAt || !edges) return null;
|
||||
const offsets = section.samples.map((sample) => sample.offset_m ?? 0);
|
||||
const limits = { left: Math.max(...offsets), right: Math.min(...offsets) };
|
||||
let extent = 0;
|
||||
for (const side of ["left", "right"] as const) {
|
||||
const edge = side === "left" ? edges.left : edges.right;
|
||||
const toe = slopeToeOffset(designAt, groundAt, edge.offset_m, limits[side]);
|
||||
extent = Math.max(extent, Math.abs(toe));
|
||||
}
|
||||
return extent > 0 ? extent + 0.5 : null;
|
||||
}
|
||||
|
||||
@@ -8,15 +8,9 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { PIPE_TYPES } from "@config/config_frontend";
|
||||
import { PIPE_DISPLAY_NAME } from "../B05_Profile/B05_Profile_UI_IrregularStations";
|
||||
import { pipeTitleKinds } from "../common_util/common_util_structure_catalog";
|
||||
import { PIPE_DISPLAY_NAME, pipeTitleKinds } from "../common_util/common_util_structure_catalog";
|
||||
import type { CrossDesignChange } from "./B06_Section_UI_Cross_Design";
|
||||
import type {
|
||||
DesignProfile,
|
||||
LongitudinalSection,
|
||||
SectionDetailResponse,
|
||||
SectionSample,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import type { DesignProfile, LongitudinalSection, SectionSample } from "./B06_Section_Api_Fetch";
|
||||
|
||||
export type { CrossDesignChange };
|
||||
export type DesignChangeHandler = (chainageM: number, change: CrossDesignChange) => void;
|
||||
@@ -274,34 +268,6 @@ export function longitudinalMaxChainage(data: LongitudinalSection): number {
|
||||
return Math.max(data.length_m, samples[samples.length - 1]?.chainage_m ?? 1, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 종·횡단 공통 Y스케일. `longHeightPx`는 종단면도의 실제 렌더 높이로, 패널 리사이즈로
|
||||
* 종단도가 줄면 그 높이를 넘겨야 표고 범위가 잘리지 않는다(기본은 고정 높이).
|
||||
*/
|
||||
export function calculateYScale(
|
||||
detail: SectionDetailResponse,
|
||||
longHeightPx: number = LONG_HEIGHT,
|
||||
): YScaleOptions | undefined {
|
||||
const elevations = [
|
||||
...detail.longitudinal.samples.map((sample) => sample.elevation_m),
|
||||
...detail.cross_sections.flatMap((section) =>
|
||||
section.samples.map((sample) => sample.elevation_m),
|
||||
),
|
||||
...(detail.longitudinal.design_profiles ?? []).flatMap((profile) =>
|
||||
profile.samples.map((sample) => sample.elevation_m),
|
||||
),
|
||||
].filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
||||
if (!elevations.length) return undefined;
|
||||
const globalMinElevation = Math.min(...elevations);
|
||||
const globalMaxElevation = Math.max(...elevations);
|
||||
const plotHeight = Math.max(longHeightPx - LONG_PAD.top - LONG_PAD.bottom, 1);
|
||||
return {
|
||||
pixelsPerMeter: plotHeight / Math.max(globalMaxElevation - globalMinElevation, 1),
|
||||
globalMinElevation,
|
||||
globalMaxElevation,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* **보이는 구간의 표고 최저·최고**(종단 그래프 세로 자동 맞춤, 2026-09-04 사용자 확정).
|
||||
*
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Section_View_Menu.ts
|
||||
* B06 종단 그래프 위 **우클릭 메뉴** — B05 와 같은 부품(`mountStructureMenu`)을 그대로 쓴다.
|
||||
*
|
||||
* 왜 여기서도 여나(2026-09-12 사용자) — B05·B06 은 한 페이지다. 같은 그래프를 보면서
|
||||
* 한쪽에서만 넣고 뺄 수 있으면 사용자가 화면을 오가야 하고 직관도 깨진다.
|
||||
* B06 종단 그래프 구조물 조작 길(넣기 · 빼기 · 옮기기) — 페이지가 물려 준다.
|
||||
*
|
||||
* 정본에 바로 쓰지 않는다 — 추가·삭제·이동은 **세션에 예약**하고 [저장]·[확정]에서
|
||||
* 관 정본(`pipe_points.json`)으로 나간다(CLAUDE.md 5장). 세부 배수유역 재분할은 그때
|
||||
* 서버가 관 목록을 다시 받아 처리한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import type { StructureInstance } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { structureAnchorM } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import type { IrregularStation } from "../B05_Profile/B05_Profile_UI_IrregularStations";
|
||||
import { mountStructureMenu } from "../B05_Profile/B05_Profile_UI_Profile_Structures";
|
||||
import { pickGroups } from "../B05_Profile/B05_Profile_UI_Structures_Panel_Types";
|
||||
import { listPick } from "../common_util/common_util_structure_catalog";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
|
||||
/** 종단 그래프에서 구조물을 넣고 빼는 길 — 페이지가 물려 준다. 없으면 메뉴를 안 붙인다. */
|
||||
@@ -87,49 +80,3 @@ function moveStructureMark(
|
||||
export function isPipeMark(structure: StructureInstance): boolean {
|
||||
return String(structure.structure_id ?? "").startsWith("pipe-");
|
||||
}
|
||||
|
||||
/**
|
||||
* 종단 그래프 칸에 우클릭 메뉴를 붙인다. 그래프는 그릴 때마다 새로 만들어지므로 이
|
||||
* 함수도 그때마다 다시 부른다 — 상태를 밖에 남기지 않는다(B05 와 같은 규칙).
|
||||
*/
|
||||
export function mountSectionStructureMenu(
|
||||
host: HTMLElement,
|
||||
input: {
|
||||
structures: ReadonlyArray<StructureInstance>;
|
||||
types: ReadonlyArray<StructureType>;
|
||||
x: (chainageM: number) => number;
|
||||
chainageAt: (px: number) => number;
|
||||
maxChainageM: number;
|
||||
edit: SectionStructureEdit;
|
||||
},
|
||||
): void {
|
||||
// 메뉴가 쓰는 최소 정보만 옮겨 담는다 — B06 에는 B05 의 비정규 측점 목록이 없다.
|
||||
const stations: IrregularStation[] = input.structures.map((structure) => {
|
||||
const chainage = structureAnchorM(structure);
|
||||
const pipe = isPipeMark(structure);
|
||||
return {
|
||||
id: String(structure.structure_id),
|
||||
station: 0,
|
||||
remainder: 0,
|
||||
chainage_m: chainage,
|
||||
structure:
|
||||
input.types.find((type) => type.type_id === structure.type_id)?.name ??
|
||||
String(structure.type_id),
|
||||
origin: pipe ? "pipe" : "user",
|
||||
} as IrregularStation;
|
||||
});
|
||||
|
||||
mountStructureMenu(host, {
|
||||
stations,
|
||||
x: input.x,
|
||||
chainageAt: input.chainageAt,
|
||||
maxChainageM: input.maxChainageM,
|
||||
onRemove: (station) => {
|
||||
if (station.origin === "pipe") input.edit.removePipe(station.chainage_m);
|
||||
else input.edit.removeStructure(station.id);
|
||||
},
|
||||
onAddPipe: (chainageM) => input.edit.addPipe(chainageM),
|
||||
structureGroups: () => pickGroups(input.types, listPick("b06")), // B06 패널 거르기와 같음(129-1 · 322-1)
|
||||
onAddStructureType: (chainageM, typeId) => input.edit.addStructureType(chainageM, typeId),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,9 +28,6 @@ export const REVET_THICKNESS_M = 0.45;
|
||||
* 못 가른 자리만 이 값. 돌쌓기 벽은 `B06_Section_UI_Cross_Lean.leanOf`(품셈 13-4-4 [주]⑪ 표준경사). */
|
||||
export const REVET_LEAN_RATIO = 0.3;
|
||||
|
||||
/** 사다리꼴(배면부) 상단 폭(m) — 띠 두께의 절반(2026-08-20 사용자 확정). */
|
||||
export const REVET_TRAP_TOP_M = REVET_THICKNESS_M / 2;
|
||||
|
||||
/** 성토 위 이음 = 벽 윗면 사면 쪽 끝에서 이만큼 안(m · 371-1). */
|
||||
export const WALL_FILL_TOP_INSET_M = 0.1;
|
||||
|
||||
@@ -122,8 +119,6 @@ export function revetFormOf(m: string | null | undefined, specForm?: string | nu
|
||||
return m || specForm || REVET_FORM_DEFAULT;
|
||||
}
|
||||
|
||||
export const REVET_MATERIALS: RevetMaterial[] = ["dry", "wet", "concrete"];
|
||||
|
||||
export function materialLabel(material: RevetMaterial): string {
|
||||
return material === "dry" ? "메쌓기" : material === "wet" ? "찰쌓기" : "콘크리트";
|
||||
}
|
||||
@@ -195,7 +190,6 @@ export const EXTRA_TIER_SLOPE_M = 1.0;
|
||||
* (2026-08-21 사용자 지시).
|
||||
*/
|
||||
export const FILL_SLOPE_RATIO_MIN = 1.2;
|
||||
export const FILL_SLOPE_RATIO_MAX = 2.0;
|
||||
|
||||
/** 성토사면 길이 한계(m) — `성토_비탈면.md` §2 "5m 이내". 초과 시 옹벽·석축 등 의무. */
|
||||
export const FILL_SLOPE_MAX_LENGTH_M = 5.0;
|
||||
@@ -221,13 +215,6 @@ export const PIPE_WALL_DEFAULT_RUN_M =
|
||||
export const pipeWallDefaultD = (adjust: { x: number }): number =>
|
||||
Math.abs(adjust.x) > 1e-9 ? PIPE_WALL_DEFAULT_RUN_M : 0;
|
||||
|
||||
/**
|
||||
* 성토 전체고(노견 − 원지반) 기준(m) — 이 높이를 넘으면 1:1.2로도 사면이 5m를 넘어
|
||||
* 구조물이 필요해진다(3m × √(1+1.2²) = 4.69m, 3.2m에서 5.0m). 기슭막이를 자연 사면
|
||||
* 끝이 아니라 **사면길이 5m 지점**으로 끌어올리는 판정에 쓴다(2026-08-21 사용자 지시).
|
||||
*/
|
||||
export const FILL_STRUCTURE_HEIGHT_M = 3.0;
|
||||
|
||||
/**
|
||||
* 성토 사면이 성립하는 **최소 성토고**(m) — 노견 − 벽 이음선 상단.
|
||||
*
|
||||
@@ -237,13 +224,6 @@ export const FILL_STRUCTURE_HEIGHT_M = 3.0;
|
||||
*/
|
||||
export const FILL_MIN_RISE_M = 0.3;
|
||||
|
||||
/**
|
||||
* 집수정 바닥 여유(m) — **관 하단 아래**로 두는 깊이. 상부 여유는 기슭막이와 같은
|
||||
* `REVET_FREEBOARD_M`(0.5)을 쓴다. 집수정 깊이 = 바닥 여유 + 관경 + 상부 여유
|
||||
* (2026-08-21 사용자 확정 — 종전에는 성토 경사선과의 교점으로 높이를 잡았다).
|
||||
*/
|
||||
export const BASIN_BOTTOM_CLEARANCE_M = 0.5;
|
||||
|
||||
/**
|
||||
* 집수정 내부 높이(m) — 바닥판이 있는 ㄴ(L)·ㄷ형의 **바닥 윗면~벽 상단** 깊이.
|
||||
* 프로젝트 기본값 1.2m(2026-08-22 사용자 확정). 외부 기준은 확인되지 않았다.
|
||||
|
||||
@@ -196,26 +196,6 @@ export function cutLength(corners: { inlet: PipeEnd; outlet: PipeEnd }): number
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 유출 벽을 그 자리에 세웠을 때의 성토 사면(물매 1:n·사면길이)을 재는 함수를 만든다.
|
||||
* 관 길이 정수 맞춤이 벽을 옮겨도 되는 자리인지 판정하는 데 쓴다.
|
||||
*/
|
||||
export function outletSlopeFactory(
|
||||
info: { edge: { offset_m: number; elevation_m: number }; outward: number },
|
||||
invertAt: (offset: number) => number,
|
||||
wallHeight: number,
|
||||
): (offset: number, thickness: number) => { ratio: number; lengthM: number } {
|
||||
return (offset, thickness) => {
|
||||
const joint = offset + info.outward * (thickness / 2);
|
||||
const rise = info.edge.elevation_m - (invertAt(offset) + wallHeight);
|
||||
const run = Math.abs(joint - info.edge.offset_m);
|
||||
return {
|
||||
ratio: rise > 1e-6 ? run / rise : Number.POSITIVE_INFINITY,
|
||||
lengthM: Math.hypot(run, Math.max(rise, 0)),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 기슭막이 **자동 자리 = 노견(노폭 연장)이 최소가 되는 지점**(2026-08-22 사용자 확정).
|
||||
* 노견 끝에서 바깥으로 훑어, 표준 높이 벽으로 ① 성토고 하한(FILL_MIN_RISE_M)과
|
||||
|
||||
@@ -286,7 +286,7 @@ def _cross_sheet_plan(project_root: Path, longitudinal_path: Path) -> list[dict[
|
||||
"""
|
||||
longitudinal = _read_json(longitudinal_path)
|
||||
chainages: list[int | float] = []
|
||||
for path in sorted(_cross_files(longitudinal_path, longitudinal)):
|
||||
for path in _cross_files(longitudinal_path, longitudinal):
|
||||
match = _CROSS_ID.fullmatch(path.stem)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
@@ -9,7 +9,7 @@ import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Sections import cross_station, valid_cross_names
|
||||
from B05_Profile.B05_Profile_Engine_Sections import cross_files_in_order, cross_station
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,12 +39,8 @@ def _cross_files(longitudinal_path: Path, longitudinal: dict[str, Any]) -> list[
|
||||
if not cross_dir.is_dir():
|
||||
raise FileNotFoundError("B06 횡단면 파일을 찾을 수 없습니다.")
|
||||
stations = longitudinal.get("stations")
|
||||
# 읽기는 지우지 않음 — 잔재는 걸러 읽기만(지우기는 측점을 쓰는 쪽 몫 · R26)
|
||||
valid_names = valid_cross_names(cross_dir, stations if isinstance(stations, list) else [])
|
||||
files = sorted(cross_dir.glob("cross_*.json"))
|
||||
if valid_names:
|
||||
files = [path for path in files if path.name in valid_names]
|
||||
return files
|
||||
# 읽기는 지우지 않음 — 잔재는 걸러 읽기만(지우기는 측점을 쓰는 쪽 몫 · R26) · 측점 차례(472-2)
|
||||
return cross_files_in_order(cross_dir, stations if isinstance(stations, list) else [])
|
||||
|
||||
|
||||
def _station_map(longitudinal: dict[str, Any]) -> dict[int | float, dict[str, Any]]:
|
||||
|
||||
@@ -204,3 +204,6 @@ export function shapeNeeds(typeId: string): readonly string[] {
|
||||
|
||||
/** 관 공식 이름의 관종들(정의 선택지) — 표시 이름이 「배수관」으로 줄일 이름을 알아봄. */
|
||||
export const pipeTitleKinds = (): readonly string[] => pipeKinds;
|
||||
|
||||
/** 배수관의 화면 표시 이름(2026-08-17 사용자 지시) — 목록 · 종 · 횡단 표기가 같이 씀. */
|
||||
export const PIPE_DISPLAY_NAME = "배수관";
|
||||
|
||||
@@ -33,7 +33,7 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md
|
||||
| P5 | 노선 바뀜 계획고 옮김 | `logic/B05_Profile_Grade_Carry.ts:188` | `B05_Profile_Engine_Grade_Carry.py:149` | — | 짝 · `test_400_profile_carry` · `test_349_0v_route_save_span` |
|
||||
| P6 | 옮긴 시설 꼭짓점 세움 | `logic/B05_Profile_Grade_Seat.ts:32` · `common_util_structure_station.ts:356` | `B05_Profile_Router_Confirm.py:220 · 306` | — | 짝 · `test_427_1_follow_seat` · `test_459_route_seat_mirror` |
|
||||
| P7 | 직선화 · 자유지정 · 쉬프트 · 시종점 | `logic/B05_Profile_UI_Profile_Straighten.ts` · `logic/design_profile_free.ts` · `logic/B05_Profile_UI_Profile_Pivot.ts` · `logic/B05_Profile_EndOffsets.ts` | — | — | 브라우저(편집값만 저장 → P2) |
|
||||
| P8 | 종단 경고(기울기 · 법정 L · 최소 R · 피복) | `B05_Profile_UI_Panel_Fields.ts` · `logic/B05_Profile_UI_Profile_Guard.ts` · `B05_Profile_UI_Profile_MinCover.ts` | `B05_Profile_Engine_Grade.py` · `B05_Profile_PageSettings.py` | — | 짝 · `test_442_2_grade_limit_mirror` |
|
||||
| P8 | 종단 경고(기울기 · 법정 L · 최소 R · 피복) | `B05_Profile_UI_Panel_Fields.ts` · `logic/B05_Profile_UI_Profile_Guard.ts` · `B05_Profile_UI_Profile_MinCover.ts` · 경고 글 `logic/B05_Profile_UI_Profile_Alignment.ts` buildAlignment | `B05_Profile_Engine_Grade.py` · `B05_Profile_PageSettings.py` · 경고 글 `B05_Profile_Engine_Grade_Alignment.py` build_alignment | — | 짝 · [저장] · [확정] = `B06_Section_Server_Calc_Prebuild.py` refresh_alignment 가 경고만 다시 씀(기하 그대로) · `test_442_2_grade_limit_mirror` · `test_472_save_warnings` |
|
||||
| C1 | 횡단 설계선 | `common_util_cross_design.ts:267` · `logic/B06_Section_Cross_Stage.ts:93` · `B06_Section_Cross_Refresh.ts:330` | `B06_Section_Engine_Design.py:195` | ex | 짝 · `test_b06_cross_design_mirror` |
|
||||
| C2 | 단면적(절 · 성 · 측구 · 층따기) | `common_util_cross_design_areas.ts` | `B06_Section_Engine_Areas.py` | sc · ex | 짝 · 정본 = 표준 측점 파이썬 · 구조물 측점 Node |
|
||||
| C3 | 확정 보정(포장 · 세월교 · 기울임) | `logic/B06_Section_Cross_Enforce.ts:40` | `B06_Section_Router_Design.py` · `B06_Section_Engine_Tilt.py` | — | 짝 · `test_349_0n_cross_enforce_mirror` |
|
||||
|
||||
@@ -32,5 +32,5 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
|
||||
| B06 폼 관 측점 · 옵션 고침 | `pipeFacilities` → 세션 `pipes` · 옮긴 자리(정수 m)로 옵션 · 캐시 얹기 · 집계 찾음 | B05 관 옮김과 같음 | 배수유역 다시 나눔 | 관 차례 `PUT /drainage/pipe-points` | 14 | `B06_Section_UI_Page_Structures_Panel.ts:300-345 · 536-560` |
|
||||
| B06 구조물 패널 | 보기 `structures` → 시설 줄 `set` → 세션 `structures` | B05 구조물과 같음 | — | `PUT /route/structures` | 11 | `B06_Section_UI_Page_Structures_Panel.ts:211-227 · 582-598` |
|
||||
| B06 평면 패널 끌기 | B05 교점 끌기와 같음(같은 편집기를 옮겨 붙임) | 같음 · B06 쪽 갈아끼움이 먼저 | 같음 | `/route/replan` | 11 | `design_plan_peek.ts:61-77` · `B05_Profile_UI_RouteEdit.ts:290-293 · 599-601` · `B06_Section_UI_Page_Station_Swap.ts:35` |
|
||||
| 넓힘(표본 폭 넘음) | 없음 — 확인창 뒤 바로 서버 | 응답 뒤 상세 강제 다시 받음 | `/sections/{노선}/regenerate` | **저장 밖** — DB 바로 씀(이미 정본) | 3 | `B06_Section_UI_Page_Common.ts:32-63` · `B06_Section_Router.py:401-411` · `B06_Section_Router_Widen.py:80` |
|
||||
| 넓힘(표본 폭 넘음) | 없음 — 확인창 뒤 바로 서버 | 응답 뒤 상세 강제 다시 받음 | `/sections/{노선}/regenerate` | **저장 밖** — DB 바로 씀(이미 정본) | 3 | `B06_Section_UI_Page_Common.ts:32-63` · `B06_Section_Router.py:378-388` · `B06_Section_Router_Widen.py:80` |
|
||||
| [측점 만들기] | 없음 — 바로 서버 | 응답 뒤 상세 비움 · 강제 다시 받음 · 알림 | `/section/missing-stations` | **저장 밖** — 종단 · 횡단 파일 바로 씀 | 3 | `B06_Section_UI_Missing_Stations.ts:86-138` · `B06_Section_Router_Stations.py:236-296 · 365-407` |
|
||||
|
||||
@@ -35,7 +35,7 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
|
||||
| alignment | `flushAlignmentDraft` | `PUT /route/profile-alignment` | 초안 노선 0 편집을 새 노선에 | `design_save.ts:185-186` |
|
||||
| culvertOptions | `flushCulvertOptions`(`culvertopt` · 옛 노선 열쇠) | `pipe_points.json` 한 번 더 PUT | 옛 노선 열쇠로 읽음 | `design_save.ts:187-188` · `B06_Section_Api_Culvert_Options.ts:67-95` |
|
||||
| uphill | `flushRouteParts` | 상단측 | 건너뜀 — 횡단 차례가 옮긴 뒤 보냄 | `design_save.ts:189-190 · 285` · `B05_Profile_Api_Fetch.ts:321-342` |
|
||||
| cross | `ctx.cross(confirm)` = 횡단 패치 한 요청 | `/sections/{노선}/save` · `confirm` | `carryAfterRouteSave`(새 측점으로 옮김) | `design_save.ts:275-283` |
|
||||
| cross | `ctx.cross(confirm)` = 횡단 패치 한 요청 | `/sections/{노선}/save` · `confirm` · 재계산 맨 앞 종단 경고 다시 씀(`B06_Section_Server_Calc_Prebuild.py` refresh_alignment) | `carryAfterRouteSave`(새 측점으로 옮김) | `design_save.ts:275-283` |
|
||||
|
||||
- 차례마다 따로 커밋 — 실패 = 그 차례에서 멈춤 · 이름째 알림 · 앞 차례는 저장된 채(`design_save.ts:297-299`).
|
||||
- 저장 중 = `saving` 표시 · 묶음 맞춤은 저장 뒤로 미룸(`design_pipeline_page.ts:567`).
|
||||
|
||||
@@ -45,7 +45,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함
|
||||
|
||||
| 내용 | 자리(파일:줄) | 상태 |
|
||||
|---|---|---|
|
||||
| 저장 밖 정본 쓰기 셋 — 넓힘 `/regenerate` · [측점 만들기] · 추천 [적용] | `B06_Section_Router.py:401-411` · `B06_Section_Router_Stations.py:236-296` · `B05_Profile_UI_RouteEdit_Apply.ts:220-245` | 사용자 확정 · 확인창 · 472-6 협의 |
|
||||
| 저장 밖 정본 쓰기 셋 — 넓힘 `/regenerate` · [측점 만들기] · 추천 [적용] | `B06_Section_Router.py:378-388` · `B06_Section_Router_Stations.py:236-296` · `B05_Profile_UI_RouteEdit_Apply.ts:220-245` | 사용자 확정 · 확인창 · 472-6 협의 |
|
||||
| [저장] 9 차례 · 차례마다 따로 커밋 — 실패 = 앞 차례만 저장 | `design_save.ts:253-299` | 472-6 |
|
||||
| `pipe_points.json` 한 [저장]에 두 번 PUT(관 · 관 옵션) | `B06_Section_Api_Culvert_Options.ts:67-95` | 코드 확인 |
|
||||
| 노선 바뀐 [저장]의 관 옵션 차례 = 옛 노선 열쇠 | `design_save.ts:187-188` | 추정 |
|
||||
|
||||
@@ -20,7 +20,6 @@ source: ["docs/raw/verification/2026-07-19_verify_B05_B06_UI_Station_Improvement
|
||||
| `GET /{project_id}/sections/detail` | `B06_Section_Router.py` | 종단 결과와 다수 횡단면을 일괄 조회하고 배수관 측점에는 `culvert` 세트 제원을 결합 |
|
||||
| `GET /{project_id}/sections/road-widths` | `B06_Section_Router.py` | 임도 등급별 법정 최소 도로폭 정보(`FOREST_ROAD_MIN_WIDTH_M`) 조회 (B05 프론트엔드가 측점 가로선 반폭 계산 시 호출) |
|
||||
| `POST /{project_id}/sections/{route_id}/regenerate` | `B06_Section_Router.py:85` | 횡단 반폭 재생성 요청 (`SectionRegenerateRequest` 매핑) |
|
||||
| `POST /{project_id}/sections/{route_id}/cross-design` | `B06_Section_Router.py` | 측점별 지반·단면·측구와 집수정 `basin_adjust`(폭·높이·좌우·상하)를 `data.design`에 저장 (`CrossDesignRequest` → `CrossDesignResponse`) |
|
||||
| `GET /{project_id}/recent-company-projects` | `B06_Section_Router.py` | 동일 회사 최근 5개 프로젝트 목록 반환 API |
|
||||
| `GET /{project_id}/standard-cross-section-from/{target_id}` | `B06_Section_Router.py` | 지정 타 프로젝트의 표준단면 설계값 로드 API |
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ last_updated: 2026-08-02
|
||||
## 2. 주요 연동 API 함수
|
||||
* `getSectionsDetail(projectId)`:
|
||||
* `GET /sections/detail` 호출. 종단면도 지반선/계획선 및 다수 횡단면 데이터를 일괄 응답받습니다.
|
||||
* `crossDesignOne(projectId, routeId, data)`:
|
||||
* `POST /sections/{routeId}/cross-design` 호출. 측점별 지반유형/단면유형/측구/암반경계선 높이 갱신 시 실시간 단면적 계산 및 저장을 수행합니다.
|
||||
* `regenerateSections(projectId, routeId, width)`:
|
||||
* `POST /sections/{routeId}/regenerate` 호출. 횡단 반폭 재생성을 비동기로 요청합니다.
|
||||
* `confirmSections(projectId, routeId, massHaul)`:
|
||||
|
||||
@@ -1850,11 +1850,56 @@
|
||||
"pipe:p35100": 7.9
|
||||
},
|
||||
"warnings": [
|
||||
"chainage 1080.0m: 인접 직선이 짧아 종단곡선 길이를 0.1m(반경 1m)로 줄였습니다.",
|
||||
"chainage 340.0m: 인접 직선이 짧아 종단곡선 길이를 9.9m(반경 26m)로 줄였습니다.",
|
||||
"chainage 351.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 25m)로 줄였습니다.",
|
||||
"chainage 360.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 231m)로 줄였습니다.",
|
||||
"종단기울기 56.60%가 기준 14.00%를 초과하는 구간이 34개 있습니다."
|
||||
"100.0m: 종단곡선 R 57m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"1000.0m: 종단곡선 R 73m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"1020.0m: 종단곡선 R 31m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"1060.0m: 종단곡선 R 25m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"1080.0m: 인접 직선이 짧아 종단곡선 길이를 0.1m(반경 1m)로 줄였습니다.",
|
||||
"1080.0m: 종단곡선 R 1m · L 0.1m — 법정 최소 R 100m · L 20m 미달",
|
||||
"140.0m: 종단곡선 R 53m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"20.0m: 종단곡선 R 205m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"200.0m: 종단곡선 R 154m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"220.0m: 종단곡선 R 285m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"240.0m: 종단곡선 R 61m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"260.0m: 종단곡선 R 24m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"280.0m: 종단곡선 R 21m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"300.0m: 종단곡선 R 196m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"320.0m: 종단곡선 R 118m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"340.0m: 인접 직선이 짧아 종단곡선 길이를 9.9m(반경 26m)로 줄였습니다.",
|
||||
"340.0m: 종단곡선 R 26m · L 9.9m — 법정 최소 R 100m · L 20m 미달",
|
||||
"351.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 25m)로 줄였습니다.",
|
||||
"351.0m: 종단곡선 R 25m · L 8.1m — 법정 최소 R 100m · L 20m 미달",
|
||||
"360.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 231m)로 줄였습니다.",
|
||||
"40.0m: 종단곡선 R 55m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"400.0m: 종단곡선 R 105m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"420.0m: 종단곡선 R 100m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"440.0m: 종단곡선 R 117m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"460.0m: 종단곡선 R 48m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"480.0m: 종단곡선 R 46m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"520.0m: 종단곡선 R 24m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"540.0m: 종단곡선 R 41m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"560.0m: 종단곡선 R 53m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"580.0m: 종단곡선 R 123m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"600.0m: 종단곡선 R 40m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"620.0m: 종단곡선 R 88m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"640.0m: 종단곡선 R 147m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"660.0m: 종단곡선 R 28m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"680.0m: 종단곡선 R 14m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"700.0m: 종단곡선 R 49m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"720.0m: 종단곡선 R 38m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"740.0m: 종단곡선 R 55m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"760.0m: 종단곡선 R 36m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"780.0m: 종단곡선 R 54m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"80.0m: 종단곡선 R 58m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"800.0m: 종단곡선 R 13m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"820.0m: 종단곡선 R 45m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"840.0m: 종단곡선 R 29m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"880.0m: 종단곡선 R 29m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"900.0m: 종단곡선 R 115m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"920.0m: 종단곡선 R 44m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"940.0m: 종단곡선 R 66m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"960.0m: 종단곡선 R 23m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"980.0m: 종단곡선 R 42m · L 15.0m — 법정 최소 R 100m · L 20m 미달"
|
||||
],
|
||||
"violations": 34
|
||||
},
|
||||
@@ -2783,6 +2828,7 @@
|
||||
"1000.0m: 종단곡선 R 73m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"1020.0m: 종단곡선 R 31m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"1060.0m: 종단곡선 R 25m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"1080.0m: 인접 직선이 짧아 종단곡선 길이를 0.1m(반경 1m)로 줄였습니다.",
|
||||
"1080.0m: 종단곡선 R 1m · L 0.1m — 법정 최소 R 100m · L 20m 미달",
|
||||
"140.0m: 종단곡선 R 53m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"20.0m: 종단곡선 R 205m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
@@ -2793,8 +2839,11 @@
|
||||
"280.0m: 종단곡선 R 21m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"300.0m: 종단곡선 R 196m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"320.0m: 종단곡선 R 118m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"340.0m: 인접 직선이 짧아 종단곡선 길이를 9.9m(반경 26m)로 줄였습니다.",
|
||||
"340.0m: 종단곡선 R 26m · L 9.9m — 법정 최소 R 100m · L 20m 미달",
|
||||
"351.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 25m)로 줄였습니다.",
|
||||
"351.0m: 종단곡선 R 25m · L 8.1m — 법정 최소 R 100m · L 20m 미달",
|
||||
"360.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 231m)로 줄였습니다.",
|
||||
"40.0m: 종단곡선 R 55m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"400.0m: 종단곡선 R 105m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"420.0m: 종단곡선 R 100m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
@@ -2824,12 +2873,7 @@
|
||||
"920.0m: 종단곡선 R 44m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"940.0m: 종단곡선 R 66m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"960.0m: 종단곡선 R 23m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"980.0m: 종단곡선 R 42m · L 15.0m — 법정 최소 R 100m · L 20m 미달",
|
||||
"chainage 1080.0m: 인접 직선이 짧아 종단곡선 길이를 0.1m(반경 1m)로 줄였습니다.",
|
||||
"chainage 340.0m: 인접 직선이 짧아 종단곡선 길이를 9.9m(반경 26m)로 줄였습니다.",
|
||||
"chainage 351.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 25m)로 줄였습니다.",
|
||||
"chainage 360.0m: 인접 직선이 짧아 종단곡선 길이를 8.1m(반경 231m)로 줄였습니다.",
|
||||
"종단기울기 56.60%가 기준 14.00%를 초과하는 구간이 34개 있습니다."
|
||||
"980.0m: 종단곡선 R 42m · L 15.0m — 법정 최소 R 100m · L 20m 미달"
|
||||
],
|
||||
"violations": 34
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ const drafts = loadTs("B06_Section/B06_Section_Section_Drafts.ts", {
|
||||
"../A00_Common/design/design_page_data": loadTs("A00_Common/design/design_page_data.ts", {
|
||||
"../../B04_PreProcess/B04_PreProcess_Api_Fetch": serverFetch,
|
||||
"../../B05_Profile/B05_Profile_Api_Structures": serverStructures,
|
||||
"../../B06_Section/B06_Section_Api_Fetch": {
|
||||
"../../B06_Section/B06_Section_Api_Context": {
|
||||
fetchSectionContext: async () => ({ route_id: 1 }),
|
||||
},
|
||||
"./design_page_route": loadTs("A00_Common/design/design_page_route.ts"), // 노선 id 한 칸(386-2b)
|
||||
|
||||
@@ -23,7 +23,7 @@ const STUBS = {
|
||||
},
|
||||
},
|
||||
// 횡단 context(세션 캐시) — 노선 id 한 칸(386-2b)
|
||||
"../../B06_Section/B06_Section_Api_Fetch": {
|
||||
"../../B06_Section/B06_Section_Api_Context": {
|
||||
fetchSectionContext: async (projectId) => ({ route_id: projectId === "p-none" ? null : 304 }),
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_Api_Structures": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""472-1 기준값 — 고정 자료(`fixtures/golden_472`) 하나로 길 넷의 값을 뜸(시험 · 값 뜨기 함께 씀).
|
||||
|
||||
stored GET 상세(정본 그대로) → 누가토량(서버 Node 묶음 `computeMassHaul`)
|
||||
server_save [저장] 서버 재계산 — 정본 상세 → `enforce_design_lines` →
|
||||
`_enforce_stored_designs` → server-calc
|
||||
server_save [저장] 서버 재계산 — 정본 상세 → 종단 경고 다시 셈(`refresh_alignment` · 472 덧) →
|
||||
`enforce_design_lines` → `_enforce_stored_designs` → server-calc
|
||||
server_route 노선 저장 체인(같은 노선 · DB 없이) — `run_section_generation`(고정 geojson · npz)
|
||||
→ `_carry_profile` 같은 노선 갈래(옛 base_pvi · 편집) → `_apply_alignment_edits` →
|
||||
측점 열쇠 이월(`carry_cross_inputs`) → `compute_default_designs` → 상세 읽기 →
|
||||
@@ -125,19 +125,21 @@ def _context() -> dict[str, Any]:
|
||||
|
||||
|
||||
def _save_tail(detail: dict[str, Any], root: Path) -> tuple[list, dict | None]:
|
||||
"""[저장] 재계산 본문(`_recompute`) — 설계선 · 넓힘 · 보정 → Node 면적 · 누가토량 얹음."""
|
||||
"""[저장] 재계산(`_recompute`) — 종단 경고 · 설계선 · 넓힘 · 보정 → Node 면적 · 누가토량."""
|
||||
from B06_Section.B06_Section_Router_Design import enforce_design_lines
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import (
|
||||
_AREA_KEYS,
|
||||
_NPM_SCRIPT,
|
||||
BUNDLE,
|
||||
_enforce_stored_designs,
|
||||
refresh_alignment,
|
||||
)
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
from common_util.common_util_surface_sampler import build_surface_sampler
|
||||
|
||||
longitudinal = detail["longitudinal"]
|
||||
sections = detail["cross_sections"]
|
||||
refresh_alignment(longitudinal)
|
||||
enforce_design_lines(longitudinal, sections, root, None)
|
||||
unclosed = any((s.get("design") or {}).get("slope_unclosed") for s in sections)
|
||||
sampler = (
|
||||
@@ -163,9 +165,9 @@ def _save_tail(detail: dict[str, Any], root: Path) -> tuple[list, dict | None]:
|
||||
|
||||
def server_save(root: Path) -> dict[str, Any]:
|
||||
detail = copy.deepcopy(read("detail.json"))
|
||||
alignment = detail["longitudinal"].get("profile_alignment") or {}
|
||||
keys = _keys(detail["longitudinal"], detail["cross_sections"], root)
|
||||
sections, haul = _save_tail(detail, root)
|
||||
alignment = detail["longitudinal"].get("profile_alignment") or {}
|
||||
return values_of(
|
||||
sections, keys, haul, alignment.get("warnings") or [], alignment.get("violations") or []
|
||||
)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/* 472 덧 종단 경고 거울 헬퍼 — `logic/B05_Profile_UI_Profile_Alignment.ts` `buildAlignment` 를 그 자리에서
|
||||
* 트랜스파일해 돌림. 입력 argv[2] JSON { base, cases: [edits] } · 표준출력 JSON [{ warnings, violations }].
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const cache = new Map();
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) =>
|
||||
request.startsWith(".")
|
||||
? loadTs(path.join(path.dirname(full), `${request}.ts`))
|
||||
: require(request);
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const align = loadTs(
|
||||
path.join(__dirname, "..", "..", "B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts"),
|
||||
);
|
||||
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
input.cases.map((edits) => {
|
||||
const built = align.buildAlignment(input.base, edits);
|
||||
return { warnings: built.warnings, violations: built.violations.length };
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -44,10 +44,15 @@ class Known:
|
||||
|
||||
|
||||
KNOWN: dict[tuple[str, str], list[Known]] = {
|
||||
(
|
||||
"server_save",
|
||||
"stored",
|
||||
): [], # [저장] 재계산 = 정본 그대로(사용자 원칙 — 입력 안 바뀌면 값 안 바뀜)
|
||||
# [저장] 재계산 = 정본 그대로(사용자 원칙 — 입력 안 바뀌면 값 안 바뀜) · 경고만 지금 코드
|
||||
("server_save", "stored"): [
|
||||
Known(
|
||||
"W0",
|
||||
{"warnings": None},
|
||||
"정본 종단 경고 = 10-17 저장 때 코드(법정 R · L 미달 46 줄 전) · "
|
||||
"[저장] 재계산이 지금 코드로 다시 씀(472 덧 · 기하 그대로)",
|
||||
),
|
||||
],
|
||||
("server_route", "stored"): [
|
||||
Known(
|
||||
"R7",
|
||||
@@ -58,7 +63,7 @@ KNOWN: dict[tuple[str, str], list[Known]] = {
|
||||
"W0",
|
||||
{"warnings": None},
|
||||
"정본 종단 경고 = 10-17 저장 때 코드(법정 R · L 미달 46 줄 전) · "
|
||||
"[저장] 재계산은 종단 경고를 안 씀",
|
||||
"노선 저장 체인은 지금 코드",
|
||||
),
|
||||
],
|
||||
("browser", "server_route"): [
|
||||
@@ -76,12 +81,6 @@ KNOWN: dict[tuple[str, str], list[Known]] = {
|
||||
"측점 자리 ≤ 1 mm → 표본 · 면적 ≤ 0.07 ㎡ · 층따기 밑수 ≤ 0.52 m · "
|
||||
"누가토량 끝값 0.69 ㎥(−17,570.01 ↔ −17,570.70)",
|
||||
),
|
||||
Known(
|
||||
"W1",
|
||||
{"warnings": None},
|
||||
"종단 경고 글 두 벌(X5 · P8) — TS 「340.0m: …」 · 파이썬 「chainage 340.0m: …」 · "
|
||||
"최대 기울기 총괄 줄은 파이썬만",
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""472-2 덧 — 횡단 파일을 측점(누가거리) 차례로 읽는지.
|
||||
|
||||
이름 차례면 `cross_01080.16m` 이 `cross_01080m` 앞(「.」 < 「m」) → GET 상세의 측점 차례가
|
||||
어긋나 화면 차례 짝(detailCross)이 갈리고, B07 측점 도면 장 나눔도 차례가 뒤집힘.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B06_Section.B06_Section_Router import _read_section_detail # noqa: E402
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _cross_files # noqa: E402
|
||||
|
||||
CHAINAGES = [1060.0, 1080.0, 1080.16, 1100.0, 999.5]
|
||||
NAMES = {
|
||||
1060.0: "cross_01060m.json",
|
||||
1080.0: "cross_01080m.json",
|
||||
1080.16: "cross_01080.16m.json",
|
||||
1100.0: "cross_01100m.json",
|
||||
999.5: "cross_00999.50m.json",
|
||||
}
|
||||
|
||||
|
||||
def _stage(tmp_path: Path) -> Path:
|
||||
stage = tmp_path / "B05_Profile"
|
||||
(stage / "cross_sections").mkdir(parents=True)
|
||||
(stage / "longitudinal").mkdir()
|
||||
longitudinal = stage / "longitudinal" / "longitudinal.json"
|
||||
longitudinal.write_text(
|
||||
json.dumps({"stations": [{"chainage_m": m} for m in CHAINAGES]}), encoding="utf-8"
|
||||
)
|
||||
for chainage, name in NAMES.items():
|
||||
(stage / "cross_sections" / name).write_text(
|
||||
json.dumps({"chainage_m": chainage, "samples": []}), encoding="utf-8"
|
||||
)
|
||||
return longitudinal
|
||||
|
||||
|
||||
def test_상세_GET_은_측점_차례(tmp_path: Path) -> None:
|
||||
longitudinal = _stage(tmp_path)
|
||||
detail = _read_section_detail(tmp_path, str(longitudinal.relative_to(tmp_path)))
|
||||
got = [section["chainage_m"] for section in detail["cross_sections"]]
|
||||
assert got == sorted(CHAINAGES)
|
||||
|
||||
|
||||
def test_B07_측점_파일도_측점_차례(tmp_path: Path) -> None:
|
||||
longitudinal = _stage(tmp_path)
|
||||
files = _cross_files(longitudinal, json.loads(longitudinal.read_text(encoding="utf-8")))
|
||||
assert [path.name for path in files] == [NAMES[m] for m in sorted(CHAINAGES)]
|
||||
|
||||
|
||||
def test_B07_장_나눔도_측점_차례() -> None:
|
||||
"""장 나눔 쪽이 이름으로 다시 줄 세우지 않음(`sorted(_cross_files(…))` = 이름 차례)."""
|
||||
source = (ROOT / "B07_DesignDetail" / "B07_DesignDetail_Router_Support.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "sorted(_cross_files(" not in source
|
||||
@@ -54,9 +54,6 @@ SESSION_PULLS = (
|
||||
"A00_Common/design/design_pick_match.ts",
|
||||
"A00_Common/design/design_pipe_ids.ts",
|
||||
"A00_Common/design/design_selection.ts",
|
||||
"A00_Common/design/design_store.ts",
|
||||
"B05_Profile/B05_Profile_UI_IrregularStations.ts",
|
||||
"B05_Profile/B05_Profile_UI_Profile_History.ts",
|
||||
"B06_Section/B06_Section_Api_Fetch.ts",
|
||||
"B06_Section/B06_Section_UI_Section_Common.ts",
|
||||
"config/config_frontend.ts",
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""PLAN 472 덧 (요구 272) — [저장] · [확정] 재계산이 종단 경고를 저장 입력으로 다시 씀.
|
||||
|
||||
증상 — 검증_랩탑메인 정본 종단 경고 5 줄(10-17 옛 코드) · 지금 코드로 셈하면 50 줄 ·
|
||||
[저장] · [확정] 재계산(`B06_Section_Server_Calc_Prebuild._recompute`)이 경고를 다시 안 셈.
|
||||
고침 — `_recompute` 맨 앞 `_refresh_alignment_file` → `refresh_alignment`(저장 base_pvi · 편집 ·
|
||||
정책 · 기준 → `rebuild_alignment_profile` 의 경고만) · 기하는 저장값 그대로 · 같으면 안 씀.
|
||||
경고 글 = TS `buildAlignment` 와 한 벌(거울). 고정 자료 = `fixtures/golden_472/detail.json`.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_save_warnings.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Grade import ground_profile # noqa: E402
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import chainage_key # noqa: E402
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile # noqa: E402
|
||||
from B06_Section import B06_Section_Server_Calc_Prebuild as prebuild # noqa: E402
|
||||
|
||||
FIXTURE = HERE / "fixtures" / "golden_472"
|
||||
DETAIL = json.loads((FIXTURE / "detail.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _longitudinal() -> dict:
|
||||
return copy.deepcopy(DETAIL["longitudinal"])
|
||||
|
||||
|
||||
def test_정본_경고는_옛_코드_5줄() -> None:
|
||||
assert len(DETAIL["longitudinal"]["profile_alignment"]["warnings"]) == 5
|
||||
|
||||
|
||||
def test_다시_셈_경고만_새로_기하는_그대로() -> None:
|
||||
longitudinal = _longitudinal()
|
||||
before = copy.deepcopy(longitudinal)
|
||||
summary = prebuild.refresh_alignment(longitudinal)
|
||||
warnings = longitudinal["profile_alignment"]["warnings"]
|
||||
assert len(warnings) == 50
|
||||
assert sum("법정 최소 R" in w for w in warnings) == 46 # 432 법정 R · L 미달
|
||||
assert not [w for w in warnings if w.startswith("chainage ")]
|
||||
assert summary is not None and summary["warnings"] == warnings
|
||||
assert summary["id"] == before["design_profiles"][0]["id"]
|
||||
assert longitudinal["design_profiles"][0]["summary"]["warnings"] == warnings
|
||||
# 경고 밖 = 저장값 그대로(계획고 · 곡선 · 표본 · 위반 · 정책 — 6 자리 반올림 끝자리도)
|
||||
longitudinal["profile_alignment"]["warnings"] = before["profile_alignment"]["warnings"]
|
||||
longitudinal["design_profiles"][0]["summary"]["warnings"] = before["design_profiles"][0][
|
||||
"summary"
|
||||
]["warnings"]
|
||||
assert longitudinal == before
|
||||
|
||||
|
||||
def test_선형_없는_옛_종단은_그대로() -> None:
|
||||
longitudinal = _longitudinal()
|
||||
longitudinal["profile_alignment"].pop("base_pvi")
|
||||
keep = copy.deepcopy(longitudinal)
|
||||
assert prebuild.refresh_alignment(longitudinal) is None
|
||||
assert longitudinal == keep
|
||||
|
||||
|
||||
def test_파일_다시_씀_같으면_안_씀(tmp_path) -> None:
|
||||
path = tmp_path / "B06_Section" / "longitudinal" / "longitudinal.json"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(json.dumps(_longitudinal()), encoding="utf-8")
|
||||
relative = "B06_Section/longitudinal/longitudinal.json"
|
||||
first = prebuild._refresh_alignment_file(tmp_path, relative)
|
||||
stored = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert first is not None and len(stored["profile_alignment"]["warnings"]) == 50
|
||||
written = path.stat().st_mtime_ns
|
||||
assert prebuild._refresh_alignment_file(tmp_path, relative) == first
|
||||
assert path.stat().st_mtime_ns == written # 같은 값 — 다시 안 씀
|
||||
assert prebuild._refresh_alignment_file(tmp_path, "없음/longitudinal.json") is None
|
||||
|
||||
|
||||
def test_재계산_맨_앞에서_선형_다시_셈() -> None:
|
||||
source = inspect.getsource(prebuild._recompute)
|
||||
assert "_refresh_alignment_file" in source and "update_longitudinal_grade_summary" in source
|
||||
assert source.index("_refresh_alignment_file") < source.index("get_section_detail(")
|
||||
|
||||
|
||||
def test_TS_거울_같은_경고_목록(tmp_path) -> None:
|
||||
longitudinal = _longitudinal()
|
||||
alignment = longitudinal["profile_alignment"]
|
||||
# 옛 정책엔 법정 R · L 이 없음 — 두 쪽 다 기준으로 채운 정책(433-13 · TS `readAlignment`)
|
||||
policy = rebuild_alignment_profile(longitudinal, alignment["edits"])[0]["policy"]
|
||||
assert (
|
||||
policy["min_vertical_radius_m"] == 100.0
|
||||
and "min_vertical_radius_m" not in alignment["policy"]
|
||||
)
|
||||
chainage, ground = ground_profile(longitudinal)
|
||||
curve = alignment["curves"][1]
|
||||
station = longitudinal["stations"][5]
|
||||
cases = [
|
||||
alignment["edits"],
|
||||
{"station_offsets": {}, "curve_radii": {chainage_key(curve["chainage_m"]): 500.0}},
|
||||
{"station_offsets": {chainage_key(station["chainage_m"]): 3.0}, "curve_radii": {}},
|
||||
]
|
||||
base = {
|
||||
"policy": policy,
|
||||
"onlyExplicitCurves": bool(alignment.get("only_explicit_curves")),
|
||||
"basePvi": alignment["base_pvi"],
|
||||
"chainage": chainage.tolist(),
|
||||
"ground": ground.tolist(),
|
||||
"stations": [
|
||||
{"station_id": s.get("station_id"), "chainage_m": s["chainage_m"]}
|
||||
for s in longitudinal["stations"]
|
||||
],
|
||||
}
|
||||
source = tmp_path / "in.json"
|
||||
source.write_text(json.dumps({"base": base, "cases": cases}), encoding="utf-8")
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_472_save_warnings.cjs"), str(source)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
for edits, got in zip(cases, json.loads(proc.stdout), strict=True):
|
||||
py, _profile = rebuild_alignment_profile(copy.deepcopy(longitudinal), edits)
|
||||
assert got["warnings"] == py["warnings"]
|
||||
assert got["violations"] == len(py["violations"])
|
||||
@@ -20,7 +20,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
|
||||
from B06_Section.B06_Section_Schema import SectionContextResponse # noqa: E402
|
||||
|
||||
_FETCH = PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Fetch.ts"
|
||||
_FETCH = PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Context.ts" # 472-2 세션 창구 갈라냄
|
||||
_ROUTER = PROJECT_ROOT / "B06_Section" / "B06_Section_Router.py"
|
||||
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ def test_확정_payload_도_손나열이_없다():
|
||||
|
||||
|
||||
def test_서버_목록이_한_벌이다():
|
||||
"""라우터가 목록을 다시 적지 않고 그 한 벌을 쓴다."""
|
||||
"""라우터가 목록을 다시 적지 않는다(목록을 쓰던 `cross-design` 엔드포인트는 472-2 에서 지움)."""
|
||||
router = (ROOT / "B06_Section" / "B06_Section_Router.py").read_text(encoding="utf-8")
|
||||
assert "USER_TOUCHED_KEYS" in router
|
||||
assert router.count('"revet_follow_grade"') == 0, "라우터에 목록 사본이 남아 있음"
|
||||
|
||||
@@ -211,7 +211,9 @@ def test_저장된_표준단면이_브라우저까지_간다():
|
||||
from B06_Section.B06_Section_Schema import SectionContextResponse
|
||||
|
||||
assert "stored_standard_cross_section" in SectionContextResponse.model_fields
|
||||
fetch = (PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Fetch.ts").read_text(encoding="utf-8")
|
||||
fetch = (PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Context.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "function seedStandardCross(" in fetch
|
||||
assert fetch.count("seedStandardCross(projectId,") >= 2, "한쪽 갈래만 세움"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user