Files
Aislo/B09_Estimation/B09_Estimation_UI_DetailEdit.ts
eomsangdonandClaude Opus 5 9cf01ab15c feat(b09): 2차 편집 ① 자재단가대비표 채택 바꾸기 — 프로젝트 단위 고친 값(estimation.edits) · 서버가 얹어 다시 계산 · ↺
- B09_Estimation_Edits: 고친 값 한 벌 — 기본 조립(cached_build) 복사본에 얹음(캐시 키에 고친 값) · 기본 벌은 그대로(골든셋 무관)
  · 얹지 못한 값은 edit_skipped 로 남김 · 값 없는 슬롯은 받지 않음(422 + 까닭)
- 새 라우터 B09_Estimation_Router_Edits(GET/PUT /estimation/edits) — Router.py 는 _build_for 가 고친 값을 얹게만 바꿈
- 화면: 줄마다 채택 슬롯 고르개 · 일괄(변동없음/1~5 단가/최소단가) · 고친 줄 「사용자」 + ↺(지우면 계산값으로)
- 곁: 사용자 식 셈(B09_Estimation_Expression — ROUND·ROUNDDOWN·INT·SQRT, eval 없음)과 본표 편집 칸(UI_DetailEdit)을 먼저 둠 — 서버 편집 문이 서기 전까지 잠자 있음
- 검증: 시험 1656 통과 · ORCA 채택 저장 → 「사용자」·↺ → ↺ 뒤 고친 값 {} 로 복구 · 값 없는 슬롯 422 「경유: 1번 원천에 값이 없어」

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-14 03:16:10 +09:00

190 lines
6.4 KiB
TypeScript

/* =============================================================================
* B09_Estimation_UI_DetailEdit.ts
* 호표 본표 **편집** — 일위대가 구성행 수정(`wM_Edit_iLWi`) · 단가산출 Q 식 편집(`wM_Edit_San`) (PLAN 12장 2차)
*
* - 전부 프로젝트 단위(브레인 판정) — 칸 하나 고칠 때마다 서버에 고친 값만 보내고, 금액은 서버가 다시 셈.
* - 줄마다: 수량 칸 · ✕(줄 빼기) · D 의 기계 줄은 Q 식 칸 · 고친 줄 = 「사용자」 + ↺(계산값으로).
* - 아래: 줄 더하기 — 단가표 고르개(이름으로 찾기) + 수량.
* - ⚠ 화면이 값을 만들지 않음 — 입력한 글을 그대로 보내고, 받을 수 없는 값은 서버가 까닭과 함께 거절.
* ========================================================================== */
import { showToast } from "@ui/ui_template_elements";
import { API_BASE_URL } from "@config/config_frontend";
import { L, el, userMark } from "./B09_Estimation_UI_Sheet";
import { saveEdits, type EditChange } from "./B09_Estimation_UI_Store";
/** 서버가 줄마다 붙이는 고친 값 표시. */
export interface RowEditDto {
key: string;
user: boolean;
removed?: boolean;
added?: boolean;
was_quantity?: string;
output?: string;
q_formula?: string;
}
interface SearchRowDto {
code: string;
name: string;
spec: string;
unit: string;
kind_label: string;
}
export const ROWS_SECTION = "sheet_rows";
export const Q_SECTION = "price_basis_q";
function save(projectId: string, changes: EditChange[], reload: () => void): void {
void saveEdits(projectId, changes)
.then(() => {
showToast(L("B09_Sheet_Saved"), "success");
reload();
})
.catch((error: Error) => showToast(`${L("B09_Sheet_SaveFailed")} ${error.message}`, "error"));
}
/** 수량 칸 — Enter·칸 나가기에 보냄(바뀐 때만). */
export function quantityInput(
projectId: string,
edit: RowEditDto,
quantity: string,
reload: () => void,
): HTMLElement {
const input = el("input", "b09s-edit-input");
input.type = "text";
input.inputMode = "decimal";
input.value = quantity;
const send = (): void => {
const text = input.value.trim();
if (text === quantity) return;
save(projectId, [{ section: ROWS_SECTION, key: edit.key, value: { quantity: text } }], reload);
};
input.addEventListener("click", (event) => event.stopPropagation());
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") input.blur();
});
input.addEventListener("change", send);
return input;
}
/** 줄 끝 조작 — ✕(빼기) · 고친 줄이면 「사용자」 + ↺. */
export function rowControls(
projectId: string,
edit: RowEditDto,
reload: () => void,
editing: boolean,
): HTMLElement {
const box = el("span", "b09s-inline");
if (editing && !edit.removed && !edit.added) {
const remove = el("button", "b09s-undo", "✕");
remove.type = "button";
remove.title = L("B09_Sheet_RemoveRow");
remove.addEventListener("click", (event) => {
event.stopPropagation();
save(projectId, [{ section: ROWS_SECTION, key: edit.key, value: { removed: true } }], reload);
});
box.append(remove);
}
if (edit.user) {
box.append(
userMark(() =>
save(projectId, [{ section: ROWS_SECTION, key: edit.key, value: null }], reload),
),
);
}
return box;
}
/** D 기계 줄의 Q 식 칸 — 식을 글로 넣으면 서버가 셈 → 소수 2자리 확정 → 줄 수량 = 몫 ÷ Q. */
export function qFormulaInput(
projectId: string,
edit: RowEditDto,
reload: () => void,
): HTMLElement {
const box = el("div", "b09s-inline");
const input = el("input", "b09s-edit-input b09s-edit-formula");
input.type = "text";
input.placeholder = `Q = ${edit.output ?? ""}`;
input.value = edit.q_formula ?? "";
input.addEventListener("click", (event) => event.stopPropagation());
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") input.blur();
});
input.addEventListener("change", () => {
const text = input.value.trim();
const value = text ? { q_formula: text } : null;
save(projectId, [{ section: Q_SECTION, key: edit.key, value }], reload);
});
box.append(el("span", "b09s-head", "Q"), input);
if (edit.q_formula) {
box.append(
userMark(() => save(projectId, [{ section: Q_SECTION, key: edit.key, value: null }], reload)),
);
}
return box;
}
/** 줄 더하기 — 단가표에서 이름으로 찾아 고르고 수량을 넣음. */
export function addRowForm(
projectId: string,
code: string,
nextKey: string,
reload: () => void,
): HTMLElement {
const box = el("div", "b09s-bar");
const search = el("input", "b09s-edit-input b09s-edit-formula");
search.type = "text";
search.placeholder = L("B09_Sheet_SearchTitle");
const pick = el("select");
const amount = el("input", "b09s-edit-input");
amount.type = "text";
amount.inputMode = "decimal";
amount.placeholder = L("B09_Sheet_Col_Quantity");
const add = el("button", "b09s-undo", L("B09_Sheet_AddRow"));
add.type = "button";
let timer = 0;
search.addEventListener("input", () => {
window.clearTimeout(timer);
timer = window.setTimeout(() => {
const query = search.value.trim();
if (!query) return;
void fetch(
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/estimation/edits/search?q=${encodeURIComponent(query)}&parent=${encodeURIComponent(code)}`,
{ credentials: "include" },
)
.then((response) => response.json() as Promise<{ rows: SearchRowDto[] }>)
.then((data) => {
pick.replaceChildren(
...data.rows.map((row) => {
const option = el(
"option",
"",
`[${row.kind_label}] ${row.name} ${row.spec} (${row.unit})`.trim(),
);
option.value = row.code;
return option;
}),
);
})
.catch(() => pick.replaceChildren());
}, 250);
});
add.addEventListener("click", () => {
if (!pick.value || !amount.value.trim()) return;
save(
projectId,
[
{
section: ROWS_SECTION,
key: nextKey,
value: { ref: pick.value, quantity: amount.value.trim() },
},
],
reload,
);
});
box.append(el("span", "b09s-head", L("B09_Sheet_AddRow")), search, pick, amount, add);
return box;
}