Merge remote-tracking branch 'origin/dev' into sub_laptop_7
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* ⚠⚠ 짝: `B07_DesignDetail/B07_DesignDetail_Engine_Cad_PlanSlope.py`
|
||||
* (`route_frame` · `route_normal` · `side_is_cut` · `slope_end` · `slope_pieces` 와 그 아래
|
||||
* 단조 3차 보간 · 촘촘히 · 접힘 버림 · 353-1) — 한쪽만 고치면
|
||||
* 단조 3차 보간 · 촘촘히 · 접힘 버림 · 353-1 · 모서리 둥글림 · 364-3) — 한쪽만 고치면
|
||||
* 종단 · 횡단 편집 중 평면과 B07 계획평면도가 갈린다.
|
||||
* `footprintRecord` = `B07_DesignDetail_Router_Support_Plan.slope_records` 의 측점 한 줄.
|
||||
* 거울 시험: `resources/tester/test_346_2_plan_footprint_mirror.py`
|
||||
@@ -348,6 +348,91 @@ function turnDeg(a: number[], m: number[], b: number[]): number {
|
||||
);
|
||||
}
|
||||
|
||||
/** 비탈 끝 모서리 둥글게(364-3) — 접힘 버린 자리 · 잔 갈지자가 이보다 꺾이면 둥글림(°). 짝 같은 이름. */
|
||||
export const ROUND_TURN_DEG = 15.0;
|
||||
/** 둥글리는 앞뒤 길이(선 따라 m) — 측점 · 깎기↔쌓기 바뀜 점은 넘지 않음 · 그 틈이 ROUND_MIN_M 보다 좁으면 그대로. */
|
||||
export const ROUND_M = 3.0;
|
||||
export const ROUND_MIN_M = 0.5;
|
||||
/** 둥근 곡선 한 마디 꺾임(°) · 되풀이 상한. */
|
||||
export const ROUND_STEP_DEG = 5.0;
|
||||
export const ROUND_PASSES = 200;
|
||||
|
||||
const dist = (a: number[], b: number[]): number =>
|
||||
Math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2);
|
||||
|
||||
/** 가장 많이 꺾인 점부터 앞뒤 같은 길이를 2차 베지어로 바꿈 — 측점(pins) · 바뀜 점 · 양 끝은 그대로.
|
||||
* 돌려줌 = 새 점 · 점마다 깎기 여부 · 점마다 원래 번호(새로 만든 점 -1). 짝: `_round_corners`. */
|
||||
function roundCorners(
|
||||
points: Line,
|
||||
flags: boolean[],
|
||||
pins: boolean[],
|
||||
): [Line, boolean[], number[]] {
|
||||
const pts = points.map((p) => [...p]);
|
||||
const fl = [...flags];
|
||||
const src = points.map((_p, i) => i);
|
||||
const pinned = (i: number): boolean => src[i] >= 0 && pins[src[i]];
|
||||
const fixed = (i: number): boolean =>
|
||||
i === 0 || i === pts.length - 1 || fl[i] !== fl[i - 1] || pinned(i);
|
||||
/** i 에서 step 쪽으로 length(그 전에 붙박인 점이면 거기까지) — [남길 점 번호, 그 자리, 간 길이, 그 자리 선 방향(i 쪽)]. */
|
||||
const walk = (i: number, step: number, length: number): [number, number[], number, number[]] => {
|
||||
let gone = 0.0;
|
||||
let j = i;
|
||||
for (;;) {
|
||||
const k = j + step;
|
||||
const seg = dist(pts[j], pts[k]);
|
||||
const toward = [pts[j][0] - pts[k][0], pts[j][1] - pts[k][1]];
|
||||
if (gone + seg >= length) {
|
||||
const ratio = (length - gone) / seg;
|
||||
return [k, [pts[j][0] - toward[0] * ratio, pts[j][1] - toward[1] * ratio], length, toward];
|
||||
}
|
||||
// 측점은 반만 다가감 — 끝까지 가면 꺾임이 측점에 몰림
|
||||
if (fixed(k)) return [k, pts[k], pinned(k) ? (gone + seg) / 2.0 : gone + seg, toward];
|
||||
gone += seg;
|
||||
j = k;
|
||||
}
|
||||
};
|
||||
/** 두 끝 접선이 만나는 점(곡선이 앞뒤 선에 매끄럽게 붙음) — 못 쓰면 null. */
|
||||
const control = (a: number[], ta: number[], b: number[], tb: number[], reach: number) => {
|
||||
const den = ta[0] * tb[1] - ta[1] * tb[0];
|
||||
if (Math.abs(den) < 1e-12) return null;
|
||||
const s = ((b[0] - a[0]) * tb[1] - (b[1] - a[1]) * tb[0]) / den;
|
||||
const u = ((b[0] - a[0]) * ta[1] - (b[1] - a[1]) * ta[0]) / den;
|
||||
const c = [a[0] + ta[0] * s, a[1] + ta[1] * s];
|
||||
if (s <= 0 || u <= 0 || dist(a, c) > 2 * reach || dist(b, c) > 2 * reach) return null;
|
||||
return c;
|
||||
};
|
||||
for (let pass = 0; pass < ROUND_PASSES; pass += 1) {
|
||||
let worst = ROUND_TURN_DEG;
|
||||
let at = -1;
|
||||
let reach = 0.0;
|
||||
for (let i = 1; i < pts.length - 1; i += 1) {
|
||||
if (fixed(i)) continue;
|
||||
const turn = turnDeg(pts[i - 1], pts[i], pts[i + 1]);
|
||||
if (turn > worst) {
|
||||
const room = Math.min(walk(i, -1, ROUND_M)[2], walk(i, 1, ROUND_M)[2]);
|
||||
if (room >= ROUND_MIN_M) [worst, at, reach] = [turn, i, room];
|
||||
}
|
||||
}
|
||||
if (at < 0) break;
|
||||
const [before, a, , ta] = walk(at, -1, reach);
|
||||
const [after, b, , tb] = walk(at, 1, reach);
|
||||
const p = control(a, ta, b, tb, reach) ?? pts[at];
|
||||
const count = Math.max(2, Math.ceil(worst / ROUND_STEP_DEG));
|
||||
const curve: Line = [];
|
||||
for (let n = 0; n <= count; n += 1) {
|
||||
const t = n / count;
|
||||
const point = [0, 1].map((k) =>
|
||||
roundTo((1 - t) ** 2 * a[k] + 2 * t * (1 - t) * p[k] + t * t * b[k], 4),
|
||||
);
|
||||
if (dist(point, pts[before]) > 1e-6 && dist(point, pts[after]) > 1e-6) curve.push(point);
|
||||
}
|
||||
pts.splice(before + 1, after - before - 1, ...curve);
|
||||
fl.splice(before + 1, after - before - 1, ...curve.map(() => fl[at]));
|
||||
src.splice(before + 1, after - before - 1, ...curve.map(() => -1));
|
||||
}
|
||||
return [pts, fl, src];
|
||||
}
|
||||
|
||||
/** 곡선 · 폭이 가파르게 바뀌는 자리 — 가운데 점에서 많이 꺾이는 사이를 반씩 나눔. 짝: `_densify`. */
|
||||
function densify(
|
||||
chainages: number[],
|
||||
@@ -358,9 +443,17 @@ function densify(
|
||||
if (depth >= SPLIT_DEPTH) return [];
|
||||
const middle = (c0 + c1) / 2.0;
|
||||
const pm = place(middle, frameOf(middle));
|
||||
// 4분 점도 봄 — S 자(가운데가 현 위)는 가운데 점만으론 안 꺾여 보임(364-3)
|
||||
const q1 = place((c0 + middle) / 2.0, frameOf((c0 + middle) / 2.0));
|
||||
const q3 = place((middle + c1) / 2.0, frameOf((middle + c1) / 2.0));
|
||||
const triples = [
|
||||
...pm.map((m, k) => [p0[k], m, p1[k]]),
|
||||
...q1.map((q, k) => [p0[k], q, pm[k]]),
|
||||
...q3.map((q, k) => [pm[k], q, p1[k]]),
|
||||
];
|
||||
// 되돌아가는 꺾임(90° 넘음)은 접힘 — 버릴 점이라 나누지 않음
|
||||
const bends = pm.some((m, k) => {
|
||||
const turn = turnDeg(p0[k], m, p1[k]);
|
||||
const bends = triples.some(([a, m, b]) => {
|
||||
const turn = turnDeg(a, m, b);
|
||||
return SPLIT_TURN_DEG < turn && turn < 90.0;
|
||||
});
|
||||
if (!bends) return [];
|
||||
@@ -465,10 +558,18 @@ function sampleLines(
|
||||
left: kept.left_end_m.map((i) => cutAt(curves.left, chainages[i])),
|
||||
right: kept.right_end_m.map((i) => cutAt(curves.right, chainages[i])),
|
||||
};
|
||||
// 눈금은 도로 끝에서 비탈 끝으로 — 측점 · 눈금 간격 자리만 · 긴 것 · 짧은 것이 번갈아(납품
|
||||
// 도면 관례) · 접혀 버린 자리는 뺌.
|
||||
const ticks: Line[] = [];
|
||||
const keptSets = { left: new Set(kept.left_end_m), right: new Set(kept.right_end_m) };
|
||||
for (const side of SIDES) {
|
||||
const key = `${side}_end_m` as const;
|
||||
const pins = kept[key].map((i) => byChainage.has(roundTo(chainages[i], 3)));
|
||||
const [line, flags, src] = roundCorners(lines[key], cut[side], pins);
|
||||
lines[key] = line;
|
||||
cut[side] = flags;
|
||||
keptSets[side] = new Set(src.filter((j) => j >= 0).map((j) => kept[key][j]));
|
||||
}
|
||||
// 눈금은 도로 끝에서 비탈 끝으로 — 측점 · 눈금 간격 자리만 · 긴 것 · 짧은 것이 번갈아(납품
|
||||
// 도면 관례) · 접혀 버리거나 둥글린 자리는 뺌.
|
||||
const ticks: Line[] = [];
|
||||
const position = new Map(chainages.map((chainage, i) => [chainage, i]));
|
||||
[...base]
|
||||
.sort((a, b) => a - b)
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
`_Engine_Cad_PlanKinds.py` 가 700줄을 넘어 떼어냄(141-1) — 파일 입출력 없음 · 좌표 = 사업지 m.
|
||||
|
||||
⚠⚠ 짝: `B05_Profile/logic/B05_Profile_PlanFootprint.ts`(route_frame · route_normal · side_is_cut ·
|
||||
slope_end · slope_pieces 와 그 아래 보간 · 촘촘히 · 접힘 버림) — 종단 · 횡단 편집 중 평면
|
||||
발자국 · 한쪽만 고치면 화면과 계획평면도가 갈림.
|
||||
slope_end · slope_pieces 와 그 아래 보간 · 촘촘히 · 접힘 버림 · 모서리 둥글림) — 종단 · 횡단
|
||||
편집 중 평면 발자국 · 한쪽만 고치면 화면과 계획평면도가 갈림.
|
||||
거울 시험: `resources/tester/test_346_2_plan_footprint_mirror.py`
|
||||
"""
|
||||
|
||||
@@ -226,6 +226,100 @@ def _turn_deg(a: list[float], m: list[float], b: list[float]) -> float:
|
||||
return abs(math.degrees(math.atan2(u[0] * v[1] - u[1] * v[0], u[0] * v[0] + u[1] * v[1])))
|
||||
|
||||
|
||||
#: 비탈 끝 모서리 둥글게(364-3) — 접힘 버린 자리 · 잔 갈지자가 이보다 꺾이면 둥글림(°)
|
||||
ROUND_TURN_DEG = 15.0
|
||||
#: 둥글리는 앞뒤 길이(선 따라 m) — 측점 · 깎기↔쌓기 바뀜 점은 넘지 않음 · 그 틈이 ROUND_MIN_M 보다
|
||||
#: 좁으면 그대로
|
||||
ROUND_M = 3.0
|
||||
ROUND_MIN_M = 0.5
|
||||
#: 둥근 곡선 한 마디 꺾임(°) · 되풀이 상한
|
||||
ROUND_STEP_DEG = 5.0
|
||||
ROUND_PASSES = 200
|
||||
|
||||
|
||||
def _dist(a: list[float], b: list[float]) -> float:
|
||||
return math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2)
|
||||
|
||||
|
||||
def _round_corners(
|
||||
points: list[list[float]], flags: list[bool], pins: list[bool]
|
||||
) -> tuple[list[list[float]], list[bool], list[int]]:
|
||||
"""가장 많이 꺾인 점부터 앞뒤 같은 길이를 2차 베지어로 바꿈.
|
||||
|
||||
측점(pins) · 깎기↔쌓기 바뀜 점 · 양 끝은 그대로 둔다.
|
||||
|
||||
돌려줌 = 새 점 · 점마다 깎기 여부 · 점마다 원래 번호(새로 만든 점 -1).
|
||||
"""
|
||||
pts, fl, src = [list(p) for p in points], list(flags), list(range(len(points)))
|
||||
|
||||
def pinned(i: int) -> bool:
|
||||
return src[i] >= 0 and pins[src[i]]
|
||||
|
||||
def fixed(i: int) -> bool:
|
||||
return i == 0 or i == len(pts) - 1 or fl[i] != fl[i - 1] or pinned(i)
|
||||
|
||||
def walk(i: int, step: int, length: float) -> tuple[int, list[float], float, list[float]]:
|
||||
"""i 에서 step 쪽으로 length(그 전에 붙박인 점이면 거기까지).
|
||||
|
||||
돌려줌 = (남길 점 번호, 그 자리, 간 길이, 그 자리 선 방향 — i 쪽을 향함).
|
||||
"""
|
||||
gone, j = 0.0, i
|
||||
while True:
|
||||
k = j + step
|
||||
seg = _dist(pts[j], pts[k])
|
||||
toward = [pts[j][0] - pts[k][0], pts[j][1] - pts[k][1]]
|
||||
if gone + seg >= length:
|
||||
ratio = (length - gone) / seg
|
||||
at = [pts[j][0] - toward[0] * ratio, pts[j][1] - toward[1] * ratio]
|
||||
return k, at, length, toward
|
||||
if fixed(k):
|
||||
# 측점은 반만 다가감 — 끝까지 가면 꺾임이 측점에 몰림
|
||||
return k, pts[k], (gone + seg) / 2.0 if pinned(k) else gone + seg, toward
|
||||
gone, j = gone + seg, k
|
||||
|
||||
def control(a: list[float], ta: list[float], b: list[float], tb: list[float], reach: float):
|
||||
"""두 끝 접선이 만나는 점(곡선이 앞뒤 선에 매끄럽게 붙음) — 못 쓰면 None."""
|
||||
den = ta[0] * tb[1] - ta[1] * tb[0]
|
||||
if abs(den) < 1e-12:
|
||||
return None
|
||||
s = ((b[0] - a[0]) * tb[1] - (b[1] - a[1]) * tb[0]) / den
|
||||
u = ((b[0] - a[0]) * ta[1] - (b[1] - a[1]) * ta[0]) / den
|
||||
c = [a[0] + ta[0] * s, a[1] + ta[1] * s]
|
||||
if s <= 0 or u <= 0 or _dist(a, c) > 2 * reach or _dist(b, c) > 2 * reach:
|
||||
return None
|
||||
return c
|
||||
|
||||
for _ in range(ROUND_PASSES):
|
||||
worst, at, reach = ROUND_TURN_DEG, -1, 0.0
|
||||
for i in range(1, len(pts) - 1):
|
||||
if fixed(i):
|
||||
continue
|
||||
turn = _turn_deg(pts[i - 1], pts[i], pts[i + 1])
|
||||
if turn > worst:
|
||||
room = min(walk(i, -1, ROUND_M)[2], walk(i, 1, ROUND_M)[2])
|
||||
if room >= ROUND_MIN_M:
|
||||
worst, at, reach = turn, i, room
|
||||
if at < 0:
|
||||
break
|
||||
before, a, _gone, ta = walk(at, -1, reach)
|
||||
after, b, _gone, tb = walk(at, 1, reach)
|
||||
p = control(a, ta, b, tb, reach) or pts[at]
|
||||
count = max(2, math.ceil(worst / ROUND_STEP_DEG))
|
||||
curve = []
|
||||
for n in range(count + 1):
|
||||
t = n / count
|
||||
point = [
|
||||
round((1 - t) ** 2 * a[k] + 2 * t * (1 - t) * p[k] + t * t * b[k], 4)
|
||||
for k in (0, 1)
|
||||
]
|
||||
if _dist(point, pts[before]) > 1e-6 and _dist(point, pts[after]) > 1e-6:
|
||||
curve.append(point)
|
||||
pts[before + 1 : after] = curve
|
||||
fl[before + 1 : after] = [fl[at]] * len(curve)
|
||||
src[before + 1 : after] = [-1] * len(curve)
|
||||
return pts, fl, src
|
||||
|
||||
|
||||
def _densify(
|
||||
chainages: list[float], frame_of: Any, place: Any
|
||||
) -> tuple[list[float], list[tuple[float, float, float, float]]]:
|
||||
@@ -236,8 +330,12 @@ def _densify(
|
||||
return []
|
||||
middle = (c0 + c1) / 2.0
|
||||
pm = place(middle, frame_of(middle))
|
||||
# 4분 점도 봄 — S 자(가운데가 현 위)는 가운데 점만으론 안 꺾여 보임(364-3)
|
||||
q1 = place((c0 + middle) / 2.0, frame_of((c0 + middle) / 2.0))
|
||||
q3 = place((middle + c1) / 2.0, frame_of((middle + c1) / 2.0))
|
||||
triples = [*zip(p0, pm, p1), *zip(p0, q1, pm), *zip(pm, q3, p1)]
|
||||
# 되돌아가는 꺾임(90° 넘음)은 접힘 — 버릴 점이라 나누지 않음
|
||||
if not any(SPLIT_TURN_DEG < _turn_deg(a, m, b) < 90.0 for a, m, b in zip(p0, pm, p1)):
|
||||
if not any(SPLIT_TURN_DEG < _turn_deg(a, m, b) < 90.0 for a, m, b in triples):
|
||||
return []
|
||||
return (
|
||||
split(c0, p0, middle, pm, depth + 1) + [middle] + split(middle, pm, c1, p1, depth + 1)
|
||||
@@ -264,7 +362,8 @@ def slope_pieces(
|
||||
left_cut?, right_cut?, (x, y, nx, ny)?}] — 좌(+) · 우(-) 오프셋. 측점 자리는 횡단 파일의
|
||||
중심 · 좌축을 그대로 써서 측점에서 외곽선 폭이 횡단 설계 값과 같다. 측점 사이는 단조 3차
|
||||
보간(측점 값 그대로 지남) · 법선은 측점과 같은 ±1 m 식 · 곡선은 촘촘히 · 진행을 거스르는
|
||||
점은 버림 · 깎기↔쌓기 바뀜 자리에서 비탈 끝이 도로 끝으로 모인다(353-1).
|
||||
점은 버림 · 깎기↔쌓기 바뀜 자리에서 비탈 끝이 도로 끝으로 모인다(353-1). 버린 자리 · 잔
|
||||
갈지자에 남은 비탈 끝 모서리는 측점을 두고 둥글린다(364-3).
|
||||
비탈 끝 선은 그 쪽이 절토인지 성토인지로 나눠 두 조각에 싣는다.
|
||||
"""
|
||||
sorted_records = sorted(
|
||||
@@ -333,10 +432,15 @@ def slope_pieces(
|
||||
cut = {
|
||||
side: [_cut_at(curves[side], chainages[i]) for i in kept[f"{side}_end_m"]] for side in sides
|
||||
}
|
||||
# 눈금은 노견 끝에서 비탈 끝으로 — 측점 · 눈금 간격 자리만 · 긴 것 · 짧은 것이 번갈아
|
||||
# 선다(납품 도면 관례) · 접혀 버린 자리는 뺌.
|
||||
ticks: list[list[list[float]]] = []
|
||||
kept_sets = {key: set(kept[key]) for key in keys}
|
||||
for side in sides:
|
||||
key = f"{side}_end_m"
|
||||
pins = [round(chainages[i], 3) in by_chainage for i in kept[key]]
|
||||
lines[key], cut[side], src = _round_corners(lines[key], cut[side], pins)
|
||||
kept_sets[key] = {kept[key][j] for j in src if j >= 0}
|
||||
# 눈금은 노견 끝에서 비탈 끝으로 — 측점 · 눈금 간격 자리만 · 긴 것 · 짧은 것이 번갈아
|
||||
# 선다(납품 도면 관례) · 접혀 버리거나 둥글린 자리는 뺌.
|
||||
ticks: list[list[list[float]]] = []
|
||||
position = {chainage: i for i, chainage in enumerate(chainages)}
|
||||
for index, chainage in enumerate(sorted(base)):
|
||||
i = position[chainage]
|
||||
|
||||
@@ -92,6 +92,8 @@ def _pair(pair: dict, line: dict, ctx: Ctx) -> dict:
|
||||
{"호표": pair.get("호표")},
|
||||
)
|
||||
conv = {"배": pair.get("배"), "갈래": "별칭"}
|
||||
if "자재" in head: # 세 칸(361-2) = 만들기에 적힐 값(`Material.marks`)
|
||||
head["세칸"] = hmat.marks(line, one["이름"])
|
||||
return {**head, "이름": one["이름"], "단위": one["단위"], "입력값": vals, "못읽음": []} | {
|
||||
"출처": {k: "별칭" for k in vals},
|
||||
"환산": conv,
|
||||
@@ -225,9 +227,10 @@ def _material(line: dict, ctx: Ctx, full: bool) -> dict:
|
||||
|
||||
|
||||
def _mat_target(one: dict, line: dict, times: Any = None) -> dict:
|
||||
"""자재 · 「전국」 자리표 → 대상(배 = 환산 · `times` 주면 그것)."""
|
||||
"""자재 · 「전국」 자리표 → 대상(배 = 환산 · `times` 주면 그것 · 세칸 = 만들기에 적힐 세 칸)."""
|
||||
conv = hm.factor(line.get("단위"), one["단위"], line.get("규격"))
|
||||
target = {**hm._public(one), "입력값": {}, "못읽음": [], "출처": {}, "환산": conv}
|
||||
target["세칸"] = hmat.marks(line, one["이름"])
|
||||
ok = isinstance(times, (int, float)) and not isinstance(times, bool) and times > 0
|
||||
return {**target, "몫": None, "배": times if ok else conv["배"]}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { API_BASE_URL } from "@config/config_frontend";
|
||||
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import { call, getActiveLayer, getActiveOwner, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import type { MatMarks } from "./M02_MasterTemplete_Labor_Api";
|
||||
|
||||
/** 층 · 주인(null = 자기 층) */
|
||||
export interface HopyoScope {
|
||||
@@ -119,6 +120,8 @@ export interface HopyoPart extends HopyoTarget {
|
||||
배: number | null;
|
||||
/** 블록 절 제목(조합 덧 로직) */
|
||||
어디?: string;
|
||||
/** 자재 세 칸(361-2) — 만들기에 적힐 값(서버 `Material.marks`) */
|
||||
세칸?: MatMarks;
|
||||
}
|
||||
|
||||
/** 품 묶음 — 직종 품 · 장비 시간 · 자재 수량 */
|
||||
|
||||
@@ -266,7 +266,7 @@ function matRow(line: HopyoLine, ctx: RowsContext): HTMLElement {
|
||||
}),
|
||||
qtyCell(line, parts),
|
||||
el("td", { className: "m02-labrow__unit", text: p?.단위 ?? line.단위 }),
|
||||
...markCells({ 별도계상: line.갈래 === "별도계상" ? "별도계상" : "직접계산" }),
|
||||
...markCells(p?.세칸 ?? { 별도계상: line.갈래 === "별도계상" ? "별도계상" : "직접계산" }),
|
||||
...PRICE_SLOTS.map((slot) =>
|
||||
el("td", {
|
||||
className: `m02-labrow__num m02-labrow__price${first?.기준칸 === slot ? " is-base-price" : ""}`,
|
||||
|
||||
@@ -146,6 +146,13 @@ export const MAT_MARKS = {
|
||||
} as const;
|
||||
export type MatMarkKey = keyof typeof MAT_MARKS;
|
||||
export type MatMarks = { [K in MatMarkKey]?: (typeof MAT_MARKS)[K][number] };
|
||||
/** 「전국」 자재(361-1 · `본뜬.전국자재` · 키 · 단가 없음 · 보기만 · `Labor_National`) */
|
||||
export type NationalMat = MatMarks & {
|
||||
이름: string;
|
||||
규격: string;
|
||||
단위: string;
|
||||
수량: { 손: number };
|
||||
};
|
||||
/** 읽을 때 기본값(162-1 · 서버 `MARK_DEFAULT` 와 같음) — 저장은 비움 */
|
||||
export const MAT_MARK_DEFAULT: Required<MatMarks> = {
|
||||
자재구분: "사급",
|
||||
@@ -182,7 +189,7 @@ export interface LaborDoc {
|
||||
열: string;
|
||||
일위대가번?: string;
|
||||
단위: string | null;
|
||||
본뜬?: { 층: string; 주인: string | null; 키: string } | null;
|
||||
본뜬?: { 층: string; 주인: string | null; 키: string; 전국자재?: NationalMat[] } | null;
|
||||
담은로직: LaborRow[];
|
||||
자재: LaborMat[];
|
||||
계산값?: unknown;
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
type Layer,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { linkAreaCell } from "./M02_MasterTemplete_AreaCellPick";
|
||||
import { nationalRows } from "./M02_MasterTemplete_Labor_National";
|
||||
import { lineId, settle, type LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import {
|
||||
areaLinks,
|
||||
@@ -241,6 +242,7 @@ export function mountLaborDetail(
|
||||
},
|
||||
edit ? { open: openMat, remove: removeMat, mark: markMat } : { open: openMat },
|
||||
),
|
||||
...nationalRows(cur.본뜬), // 361-2 — 전국 자재(보기만)
|
||||
);
|
||||
drawDg();
|
||||
showSource();
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Labor_National.ts
|
||||
* 「전국」 자재 줄(PLAN 361-2) — 실무자료 만들기가 `본뜬.전국자재` 에 적은 자재(키 · 단가 없음 · 361-1)를
|
||||
* 상세 자재목록 아래 보기 전용 표로 · 이름 · 규격 · 수량 · 단위 · 세 칸(글만) · 금액 칸 비움 · 단추 없음.
|
||||
* 없으면 아무것도 안 그림 · 셈 · 문서 꼴 · 저장 길은 그대로(보기만).
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import type { LaborDoc } from "./M02_MasterTemplete_Labor_Api";
|
||||
import { markCells, markHeads } from "./M02_MasterTemplete_Labor_MatMarks";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
|
||||
const TEXT = {
|
||||
Title: [
|
||||
"전국 자재 — 지역은 프로젝트에서 · 금액 없음(보기만)",
|
||||
"Nationwide materials — region set in the project · no price (view only)",
|
||||
],
|
||||
Code: ["전국", "Nationwide"],
|
||||
Region: ["지역은 프로젝트에서", "Region set in the project"],
|
||||
} as const;
|
||||
const say = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
|
||||
|
||||
/** `본뜬.전국자재` 표 — 없으면 [] */
|
||||
export function nationalRows(origin: LaborDoc["본뜬"]): HTMLElement[] {
|
||||
const mats = origin?.전국자재 ?? [];
|
||||
if (!mats.length) return [];
|
||||
const heads = [
|
||||
tc("Labor_Code"),
|
||||
tc("Col_Name"),
|
||||
tc("Labor_Spec"),
|
||||
tc("Labor_QtyCol"),
|
||||
tc("Labor_Unit"),
|
||||
...markHeads(),
|
||||
tc("Labor_AppliedSum"),
|
||||
];
|
||||
const rows = mats.map((m) =>
|
||||
el("tr", {
|
||||
className: "m02-labrow__row",
|
||||
attrs: { "data-labor-national": m.이름 },
|
||||
children: [
|
||||
el("td", { className: "m02-labrow__code", text: say("Code") }),
|
||||
el("td", {
|
||||
className: "m02-labrow__name",
|
||||
children: [
|
||||
el("strong", { text: m.이름 }),
|
||||
el("small", { className: "m02-labrow__note", text: say("Region") }),
|
||||
],
|
||||
}),
|
||||
el("td", { className: "m02-labrow__spec", text: m.규격, attrs: { title: m.규격 } }),
|
||||
el("td", { className: "m02-labrow__num", text: String(m.수량?.손 ?? "") }),
|
||||
el("td", { className: "m02-labrow__unit", text: m.단위 }),
|
||||
...markCells(m),
|
||||
el("td", { className: "m02-labrow__num", text: "—" }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
return [
|
||||
el("p", { className: "m02-labrow__muted", text: say("Title") }),
|
||||
el("div", {
|
||||
className: "ui-general-block__table-wrap",
|
||||
attrs: { "data-labor-national-table": "" },
|
||||
children: [
|
||||
el("table", {
|
||||
className: "ui-general-block__table m02-labrow",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })],
|
||||
}),
|
||||
el("tbody", { children: rows }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,57 @@
|
||||
/* M02 「전국」 자재 줄(361-2) 헬퍼 — `Labor_National.nationalRows` 를 트랜스파일해 Node 로 돌림 ·
|
||||
* `el` 대역 = 나무 꼴 {tag, className, text, attrs, children} · stdin = {origin} · stdout = 나무 JSON. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const el = (tag, o = {}) => ({ tag, ...o, children: o.children ?? [] });
|
||||
const STUBS = {
|
||||
"@ui/ui_template_elements": { el, createSelectField: () => ({ root: el("select") }) },
|
||||
"@ui/ui_template_locale": { currentLanguageIndex: 0 },
|
||||
"@config/config_frontend": { API_BASE_URL: "/api" },
|
||||
};
|
||||
const cache = new Map();
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (request.startsWith(".") && !request.endsWith(".css"))
|
||||
return loadTs(`${path.resolve(path.dirname(full), request)}.ts`);
|
||||
return new Proxy({}, { get: () => () => ({}) });
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const national = loadTs(
|
||||
path.join(ROOT, "M02_MasterTemplete", "M02_MasterTemplete_Labor_National.ts"),
|
||||
);
|
||||
const text = (n) => (n.text ?? "") + (n.children ?? []).map(text).join(" ");
|
||||
const find = (n, tag) => [
|
||||
...(n.tag === tag ? [n] : []),
|
||||
...(n.children ?? []).flatMap((c) => find(c, tag)),
|
||||
];
|
||||
const out = input.origins.map((origin) => {
|
||||
const got = national.nationalRows(origin);
|
||||
const table = got.flatMap((n) => find(n, "table"))[0];
|
||||
return {
|
||||
count: got.length,
|
||||
title: got[0]?.text ?? null,
|
||||
heads: table ? find(table, "th").map((h) => h.text) : [],
|
||||
rows: table
|
||||
? find(table, "tr")
|
||||
.slice(1)
|
||||
.map((r) => r.children.map((c) => text(c).trim()))
|
||||
: [],
|
||||
buttons: got.flatMap((n) => [...find(n, "button"), ...find(n, "select")]).length,
|
||||
};
|
||||
});
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,131 @@
|
||||
"""364-3 절/성토선 비탈 끝 모서리 둥글림 — 요구 239 브레인 검증 세션 자료로 전후 꺾임.
|
||||
|
||||
자료 `fixtures/b05_364_3_footprint_req239.json` = 세션 측점 줄 70 + 노선. 고치기 전 세션 선:
|
||||
비탈 끝 30° 넘는 꺾임 왼 14 · 오른 3 · 최대 140.7°(접힘 버린 자리 · S 자 촘촘히 빠짐 · 잔 갈지자).
|
||||
TS(`B05_Profile_PlanFootprint.ts`) 도 같은 자료로 같은 점인지 본다.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_364_3_slope_end_round.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope as plan # noqa: E402
|
||||
|
||||
DATA = json.loads(
|
||||
(ROOT / "resources/tester/fixtures/b05_364_3_footprint_req239.json").read_text(encoding="utf-8")
|
||||
)
|
||||
ROUTE = [tuple(point) for point in DATA["route"]]
|
||||
RECORDS = DATA["records"]
|
||||
END = ROUTE[-1][0]
|
||||
TS_FILE = ROOT / "B05_Profile" / "logic" / "B05_Profile_PlanFootprint.ts"
|
||||
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
|
||||
|
||||
def _turns(lines: list) -> list[float]:
|
||||
"""조각 안 꺾임(°) — 조각 끝(깎기↔쌓기 바뀜 · 양 끝)은 뺌."""
|
||||
return [
|
||||
plan._turn_deg(line[i - 1], line[i], line[i + 1])
|
||||
for line in lines
|
||||
for i in range(1, len(line) - 1)
|
||||
]
|
||||
|
||||
|
||||
def _pieces() -> tuple:
|
||||
return plan.slope_pieces(ROUTE, RECORDS, 0.0, END, 2.0)
|
||||
|
||||
|
||||
def test_비탈_끝_30도_넘는_꺾임() -> None:
|
||||
edge, fill, cut, _ticks = _pieces()
|
||||
fill_turns, cut_turns = _turns(fill["lines"]), _turns(cut["lines"])
|
||||
over = sum(t > 30 for t in fill_turns + cut_turns)
|
||||
# 남는 하나 = 측점 860 · 861 사이 1 m 에 비탈 폭 4.4 → 6.0 m(측점 자리라 그대로)
|
||||
assert over <= 1, over
|
||||
assert max(fill_turns + cut_turns) < 40.0
|
||||
assert sum(t > 15 for t in fill_turns + cut_turns) <= 25
|
||||
assert max(_turns(edge["lines"])) < 10.0 # 도로 끝은 손대지 않음
|
||||
|
||||
|
||||
def test_둥글림을_끄면_꺾임이_남음() -> None:
|
||||
"""기준이 바뀐 길을 재는지 — 둥글림만 끄면 30° 넘는 꺾임이 그대로 나옴."""
|
||||
keep = plan.ROUND_TURN_DEG
|
||||
plan.ROUND_TURN_DEG = 1e9
|
||||
try:
|
||||
_edge, fill, cut, _ticks = _pieces()
|
||||
finally:
|
||||
plan.ROUND_TURN_DEG = keep
|
||||
turns = _turns(fill["lines"]) + _turns(cut["lines"])
|
||||
assert sum(t > 30 for t in turns) >= 5 and max(turns) > 90.0
|
||||
|
||||
|
||||
def test_측점_비탈_끝은_그대로() -> None:
|
||||
"""남은 측점 비탈 끝 점은 둥글림 뒤에도 같은 자리 점으로 남음."""
|
||||
keep = plan.ROUND_TURN_DEG
|
||||
plan.ROUND_TURN_DEG = 1e9
|
||||
try:
|
||||
_e, fill0, cut0, _t = _pieces()
|
||||
finally:
|
||||
plan.ROUND_TURN_DEG = keep
|
||||
_e, fill, cut, _t = _pieces()
|
||||
before = {tuple(p) for line in fill0["lines"] + cut0["lines"] for p in line}
|
||||
after = {tuple(p) for line in fill["lines"] + cut["lines"] for p in line}
|
||||
for record in RECORDS:
|
||||
x, y, nx, ny = plan.route_normal(ROUTE, record["chainage_m"])
|
||||
for side in ("left", "right"):
|
||||
offset = record[f"{side}_end_m"]
|
||||
want = (round(x + nx * offset, 4), round(y + ny * offset, 4))
|
||||
if want in before:
|
||||
assert want in after, (record["chainage_m"], side)
|
||||
|
||||
|
||||
DRIVER = """
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { slopePieces } from "./B05_Profile_PlanFootprint.js";
|
||||
const c = JSON.parse(readFileSync(process.argv[2], "utf-8"));
|
||||
writeFileSync(process.argv[3], JSON.stringify(slopePieces(c.route, c.records, 0, c.end, 2)));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts_pieces(tmp_path_factory: pytest.TempPathFactory) -> dict:
|
||||
out = tmp_path_factory.mktemp("ts_round")
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(TSC), str(TS_FILE), "--outDir", str(out), "--module", "esnext",
|
||||
"--target", "es2022", "--moduleResolution", "bundler", "--ignoreConfig", "--noCheck",
|
||||
"--noResolve"],
|
||||
cwd=str(ROOT), check=True, capture_output=True,
|
||||
) # fmt: skip
|
||||
(out / "driver.mjs").write_text(DRIVER, encoding="utf-8")
|
||||
payload = {"route": ROUTE, "records": RECORDS, "end": END}
|
||||
(out / "in.json").write_text(json.dumps(payload), encoding="utf-8")
|
||||
run = subprocess.run( # noqa: S603
|
||||
["node", str(out / "driver.mjs"), str(out / "in.json"), str(out / "out.json")],
|
||||
capture_output=True, text=True, cwd=str(out),
|
||||
) # fmt: skip
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads((out / "out.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_TS_도_같은_점(ts_pieces) -> None:
|
||||
edge, fill, cut, ticks = _pieces()
|
||||
for name, want, got in (
|
||||
("도로 끝", edge["lines"], ts_pieces["edge"]),
|
||||
("쌓기", fill["lines"], ts_pieces["fill"]),
|
||||
("깎기", cut["lines"], ts_pieces["cut"]),
|
||||
("눈금", ticks["ticks"], ts_pieces["ticks"]),
|
||||
):
|
||||
assert len(want) == len(got), f"{name} 선 수"
|
||||
for line_w, line_g in zip(want, got):
|
||||
assert len(line_w) == len(line_g), f"{name} 점 수"
|
||||
for pw, pg in zip(line_w, line_g):
|
||||
assert abs(pw[0] - pg[0]) < 2e-4 and abs(pw[1] - pg[1]) < 2e-4, (name, pw, pg)
|
||||
@@ -96,6 +96,22 @@ def _close(page):
|
||||
page.click(".app-settings .ui-modal__top > button")
|
||||
|
||||
|
||||
def test_다른_곳_단축키_줄마다_동작_있음():
|
||||
"""364-2 — [다른 곳 단축키] 모든 줄 = 「입력 = 동작」 · ko · en 줄 수 같음."""
|
||||
import re
|
||||
|
||||
src = open(
|
||||
os.path.join(ROOT, "ui_template", "ui_template_locale_common.ts"), encoding="utf8"
|
||||
).read()
|
||||
blocks = re.findall(r'App_Settings_Keys_\w+_Keys: \[\s*"(.*?)",\s*"(.*?)",\s*\]', src, re.S)
|
||||
assert len(blocks) == 7
|
||||
for ko, en in blocks:
|
||||
ko_lines, en_lines = ko.split("\\n"), en.split("\\n")
|
||||
assert len(ko_lines) == len(en_lines), ko
|
||||
for line in ko_lines + en_lines:
|
||||
assert " = " in line and not line.rstrip().endswith(" ="), line
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _up(), reason="sub2 화면 서버(5177) 꺼짐")
|
||||
def test_설정_창_탭_저장_물음():
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
@@ -117,7 +117,7 @@ def test_새_상태_체크_제안(client):
|
||||
assert plywood["상태"] == "맞음" and set(plywood["갈리는칸"]) == {"측압", "폼타이규격"}
|
||||
mat = first["레미콘(25-21-120)"] # 358-2 · 361-1 — 「별도계상」 표시 · 지역 변형만 = 전국
|
||||
assert mat["상태"] == "맞음" and mat["체크"] is True and "별도계상" in mat["꼬리표"]
|
||||
assert mat["대상"][0] | {"환산": None} == {
|
||||
assert mat["대상"][0] | {"환산": None, "세칸": None} == {
|
||||
"전국": True,
|
||||
"이름": "레미콘(전국)-관급",
|
||||
"규격": "25-21-120",
|
||||
@@ -126,6 +126,7 @@ def test_새_상태_체크_제안(client):
|
||||
"못읽음": [],
|
||||
"출처": {},
|
||||
"환산": None,
|
||||
"세칸": None,
|
||||
"몫": None,
|
||||
"배": 1,
|
||||
} # 특정 지역 키 없음
|
||||
|
||||
@@ -357,7 +357,7 @@ def test_좌측_판_자리와_권한():
|
||||
|
||||
def test_로직_추가_한_곳과_파일_규칙():
|
||||
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
|
||||
assert len(detail.splitlines()) == 693 # 줄 수 그대로
|
||||
assert len(detail.splitlines()) == 695 # 줄 수 그대로(361-2 전국 자재 부르는 줄 둘)
|
||||
assert "withHopyo(library)," in detail
|
||||
pick = _text("M02_MasterTemplete_Labor_Pick.ts")
|
||||
assert "more?: () => Promise<PickExtra[]>;" in pick and "...later" in pick
|
||||
@@ -485,7 +485,8 @@ def test_확인_창_표_일위대가_상세_꼴_359():
|
||||
for word in ('"고르기"', '"원문 보기"', '"엑셀 보기"', '"옵션"', '"품 비교"'):
|
||||
assert word in rows, word
|
||||
assert 'import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows";' in rows
|
||||
assert "...markCells({ 별도계상:" in rows and "...markHeads()," in rows
|
||||
assert "...markCells(p?.세칸 ?? { 별도계상:" in rows # 361-2 서버 세 칸 먼저
|
||||
assert "...markHeads()," in rows
|
||||
for head in ("Col_Order", "Sub", "Col_Name", "Col_Alias", "Labor_QtyCol", "Col_Unit"):
|
||||
assert f'tc("{head}")' in rows, head
|
||||
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"""「전국」 자재 줄(PLAN 361-2 · `M02_MasterTemplete_Labor_National`) — 사전조합 `본뜬.전국자재` 를
|
||||
상세 자재목록 아래 보기 전용 표로(있으면 줄 · 없으면 안 보임 · 단추 없음 · 금액 칸 비움 ·
|
||||
글 명사형) · 읽기 길(사전조합 GET 의 `layers.read_template`)이 `본뜬.전국자재` 를 그대로 줌 ·
|
||||
확인 창 세 칸 = 만들기에 적힐 값(`세칸`).
|
||||
층은 임시 폴더.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# 올리기 길 시험의 `client` 마당을 같이 씀
|
||||
# ruff: noqa: F811
|
||||
from resources.tester.test_m02_hopyo_build import _build
|
||||
from resources.tester.test_m02_hopyo_excel import URL, _post, client # noqa: F401
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
M02 = Path(HERE).parents[1] / "M02_MasterTemplete"
|
||||
REMICON = {
|
||||
"이름": "레미콘(전국)-관급",
|
||||
"규격": "25-21-120",
|
||||
"단위": "㎥",
|
||||
"수량": {"손": 2.5},
|
||||
"별도계상": "별도계상",
|
||||
"자재구분": "관급",
|
||||
}
|
||||
|
||||
|
||||
def _rows(origins: list) -> list:
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_m02_labor_national.cjs")],
|
||||
input=json.dumps({"origins": origins}, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=False,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_전국_자재_줄_있는_것_없는_것():
|
||||
have, plain, empty, none = _rows(
|
||||
[
|
||||
{"층": "personal", "주인": "7/42", "키": "HP-000001 제1호표", "전국자재": [REMICON]},
|
||||
{"층": "personal", "주인": "7/42", "키": "HP-000001 제3호표"},
|
||||
{"층": "personal", "주인": None, "키": "k", "전국자재": []},
|
||||
None,
|
||||
]
|
||||
)
|
||||
assert have["title"] == "전국 자재 — 지역은 프로젝트에서 · 금액 없음(보기만)"
|
||||
assert have["heads"] == [
|
||||
"코드",
|
||||
"이름",
|
||||
"규격",
|
||||
"수량산출",
|
||||
"단위",
|
||||
"자재구분",
|
||||
"별도계상",
|
||||
"운반비",
|
||||
"반영 금액",
|
||||
]
|
||||
assert have["rows"] == [
|
||||
[
|
||||
"전국",
|
||||
"레미콘(전국)-관급 지역은 프로젝트에서",
|
||||
"25-21-120",
|
||||
"2.5",
|
||||
"㎥",
|
||||
"관급",
|
||||
"별도계상",
|
||||
"포함", # 빈 칸 = 기본값 글
|
||||
"—", # 금액 칸 비움
|
||||
]
|
||||
]
|
||||
assert have["buttons"] == 0 # 고침 · 삭제 없음(보기만)
|
||||
assert plain["count"] == empty["count"] == none["count"] == 0 # 없으면 안 보임
|
||||
|
||||
|
||||
def test_소스_보기만_명사형_자리():
|
||||
code = (M02 / "M02_MasterTemplete_Labor_National.ts").read_text(encoding="utf-8")
|
||||
words = re.findall(r'"([^"]*[가-힣][^"]*)"', code)
|
||||
assert words and not [w for w in words if re.search(r"(합니다|해요|한다|하세요)", w)]
|
||||
assert "createButton" not in code and "fetch" not in code and "sessionStorage" not in code
|
||||
detail = (M02 / "M02_MasterTemplete_Labor_Detail.ts").read_text(encoding="utf-8")
|
||||
assert "...nationalRows(cur.본뜬)" in detail and len(detail.splitlines()) <= 700
|
||||
api = (M02 / "M02_MasterTemplete_Labor_Api.ts").read_text(encoding="utf-8")
|
||||
assert "전국자재?: NationalMat[]" in api
|
||||
|
||||
|
||||
def test_만든_사전조합_읽기_길에_전국자재(client):
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
assert _post(client).status_code == 200
|
||||
got = _build(client, 1, [{"차례": 0, "대상": [{"전국": True, "배": 1}]}])
|
||||
key = got["참조"]["키"]
|
||||
found = layers.read_template(layers.personal_dir(7, 42), "precombo", key)
|
||||
doc = found.get("문서") or found
|
||||
assert doc["본뜬"]["전국자재"][0] | {"수량": None} == REMICON | {"수량": None}
|
||||
assert doc["자재"] == [] # 자재 칸(M01 키만)엔 없음 — 셈 · 문서 꼴 그대로
|
||||
|
||||
|
||||
def test_확인_창_세_칸_만들기_값(client):
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
assert _post(client).status_code == 200
|
||||
fit.run_job(layers.personal_dir(7, 42), "HP-000001")
|
||||
lines = client.get(f"{URL.format('personal')}/HP-000001/1").json()["호표"]["줄"]
|
||||
at = next(x["차례"] for x in lines if x["이름"] == "레미콘(25-21-120)")
|
||||
mat = lines[at]["맞춤"]["대상"][0]
|
||||
assert mat["세칸"] == {
|
||||
"별도계상": "별도계상",
|
||||
"자재구분": "관급",
|
||||
} # 전국-관급 = 관급(옛 「사급」)
|
||||
path = f"{URL.format('personal')}/HP-000001/1/guess"
|
||||
seoul = client.post(path, json={"차례": at, "대상": [{"자재": "MT004258", "배": 1}]}).json()
|
||||
assert seoul["대상"][0]["세칸"]["자재구분"] == "사급" # 고른 자재 이름(-사급) 먼저
|
||||
rows = (M02 / "M02_MasterTemplete_Hopyo_CheckRows.ts").read_text(encoding="utf-8")
|
||||
assert "...markCells(p?.세칸 ??" in rows
|
||||
@@ -150,8 +150,8 @@ export const ui_locales_common = {
|
||||
],
|
||||
App_Settings_Keys_Profile: ["B05 종단", "B05 profile"],
|
||||
App_Settings_Keys_Profile_Keys: [
|
||||
"Ctrl+Z = 되돌림\nCtrl+Shift+Z = 다시\n방향키 = 옮김\nEnter(자유지정) = 끝\nEsc · Backspace",
|
||||
"Ctrl+Z = undo\nCtrl+Shift+Z = redo\nArrows = move\nEnter (free mode) = finish\nEsc · Backspace",
|
||||
"Ctrl+Z = 되돌림\nCtrl+Shift+Z = 다시\n방향키 = 옮김\nEnter(자유지정) = 끝\nEsc(자유지정) = 취소\nBackspace(자유지정) = 마지막 점 빼기",
|
||||
"Ctrl+Z = undo\nCtrl+Shift+Z = redo\nArrows = move\nEnter (free mode) = finish\nEsc (free mode) = cancel\nBackspace (free mode) = remove last point",
|
||||
],
|
||||
App_Settings_Keys_RouteEdit: ["B05 노선 편집", "B05 route edit"],
|
||||
App_Settings_Keys_RouteEdit_Keys: [
|
||||
@@ -160,13 +160,13 @@ export const ui_locales_common = {
|
||||
],
|
||||
App_Settings_Keys_Section: ["B06 횡단", "B06 cross section"],
|
||||
App_Settings_Keys_Section_Keys: [
|
||||
"방향키 = 측점 옮김\n▲▼ = 측점 옮김\nEnter = 고름\nSpace = 고름\nEsc",
|
||||
"Arrows = move station\n▲▼ = move station\nEnter = pick\nSpace = pick\nEsc",
|
||||
"방향키 = 측점 옮김\n▲▼ = 측점 옮김\nEnter = 고름\nSpace = 고름\nEsc = 고름 풀기\nEsc(상세 창 고르기 중) = 고르기 취소",
|
||||
"Arrows = move station\n▲▼ = move station\nEnter = pick\nSpace = pick\nEsc = clear selection\nEsc (detail window, while picking) = cancel picking",
|
||||
],
|
||||
App_Settings_Keys_Cad: ["CAD", "CAD"],
|
||||
App_Settings_Keys_Cad_Keys: [
|
||||
"F3 = 스냅\nF7 = 그리드\nF8 = 직교\nF10 = 45°\nCtrl+Z = 되돌림\nCtrl+Y = 다시\nCtrl+S(M02) = 저장\nSpace = Enter\n↑↓ = 기록 · 최근 값\nShift+↑↓ = 화면 옮김\n글자 = 명령\nCtrl+A · Delete · Esc · Enter · Ctrl+C · X · 1 · ← →",
|
||||
"F3 = snap\nF7 = grid\nF8 = ortho\nF10 = 45°\nCtrl+Z = undo\nCtrl+Y = redo\nCtrl+S (M02) = save\nSpace = Enter\n↑↓ = history · recent values\nShift+↑↓ = pan\nLetters = command\nCtrl+A · Delete · Esc · Enter · Ctrl+C · X · 1 · ← →",
|
||||
"F3 = 스냅\nF7 = 그리드\nF8 = 직교\nF10 = 45°\nCtrl+Z = 되돌림\nCtrl+Y = 다시\nCtrl+S(M02) = 저장\nSpace = Enter\n↑↓ = 기록 · 최근 값\nShift+↑↓ = 화면 옮김\n글자 = 명령\nCtrl+A = 모두 고름\nDelete = 고른 것 지움\nEsc = 명령 취소 · 쉴 때 고름 풀기\nEnter = 입력 끝\nCtrl+C = 복사\nCtrl+X = 잘라 냄\nCtrl+V = 붙여넣기\nCtrl+1 = 특성\n← → = 화면 옮김",
|
||||
"F3 = snap\nF7 = grid\nF8 = ortho\nF10 = 45°\nCtrl+Z = undo\nCtrl+Y = redo\nCtrl+S (M02) = save\nSpace = Enter\n↑↓ = history · recent values\nShift+↑↓ = pan\nLetters = command\nCtrl+A = select all\nDelete = delete selection\nEsc = cancel command · clear selection when idle\nEnter = submit input\nCtrl+C = copy\nCtrl+X = cut\nCtrl+V = paste\nCtrl+1 = properties\n← → = pan",
|
||||
],
|
||||
App_Settings_Keys_Table: ["B08 · B09 표", "B08 · B09 tables"],
|
||||
App_Settings_Keys_Table_Keys: [
|
||||
@@ -175,8 +175,8 @@ export const ui_locales_common = {
|
||||
],
|
||||
App_Settings_Keys_M02: ["M02 양식", "M02 templates"],
|
||||
App_Settings_Keys_M02_Keys: [
|
||||
"Ctrl+S = 저장\nSpace+끌기 = 화면 옮김\nCtrl+휠 = 배율\n시트 안 = 엑셀 키 그대로\nCtrl+C · Ctrl+V · Delete · Esc",
|
||||
"Ctrl+S = save\nSpace+drag = pan\nCtrl+wheel = zoom\nInside a sheet = Excel keys\nCtrl+C · Ctrl+V · Delete · Esc",
|
||||
"Ctrl+S = 저장\nSpace+끌기 = 화면 옮김\nCtrl+휠 = 배율\n시트 안 = 엑셀 키 그대로\nCtrl+C(구조물 나무) = 복사\nCtrl+V(구조물 나무) = 붙여넣기\nDelete = 고른 항목 지움\nEsc = 연결 · 열린 창 취소",
|
||||
"Ctrl+S = save\nSpace+drag = pan\nCtrl+wheel = zoom\nInside a sheet = Excel keys\nCtrl+C (structure tree) = copy\nCtrl+V (structure tree) = paste\nDelete = delete selected item\nEsc = cancel link · close open window",
|
||||
],
|
||||
App_Footer_Copyright: [
|
||||
"© 2026 임도설계 및 견적 자동화. All rights reserved.",
|
||||
|
||||
Reference in New Issue
Block a user