feat(842-2): 기계 비활성 — M01 기계 목록 「사용」 스위치 · 끈 기계는 찾기 · 고르기 · 잇기에서 뺌 · 해상 기계 79 대 끔

- 표지 = 기계 줄 맨 위 칸 `비활성: true`(없음 = 켜짐 · 값 묶음 밖) · 틀 검사 = 기계 줄에만 true(check_master OFF)
- M01 기계 목록 = 끝 열 「사용」 스위치(초안 → 페이지 [저장] · 자동저장 없음) · 끈 줄 흐림 · 머리 「비활성 같이 보기」(끄면 서버 rows state 「켜짐」)
- 뺌 = 요소 찾기 창(elements · 찾아 넣기 · 단가산출 기계 창) · logic-lib rows · M01 시험 계산 기계 고르기 표 · 내 기계 「비슷한 기계에서 시작」 ·
  불러오기 기계 잇기(M02_Machine_Find 표) · 기계경비식 「기계」 옵션(새 GET /api/m02/df/machine-off · 고른 값은 남기고 「· 비활성 기계」 꼬리)
- 셈 길은 그대로 — 끈 기계를 쓰던 단가산출 · 사본 값 같음
- 자료 = 해상장비 79 대(펌프 준설선 13 · 그래브 준설선 10 · 하천골재채취선 7 · 기중기선 5 · 예선 9 · 양묘선 11 · 토운선 6 · 이우선 2 · 대선 16) 끔
- 시험 = test_842_2_machine_off 새로 · test_804_10 옵션 글 단언을 새 글로 고침(끈 기계 거름이 들어간 줄)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-15 18:29:05 +09:00
co-authored by Claude Opus 5.5
parent 1402d7eb4d
commit 6015525efd
96 changed files with 315 additions and 22 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ import { createInputField, createSelectField } from "@ui/ui_template_input";
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createText } from "@ui/ui_template_text";
import { formatLineMoney, formatMoney } from "../common_util/common_util_money";
import { nameMachines, optionText } from "./formula_doc_options";
import { liveOptions, nameMachines, optionText } from "./formula_doc_options";
import "./calc_sheet.css";
export interface SheetLine {
@@ -164,7 +164,7 @@ export function varRow(spec: SheetInput, value: string, onInput: (v: string) =>
let control: HTMLElement;
if (spec.고르기?.length) {
const list = [...(value ? [] : [""]), ...spec.고르기.map(String)];
const items = () => list.map((o) => ({ value: o, text: optionText(o) })); // 기계 번호 = 이름(요구 692-2)
const items = () => liveOptions(list, value).map((o) => ({ value: o, text: optionText(o) })); // 기계 번호 = 이름(요구 692-2) · 끈 기계 뺌(842-2)
const f = createSelectField({
options: items(),
value,
+17 -6
View File
@@ -9,21 +9,31 @@
import { createButton } from "@ui/ui_template_button";
import { createInputField, createSelectField } from "@ui/ui_template_input";
import { varId, type DocVar, type FormulaDoc } from "../common_util/common_util_formula_doc";
import { cfMachineNames } from "../M02_MasterTemplete/DataFlow/M02_DF_Api";
import { cfMachineNames, cfMachineOff } from "../M02_MasterTemplete/DataFlow/M02_DF_Api";
import { mt } from "../M02_MasterTemplete/M02_MyMachine_Text";
import { ft } from "./formula_doc_text";
const MACHINE = /^\d{4}-\d{4}$/;
let names: Record<string, string> = {};
let off = new Set<string>(); // 842-2 끈 기계 원문번호
const machineInfo = (): Promise<Record<string, string>> =>
Promise.all([cfMachineNames(), cfMachineOff()]).then(
([got, gone]) => ((off = new Set(gone)), got),
);
/** 고르기 글 — 기계 원문번호는 이름 · 규격을 앞에 */
/** 고르기 글 — 기계 원문번호는 이름 · 규격을 앞에 · 끈 기계는 「· 비활성 기계」(842-2) */
export const optionText = (x: string, known: Record<string, string> = names): string =>
known[x] ? `${known[x]} (${x})` : x;
known[x] ? `${known[x]} (${x})${off.has(x) ? ` · ${ft("Machine_Off")}` : ""}` : x;
/** 고르기 목록 — 끈 기계 뺌(지금 고른 값은 남김 · 842-2) */
export const liveOptions = (list: string[], now: string): string[] =>
list.filter((x) => x === now || !off.has(x));
/** 목록이 모두 원문번호(빈 칸 빼고)면 이름을 받아 `redraw`(글만 다시) — 수식 탭 옵션 · 단가산출 상세 옵션 */
export function nameMachines(list: string[], redraw: () => void): void {
const codes = list.filter(Boolean);
if (codes.length && codes.every((x) => MACHINE.test(x)) && codes.some((x) => !(x in names)))
void cfMachineNames().then((got) => {
void machineInfo().then((got) => {
names = got;
redraw();
});
@@ -35,7 +45,7 @@ export function machineText(x: unknown, redraw: () => void): string | null {
if (typeof x !== "string" || !MACHINE.test(x)) return null;
if (!(x in names) && !asked) {
asked = true;
void cfMachineNames().then((got) => {
void machineInfo().then((got) => {
names = { ...names, ...got };
redraw();
});
@@ -59,7 +69,8 @@ export function buildOptions(
const now = k in given ? given[k] : v.연결?.값;
if (kind === "선택") {
const list = (v.연결?.목록 ?? []).map(String);
const items = () => list.map((x) => ({ value: x, text: optionText(x) }));
const items = () =>
liveOptions(list, String(now ?? "")).map((x) => ({ value: x, text: optionText(x) }));
const f = createSelectField({
label,
options: items(),
+1
View File
@@ -49,6 +49,7 @@ const TEXT = {
By_Line: ["식 줄로 정의", "Defined by a line"],
Pick_First: ["찾아 고름", "Search and pick"],
Material: ["재료 고르기", "Material pick"],
Machine_Off: ["비활성 기계", "Inactive machine"],
None: ["없음", "None"],
Open_Sub: ["식 펼침", "Show equation"],
Bad_Sign: ["기호 꼴 아님 · 같은 기호 있음", "Bad or duplicate symbol"],
+1 -1
View File
@@ -236,7 +236,7 @@ def find(q: str, limit: int = 30) -> list[dict]:
for r in _rows():
hay = " ".join(str(r.get(k) or "") for k in ("이름", "규격", "별칭", "상세구분", "키"))
hay = hay.lower()
if all(w in hay for w in words):
if all(w in hay for w in words) and not r.get("비활성"): # 끈 기계 뺌(842-2)
out.append(_brief(r))
if len(out) >= limit:
break
+5 -3
View File
@@ -241,8 +241,10 @@ def rows(
hits = [r for r in hits if sub in {r.get(s) for s in cm.MERGED.values()}]
if detail: # 인력 상세구분 · 기계 상세구분(기종)
hits = [r for r in hits if r.get("상세구분") == detail]
if state: # 인력 상태
hits = [r for r in hits if r.get("상태") == state]
if state: # 인력 상태 · 기계 「켜짐」 = 비활성 뺌(842-2)
hits = [
r for r in hits if (not r.get(cm.OFF) if state == "켜짐" else r.get("상태") == state)
]
if unlinked: # 품셈재료 — 아직 못 이은 줄
hits = [r for r in hits if not r.get("연결")]
page, size = max(page, 1), min(max(size, 1), 500)
@@ -463,7 +465,7 @@ def elements(group: str, q: str, limit: int) -> dict:
if data.get("그룹") != group:
continue
for row in data.get(items_key(data)) or []:
if _hit(row, q):
if _hit(row, q) and not row.get(cm.OFF): # 끈 기계 뺌(842-2)
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)]}
@@ -70,7 +70,8 @@ export function openPickTable(opt: PickTableOptions): void {
const mine = ++seq;
try {
const q = search.value.trim();
const got = await fetchRows(FILES[opt.kind], page, size, q);
const on = opt.kind === "기계" ? "켜짐" : ""; // 끈 기계 뺌(842-2)
const got = await fetchRows(FILES[opt.kind], page, size, q, false, "", "", on);
if (mine !== seq) return; // 최신 응답만
if (!got.total && fallback) {
const next = [...(q.includes(" ") ? [q.split(/\s+/)[0]] : []), ...(opt.alt ?? [])].find(
@@ -66,3 +66,17 @@ table.m01-master__grid.m01-machine__grid {
max-height: 240px;
overflow: auto;
}
/* 「사용」 열(842-2) — 스위치 · 끈 줄은 흐림 */
.m01-machine__grid th.m01-machine__use {
width: 64px;
}
.m01-machine__grid td.m01-machine__use {
text-align: center;
}
.m01-machine__row.is-off td:not(.m01-machine__use) {
color: var(--color-text-muted);
opacity: 0.6;
}
+43 -4
View File
@@ -3,14 +3,17 @@
* M01 기계 목록(PLAN 821-2 · 요구 790-2) — 표는 목록용 열만(이름 · 규격 · 구분 · 가격 · 손료계수 ·
* 주연료 · 조종원 · 시간당 경비(서버 값) · 별칭 · 비고) · 줄을 누르면 상세 페이지 · [새 기계] = 양식.
* 단위 · 출처 열 없음 · 고친 것 · 새 줄 · 지울 줄은 초안(노랑 · 새 · 지움 표시) — 저장은 페이지 [저장].
* 「사용」 열(842-2) = 스위치 · 끄면 줄 맨 위 칸 `비활성: true`(초안 → [저장]) · 끈 줄은 흐림 ·
* 머리 「비활성 같이 보기」 끄면 서버가 끈 줄을 뺌(`rows` state 「켜짐」).
* ========================================================================== */
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import { createEmpty } from "@ui/ui_template_empty";
import { createToggle } from "@ui/ui_template_toggle";
import { guessRows, pageKeepFirst, watchFit } from "@ui/ui_template_pager";
import { fetchRows, type Row, type RowsPage } from "./M01_MasterData_Api_Fetch";
import { onDraftChange, peek, same } from "./M01_MasterData_Draft";
import { editRow, onDraftChange, peek, same } from "./M01_MasterData_Draft";
import { fetchMachineHourly } from "./M01_MasterData_Machine_Api";
import { buildPager } from "./M01_MasterData_UI_Cells";
import { saveView, type Pick } from "./M01_MasterData_UI_Side";
@@ -38,6 +41,8 @@ const COLS: [string, (r: Row) => string][] = [
],
];
const val = (r: Row): Record<string, unknown> => (r["값"] ?? {}) as Record<string, unknown>;
const OFF = "비활성"; // 842-2 기계 줄 맨 위 칸 — true 만 · 켜짐 = 칸 없음
let showOff = true; // 「비활성 같이 보기」(페이지 안에서 기억)
/** 돌려받은 함수 = 걷기. `open(target)` = 상세(키 · 새 줄 `add:<차례>` · 새 기계 `new`). */
export function renderMachines(
@@ -53,9 +58,31 @@ export function renderMachines(
const hourly = new Map<string, string>();
const hourText = (key: string): string => hourly.get(key) ?? "…";
/** 「사용」 칸 — 스위치(새 줄은 빈 칸) · 바꾸면 초안 */
const useCell = (row: Row, cur: Row, key: string): HTMLElement => {
const td = el("td", { className: "m01-machine__use" });
if (!key || !data) return td;
const version = data.version;
const sw = createToggle({
label: "",
size: 20,
checked: !cur[OFF],
onChange: (on) => {
const next: Row = { ...cur };
if (on) delete next[OFF];
else next[OFF] = true;
editRow(file, version, key, row, next);
},
});
sw.root.setAttribute("aria-label", L("M01_MachineUse"));
sw.root.addEventListener("click", (e) => e.stopPropagation());
td.classList.toggle("is-changed", !!cur[OFF] !== !!row[OFF]);
td.append(sw.root);
return td;
};
const line = (row: Row, cur: Row, cls: string, target: string, key = ""): HTMLElement => {
const tr = el("tr", {
className: `m01-machine__row ${cls}`.trim(),
className: `m01-machine__row ${cls} ${cur[OFF] ? "is-off" : ""}`.trim(),
attrs: { "data-key": key || target },
children: [
...COLS.map(([, get]) => {
@@ -71,6 +98,7 @@ export function renderMachines(
}),
el("td", { text: String(cur["별칭"] ?? ""), attrs: { title: String(cur["별칭"] ?? "") } }),
el("td", { text: String(cur["비고"] ?? ""), attrs: { title: String(cur["비고"] ?? "") } }),
useCell(row, cur, key),
],
});
tr.addEventListener("click", () => open(target));
@@ -91,12 +119,21 @@ export function renderMachines(
}
const add = createButton({ label: L("M01_MachineNew"), variant: "ghost", iconStart: "plus" });
add.addEventListener("click", () => open("new"));
const offSwitch = createToggle({
label: L("M01_MachineShowOff"),
checked: showOff,
onChange: (on) => {
showOff = on;
page = 1;
void load();
},
});
const empty =
d.rows.length || draft?.adds.length
? []
: [createEmpty({ text: L("M01_NoRows"), small: true })];
host.replaceChildren(
el("div", { className: "ui-toolbar m01-master__bar", children: [add] }),
el("div", { className: "ui-toolbar m01-master__bar", children: [add, offSwitch.root] }),
el("div", {
className: "ui-table-wrap m01-master__grid-wrap m01-machine__wrap",
children: [
@@ -111,6 +148,7 @@ export function renderMachines(
el("th", { text: `${L("M01_MachineHourly")}(원)` }),
el("th", { text: "별칭" }),
el("th", { text: "비고" }),
el("th", { className: "m01-machine__use", text: L("M01_MachineUse") }),
],
}),
],
@@ -152,7 +190,8 @@ export function renderMachines(
const load = async (): Promise<void> => {
const mine = ++ticket;
try {
const got = await fetchRows(file, page, size, q, false, pick.sub, pick.detail, pick.state);
const state = showOff ? pick.state : "켜짐";
const got = await fetchRows(file, page, size, q, false, pick.sub, pick.detail, state);
if (mine !== ticket) return;
data = got;
saveView(pick.group, { page: data.page });
@@ -28,3 +28,9 @@ export function cfMachineNames(): Promise<Record<string, string>> {
.catch(() => ((machineNames = null), {}));
return machineNames;
}
/** 끈 기계 원문번호(842-2) — 이름과 함께 한 번 받아 둠(못 받으면 빈 목록) */
export const cfMachineOff = (): Promise<string[]> =>
fetch(`${API_BASE_URL}/m02/df/machine-off`, { credentials: "include" })
.then((r) => (r.ok ? (r.json() as Promise<string[]>) : []))
.catch(() => []);
@@ -33,6 +33,17 @@ def get_machine_names() -> dict:
return out
@router.get("/machine-off")
def get_machine_off() -> list[str]:
"""끈 기계 원문번호(842-2) — 그 번호 기계가 모두 「비활성」 · 옵션 고르기 목록에서 뺌(고른 값은 남김)."""
on, off = set(), set()
for data in store.loaded()[0].values():
if data.get("그룹") == "기계":
for row in data.get(store.items_key(data)) or []:
(off if row.get("비활성") else on).add(str(row.get("원문번호") or ""))
return sorted(off - on - {""})
@router.get("/standards")
def get_standards() -> dict:
try:
+2 -1
View File
@@ -48,7 +48,8 @@ def get_materials(
def get_rows(file: str, page: int = 1, size: int = 50, q: str = "", sub: str = "") -> dict:
if not file.startswith(ROW_FILES):
raise HTTPException(status_code=403, detail="요소 파일(인력 · 재료 · 기계)만 읽음")
return m01.get_rows(file, page, size, q, False, sub, "", "")
on = "켜짐" if file.startswith("기계") else "" # 끈 기계 뺌(842-2)
return m01.get_rows(file, page, size, q, False, sub, "", on)
@router.get("/table")
+1 -1
View File
@@ -145,7 +145,7 @@ class Row:
@lru_cache(maxsize=4)
def _table(whole: Any) -> tuple[list[Row], frozenset[str], dict[str, Row]]:
rows = [Row(r) for r in whole.numbers.get("EQ", {}).values()]
rows = [Row(r) for r in whole.numbers.get("EQ", {}).values() if not r.get("비활성")] # 842-2
words = frozenset(w for r in rows for w in r.words)
return rows, words, {r.key: r for r in rows}
+1 -1
View File
@@ -54,7 +54,7 @@ def find(q: str, limit: int = 30) -> list[dict]:
if all(w in f"{r.get('이름')} {r.get('규격')} {r.get('키')}".lower() for w in words)
]
exact = (m01.loaded()[1].numbers.get("EQ") or {}).get(q.strip())
head = [exact] if exact is not None and not mym.mine(exact.get("키")) else []
head = [exact] if exact and not mym.mine(exact.get("키")) and not exact.get("비활성") else []
m01_rows = [*head, *_m01m().find(q, limit)]
seen = set()
for r in m01_rows:
@@ -136,6 +136,9 @@ COLS = {
}
COLS["한국은행환율"] = COLS["유가전력"]
ALIAS = "별칭" # 요소 줄 덧칸(815-2) — 열 한 벌 밖 · 있으면 빈 글 아님
OFF = (
"비활성" # 기계 줄 맨 위 칸(842-2) — 있으면 true 만 · 없음 = 켜짐 · 찾기 · 고르기 · 잇기에서 뺌
)
def _cols_of(row: dict) -> tuple:
@@ -299,6 +302,8 @@ def check_form(name: str, data: dict) -> list[str]:
out += _check_labor(where, row)
if ALIAS in row and not (isinstance(row[ALIAS], str) and row[ALIAS].strip()):
out.append(f"{where} · 「{ALIAS}」 이 빈 글 · 글 아님(빈 칸이면 칸을 두지 않음)")
if OFF in row and (row[OFF] is not True or data.get("그룹") != "기계"):
out.append(f"{where} · 「{OFF}」 는 기계 줄에만 true(켜짐이면 칸을 두지 않음)")
if cols and _cols_of(row) != cols:
out.append(f"{where} · 열이 한 벌이 아님 — 「{' · '.join(cols)}」")
if book == "자재품목" and row.get(mm.LOAD) not in mm.LOADS:
@@ -1,5 +1,6 @@
{
"키": "EQ000541",
"비활성": true,
"원문번호": "9010-0003",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000542",
"비활성": true,
"원문번호": "9010-0006",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000543",
"비활성": true,
"원문번호": "9010-0010",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000544",
"비활성": true,
"원문번호": "9010-0012",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000545",
"비활성": true,
"원문번호": "9010-0020",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000546",
"비활성": true,
"원문번호": "9010-0022",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000547",
"비활성": true,
"원문번호": "9010-0033",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000548",
"비활성": true,
"원문번호": "9010-0040",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000549",
"비활성": true,
"원문번호": "9010-0044",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000550",
"비활성": true,
"원문번호": "9010-0060",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000551",
"비활성": true,
"원문번호": "9010-0080",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000552",
"비활성": true,
"원문번호": "9010-0120",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000553",
"비활성": true,
"원문번호": "9010-0200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000554",
"비활성": true,
"원문번호": "9020-0010",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000555",
"비활성": true,
"원문번호": "9020-0015",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000556",
"비활성": true,
"원문번호": "9020-0016",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000557",
"비활성": true,
"원문번호": "9020-0022",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000558",
"비활성": true,
"원문번호": "9020-0035",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000559",
"비활성": true,
"원문번호": "9020-0050",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000560",
"비활성": true,
"원문번호": "9020-0072",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000561",
"비활성": true,
"원문번호": "9020-0160",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000562",
"비활성": true,
"원문번호": "9020-0180",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000563",
"비활성": true,
"원문번호": "9020-0200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000564",
"비활성": true,
"원문번호": "9030-0016",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000565",
"비활성": true,
"원문번호": "9030-0018",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000566",
"비활성": true,
"원문번호": "9030-0025",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000567",
"비활성": true,
"원문번호": "9030-0035",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000568",
"비활성": true,
"원문번호": "9030-0045",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000569",
"비활성": true,
"원문번호": "9030-0050",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000570",
"비활성": true,
"원문번호": "9030-0080",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000571",
"비활성": true,
"원문번호": "9030-0100",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000572",
"비활성": true,
"원문번호": "9030-0240",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000573",
"비활성": true,
"원문번호": "9040-0010",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000574",
"비활성": true,
"원문번호": "9040-0030",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000575",
"비활성": true,
"원문번호": "9040-0050",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000576",
"비활성": true,
"원문번호": "9040-0060",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000577",
"비활성": true,
"원문번호": "9040-0100",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000578",
"비활성": true,
"원문번호": "9040-0120",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000579",
"비활성": true,
"원문번호": "9040-0200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000580",
"비활성": true,
"원문번호": "9040-0250",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000581",
"비활성": true,
"원문번호": "9040-0300",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000582",
"비활성": true,
"원문번호": "9040-0380",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000583",
"비활성": true,
"원문번호": "9040-0680",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000584",
"비활성": true,
"원문번호": "9050-0075",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000585",
"비활성": true,
"원문번호": "9050-0150",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000586",
"비활성": true,
"원문번호": "9050-0450",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000587",
"비활성": true,
"원문번호": "9050-0750",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000588",
"비활성": true,
"원문번호": "9050-0850",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000589",
"비활성": true,
"원문번호": "9060-0060",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000590",
"비활성": true,
"원문번호": "9060-0100",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000591",
"비활성": true,
"원문번호": "9060-0200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000592",
"비활성": true,
"원문번호": "9060-0300",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000593",
"비활성": true,
"원문번호": "9060-0500",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000594",
"비활성": true,
"원문번호": "9060-0600",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000595",
"비활성": true,
"원문번호": "9070-0015",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000596",
"비활성": true,
"원문번호": "9070-0020",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000597",
"비활성": true,
"원문번호": "9080-0050",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000598",
"비활성": true,
"원문번호": "9080-0080",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000599",
"비활성": true,
"원문번호": "9080-0100",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000600",
"비활성": true,
"원문번호": "9080-0120",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000601",
"비활성": true,
"원문번호": "9080-0150",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000602",
"비활성": true,
"원문번호": "9080-0200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000603",
"비활성": true,
"원문번호": "9080-0300",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000604",
"비활성": true,
"원문번호": "9080-0500",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000605",
"비활성": true,
"원문번호": "9080-0700",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000606",
"비활성": true,
"원문번호": "9080-1000",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000607",
"비활성": true,
"원문번호": "9080-1100",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000608",
"비활성": true,
"원문번호": "9080-1400",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000609",
"비활성": true,
"원문번호": "9080-1500",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000610",
"비활성": true,
"원문번호": "9080-1750",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000611",
"비활성": true,
"원문번호": "9080-2000",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000612",
"비활성": true,
"원문번호": "9080-3000",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000613",
"비활성": true,
"원문번호": "9090-0800",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000614",
"비활성": true,
"원문번호": "9090-1000",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000615",
"비활성": true,
"원문번호": "9090-1200",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000616",
"비활성": true,
"원문번호": "9090-1300",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000617",
"비활성": true,
"원문번호": "9090-1400",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000618",
"비활성": true,
"원문번호": "9090-1500",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -1,5 +1,6 @@
{
"키": "EQ000619",
"비활성": true,
"원문번호": "9090-1600",
"원문": "건설품셈",
"구분": "[90]해상장비",
@@ -35,8 +35,11 @@ def test_기계_이름_표_문서_목록_전수() -> None:
def test_옵션_부품_글만_바꿈() -> None:
src = (ROOT / "A00_Common/formula_doc_options.ts").read_text(encoding="utf-8")
assert "known[x] ? `${known[x]} (${x})` : x" in src
assert "list.map((x) => ({ value: x, text: optionText(x) }))" in src
assert "known[x] ? `${known[x]} (${x})${off.has(x)" in src # 842-2 끈 기계 꼬리
assert (
'liveOptions(list, String(now ?? "")).map((x) => ({ value: x, text: optionText(x) }))'
in src
)
assert "nameMachines(list, () => f.setOptions(items(), f.select.value));" in src
api = (ROOT / "M02_MasterTemplete/DataFlow/M02_DF_Api.ts").read_text(encoding="utf-8")
assert "machineNames ??= fetch(" in api # 한 번만 받음
+118
View File
@@ -0,0 +1,118 @@
"""842-2 기계 비활성 — 지킴 시험(쓰기는 임시 사본만).
① 틀 검사 — 기계 줄 맨 위 칸 `비활성` = true 만 · 다른 그룹 줄 · false 는 오류
② 저장 왕복 — M01 저장 길이 표지를 받고(그 대 파일 하나) 지우면 칸이 사라짐 · 셈 값 그대로
③ 끈 기계는 찾기 · 고르기 · 잇기에서 안 나옴 — 요소 찾기 창 · 비슷한 기계 · 불러오기 기계 잇기 ·
목록 거름 「켜짐」 · 끈 원문번호 목록 · 셈(시간당)은 그대로
"""
from __future__ import annotations
import copy
import shutil
import sys
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as store
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "resources" / "master_data" / "scripts"))
import master_machine_files as mmf # noqa: E402
from M01_MasterData import M01_MasterData_Machine as m01m # noqa: E402
from M02_MasterTemplete import M02_Machine_Find as mfind # noqa: E402
from M02_MasterTemplete import M02_Machine_Hourly as mh # noqa: E402
from M02_MasterTemplete.DataFlow import M02_DF_Router as dfr # noqa: E402
REAL = store.FOLDER
cm, mk, M = store.cm, store.mk, mmf.NAME
@pytest.fixture
def master(tmp_path: Path, monkeypatch) -> Path:
for path in REAL.glob("*.json"):
if not path.name.startswith("_") or path.name == mk.BOOK.name:
shutil.copy(path, tmp_path / path.name)
shutil.copytree(REAL / mmf.DIR, tmp_path / mmf.DIR)
monkeypatch.setattr(store, "FOLDER", tmp_path)
return tmp_path
def _rows() -> list[dict]:
return store.read(M)[0]["줄"]
def _part(*changes: dict) -> dict:
return {"file": M, "version": store.read(M)[1], "changes": list(changes)}
# ① 틀 검사 ──────────────────────────────────────────────────────────
def test_form_check() -> None:
data = copy.deepcopy(store.read(M)[0])
data["줄"] = data["줄"][:3]
assert cm.check_form(M, data) == []
data["줄"][0][cm.OFF] = True
assert cm.check_form(M, data) == []
data["줄"][0][cm.OFF] = False
assert any(cm.OFF in e for e in cm.check_form(M, data))
labor = copy.deepcopy(store.read("인력.json")[0])
labor["줄"] = labor["줄"][:1]
labor["줄"][0][cm.OFF] = True
assert any(cm.OFF in e for e in cm.check_form("인력.json", labor))
# ② 저장 왕복 ───────────────────────────────────────────────────────
def test_save_round_trip(master: Path) -> None:
key = next(r["키"] for r in _rows() if not r.get(cm.OFF) and r["이름"].startswith("불도저"))
row = copy.deepcopy(next(r for r in _rows() if r["키"] == key))
before = mh.hourly(key)
snap = {p.name: p.read_bytes() for p in (master / mmf.DIR).glob("*.json")}
store.save([_part({"op": "edit", "key": key, "row": {**row, cm.OFF: True}})])
after = {p.name: p.read_bytes() for p in (master / mmf.DIR).glob("*.json")}
assert {n for n in after if after[n] != snap[n]} == {f"{key}.json"}
got = next(r for r in _rows() if r["키"] == key)
assert got[cm.OFF] is True and got["값"] == row["값"]
assert mh.hourly(key) == before # 셈은 그대로(쓰던 단가산출 · 사본이 깨지지 않음)
assert key not in {x["ref"] for x in store.elements("기계", "불도저", 200)["items"]}
store.save([_part({"op": "edit", "key": key, "row": row})])
assert (master / mmf.DIR / f"{key}.json").read_bytes() == snap[f"{key}.json"]
# ③ 찾기 · 고르기 · 잇기에서 뺌 ──────────────────────────────────────
def _off() -> list[dict]:
got = [r for r in _rows() if r.get(cm.OFF)]
if not got:
pytest.skip("끈 기계 없음")
return got
def test_hidden_everywhere() -> None:
off = _off()
keys = {r["키"] for r in off}
for r in off[:: max(1, len(off) // 6)]:
name = str(r["상세구분"] or r["이름"]).replace(" ", "")
assert not keys & {x["ref"] for x in store.elements("기계", name, 200)["items"]}
assert not keys & {x["키"] for x in m01m.find(name, 200)}
with mfind.m01_only():
got = mfind.find(r["이름"], r.get("규격") or "")
assert got["키"] not in keys and not keys & {x["키"] for x in got["후보"]}
listed = store.rows(M, 1, 500, "", state="켜짐")
assert not keys & {r["키"] for r in listed["rows"]}
assert listed["total"] == len(_rows()) - len(keys)
assert store.rows(M, 1, 1, "")["total"] == len(_rows()) # 같이 보기 = 전부
codes = {str(r["원문번호"]) for r in off} - {
str(r["원문번호"]) for r in _rows() if not r.get(cm.OFF)
}
assert set(dfr.get_machine_off()) == codes
assert all(c in dfr.get_machine_names() for c in codes) # 고른 값 글은 그대로
def test_sea_machines_off() -> None:
"""842-2 ㉣ 해상 기계([90]해상장비 = 선원류 칸 있는 대)는 끔 — 사용자가 다시 켤 수 있으므로 켠 대는 알림만."""
sea = [r for r in _rows() if "해상" in str(r.get("구분"))]
assert len(sea) == 79
on = [r["키"] for r in sea if not r.get(cm.OFF)]
if on:
pytest.skip(f"다시 켠 해상 기계 {len(on)} 대 — 사용자 판단")
@@ -49,6 +49,8 @@ export const ui_locales_machine = {
M01_MachineNew: ["새 기계", "New machine"],
M01_MachineBack: ["목록으로", "Back to list"],
M01_MachineHourly: ["시간당 경비", "Hourly cost"],
M01_MachineUse: ["사용", "Active"],
M01_MachineShowOff: ["비활성 같이 보기", "Show inactive"],
M01_MachineMeta: ["원문 · 구분", "Source · group"],
M01_MachineUses: ["쓰는 곳", "Used by"],
M01_MachineNoUses: ["쓰는 곳 없음", "Not used"],