diff --git a/A00_Common/b_workflow_nav.ts b/A00_Common/b_workflow_nav.ts index 87f23a048..844447241 100644 --- a/A00_Common/b_workflow_nav.ts +++ b/A00_Common/b_workflow_nav.ts @@ -4,6 +4,12 @@ import { ROUTES, type RoutePath, } from "@config/config_frontend"; +import { + hideLoadingOverlay, + setLoadingOverlayProgress, + showLoadingOverlay, + showToast, +} from "@ui/ui_template_elements"; import type { WorkflowStage } from "@ui/ui_template_workflow_layout"; import { fetchConfirmedSurface, @@ -93,9 +99,9 @@ export async function dropGoneProject(): Promise { */ const PRELOAD_REQUIRED_ROUTES: readonly RoutePath[] = [ROUTES.B04_PREPROCESS, ROUTES.B05_PROFILE]; -export function goToWorkflowStage(projectId: string, route: RoutePath): void { +export function goToWorkflowStage(projectId: string, route: RoutePath): Promise { // 다른 프로젝트에 들어가면 그 밖 프로젝트 캐시를 먼저 지운다(290-1). - void enterProject( + return enterProject( projectId, async (id) => (await fetchWorkflowState(id)).project_name || id, ).then(() => { @@ -106,10 +112,84 @@ export function goToWorkflowStage(projectId: string, route: RoutePath): void { navigateTo(route); return; } - void routeAfterPreloadCheck(projectId, route); + return routeAfterPreloadCheck(projectId, route); }); } +/** 페이지 넘김 한 단계(520-3) — 잠금판에 「n/N 이름 · 지난 초」. */ +export interface StageStep { + name: string; + /** false = 머묾(까닭은 단계가 알림) · 던지면 까닭 토스트 뒤 머묾 */ + run: () => Promise; + /** 서버 진행 묻기(선택 · 1 s 마다) — 비율 0~1 · 글 */ + progress?: () => Promise<{ ratio: number | null; label?: string } | null>; +} + +const PROGRESS_POLL_MS = 1000; +let moving = false; + +/** 페이지 넘김 공용(520-3) — 잠금 → 단계 차례 → 실패면 잠금 풀고 머묾 → 성공이면 이동. + * target = 워크플로 단계 주소 또는 이동 함수(B09 → 인쇄 미리보기). 참 = 옮김. */ +export async function moveStage( + projectId: string, + target: RoutePath | (() => void), + steps: readonly StageStep[], +): Promise { + if (moving) return false; + moving = true; + const started = Date.now(); + let index = 0; + let server: { ratio: number | null; label?: string } | null = null; + const paint = (): void => { + const step = steps[index]; + const seconds = Math.floor((Date.now() - started) / 1000); + const head = step ? `${index + 1}/${steps.length} ${step.name}` : "페이지 여는 중"; + const tail = step && server?.label ? ` — ${server.label}` : ""; + const ratio = step ? (index + (server?.ratio ?? 0)) / steps.length : 1; + setLoadingOverlayProgress(ratio, `${head}${tail} · ${seconds}초`); + }; + showLoadingOverlay(); + let asking = false; + const timer = window.setInterval(() => { + const at = index; + const ask = steps[at]?.progress; + if (!ask) return paint(); + if (asking) return; + asking = true; + void ask() + .then( + (next) => { + if (at === index) server = next; + }, + () => undefined, + ) + .finally(() => { + asking = false; + paint(); + }); + }, PROGRESS_POLL_MS); + try { + for (; index < steps.length; index += 1) { + server = null; + paint(); + if ((await steps[index].run()) === false) return false; + } + paint(); + if (typeof target === "function") target(); + else await goToWorkflowStage(projectId, target); + return true; + } catch (error) { + const name = steps[index]?.name ?? "페이지 이동"; + const reason = error instanceof Error ? error.message : String(error); + showToast(`${name}에서 멈췄습니다 — ${reason}`, "error"); + return false; + } finally { + window.clearInterval(timer); + hideLoadingOverlay(); + moving = false; + } +} + /** * 담아 둔 자료가 지금 확정본과 같은지 확인하고 화면을 정한다. * diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts index b7d7a6199..f131d079e 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts @@ -31,8 +31,9 @@ import { workflowSteps } from "../A00_Common/b_page_scaffold"; import { clearWorkflowStateCache, fetchWorkflowState, - goToWorkflowStage, + moveStage, WORKFLOW_STEP_ROUTES, + type StageStep, type WorkflowState, } from "../A00_Common/b_workflow_nav"; import { @@ -484,28 +485,16 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { }); placeTabs(); - /** 페이지 이동(281) — 고친 도면 저장 → B08 · B09 면 단계 4 완료 → 이동 · 실패면 머묾. */ - let moving = false; - const moveToStage = async (stepIndex: number): Promise => { - if (!projectId || moving) return; - moving = true; - try { - if (!(await saveEditedDrawings(store))) return; - if (stepIndex > 4 && drawings.length > 0) { - try { - await completeDesignStage(projectId); - } catch (error) { - showToast( - error instanceof Error ? error.message : "상세 설계 단계를 완료하지 못했습니다.", - "error", - ); - return; - } - } - goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]); - } finally { - moving = false; - } + /** 페이지 이동(281 · 520-3) — 고친 도면 저장 → B08 · B09 면 단계 4 완료 → 이동 · 실패면 머묾. */ + const moveToStage = (stepIndex: number): void => { + if (!projectId) return; + const steps: StageStep[] = [{ name: "도면 저장", run: () => saveEditedDrawings(store) }]; + if (stepIndex > 4 && drawings.length > 0) + steps.push({ + name: "상세 설계 단계 완료", + run: async () => void (await completeDesignStage(projectId)), + }); + void moveStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex], steps); }; const layout = createWorkflowLayout({ @@ -519,7 +508,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { currentStage: 5, routes: WORKFLOW_STEP_ROUTES, // ☰ · 진행 단계 판 · [<] [>] 한 길 — 저장 후 이동(옛 「모든 횡단 확정」 잠금 · 자동 B08 이동 없음) - onStepClick: (stepIndex) => void moveToStage(stepIndex), + onStepClick: (stepIndex) => moveToStage(stepIndex), }); layout.root.classList.add("b07-design-layout"); root.replaceChildren(layout.root); diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index fd8f4d8f1..40c520763 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -14,11 +14,11 @@ import "@ui/ui_template_general_layout.css"; import "./B08_Quantity_UI_Page.css"; import { createWorkflowLayout } from "@ui/ui_template_workflow_layout"; -import { createButton, showToast } from "@ui/ui_template_elements"; +import { createButton } from "@ui/ui_template_elements"; import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend"; import { workflowSteps } from "../A00_Common/b_page_scaffold"; -import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav"; +import { moveStage, WORKFLOW_STEP_ROUTES, type StageStep } from "../A00_Common/b_workflow_nav"; import { renderStructureSummary } from "./B08_Quantity_UI_Summary"; import { renderMaterialSheet } from "./B08_Quantity_UI_Material"; import { renderHydro, request } from "./B08_Quantity_UI_Hydro"; @@ -166,17 +166,15 @@ export function renderB08Quantity(root: HTMLElement): void { routes: WORKFLOW_STEP_ROUTES, onStepClick: (stepIndex: number) => { if (!projectId) return; - void (async () => { - if (!(await saveShown())) return; - // B09 쪽 = 단계 5 완료 뒤(요구 300 B1) · 실패면 머묾 - if (stepIndex > 5) - try { - await request(`/projects/${projectId}/quantity/complete`, { method: "POST" }); - } catch (error) { - return showToast(`단계를 마치지 못했습니다 — ${String(error)}`, "error"); - } - goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]); - })(); + const steps: StageStep[] = [{ name: "수량 저장", run: saveShown }]; + // B09 쪽 = 단계 5 완료 뒤(요구 300 B1) · 실패면 머묾 + if (stepIndex > 5) + steps.push({ + name: "수량 산출 단계 완료", + run: async () => + void (await request(`/projects/${projectId}/quantity/complete`, { method: "POST" })), + }); + void moveStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex], steps); }, }); root.append(layout.root); diff --git a/B09_Estimation/B09_Estimation_UI_Shell.ts b/B09_Estimation/B09_Estimation_UI_Shell.ts index e9dc4b02d..e35d42b3d 100644 --- a/B09_Estimation/B09_Estimation_UI_Shell.ts +++ b/B09_Estimation/B09_Estimation_UI_Shell.ts @@ -27,7 +27,7 @@ import { attachHoverInfo } from "@ui/ui_template_hover_info"; import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; import { clearState, readState, writeState } from "../A00_Common/b_page_state"; import { workflowSteps } from "../A00_Common/b_page_scaffold"; -import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav"; +import { moveStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav"; import { computeBase, type Row } from "./B09_Estimation_Calc_Base"; import { computeEstimation } from "./B09_Estimation_Calc_Bill"; import { computeLabor, daegaPrice } from "./B09_Estimation_Calc_Labor"; @@ -357,9 +357,9 @@ export function renderB09Estimation(root: HTMLElement): void { // 481-2 단계 이동 = 고친 값 저장 성공 뒤 · 실패면 머묾 · 불러오기 전 · 고침 없음 = 요청 없이 onStepClick: (stepIndex: number) => { if (!projectId) return; - void saveChanged().then( - (ok) => ok && goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]), - ); + void moveStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex], [ + { name: "원가 저장", run: saveChanged }, + ]); }, }); // [>] = 다음 단계 없음 → 공용 틀 대신 여기서 — 저장 → 단계 6 완료 → 초안 다음 길 · 초안 없으면 흐림 그대로 @@ -369,15 +369,14 @@ export function renderB09Estimation(root: HTMLElement): void { left.addEventListener("ui-step-nav", (event) => { if ((event as CustomEvent).detail !== 1 || !goNext || !projectId) return; event.stopPropagation(); - void (async () => { - if (!(await saveChanged())) return; - try { - await request(`/projects/${projectId}/estimation/complete`, { method: "POST" }); - } catch (error) { - return showToast(`단계를 마치지 못했습니다 — ${String(error)}`, "error"); - } - goNext(); - })(); + void moveStage(projectId, goNext, [ + { name: "원가 저장", run: saveChanged }, + { + name: "원가 계산 단계 완료", + run: async () => + void (await request(`/projects/${projectId}/estimation/complete`, { method: "POST" })), + }, + ]); }); root.append(layout.root); show(current); diff --git a/resources/tester/helper_298_2_b08_b09_buttons.cjs b/resources/tester/helper_298_2_b08_b09_buttons.cjs index 5a70069b4..5c1c6056e 100644 --- a/resources/tester/helper_298_2_b08_b09_buttons.cjs +++ b/resources/tester/helper_298_2_b08_b09_buttons.cjs @@ -113,7 +113,19 @@ const common = { "../A00_Common/b_page_scaffold": { workflowSteps: () => [] }, "../A00_Common/b_workflow_nav": { WORKFLOW_STEP_ROUTES: ["b03", "b04", "b05", "b06", "b07", "b08", "b09"], - goToWorkflowStage: (_p, route) => calls.push(`go:${route}`), + // 520-3 공용 페이지 넘김 — 단계 차례 · false · 던짐 = 머묾(잠금 · 진행 글은 test_520_3) + moveStage: async (_p, target, steps) => { + for (const step of steps) + try { + if ((await step.run()) === false) return false; + } catch { + calls.push("toast:error"); + return false; + } + if (typeof target === "function") target(); + else calls.push(`go:${target}`); + return true; + }, }, "../A00_Common/b_page_state": { readState: (name) => store.get(name) ?? null, diff --git a/resources/tester/helper_481_2_save_move.cjs b/resources/tester/helper_481_2_save_move.cjs index c7ee6cf34..5e35bef7c 100644 --- a/resources/tester/helper_481_2_save_move.cjs +++ b/resources/tester/helper_481_2_save_move.cjs @@ -101,7 +101,19 @@ const common = { "../A00_Common/b_page_scaffold": { workflowSteps: () => [] }, "../A00_Common/b_workflow_nav": { WORKFLOW_STEP_ROUTES: ["b03", "b04", "b05", "b06", "b07", "b08", "b09"], - goToWorkflowStage: (_p, route) => calls.push(`go:${route}`), + // 520-3 공용 페이지 넘김 — 단계 차례 · false · 던짐 = 머묾(잠금 · 진행 글은 test_520_3) + moveStage: async (_p, target, steps) => { + for (const step of steps) + try { + if ((await step.run()) === false) return false; + } catch { + calls.push("toast:error"); + return false; + } + if (typeof target === "function") target(); + else calls.push(`go:${target}`); + return true; + }, }, "../A00_Common/b_page_state": { readState: (name) => store.get(name) ?? null, diff --git a/resources/tester/helper_520_3_move_stage.cjs b/resources/tester/helper_520_3_move_stage.cjs new file mode 100644 index 000000000..491031661 --- /dev/null +++ b/resources/tester/helper_520_3_move_stage.cjs @@ -0,0 +1,79 @@ +/* 520-3 페이지 넘김 공용 `moveStage` — 진짜 `b_workflow_nav.ts` 를 트랜스파일해 가짜 잠금 · 토스트 · 이동 위에서 돌리고 + * 잠금 · 진행 글 · 머묾 · 이동을 JSON 으로 냄. */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const ROOT = path.join(__dirname, "..", ".."); + +const calls = []; +let lock = 0; +const stubs = { + "@config/config_frontend": { API_BASE_URL: "/api", CURRENT_PROJECT_ID_KEY: "pid", ROUTES: {} }, + "@ui/ui_template_elements": { + showLoadingOverlay: () => (lock += 1), + hideLoadingOverlay: () => (lock = Math.max(0, lock - 1)), + setLoadingOverlayProgress: (ratio, label) => calls.push(`p:${ratio.toFixed(2)}:${label}`), + showToast: (text, kind) => calls.push(`toast:${kind}:${text}`), + }, + "../B04_PreProcess/B04_PreProcess_Api_Fetch": {}, + "./b_asset_cache": {}, + "./b_project_switch": { enterProject: async () => calls.push("enter") }, + "./router": { navigateTo: (route) => calls.push(`go:${route}`) }, +}; +const js = ts.transpileModule( + fs.readFileSync(path.join(ROOT, "A00_Common/b_workflow_nav.ts"), "utf8"), + { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }, +).outputText; +const mod = { exports: {} }; +global.window = { setInterval, clearInterval }; +global.localStorage = { setItem() {} }; +new Function("exports", "module", "require", js)(mod.exports, mod, (id) => stubs[id] ?? {}); +const { moveStage } = mod.exports; + +const wait = (ms) => new Promise((r) => setTimeout(r, ms)); +const run = async (target, steps) => { + calls.length = 0; + const ok = await moveStage("P1", target, steps); + return { ok, lock, calls: calls.splice(0) }; +}; + +(async () => { + const out = {}; + let lockSeen = -1; + out.ok = await run("b08", [ + { name: "저장", run: async () => void (lockSeen = lock) }, + { name: "완료", run: async () => true }, + ]); + out.ok.lockSeen = lockSeen; + out.stop = await run("b08", [ + { name: "저장", run: async () => false }, + { name: "완료", run: async () => calls.push("완료 돎") }, + ]); + out.fail = await run("b08", [ + { + name: "저장", + run: async () => { + throw new Error("Failed to fetch"); + }, + }, + ]); + let asked = 0; + out.poll = await run( + () => calls.push("go:fn"), + [ + { + name: "서버 저장", + run: () => wait(1150), + progress: async () => ((asked += 1), { ratio: 0.5, label: "관 차례" }), + }, + ], + ); + out.poll.asked = asked; + // 넘기는 중 또 누름 = 무시 + const first = moveStage("P1", "b07", [{ name: "느림", run: () => wait(50) }]); + out.busy = await moveStage("P1", "b09", []); + await first; + process.stdout.write(JSON.stringify(out)); +})(); diff --git a/resources/tester/test_280_5_b07_settings.py b/resources/tester/test_280_5_b07_settings.py index 9fec1b557..1052ac77c 100644 --- a/resources/tester/test_280_5_b07_settings.py +++ b/resources/tester/test_280_5_b07_settings.py @@ -142,12 +142,12 @@ def test_페이지_확정_단추_잠금_자동이동_없음(): assert gone not in page, gone assert "confirmed: false, // 잠금 없음(요구 281)" in page assert "frameRow.append(frameEditor.button);" in page # 298-2 — [도각 편집] = 윗줄 - assert "onStepClick: (stepIndex) => void moveToStage(stepIndex)," in page + assert "onStepClick: (stepIndex) => moveToStage(stepIndex)," in page move = page[ - page.index("const moveToStage = async") : page.index("const layout = createWorkflowLayout") + page.index("const moveToStage = (") : page.index("const layout = createWorkflowLayout") ] assert move.index("saveEditedDrawings(store)") < move.index("completeDesignStage(projectId)") - assert move.index("completeDesignStage(projectId)") < move.index("goToWorkflowStage(") + assert move.index("completeDesignStage(projectId)") < move.index("moveStage(projectId") assert "if (stepIndex > 4 && drawings.length > 0)" in move assert "currentStage: 5," in page assert len(page.splitlines()) <= 700 diff --git a/resources/tester/test_290_1_project_purge.py b/resources/tester/test_290_1_project_purge.py index f401a5705..bc5e4311a 100644 --- a/resources/tester/test_290_1_project_purge.py +++ b/resources/tester/test_290_1_project_purge.py @@ -59,6 +59,6 @@ def test_빈_초안은_고침_아님_취향은_둠(out): def test_공용_이동_한_곳에서_부름(): nav = (ROOT / "A00_Common/b_workflow_nav.ts").read_text(encoding="utf-8") - assert "void enterProject(" in nav and "./b_project_switch" in nav + assert "return enterProject(" in nav and "./b_project_switch" in nav ui = (ROOT / "ui_template/ui_template_elements.ts").read_text(encoding="utf-8") assert 'cancelLabel = "취소"' in ui and "label: cancelLabel" in ui diff --git a/resources/tester/test_520_3_move_stage.py b/resources/tester/test_520_3_move_stage.py new file mode 100644 index 000000000..22a5fe00f --- /dev/null +++ b/resources/tester/test_520_3_move_stage.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +"""520-3 페이지 넘김 공용 `moveStage` — 잠금 → 「n/N 이름 · 지난 초」 → 실패면 잠금 풀고 머묾 + 까닭 → 성공이면 이동. + +`helper_520_3_move_stage.cjs` 가 진짜 `A00_Common/b_workflow_nav.ts` 를 가짜 잠금 · 토스트 · 이동 위에서 돌림. +""" + +import json +import os +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +HERE = os.path.dirname(os.path.abspath(__file__)) + + +@pytest.fixture(scope="module") +def r(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_520_3_move_stage.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_성공_잠금_단계_글_이동_뒤_풀림(r): + o = r["ok"] + assert o["ok"] is True and o["lock"] == 0 and o["lockSeen"] == 1 + assert o["calls"][0] == "p:0.00:1/2 저장 · 0초" + assert o["calls"][1] == "p:0.50:2/2 완료 · 0초" + assert o["calls"][2] == "p:1.00:페이지 여는 중 · 0초" + assert o["calls"][-2:] == ["enter", "go:b08"] + + +def test_단계가_거짓이면_머묾_뒤_단계_안_돎(r): + o = r["stop"] + assert o["ok"] is False and o["lock"] == 0 + assert not any(c.startswith("go:") or c == "완료 돎" for c in o["calls"]) + + +def test_던지면_까닭_토스트_풀림_머묾(r): + o = r["fail"] + assert o["ok"] is False and o["lock"] == 0 + assert "toast:error:저장에서 멈췄습니다 — Failed to fetch" in o["calls"] + assert not any(c.startswith("go:") for c in o["calls"]) + + +def test_서버_진행_묻기_글_비율_함수_이동(r): + o = r["poll"] + assert o["asked"] >= 1 and o["lock"] == 0 + assert any(c.startswith("p:0.50:1/1 서버 저장 — 관 차례 · 1초") for c in o["calls"]) + assert o["calls"][-1] == "go:fn" + + +def test_넘기는_중_또_누름_무시(r): + assert r["busy"] is False + + +def test_페이지_셋이_공용_길(): + for rel in ( + "B07_DesignDetail/B07_DesignDetail_UI_Page.ts", + "B08_Quantity/B08_Quantity_UI_Page.ts", + "B09_Estimation/B09_Estimation_UI_Shell.ts", + ): + page = (ROOT / rel).read_text(encoding="utf-8") + assert "void moveStage(projectId" in page and "goToWorkflowStage(" not in page, rel diff --git a/ui_template/ui_template_elements.ts b/ui_template/ui_template_elements.ts index 6911020ba..43051d1c4 100644 --- a/ui_template/ui_template_elements.ts +++ b/ui_template/ui_template_elements.ts @@ -10,6 +10,7 @@ * ========================================================================== */ import { el } from "./ui_template_elements_base"; +import { createProgressCircle, type ProgressCircleHandle } from "./ui_template_progress"; // 차트·기본 스타일 조각은 파일이 700줄을 넘어 떼어냈다(2026-09-04). // 여기서 그대로 다시 내보내 호출부의 import 경로는 불변이다. @@ -225,6 +226,7 @@ export function createTag(label: string, variant: TagVariant = "neutral"): HTMLS let overlayEl: HTMLDivElement | null = null; let overlayCount = 0; +let overlayProgress: ProgressCircleHandle | null = null; function ensureOverlay(): HTMLDivElement { if (overlayEl) return overlayEl; @@ -248,10 +250,25 @@ export function showLoadingOverlay(): void { export function hideLoadingOverlay(): void { overlayCount = Math.max(0, overlayCount - 1); if (overlayCount === 0 && overlayEl) { - overlayEl.classList.remove("is-active"); + overlayEl.classList.remove("is-active", "ui-overlay--progress"); + overlayProgress?.remove(); + overlayProgress = null; } } +/** 띄운 잠금판에 진행 원 · 글(520-3) — 스피너 자리 · 모든 참조가 풀리면 함께 걷힘. + * ratio null = 진행률 미상(회전만). 잠금판이 안 떠 있으면 아무것도 안 함. */ +export function setLoadingOverlayProgress(ratio: number | null, label?: string): void { + if (overlayCount === 0) return; + const overlay = ensureOverlay(); + if (!overlayProgress) { + overlayProgress = createProgressCircle(); + overlay.append(overlayProgress.root); + overlay.classList.add("ui-overlay--progress"); + } + overlayProgress.set(ratio, label); +} + /* ----------------------------------------------------------------------------- * 6. 토스트 알림 (Toast) — 성공/에러 배너 * -------------------------------------------------------------------------- */ diff --git a/ui_template/ui_template_elements_styles.ts b/ui_template/ui_template_elements_styles.ts index 7ac8d386d..811bdeef2 100644 --- a/ui_template/ui_template_elements_styles.ts +++ b/ui_template/ui_template_elements_styles.ts @@ -204,6 +204,9 @@ const BASE_CSS = ` z-index: var(--z-overlay); } .ui-overlay.is-active { display: flex; } +/* 진행 잠금판(520-3) — 스피너 대신 진행 원 · 단계 글 */ +.ui-overlay--progress > .ui-spinner { display: none; } +.ui-overlay .ui-progress-circle__label { max-width: 40ch; } .ui-spinner { width: 40px; height: 40px;