/* ============================================================================= * 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 }[]; } 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 => (await call<{ 줄: HopyoFile[] }>(at(scope, "/hopyo"))).줄; export const readHopyo = (scope: HopyoScope, key: string): Promise => call(at(scope, `/hopyo/${enc(key)}`)); /** 격자 — 없으면 404 「격자 없음 — 다시 올리면 보임」(옛 파일) */ export const readHopyoGrid = (scope: HopyoScope, key: string): Promise => call(at(scope, `/hopyo-grid/${enc(key)}`)); /** [끝내기] · 화면 떠남(743-1) — `keepalive` = 페이지를 닫아도 요청은 감 */ export const deleteHopyo = (scope: HopyoScope, key: string, keepalive = false): Promise => call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE", keepalive }); /** 엑셀 올리기 — 서버 까닭 글 그대로(시트 없음 · 20 MB · 엉터리) */ export async function uploadHopyo(scope: HopyoScope, file: File): Promise { 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 => 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>; 후보: { 비목: "노임" | "자재"; 키: 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 => ( 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({ 바꿈 }) });