Merge remote-tracking branch 'origin/sub_laptop_2' into sub_laptop_3
This commit is contained in:
@@ -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);
|
||||
@@ -15,6 +15,8 @@ export interface SessionUser {
|
||||
role?: string;
|
||||
company_id?: number | null;
|
||||
is_master?: boolean;
|
||||
/** 산림청 = 시스템관리자 · *.go.kr 메일(요구 345) — 예정노선 만들기 */
|
||||
is_forest_service?: boolean;
|
||||
}
|
||||
|
||||
async function post(path: string, body?: object): Promise<ApiResult> {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* 대시보드 [+] 고르기 모달(528-2) — 카드 넷 한 줄 · 그림 선 = 글자 색(밝음 · 어두움) */
|
||||
.b01-new-project__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--spacing-12);
|
||||
}
|
||||
|
||||
.b01-new-project__card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-16) var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b01-new-project__card:hover:not(.is-dim, .is-soon) {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.b01-new-project__card.is-dim,
|
||||
.b01-new-project__card.is-soon {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.b01-new-project__icon svg {
|
||||
display: block;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.b01-new-project__badge {
|
||||
position: absolute;
|
||||
top: var(--spacing-4);
|
||||
right: var(--spacing-4);
|
||||
padding: 0 var(--spacing-4);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-border);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/* =============================================================================
|
||||
* B01_Dashboard_UI_NewProject.ts
|
||||
* 대시보드 [+] 고르기 모달(요구 345 · PLAN 528-2) — 카드 넷(그림 + 제목) = 예정노선 · 임도설계 ·
|
||||
* 사방설계 · 안정성평가. 그림 = SVG 직접(선 색 = 글자 색 → 밝음 · 어두움 그대로).
|
||||
* 예정노선 = 산림청(세션 is_forest_service)만 · 아니면 흐림 + 까닭 툴팁(누름 = 같은 까닭 알림)
|
||||
* 사방설계 = 「준비 중」 · 임도설계 = B02 · 예정노선 = D01 · 안정성평가 = E01
|
||||
* ========================================================================== */
|
||||
|
||||
import "./B01_Dashboard_UI_NewProject.css";
|
||||
import { ROUTES, type RoutePath } from "@config/config_frontend";
|
||||
import { el, showToast } from "@ui/ui_template_elements";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
|
||||
/** 48 칸 그림 — 선 · 면은 currentColor */
|
||||
const ICONS = {
|
||||
plan: `<path d="M6 12l12-5 12 5 12-5v29l-12 5-12-5-12 5z"/><path d="M18 7v29M30 12v29"/><path d="M11 33c5-3 8-12 14-12s7 6 12-4" stroke-dasharray="3 3"/><circle cx="11" cy="33" r="2.5" fill="currentColor"/><circle cx="37" cy="17" r="2.5" fill="currentColor"/>`,
|
||||
road: `<path d="M8 42c6-10 18-6 20-16S20 12 26 6"/><path d="M20 42c6-10 18-6 20-16S32 12 38 6"/><path d="M14 42c6-10 18-6 20-16S26 12 32 6" stroke-dasharray="3 4"/>`,
|
||||
erosion: `<path d="M4 14c6 6 10 12 10 20M44 14c-6 6-10 12-10 20"/><path d="M12 26h24v8H12z"/><path d="M16 26v8M22 26v8M28 26v8M32 26v8"/><path d="M14 40c4-2 6 2 10 0s6 2 10 0" />`,
|
||||
stability: `<path d="M4 40h40"/><path d="M4 40V30l12-2 14-16h14v28"/><path d="M10 29a16 16 0 0 0 28-17" stroke-dasharray="3 3"/><path d="M36 14l3-3 1 4" />`,
|
||||
} as const;
|
||||
|
||||
interface CardSpec {
|
||||
icon: keyof typeof ICONS;
|
||||
title: LocaleKey;
|
||||
route?: RoutePath;
|
||||
/** 못 고르는 까닭(툴팁 · 누름 알림) — 없으면 고를 수 있음 */
|
||||
reason?: LocaleKey;
|
||||
soon?: boolean;
|
||||
}
|
||||
|
||||
function svgIcon(kind: keyof typeof ICONS): HTMLElement {
|
||||
const box = el("span", { className: "b01-new-project__icon", attrs: { "aria-hidden": "true" } });
|
||||
box.innerHTML = `<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${ICONS[kind]}</svg>`;
|
||||
return box;
|
||||
}
|
||||
|
||||
function card(spec: CardSpec, close: () => void): HTMLButtonElement {
|
||||
const button = el("button", {
|
||||
className: "b01-new-project__card",
|
||||
attrs: { type: "button" },
|
||||
children: [svgIcon(spec.icon), el("span", { text: t(spec.title) })],
|
||||
});
|
||||
button.dataset.newProject = spec.icon;
|
||||
if (spec.soon) {
|
||||
button.classList.add("is-soon");
|
||||
button.append(el("span", { className: "b01-new-project__badge", text: t("DE_New_Soon") }));
|
||||
}
|
||||
if (spec.reason) {
|
||||
button.classList.add("is-dim");
|
||||
button.title = t(spec.reason);
|
||||
}
|
||||
button.addEventListener("click", () => {
|
||||
if (spec.reason || spec.soon) return void showToast(t(spec.reason ?? "DE_New_Soon"));
|
||||
close();
|
||||
navigateTo(spec.route!);
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
/** [+] — 세션의 산림청 판정으로 예정노선 카드 켬 · 흐림 */
|
||||
export async function openNewProjectPicker(): Promise<void> {
|
||||
const forest = Boolean((await fetchSessionUser())?.is_forest_service);
|
||||
const specs: CardSpec[] = [
|
||||
{
|
||||
icon: "plan",
|
||||
title: "DE_New_Plan",
|
||||
route: ROUTES.D01_PLAN_REGISTER,
|
||||
reason: forest ? undefined : "DE_New_PlanForestOnly",
|
||||
},
|
||||
{ icon: "road", title: "DE_New_Road", route: ROUTES.B02_PROJ_REGISTER },
|
||||
{ icon: "erosion", title: "DE_New_Erosion", soon: true },
|
||||
{ icon: "stability", title: "DE_New_Stability", route: ROUTES.E01_STAB_REGISTER },
|
||||
];
|
||||
openModal({
|
||||
title: t("DE_New_Title"),
|
||||
closeLabel: t("Common_Btn_Close"),
|
||||
dialogClass: "b01-new-project",
|
||||
mount: (body, close) =>
|
||||
body.append(
|
||||
el("div", {
|
||||
className: "b01-new-project__grid",
|
||||
children: specs.map((spec) => card(spec, close)),
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
import { openAddMemberModal, openCreateCompanyModal } from "./B01_Dashboard_UI_Modals";
|
||||
import { buildProfileForm, buildSecurityForm } from "./B01_Dashboard_UI_Profile";
|
||||
import { openCompanyPicker } from "./B01_Dashboard_UI_CompanyPick";
|
||||
import { openNewProjectPicker } from "./B01_Dashboard_UI_NewProject";
|
||||
import { projectTable } from "./B01_Dashboard_UI_Projects";
|
||||
import { buildResourcePanel } from "./B01_Dashboard_UI_Resources";
|
||||
import { buildTempUploadSection } from "./B01_Dashboard_UI_TempUpload";
|
||||
@@ -260,7 +261,7 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
buildSystemSettingsSection(state),
|
||||
withCompanyPill(
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.allProjects, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
||||
createButton({ label: "+", onClick: () => void openNewProjectPicker() }),
|
||||
]),
|
||||
state,
|
||||
),
|
||||
@@ -294,7 +295,7 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
grid.append(
|
||||
buildSystemSettingsSection(state),
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.companyProjects, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
||||
createButton({ label: "+", onClick: () => void openNewProjectPicker() }),
|
||||
]),
|
||||
buildTempUploadSection(),
|
||||
section(L("B01_Dashboard_Members"), memberTable(state.members, state.user), true, [
|
||||
@@ -307,7 +308,7 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
grid.append(
|
||||
buildSystemSettingsSection(state),
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.userProjects, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
||||
createButton({ label: "+", onClick: () => void openNewProjectPicker() }),
|
||||
]),
|
||||
// 임시 보관함 — 프로젝트를 만들기 전에 자료를 올려 두는 곳이라 프로젝트 목록
|
||||
// 바로 아래에 둔다(2026-08-08 사용자 지시).
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
373 화면이 구간 `legs[].path` 로 캐시 → [저장])
|
||||
GET /api/projects/{id}/quantity/milestone/reverse?lon=&lat= 지도에서 고른 소재지(244-13) →
|
||||
VWorld 주소 한 줄(서버 단독 · 저장 안 함 · 못 찾음 = null)
|
||||
GET /api/projects/{id}/quantity/milestone/map-key 카카오 지도 JavaScript 키(244-9 ·
|
||||
공개용 · .env KAKAO_JS_KEY · 없으면 null)
|
||||
|
||||
정본 = 프로젝트 storage `B08_Quantity/milestone/`(`milestone.json` + `<경로 id>.<확장자>` 사용자
|
||||
그림 · `auto/<경로 id>.png` 자동 그림 · 폴더가 목록 · B09 견적서 파일 길 꼴) · 보이는 그림 = 사용자
|
||||
@@ -676,12 +674,3 @@ async def milestone_directions(project_id: UUID, body: Directions) -> dict[str,
|
||||
"""[길찾기] 한 구간 — 단추 누를 때만(조작 중 자동 호출 없음) · 길 선은 점 줄여 돌려줌(373)."""
|
||||
got = await asyncio.to_thread(directions_km, body.origin, body.destination)
|
||||
return {**got, "path": thin_path(got["path"])} if got.get("path") else got
|
||||
|
||||
|
||||
KAKAO_JS = "KAKAO_JS_KEY"
|
||||
|
||||
|
||||
@router.get(PATH + "/map-key")
|
||||
async def milestone_map_key(project_id: UUID) -> dict[str, Any]:
|
||||
"""카카오 지도 JavaScript 키(공개용 · 도메인 등록으로 막힘) — 없으면 null = 화면 안내 한 줄."""
|
||||
return {"key": os.environ.get(KAKAO_JS, "").strip() or None}
|
||||
|
||||
@@ -188,35 +188,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 경로 지도(244-9) — 카카오 지도 칸 · 표지 이름 · 높이 어디서나 한 값(150) */
|
||||
.b08-milestone__map {
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b08-milestone__map-label {
|
||||
padding: 2px 6px;
|
||||
border: 1px solid #3c3c3c;
|
||||
background: #fff;
|
||||
color: #141414;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 262-2 고정 시점 표지 — 둥근 붉은 알약(고르는 표지 = 기본 핀 + 흰 이름표와 구별) */
|
||||
.b08-milestone__map-start {
|
||||
padding: 3px 10px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 999px;
|
||||
background: #e60046;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 등록 · 연결선 모달(244-13) — 더 큰 창 · 2열 칸 */
|
||||
.ui-modal.b08-milestone__modal {
|
||||
width: min(960px, 94vw);
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
type SegKind,
|
||||
} from "./B08_Quantity_Milestone_Calc";
|
||||
import { ownerOf, segmentEnds, setLeg } from "./B08_Quantity_Milestone_Layout";
|
||||
import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
||||
import { editMap, type MapPin } from "@ui/ui_template_kakao_map";
|
||||
import { fmtKm, toastKm } from "./B08_Quantity_UI_Milestone_Modal";
|
||||
import { reverseAddress } from "./B08_Quantity_UI_Milestone_Place";
|
||||
import {
|
||||
@@ -224,7 +224,7 @@ export function openLegInfo(projectId: string, from: string): void {
|
||||
drawMap();
|
||||
};
|
||||
// 끝점 끌어 옮김 = 그 소재지 고침(주소 다시) + 다시 길찾기
|
||||
const map = editMap(projectId, {
|
||||
const map = editMap({
|
||||
hint: "경로 = 카카오 길찾기 · 거래처 · 기준점 표지를 끌어 옮기면 다시 길찾기",
|
||||
onMove: (key, lon, lat) => {
|
||||
apply();
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
} from "./B08_Quantity_Milestone_Calc";
|
||||
import { setLeg } from "./B08_Quantity_Milestone_Layout";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
||||
import { editMap, type MapPin } from "@ui/ui_template_kakao_map";
|
||||
import { placePicker } from "./B08_Quantity_UI_Milestone_Place";
|
||||
import {
|
||||
capturePlace,
|
||||
@@ -305,7 +305,7 @@ export function openSiteInfo(projectId: string): void {
|
||||
});
|
||||
save.dataset.b08MilestoneSite = "save";
|
||||
actions.append(save);
|
||||
const map = editMap(projectId, { hint: "", onMove: () => undefined });
|
||||
const map = editMap({ hint: "", onMove: () => undefined });
|
||||
const pins = (s: {
|
||||
start_lonlat: [number, number] | null;
|
||||
mid_lonlat: [number, number] | null;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 거리이정표 등록 모달 「소재지」 칸(PLAN 244-13 · 사용자 147번 5) — 주소 찾기 · 지도에서 고르기만(직접 입력 없음).
|
||||
*
|
||||
* 주소 찾기 = B01 회사 주소와 같은 서버 후보 검색(VWorld 대리 · `searchAddress`) → 후보 고름 = 소재지 · 표지
|
||||
* 지도 = 다음(카카오) 지도 JS(`…_KakaoMap` editMap) — 지도 누름 = 그 자리 소재지 · 표지 끌어 옮김 = 다시 고름 ·
|
||||
* 지도 = 다음(카카오) 지도 JS(`ui_template_kakao_map` editMap) — 지도 누름 = 그 자리 소재지 · 표지 끌어 옮김 = 다시 고름 ·
|
||||
* 고른 좌표 → 서버 GET …/milestone/reverse(VWorld 주소 한 줄 · 못 찾음 = 위도, 경도 글)
|
||||
* 값 = 주소 글 · 경도 · 위도(캐시 · [저장] 때 서버)
|
||||
* ========================================================================== */
|
||||
@@ -11,7 +11,7 @@
|
||||
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { searchAddress, type AddressCandidate } from "../B01_Dashboard/B01_Dashboard_Api_Fetch";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { editMap } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
||||
import { editMap } from "@ui/ui_template_kakao_map";
|
||||
import { liveMilestone, startPoint } from "./B08_Quantity_UI_Milestone_State";
|
||||
|
||||
export interface Place {
|
||||
@@ -54,7 +54,7 @@ export function placePicker(projectId: string, initial: Place, name: () => strin
|
||||
where.dataset.b08MilestonePlace = "value";
|
||||
// 262-2 시점 = 거리이정표가 쓰는 시점(끌어 옮긴 덮개 · 없으면 노선 시점)
|
||||
const start = (): [number, number] | null => startPoint(liveMilestone(projectId));
|
||||
const map = editMap(projectId, {
|
||||
const map = editMap({
|
||||
hint: "지도 누름 = 그 자리를 소재지로 · 표지 끌어 옮김",
|
||||
onClick: (lon, lat) => void moveTo(lon, lat),
|
||||
onMove: (_key, lon, lat) => void moveTo(lon, lat),
|
||||
|
||||
@@ -20,7 +20,7 @@ import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import type { MilestoneData, MilestoneRoute } from "./B08_Quantity_Milestone_Calc";
|
||||
import { chainOf, ownerOf, reachesSite } from "./B08_Quantity_Milestone_Layout";
|
||||
import { DEST_TEXT } from "./B08_Quantity_Milestone_Table";
|
||||
import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
||||
import { editMap, type MapPin } from "@ui/ui_template_kakao_map";
|
||||
import { openPlaceModal } from "./B08_Quantity_UI_Milestone_Modal";
|
||||
import { reverseAddress } from "./B08_Quantity_UI_Milestone_Place";
|
||||
import {
|
||||
@@ -185,7 +185,7 @@ export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
putMilestone(projectId, fresh);
|
||||
await refind(touchingSegments(fresh, key));
|
||||
};
|
||||
const map = editMap(projectId, {
|
||||
const map = editMap({
|
||||
hint: "지도 누름 = 그 자리를 소재지로 · 표지 끌어 옮김",
|
||||
onClick: (lon, lat) => void moveTo(routeId, lon, lat),
|
||||
onMove: (key, lon, lat) => void moveTo(key, lon, lat),
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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]]:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"""카카오 지도 JavaScript 키 공용 길(529-2) — B08 거리이정표 · D 예정노선 지도가 씀.
|
||||
|
||||
GET /api/map/kakao-key {key} — 공개용(도메인 등록으로 막힘) · .env KAKAO_JS_KEY · 없으면 null
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix="/api/map", tags=["map"])
|
||||
|
||||
KAKAO_JS = "KAKAO_JS_KEY"
|
||||
|
||||
|
||||
@router.get("/kakao-key")
|
||||
async def kakao_map_key() -> dict[str, Any]:
|
||||
"""없으면 null = 화면 안내 한 줄."""
|
||||
return {"key": os.environ.get(KAKAO_JS, "").strip() or None}
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ from common_util.common_util_auth import (
|
||||
# 개발이 아니면 세 입구 모두 403 으로 거절한다(화면 단추 숨김은 보조).
|
||||
from common_util.common_util_dev_unlock_router import router as dev_unlock_router
|
||||
from common_util.common_util_help_router import router as help_router
|
||||
from common_util.common_util_map_router import router as map_router
|
||||
from common_util.common_util_resource_monitor import sample_resources_loop
|
||||
from common_util.common_util_temp_cleanup import cleanup_expired_temp_uploads_loop
|
||||
from config.config_db import close_db_pool, get_db_pool, init_db_pool
|
||||
@@ -519,6 +520,7 @@ app.include_router(m02_project_items_router, dependencies=protected)
|
||||
app.include_router(dev_unlock_router, dependencies=protected_with_company)
|
||||
# [?] 설명서 글(54-2) — 로그인만 · 파일을 부를 때마다 읽음.
|
||||
app.include_router(help_router, dependencies=protected)
|
||||
app.include_router(map_router, dependencies=protected)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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) };
|
||||
|
||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
LAYOUT = "B08_Quantity/B08_Quantity_Milestone_Layout.ts"
|
||||
KMAP = "B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts"
|
||||
KMAP = "ui_template/ui_template_kakao_map.ts"
|
||||
ORIGIN = "http://100.72.32.27:5173"
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ STUBS = f"""
|
||||
const el = (tag: string, o: any = {{}}) => ({{ tag, hidden: false, textContent: o.text ?? "",
|
||||
className: o.className, dataset: {{}}, children: o.children ?? [] }});
|
||||
const requestAnimationFrame = (f: () => void) => setTimeout(f, 0);
|
||||
const request = async () => ({{ key: "fake-js-key" }});
|
||||
const API_BASE_URL = "";
|
||||
(globalThis as any).fetch = async () => ({{ ok: true, json: async () => ({{ key: "fake-js-key" }}) }});
|
||||
const injected: string[] = [];
|
||||
(globalThis as any).window = globalThis;
|
||||
(globalThis as any).location = {{ origin: {json.dumps(ORIGIN)} }};
|
||||
@@ -50,7 +51,7 @@ def _view(setup):
|
||||
+ IMPORTS.sub("", _read(KMAP))
|
||||
+ """
|
||||
// 156 경로 탭 지도 칸 없앰 — 같은 SDK 싣기 · 안내 길 = 연결선 모달 지도 칸(editMap)
|
||||
const view = editMap("p1", { onMove: () => {}, hint: "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" });
|
||||
const view = editMap({ onMove: () => {}, hint: "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" });
|
||||
view.set([{ key: "r1", name: "거창군청", lon: 127.9, lat: 35.68, movable: true }], routePaths());
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
const [hint, box] = view.root.children;
|
||||
|
||||
@@ -30,7 +30,7 @@ IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
B08 = "B08_Quantity/B08_Quantity_"
|
||||
CALC = B08 + "Milestone_Calc.ts"
|
||||
LAYOUT = B08 + "Milestone_Layout.ts"
|
||||
KMAP = B08 + "UI_Milestone_KakaoMap.ts"
|
||||
KMAP = "ui_template/ui_template_kakao_map.ts"
|
||||
STATE = B08 + "UI_Milestone_State.ts"
|
||||
BASE = "/api/projects/00000000-0000-0000-0000-000000000001/quantity/milestone"
|
||||
|
||||
@@ -158,7 +158,8 @@ const el = (tag: string, o: any = {}) => ({ tag, hidden: false, textContent: o.t
|
||||
const requestAnimationFrame = (f: () => void) => setTimeout(f, 0);
|
||||
(globalThis as any).window = globalThis;
|
||||
(globalThis as any).document = { createElement: () => ({}), head: { append: () => undefined } };
|
||||
const request = async () => ({ key: "fake-js-key" });
|
||||
const API_BASE_URL = "";
|
||||
(globalThis as any).fetch = async () => ({ ok: true, json: async () => ({ key: "fake-js-key" }) });
|
||||
const LOG: any = { markers: [], handlers: [], lines: 0 };
|
||||
class LatLng { lat: number; lng: number;
|
||||
constructor(lat: number, lng: number) { this.lat = lat; this.lng = lng; }
|
||||
@@ -183,7 +184,7 @@ def test_고르는_지도_누름_끌기():
|
||||
[KMAP],
|
||||
"""
|
||||
const moved: any[] = []; const clicked: any[] = [];
|
||||
const m = editMap("p1", { hint: "h", onMove: (k, lon, lat) => moved.push([k, lon, lat]),
|
||||
const m = editMap({ hint: "h", onMove: (k, lon, lat) => moved.push([k, lon, lat]),
|
||||
onClick: (lon, lat) => clicked.push([lon, lat]), center: () => [127.83, 35.64] });
|
||||
m.set([], []);
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
@@ -292,7 +293,7 @@ def test_화면_등록_모달_연결선_모달():
|
||||
assert "소재지" in modal and "거래처 위치" not in modal
|
||||
for text in (modal, place):
|
||||
assert 'label: "직접 입력"' not in text and "buildAddressField" not in text
|
||||
assert "editMap(projectId" in place and "onClick: (lon, lat) => void moveTo(lon, lat)" in place
|
||||
assert "editMap({" in place and "onClick: (lon, lat) => void moveTo(lon, lat)" in place
|
||||
assert "/quantity/milestone/reverse?lon=" in place
|
||||
# ② 상자 누름 = 같은 등록 모달 · [기준점 +] 없음
|
||||
assert "base: (id) => openPlaceModal(projectId, { add: false, id })," in diagram
|
||||
|
||||
@@ -151,7 +151,7 @@ def test_화면_모달_팬줌_그림탭():
|
||||
assert 'from "../B01_Dashboard/B01_Dashboard_Api_Fetch"' in place
|
||||
assert "searchAddress(text)" in place
|
||||
# ② 시점 모달 — 지도 = 공용 카카오 지도(255-1 · 주소 줄 지움)
|
||||
assert "/quantity/milestone/site-address" in modal and "editMap(projectId" in modal
|
||||
assert "/quantity/milestone/site-address" in modal and "editMap({" in modal
|
||||
# ⑤ 거래처 상자 = 같은 등록 모달(고침 · 삭제 · 244-13) · 구간 km · 도로 구분 = 연결선 모달
|
||||
for text in ('"수정"', 'label: "삭제"', "rowValues()", "putMilestone(projectId, fresh)"):
|
||||
assert text in add
|
||||
|
||||
@@ -22,11 +22,12 @@ if str(ROOT) not in sys.path:
|
||||
|
||||
from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import migrate_milestone # noqa: E402
|
||||
from common_util import common_util_map_router as map_router # noqa: E402
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
BASE = "/api/projects/00000000-0000-0000-0000-000000000001/quantity/milestone"
|
||||
LAYOUT = "B08_Quantity/B08_Quantity_Milestone_Layout.ts"
|
||||
KMAP = "B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts"
|
||||
KMAP = "ui_template/ui_template_kakao_map.ts"
|
||||
GEOCHANG = {"lon": 127.9095, "lat": 35.6866}
|
||||
BYEONGGOK = {"lon": 127.85, "lat": 35.66}
|
||||
BODY = {
|
||||
@@ -108,14 +109,14 @@ def test_좌표_저장_안_함(tmp_path):
|
||||
|
||||
def test_JS_키_길(monkeypatch):
|
||||
app = FastAPI()
|
||||
app.include_router(ms.router)
|
||||
app.include_router(map_router.router) # 529-2 공용 길로 옮김
|
||||
client = TestClient(app)
|
||||
monkeypatch.delenv(ms.KAKAO_JS, raising=False)
|
||||
assert client.get(BASE + "/map-key").json() == {"key": None}
|
||||
monkeypatch.setenv(ms.KAKAO_JS, " ")
|
||||
assert client.get(BASE + "/map-key").json() == {"key": None}
|
||||
monkeypatch.setenv(ms.KAKAO_JS, "fake-js-key")
|
||||
assert client.get(BASE + "/map-key").json() == {"key": "fake-js-key"}
|
||||
monkeypatch.delenv(map_router.KAKAO_JS, raising=False)
|
||||
assert client.get("/api/map/kakao-key").json() == {"key": None}
|
||||
monkeypatch.setenv(map_router.KAKAO_JS, " ")
|
||||
assert client.get("/api/map/kakao-key").json() == {"key": None}
|
||||
monkeypatch.setenv(map_router.KAKAO_JS, "fake-js-key")
|
||||
assert client.get("/api/map/kakao-key").json() == {"key": "fake-js-key"}
|
||||
|
||||
|
||||
def test_표지_셋():
|
||||
@@ -164,10 +165,11 @@ def _map(key, paths, extra=""):
|
||||
const KEY = {json.dumps(key)};
|
||||
const PATHS = {json.dumps(paths)};
|
||||
const asked: string[] = [];
|
||||
const request = async (url: string) => (asked.push(url), {{ key: KEY }});
|
||||
const API_BASE_URL = "";
|
||||
(globalThis as any).fetch = async (url: string) => (asked.push(url), {{ ok: true, json: async () => ({{ key: KEY }}) }});
|
||||
{extra}
|
||||
// 156 경로 탭 지도 칸 없앰 — 같은 SDK 싣기 · 안내 길 = 연결선 모달 지도 칸(editMap)
|
||||
const view = editMap("p1", {{ onMove: () => {{}}, hint: "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" }});
|
||||
const view = editMap({{ onMove: () => {{}}, hint: "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" }});
|
||||
view.set([{{ key: "r1", name: "거창군청", lon: 127.9, lat: 35.68, movable: true }},
|
||||
{{ key: "base", name: "병곡리", lon: 127.85, lat: 35.66, movable: true }},
|
||||
{{ key: "start", name: "시점", lon: 127.83, lat: 35.64, movable: false }}], PATHS);
|
||||
@@ -181,7 +183,7 @@ console.log(JSON.stringify({{ asked, injected, hint: hint.textContent, boxHidden
|
||||
|
||||
def test_키_없음_안내_한_줄():
|
||||
got = _map(None, [])
|
||||
assert got["asked"] == ["/projects/p1/quantity/milestone/map-key"]
|
||||
assert got["asked"] == ["/map/kakao-key"]
|
||||
assert got["injected"] == [] # SDK 안 부름
|
||||
assert got["hint"] == (
|
||||
"카카오 JavaScript 키 · 도메인 등록 필요 — 지도 대신 [그림 변경]으로 넣으세요"
|
||||
|
||||
@@ -73,11 +73,12 @@ def test_폭_전체_지도_높이_한_값():
|
||||
block = css[css.index(".b08-milestone {") :]
|
||||
assert "width: 100%;" in block[: block.index("}")]
|
||||
assert ".b08-milestone > .ui-data-table .ui-data-table__table {\n width: 100%;" in css
|
||||
assert css.count("height: 480px;") == 1 and ".b08-milestone__map {" in css
|
||||
kcss = _read("ui_template/ui_template_kakao_map.css") # 529-2 공용 지도 칸
|
||||
assert kcss.count("height: 480px;") == 1 and ".ui-kakao-map__box {" in kcss
|
||||
|
||||
|
||||
def test_153_지도_보기_유지_상자_색_끌어_옮김():
|
||||
kmap = _read("B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts")
|
||||
kmap = _read("ui_template/ui_template_kakao_map.ts")
|
||||
assert (
|
||||
"let fitted = false;" in kmap and "map.panTo(" in kmap
|
||||
) # 처음 한 번만 맞춤 · 그 뒤 줌 그대로
|
||||
@@ -170,9 +171,7 @@ def test_157_도구_왼쪽_아래_경로_탭_지도_그림_변경_초기화():
|
||||
route = _read("B08_Quantity/B08_Quantity_UI_Milestone_Route.ts")
|
||||
assert 'label: "그림 변경"' in route and 'label: "초기화"' in route
|
||||
assert "그림 지우기" not in route and "자동 그림으로 되돌리기" not in route.split("*/", 1)[1]
|
||||
assert (
|
||||
"map.root.hidden = user;" in route and "editMap(projectId" in route
|
||||
) # 그림 없으면 길찾기 지도
|
||||
assert "map.root.hidden = user;" in route and "editMap({" in route # 그림 없으면 길찾기 지도
|
||||
assert ".b08-milestone__route-head {" in css and "justify-content: space-between;" in css
|
||||
state = _read("B08_Quantity/B08_Quantity_UI_Milestone_State.ts")
|
||||
assert "export const hasUserImage" in state and "canRevertImage" not in state
|
||||
|
||||
@@ -46,6 +46,6 @@ def test_시점_모달_km_카카오_지도():
|
||||
# 설명 줄 · 등록 주소 · 브이월드 주소 줄 · B01 타일 지도 없음
|
||||
for gone in ("시점 = 노선 첫 점", "프로젝트 등록 주소", "브이월드 주소", "buildAddressMap"):
|
||||
assert gone not in modal
|
||||
assert "editMap(projectId" in site
|
||||
assert "editMap({" in site
|
||||
assert '["start", "시점", s.start_lonlat]' in site and '["mid", "중점", s.mid_lonlat]' in site
|
||||
assert 'dialogClass: "b08-milestone__modal"' in site # 다른 모달과 같은 창 · 지도 높이
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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<readonly string[]> = [
|
||||
[
|
||||
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<HTMLElement>("[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();
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/* 카카오 지도 칸 공용(529-2) — 지도 · 안내 · 표지 이름 · 높이 어디서나 한 값(150) */
|
||||
.ui-kakao-map__box {
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ui-kakao-map__hint {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ui-kakao-map__label {
|
||||
padding: 2px 6px;
|
||||
border: 1px solid #3c3c3c;
|
||||
background: #fff;
|
||||
color: #141414;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 262-2 고정 시점 표지 — 둥근 붉은 알약(고르는 표지 = 기본 핀 + 흰 이름표와 구별) */
|
||||
.ui-kakao-map__start {
|
||||
padding: 3px 10px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 999px;
|
||||
background: #e60046;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
+25
-28
@@ -1,17 +1,16 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_UI_Milestone_KakaoMap.ts
|
||||
* 거리이정표 카카오 지도 칸(PLAN 244-9 · 사용자 138번) — 등록 모달(소재지 고르기) · 연결선 모달(길 선 · 표지 끌기)
|
||||
* 한 벌(`editMap`) · 경로 하위 탭 지도 칸은 없앰(156 · 탭 = 그림만).
|
||||
* ui_template_kakao_map.ts
|
||||
* 카카오 지도 칸 공용(PLAN 244-9 · 529-2) — 고르는 지도 한 벌(`editMap`) · B08 거리이정표 · D 예정노선이 씀.
|
||||
*
|
||||
* SDK = dapi.kakao.com/v2/maps/sdk.js(autoload=false) · 키 = 서버 GET …/milestone/map-key
|
||||
* SDK = dapi.kakao.com/v2/maps/sdk.js(autoload=false) · 키 = 서버 GET /api/map/kakao-key
|
||||
* (.env JS 키 · 공개용 · 도메인 등록으로 막힘) · 키 없음 · 못 뜸 = 안내 한 줄 + 사진 올리기
|
||||
* 길 선 = 마지막 [길찾기] 응답 경로 좌표(메모리만 · 저장 안 함) · 처음 한 번만 맞춤(153 · 그 뒤 보기 그대로)
|
||||
* 경로 그림 = 서버 자동 캡처(244-11 · `…_State` autoRouteImage) — 브라우저 캡처 없음(접속 주소 무관)
|
||||
* 길 선 = 부르는 쪽이 준 [위도, 경도] 목록 · 처음 한 번만 맞춤(153 · 그 뒤 보기 그대로)
|
||||
* (244-10) SDK 못 받음(도메인 미등록 401) = 지금 주소(location.origin) 등록 안내 한 줄
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import "./ui_template_kakao_map.css";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { el } from "./ui_template_elements";
|
||||
|
||||
export const NO_MAP =
|
||||
"카카오 JavaScript 키 · 도메인 등록 필요 — 지도 대신 [그림 변경]으로 넣으세요";
|
||||
@@ -58,11 +57,12 @@ interface KakaoMaps {
|
||||
let sdk: Promise<KakaoMaps> | null = null;
|
||||
|
||||
/** SDK 한 번 싣기 — 키 없음 · 못 받음 = NO_MAP 으로 거절(다음 부름 때 다시) */
|
||||
export function loadKakaoMaps(projectId: string): Promise<KakaoMaps> {
|
||||
export function loadKakaoMaps(): Promise<KakaoMaps> {
|
||||
sdk ??= (async () => {
|
||||
const { key } = await request<{ key: string | null }>(
|
||||
`/projects/${projectId}/quantity/milestone/map-key`,
|
||||
);
|
||||
const response = await fetch(`${API_BASE_URL}/map/kakao-key`, { credentials: "include" });
|
||||
const { key } = response.ok
|
||||
? ((await response.json()) as { key: string | null })
|
||||
: { key: null };
|
||||
if (!key) throw new Error(NO_MAP);
|
||||
const w = window as unknown as { kakao?: { maps: KakaoMaps } };
|
||||
if (!w.kakao?.maps) {
|
||||
@@ -109,24 +109,21 @@ export interface EditMap {
|
||||
}
|
||||
|
||||
/** 고르는 지도 — 움직일 표지 끌기 = onMove · 지도 누름 = onClick(있을 때만) · 점 없으면 center(경도, 위도) */
|
||||
export function editMap(
|
||||
projectId: string,
|
||||
opts: {
|
||||
onMove: (key: string, lon: number, lat: number) => void;
|
||||
onClick?: (lon: number, lat: number) => void;
|
||||
center?: () => [number, number] | null;
|
||||
hint: string;
|
||||
},
|
||||
): EditMap {
|
||||
const box = el("div", { className: "b08-milestone__map" });
|
||||
const hint = el("p", { className: "b08-options__hint", text: opts.hint });
|
||||
hint.dataset.b08MilestoneMapHint = "";
|
||||
export function editMap(opts: {
|
||||
onMove: (key: string, lon: number, lat: number) => void;
|
||||
onClick?: (lon: number, lat: number) => void;
|
||||
center?: () => [number, number] | null;
|
||||
hint: string;
|
||||
}): EditMap {
|
||||
const box = el("div", { className: "ui-kakao-map__box" });
|
||||
const hint = el("p", { className: "ui-kakao-map__hint", text: opts.hint });
|
||||
hint.dataset.kakaoMapHint = "";
|
||||
let map: KMap | null = null;
|
||||
let drawn: KOverlay[] = [];
|
||||
let fitted = false; // 153 — 처음 한 번만 맞춤 · 그 뒤 고름 · 끌기는 보기(중심 · 줌) 그대로
|
||||
|
||||
const set = (pins: MapPin[], lines: Array<Array<[number, number]>>): void => {
|
||||
loadKakaoMaps(projectId).then(
|
||||
loadKakaoMaps().then(
|
||||
(maps) =>
|
||||
requestAnimationFrame(() => {
|
||||
const moving = pins.filter((p) => !p.fixed);
|
||||
@@ -164,7 +161,7 @@ export function editMap(
|
||||
new maps.CustomOverlay({
|
||||
position: at,
|
||||
content: el("span", {
|
||||
className: "b08-milestone__map-start",
|
||||
className: "ui-kakao-map__start",
|
||||
text: p.name,
|
||||
}),
|
||||
yAnchor: 0.5,
|
||||
@@ -180,7 +177,7 @@ export function editMap(
|
||||
marker,
|
||||
new maps.CustomOverlay({
|
||||
position: at,
|
||||
content: el("span", { className: "b08-milestone__map-label", text: p.name }),
|
||||
content: el("span", { className: "ui-kakao-map__label", text: p.name }),
|
||||
yAnchor: 2.6,
|
||||
}),
|
||||
];
|
||||
@@ -211,7 +208,7 @@ export function editMap(
|
||||
};
|
||||
|
||||
return {
|
||||
root: el("div", { className: "b08-milestone__map-wrap", children: [hint, box] }),
|
||||
root: el("div", { className: "ui-kakao-map", children: [hint, box] }),
|
||||
set,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/* =============================================================================
|
||||
* 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."],
|
||||
|
||||
/* --- 대시보드 [+] 고르기(528-2) --- */
|
||||
DE_New_Title: ["새 프로젝트", "New Project"],
|
||||
DE_New_Plan: ["예정노선", "Planned Route"],
|
||||
DE_New_Road: ["임도설계", "Forest Road Design"],
|
||||
DE_New_Erosion: ["사방설계", "Erosion Control"],
|
||||
DE_New_Stability: ["안정성평가", "Stability Assessment"],
|
||||
DE_New_Soon: ["준비 중", "Coming soon"],
|
||||
DE_New_PlanForestOnly: [
|
||||
"예정노선은 산림청(시스템 관리자 · go.kr 메일)만 만들 수 있습니다.",
|
||||
"Only the Korea Forest Service (system admin · go.kr mail) can create planned routes.",
|
||||
],
|
||||
} as const satisfies Record<string, LocaleEntry>;
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user