From a1069f270af7afc0621292088c03b30ae5e744d6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 25 Oct 2026 19:32:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(529-1):=20=EB=8B=A8=EA=B3=84=20=ED=8B=80?= =?UTF-8?q?=20=EC=9D=B8=EC=9E=90(=EC=95=84=EC=9D=B4=EC=BD=98=20=C2=B7=20[]=20=EB=AC=B6=EC=9D=8C=20=C2=B7=20=EB=8F=84=EC=9B=80?= =?UTF-8?q?=EB=A7=90=20=EC=9D=B4=EB=A6=84)=20=C2=B7=20D=20=C2=B7=20E=20?= =?UTF-8?q?=EB=8B=A8=EA=B3=84=20=EB=B9=88=20=EA=B8=B8=20=EC=9D=BC=EA=B3=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - createWorkflowLayout icons 인자(없으면 B 일곱 아이콘 그대로) - [?] [<] [>] 묶음 = B05~B09 · D02~D04 · E02~E05 · 묶음 안에서만 오감 · 설명서 이름 B0N/D0N/E0N - 서버 설명서 주소 D01~D04 · E01~E05 허용 · 우클릭 막기 · 돋보기 d · e 주소 - ROUTES D01~E05 · PLAN_STEP_ROUTES · STAB_STEP_ROUTES · A00_Common/group_workflow(빈 단계 = 틀 · 좌측 [?] 줄 · 단계 막대) - 페이지 폴더마다 draft/<폴더>_Route.ts · tsconfig 에 D0* · E0* · 문구 ui_template_locale_de - 시험: 280-2 대역에 D · E 묶음 · 54-2 D · E 설명서 길 Co-Authored-By: Claude Opus 5.5 --- A00_Common/group_workflow.ts | 60 +++++++++++++++++++ D02_PlanBase/draft/D02_PlanBase_Route.ts | 17 ++++++ .../draft/D03_PlanAnalysis_Route.ts | 17 ++++++ .../draft/D04_PlanConfirm_Route.ts | 17 ++++++ E02_StabGround/draft/E02_StabGround_Route.ts | 17 ++++++ .../draft/E03_StabSection_Route.ts | 17 ++++++ E04_Stab3D/draft/E04_Stab3D_Route.ts | 17 ++++++ E05_StabReport/draft/E05_StabReport_Route.ts | 17 ++++++ common_util/common_util_help_router.py | 2 +- config/config_frontend.ts | 25 ++++++++ resources/tester/helper_280_2_step_nav.cjs | 14 ++++- resources/tester/test_280_2_step_nav.py | 13 ++++ resources/tester/test_54_2_help_files.py | 6 +- tsconfig.json | 2 +- ui_template/ui_template_help.ts | 42 +++++++------ ui_template/ui_template_locale.ts | 3 + ui_template/ui_template_locale_de.ts | 21 +++++++ ui_template/ui_template_loupe.ts | 4 +- ui_template/ui_template_native_menu.ts | 4 +- ui_template/ui_template_workflow_layout.ts | 4 +- 20 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 A00_Common/group_workflow.ts create mode 100644 D02_PlanBase/draft/D02_PlanBase_Route.ts create mode 100644 D03_PlanAnalysis/draft/D03_PlanAnalysis_Route.ts create mode 100644 D04_PlanConfirm/draft/D04_PlanConfirm_Route.ts create mode 100644 E02_StabGround/draft/E02_StabGround_Route.ts create mode 100644 E03_StabSection/draft/E03_StabSection_Route.ts create mode 100644 E04_Stab3D/draft/E04_Stab3D_Route.ts create mode 100644 E05_StabReport/draft/E05_StabReport_Route.ts create mode 100644 ui_template/ui_template_locale_de.ts diff --git a/A00_Common/group_workflow.ts b/A00_Common/group_workflow.ts new file mode 100644 index 000000000..4ece4cdaa --- /dev/null +++ b/A00_Common/group_workflow.ts @@ -0,0 +1,60 @@ +/* ============================================================================= + * group_workflow.ts + * D 예정노선 · E 안정성평가 단계 묶음(요구 345 · PLAN 529-1) — B05~B09 틀(`createWorkflowLayout`)을 + * 단계 목록 · 아이콘 · 주소 인자로 그대로 씀. 빈 단계 = 틀 · 좌측 패널([?] [<] [>]) · 단계 막대만. + * 페이지 길 = 각 폴더 `draft/<폴더>_Route.ts` · 본문이 생기면 그 폴더 페이지로 갈아 끼움. + * ========================================================================== */ + +import { PLAN_STEP_ROUTES, STAB_STEP_ROUTES, type RoutePath } from "@config/config_frontend"; +import { el } from "@ui/ui_template_elements"; +import { t, type LocaleKey } from "@ui/ui_template_locale"; +import { createWorkflowLayout } from "@ui/ui_template_workflow_layout"; +import { navigateTo, type PageRenderer } from "./router"; + +interface GroupFlow { + routes: readonly RoutePath[]; + labels: readonly LocaleKey[]; + icons: readonly string[]; +} + +const FLOWS: readonly GroupFlow[] = [ + { + routes: PLAN_STEP_ROUTES, + labels: ["DE_Step_PlanBase", "DE_Step_PlanAnalysis", "DE_Step_PlanConfirm"], + icons: ["📍", "🧭", "✅"], + }, + { + routes: STAB_STEP_ROUTES, + labels: ["DE_Step_StabGround", "DE_Step_StabSection", "DE_Step_Stab3D", "DE_Step_StabReport"], + icons: ["🪨", "📐", "🧊", "📄"], + }, +]; + +/** 단계 틀을 세움 — mainContent · leftPanel 이 없으면 빈 단계 안내 · [?] 줄만 */ +export function mountGroupStep( + root: HTMLElement, + route: RoutePath, + parts: { mainContent?: HTMLElement; leftPanel?: HTMLElement } = {}, +): void { + const flow = FLOWS.find((f) => f.routes.includes(route)); + if (!flow) throw new Error(`단계 묶음 밖 주소: ${route}`); + const active = flow.routes.indexOf(route); + const layout = createWorkflowLayout({ + title: t(flow.labels[active]), + steps: flow.labels.map((key) => t(key)), + icons: flow.icons, + activeStep: active, + routes: flow.routes, + leftPanel: parts.leftPanel, + mainContent: + parts.mainContent ?? el("p", { className: "app-placeholder", text: t("DE_Step_Empty") }), + onStepClick: (_index, next) => navigateTo(next as RoutePath), + }); + root.append(layout.root); +} + +/** 빈 단계 페이지(길 선언의 load 가 부름) */ +export const groupStepPage = + (route: RoutePath): PageRenderer => + (root) => + mountGroupStep(root, route); diff --git a/D02_PlanBase/draft/D02_PlanBase_Route.ts b/D02_PlanBase/draft/D02_PlanBase_Route.ts new file mode 100644 index 000000000..1db951dd7 --- /dev/null +++ b/D02_PlanBase/draft/D02_PlanBase_Route.ts @@ -0,0 +1,17 @@ +/* ============================================================================= + * D02_PlanBase_Route.ts + * 길 선언(PLAN 529-1) — 라우터가 페이지 폴더마다 draft/*_Route.ts 를 glob 해 등록. + * 로그인 · 회사 · 현재 프로젝트 필요 · 꼬리말 없음 · 본문 = 빈 단계(틀 · 패널 · 단계 막대) + * ========================================================================== */ + +import { ROUTES } from "@config/config_frontend"; +import type { DraftRoute } from "../../A00_Common/router"; + +export const ROUTE: DraftRoute = { + path: ROUTES.D02_PLAN_BASE, + workflow: true, + project: true, + footerless: true, + load: async () => + (await import("../../A00_Common/group_workflow")).groupStepPage(ROUTES.D02_PLAN_BASE), +}; diff --git a/D03_PlanAnalysis/draft/D03_PlanAnalysis_Route.ts b/D03_PlanAnalysis/draft/D03_PlanAnalysis_Route.ts new file mode 100644 index 000000000..00fe6d874 --- /dev/null +++ b/D03_PlanAnalysis/draft/D03_PlanAnalysis_Route.ts @@ -0,0 +1,17 @@ +/* ============================================================================= + * D03_PlanAnalysis_Route.ts + * 길 선언(PLAN 529-1) — 라우터가 페이지 폴더마다 draft/*_Route.ts 를 glob 해 등록. + * 로그인 · 회사 · 현재 프로젝트 필요 · 꼬리말 없음 · 본문 = 빈 단계(틀 · 패널 · 단계 막대) + * ========================================================================== */ + +import { ROUTES } from "@config/config_frontend"; +import type { DraftRoute } from "../../A00_Common/router"; + +export const ROUTE: DraftRoute = { + path: ROUTES.D03_PLAN_ANALYSIS, + workflow: true, + project: true, + footerless: true, + load: async () => + (await import("../../A00_Common/group_workflow")).groupStepPage(ROUTES.D03_PLAN_ANALYSIS), +}; diff --git a/D04_PlanConfirm/draft/D04_PlanConfirm_Route.ts b/D04_PlanConfirm/draft/D04_PlanConfirm_Route.ts new file mode 100644 index 000000000..c59fad866 --- /dev/null +++ b/D04_PlanConfirm/draft/D04_PlanConfirm_Route.ts @@ -0,0 +1,17 @@ +/* ============================================================================= + * D04_PlanConfirm_Route.ts + * 길 선언(PLAN 529-1) — 라우터가 페이지 폴더마다 draft/*_Route.ts 를 glob 해 등록. + * 로그인 · 회사 · 현재 프로젝트 필요 · 꼬리말 없음 · 본문 = 빈 단계(틀 · 패널 · 단계 막대) + * ========================================================================== */ + +import { ROUTES } from "@config/config_frontend"; +import type { DraftRoute } from "../../A00_Common/router"; + +export const ROUTE: DraftRoute = { + path: ROUTES.D04_PLAN_CONFIRM, + workflow: true, + project: true, + footerless: true, + load: async () => + (await import("../../A00_Common/group_workflow")).groupStepPage(ROUTES.D04_PLAN_CONFIRM), +}; diff --git a/E02_StabGround/draft/E02_StabGround_Route.ts b/E02_StabGround/draft/E02_StabGround_Route.ts new file mode 100644 index 000000000..13e8f23bb --- /dev/null +++ b/E02_StabGround/draft/E02_StabGround_Route.ts @@ -0,0 +1,17 @@ +/* ============================================================================= + * E02_StabGround_Route.ts + * 길 선언(PLAN 529-1) — 라우터가 페이지 폴더마다 draft/*_Route.ts 를 glob 해 등록. + * 로그인 · 회사 · 현재 프로젝트 필요 · 꼬리말 없음 · 본문 = 빈 단계(틀 · 패널 · 단계 막대) + * ========================================================================== */ + +import { ROUTES } from "@config/config_frontend"; +import type { DraftRoute } from "../../A00_Common/router"; + +export const ROUTE: DraftRoute = { + path: ROUTES.E02_STAB_GROUND, + workflow: true, + project: true, + footerless: true, + load: async () => + (await import("../../A00_Common/group_workflow")).groupStepPage(ROUTES.E02_STAB_GROUND), +}; diff --git a/E03_StabSection/draft/E03_StabSection_Route.ts b/E03_StabSection/draft/E03_StabSection_Route.ts new file mode 100644 index 000000000..4b3805d51 --- /dev/null +++ b/E03_StabSection/draft/E03_StabSection_Route.ts @@ -0,0 +1,17 @@ +/* ============================================================================= + * E03_StabSection_Route.ts + * 길 선언(PLAN 529-1) — 라우터가 페이지 폴더마다 draft/*_Route.ts 를 glob 해 등록. + * 로그인 · 회사 · 현재 프로젝트 필요 · 꼬리말 없음 · 본문 = 빈 단계(틀 · 패널 · 단계 막대) + * ========================================================================== */ + +import { ROUTES } from "@config/config_frontend"; +import type { DraftRoute } from "../../A00_Common/router"; + +export const ROUTE: DraftRoute = { + path: ROUTES.E03_STAB_SECTION, + workflow: true, + project: true, + footerless: true, + load: async () => + (await import("../../A00_Common/group_workflow")).groupStepPage(ROUTES.E03_STAB_SECTION), +}; diff --git a/E04_Stab3D/draft/E04_Stab3D_Route.ts b/E04_Stab3D/draft/E04_Stab3D_Route.ts new file mode 100644 index 000000000..0f7aa9118 --- /dev/null +++ b/E04_Stab3D/draft/E04_Stab3D_Route.ts @@ -0,0 +1,17 @@ +/* ============================================================================= + * E04_Stab3D_Route.ts + * 길 선언(PLAN 529-1) — 라우터가 페이지 폴더마다 draft/*_Route.ts 를 glob 해 등록. + * 로그인 · 회사 · 현재 프로젝트 필요 · 꼬리말 없음 · 본문 = 빈 단계(틀 · 패널 · 단계 막대) + * ========================================================================== */ + +import { ROUTES } from "@config/config_frontend"; +import type { DraftRoute } from "../../A00_Common/router"; + +export const ROUTE: DraftRoute = { + path: ROUTES.E04_STAB_3D, + workflow: true, + project: true, + footerless: true, + load: async () => + (await import("../../A00_Common/group_workflow")).groupStepPage(ROUTES.E04_STAB_3D), +}; diff --git a/E05_StabReport/draft/E05_StabReport_Route.ts b/E05_StabReport/draft/E05_StabReport_Route.ts new file mode 100644 index 000000000..62a5a5bb7 --- /dev/null +++ b/E05_StabReport/draft/E05_StabReport_Route.ts @@ -0,0 +1,17 @@ +/* ============================================================================= + * E05_StabReport_Route.ts + * 길 선언(PLAN 529-1) — 라우터가 페이지 폴더마다 draft/*_Route.ts 를 glob 해 등록. + * 로그인 · 회사 · 현재 프로젝트 필요 · 꼬리말 없음 · 본문 = 빈 단계(틀 · 패널 · 단계 막대) + * ========================================================================== */ + +import { ROUTES } from "@config/config_frontend"; +import type { DraftRoute } from "../../A00_Common/router"; + +export const ROUTE: DraftRoute = { + path: ROUTES.E05_STAB_REPORT, + workflow: true, + project: true, + footerless: true, + load: async () => + (await import("../../A00_Common/group_workflow")).groupStepPage(ROUTES.E05_STAB_REPORT), +}; diff --git a/common_util/common_util_help_router.py b/common_util/common_util_help_router.py index 1f9c4be92..f08a44ba8 100644 --- a/common_util/common_util_help_router.py +++ b/common_util/common_util_help_router.py @@ -14,7 +14,7 @@ from fastapi import APIRouter, HTTPException router = APIRouter(prefix="/api/help", tags=["help"]) HELP_ROOT = Path(__file__).resolve().parent.parent / "resources" / "help" -PAGE_RE = re.compile(r"^B0[2-9]$") +PAGE_RE = re.compile(r"^(B0[2-9]|D0[1-4]|E0[1-5])$") def read_help(page: str, root: Path = HELP_ROOT) -> list[dict[str, str]]: diff --git a/config/config_frontend.ts b/config/config_frontend.ts index 01ddc0a75..9d295ec1c 100644 --- a/config/config_frontend.ts +++ b/config/config_frontend.ts @@ -120,8 +120,33 @@ export const ROUTES = { M01_MASTER_DATA: "m01-master-data", // 마스터 템플릿 — 표 양식 · 도면 양식(시스템 · 회사 · 개인 · 프로젝트 층). 화면은 로그인만 확인. M02_MASTER_TEMPLATE: "m02-master-template", + // D 예정노선 · E 안정성평가(요구 345) — 길 선언은 페이지 폴더 `draft/*_Route.ts` + D01_PLAN_REGISTER: "d01-plan-register", + D02_PLAN_BASE: "d02-plan-base", + D03_PLAN_ANALYSIS: "d03-plan-analysis", + D04_PLAN_CONFIRM: "d04-plan-confirm", + E01_STAB_REGISTER: "e01-stab-register", + E02_STAB_GROUND: "e02-stab-ground", + E03_STAB_SECTION: "e03-stab-section", + E04_STAB_3D: "e04-stab-3d", + E05_STAB_REPORT: "e05-stab-report", } as const; +/** D 예정노선 단계 주소(D02 → D04 · 등록 D01 은 단계 밖) */ +export const PLAN_STEP_ROUTES = [ + ROUTES.D02_PLAN_BASE, + ROUTES.D03_PLAN_ANALYSIS, + ROUTES.D04_PLAN_CONFIRM, +] as const; + +/** E 안정성평가 단계 주소(E02 → E05 · 생성 E01 은 단계 밖) */ +export const STAB_STEP_ROUTES = [ + ROUTES.E02_STAB_GROUND, + ROUTES.E03_STAB_SECTION, + ROUTES.E04_STAB_3D, + ROUTES.E05_STAB_REPORT, +] as const; + export type RouteKey = keyof typeof ROUTES; export type RoutePath = (typeof ROUTES)[RouteKey]; diff --git a/resources/tester/helper_280_2_step_nav.cjs b/resources/tester/helper_280_2_step_nav.cjs index 3e62d8c86..92ed1250a 100644 --- a/resources/tester/helper_280_2_step_nav.cjs +++ b/resources/tester/helper_280_2_step_nav.cjs @@ -115,7 +115,12 @@ const toasts = []; let role = "SYSTEM_ADMIN"; let help; const STUBS = { - "@config/config_frontend": { ROUTES, API_BASE_URL: "" }, + "@config/config_frontend": { + 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"], + }, "./ui_template_locale": { t: (key) => key }, "./ui_template_modal": { openModal: () => undefined }, "./ui_template_elements": { @@ -190,6 +195,13 @@ const ALL = [ "b09-print", "m01-master-data", "m02-master-template", + // 529-1 D · E 묶음 — 묶음 안에서만 [<] [>] · 등록(D01 · E01) = [?] 만 + "d01-plan-register", + "d02-plan-base", + "d04-plan-confirm", + "e01-stab-register", + "e02-stab-ground", + "e05-stab-report", ]; const rows = {}; for (const route of ALL) rows[route] = { helpRow: rowOf(route, true), plain: rowOf(route, false) }; diff --git a/resources/tester/test_280_2_step_nav.py b/resources/tester/test_280_2_step_nav.py index 7693b2b47..20a92d14c 100644 --- a/resources/tester/test_280_2_step_nav.py +++ b/resources/tester/test_280_2_step_nav.py @@ -32,6 +32,19 @@ def results(): return json.loads(proc.stdout) +def test_529_1_D_E_묶음_안에서만_앞뒤(results): + rows = results["rows"] + for route in ("d01-plan-register", "e01-stab-register"): + assert rows[route]["plain"]["labels"] == ["?", "저장"], route + for route in ("d02-plan-base", "d04-plan-confirm", "e02-stab-ground", "e05-stab-report"): + assert rows[route]["plain"]["labels"] == ["?", "<", ">", "저장"], route + # 묶음 첫 단계 [<] · 끝 단계 [>] 흐림(B 묶음과 따로) + assert rows["d02-plan-base"]["plain"]["disabled"][:3] == [False, True, False] + 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] + + @pytest.mark.parametrize("shape", ["helpRow", "plain"]) def test_다섯_주소만_물음_앞뒤_차례(results, shape): for route in FIVE: diff --git a/resources/tester/test_54_2_help_files.py b/resources/tester/test_54_2_help_files.py index 480fda24a..1bc48b728 100644 --- a/resources/tester/test_54_2_help_files.py +++ b/resources/tester/test_54_2_help_files.py @@ -28,11 +28,15 @@ def test_title_order_and_live_edit(): assert "고친 글" in read_help("B05", tmp_path)[1]["text"] -@pytest.mark.parametrize("page", ["B01", "B10", "../B05", "b05"]) +@pytest.mark.parametrize("page", ["B01", "B10", "../B05", "b05", "D05", "E06", "C01"]) def test_bad_page_refused(page): with pytest.raises(HTTPException): read_help(page) +def test_529_1_D_E_pages_open_empty(tmp_path): + assert read_help("D02", tmp_path) == [] and read_help("E05", tmp_path) == [] + + def test_root_is_repo_resources(): assert HELP_ROOT.parts[-2:] == ("resources", "help") diff --git a/tsconfig.json b/tsconfig.json index 30bf838b6..c2c101de7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,6 @@ "@kninnug/constrainautor": ["config/node_modules/@kninnug/constrainautor"], } }, - "include": ["A00_Common", "A0*", "B0*", "B1*", "M0*", "Z0*", "ui_template", "config/**/*", "common_util"], + "include": ["A00_Common", "A0*", "B0*", "B1*", "M0*", "D0*", "E0*", "Z0*", "ui_template", "config/**/*", "common_util"], "exclude": ["0_old", "node_modules", "venv", "dist"] } diff --git a/ui_template/ui_template_help.ts b/ui_template/ui_template_help.ts index c8d7f13ec..002d0cb36 100644 --- a/ui_template/ui_template_help.ts +++ b/ui_template/ui_template_help.ts @@ -5,12 +5,12 @@ * 페이지 = 주소(#/b05-…)의 B0N · 먼저 여는 항목 = 좌측 패널에서 보이는 `[data-help-topic]` 중 마지막 * (지금 연 상자 · 구조물 종류) · 없으면 첫 파일(00_설명서). * 단추 자리 = `.ui-sidebar-actions` 안 `[data-help-row]`(없으면 그 줄 자신)의 맨 앞. - * [<] [>](요구 280-2) = [?] 바로 뒤 · B05~B09 다섯 주소만 · 누르면 `ui-step-nav` 를 올려 보냄 → + * [<] [>](요구 280-2) = [?] 바로 뒤 · 단계 묶음(B05~B09 · D02~D04 · E02~E05 · 529-1) 주소만 · 누르면 `ui-step-nav` 를 올려 보냄 → * 진행 단계 틀(`createWorkflowLayout`)이 이웃 단계 단추를 누름(저장 · 막힘은 각 페이지 `onStepClick`). * ========================================================================== */ import "./ui_template_help.css"; -import { API_BASE_URL, ROUTES } from "@config/config_frontend"; +import { API_BASE_URL, PLAN_STEP_ROUTES, ROUTES, STAB_STEP_ROUTES } from "@config/config_frontend"; import { createButton, el } from "./ui_template_elements"; import { openModal } from "./ui_template_modal"; @@ -23,18 +23,23 @@ interface HelpItem { const TEXT = { button: "?", tip: "설명서", title: "설명서", close: "닫기", empty: "설명 글 없음" }; const STEP_TEXT = { prev: "<", next: ">", prevTip: "이전 단계", nextTip: "다음 단계" }; -/** [<] [>] 를 붙이는 주소 — 목록으로 거름(b09-print · B03 · B04 · M01 · M02 뺌) */ -const STEP_NAV_ROUTES = [ - ROUTES.B05_PROFILE, - ROUTES.B06_SECTION, - ROUTES.B07_DESIGN_DETAIL, - ROUTES.B08_QUANTITY, - ROUTES.B09_ESTIMATION, -] as const; +/** [<] [>] 를 붙이는 주소 묶음 — 묶음 안에서만 오감(b09-print · B03 · B04 · M01 · M02 · D01 · E01 뺌) */ +const STEP_NAV_GROUPS: ReadonlyArray = [ + [ + ROUTES.B05_PROFILE, + ROUTES.B06_SECTION, + ROUTES.B07_DESIGN_DETAIL, + ROUTES.B08_QUANTITY, + ROUTES.B09_ESTIMATION, + ], + PLAN_STEP_ROUTES, + STAB_STEP_ROUTES, +]; +/** 설명서 폴더 이름 = 주소 머리(#/b05-… → B05 · #/d02-… → D02) */ function currentPage(): string { - const n = /^#\/b0(\d)/i.exec(location.hash)?.[1]; - return n ? `B0${n}` : ""; + const head = /^#\/([bde]0\d)/i.exec(location.hash)?.[1]; + return head ? head.toUpperCase() : ""; } function escapeHtml(text: string): string { @@ -186,24 +191,25 @@ function createStepNavButton(step: -1 | 1): HTMLButtonElement { return button; } -/** 지금 주소의 [<] [>] 차례 — 목록 밖이면 -1 */ -function stepNavIndex(): number { +/** 지금 주소의 묶음 · [<] [>] 차례 — 목록 밖이면 차례 -1 */ +function stepNavIndex(): { index: number; last: number } { const route = location.hash.replace(/^#\/?/, "").split("?")[0]; - return (STEP_NAV_ROUTES as readonly string[]).indexOf(route); + const group = STEP_NAV_GROUPS.find((routes) => routes.includes(route)) ?? []; + return { index: group.indexOf(route), last: group.length - 1 }; } /** 아래 단추 묶음에 [?] 를 한 번 — `[data-help-row]` 가 있으면 그 줄 · 없으면 묶음 자신의 맨 앞 */ export function placeHelpButton(actions: HTMLElement): void { if (actions.querySelector(".ui-help-btn")) return; const row = actions.querySelector("[data-help-row]") ?? actions; - if (stepNavIndex() < 0) return void row.prepend(createHelpButton()); + if (stepNavIndex().index < 0) return void row.prepend(createHelpButton()); const prev = createStepNavButton(-1); const next = createStepNavButton(1); // 첫 단계(B05) [<] · 마지막 단계(B09) [>] 흐림 — B05 ↔ B06 은 페이지를 다시 안 세우므로 주소마다 다시 맞춤 const sync = (): void => { - const index = stepNavIndex(); + const { index, last } = stepNavIndex(); prev.disabled = index <= 0; - next.disabled = index === STEP_NAV_ROUTES.length - 1; + next.disabled = index === last; }; const onHash = (): void => { if (prev.isConnected) sync(); diff --git a/ui_template/ui_template_locale.ts b/ui_template/ui_template_locale.ts index 51b1be1e4..042d85609 100644 --- a/ui_template/ui_template_locale.ts +++ b/ui_template/ui_template_locale.ts @@ -14,6 +14,7 @@ * - ui_template_locale_b1.ts : B 그룹 전반부 (B01~B04) * - ui_template_locale_b2.ts : B 그룹 후반부 (B05~B07) * - ui_template_locale_b5.ts : B 그룹 끝 (B08~B11) + * - ui_template_locale_de.ts : D 예정노선 · E 안정성평가 · 대시보드 [+] 고르기 * * 이 파일의 export 시그니처는 분할 이전과 동일하므로 소비처 import 수정은 불필요하다. * ========================================================================== */ @@ -28,6 +29,7 @@ import { ui_locales_b3 } from "./ui_template_locale_b3"; import { ui_locales_b5 } from "./ui_template_locale_b5"; import { ui_locales_m1 } from "./ui_template_locale_m1"; import { ui_locales_m2 } from "./ui_template_locale_m2"; +import { ui_locales_de } from "./ui_template_locale_de"; /** 지원 언어 인덱스: 0 = 한국어, 1 = 영어 */ export const LANGUAGES = ["ko", "en"] as const; @@ -65,6 +67,7 @@ export const ui_locales = { ...ui_locales_b5, ...ui_locales_m1, ...ui_locales_m2, + ...ui_locales_de, } as const; export type LocaleKey = keyof typeof ui_locales; diff --git a/ui_template/ui_template_locale_de.ts b/ui_template/ui_template_locale_de.ts new file mode 100644 index 000000000..046e4c4a2 --- /dev/null +++ b/ui_template/ui_template_locale_de.ts @@ -0,0 +1,21 @@ +/* ============================================================================= + * ui_template_locale_de.ts + * 다국어 사전 — D 예정노선 · E 안정성평가 그룹(요구 345) · 대시보드 [+] 고르기 + * + * 신규 문구는 해당 섹션 최하단에 추가. + * 이 파일은 사전 데이터만 담는다. 합성·언어 전환·t() 헬퍼는 ui_template_locale.ts 참조. + * ========================================================================== */ + +import type { LocaleEntry } from "./ui_template_locale_common"; + +export const ui_locales_de = { + /* --- 단계 막대 · 빈 단계 --- */ + DE_Step_PlanBase: ["기준 지정", "Base Points"], + DE_Step_PlanAnalysis: ["노선 분석", "Route Analysis"], + DE_Step_PlanConfirm: ["확정 · 내보내기", "Confirm · Export"], + DE_Step_StabGround: ["지반 조건", "Ground"], + DE_Step_StabSection: ["2D 해석", "2D Analysis"], + DE_Step_Stab3D: ["3D 해석", "3D Analysis"], + DE_Step_StabReport: ["보고서", "Report"], + DE_Step_Empty: ["준비 중인 단계입니다.", "This step is not ready yet."], +} as const satisfies Record; diff --git a/ui_template/ui_template_loupe.ts b/ui_template/ui_template_loupe.ts index a7768fb54..dc083b541 100644 --- a/ui_template/ui_template_loupe.ts +++ b/ui_template/ui_template_loupe.ts @@ -33,8 +33,8 @@ export const LOUPE_HOLD_MS = 400; export const LOUPE_REFRESH_MS = 500; /** 화면 끝과 원 사이 여백(px) — 테두리가 안 잘리게 */ export const LOUPE_EDGE = 4; -/** 붙일 주소 — B01~B09 · M01 · M02(A 영역 · 로그인 · B10 · B11 은 뺌) */ -export const LOUPE_ROUTE = /^(b0[1-9]|m0[12])-/; +/** 붙일 주소 — B01~B09 · M01 · M02 · D01~D04 · E01~E05(A 영역 · 로그인 · B10 · B11 은 뺌) */ +export const LOUPE_ROUTE = /^(b0[1-9]|m0[12]|d0[1-4]|e0[1-5])-/; const SVG_NS = "http://www.w3.org/2000/svg"; /** CAD 영역 — cad_host 가 끼운 상자와 iframe */ diff --git a/ui_template/ui_template_native_menu.ts b/ui_template/ui_template_native_menu.ts index d5da108d9..993c81901 100644 --- a/ui_template/ui_template_native_menu.ts +++ b/ui_template/ui_template_native_menu.ts @@ -7,8 +7,8 @@ * 드롭다운 초점 빼기 — 마우스로 고른 뒤 초점이 남으면 휠 · ↑↓ 가 값을 또 바꿈(요구 280-6). * ========================================================================== */ -/** 막을 주소 — B · M 영역(`b01-…` · `m02-…`) · A 영역은 그대로 */ -export const NATIVE_MENU_ROUTE = /^[bm]\d/; +/** 막을 주소 — B · M · D · E 영역(`b01-…` · `m02-…` · `d02-…`) · A 영역은 그대로 */ +export const NATIVE_MENU_ROUTE = /^[bmde]\d/; /** `win` 의 기본 메뉴를 막음 — `isOn()` 이 거짓이면(예: A 영역 주소) 그대로 둠 */ export function blockNativeMenu(win: Window, isOn: () => boolean): void { diff --git a/ui_template/ui_template_workflow_layout.ts b/ui_template/ui_template_workflow_layout.ts index 07ff4ba68..8d87be6b7 100644 --- a/ui_template/ui_template_workflow_layout.ts +++ b/ui_template/ui_template_workflow_layout.ts @@ -30,6 +30,8 @@ export interface WorkflowLayoutOptions { /** 막힌(NOT_STARTED) 단계라도 켤 칸 — 수 하나 또는 목록(B05 · B06 = 2 · 3 · 4 · 496-1) */ currentStage?: number | readonly number[]; routes?: readonly string[]; + /** 단계 아이콘 — 없으면 B 일곱 단계 아이콘(D · E 그룹은 제 목록 · 529-1) */ + icons?: readonly string[]; onStepClick?: (stepIndex: number, route: string) => void; /** 좌측 패널이 열리고 닫힐 때(처음 앉힘 포함) — 닫힌 동안 바뀐 안쪽을 열 때 맞춤(499-1). */ onOptionsOpenChange?: (isOpen: boolean) => void; @@ -212,7 +214,7 @@ export function createWorkflowLayout(options: WorkflowLayoutOptions): WorkflowLa currentStage: options.currentStage, routes: options.routes, orientation: "vertical", - icons: WORKFLOW_STEP_ICONS, + icons: options.icons ?? WORKFLOW_STEP_ICONS, onStepClick: options.onStepClick, homeButton: true, });