Merge remote-tracking branch 'origin/dev' into sub_laptop_3
This commit is contained in:
@@ -16,6 +16,7 @@ import { el } from "@ui/ui_template_elements";
|
||||
import { createInputField, createSelectField } from "@ui/ui_template_input";
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { formatLineMoney, formatMoney } from "../common_util/common_util_money";
|
||||
import "./calc_sheet.css";
|
||||
|
||||
export interface SheetLine {
|
||||
@@ -29,6 +30,8 @@ export interface SheetLine {
|
||||
장비?: boolean | null;
|
||||
기준단위?: string | null;
|
||||
환산근거?: string;
|
||||
/** (753-1) 옵션으로 빠진 줄 — 그리지 않음(소계 · 쓰는 입력 칸은 그대로) */
|
||||
옵션꺼짐?: boolean;
|
||||
/** 714-1b 장비 줄 펼침 — 부른 기계경비 로직의 시간당 줄(조종원 · 주연료 · 잡재료비 · 기계손료) */
|
||||
펼침?: SheetUnfold | null;
|
||||
}
|
||||
@@ -146,7 +149,7 @@ const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 |
|
||||
const bundleName = (b: string): string => (b in TEXT ? t(b as keyof typeof TEXT) : b);
|
||||
|
||||
const won = (v: number | null, digits = 0): string =>
|
||||
v === null ? "-" : v.toLocaleString("ko-KR", { maximumFractionDigits: digits });
|
||||
v === null ? "-" : digits ? formatLineMoney(v) : formatMoney(v);
|
||||
|
||||
const out = (v: number | null, key = false, digits = 0): HTMLElement =>
|
||||
el("span", { className: `ui-calc-row__out${key ? " is-key" : ""}`, text: won(v, digits) });
|
||||
@@ -270,6 +273,10 @@ function headRows(
|
||||
const subtotal = (g: SheetGroup): HTMLElement =>
|
||||
createCalcFormulaRow(`${g.비목}${g.끝수 ? ` (${g.끝수})` : ""}`, null, out(g.소계, true));
|
||||
|
||||
/** 옵션으로 빠진 줄(753-1)은 안 그림 — 장비 · 하위 몫 줄은 몫이 다 빠졌을 때만 */
|
||||
const shown = (l: SheetLine | SheetMerged): boolean =>
|
||||
"몫" in l ? l.몫.some((x) => !x.줄.옵션꺼짐) : !l.옵션꺼짐;
|
||||
|
||||
/** 계산서 줄들 — 부르는 쪽 세로 칸(flex column)에 그대로 붙임 */
|
||||
export function calcSheetRows(o: SheetOptions): HTMLElement[] {
|
||||
const rows: HTMLElement[] = [];
|
||||
@@ -278,7 +285,8 @@ export function calcSheetRows(o: SheetOptions): HTMLElement[] {
|
||||
for (const b of bundleLines(o.묶음)) {
|
||||
const name = bundleName(b.묶음);
|
||||
rows.push(...headRows(name, b.쓰는입력, o, specs, b.기준단위, b.환산근거));
|
||||
for (const line of b.줄) rows.push(...("몫" in line ? mergedRows(line) : [lineRow(line)]));
|
||||
for (const line of b.줄.filter(shown))
|
||||
rows.push(...("몫" in line ? mergedRows(line) : [lineRow(line)]));
|
||||
rows.push(createCalcFormulaRow(name, null, out(b.소계, true, 1)));
|
||||
}
|
||||
rows.push(...o.묶음.map(subtotal)); // 맨 아래 비목 소계(서버 끝수 그대로) — 장비 노무는 여기서 인력 노무와 더해짐
|
||||
@@ -286,7 +294,7 @@ export function calcSheetRows(o: SheetOptions): HTMLElement[] {
|
||||
for (const g of o.묶음) {
|
||||
const uses = [...new Set(g.줄.flatMap((l) => l.쓰는입력 ?? []))];
|
||||
rows.push(...headRows(g.비목, uses, o, specs));
|
||||
rows.push(...g.줄.map((line) => lineRow(line)), subtotal(g));
|
||||
rows.push(...g.줄.filter(shown).map((line) => lineRow(line)), subtotal(g));
|
||||
}
|
||||
}
|
||||
rows.push(createCalcFormulaRow(o.sumLabel, null, out(o.계, true)));
|
||||
|
||||
@@ -52,6 +52,22 @@ import { designSignature, markDesignClean } from "./design_leave";
|
||||
import { routeGradeTouch } from "./design_pipeline_page";
|
||||
import type { GradeTouch } from "../../B05_Profile/logic/B05_Profile_Pipeline_Grade";
|
||||
|
||||
/** [저장] · [확정] 막음(755-3) — 글을 돌려주면 알림하고 안 냄 · `root` 가 화면에서 빠지면 풂. */
|
||||
const saveHolds: Array<{ root: HTMLElement; hold: () => string | null }> = [];
|
||||
export function holdDesignSave(root: HTMLElement, hold: () => string | null): void {
|
||||
saveHolds.push({ root, hold });
|
||||
}
|
||||
function heldSave(): string | null {
|
||||
for (let index = saveHolds.length - 1; index >= 0; index -= 1) {
|
||||
if (!saveHolds[index].root.isConnected) saveHolds.splice(index, 1);
|
||||
}
|
||||
for (const { hold } of saveHolds) {
|
||||
const why = hold();
|
||||
if (why) return why;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface DesignSaveContext {
|
||||
projectId: string;
|
||||
routeId: () => number | null;
|
||||
@@ -249,6 +265,8 @@ export async function saveDesign(
|
||||
const confirm = options.confirm ?? false;
|
||||
const routeId = ctx.routeId();
|
||||
if (routeId === null) return false;
|
||||
const held = heldSave();
|
||||
if (held) return (showToast(held, "warning"), false);
|
||||
saving.add(ctx.projectId);
|
||||
let locked = confirm;
|
||||
if (locked) showLoadingOverlay();
|
||||
|
||||
@@ -122,6 +122,23 @@ def extract_contours_from_grid(
|
||||
return contours_geojson_list
|
||||
|
||||
|
||||
def contour_cache_is_stale(contour_path: Path, model_path: Path) -> bool:
|
||||
"""등고선 캐시가 없거나 그 면을 낳은 파일보다 옛것이면 True(754-1).
|
||||
|
||||
낳은 파일 = 모델 npz · 같은 필터 `dtm_{필터}.npz`(외곽 자르기) · implicit 굳힌 격자.
|
||||
"""
|
||||
stem = Path(model_path).stem.removesuffix("_smooth")
|
||||
folder = Path(model_path).parent
|
||||
sources = [Path(model_path), folder / f"dtm_{stem.split('_', 1)[-1]}.npz"]
|
||||
if stem.startswith("implicit_"):
|
||||
sources.append(folder / f"{stem}_grid.npz")
|
||||
try:
|
||||
built = Path(contour_path).stat().st_mtime_ns
|
||||
except OSError:
|
||||
return True
|
||||
return any(path.is_file() and path.stat().st_mtime_ns > built for path in sources)
|
||||
|
||||
|
||||
def _load_footprint_mask(
|
||||
model_npz_path: Path, x_coords: np.ndarray, y_coords: np.ndarray
|
||||
) -> np.ndarray | None:
|
||||
@@ -137,10 +154,10 @@ def _load_footprint_mask(
|
||||
if not dtm_path.exists():
|
||||
return None
|
||||
try:
|
||||
d = np.load(dtm_path)
|
||||
dtm_x = np.asarray(d["x"]).ravel()
|
||||
dtm_y = np.asarray(d["y"]).ravel()
|
||||
dtm_mask = np.asarray(d["valid_mask"], dtype=bool)
|
||||
with np.load(dtm_path) as d:
|
||||
dtm_x = np.asarray(d["x"]).ravel()
|
||||
dtm_y = np.asarray(d["y"]).ravel()
|
||||
dtm_mask = np.asarray(d["valid_mask"], dtype=bool)
|
||||
except Exception:
|
||||
return None
|
||||
if len(dtm_x) < 2 or len(dtm_y) < 2:
|
||||
@@ -360,7 +377,21 @@ def extract_contours(
|
||||
model_npz_path = Path(model_npz_path)
|
||||
if not model_npz_path.exists():
|
||||
raise FileNotFoundError(f"모델 파일이 존재하지 않습니다: {model_npz_path}")
|
||||
data = np.load(model_npz_path)
|
||||
# 실패해도 모델 파일을 닫음 — 열린 채 남으면 Windows 에서 다시 짓기(교체)가 막힘(754-1)
|
||||
with np.load(model_npz_path) as data:
|
||||
return _extract_loaded(
|
||||
data, model_npz_path, representation, interval, target_grid_m, scene_center
|
||||
)
|
||||
|
||||
|
||||
def _extract_loaded(
|
||||
data: Any,
|
||||
model_npz_path: Path,
|
||||
representation: str,
|
||||
interval: float,
|
||||
target_grid_m: float,
|
||||
scene_center: tuple[float, float, float] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
|
||||
if representation == "regular_grid":
|
||||
x_coords, y_coords, z_grid, valid_mask = (
|
||||
|
||||
@@ -16,6 +16,7 @@ import numpy as np
|
||||
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Contour import (
|
||||
CONTOUR_EXTRACTOR_VERSION,
|
||||
contour_cache_is_stale,
|
||||
extract_contours,
|
||||
)
|
||||
from B04_PreProcess.B04_PreProcess_Engine_ModelBuild import BUILDERS
|
||||
@@ -191,11 +192,9 @@ def _build_all_terrain_models(
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
manifest_path = output_dir / "manifest.json"
|
||||
filters = tuple(key for key in config["source_filters"] if key in ground_masks)
|
||||
methods_list = [key for key in config["precompute"] if key in BUILDERS]
|
||||
if "dtm" in methods_list:
|
||||
methods_list.remove("dtm")
|
||||
methods_list.insert(0, "dtm")
|
||||
methods = tuple(methods_list)
|
||||
# dtm 은 늘 맨 앞 — 같은 필터 외곽 · implicit 굳힌 격자 · 등고선 자르기의 바탕이라
|
||||
# 어느 표현을 지어도 낡았으면(manifest 항목 없음 · 지문 다름) 같이 다시 지음(754-1).
|
||||
methods = ("dtm", *(key for key in config["precompute"] if key in BUILDERS and key != "dtm"))
|
||||
# 무거운 implicit 은 기본 필터(첫 필터)에만 — 분류 LAS 의 csf 까지 두 번 짓지 않음(요구 558).
|
||||
later_methods = tuple(method for method in methods if method != "implicit")
|
||||
signature = config_signature(config)
|
||||
@@ -244,12 +243,14 @@ def _build_all_terrain_models(
|
||||
if not force and _cache_is_valid(output_dir, stem, method, entry, config):
|
||||
interval = float(config.get("contour_interval_meters", 1.0))
|
||||
contour_path = output_dir / f"contour_{filter_key}_{method}_{interval}m.json"
|
||||
needs_contours = not contour_path.exists()
|
||||
needs_contours = contour_cache_is_stale(contour_path, output_dir / f"{stem}.npz")
|
||||
if method in config.get("smoothing_methods", ("dtm", "tin")):
|
||||
smooth_contour_path = (
|
||||
output_dir / f"contour_{filter_key}_{method}_smooth_{interval}m.json"
|
||||
)
|
||||
needs_contours = needs_contours or not smooth_contour_path.exists()
|
||||
needs_contours = needs_contours or contour_cache_is_stale(
|
||||
smooth_contour_path, output_dir / f"{stem}_smooth.npz"
|
||||
)
|
||||
if needs_contours:
|
||||
_cache_contours_safely(
|
||||
output_dir,
|
||||
|
||||
@@ -16,6 +16,7 @@ from fastapi.responses import JSONResponse, Response
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Contour import (
|
||||
CONTOUR_EXTRACTOR_VERSION,
|
||||
contour_cache_is_stale,
|
||||
extract_contours,
|
||||
)
|
||||
from common_util.common_util_atomic import atomic_write_bytes
|
||||
@@ -37,10 +38,10 @@ MODEL_REPRESENTATIONS = {
|
||||
|
||||
|
||||
def _is_contour_cache_current(contour_path: Path, model_path: Path) -> bool:
|
||||
"""캐시가 현재 추출기 버전이고 기반 모델 npz보다 최신인지 검사한다."""
|
||||
"""캐시가 현재 추출기 버전이고 모델 · 같은 필터 dtm · 굳힌 격자보다 최신인지 검사한다."""
|
||||
if contour_cache_is_stale(contour_path, model_path):
|
||||
return False
|
||||
try:
|
||||
if contour_path.stat().st_mtime < model_path.stat().st_mtime:
|
||||
return False
|
||||
with contour_path.open("rb") as cache_file:
|
||||
head = cache_file.read(256).decode("utf-8", errors="ignore")
|
||||
except OSError:
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
계산 자리 ③ 서버 단독 · 읽기만(파일 안 만듦). 원본 = 올린(받은) 노선 파일 꼭짓점을 사업지 좌표로 ·
|
||||
사용 범위(`projects.route_start_m · route_end_m`) · 지표면 트림까지 — 체인 예정노선
|
||||
(`load_design_route`)과 같은 차례에서 **조밀화만 뺌**. 고친 꼭짓점은 `POST /route/follow` 가
|
||||
(`load_design_route`)과 같은 차례에서 **조밀화만 뺌**. 끝 = 체인이 자른 `expected_route.csv`
|
||||
양끝(755-3 · 지표면이 그 뒤 넓어져도 화면 예상노선 · 저장 csv 와 같은 길이).
|
||||
고친 꼭짓점은 `POST /route/follow` 가
|
||||
같은 간격(`spacing_m`)으로 조밀화한 뒤 추적 → 안 고친 원본 = 체인 초기선과 거리 0(`test_635_2`).
|
||||
|
||||
GET /api/projects/{id}/route/plan/source
|
||||
@@ -13,15 +15,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
from shapely.geometry import LineString, Point
|
||||
|
||||
from B05_Profile.B05_Profile_Router_Replan import _PROJECT_PATH_MISSING, _project_paths
|
||||
from common_util.common_util_route_geometry import load_design_route, trim_route_to_surface
|
||||
from common_util.common_util_route_geometry import (
|
||||
clip_route_by_chainage,
|
||||
expected_route_csv_path,
|
||||
load_design_route,
|
||||
read_planned_route_csv,
|
||||
trim_route_to_surface,
|
||||
)
|
||||
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
||||
from common_util.common_util_surface_sampler import build_surface_sampler
|
||||
from config.config_db import get_db_pool
|
||||
@@ -57,9 +67,23 @@ def source_vertices(
|
||||
sampler = None
|
||||
if sampler is not None:
|
||||
points = trim_route_to_surface(points, sampler)
|
||||
points = clip_to_expected(root, points)
|
||||
return [[float(x), float(y)] for x, y in points] if len(points) >= 2 else []
|
||||
|
||||
|
||||
def clip_to_expected(root: Path, points: list[tuple[float, float]]) -> list[tuple[float, float]]:
|
||||
"""체인이 자른 예정노선 파일 양끝에서 자름(755-3) — 파일 없음 · 2점 미만 = 그대로."""
|
||||
path = expected_route_csv_path(root)
|
||||
stored = read_planned_route_csv(path) if path.is_file() else None
|
||||
if stored is None or len(stored.vertices) < 2 or len(points) < 2:
|
||||
return points
|
||||
line = LineString(points)
|
||||
ends = [line.project(Point(v.x, v.y)) for v in (stored.vertices[0], stored.vertices[-1])]
|
||||
clipped = clip_route_by_chainage(points, min(ends), max(ends))
|
||||
# 파일 좌표 반올림(0.1 mm) = 끝이 꼭짓점 바로 앞에 떨어짐 — 1 mm 안 겹친 점은 하나만
|
||||
return [p for i, p in enumerate(clipped) if not i or math.dist(p, clipped[i - 1]) > 1e-3]
|
||||
|
||||
|
||||
@router.get("/{project_id}/route/plan/source", response_model=None)
|
||||
async def read_plan_source(project_id: UUID) -> dict[str, Any] | JSONResponse:
|
||||
paths = await _project_paths(project_id)
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_ExpectedDraw.ts
|
||||
* B05 평면 예정노선 수정(PLAN 626-3 · 635-3 · 요구 530 · 535-1 · 536 · 546) — 예정노선 키트(`createPlanKit` 「b05」)를 엮음.
|
||||
* 왼쪽 칸 = 키트 「위치」(주소 · 장소 찾기 → 지도 이동만) · 「그리기」(머리 「예정노선 수정」 켬끔 · 받은 노선 켬끔 ·
|
||||
* 길이 비교 · [받은 노선으로]) — 자리 = `B05_Profile_UI_Panel_Plan`. 숨김 = 기능 표 「b05」(`D03_PlanAnalysis_UI_Features`).
|
||||
* B05 평면 예정노선 수정(PLAN 626-3 · 635-3 · 755 · 요구 530 · 535-1 · 536 · 546 · 589) — 예정노선 키트(`createPlanKit` 「b05」)를 엮음.
|
||||
* 왼쪽 칸 = 키트 「그리기」 하나(머리 「예정노선 수정」 켬끔 · 받은 노선 켬끔 · 길이 비교 · [받은 노선으로]) —
|
||||
* 자리 = `B05_Profile_UI_Panel_Plan`. 숨김 = 기능 표 「b05」(`D03_PlanAnalysis_UI_Features` · 위치 칸 숨김).
|
||||
* 켬 = 지도 누름 · 키를 먼저 받음(키트 `bindPlanInput` capture) · 다른 계획노선 그림 어둡게(`--draw-map-dim` · 장면 `overAll.dim`) ·
|
||||
* 가운데 · 오른쪽 끌기(지도 이동) · 휠은 그대로. 끔 = 지금 B05 평면 편집 그대로(예정노선은 보기만).
|
||||
* 고칠 선 = 받은 원본 꼭짓점(`GET /route/plan/source` · 635-2) — 고친 뒤엔 고친 꼭짓점.
|
||||
* 한 번 고침(긋기 끝 · 놓음 · 지움) → (켠 뒤 처음만) 확인창 → `POST /route/follow`(저장 0) → `onFollow(평면, 예정노선)` =
|
||||
* 편집기 한 걸음(되돌리기 됨) → 페이지 [저장] = 손 편집과 같은 길 + 초안 `expected`(`expected_route_edit.csv`).
|
||||
* 켠 동안(755) = 판만 고침 · 끊김 허용(가운데 묶음 지움) · 되돌리기 = 이 판 사진첩(Ctrl+Z · 도구줄 단추) ·
|
||||
* 재기 켬 = D03 재기 겹(고치는 판 따라 · 두 점 곧은 거리 · `createPlanMeasure`).
|
||||
* 끌 때 한 번 = 끊겨 있으면 알림하고 안 끔 · 바뀌었으면 확인창 → `POST /route/follow`(저장 0 · 폴리라인 두 단계) →
|
||||
* `onFollow(평면, 예정노선)` = 편집기 한 걸음 → 페이지 [저장] = 손 편집과 같은 길 + 초안 `expected`(`expected_route_edit.csv`).
|
||||
* [받은 노선으로] = 받은 꼭짓점으로 다시 추적 · 초안 `expected` = [](고친 예정노선 지움) — 한 걸음 · 페이지 [초기화] 는 그대로.
|
||||
* 켠 채 판을 고쳤으면(755-3) = [저장] · [확정] 막고 알림(`holdDesignSave`) · 떠남 = 공용 떠남 물음 · 탭 닫기 = 브라우저 물음.
|
||||
* 예정노선 세 벌 = 받은 노선(`expected_route.csv` · 안 덮음 · 고쳤으면 옅은 점선) · 저장본(`expected_edited`) · 이번 고침(`edit`).
|
||||
* 같은 예정노선 다른 세트(635-4) = 흐린 겹 보기만(`B05_Profile_UI_PlanContext`) · 그 끝 = 긋기 · 끌기 붙을 자리 · 벗어나면 안내 한 줄.
|
||||
* ========================================================================== */
|
||||
|
||||
import { keyIs } from "../A00_Common/key_registry";
|
||||
import { guardProjectLeave } from "../A00_Common/b_project_switch";
|
||||
import { holdDesignSave } from "../A00_Common/design/design_save";
|
||||
import { listenWhileAlive } from "@ui/ui_template_window_watch";
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { askConfirm } from "@ui/ui_template_modal";
|
||||
@@ -21,12 +26,12 @@ import { createText } from "@ui/ui_template_text";
|
||||
import { createToggle } from "@ui/ui_template_toggle";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import type { ViewState } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { heightAt as gridHeightAt } from "../common_util/common_util_surface_sampler";
|
||||
import type { PlanNetwork } from "../D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan";
|
||||
import { strokeCased } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint";
|
||||
import { bindPlanInput, createPlanKit } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Kit";
|
||||
import type { ScreenMap } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map";
|
||||
import { createPlaceFind } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Site";
|
||||
import type { PlanMapLayer, ScreenMap } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map";
|
||||
import { createPlanMeasure } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Measure";
|
||||
import { requestJson, type RoutePlanResponse } from "./B05_Profile_Api_Replan";
|
||||
import { loadDesignTerrainGrid, type DesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
|
||||
import { createPlanCoords, fillPlanPanel } from "./B05_Profile_UI_Panel_Plan";
|
||||
@@ -39,10 +44,9 @@ import { flattenServerPlan, type FlattenedPlan } from "./logic/B05_Profile_UI_Ro
|
||||
|
||||
type Vertex = [number, number];
|
||||
|
||||
const HINT =
|
||||
"[긋기] · 우클릭 「여기서 긋기 시작」 · 선 끝에서 이어 긋기 · 고르고 Delete · 노드 끌기 — 한 번 고칠 때마다 평면선을 다시 추적합니다.";
|
||||
const EMPTY_HINT = "예정노선이 없습니다 — 빈 땅 우클릭 「여기서 긋기 시작」으로 새로 긋습니다.";
|
||||
const OFF_HINT = "「예정노선 수정」을 켜면 지도에서 예정노선을 고칩니다.";
|
||||
const HELD =
|
||||
"예정노선 수정 중 — 「예정노선 수정」을 끈 뒤 [저장]합니다(켠 동안 고친 판은 끌 때 평면선에 들어감).";
|
||||
|
||||
/** `POST /route/follow` 응답 — GET `/route/plan` 칸 이름(626-2). */
|
||||
type FollowResponse = Pick<RoutePlanResponse, "planned" | "nodes" | "curves" | "node_edits">;
|
||||
@@ -58,11 +62,13 @@ export interface ExpectedDrawParams {
|
||||
rotation: () => Pick<MapRotation, "unrotate" | "rerotate" | "uprightRad">;
|
||||
pxPerMeter: () => number;
|
||||
view: () => ViewState;
|
||||
/** 바탕 틀(경위도 ↔ 화면) · 보기 바꿈 — 위치 찾기 → 지도 이동. */
|
||||
frame: () => { meta: VWorldMeta | null; setView: (next: ViewState) => void };
|
||||
redraw: () => void;
|
||||
/** 켬 — 편집기가 고른 노드 · 곡선 판을 풂. */
|
||||
onOpen: () => void;
|
||||
/** 도구줄 재기 단추 켬 — 켠 동안 D03 재기 겹이 누름을 받음(755-2). */
|
||||
measuring: () => boolean;
|
||||
/** 켠 동안 사진첩이 바뀜 — 도구줄 되돌리기 단추 맞춤. */
|
||||
onHistory: () => void;
|
||||
/** 다시 추적한 평면 · 고친 예정노선(빈 = 받은 노선으로) — 편집기 한 걸음. */
|
||||
onFollow: (plan: FlattenedPlan, expected: Vertex[]) => void;
|
||||
}
|
||||
@@ -78,6 +84,15 @@ export interface ExpectedDraw {
|
||||
paint: (context: CanvasRenderingContext2D) => void;
|
||||
/** 아래 그림 불투명(켠 동안 `--draw-map-dim`). */
|
||||
dim: () => number;
|
||||
/** 켠 동안 되돌리기 — 이 판 사진첩(끔 = null · 페이지 사진첩). */
|
||||
history: () => ExpectedHistory | null;
|
||||
}
|
||||
|
||||
export interface ExpectedHistory {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
canUndo: () => boolean;
|
||||
canRedo: () => boolean;
|
||||
}
|
||||
|
||||
const sameLine = (a: ReadonlyArray<Vertex>, b: ReadonlyArray<Vertex>): boolean =>
|
||||
@@ -99,10 +114,14 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
/** 켠 뒤 확인창을 물었나 — 다시 추적 뒤 평면은 손 고침이 없음(켠 동안 계획선 안 집힘). */
|
||||
let asked = false;
|
||||
let grid: DesignTerrainGrid | null = null;
|
||||
/** 고치는 판(예정노선 꼴 · 한 선) · 판을 지은 꼭짓점 · 긋기 시작 때 판(끝나면 바뀜을 봄). */
|
||||
/** 고치는 판(예정노선 꼴 · 켠 동안 끊김 허용) · 판을 지은 꼭짓점. */
|
||||
let network: PlanNetwork = { lines: [], pieces: [] };
|
||||
let base: Vertex[] = [];
|
||||
let strokeBase: PlanNetwork | null = null;
|
||||
/** 켠 동안 사진첩(755-1) — `commit` 한 번 = 한 걸음 · 판을 다시 지으면 비움. */
|
||||
let undoStack: PlanNetwork[] = [];
|
||||
let redoStack: PlanNetwork[] = [];
|
||||
/** 재기 겹이 받던 때(켬끔 바뀜 = 잰 것 지움). */
|
||||
let measured = false;
|
||||
|
||||
const line = (): Vertex[] =>
|
||||
edit === null ? (saved.length > 1 ? saved : original) : edit.length > 1 ? edit : original;
|
||||
@@ -113,26 +132,28 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
const shown = line();
|
||||
base = (shown === original ? received() : shown).map(([x, y]): Vertex => [x, y]);
|
||||
const vertices = base.map(([x, y]): Vertex => [x, y]);
|
||||
undoStack = [];
|
||||
redoStack = [];
|
||||
return { lines: base.length > 1 ? [{ id: "main", vertices, branch: null }] : [], pieces: [] };
|
||||
};
|
||||
|
||||
// ── 위치 칸 — 찾기 → 지도 가운데로(지형 받기 · 기준점 없음 · 기능 표 「terrain」 숨김) · 좌표 = 서버 변환(635-5) ──
|
||||
const coords = createPlanCoords(params.projectId, () => kit.props.render());
|
||||
const find = createPlaceFind((hit) => {
|
||||
void coords.toPlan(hit.lon, hit.lat).then((at) => {
|
||||
const view = params.view();
|
||||
if (!at || view.width <= 0) return;
|
||||
const [sx, sy] = params.toScreen(at);
|
||||
params.frame().setView({
|
||||
...view,
|
||||
offsetX: view.offsetX + view.width / 2 - sx,
|
||||
offsetY: view.offsetY + view.height / 2 - sy,
|
||||
});
|
||||
params.redraw();
|
||||
});
|
||||
const measuring = (): boolean => open && params.measuring();
|
||||
/** 켠 채 판을 고침 — [저장] · 떠남이면 잃음(755-3). */
|
||||
const pending = (): boolean => open && undoStack.length > 0;
|
||||
holdDesignSave(canvas, () => (pending() ? HELD : null));
|
||||
guardProjectLeave(canvas, {
|
||||
dirty: pending,
|
||||
save: async () => (showToast(HELD, "warning"), false),
|
||||
message: HELD,
|
||||
});
|
||||
|
||||
// 속성 판 경위도 = 서버 변환(635-5)
|
||||
const coords = createPlanCoords(params.projectId, () => kit.props.render());
|
||||
const others = createPlanContextLayer(params.projectId, params.redraw);
|
||||
// 재기 = 예정노선 페이지 재기 한 벌(첫 누름 선 위 = 선 따라 · 밖 = 두 점 곧은 거리 · 높이 = 설계 지형 격자)
|
||||
const measure = createPlanMeasure(
|
||||
() => z,
|
||||
() => network,
|
||||
);
|
||||
|
||||
// ── 예정노선 키트(긋기 · 고르기 · 메뉴 · 속성 판 · 칸 = 예정노선 페이지와 같은 한 벌) ──
|
||||
const kit = createPlanKit({
|
||||
@@ -146,21 +167,44 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
heightAt: () => z,
|
||||
lonLat: coords.lonLat,
|
||||
editable: () => open && !busy,
|
||||
idle: () => open,
|
||||
onMode,
|
||||
idle: () => open && !measuring(),
|
||||
onMode: () => params.redraw(),
|
||||
onSelect: () => (kit.props.render(), params.redraw()),
|
||||
snaps: others.joints,
|
||||
cards: { site: { parts: [find.root] } },
|
||||
onKey: (event) =>
|
||||
keyIs(event, "plan.undo")
|
||||
? (step("undo"), true)
|
||||
: keyIs(event, "plan.redo")
|
||||
? (step("redo"), true)
|
||||
: measuring() && event.key === "Escape"
|
||||
? (measure.clear(), true)
|
||||
: false,
|
||||
cards: {},
|
||||
});
|
||||
const { draw, edit: editor, props, menu } = kit;
|
||||
const layers = kit.layers([], []);
|
||||
const measureLayers = kit.layers([], [measure.layer]);
|
||||
/** 지금 겹 — 재기 켬이면 재기 겹(고르기 · 노드 끌기 쉼) · 켬끔이 바뀌면 잰 것 지움 · 긋기 끝. */
|
||||
const liveLayers = (): PlanMapLayer[] => {
|
||||
const on = measuring();
|
||||
if (on !== measured) {
|
||||
measured = on;
|
||||
measure.clear();
|
||||
if (on && draw.mode() !== "idle") draw.setMode("idle");
|
||||
}
|
||||
return on ? measureLayers : layers;
|
||||
};
|
||||
props.panel.classList.add("b05-routeedit__expected-props");
|
||||
viewport.append(props.panel);
|
||||
menu.mount(viewport);
|
||||
|
||||
// ── 그리기 칸 — 머리 「예정노선 수정」 · 받은 노선 · 길이 비교 · [받은 노선으로] ──
|
||||
const drawCard = kit.cards.find((card) => card.contains(draw.panel))!;
|
||||
const toggle = createToggle({ label: "예정노선 수정", size: 20, onChange: (on) => setOpen(on) });
|
||||
const toggle = createToggle({
|
||||
label: "예정노선 수정",
|
||||
size: 20,
|
||||
onChange: (on) => void setOpen(on),
|
||||
});
|
||||
toggle.root.addEventListener("click", (event) => event.stopPropagation()); // 칸 접기로 안 올라감
|
||||
drawCard.firstElementChild!.append(toggle.root);
|
||||
const originalToggle = createToggle({
|
||||
@@ -182,10 +226,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
row.className = "b05-routeedit__expected-row";
|
||||
row.append(originalToggle.root, resetButton);
|
||||
drawCard.append(row, compare, jointHint, hint);
|
||||
fillPlanPanel({
|
||||
site: kit.cards.find((card) => card.contains(find.root)) ?? null,
|
||||
draw: drawCard,
|
||||
});
|
||||
fillPlanPanel(drawCard);
|
||||
|
||||
function syncCard(): void {
|
||||
const edited = changed();
|
||||
@@ -196,15 +237,17 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
compare.hidden = !edited;
|
||||
jointHint.textContent = others.note(line()) ?? "";
|
||||
jointHint.hidden = !jointHint.textContent;
|
||||
hint.textContent = !open ? OFF_HINT : network.lines.length ? HINT : EMPTY_HINT;
|
||||
hint.hidden = open; // 켠 동안 설명 걷음(589-7)
|
||||
}
|
||||
|
||||
function setOpen(next: boolean): void {
|
||||
async function setOpen(next: boolean): Promise<void> {
|
||||
if (draw.mode() !== "idle") draw.setMode("idle");
|
||||
if (!next && open && !(await finish())) return toggle.set(true);
|
||||
open = next;
|
||||
asked = false;
|
||||
network = fromLine();
|
||||
editor.select([]);
|
||||
measure.clear();
|
||||
draw.setEditable(open);
|
||||
toggle.set(open);
|
||||
if (open) {
|
||||
@@ -215,53 +258,56 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
}
|
||||
syncCard();
|
||||
props.render();
|
||||
params.onHistory();
|
||||
params.redraw();
|
||||
}
|
||||
|
||||
/** 꼴 한 걸음(예정노선 `commit`) — 긋는 중이면 판만 · 쉴 때(놓음 · 지움) = 다시 추적. */
|
||||
/** 꼴 한 걸음(예정노선 `commit`) — 켠 동안 판만 · 이 판 사진첩에 쌓음(평면선 = 끌 때 한 번). */
|
||||
function commit(next: PlanNetwork): void {
|
||||
undoStack.push(network);
|
||||
redoStack = [];
|
||||
network = next;
|
||||
props.render();
|
||||
params.redraw();
|
||||
if (draw.mode() === "idle") void apply();
|
||||
}
|
||||
|
||||
/** 긋기 들어감 = 판 기억 · 긋기 끝 = 바뀌었으면 다시 추적. */
|
||||
function onMode(): void {
|
||||
if (draw.mode() === "draw") {
|
||||
strokeBase ??= network;
|
||||
return params.redraw();
|
||||
}
|
||||
const before = strokeBase;
|
||||
strokeBase = null;
|
||||
if (before && JSON.stringify(before.lines) !== JSON.stringify(network.lines)) void apply();
|
||||
kit.sync(); // 지운 고름 · 판 맞춤(예정노선 페이지 `refresh` 와 같음)
|
||||
params.onHistory();
|
||||
params.redraw();
|
||||
}
|
||||
|
||||
function revert(): void {
|
||||
network = fromLine();
|
||||
function step(which: "undo" | "redo"): void {
|
||||
const [from, to] = which === "undo" ? [undoStack, redoStack] : [redoStack, undoStack];
|
||||
const last = from.pop();
|
||||
if (!last) return;
|
||||
if (draw.mode() !== "idle") draw.setMode("idle");
|
||||
to.push(network);
|
||||
network = last;
|
||||
editor.select([]);
|
||||
kit.sync();
|
||||
syncCard();
|
||||
params.onHistory();
|
||||
params.redraw();
|
||||
}
|
||||
|
||||
/** 고친 판 → 다시 추적(한 선 · 점 2 이상 · 판을 지은 꼭짓점과 다를 때만). */
|
||||
async function apply(): Promise<void> {
|
||||
if (busy || !open) return;
|
||||
/** 끌 때 한 번 — 끊겼으면 알림 · 안 끔 · 바뀌었으면 다시 추적(실패 · 물음 취소 = 안 끔). 참 = 꺼도 됨. */
|
||||
async function finish(): Promise<boolean> {
|
||||
if (busy) return false;
|
||||
const drawn = network.lines.filter((entry) => entry.vertices.length > 1);
|
||||
syncCard();
|
||||
if (!drawn.length) return; // 다 지움 — 새로 그을 때까지 평면 그대로
|
||||
if (drawn.length > 1) {
|
||||
showToast("예정노선은 한 선입니다.", "info");
|
||||
return revert();
|
||||
showToast(
|
||||
`예정노선이 ${drawn.length} 토막으로 끊겨 있습니다 — 선 끝에서 이어 긋거나 [잇기]로 이은 뒤 끕니다.`,
|
||||
"info",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!drawn.length) {
|
||||
if (base.length > 1)
|
||||
showToast("예정노선이 비어 고친 것을 버렸습니다 — 평면선 그대로.", "info");
|
||||
return true;
|
||||
}
|
||||
const points = drawn[0].vertices.map(([x, y]): Vertex => [x, y]);
|
||||
if (!sameLine(points, base)) await follow(points, points);
|
||||
return sameLine(points, base) || (await follow(points, points));
|
||||
}
|
||||
|
||||
/** 켠 뒤 처음만 확인창 → `/route/follow`(저장 0) → 편집기 한 걸음(초안 `expected` = `expected`). */
|
||||
async function follow(points: Vertex[], expected: Vertex[]): Promise<void> {
|
||||
if (busy || points.length < 2) return;
|
||||
/** 확인창(켠 뒤 처음만) → `/route/follow`(저장 0) → 편집기 한 걸음(초안 `expected` = `expected`). 참 = 됨. */
|
||||
async function follow(points: Vertex[], expected: Vertex[]): Promise<boolean> {
|
||||
if (busy || points.length < 2) return false;
|
||||
if (!asked) {
|
||||
const ok = await askConfirm({
|
||||
title: "평면선 다시 추적",
|
||||
@@ -270,7 +316,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
"받은 노선(원본)은 그대로 · [저장] 전까지는 되돌리기로 물릴 수 있습니다.",
|
||||
okLabel: "다시 추적",
|
||||
});
|
||||
if (!ok) return revert();
|
||||
if (!ok) return false;
|
||||
asked = true;
|
||||
}
|
||||
busy = true;
|
||||
@@ -286,12 +332,13 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
flattenServerPlan(plan.nodes ?? [], plan.curves ?? [], plan.node_edits),
|
||||
expected,
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
showToast(
|
||||
`평면선을 다시 추적하지 못했습니다.${error instanceof Error ? ` ${error.message}` : ""}`,
|
||||
"error",
|
||||
);
|
||||
revert();
|
||||
return false;
|
||||
} finally {
|
||||
busy = false;
|
||||
syncCard();
|
||||
@@ -331,14 +378,13 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
return params.rotation().unrotate(event.clientX - rect.left, event.clientY - rect.top);
|
||||
},
|
||||
screen: map,
|
||||
layers: () => layers,
|
||||
layers: liveLayers,
|
||||
redraw: params.redraw,
|
||||
capture: { active: () => open, busy: () => busy },
|
||||
});
|
||||
|
||||
const onKey = (event: KeyboardEvent): void => {
|
||||
if (!open) return;
|
||||
if (keyIs(event, "plan.undo") || keyIs(event, "plan.redo")) return; // 되돌리기 = 페이지 사진첩
|
||||
if (!kit.key(event)) return;
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
@@ -357,16 +403,15 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
if (i) context.lineTo(sx, sy);
|
||||
else context.moveTo(sx, sy);
|
||||
});
|
||||
context.globalAlpha = 0.7;
|
||||
context.setLineDash([2, 5]);
|
||||
context.lineWidth = 2;
|
||||
context.strokeStyle = themeColor("--draw-text-secondary", "#9ca3af"); // 받은 노선 = 옅은 점선
|
||||
context.stroke();
|
||||
context.lineCap = "round";
|
||||
context.lineJoin = "round";
|
||||
// 받은 노선 = 분홍 점선 + 검은 테(589-6 · 계획 주황 · 예정 민트 · 추천 노랑 · 등고선과 가름 · 테마 안 따름)
|
||||
strokeCased(context, themeColor("--draw-map-route-3", "#ff8cbc"), 2.5, [8, 5]);
|
||||
context.restore();
|
||||
}
|
||||
if (!open) return;
|
||||
const screen = map();
|
||||
for (const layer of layers) {
|
||||
for (const layer of liveLayers()) {
|
||||
context.save();
|
||||
layer.draw(context, screen); // 고치는 예정노선 = 맨 위(D9) · 고름 · 미리보기
|
||||
context.restore();
|
||||
@@ -385,9 +430,14 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
busy,
|
||||
compare: compare.textContent,
|
||||
joint: jointHint.textContent,
|
||||
undo: undoStack.length,
|
||||
redo: redoStack.length,
|
||||
measuring: measuring(),
|
||||
}),
|
||||
measure: () => measure.result(),
|
||||
others: others.probe,
|
||||
line,
|
||||
network: () => network,
|
||||
original: () => original,
|
||||
source: () => source,
|
||||
edit: () => edit,
|
||||
@@ -418,7 +468,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
setEdit(next) {
|
||||
edit = next;
|
||||
syncCard();
|
||||
if (draw.mode() !== "idle") return; // 긋는 중 되돌림 — 긋기가 끝나면 그 판으로 다시 추적
|
||||
if (draw.mode() !== "idle") return; // 긋는 중 = 긋기 판 그대로
|
||||
network = fromLine();
|
||||
editor.sync();
|
||||
props.render();
|
||||
@@ -427,5 +477,14 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
line,
|
||||
paint,
|
||||
dim: () => (open ? parseFloat(themeColor("--draw-map-dim", "0.35")) || 0.35 : 1),
|
||||
history: () =>
|
||||
open
|
||||
? {
|
||||
undo: () => step("undo"),
|
||||
redo: () => step("redo"),
|
||||
canUndo: () => undoStack.length > 0,
|
||||
canRedo: () => redoStack.length > 0,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Panel_Plan.ts
|
||||
* 종단 보기 왼쪽 「위치」 · 「그리기」 칸 자리(PLAN 635-3 · 요구 546) — 예정노선 키트(`createPlanKit` 「b05」) 칸을 앉힘.
|
||||
* 칸은 페이지가 먼저 세우고 편집기는 노선을 읽은 뒤 섬 → 칸 = 빈 자리(order 1 · 2) · 편집기가 `fillPlanPanel` 로 채움.
|
||||
* 종단 보기 왼쪽 「그리기」 칸 자리(PLAN 635-3 · 755-2 · 요구 546 · 589-8 위치 칸 걷음) — 예정노선 키트(`createPlanKit` 「b05」) 칸을 앉힘.
|
||||
* 칸은 페이지가 먼저 세우고 편집기는 노선을 읽은 뒤 섬 → 칸 = 빈 자리(order 1) · 편집기가 `fillPlanPanel` 로 채움.
|
||||
* 늦게 온 쪽이 붙임 · 새 칸 자리(페이지 다시 세움) = 떠난 편집기 칸(문서 밖) 버림 · 비면 자리 숨김.
|
||||
* 좌표 변환(635-5) = 서버(`POST /route/coords` · 사업지 좌표계 pyproj) — 찾기 이동 · 속성 판 경위도 때만 부름.
|
||||
* 좌표 변환(635-5) = 서버(`POST /route/coords` · 사업지 좌표계 pyproj) — 속성 판 경위도 때만 부름.
|
||||
* ========================================================================== */
|
||||
|
||||
import { registerPanelSection } from "../A00_Common/design/design_panel";
|
||||
@@ -11,55 +11,43 @@ import { requestJson } from "./B05_Profile_Api_Replan";
|
||||
|
||||
type Vertex = [number, number];
|
||||
|
||||
type Slot = "site" | "draw";
|
||||
const holders: Partial<Record<Slot, HTMLElement>> = {};
|
||||
const cards: Partial<Record<Slot, HTMLElement | null>> = {};
|
||||
let holder: HTMLElement | null = null;
|
||||
let card: HTMLElement | null = null;
|
||||
|
||||
const attach = (slot: Slot): void => {
|
||||
const holder = holders[slot];
|
||||
const attach = (): void => {
|
||||
if (!holder) return;
|
||||
const card = cards[slot];
|
||||
holder.replaceChildren(...(card ? [card] : []));
|
||||
holder.hidden = !card;
|
||||
};
|
||||
|
||||
for (const [slot, order, title] of [
|
||||
["site", 1, "위치"],
|
||||
["draw", 2, "그리기"],
|
||||
] as const)
|
||||
registerPanelSection({
|
||||
id: `plan-${slot}`,
|
||||
title,
|
||||
views: ["profile"],
|
||||
order,
|
||||
build: () => {
|
||||
const holder = document.createElement("div");
|
||||
holder.className = "b05-route__plan-slot";
|
||||
holders[slot] = holder;
|
||||
if (!cards[slot]?.isConnected) cards[slot] = null; // 떠난 편집기 칸
|
||||
attach(slot);
|
||||
return holder;
|
||||
},
|
||||
});
|
||||
registerPanelSection({
|
||||
id: "plan-draw",
|
||||
title: "그리기",
|
||||
views: ["profile"],
|
||||
order: 1,
|
||||
build: () => {
|
||||
holder = document.createElement("div");
|
||||
holder.className = "b05-route__plan-slot";
|
||||
if (!card?.isConnected) card = null; // 떠난 편집기 칸
|
||||
attach();
|
||||
return holder;
|
||||
},
|
||||
});
|
||||
|
||||
/** 키트 칸을 앉힘(새 편집기가 갈음). */
|
||||
export function fillPlanPanel(next: Record<Slot, HTMLElement | null>): void {
|
||||
for (const slot of ["site", "draw"] as const) {
|
||||
cards[slot] = next[slot];
|
||||
attach(slot);
|
||||
}
|
||||
export function fillPlanPanel(next: HTMLElement | null): void {
|
||||
card = next;
|
||||
attach();
|
||||
}
|
||||
|
||||
export interface PlanCoords {
|
||||
/** 경위도 → 사업지 [x, y] m(못 받으면 null). */
|
||||
toPlan: (lon: number, lat: number) => Promise<Vertex | null>;
|
||||
/** 사업지 → 경위도 — 받은 점이면 바로 · 처음이면 null 주고 한 번 물음(오면 `onLonLat`). */
|
||||
lonLat: (x: number, y: number) => Vertex | null;
|
||||
}
|
||||
|
||||
/** 경위도 ↔ 사업지 좌표 서버 변환 · 같은 점은 캐시(끌기 중엔 안 부름 — 판은 고름 · 놓음 때만 그림). */
|
||||
/** 사업지 → 경위도 서버 변환 · 같은 점은 캐시(끌기 중엔 안 부름 — 판은 고름 · 놓음 때만 그림). */
|
||||
export function createPlanCoords(projectId: string, onLonLat: () => void): PlanCoords {
|
||||
const ask = (to: "plan" | "lonlat", point: Vertex): Promise<Vertex | null> =>
|
||||
const ask = (to: "lonlat", point: Vertex): Promise<Vertex | null> =>
|
||||
requestJson<{ points: Vertex[] }>(
|
||||
`/projects/${projectId}/route/coords`,
|
||||
{ method: "POST", body: JSON.stringify({ to, points: [point] }) },
|
||||
@@ -70,7 +58,6 @@ export function createPlanCoords(projectId: string, onLonLat: () => void): PlanC
|
||||
);
|
||||
const known = new Map<string, Vertex | null>();
|
||||
return {
|
||||
toPlan: (lon, lat) => ask("plan", [lon, lat]),
|
||||
lonLat(x, y) {
|
||||
const key = `${x},${y}`;
|
||||
if (known.has(key)) return known.get(key) ?? null;
|
||||
|
||||
@@ -129,9 +129,10 @@ export async function mountRouteEditor(
|
||||
rotation: () => rotation,
|
||||
pxPerMeter: () => pxPerMeter(),
|
||||
view: () => view,
|
||||
frame: () => ({ meta, setView: (next: ViewState) => (view = next) }),
|
||||
redraw: () => (draw(), sheetFollow?.poke()), // 켠 동안 예정노선이 도엽 밖으로 가면 다시 받음(589-10)
|
||||
onOpen: () => ((picked = -1), syncCurveBar()),
|
||||
measuring: () => measureControls.mode(), // 켠 동안 재기 = D03 재기 겹(755-2)
|
||||
onHistory: () => historyControls.sync(), // 켠 동안 되돌리기 = 예정노선 판(755-1)
|
||||
onFollow: (flat, line) =>
|
||||
restoreSnapshot(
|
||||
{ ...flat, apexLock: [], picked: -1, rec: initial.tag(), expected: line },
|
||||
@@ -500,6 +501,7 @@ export async function mountRouteEditor(
|
||||
const historyControls = bindHistoryControls({
|
||||
overlay,
|
||||
getHistory: () => history,
|
||||
local: () => expectedDraw.history(),
|
||||
restore: (snapshot, message) => {
|
||||
initial.restoreTag(snapshot.rec); // 「현재 추천」도 그 걸음 것으로 — 노선과 짝.
|
||||
restoreSnapshot(snapshot, message, false);
|
||||
|
||||
@@ -143,6 +143,13 @@ export interface HistoryControlsParams {
|
||||
overlay: HTMLElement;
|
||||
/** 아직 노선을 못 읽었으면 null 이다(단추는 꺼진 채로 둔다). */
|
||||
getHistory: () => RouteEditHistory | null;
|
||||
/** 예정노선 수정 켠 동안 사진첩(755-1) — 있으면 단추가 이쪽을 씀. */
|
||||
local?: () => {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
canUndo: () => boolean;
|
||||
canRedo: () => boolean;
|
||||
} | null;
|
||||
/** 사진 한 벌을 화면에 되살린다. */
|
||||
restore: (snapshot: RouteEditSnapshot, message: string) => void;
|
||||
}
|
||||
@@ -162,12 +169,14 @@ export function bindHistoryControls(params: HistoryControlsParams): {
|
||||
const redoBtn = overlay.querySelector<HTMLButtonElement>('[data-act="redo"]')!;
|
||||
|
||||
const sync = (): void => {
|
||||
const history = getHistory();
|
||||
const history = params.local?.() ?? getHistory();
|
||||
undoBtn.disabled = !history?.canUndo();
|
||||
redoBtn.disabled = !history?.canRedo();
|
||||
};
|
||||
|
||||
const step = (which: "undo" | "redo"): void => {
|
||||
const local = params.local?.();
|
||||
if (local) return (local[which](), sync());
|
||||
const history = getHistory();
|
||||
if (!history) return;
|
||||
const snapshot = history[which]();
|
||||
|
||||
@@ -557,6 +557,14 @@ export function createStructuresSection(
|
||||
groupSelect.replaceChildren(new Option("— 선택 —", ""), ...groupOptions(next, pick));
|
||||
groupSelect.value = "";
|
||||
syncTypeOptions();
|
||||
// 늦게 온 종류 목록 = 고른 관 폼을 새 목록으로 다시 엶 — 고름만 남고 폼이 비던 흠(754-2) ·
|
||||
// 구조물 폼은 뒤따르는 `setStructures` 가 다시 엶 · 임시 배치 중 입력은 둠
|
||||
if (tempPipeChainage !== null) return;
|
||||
const at = selectedPipeChainage;
|
||||
const pipe = pipeFacilities.find(
|
||||
(entry) => at !== null && Math.abs(entry.chainage_m - at) < PIPE_MATCH_M,
|
||||
);
|
||||
if (pipe) loadPipeForm(pipe);
|
||||
},
|
||||
setStructures(next) {
|
||||
// 서버 정본 주입 — onChange를 울리지 않는다. 울리면 Page가 다시 저장을 걸어
|
||||
|
||||
@@ -34,7 +34,12 @@ export interface PlanFeatureRow {
|
||||
}
|
||||
|
||||
export const PLAN_FEATURES: readonly PlanFeatureRow[] = [
|
||||
{ key: "site", name: "주소 · 장소 찾기(B05 = 지도 이동만)", pages: { d03: "use", b05: "use" } },
|
||||
{
|
||||
key: "site",
|
||||
name: "주소 · 장소 찾기",
|
||||
pages: { d03: "use", b05: "hide" },
|
||||
why: "B 사업지 = 전처리 확정",
|
||||
},
|
||||
{
|
||||
key: "draw",
|
||||
name: "긋기 네 방식 · 길이 · 기울기 · Tab · Shift · 부채꼴 · ± 색",
|
||||
@@ -92,9 +97,8 @@ export const PLAN_FEATURES: readonly PlanFeatureRow[] = [
|
||||
},
|
||||
{
|
||||
key: "join",
|
||||
name: "[잇기] 두 선 끝 이음",
|
||||
pages: { d03: "use", b05: "hide" },
|
||||
why: "B 계획선 한 줄",
|
||||
name: "[잇기] 두 선 끝 이음(B05 = 켠 동안 끊긴 곳 이음)",
|
||||
pages: { d03: "use", b05: "use" },
|
||||
},
|
||||
{
|
||||
key: "others",
|
||||
|
||||
@@ -158,7 +158,8 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
|
||||
onInput ? specs.find((x) => x.이름 === name) : undefined;
|
||||
// 737-2 비목마다 구분 상자 — 맨 위 옵션 칸 · 아래 두 칸 식 줄들 · 소계
|
||||
const boxes = text.groups.map((g) => {
|
||||
const lines = g.줄.map((line, i) => {
|
||||
const visible = g.줄.filter((l) => !l.옵션꺼짐); // 753-2 옵션으로 빠진 줄은 안 그림
|
||||
const lines = visible.map((line, i) => {
|
||||
const pair = String(line.짝 ?? `${g.비목}${i}`);
|
||||
const open = ctx.open?.(line);
|
||||
return el("div", {
|
||||
|
||||
@@ -615,9 +615,12 @@ function boxCard(
|
||||
const notes = [...new Set(lines.map((e) => e.textLine?.환산근거).filter(Boolean))];
|
||||
const { root, tbody } = tableShell(COLS, { density: "data" });
|
||||
root.classList.add("m01lab__ho");
|
||||
// 753-2 옵션으로 빠진 줄은 안 그림(옵션 칸 · 소계는 그대로 — 옵션을 바꾸면 보이는 줄이 바뀜)
|
||||
const off = (e: Entry): boolean => !!(e.line?.옵션꺼짐 || e.textLine?.옵션꺼짐);
|
||||
const rows = b.줄.filter((r) => !("몫" in r ? r.몫.every(off) : off(r)));
|
||||
tbody.append(
|
||||
...(b.줄.length
|
||||
? b.줄.flatMap((r) => ("몫" in r ? mergedRows(r, ctx) : [lineRow(r, ctx)]))
|
||||
...(rows.length
|
||||
? rows.flatMap((r) => ("몫" in r ? mergedRows(r, ctx) : [lineRow(r, ctx)]))
|
||||
: [emptyRow(COLS, tl("Empty_Group"))]),
|
||||
);
|
||||
const sub = tableRow(COLS, [
|
||||
|
||||
@@ -105,6 +105,8 @@ export interface CalcLine {
|
||||
까닭?: string;
|
||||
/** (M02 744-1) 둘째 기초 세부 줄 — `수량`은 M01 원문 · 이 칸은 1 기준단위 몫 비목(금액 · 비목은 머리 수량 곱한 상자 몫) */
|
||||
원문?: { 비목?: Record<string, number | null> };
|
||||
/** (753-1) 옵션으로 빠진 줄 — 화면은 안 그림 */
|
||||
옵션꺼짐?: boolean;
|
||||
}
|
||||
|
||||
/** % 덧줄 표기(149-4) — 율 있으면 수량 = 율 · 단위 % · 이름 뒤 규격 괄호 · 금액은 그대로(M01 · M02 한 벌) */
|
||||
@@ -160,6 +162,8 @@ export interface TextLine {
|
||||
환산근거?: string;
|
||||
/** (M02 750-1) SK 셈 답 줄 cid — 상세 줄과 이음 */
|
||||
cid?: string;
|
||||
/** (753-1) 옵션으로 빠진 줄(수량 0 이 만약 갈래 탓) — 화면은 그 줄을 안 그림(금액 · 차례 그대로) */
|
||||
옵션꺼짐?: boolean;
|
||||
}
|
||||
/** `POST /text` 답 — 서버가 만든 읽는 식 줄(화면은 그대로 찍음) */
|
||||
export type TextAnswer =
|
||||
|
||||
@@ -262,7 +262,8 @@ export function answerView(answer: CalcAnswer, draft: boolean, money: boolean):
|
||||
}
|
||||
const parts: HTMLElement[] = [...note];
|
||||
if (answer.lines) {
|
||||
const rows = answer.lines.map((line) =>
|
||||
const visible = answer.lines.filter((line) => !line.옵션꺼짐); // 753-2 옵션으로 빠진 줄은 안 그림
|
||||
const rows = visible.map((line) =>
|
||||
el("tr", {
|
||||
children: [
|
||||
el("td", {
|
||||
|
||||
@@ -26,7 +26,7 @@ import type { LaborCalc } from "./M02_MasterTemplete_Labor_Api";
|
||||
import { markCells, markHeads } from "./M02_MasterTemplete_Labor_MatMarks";
|
||||
import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { formatPrice } from "../common_util/common_util_money";
|
||||
import { formatNumber, formatPrice } from "../common_util/common_util_money";
|
||||
import { tableOf } from "./logic/M02_MasterTemplete_Hopyo_Draft";
|
||||
|
||||
/** 드롭다운 — 저장 값(서버 뺌 까닭) → 화면 글 */
|
||||
@@ -62,8 +62,7 @@ export interface RowsContext {
|
||||
options: (line: HopyoLine, at: number) => void;
|
||||
}
|
||||
|
||||
const num = (v: number): string =>
|
||||
Number(v.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 });
|
||||
const num = formatNumber;
|
||||
const small = (text: string, cls = "m02-labrow__note"): HTMLElement =>
|
||||
el("small", { className: cls, text, attrs: { title: text } });
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { t as L, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createTable } from "@ui/ui_template_table";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { formatNumber, formatPrice } from "../common_util/common_util_money";
|
||||
import type {
|
||||
CompareLook,
|
||||
ElemCand,
|
||||
@@ -26,7 +27,7 @@ const LOOK: Record<CompareLook, { tone: BadgeTone; key: LocaleKey }> = {
|
||||
기초만: { tone: "neutral", key: "M02_CmpBase" },
|
||||
};
|
||||
const LOOKS = Object.keys(LOOK) as CompareLook[];
|
||||
const num = (n: number | null): string => (n === null ? "—" : String(Number(n.toPrecision(4))));
|
||||
const num = (n: number | null): string => (n === null ? "—" : formatNumber(n));
|
||||
|
||||
/** 선택 칸 글 뒤 요약 — 「같음 3 · 품 다름 1」 · 견줌 못함 = "" */
|
||||
export const compareBrief = (c: SangeunCompare | null | undefined): string =>
|
||||
@@ -112,7 +113,7 @@ export function elemChoices(cands: ElemCand[], pick: (c: ElemCand) => void): HTM
|
||||
const box = el("div", { className: "ui-toolbar", attrs: { "data-elem": "" } });
|
||||
box.title = L("M02_ElemTitle");
|
||||
for (const c of cands.slice(0, 3)) {
|
||||
const price = c.단가 ? ` · ${c.단가.toLocaleString()}` : "";
|
||||
const price = c.단가 ? ` · ${formatPrice(c.단가)}` : "";
|
||||
const b = createButton({
|
||||
label: `${c.이름}${c.규격 ? ` ${c.규격}` : ""} [${c.단위 ?? ""}]${price}`,
|
||||
size: 24,
|
||||
|
||||
@@ -38,6 +38,7 @@ import type { Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { baseHeads } from "./M02_MasterTemplete_Sangeun_BaseHead";
|
||||
import { calcSangeun, removedStrip } from "./M02_MasterTemplete_Sangeun_Cover";
|
||||
import { openSangeunLinePicker } from "./M02_MasterTemplete_Sangeun_Pick";
|
||||
import { createSangeunSide, tabOf, type SourceTab } from "./M02_MasterTemplete_Sangeun_Side";
|
||||
import { baseUnitOf, buildUnitSwap, type SwapHow } from "./M02_MasterTemplete_Sangeun_Unit";
|
||||
import {
|
||||
dropBase,
|
||||
@@ -63,6 +64,8 @@ export interface SangeunDetailOptions {
|
||||
owner: string | null;
|
||||
readOnly: boolean;
|
||||
onChanged: (dirty: boolean) => void;
|
||||
/** 752-1 오른쪽 원문 패널 — 단가산출 화면만 켬(일위대가 줄 모달 안 상세는 끔) */
|
||||
source?: boolean;
|
||||
}
|
||||
|
||||
const LIB = { read: "/m02/logic-lib", calc: "/m02/logic-lib" };
|
||||
@@ -90,6 +93,9 @@ export function mountSangeunDetail(
|
||||
let fail = "";
|
||||
let extra: LogicInput[] = []; // 더한줄 로직 입력 중 바탕에 없는 것 = 옵션 칸(725-6 · 727-3)
|
||||
const inputsOf = new Map<string, LogicInput[]>();
|
||||
const infoOf = new Map<string, SourceTab>(); // 더한줄 로직 → 원문 탭(752-1)
|
||||
let extraKeys: string[] = [];
|
||||
const side = opt.source ? createSangeunSide() : null;
|
||||
let seq = 0;
|
||||
let alive = true;
|
||||
|
||||
@@ -141,19 +147,17 @@ export function mountSangeunDetail(
|
||||
touch(false);
|
||||
void loadBase(true);
|
||||
}
|
||||
const baseHint = createText(L("M02_SangeunBaseViews"));
|
||||
host.replaceChildren(
|
||||
el("div", {
|
||||
className: "m02-sk",
|
||||
attrs: { "data-sangeun-detail": doc.키 ?? "" },
|
||||
children: [
|
||||
head,
|
||||
notes,
|
||||
el("div", { className: "m02-sk__head", children: [views.root, addButton, baseHint] }),
|
||||
editor,
|
||||
],
|
||||
}),
|
||||
);
|
||||
const main = el("div", {
|
||||
className: "m02-sk",
|
||||
attrs: { "data-sangeun-detail": doc.키 ?? "" },
|
||||
children: [
|
||||
head,
|
||||
notes,
|
||||
el("div", { className: "m02-sk__head", children: [views.root, addButton] }),
|
||||
editor,
|
||||
],
|
||||
});
|
||||
host.replaceChildren(side ? side.wrap(main) : main);
|
||||
|
||||
const opened = (): boolean => (doc.열린입력 ?? []).length > 0;
|
||||
// 바탕 없는 SK 의 지문 = 이름 · 단위 따라 바뀜 — 바탕이 있을 때만 M01 개정으로 봄(725-3) · 둘째 기초 지문(727-2)
|
||||
@@ -185,11 +189,15 @@ export function mountSangeunDetail(
|
||||
.filter((k) => !inputsOf.has(k))
|
||||
.map((k) =>
|
||||
fetchLogic(k).then(
|
||||
(r) => void inputsOf.set(k, r.logic.입력 ?? []),
|
||||
(r) => {
|
||||
inputsOf.set(k, r.logic.입력 ?? []);
|
||||
infoOf.set(k, tabOf(r.logic));
|
||||
},
|
||||
() => void inputsOf.set(k, []),
|
||||
),
|
||||
),
|
||||
);
|
||||
extraKeys = keys;
|
||||
const seen = new Set<string>((base?.logic.입력 ?? []).map((i) => i.이름));
|
||||
extra = keys
|
||||
.flatMap((k) => inputsOf.get(k) ?? [])
|
||||
@@ -306,7 +314,6 @@ export function mountSangeunDetail(
|
||||
if (!alive) return;
|
||||
drawHead();
|
||||
drawNotes();
|
||||
baseHint.hidden = !base;
|
||||
const rows = ans ? rowsOf(ans, bare, doc) : [];
|
||||
const names = new Map((bare ? rowsOf(bare, null, {}) : []).map((r) => [r.cid, r.이름]));
|
||||
const removed = [
|
||||
@@ -382,6 +389,10 @@ export function mountSangeunDetail(
|
||||
cover: { ho, est, open: lineOpener(hoCtx) }, // 748-2 텍스트 수식 변수 · 값 누름 = 같은 줄 모달
|
||||
});
|
||||
drawCalc();
|
||||
side?.set([
|
||||
...(base ? [tabOf(base.logic)] : []),
|
||||
...extraKeys.flatMap((k) => (k === baseKey() ? [] : infoOf.has(k) ? [infoOf.get(k)!] : [])),
|
||||
]);
|
||||
}
|
||||
|
||||
/** 재료 고름(줄 모달 · 시험 계산 재료 칸) = SK 문서 `고름`(cid → MT) — 둘째 기초 세부 줄은 보기만 */
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Sangeun_Side.ts
|
||||
* 단가산출(SK) 상세 오른쪽 원문 패널(PLAN 752-1 · 요구 588-3) — 일위대가 상세(47-4 · 85-4)와 같은 부품:
|
||||
* 오른쪽 서랍(`createSidePanels`) + 원문 보기(`createSourceView` · M02 로그인 길 `/m02/logic-lib/source`)
|
||||
* 기초마다 탭 — 바탕 로직 · 둘째 기초(더한줄 로직) 각각의 출처 · 기초가 하나면 탭 없음 · 없으면 안내
|
||||
* 처음 = 넓은 화면만 열림(좁으면 세로 띠로 접힘) · 열림 · 폭 기억 = `m02-sangeun-side`
|
||||
* 일위대가 줄 모달 안 상세는 안 만듦(부르는 쪽이 `source` 를 안 켬).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createSidePanels } from "@ui/ui_template_side_panels";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
|
||||
export interface SourceTab {
|
||||
label: string;
|
||||
src: string;
|
||||
}
|
||||
|
||||
/** 기초 로직 → 원문 탭(「원문번호 이름」 · 출처 글) */
|
||||
export const tabOf = (logic: {
|
||||
키: string;
|
||||
원문번호?: string;
|
||||
이름: string;
|
||||
출처?: string;
|
||||
}): SourceTab => ({
|
||||
label: `${logic.원문번호 || logic.키} ${logic.이름}`,
|
||||
src: logic.출처 ?? "",
|
||||
});
|
||||
|
||||
/** 이 폭 이상일 때만 처음부터 열림 — 상세 표 여섯 칸이 좁아지지 않게 */
|
||||
const OPEN_FROM = 1500;
|
||||
|
||||
export function createSangeunSide(): {
|
||||
wrap: (main: HTMLElement) => HTMLElement;
|
||||
set: (tabs: SourceTab[]) => void;
|
||||
} {
|
||||
const view = createSourceView({
|
||||
path: "/m02/logic-lib/source",
|
||||
pdf: "/m02/logic-lib/source-pdf",
|
||||
});
|
||||
const host = el("div", { attrs: { "data-sangeun-source": "" } });
|
||||
let tabs: SourceTab[] = [];
|
||||
let at = 0;
|
||||
const side = createSidePanels({
|
||||
prefKey: "m02-sangeun-side",
|
||||
follow: false, // 일위대가와 같음 — 상세 고정 높이 칸을 채움 · 본문과 따로 스크롤
|
||||
defaultOpen: window.innerWidth >= OPEN_FROM ? "source" : null,
|
||||
panels: [{ id: "source", title: tc("Labor_Source"), body: host }],
|
||||
onChange: () => paint(),
|
||||
});
|
||||
function paint(): void {
|
||||
if (side.current() !== "source") return; // 접힘 = 찾지 않음
|
||||
if (!tabs.length)
|
||||
return void host.replaceChildren(createText(L("M02_SangeunSourceNone"), "muted"));
|
||||
at = Math.min(at, tabs.length - 1);
|
||||
view.show(tabs[at].src);
|
||||
const bar =
|
||||
tabs.length > 1
|
||||
? [
|
||||
createSegment({
|
||||
options: tabs.map((x, i) => ({ value: String(i), label: x.label })),
|
||||
value: String(at),
|
||||
size: 28,
|
||||
ariaLabel: tc("Labor_Source"),
|
||||
onChange: (v) => ((at = Number(v)), paint()),
|
||||
}).root,
|
||||
]
|
||||
: [];
|
||||
host.replaceChildren(...bar, view.root);
|
||||
}
|
||||
return {
|
||||
wrap: (main) => el("div", { className: "ui-side-panels-row", children: [main, side.root] }),
|
||||
set: (next) => {
|
||||
if (JSON.stringify(next) === JSON.stringify(tabs)) return;
|
||||
tabs = next;
|
||||
paint();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -18,5 +18,5 @@ export function mountSangeunView(
|
||||
doc: unknown,
|
||||
opt: SangeunViewOptions,
|
||||
): { getDoc: () => unknown; destroy: () => void } {
|
||||
return mountSangeunDetail(host, doc, { ...opt, owner: opt.owner ?? null });
|
||||
return mountSangeunDetail(host, doc, { ...opt, owner: opt.owner ?? null, source: true }); // 752-1
|
||||
}
|
||||
|
||||
@@ -91,6 +91,8 @@ export interface SkLine {
|
||||
원문?: { 수량: number | null; 금액: number | null; 비목?: Record<string, number | null> };
|
||||
/** 재료 줄이 잡은 품목(시험 계산 재료 칸 · 줄 모달) */
|
||||
품목?: CalcLine["품목"];
|
||||
/** (753-1) 옵션으로 빠진 줄 — 화면은 안 그림 */
|
||||
옵션꺼짐?: boolean;
|
||||
}
|
||||
|
||||
export interface SkMat {
|
||||
@@ -153,6 +155,8 @@ export interface CoverRow {
|
||||
sub: { k: number; cid: string; scale: number } | null;
|
||||
/** 화면배(728-2) — 문서 품 = 화면 품 × t · `was` = 문서 몫 */
|
||||
t: number;
|
||||
/** (753-2) 옵션으로 빠진 줄 — 계산서에서 안 그림 */
|
||||
off: boolean;
|
||||
}
|
||||
|
||||
/* ── 순수 셈 ── */
|
||||
@@ -239,7 +243,13 @@ export function rowsOf(ans: SkAnswer, bare: SkAnswer | null, doc: SkDoc): CoverR
|
||||
금액: number | null;
|
||||
},
|
||||
parts: Record<string, number>,
|
||||
extra: { 묶음?: string | null; 율?: number; apart?: boolean; 바탕값?: SkLine["바탕값"] },
|
||||
extra: {
|
||||
묶음?: string | null;
|
||||
율?: number;
|
||||
apart?: boolean;
|
||||
바탕값?: SkLine["바탕값"];
|
||||
옵션꺼짐?: boolean;
|
||||
},
|
||||
): CoverRow => {
|
||||
const hit = ADD_RE.exec(cid);
|
||||
const added = hit ? Number(hit[1]) : null;
|
||||
@@ -283,6 +293,7 @@ export function rowsOf(ans: SkAnswer, bare: SkAnswer | null, doc: SkDoc): CoverR
|
||||
apartPick: one?.종류 === "재료",
|
||||
sub: at ? { ...at, scale: Number(owner?.수량 ?? 1) } : null,
|
||||
t: pct || at ? 1 : t, // 세부 줄 품 = M01 원문 그대로(744-1) — 화면배 안 곱함
|
||||
off: !!extra.옵션꺼짐,
|
||||
};
|
||||
};
|
||||
for (const l of flatLines(ans)) rows.push(row(l.cid, l, partsOf(l), l));
|
||||
@@ -422,6 +433,7 @@ export function sheetGroups(rows: CoverRow[]): SheetGroup[] {
|
||||
금액: amount,
|
||||
묶음: r.묶음,
|
||||
장비: entries.length > 1 && (r.sub?.cid ?? r.cid).startsWith("logic:"),
|
||||
...(r.off ? { 옵션꺼짐: true } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# 계획 이관 — 736 ~ 757 (2026-11-02)
|
||||
|
||||
## 736 일위대가 [항목 추가] · 찾기 창 하나로 (요구 568 · 570 · 575-1 · sub7)
|
||||
|
||||
- 사용자 확정(575-1 동의): 일위대가표 · 구조물 조합 카드 [로직 추가] → [항목 추가] · [자재 추가] 걷음 · 불러오기 엑셀 연결 · 별칭 장부 찾기 = 새 찾아 넣기 창
|
||||
- 정함(브레인): 재료 고름 = 「자재 목록」 자리 · 중복 검사(로직 안 자재 · 이미 있음) 그대로 · 자리마다 맞지 않는 탭 막음(별칭 장부 = 기초 · 자재만) · B09 단가 연결은 그대로(M02 밖) · 부품 모음 견본에 734 갈래 색 단추(옵션 · 요소 연결) 한 칸
|
||||
- [x] 736-1 일위대가표 · 구조물 조합 카드 단추 · 안내 글 · 재료 → 자재 목록 · 불러오기 엑셀 연결(`Hopyo_Choose.ts:300-318`) · 별칭 장부(`Hopyo_Ledger.ts:219`) → 찾아 넣기 창(탭 막음) · 견본 — 확인 = 단추 하나 · 재료 넣기 → 자재 목록 · 중복 막힘 · 엑셀 연결 한 창에서 기초 · 재료 · 기계 · 단가산출 · 사진
|
||||
- 검증완료 — 시험 55 통과(721 · 717-4 · 175 · 자재 개인 · 호표 화면 · 729-3 · 호표 흐름) · typecheck 0 · 일위대가표 · 구조물 조합 상세 단추 [항목 추가] 하나 · 재료 11,354 → 자재 목록 · 같은 자재 둘째 = 「이미 담은 자재」 막힘 · 엑셀 연결 자재 줄 = 재료 탭만(탭 줄 없음) · 부품 모음 견본 칸 밝음 · 어두움 · 사진 밝음 · 어두움 · 좁은 폭(860) · 임시 일위대가 지움 · 별칭 장부는 화면 줄 없어 글 시험으로만 · 개인 층 [신규 자재] 길은 단추와 함께 빠짐(M02_Material_Pick.ts 는 남김)
|
||||
- [x] 736-2 (sub7) 빠진 길 둘 — 개인 층 [신규 자재](옛 [자재 추가] 단추 안 길 · `M02_Material_Pick.ts`)를 찾아 넣기 창 재료 탭에 「신규 자재」로 · 엑셀 연결 창 = 줄 갈래대로 탭(노임 줄 = 인력 · 기계 줄 = 기계(M01 EQ) · 단가산출 줄 = 기초 · 단가산출 · 일위대가 · 자재 줄 = 재료) — 확인 = 개인 층 신규 자재 만들어 넣기 · 노임 · 기계 엑셀 줄 이음 · 사진
|
||||
- 검증완료 — 시험 55 통과(721 · 717-4 · 175 · 자재 개인 · 호표 화면 · 729-3 · 호표 흐름 · 신규 736-2 글 시험) · typecheck 0 · 개인 층 [신규 자재] → 자재 목록 줄(PM000002 · 원 1,234) · 기계 줄 고르기 = 기계 탭 단독(EQ 25 줄) · 노임 줄 = 인력 탭 단독 · 남은 줄 3 → 2 → 1 · 금액 계산 코드 안 건드림 · 밝음 · 어두움 · 좁은 폭 · 높이 768 · 임시 문서 · 실무자료 · 개인자재 지움
|
||||
- [x] 736-3 (sub7) 기계(M01 EQ) — 단가산출 · 일위대가 [항목 추가] 찾아 넣기 창에서 기계 고름 = 고정 줄(이음 기계 · 단가 = 한 시간 노 · 재 · 경 · 서버 `/logic-lib/machine-hourly`)
|
||||
- 검증완료 — 시험 21 통과(721 · 717-4 · 705-1) · typecheck 0 · EQ000019 굴착기 0.6㎥ 한 시간 81,811.6(노 35,412.1 · 재 22,976.5 · 경 23,423.0) · 일위대가 수량 1 금액 81,811.6 · 단가산출 계 81,812 · 임시 문서 지움
|
||||
|
||||
## 737 M02 단가산출 상세 옵션 6 열 · 합계 한 줄 바닥 고정 (요구 576 · sub2)
|
||||
|
||||
- 사용자 확정: 공통 옵션 · 인력 · 재료 · 경비 옵션 칸 = 6 열(좁으면 줄어 1 열까지) · 노무비 · 재료비 · 경비 · 계 = 1 행 · 화면 맨 아래 고정
|
||||
- 정함(브레인): 같은 공용 상세를 쓰는 M01 단가산출 상세 · 일위대가 줄 모달도 같은 꼴(모달은 모달 바닥에 고정) · 열 수 = 칸 폭 따라 6 → 4 → 3 → 2 → 1 · 부품 상자 꼴
|
||||
- [x] 737-1 옵션 칸 6 열 격자(좁으면 줄어듦) · 합계 줄 1 행 바닥 고정 — 확인 = 1920 · 1366 · 768 · 390 폭 열 수 · 합계 줄 굴려도 보임 · 금액 그대로 · 사진(밝음 · 어두움)
|
||||
- 검증완료 — 옵션 열 수 1920 → 6 · 1366 → 6 · 768 → 4 · 390 → 1(상자 폭 기준 300 · 450 · 600 · 900) · 합계 줄 한 줄 4 칸(윗면 같음) · 굴림 위 · 아래 모두 화면 바닥 고정(foot 바닥 1080 = 높이 · 끝에서 자리) · 일위대가 줄 모달 안 바닥 고정(모달 바닥 863 · 768 높이 697) · 금액 24,673 전후 같음 · M01 상세도 같은 꼴 · 시험 29 통과(신규 737 지킴 5) · typecheck 0 · ORCA 사용자 길 확인(6 열 · 바닥 1093 = 높이) · 밝음 · 어두움
|
||||
- [x] 737-2 (요구 577) 텍스트 수식 보기 = 노무비 · 재료비 · 경비 구분 상자 · 상자 맨 위 옵션 고르기(6 열 · 좁으면 줄어듦) · 계 바닥 고정 없음 · 항목별 옵션 고르기 · 바꿈 꼬리표 걷음 — 확인 = 세 상자 · 옵션 바꿈 → 금액 = 상세 보기와 같음 · 사진
|
||||
- 검증완료 — 텍스트 수식 = 노무비 · 재료비 · 경비 세 구분 상자 · 상자 맨 위 옵션 칸(지역 · 보정작업 6 열 격자) · 줄마다 붙던 옵션 꼬리표(알약) 0 · 소계 12,428 · 2,000 · 1,515 서버 그대로 · M01 텍스트 수식 같은 부품 · 밝음 · 어두움 · 768
|
||||
- [x] 737-3 (요구 578) 기본정보 다음 상자 제목 = 「단가산출 상세」(두 보기 같음 · 안 내용만 바뀜) · 제목 아래 「지금 입력 · 옵션」 꼬리표 걷음 · 공통 옵션 따로 상자 걷고 각 구분 상자 위 옵션에 같이 보임(한 값 · 어디서 바꿔도 모두 따라감) — 확인 = 제목 · 꼬리표 없음 · 공통 옵션 바꿈 → 세 상자 같은 값 · 금액 같음 · 사진
|
||||
- 검증완료 — 단가산출 상세 · 텍스트 수식 두 보기 상자 제목 모두 「단가산출 상세」 · 「지금 입력」 꼬리표 0 · 공통 옵션 따로 상자 걷고 모든 상자 맨 위에 같이 · 지역 서울로 바꿈(2번째 상자에서) → 세 상자 모두 서울 · 단가산출 상세 보기 계 24,673 → 24,761 → 되돌림 24,673 · 텍스트 수식 계 15,901 → 15,943 → 15,901 · 밝음 · 어두움 · 1366 · 768 · 390
|
||||
|
||||
## 738 M02 열기 기본 = 개인 층 · 단가산출 상세 삭제 단추 · 검색 추가 = 출처 + 수정 가능 (요구 579)
|
||||
|
||||
- 사용자 확정: 단가산출 · 일위대가 열기 = 개인 단추 누른 것처럼 · 단가산출 상세 [저장] 왼쪽 [삭제] · 검색으로 더한 항목 = 어디서 왔는지 알고 · 내부 값은 따로 고칠 수 있게
|
||||
- [x] 738-1 (sub6) M02 들어올 때 단가산출 · 일위대가 · 구조물 조합 [열기] 길 — 단가산출 · 일위대가로 오면 구조물 조합 상세 대신 그 층(기본 개인) 목록 = 개인 단추 누른 것과 같음 — 확인 = 세 길 · 주소 · 새로고침 · 권한 셋 사진
|
||||
- 검증완료 — 시스템 · 회사 관리자 · 일반 단가산출 · 일위대가 열기 = 개인 층 목록 · 구조물 조합 그대로 · 새로고침 · 대시보드 카드 길 · 밝음 · 어두움 사진 · 시험 2 통과 · typecheck 0
|
||||
- [x] 738-2 (sub2 · 737 뒤) 단가산출 상세 [저장] 왼쪽 [삭제](735 경고 · 참조 줄 지움 길 그대로) — 확인 = 쓰는 곳 있음 · 없음 · 권한 밖
|
||||
- 검증완료 — 단가산출 · 일위대가 상세 머리 단추 차례 새로고침 · 초기화 · 삭제 · 저장(1600 · 760) · 삭제 = 쓰는 곳 없음 한 줄 물음 → 목록으로 · 쓰는 곳 있음 경고 표(일위대가 · 이름 · 키 · 1째 · 층) → 확인 뒤 그 줄 지움(담은로직 2 → 1) · 취소 = 그대로 · 마스터 읽기 층 일반 · 회사 관리자 = 머리 단추 새로고침뿐(초기화 · 삭제 안 보임) · 시험 38 통과(새 8) · typecheck 0 · ORCA 8004 · 5177 일반 개인 층 · 밝음 · 어두움 · 768 · 760
|
||||
- [x] 738-3 검색 추가 = 출처 + 수정 가능 — 조사 끝(_분석_579-2) · 사용자 답(요구 581) → PLAN 740 으로
|
||||
|
||||
## 739 별칭 장부 중앙 관리 (요구 579-4 · 580 · sub1)
|
||||
|
||||
- 사용자 확정: 중앙 관리 · 진행
|
||||
- 정함(브레인): 자동으로 쓰이는 장부 = M01 중앙(마스터 층 · 승인된 짝)만 · 불러오기에서 이은 짝 = 그 불러오기에서 쓰고 중앙 승인 대기로 · 승인 전 내 제안 = 다음 불러오기에서 자동 안 붙고 「내 제안」 후보로 맨 위에 보임(한 번 눌러 고름 · 요구 20-3 「승인 받아 다음부터 자동」) · 층 장부 파일 = 버리지 않고 제안으로 읽음 · 불러오기 장부 화면 = 내 제안 목록(보기 · 고치기 · 거두기)
|
||||
- [x] 739-1 장부 읽는 차례 = 마스터(승인)만 자동 · 층 짝 = 「내 제안」 후보 · 불러오기 짝 → 승인 대기 · 장부 화면 = 내 제안 · M01 별칭 장부 승인 · 반려 그대로 — 확인 = 승인 전 다음 불러오기 자동 0 · 후보 맨 위 · 승인 뒤 자동 · 기존 층 짝 후보로 보임 · 시험 · 사진
|
||||
- 검증완료 — 시험 105 통과(739-1 새 3 · hopyo ledger · match · flow · build · screen · 729 · 733 · 718-2 m01 alias · 572) · typecheck 0 · 실측 북부청 임도 한 층 두 번(임시 storage) = 1 차 같은 불러오기 자동 248 → 2 차 승인 전 자동 0 · 내 제안 248 → M01 후보 52 승인 뒤 자동 248 · ORCA 5174 = 확인 창 「고르기 · 내 제안」 · 고르기 창 「내 제안」 1위 위 · 한 번 눌러 GC000109 체크 · 장부 화면 「내 제안 목록」 승인 칸 대기 · 거두기 · 어두움 · M01 모인 후보에 보임(승인 안 누름) · 임시 불러오기 · 장부 줄 지움
|
||||
|
||||
## 740 단가산출 · 일위대가 서로 독립 · 연결 = M01 키 · 초기화 (요구 579-2 · 581 · 근거 _분석_579-2 · sub3)
|
||||
|
||||
- 사용자 확정(581): SK · SC 서로 독립 · 연결 고리 = M01 기초 데이터 키만(알 수만 있게) · M02 문서를 넣으면 M02 키가 아니라 그 안 M01 키로 · 사용자 고친 값 초기화 = 연결된 M01 키로 다시 가져옴 · 단가산출 초기화 = 기본정보 밖 항목을 한 번에 M01 키 값으로 · [초기화] = [삭제] 왼쪽
|
||||
- 정함(브레인): 줄 = M01 키(기초 · 인력 · 재료 · 기계) + 그 문서 덮개(고친 값) · M02 → M02 참조(SK 안 SK · SC 안 SK · SC 안 SC) 없앰 → 넣을 때 그 문서 줄을 M01 키 줄 + 덮개 사본으로 펼쳐 담음 · 출처 = `본뜬`(정보만 · 셈에 안 씀) · M01 값(품 · 단가 책)은 셈 때 읽고 M01 개정 알림 그대로 · 초기화 = 덮개 · 뺀줄 걷음 · 더한줄 항목은 남기고 값만 M01 · 옵션(입력값) 남김 · 일위대가도 같은 초기화 · 기존 참조 줄(저장소 SC 참조 1) 펼쳐 옮김 · 구조물 조합 → SC 연결은 이번 범위 밖(그대로)
|
||||
- [x] 740-1 (sub3) 서버 — 넣기 펼침(SK · SC → M01 키 줄 + 덮개 사본 · 본뜬) · 참조 셈 길 걷음(옛 문서 읽기는 펼쳐 읽음) · 초기화 길(SK · SC) · 기존 참조 펼쳐 옮김 · 733 지문 · 735 삭제 · B09 굳힘 맞춤(B09 호표 블록이 덮개 반영하는지 같이 확인) — 확인 = 넣은 뒤 원본 고쳐도 안 바뀜 · 초기화 = M01 값 · 금액 전후 · 시험
|
||||
- 검증완료 — 시험 374 통과(740-1 새 10 · 727 · 728 · 729 · 733 · 735 · 715 · 717 · 719 · 725 · B09 145 · 155 · 702 · 703 · 166 · 157 · 204 · 불러오기 705 · 718 · hopyo build · flow · 572) · 실측 E1 = 옛 참조 129,073 → 481,406 · 펼친 것 129,073 그대로 · SK 넣기 펼침 전후 204,070 같음 · B09 SC 덮개 줄 = 옛 산근 6,451 대 M02 210,866 → 새 대가 블록 210,864(끝수) · 하위 호표 = 사본 블록(거창 엑셀 대조 같음 14 그대로 · 같은 기초 겹침도 블록 안) · 옮기기 실 storage 보기만 = 1/3 SC-000002 → 사본 블록 · 전후 1,446,078 같음(브레인이 --apply) · 초기화 이음 기계 = M01 한 시간 값 다시 받음 · 시험 378 통과 · ORCA 8005 · 5178 일반 계정 개인 층 SK 넣기 저장 → 상세 = 펼친 줄(변경 항목 · 계 204,070) · 원본 고침 447,400 뒤 넣은 쪽 그대로 · 초기화 미리 보기 1 · 만든 문서 지움 · 화면 파일 안 고침
|
||||
- [x] 740-2 (sub2 · 737 · 740-1 뒤) 화면 — 단가산출 · 일위대가 상세 [초기화](삭제 왼쪽 · 물음) · [삭제](738-2) · 넣은 줄 출처 꼬리표 — 확인 = 사진 · 초기화 전후 금액
|
||||
- 검증완료 — 초기화 미리 보기 = 고친 값 2 개 · 빼 둔 줄 1 개 → 물음 → 단가산출 계 24,761 → 21,893(서버 다시 셈 같음) · 옵션 지역 서울 · 덮개 · 뺀줄 걷힘 · 본뜬 그대로 · 일위대가도 같음 · 한 번 더 누름 = 「되돌릴 고침이 없음」 · 저장 안 한 고침 있음 = 먼저 물음 · 출처 꼬리표 = 단가산출 줄 이름 옆 「본뜬 SK-…」(줄 높이 그대로) + 풍선(키 · 이름 · 층 · 때) · 일위대가표 비고 칸 + 풍선 · 밝음 · 어두움 · 시험 38 통과 · 낡은 시험 4 고침(이어 붙임 목록)
|
||||
- 이음(740-1 → 740-2 · 736): 넣기 = `POST /api/m02/layers/{층}/{kind}-flat {참조, 수량?(넣을 SK 단위 수 · 기본 1), 문서?(지금 화면 문서 · 겹침 검사)}` → SK `{더한줄:[…]}` · SC `{담은로직:[…], 자재:[…]}`(화면이 끝에 붙임 · 못 펼침 400 까닭) · 초기화 = `POST …/{kind}-reset {키 | 문서, 미리보기, 판?}` → `{걷힘: 수, 문서}`(미리보기 false = 그 층에 씀 · 쓰기 권한) · 줄 `본뜬 {층, 주인, 키, 이름, 때}` = 출처 꼬리표(SK 더한줄 · SC 줄 · 자재) · 넣은 SK 한 줄 펼침 세부 cid `add:#k/add:#j` = 안 더한줄(읽기만) · 저장 · 읽기 때 옛 참조 줄도 서버가 펼침 · SC 안 SC = 사본 블록 줄 `{참조: {층: 사본, 주인: 몸 지문, 키: 본 SC, 몸: {종류, 이름, 단위, 담은로직, 자재, 본뜬}}, 별칭, 수량, 본뜬}`(원본 연결 없음 · 키 = 출처 정보 · 줄 누르면 서고 원본 대신 몸을 보임 · 셈 · 알약 키 `참조|사본|<지문>|SC-…` = 옛 참조 줄과 같은 길 · 하위 호표 끝수 · B09 하위 대가 그대로) · SC 넣기 길 답 = 그 줄 하나 · 초기화 = 사본 몸 안까지 · 이음 기계 고정 줄 단가 = M01 기계 한 시간 값 다시 받음
|
||||
- 이음(740-2 sub2 → 남): 머리 단추 [초기화] [삭제] = 새 파일 `M02_MasterTemplete_Main_Actions.ts`(UI_Main 머리에 두 단추 · `bar()` 한 줄) · 출처 꼬리표 = 단가산출 `hoView`(logic/M02_Sangeun_Cover_Model.ts) 꼬리표 칸에 `본뜬` 글 · 공용 Ho `sourceTag` 한 줄(첫 줄 = 보임 · 둘째 줄 = 풍선) · 일위대가 상세 표 이름 칸 꼬리표는 sub8 742 와 같은 표 파일이라 먼저 몫 맞춤
|
||||
|
||||
## 741 불러오기 엑셀 보기 열 폭 (요구 582-5 · sub5)
|
||||
|
||||
- [x] 741-1 엑셀 보기 열 폭 = 원본 엑셀 열 폭 따름(없으면 글 길이로 자동) · 머리 경계 끌어 열 폭 바꿈 · 두 번 눌러 맞춤 — 확인 = 실무 파일 열 폭 · 끌기 · 굴림 · 고른 칸 테두리(730) · 사진(밝음 · 어두움 · 좁은 폭)
|
||||
- 검증완료 — 시험 24 통과(741 새 4 · 704-3 · 730-2 · hopyo grid · 730 grid) + autofit 노드 시험 · typecheck 0 · xlsx 폭 없는 파일 열 폭 전부 64 px · 잘린 글 55 → 0 · xlsx 파일 폭(15열) 서버가 보냄 · 끌기 D 222 → 272 px(행 높이 같음) · 경계 오른쪽에서 두 번 눌러 맞춤 272 → 222 · 굴림 · 고른 호표 테두리 그대로(ORCA 5180 밝음 · 어두움 · Playwright 좁은 폭 760 · 높이 768) · 편집 격자 끌기 · 맞춤 같음 · 임시 불러오기 지움
|
||||
|
||||
## 742 일위대가 안 줄 수량 = 사용자 입력 저장 (요구 582-1 · 584-1 · 근거 _분석_582-123 · sub8)
|
||||
|
||||
- 사용자 확정(584-1): 일위대가 = 기초 · 요소의 조합 · 그 안 단가산출 항목 = 1 기준단위 정보(안쪽 품은 그대로) · 일위대가 하나에 단가산출이 얼마나 드는지 = 사용자 입력 · 단가산출처럼 기준단위 단가와 수량 반영 금액을 따로 보임 · 일위대가 값이 구조물 조합 · 프로젝트에서 연결됨
|
||||
- 정함(브레인): 723-1 「안 줄 수량 null」 걷음 → 줄 수량 = 저장(사용자 입력 · 처음 1) · 구조물 조합 · 공사에서 알약 · 프로젝트 값이 이어져 있으면 그 값이 앞섬(723-4 차례 = 굳힌 손 → 알약 → 저장 수량) · 불러오기로 만든 일위대가도 사용자가 다 고른 뒤 엑셀 수량을 저장 값으로 · 기존 null 줄 = 빈 칸 → 사용자 입력 · 단가산출에 일위대가를 넣으면 그 저장 수량으로 금액(「수량 없음」 없어짐)
|
||||
- [x] 742-1 서버 · 화면 — 안 줄 수량 저장 되살림 · 기준단위 단가 / 수량 반영 금액 두 칸 · 알약 · 프로젝트 값 차례 · 불러오기 수량 · 단가산출 안 일위대가 금액 — 확인 = 저장 → 다시 열기 · 구조물 조합 알약 있음 · 없음 · B09 금액 · 시험 · 사진
|
||||
- 검증완료 — 시험 267 통과(새 test_742_1 8 · 낡은 723 · 175 · 204-2 · 702-3 · 703 · 717-4 · 719 · hopyo build · flow · labor ref 고침) · typecheck 0 · B09 공사 금액 전후 같음(검증_랩탑메인 일위대가 18 줄 · 검증_sub8) · M02 구조물 조합 금액 26 전후 같음(마스터 「테스트 사전조합」 옛 손 1 · 1 이 저장 수량으로 읽혀 272,935 · item3 기준합만 바뀜 · 금액 0 그대로) · ORCA 5183 개인 층 임시 일위대가 = 열 「기준단위 단가 · 수량 반영 금액」 · 79,279.7 × 2 = 158,559.3 · 빈 줄 「수량 입력」 → 0.5 넣고 저장 → 다시 열기 2 · 0.5 그대로 · 합 203,421.6 · 글자 = 「수가 아님」 · 빈 칸 = 빈 줄 1 · 저장 안 하고 옮김 = 물음 · 마스터 읽기 칸 0 · 밝음 · 어두움 · 임시 지움
|
||||
- 이음(742-1 → 740-1 sub3): 일위대가(SC) 문서 안 줄 · 자재 `수량` = `{손: 수}`(저장 · 사용자 입력 · 새 줄 1) | null(빈 칸 · 금액 빠짐 · 「수량 입력」) · 비율 줄 = 율 그대로 · 층 셈 차례 = 알약 값(given) → 저장 손 · 공사 굳힌 사본(`refs.with_pills`) = `{저장: 수}` 로 옮겨 굳힘 → 공사 차례 = 굳힌 손(옛 사본 · 707-7) → 알약 → 저장 · 740-1 SC 펼침 수량 = 그 줄 `수량.손`(null = 펼칠 수량 없음) · `M02_Labor_Ref` 고침 = `clean_doc` 끝 · `no_qty` 지움 · `with_pills` 만(다른 함수 안 고침)
|
||||
|
||||
## 743 불러오기 엑셀 보관 안 함 (요구 582-3 · 584-2 · sub1 · 739 뒤)
|
||||
|
||||
- 사용자 확정(584-2): 불러온 엑셀 자료 자체를 보관하지 않음 · 필요하면 다시 불러와 씀
|
||||
- 정함(브레인): 서버 임시 자리 = 그 불러오기 동안만(끝내기 · 화면 떠남 · 하루 지나면 지움) · 같은 엑셀 다시 올림 막힘(409) 걷음 · 만든 단가산출 · 일위대가 안 엑셀 사본(실무 수량 · 엑셀 품 · 메모) 걷고 출처(파일 이름 · 때)만 · 옛 실무자료 파일 정리 · 별칭 제안(739)은 남김
|
||||
- [x] 743-1 임시 자리 · 지움 때 · 다시 올림 · 사본 걷음 · 옛 파일 정리 — 확인 = 끝내기 뒤 남는 파일 0 · 같은 파일 다시 올림 됨 · 만든 문서 금액 그대로
|
||||
- 검증완료 — 시험 108 통과(743 새 5 · 718-1 · 705-2 · hopyo excel · fit · screen · 729 · 739 등) · typecheck 0 · 끝내기 뒤 HP · 격자 · 맞춤 캐시 0 · 만든 SC · SK 1 단위 계 전후 같음 · 본뜬 = 파일 · 때 · 누가만 · 같은 엑셀 다시 올림 HP-000002(409 없음) · 하루 지난 HP 숨김 → 올림 때 지움 · ORCA 5174(일반 계정 개인 층): 올림 → 다른 화면 = 파일 0 · 같은 파일 다시 올림 새 키 · 페이지 새로고침 = 파일 0 · 뒤 셈 캐시 안 남음 · 옛 파일 정리 미리 보기 5 폴더 9 건(쓰기 0 · --apply 는 브레인)
|
||||
- 이음(743-1 ↔ 742-1 sub8): [끝내기] · 화면 떠남 · 하루 지남 = `M02_Hopyo_Forget.finish` 한 길 · 만든 SC · SK `본뜬` → `{층 「실무자료」, 파일, 때, 누가, 전국자재?, 복사원본?}`(키 · 원본 · 확인 · 갈래 · 엑셀품 · 못옮김 · 놓음 · 품셈 · 또(그 HP 몫) 걷음) · SC 줄 `메모` 「실무 …」 걷음 · `본뜬.실무수량` = 모든 값이 그 줄 `수량.손` 과 같을 때만 걷음(742 저장 수량이 정본 · 다르면 남김) · `Build._save` 안 고침
|
||||
|
||||
## 744 단가산출 기초 품 그대로 보임 · 되돌리기 빈틈 (요구 582-1 · sub3 → sub2)
|
||||
|
||||
- 사용자 확정(582-1): M02 단가산출에 기초를 가져오면 M01 기초를 복사 · 품 유지 · 사용자가 바꿀 수 있음
|
||||
- 정함(브레인): 둘째 기초 세부 품 = M01 원문 그대로 보이고 「몇 개(배)」는 기초 머리 수량 칸 하나로(지금 품에 곱해 0.871 → 0.00871 로 보임 걷음) · 단위 바꾼 단가산출은 728 환산 그대로(사용자 요구) · 되돌리기 빈틈 셋([기초 빼기] · 더한 항목 빼기 · 더한 항목 수량 되돌림 → 처음 값) 고침
|
||||
- [x] 744-1 둘째 기초 품 원문 + 머리 수량 칸 · 되돌리기 셋 — 확인 = GF000316 0.871 그대로 · 수량 칸 0.01 · 금액 같음 · 되돌림 → 처음 값
|
||||
- 검증완료 — 화면 몫 = 748-0(94e330a5f) · 서버 몫 끝(sub3 · 6c0211af1) — 둘째 기초 펼침 줄 원문 품 = GF000316 0.871 · 상자 몫 0.00871 · 머리 수량 0.01 · 금액 같음 · 처음수량 · 뺀더한줄 저장 · 셈 · 지문 안 듦 · 시험 4 · ORCA 8005 API 확인 · 화면(sub2) 남음
|
||||
- 이음(744-1 서버 → sub2 화면): 둘째 기초 `펼침` 줄 덧칸 `원문 {수량, 금액, 비목}` = 그 기초 1 기준단위 몫(M01 원문 품 · 머리 수량 안 곱함 · 화면 품 칸은 이것) · 줄 `수량 · 금액 · 비목` = 머리 수량 곱한 몫(상자 합 · 그대로) · 머리 줄 수량 = 더한줄 `수량` · 더한줄 항목 `처음수량`(넣을 때 화면이 적음 · 없으면 서버가 저장 때 지금 수량) · [되돌림] = 수량 ← 처음수량 · SK · SC 줄 패치 `뺀더한줄 [{차례, 줄}]` = [기초 빼기] · 더한 항목 빼기 때 화면이 옮겨 둠 · [되살림] = 그 차례에 다시 끼움(열쇠 당김 = 화면) · 셈 · 지문 안 듦 · 못 읽는 기억 줄은 서버가 버림
|
||||
|
||||
## 745 불러오기 미리 고름 (요구 582-2 · 4 · 584-3 · 근거 _분석_582-24 · sub5 · 741 뒤)
|
||||
|
||||
- 사용자 확정(584-3): 미리 고름 = 「다른 점」 줄만 · 누르기는 사용자
|
||||
- [x] 745-1 고르기 창 「다른 점」 줄에 블록 인용 조항 = 1위 기초 조항이면 미리 고름 점(자동 확정 아님) — 확인 = 북부청 표본 점 찍힘 · 맞음 비율 · 사용자가 눌러야 만듦
|
||||
- 검증완료 — 북부청 표본(임도 · 사방) 「다른 점」 28 줄 전부 점(1위 맞음 28/28 · 100 %) · 「후보」 21 줄 점 0 · 점 줄도 체크 안 됨(눌러야 만듦) · test_m02_hopyo_match 28 · test_m02_precombo_scope 7 통과 · typecheck 0 · ORCA 사진(고르기 창 점 · 후보 줄 점 0)
|
||||
|
||||
## 746 불러오기 후보 블록 줄 비교 · 요소 후보 · 단위 같은 성격 (요구 585 · 근거 _분석_585 · sub4)
|
||||
|
||||
- 정함(브레인): 지금도 일위대가는 줄마다 맞춤 · 요소로 기초를 거꾸로 찾기는 정답률이 떨어져(163 → 107) 넣지 않음 · 대신 「고름 대기」 후보 블록에도 1위 기초와 줄 단위 비교(같은 항목 · 다른 품 · 더한 항목 · 뺀 항목)를 붙여 보여 줌(판정 · 미리 고름은 그대로 = 584-3 「다른 점」만) · 못옮김 · 기계 줄에 M01 요소 후보 · 단위 모듈 hr ↔ 시간 · 개 ↔ 개소 같은 성격
|
||||
- [x] 746-1 (sub4) 서버 — 후보 블록 줄 비교(응답 칸) · 못옮김 · 기계 줄 요소 후보 · 단위 별칭 둘 — 확인 = 고름 대기 89 중 비교 붙는 블록 수(72 안팎) · 골든 1위 그대로 · 단위 고름 대기 1~3 블록 풀림 · 시험
|
||||
- 검증완료 — 고름 대기 89 중 1위 줄비교 72(셋 중 하나 82 · 골든 고름 대기 60 중 붙음 48 · 그중 1위 맞음 42) · 골든 1위 163 그대로 · 판정 수 그대로 · 단위 별칭 = 개 ↔ 개소 1 블록 견줌 풀림(hr ↔ 시간 걸린 블록 0) · 못옮김 후보 8/9 · 기계 줄 후보 32/32 · M01 로직 3,992 값 같음 · 고름 대기 블록당 +0.13 초 · 시험 215 통과 · c0319a4f5
|
||||
- [x] 746-2 (sub5 · 745 와 같이) 고르기 창에 그 비교 표 · 요소 후보 — 확인 = 사진
|
||||
- 검증완료 — 북부청 임도 표본 고름 대기 12 블록 전부 비교 표(후보 단추 앞 셋 · 표 37 줄 · 같음/품 다름/엑셀만 배지 · 후보 고름 칸과 표 동기) · 못옮김 창(단산60) 자재 요소 후보 셋 → 누름 = 고른 것 MT007130 · 북부청 사방 안 이은 줄 창 기계 줄(제11호표 콘크리트 펌프차) 요소 후보 셋 · 기초 후보 칸에 요소 후보 안 섞임 · 밝음 · 어두움 · 1366×768 · 1920×1080 · 1100 폭 사진 · 시험 11 통과(새 4 · 729-3 · 746-1) · typecheck 0 · 올린 파일 · 만든 SK 35 지움(남은 SK-000001 = sub7)
|
||||
- 이음(746-1 → 746-2 · 응답 칸): [단가산출 만들기] `고름대기[].후보[0~2].줄비교` = null(견줌 못함 · 단위 성격 다름) | `{줄 [{갈래 인력|장비|자재, 이름, 규격, 단위, 엑셀, 기초, 꼴 같음|품다름|엑셀만|기초만}], 수 {같음, 품다름, 엑셀만, 기초만}, 못옮김 [...], 환산?}` (엑셀 · 기초 = 1 단위 품 · 없으면 null) · 못옮김 · 안 이은 기계 줄(`남은줄[]` 갈래 기계) `후보` = `[{키, 갈래 노임|기계|자재, 이름, 규격, 단위, 단가}]` 셋까지(고르면 지금 잇기 · 이음 길 그대로)
|
||||
|
||||
## 747 M02 단가산출 재료 수량 · 재료비 빠짐 (요구 586 · sub7)
|
||||
|
||||
- 사용자 확정: M01 기초에는 재료 수량이 있는데 M02 단가산출로 가져오면 재료비 항목 수량이 모두 없음(개인 「테스트1」 · GC000211 6-3-1 합판거푸집) · 수량 유지 · 사용자가 바꿀 수 있게(582-1)
|
||||
- 브레인 실측: 서버 셈 답 `재료` 목록에는 수량 있음(합판 1.03 · 각재 0.038 · 폼타이 0) · 그러나 `줄`에 재료 줄 없음 · `기준` 재료비 = 소모자재 1,404.81 뿐(합판 · 각재 금액 빠짐) · 문서 덮개 · 더한줄 · 뺀줄 없음 · `자재표시` · `고름` 쪽 짐작
|
||||
- [x] 747-1 까닭(서버 셈 · 자재표시 · 고름 · 화면) → 고침: 기초 재료 줄 = M01 수량 그대로 보임 · 재료비에 듦(값 칸 = M01 기본) · 사용자가 수량 · 값 칸 바꿀 수 있음 — 확인 = 테스트1 재료 줄 수량 · 재료비 = M01 단가산출 기초 같은 입력 값 · 기존 SK 금액 전후 표
|
||||
- 검증완료 — 까닭 = SK 셈이 바탕 로직 재료에 일위대가 기본(별도계상 · 162-1)을 써 줄 · 재료비에서 뺌 → SK 만 빈 칸 = 직접계산(일위대가 덮개 줄 그대로) · 테스트1 재료비 1,404.81 → 36,525.06 · 계 89,299.60 → 124,419.85 = M01 GC000211 같음 · 합판 1.03 · 각재 0.038 · 폼타이 0 · 화면 합판 2 덮개 → 재료 소계 47,979(폐기로 되돌림 · 파일 같음) · 다른 기초 6(GF000002 · 011 · 012 · GC000001 · 002 · 014) M01 같음 · 기존 SK 17 중 바뀜 3(GC000211 바탕 · 개인 1/10 SK-000003 36,069.61 → 47,553.94 · SK-000008 59,255.22 → 74,813.50 · 1/3 SK-000002) 14 같음 · 시험 295 + 새 3 통과 · typecheck 0
|
||||
|
||||
## 748 단가산출 상세 화면 정리 (요구 588-5~9 · 요구서_588 · sub2)
|
||||
|
||||
- 사용자 확정: 588-5~9 원문
|
||||
- 정함(브레인): 공용 상세라 M01 단가산출 기초 화면도 같이 · 744 화면 몫(둘째 기초 품 원문 + 머리 수량 칸 · 되돌림 셋) 먼저
|
||||
- 이음(748 sub2 → 남): 공용 상세 줄 모달 = `Modal.openMaterialModal` edit 인자 (close, head) · 연결 키 = 제목 오른쪽 · `HoEdit.more` = {head, body} · 텍스트 수식 줄 누름 = `Ho.lineOpener(ctx)` → `FormulaContext.open` · 바닥 줄 = 새 `LogicLab_Foot.buildFoot` · 둘째 기초 머리 = 새 `Sangeun_BaseHead` · 되돌리기 기억 = 새 `logic/M02_Sangeun_Undo`(문서 `뺀더한줄` · `처음수량` · 일위대가 줄 패치 `뺀더한줄`) · 세부 줄 품 = 서버 `원문.수량`(`hoView` 줄 수량 교체 · 금액 · 비목 그대로)
|
||||
- [x] 748-0 (744-1 화면 몫) 둘째 기초 세부 품 = 원문 · 기초 머리 수량 칸 하나 · 되돌림 셋([기초 빼기] · 더한 항목 빼기 · 더한 항목 수량) 화면 — 확인 = GF000316 0.871 · 수량 칸 · 되돌림 → 처음 값
|
||||
- 검증완료 — 검척(GF000316) 둘째 기초 추가: 세부 품 0.871 그대로 · 머리 수량 칸 0.01 · 금액 2,051(= 0.00871 × 235,459 · 계 24,673 → 26,724 합 +2,051) · 수량 0.05 → 되돌림 → 계 24,673.21 같음 · [기초 빼기] → [되살리기] 계 24,673.21 같음 · 빈 칸 = 「0 이상 수만」 값 안 바뀜 · 시험 test_748_0 + 727 · 728 · 717 · 740 통과 · typecheck 0 · 사진 tmp/shots_748/m_*
|
||||
- [x] 748-1 줄 모달 = 머리 제목 오른쪽 (연결 키) · [적용] [삭제](← 빼기) [닫기] 차례 · 옛 빼기 아래 정보 걷음 — 확인 = 사진 · 적용 · 삭제 동작 그대로
|
||||
- 검증완료 — 제목 「보통인부 (LB000002)」 · 머리 [적용] [삭제] [닫기] 차례(ORCA · Playwright 밝음 · 어두움 · 1366 · 768) · 적용 0.05 → 계 24,673 → 26,566 · 삭제 → 되살리기 줄 · M01 단가산출 기초(GF000002) 모달 [적용] [닫기]
|
||||
- [x] 748-2 텍스트 수식 = 행 고르기 걷음 · 이름식 변수명 · 값 든 식 값마다 누름 → 748-1 모달 — 확인 = 변수 · 값 누름 → 그 항목 모달
|
||||
- 검증완료 — 행 고르기 없음 · 이름식 변수 · 값 든 식 수 하나하나 단추(첫 3 줄 8 · 5 · 9 개) 누름 → 같은 줄 모달 · M01 GF000002 단추 44 개 누름 → 벌목부 (LB000037) [적용] [닫기] · 시험 test_748_2
|
||||
- [x] 748-3 상세 보기 항목 옵션 값 꼬리표 걷음 · 텍스트 수식 바닥 노무비 · 재료비 · 경비 고정(계 없음) · 상자 사이 간격 = 바닥 줄 위 여유 — 확인 = 사진(밝음 · 어두움 · 1366 · 768) · 금액 그대로
|
||||
- 검증완료 — 줄 옵션 값 칩 걷음 · 텍스트 수식 바닥 노무비 12,428 · 재료비 1,958 · 경비 1,515 세 칸(계 없음 · 상자 소계 그대로) · 상자 사이 간격 12px = 바닥 줄 위 여유 12px(상세 · 텍스트 수식 같음) · 금액 전후 같음(24,673.21) · 시험 test_737
|
||||
|
||||
## 750 단가산출 보기 넷 셈 하나로 (요구 588-1 · 근거 _분석_588-134 · sub3 → sub2)
|
||||
|
||||
- 사용자 확정(588-1): 네 보기 셈은 같고 보이기만 다름 · 상세 보기가 맞음 · 옵션 바꿈 셈 빨라짐
|
||||
- 정함(브레인): SK 셈 답 하나에 텍스트 수식 · 시험 계산 줄까지 실어 네 보기를 그 답 하나로 그림(바탕 M01 로직 따로 셈 걷음 · 별도계상 · 단위 환산 마무리) · 옵션 바꿈 = 서버 셈 한 번 · 일위대가 줄 모달 안 상세도 같은 답(747 한쪽만 든 흠 · 124,419.85 대 89,299.60) · M01 화면은 그대로 동작
|
||||
- [x] 750-1 (sub3) 서버 — SK 셈 답에 텍스트 수식 · 시험 계산 줄 · 일위대가 줄 모달 상세 같은 답 — 확인 = 변형 일곱(줄 빼기 · 더하기 · 덧줄 · 별도계상 · 단위 환산 · 재료 고름 · 기본) 네 보기 줄 · 계 같음 · 옵션 바꿈 호출 수 · 시간 전후
|
||||
- 검증완료 — 테스트1 변형 일곱(기본 12 줄 124,419.85 · 줄 빼기 11 · 99,589.41 · 더하기 13 · 133,023.25 · 덧줄 124,771.06 · 별도계상 11 · 112,725.23 · 단위 환산 1,244,198.53 · 재료 고름 124,563.39) 상세 · 인력재료경비 · 시험 계산 · 텍스트 수식 줄 · 계 같음(8005 HTTP · cid 대조) · 무작위 로직 120 · storage SK 17 · 참조 바탕 같음 · 일위대가 줄 모달 = 그 줄(storage · 마스터 일위대가 150 줄 다름 23 → 0 · ORCA 개인 층 GC000211 모달 89,300 = 줄 89,299.6 · 임시 사전조합 지움) · 옵션 바꿈 부름 5 → 2 · HTTP 약 3.3 초 → 0.5 초 · M01 /m01/calc · /text 124,419.85 12 줄 그대로 · 시험 57 통과 · typecheck 0
|
||||
- [x] 750-2 (sub2 · 748 뒤) 화면 — 네 보기가 750-1 답 하나로 그림 · 옵션 바꿈 한 번 셈 — 확인 = 테스트1 · 변형 넷 보기 같음 · 시간
|
||||
- 검증완료 — 테스트 변형 일곱(기본 · 줄 빼기 · 더하기 · 덧줄 · 별도계상 · 단위 환산 · 재료 고름) 네 보기 줄 수 · 이름 · 계 같음(상세 · 인력재료경비 · 시험 계산 · 텍스트 수식 = 124,420 · 99,589 · 133,023 · 124,771 · 112,725 · 1,244,199 · 124,563 · 별도계상 합판은 상세 · 계산서에 기준 밖 줄로 남고 시험 계산 · 텍스트 수식엔 없음) · 열 때 /logic-lib/calc · text 부름 0 · 옵션 한 번 바꿈 = SK 셈 2 부름(답 + 바탕 원값 · 옛 5 부름) · 서버 셈 0.05 초 대 옛 2.9 초 · 브라우저 0.5 초 · 재료 고름 = 문서 고름(cid) 1 회 셈 · 관련 시험 묶음 통과(41 · 36 · 35 · 새 3) · typecheck 0 · 사진 tmp/shots_750/750_* (밝음 · 어두움 · 1366 · 768) · 일위대가 줄 모달 = 같은 상세(서버 줄모드 시험)이라 화면 별도 확인 못 함(개인 층에 일위대가 없음) · 커밋 db590e6fa
|
||||
- 이음(750-1 → 750-2): `POST /layers/{층}/sangeun/calc` 답 덧칸 — `글` = M01 `/text` 답 꼴(ok · groups[비목 · 줄 · 소계 · 끝수] · 계 · 별칭 · 기준단위 · 줄 칸 = 이름 · 글 · 이름글 · 조각 · 요소조각 · 금액 · 까닭 · 짝 · annotate 덧칸 + `cid`) · 글 금액 · 소계 · 계 = SK `줄` · `기준` 그대로(별도계상 뺌 · 단위 환산 ÷배 · 못 셈 줄 까닭) · 줄마다 `cid` = SK 줄 cid · `글.나눔` = 배(≠1 · 글 식은 바탕 단위) · `중간` = 시험 계산 중간값(수 · 글만) · 시험 계산 = {ok: true, lines: `줄`, sums: `기준`, middle: `중간`} · 재료 고름 = 문서 `고름`(cid → MT) · 일위대가 줄 모달 = 문서 `줄모드: true`(`lineDoc` · 750-1 이 넣음) · 몸 `보기: false` = 덧칸 안 지음(바탕 원값 셈 `plain` 에 · 서버 품 절반) · 옛 `/logic-lib/calc` · `/text` 부름 걷음은 750-2
|
||||
|
||||
## 752 M02 단가산출 우측 원문 보기 (요구 588-3 · sub2 · 750-2 뒤)
|
||||
|
||||
- 정함(브레인): 일위대가 상세가 쓰는 원문 서버 길 · 원문 보기 부품 재사용 · 바탕 · 둘째 기초가 여럿이면 기초마다 탭 · 일위대가 줄 모달 안에서는 끔
|
||||
- [x] 752-1 단가산출 상세 우측 원문 패널 — 확인 = 테스트1 6-3-1 원문 · 기초 둘 탭 · 좁은 폭 접힘 · 사진
|
||||
- 검증완료 — 테스트 6-3-1 바탕 원문 패널(건설품셈 공통 6-3-1 · 6-03_거푸집.md) · 기초 둘(검척 7-13 더함) = 탭 둘 · 둘째 탭 = 산림품셈 7-13 원문 · 1600 처음 열림(밝음 · 어두움) · 1366 · 768 처음 접힘(세로 띠 · 누르면 열림) · 일위대가 줄 모달(RowSk)은 source 안 켬(시험 대조) · 시험 30 통과 · typecheck 0 · 사진 tmp/shots_750/752_* · 커밋 da45997eb
|
||||
|
||||
## 753 옵션으로 0 이 되는 줄 숨김 (요구 588-4 · sub4 → sub2)
|
||||
|
||||
- 사용자 확정(588-4): 옵션에 따라 하나만 보일 재료가 옵션 전체 · 0 으로 보임(폼타이) · M01 도 같음 · 조심히 확인 후 수정
|
||||
- 정함(브레인): (가) 서버가 셈 답 줄에 「옵션으로 빠진 줄」 표시 → M01 · M02 화면 모두 숨김(M01 정본 자료는 안 고침 · 금액 그대로) · 153 로직 356 줄 · M01 정본 줄 합치기(15 로직 39 줄)는 사용자 답 뒤(아침 물음) · 폼타이 자재 단가 빈 자료는 아침 물음
|
||||
- [x] 753-1 (sub4) 서버 표시(M01 셈 · 계산서 · SK 셈 같은 꼴) — 확인 = 테스트1 폼타이 고른 한 줄만 · 153 로직 표시 수 · 금액 전후 같음 · 시험
|
||||
- 검증완료 — 테스트1(SK 셈 · M01 /calc · /text 셋 같음) 폼타이 아님 = 셋 표시 · 사용 9.5 = 12.7 · 7.9 표시 · 9.5 만 표시 없음 · 구멍땜 0 줄 표시 없음 · 계 124,419.85 그대로 · 153 로직 고르기 값 하나씩 돌림 = 스캔 줄 0 관측 4,581 전부 표시 · 표시 붙은 로직 141 · 시험입력 한 벌 = 표시 191 줄 · 129 로직 · 0 인데 표시 없음 2(GC000480 표 칸 0 = 진짜 0) · 0 아닌데 표시 0 · 돈 로직 1,440 금액 전후 같음 · 셈 시간 그대로 · 시험 287 + 새 9 통과 · 정함: 수 입력 문턱(GC000187 타설길이 ≤ 20 장비 택일) · 범위 조건(GC000769 규격 15~50 보통인부 0)도 표시 · a36092fad
|
||||
- [x] 753-2 (sub2 · 750-2 와 같이) 화면 숨김(M01 · M02 공용 상세) — 확인 = 사진 · 옵션 바꿈 → 보이는 줄 바뀜
|
||||
- 검증완료 — 테스트 GC000211(폼타이 셋) — 폼타이 아님 = 상세 · 인력재료경비 · 시험 계산 · 텍스트 수식 넷 모두 폼타이 줄 0 · 사용 + 12.7 = 12.7 한 줄 · 9.5 = 9.5 한 줄(옵션 칸 그대로 · 바꿈 = SK 셈 2 부름 · 보이는 줄 바뀜) · 계 124,420 그대로 · M01 단가산출 기초 GC000211 같음([계산] 뒤 9 줄 · 사용 → 폼타이 12.7 한 줄 · 계 같음) · 관련 시험 86 통과(새 test_750_2 · 753-1 · 750-1 포함) · typecheck 0 · 사진 tmp/shots_750/753_* · 밝음 · 어두움 · 1366 · 768 · 커밋 a773a9d2c · 참고: test_719_4 가 B09 sheet 의 formatNumber 때문에 실패(sub6 751-1 몫 · 내 변경 밖)
|
||||
- 이음(753-1 → 753-2 · 750-1 · 응답 칸): 엔진 줄 덧칸 `옵션꺼짐: true`(아니면 칸 없음 · `master_gate`) — M01 `/calc` `lines[]` · `/text` · M02 `/logic-lib/sheet` 줄(`master_sheet_line.KEYS`) · SK 셈 답 `줄[]` · 둘째 기초 `펼침[]` 같은 칸(엔진 `run` 한 곳에서 붙음 · SK 는 저절로) · 가름 = 수량 0 이 `만약` 간 갈래 상수 0 탓(고르기 · 수 입력 문턱 · 중간값 거침) · 입력 0 · 표 칸 0 · 원문 0 = 칸 없음 · 금액 · 차례 · cid 그대로 · 화면은 그 줄을 안 그림
|
||||
|
||||
## 755 B05 예정노선 수정 흐름 (요구 589-1 · 3 · 5 · 6 · 7 · 8 · 근거 _분석_589-A · sub1)
|
||||
|
||||
- 정함(브레인): 켠 동안 선 끊김 허용(가운데 묶음 지움 됨) · 끌 때 검사 = 끊겨 있으면 알림하고 안 끔(이어 긋기 · [잇기]) · 계획 평면노선은 켠 동안 안 바꿈 → 끌 때 한 번 추적(폴리라인 = 직선 + 곡선 · 둘째 단계 = 곡선 가운데 20 m 측점 맞춤) · 켠 동안 자기 되돌리기 · 길이재기 = 켠 동안도 먹음(예정노선 재기 · 두 점) · 받은 노선 색 더 잘 보이게 · 설명 걷음 · B05 위치 상자 걷음 · 「둘째 단계 안 됨」 뜻은 아침 물음(받은 노선이 화면 예상보다 280 m 긴 흠 · 측점 밀림은 같이 가름)
|
||||
- [x] 755-1 켠 동안 끊김 허용 · 끌 때 검사 · 끌 때 한 번 추적 · 자기 되돌리기 — 확인 = 가운데 묶음 지움 · 끌 때 계획선 갱신 · 측점 31 곳 맞춤
|
||||
- 검증완료 — 검증_sub1 가운데 토막 6개(100 m) 지움 → 두 토막 [31, 28] · 켠 동안 follow 0 · 끌기 막힘 알림 · Ctrl+Z · 도구줄 되돌림 됨 · 이은 뒤 끌 때 follow 한 번(63 점 → 노드 27 · 곡선 25 · MC 20 m 측점 위 25/25) · 안 고치고 끔 follow 0 · ORCA 같은 길 · 시험 68 · typecheck 0 · dfba0e7b5
|
||||
- [x] 755-2 길이재기 · 받은 노선 색 · 설명 걷음 · 위치 상자 걷음 — 확인 = 사진 · 재기 수치
|
||||
- 검증완료 — 켠 동안 재기 선 따라 360.0 m · −1.09 % · 두 점 256.5 m(지형 밖) · Esc 지움 · 받은 노선 분홍 점선 + 검은 테(밝음 · 어두움 · 768) · 켬 설명 · 위치 칸 없음 · D03 칸 위치 · 지점 · 그리기 · 구역 그대로 · dfba0e7b5
|
||||
- [x] 755-3 받은 노선 280 m 긴 흠(source 를 csv 양끝에서 자름) · 켠 채 [저장] · 떠남 알림 — 측점 밀림 19+10 → 17+18 은 안 고침(아침 물음)
|
||||
- 검증완료 — source = csv 1,097.4 m(검증_sub1 전후 같음 · 검증_sub2 1,377.4 → 1,097.4 · follow 1,356.5 → 1,080.2) · 켠 채 판 고침 [저장] POST 0 + 알림 · 떠남 물음 · 취소 = 머묾 · 탭 새로고침 = 브라우저 물음 · 되돌리면 안 물음 · 밝음 1080 · 어두움 768 · ORCA 같은 길 · 시험 127 · typecheck 0 · 8b3560c4c · 9d5778bf5
|
||||
- 이음(755 ↔ 756): `B05_Profile_UI_RouteEdit.ts` 755 몫 = `createExpectedDraw` 인자(measuring · onHistory · frame 걷음) · `bindHistoryControls` 의 local 한 줄 · 받은 노선 색 = 기존 `--draw-map-route-3`(theme.css 안 고침) · Render · Chrome · SnapStep 안 고침 · 635_3 RouteEdit 줄 한도 683 → 690
|
||||
|
||||
## 756 B05 · D03 도구줄 · 등고선 (요구 589-2 · 4 · 10 · 12 · sub5)
|
||||
|
||||
- 정함(브레인): 켠 동안 추천 선 진한색 그대로 · 스냅 + 스냅 간격 한 아이콘 · 추천노선 = 여럿 고르기(색 구분 · 「끔」 걷음 · 하나도 안 고르면 꺼짐 · 다른 아이콘이 쓰는 여럿 고르기 부품 재사용) · 켠 동안 등고선 = 켜기 전 지도 영역 또는 보는 영역 따라 불러옴(D03 와 같은 길 · LAS 면 전체)
|
||||
- [x] 756-1 추천 선 색 · 스냅 하나 · 추천노선 여럿 고르기 · 등고선 영역 — 확인 = 사진 · 등고선 띠 폭 전후(215 → 144 px 걷힘)
|
||||
- 검증완료 — 시험 756_1(새) · 757_1(새) · 624_2 · 634_2 · 626_1 · 636_2 · 390_4 · 553_4 · 626_3 통과 · typecheck 0 · 켠 동안 추천 선 색 픽셀 수 끔과 같음(진한 그대로) · 스냅 한 아이콘(측점 No. · 10 m · 끔) · 추천노선 둘 고름(색 점 · 점선 꼴 다름) · LAS 켠 동안 띠 없음 · 도엽은 계획선 + 예정노선 띠(시험 3 건) · ORCA 5180 길 확인
|
||||
|
||||
## 757 D03 · B05 그리기 칸 긋기 단추 걷음 (요구 589-9 · sub5 · 756 뒤)
|
||||
|
||||
- 정함(브레인): 긋기 단추 걷고 옵션 넷 켬끔으로 시작 · 끝(하나라도 켜면 긋기 시작 · 모두 끄면 끝) · D03 · B05 같은 키트
|
||||
- [x] 757-1 옵션 넷 켬끔 = 시작 · 끝 — 확인 = D03 · B05 긋기 시작 · 끝 · 사진
|
||||
- 검증완료 — D03 · B05 [긋기] 단추 없음 · 방식 켬 = draw · 다른 방식 = draw 유지(방식만 바뀜) · 고른 방식 다시 누름 · Enter · Esc = idle + 칸 모두 풀림 · 노드 판 그대로 · 시험 757_1 · 553_4 · 626_3 통과 · typecheck 0 · ORCA 5180
|
||||
|
||||
## 754 B04 등고선 = 지표면 외곽 · 횡단배수 첫 열림 빈 폼 (요구 589-11 · 13 · 근거 _분석_589-B)
|
||||
|
||||
- 정함(브레인): 11 = 어느 방식을 지어도 그 필터 dtm 이 낡았으면 같이 다시 지음(한 곳) · 등고선 캐시 판단에 dtm · 격자 파일 때 넣음 · ptd 확정 시 dtm_ptd 없음 흠도 같이 · 사용자 프로젝트(가이드 · 테스터) 다시 짓기는 아침 물음 · 13 = 재현 안 됨 · 첫 열림 한 번 빈 폼 → 경쟁 조건 가름(새 탭 · 캐시 빈 · 구조물 고른 채 B06 첫 열림 반복)
|
||||
- [x] 754-1 (sub7) 등고선 · 격자 외곽 = 지금 지표면 외곽 · 캐시 판단 — 확인 = 검증 프로젝트 하나 다시 지어 등고선 외곽 = 3D 외곽 · ptd 확정 길 · 시험
|
||||
- 검증완료 — 검증_sub7 낡은 외곽 흉내(dtm 28.2 만 · 지문 다름) 뒤 [서피스 생성] classification·implicit = dtm · 격자 34.46 만 m² · glb 34.16 만 · 등고선 밖 0.16 % · dtm 새것이면 등고선 다시 셈 · ptd·implicit dtm_ptd 생김(36.19 만) → 확정 체인 B05 · B06 완료 · 노선 181 점 표본 181 · 시험 50 통과 · typecheck 0 · 3f4fd188e
|
||||
- [x] 754-2 (sub8) 횡단배수 좌측 폼 첫 열림 빈 폼 재현 · 까닭 · 고침 — 확인 = 새 탭 반복 20 번 빈 폼 0 · 사진
|
||||
- 검증완료 — 재현 B06 고름 → 첫 B05 5/5 · B05 이른 고름 3/5 빈 폼(공용 폼 setTypes 가 군 · 종류만 비움) → 고친 뒤 새 탭 24+20+10 번 빈 폼 0 · 옵션 값(관경 1000 · 집수정 1.5 · 기슭막이 10) 예정노선 수정 켬끔 · 새로고침 · 1080/768 · 밝음/어두움 그대로 · 저장 → 다시 열기 같은 값 · 시험 38 · typecheck 0 · 검증 값 되돌림
|
||||
@@ -0,0 +1,21 @@
|
||||
# 사용자 요구 장부 (11-03)
|
||||
|
||||
## 요구 590 (11-03 · 사용자 · 감시 점검)
|
||||
|
||||
감시스크립트 이상함 점검. 서브3 질문 맞음
|
||||
|
||||
## 요구 591 (11-03 · 사용자 · 590 이어서)
|
||||
|
||||
서브3의 bash 승인은 내가 넣었음
|
||||
|
||||
## 요구 592 (11-03 · 사용자)
|
||||
|
||||
bash 승인으로 종종 걸림 대책 확인
|
||||
|
||||
## 요구 593 (11-03 · 사용자 · 592 이어서)
|
||||
|
||||
그럼 정리는 어떻게 함? 삭제는 대부분 tmp 폴다 같은데 tmp폴다는 마음대로 써도 됨.
|
||||
|
||||
## 요구 594 (11-03 · 사용자 · 593 이어서)
|
||||
|
||||
지금까지 대부분 지우기가 tmp폴다가 맞지? 최근 일주일만 보면됨
|
||||
@@ -76,9 +76,7 @@ def test_D03_긋기_고르기_메뉴_속성판_그대로_한_선():
|
||||
for fake in ("NO_POINTS", "epsg: () => 0", "addPoint: () => undefined", "oneLine"):
|
||||
assert fake not in ui, fake
|
||||
assert "if (!kit.key(event)) return;" in ui # 같은 단축키(키트)
|
||||
assert (
|
||||
'keyIs(event, "plan.undo") || keyIs(event, "plan.redo")' in ui
|
||||
) # 되돌리기 = 페이지 사진첩
|
||||
assert '? (step("undo"), true)' in ui # 켠 동안 Ctrl+Z = 이 판 사진첩(755-1)
|
||||
# 지도 입력 = 키트 한 함수 · 잡기 단계(capture) — 재기 · 계획선 고르기보다 먼저
|
||||
assert "capture: { active: () => open, busy: () => busy }," in ui
|
||||
assert "canvas.addEventListener(type, run, !!capture)" in kit
|
||||
@@ -99,7 +97,7 @@ def test_D03_긋기_고르기_메뉴_속성판_그대로_한_선():
|
||||
for src in (draw, menu, props):
|
||||
assert "oneLine" not in src
|
||||
table = _src(D03 / "D03_PlanAnalysis_UI_Features.ts")
|
||||
# 638-3 — 세트 칸 · 세트 줄 = B05 보기만 · [잇기] = B05 숨김 · 세트 고침 = assignSet 한 길
|
||||
# 638-3 — 세트 칸 · 세트 줄 = B05 보기만 · 세트 고침 = assignSet 한 길 · 755-1 [잇기] = B05 사용(끊긴 곳 이음)
|
||||
assert 'disabled: !params.editable() || !featureOn(page, "set")' in props
|
||||
assert 'featureOn(page, "join") && edit.isEnd(' in props + menu
|
||||
assert "setPiecesYear" not in props and "setLineYear" not in _src(
|
||||
@@ -109,7 +107,7 @@ def test_D03_긋기_고르기_메뉴_속성판_그대로_한_선():
|
||||
("branch", "hide"),
|
||||
("years", "view"),
|
||||
("set", "view"),
|
||||
("join", "hide"),
|
||||
("join", "use"),
|
||||
("points", "hide"),
|
||||
("draw", "use"),
|
||||
):
|
||||
|
||||
@@ -133,6 +133,31 @@ def test_plan_source_skips_densify_keeps_trim(tmp_path: Path, monkeypatch) -> No
|
||||
assert asyncio.run(source.read_plan_source(uuid4())).status_code == 404
|
||||
|
||||
|
||||
def test_plan_source_clips_to_expected_csv_ends(tmp_path: Path, monkeypatch) -> None:
|
||||
"""755-3 — 지표면이 넓어져 트림이 덜 잘라도 체인 예정노선 파일 양끝에서 자름(같은 길이)."""
|
||||
monkeypatch.setattr(
|
||||
source,
|
||||
"load_design_route",
|
||||
lambda *a: SimpleNamespace(vertices=[SimpleNamespace(x=x, y=y) for x, y in SPARSE]),
|
||||
)
|
||||
monkeypatch.setattr(source, "build_surface_sampler", lambda *a: object())
|
||||
monkeypatch.setattr(source, "trim_route_to_surface", lambda points, sampler: points)
|
||||
surface = {"source_filter": "classification", "method": "dtm", "smooth": True}
|
||||
dense = densify_route(SPARSE[3:30], source.SPACING_M)
|
||||
write_route_csv(expected_route_csv_path(tmp_path), [{"x": x, "y": y} for x, y in dense])
|
||||
got = source.source_vertices(tmp_path, surface, None)
|
||||
|
||||
def length(points):
|
||||
return sum(math.dist(a, b) for a, b in zip(points, points[1:]))
|
||||
|
||||
assert abs(length(got) - length(dense)) < 1e-3 # 파일 좌표 0.1 mm 반올림
|
||||
assert math.dist(got[0], SPARSE[3]) < 1e-3 and math.dist(got[-1], SPARSE[29]) < 1e-3
|
||||
assert len(got) == 27 # 끝 = 꼭짓점 바로 앞 · 겹친 점 없음
|
||||
# 파일 없음 = 그대로
|
||||
expected_route_csv_path(tmp_path).unlink()
|
||||
assert len(source.source_vertices(tmp_path, surface, None)) == len(SPARSE)
|
||||
|
||||
|
||||
MAIN = {
|
||||
"id": "main",
|
||||
"vertices": [[200000.0, 450000.0], [200100.0, 450000.0], [200200.0, 450000.0]],
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""635-3 B05 왼쪽 「위치」 · 「그리기」 칸 · 「예정노선 수정」 · 받은 노선 비교 · [받은 노선으로] · 어둡게(요구 546).
|
||||
"""635-3 B05 왼쪽 「그리기」 칸(755-2 위치 칸 걷음) · 「예정노선 수정」 · 받은 노선 비교 · [받은 노선으로] · 어둡게(요구 546).
|
||||
|
||||
- 칸 = 예정노선 키트(`createPlanKit` 「b05」) 칸 · 자리 = `registerPanelSection` order 1 · 2(종단 보기) · 편집기가 채움.
|
||||
- 위치 = 찾기 → 지도 이동만(지형 받기 · 기준점 없음) · 고칠 선 = 받은 원본 꼭짓점(`GET /route/plan/source` · 635-2).
|
||||
- 칸 = 예정노선 키트(`createPlanKit` 「b05」) 칸 · 자리 = `registerPanelSection` order 1(종단 보기) · 편집기가 채움.
|
||||
- 위치 칸 = 기능 표 「site」 b05 숨김(589-8) · 고칠 선 = 받은 원본 꼭짓점(`GET /route/plan/source` · 635-2).
|
||||
- [받은 노선으로] = 받은 꼭짓점 다시 추적 + 초안 `expected` [](고친 예정노선 지움) — 한 걸음 · 페이지 [초기화] 그대로.
|
||||
- 켬 = 아래 계획노선 그림 불투명 `--draw-map-dim`(0.35) · 등고 높이값 · 예정노선 그림은 그대로.
|
||||
- 큰 파일 안 늘림(RouteEdit 683 · Render 648 + 10 안).
|
||||
- 큰 파일 안 늘림(RouteEdit 690 · Render 648 + 10 안).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_635_3_b05_plan_panel.py -q
|
||||
"""
|
||||
@@ -22,10 +22,10 @@ def _src(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_왼쪽_칸_둘_종단_보기_위():
|
||||
def test_왼쪽_칸_하나_종단_보기_위():
|
||||
panel = _src(B05 / "B05_Profile_UI_Panel_Plan.ts")
|
||||
assert '["site", 1, "위치"]' in panel and '["draw", 2, "그리기"]' in panel
|
||||
assert 'views: ["profile"]' in panel and "id: `plan-${slot}`" in panel
|
||||
assert 'id: "plan-draw",' in panel and 'title: "그리기",' in panel and "order: 1," in panel
|
||||
assert 'views: ["profile"]' in panel and '"site"' not in panel and '위치"' not in panel
|
||||
assert "export function fillPlanPanel(" in panel
|
||||
assert "holder.hidden = !card;" in panel # 비면 자리 숨김
|
||||
# 다른 칸(구조물 배치 10 · 페이지 설정 20)보다 위
|
||||
@@ -33,18 +33,18 @@ def test_왼쪽_칸_둘_종단_보기_위():
|
||||
assert "order: 20" in _src(B05 / "B05_Profile_UI_Panel_Sections.ts")
|
||||
|
||||
|
||||
def test_키트_칸_위치_찾기_이동만_원본_꼭짓점():
|
||||
def test_키트_칸_위치_걷음_원본_꼭짓점():
|
||||
ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts")
|
||||
body = ui.split("*/", 1)[1]
|
||||
assert 'page: "b05",' in body and "cards: { site: { parts: [find.root] } }," in body
|
||||
assert "fillPlanPanel({" in body and "createPlaceFind(" in body
|
||||
assert "coords.toPlan(hit.lon, hit.lat)" in body # 지도 이동만(서버 변환 · 635-5)
|
||||
assert 'page: "b05",' in body and "cards: {}," in body
|
||||
assert "fillPlanPanel(drawCard);" in body and "createPlaceFind" not in body
|
||||
assert "toPlan" not in body and "frame" not in body # 찾기 이동 걷음(589-8)
|
||||
for banned in ("createPlanTerrain", "ensureCovered", "createPlanSite", "createFloatPanel"):
|
||||
assert banned not in body, banned
|
||||
assert "/route/plan/source" in body and "expected_source" in body
|
||||
assert "shown === original ? received() : shown" in body # 안 고친 선 = 원본 꼭짓점
|
||||
features = _src(ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Features.ts")
|
||||
assert re.search(r'key: "site",[^}]*b05: "use"', features)
|
||||
assert re.search(r'key: "site",[^}]*b05: "hide"', features, re.S)
|
||||
assert re.search(r'key: "terrain",[^}]*b05: "hide"', features, re.S)
|
||||
|
||||
|
||||
@@ -56,10 +56,10 @@ def test_수정_켬끔_받은_노선_비교_되돌림():
|
||||
assert "받은 노선 ${meters(received())} m → 고친 노선 ${meters(line())} m" in body
|
||||
assert 'label: "받은 노선으로"' in body and "follow(received(), [])" in body
|
||||
assert "resetButton.disabled = !edited || busy;" in body
|
||||
assert "if (!sameLine(points, base)) await follow(points, points);" in body
|
||||
assert "return sameLine(points, base) || (await follow(points, points));" in body # 끌 때 한 번
|
||||
main = _src(B05 / "B05_Profile_UI_RouteEdit.ts")
|
||||
assert "frame: () => ({ meta, setView: (next: ViewState) => (view = next) })," in main
|
||||
assert len(main.splitlines()) <= 683
|
||||
assert "frame:" not in main
|
||||
assert len(main.splitlines()) <= 690
|
||||
|
||||
|
||||
def test_켬_때_아래_노선_어둡게():
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""635-5 B05 경위도 ↔ 사업지 좌표 서버 변환(요구 546) — 찾기 이동 · 속성 판 경위도.
|
||||
"""635-5 B05 경위도 ↔ 사업지 좌표 서버 변환(요구 546) — 속성 판 경위도(찾기 이동 = 755-2 걷음).
|
||||
|
||||
- 서버 `POST /route/coords` = 작업 좌표계(`resolve_project_crs` · WKT 그대로) pyproj 묶음 변환.
|
||||
- 화면 = 찾기 이동 · 노드 고름 때만 부름 · 같은 점 캐시 · 바탕 `tm_grid` 늘려 쓰기 걷음.
|
||||
- 화면 = 노드 고름 때만 부름 · 같은 점 캐시 · 바탕 `tm_grid` 늘려 쓰기 걷음.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_635_5_b05_coords.py -q
|
||||
"""
|
||||
@@ -50,12 +50,12 @@ def test_라우터_등록():
|
||||
assert "resolve_project_crs(root, route_crs_input=" in router
|
||||
|
||||
|
||||
def test_화면_서버_변환_찾기_고름만():
|
||||
def test_화면_서버_변환_고름만():
|
||||
panel = (B05 / "B05_Profile_UI_Panel_Plan.ts").read_text(encoding="utf-8")
|
||||
assert "`/projects/${projectId}/route/coords`" in panel # requestJson 이 /api 를 붙임
|
||||
assert "known.has(key)" in panel # 같은 점 캐시
|
||||
ui = (B05 / "B05_Profile_UI_ExpectedDraw.ts").read_text(encoding="utf-8")
|
||||
assert "coords.toPlan(hit.lon, hit.lat)" in ui and "lonLat: coords.lonLat," in ui
|
||||
assert "lonLat: coords.lonLat," in ui and "toPlan" not in ui + panel
|
||||
assert "normOfLonLat" not in ui and "tm_grid` 늘려" not in ui
|
||||
# 끌기 중엔 판을 안 그림 — 속성 판은 고름 · 놓음(commit) 때만
|
||||
kit = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Kit.ts").read_text(encoding="utf-8")
|
||||
|
||||
@@ -36,6 +36,7 @@ def made():
|
||||
|
||||
def _ts(sg, machines):
|
||||
files = [
|
||||
"common_util/common_util_money.ts",
|
||||
"B09_Estimation/B09_Estimation_Calc_Base.ts",
|
||||
"A00_Common/calc_sheet.ts",
|
||||
"B09_Estimation/B09_Estimation_UI_Tabs_Sangeun_Sheet.ts",
|
||||
|
||||
@@ -26,7 +26,7 @@ def _text(path: Path) -> str:
|
||||
|
||||
def test_721_1_단추_이름과_자리():
|
||||
detail = _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
assert "children: [views.root, addButton, baseHint]" in detail
|
||||
assert "children: [views.root, addButton]" in detail # 750-2 「시험 계산 = 바탕」 안내 걷음
|
||||
assert "M02_SangeunPickBase" not in detail # 727-2 단추 걷음
|
||||
locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts")
|
||||
assert "M02_SangeunPickBase" not in locale
|
||||
|
||||
@@ -30,3 +30,66 @@ def test_calc_view_answer_hook_skips_logic_lib():
|
||||
ts = _ts("M01_MasterData/M01_MasterData_UI_Logic_Calc.ts")
|
||||
assert "ctx.onText && !ctx.answer" in ts # 미리보기 안 부름
|
||||
assert "if (ctx.answer)" in ts and "runCalc" in ts # M01 옛 길 그대로
|
||||
|
||||
|
||||
def test_752_1_source_panel_only_on_sangeun_screen():
|
||||
"""752-1 원문 패널 — 단가산출 화면만 켬 · 일위대가 줄 모달 안 상세는 끔 · 기초마다 탭 · 좁은 폭 접힘."""
|
||||
side = _ts("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Side.ts")
|
||||
assert "/m02/logic-lib/source" in side and "createSidePanels" in side
|
||||
assert "OPEN_FROM" in side and "createSegment" in side
|
||||
assert "source: true" in _ts("M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts")
|
||||
assert "source" not in _ts("M02_MasterTemplete/M02_MasterTemplete_Labor_RowSk.ts")
|
||||
detail = _ts("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
assert "opt.source ? createSangeunSide()" in detail and "side?.set(" in detail
|
||||
|
||||
|
||||
def test_753_2_option_off_flag_reaches_sheet_and_views():
|
||||
"""753-2 옵션으로 빠진 줄 — SK 줄 `옵션꺼짐` → 표 줄 `off` → 계산서 줄 `옵션꺼짐`(공용 계산서 · 상세 ·
|
||||
텍스트 수식 · 시험 계산이 안 그림) · 소계 · 금액은 그대로."""
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
if shutil.which("node") is None:
|
||||
pytest.skip("node 가 없음")
|
||||
m02 = ROOT / "M02_MasterTemplete"
|
||||
strip = lambda p: re.sub(r"^import .*$", "", p.read_text("utf-8"), flags=re.M) # noqa: E731
|
||||
src = "\n".join(
|
||||
[
|
||||
strip(m02 / "logic" / "M02_Born_Tag.ts"),
|
||||
strip(m02 / "logic" / "M02_Sangeun_Undo.ts"),
|
||||
strip(m02 / "logic" / "M02_Sangeun_Cover_Model.ts"),
|
||||
"""
|
||||
const line = (cid, 이름, 수량, 금액, 꺼짐) => ({ cid, 이름, 단위: "개", 수량, 단가: 100, 금액,
|
||||
비목: { 재료비: 금액 }, 묶음: "재료", ...(꺼짐 ? { 옵션꺼짐: true } : {}) });
|
||||
const ans = { 기준: null, 짝잃음: [], 바탕판: "x", 정체: ["material:A#0", "material:B#0"],
|
||||
줄: [line("material:A#0", "폼타이 12.7", 0, 0, true), line("material:B#0", "합판", 1, 100, false)] };
|
||||
const rows = rowsOf(ans, null, {});
|
||||
const groups = sheetGroups(rows);
|
||||
console.log(JSON.stringify({ off: rows.map((r) => r.off),
|
||||
sheet: groups.map((g) => [g.비목, g.소계, g.줄.map((x) => [x.이름, x.옵션꺼짐 ?? false, x.금액])]) }));
|
||||
""",
|
||||
]
|
||||
)
|
||||
out = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", src],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert out.returncode == 0, out.stderr
|
||||
got = json.loads(out.stdout)
|
||||
assert got["off"] == [True, False]
|
||||
assert got["sheet"] == [["재료비", 100, [["폼타이 12.7", True, 0], ["합판", False, 100]]]]
|
||||
sheet = (ROOT / "A00_Common" / "calc_sheet.ts").read_text("utf-8")
|
||||
assert "b.줄.filter(shown)" in sheet and "g.줄.filter(shown)" in sheet
|
||||
ho = (ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Ho.ts").read_text("utf-8")
|
||||
assert "옵션꺼짐" in ho
|
||||
assert "옵션꺼짐" in (
|
||||
ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Formula.ts"
|
||||
).read_text("utf-8")
|
||||
assert "옵션꺼짐" in (ROOT / "M01_MasterData" / "M01_MasterData_UI_Logic_Calc.ts").read_text(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""754-1 등고선 · 격자 외곽 = 지금 지표면 외곽 (요구 589-11).
|
||||
|
||||
implicit 하나만 지어도 같은 필터 dtm 이 낡았으면 같이 다시 지음 · 등고선 캐시는 dtm · 굳힌 격자가
|
||||
바뀌면 다시 셈 · ptd 처럼 dtm 없던 필터도 굳힌 격자가 생김. 서버 함수만 — 합성 점군 · 임시 폴더.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Contour import contour_cache_is_stale
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Pipeline import build_all_terrain_models
|
||||
from common_util.common_util_surface_sampler import implicit_grid_npz
|
||||
from config import config_system as cfg
|
||||
|
||||
|
||||
def _banded_plane() -> tuple[dict, np.ndarray]:
|
||||
axis = np.arange(0.0, 60.0, 0.5)
|
||||
x, y = np.meshgrid(axis, axis)
|
||||
points = np.column_stack([x.ravel(), y.ravel(), (100.0 + 0.2 * x).ravel()])
|
||||
points = points[~((points[:, 0] > 28) & (points[:, 0] < 33))] # 5 m 빈 띠(z 106 = x 30)
|
||||
data = {"xyz": points, "bounds": np.array([points.min(axis=0), points.max(axis=0)]).T}
|
||||
return data, np.ones(len(points), bool)
|
||||
|
||||
|
||||
def _config(filter_key: str, precompute: list[str], close_m: float) -> dict:
|
||||
config = cfg.build_surface_model_config()
|
||||
config.update(
|
||||
{
|
||||
"source_filters": [filter_key],
|
||||
"precompute": precompute,
|
||||
"footprint_gap_close_meters": close_m,
|
||||
"implicit_min_points": 500,
|
||||
"implicit_max_points": 2_000,
|
||||
}
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _valid_area(path: Path) -> float:
|
||||
with np.load(path) as grid:
|
||||
resolution = float(np.asarray(grid["resolution"]).ravel()[0])
|
||||
return float(np.count_nonzero(grid["valid_mask"])) * resolution**2
|
||||
|
||||
|
||||
def _band_contour(path: Path) -> bool:
|
||||
lines = json.loads(path.read_text(encoding="utf-8"))["contours"]
|
||||
return any(abs(line["level"] - 106.0) < 1e-6 for line in lines)
|
||||
|
||||
|
||||
def test_implicit_만_지어도_낡은_dtm_같이_다시(tmp_path: Path) -> None:
|
||||
data, mask = _banded_plane()
|
||||
masks = {"csf": mask}
|
||||
build_all_terrain_models(data, masks, tmp_path, _config("csf", ["dtm", "implicit"], 1.0))
|
||||
narrow = _valid_area(tmp_path / "dtm_csf.npz")
|
||||
contour = tmp_path / "contour_csf_implicit_1.0m.json"
|
||||
assert not _band_contour(contour) # 1 m 닫기 = 띠 빈칸 · 106 m 줄 잘림
|
||||
|
||||
# 화면 [서피스 생성] = 고른 표현 하나만 — 외곽 설정(지문)이 바뀌었으니 dtm 도 다시
|
||||
manifest = build_all_terrain_models(data, masks, tmp_path, _config("csf", ["implicit"], 3.0))
|
||||
methods = manifest["source_filters"]["csf"]["methods"]
|
||||
assert methods["dtm"]["status"] == "completed"
|
||||
wide = _valid_area(tmp_path / "dtm_csf.npz")
|
||||
grid_area = _valid_area(implicit_grid_npz(tmp_path, "csf"))
|
||||
glb_area = methods["implicit"]["footprint_area_m2"]
|
||||
assert wide > 1.05 * narrow
|
||||
assert grid_area == wide # 격자 외곽 = dtm 외곽
|
||||
assert abs(glb_area - wide) / wide < 0.05 # 3D 외곽 = 격자 외곽
|
||||
assert _band_contour(contour) # 등고선이 넓힌 띠까지
|
||||
assert not contour_cache_is_stale(contour, tmp_path / "implicit_csf.npz")
|
||||
|
||||
|
||||
def test_dtm_이_바뀌면_등고선_캐시_낡음(tmp_path: Path) -> None:
|
||||
model = tmp_path / "implicit_csf.npz"
|
||||
dtm = tmp_path / "dtm_csf.npz"
|
||||
grid = tmp_path / "implicit_csf_grid.npz"
|
||||
contour = tmp_path / "contour_csf_implicit_1.0m.json"
|
||||
assert contour_cache_is_stale(contour, model) # 없음
|
||||
for index, path in enumerate((model, dtm, grid, contour)):
|
||||
path.write_bytes(b"x")
|
||||
os.utime(path, ns=(10**18 + index, 10**18 + index))
|
||||
assert not contour_cache_is_stale(contour, model)
|
||||
for newer in (dtm, grid):
|
||||
os.utime(newer, ns=(2 * 10**18, 2 * 10**18))
|
||||
assert contour_cache_is_stale(contour, model)
|
||||
os.utime(newer, ns=(10**18, 10**18))
|
||||
tin_smooth = tmp_path / "tin_csf_smooth.npz"
|
||||
tin_smooth.write_bytes(b"x")
|
||||
os.utime(tin_smooth, ns=(10**18, 10**18))
|
||||
os.utime(dtm, ns=(2 * 10**18, 2 * 10**18))
|
||||
assert contour_cache_is_stale(contour, tin_smooth) # 스무딩 면도 같은 필터 dtm 을 봄
|
||||
|
||||
|
||||
def test_ptd_implicit_만_지어도_굳힌_격자(tmp_path: Path) -> None:
|
||||
data, mask = _banded_plane()
|
||||
build_all_terrain_models(data, {"ptd": mask}, tmp_path, _config("ptd", ["implicit"], 3.0))
|
||||
assert (tmp_path / "dtm_ptd.npz").is_file()
|
||||
grid = implicit_grid_npz(tmp_path, "ptd") # B05 · B06 표본이 읽는 파일
|
||||
with np.load(grid) as loaded:
|
||||
assert np.isfinite(loaded["z"][loaded["valid_mask"]]).all()
|
||||
@@ -0,0 +1,37 @@
|
||||
"""PLAN 754-2 — 구조물 종류 목록이 늦게 와도(`setTypes`) 고른 관 폼이 비지 않음.
|
||||
|
||||
B05 · B06 한 폼(`B05_Profile_UI_Structures_Panel.ts`)을 두 보기가 따로 불러 `setTypes` 를 부름 —
|
||||
종류 목록을 다시 세우며 군 · 종류를 빈칸으로 돌리므로, 고름이 남아 있으면 그 폼을 다시 열어야 함.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
ROOT = os.path.join(os.path.dirname(__file__), "..", "..")
|
||||
|
||||
|
||||
def _set_types_body() -> str:
|
||||
path = os.path.join(ROOT, "B05_Profile", "B05_Profile_UI_Structures_Panel.ts")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
src = f.read()
|
||||
match = re.search(
|
||||
r"\n setTypes\(next, pick = \(\) => true\) \{\n(.*?)\n \},\n", src, re.S
|
||||
)
|
||||
assert match, "setTypes 본문을 못 찾음"
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def test_종류를_다시_세운_뒤_고른_관_폼을_다시_엶() -> None:
|
||||
body = _set_types_body()
|
||||
reset = body.index("syncTypeOptions();")
|
||||
assert body.index("loadPipeForm(pipe)", reset) > reset
|
||||
assert "selectedPipeChainage" in body[reset:]
|
||||
|
||||
|
||||
def test_임시_배치_중이면_폼을_다시_열지_않음() -> None:
|
||||
"""임시 배치(A군 우클릭 추가) 중 입력을 지우지 않게 — 옛 동작 그대로."""
|
||||
body = _set_types_body()
|
||||
reset = body.index("syncTypeOptions();")
|
||||
assert body.index("tempPipeChainage !== null", reset) < body.index("loadPipeForm(pipe)")
|
||||
@@ -0,0 +1,98 @@
|
||||
"""755 B05 평면 예정노선 수정 흐름(요구 589-1 · 3 · 5 · 6 · 7 · 8).
|
||||
|
||||
- 755-1 켠 동안 = 판만 고침 · 끊김 허용(가운데 묶음 지움) · 이 판 사진첩(Ctrl+Z · 도구줄 단추) ·
|
||||
끌 때 한 번 = 끊겼으면 알림하고 안 끔 · 바뀌었으면 `/route/follow` 한 걸음 · [잇기] = B05 사용.
|
||||
- 755-2 켠 동안 재기 = D03 재기 겹 · 받은 노선 = 분홍 점선 + 검은 테 · 켬 설명 걷음 · 위치 칸 걷음.
|
||||
- 755-3 켠 채 판 고침 = [저장] · [확정] 막고 알림 · 떠남 = 공용 떠남 물음(탭 닫기 = 브라우저 물음).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_755_expected_flow.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
B05 = ROOT / "B05_Profile"
|
||||
D03 = ROOT / "D03_PlanAnalysis"
|
||||
|
||||
|
||||
def _body() -> str:
|
||||
return (B05 / "B05_Profile_UI_ExpectedDraw.ts").read_text("utf-8").split("*/", 1)[1]
|
||||
|
||||
|
||||
def _fn(body: str, head: str) -> str:
|
||||
start = body.index(head)
|
||||
end = body.index("\n }\n", start)
|
||||
return body[start:end]
|
||||
|
||||
|
||||
def test_켠_동안_판만_한_걸음_사진첩():
|
||||
body = _body().replace("\r\n", "\n")
|
||||
commit = _fn(body, "function commit(")
|
||||
assert "undoStack.push(network);" in commit and "redoStack = [];" in commit
|
||||
assert "follow(" not in commit and "apply(" not in commit # 켠 동안 평면선 안 바꿈
|
||||
assert "function apply(" not in body and "strokeBase" not in body
|
||||
assert "onMode: () => params.redraw()," in body # 긋기 끝 = 다시 추적 아님
|
||||
assert 'keyIs(event, "plan.undo")\n ? (step("undo"), true)' in body
|
||||
assert "history: () =>\n open" in body # 끔 = 페이지 사진첩
|
||||
history = (B05 / "B05_Profile_UI_RouteEdit_History.ts").read_text("utf-8")
|
||||
assert "const local = params.local?.();" in history
|
||||
assert "params.local?.() ?? getHistory()" in history
|
||||
main = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text("utf-8")
|
||||
assert "local: () => expectedDraw.history()," in main
|
||||
assert "onHistory: () => historyControls.sync()," in main
|
||||
|
||||
|
||||
def test_끌_때_끊김_알림_안_끔_한_번_추적():
|
||||
body = _body().replace("\r\n", "\n")
|
||||
open_ = _fn(body, "async function setOpen(")
|
||||
assert "if (!next && open && !(await finish())) return toggle.set(true);" in open_
|
||||
finish = _fn(body, "async function finish(")
|
||||
assert "drawn.length > 1" in finish and "return false;" in finish and "showToast(" in finish
|
||||
assert "return sameLine(points, base) || (await follow(points, points));" in finish
|
||||
follow = _fn(body, "async function follow(")
|
||||
assert (
|
||||
"/route/follow" in follow
|
||||
and "return true;" in follow
|
||||
and "if (!ok) return false;" in follow
|
||||
)
|
||||
assert "revert" not in body # 실패 · 취소 = 판 그대로(켠 채)
|
||||
table = (D03 / "D03_PlanAnalysis_UI_Features.ts").read_text("utf-8")
|
||||
assert re.search(r'key: "join",[^}]*b05: "use"', table, re.S)
|
||||
assert re.search(r'key: "branch",[^}]*b05: "hide"', table, re.S) # 갈래 · 둘째 선 그대로 막음
|
||||
|
||||
|
||||
def test_재기_D03_겹_켠_동안():
|
||||
body = _body()
|
||||
assert "createPlanMeasure(" in body and "params.measuring()" in body
|
||||
assert "kit.layers([], [measure.layer])" in body and "layers: liveLayers," in body
|
||||
assert "idle: () => open && !measuring()," in body # 재는 동안 고르기 · 끌기 쉼
|
||||
main = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text("utf-8")
|
||||
assert "measuring: () => measureControls.mode()," in main
|
||||
|
||||
|
||||
def test_받은_노선_색_설명_위치_걷음():
|
||||
body = _body()
|
||||
assert 'strokeCased(context, themeColor("--draw-map-route-3", "#ff8cbc"), 2.5, [8, 5]);' in body
|
||||
assert "--draw-text-secondary" not in body
|
||||
assert "HINT =" not in body.replace("OFF_HINT =", "") and "hint.hidden = open;" in body
|
||||
table = (D03 / "D03_PlanAnalysis_UI_Features.ts").read_text("utf-8")
|
||||
assert re.search(r'key: "site",[^}]*b05: "hide"', table, re.S)
|
||||
assert re.search(r'key: "site",[^}]*d03: "use"', table, re.S) # D03 그대로
|
||||
theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8")
|
||||
assert "--draw-map-route-3:" in theme
|
||||
|
||||
|
||||
def test_켠_채_저장_떠남_알림():
|
||||
body = _body().replace("\r\n", "\n")
|
||||
assert "const pending = (): boolean => open && undoStack.length > 0;" in body
|
||||
assert "holdDesignSave(canvas, () => (pending() ? HELD : null));" in body
|
||||
assert "guardProjectLeave(canvas, {\n dirty: pending," in body
|
||||
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text("utf-8")
|
||||
save = save.replace("\r\n", "\n")
|
||||
head = save[save.index("export async function saveDesign(") :]
|
||||
held = head.index("const held = heldSave();")
|
||||
assert held < head.index("saving.add(ctx.projectId);") # 막음 = 저장 줄 앞 · 가림막 없음
|
||||
assert held < head.index("showLoadingOverlay")
|
||||
@@ -207,10 +207,7 @@ export const ui_locales_m2 = {
|
||||
"Lines not priced — total uses the rest · drop the line or fill its input",
|
||||
],
|
||||
M02_SangeunScaleOnly: ["열린 입력 — 품은 배율만", "Open inputs — quantity as multiplier only"],
|
||||
M02_SangeunBaseViews: [
|
||||
"시험 계산 · 텍스트 수식 = 바탕 단가산출 기초 + 고친 품 · 단가(뺀 줄 · 더한 줄 빼고)",
|
||||
"Test calc · formula = base M01 logic + edited qty · price (no dropped · added lines)",
|
||||
],
|
||||
M02_SangeunSourceNone: ["원문 연결 없음 — 바탕 로직 없음", "No source — no base logic"],
|
||||
M02_SangeunWait: ["셈 기다리는 중", "Calculating…"],
|
||||
M02_SangeunNoInputs: [
|
||||
"바탕 단가산출 기초 없음 — 시험 입력 없음",
|
||||
|
||||
Reference in New Issue
Block a user