- 좌측 판(Hopyo_Side): 일위대가 사전조합 아래 · 구조물 조합 층 · 쓰기 권한 없으면 [엑셀 올리기] 꺼짐 · 숨은 file 입력 .xls,.xlsx · 파일 줄 → 본 화면
- 본 화면(Hopyo_View): 파일 · 시트 · 호표 수 · 검산 · 맞춤 n/m · 호표 표 + 찾기 · [지우기] / 호표 줄 표(갈래 · 품명 · 규격 · 수량 · 단위 · 엑셀 단가 · 맞춤 · 대상 · 입력값 · 환산) · [고르기](후보 5 + 다른 로직 · 자재 찾기 → 미리 채움) · [확정] · [빼기] · [되살림] · [같음 모두 확정] · 빠진 줄 · [사전조합 만들기] → 값 비교 · 다시 씀이면 [새로 만들기](확인 뒤 덮음)
- [로직 추가](Hopyo_Pick · Labor_Pick 늦은 덧줄 `more` · 종류 고르기): 문서 층 · 위 층 호표 줄(맞춤 n/m) → 고르면 서버가 문서 층 사전조합을 만들고 참조 줄(별칭 = 호표 품명) · 빠진 줄 알림 · 프로젝트 층 없음
- Labor_Detail: 제자리 넷 줄(가져오기 · pick 별칭 받음 · withHopyo) · 693 줄 그대로
- 서버: GET /layers/{층}/hopyo-picks(이 층 · 위 층 · 이 층 기억)
- 색 = 공용 토큰(밝음 · 어두움) · 시험 test_m02_hopyo_screen 4 + hopyo-picks 1 · typecheck 0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
191 lines
5.8 KiB
TypeScript
191 lines
5.8 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Hopyo_Api.ts
|
|
* 일위대가 실무자료(PLAN 324) 서버 길 — `/api/m02/layers/{층}/hopyo…` · 층 = 화면 층(`Api_Fetch` 지금 층) ·
|
|
* 올리기 = FormData(숨은 file 입력) · 그 밖 = 공용 `call`(서버 까닭 글 그대로).
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
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];
|
|
}
|
|
|
|
export interface HopyoView extends HopyoFile {
|
|
검산: { 어긋남: string[]; 총괄표: number };
|
|
호표: HopyoBrief[];
|
|
맞춤: [number, number];
|
|
}
|
|
|
|
/** 맞춤 대상 — 로직 · 자재 · 다른 호표 */
|
|
export interface HopyoTarget {
|
|
로직?: string;
|
|
자재?: string;
|
|
호표?: number;
|
|
이름?: string;
|
|
규격?: string;
|
|
단위?: string | null;
|
|
점수?: number;
|
|
}
|
|
|
|
export interface HopyoMatch {
|
|
열쇠: string;
|
|
상태: "기억" | "같음" | "후보" | "없음" | "빼기" | "호표" | "%";
|
|
대상: HopyoTarget | null;
|
|
입력값: Record<string, unknown>;
|
|
못읽음: string[];
|
|
환산: { 배: number | null; 갈래: string } | null;
|
|
들어감: boolean;
|
|
후보?: HopyoTarget[];
|
|
}
|
|
|
|
export interface HopyoLine {
|
|
차례: number;
|
|
이름: string;
|
|
규격: string;
|
|
수량: number | null;
|
|
단위: string;
|
|
갈래: string;
|
|
단가: Money;
|
|
금액: Money;
|
|
비고: string;
|
|
맞춤: HopyoMatch;
|
|
}
|
|
|
|
export interface HopyoOne extends HopyoFile {
|
|
호표: HopyoBrief & { 줄: HopyoLine[] };
|
|
}
|
|
|
|
export interface HopyoGuess {
|
|
대상: HopyoTarget;
|
|
입력값: Record<string, unknown>;
|
|
못읽음: string[];
|
|
환산: { 배: number | null; 갈래: string };
|
|
}
|
|
|
|
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 };
|
|
}
|
|
|
|
/** [로직 추가] 「실무 호표」 줄 하나 */
|
|
export interface HopyoPickRow extends Omit<HopyoBrief, "금액"> {
|
|
층: Layer;
|
|
주인: string | null;
|
|
키: string;
|
|
파일: string;
|
|
}
|
|
|
|
/** 확정 한 줄 — 로직 · 자재 · 빼기 · null(지움) */
|
|
export type HopyoMemory =
|
|
| { 로직: string; 입력값: Record<string, unknown>; 환산: number }
|
|
| { 자재: string; 입력값: Record<string, unknown>; 환산: number }
|
|
| { 빼기: true }
|
|
| null;
|
|
|
|
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)}`));
|
|
|
|
export const readHopyoOne = (scope: HopyoScope, key: string, no: number): Promise<HopyoOne> =>
|
|
call(at(scope, `/hopyo/${enc(key)}/${no}`));
|
|
|
|
export const guessHopyo = (
|
|
scope: HopyoScope,
|
|
key: string,
|
|
no: number,
|
|
차례: number,
|
|
대상: HopyoTarget,
|
|
): Promise<HopyoGuess> =>
|
|
call(at(scope, `/hopyo/${enc(key)}/${no}/guess`), {
|
|
method: "POST",
|
|
body: JSON.stringify({ 차례, 대상 }),
|
|
});
|
|
|
|
export const saveHopyoMemory = (
|
|
scope: HopyoScope,
|
|
줄: Record<string, HopyoMemory>,
|
|
): Promise<unknown> =>
|
|
call(at(scope, "/hopyo-memory"), { method: "POST", 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 },
|
|
새로 = false,
|
|
): Promise<HopyoBuilt> =>
|
|
call(at(target, "/hopyo-build"), {
|
|
method: "POST",
|
|
body: JSON.stringify({ ...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;
|
|
}
|