Merge remote-tracking branch 'origin/dev' into sub_laptop_2
This commit is contained in:
@@ -53,9 +53,12 @@ export interface EasySaved {
|
||||
row: LogicRow;
|
||||
}
|
||||
|
||||
/** 쓰는 곳 한 줄 — 일위대가 조합 · 이 로직을 부르는 다른 로직 */
|
||||
/** 쓰는 곳 한 줄 — 일위대가 조합 · 이 로직을 부르는 다른 로직
|
||||
* `where` = 회사 · 개인 층 조합의 자리 글(「회사 · 산림조합」) · 마스터 층 · 로직은 없음
|
||||
* 층을 못 읽었으면 `key` "" · `name` = 「일부를 못 읽음」 한 줄(지우기 전에 확인하게) */
|
||||
export interface EasyUse {
|
||||
kind: "combo" | "logic";
|
||||
where?: string;
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -158,6 +161,14 @@ const TEXT = {
|
||||
"이 기계는 시간당 값을 셀 자료가 없음 — 다른 기계를 골라 주세요",
|
||||
"No hourly cost data for this machine — pick another",
|
||||
],
|
||||
AtCompany: ["회사 · {name}", "Company · {name}"],
|
||||
AtPerson: ["개인 · {name}", "Personal · {name}"],
|
||||
MyCompany: ["내 회사", "My company"],
|
||||
Me: ["나", "Me"],
|
||||
Unseen: [
|
||||
"회사 · 개인 조합 일부를 못 읽음 — 지우기 전에 확인 필요",
|
||||
"Some company or personal combos could not be read — check before deleting",
|
||||
],
|
||||
} as const satisfies Record<string, readonly [string, string]>;
|
||||
|
||||
type SayKey = keyof typeof TEXT;
|
||||
@@ -256,6 +267,26 @@ export function machineCall(m: MachineLogic, code: string): EasyMachineElement {
|
||||
const rest = needInputs.map((i) => `, ${quote(i.이름)}=${quote(i.이름)}`).join("");
|
||||
return { element: `로직(${m.key}, ${MACHINE}='${code}'${rest})`, needInputs };
|
||||
}
|
||||
|
||||
/** 층 조합 줄 통째(`담은로직` 포함) */
|
||||
export interface LayerCombo {
|
||||
키: string;
|
||||
이름?: string;
|
||||
담은로직?: { 로직?: string }[];
|
||||
}
|
||||
|
||||
/** 이 로직을 담은 층 조합 — `layer` = company · personal · `name` = 층 주인 이름 */
|
||||
export function holding(
|
||||
combos: LayerCombo[],
|
||||
key: string,
|
||||
layer: "company" | "personal",
|
||||
name: string,
|
||||
): EasyUse[] {
|
||||
const where = say(layer === "company" ? "AtCompany" : "AtPerson", { name });
|
||||
return combos
|
||||
.filter((c) => (c.담은로직 ?? []).some((one) => one?.로직 === key))
|
||||
.map((c): EasyUse => ({ kind: "combo", where, key: c.키, name: c.이름 ?? "" }));
|
||||
}
|
||||
/* ── 순수 끝 ── */
|
||||
|
||||
const OWN = "로직_자체.json"; // 내가 만든 것 한 자리(서버 `Store_Make.OWN`)
|
||||
@@ -263,10 +294,15 @@ const OWNER = "현장";
|
||||
const CALC_WAIT_MS = 300;
|
||||
const MACHINE_FILE = "건설기계"; // 기계 시간당 경비 로직이 든 장 파일(「08장 건설기계」)
|
||||
|
||||
async function request<T>(path: string, body?: unknown): Promise<T> {
|
||||
/** `/api/m01` 길 · M02 층 조합 읽기는 `m02` */
|
||||
const request = <T>(path: string, body?: unknown): Promise<T> =>
|
||||
send<T>(`${API_BASE_URL}/m01${path}`, body);
|
||||
const m02 = <T>(path: string): Promise<T> => send<T>(`${API_BASE_URL}/m02${path}`);
|
||||
|
||||
async function send<T>(url: string, body?: unknown): Promise<T> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${API_BASE_URL}/m01${path}`, {
|
||||
response = await fetch(url, {
|
||||
method: body === undefined ? "GET" : "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -385,13 +421,15 @@ export async function deleteEasy(key: string, version: string): Promise<{ versio
|
||||
return { version: got.version };
|
||||
}
|
||||
|
||||
/** 쓰는 곳 — 담은 일위대가 조합(마스터 층) + 이 로직을 부르는 내가 만든 것.
|
||||
/** 쓰는 곳 — 담은 일위대가 조합(마스터 · 회사 · 개인 층) + 이 로직을 부르는 내가 만든 것.
|
||||
* 지우기 누를 때 부름(무거움) · 서버 지우기 검사는 마스터 층 조합만 막음.
|
||||
* ponytail: 부르는 로직은 내가 만든 것만 하나씩 받아 훑음(품셈 기본은 내 것을 안 부름) —
|
||||
* 내 것이 수백이 되면 서버에 「부르는 로직」 길을 더할 것 */
|
||||
export async function usesOf(key: string): Promise<EasyUse[]> {
|
||||
const [combos, mine] = await Promise.all([
|
||||
const [combos, mine, layered] = await Promise.all([
|
||||
request<{ combos: { 키: string; 이름: string }[] }>(`/logic/combos?${query({ key })}`),
|
||||
request<{ logics: Summary[] }>(`/logics?${query({ sub: "자체" })}`),
|
||||
layerUses(key),
|
||||
]);
|
||||
const call = `로직(${key}`;
|
||||
const others = mine.logics.filter((s) => s.키 !== key);
|
||||
@@ -401,10 +439,73 @@ export async function usesOf(key: string): Promise<EasyUse[]> {
|
||||
.map((one): EasyUse => ({ kind: "logic", key: one.logic.키, name: one.logic.이름 }));
|
||||
return [
|
||||
...combos.combos.map((c): EasyUse => ({ kind: "combo", key: c.키, name: c.이름 })),
|
||||
...layered,
|
||||
...callers,
|
||||
];
|
||||
}
|
||||
|
||||
interface LayerTarget {
|
||||
layer: "company" | "personal";
|
||||
owner: string;
|
||||
name: string;
|
||||
rows?: { 키: string }[];
|
||||
}
|
||||
|
||||
/** 회사 · 개인 층 조합 가운데 이 로직을 담은 것 — M02 층 길 읽기만(시스템 관리자는 모든 층).
|
||||
* 내 층 = `/combos/all` · 남의 회사 = `/layers/company/owners` · 회사마다 사람 = `/layers/personal/owners`.
|
||||
* 못 읽은 층이 있으면 「일부를 못 읽음」 한 줄을 더함.
|
||||
* ponytail: 층마다 조합 하나씩 받아 훑음 — 조합이 수천이면 서버에 「이 로직을 담은 층 조합」 길을 더할 것 */
|
||||
async function layerUses(key: string): Promise<EasyUse[]> {
|
||||
let missed = false;
|
||||
const soft = <T>(p: Promise<T>, empty: T): Promise<T> =>
|
||||
p.catch(() => {
|
||||
missed = true;
|
||||
return empty;
|
||||
});
|
||||
type Own = { layer: string; owner: string | null; 줄: { 키: string }[] };
|
||||
type Owner = { owner: string; name: string };
|
||||
const own = await soft(m02<Own[]>("/combos/all"), []);
|
||||
const targets: LayerTarget[] = [];
|
||||
let myCompany = "";
|
||||
for (const one of own) {
|
||||
if (one.layer === "system" || !one.owner) continue;
|
||||
const company = one.layer === "company";
|
||||
if (company) myCompany = one.owner;
|
||||
targets.push({
|
||||
layer: company ? "company" : "personal",
|
||||
owner: one.owner,
|
||||
name: say(company ? "MyCompany" : "Me"),
|
||||
rows: one.줄,
|
||||
});
|
||||
}
|
||||
const companies = await soft(m02<Owner[]>("/layers/company/owners"), []);
|
||||
for (const c of companies) targets.push({ layer: "company", ...c });
|
||||
const people = await Promise.all(
|
||||
[myCompany, ...companies.map((c) => c.owner)]
|
||||
.filter(Boolean)
|
||||
.map((c) => soft(m02<Owner[]>(`/layers/personal/owners?${query({ company: c })}`), [])),
|
||||
);
|
||||
for (const p of people.flat()) targets.push({ layer: "personal", ...p });
|
||||
|
||||
const found = await Promise.all(
|
||||
targets.map(async (t) => {
|
||||
const at = `/layers/${t.layer}/combos`;
|
||||
const own = `?${query({ owner: t.owner })}`;
|
||||
const rows = t.rows ?? (await soft(m02<{ 줄: { 키: string }[] }>(at + own), { 줄: [] })).줄;
|
||||
const combos = await Promise.all(
|
||||
rows.map((r) =>
|
||||
soft(m02<{ combo: LayerCombo }>(`${at}/${encodeURIComponent(r.키)}${own}`), null),
|
||||
),
|
||||
);
|
||||
const full = combos.flatMap((x) => (x ? [x.combo] : []));
|
||||
return holding(full, key, t.layer, t.name);
|
||||
}),
|
||||
);
|
||||
const out = found.flat();
|
||||
if (missed) out.push({ kind: "combo", key: "", name: say("Unseen") });
|
||||
return out;
|
||||
}
|
||||
|
||||
let family: Promise<MachineLogic[]> | null = null;
|
||||
|
||||
/** 기계 시간당 경비 로직 묶음 — 한 번 받아 둠(실패하면 다음에 다시) */
|
||||
|
||||
@@ -124,6 +124,22 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.m01-easytable__hint {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
margin: 0;
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border-radius: var(--radius-lg);
|
||||
background: color-mix(in srgb, var(--color-accent) 10%, var(--color-canvas));
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.m01-easytable__hint:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m01-easytable__warn {
|
||||
margin: 0;
|
||||
color: var(--color-danger);
|
||||
|
||||
@@ -45,6 +45,10 @@ const TEXT = {
|
||||
Total: ["합계", "Total"],
|
||||
Wait: ["계산 중", "Calculating"],
|
||||
Stopped: ["계산 멈춤", "Calculation stopped"],
|
||||
DesignHint: [
|
||||
"이 값은 설계 때 정함 — 늘 이 줄로 하려면",
|
||||
"This value is set at design — to always use this row,",
|
||||
],
|
||||
Mismatch: [
|
||||
"고른 줄이 이 조건으로는 안 걸림 — 조건을 다시 보세요",
|
||||
"The picked row isn't matched by these conditions",
|
||||
@@ -351,6 +355,8 @@ interface CallState {
|
||||
picked: number | null;
|
||||
/** 「설계 때 정함」 조건을 고른 줄 값으로 미리 봄 */
|
||||
tryValues: Record<string, unknown>;
|
||||
/** 마지막으로 누른 줄이 미리 보기만 한 「설계 때 정함」 조건 */
|
||||
designHint: string[];
|
||||
}
|
||||
|
||||
const showCell = (v: Cell): string => {
|
||||
@@ -367,7 +373,7 @@ function startState(call: FindCall, doc: EasyDoc): CallState {
|
||||
const mode: Mode = names.has(a.rhs) ? "design" : lit !== null ? "fix" : "asis";
|
||||
return { key: a.key, orig: a.rhs, mode, input, fix: lit };
|
||||
});
|
||||
return { conds, col: call.col, picked: null, tryValues: {} };
|
||||
return { conds, col: call.col, picked: null, tryValues: {}, designHint: [] };
|
||||
}
|
||||
|
||||
function stateArgs(st: CallState, table: EasyTable | null): FindArg[] | null {
|
||||
@@ -465,6 +471,7 @@ async function mountTable(
|
||||
const grid = el("div", { className: "m01-easytable__grid" });
|
||||
const money = el("div", { className: "m01-easytable__money" });
|
||||
const warn = el("p", { className: "m01-easytable__warn" });
|
||||
const hint = el("p", { className: "m01-easytable__hint" });
|
||||
const use = createButton({ label: tt("Use"), onClick: () => finish(newFormula()) });
|
||||
const cancel = createButton({
|
||||
label: tt("Cancel"),
|
||||
@@ -491,6 +498,7 @@ async function mountTable(
|
||||
const st = states[now];
|
||||
const row = table?.줄[idx];
|
||||
if (!table || !row) return;
|
||||
st.designHint = [];
|
||||
for (const key of Object.keys(table.조건)) {
|
||||
if (row[key] === undefined) continue;
|
||||
let c = st.conds.find((x) => x.key === key);
|
||||
@@ -504,6 +512,7 @@ async function mountTable(
|
||||
? rangeNumber(row[key] as [number | null, number | null], table.범위규칙 ?? "")
|
||||
: row[key];
|
||||
st.tryValues[c.input] = n;
|
||||
st.designHint.push(key);
|
||||
} else {
|
||||
c.mode = "fix";
|
||||
c.fix = row[key];
|
||||
@@ -521,9 +530,36 @@ async function mountTable(
|
||||
c.fix = row?.[c.key] ?? (c.input ? (inputs()[c.input] as Cell) : null);
|
||||
}
|
||||
c.mode = mode;
|
||||
states[now].designHint = states[now].designHint.filter((k) => k !== c.key);
|
||||
void refresh();
|
||||
};
|
||||
|
||||
/** 누른 줄의 「설계 때 정함」 조건을 한 번에 그 줄 값으로 고정 */
|
||||
const fixPicked = (): void => {
|
||||
const st = states[now];
|
||||
const row = st.picked === null ? null : tbl[now]?.줄[st.picked];
|
||||
if (!row) return;
|
||||
for (const c of st.conds)
|
||||
if (st.designHint.includes(c.key)) {
|
||||
c.mode = "fix";
|
||||
c.fix = row[c.key];
|
||||
}
|
||||
st.designHint = [];
|
||||
void refresh();
|
||||
};
|
||||
|
||||
const paintHint = (): void => {
|
||||
const st = states[now];
|
||||
hint.replaceChildren(
|
||||
...(st.picked !== null && st.designHint.length
|
||||
? [
|
||||
el("span", { text: `${tt("DesignHint")} ` }),
|
||||
createButton({ label: tt("Fix"), size: "sm", onClick: fixPicked }),
|
||||
]
|
||||
: []),
|
||||
);
|
||||
};
|
||||
|
||||
const paintConds = (): void => {
|
||||
const st = states[now];
|
||||
condBox.replaceChildren(
|
||||
@@ -579,10 +615,7 @@ async function mountTable(
|
||||
const th = (text: string, cls = ""): HTMLElement => el("th", { className: cls, text });
|
||||
const rows = table.줄.map((row, idx) => {
|
||||
const tr = el("tr", {
|
||||
className: [
|
||||
idx === hit && !busy ? "is-hit" : "",
|
||||
idx === st.picked ? "is-picked" : "",
|
||||
].join(" "),
|
||||
className: `${idx === hit && !busy ? "is-hit" : ""} ${idx === st.picked ? "is-picked" : ""}`,
|
||||
children: [
|
||||
...conds.map((k) => el("td", { text: showCell(row[k]) })),
|
||||
...vals.map((k) =>
|
||||
@@ -600,28 +633,13 @@ async function mountTable(
|
||||
});
|
||||
return tr;
|
||||
});
|
||||
grid.replaceChildren(
|
||||
el("table", {
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: [
|
||||
...conds.map((k) => th(k)),
|
||||
...vals.map((k) =>
|
||||
th(
|
||||
`${k}${table.값칸[k] ? ` (${table.값칸[k]})` : ""}`,
|
||||
k === st.col ? "is-col" : "",
|
||||
),
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
el("tbody", { children: rows }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
const unit = (k: string): string => (table.값칸[k] ? `${k} (${table.값칸[k]})` : k);
|
||||
const top = [
|
||||
...conds.map((k) => th(k)),
|
||||
...vals.map((k) => th(unit(k), k === st.col ? "is-col" : "")),
|
||||
];
|
||||
const thead = el("thead", { children: [el("tr", { children: top })] });
|
||||
grid.replaceChildren(el("table", { children: [thead, el("tbody", { children: rows })] }));
|
||||
};
|
||||
|
||||
const paint = (): void => {
|
||||
@@ -646,6 +664,7 @@ async function mountTable(
|
||||
el("span", { className: "m01-easytable__note", text: tt("Hint") }),
|
||||
);
|
||||
paintConds();
|
||||
paintHint();
|
||||
paintGrid();
|
||||
const picked = states[now].picked;
|
||||
const mismatch = !busy && picked !== null && last.hits[now] !== picked;
|
||||
@@ -667,6 +686,7 @@ async function mountTable(
|
||||
which,
|
||||
head,
|
||||
condBox,
|
||||
hint,
|
||||
grid,
|
||||
money,
|
||||
warn,
|
||||
|
||||
@@ -282,6 +282,34 @@ def test_기계_부름글이_서버_검사와_계산을_지남(client: TestClien
|
||||
_call(client, "/api/m01/logic/delete", {"key": made["key"], "version": made["version"]})
|
||||
|
||||
|
||||
# ── 회사 · 개인 층 조합 ────────────────────────────────────────────────
|
||||
@pytest.mark.skipif(NO_NODE, reason="node 가 없음")
|
||||
def test_층_조합_가운데_이_로직을_담은_것만_자리_글과_함께() -> None:
|
||||
combos = [
|
||||
{
|
||||
"키": "UC3-000001",
|
||||
"이름": "돌쌓기 한 벌",
|
||||
"담은로직": [{"로직": "GX000001", "메모": None}],
|
||||
},
|
||||
{"키": "UC3-000002", "이름": "다른 것", "담은로직": [{"로직": "GX000002"}]},
|
||||
{"키": "UC3-000003", "이름": "빈 것"},
|
||||
{
|
||||
"키": "UC3-000004",
|
||||
"이름": "둘째 자리",
|
||||
"담은로직": [{"로직": "GF000001"}, {"로직": "GX000001"}],
|
||||
},
|
||||
]
|
||||
got = _node(
|
||||
f"holding({json.dumps(combos, ensure_ascii=False)}, 'GX000001', 'company', '산림조합')"
|
||||
)
|
||||
assert got == [
|
||||
{"kind": "combo", "where": "회사 · 산림조합", "key": "UC3-000001", "name": "돌쌓기 한 벌"},
|
||||
{"kind": "combo", "where": "회사 · 산림조합", "key": "UC3-000004", "name": "둘째 자리"},
|
||||
]
|
||||
person = _node("holding([{키: 'UP15-000001', 담은로직: [{로직: 'K'}]}], 'K', 'personal', '나')")
|
||||
assert person == [{"kind": "combo", "where": "개인 · 나", "key": "UP15-000001", "name": ""}]
|
||||
|
||||
|
||||
# ── 쉬운 말 ─────────────────────────────────────────────────────────────
|
||||
@pytest.mark.skipif(NO_NODE, reason="node 가 없음")
|
||||
def test_plainError_상태와_검사글을_쉬운_말로() -> None:
|
||||
|
||||
Reference in New Issue
Block a user