feat(dev): 확정 없이 다음으로 — 등록 + 좌측 단추 (2/2)

앞 커밋 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) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 06:48:19 +09:00
co-authored by Claude Opus 5
parent 618b6cf4c5
commit 6e114ac990
3 changed files with 98 additions and 0 deletions
+78
View File
@@ -233,6 +233,72 @@ interface DraftSettings {
dirty: boolean; 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` 는 저장 뒤 표를 다시 그리는 손잡이다 — 조건이 바뀌면 집계·운반 값이 달라진다. */ * `reload` 는 저장 뒤 표를 다시 그리는 손잡이다 — 조건이 바뀌면 집계·운반 값이 달라진다. */
function buildQuantitySidePanel( function buildQuantitySidePanel(
@@ -244,6 +310,18 @@ function buildQuantitySidePanel(
const panel = document.createElement("div"); const panel = document.createElement("div");
panel.className = "b08-quantity__panel"; 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"))); panel.append(field(L("B08_Quantity_Side_Method"), L("B08_Quantity_Side_Method_Value")));
const entries = Object.entries(table?.conversion_factors ?? {}); const entries = Object.entries(table?.conversion_factors ?? {});
+7
View File
@@ -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 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 B09_Estimation.B09_Estimation_Router import router as b09_estimation_router
from common_util.common_util_audit import note_api_call, record_call_burst 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 ( from common_util.common_util_auth import (
require_company, require_company,
require_project_access, 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_earthwork_router, dependencies=protected_with_company)
app.include_router(b08_material_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(b09_estimation_router, dependencies=protected_with_company)
# 개발 전용 잠금 해제 — 다른 라우터와 **같은 보호**를 받는다(로그인·회사·프로젝트 접근).
# 그 위에 서버가 환경까지 한 번 더 본다.
app.include_router(dev_unlock_router, dependencies=protected_with_company)
# ───────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────
+13
View File
@@ -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.", "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: ["표토제거", "Topsoil Removal"],
B08_Quantity_Side_Topsoil_Label: ["표토 두께(m)", "Topsoil thickness (m)"], B08_Quantity_Side_Topsoil_Label: ["표토 두께(m)", "Topsoil thickness (m)"],
B08_Quantity_Unset_Placeholder: ["안 정함", "Not set"], B08_Quantity_Unset_Placeholder: ["안 정함", "Not set"],