fix(theme): white text on colored buttons/badges in dark mode (--color-on-accent)
Root cause: dark theme remaps --color-white to --dk-surface (a dark color), so every 'color: var(--color-white)' on a colored background (primary/error/ success buttons, badges, chips, hero CTAs) rendered dark/invisible text in dark mode (e.g. '등록 신청' blue button showed navy text). Add always-white --color-on-accent token and repoint 77 text-color usages across 28 files to it. Background/border usages of --color-white untouched (they legitimately flip to dark surface). Full-CSS audit applied. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
87f310f40a
commit
55e27b3309
@ -1,5 +1,6 @@
|
|||||||
import { Navigate, Outlet, Route, Routes, useNavigate } from 'react-router-dom';
|
import { Navigate, Outlet, Route, Routes, useNavigate } from 'react-router-dom';
|
||||||
import { useAuthStore } from './store/authStore';
|
import { useAuthStore } from './store/authStore';
|
||||||
|
import { landingPathFor } from './lib/roleTrack';
|
||||||
import { LoginPage } from './screens/login/LoginPage';
|
import { LoginPage } from './screens/login/LoginPage';
|
||||||
import { OtpSetupPage } from './screens/login/OtpSetupPage';
|
import { OtpSetupPage } from './screens/login/OtpSetupPage';
|
||||||
import { BoothLayoutEditorPage } from './screens/floorplan/BoothLayoutEditorPage';
|
import { BoothLayoutEditorPage } from './screens/floorplan/BoothLayoutEditorPage';
|
||||||
@ -99,12 +100,16 @@ function RequireAuth({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 루트/미매칭 분기 — 인증=홈 리다이렉트, 미인증=SCR-T0 3-트랙 공개 관문 직접 렌더
|
* 루트/미매칭 분기 — 인증=역할별 랜딩 리다이렉트, 미인증=SCR-T0 3-트랙 공개 관문 직접 렌더
|
||||||
* (design.md §3C-0: 미인증 `/`는 대외 유입 관문. 2026-07-12 소유자 지적 반영).
|
* (design.md §3C-0: 미인증 `/`는 대외 유입 관문. 2026-07-12 소유자 지적 반영).
|
||||||
|
* 인증 시 랜딩은 completeLogin 과 동일 규칙(plan_role_routing §2-1) — ops·business=/home(회귀 0),
|
||||||
|
* admin→/admin, agency→/contractor/dashboard, visitor→/visitor. 로그인 직후·`/`·catch-all 일관.
|
||||||
*/
|
*/
|
||||||
function RootGate() {
|
function RootGate() {
|
||||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||||
if (isAuthenticated) return <Navigate to="/home" replace />;
|
const user = useAuthStore((s) => s.user);
|
||||||
|
const workspaces = useAuthStore((s) => s.workspaces);
|
||||||
|
if (isAuthenticated) return <Navigate to={landingPathFor(user, workspaces)} replace />;
|
||||||
return <PublicGatewayPage />;
|
return <PublicGatewayPage />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
|
import type { Track } from '../../lib/roleTrack';
|
||||||
import { useThemeStore } from '../../store/themeStore';
|
import { useThemeStore } from '../../store/themeStore';
|
||||||
import { useMdiStore } from '../../store/mdiStore';
|
import { useMdiStore } from '../../store/mdiStore';
|
||||||
import { useUiStore, isMobileViewport } from '../../store/uiStore';
|
import { useUiStore, isMobileViewport } from '../../store/uiStore';
|
||||||
@ -55,6 +56,8 @@ interface NavItem {
|
|||||||
scoped?: boolean;
|
scoped?: boolean;
|
||||||
/** true → 클릭 불가한 소그룹 구분 헤더(시스템관리 20항목 정리용). */
|
/** true → 클릭 불가한 소그룹 구분 헤더(시스템관리 20항목 정리용). */
|
||||||
section?: boolean;
|
section?: boolean;
|
||||||
|
/** 노출 허용 트랙(미지정=전 트랙). plan §2-3 항목 단위 필터(예: 플로어플랜=agency 숨김). */
|
||||||
|
tracks?: Track[];
|
||||||
}
|
}
|
||||||
interface NavGroupDef {
|
interface NavGroupDef {
|
||||||
id: string;
|
id: string;
|
||||||
@ -64,6 +67,17 @@ interface NavGroupDef {
|
|||||||
items: NavItem[];
|
items: NavItem[];
|
||||||
/** 관리자 전용 그룹 — useIsAdmin() 시에만 노출. */
|
/** 관리자 전용 그룹 — useIsAdmin() 시에만 노출. */
|
||||||
admin?: boolean;
|
admin?: boolean;
|
||||||
|
/** 노출 허용 트랙(미지정=전 트랙). plan §2-3 그룹 노출 매트릭스. */
|
||||||
|
tracks?: Track[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** plan §2-3 노출 매트릭스 — 트랙 상수(가독성용). system 은 admin 플래그로 별도 게이트. */
|
||||||
|
const BIZ_OPS_ADMIN: Track[] = ['business', 'ops', 'admin'];
|
||||||
|
const WORK_TRACKS: Track[] = ['business', 'ops', 'agency', 'admin'];
|
||||||
|
|
||||||
|
/** tracks 미지정(전 트랙 허용) 또는 현재 트랙 포함 시 노출. */
|
||||||
|
function trackAllows(tracks: Track[] | undefined, active: Track): boolean {
|
||||||
|
return !tracks || tracks.includes(active);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 홈 — 로그인 후 랜딩(단일 최상위 링크, 아코디언 밖). */
|
/** 홈 — 로그인 후 랜딩(단일 최상위 링크, 아코디언 밖). */
|
||||||
@ -80,6 +94,7 @@ const GROUPS: NavGroupDef[] = [
|
|||||||
label: '전시 운영',
|
label: '전시 운영',
|
||||||
labelKey: 'shell.menu.groups.ops',
|
labelKey: 'shell.menu.groups.ops',
|
||||||
Icon: IconOperations,
|
Icon: IconOperations,
|
||||||
|
tracks: BIZ_OPS_ADMIN, // agency·visitor ✕
|
||||||
items: [
|
items: [
|
||||||
{ key: 'dashboard', label: '대시보드', labelKey: 'shell.menu.items.dashboard', Icon: IconDashboard, scoped: true },
|
{ key: 'dashboard', label: '대시보드', labelKey: 'shell.menu.items.dashboard', Icon: IconDashboard, scoped: true },
|
||||||
{ key: 'exhibitors', label: '참가업체 관리', labelKey: 'shell.menu.items.exhibitors', Icon: IconExhibitors, scoped: true },
|
{ key: 'exhibitors', label: '참가업체 관리', labelKey: 'shell.menu.items.exhibitors', Icon: IconExhibitors, scoped: true },
|
||||||
@ -94,8 +109,10 @@ const GROUPS: NavGroupDef[] = [
|
|||||||
label: '설계·시공',
|
label: '설계·시공',
|
||||||
labelKey: 'shell.menu.groups.design',
|
labelKey: 'shell.menu.groups.design',
|
||||||
Icon: IconFloorplan,
|
Icon: IconFloorplan,
|
||||||
|
tracks: WORK_TRACKS, // business·ops·admin ● + agency 부분(항목 단위)
|
||||||
items: [
|
items: [
|
||||||
{ key: 'floorplan', label: '플로어플랜 스튜디오', labelKey: 'shell.menu.items.floorplan', Icon: IconFloorplan, scoped: true },
|
// 플로어플랜 편집은 agency 숨김(주최자·홀매니저·관리자 전용).
|
||||||
|
{ key: 'floorplan', label: '플로어플랜 스튜디오', labelKey: 'shell.menu.items.floorplan', Icon: IconFloorplan, scoped: true, tracks: BIZ_OPS_ADMIN },
|
||||||
{ key: 'documents', label: '서류·마일스톤', labelKey: 'shell.menu.items.documents', Icon: IconDocument, to: '/docs' },
|
{ key: 'documents', label: '서류·마일스톤', labelKey: 'shell.menu.items.documents', Icon: IconDocument, to: '/docs' },
|
||||||
{ key: 'auctions', label: '공사 옥션', labelKey: 'shell.menu.items.auctions', Icon: IconSettlement, to: '/auctions' },
|
{ key: 'auctions', label: '공사 옥션', labelKey: 'shell.menu.items.auctions', Icon: IconSettlement, to: '/auctions' },
|
||||||
{ key: 'contractor', label: '수주 부스', labelKey: 'shell.menu.items.contractor', Icon: IconExhibitors, to: '/contractor/dashboard' },
|
{ key: 'contractor', label: '수주 부스', labelKey: 'shell.menu.items.contractor', Icon: IconExhibitors, to: '/contractor/dashboard' },
|
||||||
@ -107,6 +124,7 @@ const GROUPS: NavGroupDef[] = [
|
|||||||
label: '관람객·마케팅',
|
label: '관람객·마케팅',
|
||||||
labelKey: 'shell.menu.groups.visitor',
|
labelKey: 'shell.menu.groups.visitor',
|
||||||
Icon: IconUsers,
|
Icon: IconUsers,
|
||||||
|
tracks: BIZ_OPS_ADMIN, // agency·visitor ✕
|
||||||
items: [
|
items: [
|
||||||
{ key: 'visitors', label: '관람객', labelKey: 'shell.menu.items.visitors', Icon: IconUsers, to: '/visitors' },
|
{ key: 'visitors', label: '관람객', labelKey: 'shell.menu.items.visitors', Icon: IconUsers, to: '/visitors' },
|
||||||
{ key: 'checkin', label: '체크인 데스크', labelKey: 'shell.menu.items.checkin', Icon: IconCheckCircle, to: '/visitors/checkin' },
|
{ key: 'checkin', label: '체크인 데스크', labelKey: 'shell.menu.items.checkin', Icon: IconCheckCircle, to: '/visitors/checkin' },
|
||||||
@ -121,6 +139,7 @@ const GROUPS: NavGroupDef[] = [
|
|||||||
label: '정산·분석',
|
label: '정산·분석',
|
||||||
labelKey: 'shell.menu.groups.finance',
|
labelKey: 'shell.menu.groups.finance',
|
||||||
Icon: IconAnalytics,
|
Icon: IconAnalytics,
|
||||||
|
tracks: BIZ_OPS_ADMIN, // agency·visitor ✕
|
||||||
items: [
|
items: [
|
||||||
{ key: 'settlement', label: '정산', labelKey: 'shell.menu.items.settlement', Icon: IconSettlement, to: '/settlement' },
|
{ key: 'settlement', label: '정산', labelKey: 'shell.menu.items.settlement', Icon: IconSettlement, to: '/settlement' },
|
||||||
{ key: 'analytics', label: '경영분석', labelKey: 'shell.menu.items.analytics', Icon: IconAnalytics, to: '/analytics' },
|
{ key: 'analytics', label: '경영분석', labelKey: 'shell.menu.items.analytics', Icon: IconAnalytics, to: '/analytics' },
|
||||||
@ -131,6 +150,7 @@ const GROUPS: NavGroupDef[] = [
|
|||||||
label: '업무 공통',
|
label: '업무 공통',
|
||||||
labelKey: 'shell.menu.groups.work',
|
labelKey: 'shell.menu.groups.work',
|
||||||
Icon: IconDocument,
|
Icon: IconDocument,
|
||||||
|
tracks: WORK_TRACKS, // 전 업무 트랙 ● (visitor ✕)
|
||||||
items: [
|
items: [
|
||||||
{ key: 'worklog', label: '업무일지', labelKey: 'shell.menu.items.worklog', Icon: IconDocument, to: '/work/worklog' },
|
{ key: 'worklog', label: '업무일지', labelKey: 'shell.menu.items.worklog', Icon: IconDocument, to: '/work/worklog' },
|
||||||
{ key: 'work-schedule', label: '일정', labelKey: 'shell.menu.items.workSchedule', Icon: IconCalendar, to: '/work/schedule' },
|
{ key: 'work-schedule', label: '일정', labelKey: 'shell.menu.items.workSchedule', Icon: IconCalendar, to: '/work/schedule' },
|
||||||
@ -232,6 +252,7 @@ export function AppShell() {
|
|||||||
const openTab = useMdiStore((s) => s.openTab);
|
const openTab = useMdiStore((s) => s.openTab);
|
||||||
const resolvedEventId = useResolvedEventId();
|
const resolvedEventId = useResolvedEventId();
|
||||||
const isAdmin = useIsAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
|
const activeTrack = useAuthStore((s) => s.activeTrack());
|
||||||
const isDark = themeResolved === 'dark';
|
const isDark = themeResolved === 'dark';
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -274,7 +295,10 @@ export function AppShell() {
|
|||||||
closeOnMobile();
|
closeOnMobile();
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleGroups = GROUPS.filter((g) => !g.admin || isAdmin);
|
// 관리자 그룹은 isAdmin 게이트 유지 + 트랙 노출 매트릭스(plan §2-3). 미지정 그룹=전 트랙.
|
||||||
|
const visibleGroups = GROUPS.filter(
|
||||||
|
(g) => (!g.admin || isAdmin) && trackAllows(g.tracks, activeTrack),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`kx-shell ${navCollapsed ? 'is-nav-collapsed' : ''}`}>
|
<div className={`kx-shell ${navCollapsed ? 'is-nav-collapsed' : ''}`}>
|
||||||
@ -343,7 +367,9 @@ export function AppShell() {
|
|||||||
</button>
|
</button>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<ul className="kx-shell__nav kx-shell__nav--sub">
|
<ul className="kx-shell__nav kx-shell__nav--sub">
|
||||||
{group.items.map((item) =>
|
{group.items
|
||||||
|
.filter((item) => trackAllows(item.tracks, activeTrack))
|
||||||
|
.map((item) =>
|
||||||
item.section ? (
|
item.section ? (
|
||||||
// 소그룹 구분 헤더(클릭 불가) — 시스템관리 20항목 정리(WISE 구분선 패턴).
|
// 소그룹 구분 헤더(클릭 불가) — 시스템관리 20항목 정리(WISE 구분선 패턴).
|
||||||
<li key={item.key} className="kx-shell__nav-section" aria-hidden="true">
|
<li key={item.key} className="kx-shell__nav-section" aria-hidden="true">
|
||||||
|
|||||||
@ -89,7 +89,7 @@
|
|||||||
.kx-aiimg__shot,
|
.kx-aiimg__shot,
|
||||||
.kx-aiimg__ver {
|
.kx-aiimg__ver {
|
||||||
background: rgba(16, 24, 40, 0.72);
|
background: rgba(16, 24, 40, 0.72);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@ -97,7 +97,7 @@
|
|||||||
}
|
}
|
||||||
.kx-aiimg__sample {
|
.kx-aiimg__sample {
|
||||||
background: rgba(109, 74, 255, 0.9);
|
background: rgba(109, 74, 255, 0.9);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@ -111,7 +111,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
background: var(--color-ai-accent);
|
background: var(--color-ai-accent);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
.kx-btn--primary {
|
.kx-btn--primary {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kx-btn--primary:not(:disabled):hover {
|
.kx-btn--primary:not(:disabled):hover {
|
||||||
background: var(--color-primary-700);
|
background: var(--color-primary-700);
|
||||||
@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
.kx-btn--ai {
|
.kx-btn--ai {
|
||||||
background: var(--color-ai-accent);
|
background: var(--color-ai-accent);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
box-shadow: 0 0 12px var(--ai-glow);
|
box-shadow: 0 0 12px var(--ai-glow);
|
||||||
}
|
}
|
||||||
.kx-btn--ai:not(:disabled):hover {
|
.kx-btn--ai:not(:disabled):hover {
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
font-size: var(--fs-caption);
|
font-size: var(--fs-caption);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
background: rgba(16, 24, 40, 0.72);
|
background: rgba(16, 24, 40, 0.72);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
70
src/frontend/src/lib/roleTrack.ts
Normal file
70
src/frontend/src/lib/roleTrack.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 역할별 랜딩·트랙 판정 (단일 출처) — _workspace/plan_role_routing.md §1 로직 그대로.
|
||||||
|
* 웹·모바일 공통 규칙. 순수 함수 + authStore 파생 셀렉터(activeTrack)에서 재사용.
|
||||||
|
*
|
||||||
|
* 우선순위: admin > ops > business > agency > visitor.
|
||||||
|
* 회귀 0 원칙: ops·business 랜딩은 기존 `/home` 유지, admin·agency·visitor 만 신규 분기.
|
||||||
|
*/
|
||||||
|
import type { AuthUser, EventRole, WorkspaceDto } from '../api/types';
|
||||||
|
|
||||||
|
export type Track = 'visitor' | 'business' | 'agency' | 'ops' | 'admin';
|
||||||
|
|
||||||
|
/** 워크스페이스 역할 → 트랙(스코프 판정용). */
|
||||||
|
export function trackOf(role: EventRole | null | undefined): Track {
|
||||||
|
switch (role) {
|
||||||
|
case 'ORGANIZER':
|
||||||
|
case 'EXHIBITOR':
|
||||||
|
return 'business';
|
||||||
|
case 'CONTRACTOR':
|
||||||
|
return 'agency';
|
||||||
|
case 'HALL_MANAGER':
|
||||||
|
return 'ops';
|
||||||
|
default:
|
||||||
|
return 'visitor';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 랜딩 트랙 결정(primaryTrack 우선순위). plan §1 resolveLandingTrack 이식.
|
||||||
|
* 판정 불가(예외) 시 안전 폴백은 호출부에서 처리(랜딩 경로 /home).
|
||||||
|
*/
|
||||||
|
export function resolveLandingTrack(
|
||||||
|
user: AuthUser | null | undefined,
|
||||||
|
workspaces: WorkspaceDto[],
|
||||||
|
): Track {
|
||||||
|
const role = (user?.roleCode ?? '').toUpperCase();
|
||||||
|
const ws = workspaces ?? [];
|
||||||
|
if (role === 'ADMIN') return 'admin';
|
||||||
|
if (role === 'MANAGER' || ws.some((w) => w.myRole === 'HALL_MANAGER')) return 'ops';
|
||||||
|
if (ws.some((w) => w.myRole === 'ORGANIZER' || w.myRole === 'EXHIBITOR')) return 'business';
|
||||||
|
if (ws.some((w) => w.myRole === 'CONTRACTOR')) return 'agency';
|
||||||
|
return 'visitor';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 트랙 → 로그인 직후 랜딩 라우트. ops·business=/home(회귀 0), admin·agency·visitor 만 신규. */
|
||||||
|
export function landingPathForTrack(track: Track): string {
|
||||||
|
switch (track) {
|
||||||
|
case 'admin':
|
||||||
|
return '/admin';
|
||||||
|
case 'agency':
|
||||||
|
return '/contractor/dashboard';
|
||||||
|
case 'visitor':
|
||||||
|
return '/visitor';
|
||||||
|
case 'ops':
|
||||||
|
case 'business':
|
||||||
|
default:
|
||||||
|
return '/home';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 사용자·워크스페이스로부터 랜딩 라우트 계산(예외 시 /home 폴백). */
|
||||||
|
export function landingPathFor(
|
||||||
|
user: AuthUser | null | undefined,
|
||||||
|
workspaces: WorkspaceDto[],
|
||||||
|
): string {
|
||||||
|
try {
|
||||||
|
return landingPathForTrack(resolveLandingTrack(user, workspaces));
|
||||||
|
} catch {
|
||||||
|
return '/home';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -390,8 +390,8 @@
|
|||||||
color: var(--color-neutral-500);
|
color: var(--color-neutral-500);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.kx-adm-wiz__step.is-done .kx-adm-wiz__dot { background: var(--color-success); color: var(--color-white); }
|
.kx-adm-wiz__step.is-done .kx-adm-wiz__dot { background: var(--color-success); color: var(--color-on-accent); }
|
||||||
.kx-adm-wiz__step.is-current .kx-adm-wiz__dot { background: var(--color-primary-600); color: var(--color-white); }
|
.kx-adm-wiz__step.is-current .kx-adm-wiz__dot { background: var(--color-primary-600); color: var(--color-on-accent); }
|
||||||
.kx-adm-wiz__body { display: flex; flex-direction: column; gap: 2px; padding-top: 3px; }
|
.kx-adm-wiz__body { display: flex; flex-direction: column; gap: 2px; padding-top: 3px; }
|
||||||
.kx-adm-wiz__label { font-size: var(--fs-body); font-weight: 600; color: var(--color-neutral-900); }
|
.kx-adm-wiz__label { font-size: var(--fs-body); font-weight: 600; color: var(--color-neutral-900); }
|
||||||
.kx-adm-wiz__step.is-todo .kx-adm-wiz__label { color: var(--color-neutral-500); }
|
.kx-adm-wiz__step.is-todo .kx-adm-wiz__label { color: var(--color-neutral-500); }
|
||||||
|
|||||||
@ -82,7 +82,7 @@
|
|||||||
.kx-appqr__ver {
|
.kx-appqr__ver {
|
||||||
font-size: var(--fs-caption);
|
font-size: var(--fs-caption);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
|
|||||||
@ -102,7 +102,7 @@
|
|||||||
}
|
}
|
||||||
.kx-rev__tabs button.is-active {
|
.kx-rev__tabs button.is-active {
|
||||||
background: rgba(0, 102, 179, 0.35);
|
background: rgba(0, 102, 179, 0.35);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
border: 1px solid rgba(0, 102, 179, 0.5);
|
border: 1px solid rgba(0, 102, 179, 0.5);
|
||||||
}
|
}
|
||||||
.kx-rev__overlay-toggle {
|
.kx-rev__overlay-toggle {
|
||||||
@ -112,7 +112,7 @@
|
|||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
border: 1px solid var(--color-ai-accent);
|
border: 1px solid var(--color-ai-accent);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
font-size: var(--fs-caption);
|
font-size: var(--fs-caption);
|
||||||
@ -182,7 +182,7 @@
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--color-error);
|
background: var(--color-error);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -206,7 +206,7 @@
|
|||||||
.kx-rev__zoom button {
|
.kx-rev__zoom button {
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|||||||
@ -232,7 +232,7 @@
|
|||||||
}
|
}
|
||||||
.kx-auc-open__head {
|
.kx-auc-open__head {
|
||||||
background: var(--color-primary-700);
|
background: var(--color-primary-700);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
}
|
}
|
||||||
.kx-auc-open__head h2 {
|
.kx-auc-open__head h2 {
|
||||||
@ -631,7 +631,7 @@
|
|||||||
}
|
}
|
||||||
.kx-rank__row--me .kx-rank__badge {
|
.kx-rank__row--me .kx-rank__badge {
|
||||||
background: var(--color-ai-accent);
|
background: var(--color-ai-accent);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kx-rank__main {
|
.kx-rank__main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -797,7 +797,7 @@
|
|||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
background: var(--color-primary-700);
|
background: var(--color-primary-700);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
@ -902,7 +902,7 @@
|
|||||||
height: 44px;
|
height: 44px;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
background: var(--color-primary-700);
|
background: var(--color-primary-700);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -1008,7 +1008,7 @@
|
|||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: var(--color-neutral-900);
|
background: var(--color-neutral-900);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
font-size: var(--fs-body);
|
font-size: var(--fs-body);
|
||||||
box-shadow: var(--shadow-level2);
|
box-shadow: var(--shadow-level2);
|
||||||
|
|||||||
@ -470,7 +470,7 @@
|
|||||||
.kx-cms-step.is-done .kx-cms-step__dot {
|
.kx-cms-step.is-done .kx-cms-step__dot {
|
||||||
background: var(--color-success);
|
background: var(--color-success);
|
||||||
border-color: var(--color-success);
|
border-color: var(--color-success);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kx-cms-step.is-current {
|
.kx-cms-step.is-current {
|
||||||
color: var(--color-primary-700);
|
color: var(--color-primary-700);
|
||||||
@ -688,7 +688,7 @@
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -707,7 +707,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--space-6);
|
padding: var(--space-6);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
background:
|
background:
|
||||||
linear-gradient(90deg, rgba(0, 76, 134, 0.92), rgba(0, 76, 134, 0.35)),
|
linear-gradient(90deg, rgba(0, 76, 134, 0.92), rgba(0, 76, 134, 0.35)),
|
||||||
linear-gradient(135deg, var(--color-primary-700), var(--color-canvas-bg));
|
linear-gradient(135deg, var(--color-primary-700), var(--color-canvas-bg));
|
||||||
@ -754,7 +754,7 @@
|
|||||||
}
|
}
|
||||||
.kx-mb-btn--ghost {
|
.kx-mb-btn--ghost {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
border-color: rgba(255, 255, 255, 0.4);
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
}
|
}
|
||||||
.kx-mb-section {
|
.kx-mb-section {
|
||||||
@ -807,7 +807,7 @@
|
|||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,7 +134,7 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: var(--color-error);
|
background: var(--color-error);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kx-comp__item.is-warn .kx-comp__pin {
|
.kx-comp__item.is-warn .kx-comp__pin {
|
||||||
background: var(--color-violation-warn);
|
background: var(--color-violation-warn);
|
||||||
@ -233,7 +233,7 @@
|
|||||||
right: 6%;
|
right: 6%;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
background: var(--color-error);
|
background: var(--color-error);
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
|||||||
@ -154,11 +154,11 @@
|
|||||||
}
|
}
|
||||||
.kx-step--done .kx-step__dot {
|
.kx-step--done .kx-step__dot {
|
||||||
background: var(--color-primary-700);
|
background: var(--color-primary-700);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kx-step--current .kx-step__dot {
|
.kx-step--current .kx-step__dot {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
box-shadow: 0 0 0 4px var(--color-primary-100);
|
box-shadow: 0 0 0 4px var(--color-primary-100);
|
||||||
}
|
}
|
||||||
.kx-step__label {
|
.kx-step__label {
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
padding: 8px 18px;
|
padding: 8px 18px;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-size: var(--fs-h2);
|
font-size: var(--fs-h2);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@ -79,7 +79,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
background: var(--color-neutral-200);
|
background: var(--color-neutral-200);
|
||||||
}
|
}
|
||||||
.kx-timeline__node.is-done .kx-timeline__dot {
|
.kx-timeline__node.is-done .kx-timeline__dot {
|
||||||
|
|||||||
@ -109,7 +109,7 @@
|
|||||||
.kx-stepper__item.is-done .kx-stepper__dot {
|
.kx-stepper__item.is-done .kx-stepper__dot {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
border-color: var(--color-primary-600);
|
border-color: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kx-stepper__item.is-current .kx-stepper__dot {
|
.kx-stepper__item.is-current .kx-stepper__dot {
|
||||||
border-color: var(--color-primary-600);
|
border-color: var(--color-primary-600);
|
||||||
|
|||||||
@ -363,7 +363,7 @@
|
|||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@ -63,7 +63,7 @@
|
|||||||
.kx-gallery__chip.is-active {
|
.kx-gallery__chip.is-active {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
border-color: var(--color-primary-600);
|
border-color: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 마소너리(간이 — CSS columns) */
|
/* 마소너리(간이 — CSS columns) */
|
||||||
|
|||||||
@ -366,7 +366,7 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
background: var(--color-neutral-900);
|
background: var(--color-neutral-900);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
padding: 10px 18px;
|
padding: 10px 18px;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
font-size: var(--fs-caption);
|
font-size: var(--fs-caption);
|
||||||
|
|||||||
@ -1132,7 +1132,7 @@
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-ai-accent);
|
background: var(--color-ai-accent);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
.kx-home__aihub-heading {
|
.kx-home__aihub-heading {
|
||||||
@ -1239,7 +1239,7 @@
|
|||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
background: var(--color-ai-accent);
|
background: var(--color-ai-accent);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kx-home__aihub-card-desc {
|
.kx-home__aihub-card-desc {
|
||||||
font-size: var(--fs-caption);
|
font-size: var(--fs-caption);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { authApi } from '../../api/endpoints';
|
import { authApi } from '../../api/endpoints';
|
||||||
import { ApiRequestError } from '../../api/client';
|
import { ApiRequestError } from '../../api/client';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
|
import { landingPathFor } from '../../lib/roleTrack';
|
||||||
import { Button } from '../../components/ui/Button';
|
import { Button } from '../../components/ui/Button';
|
||||||
import { LanguageSwitcher } from '../../components/ui/LanguageSwitcher';
|
import { LanguageSwitcher } from '../../components/ui/LanguageSwitcher';
|
||||||
import { InviteDialog } from './InviteDialog';
|
import { InviteDialog } from './InviteDialog';
|
||||||
@ -48,14 +49,15 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 최종 로그인 확정(공통) — 토큰 적용 후 메인 홈(SCR-HOME)으로 랜딩(v2.1.2).
|
// 최종 로그인 확정(공통) — 토큰 적용 후 역할별 메인으로 랜딩(plan_role_routing §2-1).
|
||||||
// 회원가입 직후 흐름도 로그인 성공 경로를 공유하므로 동일하게 /home 으로 랜딩한다.
|
// ops·business=/home(회귀 0), admin→/admin, agency→/contractor/dashboard, visitor→/visitor.
|
||||||
|
// 판정 불가 시 안전 폴백 /home. 회원가입 직후 흐름도 동일 경로를 공유한다.
|
||||||
function completeLogin(res: LoginResponse) {
|
function completeLogin(res: LoginResponse) {
|
||||||
if (remember) localStorage.setItem(REMEMBER_KEY, email.trim());
|
if (remember) localStorage.setItem(REMEMBER_KEY, email.trim());
|
||||||
else localStorage.removeItem(REMEMBER_KEY);
|
else localStorage.removeItem(REMEMBER_KEY);
|
||||||
applyLogin(res);
|
applyLogin(res);
|
||||||
setOtpChallenge(null);
|
setOtpChallenge(null);
|
||||||
navigate('/home');
|
navigate(landingPathFor(res.user, res.workspaces));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLogin(e: React.FormEvent) {
|
async function handleLogin(e: React.FormEvent) {
|
||||||
|
|||||||
@ -103,7 +103,7 @@
|
|||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
.kx-login__headline {
|
.kx-login__headline {
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-size: var(--fs-display);
|
font-size: var(--fs-display);
|
||||||
line-height: var(--lh-display);
|
line-height: var(--lh-display);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -149,7 +149,7 @@
|
|||||||
height: 36px;
|
height: 36px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -321,7 +321,7 @@
|
|||||||
height: 56px;
|
height: 56px;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
background: var(--color-success, #16a34a);
|
background: var(--color-success, #16a34a);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@ -661,7 +661,7 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kx-mkt__check.is-done {
|
.kx-mkt__check.is-done {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
|
|||||||
@ -312,7 +312,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-size: var(--fs-h3);
|
font-size: var(--fs-h3);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -488,7 +488,7 @@
|
|||||||
}
|
}
|
||||||
.kx-m__verdict button.is-fail {
|
.kx-m__verdict button.is-fail {
|
||||||
background: var(--color-error);
|
background: var(--color-error);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kx-m__icard-evidence {
|
.kx-m__icard-evidence {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -126,7 +126,7 @@
|
|||||||
right: var(--space-3);
|
right: var(--space-3);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
background: var(--color-error);
|
background: var(--color-error);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
|
|||||||
@ -76,14 +76,14 @@
|
|||||||
}
|
}
|
||||||
.kxp-btn--primary {
|
.kxp-btn--primary {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kxp-btn--primary:hover {
|
.kxp-btn--primary:hover {
|
||||||
background: var(--color-primary-700);
|
background: var(--color-primary-700);
|
||||||
}
|
}
|
||||||
.kxp-btn--ghost {
|
.kxp-btn--ghost {
|
||||||
background: rgba(255, 255, 255, 0.14);
|
background: rgba(255, 255, 255, 0.14);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
border-color: rgba(255, 255, 255, 0.35);
|
border-color: rgba(255, 255, 255, 0.35);
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
@ -106,7 +106,7 @@
|
|||||||
}
|
}
|
||||||
.kxp-btn--danger:hover {
|
.kxp-btn--danger:hover {
|
||||||
background: var(--color-error);
|
background: var(--color-error);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kxp-btn--lg {
|
.kxp-btn--lg {
|
||||||
padding: 16px 28px;
|
padding: 16px 28px;
|
||||||
@ -145,7 +145,7 @@
|
|||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kxp-dday--warn {
|
.kxp-dday--warn {
|
||||||
background: var(--color-warning);
|
background: var(--color-warning);
|
||||||
@ -435,7 +435,7 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
background: rgba(255, 255, 255, 0.16);
|
background: rgba(255, 255, 255, 0.16);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
padding: 6px 14px;
|
padding: 6px 14px;
|
||||||
@ -445,7 +445,7 @@
|
|||||||
font-size: clamp(30px, 6vw, 52px);
|
font-size: clamp(30px, 6vw, 52px);
|
||||||
line-height: 1.12;
|
line-height: 1.12;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@ -472,7 +472,7 @@
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kxp-hero__metacard small {
|
.kxp-hero__metacard small {
|
||||||
display: block;
|
display: block;
|
||||||
@ -908,7 +908,7 @@
|
|||||||
.kxp-footer__logo {
|
.kxp-footer__logo {
|
||||||
font-size: var(--fs-h2);
|
font-size: var(--fs-h2);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.kxp-footer__desc {
|
.kxp-footer__desc {
|
||||||
@ -928,7 +928,7 @@
|
|||||||
}
|
}
|
||||||
.kxp-footer__col h4 {
|
.kxp-footer__col h4 {
|
||||||
font-size: var(--fs-body);
|
font-size: var(--fs-body);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
}
|
}
|
||||||
.kxp-footer__col a,
|
.kxp-footer__col a,
|
||||||
@ -937,7 +937,7 @@
|
|||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
.kxp-footer__col a:hover {
|
.kxp-footer__col a:hover {
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kxp-footer__bottom {
|
.kxp-footer__bottom {
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
@ -1147,7 +1147,7 @@
|
|||||||
}
|
}
|
||||||
.kxp-daytab.is-active {
|
.kxp-daytab.is-active {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
border-color: var(--color-primary-600);
|
border-color: var(--color-primary-600);
|
||||||
}
|
}
|
||||||
/* 연사 카드 */
|
/* 연사 카드 */
|
||||||
@ -1299,7 +1299,7 @@
|
|||||||
.kxp-fp__chip.is-active {
|
.kxp-fp__chip.is-active {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
border-color: var(--color-primary-600);
|
border-color: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kxp-fp__list {
|
.kxp-fp__list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -1430,7 +1430,7 @@
|
|||||||
right: 3px;
|
right: 3px;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
background: var(--color-error);
|
background: var(--color-error);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
@ -1708,7 +1708,7 @@
|
|||||||
.kxp-step.is-done .kxp-step__dot {
|
.kxp-step.is-done .kxp-step__dot {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
border-color: var(--color-primary-600);
|
border-color: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kxp-step__label {
|
.kxp-step__label {
|
||||||
font-size: var(--fs-caption);
|
font-size: var(--fs-caption);
|
||||||
@ -1780,7 +1780,7 @@
|
|||||||
}
|
}
|
||||||
.kxp-typecard.is-active .kxp-typecard__ic {
|
.kxp-typecard.is-active .kxp-typecard__ic {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
border-color: var(--color-primary-600);
|
border-color: var(--color-primary-600);
|
||||||
}
|
}
|
||||||
.kxp-typecard h3 {
|
.kxp-typecard h3 {
|
||||||
@ -1812,7 +1812,7 @@
|
|||||||
.kxp-interest.is-active {
|
.kxp-interest.is-active {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
border-color: var(--color-primary-600);
|
border-color: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kxp-regnav {
|
.kxp-regnav {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -1847,7 +1847,7 @@
|
|||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.kxp-badge__top {
|
.kxp-badge__top {
|
||||||
@ -2029,7 +2029,7 @@
|
|||||||
}
|
}
|
||||||
.kxp-cta-banner {
|
.kxp-cta-banner {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 64px 16px;
|
padding: 64px 16px;
|
||||||
}
|
}
|
||||||
@ -3250,7 +3250,7 @@
|
|||||||
max-width: 860px;
|
max-width: 860px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kxp-vhero__title {
|
.kxp-vhero__title {
|
||||||
font-size: clamp(28px, 5.2vw, 46px);
|
font-size: clamp(28px, 5.2vw, 46px);
|
||||||
@ -3258,7 +3258,7 @@
|
|||||||
line-height: 1.18;
|
line-height: 1.18;
|
||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
margin: 8px 0 12px;
|
margin: 8px 0 12px;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kxp-vhero__lead {
|
.kxp-vhero__lead {
|
||||||
font-size: clamp(15px, 2.2vw, 19px);
|
font-size: clamp(15px, 2.2vw, 19px);
|
||||||
@ -3321,7 +3321,7 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--color-ai-accent);
|
background: var(--color-ai-accent);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s ease;
|
transition: background 0.15s ease;
|
||||||
}
|
}
|
||||||
@ -3347,7 +3347,7 @@
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
background: rgba(255, 255, 255, 0.14);
|
background: rgba(255, 255, 255, 0.14);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -3488,7 +3488,7 @@
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.kxp-ai__evfallback {
|
.kxp-ai__evfallback {
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
@ -3907,7 +3907,7 @@ a.kxp-ai__cite:hover {
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -210,7 +210,7 @@
|
|||||||
}
|
}
|
||||||
.kx-cal__cell.is-today {
|
.kx-cal__cell.is-today {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.kx-cal__cell.is-today.has-event::after {
|
.kx-cal__cell.is-today.has-event::after {
|
||||||
|
|||||||
@ -244,7 +244,7 @@
|
|||||||
.kx-sg__toast {
|
.kx-sg__toast {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: var(--color-neutral-900);
|
background: var(--color-neutral-900);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-size: var(--fs-body);
|
font-size: var(--fs-body);
|
||||||
padding: 10px var(--space-4);
|
padding: 10px var(--space-4);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
|||||||
@ -63,7 +63,7 @@
|
|||||||
.kx-uord__wizard li.is-done::before {
|
.kx-uord__wizard li.is-done::before {
|
||||||
content: '✓';
|
content: '✓';
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kx-uord__wizard li.is-current {
|
.kx-uord__wizard li.is-current {
|
||||||
color: var(--color-primary-700);
|
color: var(--color-primary-700);
|
||||||
@ -71,7 +71,7 @@
|
|||||||
}
|
}
|
||||||
.kx-uord__wizard li.is-current::before {
|
.kx-uord__wizard li.is-current::before {
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.kx-uord__body {
|
.kx-uord__body {
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
font-size: var(--fs-caption);
|
font-size: var(--fs-caption);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
.kx-wire__legend-item::before {
|
.kx-wire__legend-item::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
|||||||
@ -248,7 +248,7 @@
|
|||||||
.kx-badgecard__top {
|
.kx-badgecard__top {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.14em;
|
letter-spacing: 0.14em;
|
||||||
@ -280,7 +280,7 @@
|
|||||||
.kx-badgecard__tag {
|
.kx-badgecard__tag {
|
||||||
font-size: 7px;
|
font-size: 7px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
background: var(--color-ai-accent);
|
background: var(--color-ai-accent);
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
@ -490,7 +490,7 @@
|
|||||||
.kx-gauge__num {
|
.kx-gauge__num {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--radius-pill);
|
||||||
padding: 1px 8px;
|
padding: 1px 8px;
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { getAccessToken, setAccessToken } from '../api/client';
|
import { getAccessToken, setAccessToken } from '../api/client';
|
||||||
import type { AuthUser, EventRole, LoginResponse, WorkspaceDto } from '../api/types';
|
import type { AuthUser, EventRole, LoginResponse, WorkspaceDto } from '../api/types';
|
||||||
|
import { resolveLandingTrack, trackOf, type Track } from '../lib/roleTrack';
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
user: AuthUser | null;
|
user: AuthUser | null;
|
||||||
@ -18,6 +19,8 @@ interface AuthState {
|
|||||||
roleForCurrent: () => EventRole | null;
|
roleForCurrent: () => EventRole | null;
|
||||||
// 전역 역할(app_user.role_code — 'ADMIN' | 'MANAGER' | …). 없으면 null. 관리자 판정(AdminGuard) 등에 사용.
|
// 전역 역할(app_user.role_code — 'ADMIN' | 'MANAGER' | …). 없으면 null. 관리자 판정(AdminGuard) 등에 사용.
|
||||||
globalRole: () => string | null;
|
globalRole: () => string | null;
|
||||||
|
// 반응형 활성 트랙(메뉴/탭 스코프). 행사 선택 시 해당 역할 트랙, 아니면 랜딩 트랙(plan §1).
|
||||||
|
activeTrack: () => Track;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||||
@ -50,4 +53,14 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
roleForCurrent: () => get().currentWorkspace()?.myRole ?? null,
|
roleForCurrent: () => get().currentWorkspace()?.myRole ?? null,
|
||||||
|
|
||||||
globalRole: () => get().user?.roleCode ?? null,
|
globalRole: () => get().user?.roleCode ?? null,
|
||||||
|
|
||||||
|
activeTrack: () => {
|
||||||
|
const { user, workspaces, currentEventId } = get();
|
||||||
|
// 행사 선택 시 그 역할 트랙 우선(단 현재 워크스페이스 해소 가능한 경우).
|
||||||
|
if (currentEventId) {
|
||||||
|
const ws = workspaces.find((w) => w.eventId === currentEventId);
|
||||||
|
if (ws) return trackOf(ws.myRole);
|
||||||
|
}
|
||||||
|
return resolveLandingTrack(user, workspaces);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -220,12 +220,12 @@ textarea {
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-primary-600);
|
background: var(--color-primary-600);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
font-size: var(--fs-body);
|
font-size: var(--fs-body);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: top 0.15s ease;
|
transition: top 0.15s ease;
|
||||||
}
|
}
|
||||||
.kx-skip-link:focus-visible {
|
.kx-skip-link:focus-visible {
|
||||||
top: var(--space-3);
|
top: var(--space-3);
|
||||||
color: var(--color-white);
|
color: var(--color-on-accent);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
--color-neutral-100: #f2f4f7;
|
--color-neutral-100: #f2f4f7;
|
||||||
--color-neutral-050: #f9fafb; /* 앱 배경 */
|
--color-neutral-050: #f9fafb; /* 앱 배경 */
|
||||||
--color-white: #ffffff;
|
--color-white: #ffffff;
|
||||||
|
--color-on-accent: #ffffff; /* 색상(파랑/빨강 등) 배경 위 텍스트 — 다크에서도 항상 흰색 유지(--color-white 는 다크에서 서피스로 반전되므로 버튼/배지 글자엔 이 토큰 사용) */
|
||||||
|
|
||||||
--color-canvas-bg: #1c2536; /* 에디터 캔버스 다크 서피스 */
|
--color-canvas-bg: #1c2536; /* 에디터 캔버스 다크 서피스 */
|
||||||
--color-canvas-line: #2f3b52; /* 캔버스 위 보조 라인 */
|
--color-canvas-line: #2f3b52; /* 캔버스 위 보조 라인 */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user