From ec9943417ce6aec6a6969155f0cef2b082de4705 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 8 Aug 2026 10:15:07 +0900 Subject: [PATCH] =?UTF-8?q?feat(B07,B09):=20B07=5FQuantity=20=EC=85=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=8B=A0=EC=84=A4=20+=20B09=5FEs?= =?UTF-8?q?timation=20=EC=A0=91=EB=91=90=EC=82=AC=20=EC=A0=95=EB=A6=AC=20+?= =?UTF-8?q?=20=EB=8B=A8=EA=B3=84=20=EC=88=9C=EC=84=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- A00_Common/b_page_scaffold.ts | 5 +- A00_Common/b_workflow_nav.ts | 2 +- A00_Common/main.ts | 2 +- A00_Common/router.ts | 14 ++-- B07_Quantity/B07_Quantity_Router.py | 37 +++++++++ B07_Quantity/B07_Quantity_UI_Page.ts | 81 +++++++++++++++++++ .../B09_Estimation_UI_Page.ts | 2 +- common_util/common_util_storage.py | 2 +- common_util/common_util_workflow_state.py | 14 ++-- config/config_frontend.ts | 4 +- config/config_system.py | 2 +- db_management/001_create_schema.sql | 4 +- db_management/006_workflow_state.sql | 2 +- main.py | 2 + ui_template/ui_template_locale_a.ts | 12 +-- ui_template/ui_template_locale_b2.ts | 15 +++- ui_template/ui_template_workflow_layout.ts | 2 +- 17 files changed, 170 insertions(+), 32 deletions(-) create mode 100644 B07_Quantity/B07_Quantity_Router.py create mode 100644 B07_Quantity/B07_Quantity_UI_Page.ts rename B09_wf6_Estimation/B09_wf6_Estimation_UI_Page.ts => B09_Estimation/B09_Estimation_UI_Page.ts (97%) diff --git a/A00_Common/b_page_scaffold.ts b/A00_Common/b_page_scaffold.ts index 6cdad372..2aac7ece 100644 --- a/A00_Common/b_page_scaffold.ts +++ b/A00_Common/b_page_scaffold.ts @@ -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"), ]; } diff --git a/A00_Common/b_workflow_nav.ts b/A00_Common/b_workflow_nav.ts index d6da15b6..95993b36 100644 --- a/A00_Common/b_workflow_nav.ts +++ b/A00_Common/b_workflow_nav.ts @@ -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 { diff --git a/A00_Common/main.ts b/A00_Common/main.ts index f206b9fa..87d8b4c3 100644 --- a/A00_Common/main.ts +++ b/A00_Common/main.ts @@ -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, ]; diff --git a/A00_Common/router.ts b/A00_Common/router.ts index f86f70e3..9c694154 100644 --- a/A00_Common/router.ts +++ b/A00_Common/router.ts @@ -45,12 +45,14 @@ const routeTable: Partial Promise>> = { (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 { 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, diff --git a/B07_Quantity/B07_Quantity_Router.py b/B07_Quantity/B07_Quantity_Router.py new file mode 100644 index 00000000..934f2f95 --- /dev/null +++ b/B07_Quantity/B07_Quantity_Router.py @@ -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)}) diff --git a/B07_Quantity/B07_Quantity_UI_Page.ts b/B07_Quantity/B07_Quantity_UI_Page.ts new file mode 100644 index 00000000..82744f30 --- /dev/null +++ b/B07_Quantity/B07_Quantity_UI_Page.ts @@ -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 { + 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 { + const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); + await renderPendingWorkflow(root, { + title: L("B07_Quantity_Title"), + steps: workflowSteps(), + activeStep: 4, + leftPanel: buildQuantitySidePanel(projectId), + }); +} diff --git a/B09_wf6_Estimation/B09_wf6_Estimation_UI_Page.ts b/B09_Estimation/B09_Estimation_UI_Page.ts similarity index 97% rename from B09_wf6_Estimation/B09_wf6_Estimation_UI_Page.ts rename to B09_Estimation/B09_Estimation_UI_Page.ts index 56d28884..879aef9b 100644 --- a/B09_wf6_Estimation/B09_wf6_Estimation_UI_Page.ts +++ b/B09_Estimation/B09_Estimation_UI_Page.ts @@ -1,5 +1,5 @@ /* ============================================================================= - * B09_wf6_Estimation_UI_Page.ts + * B09_Estimation_UI_Page.ts * 로그인 후 09: 6차 워크플로우 (견적·문서) * * ⚠️ 본문 준비 중 — 워크플로우 셸(헤더+스텝바)만 구성. 추후 구체화. diff --git a/common_util/common_util_storage.py b/common_util/common_util_storage.py index 853956ba..54a6d449 100644 --- a/common_util/common_util_storage.py +++ b/common_util/common_util_storage.py @@ -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"), ) diff --git a/common_util/common_util_workflow_state.py b/common_util/common_util_workflow_state.py index 93f2f799..b6d85b21 100644 --- a/common_util/common_util_workflow_state.py +++ b/common_util/common_util_workflow_state.py @@ -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) ] diff --git a/config/config_frontend.ts b/config/config_frontend.ts index a6695f00..2ebddccf 100644 --- a/config/config_frontend.ts +++ b/config/config_frontend.ts @@ -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, diff --git a/config/config_system.py b/config/config_system.py index f38cdaa7..783355cf 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -692,7 +692,7 @@ PROJECT_STORAGE_STAGE_DIRS = frozenset( "B06_Section", "B08_DesignDetail", "B07_Quantity", - "B09_wf6_Estimation", + "B09_Estimation", } ) diff --git a/db_management/001_create_schema.sql b/db_management/001_create_schema.sql index c40b75f5..9e5a7394 100644 --- a/db_management/001_create_schema.sql +++ b/db_management/001_create_schema.sql @@ -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 diff --git a/db_management/006_workflow_state.sql b/db_management/006_workflow_state.sql index 16e86b6c..413f1f09 100644 --- a/db_management/006_workflow_state.sql +++ b/db_management/006_workflow_state.sql @@ -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, -- 해당 단계에서 사용자가 선택/입력한 값 (재실행 시 재사용) diff --git a/main.py b/main.py index d48d1beb..abc52ced 100644 --- a/main.py +++ b/main.py @@ -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) diff --git a/ui_template/ui_template_locale_a.ts b/ui_template/ui_template_locale_a.ts index f1452096..1768a183 100644 --- a/ui_template/ui_template_locale_a.ts +++ b/ui_template/ui_template_locale_a.ts @@ -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로 출력합니다.", diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index b7eeae2c..b606ea79 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -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 결재 --- */ diff --git a/ui_template/ui_template_workflow_layout.ts b/ui_template/ui_template_workflow_layout.ts index b040bb77..f2a766be 100644 --- a/ui_template/ui_template_workflow_layout.ts +++ b/ui_template/ui_template_workflow_layout.ts @@ -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[],