진행중

This commit is contained in:
2026-04-08 04:55:12 +09:00
parent 4603ac7fd6
commit 06ab990dbd
23 changed files with 3679 additions and 1098 deletions
+74 -19
View File
@@ -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 ? "관리자" : "홈"} &rsaquo; <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>