feat: 운영 안정성과 세션 음성 경험 개선
This commit is contained in:
parent
facc4ad2d9
commit
c788343467
95 changed files with 8431 additions and 1785 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import { useEffect, type ReactNode } from "react";
|
||||
import { useEffect, useLayoutEffect, useRef, type ReactNode } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Topbar } from "./Topbar";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { designRoleOf, useAuth, type Role } from "../../lib/auth";
|
||||
import { canAccessRole, designRoleOf, useAuth, type Role } from "../../lib/auth";
|
||||
|
||||
export interface AppShellProps {
|
||||
children: ReactNode;
|
||||
|
|
@ -28,6 +29,8 @@ export interface AppShellProps {
|
|||
*/
|
||||
export function AppShell({ children, className, contextLabel, hideNav, bleed, wide, hideTopbar, navRole }: AppShellProps) {
|
||||
const { user } = useAuth();
|
||||
const { pathname } = useLocation();
|
||||
const mainRef = useRef<HTMLElement | null>(null);
|
||||
const showNav = !hideNav && !!user;
|
||||
const shellRole = navRole ?? user?.role;
|
||||
const mainClassName = [
|
||||
|
|
@ -47,6 +50,34 @@ export function AppShell({ children, className, contextLabel, hideNav, bleed, wi
|
|||
};
|
||||
}, [shellRole, user]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const resetMainScroll = () => {
|
||||
const main = mainRef.current;
|
||||
if (!main) return;
|
||||
main.scrollTop = 0;
|
||||
main.scrollLeft = 0;
|
||||
};
|
||||
resetMainScroll();
|
||||
const frame = window.requestAnimationFrame(resetMainScroll);
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePageShow = () => {
|
||||
const main = mainRef.current;
|
||||
if (!main) return;
|
||||
main.scrollTop = 0;
|
||||
main.scrollLeft = 0;
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!mainRef.current) return;
|
||||
mainRef.current.scrollTop = 0;
|
||||
mainRef.current.scrollLeft = 0;
|
||||
});
|
||||
};
|
||||
window.addEventListener("pageshow", handlePageShow);
|
||||
return () => window.removeEventListener("pageshow", handlePageShow);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
|
|
@ -60,8 +91,13 @@ export function AppShell({ children, className, contextLabel, hideNav, bleed, wi
|
|||
>
|
||||
{hideTopbar ? null : <Topbar contextLabel={contextLabel} />}
|
||||
<div className={"vg-shell__body" + (showNav ? "" : " vg-shell__body--bare")}>
|
||||
{showNav ? <Sidebar role={shellRole ?? user.role} /> : null}
|
||||
<main className={mainClassName}>
|
||||
{showNav ? (
|
||||
<Sidebar
|
||||
role={shellRole ?? user.role}
|
||||
showAdminEntry={canAccessRole(user, "admin")}
|
||||
/>
|
||||
) : null}
|
||||
<main ref={mainRef} className={mainClassName}>
|
||||
<div className="vg-main__inner">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -43,19 +43,30 @@ const NAV_BY_ROLE: Record<Role, NavItem[]> = {
|
|||
],
|
||||
};
|
||||
|
||||
export function navItemsFor(role: Role): NavItem[] {
|
||||
return NAV_BY_ROLE[role];
|
||||
const ADMIN_ENTRY: NavItem = {
|
||||
to: "/admin",
|
||||
label: "운영 콘솔",
|
||||
icon: "shield",
|
||||
end: true,
|
||||
};
|
||||
|
||||
export function navItemsFor(role: Role, showAdminEntry = false): NavItem[] {
|
||||
const items = NAV_BY_ROLE[role];
|
||||
if (role === "admin" || !showAdminEntry) return items;
|
||||
return [ADMIN_ENTRY, ...items];
|
||||
}
|
||||
|
||||
export interface SidebarProps {
|
||||
role: Role;
|
||||
/** 기본 역할 화면에서도 관리자 권한 사용자가 운영 콘솔로 돌아갈 수 있게 한다. */
|
||||
showAdminEntry?: boolean;
|
||||
/** 상단 그룹 라벨 (기본 "메뉴") */
|
||||
groupLabel?: string;
|
||||
}
|
||||
|
||||
/** 좌측 네비 (240px). 활성 = accent-tint 알약. 강조바 금지. §6.3 */
|
||||
export function Sidebar({ role, groupLabel = "메뉴" }: SidebarProps) {
|
||||
const items = navItemsFor(role);
|
||||
export function Sidebar({ role, showAdminEntry = false, groupLabel = "메뉴" }: SidebarProps) {
|
||||
const items = navItemsFor(role, showAdminEntry);
|
||||
return (
|
||||
<nav className="vg-nav" aria-label="주 메뉴">
|
||||
<span className="vg-nav__label">{groupLabel}</span>
|
||||
|
|
|
|||
|
|
@ -451,17 +451,17 @@ body[data-role] .vg-shell--botanical .vg-topbar__avatar {
|
|||
}
|
||||
body[data-role] .vg-shell--botanical .vg-shell__body:not(.vg-shell__body--bare) {
|
||||
--nav-cur: var(--nav-w);
|
||||
background-color: var(--glass-canvas);
|
||||
/* 흰 종이처럼 보이던 불투명 면을 낮추고, 카드가 떠 보이는 옅은 세이지 캔버스로 정리한다. */
|
||||
background-color: color-mix(in srgb, var(--glass-canvas) 94%, var(--accent));
|
||||
background-image:
|
||||
linear-gradient(var(--botanical-hair), var(--botanical-hair)),
|
||||
radial-gradient(circle at 68% 18%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 34rem),
|
||||
radial-gradient(circle at 42% 72%, color-mix(in srgb, var(--botanical-gold) 13%, transparent), transparent 42rem),
|
||||
linear-gradient(135deg, color-mix(in srgb, var(--bg-app) 28%, transparent), color-mix(in srgb, var(--bg-app) 62%, transparent)),
|
||||
var(--asset-botanical-corner),
|
||||
radial-gradient(circle at 76% 10%, var(--botanical-glow), transparent 30rem);
|
||||
background-repeat: no-repeat;
|
||||
background-position: var(--nav-cur) 0, center top, center bottom, center top, right top, center top;
|
||||
background-size: 1px 100%, 100% 52rem, 100% 62rem, 100% 46rem, min(66vw, 980px) auto, 100% 46rem;
|
||||
background-position: var(--nav-cur) 0, center top, center bottom, right top, center top;
|
||||
background-size: 1px 100%, 100% 52rem, 100% 62rem, min(66vw, 980px) auto, 100% 46rem;
|
||||
}
|
||||
body[data-role] .vg-shell--botanical .vg-nav {
|
||||
top: auto;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue