diff --git a/apps/web/public/app.js b/apps/web/public/app.js index 3867935..cb0a221 100644 --- a/apps/web/public/app.js +++ b/apps/web/public/app.js @@ -24,11 +24,19 @@ const state = { "" }; +const LOGIN_SESSION_KEY = "cargoRadarLoginReady"; +const LOGIN_NAME_KEY = "cargoRadarLoginName"; + const elements = { + loginScreen: document.getElementById("loginScreen"), + loginForm: document.getElementById("loginForm"), + loginNameInput: document.getElementById("loginNameInput"), + loginCodeInput: document.getElementById("loginCodeInput"), connectionStatus: document.getElementById("connectionStatus"), systemClock: document.getElementById("systemClock"), refreshButton: document.getElementById("refreshButton"), demoButton: document.getElementById("demoButton"), + logoutButton: document.getElementById("logoutButton"), locationsTab: document.getElementById("locationsTab"), driversTab: document.getElementById("driversTab"), locationsPanel: document.getElementById("locationsPanel"), @@ -113,13 +121,20 @@ const VEHICLE_TYPES = { }; document.addEventListener("DOMContentLoaded", () => { + const sessionReady = initializeLoginState(); startSystemClock(); initializeMap(); bindActions(); + if (sessionReady) { + startDataServices(); + } +}); + +function startDataServices() { refreshDrivers(); refreshLatestLocations(); connectEvents(); -}); +} async function initializeMap() { const kakaoJavaScriptKey = window.CARGORADAR_CONFIG?.kakaoJavaScriptKey || ""; @@ -247,8 +262,10 @@ function loadScript(id, src) { } function bindActions() { + elements.loginForm.addEventListener("submit", submitLoginForm); elements.refreshButton.addEventListener("click", refreshLatestLocations); elements.demoButton.addEventListener("click", postDemoLocation); + elements.logoutButton.addEventListener("click", showLoginScreen); elements.locationsTab.addEventListener("click", () => setActiveView("locations")); elements.driversTab.addEventListener("click", () => setActiveView("drivers")); elements.driverForm.addEventListener("submit", submitDriverForm); @@ -278,6 +295,63 @@ function bindActions() { } } +function initializeLoginState() { + const sessionReady = window.localStorage.getItem(LOGIN_SESSION_KEY) === "true"; + const savedName = window.localStorage.getItem(LOGIN_NAME_KEY) || ""; + elements.loginNameInput.value = savedName; + + if (sessionReady) { + hideLoginScreen({ instant: true }); + return true; + } + + showLoginScreen({ instant: true }); + return false; +} + +function submitLoginForm(event) { + event.preventDefault(); + + const name = elements.loginNameInput.value.trim(); + const code = elements.loginCodeInput.value.trim(); + state.adminToken = code || state.adminToken || "demo"; + + window.localStorage.setItem(LOGIN_SESSION_KEY, "true"); + window.localStorage.setItem(LOGIN_NAME_KEY, name || "관제 담당자"); + window.localStorage.setItem("cargoRadarAdminToken", state.adminToken); + elements.loginCodeInput.value = ""; + hideLoginScreen(); + startDataServices(); +} + +function showLoginScreen(options = {}) { + window.localStorage.removeItem(LOGIN_SESSION_KEY); + if (state.eventSource) { + state.eventSource.close(); + state.eventSource = null; + } + document.body.classList.remove("is-login-closing"); + document.body.classList.add("is-login-active"); + if (options.instant) { + document.body.classList.add("is-login-ready"); + } + window.setTimeout(() => elements.loginNameInput.focus(), options.instant ? 80 : 220); +} + +function hideLoginScreen(options = {}) { + if (options.instant) { + document.body.classList.remove("is-login-active", "is-login-closing"); + document.body.classList.add("is-login-ready"); + return; + } + + document.body.classList.add("is-login-closing"); + window.setTimeout(() => { + document.body.classList.remove("is-login-active", "is-login-closing"); + document.body.classList.add("is-login-ready"); + }, 620); +} + function startSystemClock() { const updateClock = () => { const now = new Date(); diff --git a/apps/web/public/assets/login-logistics-yard.png b/apps/web/public/assets/login-logistics-yard.png new file mode 100644 index 0000000..0d707e5 Binary files /dev/null and b/apps/web/public/assets/login-logistics-yard.png differ diff --git a/apps/web/public/index.html b/apps/web/public/index.html index 53965d7..4f11aed 100644 --- a/apps/web/public/index.html +++ b/apps/web/public/index.html @@ -6,7 +6,43 @@ CargoRadar 관제 - + +
+ + + + + +
+ + + +
+
+
@@ -43,6 +79,7 @@ 테스트 전송 +
diff --git a/apps/web/public/styles.css b/apps/web/public/styles.css index a5c7fdc..2c69766 100644 --- a/apps/web/public/styles.css +++ b/apps/web/public/styles.css @@ -60,11 +60,302 @@ button:hover { background: #f9fbfc; } +body.is-login-active { + overflow: hidden; +} + +body.is-login-active .app-shell { + filter: blur(8px); + pointer-events: none; + transform: scale(0.985); +} + +.login-screen { + position: fixed; + inset: 0; + z-index: 2000; + display: none; + min-height: 100%; + overflow: hidden; + background: #101923; + color: #ffffff; +} + +body.is-login-active .login-screen { + display: grid; + place-items: center; +} + +body.is-login-closing .login-screen { + animation: login-screen-out 0.62s ease forwards; +} + +.login-visual, +.login-shade { + position: absolute; + inset: 0; +} + +.login-visual { + background: + url("/assets/login-logistics-yard.png") center / cover no-repeat; + transform: scale(1.035); + animation: login-photo-settle 8s ease-out forwards; +} + +.login-shade { + background: + linear-gradient(90deg, rgba(10, 18, 28, 0.78) 0%, rgba(10, 18, 28, 0.5) 46%, rgba(10, 18, 28, 0.42) 100%), + linear-gradient(0deg, rgba(9, 16, 24, 0.42), rgba(9, 16, 24, 0.08)); +} + +.login-route-line { + position: absolute; + width: 36vw; + height: 1px; + background: rgba(255, 255, 255, 0.38); + opacity: 0; + transform-origin: left center; + animation: login-line-pass 3.8s ease-in-out infinite; +} + +.login-route-line::after { + position: absolute; + top: -3px; + right: 0; + width: 7px; + height: 7px; + border-radius: 50%; + background: #ffffff; + content: ""; +} + +.login-route-line-a { + --line-angle: -11deg; + top: 28%; + left: 8%; + transform: rotate(-11deg); +} + +.login-route-line-b { + --line-angle: 16deg; + right: 9%; + bottom: 22%; + animation-delay: 1.4s; + transform: rotate(16deg); +} + +.login-content { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(320px, 390px); + align-items: end; + gap: clamp(28px, 5vw, 72px); + width: min(1120px, calc(100% - 56px)); +} + +.login-copy { + display: grid; + gap: 14px; + max-width: 610px; + padding-bottom: 8px; + animation: login-copy-in 0.82s cubic-bezier(0.2, 0.72, 0.2, 1) both; +} + +.login-brand-mark { + width: 46px; + height: 46px; + border-color: rgba(255, 255, 255, 0.2); + background: rgba(12, 23, 35, 0.78); +} + +.login-eyebrow { + color: rgba(255, 255, 255, 0.72); + font-size: 13px; + font-weight: 600; +} + +.login-copy h1 { + max-width: 640px; + margin: 0; + color: #ffffff; + font-size: clamp(36px, 5vw, 58px); + font-weight: 700; + line-height: 1.12; + word-break: keep-all; +} + +.login-copy p { + max-width: 520px; + margin: 0; + color: rgba(255, 255, 255, 0.78); + font-size: 17px; + line-height: 1.65; + word-break: keep-all; +} + +.login-signal-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 4px; +} + +.login-signal-list span { + min-height: 30px; + padding: 7px 10px; + border: 1px solid rgba(255, 255, 255, 0.22); + border-radius: 4px; + background: rgba(12, 23, 35, 0.42); + color: rgba(255, 255, 255, 0.82); + font-size: 13px; + font-weight: 500; +} + +.login-card { + display: grid; + gap: 14px; + padding: 24px; + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 8px; + background: rgba(255, 255, 255, 0.94); + box-shadow: 0 18px 54px rgba(5, 13, 22, 0.26); + color: var(--text); + animation: login-card-in 0.76s 0.14s cubic-bezier(0.2, 0.72, 0.2, 1) both; +} + +.login-card > strong { + color: var(--navy); + font-size: 22px; + line-height: 1.2; +} + +.login-card label { + display: grid; + gap: 6px; +} + +.login-card label span { + color: var(--muted); + font-size: 12px; + font-weight: 600; +} + +.login-card input { + width: 100%; + min-height: 42px; + padding: 0 11px; + border: 1px solid #cbd5df; + border-radius: 5px; + background: #ffffff; + color: var(--text); + font: inherit; +} + +.login-card input:focus { + border-color: #174ea6; + outline: 3px solid rgba(23, 78, 166, 0.16); +} + +.login-card button { + min-height: 44px; + margin-top: 2px; + border-color: #174ea6; + border-radius: 5px; + background: #174ea6; + color: #ffffff; + font-weight: 600; +} + +.login-card button:hover { + border-color: #123f86; + background: #123f86; +} + +.login-card small { + color: var(--muted); + font-size: 12px; +} + +@keyframes login-photo-settle { + from { + transform: scale(1.07) translateX(-1.4%); + } + + to { + transform: scale(1.015) translateX(0); + } +} + +@keyframes login-copy-in { + from { + opacity: 0; + transform: translateY(18px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes login-card-in { + from { + opacity: 0; + transform: translateY(22px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes login-line-pass { + 0% { + opacity: 0; + transform: translateX(-28px) rotate(var(--line-angle, 0deg)); + } + + 28%, + 68% { + opacity: 0.7; + } + + 100% { + opacity: 0; + transform: translateX(44px) rotate(var(--line-angle, 0deg)); + } +} + +@keyframes login-screen-out { + to { + opacity: 0; + transform: scale(1.02); + } +} + +@media (prefers-reduced-motion: reduce) { + .login-visual, + .login-route-line, + .login-copy, + .login-card, + body.is-login-closing .login-screen { + animation: none; + } + + .app-shell { + transition: none; + } +} + .app-shell { display: grid; grid-template-rows: 64px minmax(0, 1fr); width: 100%; height: 100%; + transition: filter 0.42s ease, transform 0.42s ease; } .topbar { @@ -144,6 +435,10 @@ button:hover { padding: 0 11px; } +.toolbar-button.secondary { + color: var(--muted); +} + .button-icon, .button-icon svg { display: block; @@ -1508,6 +1803,30 @@ button:hover { } @media (max-width: 860px) { + .login-content { + align-content: center; + grid-template-columns: 1fr; + gap: 24px; + width: min(520px, calc(100% - 32px)); + } + + .login-copy { + gap: 10px; + padding-bottom: 0; + } + + .login-copy h1 { + font-size: clamp(30px, 9vw, 42px); + } + + .login-copy p { + font-size: 15px; + } + + .login-card { + padding: 20px; + } + .app-shell { grid-template-rows: auto minmax(0, 1fr); } @@ -1593,6 +1912,33 @@ button:hover { } @media (max-width: 560px) { + .login-screen { + overflow: auto; + } + + .login-content { + min-height: 100%; + padding: 26px 0; + } + + .login-brand-mark { + width: 40px; + height: 40px; + } + + .login-copy h1 { + font-size: 30px; + } + + .login-signal-list { + gap: 6px; + } + + .login-signal-list span { + min-height: 28px; + font-size: 12px; + } + .workspace { grid-template-rows: minmax(300px, 42vh) minmax(0, 1fr); }