From 0909db020f6f9fe2faf3912a0fc5ce85e3761d0f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 27 Oct 2026 20:23:16 +0900 Subject: [PATCH] =?UTF-8?q?feat(E):=20=EB=8B=A8=EA=B3=84=20=EC=B0=A8?= =?UTF-8?q?=EB=A1=80=20=EB=B3=B4=EA=B3=A0=EC=84=9C=20=EB=A7=A8=20=EB=92=A4?= =?UTF-8?q?=20=C2=B7=20DB=20=EB=8B=A8=EA=B3=84=20=ED=96=89=20=EC=97=AC?= =?UTF-8?q?=EC=84=AF=20=C2=B7=20=EC=B2=B4=EC=9D=B8=20=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=ED=95=A8=EC=88=98=20(PLAN=20565-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 차례 = 지반 · 2D · 3D · 암반 · 광역 · 보고서 — STAB_STEP_ROUTES · 단계 막대 이름 · 아이콘 · 대시보드 요약 이름 같은 차례 - 단계 행 STABILITY 여섯(STAB_AREA 더함) · 022_stab_stages_six.sql = 옛 행 열쇠로 번호만 바꿈 · 없는 광역 행 NOT_STARTED · 다시 돌려도 같음 - mark_stab_step(project_id, step, state, message) — 체인 낱말(ground · 2d · 3d · rock · area · report × run · done · skip · error · cancel) → 단계 행 상태 · 열쇠로 찾음 · 실패 까닭만 남김 - E05 빈 상태 글 = E02 [해석 시작] · 보고서 안내에 암반 · 확률 - 시험 528 단계 여섯 · 상태 함수 · 280_2 가짜 차례 여섯 + 진짜 차례 끝 = E05 Co-Authored-By: Claude Opus 5.5 --- A00_Common/group_workflow.ts | 4 +- B01_Dashboard/B01_Dashboard_Summary.py | 2 +- B01_Dashboard/B01_Dashboard_UI_Projects.ts | 2 +- E05_StabReport/E05_StabReport_Build.py | 2 +- common_util/common_util_workflow_state.py | 90 ++++++++++++++++++++- config/config_frontend.ts | 4 +- db_management/022_stab_stages_six.sql | 31 +++++++ resources/tester/helper_280_2_step_nav.cjs | 11 ++- resources/tester/test_280_2_step_nav.py | 8 ++ resources/tester/test_528_project_groups.py | 34 +++++++- ui_template/ui_template_locale_e05.ts | 4 +- 11 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 db_management/022_stab_stages_six.sql diff --git a/A00_Common/group_workflow.ts b/A00_Common/group_workflow.ts index 5217705d5..9bdda4a68 100644 --- a/A00_Common/group_workflow.ts +++ b/A00_Common/group_workflow.ts @@ -37,11 +37,11 @@ const FLOWS: readonly GroupFlow[] = [ "DE_Step_StabGround", "DE_Step_StabSection", "DE_Step_Stab3D", - "DE_Step_StabReport", "DE_Step_StabRock", "DE_Step_StabArea", + "DE_Step_StabReport", ], - icons: ["🪨", "📐", "🧊", "📄", "⛰️", "🗺️"], + icons: ["🪨", "📐", "🧊", "⛰️", "🗺️", "📄"], }, ]; diff --git a/B01_Dashboard/B01_Dashboard_Summary.py b/B01_Dashboard/B01_Dashboard_Summary.py index eeec1b3a3..d9f7f04cd 100644 --- a/B01_Dashboard/B01_Dashboard_Summary.py +++ b/B01_Dashboard/B01_Dashboard_Summary.py @@ -26,7 +26,7 @@ THUMB_POINTS = 100 STAGE_NAMES: dict[str, tuple[str, ...]] = { "ROAD": ("파일입력", "전처리", "노선설계", "횡단설계", "상세설계", "수량산출", "원가계산"), "PLAN": ("기준 지정", "노선 분석", "확정 · 내보내기"), - "STABILITY": ("지반 조건", "2D 해석", "3D 해석", "보고서", "암반 해석", "광역 위험"), + "STABILITY": ("지반 조건", "2D 해석", "3D 해석", "암반 해석", "광역 위험", "보고서"), } diff --git a/B01_Dashboard/B01_Dashboard_UI_Projects.ts b/B01_Dashboard/B01_Dashboard_UI_Projects.ts index 313aeac3f..ba7ae0660 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Projects.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Projects.ts @@ -66,7 +66,7 @@ export function projectTable(projects: ProjectItem[], currentUser: DashboardUser ); } -/** 단계 막대 — 종류별 단계(ROAD = B03~B09 · PLAN = D02~D04 · STABILITY = E02~E05) · 누름 = 그 단계 화면 */ +/** 단계 막대 — 종류별 단계(ROAD = B03~B09 · PLAN = D02~D04 · STABILITY = E02 · E03 · E04 · E06 · E07 · E05) · 누름 = 그 단계 화면 */ export function workflow(project: ProjectItem): HTMLElement { const group = groupFlowOf(project.project_kind); const routes = group?.routes ?? WORKFLOW_STEP_ROUTES; diff --git a/E05_StabReport/E05_StabReport_Build.py b/E05_StabReport/E05_StabReport_Build.py index bf0407150..acb6df786 100644 --- a/E05_StabReport/E05_StabReport_Build.py +++ b/E05_StabReport/E05_StabReport_Build.py @@ -194,7 +194,7 @@ def gather(project_root: Path) -> dict[str, Any]: project_root = Path(project_root) r2 = _result2d(project_root) if not r2 or not r2.get("stations"): - raise MissingInput("2D 해석 결과가 없습니다 — E03 에서 [해석] 뒤 만드세요.") + raise MissingInput("2D 해석 결과가 없습니다 — E02 에서 [해석 시작] 뒤 만드세요.") root = source.source_dir(project_root) db = source.db_snapshot(root) ground = _ground(project_root) diff --git a/common_util/common_util_workflow_state.py b/common_util/common_util_workflow_state.py index b6bb4f882..aaf1657e1 100644 --- a/common_util/common_util_workflow_state.py +++ b/common_util/common_util_workflow_state.py @@ -20,11 +20,37 @@ STAGE_KEYS = [ ] -# 종류별 단계(요구 345) — ROAD = B03~B09 · PLAN = D02~D04 · STABILITY = E02~E06. +# 종류별 단계(요구 345 · 390) — ROAD = B03~B09 · PLAN = D02~D04 · +# STABILITY = E02 · E03 · E04 · E06 · E07 · E05(보고서 맨 뒤 · 옛 행 = 022 SQL 로 다시 깖). STAGE_KEYS_BY_KIND = { "ROAD": STAGE_KEYS, "PLAN": ["PLAN_BASE", "PLAN_ANALYSIS", "PLAN_CONFIRM"], - "STABILITY": ["STAB_GROUND", "STAB_SECTION", "STAB_3D", "STAB_REPORT", "STAB_ROCK"], + "STABILITY": [ + "STAB_GROUND", + "STAB_SECTION", + "STAB_3D", + "STAB_ROCK", + "STAB_AREA", + "STAB_REPORT", + ], +} + +# E 체인 단계 열쇠(`E02_StabGround_Chain.STEPS` + 지반) → 단계 행 열쇠 +STAB_STEP_STAGE_KEYS = { + "ground": "STAB_GROUND", + "2d": "STAB_SECTION", + "3d": "STAB_3D", + "rock": "STAB_ROCK", + "area": "STAB_AREA", + "report": "STAB_REPORT", +} +# 체인 단계 상태 → 단계 행 상태(건너뜀 = 저장본이 입력과 같음 · 끊음 = 옛 결과 낡았을 수 있음) +STAB_STEP_STATES = { + "run": "IN_PROGRESS", + "done": "COMPLETE", + "skip": "COMPLETE", + "error": "FAILED", + "cancel": "STALE", } @@ -208,6 +234,66 @@ async def mark_stage( ) +async def mark_stab_step( + project_id: str, + step: str, + state: str, + message: str | None = None, + cursor: aiomysql.DictCursor | None = None, +) -> None: + """E 안정성평가 단계 상태 한 곳(요구 390 · PLAN 565-1) — 대시보드 막대 색. + + `step` = `STAB_STEP_STAGE_KEYS` 열쇠 · `state` = `STAB_STEP_STATES` 열쇠(체인 낱말 그대로) · + `message` = 실패 까닭(행 칸 길이로 자름). 커서 없으면 제 트랜잭션 · DB 없으면 아무것도 안 함. + 뒤 단계를 STALE 로 돌리지 않음(체인이 차례로 다 돎). + """ + stage_key = STAB_STEP_STAGE_KEYS[step] + row_state = STAB_STEP_STATES[state] + if cursor is None: + from config import config_db + + if config_db.db_pool is None: + return + async with config_db.get_db_pool().acquire() as connection: + await connection.begin() + try: + async with connection.cursor() as own: + await mark_stab_step(project_id, step, state, message, own) + await connection.commit() + except Exception: + await connection.rollback() + raise + return + now = datetime.utcnow() + trimmed = (message or "").strip()[:_STAGE_MESSAGE_MAX_LENGTH] or None + await cursor.execute( + """ + UPDATE project_workflow_stages + SET state = %s, + progress_percent = CASE WHEN %s = 'COMPLETE' THEN 100 + WHEN %s = 'IN_PROGRESS' THEN 0 ELSE progress_percent END, + message = CASE WHEN message LIKE 'DEV_UNLOCK:%%' THEN message ELSE %s END, + started_at = CASE WHEN %s = 'IN_PROGRESS' THEN %s ELSE started_at END, + completed_at = CASE WHEN %s = 'COMPLETE' THEN %s + WHEN %s = 'IN_PROGRESS' THEN NULL ELSE completed_at END + WHERE project_id = %s AND stage_key = %s + """, + ( + row_state, + row_state, + row_state, + trimmed if row_state == "FAILED" else None, + row_state, + now, + row_state, + now, + row_state, + project_id, + stage_key, + ), + ) + + async def update_stage_progress( cursor: aiomysql.DictCursor, project_id: str, stage_no: int, progress: int ) -> None: diff --git a/config/config_frontend.ts b/config/config_frontend.ts index e256ad5e0..c65487277 100644 --- a/config/config_frontend.ts +++ b/config/config_frontend.ts @@ -141,14 +141,14 @@ export const PLAN_STEP_ROUTES = [ ROUTES.D04_PLAN_CONFIRM, ] as const; -/** E 안정성평가 단계 주소(E02 → E06 · 광역 E07 · 생성 E01 은 단계 밖) */ +/** E 안정성평가 단계 주소(E02 → E04 · 암반 E06 · 광역 E07 · 보고서 E05 맨 뒤 · 생성 E01 은 단계 밖) */ export const STAB_STEP_ROUTES = [ ROUTES.E02_STAB_GROUND, ROUTES.E03_STAB_SECTION, ROUTES.E04_STAB_3D, - ROUTES.E05_STAB_REPORT, ROUTES.E06_STAB_ROCK, ROUTES.E07_STAB_AREA, + ROUTES.E05_STAB_REPORT, ] as const; export type RouteKey = keyof typeof ROUTES; diff --git a/db_management/022_stab_stages_six.sql b/db_management/022_stab_stages_six.sql new file mode 100644 index 000000000..f0a50ee73 --- /dev/null +++ b/db_management/022_stab_stages_six.sql @@ -0,0 +1,31 @@ +-- 022_stab_stages_six.sql +-- 안정성평가 단계 행 여섯 · 보고서 맨 뒤 (요구서 390 3장 5 · PLAN 565-1) +-- 차례 = STAB_GROUND 0 · STAB_SECTION 1 · STAB_3D 2 · STAB_ROCK 3 · STAB_AREA 4 · STAB_REPORT 5 +-- 옛 행(다섯 · REPORT 3 · ROCK 4)은 열쇠로 번호만 바꿈(상태 그대로) · 없는 광역 행 = NOT_STARTED · 다시 돌려도 같음 + +USE aislo_db; + +-- 1) 옛 번호 비켜 둠(uq_project_stage 겹침 피함) +UPDATE project_workflow_stages w +JOIN projects p ON p.id = w.project_id +SET w.stage_no = w.stage_no + 100 +WHERE p.project_kind = 'STABILITY' AND w.stage_no < 100; + +-- 2) 열쇠로 새 번호 +UPDATE project_workflow_stages w +JOIN projects p ON p.id = w.project_id +SET w.stage_no = CASE w.stage_key + WHEN 'STAB_GROUND' THEN 0 + WHEN 'STAB_SECTION' THEN 1 + WHEN 'STAB_3D' THEN 2 + WHEN 'STAB_ROCK' THEN 3 + WHEN 'STAB_AREA' THEN 4 + WHEN 'STAB_REPORT' THEN 5 +END +WHERE p.project_kind = 'STABILITY' AND w.stage_no >= 100; + +-- 3) 없는 광역 행 +INSERT IGNORE INTO project_workflow_stages (project_id, stage_no, stage_key, state, progress_percent) +SELECT p.id, 4, 'STAB_AREA', 'NOT_STARTED', 0 +FROM projects p +WHERE p.project_kind = 'STABILITY'; diff --git a/resources/tester/helper_280_2_step_nav.cjs b/resources/tester/helper_280_2_step_nav.cjs index 01d9efb6d..799750e73 100644 --- a/resources/tester/helper_280_2_step_nav.cjs +++ b/resources/tester/helper_280_2_step_nav.cjs @@ -119,7 +119,15 @@ const STUBS = { ROUTES, API_BASE_URL: "", PLAN_STEP_ROUTES: ["d02-plan-base", "d03-plan-analysis", "d04-plan-confirm"], - STAB_STEP_ROUTES: ["e02-stab-ground", "e03-stab-section", "e04-stab-3d", "e05-stab-report"], + // 565-1 — 보고서(E05) 맨 뒤 + STAB_STEP_ROUTES: [ + "e02-stab-ground", + "e03-stab-section", + "e04-stab-3d", + "e06-stab-rock", + "e07-stab-area", + "e05-stab-report", + ], }, "./ui_template_locale": { t: (key) => key }, "./ui_template_modal": { openModal: () => undefined }, @@ -202,6 +210,7 @@ const ALL = [ "d04-plan-confirm", "e01-stab-register", "e02-stab-ground", + "e07-stab-area", "e05-stab-report", ]; const rows = {}; diff --git a/resources/tester/test_280_2_step_nav.py b/resources/tester/test_280_2_step_nav.py index 20a92d14c..2e8ae7087 100644 --- a/resources/tester/test_280_2_step_nav.py +++ b/resources/tester/test_280_2_step_nav.py @@ -43,6 +43,14 @@ def test_529_1_D_E_묶음_안에서만_앞뒤(results): assert rows["d04-plan-confirm"]["plain"]["disabled"][:3] == [False, False, True] assert rows["e02-stab-ground"]["plain"]["disabled"][:3] == [False, True, False] assert rows["e05-stab-report"]["plain"]["disabled"][:3] == [False, False, True] + # 565-1 — 광역(E07) 뒤에 보고서 · 진짜 차례도 E05 가 끝 + assert rows["e07-stab-area"]["plain"]["disabled"][:3] == [False, False, False] + config = (ROOT / "config" / "config_frontend.ts").read_text(encoding="utf-8") + stab = config.split("export const STAB_STEP_ROUTES = [")[1].split("]")[0] + assert [s.strip() for s in stab.split(",") if s.strip()][-2:] == [ + "ROUTES.E07_STAB_AREA", + "ROUTES.E05_STAB_REPORT", + ] @pytest.mark.parametrize("shape", ["helpRow", "plain"]) diff --git a/resources/tester/test_528_project_groups.py b/resources/tester/test_528_project_groups.py index 9f76ff31f..907fe4cce 100644 --- a/resources/tester/test_528_project_groups.py +++ b/resources/tester/test_528_project_groups.py @@ -19,7 +19,12 @@ from A07_Register.A07_Register_Schema import RegisterRequest from common_util import common_util_auth as auth from common_util import common_util_auth_repository as auth_repo from common_util.common_util_project_kind import KIND_STAGE_DIRS, init_kind_storage -from common_util.common_util_workflow_state import initialize_project_stages +from common_util.common_util_workflow_state import ( + STAB_STEP_STAGE_KEYS, + STAGE_KEYS_BY_KIND, + initialize_project_stages, + mark_stab_step, +) from config.config_system import PROJECT_STORAGE_STAGE_DIRS from D01_PlanRegister import D01_PlanRegister_Router as d01 from E01_StabRegister import E01_StabRegister_Router as e01 @@ -261,13 +266,38 @@ def test_register_ignores_sent_company(): assert "company_name" not in data -@pytest.mark.parametrize("kind,count", [("ROAD", 7), ("PLAN", 3), ("STABILITY", 5)]) +@pytest.mark.parametrize("kind,count", [("ROAD", 7), ("PLAN", 3), ("STABILITY", 6)]) def test_stage_seed_by_kind(kind, count): log = [] asyncio.run(initialize_project_stages(FakeCursor({}, log), "p1", kind)) assert len(log) == count if kind == "PLAN": assert [a[2] for _, a in log] == ["PLAN_BASE", "PLAN_ANALYSIS", "PLAN_CONFIRM"] + if kind == "STABILITY": # 565-1 — 보고서 맨 뒤 + assert [a[2] for _, a in log][3:] == ["STAB_ROCK", "STAB_AREA", "STAB_REPORT"] + + +def test_565_1_stab_step_marks_by_key(): + """체인 낱말 → 행 상태 · 열쇠로 찾음 · 실패 까닭만 남김 · 모르는 낱말 거절.""" + log = [] + for step, state, message in ( + ("3d", "run", None), + ("3d", "skip", None), + ("area", "error", "x" * 300), + ("report", "cancel", None), + ): + asyncio.run(mark_stab_step("p1", step, state, message, FakeCursor({}, log))) + rows = [(args[0], args[3], args[-1]) for _, args in log] + assert rows == [ + ("IN_PROGRESS", None, "STAB_3D"), + ("COMPLETE", None, "STAB_3D"), + ("FAILED", "x" * 200, "STAB_AREA"), + ("STALE", None, "STAB_REPORT"), + ] + assert all("stage_key = %s" in sql for sql, _ in log) + assert set(STAB_STEP_STAGE_KEYS.values()) == set(STAGE_KEYS_BY_KIND["STABILITY"]) + with pytest.raises(KeyError): + asyncio.run(mark_stab_step("p1", "2d", "weird", None, FakeCursor({}, log))) def test_storage_dirs_allowed(): diff --git a/ui_template/ui_template_locale_e05.ts b/ui_template/ui_template_locale_e05.ts index 1e1b979b0..27dc95934 100644 --- a/ui_template/ui_template_locale_e05.ts +++ b/ui_template/ui_template_locale_e05.ts @@ -10,8 +10,8 @@ import type { LocaleEntry } from "./ui_template_locale_common"; export const ui_locales_e05 = { /* --- E05 보고서 --- */ E05_Note: [ - "표지 · 개요는 사용자 몫 — [보고서 갱신] 때 그대로 둡니다. 목차 · 입력조건 · 측점표 · 대표단면 · 3D · 광역 · 보강 · 부록은 해석 결과로 다시 씁니다.", - "Cover and summary are yours — kept on [Refresh report]. Contents, conditions, stations, sections, 3D, area, reinforcement and appendix sheets are rewritten from results.", + "표지 · 개요는 사용자 몫 — [보고서 갱신] 때 그대로 둡니다. 목차 · 입력조건 · 측점표 · 대표단면 · 3D · 광역 · 보강 · 암반 · 확률 · 부록은 해석 결과로 다시 씁니다.", + "Cover and summary are yours — kept on [Refresh report]. Contents, conditions, stations, sections, 3D, area, reinforcement, rock, probability and appendix sheets are rewritten from results.", ], E05_Btn_Refresh: ["보고서 갱신", "Refresh report"], E05_Btn_Xlsx: ["엑셀", "Excel"],