wip feat(M01): 기계 구분·상세구분 2단 거름 · 소요량/계수 용도 표시·대상 거름
기계.json 머리 구분 딕셔너리 추가(세부분류 유지) · UI_Side 기계 2단 필터 Store/Router/Api_Fetch usage 쿼리 추가 · UI_Tables 용도 표시 + 대상 드롭다운 locale 문구 추가 · 관련 시험 수정·추가
This commit is contained in:
@@ -41,6 +41,13 @@ export interface RowsPage {
|
||||
refs: Record<string, string>;
|
||||
}
|
||||
|
||||
/** 소요량·계수 표의 용도 — 공종(원문 절 이름) · 대상(인력 · 기계 · 재료 · 할증 · 환산 · 작업량 조건) · 이 표를 쓰는 로직 키. */
|
||||
export interface TableUsage {
|
||||
공종: string;
|
||||
대상: string[];
|
||||
로직키: string[];
|
||||
}
|
||||
|
||||
export interface TableHead {
|
||||
file: string;
|
||||
키: string;
|
||||
@@ -133,7 +140,8 @@ export const fetchTables = (
|
||||
q: string,
|
||||
page: number,
|
||||
size: number,
|
||||
): Promise<TablesPage> => get("/tables", { group, sub, detail, q, page, size });
|
||||
usage = "",
|
||||
): Promise<TablesPage> => get("/tables", { group, sub, detail, q, page, size, usage });
|
||||
|
||||
export const fetchTable = (
|
||||
file: string,
|
||||
|
||||
@@ -95,8 +95,9 @@ def get_tables(
|
||||
q: str = "",
|
||||
page: int = 1,
|
||||
size: int = 30,
|
||||
usage: str = "",
|
||||
) -> dict:
|
||||
return _call(store.tables, file, group, sub, detail, q, page, size)
|
||||
return _call(store.tables, file, group, sub, detail, q, page, size, usage)
|
||||
|
||||
|
||||
@router.get("/table")
|
||||
|
||||
@@ -130,7 +130,7 @@ _STATES = ("공표", "산정", "미공표", "미확보", "추정") # 인력 「
|
||||
|
||||
|
||||
def subs(file: str = "", group: str = "") -> dict:
|
||||
"""하위 거름 목록 — 한 테이블 파일은 머리 묶음(인력 「구분」 · 기계 「세부분류」) ·
|
||||
"""하위 거름 목록 — 한 테이블 파일은 머리 묶음(인력 · 기계 모두 「구분」 → 「상세구분」) ·
|
||||
장 그룹(소요량 · 계수 · 로직)은 장 파일에서 모음(구분 = 원문 + 부문 · 상세구분 = 장 · 차례대로)."""
|
||||
if group:
|
||||
if group not in cm.mf.GROUPS:
|
||||
@@ -184,9 +184,9 @@ def rows(
|
||||
) -> dict:
|
||||
data, version = read(file)
|
||||
hits = [r for r in data.get(items_key(data)) or [] if _hit(r, q)]
|
||||
if sub: # 인력 구분 · 기계 세부분류
|
||||
if sub: # 인력·기계 구분 (세부분류는 남은 옛 화면 호출 대비)
|
||||
hits = [r for r in hits if sub in (r.get("구분"), r.get("세부분류"))]
|
||||
if detail: # 인력 상세구분
|
||||
if detail: # 인력 상세구분 · 기계 상세구분(기종)
|
||||
hits = [r for r in hits if r.get("상세구분") == detail]
|
||||
if state: # 인력 상태
|
||||
hits = [r for r in hits if r.get("상태") == state]
|
||||
@@ -227,8 +227,10 @@ def tables(
|
||||
q: str = "",
|
||||
page: int = 1,
|
||||
size: int = 30,
|
||||
usage: str = "",
|
||||
) -> dict:
|
||||
"""표 목록 — 파일 하나 또는 그룹 전체(구분 · 상세구분 · 찾기로 거름 · 장 차례대로 · 쪽 나눔)."""
|
||||
"""표 목록 — 파일 하나 또는 그룹 전체(구분 · 상세구분 · 용도 대상 · 찾기로 거름 · 장 차례대로 · 쪽 나눔).
|
||||
`usage` = 용도.대상(인력 · 기계 · 재료 · 할증 · 환산 · 작업량 조건) 하나."""
|
||||
if file:
|
||||
names = [file]
|
||||
elif group in cm.mf.TABLE_GROUPS:
|
||||
@@ -245,6 +247,8 @@ def tables(
|
||||
continue
|
||||
if detail and t.get("상세구분") != detail:
|
||||
continue
|
||||
if usage and usage not in ((t.get("용도") or {}).get("대상") or []):
|
||||
continue
|
||||
if _hit(t, q):
|
||||
hits.append(
|
||||
{
|
||||
|
||||
@@ -11,10 +11,11 @@ import { t as L } from "@ui/ui_template_locale";
|
||||
import { fetchFiles, fetchSubsAll, type FileInfo, type SubInfo } from "./M01_MasterData_Api_Fetch";
|
||||
import { renderTree, type MakeRow } from "./M01_MasterData_UI_Tree";
|
||||
|
||||
/** 컨테이너마다 기억하는 화면 상태 — 찾기 글 · 쪽 (거름은 `m01.filter.*`) */
|
||||
/** 컨테이너마다 기억하는 화면 상태 — 찾기 글 · 쪽 · 소요량·계수 표의 용도 대상 거름 (거름은 `m01.filter.*`) */
|
||||
export interface View {
|
||||
q: string;
|
||||
page: number;
|
||||
usage?: string;
|
||||
}
|
||||
|
||||
const viewKey = (group: string): string => `m01.view.${group}`;
|
||||
@@ -262,7 +263,7 @@ export function buildSide(
|
||||
const one = files[0];
|
||||
const dropped: Partial<Record<Group, { label: string; detail?: string }>> = {
|
||||
인력: { label: L("M01_LaborSub"), detail: L("M01_LaborDetail") },
|
||||
기계: { label: L("M01_MachineSub") },
|
||||
기계: { label: L("M01_MachineSub"), detail: L("M01_MachineDetail") },
|
||||
소요량: { label: L("M01_LaborSub"), detail: L("M01_LaborDetail") },
|
||||
계수: { label: L("M01_LaborSub"), detail: L("M01_LaborDetail") },
|
||||
};
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
}
|
||||
|
||||
.m01-master__head,
|
||||
.m01-master__bar {
|
||||
.m01-master__bar,
|
||||
.m01-master__filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
@@ -4,9 +4,15 @@
|
||||
* → 표 하나(조건 칸 + 값 칸의 긴 줄) · 눌러 고침(노랑) · 줄 더하기·지우기
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { fetchTable, fetchTables, type Row, type TableHead } from "./M01_MasterData_Api_Fetch";
|
||||
import {
|
||||
fetchTable,
|
||||
fetchTables,
|
||||
type Row,
|
||||
type TableHead,
|
||||
type TableUsage,
|
||||
} from "./M01_MasterData_Api_Fetch";
|
||||
import { saveView, type Pick } from "./M01_MasterData_UI_Side";
|
||||
import {
|
||||
onDraftChange,
|
||||
@@ -27,16 +33,31 @@ import {
|
||||
|
||||
const LIST_SIZE = 30;
|
||||
const LIST_COLS = ["키", "원문번호", "구분", "상세구분", "이름", "기준", "출처", "비고"] as const;
|
||||
/** 용도 대상 거름 드롭다운 후보 — PLAN 1-1 셋째 줄 그대로. */
|
||||
const USAGE_TARGETS = ["인력", "기계", "재료", "할증", "환산", "작업량 조건"];
|
||||
|
||||
/** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. */
|
||||
export function renderTables(host: HTMLElement, pick: Pick, q: string): () => void {
|
||||
let stop = (): void => {};
|
||||
let usage = pick.view?.usage ?? "";
|
||||
const list = (page = pick.view?.page ?? 1): void => {
|
||||
stop();
|
||||
stop = (): void => {};
|
||||
void showList(host, pick, q, page, (file, key) => {
|
||||
stop = openTable(host, file, key, () => list(page));
|
||||
});
|
||||
void showList(
|
||||
host,
|
||||
pick,
|
||||
q,
|
||||
usage,
|
||||
page,
|
||||
(file, key) => {
|
||||
stop = openTable(host, file, key, () => list(page));
|
||||
},
|
||||
(next) => {
|
||||
usage = next;
|
||||
saveView(pick.group, { usage, page: 1 });
|
||||
list(1);
|
||||
},
|
||||
);
|
||||
};
|
||||
list();
|
||||
const off = onDraftChange(() => host.querySelector(".m01-master__tables") && markChanged(host));
|
||||
@@ -58,17 +79,19 @@ async function showList(
|
||||
host: HTMLElement,
|
||||
pick: Pick,
|
||||
q: string,
|
||||
usage: string,
|
||||
page: number,
|
||||
open: (file: string, key: string) => void,
|
||||
onUsageChange: (usage: string) => void,
|
||||
): Promise<void> {
|
||||
let got: { total: number; tables: TableHead[] };
|
||||
try {
|
||||
got = await fetchTables(pick.group, pick.sub, pick.detail, q, page, LIST_SIZE);
|
||||
got = await fetchTables(pick.group, pick.sub, pick.detail, q, page, LIST_SIZE, usage);
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
|
||||
return;
|
||||
}
|
||||
saveView(pick.group, { page });
|
||||
saveView(pick.group, { page, usage });
|
||||
const rows = got.tables.map((t) => {
|
||||
const tr = el("tr", {
|
||||
className: "m01-master__table-row",
|
||||
@@ -81,8 +104,24 @@ async function showList(
|
||||
tr.addEventListener("click", () => open(t.file, t.키));
|
||||
return tr;
|
||||
});
|
||||
const usageField = createSelectField({
|
||||
options: [
|
||||
{ value: "", text: L("M01_All") },
|
||||
...USAGE_TARGETS.map((v) => ({ value: v, text: v })),
|
||||
],
|
||||
value: usage,
|
||||
compact: true,
|
||||
label: L("M01_UsageTarget"),
|
||||
});
|
||||
usageField.select.addEventListener("change", () => onUsageChange(usageField.select.value));
|
||||
host.replaceChildren(
|
||||
buildPager(got.total, LIST_SIZE, page, (p) => void showList(host, pick, q, p, open)),
|
||||
el("div", { className: "m01-master__filter", children: [usageField.root] }),
|
||||
buildPager(
|
||||
got.total,
|
||||
LIST_SIZE,
|
||||
page,
|
||||
(p) => void showList(host, pick, q, usage, p, open, onUsageChange),
|
||||
),
|
||||
el("div", {
|
||||
className: "m01-master__grid-wrap m01-master__tables",
|
||||
children: [
|
||||
@@ -169,6 +208,7 @@ function openTable(host: HTMLElement, file: string, key: string, back: () => voi
|
||||
const notes = ((table.주 ?? []) as string[]).map((n) =>
|
||||
el("p", { className: "m01-master__muted", text: n }),
|
||||
);
|
||||
const usage = table.용도 as TableUsage | undefined;
|
||||
host.replaceChildren(
|
||||
el("div", {
|
||||
className: "m01-master__bar",
|
||||
@@ -179,6 +219,17 @@ function openTable(host: HTMLElement, file: string, key: string, back: () => voi
|
||||
className: "m01-master__muted",
|
||||
text: `${key} · ${table.기준 || "—"} · ${table.출처 || ""}`,
|
||||
}),
|
||||
...(usage
|
||||
? [
|
||||
el("span", {
|
||||
className: "m01-master__muted",
|
||||
attrs: { title: usage.로직키.join(", ") },
|
||||
text: `${L("M01_TableUsage")}: ${usage.공종} · ${usage.대상.join(", ")} · ${L(
|
||||
"M01_TableUsageLogicCount",
|
||||
).replace("{value}", String(usage.로직키.length))}`,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
add,
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -815,6 +815,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"구분": {
|
||||
"[00]토공기계": {
|
||||
"원문": "건설품셈",
|
||||
"상세구분": []
|
||||
},
|
||||
"[10]다짐기계": {
|
||||
"원문": "건설품셈",
|
||||
"상세구분": []
|
||||
},
|
||||
"[20]운반 및 하역기계": {
|
||||
"원문": "건설품셈",
|
||||
"상세구분": []
|
||||
},
|
||||
"[30]포장기계": {
|
||||
"원문": "건설품셈",
|
||||
"상세구분": []
|
||||
},
|
||||
"[40]콘크리트기계": {
|
||||
"원문": "건설품셈",
|
||||
"상세구분": []
|
||||
},
|
||||
"[50]골재생산기계 등": {
|
||||
"원문": "건설품셈",
|
||||
"상세구분": []
|
||||
},
|
||||
"[60]기초공사용 기계": {
|
||||
"원문": "건설품셈",
|
||||
"상세구분": []
|
||||
},
|
||||
"[70]기타기계": {
|
||||
"원문": "건설품셈",
|
||||
"상세구분": []
|
||||
},
|
||||
"[80]스마트 건설장비": {
|
||||
"원문": "건설품셈",
|
||||
"상세구분": []
|
||||
},
|
||||
"[90]해상장비": {
|
||||
"원문": "건설품셈",
|
||||
"상세구분": []
|
||||
},
|
||||
"임업기계": {
|
||||
"원문": "산림품셈",
|
||||
"상세구분": []
|
||||
}
|
||||
},
|
||||
"줄": [
|
||||
{
|
||||
"키": "EQ000001",
|
||||
|
||||
@@ -113,6 +113,25 @@ def test_표_목록은_구분_상세구분으로_거름(client: TestClient) -> N
|
||||
assert page["page"] == 2 and len(page["tables"]) == 5 and page["total"] == every["total"]
|
||||
|
||||
|
||||
def test_표_용도_대상으로_거름(client: TestClient) -> None:
|
||||
"""PLAN 1-1 셋째 줄 — 표마다 붙은 용도(공종·대상·로직키)로 거름."""
|
||||
one = _get(client, "/api/m01/tables", group="소요량", size=1)["tables"][0]
|
||||
usage = _get(client, "/api/m01/table", file=one["file"], key=one["키"])["table"]["용도"]
|
||||
assert usage["공종"] and usage["대상"]
|
||||
target = usage["대상"][0]
|
||||
narrowed = _get(
|
||||
client, "/api/m01/tables", group="소요량", q=one["이름"], usage=target, size=500
|
||||
)
|
||||
assert one["키"] in {t["키"] for t in narrowed["tables"]}
|
||||
ALL_TARGETS = ("인력", "기계", "재료", "할증", "환산", "작업량 조건")
|
||||
other = next((v for v in ALL_TARGETS if v not in usage["대상"]), None)
|
||||
if other:
|
||||
excluded = _get(
|
||||
client, "/api/m01/tables", group="소요량", q=one["이름"], usage=other, size=500
|
||||
)
|
||||
assert one["키"] not in {t["키"] for t in excluded["tables"]}
|
||||
|
||||
|
||||
def test_갈래_목록은_서버가_줌(client: TestClient) -> None:
|
||||
"""소요량 · 계수 · 로직 컨테이너의 구분 · 상세구분 — 장 차례대로."""
|
||||
for group in ("소요량", "계수", "로직"):
|
||||
@@ -332,10 +351,10 @@ def test_구분_목록(client: TestClient) -> None:
|
||||
survey = next(s for s in got["subs"] if s["name"] == "측량")
|
||||
assert survey["book"] == "측량노임" and "기능계 도화" in survey["details"]
|
||||
machine = _get(client, "/api/m01/subs", file="기계.json")
|
||||
assert machine["slot"] == "세부분류" and [s["name"] for s in machine["subs"]] == [
|
||||
"건설품셈",
|
||||
"산림품셈",
|
||||
]
|
||||
names = [s["name"] for s in machine["subs"]]
|
||||
assert machine["slot"] == "구분" and names[0] == "[00]토공기계" and names[-1] == "임업기계"
|
||||
earthwork = next(s for s in machine["subs"] if s["name"] == "[00]토공기계")
|
||||
assert earthwork["book"] == "건설품셈" and "불도저" in earthwork["details"]
|
||||
assert _get(client, "/api/m01/subs", file="재료_자재품목.json")["subs"] == []
|
||||
|
||||
|
||||
|
||||
@@ -58,7 +58,11 @@ export const ui_locales_m1 = {
|
||||
M01_FuelSub: ["구분", "Category"],
|
||||
M01_FuelDetail: ["상세구분", "Detail"],
|
||||
M01_LaborState: ["상태", "Status"],
|
||||
M01_MachineSub: ["세부분류", "Sub-category"],
|
||||
M01_MachineSub: ["구분", "Category"],
|
||||
M01_MachineDetail: ["상세구분", "Detail"],
|
||||
M01_UsageTarget: ["대상", "Target"],
|
||||
M01_TableUsage: ["용도", "Usage"],
|
||||
M01_TableUsageLogicCount: ["로직 {value}개", "{value} logic rules"],
|
||||
M01_GroupLabor: ["인력", "Labor"],
|
||||
M01_GroupMaterial: ["재료", "Materials"],
|
||||
M01_GroupMachine: ["기계", "Machines"],
|
||||
|
||||
Reference in New Issue
Block a user