fix(b08): 프로그램 기본 발행본엔 원문 공사명·파일명을 안 실음(브레인 ②) — 출처에 「원문에서 뽑음 — 공사명은 발행 시 가림」 · 원본(개인 단)·회사 단 발행은 그대로(③) · 발행 확인창에 「이 항목은 「○○」에서 뽑은 것 — 공사명은 빼고 발행됩니다」
화면(936be972 · 봉화 호표 가져온 장): 확인창 문구 확인 · 공용 storage 라 취소(쓰기 없음) · 가림은 임시 storage 시험 · 되돌림 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
@@ -28,6 +28,9 @@ LIBRARY_SUBDIR = "library"
|
|||||||
|
|
||||||
#: 프로그램 기본 작업본 자리 — 회사 번호 폴더와 안 겹치게 밑줄 이름.
|
#: 프로그램 기본 작업본 자리 — 회사 번호 폴더와 안 겹치게 밑줄 이름.
|
||||||
PROGRAM_SUBDIR = "_program"
|
PROGRAM_SUBDIR = "_program"
|
||||||
|
#: 프로그램 기본 발행본에서 뺄 출처 칸 — 어느 공사인지(2026-09-14 브레인 판정 ②).
|
||||||
|
ORIGIN_MASKED_KEYS = ("project", "file")
|
||||||
|
ORIGIN_MASKED_NOTE = "원문에서 뽑음 — 공사명은 발행 시 가림"
|
||||||
|
|
||||||
|
|
||||||
def program_library_dir() -> Path:
|
def program_library_dir() -> Path:
|
||||||
@@ -194,6 +197,10 @@ def save_personal(
|
|||||||
for row in overridden_rows(template, overrides)
|
for row in overridden_rows(template, overrides)
|
||||||
]
|
]
|
||||||
item = {k: v for k, v in template.items() if k != "imported_from"}
|
item = {k: v for k, v in template.items() if k != "imported_from"}
|
||||||
|
if tier == "program" and isinstance(item.get("origin"), dict):
|
||||||
|
# 모든 회사로 가는 발행본 — 원문 공사명·파일명은 안 실음(원본·회사 단은 그대로 · 브레인 ②③).
|
||||||
|
kept = {k: v for k, v in item["origin"].items() if k not in ORIGIN_MASKED_KEYS}
|
||||||
|
item["origin"] = {**kept, "masked": ORIGIN_MASKED_NOTE}
|
||||||
if unit_price_rows is not None:
|
if unit_price_rows is not None:
|
||||||
item["unit_price"] = {**(item.get("unit_price") or {}), "rows": unit_price_rows}
|
item["unit_price"] = {**(item.get("unit_price") or {}), "rows": unit_price_rows}
|
||||||
_write(folder, {**item, "code": code, "library_tier": tier, "rows": rows})
|
_write(folder, {**item, "code": code, "library_tier": tier, "rows": rows})
|
||||||
|
|||||||
@@ -441,6 +441,8 @@ def apply_templates(
|
|||||||
"name": template.get("name"),
|
"name": template.get("name"),
|
||||||
"code": template.get("code"),
|
"code": template.get("code"),
|
||||||
"imported_from": template.get("imported_from"),
|
"imported_from": template.get("imported_from"),
|
||||||
|
# 발행 확인창이 「○○공사에서 뽑은 것 — 공사명은 빼고 발행」을 보이게(브레인 ②).
|
||||||
|
"origin_project": (template.get("origin") or {}).get("project"),
|
||||||
}
|
}
|
||||||
sheet["formula_sheet"] = body
|
sheet["formula_sheet"] = body
|
||||||
# 실무 관측값 같은 「대안 후보」 — 값을 바꾸지 않고 칸 옆에 보이기만(판정 Ⓑ).
|
# 실무 관측값 같은 「대안 후보」 — 값을 바꾸지 않고 칸 옆에 보이기만(판정 Ⓑ).
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ export interface StructureSheet extends StandardSheetSpec {
|
|||||||
name: string;
|
name: string;
|
||||||
code?: string | null;
|
code?: string | null;
|
||||||
imported_from?: string | null;
|
imported_from?: string | null;
|
||||||
|
/** 뽑아 온 원문 공사명(STmate 고정형) — 프로그램 기본 발행 때 가림. */
|
||||||
|
origin_project?: string | null;
|
||||||
};
|
};
|
||||||
/** 화면이 조작 중 왕복 없이 다시 풀 장 한 벌(L=1, 고친 식 얹힘). */
|
/** 화면이 조작 중 왕복 없이 다시 풀 장 한 벌(L=1, 고친 식 얹힘). */
|
||||||
formula_sheet?: FormulaSheet;
|
formula_sheet?: FormulaSheet;
|
||||||
@@ -422,6 +424,7 @@ export function renderStructureSheets(projectId: string | null): HTMLElement {
|
|||||||
sheetKey: sheet.key,
|
sheetKey: sheet.key,
|
||||||
typeId: sheet.library_item.type_id,
|
typeId: sheet.library_item.type_id,
|
||||||
currentCode: sheet.library_item.code ?? null,
|
currentCode: sheet.library_item.code ?? null,
|
||||||
|
originProject: sheet.library_item.origin_project,
|
||||||
isDirty: () => dirty,
|
isDirty: () => dirty,
|
||||||
confirmTake: () => {
|
confirmTake: () => {
|
||||||
const edited = editedRows(sheet);
|
const edited = editedRows(sheet);
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export interface LibraryPanelOptions {
|
|||||||
/** 저장 안 한 식이 있으면 [내 라이브러리에 저장]을 막음 — 저장된 식만 개인 단으로 감. */
|
/** 저장 안 한 식이 있으면 [내 라이브러리에 저장]을 막음 — 저장된 식만 개인 단으로 감. */
|
||||||
isDirty: () => boolean;
|
isDirty: () => boolean;
|
||||||
onImported: (notes: string[]) => Promise<void>;
|
onImported: (notes: string[]) => Promise<void>;
|
||||||
|
/** 이 장 양식이 뽑아 온 원문 공사명 — 프로그램 기본 발행 확인창에 「빼고 발행」을 알림. */
|
||||||
|
originProject?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 가져오기 · [내 라이브러리에 저장] · [내 것 지우기] 칸. 개인 단 두 단추는 프로젝트를 안 바꿈. */
|
/** 가져오기 · [내 라이브러리에 저장] · [내 것 지우기] 칸. 개인 단 두 단추는 프로젝트를 안 바꿈. */
|
||||||
@@ -243,7 +245,15 @@ export function buildLibraryPanel(options: LibraryPanelOptions): HTMLElement {
|
|||||||
const button = personal(label, async () => {
|
const button = personal(label, async () => {
|
||||||
if (isDirty()) return "저장 안 한 식이 있음 — [식 저장] 먼저";
|
if (isDirty()) return "저장 안 한 식이 있음 — [식 저장] 먼저";
|
||||||
const whom = tier === "program" ? "모든 회사가 쓰는 프로그램 기본" : "우리 회사 라이브러리";
|
const whom = tier === "program" ? "모든 회사가 쓰는 프로그램 기본" : "우리 회사 라이브러리";
|
||||||
if (!window.confirm(`이 장의 양식과 고친 식을 ${whom}에 발행 — 같은 종류가 있으면 덮어씀`)) {
|
const masked =
|
||||||
|
tier === "program" && options.originProject
|
||||||
|
? `\n이 항목은 「${options.originProject}」에서 뽑은 것 — 공사명은 빼고 발행됩니다`
|
||||||
|
: "";
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
`이 장의 양식과 고친 식을 ${whom}에 발행 — 같은 종류가 있으면 덮어씀${masked}`,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
await readJson(
|
await readJson(
|
||||||
|
|||||||
@@ -261,6 +261,41 @@ def test_발행은_권한대로_회사_기본_단에_같은_모양으로(client:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_프로그램_기본_발행본엔_원문_공사명_파일명을_안_싣는다(tmp_path: Path) -> None:
|
||||||
|
"""2026-09-14 브레인 ② — 모든 회사로 가는 발행본에서만 가림 · 원본·회사 단은 그대로(③)."""
|
||||||
|
origin = {
|
||||||
|
"kind": "stmate_xlsx",
|
||||||
|
"file": "봉화.xlsx",
|
||||||
|
"project": "2024년 봉화 임도",
|
||||||
|
"hopyo_no": 6,
|
||||||
|
}
|
||||||
|
template = {**load_template("masonry_wet"), "origin": origin}
|
||||||
|
program = library_module.save_personal(tmp_path / "program", template, None, None, "program")
|
||||||
|
company = library_module.save_personal(tmp_path / "company", template, None, None, "company")
|
||||||
|
published = json.loads((tmp_path / "program" / f"{program}.json").read_text(encoding="utf-8"))
|
||||||
|
kept = json.loads((tmp_path / "company" / f"{company}.json").read_text(encoding="utf-8"))
|
||||||
|
assert "project" not in published["origin"] and "file" not in published["origin"]
|
||||||
|
assert published["origin"]["masked"] == "원문에서 뽑음 — 공사명은 발행 시 가림"
|
||||||
|
assert published["origin"]["kind"] == "stmate_xlsx"
|
||||||
|
assert kept["origin"] == origin
|
||||||
|
assert template["origin"] == origin # 원본은 안 건드림
|
||||||
|
|
||||||
|
|
||||||
|
def test_발행_확인창이_가릴_공사명을_보인다() -> None:
|
||||||
|
ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet_Library.ts").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert "originProject" in ui and "공사명은 빼고 발행됩니다" in ui
|
||||||
|
sheet_ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet.ts").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert "originProject: sheet.library_item.origin_project" in sheet_ui
|
||||||
|
template_py = (ROOT / "B08_Quantity" / "B08_Quantity_Engine_StructureTemplate.py").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert '"origin_project"' in template_py
|
||||||
|
|
||||||
|
|
||||||
def test_회사_없는_사람은_개인_단을_못_쓴다(client: TestClient) -> None:
|
def test_회사_없는_사람은_개인_단을_못_쓴다(client: TestClient) -> None:
|
||||||
client.app.dependency_overrides[verify_session] = lambda: {"company_id": None, "user_id": 1}
|
client.app.dependency_overrides[verify_session] = lambda: {"company_id": None, "user_id": 1}
|
||||||
sheet = _sheet(client)
|
sheet = _sheet(client)
|
||||||
|
|||||||
Reference in New Issue
Block a user