feat(m02): 자재목록 세 칸 기본값 — 「일반」 지움 · 빈 칸 사급 · 별도계상 · 포함 · 별도계상이면 운반비 꺼짐(162-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
This commit is contained in:
2026-10-09 23:49:25 +09:00
co-authored by Claude Opus 5.5
parent 35d6b68c41
commit 365fef4fd4
5 changed files with 73 additions and 23 deletions
+9 -2
View File
@@ -21,7 +21,8 @@
같은 품목 두 번은 [저장] 때 400.
승격(134-1) = 로직 줄 「단가승격」 표시(`true` 만 · 없음 = 단가산출) · 금액 무관 · B09 호표 몫.
자재 세 칸(148-4) = `MARKS` 허용 값만(그 밖 · 빈 것 버림 · 금액 무관) — 사용자 자재 = 줄 덧칸 ·
로직 자재 = 로직 줄 `자재표시 {호표 차례: {세 칸}}`(차례가 재료 줄인 것만 · 틀리면 버림).
로직 자재 = 로직 줄 `자재표시 {호표 차례: {세 칸}}`(차례가 재료 줄인 것만 · 틀리면 버림) ·
빈 칸 = 읽을 때 `MARK_DEFAULT`(`mark_of` · 저장은 비움 · 옛 「일반」 = 버림 → 사급 · 162-1).
% 덧줄(149-4) = 식 「X * Y / 100」 덧줄에 덧칸 `율`(Y) · `규격`(「노무비의 %」 등 · `pct_extras`) — 표기만 ·
단위 · 수량 · 금액 그대로(「식 · 1」) · 화면이 율 · % 로 그림 · M01 시험계산 표도 이 함수.
"""
@@ -46,10 +47,11 @@ COSTS = mf.COST_ITEMS # 노무비 · 재료비 · 경비
MATERIAL_HEAD = "자재|"
_MAT_COLS = ("코드", "이름", "규격", "단위")
MARKS = {
"자재구분": ("일반", "관급", "사급"),
"자재구분": ("관급", "사급"),
"별도계상": ("별도계상", "직접계산"),
"운반비": ("포함", "미포함"),
}
MARK_DEFAULT = {"자재구분": "사급", "별도계상": "별도계상", "운반비": "포함"} # 읽을 때 기본(162-1)
ALIAS_MAX = 40 # 별칭(44-6) 글자 수 상한 — 화면 칸과 같음
_BRACE = re.compile(r"\{([^{}]+)\}") # 품목 조건의 {입력}
@@ -217,6 +219,11 @@ def marks(one: Any) -> dict[str, str]:
return {k: got[k] for k, allowed in MARKS.items() if got.get(k) in allowed}
def mark_of(one: Any, key: str) -> str:
"""세 칸 한 칸 읽기(162-1) — 허용 값 아니면(빈 · 옛 「일반」) 기본값."""
return marks(one).get(key) or MARK_DEFAULT[key]
def _mat_marks(value: Any, logic: dict) -> dict[str, dict[str, str]] | None:
"""로직 자재 세 칸(148-4) `{호표 차례: {세 칸}}` — 맨 위 층 재료 줄 차례 · 허용 값만 ·
빈 것 = None."""
@@ -138,14 +138,20 @@ export interface LaborRow {
/** 로직 자재 세 칸(148-4) — {호표 차례: 세 칸} · 금액 무관 */
자재표시?: Record<string, MatMarks>;
}
/** 자재 세 칸(148-4 · 서버 `M02_Labor_Doc.MARKS` 와 같음) — 처음 값 없음(「—」) · 금액 무관 */
/** 자재 세 칸(148-4 · 서버 `M02_Labor_Doc.MARKS` 와 같음) — 빈 칸 = 읽을 때 기본값 · 금액 무관 */
export const MAT_MARKS = {
자재구분: ["일반", "관급", "사급"],
자재구분: ["관급", "사급"],
별도계상: ["별도계상", "직접계산"],
운반비: ["포함", "미포함"],
} as const;
export type MatMarkKey = keyof typeof MAT_MARKS;
export type MatMarks = { [K in MatMarkKey]?: (typeof MAT_MARKS)[K][number] };
/** 읽을 때 기본값(162-1 · 서버 `MARK_DEFAULT` 와 같음) — 저장은 비움 */
export const MAT_MARK_DEFAULT: Required<MatMarks> = {
자재구분: "사급",
별도계상: "별도계상",
운반비: "포함",
};
/** 허용 값만 남김(서버 `marks` 와 같은 규칙) */
export function cleanMarks(v: unknown): MatMarks {
const got = (v && typeof v === "object" ? v : {}) as Record<string, unknown>;
@@ -1,12 +1,14 @@
/* =============================================================================
* M02_MasterTemplete_Labor_MatMarks.ts
* 자재목록 세 칸(148-4) — 자재구분(일반 · 관급 · 사급) · 별도계상(별도계상 · 직접계산) · 운반비(포함 · 미포함)
* · 고칠 수 있으면 드롭다운(처음 값 빈 「—」 · 줄 누름과 따로) · 아니면 글 · 금액 무관
* 자재목록 세 칸(148-4) — 자재구분(관급 · 사급) · 별도계상(별도계상 · 직접계산) · 운반비(포함 · 미포함)
* · 고칠 수 있으면 드롭다운(줄 누름과 따로) · 아니면 글 · 금액 무관
* · 빈 칸 = 기본값 표시(162-1 · `MAT_MARK_DEFAULT` · 저장은 비움) · 별도계상이면 운반비 드롭다운 꺼짐(값 그대로)
* · 사용자 자재 = 줄 덧칸 · 로직 자재 = 로직 줄 `자재표시 {호표 차례: 세 칸}`(`Labor_Api` 정리 · 서버 `clean`)
* ========================================================================== */
import { createSelectField, el } from "@ui/ui_template_elements";
import {
MAT_MARK_DEFAULT,
MAT_MARKS,
type LaborRow,
type MatMarkKey,
@@ -21,7 +23,6 @@ const HEAD: Record<MatMarkKey, ComboTextKey> = {
};
/** 선택지 글(저장 값은 한국어 그대로) */
const WORD: Record<string, ComboTextKey> = {
일반: "Mark_General",
관급: "Mark_Gov",
사급: "Mark_Private",
별도계상: "Mark_Separate",
@@ -33,33 +34,36 @@ const KEYS = Object.keys(MAT_MARKS) as MatMarkKey[];
export const markHeads = (): string[] => KEYS.map((k) => tc(HEAD[k]));
/** 세 칸 — `set` 없으면 글만(빈 = 「—」) */
/** 세 칸 — `set` 없으면 글만(빈 = 기본값 글) */
export function markCells(
value: MatMarks,
set?: (key: MatMarkKey, v: string) => void,
): HTMLElement[] {
return KEYS.map((key) => {
const now = value[key] ?? "";
const picks: Partial<Record<MatMarkKey, HTMLSelectElement>> = {};
const cells = KEYS.map((key) => {
const now = value[key] ?? MAT_MARK_DEFAULT[key];
const attrs = { "data-label": tc(HEAD[key]), "data-mat-mark": key };
if (!set)
return el("td", {
className: "m02-labrow__mark",
text: now ? tc(WORD[now]) : "—",
attrs,
});
if (!set) return el("td", { className: "m02-labrow__mark", text: tc(WORD[now]), attrs });
const pick = createSelectField({
options: [
{ value: "", text: "—" },
...MAT_MARKS[key].map((v) => ({ value: v, text: tc(WORD[v]) })),
],
options: MAT_MARKS[key].map((v) => ({ value: v, text: tc(WORD[v]) })),
value: now,
compact: true,
ariaLabel: tc(HEAD[key]),
onChange: (v) => set(key, v),
onChange: (v) => {
set(key, v);
if (key === "별도계상") haulOff(v);
},
});
picks[key] = pick.select;
pick.root.addEventListener("click", (e) => e.stopPropagation()); // 줄 누름과 따로
return el("td", { className: "m02-labrow__mark", attrs, children: [pick.root] });
});
// 별도계상이면 운반비 꺼짐 — 값은 그대로
const haulOff = (sep: string) => {
if (picks.운반비) picks.운반비.disabled = sep === "별도계상";
};
haulOff(value.별도계상 ?? MAT_MARK_DEFAULT.별도계상);
return cells;
}
/** 한 칸 바꿈 — 빈 값 = 칸 지움 */
@@ -118,7 +118,6 @@ const TEXT = {
Mark_Kind: ["자재구분", "Material type"],
Mark_Separate: ["별도계상", "Separate"],
Mark_Haul: ["운반비", "Haulage"],
Mark_General: ["일반", "General"],
Mark_Gov: ["관급", "Gov-supplied"],
Mark_Private: ["사급", "Contractor-supplied"],
Mark_Direct: ["직접계산", "Direct"],