- 구성행(sheet_rows): 수량 고치기 · 줄 빼기(수량 0, 흐리게) · 줄 더하기(단가표 고르개) — 비율 줄·돌림 참조는 받지 않음
- Q 식(price_basis_q): 식은 명세 13장 식 언어 — B08 구조물도 풀이기(evaluate_sheets, Node 한 벌)를 그대로 부름 ·
소수 2자리 사사오입 확정 → 새 수량 = 수량 × 옛 Q ÷ 새 Q · 비고에 「Q(사용자) = 식 = 값」
· PriceDetail.output(시공능력 Q)을 Q 로 선 장비 줄 넷 자리(굴착기·도자·직접 작업량·암 잎)에서 실음 · 저장 모양에도 실음
- 따로 짰던 파이썬 식 셈(B09_Estimation_Expression)은 걷어냄 — 식 풀이 두 벌 금지(브레인 판정)
- 새 문: GET /estimation/edits/sheet/{code}(본표 + 줄마다 고친 값 표시) · GET /estimation/edits/search(줄 더하기 고르개)
- 화면: 본표 [편집] — 수량 칸 · ✕ · Q 식 칸 · 줄 더하기 · 고친 줄 「사용자」 + ↺
- 검증: 시험 1664 통과 · 골든셋 초록 · ORCA — 제 3 호표 보통인부 0.023→0.046 이면 5,652→9,610 · 내역 본체 122,848,989→122,857,198,
↺ 뒤 5,652 · 산근 2호표 Q 58.21→116.42 이면 1,695→847, ↺ 뒤 1,695 · 틀린 식 422 「모르는 이름: abc」 · 고친 값 {} 로 복구
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
277 lines
8.0 KiB
TypeScript
277 lines
8.0 KiB
TypeScript
/* =============================================================================
|
|
* B09_Estimation_UI_Store.ts
|
|
* B09 내역서·일위대가·단가산출근거 탭이 **한 벌로** 쓰는 서버 자료 (PLAN 12장)
|
|
*
|
|
* - 설계내역서 응답 한 벌에 호표 목록(`unit_price_sheet`)·단산 목록(`price_basis`)이 함께 옴 —
|
|
* 세 탭이 같은 응답을 봄(번호가 탭끼리 갈리지 않게). 번호는 서버가 낼 때마다 매김(저장 안 함).
|
|
* - 캐시는 메모리 한 벌(프로젝트별) — [다시 불러오기] 가 비움. 값을 저장하지 않음(보기 전용 1차).
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import type { ProvenancePayload } from "@ui/ui_template_provenance";
|
|
import type { RowEditDto } from "./B09_Estimation_UI_DetailEdit";
|
|
|
|
export interface BillNoteDto {
|
|
column: string;
|
|
text: string;
|
|
}
|
|
|
|
export interface BillRowDto {
|
|
item_no: string;
|
|
level: number;
|
|
code: string | null;
|
|
name: string;
|
|
spec: string;
|
|
unit: string;
|
|
quantity: string | null;
|
|
quantity_shown: string | null;
|
|
quantity_digits: number | null;
|
|
unit_price_krw: string | null;
|
|
amount_krw: string | null;
|
|
material_krw: string;
|
|
labor_krw: string;
|
|
expense_krw: string;
|
|
unit_material_krw: string | null;
|
|
unit_labor_krw: string | null;
|
|
unit_expense_krw: string | null;
|
|
price_code: string;
|
|
unconfirmed: number;
|
|
price_basis_label: string;
|
|
is_group: boolean;
|
|
in_bill: boolean;
|
|
note: string;
|
|
notes: BillNoteDto[];
|
|
}
|
|
|
|
export interface SheetEntryDto {
|
|
number: number;
|
|
label: string;
|
|
code: string;
|
|
name: string;
|
|
spec: string;
|
|
unit: string;
|
|
material_krw: string;
|
|
labor_krw: string;
|
|
expense_krw: string;
|
|
total_krw?: string;
|
|
unit_price_krw?: string;
|
|
source?: string;
|
|
unconfirmed?: number;
|
|
ref_code?: string;
|
|
unit_price_codes?: string[];
|
|
}
|
|
|
|
export interface MissingDto {
|
|
name: string;
|
|
unit?: string;
|
|
quantity?: string;
|
|
reason: string;
|
|
code?: string;
|
|
blocked_kind?: string;
|
|
}
|
|
|
|
/** 집계표·목록표 한 줄 — 서버 `resource_summary`·`bill_lists` 그대로. */
|
|
export interface ResourceRowDto {
|
|
number: number;
|
|
code: string;
|
|
name: string;
|
|
spec: string;
|
|
unit: string;
|
|
quantity?: string;
|
|
unit_price_krw: string | null;
|
|
amount_krw?: string | null;
|
|
labor_krw?: string;
|
|
material_krw?: string;
|
|
expense_krw?: string;
|
|
unit_labor_krw?: string;
|
|
unit_material_krw?: string;
|
|
unit_expense_krw?: string;
|
|
note: string;
|
|
}
|
|
|
|
export type ResourceGroup = "labor" | "material" | "expense" | "machine" | "lumpsum";
|
|
|
|
export interface MaterialSheetRowDto {
|
|
name: string;
|
|
spec: string;
|
|
unit: string;
|
|
total_amount: string;
|
|
unit_price_krw: string | null;
|
|
amount_krw: string | null;
|
|
note: string;
|
|
}
|
|
|
|
export interface MaterialSheetDto {
|
|
contractor: MaterialSheetRowDto[];
|
|
owner: MaterialSheetRowDto[];
|
|
unknown: MaterialSheetRowDto[];
|
|
contractor_total_krw: string;
|
|
owner_total_krw: string;
|
|
notes: string[];
|
|
}
|
|
|
|
export interface BillDto {
|
|
/** 근거 사전 — 개발환경에서만 옴. */
|
|
provenance?: ProvenancePayload;
|
|
resources: {
|
|
groups: Record<ResourceGroup, ResourceRowDto[]>;
|
|
missing: string[];
|
|
note: string;
|
|
};
|
|
lists: Record<ResourceGroup, ResourceRowDto[]>;
|
|
rows: BillRowDto[];
|
|
excluded: BillRowDto[];
|
|
summary: {
|
|
body_total_krw: string;
|
|
detail_rows: number;
|
|
missing: MissingDto[];
|
|
unconfirmed_count: number;
|
|
notes: string[];
|
|
material_sheet: MaterialSheetDto | null;
|
|
};
|
|
price_basis: { entries: SheetEntryDto[] };
|
|
unit_price_sheet: { entries: SheetEntryDto[] };
|
|
}
|
|
|
|
export interface DetailRowDto {
|
|
ref_code?: string;
|
|
code?: string;
|
|
name: string;
|
|
spec: string;
|
|
unit: string;
|
|
kind?: string;
|
|
source_label?: string;
|
|
source?: string;
|
|
drillable: boolean;
|
|
quantity: string;
|
|
unit_material?: string;
|
|
unit_labor?: string;
|
|
unit_expense?: string;
|
|
unit_total?: string;
|
|
material: string;
|
|
labor: string;
|
|
expense: string;
|
|
total: string;
|
|
note: string;
|
|
/** 고친 값 표시(편집 문으로 받을 때만) — 2차 편집. */
|
|
edit?: RowEditDto;
|
|
}
|
|
|
|
export interface DetailDto {
|
|
code: string;
|
|
name: string;
|
|
spec: string;
|
|
unit: string;
|
|
kind: string;
|
|
material: string;
|
|
labor: string;
|
|
expense: string;
|
|
total: string;
|
|
rows: DetailRowDto[];
|
|
unattached_note: string;
|
|
known_gap_note: string;
|
|
/** 구성행·Q 식을 고칠 수 있는 본표(B·D)인가 · 줄 더하기에 쓸 다음 칸 이름. */
|
|
editable?: boolean;
|
|
next_add_key?: string;
|
|
}
|
|
|
|
const bills = new Map<string, Promise<BillDto>>();
|
|
|
|
async function getJson<T>(path: string): Promise<T> {
|
|
const response = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" });
|
|
const body = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
throw new Error(String((body as { message?: string }).message || response.status));
|
|
}
|
|
return body as T;
|
|
}
|
|
|
|
/** 사용자가 고친 값 한 벌(프로젝트 단위 · 서버 `estimation.edits`). 구획 → {칸: 값}. */
|
|
export type EditsDto = Record<string, Record<string, unknown>>;
|
|
|
|
export interface EditChange {
|
|
section: string;
|
|
key: string;
|
|
/** `null` = 그 칸을 지움(↺ 계산값으로 돌아감). */
|
|
value: unknown;
|
|
}
|
|
|
|
export async function loadEdits(
|
|
projectId: string,
|
|
): Promise<{ edits: EditsDto; skipped: string[] }> {
|
|
return getJson(`/projects/${encodeURIComponent(projectId)}/estimation/edits`);
|
|
}
|
|
|
|
/** 고친 값 저장 — 금액은 서버가 다시 계산하므로 내역 한 벌도 비움. 받지 못하면 까닭을 던짐. */
|
|
export async function saveEdits(projectId: string, changes: EditChange[]): Promise<EditsDto> {
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/estimation/edits`,
|
|
{
|
|
method: "PUT",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ changes }),
|
|
},
|
|
);
|
|
const body = (await response.json().catch(() => ({}))) as { message?: string; edits?: EditsDto };
|
|
if (!response.ok) throw new Error(body.message || String(response.status));
|
|
forgetBill(projectId);
|
|
return body.edits ?? {};
|
|
}
|
|
|
|
/** 산출 조건을 저장한 뒤 — 단가가 다시 서므로 다음에 고르는 탭이 새로 받게 비움. */
|
|
export function forgetBill(projectId: string): void {
|
|
bills.delete(projectId);
|
|
}
|
|
|
|
/** 설계내역서 한 벌 — 세 탭이 같은 응답을 봄. `force` 면 다시 받음. */
|
|
export function loadBill(projectId: string, force = false): Promise<BillDto> {
|
|
if (force || !bills.has(projectId)) {
|
|
const request = getJson<BillDto>(`/projects/${encodeURIComponent(projectId)}/estimation/bill`);
|
|
request.catch(() => bills.delete(projectId));
|
|
bills.set(projectId, request);
|
|
}
|
|
return bills.get(projectId) as Promise<BillDto>;
|
|
}
|
|
|
|
export interface PriceSlotDto {
|
|
name: string;
|
|
price_krw: string | null;
|
|
source_note: string;
|
|
adopted: boolean;
|
|
}
|
|
|
|
export interface PriceCompareDto {
|
|
material_comparison: {
|
|
slot_names: string[];
|
|
rows: Array<{
|
|
code: string;
|
|
name: string;
|
|
spec: string;
|
|
unit: string;
|
|
slots: PriceSlotDto[];
|
|
min_slot: number | null;
|
|
adopted_slot: number;
|
|
adopted_price_krw: string | null;
|
|
note: string;
|
|
}>;
|
|
notes: string[];
|
|
};
|
|
}
|
|
|
|
/** 자재단가대비표 — 슬롯 여섯 · 채택 · 최소단가(서버 `material_price_comparison`). */
|
|
export function loadPriceCompare(projectId: string): Promise<PriceCompareDto> {
|
|
return getJson<PriceCompareDto>(
|
|
`/projects/${encodeURIComponent(projectId)}/estimation/price-sources`,
|
|
);
|
|
}
|
|
|
|
/** 호표 본표 — 일위대가(B)·단가산출(D)은 편집 문(줄마다 고친 값 표시), 시간당 중기(X)는 `unit-prices`. */
|
|
export function loadDetail(projectId: string, code: string): Promise<DetailDto> {
|
|
const path =
|
|
code.startsWith("B-") || code.startsWith("D-")
|
|
? `edits/sheet/${encodeURIComponent(code)}`
|
|
: `unit-prices/${encodeURIComponent(code)}`;
|
|
return getJson<DetailDto>(`/projects/${encodeURIComponent(projectId)}/estimation/${path}`);
|
|
}
|