- 확인 창 줄 표: 한글 낱말 단위 줄바꿈 · 품명 · 규격 최소 폭 · 옵션 · 품 비교 항목마다 한 줄 · 단추 칸 세로 쌓음(좁게) - 별칭 장부 표: 품명 · 규격 같은 손질 - 만들기(확인 창 · 엑셀 보기 창 일괄) 뒤 document 알림 → 좌측 「일위대가 사전조합」 목록 다시 읽음 - 엑셀 보기 창: 뒤 셈 중이면 3 초마다 목록 「제안 n/m」 다시 읽음 · 펼침 표 숫자 판정 칸 = 맞음 n · 다른 점 n - 품 비교 판정 함수 · 서버 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
339 lines
11 KiB
TypeScript
339 lines
11 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Hopyo_Api.ts
|
|
* 일위대가 실무자료(PLAN 324 · 338) 서버 길 — `/api/m02/layers/{층}/hopyo…` · 층 = 화면 층(`Api_Fetch` 지금 층) ·
|
|
* 올리기 = FormData(숨은 file 입력) · 그 밖 = 공용 `call`(서버 까닭 글 그대로) ·
|
|
* 맞춤 응답 꼴 = 설계 3-3(상태 · 체크 제안 · 대상 목록 · 품 비교) · 만들기 몸 = 확인한 줄만(3-6) · 별칭 장부(3-5).
|
|
* ========================================================================== */
|
|
|
|
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 { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
|
|
|
/** 층 · 주인(null = 자기 층) */
|
|
export interface HopyoScope {
|
|
layer: Layer;
|
|
owner: string | null;
|
|
}
|
|
|
|
export interface HopyoFile {
|
|
키: string;
|
|
파일: string;
|
|
올린때: string;
|
|
올린이: { id: number; 이름: string | null } | null;
|
|
시트: string;
|
|
호표수: number;
|
|
어긋남: number;
|
|
}
|
|
|
|
export type Money = Record<"계" | "노무비" | "재료비" | "경비", number | null>;
|
|
|
|
export interface HopyoBrief {
|
|
번: number;
|
|
이름: string;
|
|
규격: string;
|
|
단위: string;
|
|
금액: Money;
|
|
/** [체크 제안 수, 줄 수] */
|
|
맞춤: [number, number];
|
|
/** 일위대가 시트 행 [시작, 끝](엑셀 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];
|
|
/** 뒤 숫자 판정 셈(끝나면 맞춤 수가 바뀜) */
|
|
셈?: HopyoCalc;
|
|
}
|
|
|
|
/** 후보가 단가산출 근거와 맞는 것(330-2) — 맞은 조항 글 · 겹친 직종 · 단위 같음 · 점수 · 까닭 꼬리표 */
|
|
export interface HopyoBasisHit {
|
|
조항: string[];
|
|
직종: string[];
|
|
단위: boolean;
|
|
점수: number;
|
|
까닭: string;
|
|
}
|
|
|
|
/** 단산 줄 근거(330-1) — 단가산출 블록의 출처([책, 번호]) · 직종 · 엑셀 행 [시작, 끝] */
|
|
export interface HopyoLineBasis {
|
|
출처: [string, string][];
|
|
직종: string[];
|
|
행: [number, number];
|
|
}
|
|
|
|
/** 후보 · 고른 대상 글 — 로직 · 자재 · 다른 호표 */
|
|
export interface HopyoTarget {
|
|
로직?: string;
|
|
자재?: string;
|
|
호표?: number;
|
|
이름?: string;
|
|
규격?: string;
|
|
단위?: string | null;
|
|
점수?: number;
|
|
/** 근거 맞춤(330 · 옛 파일 · 근거 없는 줄은 없음) */
|
|
근거?: HopyoBasisHit;
|
|
/** 후보 판정(338) — 맞음 · 다른 점 · 후보 */
|
|
판정?: "맞음" | "다른 점" | "후보";
|
|
글?: string;
|
|
}
|
|
|
|
/** 맞춤 대상 하나(338 · 로직 여럿 가능) — 배 = 환산 × 몫 · 우리 수량 = 엑셀 수량 × 배 */
|
|
export interface HopyoPart extends HopyoTarget {
|
|
입력값: Record<string, unknown>;
|
|
못읽음?: string[];
|
|
/** 칸마다 값 출처 — 별칭 · 블록 · 규격 · 자동값 */
|
|
출처?: Record<string, string>;
|
|
환산?: { 배: number | null; 갈래: string } | null;
|
|
몫?: number | null;
|
|
배: number | null;
|
|
/** 블록 절 제목(조합 덧 로직) */
|
|
어디?: string;
|
|
}
|
|
|
|
/** 품 묶음 — 직종 품 · 장비 시간 · 자재 수량 */
|
|
export interface HopyoQty {
|
|
인력?: Record<string, number>;
|
|
장비?: { 갈래: string | string[]; 시간: number }[];
|
|
자재?: { 이름: string; 수량: number }[];
|
|
}
|
|
|
|
export type HopyoState =
|
|
"맞음" | "별칭" | "다른 점" | "후보" | "없음" | "뺌" | "호표" | "%" | "노임";
|
|
|
|
export interface HopyoMatch {
|
|
열쇠: string;
|
|
상태: HopyoState;
|
|
/** 처음 체크 제안(확인 대기 — 들어감 아님) */
|
|
체크: boolean;
|
|
대상: HopyoPart[];
|
|
후보?: HopyoTarget[];
|
|
글?: string;
|
|
꼬리표?: string[];
|
|
품?: { 엑셀: HopyoQty; 우리: HopyoQty; 같음: Record<string, boolean> };
|
|
갈리는칸?: string[];
|
|
별칭?: { 층: Layer; 주인: string | null; 규격다름?: boolean; 근거다름?: boolean };
|
|
뺌?: string;
|
|
}
|
|
|
|
/** 숫자 판정 셈 — 끝 · 한 블록 · 모두 · 까닭(우리 로직 준비 중 등) */
|
|
export interface HopyoCalc {
|
|
끝: boolean;
|
|
한: number;
|
|
모두: number;
|
|
까닭?: string;
|
|
}
|
|
|
|
export interface HopyoLine {
|
|
차례: number;
|
|
이름: string;
|
|
규격: string;
|
|
수량: number | null;
|
|
단위: string;
|
|
갈래: string;
|
|
단가: Money;
|
|
금액: Money;
|
|
비고: string;
|
|
맞춤: HopyoMatch;
|
|
/** 단가산출 근거(330 · 「단산N참조」 줄만) */
|
|
근거?: HopyoLineBasis;
|
|
}
|
|
|
|
export interface HopyoOne extends HopyoFile {
|
|
호표: HopyoBrief & {
|
|
줄: HopyoLine[];
|
|
/** 직접 줄 호표 통째 제안(노임 · 장비를 바로 적은 호표) */
|
|
통째?: { 대상: HopyoPart[]; 글?: string; 체크: boolean } | null;
|
|
셈?: HopyoCalc;
|
|
/** 만들 층에 이 호표로 만든 사전조합 */
|
|
만든?: { 키: string; 확인: boolean } | null;
|
|
};
|
|
}
|
|
|
|
/** 미리 셈(저장 안 함) — 고른 대상 · 옵션으로 우리 품 */
|
|
export interface HopyoGuess {
|
|
대상: HopyoPart[];
|
|
품?: HopyoMatch["품"];
|
|
글?: string;
|
|
}
|
|
|
|
/** 만들기 몸 대상 하나 — 로직 · 자재 · 호표 중 하나(금액 · 품 칸 없음 — 서버가 셈) */
|
|
export interface BuildTarget {
|
|
로직?: string;
|
|
자재?: string;
|
|
호표?: number;
|
|
입력값?: Record<string, unknown>;
|
|
배: number;
|
|
몫?: number | null;
|
|
}
|
|
|
|
/** 확인한 줄 — 대상 목록 또는 뺌 까닭 */
|
|
export interface BuildLine {
|
|
차례: number;
|
|
대상?: BuildTarget[];
|
|
뺌?: "우리에 없음" | "안 씀";
|
|
}
|
|
|
|
export interface HopyoMissing {
|
|
차례: number;
|
|
이름: string;
|
|
규격: string;
|
|
수량: number | null;
|
|
단위: string;
|
|
금액: number | null;
|
|
까닭: string;
|
|
}
|
|
|
|
export interface HopyoBuilt {
|
|
참조: LaborRef;
|
|
이름: string;
|
|
다시씀: boolean;
|
|
빠진줄: HopyoMissing[];
|
|
비교: { 엑셀: number | null; 우리: number | null; 차이: number | null; 까닭: string | null };
|
|
장부?: { 씀: number };
|
|
}
|
|
|
|
/** [로직 추가] 「실무 호표」 줄 하나 — 확인 = 문서 층에 확인된 사전조합 */
|
|
export interface HopyoPickRow extends Omit<HopyoBrief, "금액"> {
|
|
층: Layer;
|
|
주인: string | null;
|
|
키: string;
|
|
파일: string;
|
|
확인?: LaborRef | null;
|
|
}
|
|
|
|
/** 별칭 장부 짝 하나 */
|
|
export interface LedgerPair {
|
|
로직?: 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;
|
|
}
|
|
|
|
const enc = encodeURIComponent;
|
|
/** 지금 화면 층(구조물 조합 층) */
|
|
export const pageScope = (): HopyoScope => ({ layer: getActiveLayer(), owner: getActiveOwner() });
|
|
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)}`));
|
|
|
|
/** 호표 하나 — `to` = 만들 층(호표 참조 줄 체크 · 만든 · 없으면 파일 층) */
|
|
export const readHopyoOne = (
|
|
scope: HopyoScope,
|
|
key: string,
|
|
no: number,
|
|
to?: HopyoScope,
|
|
): Promise<HopyoOne> => {
|
|
const want = to ? `?to=${to.layer}${to.owner ? `&to_owner=${enc(to.owner)}` : ""}` : "";
|
|
return call(at(scope, `/hopyo/${enc(key)}/${no}${want}`));
|
|
};
|
|
|
|
/** 미리 셈(저장 안 함) — 고른 대상 목록 · 옵션 → 우리 품 */
|
|
export const guessHopyo = (
|
|
scope: HopyoScope,
|
|
key: string,
|
|
no: number,
|
|
차례: number,
|
|
대상: BuildTarget[],
|
|
): Promise<HopyoGuess> =>
|
|
call(at(scope, `/hopyo/${enc(key)}/${no}/guess`), {
|
|
method: "POST",
|
|
body: JSON.stringify({ 차례, 대상 }),
|
|
});
|
|
|
|
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({ 줄 }),
|
|
})
|
|
).줄;
|
|
|
|
export const deleteHopyo = (scope: HopyoScope, key: string): Promise<unknown> =>
|
|
call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE" });
|
|
|
|
/** [확인하고 사전조합 만들기] — 확인한 줄만 · 직접 줄 호표 = `통째` → `target` 층 사전조합(파일 층 = `from`) */
|
|
export const buildHopyo = (
|
|
target: HopyoScope,
|
|
from: { 층: Layer; 주인: string | null; 키: string; 번: number },
|
|
줄: BuildLine[],
|
|
통째?: { 대상: BuildTarget[] } | null,
|
|
): Promise<HopyoBuilt> =>
|
|
call(at(target, "/hopyo-build"), {
|
|
method: "POST",
|
|
body: JSON.stringify(통째 ? { ...from, 통째 } : { ...from, 줄 }),
|
|
});
|
|
|
|
export const listHopyoPicks = async (scope: HopyoScope): Promise<HopyoPickRow[]> =>
|
|
(await call<{ 줄: HopyoPickRow[] }>(at(scope, "/hopyo-picks"))).줄;
|
|
|
|
/** 엑셀 올리기 — 서버 까닭 글 그대로(시트 없음 · 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;
|
|
}
|