로그인 후 관제 연결 단계 추가

This commit is contained in:
2026-06-08 13:22:41 +09:00
parent f3df978769
commit 8d7d2fea81
3 changed files with 260 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ const state = {
vehicleSearch: "", vehicleSearch: "",
vehicleStatusFilter: "all", vehicleStatusFilter: "all",
eventSource: null, eventSource: null,
launchTimer: 0,
activeView: "locations", activeView: "locations",
adminToken: new URLSearchParams(window.location.search).get("adminToken") || adminToken: new URLSearchParams(window.location.search).get("adminToken") ||
window.localStorage.getItem("cargoRadarAdminToken") || window.localStorage.getItem("cargoRadarAdminToken") ||
@@ -26,12 +27,35 @@ const state = {
const LOGIN_SESSION_KEY = "cargoRadarLoginReady"; const LOGIN_SESSION_KEY = "cargoRadarLoginReady";
const LOGIN_NAME_KEY = "cargoRadarLoginName"; const LOGIN_NAME_KEY = "cargoRadarLoginName";
const LAUNCH_STEPS = [
{
title: "관제 데이터 연결 중",
message: "운행 차량의 최신 위치를 불러오고 있습니다."
},
{
title: "차량 위치 불러오는 중",
message: "수신 중인 차량과 화물 정보를 정리합니다."
},
{
title: "경로 데이터 확인 중",
message: "목적지 경로와 도착 시간을 확인합니다."
},
{
title: "관제 화면 준비 완료",
message: "실시간 관제 화면으로 이동합니다."
}
];
const elements = { const elements = {
loginScreen: document.getElementById("loginScreen"), loginScreen: document.getElementById("loginScreen"),
loginForm: document.getElementById("loginForm"), loginForm: document.getElementById("loginForm"),
loginNameInput: document.getElementById("loginNameInput"), loginNameInput: document.getElementById("loginNameInput"),
loginCodeInput: document.getElementById("loginCodeInput"), 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"), connectionStatus: document.getElementById("connectionStatus"),
systemClock: document.getElementById("systemClock"), systemClock: document.getElementById("systemClock"),
refreshButton: document.getElementById("refreshButton"), refreshButton: document.getElementById("refreshButton"),
@@ -320,11 +344,12 @@ function submitLoginForm(event) {
window.localStorage.setItem(LOGIN_NAME_KEY, name || "관제 담당자"); window.localStorage.setItem(LOGIN_NAME_KEY, name || "관제 담당자");
window.localStorage.setItem("cargoRadarAdminToken", state.adminToken); window.localStorage.setItem("cargoRadarAdminToken", state.adminToken);
elements.loginCodeInput.value = ""; elements.loginCodeInput.value = "";
hideLoginScreen(); beginLaunchSequence();
startDataServices(); startDataServices();
} }
function showLoginScreen(options = {}) { function showLoginScreen(options = {}) {
clearLaunchSequence();
window.localStorage.removeItem(LOGIN_SESSION_KEY); window.localStorage.removeItem(LOGIN_SESSION_KEY);
if (state.eventSource) { if (state.eventSource) {
state.eventSource.close(); state.eventSource.close();
@@ -352,6 +377,56 @@ function hideLoginScreen(options = {}) {
}, 620); }, 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() { function startSystemClock() {
const updateClock = () => { const updateClock = () => {
const now = new Date(); const now = new Date();

View File

@@ -43,6 +43,22 @@
</div> </div>
</section> </section>
<section id="launchScreen" class="launch-screen" aria-label="관제 연결 상태" aria-live="polite">
<div class="launch-panel">
<span class="section-eyebrow">연결 확인</span>
<strong id="launchTitle">관제 데이터 연결 중</strong>
<p id="launchMessage">운행 차량의 최신 위치를 불러오고 있습니다.</p>
<div class="launch-progress" aria-hidden="true">
<span id="launchProgress"></span>
</div>
<ol class="launch-steps">
<li data-launch-step="0" class="is-active">관제 데이터</li>
<li data-launch-step="1">차량 위치</li>
<li data-launch-step="2">경로 정보</li>
</ol>
</div>
</section>
<div class="app-shell"> <div class="app-shell">
<header class="topbar"> <header class="topbar">
<div class="brand-lockup"> <div class="brand-lockup">

View File

@@ -70,6 +70,12 @@ body.is-login-active .app-shell {
transform: scale(0.985); 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 { .login-screen {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -90,6 +96,126 @@ body.is-login-closing .login-screen {
animation: login-screen-out 0.62s ease forwards; 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-visual,
.login-shade { .login-shade {
position: absolute; 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) { @media (prefers-reduced-motion: reduce) {
.login-visual, .login-visual,
.login-route-line, .login-route-line,
.login-copy, .login-copy,
.login-card, .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; animation: none;
} }
.launch-progress span,
.app-shell { .app-shell {
transition: none; transition: none;
} }
@@ -1803,6 +1961,15 @@ body.is-login-closing .login-screen {
} }
@media (max-width: 860px) { @media (max-width: 860px) {
.launch-panel {
width: min(390px, calc(100% - 20px));
padding: 20px;
}
.launch-steps {
grid-template-columns: 1fr;
}
.login-content { .login-content {
align-content: center; align-content: center;
grid-template-columns: 1fr; grid-template-columns: 1fr;