From 6e114ac9908bebb7644decb594031e42e1eba97b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 06:48:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(dev):=20=ED=99=95=EC=A0=95=20=EC=97=86?= =?UTF-8?q?=EC=9D=B4=20=EB=8B=A4=EC=9D=8C=EC=9C=BC=EB=A1=9C=20=E2=80=94=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20+=20=EC=A2=8C=EC=B8=A1=20=EB=8B=A8?= =?UTF-8?q?=EC=B6=94=20(2/2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 앞 커밋 618b6cf4 의 신규 파일을 등록하고 화면 단추를 붙임. 두 걸음 규칙대로 파일이 먼저 push 된 뒤 등록 줄이 감. 문은 앞뒤 둘 - 서버(정본): ENVIRONMENT 가 개발이 아니면 세 입구 모두 403. 라우터는 다른 것과 같은 보호(로그인·회사·프로젝트 접근)를 받고 그 위에 환경을 한 번 더 봄 - 화면(보조): import.meta.env.DEV 일 때만 단추가 그려짐 눌렀을 때 바뀌는 것 — project_workflow_stages.state 한 칸뿐. 계산은 안 돌고 정본 값도 안 만듦. 실측(b269ea34): 우회 전 [] → 우회 [3 STALE · 4 NOT_STARTED · 5 NOT_STARTED] → 되돌림 → [] 이미 COMPLETE 인 0·1·2 는 안 건드림 (진짜 확정은 보존) 되돌리기 — 푼 단계의 옛 상태를 message 에 DEV_UNLOCK:<옛상태> 로 적어 두고 [원래대로 되돌리기]가 그대로 복원. 그 표시가 없으면 진짜 확정이라 안 만짐. 드러남 — 우회 중이면 좌측에 「지금 확정을 건너뛴 상태입니다 — 값이 비어 보이는 것은 정상입니다. 건너뛴 단계 (3, 4, 5)」가 뜸. ⚠ 화면에서 잡은 것 — 단추를 패널 아래에 두니 overflow:hidden 에 잘려 화면 밖(y=772, 패널 740)이라 눌리지 않았음. 맨 위로 옮김(상태 경고이기도 함). 시험: 677 passed · 24 skipped (B05 코리도 1건 기존 깨짐, 무관) · tsc 오류 0. Co-Authored-By: Claude Opus 5 (1M context) --- B08_Quantity/B08_Quantity_UI_Page.ts | 78 ++++++++++++++++++++++++++++ main.py | 7 +++ ui_template/ui_template_locale_b2.ts | 13 +++++ 3 files changed, 98 insertions(+) diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index 80fa1c88..affaff1c 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -233,6 +233,72 @@ interface DraftSettings { dirty: boolean; } +/** 개발 전용 「확정 없이 다음으로」 한 줄 — 단추 둘 + 지금 상태 안내. + * + * ⚠ **조용히 넘어가지 않는다.** 우회로 열린 상태면 「확정을 건너뛴 상태입니다」를 띄운다 — + * 안 그러면 다음 사람이 「왜 값이 없나」로 헤맨다. + * ⚠ **되돌리는 단추를 같은 줄에 둔다.** 되돌릴 길이 안 보이면 검증용 프로젝트가 굳는다. + */ +function devUnlockRow(projectId: string, reload: () => void): HTMLElement { + const row = document.createElement("div"); + row.className = "b08-quantity__dev"; + + const title = document.createElement("p"); + title.className = "b08-quantity__note"; + title.textContent = L("B08_Quantity_Dev_Title"); + row.append(title); + + const state = document.createElement("p"); + state.className = "b08-quantity__note"; + row.append(state); + + const paint = (): void => { + fetch(`/api/projects/${projectId}/dev/unlock`, { credentials: "include" }) + .then((response) => (response.ok ? response.json() : null)) + .then((body: { bypassed_stages?: number[] } | null) => { + const stages = body?.bypassed_stages ?? []; + state.textContent = stages.length + ? `${L("B08_Quantity_Dev_Bypassed")} (${stages.join(", ")})` + : L("B08_Quantity_Dev_Normal"); + }) + .catch(() => { + state.textContent = L("B08_Quantity_Dev_Normal"); + }); + }; + paint(); + + const call = (method: "POST" | "DELETE", button: HTMLButtonElement): void => { + button.disabled = true; + fetch(`/api/projects/${projectId}/dev/unlock`, { method, credentials: "include" }) + .then((response) => { + if (!response.ok) throw new Error(String(response.status)); + showToast(L("B08_Quantity_Dev_Done"), "success"); + paint(); + reload(); + }) + .catch(() => showToast(L("B08_Quantity_Dev_Failed"), "error")) + .finally(() => { + button.disabled = false; + }); + }; + + const unlock = createButton({ + label: L("B08_Quantity_Dev_Unlock"), + variant: "ghost", + onClick: () => call("POST", unlock), + }); + const relock = createButton({ + label: L("B08_Quantity_Dev_Relock"), + variant: "ghost", + onClick: () => call("DELETE", relock), + }); + const buttons = document.createElement("div"); + buttons.className = "b08-quantity__actions ui-sidebar-actions"; + buttons.append(unlock, relock); + row.append(buttons); + return row; +} + /** 좌측 패널: 산출 조건 + 하단 [저장]·[확정] 액션 행. * `reload` 는 저장 뒤 표를 다시 그리는 손잡이다 — 조건이 바뀌면 집계·운반 값이 달라진다. */ function buildQuantitySidePanel( @@ -244,6 +310,18 @@ function buildQuantitySidePanel( const panel = document.createElement("div"); panel.className = "b08-quantity__panel"; + // ── 개발 전용 「확정 없이 다음으로」 ──────────────────────────────────────── + // ⚠ **맨 위에 둔다.** 패널이 `overflow: hidden` 이라 아래에 붙이면 화면 밖으로 + // 밀려 **눌리지 않는다**(2026-09-08 화면에서 실제로 그랬다 — 단추 y=772, + // 패널 높이 740). 상태 경고이기도 하니 자리도 여기가 맞다. + // ⚠ **이것은 보조 문일 뿐이다.** 진짜 문은 서버가 `ENVIRONMENT` 로 막는다 + // (`common_util_dev_unlock`). 화면만 숨기면 API 는 그대로 뚫려 있다. + // ⚠ **계산을 대신 돌리지 않는다** — 워크플로 잠금만 푼다. 값이 비어 보이는 것은 + // 정상이고, 그것을 「미확보」로 보이는 것이 이 화면이 이미 하는 일이다. + if (import.meta.env.DEV && projectId) { + panel.append(devUnlockRow(projectId, reload)); + } + // 계수는 서버 상수가 유일한 정의처다 — 화면은 보여 주기만 하고 값을 다시 적지 않는다. panel.append(field(L("B08_Quantity_Side_Method"), L("B08_Quantity_Side_Method_Value"))); const entries = Object.entries(table?.conversion_factors ?? {}); diff --git a/main.py b/main.py index 135572f7..08689bd8 100644 --- a/main.py +++ b/main.py @@ -63,6 +63,10 @@ from B08_Quantity.B08_Quantity_Router_Earthwork import router as b08_earthwork_r from B08_Quantity.B08_Quantity_Router_Material import router as b08_material_router from B09_Estimation.B09_Estimation_Router import router as b09_estimation_router from common_util.common_util_audit import note_api_call, record_call_burst + +# 개발환경 전용 — 「확정 없이 다음으로」. **문은 서버가 정본이다** — `ENVIRONMENT` 가 +# 개발이 아니면 세 입구 모두 403 으로 거절한다(화면 단추 숨김은 보조). +from common_util.common_util_dev_unlock_router import router as dev_unlock_router from common_util.common_util_auth import ( require_company, require_project_access, @@ -541,6 +545,9 @@ app.include_router(b08_quantity_router, dependencies=protected_with_company) app.include_router(b08_earthwork_router, dependencies=protected_with_company) app.include_router(b08_material_router, dependencies=protected_with_company) app.include_router(b09_estimation_router, dependencies=protected_with_company) +# 개발 전용 잠금 해제 — 다른 라우터와 **같은 보호**를 받는다(로그인·회사·프로젝트 접근). +# 그 위에 서버가 환경까지 한 번 더 본다. +app.include_router(dev_unlock_router, dependencies=protected_with_company) # ───────────────────────────────────────────────────────────────────────── diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 791880b5..85712f86 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -632,6 +632,19 @@ export const ui_locales_b2 = { "콘크리트 물량이 「콘크리트 타설」 공종으로 견적에 넘어갑니다.", "The placing method changes this work item's unit price — concrete volume is handed over as a placing work item.", ], + B08_Quantity_Dev_Title: [ + "⚠ 개발 전용 — 확정을 건너뛰고 다음 단계를 엽니다(계산은 돌지 않습니다).", + "⚠ Dev only — unlocks the next stage without confirming (no recalculation).", + ], + B08_Quantity_Dev_Unlock: ["확정 없이 다음으로", "Skip confirm"], + B08_Quantity_Dev_Relock: ["원래대로 되돌리기", "Undo skip"], + B08_Quantity_Dev_Bypassed: [ + "지금 확정을 건너뛴 상태입니다 — 값이 비어 보이는 것은 정상입니다. 건너뛴 단계", + "Currently skipping confirmation — empty values are expected. Skipped stages", + ], + B08_Quantity_Dev_Normal: ["건너뛴 단계 없음 (정상 상태)", "No skipped stages"], + B08_Quantity_Dev_Done: ["단계 잠금을 바꿨습니다.", "Stage lock updated."], + B08_Quantity_Dev_Failed: ["단계 잠금을 바꾸지 못했습니다.", "Failed to update stage lock."], B08_Quantity_Side_Topsoil: ["표토제거", "Topsoil Removal"], B08_Quantity_Side_Topsoil_Label: ["표토 두께(m)", "Topsoil thickness (m)"], B08_Quantity_Unset_Placeholder: ["안 정함", "Not set"],