Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_8
This commit is contained in:
@@ -91,7 +91,7 @@ def _build(
|
||||
return detail, carried
|
||||
|
||||
|
||||
# 관 보호공 · 유입 집수정 높이 칸 — 새 관 기본 = 관경 + 0.5(139-1)
|
||||
# 관 보호공 · 유입 집수정 높이 칸 — 새 관 기본 = 관경 + 0.5(139-1) · 유입 흙막이만 + 0.7(211-1)
|
||||
PIPE_HEIGHT_KEYS = ("inlet_revet_height_m", "outlet_revet_height_m", "inlet_basin_height_m")
|
||||
|
||||
|
||||
@@ -130,7 +130,10 @@ def _apply_new_defaults(
|
||||
if diameter_m > 0:
|
||||
for key in PIPE_HEIGHT_KEYS:
|
||||
if key in keys:
|
||||
options.setdefault(key, pipe_wall_min_height_m(diameter_m))
|
||||
side = (
|
||||
"inlet" if key == "inlet_revet_height_m" else ""
|
||||
) # 유입 흙막이 + 0.7(211-1)
|
||||
options.setdefault(key, pipe_wall_min_height_m(diameter_m, side))
|
||||
if point.facility == "ford_bridge" and "ford_height_m" not in options:
|
||||
flow = (flows or {}).get(round(point.chainage_m, 2))
|
||||
height = ford_height_m(flow, float(options.get("ford_width_m") or 0))
|
||||
|
||||
@@ -78,19 +78,23 @@ REVET_FACE_SLOPE = 0.3
|
||||
# `test_b06_pipe_wall_height_mirror`.
|
||||
REVET_HEIGHT_STEP_M = 0.1 # 높이 눈금
|
||||
REVET_EMBED_DEPTH_M = 0.5 # 근입 깊이
|
||||
# 관 보호공 **유입** 흙막이 빈 칸 기본 = 관경(올림) + 0.7 — 유출 · 집수정은 근입 0.5 그대로(211-1).
|
||||
# ⚠ TS 짝 `common_util_culvert_sets.INLET_REVET_ADD_M` · `_Cross_Culvert_Const.INLET_REVET_ADD_M`.
|
||||
INLET_REVET_ADD_M = 0.7
|
||||
# 옛 정의 기본 높이(10-6 표 · 관경 무관) — 관경 > 1.0 관에 이 값이면 관 윗면보다 낮은 벽이라
|
||||
# 읽을 때 관경 기준 값으로 봄(139-1).
|
||||
OLD_PIPE_WALL_DEFAULT_M = 1.5
|
||||
|
||||
|
||||
def pipe_wall_min_height_m(diameter_m: float) -> float:
|
||||
def pipe_wall_min_height_m(diameter_m: float, side: str = "") -> float:
|
||||
"""관 벽 **순수 높이**(근입 포함) 기본값 = 관경(0.1m 로 올림) + 근입 0.5 — 벽 윗면 = 관 윗면(139-1).
|
||||
|
||||
관 · 유입 집수정 높이 칸의 새 기본값이자 빈 칸 값.
|
||||
관 · 유입 집수정 높이 칸의 새 기본값이자 빈 칸 값. `side="inlet"`(유입 흙막이) = + 0.7(211-1).
|
||||
"""
|
||||
raw = float(diameter_m)
|
||||
steps = math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9)
|
||||
return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3)
|
||||
add = INLET_REVET_ADD_M if side == "inlet" else REVET_EMBED_DEPTH_M
|
||||
return round(steps * REVET_HEIGHT_STEP_M + add, 3)
|
||||
|
||||
|
||||
def pipe_wall_height_m(stored: float | None, diameter_m: float) -> float | None:
|
||||
@@ -211,10 +215,11 @@ def _side_spec(
|
||||
spec["basin_form"] = str(options.get("inlet_structure") or "") or None
|
||||
return spec
|
||||
|
||||
# 높이를 안 적었으면 **관경 기준 높이**(관경 + 0.5 · 139-1) · 옛 1.5 도 관경 > 1.0 이면 그 값.
|
||||
# 높이를 안 적었으면 **관경 기준 높이**(관경 + 0.5 · 유입 + 0.7 · 139-1 · 211-1) ·
|
||||
# 옛 1.5 도 관경 > 1.0 이면 관경 + 0.5(저장 값 해석 · 안 바뀜).
|
||||
height = pipe_wall_height_m(_number(options.get(f"{side}_revet_height_m"), None), diameter_m)
|
||||
if height is None:
|
||||
height = pipe_wall_min_height_m(diameter_m)
|
||||
height = pipe_wall_min_height_m(diameter_m, side)
|
||||
length = _number(
|
||||
options.get(f"{side}_revet_length_m"),
|
||||
None,
|
||||
|
||||
@@ -121,7 +121,11 @@ class _SectionGeometry:
|
||||
# ⚠ 옛 저장분(`ditch_choice` 가 없던 것)은 `legacy_ditch_enabled` 로 온다. 그 값은
|
||||
# **자동값과 다를 때만 뜻이 있다** — 같으면 자동이 그렇게 냈던 것이고, 다르면
|
||||
# 사용자가 일부러 바꾼 것이다. 그래서 **다를 때만** 선택으로 살린다(설계 의도 보존).
|
||||
if section_mode == "both_fill":
|
||||
# ⚠ 양측 성토는 **판정된 역할**로 가른다(206-1) — 입력 `section_mode` 는 옛 저장값일 수
|
||||
# 있다(계획고를 올려 우 절토 → 양측 성토 · 입력은 right_cut 그대로). 입력으로 가르면 옛
|
||||
# 측구 결과가 선택으로 둔갑해 양측 성토 단면에 측구가 섰다. ⚠ TS 짝 `SectionGeometry`.
|
||||
both_fill = self.left_role == "fill" and self.right_role == "fill"
|
||||
if both_fill:
|
||||
auto_ditch = False
|
||||
elif ground_at is not None:
|
||||
ditch_edge = self.left_extent if ditch_side == "left" else -self.right_extent
|
||||
@@ -136,10 +140,10 @@ class _SectionGeometry:
|
||||
# 없는데, 그것을 선택으로 굳히면 **다시 같은 병**(지형이 바뀌어도 안 따라감)이 된다.
|
||||
if choice is not None and bool(choice) == auto_ditch:
|
||||
choice = None
|
||||
# 양성(both_fill)은 측구가 설 자리가 없다 — 선택보다 기하가 먼저다.
|
||||
self.has_ditch = auto_ditch if (choice is None or section_mode == "both_fill") else choice
|
||||
# 양성(both_fill)은 측구가 설 자리가 없다 — 선택보다 기하가 먼저 · 남은 선택은 비움.
|
||||
self.has_ditch = auto_ditch if (choice is None or both_fill) else choice
|
||||
#: 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다.
|
||||
self.ditch_choice = choice
|
||||
self.ditch_choice = None if both_fill else choice
|
||||
|
||||
# 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전**
|
||||
# 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도.
|
||||
|
||||
@@ -71,6 +71,10 @@ export function pipeWallThicknessM(kind: string | null, diameterM: number): numb
|
||||
*/
|
||||
export const REVET_EMBED_DEPTH_M = 0.5;
|
||||
|
||||
/** 관 보호공 **유입** 흙막이 빈 칸 기본 순수 높이 = 관경(올림) + 0.7(211-1) — 유출은 근입 0.5 그대로.
|
||||
* ⚠ 파이썬 짝 `INLET_REVET_ADD_M` · 세트 짝 `common_util_culvert_sets.INLET_REVET_ADD_M`(거울 시험). */
|
||||
export const INLET_REVET_ADD_M = 0.7;
|
||||
|
||||
/* 기슭막이 높이는 형태와 무관하게 **사용자 입력 그대로** — 형태별 높이 한계·원지반 맞춤
|
||||
* 자동 자르기 없음. 형태가 바꾸는 것은 안 무늬뿐. 높이 기준은 **순수 높이**(벽 바닥~상단,
|
||||
* 근입 0.5m 포함). 단 하나 — 벽 윗면은 노면 위로 올라오지 않음(`buildRevetWallGeometry`). */
|
||||
@@ -138,7 +142,7 @@ export function revetWallSpec(
|
||||
// 관경 기준 높이는 기본값만 — 하한은 그림이 서는 값뿐(54-51 · 사용자가 고름).
|
||||
const preferred = hiddenPipe
|
||||
? (spec.revet_height_m ?? autoHeightM ?? REVET_EMBED_DEPTH_M)
|
||||
: (spec.revet_height_m ?? pipeWallMinPureHeight(diameterM)); // 칸 값 → 그림(139-1)
|
||||
: (spec.revet_height_m ?? pipeWallMinPureHeight(diameterM, spec.role)); // 칸 값 → 그림(139-1)
|
||||
const pureHeight = Math.max(adjust.h ?? preferred, WALL_MIN_PURE_HEIGHT_M);
|
||||
return {
|
||||
form,
|
||||
@@ -155,10 +159,12 @@ export const PIPE_CONNECT_GRADE = Math.tan((3 * Math.PI) / 180);
|
||||
/**
|
||||
* 배관 기슭막이 관 하부 여유(m) — 관 invert 아래 벽 바닥까지의 깊이 = 근입 0.5.
|
||||
* **순수 높이 기본 = 관경 + 0.5**(땅속 근입 0.5 포함 벽 전체 높이 · 벽 윗면 = 관 윗면 ·
|
||||
* 10-6 표 Ø1000 = 1.5 · 139-1). 칸을 비우면 이 값.
|
||||
* 10-6 표 Ø1000 = 1.5 · 139-1 · 유입 흙막이 = 관경 + 0.7 · 211-1). 칸을 비우면 이 값.
|
||||
*/
|
||||
export function pipeWallMinPureHeight(diameterM: number): number {
|
||||
return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M;
|
||||
export function pipeWallMinPureHeight(diameterM: number, side = ""): number {
|
||||
return (
|
||||
revetTargetHeight(diameterM) + (side === "inlet" ? INLET_REVET_ADD_M : REVET_EMBED_DEPTH_M)
|
||||
);
|
||||
}
|
||||
/** 벽 순수 높이 하한(m) — 0 이하는 그림이 안 섬. 관경 · 근입 하한 없음(54-51). */
|
||||
export const WALL_MIN_PURE_HEIGHT_M = 0.1;
|
||||
|
||||
@@ -388,8 +388,10 @@ export function buildDesignControls(
|
||||
groundSegment.classList.add("b06-design__seg--header");
|
||||
// 단면 유형은 지형에서 자동 판정되며(D-2), 제목행 pill로 표시한다(E-3, Cross_View에서 생성).
|
||||
// 아래 옵션은 선행 조건 미충족 시 비활성 = 숨김(179-2 · CSS) — 편측의 쪽은 알약 글에.
|
||||
const rockCut = isRock(state.ground) && state.mode !== "both_fill";
|
||||
const hasDitch = state.mode !== "both_fill";
|
||||
// 숨김은 「막힘 + 그려진 측구 없음」일 때만(206-1) — 측구가 그려져 있으면 단추를 보여 끌 수 있게.
|
||||
const ditchDrawn = design?.ditch_enabled === true;
|
||||
const rockCut = isRock(state.ground) && (state.mode !== "both_fill" || ditchDrawn);
|
||||
const hasDitch = state.mode !== "both_fill" || ditchDrawn;
|
||||
// 측구 생성 여부(자동 판정 or 사용자 override) — 측구형식은 측구가 있을 때만 의미 있다.
|
||||
// 화면 표시는 **결과**를 보인다 — 선택이 없으면 자동으로 선 결과가 답이다.
|
||||
const ditchOn = state.ditchEnabled ?? design?.ditch_enabled ?? true;
|
||||
|
||||
@@ -106,6 +106,8 @@ interface StaleCandidate {
|
||||
geometry_preset?: string;
|
||||
two_stage_slope?: boolean;
|
||||
surface_drop_m?: number;
|
||||
section_mode?: string;
|
||||
ditch_enabled?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -120,6 +122,8 @@ function isStaleSection(
|
||||
// 이 조건이 B06 페이지에만 있어 B05는 재계산을 건너뛰었고, 같은 데이터인데 두 화면의
|
||||
// 절토량이 갈렸다(2026-09-03 실측 228.52㎡ ↔ 270.12㎡). 판정을 여기 한 곳으로 모은다.
|
||||
if (design.geometry_preset === "rock" && design.two_stage_slope === undefined) return true;
|
||||
// 옛 엔진이 양측 성토에 측구를 세워 둔 저장분(206-1) — 지금 엔진으로 다시 셈.
|
||||
if (design.section_mode === "both_fill" && design.ditch_enabled === true) return true;
|
||||
if (!profiles?.length) return false;
|
||||
const planned = designElevationAt(profiles, section.chainage_m);
|
||||
if (planned === undefined) return false;
|
||||
|
||||
@@ -187,11 +187,13 @@ def _revets(
|
||||
sides: list[tuple[str, str | None]],
|
||||
empty_height: float | None,
|
||||
names: dict[str, str],
|
||||
inlet_empty_height: float | None = None,
|
||||
) -> None:
|
||||
"""흙막이 — 유입 · 유출(흙막이일 때 · `sides`) + 흙막이 n(container_rows 가 선 단까지만 냄).
|
||||
|
||||
저장 키 = 필수는 `{쪽}_revet_{칸}_m` · 선택 흙막이는 `soil_add_{n}_{칸}`(명세 인스턴스 키) ·
|
||||
빈 높이 = `empty_height`(None 이면 「미입력」 열) · 머리 글은 관 정의 이름(공용 열).
|
||||
빈 높이 = `empty_height`(None 이면 「미입력」 열 · 관 유입 쪽만 `inlet_empty_height` · 211-1) ·
|
||||
머리 글은 관 정의 이름(공용 열).
|
||||
"""
|
||||
sources = [(f"{side}_revet_", "_m", side, combo) for side, combo in sides]
|
||||
sources += [
|
||||
@@ -203,7 +205,9 @@ def _revets(
|
||||
form = str(options.get(f"{head}form") or "")
|
||||
height = _num(options.get(f"{head}height{unit}"))
|
||||
if height is None:
|
||||
height = empty_height
|
||||
height = (
|
||||
inlet_empty_height if container == "inlet" and inlet_empty_height else empty_height
|
||||
)
|
||||
height_text = fill._label("height_m", height)
|
||||
sheet.add(
|
||||
at,
|
||||
@@ -588,7 +592,8 @@ def pipe_summary(
|
||||
)
|
||||
wall_options = {**options, **moved}
|
||||
wall = pipe_wall_min_height_m(diameter / 1000) if diameter else None
|
||||
_revets(sheet, at, wall_options, links, sources, wall, names)
|
||||
wall_in = pipe_wall_min_height_m(diameter / 1000, "inlet") if diameter else None
|
||||
_revets(sheet, at, wall_options, links, sources, wall, names, wall_in)
|
||||
_chutes(sheet, at, options, links, names)
|
||||
|
||||
out = sheet.result()
|
||||
|
||||
@@ -26,6 +26,8 @@ import master_keys as mk # noqa: E402
|
||||
import master_text as mtx # noqa: E402
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store_Cache as cache # noqa: E402
|
||||
from M01_MasterData.M01_MasterData_Store_Find import hit as _hit # noqa: E402
|
||||
from M01_MasterData.M01_MasterData_Store_Find import squash as _squash # noqa: E402
|
||||
from M01_MasterData.M01_MasterData_Store_Shape import dump, keep_shape # noqa: E402
|
||||
|
||||
_COPY = os.environ.get("AISLO_MASTER_DIR") # 화면 저장 시험 사본 폴더(정함 13) · 없으면 정본
|
||||
@@ -88,11 +90,6 @@ def chapter_of(file: str, data: dict | None = None) -> str:
|
||||
return f"{division} {m.group(1)}장 {m.group(2).replace('_', ' ')}".strip()
|
||||
|
||||
|
||||
def _hit(row: dict, q: str) -> bool:
|
||||
q = _squash(q) # 띄어쓰기 무시 — 「골 함 석」 ↔ 「함석」
|
||||
return not q or any(q in _squash(row.get(k, "")) for k in ("키", "원문번호", "이름", "옛이름"))
|
||||
|
||||
|
||||
def _book(data: dict) -> str | None:
|
||||
"""머리 원문 이름 — 한 테이블 파일(기계)은 머리 「원문」 이 갈래 묶음이라 이름이 묶음 안에."""
|
||||
return data.get("원문") if isinstance(data.get("원문"), str) else None
|
||||
@@ -367,6 +364,7 @@ def logics(sub: str, detail: str, q: str, blocked: int | None, owner: str = "")
|
||||
"소유",
|
||||
)
|
||||
},
|
||||
"별칭": row.get("별칭") or "", # 없으면 빈칸(208-1)
|
||||
"blocked": bool(reasons),
|
||||
"reasons": reasons,
|
||||
}
|
||||
@@ -449,17 +447,14 @@ def elements(group: str, q: str, limit: int) -> dict:
|
||||
continue
|
||||
for row in data.get(items_key(data)) or []:
|
||||
if _hit(row, q):
|
||||
hits.append({**_brief(str(row.get("키")), row), "file": name})
|
||||
more = {"별칭": row.get("별칭") or ""} if group == "로직" else {} # 208-1
|
||||
hits.append({**_brief(str(row.get("키")), row), "file": name, **more})
|
||||
return {"total": len(hits), "items": hits[: min(max(limit, 1), 200)]}
|
||||
|
||||
|
||||
_PICK = {"price": "재료_자재품목.json", "job": "인력.json"}
|
||||
|
||||
|
||||
def _squash(text) -> str:
|
||||
return "".join(str(text or "").lower().split())
|
||||
|
||||
|
||||
def _lowest(row: dict) -> object:
|
||||
"""자재품목 줄 — 값 열 다섯 가운데 낮은 값(시험 계산용) · 값 칸이 있는 줄은 그 값 그대로."""
|
||||
if "값" in row:
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""찾기 맞춤 — 낱말(띄어 나눔)마다 키 · 원문번호 · 이름 · 옛이름 · 별칭 가운데 하나에 들면 맞음.
|
||||
|
||||
띄어쓰기 · 대소문자 무시 · 별칭 = 설계자가 로직을 찾는 다른 이름(쉼표로 여럿 · 208-1).
|
||||
짝: `ui_template/ui_template_logic_find.ts` `matchLogic`(화면 쪽) ·
|
||||
거울 시험 `test_208_1_logic_alias.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
FIELDS = ("키", "원문번호", "이름", "옛이름", "별칭")
|
||||
|
||||
|
||||
def squash(text) -> str:
|
||||
return "".join(str(text or "").lower().split())
|
||||
|
||||
|
||||
def hit(row: dict, q: str) -> bool:
|
||||
words = [squash(w) for w in str(q or "").split()]
|
||||
fields = [squash(row.get(k)) for k in FIELDS]
|
||||
return all(any(w in f for f in fields) for w in words)
|
||||
@@ -135,7 +135,10 @@ export async function mountM01LogicLab(
|
||||
const waiting = el("p", { className: "m01-logic__muted", text: tx("Loading") });
|
||||
let loaded = false; // 첫 목록이 오기 전에는 「0 줄」 빈 표를 보이지 않음
|
||||
const pending = el("span", { className: "m01-logic__muted" });
|
||||
const list = buildList((item) => void open(item.id, item.sub, item.key));
|
||||
const list = buildList(
|
||||
(item) => void open(item.id, item.sub, item.key),
|
||||
(item, alias) => void onAlias(item, alias),
|
||||
);
|
||||
const back = createButton({ label: tx("Bar_Back"), variant: "ghost", onClick: () => show(null) });
|
||||
|
||||
const persist = (): void => {
|
||||
@@ -146,8 +149,10 @@ export async function mountM01LogicLab(
|
||||
}
|
||||
const marks = new Map<string, ListMark>();
|
||||
const extra: ListItem[] = [];
|
||||
const aliases = new Map<string, string>();
|
||||
for (const [id, d] of Object.entries(drafts)) {
|
||||
marks.set(id, d.origKey === null ? "new" : d.row === null ? "deleted" : "edited");
|
||||
if (d.row) aliases.set(id, d.row.별칭 ?? "");
|
||||
if (d.origKey === null && d.row) {
|
||||
extra.push({
|
||||
id,
|
||||
@@ -156,6 +161,7 @@ export async function mountM01LogicLab(
|
||||
key: d.row.키,
|
||||
number: d.row.원문번호,
|
||||
name: d.row.이름,
|
||||
alias: d.row.별칭 ?? "",
|
||||
unit: d.row.결과단위,
|
||||
source: d.row.출처,
|
||||
blocked: false,
|
||||
@@ -163,7 +169,7 @@ export async function mountM01LogicLab(
|
||||
});
|
||||
}
|
||||
}
|
||||
list.setMarks(marks, extra);
|
||||
list.setMarks(marks, extra, aliases);
|
||||
const n = Object.keys(drafts).length;
|
||||
pending.textContent = n ? tx("Bar_Pending", { n }) : "";
|
||||
saveButton.disabled = discardButton.disabled = n === 0;
|
||||
@@ -423,6 +429,26 @@ export async function mountM01LogicLab(
|
||||
persist();
|
||||
};
|
||||
|
||||
/** 목록 「별칭」 칸 고침(208-1) — 그 줄의 별칭만 바꿔 캐시에 · [저장] 때 다른 고침과 함께 나감 */
|
||||
const onAlias = async (item: ListItem, alias: string): Promise<void> => {
|
||||
const draft = drafts[item.id];
|
||||
if (draft && !draft.row) return; // 지운 줄
|
||||
try {
|
||||
const one = draft?.origKey === null ? null : await fetchLogic(item.key);
|
||||
const row = clone((draft?.row ?? one?.logic) as LogicRow);
|
||||
if (alias) row.별칭 = alias;
|
||||
else delete row.별칭;
|
||||
if (one && JSON.stringify(row) === JSON.stringify(one.logic)) delete drafts[item.id];
|
||||
else
|
||||
drafts[item.id] = draft
|
||||
? { ...draft, row }
|
||||
: { sub: item.sub, file: one!.file, version: one!.version, origKey: item.key, row };
|
||||
persist();
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (): Promise<void> => {
|
||||
if (!opened?.row) return;
|
||||
if (
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface LogicRow {
|
||||
덧줄?: NamedFormula[];
|
||||
끝수?: string | null;
|
||||
비고?: string;
|
||||
/** 설계자가 찾는 다른 이름 — 쉼표로 여럿 · 없으면 칸 없음(208-1) */
|
||||
별칭?: string;
|
||||
[extra: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -54,6 +56,8 @@ export interface LogicSummary {
|
||||
이름: string;
|
||||
결과단위: string;
|
||||
출처: string;
|
||||
/** 없으면 빈칸(208-1) */
|
||||
별칭?: string;
|
||||
blocked: boolean;
|
||||
reasons: string[];
|
||||
}
|
||||
@@ -66,6 +70,8 @@ export interface ElementBrief {
|
||||
단위?: unknown;
|
||||
값?: unknown;
|
||||
값칸?: Record<string, string> | null;
|
||||
/** 로직 그룹만 — 별칭(208-1) */
|
||||
별칭?: string;
|
||||
/** 재료 후보 줄 — 값 열 다섯(지역·계약종별별 값) */
|
||||
값들?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
* M01_MasterData_UI_Logic_List.ts
|
||||
* 로직 목록 — 가운데 표 · 왼쪽 「전체」 + 구분 → 상세구분 거름이 고른 범위 + 이름 찾기
|
||||
* 파일을 가로질러 한 목록 · 쪽 나눔 · 막힘(⛔)·고침(●) 표시 · 거르기는 화면에서(목록은 한 번 받음)
|
||||
* 「별칭」 열(208-1) — 이름 옆 · 고치면 `onAlias`(캐시에 쌓고 [저장] 때 나감) · 찾기는 공용 `matchLogic`
|
||||
* ========================================================================== */
|
||||
|
||||
import { createInputField, el } from "@ui/ui_template_elements";
|
||||
import { aliasText, matchLogic } from "@ui/ui_template_logic_find";
|
||||
import { buildPager } from "./M01_MasterData_UI_Cells";
|
||||
import type { LogicSummary } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
@@ -19,6 +21,8 @@ export interface ListItem {
|
||||
key: string;
|
||||
number: string;
|
||||
name: string;
|
||||
/** 쉼표로 여럿 · 없으면 빈칸 */
|
||||
alias: string;
|
||||
unit: string;
|
||||
source: string;
|
||||
blocked: boolean;
|
||||
@@ -28,8 +32,8 @@ export interface ListItem {
|
||||
export interface ListHandle {
|
||||
root: HTMLElement;
|
||||
setItems: (items: LogicSummary[]) => void;
|
||||
/** 저장 안 한 새 로직도 목록 맨 위에 */
|
||||
setMarks: (marks: Map<string, ListMark>, extra: ListItem[]) => void;
|
||||
/** 저장 안 한 새 로직도 목록 맨 위에 · aliases = 캐시에 고친 별칭(id → 글) */
|
||||
setMarks: (marks: Map<string, ListMark>, extra: ListItem[], aliases: Map<string, string>) => void;
|
||||
/** 왼쪽 거름이 고른 범위 — 비면 전체 */
|
||||
setPick: (sub: string, detail: string) => void;
|
||||
total: () => number;
|
||||
@@ -38,12 +42,27 @@ export interface ListHandle {
|
||||
export const logicId = (sub: string, key: string): string => `${sub}\n${key}`;
|
||||
|
||||
const SIZE = 30;
|
||||
const COLS = ["키", "원문번호", "구분", "상세구분", "이름", "결과 단위", "출처", ""] as const;
|
||||
const COLS = [
|
||||
"키",
|
||||
"원문번호",
|
||||
"구분",
|
||||
"상세구분",
|
||||
"이름",
|
||||
"별칭",
|
||||
"결과 단위",
|
||||
"출처",
|
||||
"",
|
||||
] as const;
|
||||
|
||||
export function buildList(onOpen: (item: ListItem) => void): ListHandle {
|
||||
export function buildList(
|
||||
onOpen: (item: ListItem) => void,
|
||||
onAlias: (item: ListItem, alias: string) => void,
|
||||
): ListHandle {
|
||||
let items: ListItem[] = [];
|
||||
let extra: ListItem[] = [];
|
||||
let marks = new Map<string, ListMark>();
|
||||
let aliases = new Map<string, string>();
|
||||
const aliasOf = (x: ListItem): string => aliases.get(x.id) ?? x.alias;
|
||||
let page = 1;
|
||||
const pick = { sub: "", detail: "" };
|
||||
const searchField = createInputField({ type: "search", placeholder: tx("List_Search") });
|
||||
@@ -51,12 +70,19 @@ export function buildList(onOpen: (item: ListItem) => void): ListHandle {
|
||||
const grid = el("div", { className: "m01-master__grid-wrap" });
|
||||
|
||||
const draw = (): void => {
|
||||
const q = search.value.trim().toLowerCase();
|
||||
const q = search.value;
|
||||
const shown = [...extra, ...items].filter(
|
||||
(x) =>
|
||||
(!pick.sub || x.sub === pick.sub) &&
|
||||
(!pick.detail || x.detail === pick.detail) &&
|
||||
(!q || [x.key, x.number, x.name].some((v) => v.toLowerCase().includes(q))),
|
||||
matchLogic(q, {
|
||||
키: x.key,
|
||||
원문번호: x.number,
|
||||
이름: x.name,
|
||||
구분: x.sub,
|
||||
상세구분: x.detail,
|
||||
별칭: aliasOf(x),
|
||||
}).ok,
|
||||
);
|
||||
page = Math.min(page, Math.max(1, Math.ceil(shown.length / SIZE)));
|
||||
const rows = shown.slice((page - 1) * SIZE, page * SIZE).map(line);
|
||||
@@ -87,24 +113,36 @@ export function buildList(onOpen: (item: ListItem) => void): ListHandle {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const cell = (v: string): HTMLElement => el("td", { text: v, attrs: { title: v } });
|
||||
const tr = el("tr", {
|
||||
className: `m01-master__table-row${mark ? " is-changed" : ""}`,
|
||||
children: [
|
||||
item.key,
|
||||
item.number,
|
||||
item.sub,
|
||||
item.detail,
|
||||
item.name,
|
||||
item.unit,
|
||||
item.source,
|
||||
state,
|
||||
].map((v) => el("td", { text: v, attrs: { title: v } })),
|
||||
...[item.key, item.number, item.sub, item.detail, item.name].map(cell),
|
||||
aliasCell(item, mark === "deleted"),
|
||||
...[item.unit, item.source, state].map(cell),
|
||||
],
|
||||
});
|
||||
if (item.blocked) tr.title = item.reasons.join("\n");
|
||||
tr.addEventListener("click", () => onOpen(item));
|
||||
return tr;
|
||||
};
|
||||
|
||||
/** 별칭 칸 — 눌러도 로직이 안 열림 · 고친 글은 「a, b」 꼴로 */
|
||||
const aliasCell = (item: ListItem, locked: boolean): HTMLElement => {
|
||||
const input = el("input", {
|
||||
className: "m01-logic__alias",
|
||||
attrs: { type: "text", value: aliasOf(item), placeholder: "—", title: tx("List_Alias") },
|
||||
}) as HTMLInputElement;
|
||||
input.disabled = locked;
|
||||
input.addEventListener("click", (e) => e.stopPropagation());
|
||||
input.addEventListener("change", () => {
|
||||
const next = aliasText(input.value);
|
||||
input.value = next;
|
||||
if (next !== aliasOf(item)) onAlias(item, next);
|
||||
});
|
||||
return el("td", { children: [input] });
|
||||
};
|
||||
|
||||
search.addEventListener("input", () => go(1));
|
||||
let pager = buildPager(0, SIZE, 1, go);
|
||||
|
||||
@@ -128,6 +166,7 @@ export function buildList(onOpen: (item: ListItem) => void): ListHandle {
|
||||
key: x.키,
|
||||
number: x.원문번호,
|
||||
name: x.이름,
|
||||
alias: x.별칭 ?? "",
|
||||
unit: x.결과단위,
|
||||
source: x.출처,
|
||||
blocked: x.blocked,
|
||||
@@ -135,9 +174,10 @@ export function buildList(onOpen: (item: ListItem) => void): ListHandle {
|
||||
}));
|
||||
draw();
|
||||
},
|
||||
setMarks: (next, more) => {
|
||||
setMarks: (next, more, edited) => {
|
||||
marks = next;
|
||||
extra = more;
|
||||
aliases = edited;
|
||||
draw();
|
||||
},
|
||||
setPick: (sub, detail) => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* M01_MasterData_UI_Logic_Pick.ts
|
||||
* 고르기 두 가지
|
||||
* ① 요소 찾기 창 — 그룹 전체에서 이름·키·원문번호로 찾아 호표 줄에 넣음(`GET /elements`)
|
||||
* 로직 그룹은 별칭으로도 찾음(서버) · 별칭으로 맞은 줄은 그 별칭을 작게(공용 `matchLogic` · 208-1)
|
||||
* 표 고르기(소요량·계수)는 값칸 단추를 눌러 `찾기(…).칸` 으로 수량에 넣음
|
||||
* ② 재료·기계 바꿔 고르기 — 구분 → 상세구분 → 후보 순으로 좁힘
|
||||
* 바꾼 것은 시험 계산용 복사본에만 · 정본 로직은 그대로
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { aliasNote, matchLogic } from "@ui/ui_template_logic_find";
|
||||
import {
|
||||
fetchMaterials,
|
||||
searchElements,
|
||||
@@ -77,7 +79,11 @@ export function openPicker(mode: PickMode, group: string, done: PickDone): void
|
||||
const found = await searchElements(select.value, search.value.trim());
|
||||
if (mine !== seq) return; // 최신 응답만
|
||||
count.textContent = tx("Pick_Total", { n: found.total });
|
||||
list.replaceChildren(...found.items.map((item) => itemRow(item, mode, choose)));
|
||||
const note = (item: ElementBrief): string =>
|
||||
select.value === "로직"
|
||||
? aliasNote(matchLogic(search.value, { ...item, 키: item.ref, 이름: item.이름 }).alias)
|
||||
: "";
|
||||
list.replaceChildren(...found.items.map((item) => itemRow(item, mode, choose, note(item))));
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
}
|
||||
@@ -97,12 +103,16 @@ function itemRow(
|
||||
item: ElementBrief,
|
||||
mode: PickMode,
|
||||
choose: (item: ElementBrief, column?: string) => void,
|
||||
note = "",
|
||||
): HTMLElement {
|
||||
const text = el("div", {
|
||||
className: "m01-logic__stack",
|
||||
children: [
|
||||
el("strong", { text: `${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() }),
|
||||
el("span", { className: "m01-logic__muted", text: item.ref }),
|
||||
el("span", {
|
||||
className: "m01-logic__muted",
|
||||
text: [item.ref, note].filter(Boolean).join(" · "),
|
||||
}),
|
||||
],
|
||||
});
|
||||
if (mode === "table") {
|
||||
|
||||
@@ -266,6 +266,23 @@ input.m01-logic__input {
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* 목록 「별칭」 칸(208-1) — 칸 안 글 상자 · 테두리는 올렸을 때만 */
|
||||
.m01-logic__alias {
|
||||
width: 160px;
|
||||
padding: 2px var(--spacing-4);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-buttons);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.m01-logic__alias:hover,
|
||||
.m01-logic__alias:focus {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-canvas);
|
||||
}
|
||||
|
||||
.m01-logic__bad {
|
||||
color: var(--color-danger);
|
||||
font-size: var(--text-caption);
|
||||
|
||||
@@ -7,7 +7,8 @@ import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
|
||||
const TEXT = {
|
||||
Title: ["단가산출 로직", "Unit-price logic"],
|
||||
List_Search: ["이름 찾기", "Search name"],
|
||||
List_Search: ["이름 · 별칭 찾기", "Search name or alias"],
|
||||
List_Alias: ["별칭 — 쉼표로 여럿 · 고친 뒤 [저장]", "Aliases — comma separated · then [Save]"],
|
||||
List_AllBooks: ["원문 모두", "All sources"],
|
||||
List_AllChapters: ["장 모두", "All chapters"],
|
||||
List_BlockedOnly: ["막힘만", "Blocked only"],
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
CfScope,
|
||||
CfVariable,
|
||||
} from "./M02_DF_Model";
|
||||
import { matchLogic } from "@ui/ui_template_logic_find";
|
||||
|
||||
/** 입력 상자가 고르는 그룹 — 화면 차례 */
|
||||
export const GROUPS: CfGroup[] = [
|
||||
@@ -285,21 +286,21 @@ export interface MasterLogicBrief {
|
||||
원문번호: string;
|
||||
이름: string;
|
||||
결과단위: string;
|
||||
/** 쉼표로 여럿(208-1) */
|
||||
별칭?: string;
|
||||
blocked?: boolean;
|
||||
reasons?: string[];
|
||||
}
|
||||
|
||||
/** 찾기 — 띄어쓰기로 나눈 말을 모두 포함(이름 · 원문번호 · 키 · 구분 · 상세구분 · 띄어쓰기는 가리지 않음) · 원문(구분) · 장(상세구분) 거름 */
|
||||
/** 찾기 — 공용 `matchLogic`(띄어 나눈 말마다 이름 · 원문번호 · 키 · 구분 · 상세구분 · 별칭 · 208-1) · 원문(구분) · 장(상세구분) 거름 */
|
||||
export function filterLogics(
|
||||
items: MasterLogicBrief[],
|
||||
q: { text: string; book?: string; chapter?: string },
|
||||
): MasterLogicBrief[] {
|
||||
const words = q.text.split(/\s+/).map(squash).filter(Boolean);
|
||||
return items.filter((i) => {
|
||||
if (q.book && i.구분 !== q.book) return false;
|
||||
if (q.chapter && i.상세구분 !== q.chapter) return false;
|
||||
const hay = squash(`${i.이름} ${i.원문번호} ${i.키} ${i.구분} ${i.상세구분}`);
|
||||
return words.every((w) => hay.includes(w));
|
||||
return matchLogic(q.text, i).ok;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { aliasNote, matchLogic } from "@ui/ui_template_logic_find";
|
||||
import {
|
||||
fetchAuto,
|
||||
fetchLogic,
|
||||
@@ -193,6 +194,11 @@ export function renderLogicInput(
|
||||
children: [
|
||||
el("strong", { text: i.이름 }),
|
||||
el("span", { className: "m02-dfip__muted", text: ` ${i.원문번호}` }),
|
||||
// 별칭으로 맞았으면 그 별칭을 작게(208-1)
|
||||
el("span", {
|
||||
className: "m02-dfip__muted",
|
||||
text: ` ${aliasNote(matchLogic(q.input.value, i).alias)}`,
|
||||
}),
|
||||
el("span", {
|
||||
className: "m02-dfip__tag",
|
||||
text: i.blocked ? t("Blocked") : i.상세구분,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createFindList } from "@ui/ui_template_find_list";
|
||||
import { aliasNote, matchLogic } from "@ui/ui_template_logic_find";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import type { LogicSummary, SubBrief } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
@@ -86,7 +87,7 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
(x) =>
|
||||
(!sub.select.value || x.구분 === sub.select.value) &&
|
||||
(!detail.select.value || x.상세구분 === detail.select.value) &&
|
||||
(!q || `${x.키} ${x.이름}`.toLowerCase().includes(q)),
|
||||
matchLogic(finder.search.value, x).ok, // 이름 · 키 · 별칭 …(공용 · 208-1)
|
||||
);
|
||||
const libFound = libRows.filter(
|
||||
(x) =>
|
||||
@@ -106,7 +107,9 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
key: x.키,
|
||||
tag: tc("Kind_Logic"),
|
||||
name: x.이름,
|
||||
sub: `${x.키} · ${x.구분} ${x.상세구분}`.trim(),
|
||||
sub: [`${x.키} · ${x.구분} ${x.상세구분}`.trim(), aliasNote(matchLogic(q, x).alias)]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
unit: `${x.결과단위}${x.blocked ? ` · ${tc("Pick_Blocked")}` : ""}`,
|
||||
onPick: () => {
|
||||
close();
|
||||
|
||||
@@ -188,8 +188,11 @@ export class SectionGeometry {
|
||||
// `hasDitch`** 로 가른다.
|
||||
// ⚠ 옛 저장분은 `ditchEnabled` 로 온다. 그 값은 **자동값과 다를 때만 뜻이 있다** —
|
||||
// 같으면 자동이 그렇게 냈던 것이고, 다르면 사용자가 일부러 바꾼 것이다.
|
||||
// ⚠ 양측 성토는 **판정된 역할**로 가른다(206-1 · 짝: 파이썬) — 입력 `sectionMode` 는 옛
|
||||
// 저장값일 수 있다. 입력으로 가르면 옛 측구 결과가 선택으로 둔갑해 양측 성토에 측구가 섰다.
|
||||
const bothFill = this.leftRole === "fill" && this.rightRole === "fill";
|
||||
let autoDitch: boolean;
|
||||
if (params.sectionMode === "both_fill") {
|
||||
if (bothFill) {
|
||||
autoDitch = false;
|
||||
} else if (groundAt !== null) {
|
||||
const ditchEdge = params.ditchSide === "left" ? this.leftExtent : -this.rightExtent;
|
||||
@@ -206,10 +209,10 @@ export class SectionGeometry {
|
||||
// 「자동으로 되돌리기」 단추가 없고, 원래 값으로 다시 누른 것을 선택으로 굳히면
|
||||
// **다시 같은 병**이 된다.
|
||||
if (choice !== null && Boolean(choice) === autoDitch) choice = null;
|
||||
// 양성(both_fill)은 측구가 설 자리가 없다 — 선택보다 기하가 먼저다.
|
||||
this.hasDitch = choice === null || params.sectionMode === "both_fill" ? autoDitch : choice;
|
||||
// 양성(both_fill)은 측구가 설 자리가 없다 — 선택보다 기하가 먼저 · 남은 선택은 비움.
|
||||
this.hasDitch = choice === null || bothFill ? autoDitch : choice;
|
||||
/** 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. */
|
||||
this.ditchChoice = choice;
|
||||
this.ditchChoice = bothFill ? null : choice;
|
||||
|
||||
// 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전**
|
||||
// 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도. ⚠ 파이썬 짝 `_SectionGeometry`.
|
||||
|
||||
@@ -51,12 +51,15 @@ export const BOX_COVER_M = 0.5;
|
||||
export const REVET_HEIGHT_STEP_M = 0.1;
|
||||
export const REVET_EMBED_DEPTH_M = 0.5;
|
||||
export const OLD_PIPE_WALL_DEFAULT_M = 1.5;
|
||||
/** 관 보호공 유입 흙막이 빈 칸 기본 = 관경(올림) + 0.7(211-1) — 유출 · 집수정은 근입 0.5 그대로. */
|
||||
export const INLET_REVET_ADD_M = 0.7;
|
||||
|
||||
/** 관 벽 순수 높이(근입 포함) 기본값 — 파이썬 `pipe_wall_min_height_m`. */
|
||||
export function pipeWallMinHeightM(diameterM: number): number {
|
||||
/** 관 벽 순수 높이(근입 포함) 기본값 — 파이썬 `pipe_wall_min_height_m`. `side="inlet"` = + 0.7. */
|
||||
export function pipeWallMinHeightM(diameterM: number, side = ""): number {
|
||||
const raw = diameterM;
|
||||
const steps = Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9);
|
||||
return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3);
|
||||
const add = side === "inlet" ? INLET_REVET_ADD_M : REVET_EMBED_DEPTH_M;
|
||||
return round(steps * REVET_HEIGHT_STEP_M + add, 3);
|
||||
}
|
||||
|
||||
/** 관 벽 · 유입 집수정 높이 칸 읽기 — 옛 기본 1.5 이고 관경 > 1.0 이면 관경 기준 값(139-1) ·
|
||||
@@ -71,7 +74,8 @@ export function pipeWallHeightM(stored: number | null, diameterM: number): numbe
|
||||
return stored;
|
||||
}
|
||||
|
||||
/** 관 보호공 · 유입 집수정 높이 칸 — 새 관 기본 = 관경 + 0.5(139-1 · 서버 짝 `PIPE_HEIGHT_KEYS`). */
|
||||
/** 관 보호공 · 유입 집수정 높이 칸 — 새 관 기본 = 관경 + 0.5(139-1) · 유입 흙막이만 + 0.7(211-1)
|
||||
* (서버 짝 `PIPE_HEIGHT_KEYS`). */
|
||||
export const PIPE_HEIGHT_KEYS = [
|
||||
"inlet_revet_height_m",
|
||||
"outlet_revet_height_m",
|
||||
@@ -86,12 +90,14 @@ export function fillPipeHeights(
|
||||
const diameterM = Number(values.pipe_diameter_mm) / 1000;
|
||||
if (!(diameterM > 0)) return;
|
||||
for (const key of PIPE_HEIGHT_KEYS)
|
||||
if (keys.includes(key)) values[key] ??= pipeWallMinHeightM(diameterM);
|
||||
if (keys.includes(key))
|
||||
values[key] ??= pipeWallMinHeightM(diameterM, key === "inlet_revet_height_m" ? "inlet" : "");
|
||||
}
|
||||
|
||||
/** 배관 관 보호공 높이 칸(유입 · 유출 · 유입 집수정 = 항목 `height`) — 관경 + 0.5 규칙(139-1).
|
||||
/** 배관 관 보호공 높이 칸(유입 · 유출 · 유입 집수정 = 항목 `height`) — 관경 + 0.5 규칙(139-1) ·
|
||||
* 유입 흙막이 + 0.7(211-1).
|
||||
* `previousMm` 없음 = 읽을 때(옛 1.5 · 관경 > 1.0 옮김) · 있음 = 관경을 바꿈(옛 관경 규칙 값 · 옛 1.5
|
||||
* 칸만 새 규칙 값으로 · 손 값 · 빈 칸은 그대로). */
|
||||
* 칸만 새 규칙 값으로 · 손 값 · 빈 칸은 그대로 · 유입은 옛 + 0.5 값도 새 + 0.7 로 따라감). */
|
||||
export function followPipeHeights(
|
||||
state: Record<string, Array<Record<string, string>> | undefined>,
|
||||
diameterMm: string,
|
||||
@@ -99,7 +105,13 @@ export function followPipeHeights(
|
||||
): void {
|
||||
const d = Number(diameterMm) / 1000;
|
||||
if (!(d > 0)) return;
|
||||
const prev = previousMm ? pipeWallMinHeightM(Number(previousMm) / 1000) : null;
|
||||
const prevOf = (id: string): Array<number | null> => {
|
||||
if (!previousMm) return [];
|
||||
const p = Number(previousMm) / 1000;
|
||||
return id === "inlet"
|
||||
? [pipeWallMinHeightM(p, id), pipeWallMinHeightM(p)]
|
||||
: [pipeWallMinHeightM(p)];
|
||||
};
|
||||
for (const id of ["inlet", "outlet"]) {
|
||||
const values = state[id]?.[0];
|
||||
const raw = values?.height ?? "";
|
||||
@@ -108,8 +120,10 @@ export function followPipeHeights(
|
||||
const moved =
|
||||
previousMm === undefined
|
||||
? pipeWallHeightM(now, d)
|
||||
: [prev, OLD_PIPE_WALL_DEFAULT_M].some((v) => v !== null && Math.abs(now - v) < 1e-9)
|
||||
? pipeWallMinHeightM(d)
|
||||
: [...prevOf(id), OLD_PIPE_WALL_DEFAULT_M].some(
|
||||
(v) => v !== null && Math.abs(now - v) < 1e-9,
|
||||
)
|
||||
? pipeWallMinHeightM(d, id)
|
||||
: now;
|
||||
if (moved !== null && moved !== now) values.height = String(moved);
|
||||
}
|
||||
@@ -170,7 +184,7 @@ function sideSpec(
|
||||
|
||||
const height =
|
||||
pipeWallHeightM(num(options[`${side}_revet_height_m`], null), diameterM) ??
|
||||
pipeWallMinHeightM(diameterM);
|
||||
pipeWallMinHeightM(diameterM, side);
|
||||
const length = num(options[`${side}_revet_length_m`], null);
|
||||
const form = text(options[`${side}_revet_form`]);
|
||||
const before = num(options[`${side}_revet_before_m`], null);
|
||||
|
||||
@@ -85,3 +85,16 @@
|
||||
62. B09의 좌측 패널의 컨테이너를 공용양식을 사용한건지 확인 (확인만)
|
||||
63. B05/B06의 구조물배치 컨테이너에서 삭제버튼과 리셋버튼이 높이사이즈를 하위 컨테이너의 버튼 높이와 동일하게 구현하고 내부 컨테이너에서 삭제와 리셋버튼이 항상보이게 구현
|
||||
64. [사진] 현재 검증 서브1 프로젝트의 1+0 측점의 내용임. 이때 원래는 측구가 없어야하는 성토사면에 존재함. 이때 측구버튼과 일반L형 버튼이 없음. 숨기기 결과로 보임. 이런 현상의 원인이 궁금함. 한번 좌우를 바꾸면 재현안됨. 확실히 조사하여 반영할것. (사진: 1+0 · 양측 성토 · 성토사면 좌 18.61m · 우 0.26m · 좌|우 단추 중 「우」 켜짐 · 오른쪽 노견 바깥에 작은 측구 모양 · 측구 · 일반/L형 단추 안 보임)
|
||||
65. B06의 상단의 성토 5m 이상, 포장 권장, 소단 검토에서 버튼의 형상을 사각 라운드로 변경하고 누를시 나오는 버튼들은 현재를 유지, 그리고 메인 페이지의 스크롤이 제일 상단에 있을경우 상단 버튼으로 내역을 열면 페이지가 아래로 밀림. 안밀리게 변경, 그리고 항목을 선택하면 이동하면서 해당 버튼셋은 접힘으로 그리고 만약 선택된 상태에서 다시 열면 하이라이트 기능, 다시 말해 사용자가 측점 횡단도를 누른상태에서 상단 3버튼을 누르면 하이라이트 되게 변경
|
||||
66. M01의 단가산출로직들의 항목에 신규열인 별칭 열이 필요함 설계자들이 찾을때 사용하는 명칭과 다른 경우가 있음. 이를 보완하기 위해 별칭열을 추가해줘. 내가 별칭을 있을수도 있고 없을 수도 있지만 값은 내가 넣을거야. 그리고 이때 프로그램 전역에서 단가산출 관련 검색은 별칭도 같이 검색이 되게 해야함.
|
||||
|
||||
## 셋째 붙임 (10-10 오후)
|
||||
67. B06 횡단도 상세 모달 — 셀에 지정된 면적과 길이값을 선택하게 하였는데 안잡히는 경우가 있어 개선이 필요함. 상세 모달창에서 선들이 교차되는 지점들을 기준으로 선들이 분할되어야함. 그리고 선택할때는 다중 선택을 하면 원지반도 가능해야함. 그럼 문제 없어보임. 그럼 면적에서도 폐쇄 영역을 지정하기 좋아보임. 추가로 선택영 지점의 면적과 선들의 길이의 값을 보는 기능이 있었으면 함. 이는 메인페이지의 기능이 있으니 확인, 잘 반영되었다면 메인페이지에도 구현한 생각임.
|
||||
68. 간혹가다 B06의 상단에 누락된 구조물 측점이라고 측점추가 버튼이 나오는데 이유는? (조사)
|
||||
69. 관보호공 유입 측 흙막이의 경우에 기본 높이를 배관 + 0.5m로 지정되어 있을것임. 이걸 0.7m로 변경
|
||||
70. 도수로/돌붙입의 구조물 형상을 잡고 이동하는 경우 토스트 메세지로 '기준측점을 옮겼습니다 ....' 나오는데 나는 도수로와 돌붙임의 이동에 대해 요청한건 없음. 왜그런지 분석하고 알려줘. (조사)
|
||||
71. B07 (기반영요청사항) 횡단도의 격자에 배치를 옮기기 위해 선을 선택할때 원지반선이 안잡힘
|
||||
72. B07 (기반영요청사항) 추가로 레이어별로 측점의 원지반선과 설계선을 잡을 수 있는 레이어 자동 생성
|
||||
73. B07 횡단도의 붉은색 격사선을 하이드쇼할수 있는 버튼 기능은 좋은데 뒤에 검은 격자선이 하나더 있음. 확인 후 삭제
|
||||
74. B07 횡단도 표준 옵션의 버튼들이 양식화와 일원화 안되어 있음. 확인후 정리할 수 있는 양식으로 적용
|
||||
76. 다음으로 중요한 문제인데 특정 측점에 2개의 측점이 숨어 있는 경우가 있음. 엄상준_검증 프로젝트에서 13+3 지점에 대해 이유를 확인해줘. 원인파악을 제대로 할것 계속 반복적으로 동일한 문제가 발생됨
|
||||
|
||||
@@ -59,13 +59,16 @@ def test_화면_높이_규칙(tmp_path: Path) -> None:
|
||||
assert run.returncode == 0, run.stderr[-2000:]
|
||||
got = json.loads(result.read_text(encoding="utf-8"))
|
||||
|
||||
for key in ("inlet_revet_height_m", "outlet_revet_height_m", "inlet_basin_height_m"):
|
||||
for key in ("outlet_revet_height_m", "inlet_basin_height_m"):
|
||||
assert got["fresh"][key] == pytest.approx(1.7), key # Φ1200 → 1.7
|
||||
assert got["fresh"]["inlet_revet_height_m"] == pytest.approx(1.9) # 유입 흙막이 + 0.7(211-1)
|
||||
assert "inlet_revet_height_m" not in got["noPipe"] # 관경 없음 = 안 채움
|
||||
assert got["read"]["inlet"][0]["height"] == "1.7" # 옛 1.5 · Φ1200 → 1.7
|
||||
assert got["read"]["outlet"][0]["height"] == "1.6" # 손 값 그대로
|
||||
assert got["small"]["inlet"][0]["height"] == "1.5" # Φ800 의 1.5 는 그대로
|
||||
assert got["change"]["inlet"][0]["height"] == "2" # 옛 규칙 값(1.7) → 새 규칙(2.0)
|
||||
assert (
|
||||
got["change"]["inlet"][0]["height"] == "2.2"
|
||||
) # 옛 + 0.5 값(1.7) → 유입 새 규칙(1.5 + 0.7)
|
||||
assert got["change"]["outlet"][0]["height"] == "2.2" # 손 값 그대로
|
||||
assert got["old"]["inlet"][0]["height"] == "1.3" # 옛 1.5 → Φ800 규칙
|
||||
assert got["old"]["inlet"][0]["height"] == "1.5" # 옛 1.5 → Φ800 유입 규칙(0.8 + 0.7)
|
||||
assert got["old"]["outlet"][0]["height"] == "" # 빈 칸 그대로
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 206-1 — 양측 성토 측점에 측구가 남는 흠(검증_sub1 No.1+0 사본 재현).
|
||||
|
||||
원인 — 측구 판정의 「양측 성토 = 측구 없음」이 **입력** `section_mode` 를 봤다. 입력은 옛 저장값
|
||||
(초기값 right_cut)이고, 계획고를 +1.98 m 올려 지형 판정(역할)은 양측 성토가 됐는데 입력이 그대로라
|
||||
옛 결과 `ditch_enabled=True` 가 사용자 선택으로 둔갑(ditch_choice=True)해 측구가 섰다.
|
||||
좌 · 우를 누르면 카드가 판정값 both_fill 을 입력으로 다시 보내 그때서야 사라졌다.
|
||||
고침 — 판정된 역할(양쪽 성토)로 가름 · 파이썬 · TS 짝 같은 규칙 · 옛 저장분은 낡음으로 다시 셈.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||
|
||||
# 검증_sub1(7e420ed6) 노선 230 · No.1+0(20 m) 지반선 사본 — -20 ~ +20 m · 0.5 m 간격
|
||||
_Z = [
|
||||
885.0166, 884.8327, 884.6401, 884.4294, 884.2270, 883.7564, 883.2482, 882.8840, 882.5737,
|
||||
882.3786, 882.2795, 881.9692, 881.3788, 880.7761, 880.1219, 879.4944, 879.1738, 878.8550,
|
||||
878.6597, 878.4670, 878.1648, 877.8416, 877.5119, 877.1749, 876.8564, 876.5584, 876.2612,
|
||||
875.9646, 875.6791, 875.4169, 875.1405, 874.7286, 874.3079, 874.0026, 873.7065, 873.4074,
|
||||
873.1107, 872.7726, 872.4127, 872.1306, 871.9252, 871.7424, 871.6078, 871.3723, 870.7716,
|
||||
870.1964, 869.8586, 869.5135, 869.2563, 869.0105, 868.5769, 868.0912, 867.7274, 867.4369,
|
||||
867.1615, 866.9033, 866.6590, 866.4489, 866.2281, 865.9711, 865.7139, 865.4391, 865.1650,
|
||||
864.8953, 864.6289, 864.3219, 863.9996, 863.6703, 863.3343, 863.0443, 862.8261, 862.5776,
|
||||
862.2364, 861.9035, 861.6491, 861.3932, 861.1137, 860.8302, 860.6107, 860.4051, 860.2006,
|
||||
] # fmt: skip
|
||||
SAMPLES = [{"offset_m": -20.0 + 0.5 * i, "elevation_m": z} for i, z in enumerate(_Z)]
|
||||
ELEV = 873.900731 # 지금 계획고(초기값 871.9252 에서 +1.98 m)
|
||||
# 그 프로젝트 저장분과 같은 입력 — 옛 단면유형 · 옛 측구 결과
|
||||
STORED = {
|
||||
"ground_type": "ripping_rock",
|
||||
"ditch_side": "right",
|
||||
"ditch_type": "standard",
|
||||
"rock_boundary_offset_m": -0.5,
|
||||
"two_stage_slope": True,
|
||||
}
|
||||
|
||||
|
||||
_GROUP = {
|
||||
"road_width_m": 3,
|
||||
"shoulder_left_m": 0.5,
|
||||
"shoulder_right_m": 0.5,
|
||||
"ditch": {"top_width_m": 0.9, "bottom_width_m": 0.3, "depth_m": 0.3},
|
||||
"fill_slope_ratio": 1.2,
|
||||
}
|
||||
# 그 프로젝트 종단 options 의 표준 횡단면
|
||||
STANDARD = {
|
||||
"soil": {**_GROUP, "cross_slope_pct": {"min": 3, "max": 5}, "cut_slope_ratio": 1},
|
||||
"rock": {
|
||||
**_GROUP,
|
||||
"ditch_l_type": {"width_m": 0.5, "depth_m": 0.1},
|
||||
"cross_slope_pct": {"min": 3, "max": 5},
|
||||
"cut_slope_ratio": 0.4,
|
||||
},
|
||||
"paved": {
|
||||
**_GROUP,
|
||||
"cross_slope_pct": {"min": 1.5, "max": 2},
|
||||
"cut_slope_ratio": 1,
|
||||
"pavement_thickness_m": 0.2,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _design(section_mode: str, **kwargs):
|
||||
return compute_cross_design(
|
||||
SAMPLES, ELEV, section_mode=section_mode, standard=STANDARD, **STORED, **kwargs
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stale",
|
||||
[
|
||||
{"ditch_enabled": True}, # 옛 결과만(초기값 → 계획고 변경 재계산)
|
||||
{"ditch_enabled": True, "ditch_choice": True}, # 둔갑한 선택이 저장된 뒤(지금 DB)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("mode", ["right_cut", "both_cut", "left_cut"])
|
||||
def test_사본_양측_성토면_옛_입력이어도_측구_없음(mode: str, stale: dict) -> None:
|
||||
design = _design(mode, **stale)
|
||||
assert design["section_mode"] == "both_fill"
|
||||
assert design["ditch_enabled"] is False, "양측 성토인데 측구가 섰다"
|
||||
assert design["ditch_area_m2"] == 0.0
|
||||
assert design["ditch"] == {"type": "none"}
|
||||
assert design["ditch_choice"] is None, "옛 결과가 선택으로 남았다"
|
||||
|
||||
|
||||
def test_사본_옛_입력과_바른_입력이_같은_형상() -> None:
|
||||
"""좌 · 우를 눌러야만 사라지던 것 — 이제 입력이 옛 값이어도 처음부터 같은 형상 · 면적."""
|
||||
stale = _design("right_cut", ditch_enabled=True, ditch_choice=True)
|
||||
fresh = _design("both_fill")
|
||||
for key in ("design_line", "fill_area_m2", "cut_area_m2", "ditch_area_m2"):
|
||||
assert stale[key] == fresh[key], key
|
||||
assert fresh["fill_area_m2"] == pytest.approx(34.0513, abs=1e-4)
|
||||
# 우 노견 바깥에 측구 꼭짓점(-2.3 · -2.6 · -2.9 m = 상단 0.9 · 저폭 0.3 사다리꼴)이 없다
|
||||
offsets = {p["offset_m"] for p in fresh["design_line"]}
|
||||
assert not offsets & {-2.3, -2.6, -2.9}
|
||||
|
||||
|
||||
def test_한쪽이라도_절토면_측구_그대로() -> None:
|
||||
"""양측 성토 판정은 역할로만 — 우 절토 지형은 옛 입력 both_fill 이어도 측구 섬."""
|
||||
design = _design("both_fill", ditch_enabled=False)
|
||||
assert design["section_mode"] == "both_fill" # 이 지형은 양측 성토
|
||||
cut = compute_cross_design(SAMPLES, ELEV - 2.5, section_mode="both_fill", **STORED)
|
||||
assert cut["section_mode"] in ("right_cut", "both_cut")
|
||||
assert cut["ditch_enabled"] is True and cut["ditch_side"] == "right"
|
||||
|
||||
|
||||
def test_TS_짝도_같은_값(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""거울 — 같은 사본을 TS `computeCrossDesign` 에 넣어 파이썬과 같은지(측구 없음까지)."""
|
||||
import test_b06_cross_design_mirror as mirror
|
||||
|
||||
cases = [
|
||||
{
|
||||
"name": f"206-1 사본 {mode}",
|
||||
"samples": SAMPLES,
|
||||
"design_elevation_m": ELEV,
|
||||
"options": {
|
||||
"section_mode": mode,
|
||||
**STORED,
|
||||
"ditch_enabled": True,
|
||||
"ditch_choice": True,
|
||||
},
|
||||
}
|
||||
for mode in ("right_cut", "both_fill")
|
||||
]
|
||||
monkeypatch.setattr(mirror, "CASES", cases)
|
||||
py, ts = mirror._python_results(), mirror._ts_results(tmp_path)
|
||||
problems = []
|
||||
for case, left, right in zip(cases, py, ts):
|
||||
problems.extend(mirror._diff(left, right, case["name"]))
|
||||
assert not problems, "\n".join(problems[:20])
|
||||
assert all(item["ditch_enabled"] is False for item in ts)
|
||||
|
||||
|
||||
def _src(rel: str) -> str:
|
||||
return (PROJECT_ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_옛_저장분은_낡음으로_다시_셈() -> None:
|
||||
"""DB 에 남은 양측 성토 + 측구 결과 → B05 · B06 진입 때 브라우저가 다시 셈(공용 낡음 판정)."""
|
||||
common = _src("B06_Section/B06_Section_UI_Section_Common.ts")
|
||||
assert 'design.section_mode === "both_fill" && design.ditch_enabled === true' in common
|
||||
|
||||
|
||||
def test_측구가_그려져_있으면_단추를_숨기지_않음() -> None:
|
||||
"""179-2 숨김은 「막힘 + 그려진 측구 없음」일 때만 — 그려진 측구는 끌 수 있어야 함."""
|
||||
cd = _src("B06_Section/B06_Section_UI_Cross_Design.ts")
|
||||
assert "const ditchDrawn = design?.ditch_enabled === true;" in cd
|
||||
assert 'const hasDitch = state.mode !== "both_fill" || ditchDrawn;' in cd
|
||||
assert '(state.mode !== "both_fill" || ditchDrawn)' in cd # 측구형식도 같은 규칙
|
||||
@@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 208-1 — 단가산출 로직 「별칭」 칸 · 로직 찾기는 이름 + 별칭.
|
||||
|
||||
① 서버 찾기 맞춤(`M01_MasterData_Store_Find.hit`) ↔ 화면 공용 맞춤(`ui_template_logic_find.ts`) 거울
|
||||
② 읽기(없으면 빈칸) · 저장 왕복 — 임시 사본 폴더만(정본 마스터 파일은 안 바뀜)
|
||||
③ M01 목록 「별칭」 열(이름 옆) · 쓰는 곳마다 공용 맞춤을 부름
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from M01_MasterData import M01_MasterData_Router as router_module
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
from M01_MasterData.M01_MasterData_Store_Find import hit
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
REAL = store.FOLDER
|
||||
LOGIC_KEY = "GC000091" # 공통 3-3-1 암발파(미진동굴착 TYPE-Ⅰ)
|
||||
LOGIC_FILE = "로직_건설품셈_03장_토공사.json"
|
||||
|
||||
ROW = {
|
||||
"키": "GF000001",
|
||||
"원문번호": "9-1-1",
|
||||
"이름": "흙깎기(보통토사)",
|
||||
"별칭": "절토, 터파기 기계",
|
||||
}
|
||||
# (찾는 글, 맞음, 별칭으로 맞은 것)
|
||||
CASES = [
|
||||
("흙깎기", True, ""),
|
||||
("흙 깎기", True, ""), # 띄어쓰기 무시 · 낱말마다
|
||||
("절토", True, "절토"),
|
||||
("터파기", True, "터파기 기계"),
|
||||
("터파기기계", True, "터파기 기계"), # 별칭 안 띄어쓰기 무시
|
||||
("절토 기계", True, "절토, 터파기 기계"), # 낱말이 여러 별칭에 나뉘어 듦
|
||||
("흙깎기 절토", True, "절토"), # 이름 + 별칭
|
||||
("gf000001", True, ""),
|
||||
("9-1", True, ""),
|
||||
("성토", False, ""),
|
||||
("", True, ""),
|
||||
]
|
||||
|
||||
|
||||
def _node(src: str) -> str:
|
||||
if shutil.which("node") is None:
|
||||
pytest.skip("node 가 없음")
|
||||
done = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--input-type=module", "-e", src],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
cwd=ROOT,
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return done.stdout
|
||||
|
||||
|
||||
def test_서버_맞춤_이름_별칭_여러_별칭() -> None:
|
||||
for q, ok, _ in CASES:
|
||||
assert hit(ROW, q) is ok, q
|
||||
assert hit({**ROW, "별칭": ""}, "절토") is False # 별칭 없음 = 빈칸
|
||||
assert hit({k: v for k, v in ROW.items() if k != "별칭"}, "흙깎기") is True
|
||||
|
||||
|
||||
def test_화면_맞춤_거울_서버와_같음() -> None:
|
||||
url = (ROOT / "ui_template" / "ui_template_logic_find.ts").as_uri()
|
||||
src = f"""
|
||||
import {{ matchLogic, aliasText, aliasesOf, aliasNote }} from "{url}";
|
||||
const row = {json.dumps(ROW, ensure_ascii=False)};
|
||||
const cases = {json.dumps([c[0] for c in CASES], ensure_ascii=False)};
|
||||
console.log(JSON.stringify({{
|
||||
hits: cases.map((q) => matchLogic(q, row)),
|
||||
text: aliasText(" 절토 ,, 터파기 기계,"),
|
||||
none: aliasesOf(undefined),
|
||||
note: [aliasNote("절토"), aliasNote("")],
|
||||
chapter: matchLogic("토공", {{ ...row, 상세구분: "09장 토공" }}).ok,
|
||||
}}));
|
||||
"""
|
||||
got = json.loads(_node(src))
|
||||
for (q, ok, alias), one in zip(CASES, got["hits"]):
|
||||
assert one == {"ok": ok, "alias": alias}, q
|
||||
assert one["ok"] is hit(ROW, q), q # 서버와 같은 판정
|
||||
assert got["text"] == "절토, 터파기 기계"
|
||||
assert got["none"] == []
|
||||
assert got["note"] == ["별칭 절토", ""]
|
||||
assert got["chapter"] is True # 구분 · 상세구분은 화면만 더 봄
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path: Path, monkeypatch) -> TestClient:
|
||||
for path in REAL.glob("*.json"):
|
||||
if not path.name.startswith("_") or path.name == store.mk.BOOK.name:
|
||||
shutil.copy(path, tmp_path / path.name)
|
||||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||||
app = FastAPI()
|
||||
app.include_router(router_module.router)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _logics(client: TestClient, **params) -> list[dict]:
|
||||
res = client.get("/api/m01/logics", params=params)
|
||||
assert res.status_code == 200, res.text
|
||||
return res.json()["logics"]
|
||||
|
||||
|
||||
def _save(client: TestClient, row: dict) -> None:
|
||||
one = client.get("/api/m01/logic", params={"key": LOGIC_KEY}).json()
|
||||
part = {
|
||||
"file": one["file"],
|
||||
"version": one["version"],
|
||||
"changes": [{"op": "edit", "key": LOGIC_KEY, "row": row}],
|
||||
}
|
||||
res = client.post("/api/m01/save", json={"files": [part]})
|
||||
assert res.status_code == 200, res.text
|
||||
|
||||
|
||||
def test_읽기_빈칸_저장_왕복_임시_사본만(client: TestClient) -> None:
|
||||
real = (REAL / LOGIC_FILE).read_bytes()
|
||||
listed = _logics(client)
|
||||
assert listed and all(isinstance(x["별칭"], str) for x in listed) # 없으면 빈칸
|
||||
one = client.get("/api/m01/logic", params={"key": LOGIC_KEY}).json()
|
||||
base = one["logic"]
|
||||
assert "별칭" not in base # 마스터 파일에 일괄로 쓰지 않음
|
||||
|
||||
_save(client, {**base, "별칭": "미진동 발파, 암깨기"})
|
||||
got = [x for x in _logics(client, q="암깨기") if x["키"] == LOGIC_KEY]
|
||||
assert got and got[0]["별칭"] == "미진동 발파, 암깨기"
|
||||
assert LOGIC_KEY in [x["키"] for x in _logics(client, q="미진동 발파")]
|
||||
found = client.get("/api/m01/elements", params={"group": "로직", "q": "암깨기"}).json()
|
||||
assert [x["별칭"] for x in found["items"] if x["ref"] == LOGIC_KEY] == ["미진동 발파, 암깨기"]
|
||||
saved = json.loads((store.FOLDER / LOGIC_FILE).read_text(encoding="utf-8"))
|
||||
assert next(r for r in saved["줄"] if r["키"] == LOGIC_KEY)["별칭"] == "미진동 발파, 암깨기"
|
||||
|
||||
_save(client, base) # 지우면 칸째 빠짐 · 찾기에도 안 걸림
|
||||
assert LOGIC_KEY not in [x["키"] for x in _logics(client, q="암깨기")]
|
||||
after = json.loads((store.FOLDER / LOGIC_FILE).read_text(encoding="utf-8"))
|
||||
assert after == json.loads(real.decode("utf-8")) # 왕복 뒤 내용 그대로
|
||||
assert (REAL / LOGIC_FILE).read_bytes() == real # 정본은 안 건드림
|
||||
|
||||
|
||||
def _src(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_M01_목록_별칭_열은_이름_옆_고치면_캐시() -> None:
|
||||
ts = _src("M01_MasterData/M01_MasterData_UI_Logic_List.ts")
|
||||
cols = ts[ts.index("const COLS") : ts.index("as const")]
|
||||
names = [x.strip().strip('"') for x in cols[cols.index("[") + 1 :].split(",")]
|
||||
assert names[names.index("이름") + 1] == "별칭" # 이름 바로 옆
|
||||
assert "aliasCell(item" in ts and "onAlias(item, next)" in ts
|
||||
assert "e.stopPropagation()" in ts # 별칭 칸을 눌러도 로직이 안 열림
|
||||
lab = _src("M01_MasterData/M01_MasterData_UI_LogicLab.ts")
|
||||
assert "const onAlias = async" in lab and "list.setMarks(marks, extra, aliases)" in lab
|
||||
# 자동저장 없음 — 캐시(drafts)에만 · [저장] 이 보냄
|
||||
body = lab[lab.index("const onAlias = async") : lab.index("const onDelete = async")]
|
||||
assert "saveFiles" not in body and "persist()" in body
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rel",
|
||||
[
|
||||
"M01_MasterData/M01_MasterData_UI_Logic_List.ts", # M01 로직 목록
|
||||
"M01_MasterData/M01_MasterData_UI_Logic_Pick.ts", # M01 요소 찾기 창 · 로직 그룹
|
||||
"M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Build.ts", # M02 흐름도 다른 로직 고르기
|
||||
"M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Logic.ts", # 〃 결과 별칭 표시
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts", # M02 · B09 [로직 더하기]
|
||||
],
|
||||
)
|
||||
def test_쓰는_곳마다_공용_맞춤을_부름(rel: str) -> None:
|
||||
ts = _src(rel)
|
||||
assert 'from "@ui/ui_template_logic_find"' in ts
|
||||
assert "matchLogic(" in ts
|
||||
|
||||
|
||||
def test_서버_찾기도_한_벌() -> None:
|
||||
py = _src("M01_MasterData/M01_MasterData_Store.py")
|
||||
assert "import hit as _hit" in py and "def _hit" not in py
|
||||
@@ -0,0 +1,219 @@
|
||||
"""211-1 관 보호공 **유입** 흙막이 빈 칸 기본 높이 = 관경(0.1 올림) + 0.7 — 유출 · 집수정은 + 0.5 그대로.
|
||||
|
||||
못박는 것
|
||||
- 파이썬 `pipe_wall_min_height_m(d, "inlet")` · 새 관 기본값(`_apply_new_defaults`) · 관 세트 스펙 · B08 집계 빈 높이.
|
||||
- TS 짝 `buildCulvertSets` ↔ 파이썬 `_culvert_set`(유입 · 유출 스펙 같은 값) · 횡단도 `revetWallSpec`.
|
||||
- 저장된 높이는 그대로 · 유출 · 집수정 무변 · 흙막이 아닌 시설(숨은 관)은 본체 높이 그대로.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B04_PreProcess import B04_PreProcess_Router_Basins as basins
|
||||
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
||||
from B06_Section import B06_Section_Engine_Culvert as culvert
|
||||
from B08_Quantity import B08_Quantity_Router_Summary as summary
|
||||
from common_util.common_util_drainage_pipes import PipePoint
|
||||
from M02_MasterTemplete import M02_Item_Snapshot as snapshots
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
|
||||
|
||||
def _revet(side: str, height: float | None = None) -> dict:
|
||||
found = {
|
||||
f"{side}_type": "기슭막이",
|
||||
f"{side}_revet_form": "돌쌓기(메)",
|
||||
f"{side}_revet_before_m": 5,
|
||||
f"{side}_revet_after_m": 5,
|
||||
}
|
||||
if height is not None:
|
||||
found[f"{side}_revet_height_m"] = height
|
||||
return found
|
||||
|
||||
|
||||
def test_파이썬_기본_높이_유입만_0_7():
|
||||
h = culvert.pipe_wall_min_height_m
|
||||
assert h(0.8, "inlet") == 1.5 and h(0.8, "outlet") == 1.3
|
||||
assert h(1.2, "inlet") == 1.9 and h(1.2) == 1.7 # 쪽 안 줌 = 옛 값(집수정)
|
||||
assert h(1.0, "inlet") == 1.7 and h(1.0, "outlet") == 1.5
|
||||
assert h(0.45, "inlet") == 1.2 # 0.5 로 올림 + 0.7
|
||||
|
||||
|
||||
def test_새_관_기본값_유입_흙막이만_0_7():
|
||||
fresh = PipePoint(chainage_m=10.0, source="stream", options={"pipe_diameter_mm": 1200})
|
||||
basins._apply_new_defaults([fresh])
|
||||
assert fresh.options["inlet_revet_height_m"] == pytest.approx(1.9)
|
||||
assert fresh.options["outlet_revet_height_m"] == pytest.approx(1.7)
|
||||
assert fresh.options["inlet_basin_height_m"] == pytest.approx(1.7)
|
||||
# 이미 적은 높이(추천 · 손 값)는 그대로 · 나머지 칸만 채움
|
||||
kept = PipePoint(
|
||||
chainage_m=20.0,
|
||||
source="stream",
|
||||
options={"pipe_diameter_mm": 1200, "inlet_revet_height_m": 1.2},
|
||||
)
|
||||
basins._apply_new_defaults([kept])
|
||||
assert kept.options["inlet_revet_height_m"] == 1.2
|
||||
assert kept.options["outlet_revet_height_m"] == pytest.approx(1.7)
|
||||
|
||||
|
||||
def test_관_세트_빈_높이_유입_0_7_저장값_그대로():
|
||||
blank = culvert._culvert_set({"pipe_diameter_mm": 1200, **_revet("inlet"), **_revet("outlet")})
|
||||
assert blank["inlet"]["revet_height_m"] == pytest.approx(1.9)
|
||||
assert blank["outlet"]["revet_height_m"] == pytest.approx(1.7)
|
||||
stored = culvert._culvert_set(
|
||||
{"pipe_diameter_mm": 1200, **_revet("inlet", 1.2), **_revet("outlet", 2.4)}
|
||||
)
|
||||
assert stored["inlet"]["revet_height_m"] == 1.2 and stored["outlet"]["revet_height_m"] == 2.4
|
||||
# 옛 기본 1.5 · 관경 > 1.0 = 저장 값 해석(139-1) — 관경 + 0.5 그대로
|
||||
legacy = culvert._culvert_set({"pipe_diameter_mm": 1200, **_revet("inlet", 1.5)})
|
||||
assert legacy["inlet"]["revet_height_m"] == pytest.approx(1.7)
|
||||
|
||||
|
||||
_RUNNER = """
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { buildCulvertSets } from "./common_util_culvert_sets.js";
|
||||
const input = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
||||
const sets = buildCulvertSets(input.points, input.registry);
|
||||
writeFileSync(process.argv[3], JSON.stringify([...sets.entries()]));
|
||||
"""
|
||||
|
||||
_WALL_RUNNER = """
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const.js";
|
||||
const pick = (spec, hidden, auto) => revetWallSpec(spec, {}, hidden, 1.2, auto).pureHeight;
|
||||
const base = { structure: "기슭막이" };
|
||||
writeFileSync(process.argv[2], JSON.stringify({
|
||||
inlet: pick({ ...base, role: "inlet" }, false),
|
||||
outlet: pick({ ...base, role: "outlet" }, false),
|
||||
stored: pick({ ...base, role: "inlet", revet_height_m: 1.2 }, false),
|
||||
hidden: pick({ ...base, role: "inlet" }, true, 2.0),
|
||||
}));
|
||||
"""
|
||||
|
||||
|
||||
def _tsc(source: Path, out: Path) -> None:
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일 · 타입 오류가 있어도 JS 는 나옴
|
||||
[
|
||||
"node",
|
||||
str(TSC),
|
||||
str(source),
|
||||
"--outDir",
|
||||
str(out),
|
||||
"--module",
|
||||
"esnext",
|
||||
"--target",
|
||||
"es2022",
|
||||
"--moduleResolution",
|
||||
"bundler",
|
||||
"--skipLibCheck",
|
||||
"--ignoreConfig",
|
||||
],
|
||||
cwd=str(ROOT),
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
|
||||
def test_TS_세트_거울(tmp_path: Path) -> None:
|
||||
points = [
|
||||
{
|
||||
"chainage_m": 20.0,
|
||||
"facility": "pipe",
|
||||
"options": {"pipe_diameter_mm": 1200, **_revet("inlet"), **_revet("outlet")},
|
||||
},
|
||||
{
|
||||
"chainage_m": 40.0,
|
||||
"facility": "pipe",
|
||||
"options": {"pipe_diameter_mm": "800", **_revet("inlet", 1.2), **_revet("outlet")},
|
||||
},
|
||||
{
|
||||
"chainage_m": 60.0,
|
||||
"facility": "pipe",
|
||||
"options": {"pipe_diameter_mm": 1200, **_revet("inlet", 1.5)}, # 옛 1.5
|
||||
},
|
||||
]
|
||||
registry = {
|
||||
type_id: {o.key: o.default for o in entry.options}
|
||||
for type_id, entry in structure_type_map().items()
|
||||
if type_id in ("pipe", "ford_bridge", "ford_pavement", "box_culvert")
|
||||
}
|
||||
out = tmp_path / "js"
|
||||
_tsc(ROOT / "common_util" / "common_util_culvert_sets.ts", out)
|
||||
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
||||
payload, result = tmp_path / "in.json", tmp_path / "out.json"
|
||||
payload.write_text(json.dumps({"points": points, "registry": registry}), encoding="utf-8")
|
||||
run = subprocess.run( # noqa: S603
|
||||
["node", str(out / "runner.mjs"), str(payload), str(result)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert run.returncode == 0, run.stderr[-2000:]
|
||||
got = {at: spec for at, spec in json.loads(result.read_text(encoding="utf-8"))}
|
||||
for point in points:
|
||||
want = culvert._culvert_set(point["options"])
|
||||
spec = got[point["chainage_m"]]
|
||||
for side in ("inlet", "outlet"):
|
||||
assert spec[side].get("revet_height_m") == want[side].get("revet_height_m"), point
|
||||
assert got[20.0]["inlet"]["revet_height_m"] == pytest.approx(1.9)
|
||||
assert got[20.0]["outlet"]["revet_height_m"] == pytest.approx(1.7)
|
||||
assert got[40.0]["inlet"]["revet_height_m"] == 1.2 # 저장 값 그대로
|
||||
assert got[40.0]["outlet"]["revet_height_m"] == pytest.approx(1.3)
|
||||
assert got[60.0]["inlet"]["revet_height_m"] == pytest.approx(1.7) # 옛 1.5 → 관경 + 0.5
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
|
||||
def test_횡단도_벽_기본_높이(tmp_path: Path) -> None:
|
||||
out = tmp_path / "js"
|
||||
_tsc(ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Const.ts", out)
|
||||
built = next(
|
||||
out.rglob("B06_Section_UI_Cross_Culvert_Const.js")
|
||||
) # 가져온 파일 따라 경로가 깊어짐
|
||||
runner = built.parent / "runner.mjs"
|
||||
runner.write_text(_WALL_RUNNER, encoding="utf-8")
|
||||
result = tmp_path / "out.json"
|
||||
run = subprocess.run( # noqa: S603
|
||||
["node", str(runner), str(result)], capture_output=True, text=True
|
||||
)
|
||||
assert run.returncode == 0, run.stderr[-2000:]
|
||||
got = json.loads(result.read_text(encoding="utf-8"))
|
||||
assert got["inlet"] == pytest.approx(1.9) # Φ1200 + 0.7
|
||||
assert got["outlet"] == pytest.approx(1.7) # 무변
|
||||
assert got["stored"] == 1.2 # 칸 값 그대로
|
||||
assert got["hidden"] == 2.0 # 숨은 관(세월교 · 박스) = 본체 높이 그대로
|
||||
|
||||
|
||||
def test_B08_집계_빈_높이_유입만_0_7(tmp_path: Path) -> None:
|
||||
combo = {"layer": "system", "owner": None, "key": "SS000011", "name": "횡단배수 배관"}
|
||||
root = tmp_path / "proj"
|
||||
edits = root / "B04_PreProcess/drainage/edits"
|
||||
edits.mkdir(parents=True)
|
||||
point = {
|
||||
"chainage_m": 1433.0,
|
||||
"source": "user",
|
||||
"options": {"pipe_diameter_mm": 800, **_revet("inlet"), **_revet("outlet")},
|
||||
"summary_item": combo,
|
||||
"group_items": {"gnbf9m9": combo, "g9lq9uk": combo},
|
||||
}
|
||||
(edits / "pipe_points.json").write_text(
|
||||
json.dumps({"points": [point]}, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
designs = [
|
||||
{
|
||||
"chainage_m": 1433.0,
|
||||
"design": {"pipe_length_m": 18, "inlet_basin": None, "extra_walls": []},
|
||||
}
|
||||
]
|
||||
names = definition.read()["종류"]["pipe"]["이름"]
|
||||
linked = {owner: {} for owner in snapshots.assignments(root)}
|
||||
out = summary.pipe_summary(root, designs, names, 6.0, linked=linked)
|
||||
ids = [c["id"] for c in out["columns"] if c["id"].startswith("pipe:revet")]
|
||||
assert ids == ["pipe:revet|돌쌓기(메)|1.3", "pipe:revet|돌쌓기(메)|1.5"], (
|
||||
ids
|
||||
) # 유출 1.3 · 유입 1.5
|
||||
@@ -34,8 +34,8 @@ def test_새_관만_기본값_빈_키에만():
|
||||
fresh = PipePoint(chainage_m=10.0, source="stream", options={"pipe_diameter_mm": 800})
|
||||
basins._apply_new_defaults([fresh])
|
||||
# 추천(먼저 들어간 값)이 이김 · 집수정 모양 L 도 실제 값(54-39)
|
||||
# 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1 · Φ800 → 1.3)
|
||||
heights = dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.3)
|
||||
# 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1 · Φ800 → 1.3) · 유입 흙막이만 + 0.7(211-1 → 1.5)
|
||||
heights = {**dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.3), "inlet_revet_height_m": 1.5}
|
||||
expected = {**_defaults("pipe"), "pipe_diameter_mm": 800, "inlet_structure": "L", **heights}
|
||||
assert fresh.options == expected
|
||||
assert fresh.options["pipe_kind"] == "파형강관" and fresh.options["inlet_type"] == "집수정"
|
||||
@@ -66,7 +66,8 @@ def test_원본에_있던_관은_빈칸_그대로(monkeypatch):
|
||||
by = {p.chainage_m: p for p in points}
|
||||
assert by[100.0].options is None # 옛 빈칸 그대로
|
||||
# 새로 보충된 관 = 기본값 + 집수정 모양 L(54-39)
|
||||
heights = dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.5) # 기본 관경 1000 → 1.5(139-1)
|
||||
# 기본 관경 1000 → 1.5(139-1) · 유입 흙막이 1.7(211-1)
|
||||
heights = {**dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.5), "inlet_revet_height_m": 1.7}
|
||||
assert by[300.0].options == {**_defaults("pipe"), "inlet_structure": "L", **heights}
|
||||
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ const camel = {
|
||||
two_stage_slope: "twoStageSlope",
|
||||
cut_slope_ratio: "cutSlopeRatio",
|
||||
ditch_enabled: "ditchEnabled",
|
||||
ditch_choice: "ditchChoice",
|
||||
surface_drop_m: "surfaceDropM",
|
||||
plan_radius_m: "planRadiusM",
|
||||
curve_outer_side: "curveOuterSide",
|
||||
|
||||
@@ -26,7 +26,7 @@ def _ts_const(source: str, name: str) -> float:
|
||||
|
||||
|
||||
def test_상수_셋이_TS_와_같다() -> None:
|
||||
for name in ("REVET_HEIGHT_STEP_M", "REVET_EMBED_DEPTH_M"):
|
||||
for name in ("REVET_HEIGHT_STEP_M", "REVET_EMBED_DEPTH_M", "INLET_REVET_ADD_M"):
|
||||
assert getattr(culvert, name) == _ts_const(TS, name) == _ts_const(SETS, name), name
|
||||
assert culvert.OLD_PIPE_WALL_DEFAULT_M == _ts_const(SETS, "OLD_PIPE_WALL_DEFAULT_M")
|
||||
|
||||
@@ -35,9 +35,16 @@ def test_TS_식_모양이_그대로다() -> None:
|
||||
"""식을 바꾸면 여기서 잡힘 — 파이썬 식도 함께 고칠 것."""
|
||||
assert "const raw = diameterM;" in TS and "const raw = diameterM;" in SETS
|
||||
assert "Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9) * REVET_HEIGHT_STEP_M" in TS
|
||||
assert "return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M;" in TS
|
||||
# 그림이 칸 값을 읽음(139-1) — 배관 기본 = 칸 → 관경 기준
|
||||
assert "(spec.revet_height_m ?? pipeWallMinPureHeight(diameterM))" in TS
|
||||
assert (
|
||||
'revetTargetHeight(diameterM) + (side === "inlet" ? INLET_REVET_ADD_M : REVET_EMBED_DEPTH_M)'
|
||||
in (
|
||||
" ".join(TS.split()) # 줄바꿈 · 들여쓰기 무시(포맷터가 접음)
|
||||
)
|
||||
.replace("( ", "(")
|
||||
.replace(" )", ")")
|
||||
)
|
||||
# 그림이 칸 값을 읽음(139-1) — 배관 기본 = 칸 → 관경 기준(유입 + 0.7 · 211-1)
|
||||
assert "(spec.revet_height_m ?? pipeWallMinPureHeight(diameterM, spec.role))" in TS
|
||||
# 옛 1.5 옮김 — TS 세트 짝 식
|
||||
assert "Math.abs(stored - OLD_PIPE_WALL_DEFAULT_M) < 1e-9 &&" in SETS
|
||||
assert "diameterM > 1.0 + 1e-9" in SETS
|
||||
|
||||
@@ -18,12 +18,17 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const [root, file] = process.argv.slice(1);
|
||||
const ts = require(path.join(root, "config", "node_modules", "typescript"));
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, () => ({}));
|
||||
const m = box.exports;
|
||||
const load = (src, req) => {
|
||||
const js = ts.transpileModule(fs.readFileSync(src, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
return box.exports;
|
||||
};
|
||||
// 로직 찾기는 공용 맞춤(208-1) — 그것만 실제로 싣고 나머지 import 는 빈 것
|
||||
const find = load(path.join(root, "ui_template", "ui_template_logic_find.ts"), () => ({}));
|
||||
const m = load(file, (p) => (p.endsWith("ui_template_logic_find") ? find : {}));
|
||||
(async () => {
|
||||
const jobs = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const out = [];
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/* =============================================================================
|
||||
* ui_template_logic_find.ts
|
||||
* 단가산출 로직 찾기 맞춤(208-1) — 로직을 찾는 화면은 모두 이것 하나를 부름.
|
||||
* 낱말(띄어 나눔)마다 키 · 원문번호 · 이름 · 구분 · 상세구분 · 별칭(쉼표로 여럿) 가운데 하나에 들면 맞음 ·
|
||||
* 띄어쓰기 · 대소문자 무시. 별칭으로 맞았으면 그 별칭을 돌려줌(결과에 작게).
|
||||
* 짝: `M01_MasterData_Store_Find.hit`(서버 찾기 — 키 · 원문번호 · 이름 · 별칭 · 낱말 규칙이 같음 ·
|
||||
* 구분 · 상세구분은 화면만) · 거울 시험 `test_208_1_logic_alias.py`.
|
||||
* ========================================================================== */
|
||||
|
||||
export interface LogicFindRow {
|
||||
키?: string;
|
||||
원문번호?: string;
|
||||
이름?: string;
|
||||
구분?: string;
|
||||
상세구분?: string;
|
||||
/** 쉼표로 여럿 · 없으면 빈칸 */
|
||||
별칭?: string | null;
|
||||
}
|
||||
|
||||
export interface LogicFindHit {
|
||||
ok: boolean;
|
||||
/** 별칭으로 맞았으면 맞은 별칭(쉼표로 이음) · 아니면 빈 글 */
|
||||
alias: string;
|
||||
}
|
||||
|
||||
const squash = (v: unknown): string =>
|
||||
String(v ?? "")
|
||||
.replace(/\s+/g, "")
|
||||
.toLowerCase();
|
||||
|
||||
/** 별칭 칸 → 낱낱 별칭(쉼표로 나눔 · 빈 것 뺌) */
|
||||
export const aliasesOf = (v: unknown): string[] =>
|
||||
String(v ?? "")
|
||||
.split(",")
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
/** 고친 별칭 글을 저장 꼴로 — 「a ,b,, c」 → 「a, b, c」 */
|
||||
export const aliasText = (v: unknown): string => aliasesOf(v).join(", ");
|
||||
|
||||
/** 결과에 작게 붙이는 글 — 별칭으로 안 맞았으면 빈 글 */
|
||||
export const aliasNote = (alias: string): string => (alias ? `별칭 ${alias}` : "");
|
||||
|
||||
export function matchLogic(q: string, row: LogicFindRow): LogicFindHit {
|
||||
const words = q.split(/\s+/).map(squash).filter(Boolean);
|
||||
const fields = [row.키, row.원문번호, row.이름, row.구분, row.상세구분].map(squash);
|
||||
const missed = words.filter((w) => !fields.some((f) => f.includes(w)));
|
||||
if (!missed.length) return { ok: true, alias: "" };
|
||||
const aliases = aliasesOf(row.별칭);
|
||||
const has = (a: string, w: string): boolean => squash(a).includes(w);
|
||||
if (!missed.every((w) => aliases.some((a) => has(a, w)))) return { ok: false, alias: "" };
|
||||
return { ok: true, alias: aliases.filter((a) => missed.some((w) => has(a, w))).join(", ") };
|
||||
}
|
||||
Reference in New Issue
Block a user