진행중
This commit is contained in:
@@ -129,7 +129,7 @@ export default function LoginPage() {
|
||||
<div className="orbit-core" />
|
||||
</div>
|
||||
|
||||
<div className="logo"><h1>INVION</h1></div>
|
||||
<div className="logo"><h1>Invy.one</h1></div>
|
||||
<div className="login-sub">Cosmic Command Center</div>
|
||||
|
||||
<form onSubmit={handleLogin}>
|
||||
@@ -157,7 +157,7 @@ export default function LoginPage() {
|
||||
</form>
|
||||
|
||||
<div ref={errRef} className="err-msg">{error || "아이디 또는 비밀번호를 확인해주세요"}</div>
|
||||
<div className="login-ft">© 2026 INVION</div>
|
||||
<div className="login-ft">© 2026 Invy.one</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function MainPage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">플랫폼</p>
|
||||
<p className="text-sm font-medium">WACE ERP/PLM</p>
|
||||
<p className="text-sm font-medium">Invyone ERP/PLM</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function MainHomePage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">플랫폼</p>
|
||||
<p className="text-sm font-medium">WACE ERP/PLM</p>
|
||||
<p className="text-sm font-medium">Invyone ERP/PLM</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -855,8 +855,9 @@ select {
|
||||
mask-repeat: repeat, no-repeat;
|
||||
mask-position: 0 0, 0 0;
|
||||
/* ease-in-out-cubic — 대칭형 곡선으로 시간/progress 가 거의 linear 에 가깝게 진행됨.
|
||||
1800ms 중 약 1260ms 가 시각적 reveal 에 쓰임 — 빠르지도 늦지도 않은 sweet spot. */
|
||||
animation: vt-soft-reveal 1800ms cubic-bezier(0.65, 0, 0.35, 1) forwards;
|
||||
duration 은 themeTransition.ts 가 방향에 따라 --vt-duration 변수로 지정.
|
||||
검정→대비 강해서 빨라 보이는 perceived speed 비대칭을 보정하기 위해 dark 방향은 더 김. */
|
||||
animation: vt-soft-reveal var(--vt-duration, 1800ms) cubic-bezier(0.65, 0, 0.35, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes vt-soft-reveal {
|
||||
|
||||
@@ -20,9 +20,9 @@ interface CompanySwitcherProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* WACE 관리자 전용: 회사 선택 및 전환 컴포넌트
|
||||
* Invyone 관리자 전용: 회사 선택 및 전환 컴포넌트
|
||||
*
|
||||
* - WACE 관리자(company_code = "*", userType = "SUPER_ADMIN")만 표시
|
||||
* - Invyone 관리자(company_code = "*", userType = "SUPER_ADMIN")만 표시
|
||||
* - 회사 선택 시 해당 회사로 전환하여 시스템 사용
|
||||
* - JWT 토큰 재발급으로 company_code 변경
|
||||
*/
|
||||
@@ -33,15 +33,15 @@ export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProp
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// WACE 관리자 권한 체크 (user_type만 확인)
|
||||
const isWaceAdmin = user?.user_type === "SUPER_ADMIN";
|
||||
// Invyone 관리자 권한 체크 (user_type만 확인)
|
||||
const isInvyoneAdmin = user?.user_type === "SUPER_ADMIN";
|
||||
|
||||
// 현재 선택된 회사명 표시
|
||||
const currentCompanyName = React.useMemo(() => {
|
||||
if (!user?.company_code) return "로딩 중...";
|
||||
|
||||
if (user.company_code === "*") {
|
||||
return "WACE (최고 관리자)";
|
||||
return "Invyone (최고 관리자)";
|
||||
}
|
||||
|
||||
// companies 배열에서 현재 회사 찾기
|
||||
@@ -51,10 +51,10 @@ export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProp
|
||||
|
||||
// 회사 목록 조회
|
||||
useEffect(() => {
|
||||
if (isWaceAdmin && isOpen) {
|
||||
if (isInvyoneAdmin && isOpen) {
|
||||
fetchCompanies();
|
||||
}
|
||||
}, [isWaceAdmin, isOpen]);
|
||||
}, [isInvyoneAdmin, isOpen]);
|
||||
|
||||
// 검색 필터링
|
||||
useEffect(() => {
|
||||
@@ -75,24 +75,24 @@ export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProp
|
||||
const response = await apiClient.get("/admin/companies/db");
|
||||
|
||||
if (response.data.success) {
|
||||
// 활성 상태의 회사만 필터링 + company_code="*" 제외 (WACE는 별도 추가)
|
||||
// 활성 상태의 회사만 필터링 + company_code="*" 제외 (Invyone는 별도 추가)
|
||||
const activeCompanies = response.data.data
|
||||
.filter((c: Company) => c.company_code !== "*") // DB의 "*" 제외
|
||||
.filter((c: Company) => c.status === "active" || !c.status)
|
||||
.sort((a: Company, b: Company) => a.company_name.localeCompare(b.company_name));
|
||||
|
||||
// WACE 복귀 옵션 추가
|
||||
const companiesWithWace: Company[] = [
|
||||
// Invyone 복귀 옵션 추가
|
||||
const companiesWithInvyone: Company[] = [
|
||||
{
|
||||
company_code: "*",
|
||||
company_name: "WACE (최고 관리자)",
|
||||
company_name: "Invyone (최고 관리자)",
|
||||
status: "active",
|
||||
},
|
||||
...activeCompanies,
|
||||
];
|
||||
|
||||
setCompanies(companiesWithWace);
|
||||
setFilteredCompanies(companiesWithWace);
|
||||
setCompanies(companiesWithInvyone);
|
||||
setFilteredCompanies(companiesWithInvyone);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("회사 목록 조회 실패", error);
|
||||
@@ -124,8 +124,8 @@ export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProp
|
||||
}
|
||||
};
|
||||
|
||||
// WACE 관리자가 아니면 렌더링하지 않음
|
||||
if (!isWaceAdmin) {
|
||||
// Invyone 관리자가 아니면 렌더링하지 않음
|
||||
if (!isInvyoneAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
const companyCode = (user as ExtendedUserInfo)?.company_code;
|
||||
|
||||
if (companyCode === "*") {
|
||||
setCurrentCompanyName("WACE (최고 관리자)");
|
||||
setCurrentCompanyName("Invyone (최고 관리자)");
|
||||
} else if (companyCode) {
|
||||
try {
|
||||
const response = await apiClient.get("/admin/companies/db");
|
||||
@@ -455,28 +455,84 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
toast.warning("이 메뉴에 할당된 화면이 없습니다. 메뉴 설정을 확인해주세요.");
|
||||
};
|
||||
|
||||
const handleModeSwitch = useCallback(() => {
|
||||
const handleModeSwitch = useCallback((e?: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (modeTransition !== "idle") return;
|
||||
|
||||
// 레퍼런스 invion-layout-v5.html switchMode() 를 그대로 따름:
|
||||
// Phase 1 (0ms) : mode-fade 오버레이 페이드인 + 사이드바 slide-out + 탭 fade-out
|
||||
// Phase 2 (300ms) : 모드 swap (React 재렌더) → 새 사이드바/탭 등장
|
||||
// Phase 3 (600ms) : 오버레이 페이드아웃 + 정리
|
||||
const fade = document.getElementById("v5-mode-fade");
|
||||
// 강화된 mode transition — 옵션 b/c/e/f 적용 (d 버튼 burst 제거, mode-fade overlay 도 제거):
|
||||
// Phase 1 (0ms): 사이드바 items morph-out (stagger), 헤더 glow flash,
|
||||
// breadcrumb swap-out, 이탈 시 admin badge zoom-out
|
||||
// Phase 2 (350ms): React 가 모드 swap → 새 메뉴 morph-in (stagger), breadcrumb swap-in,
|
||||
// 진입 시 admin badge zoom-in
|
||||
// Phase 3 (~950ms): 모든 클래스 정리, idle 복귀
|
||||
const goingToAdmin = !isAdminMode;
|
||||
setModeTransition("out");
|
||||
fade?.classList.add("in");
|
||||
|
||||
// (b) 사이드바 items morph-out — stagger
|
||||
const oldItems = Array.from(document.querySelectorAll<HTMLElement>(".v5-side .v5-si"));
|
||||
oldItems.forEach((it, i) => {
|
||||
it.style.animationDelay = `${i * 35}ms`;
|
||||
it.classList.add("mode-morph-out");
|
||||
});
|
||||
|
||||
// (c) 헤더 glow line flash
|
||||
const hdrGlow = document.querySelector<HTMLElement>(".v5-hdr-glow");
|
||||
if (hdrGlow) {
|
||||
hdrGlow.classList.remove("mode-flash");
|
||||
void hdrGlow.offsetWidth; // reflow → 재시작 가능하게
|
||||
hdrGlow.classList.add("mode-flash");
|
||||
}
|
||||
|
||||
// (d) 토글 버튼 burst 효과는 제거됨 — 가운데 밝아지는 게 거슬려서 통째로 뺌
|
||||
|
||||
// (e) breadcrumb swap-out
|
||||
const bc = document.querySelector<HTMLElement>(".v5-hdr-bc");
|
||||
bc?.classList.remove("mode-swap-in");
|
||||
bc?.classList.add("mode-swap-out");
|
||||
|
||||
// (f) admin badge zoom-out (이탈 시에만)
|
||||
if (!goingToAdmin) {
|
||||
const badge = document.querySelector<HTMLElement>(".v5-admin-badge");
|
||||
badge?.classList.remove("mode-zoom-in");
|
||||
badge?.classList.add("mode-zoom-out");
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// Phase 2: swap mode — React re-renders with new menus/tabs
|
||||
// Phase 2: 모드 swap → React 재렌더 (새 메뉴/탭/breadcrumb)
|
||||
setTabMode(isAdminMode ? "user" : "admin");
|
||||
setModeTransition("in");
|
||||
|
||||
// Phase 3: 오버레이 페이드아웃 후 정리
|
||||
// 다음 프레임에 새 items 에 morph-in 적용
|
||||
requestAnimationFrame(() => {
|
||||
const newItems = Array.from(document.querySelectorAll<HTMLElement>(".v5-side .v5-si"));
|
||||
newItems.forEach((it, i) => {
|
||||
it.style.animationDelay = `${i * 45}ms`;
|
||||
it.classList.add("mode-morph-in");
|
||||
});
|
||||
|
||||
// breadcrumb swap-in (텍스트는 이미 새 모드 기준)
|
||||
const newBc = document.querySelector<HTMLElement>(".v5-hdr-bc");
|
||||
newBc?.classList.remove("mode-swap-out");
|
||||
newBc?.classList.add("mode-swap-in");
|
||||
|
||||
// (f) admin badge zoom-in (진입 시에만)
|
||||
if (goingToAdmin) {
|
||||
const newBadge = document.querySelector<HTMLElement>(".v5-admin-badge");
|
||||
newBadge?.classList.remove("mode-zoom-out");
|
||||
newBadge?.classList.add("mode-zoom-in");
|
||||
}
|
||||
});
|
||||
|
||||
// Phase 3: 모든 애니메이션 클래스 정리
|
||||
setTimeout(() => {
|
||||
fade?.classList.remove("in");
|
||||
setModeTransition("idle");
|
||||
}, 300);
|
||||
}, 300);
|
||||
document.querySelectorAll<HTMLElement>(".v5-side .v5-si").forEach((it) => {
|
||||
it.classList.remove("mode-morph-in", "mode-morph-out");
|
||||
it.style.animationDelay = "";
|
||||
});
|
||||
document.querySelector<HTMLElement>(".v5-hdr-bc")?.classList.remove("mode-swap-in", "mode-swap-out");
|
||||
document.querySelector<HTMLElement>(".v5-admin-badge")?.classList.remove("mode-zoom-in", "mode-zoom-out");
|
||||
}, 600);
|
||||
}, 350);
|
||||
}, [isAdminMode, setTabMode, modeTransition]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -727,9 +783,6 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
{/* Theme fade overlay */}
|
||||
<div className="v5-theme-fade" id="v5-theme-fade" />
|
||||
|
||||
{/* Mode transition fade overlay — radial gradient 으로 화면을 한 번 덮어 사이드바/탭 swap 을 가림 */}
|
||||
<div className="v5-mode-fade" id="v5-mode-fade" />
|
||||
|
||||
{/* V5 Shell */}
|
||||
<div className={`v5-shell ${isAdminMode ? "v5-admin-mode" : ""}`}>
|
||||
{/* ===== Glass Header ===== */}
|
||||
@@ -739,7 +792,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
<button className="v5-mobile-toggle" onClick={() => setSidebarOpen(!sidebarOpen)}>
|
||||
<Menu size={16} />
|
||||
</button>
|
||||
<div className="v5-hdr-logo">INVION</div>
|
||||
<div className="v5-hdr-logo">Invy.one</div>
|
||||
<div className="v5-hdr-bc">
|
||||
{isAdminMode ? "관리자" : "홈"} › <b>{breadcrumbText}</b>
|
||||
</div>
|
||||
@@ -748,6 +801,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
관리자 모드
|
||||
</div>
|
||||
</div>
|
||||
{/* mode transition 헤더 glow 라인 — 평소엔 opacity 0, mode change 시에만 flash */}
|
||||
<div className="v5-hdr-glow" />
|
||||
<div className="v5-hdr-r">
|
||||
{/* Theme pill */}
|
||||
<div className="v5-pill">
|
||||
@@ -773,9 +828,9 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
<div className="v5-bell-dot" />
|
||||
</button>
|
||||
|
||||
{/* Admin toggle (gear ↔ home) */}
|
||||
{/* Admin toggle (gear ↔ home) — 이벤트 전달해서 burst origin 으로 사용 */}
|
||||
{isAdmin && (
|
||||
<button className="v5-admin-btn" onClick={handleModeSwitch} title={isAdminMode ? "홈으로" : "관리자"}>
|
||||
<button className="v5-admin-btn" onClick={(e) => handleModeSwitch(e)} title={isAdminMode ? "홈으로" : "관리자"}>
|
||||
<Settings size={14} className="ic-gear" />
|
||||
<Home size={14} className="ic-home" />
|
||||
<span className="v5-admin-label">{isAdminMode ? "홈으로" : "관리자"}</span>
|
||||
|
||||
@@ -10,7 +10,7 @@ export function Logo() {
|
||||
<div className="flex items-center justify-center">
|
||||
<Image
|
||||
src="/images/invion.png"
|
||||
alt="WACE 솔루션 로고"
|
||||
alt="Invyone 솔루션 로고"
|
||||
width={120}
|
||||
height={32}
|
||||
className="h-8 object-contain"
|
||||
|
||||
@@ -550,7 +550,7 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
||||
>
|
||||
{onToggleCollapse && (
|
||||
<button className="v5-tab-toggle" onClick={onToggleCollapse} title={collapsed ? "탭 펼치기" : "탭 접기"}>
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
<ChevronUp className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{displayVisible.map((tab, i) => renderTab(tab, i))}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
export const LAYOUT_CONFIG = {
|
||||
COMPANY_NAME: "WACE 솔루션",
|
||||
COMPANY_NAME: "Invyone 솔루션",
|
||||
API_BASE_URL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8081/api",
|
||||
|
||||
ENDPOINTS: {
|
||||
|
||||
@@ -180,7 +180,7 @@ export const useAuth = () => {
|
||||
}, [fetchCurrentUser, checkAuthStatus]);
|
||||
|
||||
/**
|
||||
* 회사 전환 처리 (WACE 관리자 전용)
|
||||
* 회사 전환 처리 (Invyone 관리자 전용)
|
||||
*/
|
||||
const switchCompany = useCallback(
|
||||
async (companyCode: string): Promise<{ success: boolean; message: string }> => {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const ConditionalContainerDefinition = {
|
||||
description: "셀렉트박스 값에 따라 다른 UI를 표시하는 조건부 컨테이너",
|
||||
icon: "GitBranch",
|
||||
version: "1.0.0",
|
||||
author: "WACE",
|
||||
author: "Invyone",
|
||||
tags: ["조건부", "분기", "동적", "레이아웃"],
|
||||
|
||||
default_size: {
|
||||
|
||||
@@ -32,7 +32,7 @@ export const SectionCardDefinition = createComponentDefinition({
|
||||
icon: "LayoutPanelTop",
|
||||
tags: ["섹션", "그룹", "카드", "컨테이너", "제목", "card"],
|
||||
version: "1.0.0",
|
||||
author: "WACE",
|
||||
author: "Invyone",
|
||||
hidden: true, // v2-section-card 사용으로 패널에서 숨김
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export const SectionPaperDefinition = createComponentDefinition({
|
||||
icon: "Square",
|
||||
tags: ["섹션", "그룹", "배경", "컨테이너", "색종이", "paper"],
|
||||
version: "1.0.0",
|
||||
author: "WACE",
|
||||
author: "Invyone",
|
||||
hidden: true, // v2-section-paper 사용으로 패널에서 숨김
|
||||
});
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ ComponentRegistry.registerComponent({
|
||||
renderer: TableSearchWidgetRenderer.render as any,
|
||||
config_panel: TableSearchWidgetConfigPanel,
|
||||
version: "1.0.0",
|
||||
author: "WACE",
|
||||
author: "Invyone",
|
||||
hidden: true, // v2-table-search-widget 사용으로 패널에서 숨김
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const V2SectionCardDefinition = createComponentDefinition({
|
||||
icon: "LayoutPanelTop",
|
||||
tags: ["섹션", "그룹", "카드", "컨테이너", "제목", "card"],
|
||||
version: "1.0.0",
|
||||
author: "WACE",
|
||||
author: "Invyone",
|
||||
});
|
||||
|
||||
// 컴포넌트는 SectionCardRenderer에서 자동 등록됩니다
|
||||
|
||||
@@ -29,7 +29,7 @@ export const V2SectionPaperDefinition = createComponentDefinition({
|
||||
icon: "Square",
|
||||
tags: ["섹션", "그룹", "배경", "컨테이너", "색종이", "paper"],
|
||||
version: "1.0.0",
|
||||
author: "WACE",
|
||||
author: "Invyone",
|
||||
});
|
||||
|
||||
// 컴포넌트는 SectionPaperRenderer에서 자동 등록됩니다
|
||||
|
||||
@@ -21,7 +21,7 @@ ComponentRegistry.registerComponent({
|
||||
renderer: TableSearchWidgetRenderer.render as any,
|
||||
config_panel: V2TableSearchWidgetConfigPanel,
|
||||
version: "1.0.0",
|
||||
author: "WACE",
|
||||
author: "Invyone",
|
||||
});
|
||||
|
||||
export { TableSearchWidget } from "./TableSearchWidget";
|
||||
|
||||
@@ -48,6 +48,9 @@ export function animatedThemeChange(
|
||||
root.style.setProperty("--reveal-x", `${x}px`);
|
||||
root.style.setProperty("--reveal-y", `${y}px`);
|
||||
root.style.setProperty("--reveal-max", `${endRadius}px`);
|
||||
// Perceived speed 보정: 다크로 갈 땐 검정 컨트라스트가 강해서 같은 duration 도 빠르게 느껴짐.
|
||||
// dark 방향만 약 25% 더 길게 잡아 두 방향이 비슷한 속도로 인지되도록 함.
|
||||
root.style.setProperty("--vt-duration", next === "dark" ? "2200ms" : "1700ms");
|
||||
|
||||
// View Transitions API 미지원 → 즉시 적용
|
||||
if (!doc.startViewTransition) {
|
||||
|
||||
@@ -191,15 +191,24 @@ html:not(.dark) .v5-cosmos .neb-4{width:600px;height:600px;top:-10%;right:20%;bo
|
||||
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;
|
||||
/* Admin badge — display:none 대신 opacity/transform 으로 hidden 해서 zoom-in/out 애니메이션 가능 */
|
||||
.v5-admin-badge{display:flex;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;}
|
||||
opacity:0;transform:scale(0) rotate(-30deg);pointer-events:none;}
|
||||
.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-mode .v5-admin-badge{opacity:1;transform:scale(1) rotate(0);pointer-events:auto;}
|
||||
/* badge zoom — 모드 진입 시 bouncy in, 이탈 시 quick out */
|
||||
.v5-admin-badge.mode-zoom-in{animation:v5-badge-zoom-in .55s cubic-bezier(.34,1.56,.64,1) both;}
|
||||
.v5-admin-badge.mode-zoom-out{animation:v5-badge-zoom-out .35s cubic-bezier(.4,0,1,1) both;}
|
||||
@keyframes v5-badge-zoom-in{
|
||||
0%{opacity:0;transform:scale(0) rotate(-30deg)}
|
||||
60%{opacity:1;transform:scale(1.15) rotate(5deg)}
|
||||
100%{opacity:1;transform:scale(1) rotate(0)}}
|
||||
@keyframes v5-badge-zoom-out{
|
||||
0%{opacity:1;transform:scale(1) rotate(0)}
|
||||
100%{opacity:0;transform:scale(0) rotate(30deg)}}
|
||||
.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)}}
|
||||
@@ -217,12 +226,16 @@ html:not(.dark) .v5-cosmos .neb-4{width:600px;height:600px;top:-10%;right:20%;bo
|
||||
.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);}
|
||||
/* Tab collapse — 탭 바에 통합된 좌측 핸들 (떠있는 박스 느낌 제거) */
|
||||
.v5-tab-toggle{width:32px;height:36px;border:none;background:transparent;color:var(--v5-text-muted);cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:0;font-family:inherit;
|
||||
border-right:1px solid var(--v5-glass-border);margin-right:.4rem;position:relative;
|
||||
transition:color .2s,background .2s;}
|
||||
.v5-tab-toggle::after{content:'';position:absolute;left:4px;right:5px;top:6px;bottom:6px;border-radius:8px;
|
||||
background:transparent;transition:background .2s;pointer-events:none;}
|
||||
.v5-tab-toggle:hover{color:var(--v5-primary);}
|
||||
.v5-tab-toggle:hover::after{background:var(--v5-surface-hover);}
|
||||
.v5-tab-toggle svg{position:relative;z-index:1;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;}
|
||||
@@ -480,6 +493,58 @@ html:not(.dark) .v5-cosmos .neb-4{width:600px;height:600px;top:-10%;right:20%;bo
|
||||
.v5-tabs.fade-in{animation:v5-tabsFadeIn .3s cubic-bezier(.16,1,.3,1) both;}
|
||||
@keyframes v5-tabsFadeIn{from{opacity:0}to{opacity:1}}
|
||||
|
||||
/* ===== MODE TRANSITION — sidebar items morph (option b) ===== */
|
||||
/* 각 .v5-si 가 stagger delay 로 사라졌다가 새 메뉴가 stagger 로 들어옴 */
|
||||
.v5-si.mode-morph-out{animation:v5-mode-si-out .35s cubic-bezier(.4,0,1,1) forwards;}
|
||||
.v5-si.mode-morph-in{animation:v5-mode-si-in .45s cubic-bezier(.16,1,.3,1) backwards;}
|
||||
@keyframes v5-mode-si-out{
|
||||
0%{opacity:1;transform:translateX(0) scale(1);filter:blur(0)}
|
||||
100%{opacity:0;transform:translateX(-30px) scale(.92);filter:blur(4px)}}
|
||||
@keyframes v5-mode-si-in{
|
||||
0%{opacity:0;transform:translateX(30px) scale(.92);filter:blur(4px)}
|
||||
100%{opacity:1;transform:translateX(0) scale(1);filter:blur(0)}}
|
||||
|
||||
/* ===== MODE TRANSITION — header glow line flash (option c) ===== */
|
||||
/* 헤더 아래 1px 라인이 mode change 시 굵게/번쩍 → 1px 로 settle */
|
||||
.v5-hdr-glow{position:absolute;bottom:-1px;left:0;right:0;height:1px;
|
||||
background:linear-gradient(90deg,transparent,var(--v5-primary),transparent);
|
||||
opacity:0;pointer-events:none;}
|
||||
.v5-admin-mode .v5-hdr-glow{background:linear-gradient(90deg,transparent,var(--v5-cyan),transparent);}
|
||||
.v5-hdr-glow.mode-flash{animation:v5-mode-hdr-flash 1.4s cubic-bezier(.16,1,.3,1) forwards;}
|
||||
@keyframes v5-mode-hdr-flash{
|
||||
0%{opacity:0;height:1px;filter:blur(0)}
|
||||
20%{opacity:1;height:6px;filter:blur(8px)}
|
||||
40%{opacity:1;height:4px;filter:blur(6px)}
|
||||
100%{opacity:.6;height:1px;filter:blur(0)}}
|
||||
|
||||
/* ===== MODE TRANSITION — breadcrumb text swap (option e) ===== */
|
||||
.v5-hdr-bc{display:inline-block;}
|
||||
.v5-hdr-bc.mode-swap-out{animation:v5-mode-bc-out .25s ease-in forwards;}
|
||||
.v5-hdr-bc.mode-swap-in{animation:v5-mode-bc-in .35s cubic-bezier(.16,1,.3,1) forwards;}
|
||||
@keyframes v5-mode-bc-out{
|
||||
to{opacity:0;transform:translateY(-8px) scale(.9);filter:blur(4px)}}
|
||||
@keyframes v5-mode-bc-in{
|
||||
from{opacity:0;transform:translateY(8px) scale(.9);filter:blur(4px)}
|
||||
to{opacity:1;transform:translateY(0) scale(1);filter:blur(0)}}
|
||||
|
||||
/* ===== MODE TRANSITION — toggle button burst (option d, 절제된 버전) ===== */
|
||||
/* JS 가 .v5-mode-burst 컨테이너를 body 에 append. ring 1개 + particle 6개 정도로 미니멀. */
|
||||
.v5-mode-burst{position:fixed;pointer-events:none;z-index:1000;}
|
||||
.v5-mode-burst .burst-ring{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
|
||||
width:0;height:0;border-radius:50%;border:1.5px solid var(--v5-primary);opacity:.6;
|
||||
animation:v5-mode-burst-ring .65s cubic-bezier(.16,1,.3,1) forwards;}
|
||||
.v5-mode-burst.admin .burst-ring{border-color:var(--v5-cyan);}
|
||||
@keyframes v5-mode-burst-ring{
|
||||
0%{width:0;height:0;opacity:.8;border-width:1.5px}
|
||||
100%{width:140px;height:140px;opacity:0;border-width:.5px}}
|
||||
.v5-mode-burst .burst-particle{position:absolute;left:50%;top:50%;width:3px;height:3px;
|
||||
border-radius:50%;background:var(--v5-primary);box-shadow:0 0 4px var(--v5-primary);opacity:.7;
|
||||
animation:v5-mode-burst-particle .55s cubic-bezier(.16,1,.3,1) forwards;}
|
||||
.v5-mode-burst.admin .burst-particle{background:var(--v5-cyan);box-shadow:0 0 4px var(--v5-cyan);}
|
||||
@keyframes v5-mode-burst-particle{
|
||||
0%{transform:translate(-50%,-50%) scale(1);opacity:.8}
|
||||
100%{transform:translate(calc(-50% + var(--bx)),calc(-50% + var(--by))) scale(0);opacity:0}}
|
||||
|
||||
/* ===== 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);}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,391 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Mode Transition Demos — invyone</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0c0b18;
|
||||
--bg-subtle: #14122a;
|
||||
--surface: rgba(255,255,255,.04);
|
||||
--surface-hover: rgba(255,255,255,.08);
|
||||
--glass: rgba(255,255,255,.025);
|
||||
--glass-border: rgba(255,255,255,.08);
|
||||
--text: #eae8f4;
|
||||
--text-sec: #8d8ba8;
|
||||
--text-muted: #5a587a;
|
||||
--primary: #a29bfe; /* user mode accent — 보라 */
|
||||
--primary-glow: rgba(162,155,254,.45);
|
||||
--cyan: #55efc4; /* admin mode accent — 청록 */
|
||||
--cyan-glow: rgba(85,239,196,.45);
|
||||
--pink: #fd79a8;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); overflow: hidden; }
|
||||
|
||||
/* ===== 컨트롤 패널 ===== */
|
||||
.controls {
|
||||
position: fixed; top: 12px; left: 50%; transform: translateX(-50%);
|
||||
background: rgba(0,0,0,.6); backdrop-filter: blur(20px); border: 1px solid var(--glass-border);
|
||||
border-radius: 14px; padding: 10px; display: flex; gap: 6px; z-index: 1000; font-size: 11px;
|
||||
flex-wrap: wrap; max-width: 90vw; justify-content: center;
|
||||
}
|
||||
.controls button {
|
||||
background: rgba(255,255,255,.05); color: var(--text); border: 1px solid var(--glass-border);
|
||||
padding: 8px 12px; border-radius: 8px; cursor: pointer; font-family: inherit; font-size: 11px;
|
||||
font-weight: 500; transition: all .2s; white-space: nowrap;
|
||||
}
|
||||
.controls button:hover { background: rgba(162,155,254,.15); border-color: var(--primary); color: var(--primary); }
|
||||
.controls .group-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase;
|
||||
letter-spacing: .08em; align-self: center; padding: 0 6px; font-weight: 700; }
|
||||
.controls .divider { width: 1px; background: var(--glass-border); margin: 0 4px; }
|
||||
|
||||
/* ===== 가짜 앱 shell ===== */
|
||||
.shell { position: fixed; inset: 0; display: flex; flex-direction: column;
|
||||
background: radial-gradient(ellipse at top right, rgba(162,155,254,.06), transparent 50%),
|
||||
radial-gradient(ellipse at bottom left, rgba(85,239,196,.04), transparent 50%),
|
||||
var(--bg);
|
||||
}
|
||||
.shell.admin {
|
||||
background: radial-gradient(ellipse at top right, rgba(85,239,196,.08), transparent 50%),
|
||||
radial-gradient(ellipse at bottom left, rgba(162,155,254,.04), transparent 50%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
/* 헤더 */
|
||||
.hdr {
|
||||
height: 52px; background: var(--glass); backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--glass-border); display: flex; align-items: center;
|
||||
padding: 0 1rem; gap: 1rem; position: relative; z-index: 20;
|
||||
}
|
||||
.hdr-logo { font-weight: 800; font-size: 1rem; letter-spacing: -.02em;
|
||||
background: linear-gradient(135deg, var(--primary), var(--cyan));
|
||||
-webkit-background-clip: text; background-clip: text; color: transparent; }
|
||||
.shell.admin .hdr-logo {
|
||||
background: linear-gradient(135deg, var(--cyan), var(--primary));
|
||||
-webkit-background-clip: text; background-clip: text; color: transparent;
|
||||
}
|
||||
.hdr-bc { font-size: .72rem; color: var(--text-muted); }
|
||||
.hdr-bc b { color: var(--text); font-weight: 600; }
|
||||
.hdr-spacer { flex: 1; }
|
||||
.admin-badge { display: none; padding: .3rem .7rem; border-radius: 999px;
|
||||
background: rgba(85,239,196,.12); color: var(--cyan); font-size: .65rem; font-weight: 600;
|
||||
border: 1px solid rgba(85,239,196,.25); align-items: center; gap: .35rem; }
|
||||
.shell.admin .admin-badge { display: inline-flex; }
|
||||
.admin-badge .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--cyan);
|
||||
box-shadow: 0 0 8px var(--cyan); animation: pulse 2s infinite; }
|
||||
@keyframes pulse { 0%,100% { box-shadow: 0 0 4px var(--cyan); } 50% { box-shadow: 0 0 12px var(--cyan); } }
|
||||
|
||||
.toggle-btn {
|
||||
background: rgba(255,255,255,.05); border: 1px solid var(--glass-border); color: var(--text-sec);
|
||||
padding: .4rem .8rem; border-radius: 10px; cursor: pointer; font-size: .7rem; font-weight: 600;
|
||||
font-family: inherit; transition: all .25s; position: relative;
|
||||
}
|
||||
.toggle-btn:hover { color: var(--primary); border-color: var(--primary); }
|
||||
.shell.admin .toggle-btn { color: var(--cyan); border-color: var(--cyan); }
|
||||
|
||||
/* 헤더 발광 라인 (option c) */
|
||||
.hdr-glow { position: absolute; bottom: -1px; left: 0; right: 0; height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--primary), transparent); opacity: 0; pointer-events: none; }
|
||||
.shell.admin .hdr-glow { background: linear-gradient(90deg, transparent, var(--cyan), transparent); }
|
||||
.hdr-glow.flash { animation: hdr-glow-flash 1.4s cubic-bezier(.16,1,.3,1) forwards; }
|
||||
@keyframes hdr-glow-flash {
|
||||
0% { opacity: 0; height: 1px; filter: blur(0); }
|
||||
20% { opacity: 1; height: 6px; filter: blur(8px); }
|
||||
40% { opacity: 1; height: 4px; filter: blur(6px); }
|
||||
100% { opacity: .6; height: 1px; filter: blur(0); }
|
||||
}
|
||||
|
||||
/* 본문 */
|
||||
.body { flex: 1; display: flex; min-height: 0; position: relative; }
|
||||
|
||||
/* 사이드바 */
|
||||
.side { width: 220px; background: var(--glass); backdrop-filter: blur(20px);
|
||||
border-right: 1px solid var(--glass-border); padding: 1rem .6rem; flex-shrink: 0;
|
||||
display: flex; flex-direction: column; gap: 1px; position: relative;
|
||||
}
|
||||
.si { padding: .55rem .7rem; border-radius: 10px; font-size: .77rem; color: var(--text-sec);
|
||||
cursor: pointer; display: flex; align-items: center; gap: .6rem;
|
||||
transition: all .25s; }
|
||||
.si:hover { background: var(--surface-hover); color: var(--text); }
|
||||
.si.on { background: linear-gradient(135deg, rgba(162,155,254,.12), rgba(162,155,254,.04));
|
||||
color: var(--primary); border: 1px solid rgba(162,155,254,.15); }
|
||||
.shell.admin .si.on { background: linear-gradient(135deg, rgba(85,239,196,.12), rgba(85,239,196,.04));
|
||||
color: var(--cyan); border: 1px solid rgba(85,239,196,.15); }
|
||||
.si .ic { width: 14px; height: 14px; opacity: .65; flex-shrink: 0; }
|
||||
|
||||
/* 컨텐츠 */
|
||||
.content { flex: 1; padding: 2rem; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 2rem; color: var(--text-muted); font-weight: 200; letter-spacing: .05em; }
|
||||
|
||||
/* ===================================================
|
||||
Option a: Color sweep — destination accent 컬러로 sweep
|
||||
=================================================== */
|
||||
.color-sweep { position: fixed; inset: 0; pointer-events: none; z-index: 50;
|
||||
background: radial-gradient(circle at 50% 50%, var(--primary-glow), transparent 60%);
|
||||
opacity: 0; }
|
||||
.color-sweep.admin { background: radial-gradient(circle at 50% 50%, var(--cyan-glow), transparent 60%); }
|
||||
.color-sweep.run {
|
||||
animation: color-sweep-run 1.2s cubic-bezier(.16,1,.3,1) forwards;
|
||||
}
|
||||
@keyframes color-sweep-run {
|
||||
0% { opacity: 0; transform: scale(.3); filter: blur(40px); }
|
||||
30% { opacity: 1; transform: scale(1.2); filter: blur(60px); }
|
||||
60% { opacity: .9; transform: scale(1.5); filter: blur(80px); }
|
||||
100% { opacity: 0; transform: scale(2); filter: blur(120px); }
|
||||
}
|
||||
|
||||
/* ===================================================
|
||||
Option b: Sidebar items morph (stagger out + in)
|
||||
=================================================== */
|
||||
.side .si.morph-out { animation: si-morph-out .35s cubic-bezier(.4,0,1,1) forwards; }
|
||||
.side .si.morph-in { animation: si-morph-in .45s cubic-bezier(.16,1,.3,1) backwards; }
|
||||
@keyframes si-morph-out {
|
||||
0% { opacity: 1; transform: translateX(0) scale(1); filter: blur(0); }
|
||||
100% { opacity: 0; transform: translateX(-30px) scale(.92); filter: blur(4px); }
|
||||
}
|
||||
@keyframes si-morph-in {
|
||||
0% { opacity: 0; transform: translateX(30px) scale(.92); filter: blur(4px); }
|
||||
100% { opacity: 1; transform: translateX(0) scale(1); filter: blur(0); }
|
||||
}
|
||||
|
||||
/* ===================================================
|
||||
Option c: Header glow line (위 .hdr-glow.flash)
|
||||
=================================================== */
|
||||
|
||||
/* ===================================================
|
||||
Option d: Toggle button burst (particle ripple)
|
||||
=================================================== */
|
||||
.burst { position: fixed; pointer-events: none; z-index: 60; }
|
||||
.burst-ring { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
|
||||
width: 20px; height: 20px; border-radius: 50%; border: 2px solid var(--primary);
|
||||
animation: burst-ring 1s cubic-bezier(.16,1,.3,1) forwards; }
|
||||
.burst.admin .burst-ring { border-color: var(--cyan); }
|
||||
@keyframes burst-ring {
|
||||
0% { width: 0; height: 0; opacity: 1; border-width: 4px; }
|
||||
100% { width: 600px; height: 600px; opacity: 0; border-width: 1px; }
|
||||
}
|
||||
.burst-ring:nth-child(2) { animation-delay: .12s; }
|
||||
.burst-ring:nth-child(3) { animation-delay: .24s; }
|
||||
.burst-particle { position: absolute; left: 50%; top: 50%; width: 4px; height: 4px;
|
||||
border-radius: 50%; background: var(--primary); box-shadow: 0 0 8px var(--primary);
|
||||
animation: burst-particle .9s cubic-bezier(.16,1,.3,1) forwards; }
|
||||
.burst.admin .burst-particle { background: var(--cyan); box-shadow: 0 0 8px var(--cyan); }
|
||||
@keyframes burst-particle {
|
||||
0% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
|
||||
100% { transform: translate(calc(-50% + var(--bx)), calc(-50% + var(--by))) scale(0); opacity: 0; }
|
||||
}
|
||||
|
||||
/* ===================================================
|
||||
Option e: Breadcrumb text swap
|
||||
=================================================== */
|
||||
.hdr-bc.swap-out { animation: bc-swap-out .25s ease-in forwards; }
|
||||
.hdr-bc.swap-in { animation: bc-swap-in .35s cubic-bezier(.16,1,.3,1) forwards; }
|
||||
@keyframes bc-swap-out {
|
||||
to { opacity: 0; transform: translateY(-8px) scale(.9); filter: blur(4px); }
|
||||
}
|
||||
@keyframes bc-swap-in {
|
||||
from { opacity: 0; transform: translateY(8px) scale(.9); filter: blur(4px); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
|
||||
}
|
||||
|
||||
/* ===================================================
|
||||
Option f: Admin badge zoom in/out
|
||||
=================================================== */
|
||||
.admin-badge.zoom-in { animation: badge-zoom-in .55s cubic-bezier(.34,1.56,.64,1) backwards; }
|
||||
.admin-badge.zoom-out { animation: badge-zoom-out .35s cubic-bezier(.4,0,1,1) forwards; }
|
||||
@keyframes badge-zoom-in {
|
||||
0% { opacity: 0; transform: scale(0) rotate(-30deg); }
|
||||
60% { opacity: 1; transform: scale(1.15) rotate(5deg); }
|
||||
100% { opacity: 1; transform: scale(1) rotate(0); }
|
||||
}
|
||||
@keyframes badge-zoom-out {
|
||||
to { opacity: 0; transform: scale(0) rotate(30deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="controls">
|
||||
<span class="group-label">개별</span>
|
||||
<button onclick="run('a')">a) 색상 sweep</button>
|
||||
<button onclick="run('b')">b) 사이드바 morph</button>
|
||||
<button onclick="run('c')">c) 헤더 glow</button>
|
||||
<button onclick="run('d')">d) 버튼 burst</button>
|
||||
<button onclick="run('e')">e) breadcrumb swap</button>
|
||||
<button onclick="run('f')">f) badge zoom</button>
|
||||
<span class="divider"></span>
|
||||
<span class="group-label">조합</span>
|
||||
<button onclick="run('abd')">a+b+d (추천)</button>
|
||||
<button onclick="run('all')">전부</button>
|
||||
</div>
|
||||
|
||||
<div class="shell" id="shell">
|
||||
<header class="hdr">
|
||||
<div class="hdr-logo">INVION</div>
|
||||
<div class="hdr-bc" id="bc">홈 › <b>대시보드</b></div>
|
||||
<div class="hdr-spacer"></div>
|
||||
<div class="admin-badge" id="badge"><div class="dot"></div>관리자 모드</div>
|
||||
<button class="toggle-btn" id="toggleBtn" onclick="run('abd')">관리자</button>
|
||||
</header>
|
||||
<div class="hdr-glow" id="hdrGlow"></div>
|
||||
|
||||
<div class="body">
|
||||
<aside class="side" id="side">
|
||||
<div class="si on"><span class="ic">●</span>대시보드</div>
|
||||
<div class="si"><span class="ic">▣</span>유저관리</div>
|
||||
<div class="si"><span class="ic">▤</span>시스템 관리</div>
|
||||
<div class="si"><span class="ic">▥</span>화면 관리</div>
|
||||
<div class="si"><span class="ic">▦</span>자동화 관리</div>
|
||||
<div class="si"><span class="ic">▧</span>AI 관리</div>
|
||||
<div class="si"><span class="ic">▨</span>결재 관리</div>
|
||||
<div class="si"><span class="ic">▩</span>로그 관리</div>
|
||||
</aside>
|
||||
<main class="content" id="content">USER MODE</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const shell = document.getElementById('shell');
|
||||
const bc = document.getElementById('bc');
|
||||
const badge = document.getElementById('badge');
|
||||
const side = document.getElementById('side');
|
||||
const content = document.getElementById('content');
|
||||
const hdrGlow = document.getElementById('hdrGlow');
|
||||
const toggleBtn = document.getElementById('toggleBtn');
|
||||
|
||||
const userMenus = ['대시보드','유저관리','시스템 관리','화면 관리','자동화 관리','AI 관리','결재 관리','로그 관리'];
|
||||
const adminMenus = ['메뉴 관리','권한 관리','코드 관리','템플릿','감사 로그','테이블 관리','다국어','배치 관리'];
|
||||
const userBC = '홈 › <b>대시보드</b>';
|
||||
const adminBC = '관리자 › <b>메뉴 관리</b>';
|
||||
|
||||
function setMode(toAdmin) {
|
||||
shell.classList.toggle('admin', toAdmin);
|
||||
toggleBtn.textContent = toAdmin ? '홈으로' : '관리자';
|
||||
content.textContent = toAdmin ? 'ADMIN MODE' : 'USER MODE';
|
||||
}
|
||||
|
||||
function swapSidebarItems(toAdmin) {
|
||||
const menus = toAdmin ? adminMenus : userMenus;
|
||||
side.innerHTML = menus.map((m, i) => `<div class="si${i===0?' on':''}"><span class="ic">${'●▣▤▥▦▧▨▩'[i]||'•'}</span>${m}</div>`).join('');
|
||||
}
|
||||
|
||||
function swapBC(toAdmin) {
|
||||
bc.innerHTML = toAdmin ? adminBC : userBC;
|
||||
}
|
||||
|
||||
let isAdmin = false;
|
||||
|
||||
function run(opt) {
|
||||
const toAdmin = !isAdmin;
|
||||
isAdmin = toAdmin;
|
||||
|
||||
// a) Color sweep
|
||||
if (opt === 'a' || opt === 'abd' || opt === 'all') {
|
||||
const sweep = document.createElement('div');
|
||||
sweep.className = 'color-sweep run' + (toAdmin ? ' admin' : '');
|
||||
document.body.appendChild(sweep);
|
||||
setTimeout(() => sweep.remove(), 1300);
|
||||
}
|
||||
|
||||
// b) Sidebar items morph
|
||||
if (opt === 'b' || opt === 'abd' || opt === 'all') {
|
||||
const items = side.querySelectorAll('.si');
|
||||
items.forEach((it, i) => {
|
||||
it.style.animationDelay = (i * 40) + 'ms';
|
||||
it.classList.add('morph-out');
|
||||
});
|
||||
setTimeout(() => {
|
||||
swapSidebarItems(toAdmin);
|
||||
const newItems = side.querySelectorAll('.si');
|
||||
newItems.forEach((it, i) => {
|
||||
it.style.animationDelay = (i * 50) + 'ms';
|
||||
it.classList.add('morph-in');
|
||||
});
|
||||
setTimeout(() => {
|
||||
newItems.forEach(it => { it.classList.remove('morph-in'); it.style.animationDelay = ''; });
|
||||
}, 800);
|
||||
}, 350 + items.length * 40);
|
||||
} else {
|
||||
// morph 안 쓸 때는 즉시 swap
|
||||
setTimeout(() => swapSidebarItems(toAdmin), 200);
|
||||
}
|
||||
|
||||
// c) Header glow
|
||||
if (opt === 'c' || opt === 'all') {
|
||||
hdrGlow.classList.remove('flash');
|
||||
void hdrGlow.offsetWidth;
|
||||
hdrGlow.classList.add('flash');
|
||||
setTimeout(() => hdrGlow.classList.remove('flash'), 1500);
|
||||
}
|
||||
|
||||
// d) Button burst
|
||||
if (opt === 'd' || opt === 'abd' || opt === 'all') {
|
||||
const rect = toggleBtn.getBoundingClientRect();
|
||||
const burst = document.createElement('div');
|
||||
burst.className = 'burst' + (toAdmin ? ' admin' : '');
|
||||
burst.style.left = (rect.left + rect.width / 2) + 'px';
|
||||
burst.style.top = (rect.top + rect.height / 2) + 'px';
|
||||
burst.style.width = '0';
|
||||
burst.style.height = '0';
|
||||
burst.innerHTML = '<div class="burst-ring"></div><div class="burst-ring"></div><div class="burst-ring"></div>';
|
||||
// particles
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const p = document.createElement('div');
|
||||
p.className = 'burst-particle';
|
||||
const angle = (i / 12) * Math.PI * 2;
|
||||
const dist = 80 + Math.random() * 40;
|
||||
p.style.setProperty('--bx', Math.cos(angle) * dist + 'px');
|
||||
p.style.setProperty('--by', Math.sin(angle) * dist + 'px');
|
||||
p.style.animationDelay = (Math.random() * .15) + 's';
|
||||
burst.appendChild(p);
|
||||
}
|
||||
document.body.appendChild(burst);
|
||||
setTimeout(() => burst.remove(), 1200);
|
||||
}
|
||||
|
||||
// e) Breadcrumb swap
|
||||
if (opt === 'e' || opt === 'all') {
|
||||
bc.classList.remove('swap-in', 'swap-out');
|
||||
void bc.offsetWidth;
|
||||
bc.classList.add('swap-out');
|
||||
setTimeout(() => {
|
||||
swapBC(toAdmin);
|
||||
bc.classList.remove('swap-out');
|
||||
bc.classList.add('swap-in');
|
||||
setTimeout(() => bc.classList.remove('swap-in'), 400);
|
||||
}, 250);
|
||||
} else {
|
||||
setTimeout(() => swapBC(toAdmin), 200);
|
||||
}
|
||||
|
||||
// f) Badge zoom
|
||||
if (opt === 'f' || opt === 'all') {
|
||||
if (toAdmin) {
|
||||
// 등장
|
||||
setTimeout(() => {
|
||||
setMode(true);
|
||||
badge.classList.add('zoom-in');
|
||||
setTimeout(() => badge.classList.remove('zoom-in'), 600);
|
||||
}, 100);
|
||||
} else {
|
||||
// 사라짐
|
||||
badge.classList.add('zoom-out');
|
||||
setTimeout(() => {
|
||||
setMode(false);
|
||||
badge.classList.remove('zoom-out');
|
||||
}, 350);
|
||||
}
|
||||
return; // setMode 가 안에서 처리됨
|
||||
}
|
||||
|
||||
// 모드 자체 swap (a/b/c/d/e/abd/all 공통)
|
||||
setTimeout(() => setMode(toAdmin), 200);
|
||||
}
|
||||
|
||||
// 첫 진입은 user mode
|
||||
setMode(false);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user