Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts
T
eomsangdonandClaude Opus 5.5 3d4b2ec1d1 feat(812-9): 가져온 기계 잇기 다시 — 기계표 전체 · 표기 정규화 · 괄호 낱말 · 범위 규격 · 별칭 먼저 · 조합 기계 둘 · 조회어 기종만
- 새 `M02_Machine_Find` — 기종 = 기계표 이름 · 괄호 붙임 · 상세구분(낱말표 24 안 씀) · 소리 표기 한 글(로우더 = 로더 · 로울러 = 롤러 · 브레이카 = 브레이커 · 굴삭기 = 굴착기 · 램 머 = 래머 …) · 기계표 괄호 낱말로 좁힘(표에 없는 낱말 발파 · 리핑 = 안 봄) · 규격 = 수 + 단위 전부(범위 · 복수 · 인치 = ㎝) · 하나 = 이음 · 여럿 = 「기계 후보 n — 고르기」 · 0 = 「기계 못 찾음」 표지(가까운 규격으로 대신 잇지 않음)
- 별칭 먼저 — 마스터 층 + 지금 층 별칭 장부 기계 짝(이름 + 규격 · 이름만 · 규격 속 할증 글 뺌) · `M02_Doc_Match_Note._ALIAS` 여섯 쌍 걷음(소리 표기 = 정규화 · 같은 기종 = 기계표 · 별칭 장부)
- 조합 기계 = 기계마다 항목 하나(요구 738-3 · 제목 = 그 기계 · 줄 쌓기 차림 `M02_Doc_NativeLayout`) · 줄 둘(「+」 · 괄호 속 다른 기종 · 「…조합」 · 덤프트럭(자동덮개) = 덤프트럭 + 자동덮개시설) — 같은 수량 식 · 엑셀 값은 첫 줄 · 다음 줄 0 + 표지 「조합 기계 — 경비는 한 덩이」 · 우리 단가로 = 각자 시간당 경비
- 원파일 EST · STmate · 엑셀 읽기기 · 일위대가 호표(`Hopyo_Elem` · `Hopyo_Fixed._eqs` · `Hopyo_Row.m01_picks`)가 같은 찾기 · STmate 묶음 기계 줄도 시간당 경비 이음
- 문서 머리 `가져온.기계`(기호마다 판정 · 후보 · 찾기어) · 못 이은 기계 줄도 기계 줄(이름 단추 · `owned_of` 파이썬 · TS 짝) · 이름 단추 조회어 = 기종 이름만(규격 뺌) · 고르면 엑셀 값 기억 그대로(금액 안 바뀜) · 문서 [저장] = 고른 EQ → 이 층 별칭 장부 줄 → M01 별칭 장부 후보(승인 흐름 그대로) · 장부 짝 글 = 「기계 EQ…」
- 마스터 층 별칭 장부(resources/master_template/실무자료/맞춤.json) = 사용자 승인 다섯(요구 735-5 · 736) — 아스팔트페이버(피니셔) = 아스팔트 피니셔 · 디젤 엔진 = 엔진(디젤) · 워터제트 = 워터젯트 · 버킷준설기 = 버킷식준설기 · 바이브레이트 봉상후렉시블 = 콘크리트 진동기 엔진식 ø45(2.6㎾)
- 시험 — 새 test_812_9(예 전부 · 정규화 · 괄호 · 범위 · 조합 둘 · 별칭 먼저 · 표지 · 고르면 별칭 줄 · 금액 전후) · test_812_3 전석쌓기 · 측구터파기 연암 항목을 기계마다 항목으로 고침(조합 기계 둘로 가름 · 요구 738-3 이 바꾼 까닭)
- 전수 목록 docs/raw/요구_1104/_목록_812-9_기계이음.xlsx

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 21:02:29 +09:00

264 lines
9.1 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Hopyo_Api.ts
* 일위대가 실무자료(PLAN 324 · 338) 서버 길 — `/api/m02/layers/{층}/hopyo…` · 층 = 화면 층(`Api_Fetch` 지금 층) ·
* 올리기 = FormData(숨은 file 입력) · 그 밖 = 공용 `call`(서버 까닭 글 그대로) ·
* 목록 · 올림 · 엑셀 보기 머리(809-1 줄 표지 줄상태) · 격자 · 지우기 · 별칭 장부 · 붙여넣기 · 다시 잇기 —
* [수정] 보기 = `Hopyo_View`(809-1) · 판정 · 검산 · 옮기기 길(785)은 809-1 에서 지움(요구 703).
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet";
import { call, getActiveLayer, getActiveOwner, type Layer } from "./M02_MasterTemplete_Api_Fetch";
import type { LaborFixed } from "./M02_MasterTemplete_Labor_Calc";
/** 뒤 셈 중 다시 읽기 사이(밀리초) */
export const POLL_MS = 3000;
/** 사전조합 · 단가산출을 만들었음(document 알림) — 좌측 목록 · 엑셀 보기 칠 다시 */
export const HOPYO_BUILT = "m02-hopyo-built";
/** 층 · 주인(null = 자기 층) */
export interface HopyoScope {
layer: Layer;
owner: string | null;
}
export interface HopyoFile {
키: string;
파일: string;
올린때: string;
올린이: { id: number; 이름: string | null } | null;
시트: string;
호표수: number;
어긋남: number;
/** 올림 응답만 — 산근 시트를 못 읽은 사유(빈 파일 · DRM 암호 · 형식 오류 · 797-1) */
읽기불가?: string;
}
export type Money = Record<"계" | "노무비" | "재료비" | "경비", number | null>;
export interface HopyoBrief {
번: number;
이름: string;
규격: string;
단위: string;
금액: Money;
/** [1차 판정 제안 있음 수, 줄 수](785-7 `Judge.view`) */
맞춤: [number, number];
/** 줄마다 [차례, 1차 판정, 제안 있음] · 만든 SC 키(칠 · `SheetMap.BriefMarks`) */
줄상태?: [number, string, boolean][];
만든?: string | null;
/** 일위대가 시트 행 [시작, 끝](엑셀 1부터 · 329 · 옛 파일 없음) */
행?: [number, number];
}
/** 엑셀 보기 창 목록 한 줄 — 호표 · 단가산출 블록 행 범위 */
export interface HopyoSpan {
번: number;
이름: string;
행: [number, number];
}
/** 엑셀 보기 창 격자(329) — 시트 「일위대가」 · 「단가산출」 */
export interface HopyoGrid {
책: Workbook;
호표: HopyoSpan[];
단산: HopyoSpan[];
}
export interface HopyoView extends HopyoFile {
검산: { 어긋남: string[]; 총괄표: number };
호표: HopyoBrief[];
맞춤: [number, number];
/** 블록 풀이 뒤 셈(785-5 파생 저장본 · 끝 = 판정 칠 다 셈) */
셈?: { 끝: boolean; 까닭?: string };
}
/** [모두 고정으로 들여오기] 결과(705-3) */
export interface HopyoBuiltAll {
호표: number;
줄: number;
품셈: number;
고정: number;
호표참조: number;
빠짐: number;
이음: number;
이음후보: number;
같음: number;
그대로: number;
다름: {
번: number;
이름: string;
엑셀: number | null;
우리: number | null;
차이: number | null;
}[];
실패: { 번: number; 까닭: string }[];
/** 만든 · 그대로 둔 사전조합(705-2 · 다시 잇기) */
만듦?: { 번: number; 이름: string; 키: string }[];
}
/** 별칭 장부 짝 하나 */
export interface LedgerPair {
로직?: string;
자재?: string;
/** 고른 것 꼴(785-3 · 812-9 기계) — 갈래 + 키 */
갈래?: string;
키?: string;
입력값: Record<string, unknown>;
배: number;
몫: number | null;
}
/** 별칭 장부 한 줄(쓰기 꼴) */
export interface LedgerEntry {
원래: { 품명: string; 규격: string; 단위: string };
갈래: "짝" | "우리에 없음" | "안 씀";
짝: LedgerPair[];
엑셀조항?: unknown[];
}
/** 별칭 장부 보기 한 줄 — 층 · 고침(이 층 · 쓰기 권한) */
export interface LedgerRow extends LedgerEntry {
열쇠: string;
누가: { id: number; 이름: string | null } | null;
때: string | null;
처음파일: string | null;
층: Layer;
주인: string | null;
고침: boolean;
/** 739-1 M01 승인 상태 — 마스터 줄 = 승인 */
상태?: "승인" | "대기" | "반려";
}
const enc = encodeURIComponent;
/** 지금 화면 층(구조물 조합 층) */
export const pageScope = (): HopyoScope => ({ layer: getActiveLayer(), owner: getActiveOwner() });
export const at = (scope: HopyoScope, path: string): string => {
const who = scope.owner && scope.layer !== "system" ? `owner=${enc(scope.owner)}` : "";
const join = path.includes("?") ? "&" : "?";
return `/layers/${scope.layer}${path}${who ? join + who : ""}`;
};
export const listHopyo = async (scope: HopyoScope): Promise<HopyoFile[]> =>
(await call<{ 줄: HopyoFile[] }>(at(scope, "/hopyo"))).줄;
export const readHopyo = (scope: HopyoScope, key: string): Promise<HopyoView> =>
call(at(scope, `/hopyo/${enc(key)}`));
/** 격자 — 없으면 404 「격자 없음 — 다시 올리면 보임」(옛 파일) */
export const readHopyoGrid = (scope: HopyoScope, key: string): Promise<HopyoGrid> =>
call(at(scope, `/hopyo-grid/${enc(key)}`));
export const readLedger = async (scope: HopyoScope): Promise<LedgerRow[]> =>
(await call<{ 줄: LedgerRow[] }>(at(scope, "/hopyo-ledger"))).줄;
/** 별칭 장부 [저장] — 고친 줄 · null(지움) · 이 층만 */
export const saveLedger = async (
scope: HopyoScope,
줄: Record<string, LedgerEntry | null>,
): Promise<LedgerRow[]> =>
(
await call<{ 줄: LedgerRow[] }>(at(scope, "/hopyo-ledger"), {
method: "PUT",
body: JSON.stringify({ 줄 }),
})
).줄;
/** [끝내기] · 화면 떠남(743-1) — `keepalive` = 페이지를 닫아도 요청은 감 */
export const deleteHopyo = (scope: HopyoScope, key: string, keepalive = false): Promise<unknown> =>
call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE", keepalive });
/** 엑셀 올리기 — 서버 까닭 글 그대로(시트 없음 · 20 MB · 엉터리) */
export async function uploadHopyo(scope: HopyoScope, file: File): Promise<HopyoFile> {
const form = new FormData();
form.append("file", file);
const response = await fetch(`${API_BASE_URL}/m02${at(scope, "/hopyo")}`, {
method: "POST",
credentials: "include",
body: form,
});
const body = (await response.json().catch(() => ({}))) as { detail?: unknown } & HopyoFile;
if (!response.ok)
throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`);
return body;
}
/** 엑셀 붙여넣기 열 짝(705-2) — 칸 차례(0부터) · 노 · 재 · 경 = 단가 칸 · 금액 = 합계 금액 칸 */
export type PasteCols = Partial<
Record<"품명" | "규격" | "수량" | "단위" | "금액" | "노" | "재" | "경", number>
>;
/** 붙여넣기 답 — 물음(열 짝 짐작 + 보기) · 줄(고정 줄 · 문서에 넣음) · 만듦(호표마다 사전조합) */
export type PasteResult =
| { 갈래: "물음"; 열: PasteCols; 짐작: boolean; 보기: string[][] }
| {
갈래: "줄";
열: PasteCols;
줄: { 고정: LaborFixed; 별칭: string | null; 메모: string; 수량: { 손: number | null } }[];
엑셀: number;
이음: number;
이음후보: number;
}
| ({ 갈래: "만듦"; 열: PasteCols; 키: string } & HopyoBuiltAll);
/** [Ctrl+V](705-2) — 붙인 글 그대로(숫자는 서버가 글에서) */
export const pasteHopyo = (
scope: HopyoScope,
글: string,
열: PasteCols | null,
opt: { 줄만?: boolean; 있음?: string[] } = {},
): Promise<PasteResult> =>
call(at(scope, "/hopyo-paste"), {
method: "POST",
body: JSON.stringify({ 글, ...(열 ? { 열 } : {}), ...opt }),
});
/** 다시 잇기 표 한 줄(705-2) — 붙인 단가 · 이은 단가(지금) · 후보 */
export interface RelinkRow {
키: string | null;
줄: string;
이름: string;
규격: string;
단위: string;
수량: number | null;
붙인: Record<"노무비" | "재료비" | "경비", number>;
이음: Record<"노임" | "자재", string | null>;
지금: Partial<Record<"노임" | "자재", number | null>>;
후보: {
비목: "노임" | "자재";
키: string;
이름: string;
규격: string;
단위: string;
단가: number | null;
}[];
}
/** 바꿈 한 줄 — 값 null = 이음 풂 */
export interface RelinkChange {
키: string;
줄: string;
노임?: string | null;
자재?: string | null;
}
export const relinkRows = async (
scope: HopyoScope,
body: { 키?: string[]; 문서?: unknown },
): Promise<RelinkRow[]> =>
(
await call<{ 줄: RelinkRow[] }>(at(scope, "/hopyo-relink/rows"), {
method: "POST",
body: JSON.stringify(body),
})
).줄;
export const relinkApply = (
scope: HopyoScope,
바꿈: RelinkChange[],
): Promise<{
바뀜: number;
문서: { 키: string; 이름: string; 전: number | null; 후: number | null }[];
되돌림: RelinkChange[];
}> => call(at(scope, "/hopyo-relink"), { method: "POST", body: JSON.stringify({ 바꿈 }) });