feat(B07): 도각 읽는 차례 = 프로젝트 작업본 → 없으면 시스템 · [도각 편집] 저장은 작업본 · [기본 도각으로] = 프로젝트 _initial (PLAN 10-3 · 10-5)
- Engine_Template — M02_Template_Layers 자리 함수로 읽고 씀 · 회사 층(storage/{회사}/templates/) 읽기 뺌
- use_project_templates · save_project_template · reset_project_template(_initial 없으면 작업본 지움) · is_project_template_customized
- Router · Router_Frame — 프로젝트 폴더를 넘김
- 도각 편집 띠 · 되돌리기 글을 프로젝트 도각으로
- 시험 test_m02_drawing_area.py — 옛 프로젝트 · 씨앗 뿌린 프로젝트 저장 · 되돌리기
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxvYb5ufV1kWdBvZbpDfu6
This commit is contained in:
@@ -175,7 +175,7 @@ export interface FrameTemplateResponse {
|
||||
status: string;
|
||||
project_id: string;
|
||||
drawing: CadDrawing;
|
||||
/** 회사가 고친 도각을 쓰고 있으면 true, 프로그램 기본 도각이면 false. */
|
||||
/** 프로젝트 도각이 처음 복사한 도각과 다르면 true. */
|
||||
customized: boolean;
|
||||
/** 자리표에 보여 줄 실제 값 — 편집 화면 전용이고 저장값은 토큰 그대로다. */
|
||||
fields?: Record<string, string>;
|
||||
@@ -237,7 +237,7 @@ export async function exportDrawing(
|
||||
};
|
||||
}
|
||||
|
||||
/** 회사 도각을 지우고 프로그램 기본 도각으로 되돌린다. */
|
||||
/** 프로젝트 도각을 처음 복사한 도각(`_initial/`)으로 되돌린다. */
|
||||
export function resetFrameTemplate(projectId: string): Promise<void> {
|
||||
return requestJson(`/projects/${projectId}/frame-template`, {
|
||||
method: "DELETE",
|
||||
|
||||
@@ -27,8 +27,19 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
FRAME_LAYER_ID,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Svg import fit_polylines, svg_polylines
|
||||
from M02_MasterTemplete.M02_Template_Layers import (
|
||||
delete_template,
|
||||
initial_dir,
|
||||
project_dir,
|
||||
reset_project,
|
||||
system_dir,
|
||||
template_path,
|
||||
version_of,
|
||||
write_template,
|
||||
)
|
||||
|
||||
_TEMPLATE_DIR = Path(__file__).resolve().parent.parent / "resources" / "master_template" / "drawing"
|
||||
# 도면 양식은 M02 양식 층(PLAN 10-5)의 `drawing` 종류다.
|
||||
_KIND = "drawing"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,23 +55,21 @@ _A1_INNER = (42.0, 47.0, 812.0, 567.0)
|
||||
# 비워 횡단 장이 한 장 더 늘었다 — 2%로 줄여 작도 영역을 쓴다(2026-08-30 사용자).
|
||||
_CONTENT_MARGIN = 0.02
|
||||
|
||||
# 회사가 자기 도각을 두는 자리 — `storage/{회사}/templates/`. 프로그램 기본 도각
|
||||
# (`resources/master_template/drawing/`)은 **읽기 전용**이고, 고객이 고친 도각은 여기 쌓인다
|
||||
# (2026-09-01 사용자 확정: "정본은 그냥 두고 수정하는 기능").
|
||||
COMPANY_TEMPLATE_SUBDIR = "templates"
|
||||
|
||||
# 이 요청이 읽을 회사 도각 폴더. 라우터가 요청마다 세운다 — 엔진 6개(종단·횡단장·
|
||||
# 읽는 차례 = 프로젝트 작업본(`{프로젝트}/templates/drawing/`) → 없으면 시스템 양식
|
||||
# (`resources/master_template/drawing/`, 읽기 전용). 설계자가 고친 도각은 작업본에 쌓인다.
|
||||
#
|
||||
# 이 요청이 읽을 프로젝트 폴더. 라우터가 요청마다 세운다 — 엔진 6개(종단·횡단장·
|
||||
# 토적도·유역도·표지·공용)의 서명을 줄줄이 고치지 않으려고 문맥 변수를 쓴다.
|
||||
# `asyncio.to_thread`가 문맥을 복사하므로 스레드로 넘어간 작도에도 그대로 따라간다.
|
||||
_company_dir: ContextVar[Path | None] = ContextVar("b07_company_template_dir", default=None)
|
||||
_project_root: ContextVar[Path | None] = ContextVar("b07_project_template_root", default=None)
|
||||
|
||||
|
||||
def use_company_templates(company_dir: Path | None) -> None:
|
||||
"""이 요청이 읽을 회사 도각 폴더를 정한다. None이면 프로그램 기본 도각."""
|
||||
_company_dir.set(company_dir)
|
||||
def use_project_templates(project_root: Path | None) -> None:
|
||||
"""이 요청이 읽을 프로젝트 폴더를 정한다. None이면 시스템 양식."""
|
||||
_project_root.set(project_root)
|
||||
|
||||
|
||||
# 이 요청이 도각 표제란에 채울 값. 회사 도각 폴더와 같은 이유로 문맥 변수다 —
|
||||
# 이 요청이 도각 표제란에 채울 값. 프로젝트 폴더와 같은 이유로 문맥 변수다 —
|
||||
# 엔진 6개의 서명을 줄줄이 고치지 않는다. 값을 못 구한 자리는 **빈칸**으로 남는다.
|
||||
_title_fields: ContextVar[dict[str, str]] = ContextVar("b07_title_fields", default={})
|
||||
|
||||
@@ -79,9 +88,14 @@ def add_title_fields(extra: dict[str, str]) -> None:
|
||||
_title_fields.set({**_title_fields.get(), **(extra or {})})
|
||||
|
||||
|
||||
def company_template_path(company_dir: Path, name: str = A1_TEMPLATE) -> Path:
|
||||
"""회사 도각 파일 경로(없을 수도 있다)."""
|
||||
return Path(company_dir) / COMPANY_TEMPLATE_SUBDIR / f"{name}.json"
|
||||
def project_template_path(project_root: Path, name: str = A1_TEMPLATE) -> Path:
|
||||
"""프로젝트 작업본 도각 파일 경로(없을 수도 있다)."""
|
||||
return template_path(project_dir(project_root), _KIND, name)
|
||||
|
||||
|
||||
def system_template_path(name: str = A1_TEMPLATE) -> Path:
|
||||
"""시스템 도각 파일 경로."""
|
||||
return template_path(system_dir(), _KIND, name)
|
||||
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
@@ -91,13 +105,13 @@ def _read_template(path_str: str, mtime_ns: int) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _load_template(name: str) -> dict[str, Any] | None:
|
||||
"""회사 도각이 있으면 그것, 없으면 프로그램 기본 도각."""
|
||||
company_dir = _company_dir.get()
|
||||
if company_dir is not None:
|
||||
override = company_template_path(company_dir, name)
|
||||
"""프로젝트 작업본이 있으면 그것, 없으면 시스템 양식(옛 프로젝트)."""
|
||||
project_root = _project_root.get()
|
||||
if project_root is not None:
|
||||
override = project_template_path(project_root, name)
|
||||
if override.is_file():
|
||||
return _read_template(str(override), override.stat().st_mtime_ns)
|
||||
path = _TEMPLATE_DIR / f"{name}.json"
|
||||
path = system_template_path(name)
|
||||
if not path.is_file():
|
||||
return None
|
||||
return _read_template(str(path), path.stat().st_mtime_ns)
|
||||
@@ -175,29 +189,33 @@ def validate_template_entities(entities: list[dict[str, Any]]) -> None:
|
||||
_check(entity, f"#{index}")
|
||||
|
||||
|
||||
def clear_company_template(company_dir: Path, name: str = A1_TEMPLATE) -> bool:
|
||||
"""회사 도각을 지워 프로그램 기본 도각으로 되돌린다. 지울 것이 없으면 False."""
|
||||
path = company_template_path(company_dir, name)
|
||||
if not path.is_file():
|
||||
def is_project_template_customized(project_root: Path, name: str = A1_TEMPLATE) -> bool:
|
||||
"""작업본이 처음 복사한 도각(`_initial/`, 없으면 시스템)과 다른가."""
|
||||
current = version_of(project_template_path(project_root, name))
|
||||
if current is None:
|
||||
return False
|
||||
path.unlink()
|
||||
return True
|
||||
initial = version_of(template_path(initial_dir(project_root), _KIND, name))
|
||||
return current != (initial or version_of(system_template_path(name)))
|
||||
|
||||
|
||||
def save_company_template(
|
||||
company_dir: Path, entities: list[dict[str, Any]], name: str = A1_TEMPLATE
|
||||
def reset_project_template(project_root: Path, name: str = A1_TEMPLATE) -> bool:
|
||||
"""[기본 도각으로] — 처음 복사한 도각(`_initial/`)을 작업본에 덮어쓴다.
|
||||
|
||||
`_initial/` 이 없는 옛 프로젝트는 작업본을 지워 시스템 양식으로 떨어뜨린다.
|
||||
되돌릴 것이 없으면 False.
|
||||
"""
|
||||
if reset_project(project_root, kind=_KIND, name=name):
|
||||
return True
|
||||
return delete_template(project_dir(project_root), _KIND, name)
|
||||
|
||||
|
||||
def save_project_template(
|
||||
project_root: Path, entities: list[dict[str, Any]], name: str = A1_TEMPLATE
|
||||
) -> Path:
|
||||
"""편집한 도각을 회사 도각 파일로 저장한다. 프로그램 기본 도각은 건드리지 않는다."""
|
||||
"""편집한 도각을 프로젝트 작업본으로 저장한다. 시스템 양식 · `_initial/` 은 건드리지 않는다."""
|
||||
validate_template_entities(entities)
|
||||
path = company_template_path(company_dir, name)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# 작도 영역은 CAD 가 모르는 칸이라 편집본에 없다 — 고치기 전 양식의 값을 이어 받는다.
|
||||
previous = path if path.is_file() else _TEMPLATE_DIR / f"{name}.json"
|
||||
area = (
|
||||
json.loads(previous.read_text(encoding="utf-8")).get("drawing_area")
|
||||
if previous.is_file()
|
||||
else None
|
||||
)
|
||||
area = (_load_template_at(project_root, name) or {}).get("drawing_area")
|
||||
document = {
|
||||
"format": DRAWING_FORMAT,
|
||||
"source": "B07 도각 편집 화면",
|
||||
@@ -206,10 +224,17 @@ def save_company_template(
|
||||
"entities": [{**entity, "layerId": FRAME_LAYER_ID} for entity in entities],
|
||||
"layers": [{"id": FRAME_LAYER_ID, "name": "도각", "isVisible": True, "isLocked": False}],
|
||||
}
|
||||
temporary = path.with_suffix(".tmp")
|
||||
temporary.write_text(json.dumps(document, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
temporary.replace(path)
|
||||
return path
|
||||
write_template(project_dir(project_root), _KIND, name, document)
|
||||
return project_template_path(project_root, name)
|
||||
|
||||
|
||||
def _load_template_at(project_root: Path, name: str) -> dict[str, Any] | None:
|
||||
"""요청 문맥과 상관없이 이 프로젝트가 읽는 도각."""
|
||||
token = _project_root.set(project_root)
|
||||
try:
|
||||
return _load_template(name)
|
||||
finally:
|
||||
_project_root.reset(token)
|
||||
|
||||
|
||||
def entities_bbox(entities: list[dict[str, Any]]) -> tuple[float, float, float, float] | None:
|
||||
|
||||
@@ -28,7 +28,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import (
|
||||
extract_quantity_table,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
use_company_templates,
|
||||
use_project_templates,
|
||||
use_title_fields,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support import (
|
||||
@@ -292,9 +292,8 @@ async def get_design_drawing(
|
||||
"""선택한 도면 원본 한 건만 읽어 CAD 스키마로 변환한다."""
|
||||
try:
|
||||
route_id, project_root, longitudinal_path, bypass = await _confirmed_source(project_id)
|
||||
# 이 회사가 고친 도각이 있으면 그것으로 그린다(없으면 프로그램 기본 도각).
|
||||
# 저장 경로는 `storage/{회사}/{사용자}/{프로젝트}` 이므로 두 단계 위가 회사 폴더다.
|
||||
use_company_templates(project_root.parent.parent)
|
||||
# 프로젝트 작업본 도각이 있으면 그것으로 그린다(없으면 시스템 양식 — 옛 프로젝트).
|
||||
use_project_templates(project_root)
|
||||
# 표제란 값도 같은 요청 문맥에 세운다 — 값이 없는 칸은 빈칸으로 나간다.
|
||||
use_title_fields(await title_block_fields(project_id))
|
||||
# 횡단도는 B06 지정 설계를 먼저 읽어 CAD 계획선(design_line)과 응답에 함께 쓴다.
|
||||
|
||||
@@ -18,12 +18,12 @@ from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Export import export_drawing
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Import import import_frame_file
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
clear_company_template,
|
||||
company_template_path,
|
||||
frame_document,
|
||||
frame_template_document,
|
||||
save_company_template,
|
||||
use_company_templates,
|
||||
is_project_template_customized,
|
||||
reset_project_template,
|
||||
save_project_template,
|
||||
use_project_templates,
|
||||
validate_template_entities,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router import title_block_fields
|
||||
@@ -42,28 +42,27 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B07 Design Detail"])
|
||||
|
||||
|
||||
async def _company_dir(project_id: UUID) -> Path:
|
||||
"""프로젝트 저장 경로에서 회사 폴더를 얻는다 — `storage/{회사}/{사용자}/{프로젝트}`."""
|
||||
async def _project_root(project_id: UUID) -> Path:
|
||||
"""프로젝트 저장 폴더 — `storage/{회사}/{사용자}/{프로젝트}`. 도각 작업본이 여기 산다."""
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
root = Path(resolve_stored_project_path(stored_path)).resolve()
|
||||
return root.parent.parent
|
||||
return Path(resolve_stored_project_path(stored_path)).resolve()
|
||||
|
||||
|
||||
@router.get("/{project_id}/frame-template", response_model=FrameTemplateResponse)
|
||||
async def get_frame_template(project_id: UUID) -> FrameTemplateResponse | JSONResponse:
|
||||
"""도각 편집 화면이 실을 도각 한 장. 회사 도각이 있으면 그것, 없으면 프로그램 기본."""
|
||||
"""도각 편집 화면이 실을 도각 한 장. 프로젝트 작업본이 있으면 그것, 없으면 시스템 양식."""
|
||||
try:
|
||||
company_dir = await _company_dir(project_id)
|
||||
use_company_templates(company_dir)
|
||||
project_root = await _project_root(project_id)
|
||||
use_project_templates(project_root)
|
||||
# 편집 화면이 자리표에 실제 값을 보여 줄 수 있게 함께 넘긴다 — 도면마다 달라지는
|
||||
# 도면명·도면번호는 여기 없다(그 자리는 자리표 이름 그대로 보인다).
|
||||
fields = await title_block_fields(project_id)
|
||||
return FrameTemplateResponse(
|
||||
project_id=str(project_id),
|
||||
drawing=frame_template_document(),
|
||||
customized=company_template_path(company_dir).is_file(),
|
||||
customized=is_project_template_customized(project_root),
|
||||
fields=fields,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
@@ -123,7 +122,7 @@ async def import_frame_template(
|
||||
) -> FrameTemplateImportResponse | JSONResponse:
|
||||
"""외부 도각 파일(DXF·DWG)을 읽어 **편집 화면에 실을 도면**으로 돌려준다.
|
||||
|
||||
아직 저장하지 않는다 — 사용자가 자리표를 놓고 [완료]를 눌러야 회사 도각이 된다.
|
||||
아직 저장하지 않는다 — 사용자가 자리표를 놓고 [완료]를 눌러야 프로젝트 도각이 된다.
|
||||
"""
|
||||
try:
|
||||
data = await file.read()
|
||||
@@ -150,7 +149,7 @@ async def import_frame_template(
|
||||
async def put_frame_template(
|
||||
project_id: UUID, request: FrameTemplateSaveRequest
|
||||
) -> FrameTemplateSaveResponse | JSONResponse:
|
||||
"""편집한 도각을 회사 도각으로 저장한다. 프로그램 기본 도각은 그대로 둔다.
|
||||
"""편집한 도각을 프로젝트 작업본으로 저장한다. 시스템 양식 · `_initial/` 은 그대로 둔다.
|
||||
|
||||
이미 확정한 도면은 저장본을 그대로 쓰므로 옛 도각을 유지한다 — 확정을 풀면
|
||||
다음에 열 때 새 도각으로 다시 그려진다(2026-09-01 사용자 확정).
|
||||
@@ -159,8 +158,8 @@ async def put_frame_template(
|
||||
entities = request.drawing.get("entities")
|
||||
if not isinstance(entities, list):
|
||||
raise ValueError("도각 엔티티가 없습니다.")
|
||||
company_dir = await _company_dir(project_id)
|
||||
await asyncio.to_thread(save_company_template, company_dir, entities)
|
||||
project_root = await _project_root(project_id)
|
||||
await asyncio.to_thread(save_project_template, project_root, entities)
|
||||
return FrameTemplateSaveResponse(project_id=str(project_id))
|
||||
except (FileNotFoundError, ValueError) as exc:
|
||||
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
|
||||
@@ -174,15 +173,18 @@ async def put_frame_template(
|
||||
|
||||
@router.delete("/{project_id}/frame-template", response_model=FrameTemplateSaveResponse)
|
||||
async def delete_frame_template(project_id: UUID) -> FrameTemplateSaveResponse | JSONResponse:
|
||||
"""회사 도각을 지워 **프로그램 기본 도각으로 되돌린다** (2026-09-01 신설).
|
||||
"""[기본 도각으로] — 프로젝트 `_initial/` 도각을 작업본에 덮어쓴다(재계산 아님).
|
||||
|
||||
되돌릴 길이 없으면 회사 도각을 한 번 잘못 저장한 것만으로 도면이 열리지 않는다.
|
||||
확정한 도면은 저장본을 쓰므로 그대로고, 확정하지 않은 도면부터 기본 도각으로 나온다.
|
||||
되돌릴 길이 없으면 도각을 한 번 잘못 저장한 것만으로 도면이 열리지 않는다.
|
||||
확정한 도면은 저장본을 쓰므로 그대로고, 확정하지 않은 도면부터 되돌린 도각으로 나온다.
|
||||
"""
|
||||
try:
|
||||
company_dir = await _company_dir(project_id)
|
||||
removed = await asyncio.to_thread(clear_company_template, company_dir)
|
||||
return FrameTemplateSaveResponse(project_id=str(project_id), customized=not removed)
|
||||
project_root = await _project_root(project_id)
|
||||
await asyncio.to_thread(reset_project_template, project_root)
|
||||
return FrameTemplateSaveResponse(
|
||||
project_id=str(project_id),
|
||||
customized=is_project_template_customized(project_root),
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
|
||||
except Exception:
|
||||
|
||||
@@ -137,7 +137,7 @@ class FrameTemplateSaveRequest(BaseModel):
|
||||
|
||||
|
||||
class FrameTemplateSaveResponse(BaseModel):
|
||||
"""회사 도각 저장 결과."""
|
||||
"""프로젝트 도각 저장 결과."""
|
||||
|
||||
status: str = "success"
|
||||
project_id: str
|
||||
|
||||
@@ -1,32 +1,41 @@
|
||||
"""M02 도면 양식 작도 영역 칸(`drawing_area`) — 양식이 정본 · 없으면 A1 기본값 (PLAN 10-3)."""
|
||||
"""M02 도면 양식 — 작도 영역 칸 · B07 도각 층(프로젝트 작업본 → 시스템) (PLAN 10-3)."""
|
||||
|
||||
import json
|
||||
|
||||
from B07_DesignDetail import B07_DesignDetail_Engine_Template as engine
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
A1 = engine.A1_TEMPLATE
|
||||
|
||||
|
||||
def _company_template(tmp_path, document):
|
||||
path = engine.company_template_path(tmp_path)
|
||||
def _project_template(project_root, document):
|
||||
path = engine.project_template_path(project_root)
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(json.dumps(document), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def _entities(project_root):
|
||||
engine.use_project_templates(project_root)
|
||||
try:
|
||||
return engine.template_entities()
|
||||
finally:
|
||||
engine.use_project_templates(None)
|
||||
|
||||
|
||||
def test_system_a1_reads_drawing_area():
|
||||
engine.use_company_templates(None)
|
||||
engine.use_project_templates(None)
|
||||
assert engine.drawing_area() == (42.0, 47.0, 812.0, 567.0)
|
||||
# 도각 배치는 옮기기 전 박힌 값과 같다 — 콘텐츠 중심 = 작도 영역 중심.
|
||||
frame = engine.frame_entities("t", (0.0, 0.0, 100.0, 100.0), fit=False)
|
||||
assert frame
|
||||
assert len(engine.template_entities()) == 51
|
||||
|
||||
|
||||
def test_missing_area_falls_back_to_a1(tmp_path):
|
||||
_company_template(tmp_path, {"format": 6, "entities": [], "layers": []})
|
||||
engine.use_company_templates(tmp_path)
|
||||
_project_template(tmp_path, {"format": 6, "entities": [], "layers": []})
|
||||
engine.use_project_templates(tmp_path)
|
||||
try:
|
||||
assert engine.drawing_area() == engine._A1_INNER
|
||||
finally:
|
||||
engine.use_company_templates(None)
|
||||
engine.use_project_templates(None)
|
||||
|
||||
|
||||
def test_template_area_moves_content(tmp_path):
|
||||
@@ -35,11 +44,11 @@ def test_template_area_moves_content(tmp_path):
|
||||
"type": "Line",
|
||||
"shapeData": {"startPoint": {"x": 0, "y": 0}, "endPoint": {"x": 10, "y": 0}},
|
||||
}
|
||||
_company_template(
|
||||
_project_template(
|
||||
tmp_path,
|
||||
{"format": 6, "drawing_area": [0, 0, 400, 200], "entities": [line], "layers": []},
|
||||
)
|
||||
engine.use_company_templates(tmp_path)
|
||||
engine.use_project_templates(tmp_path)
|
||||
try:
|
||||
assert engine.drawing_area() == (0.0, 0.0, 400.0, 200.0)
|
||||
assert engine.usable_area() == (400 * 0.96, 200 * 0.96)
|
||||
@@ -47,19 +56,51 @@ def test_template_area_moves_content(tmp_path):
|
||||
placed = engine.frame_entities("t", (490.0, 490.0, 510.0, 510.0), fit=False)
|
||||
assert placed[0]["shapeData"]["startPoint"] == {"x": 300.0, "y": 400.0}
|
||||
finally:
|
||||
engine.use_company_templates(None)
|
||||
engine.use_project_templates(None)
|
||||
|
||||
|
||||
def test_bad_area_falls_back(tmp_path):
|
||||
_company_template(tmp_path, {"drawing_area": [10, 10, 5, 5], "entities": []})
|
||||
engine.use_company_templates(tmp_path)
|
||||
_project_template(tmp_path, {"drawing_area": [10, 10, 5, 5], "entities": []})
|
||||
engine.use_project_templates(tmp_path)
|
||||
try:
|
||||
assert engine.drawing_area() == engine._A1_INNER
|
||||
finally:
|
||||
engine.use_company_templates(None)
|
||||
engine.use_project_templates(None)
|
||||
|
||||
|
||||
def test_save_keeps_area(tmp_path):
|
||||
# 편집본(CAD)에는 작도 영역 칸이 없다 — 저장이 시스템 양식 값을 이어 받는다.
|
||||
path = engine.save_company_template(tmp_path, [])
|
||||
assert json.loads(path.read_text(encoding="utf-8"))["drawing_area"] == [42, 47, 812, 567]
|
||||
def test_old_project_reads_system(tmp_path):
|
||||
# 작업본이 없는 옛 프로젝트 — 시스템 양식으로 떨어진다.
|
||||
assert len(_entities(tmp_path)) == 51
|
||||
assert engine.is_project_template_customized(tmp_path) is False
|
||||
|
||||
|
||||
def test_save_reset_on_seeded_project(tmp_path):
|
||||
layers.seed_project(tmp_path)
|
||||
assert engine.project_template_path(tmp_path).is_file()
|
||||
assert engine.is_project_template_customized(tmp_path) is False
|
||||
|
||||
# [완료] — 작업본에만 쓴다 · 작도 영역은 이어 받는다 · `_initial/` · 시스템은 그대로.
|
||||
path = engine.save_project_template(tmp_path, [])
|
||||
saved = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert saved["drawing_area"] == [42, 47, 812, 567]
|
||||
assert _entities(tmp_path) == []
|
||||
assert engine.is_project_template_customized(tmp_path) is True
|
||||
initial = layers.template_path(layers.initial_dir(tmp_path), "drawing", A1)
|
||||
assert len(json.loads(initial.read_text(encoding="utf-8"))["entities"]) == 51
|
||||
assert (
|
||||
len(json.loads(engine.system_template_path().read_text(encoding="utf-8"))["entities"]) == 51
|
||||
)
|
||||
|
||||
# [기본 도각으로] — `_initial/` 을 작업본에 덮어쓴다.
|
||||
assert engine.reset_project_template(tmp_path) is True
|
||||
assert len(_entities(tmp_path)) == 51
|
||||
assert engine.is_project_template_customized(tmp_path) is False
|
||||
|
||||
|
||||
def test_reset_on_old_project_drops_to_system(tmp_path):
|
||||
engine.save_project_template(tmp_path, [])
|
||||
assert _entities(tmp_path) == []
|
||||
assert engine.reset_project_template(tmp_path) is True
|
||||
assert not engine.project_template_path(tmp_path).exists()
|
||||
assert len(_entities(tmp_path)) == 51
|
||||
assert engine.reset_project_template(tmp_path) is False
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* 도각 편집 모드 — 캐드 화면에서 도각(양식)만 따로 열어 고치고 [완료]로 한 번에 반영한다.
|
||||
* B07 에서 떼어 공용으로 둔다 — 도각을 읽고 쓰는 길(`api`)만 페이지가 넘긴다.
|
||||
*
|
||||
* 정본(`resources/master_template/drawing/00_template_A1.json`)은 프로그램 기본 도각이라
|
||||
* 건드리지 않는다. 고친 도각은 **회사 도각**(`storage/{회사}/templates/`)으로 저장되고,
|
||||
* 이후 그리는 도면이 그것을 쓴다 (2026-09-01 사용자 확정).
|
||||
* 시스템 양식(`resources/master_template/drawing/00_template_A1.json`)은 건드리지 않는다.
|
||||
* 고친 도각은 **프로젝트 도각**(작업본 `{프로젝트}/templates/drawing/`)으로 저장되고,
|
||||
* 이후 그리는 도면이 그것을 쓴다 (PLAN 10-5).
|
||||
*
|
||||
* 이미 확정한 도면은 저장본을 그대로 쓰므로 옛 도각을 유지한다 — 확정을 풀면 다시 그려진다.
|
||||
*/
|
||||
@@ -72,7 +72,7 @@ export function createFrameTemplateEditor<D extends CadHostDrawing>(
|
||||
onClick: () => void finish(),
|
||||
});
|
||||
/**
|
||||
* 회사 도각을 지우고 프로그램 기본 도각으로 되돌린다 (2026-09-01 신설).
|
||||
* 프로젝트 도각을 처음 복사한 도각(`_initial/`)으로 되돌린다 (2026-09-01 신설).
|
||||
* 되돌릴 길이 없으면 도각을 한 번 잘못 저장한 것만으로 도면이 열리지 않는다.
|
||||
*/
|
||||
const resetButton = createButton({
|
||||
@@ -83,7 +83,7 @@ export function createFrameTemplateEditor<D extends CadHostDrawing>(
|
||||
/**
|
||||
* 회사가 쓰던 도각을 파일로 들인다 (2026-09-06 사용자 지시). DWG 는 서버에 변환기가
|
||||
* 있을 때만 읽고, 없으면 「DXF 로 저장해 달라」는 안내가 뜬다. 불러온 도각은 아직
|
||||
* 저장되지 않는다 — 자리표를 놓고 [완료]를 눌러야 회사 도각이 된다.
|
||||
* 저장되지 않는다 — 자리표를 놓고 [완료]를 눌러야 프로젝트 도각이 된다.
|
||||
*/
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
@@ -153,8 +153,8 @@ export function createFrameTemplateEditor<D extends CadHostDrawing>(
|
||||
button.disabled = true;
|
||||
banner.hidden = false;
|
||||
label.textContent = response.customized
|
||||
? "도각 편집 중 — 회사 도각을 고치고 있습니다."
|
||||
: "도각 편집 중 — 기본 도각을 고치면 회사 도각으로 저장됩니다.";
|
||||
? "도각 편집 중 — 이 프로젝트에서 고친 도각입니다."
|
||||
: "도각 편집 중 — 고치면 이 프로젝트 도각으로 저장됩니다.";
|
||||
options.sendLoad(response.drawing, null, true, frameFields);
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : "도각을 불러오지 못했습니다.", "error");
|
||||
@@ -164,8 +164,8 @@ export function createFrameTemplateEditor<D extends CadHostDrawing>(
|
||||
async function resetToDefault(): Promise<void> {
|
||||
if (
|
||||
!window.confirm(
|
||||
"회사 도각을 지우고 프로그램 기본 도각으로 되돌립니다.\n" +
|
||||
"확정하지 않은 도면부터 기본 도각으로 나옵니다. 계속할까요?",
|
||||
"이 프로젝트 도각을 처음 복사한 도각으로 되돌립니다.\n" +
|
||||
"확정하지 않은 도면부터 되돌린 도각으로 나옵니다. 계속할까요?",
|
||||
)
|
||||
)
|
||||
return;
|
||||
@@ -173,7 +173,7 @@ export function createFrameTemplateEditor<D extends CadHostDrawing>(
|
||||
try {
|
||||
await options.api.reset();
|
||||
options.onSaved();
|
||||
showToast("기본 도각으로 되돌렸습니다.", "success");
|
||||
showToast("처음 복사한 도각으로 되돌렸습니다.", "success");
|
||||
leave();
|
||||
} catch (error) {
|
||||
showToast(
|
||||
|
||||
Reference in New Issue
Block a user