feat(736-1): 일위대가 [항목 추가] 하나 · 찾아 넣기 창 하나로 — 자재 추가 단추 걷음 · 엑셀 연결 · 별칭 장부 · 견본

- 일위대가표 · 구조물 조합 상세 [로직 추가] → [항목 추가] · [자재 추가] 걷음 · 재료 고름 = 자재 목록(로직 안 자재 · 이미 있는 자재 막힘 그대로)
- 찾아 넣기 창 탭 막기(kinds) · 찾기 칸 미리 넣기(query) · 재료 길(material)
- 불러오기 엑셀 연결 = 「다른 항목 검색…」 한 창(기초 · 재료) · 별칭 장부 = 기초 · 재료 둘
- 부품 모음 단추 견본에 갈래 색(옵션값 · 요소 연결) 칸
- 낡은 시험 고침: 175(머리 단추) · 721(부르는 곳) · 자재 개인(상세 단추) · 호표 화면(장부 단추 글) · 헬퍼 넷(Find_Pick 대역)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 16:50:34 +09:00
co-authored by Claude Sonnet 5.5
parent f82e64ce29
commit f97ea96c08
18 changed files with 152 additions and 79 deletions
@@ -60,13 +60,12 @@ export const CARD_HELP: Record<"structure" | "labor" | "area", CardHelp> = {
features: [
SAVE,
take("일위대가"),
["수정", "일위대가 상세를 엶 — 로직 추가 · 순서 바꾸기 · 다이어그램은 여기"],
["수정", "일위대가 상세를 엶 — 항목 추가 · 순서 바꾸기 · 다이어그램은 여기"],
clear("일위대가"),
["로직 줄 누름", "넣을 값 창 — 그 로직의 넣을 값 고침"],
["수량산출 칸", "수기 입력 · 면적산출 칸 고르기"],
["별칭 칸", "줄에 따로 이름 붙임"],
["줄 끝 버튼", "그 줄을 뺌"],
["자재 추가", "자재를 골라 자재목록에 넣음"],
FOLD,
],
steps: [
@@ -7,9 +7,11 @@
* · M02 단가산출 · 일위대가 = 위 층(마스터 › 회사 › 개인) 서고 목록(`/layers/{층}/templates`) · 고름 = 참조 {층 · 주인 · 키}
* · 인력 · 재료 고름 = `element` · 일위대가 고정 줄 꼴 = `fixedOf`(705-1 이음 노임 LB · 자재 MT · 수량 1 은 부르는 쪽) ·
* `element` 없으면 그 줄 회색 막힘(까닭 = 일위대가 표 개편 뒤 · 723)
* · 부르는 곳 = M02 단가산출 [항목 추가](`Sangeun_Pick`) · 일위대가 [로직 추가]
* · 부르는 곳 = M02 단가산출 [항목 추가](`Sangeun_Pick`) · 일위대가 [항목 추가]
* (`Labor_Pick` 이 `lib.sangeun` 이면 이 창) — B09 · 불러오기는 옛 창(`Labor_Pick`)
* · (727-1) 기초 = 로직마다 한 줄(`logicHeads` · 옵션은 넣은 뒤 고름 · 옛 창도 같이) · 서버 목록 그대로
* · (736-1) `kinds` = 탭 막기(자리마다 고를 수 있는 종류만 · 불러오기 엑셀 연결 · 별칭 장부) · `material` = 재료 고름을
* 일위대가 「자재 목록」 자리로(`element` 대신) · `query` = 찾기 칸 미리 넣기 · 부르는 곳 = 일위대가 [항목 추가]
* · (728-2) 돈 아닌 기초(결과단위가 「원」 아님 · ㎥/시간 등 21 개) = 목록에서 뺌
* ========================================================================== */
@@ -46,10 +48,19 @@ export interface M01Element {
값: number | null;
}
/** 고를 수 있는 종류 — logic = M01 단가산출 · 기초 로직 · labor = 인력 · material = 재료 · sk = M02 단가산출 · pre = 일위대가 */
export type FindKind = "logic" | "labor" | "material" | "sk" | "pre";
export interface FindPickOptions {
layer: Layer;
owner: string | null;
title?: string;
/** 736-1 탭 막기 — 이 종류만 보임(없으면 다 · 탭 하나만 남으면 탭 줄 감춤) */
kinds?: FindKind[];
/** 찾기 칸에 미리 넣을 글 */
query?: string;
/** 재료 고름 — 있으면 재료는 `element` 대신 이쪽(일위대가 「자재 목록」 자리) */
material?: (e: M01Element) => void;
/** M01 단가산출 로직 고름 */
logic: (x: LogicSummary) => void;
/** M02 단가산출 · 일위대가 고름(참조) */
@@ -117,7 +128,17 @@ interface Line {
}
export function openFindPicker(opt: FindPickOptions): void {
let tab: Tab = "m01";
const ok = (k: FindKind): boolean => !opt.kinds || opt.kinds.includes(k);
const allowed = (
[
["m01", ok("logic") || ok("labor") || ok("material")],
["sk", ok("sk")],
["pre", ok("pre")],
] as const
)
.filter(([, on]) => on)
.map(([k]) => k as Tab);
let tab: Tab = allowed[0] ?? "m01";
let close = (): void => {};
let logics: LogicSummary[] = [];
let std: StandardRow[] | null = null;
@@ -182,7 +203,7 @@ export function openFindPicker(opt: FindPickOptions): void {
단위: unitOf(m.단위 ?? ""),
값: Number.isFinite(Number(m.값)) && m.값 !== null ? Number(m.값) : null,
};
const put = opt.element;
const put = kind === "재료" && opt.material ? opt.material : opt.element;
return {
품명: m.이름,
규격: e.규격,
@@ -208,7 +229,11 @@ export function openFindPicker(opt: FindPickOptions): void {
function draw(): void {
const q = search.input.value.trim();
if (tab === "m01") {
shown = [...logicLines(q), ...elementLines("인력", people), ...elementLines("재료", goods)];
shown = [
...(ok("logic") ? logicLines(q) : []),
...(ok("labor") ? elementLines("인력", people) : []),
...(ok("material") ? elementLines("재료", goods) : []),
];
} else {
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
const kind = tab === "sk" ? L("M02_PickSangeun") : L("M02_PrecomboTitle");
@@ -231,7 +256,10 @@ export function openFindPicker(opt: FindPickOptions): void {
`/logic-lib/pick?kind=${kind}&q=${encodeURIComponent(q)}&limit=200`,
).then((d) => d.items);
try {
const [p, g] = await Promise.all([get("job"), q ? get("price") : Promise.resolve([])]);
const [p, g] = await Promise.all([
ok("labor") ? get("job") : Promise.resolve([]),
q && ok("material") ? get("price") : Promise.resolve([]),
]);
if (mine !== seq) return;
[people, goods] = [p, g];
} catch (error) {
@@ -252,7 +280,7 @@ export function openFindPicker(opt: FindPickOptions): void {
{ key: "m01", label: L("M02_FindM01") },
{ key: "sk", label: L("M02_PickSangeun") },
{ key: "pre", label: L("M02_PrecomboTitle") },
],
].filter((x) => allowed.includes(x.key as Tab)),
value: tab,
kind: "line",
onChange: (key) => {
@@ -272,10 +300,11 @@ export function openFindPicker(opt: FindPickOptions): void {
el("div", {
className: "m02-std-pick",
attrs: { "data-find-pick": "" },
children: [tabs.root, search.root, hint, table.root],
children: [...(allowed.length > 1 ? [tabs.root] : []), search.root, hint, table.root],
}),
),
}));
if (opt.query) search.input.value = opt.query;
search.input.focus();
draw();
@@ -292,7 +321,7 @@ export function openFindPicker(opt: FindPickOptions): void {
void upper(opt.layer, opt.owner)
.then((scopes) =>
Promise.all(
(["sk", "pre"] as const).map((t) =>
(["sk", "pre"] as const).filter(ok).map((t) =>
listLayerDocs(scopes, t === "sk" ? SANGEUN_KIND : "precombo").then((rows) => {
lists[t] = rows;
tabs.setBadge(t, rows.length);
@@ -5,7 +5,7 @@
* · 후보 묶음(358-1 · 숨김 없음) = 1위 + 판정 / 출처 같음 / 이름 닮은 로직(옛 324 다섯) / 숫자만 닮음(접힘 ·
* 뜻 낱말 안 겹치는 「후보」는 회색 끝) · 같은 이름이면 책(산림 · 건설) · 판정 꼬리표 · 근거 꼬리표(근거 맞음 초록)
* · 고름 = [후보](한 로직) · 또는 체크 + 몫 칸 → [일괄 적용(n)](조합) · 옵션은 부르는 쪽이 서버 `guess` 로 채움
* · [다른 로직 검색] = `openLogicPicker` 에 품명 첫 뜻 낱말 · [다른 자재 검색] = 자재 줄 · 재료비 있는 줄(358-2)
* · [다른 항목 검색](736-1) = 찾아 넣기 창(`Find_Pick`) 하나 · 품명 첫 뜻 낱말 · 기초 = 자재 아닌 줄 · 재료 = 자재 · 재료비 있는 줄(358-2)
* · 자재 줄(361-1) = 1위 / 전국(자리표 · 지역 고른 뒤 되돌림) / 지역 변형(접힘 · 하나 고르면 그 지역) / 그 밖
* 저장 안 함 — 고른 것은 부르는 쪽 캐시(확인 창)로만 감.
* ========================================================================== */
@@ -21,8 +21,8 @@ import type {
HopyoQty,
HopyoTarget,
} from "./M02_MasterTemplete_Hopyo_Api";
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
import { pickMaterial } from "./M02_Material_Pick";
import { getActiveLayer, getActiveOwner } from "./M02_MasterTemplete_Api_Fetch";
import { type FindKind, openFindPicker } from "./M02_MasterTemplete_Find_Pick";
import "./M02_MasterTemplete_Hopyo.css";
const MATERIAL = new Set(["자재", "별도계상"]);
@@ -103,7 +103,7 @@ export const qtyText = (q: HopyoQty | undefined): string =>
].join(" · ")
: "";
/** [다른 로직 검색] 미리 넣을 글 — 품명 괄호 밖 첫 뜻 낱말(358-1 · 조항 번호는 모든 낱말 맞춤이라 0건 위험) */
/** [다른 항목 검색] 미리 넣을 글 — 품명 괄호 밖 첫 뜻 낱말(358-1 · 조항 번호는 모든 낱말 맞춤이라 0건 위험) */
export function searchHint(line: Pick<HopyoLine, "이름">): string {
return /[가-힣A-Za-z]{2,}/.exec(line.이름.replace(/\([^()]*\)/g, " "))?.[0] ?? "";
}
@@ -294,28 +294,27 @@ export function openHopyoChoose(opt: ChooseOptions): void {
: []),
],
});
const findMaterial = createButton({
label: "다른 자재 검색…",
// 736-1 찾아 넣기 창 하나 — 이을 수 있는 종류만(자재 줄 = 재료 · 그 밖 = 기초 로직 · 재료비 있는 줄은 재료도)
const kinds: FindKind[] = [
...(material ? [] : (["logic"] as const)),
...(canPickMaterial(line) ? (["material"] as const) : []),
];
const find = createButton({
label: "다른 항목 검색…",
size: "sm",
onClick: () => {
close();
pickMaterial((x) => opt.done([{ 자재: x.코드, 배: 1 }]));
},
});
const findLogic = createButton({
label: "다른 로직 검색…",
size: "sm",
onClick: () => {
close();
openLogicPicker((x) => opt.done([{ 로직: x.키, 입력값: {}, 배: 1 }]), undefined, {
openFindPicker({
layer: getActiveLayer(),
owner: getActiveOwner(),
kinds,
query: searchHint(line),
logic: (x) => opt.done([{ 로직: x.키, 입력값: {}, 배: 1 }]),
material: (e) => opt.done([{ 자재: e.코드, 배: 1 }]),
});
},
});
const more = [
...(material ? [] : [findLogic]),
...(canPickMaterial(line) ? [findMaterial] : []),
];
const more = kinds.length ? [find] : [];
body.append(
...(head.childElementCount ? [head] : []),
note(
@@ -3,7 +3,7 @@
* 일위대가 실무자료 별칭 장부 화면(PLAN 338-3 · 설계 4-4) — 본 칸 · 좌측 판 [별칭 장부] 로 엶.
* · 표 = 엑셀 품명 · 규격 · 단위 | 갈래 | 우리 로직(여럿) · 옵션 · 배 · 몫 | 누가 · 때 · 처음 파일 | 층(위 층 회색 읽기만)
* · 뜻(359-4 · 머리 한 줄 · 좌측 단추 title) = 엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안
* · 할 일 = 찾기 · 갈래 거름 · 갈래 바꿈 · [로직 바꿈] · [로직 추가] · 배 · 몫 · [옵션] · [삭제] ·
* · 할 일 = 찾기 · 갈래 거름 · 갈래 바꿈 · [로직 바꿈] · [항목 추가] · 배 · 몫 · [옵션] · [삭제] ·
* 위 층 줄 [내 층 복사] · 갈래 글 = 확인 창 드롭다운과 같음(짝 · 로직없음 · 사용안함 · 저장 값 그대로)
* · 고친 것 = sessionStorage `m02-hopyo-ledger:{층}:{주인}` → [저장] 때만 `PUT`(서버가 짝을 다시 검사)
* · 안 둠 = 발주처 칸 · 자동 올림 · 자동 합침 · 쓰인 횟수 · 회사로 올리기.
@@ -23,7 +23,7 @@ import {
import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api";
import { dateTimeText } from "../common_util/common_util_time";
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
import "./M02_MasterTemplete_Hopyo.css";
export const LEDGER_TITLE = "별칭 장부";
@@ -212,13 +212,19 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
if (mine && r.갈래 === "짝")
pairs.push(
createButton({
label: "로직 추가",
label: "항목 추가",
variant: "ghost",
size: "sm",
onClick: () =>
openLogicPicker((x) =>
edit(r, { 짝: [...r.짝, { 로직: x.키, 입력값: {}, 배: 1, 몫: null }] }),
),
// 736-1 별칭 장부가 가리킬 수 있는 것 = 기초 로직 · 재료 둘(탭 막음)
openFindPicker({
layer: opt.scope.layer,
owner: opt.scope.owner,
kinds: ["logic", "material"],
logic: (x) => edit(r, { 짝: [...r.짝, { 로직: x.키, 입력값: {}, 배: 1, 몫: null }] }),
material: (e) =>
edit(r, { 짝: [...r.짝, { 자재: e.코드, 입력값: {}, 배: 1, 몫: null }] }),
}),
}),
);
return el("td", { className: "m02-hopyo__parts", children: pairs });
@@ -64,7 +64,6 @@ import { laborGrid } from "./M02_MasterTemplete_Labor_Grid";
import { tc } from "./M02_MasterTemplete_Labor_Text";
import { insideKeys, releaseTied } from "./M02_Labor_LogicMats_Model";
import { keepKnown } from "./M02_Labor_Values_Model";
import { addMaterialButton } from "./M02_Material_Pick";
import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";
import { mountLaborTable } from "./M02_MasterTemplete_Labor_Table";
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css";
@@ -79,8 +78,8 @@ export interface LaborDetailOptions {
readOnly: boolean;
/** 저장 안 한 고침이 생기거나 사라짐 */
onChanged: (dirty: boolean) => void;
/** 구조물 조합 카드 갈래(43-4) — 단가산출 로직 표 + 자재목록만 · 위로 · 아래로 · 로직 추가 ·
* 자재 추가 · 정보(단위 · 본뜬) · 다이어그램 없음 · 옵션 · 삭제 · 줄 · 수량 모달은 상세와 같음 */
/** 구조물 조합 카드 갈래(43-4) — 단가산출 로직 표 + 자재목록만 · 위로 · 아래로 · 항목 추가 ·
* 정보(단위 · 본뜬) · 다이어그램 없음 · 옵션 · 삭제 · 줄 · 수량 모달은 상세와 같음 */
card?: boolean;
/** 일위대가 사전조합 서고(50-3) — 사전조합 문서(`column` = 키) */
mode?: "precombo";
@@ -515,6 +514,20 @@ export function mountLaborDetail(
cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량: null });
changed();
},
/** 736-1 찾아 넣기 창에서 재료를 고름 — 「자재 목록」 줄 · 로직 안 자재 · 이미 있는 자재는 막음 */
material: (m: {
코드: string;
이름: string | null;
규격: string | null;
단위: string | null;
}) => {
const why = insideKeys((calc ?? last)?.줄).has(m.코드)
? "Labor_InLogicMat"
: cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat";
if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error");
cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null });
changed();
},
};
const addLogic = createButton({
label: tc("AddLogic"),
@@ -531,18 +544,6 @@ export function mountLaborDetail(
withHopyo(library),
),
});
const addMat = addMaterialButton(
tc("Labor_AddMat"),
(m) => {
const why = insideKeys((calc ?? last)?.줄).has(m.코드)
? "Labor_InLogicMat"
: cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat";
if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error");
cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null });
changed();
},
opt.layer === "personal" ? { owner: library.owner } : undefined, // 개인 층만 신규 자재(148-3)
);
// 단위 = 표준 단위 목록에서 고르기(지금 값이 목록에 없어도 선택지로 둠) · 저장은 [저장]
const unit = {
@@ -602,11 +603,11 @@ export function mountLaborDetail(
],
}),
]),
// 사전조합 = 같은 [로직 추가](51-11)
// 사전조합 = 같은 [항목 추가](51-11)
section(tc("Grid_Title"), null, gridHost),
...[
section(tc("Logics_Title"), opt.card ? null : addLogic, logicHost),
section(tc("Labor_Mats"), opt.card ? null : addMat, matHost),
section(tc("Labor_Mats"), null, matHost),
].filter(() => edit || !opt.card), // 읽기만 카드 = 격자만
...(opt.card ? [] : [dgBox]),
],
@@ -39,7 +39,13 @@ import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀
import "./M02_MasterTemplete_Labor.css";
import { openModal } from "@ui/ui_template_modal";
import { createText } from "@ui/ui_template_text";
import { fixedOf, logicHeads, openFindPicker, upper } from "./M02_MasterTemplete_Find_Pick";
import {
fixedOf,
logicHeads,
type M01Element,
openFindPicker,
upper,
} from "./M02_MasterTemplete_Find_Pick";
export { withHopyo } from "./M02_MasterTemplete_Hopyo_Pick";
@@ -78,6 +84,8 @@ export interface LibraryPick {
sangeun?: boolean;
/** 721-2 M01 인력 · 재료 → 일위대가 고정 줄(705-1 이음 꼴 · 담은로직 `{로직: "", 고정, 수량}`) */
fixed?: (fx: LaborFixed, name: string) => void;
/** 736-1 M01 재료 → 일위대가 「자재 목록」 줄(있으면 재료는 `fixed` 대신 이쪽 · [자재 추가] 단추 걷음) */
material?: (e: M01Element) => void;
}
const LAYER_TEXT = {
@@ -101,6 +109,7 @@ export function openLogicPicker(
logic: done,
ref: lib.pick,
element: fixed && ((e) => fixed(fixedOf(e), e.이름)),
material: lib.material,
});
}
const subs: SubBrief[] = [];
@@ -5,7 +5,7 @@
* · 옵션 값 = 규격 칸 · 「빈 값 n」 = 비고 칸 붉게(입력 빈 로직 줄)
* · 수량 칸 = 확인용 임시 입력(723-1 · 휘발 · 문서 · 저장 · dirty 안 건드림) → 그 줄 금액 · 합계 미리 보기
* (짝 셈 `Labor_Calc.settle` · 서버 안 부름) · 문서 안 줄 수량은 늘 null(쓰는 쪽 알약으로 이음)
* · 머리 단추 = [로직 추가] [자재 추가] · 줄 우클릭 · 줄 끝 ⋯ = 위로 · 아래로 · 별칭 · 삭제
* · 머리 단추 = [항목 추가](736-1 · 자재는 찾기 창에서 고름 → 자재 목록) · 줄 우클릭 · 줄 끝 ⋯ = 위로 · 아래로 · 별칭 · 삭제
* · 줄 누름 = 줄 모달(`Labor_RowModal` — 옵션 · 메모 · 켜쌓기율 · 로직 자재 바꾸기 · 세 칸 · 단가산출) ·
* 사전조합 참조 줄 = 서고 원본 모달(`Labor_RefEdit`) · 사용자 자재 줄 = 자재 모달(세 칸)
* · 금액 = 서버 calc(최신 응답만 · 저장 안 함) · [저장]은 UI_Main(서버가 수량 걷고 다시 셈)
@@ -41,7 +41,6 @@ import { openMatModal, openRowModal } from "./M02_MasterTemplete_Labor_RowModal"
import { tc } from "./M02_MasterTemplete_Labor_Text";
import { insideKeys } from "./M02_Labor_LogicMats_Model";
import { valueLine } from "./M02_Labor_Values_Model";
import { addMaterialButton } from "./M02_Material_Pick";
import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";
import { libMeta } from "./M02_MasterTemplete_Labor_LibMeta";
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css";
@@ -374,6 +373,20 @@ export function mountLaborTable(
cur.담은로직.push({ 로직: "", 고정: fx, 별칭: null, 메모: null, 입력값: {}, 수량: null });
changed();
},
/** 736-1 찾아 넣기 창에서 재료를 고름 — 「자재 목록」 줄 · 로직 안 자재 · 이미 있는 자재는 막음 */
material: (m: {
코드: string;
이름: string | null;
규격: string | null;
단위: string | null;
}) => {
const why = insideKeys((calc ?? last)?.줄).has(m.코드)
? "Labor_InLogicMat"
: cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat";
if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error");
cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null });
changed();
},
};
const addLogic = createButton({
label: tc("AddLogic"),
@@ -386,18 +399,6 @@ export function mountLaborTable(
void loadLogics().then(() => changed());
}, withHopyo(library)),
});
const addMat = addMaterialButton(
tc("Labor_AddMat"),
(m) => {
const why = insideKeys((calc ?? last)?.줄).has(m.코드)
? "Labor_InLogicMat"
: cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat";
if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error");
cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null });
changed();
},
opt.layer === "personal" ? { owner: library.owner } : undefined,
);
// 정보 칸 — 이름 · 구분 · 상세구분 · 단위 · 본뜬
const nameField = createInputField({ type: "text", label: tc("Lib_Name") });
@@ -461,7 +462,7 @@ export function mountLaborTable(
children: [
el("h3", { text: tc("Grid_Title") }),
...(edit
? [el("div", { className: "ui-btn-group", children: [addLogic, addMat] })]
? [el("div", { className: "ui-btn-group", children: [addLogic] })]
: []),
],
}),
@@ -50,7 +50,7 @@ const TEXT = {
"단가산출 기초 줄을 고르면 그 원문이 보임",
"Pick a logic row to see its source",
],
AddLogic: ["로직 추가", "Add logic"],
AddLogic: ["항목 추가", "Add item"],
NoLogic: ["단가산출 기초 없음", "No logics yet"],
Open: ["열기", "Open"],
Pick_Title: ["로직 고르기", "Pick a logic"],
@@ -10,6 +10,7 @@ const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} }
const STUB_FILES = [
"M02_MasterTemplete_Labor_Pick",
"M02_Material_Pick",
"M02_MasterTemplete_Find_Pick",
"M02_MasterTemplete_Labor_Api",
"M02_MasterTemplete_Labor_Input",
"M01_MasterData_UI_LogicLab_Source",
@@ -11,6 +11,7 @@ const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } };
const STUB_FILES = [
"M02_MasterTemplete_Labor_Pick",
"M02_Material_Pick",
"M02_MasterTemplete_Find_Pick",
"M02_MasterTemplete_Labor_Api",
"M02_MasterTemplete_Labor_Input",
"M01_MasterData_UI_LogicLab_Source",
@@ -12,6 +12,7 @@ const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } };
const STUB_FILES = [
"M02_MasterTemplete_Labor_Pick",
"M02_Material_Pick",
"M02_MasterTemplete_Find_Pick",
"M02_MasterTemplete_Labor_Api",
"M02_MasterTemplete_Labor_Input",
"M01_MasterData_UI_LogicLab_Source",
@@ -12,6 +12,7 @@ const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } };
const STUB_FILES = [
"M02_MasterTemplete_Labor_Pick",
"M02_Material_Pick",
"M02_MasterTemplete_Find_Pick",
"M02_MasterTemplete_Labor_Api",
"M02_MasterTemplete_Labor_Input",
"M01_MasterData_UI_LogicLab_Source",
+2 -2
View File
@@ -35,7 +35,7 @@ def _save(folder, doc):
def test_화면_서고_모드도_자재목록_세_칸_자재_저장(world): # noqa: F811
"""723-2 — 서고 = 일위대가표만(`Labor_Table`) · [자재 추가] 머리 · 세 칸 = 줄 모달(`Labor_RowModal`)."""
"""723-2 — 서고 = 일위대가표만(`Labor_Table`) · [항목 추가] 머리(736-1 · 자재 추가 단추 걷음) · 세 칸 = 줄 모달(`Labor_RowModal`)."""
read = lambda name: open(os.path.join(SRC, name), encoding="utf-8").read() # noqa: E731
detail, lib = (
read("M02_MasterTemplete_Labor_Detail.ts"),
@@ -43,7 +43,7 @@ def test_화면_서고_모드도_자재목록_세_칸_자재_저장(world): # n
)
modal = read("M02_MasterTemplete_Labor_RowModal.ts")
assert 'if (opt.mode === "precombo") return mountLaborTable(host, doc, opt);' in detail
assert 'className: "ui-btn-group", children: [addLogic, addMat]' in lib
assert 'className: "ui-btn-group", children: [addLogic]' in lib and "addMat" not in lib
assert "setLogicMark(row, at, key, v);" in modal and "setMark(o.mat, key, v);" in modal
assert "delete out.자재" not in lib and "delete out.열" in lib
+28 -3
View File
@@ -43,15 +43,16 @@ def test_721_2_부르는_곳_셋만_새_창():
assert "logicOnly" not in find
assert "openFindPicker({" in _text(M02 / "M02_MasterTemplete_Sangeun_Pick.ts")
assert "sangeun: true," in _text(M02 / "M02_MasterTemplete_Hopyo_Pick.ts") # 일위대가
# 옛 창 그대로 — B09 · 불러오기는 sangeun 없이 부름
# 옛 창 그대로 — B09 단가 연결은 sangeun 없이 부름(736-1 불러오기 · 별칭 장부는 찾아 넣기 창으로)
for f in (
ROOT / "B09_Estimation" / "B09_Estimation_UI_Link.ts",
ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Link.ts",
M02 / "M02_MasterTemplete_Hopyo_Choose.ts",
M02 / "M02_MasterTemplete_Hopyo_Ledger.ts",
):
text = _text(f)
assert "openLogicPicker(" in text and "sangeun:" not in text, f.name
for f in ("Hopyo_Choose", "Hopyo_Ledger"):
text = _text(M02 / f"M02_MasterTemplete_{f}.ts")
assert "openFindPicker({" in text and "openLogicPicker" not in text.split("*/", 1)[1], f
# 탭 셋 · 찾기 칸 하나 · 거름 고르기 없음 · M01 = 로직 · 인력 · 재료(기계 · 호표 없음)
for key in ('key: "m01"', 'key: "sk"', 'key: "pre"'):
assert key in find
@@ -105,3 +106,27 @@ def test_721_2_인력_재료_서버가_받아_셈():
assert sk["노무비"] == pytest.approx(wage) and sk["재료비"] > 0
assert got["반영합"]["계"] == pytest.approx(sk["계"])
assert [ln["이은"] for ln in got["줄"]] == [["노무비"], ["재료비"]]
def test_736_1_항목_추가_찾기_창_하나():
"""736-1 — [로직 추가] → [항목 추가] · [자재 추가] 걷음 · 재료 = 자재 목록 · 자리마다 탭 막음."""
locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts")
assert "[로직 추가]" not in locale and "기초는 [항목 추가]로" in locale
assert 'AddLogic: ["항목 추가", "Add item"]' in _text(M02 / "M02_MasterTemplete_Labor_Text.ts")
table = _text(M02 / "M02_MasterTemplete_Labor_Table.ts")
detail = _text(M02 / "M02_MasterTemplete_Labor_Detail.ts")
for text in (table, detail):
assert "addMaterialButton" not in text and "addMat" not in text
assert "material: (m: {" in text # 재료 고름 → 「자재 목록」
assert '"Labor_InLogicMat"' in text and '"Labor_AlreadyMat"' in text # 중복 검사 그대로
pick = _text(M02 / "M02_MasterTemplete_Labor_Pick.ts")
assert "material: lib.material," in pick
find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts")
assert 'kind === "재료" && opt.material ? opt.material : opt.element' in find
assert 'export type FindKind = "logic" | "labor" | "material" | "sk" | "pre";' in find
# 불러오기 엑셀 연결 = 기초(자재 아닌 줄) · 재료(자재 · 재료비 줄) · 별칭 장부 = 기초 · 재료 둘
choose = _text(M02 / "M02_MasterTemplete_Hopyo_Choose.ts")
assert "다른 로직 검색" not in choose and "다른 자재 검색" not in choose
assert 'material ? [] : (["logic"] as const)' in choose
assert 'canPickMaterial(line) ? (["material"] as const) : []' in choose
assert 'kinds: ["logic", "material"],' in _text(M02 / "M02_MasterTemplete_Hopyo_Ledger.ts")
+1 -1
View File
@@ -517,7 +517,7 @@ def test_명사형_단추와_별칭_장부_풀이_359():
assert hint in ledger and "note(`${LEDGER_HINT}" in ledger
side = _text("M02_MasterTemplete_Hopyo_Side.ts")
assert "title: LEDGER_HINT" in side
for label in ('"변경 취소"', '"로직 추가"', '"내 층 복사"', '"삭제"'):
for label in ('"변경 취소"', '"항목 추가"', '"내 층 복사"', '"삭제"'):
assert f"label: {label}" in ledger, label
assert '"우리에 없음": "로직없음"' in ledger and '"안 씀": "사용안함"' in ledger
mine = ("Hopyo_Side", "Hopyo_Sheet", "Hopyo_Check", "Hopyo_CheckRows", "Hopyo_Ledger")
+1 -1
View File
@@ -228,4 +228,4 @@ def test_화면_신규_자재는_머리_단추_개인_층만():
"`/layers/personal/materials" in pick and "키:" not in pick.split("JSON.stringify")[1][:200]
)
detail = open(os.path.join(here, "M02_MasterTemplete_Labor_Detail.ts"), encoding="utf-8").read()
assert 'opt.layer === "personal" ? { owner: library.owner } : undefined' in detail
assert "addMaterialButton" not in detail # 736-1 [자재 추가] 단추 걷음 — 재료는 찾아 넣기 창
+1 -1
View File
@@ -306,7 +306,7 @@ export const ui_locales_m2 = {
M02_GridLink: ["연결", "Link"],
M02_GridLinkTitle: ["안 이은 줄 연결", "Link unlinked rows"],
M02_GridLinkNoLogic: [
"고정 줄은 노임 · 자재 · 기계로 이음 — 기초는 [로직 추가]로 줄을 바꿈",
"고정 줄은 노임 · 자재 · 기계로 이음 — 기초는 [항목 추가]로 줄을 바꿈",
"Fixed rows link to labor, material or machine only",
],
M02_LibUnlinked: ["안 이은 줄", "Unlinked"],
+2 -2
View File
@@ -303,8 +303,8 @@ function tabsDemo(s: KitState): HTMLElement | null {
// prettier-ignore
export const KIT_PARTS: KitPart[] = [
// ── 552-2 기본 ─────────────────────────────────────────────
{ key: "button", name: "단추", group: "basic", kinds: "주 · 보조 · 고스트 글 · 위험 · 위험 고스트 · 유리 · 글 링크 · 여덟 단", file: "ui_template_button.ts",
demo: (s) => (s === "error" ? null : row(...([["저장", "filled"], ["취소", "ghost"], ["보기", "pill"], ["삭제", "danger"], ["초기화", "danger-ghost"], ["사용법", "link"]] as const).map(([label, variant]) => markState(createButton({ label, variant, size: 28, onClick: noop }), s)))),
{ key: "button", name: "단추", group: "basic", kinds: "주 · 보조 · 고스트 글 · 위험 · 위험 고스트 · 유리 · 글 링크 · 갈래 색(옵션값 · 요소 연결) · 여덟 단", file: "ui_template_button.ts",
demo: (s) => (s === "error" ? null : row(...([["저장", "filled"], ["취소", "ghost"], ["보기", "pill"], ["삭제", "danger"], ["초기화", "danger-ghost"], ["사용법", "link"]] as const).map(([label, variant]) => markState(createButton({ label, variant, size: 28, onClick: noop }), s)), ...([["옵션값", "option"], ["요소 연결", "link"]] as const).map(([label, tone]) => markState(createButton({ label, variant: "ghost", size: 20, tone, onClick: noop }), s)))),
sizes: (z) => createButton({ label: String(z), size: z, iconStart: "plus", onClick: noop }) },
{ key: "icon-button", name: "아이콘 단추", group: "basic", kinds: "고스트 · 선 · 채움 · 유리 · 켜짐(aria-pressed) · 글자 기호 · 여덟 단 · title 필수", file: "ui_template_button.ts",
demo: (s) => (s === "error" ? null : row(markState(createIconButton({ icon: "settings", title: "설정" }), s), markState(createIconButton({ icon: "refresh", title: "새로 고침", variant: "outline" }), s), markState(createIconButton({ icon: "plus", title: "추가", variant: "filled" }), s), markState(createIconButton({ icon: "eye", title: "보기", pressed: true }), s), markState(createIconButton({ glyph: "▲", title: "위로", variant: "outline" }), s), markState(createIconButton({ icon: "adjustments", title: "조정", variant: "glass" }), s))),