diff --git a/apps/web/public/app.js b/apps/web/public/app.js index cb0a221..37a9295 100644 --- a/apps/web/public/app.js +++ b/apps/web/public/app.js @@ -18,6 +18,7 @@ const state = { vehicleSearch: "", vehicleStatusFilter: "all", eventSource: null, + launchTimer: 0, activeView: "locations", adminToken: new URLSearchParams(window.location.search).get("adminToken") || window.localStorage.getItem("cargoRadarAdminToken") || @@ -26,12 +27,35 @@ const state = { const LOGIN_SESSION_KEY = "cargoRadarLoginReady"; const LOGIN_NAME_KEY = "cargoRadarLoginName"; +const LAUNCH_STEPS = [ + { + title: "관제 데이터 연결 중", + message: "운행 차량의 최신 위치를 불러오고 있습니다." + }, + { + title: "차량 위치 불러오는 중", + message: "수신 중인 차량과 화물 정보를 정리합니다." + }, + { + title: "경로 데이터 확인 중", + message: "목적지 경로와 도착 시간을 확인합니다." + }, + { + title: "관제 화면 준비 완료", + message: "실시간 관제 화면으로 이동합니다." + } +]; const elements = { loginScreen: document.getElementById("loginScreen"), loginForm: document.getElementById("loginForm"), loginNameInput: document.getElementById("loginNameInput"), loginCodeInput: document.getElementById("loginCodeInput"), + launchScreen: document.getElementById("launchScreen"), + launchTitle: document.getElementById("launchTitle"), + launchMessage: document.getElementById("launchMessage"), + launchProgress: document.getElementById("launchProgress"), + launchSteps: [...document.querySelectorAll("[data-launch-step]")], connectionStatus: document.getElementById("connectionStatus"), systemClock: document.getElementById("systemClock"), refreshButton: document.getElementById("refreshButton"), @@ -320,11 +344,12 @@ function submitLoginForm(event) { window.localStorage.setItem(LOGIN_NAME_KEY, name || "관제 담당자"); window.localStorage.setItem("cargoRadarAdminToken", state.adminToken); elements.loginCodeInput.value = ""; - hideLoginScreen(); + beginLaunchSequence(); startDataServices(); } function showLoginScreen(options = {}) { + clearLaunchSequence(); window.localStorage.removeItem(LOGIN_SESSION_KEY); if (state.eventSource) { state.eventSource.close(); @@ -352,6 +377,56 @@ function hideLoginScreen(options = {}) { }, 620); } +function beginLaunchSequence() { + clearLaunchSequence(); + document.body.classList.add("is-launch-active"); + setLaunchStep(0); + hideLoginScreen(); + + let stepIndex = 0; + const advance = () => { + stepIndex += 1; + if (stepIndex >= LAUNCH_STEPS.length) { + finishLaunchSequence(); + return; + } + + setLaunchStep(stepIndex); + state.launchTimer = window.setTimeout(advance, stepIndex === LAUNCH_STEPS.length - 1 ? 460 : 620); + }; + + state.launchTimer = window.setTimeout(advance, 620); +} + +function setLaunchStep(index) { + const step = LAUNCH_STEPS[index] || LAUNCH_STEPS[0]; + elements.launchTitle.textContent = step.title; + elements.launchMessage.textContent = step.message; + elements.launchProgress.style.width = `${Math.min(100, ((index + 1) / LAUNCH_STEPS.length) * 100)}%`; + + for (const item of elements.launchSteps) { + const itemIndex = Number(item.dataset.launchStep); + item.classList.toggle("is-active", itemIndex === index); + item.classList.toggle("is-complete", itemIndex < index); + } +} + +function finishLaunchSequence() { + document.body.classList.add("is-launch-closing"); + state.launchTimer = window.setTimeout(() => { + document.body.classList.remove("is-launch-active", "is-launch-closing"); + state.launchTimer = 0; + }, 360); +} + +function clearLaunchSequence() { + if (state.launchTimer) { + window.clearTimeout(state.launchTimer); + state.launchTimer = 0; + } + document.body.classList.remove("is-launch-active", "is-launch-closing"); +} + function startSystemClock() { const updateClock = () => { const now = new Date(); diff --git a/apps/web/public/index.html b/apps/web/public/index.html index 4f11aed..2168c05 100644 --- a/apps/web/public/index.html +++ b/apps/web/public/index.html @@ -43,6 +43,22 @@ +
+
+ 연결 확인 + 관제 데이터 연결 중 +

운행 차량의 최신 위치를 불러오고 있습니다.

+ +
    +
  1. 관제 데이터
  2. +
  3. 차량 위치
  4. +
  5. 경로 정보
  6. +
+
+
+
diff --git a/apps/web/public/styles.css b/apps/web/public/styles.css index 2c69766..9bd1119 100644 --- a/apps/web/public/styles.css +++ b/apps/web/public/styles.css @@ -70,6 +70,12 @@ body.is-login-active .app-shell { transform: scale(0.985); } +body.is-launch-active .app-shell { + filter: blur(3px) saturate(0.92); + pointer-events: none; + transform: scale(0.995); +} + .login-screen { position: fixed; inset: 0; @@ -90,6 +96,126 @@ body.is-login-closing .login-screen { animation: login-screen-out 0.62s ease forwards; } +.launch-screen { + position: fixed; + inset: 0; + z-index: 1900; + display: none; + place-items: center; + padding: 28px; + background: rgba(12, 20, 30, 0.28); + color: var(--text); + pointer-events: none; +} + +body.is-launch-active .launch-screen { + display: grid; + animation: launch-screen-in 0.34s ease both; +} + +body.is-launch-closing .launch-screen { + animation: launch-screen-out 0.36s ease forwards; +} + +.launch-panel { + display: grid; + gap: 12px; + width: min(420px, calc(100% - 24px)); + padding: 22px; + border: 1px solid rgba(203, 213, 223, 0.92); + border-radius: 8px; + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 18px 48px rgba(5, 13, 22, 0.22); + animation: launch-panel-in 0.44s cubic-bezier(0.2, 0.72, 0.2, 1) both; +} + +.launch-panel > strong { + color: var(--navy); + font-size: 22px; + line-height: 1.2; +} + +.launch-panel p { + margin: 0; + color: var(--muted); + font-size: 14px; + line-height: 1.5; + word-break: keep-all; +} + +.launch-progress { + display: block; + height: 4px; + margin-top: 4px; + border-radius: 999px; + background: #e2e8ef; + overflow: hidden; +} + +.launch-progress span { + display: block; + width: 0; + height: 100%; + border-radius: inherit; + background: #174ea6; + transition: width 0.46s ease; +} + +.launch-steps { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; + margin: 4px 0 0; + padding: 0; + list-style: none; +} + +.launch-steps li { + position: relative; + min-width: 0; + padding: 8px 8px 8px 24px; + border: 1px solid #e1e6eb; + border-radius: 5px; + background: #f8fafc; + color: var(--muted); + font-size: 12px; + line-height: 1.2; + white-space: nowrap; +} + +.launch-steps li::before { + position: absolute; + top: 50%; + left: 8px; + width: 8px; + height: 8px; + border: 2px solid #cbd5df; + border-radius: 50%; + content: ""; + transform: translateY(-50%); +} + +.launch-steps li.is-active { + border-color: rgba(23, 78, 166, 0.28); + background: #eef5ff; + color: #174ea6; +} + +.launch-steps li.is-active::before { + border-color: #174ea6; + background: #174ea6; + box-shadow: 0 0 0 4px rgba(23, 78, 166, 0.12); +} + +.launch-steps li.is-complete { + color: var(--green); +} + +.launch-steps li.is-complete::before { + border-color: var(--green); + background: var(--green); +} + .login-visual, .login-shade { position: absolute; @@ -336,15 +462,47 @@ body.is-login-closing .login-screen { } } +@keyframes launch-screen-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes launch-panel-in { + from { + opacity: 0; + transform: translateY(16px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes launch-screen-out { + to { + opacity: 0; + } +} + @media (prefers-reduced-motion: reduce) { .login-visual, .login-route-line, .login-copy, .login-card, - body.is-login-closing .login-screen { + body.is-login-closing .login-screen, + body.is-launch-active .launch-screen, + body.is-launch-closing .launch-screen, + .launch-panel { animation: none; } + .launch-progress span, .app-shell { transition: none; } @@ -1803,6 +1961,15 @@ body.is-login-closing .login-screen { } @media (max-width: 860px) { + .launch-panel { + width: min(390px, calc(100% - 20px)); + padding: 20px; + } + + .launch-steps { + grid-template-columns: 1fr; + } + .login-content { align-content: center; grid-template-columns: 1fr;