feat(B07,B09): B07_Quantity 셸 페이지 신설 + B09_Estimation 접두사 정리 + 단계 순서 반영

- B09_wf6_Estimation -> B09_Estimation (폴더·파일·라우트 키/슬러그)
- B07_Quantity_UI_Page.ts 신설: 워크플로우 셸 + 좌측 [확정] 버튼
  (renderPendingWorkflow에 leftPanel 옵션 추가로 공용 셸 재사용)
- B07_Quantity_Router.py 신설: POST /api/projects/{id}/quantity/confirm
  -> complete_stage(4) 전이만 수행(본문 미구현), main.py 등록
- STAGE_KEYS 재정의: FILE_INPUT/PREPROCESS/PROFILE/SECTION/QUANTITY/DESIGN_DETAIL/ESTIMATION
- 스텝바 라벨·아이콘 4↔5 스왑(수량산출이 4차, 상세설계가 5차), A02 소개 문구 스왑
- 라우터 테이블에 b07-quantity 등록(플레이스홀더 제거), locale 키 5종 추가

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 10:15:07 +09:00
co-authored by Claude Fable 5
parent 2a248ff742
commit ec9943417c
17 changed files with 170 additions and 32 deletions
+4 -1
View File
@@ -82,6 +82,8 @@ export interface PendingWorkflowOptions {
steps: string[];
/** 현재 활성 단계 인덱스 */
activeStep: number;
/** 본문은 준비 중이지만 좌측 패널만 먼저 붙일 때 (B07 수량 확정 버튼 등) */
leftPanel?: HTMLElement;
}
export async function renderPendingWorkflow(
@@ -102,6 +104,7 @@ export async function renderPendingWorkflow(
title: opts.title,
steps: opts.steps,
activeStep: opts.activeStep,
leftPanel: opts.leftPanel,
mainContent: buildPendingBlock(),
stages: workflowState?.stages,
currentStage: workflowState?.current_stage,
@@ -123,8 +126,8 @@ export function workflowSteps(): string[] {
L("WF_Step_Surface"),
L("WF_Step_Route"),
L("WF_Step_ProfileCross"),
L("WF_Step_DesignDetail"),
L("WF_Step_Quantity"),
L("WF_Step_DesignDetail"),
L("WF_Step_Estimation"),
];
}
+1 -1
View File
@@ -22,7 +22,7 @@ export const WORKFLOW_STEP_ROUTES: readonly RoutePath[] = [
ROUTES.B06_SECTION,
ROUTES.B07_QUANTITY,
ROUTES.B08_DESIGN_DETAIL,
ROUTES.B09_WF6_ESTIMATION,
ROUTES.B09_ESTIMATION,
];
export async function fetchWorkflowState(projectId: string): Promise<WorkflowState> {
+1 -1
View File
@@ -23,7 +23,7 @@ const FOOTERLESS_ROUTES: readonly RoutePath[] = [
ROUTES.B06_SECTION,
ROUTES.B07_QUANTITY,
ROUTES.B08_DESIGN_DETAIL,
ROUTES.B09_WF6_ESTIMATION,
ROUTES.B09_ESTIMATION,
ROUTES.B10_PAYMENT,
ROUTES.B11_STATUS,
];
+8 -6
View File
@@ -45,12 +45,14 @@ const routeTable: Partial<Record<RoutePath, () => Promise<PageRenderer>>> = {
(await import("../B05_Profile/B05_Profile_UI_Page")).renderB05Route,
[ROUTES.B06_SECTION]: async () =>
(await import("../B06_Section/B06_Section_UI_Page")).renderB06ProfileCross,
// B07_QUANTITY: 재작성 예정으로 기존 구현을 0_old_260726_codex.zip 백업 후 제거.
// 등록을 빼두면 renderPlaceholder가 "준비 중" 화면을 띄운다. 새 페이지 완성 시 여기에 다시 추가.
// B07_QUANTITY: 수량 본문은 재작성 예정(구 구현은 B07_Quantity/0_old_260726_codex.zip 백업).
// 지금은 셸 페이지 — 좌측 [확정]으로 stage 4를 닫고 B08 상세설계로 넘어간다.
[ROUTES.B07_QUANTITY]: async () =>
(await import("../B07_Quantity/B07_Quantity_UI_Page")).renderB07Quantity,
[ROUTES.B08_DESIGN_DETAIL]: async () =>
(await import("../B08_DesignDetail/B08_DesignDetail_UI_Page")).renderB08DesignDetail,
[ROUTES.B09_WF6_ESTIMATION]: async () =>
(await import("../B09_wf6_Estimation/B09_wf6_Estimation_UI_Page")).renderB09Estimation,
[ROUTES.B09_ESTIMATION]: async () =>
(await import("../B09_Estimation/B09_Estimation_UI_Page")).renderB09Estimation,
[ROUTES.B10_PAYMENT]: async () =>
(await import("../B10_Payment/B10_Payment_UI_Page")).renderB10Payment,
[ROUTES.B11_STATUS]: async () =>
@@ -109,9 +111,9 @@ export async function renderCurrentRoute(outlet: HTMLElement): Promise<void> {
ROUTES.B04_PREPROCESS,
ROUTES.B05_PROFILE,
ROUTES.B06_SECTION,
ROUTES.B08_DESIGN_DETAIL,
ROUTES.B07_QUANTITY,
ROUTES.B09_WF6_ESTIMATION,
ROUTES.B08_DESIGN_DETAIL,
ROUTES.B09_ESTIMATION,
ROUTES.B10_PAYMENT,
ROUTES.B11_STATUS,
ROUTES.B11_LOADING,
+37
View File
@@ -0,0 +1,37 @@
"""B07 수량 산출 라우터 — 셸 단계.
수량 본문은 미구현. 지금은 좌측 패널 [확정] 버튼이 워크플로 stage 4(QUANTITY)를
완료 처리해 B08 상세설계 진입을 여는 역할만 한다. 본문(B06 종횡단 기반 산출)을
구현할 때 이 라우터를 확장한다.
"""
import logging
from uuid import UUID
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from common_util.common_util_workflow_state import complete_stage
from config.config_db import get_db_pool
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/projects", tags=["B07 Quantity"])
@router.post("/{project_id}/quantity/confirm")
async def confirm_quantity(project_id: UUID) -> JSONResponse:
"""수량 단계 확정 — stage 4(QUANTITY)를 COMPLETE로 전이한다 (본문 미구현)."""
pool = await get_db_pool()
async with pool.acquire() as connection:
try:
async with connection.cursor() as cursor:
await complete_stage(cursor, str(project_id), 4)
await connection.commit()
except Exception:
await connection.rollback()
logger.exception("B07 수량 확정 실패: project_id=%s", project_id)
return JSONResponse(
status_code=500,
content={"status": "error", "message": "수량 단계 확정에 실패했습니다."},
)
return JSONResponse(content={"status": "success", "project_id": str(project_id)})
+81
View File
@@ -0,0 +1,81 @@
/* =============================================================================
* B07_Quantity_UI_Page.ts
* 로그인 후 07: 4차 워크플로우 (수량 산출)
*
* ⚠️ 본문 준비 중 — 워크플로우 셸 + 좌측 [확정] 버튼만 구성.
* 확정 = 워크플로 stage 4(QUANTITY) 완료 처리 후 B08 상세설계로 이동.
* 수량 본문(B06 종횡단 기반 산출)은 후속 계획에서 구현한다.
* ========================================================================== */
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton, showToast } from "@ui/ui_template_elements";
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
import { renderPendingWorkflow, workflowSteps } from "../A00_Common/b_page_scaffold";
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
/** locale 헬퍼 */
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
/** stage 4(QUANTITY) 완료 요청 — 본문 미구현 상태의 유일한 백엔드 연동. */
async function confirmQuantityStage(projectId: string): Promise<void> {
const response = await fetch(
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/quantity/confirm`,
{ method: "POST", credentials: "include" },
);
if (!response.ok) {
throw new Error(`quantity confirm failed: ${response.status}`);
}
}
/** 좌측 패널: 준비 중 안내 + 하단 [확정] 액션 행 (다른 워크플로우 페이지와 동일 배치). */
function buildQuantitySidePanel(projectId: string | null): HTMLElement {
const panel = document.createElement("div");
panel.className = "b07-quantity__panel";
const note = document.createElement("p");
note.className = "b07-quantity__pending-note";
note.textContent = L("B07_Quantity_Side_Pending");
panel.append(note);
const confirmButton = createButton({
label: L("B07_Quantity_Btn_Confirm"),
variant: "filled",
onClick: () => {
if (!projectId) {
showToast(L("B07_Quantity_Confirm_Failed"), "error");
return;
}
confirmButton.disabled = true;
confirmQuantityStage(projectId)
.then(() => {
showToast(L("B07_Quantity_Confirm_Success"), "success");
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[5]);
})
.catch(() => {
showToast(L("B07_Quantity_Confirm_Failed"), "error");
confirmButton.disabled = false;
});
},
});
const actions = document.createElement("div");
actions.className = "b07-quantity__actions ui-sidebar-actions";
actions.append(confirmButton);
panel.append(actions);
return panel;
}
/* -----------------------------------------------------------------------------
* 페이지 진입점
* -------------------------------------------------------------------------- */
export async function renderB07Quantity(root: HTMLElement): Promise<void> {
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
await renderPendingWorkflow(root, {
title: L("B07_Quantity_Title"),
steps: workflowSteps(),
activeStep: 4,
leftPanel: buildQuantitySidePanel(projectId),
});
}
@@ -1,5 +1,5 @@
/* =============================================================================
* B09_wf6_Estimation_UI_Page.ts
* B09_Estimation_UI_Page.ts
* 09: 6차 (·)
*
* (+) . .
+1 -1
View File
@@ -14,7 +14,7 @@ PROJECT_STORAGE_LAYOUT_V2 = (
("B06_Section", "cross_sections"),
("B08_DesignDetail", "structures"),
("B07_Quantity", "quantities"),
("B09_wf6_Estimation", "v1"),
("B09_Estimation", "v1"),
)
+7 -7
View File
@@ -7,13 +7,13 @@ from typing import Any, Dict
import aiomysql
STAGE_KEYS = [
"FILE_INPUT", # 0
"WF1_SURFACE", # 1
"WF2_ROUTE", # 2
"WF3_PROFILE_CROSS", # 3
"WF4_DESIGN_DETAIL", # 4
"WF5_QUANTITY", # 5
"WF6_ESTIMATION", # 6
"FILE_INPUT", # 0 (B03)
"PREPROCESS", # 1 (B04)
"PROFILE", # 2 (B05)
"SECTION", # 3 (B06)
"QUANTITY", # 4 (B07)
"DESIGN_DETAIL", # 5 (B08)
"ESTIMATION", # 6 (B09)
]
+2 -2
View File
@@ -89,7 +89,7 @@ export const ROUTES = {
B06_SECTION: "b06-section",
B07_QUANTITY: "b07-quantity",
B08_DESIGN_DETAIL: "b08-design-detail",
B09_WF6_ESTIMATION: "b09-wf6-estimation",
B09_ESTIMATION: "b09-estimation",
B10_PAYMENT: "b10-payment",
B11_STATUS: "b11-status",
// 대시보드에서 B그룹으로 처음 들어갈 때 3D·등고선을 미리 받아 두는 준비 화면.
@@ -112,7 +112,7 @@ export const PROTECTED_ROUTES: readonly RoutePath[] = [
ROUTES.B06_SECTION,
ROUTES.B07_QUANTITY,
ROUTES.B08_DESIGN_DETAIL,
ROUTES.B09_WF6_ESTIMATION,
ROUTES.B09_ESTIMATION,
ROUTES.B10_PAYMENT,
ROUTES.B11_STATUS,
ROUTES.B11_LOADING,
+1 -1
View File
@@ -692,7 +692,7 @@ PROJECT_STORAGE_STAGE_DIRS = frozenset(
"B06_Section",
"B08_DesignDetail",
"B07_Quantity",
"B09_wf6_Estimation",
"B09_Estimation",
}
)
+2 -2
View File
@@ -405,7 +405,7 @@ CREATE TABLE IF NOT EXISTS outputs (
generated_by INT, -- FK는 later
generated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
version INT DEFAULT 1,
outputs_directory_path VARCHAR(500), -- storage/.../B09_wf6_Estimation/v1/
outputs_directory_path VARCHAR(500), -- storage/.../B09_Estimation/v1/
metadata JSON -- template_used, company_name, project_name, total_cost, generation_time_sec
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -415,7 +415,7 @@ CREATE TABLE IF NOT EXISTS output_files (
output_id INT NOT NULL, -- FK는 later
file_type VARCHAR(50), -- xlsx, pdf, dxf, dwg, json, zip
original_filename VARCHAR(255),
output_file_path VARCHAR(500), -- storage/.../B09_wf6_Estimation/v1/...
output_file_path VARCHAR(500), -- storage/.../B09_Estimation/v1/...
file_size_mb FLOAT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
download_count INT DEFAULT 0
+1 -1
View File
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS project_workflow_stages (
id INT AUTO_INCREMENT PRIMARY KEY,
project_id CHAR(36) NOT NULL,
stage_no TINYINT NOT NULL, -- 0=파일입력, 1=WF1 ... 6=WF6
stage_key VARCHAR(30) NOT NULL, -- 'FILE_INPUT','WF1_SURFACE',...,'WF6_ESTIMATION'
stage_key VARCHAR(30) NOT NULL, -- 'FILE_INPUT','PREPROCESS','PROFILE','SECTION','QUANTITY','DESIGN_DETAIL','ESTIMATION'
state ENUM('NOT_STARTED','IN_PROGRESS','COMPLETE','FAILED','STALE') NOT NULL DEFAULT 'NOT_STARTED',
progress_percent TINYINT NOT NULL DEFAULT 0,
params JSON NULL, -- 해당 단계에서 사용자가 선택/입력한 값 (재실행 시 재사용)
+2
View File
@@ -40,6 +40,7 @@ from B06_Section.B06_Section_Router import router as b06_section_router
from B06_Section.B06_Section_Router_Confirm import (
router as b06_section_confirm_router,
)
from B07_Quantity.B07_Quantity_Router import router as b07_quantity_router
from B08_DesignDetail.B08_DesignDetail_Router import router as b08_design_router
from common_util.common_util_auth import require_company, verify_session
from common_util.common_util_resource_monitor import sample_resources_loop
@@ -358,6 +359,7 @@ app.include_router(tiles_router, dependencies=protected_with_company)
app.include_router(b05_route_router, dependencies=protected_with_company)
app.include_router(b06_section_router, dependencies=protected_with_company)
app.include_router(b06_section_confirm_router, dependencies=protected_with_company)
app.include_router(b07_quantity_router, dependencies=protected_with_company)
app.include_router(b08_design_router, dependencies=protected_with_company)
+6 -6
View File
@@ -68,16 +68,16 @@ export const ui_locales_a = {
"확정 노선을 따라 종단면과 횡단면을 자동 추출합니다.",
"Automatically extract longitudinal and cross sections along the confirmed route.",
],
A02_ProgDetail_Step4_Title: ["4. 상세설계", "4. Detail Design"],
A02_ProgDetail_Step4_Title: ["4. 수량산출", "4. Quantity"],
A02_ProgDetail_Step4_Desc: [
"구조물, 배수, 절·성토 등 세부 설계 요소를 편집합니다.",
"Edit detailed design elements such as structures, drainage, and cut/fill.",
],
A02_ProgDetail_Step5_Title: ["5. 수량산출", "5. Quantity"],
A02_ProgDetail_Step5_Desc: [
"토공량과 구조물 수량을 자동 계산합니다.",
"Automatically calculate earthwork volumes and structure quantities.",
],
A02_ProgDetail_Step5_Title: ["5. 상세설계", "5. Detail Design"],
A02_ProgDetail_Step5_Desc: [
"구조물, 배수, 절·성토 등 세부 설계 요소를 편집합니다.",
"Edit detailed design elements such as structures, drainage, and cut/fill.",
],
A02_ProgDetail_Step6_Title: ["6. 설계도서", "6. Design Docs"],
A02_ProgDetail_Step6_Desc: [
"견적서와 설계도서를 Excel / DWG / PDF로 출력합니다.",
+14 -1
View File
@@ -441,8 +441,21 @@ export const ui_locales_b2 = {
/* --- B07_Quantity 수량 산출 --- */
B07_Quantity_Title: ["수량산출", "Quantity"],
B07_Quantity_Side_Pending: [
"수량 산출 기능은 준비 중입니다. [확정]을 누르면 4차 단계를 마치고 상세설계로 이동합니다.",
"Quantity module is in preparation. Press [Confirm] to finish stage 4 and move to Detail Design.",
],
B07_Quantity_Btn_Confirm: ["확정", "Confirm"],
B07_Quantity_Confirm_Success: [
"수량 단계를 확정했습니다. 상세설계로 이동합니다.",
"Quantity stage confirmed. Moving to Detail Design.",
],
B07_Quantity_Confirm_Failed: [
"수량 단계 확정에 실패했습니다.",
"Failed to confirm the quantity stage.",
],
/* --- B09_wf6_Estimation 견적·문서 --- */
/* --- B09_Estimation 견적·문서 --- */
B09_Estimation_Title: ["설계도서", "Design Docs"],
/* --- B10_Payment 결재 --- */
+1 -1
View File
@@ -48,7 +48,7 @@ export interface StepBarOptions {
homeButton?: boolean;
}
export const WORKFLOW_STEP_ICONS = ["📁", "🗺️", "🛣️", "📐", "🏗️", "", "📄"] as const;
export const WORKFLOW_STEP_ICONS = ["📁", "🗺️", "🛣️", "📐", "", "🏗️", "📄"] as const;
export function createStepBar(
steps: readonly string[],