- 서버: 내역 응답에 집계표 넷(resources)·목록표(lists) — 내역이 쓴 자원을 처음 쓰인 차례로 되모음 (묶음 줄·구조물도 호표 줄도 구성까지 풀어 셈) · 재료비 집계표에 자재대(사급·관급) 줄 이어 붙임 - 중기시간금액집계표: 단가 열 없이 합계·노무·재료·경비 — 성분마다 반올림한 뒤 합(골든셋 131/144, 합계 한 번 반올림 107/144) - 재료·노무·경비 집계표: 반올림(수량 × 단가) 골든셋 279/279 — 시험 한 벌 더함 - 중기사용료 호표: 잡품 줄은 수량 칸 율(%) · 단가 칸 밑수(주연료비) · 금액 — 실무 표 그대로. 제잡비·공구손료 줄도 밑수를 단가 칸에 - 자재단가대비표: 슬롯 1~6 단가·페이지 · 채택 칸 굵게 · 최소단가 표시(서버 min_slot) - 목록표 일곱(일위대가·단가산출근거·중기·재료비·노무비·경비·일식견적): 색인만 — 서버 번호·단가 그대로, 일위대가·산근·중기 줄은 누르면 들어감 - 옛 중기 탭 줄을 새 탭으로 바꿔 끼움(중기경비계산서는 옛 표를 아래에 이어 보임) - 검증: ORCA — 중기 목록 9 · 굴착기 0.7 호표 합계 109,704 · 잡품 22 % × 21,418.1 = 4,711.9 · 노무 집계 벌목부 9,686,529 · 중기 집계 성분 합 · 시험 1638 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
209 lines
5.6 KiB
TypeScript
209 lines
5.6 KiB
TypeScript
/* =============================================================================
|
|
* B09_Estimation_UI_Store.ts
|
|
* B09 내역서·일위대가·단가산출근거 탭이 **한 벌로** 쓰는 서버 자료 (PLAN 12장)
|
|
*
|
|
* - 설계내역서 응답 한 벌에 호표 목록(`unit_price_sheet`)·단산 목록(`price_basis`)이 함께 옴 —
|
|
* 세 탭이 같은 응답을 봄(번호가 탭끼리 갈리지 않게). 번호는 서버가 낼 때마다 매김(저장 안 함).
|
|
* - 캐시는 메모리 한 벌(프로젝트별) — [다시 불러오기] 가 비움. 값을 저장하지 않음(보기 전용 1차).
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
|
|
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 BillDto {
|
|
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[];
|
|
};
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/** 설계내역서 한 벌 — 세 탭이 같은 응답을 봄. `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)·시간당 중기(X)는 `unit-prices`, 단가산출(D)은 `price-basis`. */
|
|
export function loadDetail(projectId: string, code: string): Promise<DetailDto> {
|
|
const kind = code.startsWith("D-") ? "price-basis" : "unit-prices";
|
|
return getJson<DetailDto>(
|
|
`/projects/${encodeURIComponent(projectId)}/estimation/${kind}/${encodeURIComponent(code)}`,
|
|
);
|
|
}
|