From 7c0b8c80fe0ab2244b65b14b0ba2814bb22992ea Mon Sep 17 00:00:00 2001 From: gbpark Date: Mon, 6 Apr 2026 15:54:35 +0900 Subject: [PATCH] 123 --- .../java/com/erp/security/SecurityConfig.java | 3 +- .../src/main/resources/mapper/admin.xml | 2 +- docker/dev/docker-compose.backend.mac.yml | 16 +- docker/dev/docker-compose.frontend.mac.yml | 13 +- docker/dev/frontend.Dockerfile | 4 +- frontend/app/(auth)/login/login.css | 221 ++++ frontend/app/(auth)/login/page.tsx | 165 ++- .../admin/systemMng/tableMngList/page.tsx | 16 +- frontend/app/globals.css | 3 + frontend/components/layout/AppLayout.tsx | 544 ++++----- .../components/layout/CosmicBackground.tsx | 55 + frontend/components/layout/TabBar.tsx | 31 +- frontend/invion-layout-v5.html | 973 +++++++++++++++ frontend/invion-preview-v5.html | 1049 +++++++++++++++++ frontend/styles/v5-layout.css | 465 ++++++++ scripts/dev/start-all-parallel.sh | 36 +- 16 files changed, 3255 insertions(+), 341 deletions(-) create mode 100644 frontend/app/(auth)/login/login.css create mode 100644 frontend/components/layout/CosmicBackground.tsx create mode 100644 frontend/invion-layout-v5.html create mode 100644 frontend/invion-preview-v5.html create mode 100644 frontend/styles/v5-layout.css diff --git a/backend-spring/src/main/java/com/erp/security/SecurityConfig.java b/backend-spring/src/main/java/com/erp/security/SecurityConfig.java index 0080b4b9..0caa1d53 100644 --- a/backend-spring/src/main/java/com/erp/security/SecurityConfig.java +++ b/backend-spring/src/main/java/com/erp/security/SecurityConfig.java @@ -31,8 +31,7 @@ public class SecurityConfig { .cors(cors -> cors.configurationSource(corsConfigurationSource())) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/auth/**").permitAll() - .requestMatchers("/api/multilang/batch").permitAll() + .requestMatchers("/api/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), diff --git a/backend-spring/src/main/resources/mapper/admin.xml b/backend-spring/src/main/resources/mapper/admin.xml index 00621fd4..a58d7ca5 100644 --- a/backend-spring/src/main/resources/mapper/admin.xml +++ b/backend-spring/src/main/resources/mapper/admin.xml @@ -33,7 +33,7 @@ AND MENU.STATUS = 'active' - AND MENU.MENU_TYPE = CAST(#{menu_type} AS NUMERIC) + AND MENU.MENU_TYPE = #{menu_type} diff --git a/docker/dev/docker-compose.backend.mac.yml b/docker/dev/docker-compose.backend.mac.yml index 15deac2a..5e22f741 100644 --- a/docker/dev/docker-compose.backend.mac.yml +++ b/docker/dev/docker-compose.backend.mac.yml @@ -4,32 +4,32 @@ services: build: context: ../../backend-spring dockerfile: ../docker/dev/backend-spring.Dockerfile - container_name: pms-backend-mac + container_name: pms-backend-mac-v2 ports: - - "8081:8081" + - "8082:8082" extra_hosts: - "host.docker.internal:host-gateway" environment: - SPRING_PROFILES_ACTIVE=dev - - SERVER_PORT=8081 - - SPRING_DATASOURCE_URL=jdbc:postgresql://39.117.244.52:11132/testvex + - SERVER_PORT=8082 + - SPRING_DATASOURCE_URL=jdbc:postgresql://211.115.91.141:11134/test_dev - SPRING_DATASOURCE_USERNAME=postgres - - SPRING_DATASOURCE_PASSWORD=ph0909!! + - SPRING_DATASOURCE_PASSWORD=vexplor0909!! - JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024 - JWT_EXPIRATION=86400000 - FILE_UPLOAD_DIR=./uploads volumes: - ../../backend-spring:/app networks: - - pms-network + - test-vex-network restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8081/health"] + test: ["CMD", "curl", "-f", "http://localhost:8082/health"] interval: 30s timeout: 10s retries: 3 start_period: 90s networks: - pms-network: + test-vex-network: driver: bridge diff --git a/docker/dev/docker-compose.frontend.mac.yml b/docker/dev/docker-compose.frontend.mac.yml index 7666eeee..ecbcc240 100644 --- a/docker/dev/docker-compose.frontend.mac.yml +++ b/docker/dev/docker-compose.frontend.mac.yml @@ -4,22 +4,23 @@ services: build: context: ../../frontend dockerfile: ../docker/dev/frontend.Dockerfile - container_name: pms-frontend-mac + container_name: pms-frontend-mac-v2 ports: - - "9771:3000" + - "9772:3000" environment: - - NEXT_PUBLIC_API_URL=http://localhost:8081/api - - SERVER_API_URL=http://pms-backend-mac:8081 + - NEXT_PUBLIC_API_URL=http://localhost:8082/api + - SERVER_API_URL=http://pms-backend-mac-v2:8082 - NODE_OPTIONS=--max-old-space-size=8192 - NEXT_TELEMETRY_DISABLED=1 + - WATCHPACK_POLLING=true volumes: - ../../frontend:/app - /app/node_modules - /app/.next networks: - - pms-network + - test-vex-network restart: unless-stopped networks: - pms-network: + test-vex-network: driver: bridge diff --git a/docker/dev/frontend.Dockerfile b/docker/dev/frontend.Dockerfile index cb2719bc..4520d6f1 100644 --- a/docker/dev/frontend.Dockerfile +++ b/docker/dev/frontend.Dockerfile @@ -1,5 +1,5 @@ -# Node.js 18 기반 이미지 사용 -FROM node:18-alpine +# Node.js 20 기반 이미지 사용 +FROM node:20-alpine # 작업 디렉토리 설정 WORKDIR /app diff --git a/frontend/app/(auth)/login/login.css b/frontend/app/(auth)/login/login.css new file mode 100644 index 00000000..2ea8957e --- /dev/null +++ b/frontend/app/(auth)/login/login.css @@ -0,0 +1,221 @@ +/* ===== INVION v5 Login — Cosmic Command Center ===== */ +/* Scoped under .inv-login to avoid global conflicts */ + +.inv-login { + --bg:#fafaff; --bg-subtle:#f3f2fa; --surface:rgba(255,255,255,0.55); --surface-solid:#ffffff; + --surface-hover:rgba(255,255,255,0.7); --text:#0f0e1a; --text-sec:#6b6a80; --text-muted:#9998ad; + --primary:#6c5ce7; --primary-light:#a29bfe; --primary-glow:rgba(108,92,231,0.25); + --cyan:#00cec9; --cyan-glow:rgba(0,206,201,0.2); --pink:#fd79a8; --pink-glow:rgba(253,121,168,0.15); + --red:#ff4757; --green:#00b894; --amber:#fdcb6e; + --border:rgba(108,92,231,0.12); --border-subtle:rgba(0,0,0,0.05); + --glass:rgba(255,255,255,0.45); --glass-strong:rgba(255,255,255,0.65); + --glass-border:rgba(108,92,231,0.12); + --glow-sm:0 0 20px rgba(108,92,231,0.12); --glow-md:0 0 40px rgba(108,92,231,0.2); + --glow-lg:0 0 80px rgba(108,92,231,0.25); + + position:fixed;inset:0;z-index:1; + font-family:'Inter',system-ui,sans-serif; + background:var(--bg);color:var(--text); + display:flex;align-items:center;justify-content:center; + overflow:hidden; + transition:background .5s,color .4s; +} + +.inv-login.dark { + --bg:#06050e; --bg-subtle:#0c0b18; --surface:rgba(17,16,42,0.5); --surface-solid:#11102a; + --surface-hover:rgba(25,24,64,0.6); --text:#eae8f4; --text-sec:#8d8ba8; --text-muted:#5a587a; + --primary:#a29bfe; --primary-light:#c8c4ff; --primary-glow:rgba(162,155,254,0.25); + --cyan:#55efc4; --cyan-glow:rgba(85,239,196,0.15); --pink:#fd79a8; --red:#ff6b6b; + --green:#55efc4; --amber:#ffeaa7; + --border:rgba(162,155,254,0.1); --border-subtle:rgba(255,255,255,0.04); + --glass:rgba(17,16,42,0.45); --glass-strong:rgba(17,16,42,0.65); + --glass-border:rgba(162,155,254,0.12); + --glow-sm:0 0 20px rgba(162,155,254,0.1); --glow-md:0 0 40px rgba(162,155,254,0.18); + --glow-lg:0 0 80px rgba(162,155,254,0.22); +} + +/* ===== COSMIC BACKGROUND ===== */ +.inv-login .cosmos{position:fixed;inset:0;pointer-events:none;z-index:0;overflow:hidden;} + +.inv-login .star{position:absolute;width:2px;height:2px;background:white;border-radius:50%; + animation:inv-twinkle var(--d,3s) ease-in-out infinite alternate;animation-delay:var(--dl,0s);opacity:0;} +.inv-login .star.c{width:3px;height:3px;background:var(--sc);} +@keyframes inv-twinkle{0%{opacity:0;transform:scale(.5)}100%{opacity:var(--mo,.7);transform:scale(1)}} + +.inv-login:not(.dark) .star{display:none;} +.inv-login:not(.dark) .shooting-star{display:none;} +.inv-login:not(.dark) .particle{display:none;} + +.inv-login .neb{position:absolute;border-radius:50%;filter:blur(140px);animation:inv-drift 16s ease-in-out infinite alternate;} +.inv-login .neb-1{width:700px;height:700px;top:-20%;right:-15%;background:radial-gradient(circle,var(--primary-glow),transparent 70%);animation-duration:18s;} +.inv-login .neb-2{width:600px;height:600px;bottom:-25%;left:-10%;background:radial-gradient(circle,var(--cyan-glow),transparent 70%);animation-duration:14s;animation-delay:-4s;} +.inv-login .neb-3{width:450px;height:450px;top:35%;left:40%;background:radial-gradient(circle,var(--pink-glow),transparent 70%);animation-duration:12s;animation-delay:-8s;} +.inv-login .neb-4{width:350px;height:350px;top:60%;right:25%;background:radial-gradient(circle,rgba(108,92,231,0.08),transparent 70%);animation-duration:20s;animation-delay:-2s;} +@keyframes inv-drift{0%{transform:translate(0,0) scale(1)}100%{transform:translate(30px,-25px) scale(1.1)}} + +.inv-login:not(.dark) .cosmos{background:linear-gradient(180deg,#e8e4ff 0%,#f0edff 30%,#fafaff 60%,#f5f0ff 100%);} +.inv-login:not(.dark) .neb{filter:blur(100px);} +.inv-login:not(.dark) .neb-1{width:1200px;height:500px;top:auto;bottom:-10%;right:-15%;border-radius:50%; + background:radial-gradient(ellipse,rgba(255,255,255,0.9),rgba(230,225,255,0.5),transparent 70%);animation-duration:25s;} +.inv-login:not(.dark) .neb-2{width:1000px;height:400px;top:auto;bottom:-5%;left:-10%; + background:radial-gradient(ellipse,rgba(255,255,255,0.85),rgba(200,240,255,0.4),transparent 70%);animation-duration:20s;} +.inv-login:not(.dark) .neb-3{width:800px;height:350px;top:auto;bottom:5%;left:30%; + background:radial-gradient(ellipse,rgba(255,255,255,0.8),rgba(240,220,255,0.3),transparent 70%);animation-duration:22s;} +.inv-login:not(.dark) .neb-4{width:600px;height:600px;top:-10%;right:20%;bottom:auto; + background:radial-gradient(circle,rgba(108,92,231,0.08),rgba(0,206,201,0.04),transparent 70%);} + +.inv-login .shooting-star{position:absolute;width:80px;height:1px; + background:linear-gradient(90deg,rgba(255,255,255,0.6),transparent); + transform:rotate(35deg);animation:inv-shoot 5s ease-in-out infinite;opacity:0;} +.inv-login .shooting-star:nth-child(6){animation-delay:3s;top:25%;left:65%;width:55px;transform:rotate(40deg);} +@keyframes inv-shoot{0%{opacity:0;transform:rotate(35deg) translateX(0)}3%{opacity:0.7}12%{opacity:0;transform:rotate(35deg) translateX(-350px)}100%{opacity:0}} + +.inv-login .particle{position:absolute;width:var(--sz,4px);height:var(--sz,4px);background:var(--pc,var(--primary)); + border-radius:50%;opacity:0;animation:inv-floatup var(--fd,9s) ease-in-out infinite;animation-delay:var(--fdl,0s);} +@keyframes inv-floatup{0%{opacity:0;transform:translateY(100vh) scale(0)}10%{opacity:.4}90%{opacity:.4}100%{opacity:0;transform:translateY(-80px) scale(1)}} + +/* ===== WARP CANVAS ===== */ +.inv-login .warp-canvas{position:fixed;inset:0;z-index:2;pointer-events:none;opacity:0;} +.inv-login .warp-canvas.active{opacity:1;} + +/* ===== THEME TOGGLE ===== */ +.inv-login .pill{display:flex;background:var(--surface);backdrop-filter:blur(8px);border:1px solid var(--glass-border); + border-radius:999px;padding:2px;position:absolute;top:1.5rem;right:1.5rem;z-index:20;} +.inv-login .pill button{padding:.22rem .65rem;border-radius:999px;border:none;background:transparent; + color:var(--text-muted);cursor:pointer;font-size:.6rem;font-weight:600;font-family:inherit; + transition:all .3s cubic-bezier(.4,0,.2,1);} +.inv-login .pill button.on{background:var(--primary);color:white;box-shadow:var(--glow-sm);} + +/* ===== THEME FADE ===== */ +.inv-login .theme-fade{position:fixed;inset:0;z-index:9999;pointer-events:none;opacity:0; + transition:opacity .4s cubic-bezier(.4,0,.2,1);} +.inv-login .theme-fade.in{opacity:1;} + +/* ===== LOGIN CARD ===== */ +.inv-login .login-card{ + width:400px;backdrop-filter:blur(30px) saturate(1.5);-webkit-backdrop-filter:blur(30px) saturate(1.5); + border-radius:28px;padding:2.5rem 2.5rem 2rem;background:var(--glass-strong); + border:1px solid var(--glass-border); + box-shadow:0 12px 48px rgba(0,0,0,0.08),inset 0 0 0 1px rgba(255,255,255,0.18); + animation:inv-cardIn 1s cubic-bezier(.16,1,.3,1) .3s both;position:relative;z-index:5; +} +.inv-login:not(.dark) .login-card{background:rgba(255,255,255,0.82);} +.inv-login.dark .login-card{box-shadow:0 12px 48px rgba(0,0,0,0.6),var(--glow-md),inset 0 0 0 1px rgba(162,155,254,0.06);} +@keyframes inv-cardIn{from{opacity:0;transform:translateY(40px) scale(.96)}to{opacity:1;transform:none}} + +/* Orbital decoration */ +.inv-login .login-orbit{width:80px;height:80px;margin:0 auto 1rem;position:relative;animation:inv-logoIn 1.2s cubic-bezier(.16,1,.3,1) .4s both;} +.inv-login .orbit-core{position:absolute;inset:22px;border-radius:50%; + background:linear-gradient(135deg,var(--primary),var(--cyan)); + box-shadow:0 0 30px var(--primary-glow),0 0 60px rgba(0,206,201,0.15); + animation:inv-corePulse 3s ease-in-out infinite;} +@keyframes inv-corePulse{0%,100%{box-shadow:0 0 30px var(--primary-glow),0 0 60px rgba(0,206,201,0.15)} + 50%{box-shadow:0 0 40px var(--primary-glow),0 0 80px rgba(0,206,201,0.25)}} +.inv-login .orbit-ring{position:absolute;inset:0;border-radius:50%;border:1.5px solid transparent; + border-top-color:var(--primary);border-right-color:var(--cyan); + animation:inv-orbitSpin 4s linear infinite;opacity:.6;} +.inv-login .orbit-ring-2{position:absolute;inset:6px;border-radius:50%;border:1px solid transparent; + border-bottom-color:var(--pink);border-left-color:var(--primary-light); + animation:inv-orbitSpin 6s linear infinite reverse;opacity:.4;} +@keyframes inv-orbitSpin{to{transform:rotate(360deg)}} +.inv-login .orbit-dot{position:absolute;width:6px;height:6px;border-radius:50%;background:var(--cyan); + top:-1px;left:50%;margin-left:-3px;box-shadow:0 0 10px var(--cyan-glow); + animation:inv-orbitSpin 4s linear infinite;transform-origin:3px 41px;} + +/* Fail shake */ +@keyframes inv-denied{ + 0%{transform:scale(1.1);box-shadow:0 0 60px rgba(255,71,87,0.4),0 0 0 2px rgba(255,71,87,0.5),inset 0 0 30px rgba(255,71,87,0.05)} + 18%{transform:scale(1) translateX(-6px)} + 36%{transform:scale(1) translateX(5px)} + 52%{transform:scale(1) translateX(-4px)} + 66%{transform:scale(1) translateX(3px)} + 78%{transform:scale(1) translateX(-1px)} + 100%{transform:scale(1) translateX(0);box-shadow:none} +} +.inv-login .login-card.denied{animation:inv-denied .55s cubic-bezier(.36,.07,.19,.97) both;} + +/* Logo */ +.inv-login .logo{text-align:center;margin-bottom:.15rem;animation:inv-logoIn 1.2s cubic-bezier(.16,1,.3,1) .5s both;} +.inv-login .logo h1{font-size:1.8rem;font-weight:900;letter-spacing:-.04em; + background:linear-gradient(135deg,var(--primary),var(--cyan) 60%,var(--pink)); + background-size:200% 200%;-webkit-background-clip:text;-webkit-text-fill-color:transparent; + background-clip:text;animation:inv-gshift 4s ease-in-out infinite;} +@keyframes inv-logoIn{from{opacity:0;transform:translateY(-20px);filter:blur(10px)}to{opacity:1;transform:none;filter:none}} +@keyframes inv-gshift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}} + +.inv-login .login-sub{text-align:center;font-size:.78rem;color:var(--text-muted);margin-bottom:1.8rem;animation:inv-fadeIn .8s .7s both;} +@keyframes inv-fadeIn{from{opacity:0}to{opacity:1}} + +/* Form groups */ +.inv-login .fg{margin-bottom:1rem;animation:inv-fgIn .6s cubic-bezier(.16,1,.3,1) both;} +.inv-login .fg:nth-child(1){animation-delay:.8s} +.inv-login .fg:nth-child(2){animation-delay:.9s} +@keyframes inv-fgIn{from{opacity:0;transform:translateX(-20px)}to{opacity:1;transform:none}} + +/* Input with icon */ +.inv-login .fi-wrap{position:relative;display:flex;align-items:center;} +.inv-login .fi-icon{position:absolute;left:14px;color:var(--text-muted);pointer-events:none;transition:color .3s;flex-shrink:0;} +.inv-login .fi-wrap:focus-within .fi-icon{color:var(--primary);} +.inv-login .fi{width:100%;height:48px;padding:0 1rem 0 2.6rem;border:1.5px solid var(--border);border-radius:14px; + background:var(--surface);color:var(--text);font-size:.875rem;font-family:inherit;outline:none; + backdrop-filter:blur(8px);transition:all .3s cubic-bezier(.4,0,.2,1);} +.inv-login:not(.dark) input.fi, +.inv-login:not(.dark) input.fi:focus, +.inv-login:not(.dark) input.fi:active, +.inv-login:not(.dark) input.fi:hover{ + background-color:#ffffff !important; + backdrop-filter:none !important; + -webkit-backdrop-filter:none !important; + border-color:rgba(108,92,231,0.18) !important; + color:#0f0e1a !important; +} +.inv-login:not(.dark) input.fi:-webkit-autofill, +.inv-login:not(.dark) input.fi:-webkit-autofill:focus{ + -webkit-box-shadow:0 0 0 1000px #ffffff inset !important; + -webkit-text-fill-color:#0f0e1a !important; + background-color:#ffffff !important; +} +.inv-login .fi::placeholder{color:var(--text-muted);font-weight:400;} +.inv-login .fi:focus{border-color:var(--primary);box-shadow:0 0 0 4px var(--primary-glow),var(--glow-sm);} +.inv-login .fi.error{border-color:rgba(255,71,87,0.4) !important;box-shadow:0 0 12px rgba(255,71,87,0.12) !important;} +.inv-login .pw-w .fi{padding-right:2.8rem;} +.inv-login .pw-w{position:relative;display:flex;align-items:center;} +.inv-login .pw-b{position:absolute;right:14px;top:50%;transform:translateY(-50%);background:none;border:none; + color:var(--text-muted);cursor:pointer;padding:4px;transition:color .2s;} +.inv-login .pw-b:hover{color:var(--primary);} + +/* Divider */ +.inv-login .login-divider{display:flex;align-items:center;gap:.75rem;margin:1.25rem 0;animation:inv-fgIn .6s cubic-bezier(.16,1,.3,1) 1s both;} +.inv-login .login-divider::before,.inv-login .login-divider::after{content:'';flex:1;height:1px;background:var(--border);} +.inv-login .login-divider span{font-size:.6rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;} + +/* Error message */ +.inv-login .err-msg{font-size:.72rem;color:var(--red);text-align:center;margin-top:.75rem; + opacity:0;transform:translateY(-5px);transition:opacity .4s,transform .4s;} +.inv-login .err-msg.show{opacity:1;transform:translateY(0);} + +/* Launch button */ +.inv-login .lbtn{width:100%;height:50px;border:none;border-radius:14px;font-size:.9rem;font-weight:700; + font-family:inherit;color:white;cursor:pointer;position:relative;overflow:hidden; + display:flex;align-items:center;justify-content:center;gap:.5rem; + background:linear-gradient(135deg,var(--primary),var(--cyan)); + box-shadow:0 4px 20px var(--primary-glow);transition:all .4s cubic-bezier(.4,0,.2,1); + animation:inv-fgIn .6s cubic-bezier(.16,1,.3,1) 1.1s both;} +.inv-login .lbtn:hover{transform:translateY(-2px);box-shadow:var(--glow-lg),0 8px 30px var(--primary-glow);} +.inv-login .lbtn:active{transform:translateY(0) scale(.98);} +.inv-login .lbtn:disabled{opacity:.7;cursor:not-allowed;transform:none;} +.inv-login .lbtn::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%; + background:linear-gradient(90deg,transparent,rgba(255,255,255,.2),transparent);animation:inv-shimmer 3s ease-in-out infinite;} +@keyframes inv-shimmer{0%{left:-100%}50%{left:100%}100%{left:100%}} + +/* Ripple */ +.inv-login .rip{position:absolute;border-radius:50%;background:rgba(255,255,255,.4);transform:scale(0);animation:inv-ripX .6s ease-out;pointer-events:none;} +@keyframes inv-ripX{to{transform:scale(4);opacity:0}} + +/* Spinner */ +.inv-login .spinner{display:inline-block;width:20px;height:20px;border:2.5px solid rgba(255,255,255,.3); + border-top-color:white;border-radius:50%;animation:inv-spin .6s linear infinite;} +@keyframes inv-spin{to{transform:rotate(360deg)}} + +/* Footer */ +.inv-login .login-ft{text-align:center;margin-top:1.25rem;font-size:.65rem;color:var(--text-muted);animation:inv-fadeIn .8s 1.2s both;letter-spacing:.02em;} diff --git a/frontend/app/(auth)/login/page.tsx b/frontend/app/(auth)/login/page.tsx index 73a8de80..e2fa7cad 100644 --- a/frontend/app/(auth)/login/page.tsx +++ b/frontend/app/(auth)/login/page.tsx @@ -1,9 +1,9 @@ "use client"; +import { useEffect, useRef, useCallback, useState } from "react"; import { useLogin } from "@/hooks/useLogin"; -import { LoginHeader } from "@/components/auth/LoginHeader"; -import { LoginForm } from "@/components/auth/LoginForm"; -import { LoginFooter } from "@/components/auth/LoginFooter"; +import { User, Lock, Eye, EyeOff, ArrowRight } from "lucide-react"; +import "./login.css"; export default function LoginPage() { const { @@ -11,31 +11,156 @@ export default function LoginPage() { isLoading, error, showPassword, - isPopMode, handleInputChange, handleLogin, togglePasswordVisibility, - togglePopMode, } = useLogin(); + const rootRef = useRef(null); + const cosmosRef = useRef(null); + const cardRef = useRef(null); + const fadeRef = useRef(null); + const errRef = useRef(null); + const [isDark, setIsDark] = useState(false); + + // ===== Cosmic background — stars + particles ===== + useEffect(() => { + const co = cosmosRef.current; + if (!co) return; + const cs = ["rgba(162,155,254,.8)", "rgba(85,239,196,.7)", "rgba(253,121,168,.7)"]; + for (let i = 0; i < 150; i++) { + const s = document.createElement("div"); + s.className = "star" + (Math.random() > 0.83 ? " c" : ""); + if (s.classList.contains("c")) s.style.setProperty("--sc", cs[(Math.random() * 3) | 0]); + s.style.left = Math.random() * 100 + "%"; + s.style.top = Math.random() * 100 + "%"; + s.style.setProperty("--d", (2 + Math.random() * 5) + "s"); + s.style.setProperty("--dl", Math.random() * 5 + "s"); + s.style.setProperty("--mo", (0.3 + Math.random() * 0.7) + ""); + co.appendChild(s); + } + const pc = ["var(--primary)", "var(--cyan)", "var(--pink)"]; + for (let i = 0; i < 20; i++) { + const p = document.createElement("div"); + p.className = "particle"; + p.style.left = Math.random() * 100 + "%"; + p.style.setProperty("--sz", (2 + Math.random() * 4) + "px"); + p.style.setProperty("--pc", pc[(Math.random() * 3) | 0]); + p.style.setProperty("--fd", (7 + Math.random() * 12) + "s"); + p.style.setProperty("--fdl", Math.random() * 10 + "s"); + co.appendChild(p); + } + return () => { + co.querySelectorAll(".star, .particle").forEach((el) => el.remove()); + }; + }, []); + + // ===== Theme toggle ===== + const setTheme = useCallback((t: "light" | "dark") => { + const root = rootRef.current; + const fade = fadeRef.current; + if (!root || !fade || (window as any)._themeSwitching) return; + const cur = root.classList.contains("dark") ? "dark" : "light"; + if (cur === t) return; + (window as any)._themeSwitching = true; + fade.style.background = + t === "dark" + ? "radial-gradient(ellipse at center,#0c0b18,#06050e)" + : "radial-gradient(ellipse at center,#f3f2fa,#fafaff)"; + fade.classList.add("in"); + setTimeout(() => { + root.classList.toggle("dark", t === "dark"); + setIsDark(t === "dark"); + setTimeout(() => { + fade.classList.remove("in"); + (window as any)._themeSwitching = false; + }, 50); + }, 420); + }, []); + + // ===== Show error with denied animation ===== + useEffect(() => { + if (!error || !cardRef.current || !errRef.current) return; + const card = cardRef.current; + const errEl = errRef.current; + card.classList.remove("denied"); + void card.offsetWidth; + card.classList.add("denied"); + errEl.classList.add("show"); + const t1 = setTimeout(() => card.classList.remove("denied"), 600); + const t2 = setTimeout(() => errEl.classList.remove("show"), 3000); + return () => { clearTimeout(t1); clearTimeout(t2); }; + }, [error]); + + // ===== Ripple on button ===== + const handleRipple = useCallback((e: React.MouseEvent) => { + const btn = e.currentTarget; + const r = btn.getBoundingClientRect(); + const d = document.createElement("div"); + d.className = "rip"; + const sz = Math.max(r.width, r.height) * 2; + d.style.width = d.style.height = sz + "px"; + d.style.left = e.clientX - r.left - sz / 2 + "px"; + d.style.top = e.clientY - r.top - sz / 2 + "px"; + btn.appendChild(d); + setTimeout(() => d.remove(), 600); + }, []); + return ( -
-
- +
+
- +
+
+
+
+
+
+
+
- +
+ + +
+ +
+
+
+
+
+
+
+ +

INVION

+
Cosmic Command Center
+ +
+
+
+ + +
+
+
+
+ + + +
+
+ +
ready to launch
+ + +
+ +
{error || "아이디 또는 비밀번호를 확인해주세요"}
+
© 2026 INVION
); diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index 6535b3bf..6f6e02f6 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -377,7 +377,11 @@ export default function TableManagementPage() { try { const response = await apiClient.get(`/table-management/tables/${tableName}/constraints`); if (response.data.success) { - setConstraints(response.data.data); + const data = response.data.data; + setConstraints({ + primaryKey: data.primaryKey ?? { name: "", columns: [] }, + indexes: data.indexes ?? [], + }); } } catch (error) { console.error("제약조건 로드 실패:", error); @@ -950,8 +954,8 @@ export default function TableManagementPage() { const filteredTables = useMemo(() => { const filtered = tables.filter( (table) => - table.table_name.toLowerCase().includes(searchTerm.toLowerCase()) || - table.display_name.toLowerCase().includes(searchTerm.toLowerCase()), + (table.table_name ?? '').toLowerCase().includes(searchTerm.toLowerCase()) || + (table.display_name ?? '').toLowerCase().includes(searchTerm.toLowerCase()), ); const isKorean = (str: string) => /^[가-힣ㄱ-ㅎ]/.test(str); return filtered.sort((a, b) => { @@ -1001,7 +1005,7 @@ export default function TableManagementPage() { // PK 체크박스 변경 핸들러 const handlePkToggle = useCallback( (columnName: string, checked: boolean) => { - const currentPkCols = [...constraints.primaryKey.columns]; + const currentPkCols = [...(constraints.primaryKey?.columns ?? [])]; let newPkCols: string[]; if (checked) { newPkCols = [...currentPkCols, columnName]; @@ -1012,7 +1016,7 @@ export default function TableManagementPage() { setPendingPkColumns(newPkCols); setPkDialogOpen(true); }, - [constraints.primaryKey.columns], + [constraints.primaryKey?.columns], ); // PK 변경 확인 @@ -1071,7 +1075,7 @@ export default function TableManagementPage() { // 컬럼별 인덱스 상태 헬퍼 const getColumnIndexState = useCallback( (columnName: string) => { - const isPk = constraints.primaryKey.columns.includes(columnName); + const isPk = (constraints.primaryKey?.columns ?? []).includes(columnName); const hasIndex = constraints.indexes.some( (idx) => !idx.is_unique && idx.columns.length === 1 && idx.columns[0] === columnName, ); diff --git a/frontend/app/globals.css b/frontend/app/globals.css index e693c00a..41caacde 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -5,6 +5,9 @@ @import "tailwindcss"; @import "tw-animate-css"; +/* ===== V5 Cosmic Layout System ===== */ +@import "../styles/v5-layout.css"; + /* ===== Dark Mode Variant ===== */ @custom-variant dark (&:is(.dark *)); diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index e1e4621d..b124e24b 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -35,6 +35,8 @@ import { TabBar } from "./TabBar"; import { TabContent } from "./TabContent"; import { useTabStore } from "@/stores/tabStore"; import { ThemeToggle } from "./ThemeToggle"; +import { CosmicBackground } from "./CosmicBackground"; +import { useTheme } from "next-themes"; import { DropdownMenu, DropdownMenuContent, @@ -235,10 +237,31 @@ function AppLayoutInner({ children }: AppLayoutProps) { const { user, logout, refreshUserData } = useAuth(); const { user_menus: userMenus, admin_menus: adminMenus, loading, refreshMenus } = useMenu(); const [sidebarOpen, setSidebarOpen] = useState(true); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [tabsCollapsed, setTabsCollapsed] = useState(false); + const [flyoutMenu, setFlyoutMenu] = useState<{ menu: any; rect: DOMRect } | null>(null); const [expandedMenus, setExpandedMenus] = useState>(new Set()); const [isMobile, setIsMobile] = useState(false); const [showCompanySwitcher, setShowCompanySwitcher] = useState(false); const [currentCompanyName, setCurrentCompanyName] = useState(""); + const { theme, setTheme: rawSetTheme } = useTheme(); + + const setNextTheme = useCallback((t: string) => { + if (theme === t) return; + const fade = document.getElementById("v5-theme-fade"); + if (fade) { + fade.style.background = t === "dark" + ? "radial-gradient(ellipse at center,#0c0b18,#06050e)" + : "radial-gradient(ellipse at center,#f3f2fa,#fafaff)"; + fade.classList.add("in"); + setTimeout(() => { + rawSetTheme(t); + setTimeout(() => fade.classList.remove("in"), 50); + }, 420); + } else { + rawSetTheme(t); + } + }, [theme, rawSetTheme]); // URL 직접 접근 시 탭 자동 열기 useEffect(() => { @@ -441,9 +464,32 @@ function AppLayoutInner({ children }: AppLayoutProps) { toast.warning("이 메뉴에 할당된 화면이 없습니다. 메뉴 설정을 확인해주세요."); }; - const handleModeSwitch = () => { - setTabMode(isAdminMode ? "user" : "admin"); - }; + const handleModeSwitch = useCallback(() => { + const modeFade = document.getElementById("v5-mode-fade"); + const sidebar = document.querySelector(".v5-side") as HTMLElement; + const tabBar = document.querySelector(".v5-tabs") as HTMLElement; + + // Phase 1: overlay flash + slide out + modeFade?.classList.add("in"); + sidebar?.classList.add("slide-out"); + tabBar?.classList.add("fade-out"); + + setTimeout(() => { + // Phase 2: swap mode + setTabMode(isAdminMode ? "user" : "admin"); + sidebar?.classList.remove("slide-out"); + sidebar?.classList.add("slide-in"); + tabBar?.classList.remove("fade-out"); + tabBar?.classList.add("fade-in"); + + // Phase 3: cleanup + setTimeout(() => { + modeFade?.classList.remove("in"); + sidebar?.classList.remove("slide-in"); + tabBar?.classList.remove("fade-in"); + }, 400); + }, 300); + }, [isAdminMode, setTabMode]); const handleLogout = async () => { try { @@ -512,58 +558,92 @@ function AppLayoutInner({ children }: AppLayoutProps) { [pathname, activeTab], ); - // 메뉴 트리 렌더링 (기존 MainLayout 스타일 적용) + // 접힌 사이드바에서 부모 메뉴 클릭 → 플라이아웃 + const handleCollapsedMenuClick = useCallback((menu: any, e: React.MouseEvent) => { + if (!sidebarCollapsed || !menu.hasChildren) return false; + e.stopPropagation(); + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + setFlyoutMenu((prev) => prev?.menu.id === menu.id ? null : { menu, rect }); + return true; + }, [sidebarCollapsed]); + + // 플라이아웃에서 메뉴 선택 + const handleFlyoutSelect = useCallback((child: any) => { + setFlyoutMenu(null); + handleMenuClick(child); + }, []); + + // 바깥 클릭 시 플라이아웃 닫기 + useEffect(() => { + if (!flyoutMenu) return; + const close = (e: MouseEvent) => { + if (!(e.target as HTMLElement).closest(".v5-side-flyout") && !(e.target as HTMLElement).closest(".v5-si")) { + setFlyoutMenu(null); + } + }; + document.addEventListener("click", close); + return () => document.removeEventListener("click", close); + }, [flyoutMenu]); + + // 메뉴 트리 렌더링 (v5 glassmorphism) const renderMenu = (menu: any, level: number = 0) => { const isExpanded = expandedMenus.has(menu.id); const isLeaf = !menu.hasChildren; return ( -
+
handleMenuDragStart(e, menu)} - className={`group flex min-h-[44px] cursor-pointer items-center justify-between rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150 ease-in-out sm:min-h-[40px] ${ - isMenuActive(menu) - ? "border-l-[3px] border-l-primary bg-primary/10 dark:bg-primary/15 text-primary font-semibold" - : isExpanded - ? "bg-accent/60 text-foreground" - : "text-muted-foreground hover:bg-accent hover:text-foreground" - } ${level > 0 ? "ml-6" : ""}`} - onClick={() => handleMenuClick(menu)} + className={`v5-si ${isMenuActive(menu) ? "on" : ""} ${level > 0 ? "ml-6" : ""}`} + title={menu.name} + onClick={(e) => { + if (handleCollapsedMenuClick(menu, e)) return; + handleMenuClick(menu); + }} > -
- {menu.icon} - - {menu.name} + {menu.icon} + {menu.name} + {menu.hasChildren && !sidebarCollapsed && ( + + {isExpanded ? : } -
- {menu.hasChildren && ( -
- {isExpanded ? : } -
)}
- {menu.hasChildren && isExpanded && ( -
+ {/* 플라이아웃 (접힌 상태에서만) */} + {sidebarCollapsed && flyoutMenu?.menu.id === menu.id && menu.hasChildren && ( +
+
{menu.name}
+ {menu.children?.map((child: any) => ( +
handleFlyoutSelect(child)} + > + {child.icon} + {child.name} +
+ ))} +
+ )} + + {/* 펼친 상태의 하위 메뉴 */} + {!sidebarCollapsed && menu.hasChildren && isExpanded && ( +
{menu.children?.map((child: any) => (
handleMenuDragStart(e, child)} - className={`flex min-h-[44px] cursor-pointer items-center rounded-md px-3 py-2 text-sm transition-colors duration-150 hover:cursor-pointer sm:min-h-[40px] ${ - isMenuActive(child) - ? "border-l-[3px] border-l-primary bg-primary/10 dark:bg-primary/15 text-primary font-semibold" - : "text-muted-foreground hover:bg-accent hover:text-foreground" - }`} + className={`v5-si ${isMenuActive(child) ? "on" : ""}`} onClick={() => handleMenuClick(child)} > -
- {child.icon} - - {child.name} - -
+ {child.icon} + {child.name}
))}
@@ -613,246 +693,182 @@ function AppLayoutInner({ children }: AppLayoutProps) { ); } + // Admin permission check + const isAdmin = + (user as ExtendedUserInfo)?.isAdmin || + (user as ExtendedUserInfo)?.user_type === "SUPER_ADMIN" || + (user as ExtendedUserInfo)?.user_type === "COMPANY_ADMIN" || + (user as ExtendedUserInfo)?.user_type === "ADMIN" || + (user as ExtendedUserInfo)?.user_type === "admin"; + + // Breadcrumb from active tab + const breadcrumbText = activeTab?.title || "대시보드"; + return ( -
- {/* 모바일 헤더 */} - {isMobile && ( -
-
- setSidebarOpen(!sidebarOpen)} /> - + <> + {/* Cosmic background */} + + + {/* Theme fade overlay */} +
+ + {/* Mode transition overlay */} +
+ + {/* V5 Shell */} +
+ {/* ===== Glass Header ===== */} +
+
+ {/* Mobile hamburger */} + +
INVION
+
+ {isAdminMode ? "관리자" : "홈"} › {breadcrumbText} +
+
+
+ 관리자 모드 +
- - - - - - -
-

{user.user_name || "사용자"}

-

- {user.dept_name || user.email || user.user_id} -

-
-
- - - - 프로필 - - router.push("/admin/approvalBox")}> - - 결재함 - - - - POP 모드 - - -
- -
- - - - 로그아웃 - -
-
-
- )} - - {/* 메인 컨테이너 */} -
- {sidebarOpen && isMobile && ( -
setSidebarOpen(false)} - /> - )} - - {/* 왼쪽 사이드바 */} - +
- {/* 가운데 컨텐츠 영역 - 탭 시스템 */} -
- - -
+ {/* ===== Tab Bar ===== */} + setTabsCollapsed(!tabsCollapsed)} /> + + {/* Mobile overlay */} + {sidebarOpen && isMobile && ( +
setSidebarOpen(false)} /> + )} + + {/* ===== Body (sidebar + content) ===== */} +
+ {/* Sidebar */} + + + {/* Content area */} +
+ +
+
-
+ ); } diff --git a/frontend/components/layout/CosmicBackground.tsx b/frontend/components/layout/CosmicBackground.tsx new file mode 100644 index 00000000..1dd9a54e --- /dev/null +++ b/frontend/components/layout/CosmicBackground.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +export function CosmicBackground() { + const cosmosRef = useRef(null); + + useEffect(() => { + const co = cosmosRef.current; + if (!co) return; + + // Stars + const starColors = ["rgba(162,155,254,.8)", "rgba(85,239,196,.7)", "rgba(253,121,168,.7)"]; + for (let i = 0; i < 150; i++) { + const s = document.createElement("div"); + s.className = "star" + (Math.random() > 0.83 ? " c" : ""); + if (s.classList.contains("c")) + s.style.setProperty("--sc", starColors[(Math.random() * 3) | 0]); + s.style.left = Math.random() * 100 + "%"; + s.style.top = Math.random() * 100 + "%"; + s.style.setProperty("--d", 2 + Math.random() * 5 + "s"); + s.style.setProperty("--dl", Math.random() * 5 + "s"); + s.style.setProperty("--mo", (0.3 + Math.random() * 0.7).toString()); + co.appendChild(s); + } + + // Particles + const particleColors = ["var(--v5-primary)", "var(--v5-cyan)", "var(--v5-pink)"]; + for (let i = 0; i < 20; i++) { + const p = document.createElement("div"); + p.className = "particle"; + p.style.left = Math.random() * 100 + "%"; + p.style.setProperty("--sz", 2 + Math.random() * 4 + "px"); + p.style.setProperty("--pc", particleColors[(Math.random() * 3) | 0]); + p.style.setProperty("--fd", 7 + Math.random() * 12 + "s"); + p.style.setProperty("--fdl", Math.random() * 10 + "s"); + co.appendChild(p); + } + + return () => { + co.querySelectorAll(".star, .particle").forEach((el) => el.remove()); + }; + }, []); + + return ( +
+
+
+
+
+
+
+
+ ); +} diff --git a/frontend/components/layout/TabBar.tsx b/frontend/components/layout/TabBar.tsx index 5a436a2c..d056f37c 100644 --- a/frontend/components/layout/TabBar.tsx +++ b/frontend/components/layout/TabBar.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useRef, useState, useEffect, useLayoutEffect, useCallback } from "react"; -import { X, RotateCw, ChevronDown } from "lucide-react"; +import { X, RotateCw, ChevronDown, ChevronUp } from "lucide-react"; import { useTabStore, selectTabs, selectActiveTabId, Tab } from "@/stores/tabStore"; import { menuScreenApi } from "@/lib/api/screen"; import { @@ -41,7 +41,12 @@ interface DropGhost { tabCountAtCreation: number; } -export function TabBar() { +interface TabBarProps { + collapsed?: boolean; + onToggleCollapse?: () => void; +} + +export function TabBar({ collapsed = false, onToggleCollapse }: TabBarProps) { const tabs = useTabStore(selectTabs); const activeTabId = useTabStore(selectActiveTabId); const { @@ -491,10 +496,8 @@ export function TabBar() { onLostPointerCapture={handleLostPointerCapture} onContextMenu={(e) => handleContextMenu(e, tab.id)} className={cn( - "group relative flex h-7 shrink-0 cursor-pointer items-center gap-0.5 rounded-t-md border border-b-0 px-3 select-none", - isActive - ? "text-primary z-10 -mb-px h-[30px] bg-primary/15 dark:bg-primary/20 border-primary/40 border-t-[3px] border-t-primary font-semibold" - : "bg-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground border-transparent", + "v5-tab group relative flex shrink-0 cursor-pointer items-center gap-1 px-3 select-none", + isActive && "on", )} style={{ width: TAB_WIDTH, @@ -505,7 +508,7 @@ export function TabBar() { }} title={tab.title} > - {tab.title} + {tab.title}
{isActive && ( @@ -524,10 +527,7 @@ export function TabBar() { e.stopPropagation(); closeTab(tab.id); }} - className={cn( - "text-muted-foreground hover:bg-destructive/10 hover:text-destructive flex h-4 w-4 items-center justify-center rounded-sm transition-colors", - !isActive && "opacity-0 group-hover:opacity-100", - )} + className="v5-tab-x" > @@ -542,13 +542,16 @@ export function TabBar() { <>
-
-
+ {onToggleCollapse && ( + + )} {displayVisible.map((tab, i) => renderTab(tab, i))} {hasOverflow && ( diff --git a/frontend/invion-layout-v5.html b/frontend/invion-layout-v5.html new file mode 100644 index 00000000..0e7a7c4b --- /dev/null +++ b/frontend/invion-layout-v5.html @@ -0,0 +1,973 @@ + + + + + +INVION - Layout v5 + + + + +
INVION v5 — LAYOUT SHELL
+
+
+ + +
+
+
+
+
+ + +
+ +
+
+ + + +
관리자 모드
+
+
+
+ + +
+
+ +
4
+
+
+
+ +
+
+ 알림 + +
+
+
DTG-078 통신 오류 발생
2분 전
+
3월 정산 보고서 검토 요청
14분 전
+
김대리가 DTG-003 설치 완료
30분 전
+
시스템 자동 백업 완료 (2.3GB)
1시간 전
+
최부장이 결재 3건 승인
2시간 전
+
+
+
+ +
+
P
+
+
+
P
+
Park님
park@invion.com
+
+
내 정보
+
비밀번호 변경
+
환경설정
+
+
로그아웃
+
+
+
+
+ + +
+ +
대시보드
+
DTG 관리
+
사용자 관리
+
정산
+
+ + + + + +
+ + +
+ + + + + + + +
+
+
+
+
콘텐츠 영역
+
이 영역에 대시보드, 관리 페이지 등 실제 콘텐츠가 렌더링됩니다
+
+
+
+
+
+ + + + diff --git a/frontend/invion-preview-v5.html b/frontend/invion-preview-v5.html new file mode 100644 index 00000000..80cc7429 --- /dev/null +++ b/frontend/invion-preview-v5.html @@ -0,0 +1,1049 @@ + + + + + +INVION - Preview v5 + + + + +
INVION v5 — COSMIC COMMAND CENTER
+
+ + + +
+
+
+
+
+ + +
+
+
+ + +
+
+ +
+ + + + +
로그인 성공
Park님 환영합니다
+ + + + diff --git a/frontend/styles/v5-layout.css b/frontend/styles/v5-layout.css new file mode 100644 index 00000000..825c766f --- /dev/null +++ b/frontend/styles/v5-layout.css @@ -0,0 +1,465 @@ +/* =================================================================== + INVION v5 — Cosmic Glassmorphism Layout System + All variables use --v5- prefix to avoid shadcn/Tailwind collision. + =================================================================== */ + +/* ===== V5 CSS Variables ===== */ +:root { + --v5-bg:#fafaff; --v5-bg-subtle:#f3f2fa; + --v5-surface:rgba(255,255,255,0.55); --v5-surface-solid:#ffffff; + --v5-surface-hover:rgba(255,255,255,0.7); + --v5-text:#0f0e1a; --v5-text-sec:#6b6a80; --v5-text-muted:#9998ad; + --v5-primary:#6c5ce7; --v5-primary-light:#a29bfe; --v5-primary-glow:rgba(108,92,231,0.25); + --v5-cyan:#00cec9; --v5-cyan-glow:rgba(0,206,201,0.2); + --v5-pink:#fd79a8; --v5-pink-glow:rgba(253,121,168,0.15); + --v5-red:#ff4757; --v5-green:#00b894; --v5-amber:#fdcb6e; + --v5-border:rgba(108,92,231,0.12); --v5-border-subtle:rgba(0,0,0,0.05); + --v5-glass:rgba(255,255,255,0.45); --v5-glass-strong:rgba(255,255,255,0.65); + --v5-glass-border:rgba(108,92,231,0.12); + --v5-glow-sm:0 0 20px rgba(108,92,231,0.12); + --v5-glow-md:0 0 40px rgba(108,92,231,0.2); + --v5-glow-lg:0 0 80px rgba(108,92,231,0.25); + --v5-sidebar-w:220px; +} +.dark { + --v5-bg:#06050e; --v5-bg-subtle:#0c0b18; + --v5-surface:rgba(17,16,42,0.5); --v5-surface-solid:#11102a; + --v5-surface-hover:rgba(25,24,64,0.6); + --v5-text:#eae8f4; --v5-text-sec:#8d8ba8; --v5-text-muted:#5a587a; + --v5-primary:#a29bfe; --v5-primary-light:#c8c4ff; --v5-primary-glow:rgba(162,155,254,0.25); + --v5-cyan:#55efc4; --v5-cyan-glow:rgba(85,239,196,0.15); + --v5-pink:#fd79a8; --v5-red:#ff6b6b; --v5-green:#55efc4; --v5-amber:#ffeaa7; + --v5-border:rgba(162,155,254,0.1); --v5-border-subtle:rgba(255,255,255,0.04); + --v5-glass:rgba(17,16,42,0.45); --v5-glass-strong:rgba(17,16,42,0.65); + --v5-glass-border:rgba(162,155,254,0.12); + --v5-glow-sm:0 0 20px rgba(162,155,254,0.1); + --v5-glow-md:0 0 40px rgba(162,155,254,0.18); + --v5-glow-lg:0 0 80px rgba(162,155,254,0.22); +} + +/* ===== COSMIC BACKGROUND ===== */ +.v5-cosmos{position:fixed;inset:0;pointer-events:none;z-index:0;overflow:hidden;} +.v5-cosmos .star{position:absolute;width:2px;height:2px;background:white;border-radius:50%; + animation:v5-twinkle var(--d,3s) ease-in-out infinite alternate;animation-delay:var(--dl,0s);opacity:0;} +.v5-cosmos .star.c{width:3px;height:3px;background:var(--sc);} +@keyframes v5-twinkle{0%{opacity:0;transform:scale(.5)}100%{opacity:var(--mo,.7);transform:scale(1)}} +html:not(.dark) .v5-cosmos .star{display:none;} +html:not(.dark) .v5-cosmos .shooting-star{display:none;} +html:not(.dark) .v5-cosmos .particle{display:none;} + +.v5-cosmos .neb{position:absolute;border-radius:50%;filter:blur(140px);animation:v5-drift 16s ease-in-out infinite alternate;} +.v5-cosmos .neb-1{width:700px;height:700px;top:-20%;right:-15%;background:radial-gradient(circle,var(--v5-primary-glow),transparent 70%);animation-duration:18s;} +.v5-cosmos .neb-2{width:600px;height:600px;bottom:-25%;left:-10%;background:radial-gradient(circle,var(--v5-cyan-glow),transparent 70%);animation-duration:14s;animation-delay:-4s;} +.v5-cosmos .neb-3{width:450px;height:450px;top:35%;left:40%;background:radial-gradient(circle,var(--v5-pink-glow),transparent 70%);animation-duration:12s;animation-delay:-8s;} +.v5-cosmos .neb-4{width:350px;height:350px;top:60%;right:25%;background:radial-gradient(circle,rgba(108,92,231,0.08),transparent 70%);animation-duration:20s;animation-delay:-2s;} +@keyframes v5-drift{0%{transform:translate(0,0) scale(1)}100%{transform:translate(30px,-25px) scale(1.1)}} + +html:not(.dark) .v5-cosmos{background:linear-gradient(180deg,#e8e4ff 0%,#f0edff 30%,#fafaff 60%,#f5f0ff 100%);} +html:not(.dark) .v5-cosmos .neb{filter:blur(100px);} +html:not(.dark) .v5-cosmos .neb-1{width:1200px;height:500px;top:auto;bottom:-10%;right:-15%;border-radius:50%; + background:radial-gradient(ellipse,rgba(255,255,255,0.9),rgba(230,225,255,0.5),transparent 70%);animation-duration:25s;} +html:not(.dark) .v5-cosmos .neb-2{width:1000px;height:400px;top:auto;bottom:-5%;left:-10%; + background:radial-gradient(ellipse,rgba(255,255,255,0.85),rgba(200,240,255,0.4),transparent 70%);animation-duration:20s;} +html:not(.dark) .v5-cosmos .neb-3{width:800px;height:350px;top:auto;bottom:5%;left:30%; + background:radial-gradient(ellipse,rgba(255,255,255,0.8),rgba(240,220,255,0.3),transparent 70%);animation-duration:22s;} +html:not(.dark) .v5-cosmos .neb-4{width:600px;height:600px;top:-10%;right:20%;bottom:auto; + background:radial-gradient(circle,rgba(108,92,231,0.08),rgba(0,206,201,0.04),transparent 70%);} + +.v5-cosmos .shooting-star{position:absolute;width:80px;height:1px; + background:linear-gradient(90deg,rgba(255,255,255,0.6),transparent); + transform:rotate(35deg);animation:v5-shoot 5s ease-in-out infinite;opacity:0;} +.v5-cosmos .shooting-star:nth-child(6){animation-delay:3s;top:25%;left:65%;width:55px;transform:rotate(40deg);} +@keyframes v5-shoot{0%{opacity:0;transform:rotate(35deg) translateX(0)}3%{opacity:0.7}12%{opacity:0;transform:rotate(35deg) translateX(-350px)}100%{opacity:0}} + +.v5-cosmos .particle{position:absolute;width:var(--sz,4px);height:var(--sz,4px);background:var(--pc,var(--v5-primary)); + border-radius:50%;opacity:0;animation:v5-floatup var(--fd,9s) ease-in-out infinite;animation-delay:var(--fdl,0s);} +@keyframes v5-floatup{0%{opacity:0;transform:translateY(100vh) scale(0)}10%{opacity:.4}90%{opacity:.4}100%{opacity:0;transform:translateY(-80px) scale(1)}} + +/* ===== LAYOUT SHELL ===== */ +.v5-shell{display:flex;flex-direction:column;height:100vh;position:relative;z-index:1;} + +/* ===== GLASS HEADER ===== */ +.v5-hdr{height:50px;display:flex;align-items:center;justify-content:space-between;padding:0 1.25rem; + background:var(--v5-glass);backdrop-filter:blur(20px) saturate(1.4);-webkit-backdrop-filter:blur(20px) saturate(1.4); + border-bottom:1px solid var(--v5-glass-border);position:relative;z-index:20;flex-shrink:0;} +.v5-hdr-l{display:flex;align-items:center;gap:1rem;} +.v5-hdr-logo{font-size:1.05rem;font-weight:900;letter-spacing:-.03em; + background:linear-gradient(135deg,var(--v5-primary),var(--v5-cyan));-webkit-background-clip:text; + -webkit-text-fill-color:transparent;background-clip:text;cursor:default;} +.v5-hdr-bc{font-size:.72rem;color:var(--v5-text-muted);} +.v5-hdr-bc b{color:var(--v5-text);font-weight:600;} +.v5-hdr-r{display:flex;align-items:center;gap:.65rem;} + +/* Theme pill */ +.v5-pill{display:flex;background:var(--v5-surface);backdrop-filter:blur(8px);border:1px solid var(--v5-glass-border); + border-radius:999px;padding:2px;} +.v5-pill button{padding:.22rem .65rem;border-radius:999px;border:none;background:transparent; + color:var(--v5-text-muted);cursor:pointer;font-size:.6rem;font-weight:600;font-family:inherit; + transition:all .3s cubic-bezier(.4,0,.2,1);} +.v5-pill button.on{background:var(--v5-primary);color:white;box-shadow:var(--v5-glow-sm);} + +/* Bell */ +.v5-bell{position:relative;width:32px;height:32px;border-radius:10px;border:1px solid var(--v5-glass-border); + background:var(--v5-surface);backdrop-filter:blur(8px);color:var(--v5-text-muted);cursor:pointer; + display:flex;align-items:center;justify-content:center;transition:all .2s;} +.v5-bell:hover{border-color:var(--v5-primary);color:var(--v5-primary);box-shadow:var(--v5-glow-sm);} +.v5-bell-dot{position:absolute;top:5px;right:5px;width:7px;height:7px;background:var(--v5-red);border-radius:50%;animation:v5-pdot 2s infinite;} +@keyframes v5-pdot{0%,100%{box-shadow:0 0 0 0 rgba(255,71,87,.4)}50%{box-shadow:0 0 0 5px rgba(255,71,87,0)}} + +/* Admin button */ +.v5-admin-btn{position:relative;width:32px;height:32px;border-radius:10px;border:1px solid var(--v5-glass-border); + background:var(--v5-surface);backdrop-filter:blur(8px);color:var(--v5-text-muted);cursor:pointer; + display:flex;align-items:center;justify-content:center;transition:all .25s;} +.v5-admin-btn:hover{border-color:var(--v5-primary);color:var(--v5-primary);box-shadow:var(--v5-glow-sm);transform:scale(1.1);} +.v5-admin-btn .v5-admin-label{position:absolute;top:110%;left:50%;transform:translateX(-50%); + font-size:.52rem;font-weight:600;color:var(--v5-primary);white-space:nowrap; + opacity:0;transition:opacity .2s,color .2s;pointer-events:none;} +.v5-admin-btn:hover .v5-admin-label{opacity:1;} +.v5-admin-btn .ic-gear{display:block;} +.v5-admin-btn .ic-home{display:none;} +.v5-admin-mode .v5-admin-btn .ic-gear{display:none;} +.v5-admin-mode .v5-admin-btn .ic-home{display:block;} +.v5-admin-mode .v5-admin-btn{border-color:var(--v5-cyan);color:var(--v5-cyan);background:rgba(0,206,201,.08);box-shadow:0 0 15px var(--v5-cyan-glow);} +.v5-admin-mode .v5-admin-btn:hover{color:var(--v5-cyan);border-color:var(--v5-cyan);} +.v5-admin-mode .v5-admin-btn .v5-admin-label{color:var(--v5-cyan);} + +/* Avatar */ +.v5-avatar-w{position:relative;} +.v5-avatar{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--v5-primary),var(--v5-cyan)); + display:flex;align-items:center;justify-content:center;font-size:.7rem;font-weight:700;color:white; + cursor:pointer;transition:transform .2s,box-shadow .3s;} +.v5-avatar:hover{transform:scale(1.1);box-shadow:var(--v5-glow-sm);} + +/* Avatar dropdown */ +.v5-avatar-dd{position:absolute;top:calc(100% + 10px);right:0;width:220px; + background:var(--v5-glass-strong);backdrop-filter:blur(20px) saturate(1.4);-webkit-backdrop-filter:blur(20px) saturate(1.4); + border:1px solid var(--v5-glass-border);border-radius:16px;padding:.5rem; + box-shadow:0 12px 40px rgba(0,0,0,0.12),var(--v5-glow-sm); + opacity:0;transform:translateY(-8px) scale(.96);pointer-events:none; + transition:all .25s cubic-bezier(.16,1,.3,1);z-index:100;} +.dark .v5-avatar-dd{box-shadow:0 12px 40px rgba(0,0,0,0.5),var(--v5-glow-md);} +.v5-avatar-dd.open{opacity:1;transform:none;pointer-events:auto;} +.v5-avatar-dd .av-profile{display:flex;align-items:center;gap:.6rem;padding:.55rem .6rem; + border-bottom:1px solid var(--v5-border-subtle);margin-bottom:.35rem;} +.v5-avatar-dd .av-avatar{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--v5-primary),var(--v5-cyan)); + display:flex;align-items:center;justify-content:center;font-size:.8rem;font-weight:700;color:white;flex-shrink:0;} +.v5-avatar-dd .av-name{font-size:.78rem;font-weight:700;color:var(--v5-text);} +.v5-avatar-dd .av-email{font-size:.6rem;color:var(--v5-text-muted);margin-top:.1rem;} +.v5-avatar-dd .av-item{display:flex;align-items:center;gap:.5rem;padding:.45rem .6rem; + border-radius:10px;font-size:.72rem;font-weight:500;color:var(--v5-text-sec); + cursor:pointer;transition:all .15s;} +.v5-avatar-dd .av-item:hover{background:var(--v5-surface-hover);color:var(--v5-text);transform:translateX(2px);} +.v5-avatar-dd .av-item .av-ic{width:16px;height:16px;display:flex;align-items:center;justify-content:center;opacity:.6;flex-shrink:0;} +.v5-avatar-dd .av-item:hover .av-ic{opacity:1;} +.v5-avatar-dd .av-divider{height:1px;background:var(--v5-border-subtle);margin:.3rem .6rem;} +.v5-avatar-dd .av-item.danger{color:var(--v5-red);} +.v5-avatar-dd .av-item.danger:hover{background:rgba(255,71,87,.08);} + +/* Notification panel */ +.v5-noti-panel{position:absolute;top:calc(100% + 10px);right:0;width:300px;max-height:400px; + background:var(--v5-glass-strong);backdrop-filter:blur(20px) saturate(1.4);-webkit-backdrop-filter:blur(20px) saturate(1.4); + border:1px solid var(--v5-glass-border);border-radius:16px; + box-shadow:0 12px 40px rgba(0,0,0,0.12),var(--v5-glow-sm); + opacity:0;transform:translateY(-8px) scale(.96);pointer-events:none; + transition:all .25s cubic-bezier(.16,1,.3,1);z-index:100;overflow:hidden;} +.dark .v5-noti-panel{box-shadow:0 12px 40px rgba(0,0,0,0.5),var(--v5-glow-md);} +.v5-noti-panel.open{opacity:1;transform:none;pointer-events:auto;} +.v5-noti-head{display:flex;align-items:center;justify-content:space-between;padding:.7rem .85rem; + border-bottom:1px solid var(--v5-border-subtle);} +.v5-noti-head .noti-title{font-size:.78rem;font-weight:700;color:var(--v5-text);} +.v5-noti-head .noti-clear{font-size:.58rem;font-weight:600;color:var(--v5-primary);cursor:pointer; + border:none;background:none;font-family:inherit;transition:color .2s;} +.v5-noti-head .noti-clear:hover{color:var(--v5-cyan);} +.v5-noti-list{overflow-y:auto;max-height:330px;padding:.35rem;} +.v5-noti-item{display:flex;gap:.55rem;padding:.55rem .5rem;border-radius:10px;cursor:pointer;transition:all .15s;} +.v5-noti-item:hover{background:var(--v5-surface-hover);} +.v5-noti-item.unread{background:linear-gradient(135deg,rgba(108,92,231,.05),rgba(108,92,231,.02));} +.dark .v5-noti-item.unread{background:linear-gradient(135deg,rgba(162,155,254,.06),rgba(162,155,254,.02));} +.v5-noti-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;margin-top:.3rem;} +.v5-noti-dot.info{background:var(--v5-primary);} +.v5-noti-dot.warn{background:var(--v5-amber);} +.v5-noti-dot.err{background:var(--v5-red);box-shadow:0 0 6px rgba(255,71,87,.4);} +.v5-noti-dot.ok{background:var(--v5-green);} +.v5-noti-body{flex:1;min-width:0;} +.v5-noti-msg{font-size:.7rem;font-weight:500;color:var(--v5-text);line-height:1.35;} +.v5-noti-msg b{font-weight:600;} +.v5-noti-time{font-size:.55rem;color:var(--v5-text-muted);margin-top:.15rem;} + +/* Admin mode header accent */ +.v5-admin-mode .v5-hdr{border-bottom-color:var(--v5-primary);} +.v5-admin-mode .v5-hdr::after{content:'';position:absolute;bottom:-1px;left:0;right:0;height:2px; + background:linear-gradient(90deg,var(--v5-primary),var(--v5-cyan));animation:v5-glowLine .6s ease-out both;} +@keyframes v5-glowLine{from{transform:scaleX(0);opacity:0}to{transform:scaleX(1);opacity:1}} + +/* Admin badge */ +.v5-admin-badge{display:none;align-items:center;gap:.4rem;padding:.2rem .6rem;border-radius:999px; + background:linear-gradient(135deg,rgba(108,92,231,.12),rgba(0,206,201,.08)); + border:1px solid rgba(108,92,231,.2);font-size:.58rem;font-weight:700;color:var(--v5-primary); + animation:v5-badgeIn .4s cubic-bezier(.16,1,.3,1) both;} +.dark .v5-admin-badge{background:linear-gradient(135deg,rgba(162,155,254,.12),rgba(85,239,196,.08)); + border-color:rgba(162,155,254,.2);color:var(--v5-primary-light);} +.v5-admin-mode .v5-admin-badge{display:flex;} +@keyframes v5-badgeIn{from{opacity:0;transform:scale(.8) translateX(-10px)}to{opacity:1;transform:none}} +.v5-admin-badge .badge-dot{width:6px;height:6px;border-radius:50%;background:var(--v5-cyan); + box-shadow:0 0 8px var(--v5-cyan-glow);animation:v5-bdPulse 2s infinite;} +@keyframes v5-bdPulse{0%,100%{box-shadow:0 0 4px var(--v5-cyan-glow)}50%{box-shadow:0 0 12px var(--v5-cyan-glow)}} + +/* ===== GLASS TABS ===== */ +.v5-tabs{height:36px;display:flex;align-items:stretch;padding:0 .5rem;gap:1px;overflow-x:auto; + background:var(--v5-glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px); + border-bottom:1px solid var(--v5-glass-border);position:relative;z-index:15;flex-shrink:0;} +.v5-tab{display:flex;align-items:center;gap:.4rem;padding:0 .85rem;font-size:.7rem;font-weight:500; + color:var(--v5-text-muted);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;transition:all .25s;} +.v5-tab:hover{color:var(--v5-text-sec);background:var(--v5-surface-hover);} +.v5-tab.on{color:var(--v5-primary);font-weight:600;border-bottom-color:var(--v5-primary);background:var(--v5-surface);} +.v5-tab-x{width:14px;height:14px;border-radius:3px;border:none;background:transparent;color:var(--v5-text-muted); + font-size:.6rem;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:0;transition:all .15s;} +.v5-tab:hover .v5-tab-x{opacity:1;} +.v5-tab-x:hover{background:rgba(255,71,87,.15);color:var(--v5-red);} + +/* Tab collapse */ +.v5-tab-toggle{width:28px;height:28px;border-radius:8px;border:1px solid var(--v5-glass-border); + background:var(--v5-surface);backdrop-filter:blur(8px);color:var(--v5-text-muted);cursor:pointer; + display:flex;align-items:center;justify-content:center;transition:all .25s;flex-shrink:0;margin-right:.35rem;} +.v5-tab-toggle:hover{border-color:var(--v5-primary);color:var(--v5-primary);box-shadow:var(--v5-glow-sm);} +.v5-tab-toggle svg{transition:transform .3s cubic-bezier(.4,0,.2,1);} +.v5-tabs.collapsed .v5-tab-toggle svg{transform:rotate(180deg);} +.v5-tabs.collapsed{height:0;padding:0;border:none;overflow:hidden;transition:height .3s cubic-bezier(.4,0,.2,1),padding .3s,border-width .3s;} +.v5-tabs:not(.collapsed){transition:height .3s cubic-bezier(.16,1,.3,1),padding .3s;} + +/* Tab mini icon */ +.v5-tab-mini{position:relative;display:none;width:32px;height:32px;border-radius:10px;border:1px solid var(--v5-glass-border); + background:var(--v5-surface);backdrop-filter:blur(8px);color:var(--v5-text-muted);cursor:pointer; + align-items:center;justify-content:center;transition:all .25s;} +.v5-tab-mini:hover{border-color:var(--v5-primary);color:var(--v5-primary);box-shadow:var(--v5-glow-sm);} +.v5-tab-mini.visible{display:flex;animation:v5-miniIn .3s cubic-bezier(.16,1,.3,1) both;} +@keyframes v5-miniIn{from{opacity:0;transform:scale(.7)}to{opacity:1;transform:none}} +.v5-tab-mini .tab-count{position:absolute;top:3px;right:3px;width:14px;height:14px;border-radius:50%; + background:var(--v5-primary);color:white;font-size:.5rem;font-weight:700; + display:flex;align-items:center;justify-content:center;line-height:1;} + +/* Tab dropdown */ +.v5-tab-dropdown{position:absolute;top:calc(100% + 8px);right:0;width:220px; + background:var(--v5-glass-strong);backdrop-filter:blur(20px) saturate(1.4);-webkit-backdrop-filter:blur(20px) saturate(1.4); + border:1px solid var(--v5-glass-border);border-radius:14px;padding:.4rem; + box-shadow:0 12px 40px rgba(0,0,0,0.12),var(--v5-glow-sm); + opacity:0;transform:translateY(-8px) scale(.96);pointer-events:none; + transition:all .25s cubic-bezier(.16,1,.3,1);z-index:100;} +.dark .v5-tab-dropdown{box-shadow:0 12px 40px rgba(0,0,0,0.5),var(--v5-glow-md);} +.v5-tab-dropdown.open{opacity:1;transform:none;pointer-events:auto;} +.v5-tab-dropdown .td-item{display:flex;align-items:center;justify-content:space-between; + padding:.45rem .65rem;border-radius:10px;font-size:.72rem;font-weight:500; + color:var(--v5-text-sec);cursor:pointer;transition:all .15s;gap:.4rem;} +.v5-tab-dropdown .td-item:hover{background:var(--v5-surface-hover);color:var(--v5-text);transform:translateX(2px);} +.v5-tab-dropdown .td-item.on{color:var(--v5-primary);font-weight:600; + background:linear-gradient(135deg,rgba(108,92,231,.1),rgba(108,92,231,.04));} +.v5-tab-dropdown .td-close{width:16px;height:16px;border-radius:4px;border:none; + background:transparent;color:var(--v5-text-muted);cursor:pointer;display:flex; + align-items:center;justify-content:center;font-size:.55rem;opacity:0;transition:all .15s;flex-shrink:0;} +.v5-tab-dropdown .td-item:hover .td-close{opacity:1;} +.v5-tab-dropdown .td-close:hover{background:rgba(255,71,87,.12);color:var(--v5-red);} +.v5-tab-dropdown .td-head{display:flex;align-items:center;justify-content:space-between; + padding:.3rem .65rem .5rem;border-bottom:1px solid var(--v5-border-subtle);margin-bottom:.25rem;} +.v5-tab-dropdown .td-title{font-size:.6rem;font-weight:700;color:var(--v5-text-muted);text-transform:uppercase;letter-spacing:.08em;} +.v5-tab-dropdown .td-expand{font-size:.55rem;font-weight:600;color:var(--v5-primary);cursor:pointer; + border:none;background:none;font-family:inherit;transition:color .2s;} +.v5-tab-dropdown .td-expand:hover{color:var(--v5-cyan);} + +/* ===== GLASS SIDEBAR ===== */ +.v5-body{display:flex;flex:1;overflow:hidden;position:relative;z-index:5;} +.v5-side{width:var(--v5-sidebar-w);background:var(--v5-glass);backdrop-filter:blur(20px) saturate(1.3); + -webkit-backdrop-filter:blur(20px) saturate(1.3);border-right:1px solid var(--v5-glass-border); + padding:.85rem .6rem;overflow-y:auto;display:flex;flex-direction:column;gap:1px;flex-shrink:0; + transition:width .4s cubic-bezier(.4,0,.2,1),padding .4s,transform .35s cubic-bezier(.16,1,.3,1),opacity .25s;} + +.v5-side-sec{font-size:.55rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em; + color:var(--v5-text-muted);padding:1rem .65rem .35rem; + transition:opacity .3s,height .3s,padding .3s;} +.v5-side-sec:first-child{padding-top:.25rem;} + +.v5-si{padding:.5rem .7rem;border-radius:10px;font-size:.77rem;color:var(--v5-text-sec);cursor:pointer; + transition:all .25s cubic-bezier(.4,0,.2,1);font-weight:450;display:flex;align-items:center;gap:.6rem; + position:relative;overflow:hidden;height:auto;} +.v5-si .ic{width:16px;height:16px;display:flex;align-items:center;justify-content:center;opacity:.65;flex-shrink:0;} +.v5-si:hover{background:var(--v5-surface-hover);color:var(--v5-text);transform:translateX(2px);} +.v5-si.on{background:linear-gradient(135deg,rgba(108,92,231,.12),rgba(108,92,231,.05)); + color:var(--v5-primary);font-weight:600;border:1px solid rgba(108,92,231,.15);box-shadow:var(--v5-glow-sm);} +.v5-si.on .ic{opacity:1;} +.dark .v5-si.on{background:linear-gradient(135deg,rgba(162,155,254,.14),rgba(162,155,254,.05));border-color:rgba(162,155,254,.15);} +.v5-si::before{content:'';position:absolute;left:0;top:0;width:3px;height:100%;background:var(--v5-primary); + border-radius:0 2px 2px 0;transform:scaleY(0);transition:transform .2s cubic-bezier(.4,0,.2,1);} +.v5-si.on::before{transform:scaleY(1);} + +/* Sidebar enter animation */ +.v5-side-anim .v5-si{animation:v5-slideR .4s cubic-bezier(.16,1,.3,1) both;} +@keyframes v5-slideR{from{opacity:0;transform:translateX(-16px)}to{opacity:1;transform:none}} + +/* Sidebar toggle */ +.v5-side-toggle{margin-top:auto;padding:.5rem .7rem;border-radius:10px;border:none; + background:var(--v5-surface);backdrop-filter:blur(8px);color:var(--v5-text-muted);cursor:pointer; + display:flex;align-items:center;gap:.6rem;font-size:.7rem;font-weight:500;font-family:inherit; + transition:all .25s;flex-shrink:0;} +.v5-side-toggle:hover{background:var(--v5-surface-hover);color:var(--v5-primary);} +.v5-side-toggle svg{transition:transform .3s cubic-bezier(.4,0,.2,1);} + +/* ===== SIDEBAR COLLAPSE ===== */ +.v5-side.collapsed{width:56px;padding:.85rem .4rem;overflow:visible;z-index:30;} +.v5-side.collapsed .v5-si{justify-content:center;padding:.55rem;border-radius:10px;gap:0;} +.v5-side.collapsed .v5-si span:not(.ic){width:0;overflow:hidden;opacity:0;transition:width .25s,opacity .2s;position:absolute;} +.v5-side.collapsed .v5-si{position:relative;} +.v5-side.collapsed .v5-si:hover::after{content:attr(title);position:absolute;left:calc(100% + 10px);top:50%;transform:translateY(-50%); + background:var(--v5-glass-strong);backdrop-filter:blur(12px);border:1px solid var(--v5-glass-border); + padding:.3rem .6rem;border-radius:8px;font-size:.68rem;font-weight:500;color:var(--v5-text); + white-space:nowrap;z-index:100;box-shadow:0 4px 15px rgba(0,0,0,.1);pointer-events:none;} +.v5-side.collapsed .v5-si .ic{opacity:.7;margin:0;transition:opacity .2s;} +.v5-side.collapsed .v5-si.on .ic{opacity:1;} +.v5-side.collapsed .v5-si:hover{transform:none;} +.v5-side.collapsed .v5-side-sec{height:0;overflow:hidden;padding:0;margin:0;opacity:0;transition:all .25s;} +.v5-side:not(.collapsed) .v5-si span:not(.ic){opacity:1;transition:opacity .3s .15s;} +.v5-side:not(.collapsed) .v5-side-sec{opacity:1;transition:opacity .3s .1s,height .3s,padding .3s;} + +/* Category groups */ +.v5-side-group{display:contents;} +.v5-side.collapsed .v5-side-group{display:flex;flex-direction:column;gap:1px;position:relative;} + +/* Category header (collapsed only) */ +.v5-side-cat{height:0;overflow:hidden;opacity:0;padding:0;margin:0;pointer-events:none; + display:flex;flex-direction:column;align-items:center;justify-content:center; + border-radius:10px;cursor:pointer;position:relative;color:var(--v5-text-muted); + transition:height .25s,opacity .2s,padding .25s,margin .25s;} +.v5-side.collapsed .v5-side-cat{height:auto;overflow:visible;opacity:1;pointer-events:auto; + padding:.55rem;margin-top:.4rem;transition:height .3s .05s,opacity .3s .1s,padding .3s .05s,margin .3s .05s;} +.v5-side.collapsed .v5-side-cat:first-child{margin-top:0;} +.v5-side.collapsed .v5-side-cat:hover{background:var(--v5-surface-hover);color:var(--v5-primary);} +.v5-side.collapsed .v5-side-cat.open{background:linear-gradient(135deg,rgba(108,92,231,.1),rgba(108,92,231,.04));color:var(--v5-primary);} +.dark .v5-side.collapsed .v5-side-cat.open{background:linear-gradient(135deg,rgba(162,155,254,.1),rgba(162,155,254,.04));} +.v5-side-cat .cat-label{font-size:.48rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em; + margin-top:.15rem;text-align:center;line-height:1;} +.v5-side.collapsed .v5-side-cat{animation:v5-catIn .3s cubic-bezier(.16,1,.3,1) both;} +.v5-side.collapsed .v5-side-group:nth-child(1) .v5-side-cat{animation-delay:.05s;} +.v5-side.collapsed .v5-side-group:nth-child(2) .v5-side-cat{animation-delay:.1s;} +.v5-side.collapsed .v5-side-group:nth-child(3) .v5-side-cat{animation-delay:.15s;} +.v5-side.collapsed .v5-side-group:nth-child(4) .v5-side-cat{animation-delay:.2s;} +@keyframes v5-catIn{from{opacity:0;transform:scale(.7)}to{opacity:1;transform:none}} + +/* Hide items in collapsed (shown in flyout) */ +.v5-side.collapsed .v5-side-group .v5-si{height:0;padding:0;margin:0;overflow:hidden;opacity:0; + transition:height .25s,padding .25s,opacity .15s,margin .25s;} +.v5-side.collapsed .v5-side-group .v5-side-sec{height:0;padding:0;margin:0;overflow:hidden;opacity:0;} +.v5-side:not(.collapsed) .v5-side-group .v5-si{transition:height .3s .1s,padding .3s .1s,opacity .3s .15s,margin .3s .1s;} + +/* Hide child menus when collapsed */ +.v5-side.collapsed .v5-si-child{height:0;padding:0;margin:0;overflow:hidden;opacity:0;pointer-events:none;} +/* Hide tooltip when flyout is open */ +.v5-si:has(> .v5-side-flyout.open):hover::after{display:none !important;} + +/* Collapsed toggle */ +.v5-side.collapsed .v5-side-toggle span{width:0;overflow:hidden;opacity:0;transition:width .2s,opacity .15s;} +.v5-side.collapsed .v5-side-toggle{justify-content:center;padding:.55rem;} +.v5-side.collapsed .v5-side-toggle svg{transform:rotate(180deg);} +.v5-side:not(.collapsed) .v5-side-toggle span{opacity:1;transition:opacity .3s .2s;} + +/* Flyout panel */ +.v5-side-flyout{position:absolute;left:calc(100% + 8px);top:0;width:170px; + background:var(--v5-glass-strong);backdrop-filter:blur(20px) saturate(1.4);-webkit-backdrop-filter:blur(20px) saturate(1.4); + border:1px solid var(--v5-glass-border);border-radius:14px;padding:.4rem; + box-shadow:0 12px 40px rgba(0,0,0,0.15),var(--v5-glow-sm); + opacity:0;transform:translateX(-8px) scale(.96);pointer-events:none; + transition:all .25s cubic-bezier(.16,1,.3,1);z-index:100;} +.dark .v5-side-flyout{box-shadow:0 12px 40px rgba(0,0,0,0.5),var(--v5-glow-md);} +.v5-side-flyout.open{opacity:1;transform:none;pointer-events:auto;} +.v5-side-flyout .fly-title{font-size:.58rem;font-weight:700;color:var(--v5-text-muted); + text-transform:uppercase;letter-spacing:.08em;padding:.3rem .6rem .45rem;} +.v5-side-flyout .fly-item{display:flex;align-items:center;gap:.5rem;padding:.45rem .6rem; + border-radius:10px;font-size:.72rem;font-weight:500;color:var(--v5-text-sec); + cursor:pointer;transition:all .15s;} +.v5-side-flyout .fly-item:hover{background:var(--v5-surface-hover);color:var(--v5-text);transform:translateX(2px);} +.v5-side-flyout .fly-item.on{color:var(--v5-primary);font-weight:600; + background:linear-gradient(135deg,rgba(108,92,231,.1),rgba(108,92,231,.04));} +.v5-side-flyout .fly-item .ic{width:14px;height:14px;display:flex;align-items:center;justify-content:center;opacity:.6;flex-shrink:0;} +.v5-side-flyout .fly-item.on .ic{opacity:1;} + +/* Admin sidebar accent */ +.v5-admin-side .v5-si.on{background:linear-gradient(135deg,rgba(0,206,201,.12),rgba(0,206,201,.05)); + color:var(--v5-cyan);border-color:rgba(0,206,201,.2);} +.v5-admin-side .v5-si.on .ic{opacity:1;} +.v5-admin-side .v5-si::before{background:var(--v5-cyan);} +.dark .v5-admin-side .v5-si.on{background:linear-gradient(135deg,rgba(85,239,196,.12),rgba(85,239,196,.05)); + border-color:rgba(85,239,196,.15);} + +/* ===== MODE TRANSITION ===== */ +.v5-mode-fade{position:fixed;inset:0;z-index:9998;pointer-events:none;opacity:0; + background:radial-gradient(ellipse at center,var(--v5-primary-glow),transparent 70%); + transition:opacity .5s cubic-bezier(.4,0,.2,1);} +.v5-mode-fade.in{opacity:1;} + +.v5-side.slide-out{transform:translateX(-100%);opacity:0;} +.v5-side.slide-in{animation:v5-sideSlideIn .4s cubic-bezier(.16,1,.3,1) both;} +@keyframes v5-sideSlideIn{from{transform:translateX(-30px);opacity:0}to{transform:none;opacity:1}} +.v5-tabs.fade-out{opacity:0;} +.v5-tabs.fade-in{animation:v5-tabsFadeIn .3s ease-out both;} +@keyframes v5-tabsFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}} + +/* ===== THEME TRANSITION ===== */ +.v5-theme-fade{position:fixed;inset:0;z-index:9999;pointer-events:none;opacity:0; + transition:opacity .4s cubic-bezier(.4,0,.2,1);} +.v5-theme-fade.in{opacity:1;} + +/* ===== CONTENT PLACEHOLDER ===== */ +.v5-content{flex:1;overflow-y:auto;padding:1.25rem;display:flex;flex-direction:column;gap:1rem;} +.v5-placeholder{ + flex:1;display:flex;align-items:center;justify-content:center; + border:2px dashed var(--v5-border);border-radius:16px; + background:var(--v5-glass);backdrop-filter:blur(12px); + color:var(--v5-text-muted);font-size:.85rem;font-weight:500;min-height:300px;} + +/* ===== V5 DROPDOWN GLASS OVERRIDES ===== */ +.v5-hdr-r [data-radix-popper-content-wrapper]{z-index:100 !important;} +.v5-hdr-r [role="menu"], +.v5-avatar-dd-content{ + background:var(--v5-glass-strong) !important;backdrop-filter:blur(20px) saturate(1.4) !important; + -webkit-backdrop-filter:blur(20px) saturate(1.4) !important; + border:1px solid var(--v5-glass-border) !important;border-radius:14px !important; + box-shadow:0 12px 40px rgba(0,0,0,0.12),var(--v5-glow-sm) !important; + animation:v5-ddIn .25s cubic-bezier(.16,1,.3,1) both !important; + padding:.4rem !important;} +.dark .v5-hdr-r [role="menu"], +.dark .v5-avatar-dd-content{box-shadow:0 12px 40px rgba(0,0,0,0.5),var(--v5-glow-md) !important;} +@keyframes v5-ddIn{from{opacity:0;transform:translateY(-8px) scale(.95)}to{opacity:1;transform:none}} + +/* Avatar dropdown items glass style */ +.v5-avatar-dd-content [role="menuitem"]{border-radius:8px !important;transition:all .15s !important;font-size:.75rem !important;} +.v5-avatar-dd-content [role="menuitem"]:hover{background:var(--v5-surface-hover) !important;transform:translateX(2px);} + +/* ===== MOBILE RESPONSIVE ===== */ +.v5-mobile-toggle{display:none;width:36px;height:36px;border-radius:10px;border:1px solid var(--v5-glass-border); + background:var(--v5-surface);backdrop-filter:blur(8px);color:var(--v5-text-muted);cursor:pointer; + align-items:center;justify-content:center;transition:all .2s;flex-shrink:0;} +.v5-mobile-toggle:hover{border-color:var(--v5-primary);color:var(--v5-primary);} + +.v5-side-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:25; + opacity:0;transition:opacity .3s;pointer-events:none;} +.v5-side-overlay.open{opacity:1;pointer-events:auto;} + +@media(max-width:768px){ + .v5-mobile-toggle{display:flex;} + .v5-side{position:fixed;left:0;top:0;bottom:0;z-index:30; + transform:translateX(-100%);transition:transform .35s cubic-bezier(.4,0,.2,1); + width:260px;padding-top:60px;} + .v5-side.mobile-open{transform:none;} + .v5-side-overlay{display:block;} + .v5-hdr-bc{display:none;} + .v5-admin-badge{display:none !important;} + .v5-hdr-logo{font-size:.9rem;} + .v5-tabs{padding:0 .3rem;} + .v5-tab{padding:0 .6rem;font-size:.65rem;} + .v5-tab-toggle{display:none;} + .v5-content{padding:.75rem;} + .v5-admin-label{display:none !important;} + .v5-pill{display:none;} +} +@media(min-width:769px) and (max-width:1024px){ + :root{--v5-sidebar-w:180px;} + .v5-si{font-size:.72rem;padding:.45rem .6rem;} + .v5-content{padding:1rem;} +} diff --git a/scripts/dev/start-all-parallel.sh b/scripts/dev/start-all-parallel.sh index 465e47cf..4e618ee4 100755 --- a/scripts/dev/start-all-parallel.sh +++ b/scripts/dev/start-all-parallel.sh @@ -17,9 +17,9 @@ echo "" echo "============================================" echo "0. 기존 컨테이너 정리 중..." echo "============================================" -docker rm -f pms-backend-mac pms-frontend-mac 2>/dev/null || echo "기존 컨테이너가 없습니다." -docker network rm pms-network 2>/dev/null || echo "기존 네트워크가 없습니다." -docker network create pms-network 2>/dev/null || echo "네트워크를 생성했습니다." +docker rm -f pms-backend-mac-v2 pms-frontend-mac-v2 2>/dev/null || echo "기존 컨테이너가 없습니다." +docker network rm test-vex-network 2>/dev/null || echo "기존 네트워크가 없습니다." +docker network create test-vex-network 2>/dev/null || echo "네트워크를 생성했습니다." echo "" # 병렬 빌드 시작 @@ -31,7 +31,7 @@ echo "============================================" # 백엔드 빌드 (백그라운드) echo "백엔드(Spring Boot) 빌드 시작..." ( - docker-compose -f docker/dev/docker-compose.backend.mac.yml build + docker compose -f docker/dev/docker-compose.backend.mac.yml build echo "백엔드 빌드 완료" ) & BACKEND_PID=$! @@ -39,7 +39,7 @@ BACKEND_PID=$! # 프론트엔드 빌드 (백그라운드) echo "프론트엔드 빌드 시작..." ( - docker-compose -f docker/dev/docker-compose.frontend.mac.yml build + docker compose -f docker/dev/docker-compose.frontend.mac.yml build echo "프론트엔드 빌드 완료" ) & FRONTEND_PID=$! @@ -62,17 +62,17 @@ echo "============================================" SERVICE_START=$(date +%s) # 기존 컨테이너 정리 -docker-compose -f docker/dev/docker-compose.backend.mac.yml down -v 2>/dev/null -docker-compose -f docker/dev/docker-compose.frontend.mac.yml down -v 2>/dev/null +docker compose -f docker/dev/docker-compose.backend.mac.yml down -v 2>/dev/null +docker compose -f docker/dev/docker-compose.frontend.mac.yml down -v 2>/dev/null # 백엔드 시작 (백그라운드) echo "백엔드(Spring Boot) 서비스 시작..." -docker-compose -f docker/dev/docker-compose.backend.mac.yml up -d & +docker compose -f docker/dev/docker-compose.backend.mac.yml up -d & BACKEND_START_PID=$! # 프론트엔드 시작 (백그라운드) echo "프론트엔드 서비스 시작..." -docker-compose -f docker/dev/docker-compose.frontend.mac.yml up -d & +docker compose -f docker/dev/docker-compose.frontend.mac.yml up -d & FRONTEND_START_PID=$! # 서비스 시작 완료 대기 @@ -92,21 +92,21 @@ echo "============================================" echo "모든 서비스가 시작되었습니다!" echo "============================================" echo "" -echo "[DATABASE] PostgreSQL: http://39.117.244.52:11132" -echo "[BACKEND] Spring Boot API: http://localhost:8081/api" -echo "[FRONTEND] Next.js: http://localhost:9771" +echo "[DATABASE] PostgreSQL: http://211.115.91.141:11134 (test_dev)" +echo "[BACKEND] Spring Boot API: http://localhost:8082/api" +echo "[FRONTEND] Next.js: http://localhost:9772" echo "" echo "서비스 상태 확인:" -echo " 백엔드: docker-compose -f docker/dev/docker-compose.backend.mac.yml ps" -echo " 프론트엔드: docker-compose -f docker/dev/docker-compose.frontend.mac.yml ps" +echo " 백엔드: docker compose -f docker/dev/docker-compose.backend.mac.yml ps" +echo " 프론트엔드: docker compose -f docker/dev/docker-compose.frontend.mac.yml ps" echo "" echo "로그 확인:" -echo " 백엔드: docker-compose -f docker/dev/docker-compose.backend.mac.yml logs -f" -echo " 프론트엔드: docker-compose -f docker/dev/docker-compose.frontend.mac.yml logs -f" +echo " 백엔드: docker compose -f docker/dev/docker-compose.backend.mac.yml logs -f" +echo " 프론트엔드: docker compose -f docker/dev/docker-compose.frontend.mac.yml logs -f" echo "" echo "서비스 중지:" -echo " 백엔드: docker-compose -f docker/dev/docker-compose.backend.mac.yml down" -echo " 프론트엔드: docker-compose -f docker/dev/docker-compose.frontend.mac.yml down" +echo " 백엔드: docker compose -f docker/dev/docker-compose.backend.mac.yml down" +echo " 프론트엔드: docker compose -f docker/dev/docker-compose.frontend.mac.yml down" echo "" echo "============================================"