feat(B09): 오른쪽 재료 창고 · 지난 공사 일위대가 가져오기(PLAN 703-1)

- 설계내역서 표 오른쪽 서랍(공용 side_panels · 접고 펴기 · 좁은 화면 = 아래) — 찾기 하나 + 출처 탭
  이 공사 · 내 것 · 회사 · 품셈(704-1 기준 목록 · 우클릭 계산서 보기) · 지난 공사
- 두 번 누름 · Enter = 고른 내역 줄에 단가연결 · 끌어 놓기 = 놓은 줄 · 연결 = 단가연결 모달과 같은 길(702-3 즉시 금액)
- 품셈 = 넣을 값 창에 계산서 입력 전부(목록 바꿈은 기본과 다른 것만 — 석축헐기 뒷길이 60 이 0 으로 열리던 것)
- 서버 B09_Estimation_Past + GET/POST /estimation/past — 같은 회사 다른 공사 구조물 사본 · 서고 사본 →
  이 공사 서고 사본(폴더 통째 · 가져온곳 · 덮개째 · 단가 = 이 공사 책) · 남의 회사 403
- 표 높이 셈(spaceBelow)이 뒤 형제만 보므로 서랍 = DOM 앞 · CSS order 로 오른쪽
- 시험 test_703_1 6 · 셸 꼴 글 시험 200-2 · 202-1 = 표 칸 + 서랍 꼴로 고침(낡은 글)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 14:59:58 +09:00
co-authored by Claude Opus 5.5
parent 94cbf28f16
commit fdb3eb9b77
11 changed files with 774 additions and 5 deletions
+66
View File
@@ -35,6 +35,12 @@
「내 자료집에 저장」(703-2) — 펼친 일위대가 → 개인 층 사전조합
(`B09_Estimation_Daega_Personal`) · user = 화면 캐시 덮개 ·
같은 이름 = 409 {같은이름} → 덮어쓰기(overwrite = 키) · 새 이름
GET /api/projects/{id}/estimation/past
재료 창고 「지난 공사」(703-1) — 같은 회사 다른 공사의 일위대가 목록(`B09_Estimation_Past`)
POST /api/projects/{id}/estimation/past {project_id, cover}
지난 공사 일위대가 → 이 공사 서고 사본(덮개째) → {폴더, 만듦, key, cover, 덮개, payload} ·
덮개 = 그 공사 저장 칸(화면 캐시 cover 가 비었을 때만 얹음 · 같은 꼴 블록이 먼저 있으면 빈 것) ·
같은 회사만(남의 회사 403)
운반 사본 연결 = 셈 때마다 거리로 사본 다시 고름(`Bill_Library.resolve_hauls` · `fix_keys`).
저장 자리 = 프로젝트 안 `B09_Estimation/v1/`(`B09_Estimation_Store`) ·
@@ -66,6 +72,7 @@ from B08_Quantity.B08_Quantity_Router_Milestone import read_data as read_milesto
from B08_Quantity.B08_Quantity_Router_Milestone import site_info
from B09_Estimation import B09_Estimation_Bill_Library as library
from B09_Estimation import B09_Estimation_Daega_Personal as personal
from B09_Estimation import B09_Estimation_Past as past
from B09_Estimation import B09_Estimation_Prices as prices
from B09_Estimation import B09_Estimation_Refresh as refresh
from B09_Estimation import B09_Estimation_Sangeun as sangeun
@@ -82,10 +89,12 @@ from B09_Estimation.B09_Estimation_Router_Quotes import router as quotes_router
from B09_Estimation.B09_Estimation_Summary import compute_summary, hopyo_lines, used_materials
from common_util.common_util_auth import verify_session
from common_util.common_util_project_settings import reset_section
from common_util.common_util_storage import resolve_stored_project_path
from common_util.common_util_workflow_state import mark_stage
from M02_MasterTemplete import M02_Layer_Access as access
from M02_MasterTemplete.M02_Item_Snapshot import importer
from M02_MasterTemplete.M02_MasterTemplete_Router_Items import _call, _root
from M02_MasterTemplete.M02_MasterTemplete_Router_Layers import _company_projects, _project
try: # 252 공기 초안(draft 폴더 · 지우면 공기 탭 원천 없이)
from B09_Estimation.draft import B09_Draft_Schedule_Source as schedule_draft
@@ -488,6 +497,63 @@ async def save_personal(
}
def _past_rows(projects: list[dict[str, Any]], here: str) -> list[dict[str, Any]]:
out = []
for other in projects:
if other["project_id"] == here or not other.get("storage_path"):
continue
try:
rows = past.rows_of(Path(resolve_stored_project_path(other["storage_path"])))
except (ValueError, OSError): # 못 읽는 공사 = 건너뜀
continue
ref = {"project_id": other["project_id"], "project": other["name"]}
out += [{**ref, **row} for row in rows]
return out
@router.get("/{project_id}/estimation/past")
async def past_list(
project_id: UUID, session: dict[str, Any] = Depends(verify_session)
) -> dict[str, Any]:
here = await _project(session, str(project_id))
projects = await _company_projects(int(here["company_id"]))
return {"rows": await asyncio.to_thread(_past_rows, projects, str(project_id))}
class PastBody(BaseModel):
project_id: str = Field(min_length=1, max_length=64)
cover: str = Field(min_length=3, max_length=200)
@router.post("/{project_id}/estimation/past")
async def past_import(
project_id: UUID, body: PastBody, session: dict[str, Any] = Depends(verify_session)
) -> dict[str, Any]:
here = await _project(session, str(project_id))
if body.project_id == str(project_id):
raise HTTPException(status_code=400, detail="이 공사 것은 「이 공사」 탭에서 고름")
other = await _project(session, body.project_id)
if other["company_id"] != here["company_id"]:
raise HTTPException(status_code=403, detail="같은 회사 공사에서만 가져옴")
ref = {"project_id": body.project_id, "프로젝트": other.get("name")}
try:
made = await asyncio.to_thread(
past.import_past, here["root"], other["root"], ref, body.cover, importer(session)
)
except LookupError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
out = await payload(project_id, here["root"])
daega = out["inputs"].get("daega") or {}
key = (daega.get("libs") or {}).get(made["폴더"])
# 같은 꼴 블록이 이 공사에 먼저 있으면 그 블록(다른 cover) — 덮개 안 옮김(그 블록 덮개 그대로)
cover = next((b["cover"] for b in daega.get("blocks") or [] if b["id"] == key), None)
if cover != made.pop("덮개자리"):
made["덮개"] = {}
return {**made, "key": key, "cover": cover, "payload": out}
# 166-4 견적서 파일 길 — 같은 머리 · 같은 권한
router.include_router(quotes_router)