Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1
This commit is contained in:
@@ -360,3 +360,11 @@
|
||||
.m02-hopyo-side > .m02-side__row {
|
||||
margin-top: var(--spacing-12);
|
||||
}
|
||||
|
||||
/* 고름 대기 후보 줄 비교(746-2) — 고르기 칸 아래 · 후보 단추 줄 + 표 */
|
||||
.m02-hopyo__cmp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
@@ -234,8 +234,43 @@ export interface SangeunCand {
|
||||
글: string;
|
||||
/** 729-4 — 기준단위 ↔ 엑셀 단위 성격 다름 = 환산 계수 필요(가 1 = 나 몇) */
|
||||
계수?: { 가: string; 나: string; 갈래: string } | null;
|
||||
/** 746-1 — 그 기초로 견준 줄 비교(앞 셋) · null = 견줌 못함(단위 성격 다름) */
|
||||
줄비교?: SangeunCompare | null;
|
||||
}
|
||||
|
||||
/** 줄 비교 꼴(746-1) */
|
||||
export type CompareLook = "같음" | "품다름" | "엑셀만" | "기초만";
|
||||
|
||||
/** 고름 대기 후보 줄 비교(746-1) — 줄마다 엑셀 · 기초 1 단위 품 */
|
||||
export interface SangeunCompare {
|
||||
줄: {
|
||||
갈래: "인력" | "장비" | "자재";
|
||||
이름: string;
|
||||
규격?: string;
|
||||
단위?: string;
|
||||
엑셀: number | null;
|
||||
기초: number | null;
|
||||
꼴: CompareLook;
|
||||
}[];
|
||||
수: Record<CompareLook, number>;
|
||||
못옮김: SangeunLost[];
|
||||
환산?: unknown;
|
||||
}
|
||||
|
||||
/** M01 요소 후보(746-1) — 못옮김 · 안 이은 기계 줄 · 고르면 `{노임|기계|자재: 키}` 로 이음 */
|
||||
export interface ElemCand {
|
||||
키: string;
|
||||
갈래: "노임" | "기계" | "자재";
|
||||
이름: string;
|
||||
규격?: string;
|
||||
단위?: string;
|
||||
단가?: number | null;
|
||||
}
|
||||
|
||||
/** 요소 후보인가(기초 후보 `로직` 과 가름) */
|
||||
export const isElem = (c: unknown): c is ElemCand =>
|
||||
typeof c === "object" && c !== null && "키" in c && "갈래" in c;
|
||||
|
||||
/** 안 이은 줄(729-2) — 만들면 남을 엑셀 줄 · 단가산출 줄 후보 = 고름 대기 기초 후보 */
|
||||
export interface HopyoLeft {
|
||||
차례: number;
|
||||
@@ -245,7 +280,7 @@ export interface HopyoLeft {
|
||||
수량: number | null;
|
||||
갈래: string;
|
||||
까닭: string;
|
||||
후보: SangeunCand[];
|
||||
후보: (SangeunCand | ElemCand)[];
|
||||
/** 729-4 — 못옮김 남은 SK 를 쓰는 줄 = 그 블록 · SK · 못옮김(잇기 = [단가산출 만들기]) */
|
||||
단산?: number;
|
||||
SK?: string;
|
||||
@@ -261,7 +296,7 @@ export interface SangeunLost {
|
||||
수량?: number | null;
|
||||
갈래: "자재" | "기계" | "노임";
|
||||
글?: string;
|
||||
후보?: unknown[];
|
||||
후보?: ElemCand[];
|
||||
}
|
||||
|
||||
/** 못옮김 잇기 한 줄(729-4) — 이음 키(LB · EQ · MT) 또는 뺌 까닭 */
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Compare.ts
|
||||
* 불러오기 고름 창 — 후보 블록 줄 비교 · M01 요소 후보(PLAN 746-2 · 요구 585 · 서버 746-1 `줄비교` · `후보`) —
|
||||
* · `compareView` = 고름 대기 후보(앞 셋)마다 줄 비교 표(줄 · 엑셀 품 · 기초 품 · 꼴 배지) + 못옮김 수 · 후보 바꿈 단추
|
||||
* · `elemChoices` = 못옮김 · 안 이은 기계 줄의 M01 요소 후보 셋 단추(누르면 부르는 쪽이 이음)
|
||||
* 보이기만 — 판정 · 순위 · 미리 고름은 서버 그대로 · 고르기는 부르는 쪽.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { t as L, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createTable } from "@ui/ui_template_table";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import type {
|
||||
CompareLook,
|
||||
ElemCand,
|
||||
SangeunCand,
|
||||
SangeunCompare,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
|
||||
const LOOK: Record<CompareLook, { tone: BadgeTone; key: LocaleKey }> = {
|
||||
같음: { tone: "success", key: "M02_CmpSame" },
|
||||
품다름: { tone: "warning", key: "M02_CmpDiff" },
|
||||
엑셀만: { tone: "info", key: "M02_CmpExcel" },
|
||||
기초만: { tone: "neutral", key: "M02_CmpBase" },
|
||||
};
|
||||
const LOOKS = Object.keys(LOOK) as CompareLook[];
|
||||
const num = (n: number | null): string => (n === null ? "—" : String(Number(n.toPrecision(4))));
|
||||
|
||||
/** 선택 칸 글 뒤 요약 — 「같음 3 · 품 다름 1」 · 견줌 못함 = "" */
|
||||
export const compareBrief = (c: SangeunCompare | null | undefined): string =>
|
||||
c
|
||||
? LOOKS.filter((k) => c.수[k])
|
||||
.map((k) => `${L(LOOK[k].key)} ${c.수[k]}`)
|
||||
.join(" · ")
|
||||
: "";
|
||||
|
||||
/** 줄 비교 표 하나 */
|
||||
function compareTable(c: SangeunCompare): HTMLElement {
|
||||
const rows = c.줄.map((x) => [
|
||||
`${x.갈래} ${x.이름}${x.규격 ? ` ${x.규격}` : ""}`,
|
||||
`${num(x.엑셀)} ${x.단위 ?? ""}`.trim(),
|
||||
`${num(x.기초)} ${x.단위 ?? ""}`.trim(),
|
||||
createBadge(L(LOOK[x.꼴].key), LOOK[x.꼴].tone),
|
||||
]);
|
||||
return createTable(
|
||||
[
|
||||
{ label: L("M02_SangeunColName") },
|
||||
{ label: L("M02_CmpColExcel"), width: "112px", align: "end" },
|
||||
{ label: L("M02_CmpColBase"), width: "112px", align: "end" },
|
||||
{ label: L("M02_CmpColLook"), width: "84px" },
|
||||
],
|
||||
rows,
|
||||
{ density: "data", empty: L("M02_CmpNone") },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 고름 대기 후보 줄 비교 칸 — 앞 후보 줄 비교를 후보 단추로 바꿔 봄 · `show(로직)` 으로 밖(고르기 칸)이 바꿈.
|
||||
* 줄 비교 있는 후보가 하나도 없으면 null.
|
||||
*/
|
||||
export function compareView(
|
||||
cands: SangeunCand[],
|
||||
): { root: HTMLElement; show: (로직: string) => void } | null {
|
||||
const has = cands.filter((c) => c.줄비교);
|
||||
if (!has.length) return null;
|
||||
const root = el("div", { className: "m02-hopyo__cmp", attrs: { "data-cmp": "" } });
|
||||
const body = el("div");
|
||||
const seg = createSegment({
|
||||
options: has.map((c, i) => ({ value: c.로직, label: `${i + 1}. ${c.이름}`, title: c.이름 })),
|
||||
value: has[0]!.로직,
|
||||
size: 24,
|
||||
ariaLabel: L("M02_CmpTitle").replace("{name}", ""),
|
||||
onChange: (v) => paint(v),
|
||||
});
|
||||
const paint = (로직: string): void => {
|
||||
const c = has.find((x) => x.로직 === 로직) ?? has[0]!;
|
||||
const cmp = c.줄비교!;
|
||||
body.dataset.cmpOf = c.로직;
|
||||
const lost = cmp.못옮김.length;
|
||||
body.replaceChildren(
|
||||
createText(
|
||||
[
|
||||
L("M02_CmpTitle").replace("{name}", c.이름),
|
||||
compareBrief(cmp),
|
||||
lost ? L("M02_CmpLost").replace("{n}", String(lost)) : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
"muted",
|
||||
12,
|
||||
),
|
||||
compareTable(cmp),
|
||||
);
|
||||
};
|
||||
root.append(seg.root, body);
|
||||
paint(has[0]!.로직);
|
||||
return {
|
||||
root,
|
||||
show: (로직) => {
|
||||
if (!has.some((c) => c.로직 === 로직)) return;
|
||||
seg.set(로직);
|
||||
paint(로직);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 요소 후보 단추 줄(셋까지) — 누르면 `pick(후보)` */
|
||||
export function elemChoices(cands: ElemCand[], pick: (c: ElemCand) => void): HTMLElement | null {
|
||||
if (!cands.length) return null;
|
||||
const box = el("div", { className: "ui-toolbar", attrs: { "data-elem": "" } });
|
||||
box.title = L("M02_ElemTitle");
|
||||
for (const c of cands.slice(0, 3)) {
|
||||
const price = c.단가 ? ` · ${c.단가.toLocaleString()}` : "";
|
||||
const b = createButton({
|
||||
label: `${c.이름}${c.규격 ? ` ${c.규격}` : ""} [${c.단위 ?? ""}]${price}`,
|
||||
size: 24,
|
||||
variant: "ghost",
|
||||
onClick: () => pick(c),
|
||||
});
|
||||
b.dataset.elemKey = c.키;
|
||||
b.title = `${c.키} · ${c.갈래}`;
|
||||
box.append(b);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
@@ -31,8 +31,11 @@ import {
|
||||
type HopyoPart,
|
||||
type HopyoScope,
|
||||
type LinkBase,
|
||||
type SangeunCand,
|
||||
isElem,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { bodyOf, draftKey, loadDraft, saveDraft, toTarget } from "./M02_MasterTemplete_Hopyo_Check";
|
||||
import { elemChoices } from "./M02_MasterTemplete_Hopyo_Compare";
|
||||
import { kindsOf, openHopyoChoose } from "./M02_MasterTemplete_Hopyo_Choose";
|
||||
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
||||
import { openLost } from "./M02_MasterTemplete_Hopyo_Lost";
|
||||
@@ -187,10 +190,11 @@ export function openLeft(
|
||||
|
||||
const actions = (no: number, row: HopyoLeft, line: HopyoLine | undefined): HTMLElement => {
|
||||
const box = el("div", { className: "ui-toolbar" });
|
||||
const bases = row.후보.filter((c): c is SangeunCand => !isElem(c));
|
||||
const block = (line as { 참조?: { 종류?: string; 번?: number } } | undefined)?.참조;
|
||||
if (row.후보.length && block?.종류 === "단산" && block.번 !== undefined) {
|
||||
if (bases.length && block?.종류 === "단산" && block.번 !== undefined) {
|
||||
const pick = createSelectField({
|
||||
options: row.후보.map((c) => ({
|
||||
options: bases.map((c) => ({
|
||||
value: c.로직,
|
||||
text: `${c.이름} [${c.단위}] · ${c.판정}${c.글 && c.글 !== c.판정 ? ` · ${c.글}` : ""}`,
|
||||
})),
|
||||
@@ -217,6 +221,8 @@ export function openLeft(
|
||||
lost.dataset.leftLost = String(row.단산);
|
||||
box.append(lost);
|
||||
}
|
||||
const elems = elemChoices(row.후보.filter(isElem), (c) => line && link(no, row.차례, { [c.갈래]: c.키 }, c.이름)); // prettier-ignore
|
||||
if (elems) box.append(elems);
|
||||
if (row.갈래 === "%" && line) {
|
||||
const base = createSelectField({
|
||||
options: [{ value: "", text: L("M02_LeftBase") }].concat(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* 바로 못 옮긴 엑셀 줄(노임 · 기계 · 자재)을 줄마다 M01 요소로 이음 → SK 더한줄(엑셀 수량) · [뺌] = 놓음(까닭 남김).
|
||||
* · 고름 = 캐시 `m02-hopyo-lost:{층}:{주인}:{키}:{번}` `{id: 잇기}` — 서버는 부를 때마다 그 블록 잇기 전부로 다시 셈
|
||||
* (앞 잇기 안 남음) → [잇기 만들기] 는 캐시 모두를 보냄 · 바탕 = 그 SK 문서 바탕(로직 · 입력값 · 환산 그대로)
|
||||
* · 줄마다 M01 요소 후보 셋 단추(746-2 · 서버 `후보`) — 누르면 그 줄에 이음 고름(찾기와 같은 길)
|
||||
* · 남은 못옮김 = 서버 답 그대로 표 · 0 이면 그 SK 를 쓰는 일위대가 줄이 이어짐(남으면 만들기 409)
|
||||
* 계산은 서버 — 화면은 고름 · 보이기만 · 저장은 [잇기 만들기] 누를 때만.
|
||||
* ========================================================================== */
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
type SangeunLink,
|
||||
type SangeunLost,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { elemChoices } from "./M02_MasterTemplete_Hopyo_Compare";
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const NONE = "우리에 없음";
|
||||
@@ -150,6 +152,7 @@ export function openLost(opt: LostOptions): void {
|
||||
x.규격 ?? "",
|
||||
`${x.수량 ?? ""} ${x.단위 ?? ""}`.trim(),
|
||||
x.글 ?? "",
|
||||
elemChoices(x.후보 ?? [], (c) => put(x.id, { id: x.id, [kind]: c.키 })) ?? "",
|
||||
linkText(all[x.id]),
|
||||
el("div", { className: "ui-toolbar", children: [find, drop, ...(all[x.id] ? [clear] : [])] }), // prettier-ignore
|
||||
];
|
||||
@@ -162,6 +165,7 @@ export function openLost(opt: LostOptions): void {
|
||||
{ label: L("M02_LeftSpec") },
|
||||
{ label: L("M02_SangeunColQty"), width: "90px" },
|
||||
{ label: L("M02_LeftWhy") },
|
||||
{ label: L("M02_ElemTitle") },
|
||||
{ label: L("M02_LeftPicked") },
|
||||
{ label: L("M02_LeftPick"), width: "220px" },
|
||||
],
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* · 만든 줄 표 = 블록 · 이름 · 키 · 갈래(기초 · SK 차이 있음) · 이미 만든 블록은 「그대로」 수만
|
||||
* · 고름 대기 표 = 블록마다 후보(판정 · 글) 고르기 칸 → [고른 기초로 만들기](바탕 = 고른 기초 · 엑셀 품 견줘 SK) ·
|
||||
* 단위 성격 다른 후보(729-4 `계수`) = 계수 칸(가 1 = 나 몇) 함께 → `환산 {계수}`
|
||||
* 후보마다 줄 비교 표(746-2 · `Hopyo_Compare` · 서버 `줄비교`) — 고르기 칸 아래 · 후보 단추로 바꿔 봄
|
||||
* · 못옮김(729-4) = 만든 줄마다 수 · [잇기] = 못옮김 잇기 창(`Hopyo_Lost`) · 만든 줄은 이 칸이 모아 둠(누를 때마다 더함) ·
|
||||
* 이미 만든 SK 의 남은 못옮김(729-5 `남은못옮김`)도 같은 표
|
||||
* · 같은 내용(733-1) = 갈래 칸 꼬리표 「같은 것 씀」(이 층 같은 SK 를 씀) · 「위 층에 같은 것 — 연결 권함」(회사 · 마스터 · 알림만) ·
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
type SangeunBuilt,
|
||||
type SangeunPick,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { compareBrief, compareView } from "./M02_MasterTemplete_Hopyo_Compare";
|
||||
import { openLost } from "./M02_MasterTemplete_Hopyo_Lost";
|
||||
|
||||
type Made = SangeunBuilt["만듦"][number];
|
||||
@@ -154,12 +156,15 @@ export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement
|
||||
{ label: L("M02_SangeunColName") },
|
||||
{ label: L("M02_SangeunColUnit"), width: "64px" },
|
||||
{ label: L("M02_LeftWhy") },
|
||||
{ label: L("M02_LeftBase"), width: "40%" },
|
||||
{ label: L("M02_LeftBase"), width: "62%" },
|
||||
],
|
||||
got.고름대기.map((x) => {
|
||||
const pick = createSelectField({
|
||||
options: [{ value: "", text: L("M02_WizSkNoPick") }].concat(
|
||||
x.후보.map((c) => ({ value: c.로직, text: candLabel(c) })),
|
||||
x.후보.map((c) => ({
|
||||
value: c.로직,
|
||||
text: candLabel(c) + (c.줄비교 ? ` · ${compareBrief(c.줄비교)}` : ""),
|
||||
})),
|
||||
),
|
||||
compact: true,
|
||||
size: 24,
|
||||
@@ -175,10 +180,14 @@ export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement
|
||||
coef.root.hidden = !c;
|
||||
coef.input.title = c ? `${c.가} 1 = ${c.나} ?` : "";
|
||||
};
|
||||
pick.select.addEventListener("change", need);
|
||||
const cmp = compareView(x.후보);
|
||||
pick.select.addEventListener("change", () => {
|
||||
need();
|
||||
cmp?.show(pick.select.value);
|
||||
});
|
||||
need();
|
||||
coefs.set(x.번, coef.input);
|
||||
return [`단산${x.번}`, x.이름, x.단위 ?? "", x.까닭, el("div", { className: "ui-toolbar", children: [pick.root, coef.root] })]; // prettier-ignore
|
||||
return [`단산${x.번}`, x.이름, x.단위 ?? "", x.까닭, el("div", { children: [el("div", { className: "ui-toolbar", children: [pick.root, coef.root] }), ...(cmp ? [cmp.root] : [])] })]; // prettier-ignore
|
||||
}),
|
||||
{ density: "data", empty: L("M02_WizSkNoWait") },
|
||||
);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""746-2 지킴 — 불러오기 고르기 창 후보 블록 줄 비교 · 요소 후보 화면(요구 585).
|
||||
|
||||
서버 없이 소스로 잡음: 고름 대기 칸이 줄 비교 표를 붙임 · 못옮김 창 · 안 이은 줄 창이 요소 후보 단추를 붙임 ·
|
||||
안 이은 줄 창의 기초 후보 칸에 요소 후보(`키`)가 섞여 들어가지 않음 · 서버 응답 칸 이름 · 부품 상자만 · 700 줄.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
|
||||
|
||||
|
||||
def _src(name: str) -> str:
|
||||
return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_고름_대기_칸이_줄_비교_표를_붙임():
|
||||
sk = _src("WizardSk")
|
||||
assert "compareView(x.후보)" in sk and "cmp?.show(pick.select.value)" in sk
|
||||
assert "compareBrief(c.줄비교)" in sk # 선택 칸 글에 같음 · 품다름 수
|
||||
|
||||
|
||||
def test_못옮김_창_안_이은_줄_창이_요소_후보_단추를_붙임():
|
||||
assert "elemChoices(x.후보" in _src("Lost")
|
||||
left = _src("Left")
|
||||
assert "elemChoices(row.후보.filter(isElem)" in left
|
||||
assert (
|
||||
"row.후보.filter((c): c is SangeunCand => !isElem(c))" in left
|
||||
) # 기초 후보 칸에는 요소 후보 안 듦
|
||||
|
||||
|
||||
def test_서버_응답_칸_이름_그대로():
|
||||
api = _src("Api")
|
||||
for name in (
|
||||
"줄비교?: SangeunCompare | null",
|
||||
"엑셀: number | null",
|
||||
"기초: number | null",
|
||||
"못옮김: SangeunLost[]",
|
||||
):
|
||||
assert name in api
|
||||
assert 'CompareLook = "같음" | "품다름" | "엑셀만" | "기초만"' in api
|
||||
|
||||
|
||||
def test_부품_상자만_날_요소_없음_700줄_안():
|
||||
src = _src("Compare")
|
||||
assert not re.search(r'createElement\("(button|input)"|<button|<input', src)
|
||||
for name in ("Compare", "WizardSk", "Lost", "Left"):
|
||||
assert len(_src(name).splitlines()) <= 700
|
||||
@@ -335,4 +335,15 @@ export const ui_locales_m2 = {
|
||||
"Not in M01 — kept out of the SK with a note",
|
||||
],
|
||||
M02_LostClear: ["고름 지움", "Clear"],
|
||||
M02_CmpTitle: ["줄 비교 — {name}", "Line compare — {name}"],
|
||||
M02_CmpNone: ["견줄 수 없음(단위 성격 다름)", "Cannot compare (units differ)"],
|
||||
M02_CmpSame: ["같음", "Same"],
|
||||
M02_CmpDiff: ["품 다름", "Qty differs"],
|
||||
M02_CmpExcel: ["엑셀만", "Excel only"],
|
||||
M02_CmpBase: ["기초만", "Base only"],
|
||||
M02_CmpColExcel: ["엑셀 품", "Excel qty"],
|
||||
M02_CmpColBase: ["기초 품", "Base qty"],
|
||||
M02_CmpColLook: ["꼴", "Result"],
|
||||
M02_CmpLost: ["못옮김 {n}", "{n} unmoved"],
|
||||
M02_ElemTitle: ["M01 요소 후보", "M01 element candidates"],
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user