Compare commits
3
Commits
72ec6c2135
...
3ca1a45b2b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ca1a45b2b | ||
|
|
0bb14ce2b3 | ||
|
|
ecd25ae9ee |
@@ -19,8 +19,9 @@ from typing import Any
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureTemplate import TEMPLATE_DIR
|
||||
from config.config_system import STORAGE_BASE_DIR
|
||||
|
||||
#: 단 이름 — 고르개에 이 차례로 보임(가까운 것부터).
|
||||
TIERS = ("personal", "company", "program")
|
||||
#: 단 이름 — 고르개에 이 차례로 보임(가까운 것부터). `received` = 동료가 보낸 것(공유 · 개인 단을 안 덮음).
|
||||
TIERS = ("personal", "received", "company", "program")
|
||||
RECEIVED_SUBDIR = "library_received"
|
||||
#: 항목 코드 — 파일 이름이 되므로 모양을 먼저 봄(경로 벗어남 막이).
|
||||
CODE_PATTERN = re.compile(r"^AX-ST-[0-9a-f]{8}$")
|
||||
LIBRARY_SUBDIR = "library"
|
||||
@@ -28,6 +29,9 @@ LIBRARY_SUBDIR = "library"
|
||||
|
||||
#: 프로그램 기본 작업본 자리 — 회사 번호 폴더와 안 겹치게 밑줄 이름.
|
||||
PROGRAM_SUBDIR = "_program"
|
||||
#: 프로그램 기본 발행본에서 뺄 출처 칸 — 어느 공사인지(2026-09-14 브레인 판정 ②).
|
||||
ORIGIN_MASKED_KEYS = ("project", "file")
|
||||
ORIGIN_MASKED_NOTE = "원문에서 뽑음 — 공사명은 발행 시 가림"
|
||||
|
||||
|
||||
def program_library_dir() -> Path:
|
||||
@@ -52,6 +56,7 @@ def tier_dirs(company_id: Any, user_id: Any) -> dict[str, Path]:
|
||||
company = Path(STORAGE_BASE_DIR).resolve() / str(company_id)
|
||||
if user_id is not None:
|
||||
dirs["personal"] = company / str(user_id) / LIBRARY_SUBDIR
|
||||
dirs["received"] = company / str(user_id) / RECEIVED_SUBDIR
|
||||
dirs["company"] = company / LIBRARY_SUBDIR
|
||||
dirs["program"] = program_library_dir()
|
||||
return dirs
|
||||
@@ -194,6 +199,10 @@ def save_personal(
|
||||
for row in overridden_rows(template, overrides)
|
||||
]
|
||||
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:
|
||||
item["unit_price"] = {**(item.get("unit_price") or {}), "rows": unit_price_rows}
|
||||
_write(folder, {**item, "code": code, "library_tier": tier, "rows": rows})
|
||||
@@ -223,3 +232,13 @@ def pin_program_templates(project_root: str | Path) -> int:
|
||||
import_item(project_root, item, "program")
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def share_item(item: dict[str, Any], to_folder: Path, sender: dict[str, Any]) -> str:
|
||||
"""[동료에게 보내기] — 내 개인 단 항목을 동료의 **받음 단**에 복사(개인 단을 안 덮음 · 브레인 판정 ①).
|
||||
|
||||
코드는 그대로(다시 보내면 같은 자리를 새로 씀) · 출처 공사명도 그대로(같은 회사 · 판정 ③).
|
||||
"""
|
||||
body = {k: v for k, v in item.items() if k != "imported_from"}
|
||||
_write(to_folder, {**body, "library_tier": "received", "received_from": sender})
|
||||
return str(body["code"])
|
||||
|
||||
@@ -441,6 +441,8 @@ def apply_templates(
|
||||
"name": template.get("name"),
|
||||
"code": template.get("code"),
|
||||
"imported_from": template.get("imported_from"),
|
||||
# 발행 확인창이 「○○공사에서 뽑은 것 — 공사명은 빼고 발행」을 보이게(브레인 ②).
|
||||
"origin_project": (template.get("origin") or {}).get("project"),
|
||||
}
|
||||
sheet["formula_sheet"] = body
|
||||
# 실무 관측값 같은 「대안 후보」 — 값을 바꾸지 않고 칸 옆에 보이기만(판정 Ⓑ).
|
||||
|
||||
@@ -121,7 +121,7 @@ class LibraryCloneRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
type_id: str = Field(min_length=1, max_length=100)
|
||||
tier: Literal["company", "program"]
|
||||
tier: Literal["received", "company", "program"]
|
||||
code: str = Field(pattern=r"^AX-ST-[0-9a-f]{8}$")
|
||||
|
||||
|
||||
@@ -149,3 +149,71 @@ async def clone_library_item(
|
||||
body["cloned_from"] = {"tier": payload.tier, "code": payload.code, "name": item.get("name")}
|
||||
code = await asyncio.to_thread(save_item_personal, folder, body)
|
||||
return JSONResponse(content={"status": "success", "code": code, "name": item.get("name")})
|
||||
|
||||
|
||||
async def _company_members(company_id: int) -> list[dict[str, Any]]:
|
||||
"""같은 회사 구성원 — B01 구성원 저장소를 읽기만(시험이 갈아 끼움)."""
|
||||
from B01_Dashboard.B01_Dashboard_Repository_Members import list_company_members
|
||||
|
||||
return await list_company_members(company_id)
|
||||
|
||||
|
||||
@router.get("/{project_id}/quantity/structure-sheets/library/colleagues")
|
||||
async def list_library_colleagues(
|
||||
project_id: UUID, session: dict[str, Any] = Depends(verify_session)
|
||||
) -> JSONResponse:
|
||||
"""④ 보낼 동료 — 같은 회사에서 나를 뺀 사람(이름만)."""
|
||||
company_id = session.get("company_id")
|
||||
if company_id is None:
|
||||
return _error(403, "회사에 속한 사용자만 보냅니다.")
|
||||
members = await _company_members(int(company_id))
|
||||
colleagues = [
|
||||
{"id": m["id"], "name": m.get("name") or m.get("email") or ""}
|
||||
for m in members
|
||||
if m["id"] != session.get("user_id")
|
||||
]
|
||||
return JSONResponse(content={"status": "success", "colleagues": colleagues})
|
||||
|
||||
|
||||
class LibraryShareRequest(BaseModel):
|
||||
"""내 개인 단 항목 하나를 같은 회사 동료에게."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
type_id: str = Field(min_length=1, max_length=100)
|
||||
code: str = Field(pattern=r"^AX-ST-[0-9a-f]{8}$")
|
||||
to_user_id: int
|
||||
|
||||
|
||||
@router.put("/{project_id}/quantity/structure-sheets/library/share")
|
||||
async def share_library_item(
|
||||
project_id: UUID,
|
||||
payload: LibraryShareRequest,
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
) -> JSONResponse:
|
||||
"""④ [동료에게 보내기] — 받는 쪽 「받음」 단에 복사(PLAN 4장 공유 · 브레인 판정 ①)."""
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureLibrary import (
|
||||
RECEIVED_SUBDIR,
|
||||
find_item,
|
||||
share_item,
|
||||
tier_dirs,
|
||||
)
|
||||
|
||||
company_id = session.get("company_id")
|
||||
dirs = tier_dirs(company_id, session.get("user_id"))
|
||||
if "personal" not in dirs:
|
||||
return _error(403, "회사에 속한 사용자만 보냅니다.")
|
||||
if payload.to_user_id == session.get("user_id"):
|
||||
return _error(400, "나에게는 보내지 않습니다.")
|
||||
members = {m["id"]: m for m in await _company_members(int(company_id))}
|
||||
target = members.get(payload.to_user_id)
|
||||
if target is None:
|
||||
return _error(404, "같은 회사에서 받는 사람을 찾지 못했습니다.")
|
||||
item = await asyncio.to_thread(find_item, dirs, "personal", payload.code)
|
||||
if item is None or item.get("type_id") != payload.type_id:
|
||||
return _error(404, "보낼 내 항목을 찾지 못했습니다.")
|
||||
me = members.get(session.get("user_id")) or {}
|
||||
sender = {"user_id": session.get("user_id"), "name": me.get("name") or me.get("email") or ""}
|
||||
to_folder = dirs["personal"].parents[1] / str(payload.to_user_id) / RECEIVED_SUBDIR
|
||||
code = await asyncio.to_thread(share_item, item, to_folder, sender)
|
||||
return JSONResponse(content={"status": "success", "code": code, "to": target.get("name") or ""})
|
||||
|
||||
@@ -327,7 +327,7 @@ class LibraryImportRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
type_id: str = Field(min_length=1, max_length=100)
|
||||
tier: Literal["personal", "company", "program"]
|
||||
tier: Literal["personal", "received", "company", "program"]
|
||||
code: str = Field(pattern=r"^AX-ST-[0-9a-f]{8}$")
|
||||
|
||||
|
||||
|
||||
@@ -86,6 +86,8 @@ export interface StructureSheet extends StandardSheetSpec {
|
||||
name: string;
|
||||
code?: string | null;
|
||||
imported_from?: string | null;
|
||||
/** 뽑아 온 원문 공사명(STmate 고정형) — 프로그램 기본 발행 때 가림. */
|
||||
origin_project?: string | null;
|
||||
};
|
||||
/** 화면이 조작 중 왕복 없이 다시 풀 장 한 벌(L=1, 고친 식 얹힘). */
|
||||
formula_sheet?: FormulaSheet;
|
||||
@@ -422,6 +424,7 @@ export function renderStructureSheets(projectId: string | null): HTMLElement {
|
||||
sheetKey: sheet.key,
|
||||
typeId: sheet.library_item.type_id,
|
||||
currentCode: sheet.library_item.code ?? null,
|
||||
originProject: sheet.library_item.origin_project,
|
||||
isDirty: () => dirty,
|
||||
confirmTake: () => {
|
||||
const edited = editedRows(sheet);
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { buildStmatePanel } from "./B08_Quantity_UI_StructureSheet_Stmate";
|
||||
|
||||
const TIER_LABELS: Record<string, string> = { personal: "개인", company: "회사", program: "기본" };
|
||||
const TIER_LABELS: Record<string, string> = {
|
||||
personal: "개인",
|
||||
received: "받음",
|
||||
company: "회사",
|
||||
program: "기본",
|
||||
};
|
||||
/** 항목 종류 배지 — 양식형 = 제원을 바꾸면 수량이 다시 남 · 고정형 = 박힌 수량(명세 13장). */
|
||||
const KIND_LABELS: Record<string, string> = { form: "양식형", fixed: "고정형" };
|
||||
|
||||
@@ -52,6 +57,8 @@ export interface LibraryPanelOptions {
|
||||
/** 저장 안 한 식이 있으면 [내 라이브러리에 저장]을 막음 — 저장된 식만 개인 단으로 감. */
|
||||
isDirty: () => boolean;
|
||||
onImported: (notes: string[]) => Promise<void>;
|
||||
/** 이 장 양식이 뽑아 온 원문 공사명 — 프로그램 기본 발행 확인창에 「빼고 발행」을 알림. */
|
||||
originProject?: string | null;
|
||||
}
|
||||
|
||||
/** 가져오기 · [내 라이브러리에 저장] · [내 것 지우기] 칸. 개인 단 두 단추는 프로젝트를 안 바꿈. */
|
||||
@@ -88,10 +95,63 @@ export function buildLibraryPanel(options: LibraryPanelOptions): HTMLElement {
|
||||
clone.hidden = true;
|
||||
/** 목록을 다시 받은 뒤 보일 한 줄 — 받는 동안 상태 줄이 지워져 복제 결과가 사라지지 않게. */
|
||||
let afterLoad = "";
|
||||
// 공유 — 내 개인 단 항목을 같은 회사 동료의 「받음」 단으로(브레인 판정 ①). 동료는 누를 때 받음.
|
||||
const send = document.createElement("button");
|
||||
send.type = "button";
|
||||
send.className = "b08-quantity__tab";
|
||||
send.textContent = "동료에게 보내기";
|
||||
send.hidden = true;
|
||||
const who = document.createElement("select");
|
||||
who.className = "b08-spec__input";
|
||||
who.hidden = true;
|
||||
const syncClone = (): void => {
|
||||
const [tier] = list.value.split("|");
|
||||
clone.disabled = tier === "personal";
|
||||
send.disabled = tier !== "personal";
|
||||
};
|
||||
send.addEventListener("click", () => {
|
||||
const [tier, code] = list.value.split("|");
|
||||
if (tier !== "personal" || !code) return;
|
||||
void (async () => {
|
||||
send.disabled = true;
|
||||
try {
|
||||
if (who.hidden) {
|
||||
const { colleagues } = await readJson<{ colleagues: { id: number; name: string }[] }>(
|
||||
await fetch(libraryUrl(projectId, "/colleagues"), { credentials: "include" }),
|
||||
);
|
||||
who.replaceChildren(...colleagues.map((c) => new Option(c.name, String(c.id))));
|
||||
who.hidden = colleagues.length === 0;
|
||||
status.textContent = colleagues.length
|
||||
? "받을 동료를 고르고 한 번 더 누를 것"
|
||||
: "보낼 동료가 없음";
|
||||
return;
|
||||
}
|
||||
const label = list.selectedOptions[0]?.textContent ?? "";
|
||||
const name = who.selectedOptions[0]?.textContent ?? "";
|
||||
if (
|
||||
!window.confirm(
|
||||
`「${label}」을 ${name}에게 보냄 — 받는 쪽 목록에 「받음」으로 뜸 · 그 사람 것은 안 덮음`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await readJson(
|
||||
await fetch(libraryUrl(projectId, "/share"), {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ type_id: typeId, code, to_user_id: Number(who.value) }),
|
||||
}),
|
||||
);
|
||||
who.hidden = true;
|
||||
status.textContent = `${name}에게 보냄`;
|
||||
} catch (error) {
|
||||
status.textContent = error instanceof Error ? error.message : "보내지 못함";
|
||||
} finally {
|
||||
syncClone();
|
||||
}
|
||||
})();
|
||||
});
|
||||
list.addEventListener("change", syncClone);
|
||||
clone.addEventListener("click", () => {
|
||||
const [tier, code] = list.value.split("|");
|
||||
@@ -147,7 +207,7 @@ export function buildLibraryPanel(options: LibraryPanelOptions): HTMLElement {
|
||||
return option;
|
||||
}),
|
||||
);
|
||||
list.hidden = take.hidden = clone.hidden = items.length === 0;
|
||||
list.hidden = take.hidden = clone.hidden = send.hidden = items.length === 0;
|
||||
syncClone();
|
||||
// 발행 단추 — 서버가 준 권한대로만 보임(회사 = 마스터 · 기본 = 시스템 관리자).
|
||||
toCompany.hidden = !canPublish?.company;
|
||||
@@ -243,7 +303,15 @@ export function buildLibraryPanel(options: LibraryPanelOptions): HTMLElement {
|
||||
const button = personal(label, async () => {
|
||||
if (isDirty()) return "저장 안 한 식이 있음 — [식 저장] 먼저";
|
||||
const whom = tier === "program" ? "모든 회사가 쓰는 프로그램 기본" : "우리 회사 라이브러리";
|
||||
if (!window.confirm(`이 장의 양식과 고친 식을 ${whom}에 발행 — 같은 종류가 있으면 덮어씀`)) {
|
||||
const masked =
|
||||
tier === "program" && options.originProject
|
||||
? `\n이 항목은 「${options.originProject}」에서 뽑은 것 — 공사명은 빼고 발행됩니다`
|
||||
: "";
|
||||
if (
|
||||
!window.confirm(
|
||||
`이 장의 양식과 고친 식을 ${whom}에 발행 — 같은 종류가 있으면 덮어씀${masked}`,
|
||||
)
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
await readJson(
|
||||
@@ -265,7 +333,7 @@ export function buildLibraryPanel(options: LibraryPanelOptions): HTMLElement {
|
||||
mine.className = "b08-sheet__actions";
|
||||
mine.append(save, remove, toCompany, toProgram);
|
||||
|
||||
panel.append(title, scope, load, list, take, clone, mine, status);
|
||||
panel.append(title, scope, load, list, take, clone, send, who, mine, status);
|
||||
// 고정형 항목을 만드는 둘째 길 — STmate 출력 엑셀에서 호표 하나를 뽑아 개인 단에(PLAN 4장).
|
||||
panel.append(
|
||||
buildStmatePanel({
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""라이브러리 공유 — 같은 회사 동료에게 내 항목을 복사해 보냄 (PLAN 4장 · 2026-09-14 브레인 판정 ①).
|
||||
|
||||
⚠ 받는 쪽이 모르게 들어가지 않음 — 받은 항목은 그 사람 개인 단을 덮지 않고 **「받음」 단**에 따로 섬
|
||||
(개인 단은 종류당 하나라 그대로 넣으면 동료가 고친 내 것이 조용히 덮임). 받은 뒤 가져오기·복제는 받는 사람 몫.
|
||||
⚠ 같은 회사 안만 · 출처 공사명은 그대로(같은 회사라 그 공사를 앎 · 판정 ③).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import B08_Quantity.B08_Quantity_Engine_StructureLibrary as library_module # noqa: E402
|
||||
import B08_Quantity.B08_Quantity_Router_StmateLibrary as router_module # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureTemplate import load_template # noqa: E402
|
||||
from common_util.common_util_auth import verify_session # noqa: E402
|
||||
|
||||
LIB = "/api/projects/33333333-3333-3333-3333-333333333333/quantity/structure-sheets/library"
|
||||
MINE = "AX-ST-0000aaaa"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient:
|
||||
monkeypatch.setattr(library_module, "STORAGE_BASE_DIR", str(tmp_path / "storage"))
|
||||
item = {
|
||||
**load_template("masonry_wet"),
|
||||
"code": MINE,
|
||||
"library_tier": "personal",
|
||||
"name": "내 돌쌓기",
|
||||
"origin": {"project": "봉화 임도"},
|
||||
}
|
||||
folder = tmp_path / "storage" / "7" / "42" / "library"
|
||||
folder.mkdir(parents=True)
|
||||
(folder / f"{MINE}.json").write_text(json.dumps(item), encoding="utf-8")
|
||||
# 받는 사람(43)도 같은 종류 내 것이 있음 — 덮이면 안 됨
|
||||
theirs = tmp_path / "storage" / "7" / "43" / "library"
|
||||
theirs.mkdir(parents=True)
|
||||
(theirs / "AX-ST-0000bbbb.json").write_text(
|
||||
json.dumps({**item, "code": "AX-ST-0000bbbb", "name": "동료 것"}), encoding="utf-8"
|
||||
)
|
||||
|
||||
async def members(company_id: int) -> list[dict]:
|
||||
assert company_id == 7
|
||||
return [
|
||||
{"id": 42, "name": "나", "email": "me@x"},
|
||||
{"id": 43, "name": "동료", "email": "you@x"},
|
||||
]
|
||||
|
||||
monkeypatch.setattr(router_module, "_company_members", members)
|
||||
app = FastAPI()
|
||||
app.include_router(router_module.router)
|
||||
app.dependency_overrides[verify_session] = lambda: {"company_id": 7, "user_id": 42}
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_동료_목록은_같은_회사에서_나를_뺀다(client: TestClient) -> None:
|
||||
body = client.get(f"{LIB}/colleagues").json()
|
||||
assert body["colleagues"] == [{"id": 43, "name": "동료"}]
|
||||
|
||||
|
||||
def test_보내면_받는_쪽_받음_단에_서고_그_사람_개인_단은_안_덮인다(
|
||||
client: TestClient, tmp_path: Path
|
||||
) -> None:
|
||||
sent = client.put(
|
||||
f"{LIB}/share", json={"type_id": "masonry_wet", "code": MINE, "to_user_id": 43}
|
||||
)
|
||||
assert sent.status_code == 200, sent.text
|
||||
received = tmp_path / "storage" / "7" / "43" / "library_received" / f"{MINE}.json"
|
||||
saved = json.loads(received.read_text(encoding="utf-8"))
|
||||
assert saved["library_tier"] == "received"
|
||||
assert saved["received_from"]["name"] == "나" and saved["origin"]["project"] == "봉화 임도"
|
||||
theirs = tmp_path / "storage" / "7" / "43" / "library" / "AX-ST-0000bbbb.json"
|
||||
assert json.loads(theirs.read_text(encoding="utf-8"))["name"] == "동료 것"
|
||||
dirs = library_module.tier_dirs(7, 43)
|
||||
listed = library_module.list_items(dirs, "masonry_wet")
|
||||
assert [i["tier"] for i in listed][:2] == ["personal", "received"]
|
||||
|
||||
|
||||
def test_회사_밖이나_내_것_아닌_항목은_못_보낸다(client: TestClient) -> None:
|
||||
stranger = client.put(
|
||||
f"{LIB}/share", json={"type_id": "masonry_wet", "code": MINE, "to_user_id": 99}
|
||||
)
|
||||
assert stranger.status_code == 404
|
||||
missing = client.put(
|
||||
f"{LIB}/share", json={"type_id": "masonry_wet", "code": "AX-ST-0000cccc", "to_user_id": 43}
|
||||
)
|
||||
assert missing.status_code == 404
|
||||
myself = client.put(
|
||||
f"{LIB}/share", json={"type_id": "masonry_wet", "code": MINE, "to_user_id": 42}
|
||||
)
|
||||
assert myself.status_code == 400
|
||||
|
||||
|
||||
def test_화면에_보내기_단추와_받음_표시() -> None:
|
||||
ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet_Library.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "/share" in ui and "/colleagues" in ui and "동료에게 보내기" in ui
|
||||
assert 'received: "받음"' in ui
|
||||
@@ -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:
|
||||
client.app.dependency_overrides[verify_session] = lambda: {"company_id": None, "user_id": 1}
|
||||
sheet = _sheet(client)
|
||||
|
||||
Reference in New Issue
Block a user