feat(M02): 면적산출 알약 = 일위대가 pills 길 — 로직 물량 「이름|단위」 + 자재 「자재|MT키」 · 다 고정 · 선택 입력 알약 빠짐 (PLAN 39-4 D7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
This commit is contained in:
2026-10-04 17:52:05 +09:00
co-authored by Claude Opus 5.5
parent ba1658de68
commit d21f258da6
3 changed files with 43 additions and 18 deletions
@@ -301,6 +301,20 @@ export const copyStructure = async (column: string, from: string): Promise<void>
await scoped(`/structures/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
};
/** 면적산출 알약 하나 — 로직 물량 「이름|단위」(`로직` · `배`) · 자재 「자재|MT키」(`자재`) */
export interface LaborPill {
키: string;
이름: string;
단위: string;
로직?: string[];
배?: number;
자재?: { 코드: string; 이름: string; 규격: string; 단위: string };
}
/** 그 열 일위대가의 면적산출 알약(저장본 · PLAN 39-1a pills 길) — 지금 층 */
export const fetchLaborPills = (column: string): Promise<{ 열: string; 알약: LaborPill[] }> =>
call(owned(activeLayer, `/layers/${activeLayer}/labor/${enc(column)}/pills`));
/** 다른 열(`from`) 면적산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 면적번 · 열은 그대로 */
export const copyArea = async (column: string, from: string): Promise<void> => {
await scoped(`/area/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
@@ -24,14 +24,14 @@ import {
import { openModal } from "@ui/ui_template_modal";
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
import { searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch";
import { fetchComboInputs, fetchUsableCombos } from "./M02_MasterTemplete_Labor_Api";
import { fetchUsableCombos } from "./M02_MasterTemplete_Labor_Api";
import {
copyArea,
copyMaterial,
copyStructure,
fetchLaborPills,
fetchStructureNumbers,
getActiveLayer,
readTemplate,
saveTemplate,
type StructureNumbers,
type TreeEdit,
@@ -67,9 +67,8 @@ const TEXT = {
pickCopy: "{value} 복사해 가져오기",
copied: "복사해 가져옴",
copyFailed: "복사 실패 — {value}",
noPrice: "이 열에 일위대가가 없어 알약 없음 — 구조물 조합 일위대가 줄에서 고름",
noInputs: "일위대가 {value} 에 입력값이 없어 알약 없음",
inputsFailed: "일위대가 입력값을 못 받음 — {value}",
noPills: "이 열 일위대가에 담은 로직 · 자재가 없어 알약 없음 — 일위대가 상세에서 담음",
pillsFailed: "일위대가 알약을 못 받음 — {value}",
discardCards: "카드(도면 · 면적산출 · 재료산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
};
@@ -275,7 +274,8 @@ export const columnHead = (col: StructureColumn): string =>
export const structureLabel = (number: string, col: StructureColumn): string =>
[number, columnHead(col)].filter(Boolean).join(" ");
/** 면적산출 · 재료산출 편집 화면 위 알약 막대 — 면적 = 그 열 일위대가 입력값(물량) · 재료 = 자재DB 찾기.
/** 면적산출 · 재료산출 편집 화면 아래 알약 막대 — 면적 = 그 열 일위대가의 로직 물량 「이름|단위」 + 자재 「자재|MT키」
* (서버 pills 길 · 39-4 · 다 `고정` — 더하기 · 빼기는 일위대가 쪽) · 재료 = 자재DB 찾기.
* 알약이 없으면 까닭 글(`notice`)을 같이 돌려줌 */
export async function pillsFor(
kind: SheetKind,
@@ -285,20 +285,19 @@ export async function pillsFor(
const own = await inputPills(column, "재료");
return { links: { 항목: own, 모드: "자재", 자재찾기: searchMaterials } };
}
const own = await inputPills(column, "면적"); // 선택 입력 카드가 면적산출로 고른 입력
const none = (notice: string) => ({ links: { 항목: own, 모드: "입력" as const }, notice });
const table = await readTemplate(getActiveLayer(), "table", STRUCTURE_TABLE, null).catch(
() => null,
);
const cols = (table?.문서 as { 열?: { id: string; 일위대가?: string | null }[] } | null)?.열;
const combo = cols?.find((c) => c.id === column)?.일위대가;
if (!combo) return none(TEXT.noPrice);
const none = (notice: string) => ({ links: { 항목: [], 모드: "입력" as const }, notice });
try {
const got = await fetchComboInputs(combo);
const 항목 = [...got.물량.map((q) => ({ 키: q.키, 이름: q.이름, 단위: q.단위 })), ...own];
return 항목.length ? { links: { 항목, 모드: "입력" } } : none(fill(TEXT.noInputs, combo));
const { 알약 } = await fetchLaborPills(column);
const 항목 = 알약.map((p) => ({
키: p.키,
이름: p.이름,
단위: p.단위,
...(p.자재 ? { 자재: p.자재 } : {}),
고정: true,
}));
return 항목.length ? { links: { 항목, 모드: "입력" } } : none(TEXT.noPills);
} catch (error) {
return none(fill(TEXT.inputsFailed, why(error)));
return none(fill(TEXT.pillsFailed, why(error)));
}
}
@@ -89,3 +89,15 @@ def test_면적산출_카드_알약_줄_켬끔_기본_숨김():
assert '"m02-card-pills": { bucket: "pref", scope: "global" },' in state
# 상세(전체 편집기)는 켬끔 없음 — 늘 보임
assert "is-pills-off" not in _text(M02 / "M02_MasterTemplete_UI_Main.ts")
def test_면적산출_알약은_일위대가_pills_길():
"""39-4 D7 — 로직 물량 「이름|단위」 + 자재 「자재|MT키」 · 다 고정 · 선택 입력 알약 빠짐."""
struct = _text(M02 / "M02_MasterTemplete_Structure.ts")
pills = struct.split("export async function pillsFor")[1].split("async function cardLinks")[0]
area = pills.split('if (kind === "material")')[1].split("\n }\n", 1)[1]
assert "await fetchLaborPills(column)" in area
assert "고정: true" in area and "...(p.자재 ? { 자재: p.자재 } : {})" in area
assert "inputPills" not in area and "fetchComboInputs" not in struct
api = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts")
assert "/layers/${activeLayer}/labor/${enc(column)}/pills" in api