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:
@@ -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"),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)})
|
||||
@@ -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
-1
@@ -1,5 +1,5 @@
|
||||
/* =============================================================================
|
||||
* B09_wf6_Estimation_UI_Page.ts
|
||||
* B09_Estimation_UI_Page.ts
|
||||
* 로그인 후 09: 6차 워크플로우 (견적·문서)
|
||||
*
|
||||
* ⚠️ 본문 준비 중 — 워크플로우 셸(헤더+스텝바)만 구성. 추후 구체화.
|
||||
@@ -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,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)
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -692,7 +692,7 @@ PROJECT_STORAGE_STAGE_DIRS = frozenset(
|
||||
"B06_Section",
|
||||
"B08_DesignDetail",
|
||||
"B07_Quantity",
|
||||
"B09_wf6_Estimation",
|
||||
"B09_Estimation",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, -- 해당 단계에서 사용자가 선택/입력한 값 (재실행 시 재사용)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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로 출력합니다.",
|
||||
|
||||
@@ -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 결재 --- */
|
||||
|
||||
@@ -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[],
|
||||
|
||||
Reference in New Issue
Block a user