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 { useAuthStore } from './store/authStore';
|
||||
import { landingPathFor } from './lib/roleTrack';
|
||||
import { LoginPage } from './screens/login/LoginPage';
|
||||
import { OtpSetupPage } from './screens/login/OtpSetupPage';
|
||||
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 소유자 지적 반영).
|
||||
* 인증 시 랜딩은 completeLogin 과 동일 규칙(plan_role_routing §2-1) — ops·business=/home(회귀 0),
|
||||
* admin→/admin, agency→/contractor/dashboard, visitor→/visitor. 로그인 직후·`/`·catch-all 일관.
|
||||
*/
|
||||
function RootGate() {
|
||||
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 />;
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import type { Track } from '../../lib/roleTrack';
|
||||
import { useThemeStore } from '../../store/themeStore';
|
||||
import { useMdiStore } from '../../store/mdiStore';
|
||||
import { useUiStore, isMobileViewport } from '../../store/uiStore';
|
||||
@ -55,6 +56,8 @@ interface NavItem {
|
||||
scoped?: boolean;
|
||||
/** true → 클릭 불가한 소그룹 구분 헤더(시스템관리 20항목 정리용). */
|
||||
section?: boolean;
|
||||
/** 노출 허용 트랙(미지정=전 트랙). plan §2-3 항목 단위 필터(예: 플로어플랜=agency 숨김). */
|
||||
tracks?: Track[];
|
||||
}
|
||||
interface NavGroupDef {
|
||||
id: string;
|
||||
@ -64,6 +67,17 @@ interface NavGroupDef {
|
||||
items: NavItem[];
|
||||
/** 관리자 전용 그룹 — useIsAdmin() 시에만 노출. */
|
||||
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: '전시 운영',
|
||||
labelKey: 'shell.menu.groups.ops',
|
||||
Icon: IconOperations,
|
||||
tracks: BIZ_OPS_ADMIN, // agency·visitor ✕
|
||||
items: [
|
||||
{ key: 'dashboard', label: '대시보드', labelKey: 'shell.menu.items.dashboard', Icon: IconDashboard, scoped: true },
|
||||
{ key: 'exhibitors', label: '참가업체 관리', labelKey: 'shell.menu.items.exhibitors', Icon: IconExhibitors, scoped: true },
|
||||
@ -94,8 +109,10 @@ const GROUPS: NavGroupDef[] = [
|
||||
label: '설계·시공',
|
||||
labelKey: 'shell.menu.groups.design',
|
||||
Icon: IconFloorplan,
|
||||
tracks: WORK_TRACKS, // business·ops·admin ● + agency 부분(항목 단위)
|
||||
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: 'auctions', label: '공사 옥션', labelKey: 'shell.menu.items.auctions', Icon: IconSettlement, to: '/auctions' },
|
||||
{ key: 'contractor', label: '수주 부스', labelKey: 'shell.menu.items.contractor', Icon: IconExhibitors, to: '/contractor/dashboard' },
|
||||
@ -107,6 +124,7 @@ const GROUPS: NavGroupDef[] = [
|
||||
label: '관람객·마케팅',
|
||||
labelKey: 'shell.menu.groups.visitor',
|
||||
Icon: IconUsers,
|
||||
tracks: BIZ_OPS_ADMIN, // agency·visitor ✕
|
||||
items: [
|
||||
{ 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' },
|
||||
@ -121,6 +139,7 @@ const GROUPS: NavGroupDef[] = [
|
||||
label: '정산·분석',
|
||||
labelKey: 'shell.menu.groups.finance',
|
||||
Icon: IconAnalytics,
|
||||
tracks: BIZ_OPS_ADMIN, // agency·visitor ✕
|
||||
items: [
|
||||
{ key: 'settlement', label: '정산', labelKey: 'shell.menu.items.settlement', Icon: IconSettlement, to: '/settlement' },
|
||||
{ key: 'analytics', label: '경영분석', labelKey: 'shell.menu.items.analytics', Icon: IconAnalytics, to: '/analytics' },
|
||||
@ -131,6 +150,7 @@ const GROUPS: NavGroupDef[] = [
|
||||
label: '업무 공통',
|
||||
labelKey: 'shell.menu.groups.work',
|
||||
Icon: IconDocument,
|
||||
tracks: WORK_TRACKS, // 전 업무 트랙 ● (visitor ✕)
|
||||
items: [
|
||||
{ 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' },
|
||||
@ -232,6 +252,7 @@ export function AppShell() {
|
||||
const openTab = useMdiStore((s) => s.openTab);
|
||||
const resolvedEventId = useResolvedEventId();
|
||||
const isAdmin = useIsAdmin();
|
||||
const activeTrack = useAuthStore((s) => s.activeTrack());
|
||||
const isDark = themeResolved === 'dark';
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -274,7 +295,10 @@ export function AppShell() {
|
||||
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 (
|
||||
<div className={`kx-shell ${navCollapsed ? 'is-nav-collapsed' : ''}`}>
|
||||
@ -343,7 +367,9 @@ export function AppShell() {
|
||||
</button>
|
||||
{expanded && (
|
||||
<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 ? (
|
||||
// 소그룹 구분 헤더(클릭 불가) — 시스템관리 20항목 정리(WISE 구분선 패턴).
|
||||
<li key={item.key} className="kx-shell__nav-section" aria-hidden="true">
|
||||
|
||||
@ -89,7 +89,7 @@
|
||||
.kx-aiimg__shot,
|
||||
.kx-aiimg__ver {
|
||||
background: rgba(16, 24, 40, 0.72);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
border-radius: var(--radius-pill);
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
@ -97,7 +97,7 @@
|
||||
}
|
||||
.kx-aiimg__sample {
|
||||
background: rgba(109, 74, 255, 0.9);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
border-radius: var(--radius-pill);
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
@ -111,7 +111,7 @@
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: var(--color-ai-accent);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 12px;
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
.kx-btn--primary {
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kx-btn--primary:not(:disabled):hover {
|
||||
background: var(--color-primary-700);
|
||||
@ -44,7 +44,7 @@
|
||||
|
||||
.kx-btn--ai {
|
||||
background: var(--color-ai-accent);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
box-shadow: 0 0 12px var(--ai-glow);
|
||||
}
|
||||
.kx-btn--ai:not(:disabled):hover {
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 600;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
background: rgba(16, 24, 40, 0.72);
|
||||
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);
|
||||
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-current .kx-adm-wiz__dot { background: var(--color-primary-600); 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-on-accent); }
|
||||
.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__step.is-todo .kx-adm-wiz__label { color: var(--color-neutral-500); }
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
.kx-appqr__ver {
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 700;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
background: var(--color-primary-600);
|
||||
border-radius: var(--radius-pill);
|
||||
padding: 2px 10px;
|
||||
|
||||
@ -102,7 +102,7 @@
|
||||
}
|
||||
.kx-rev__tabs button.is-active {
|
||||
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);
|
||||
}
|
||||
.kx-rev__overlay-toggle {
|
||||
@ -112,7 +112,7 @@
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--color-ai-accent);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 7px 12px;
|
||||
font-size: var(--fs-caption);
|
||||
@ -182,7 +182,7 @@
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
background: var(--color-error);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -206,7 +206,7 @@
|
||||
.kx-rev__zoom button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
@ -232,7 +232,7 @@
|
||||
}
|
||||
.kx-auc-open__head {
|
||||
background: var(--color-primary-700);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
.kx-auc-open__head h2 {
|
||||
@ -631,7 +631,7 @@
|
||||
}
|
||||
.kx-rank__row--me .kx-rank__badge {
|
||||
background: var(--color-ai-accent);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kx-rank__main {
|
||||
flex: 1;
|
||||
@ -797,7 +797,7 @@
|
||||
margin-bottom: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
background: var(--color-primary-700);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-pill);
|
||||
@ -902,7 +902,7 @@
|
||||
height: 44px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-primary-700);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -1008,7 +1008,7 @@
|
||||
gap: var(--space-2);
|
||||
padding: 12px 16px;
|
||||
background: var(--color-neutral-900);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: var(--fs-body);
|
||||
box-shadow: var(--shadow-level2);
|
||||
|
||||
@ -470,7 +470,7 @@
|
||||
.kx-cms-step.is-done .kx-cms-step__dot {
|
||||
background: var(--color-success);
|
||||
border-color: var(--color-success);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kx-cms-step.is-current {
|
||||
color: var(--color-primary-700);
|
||||
@ -688,7 +688,7 @@
|
||||
height: 28px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -707,7 +707,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--space-6);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
background:
|
||||
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));
|
||||
@ -754,7 +754,7 @@
|
||||
}
|
||||
.kx-mb-btn--ghost {
|
||||
background: transparent;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.kx-mb-section {
|
||||
@ -807,7 +807,7 @@
|
||||
padding: 2px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@ -134,7 +134,7 @@
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
background: var(--color-error);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kx-comp__item.is-warn .kx-comp__pin {
|
||||
background: var(--color-violation-warn);
|
||||
@ -233,7 +233,7 @@
|
||||
right: 6%;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
background: var(--color-error);
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
@ -154,11 +154,11 @@
|
||||
}
|
||||
.kx-step--done .kx-step__dot {
|
||||
background: var(--color-primary-700);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kx-step--current .kx-step__dot {
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
box-shadow: 0 0 0 4px var(--color-primary-100);
|
||||
}
|
||||
.kx-step__label {
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
padding: 8px 18px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-size: var(--fs-h2);
|
||||
font-weight: 700;
|
||||
}
|
||||
@ -79,7 +79,7 @@
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
background: var(--color-neutral-200);
|
||||
}
|
||||
.kx-timeline__node.is-done .kx-timeline__dot {
|
||||
|
||||
@ -109,7 +109,7 @@
|
||||
.kx-stepper__item.is-done .kx-stepper__dot {
|
||||
background: 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 {
|
||||
border-color: var(--color-primary-600);
|
||||
|
||||
@ -363,7 +363,7 @@
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@ -63,7 +63,7 @@
|
||||
.kx-gallery__chip.is-active {
|
||||
background: var(--color-primary-600);
|
||||
border-color: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
|
||||
/* 마소너리(간이 — CSS columns) */
|
||||
|
||||
@ -366,7 +366,7 @@
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-neutral-900);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
padding: 10px 18px;
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: var(--fs-caption);
|
||||
|
||||
@ -1132,7 +1132,7 @@
|
||||
height: 40px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-ai-accent);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.kx-home__aihub-heading {
|
||||
@ -1239,7 +1239,7 @@
|
||||
padding: 1px 5px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-ai-accent);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kx-home__aihub-card-desc {
|
||||
font-size: var(--fs-caption);
|
||||
|
||||
@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { authApi } from '../../api/endpoints';
|
||||
import { ApiRequestError } from '../../api/client';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { landingPathFor } from '../../lib/roleTrack';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { LanguageSwitcher } from '../../components/ui/LanguageSwitcher';
|
||||
import { InviteDialog } from './InviteDialog';
|
||||
@ -48,14 +49,15 @@ export function LoginPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 최종 로그인 확정(공통) — 토큰 적용 후 메인 홈(SCR-HOME)으로 랜딩(v2.1.2).
|
||||
// 회원가입 직후 흐름도 로그인 성공 경로를 공유하므로 동일하게 /home 으로 랜딩한다.
|
||||
// 최종 로그인 확정(공통) — 토큰 적용 후 역할별 메인으로 랜딩(plan_role_routing §2-1).
|
||||
// ops·business=/home(회귀 0), admin→/admin, agency→/contractor/dashboard, visitor→/visitor.
|
||||
// 판정 불가 시 안전 폴백 /home. 회원가입 직후 흐름도 동일 경로를 공유한다.
|
||||
function completeLogin(res: LoginResponse) {
|
||||
if (remember) localStorage.setItem(REMEMBER_KEY, email.trim());
|
||||
else localStorage.removeItem(REMEMBER_KEY);
|
||||
applyLogin(res);
|
||||
setOtpChallenge(null);
|
||||
navigate('/home');
|
||||
navigate(landingPathFor(res.user, res.workspaces));
|
||||
}
|
||||
|
||||
async function handleLogin(e: React.FormEvent) {
|
||||
|
||||
@ -103,7 +103,7 @@
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.kx-login__headline {
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-size: var(--fs-display);
|
||||
line-height: var(--lh-display);
|
||||
font-weight: 700;
|
||||
@ -149,7 +149,7 @@
|
||||
height: 36px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -321,7 +321,7 @@
|
||||
height: 56px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-success, #16a34a);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@ -661,7 +661,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kx-mkt__check.is-done {
|
||||
background: var(--color-primary-600);
|
||||
|
||||
@ -312,7 +312,7 @@
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-size: var(--fs-h3);
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@ -488,7 +488,7 @@
|
||||
}
|
||||
.kx-m__verdict button.is-fail {
|
||||
background: var(--color-error);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kx-m__icard-evidence {
|
||||
display: flex;
|
||||
|
||||
@ -126,7 +126,7 @@
|
||||
right: var(--space-3);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
background: var(--color-error);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-pill);
|
||||
|
||||
@ -76,14 +76,14 @@
|
||||
}
|
||||
.kxp-btn--primary {
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kxp-btn--primary:hover {
|
||||
background: var(--color-primary-700);
|
||||
}
|
||||
.kxp-btn--ghost {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
@ -106,7 +106,7 @@
|
||||
}
|
||||
.kxp-btn--danger:hover {
|
||||
background: var(--color-error);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kxp-btn--lg {
|
||||
padding: 16px 28px;
|
||||
@ -145,7 +145,7 @@
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kxp-dday--warn {
|
||||
background: var(--color-warning);
|
||||
@ -435,7 +435,7 @@
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
padding: 6px 14px;
|
||||
@ -445,7 +445,7 @@
|
||||
font-size: clamp(30px, 6vw, 52px);
|
||||
line-height: 1.12;
|
||||
font-weight: 800;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0;
|
||||
}
|
||||
@ -472,7 +472,7 @@
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 10px 14px;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kxp-hero__metacard small {
|
||||
display: block;
|
||||
@ -908,7 +908,7 @@
|
||||
.kxp-footer__logo {
|
||||
font-size: var(--fs-h2);
|
||||
font-weight: 800;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.kxp-footer__desc {
|
||||
@ -928,7 +928,7 @@
|
||||
}
|
||||
.kxp-footer__col h4 {
|
||||
font-size: var(--fs-body);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.kxp-footer__col a,
|
||||
@ -937,7 +937,7 @@
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.kxp-footer__col a:hover {
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kxp-footer__bottom {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
@ -1147,7 +1147,7 @@
|
||||
}
|
||||
.kxp-daytab.is-active {
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
border-color: var(--color-primary-600);
|
||||
}
|
||||
/* 연사 카드 */
|
||||
@ -1299,7 +1299,7 @@
|
||||
.kxp-fp__chip.is-active {
|
||||
background: var(--color-primary-600);
|
||||
border-color: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kxp-fp__list {
|
||||
flex: 1;
|
||||
@ -1430,7 +1430,7 @@
|
||||
right: 3px;
|
||||
font-size: 9px;
|
||||
background: var(--color-error);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
@ -1708,7 +1708,7 @@
|
||||
.kxp-step.is-done .kxp-step__dot {
|
||||
background: var(--color-primary-600);
|
||||
border-color: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kxp-step__label {
|
||||
font-size: var(--fs-caption);
|
||||
@ -1780,7 +1780,7 @@
|
||||
}
|
||||
.kxp-typecard.is-active .kxp-typecard__ic {
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
border-color: var(--color-primary-600);
|
||||
}
|
||||
.kxp-typecard h3 {
|
||||
@ -1812,7 +1812,7 @@
|
||||
.kxp-interest.is-active {
|
||||
background: var(--color-primary-600);
|
||||
border-color: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kxp-regnav {
|
||||
display: flex;
|
||||
@ -1847,7 +1847,7 @@
|
||||
background: var(--color-primary-600);
|
||||
border-radius: 24px;
|
||||
padding: 24px;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
text-align: left;
|
||||
}
|
||||
.kxp-badge__top {
|
||||
@ -2029,7 +2029,7 @@
|
||||
}
|
||||
.kxp-cta-banner {
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
text-align: center;
|
||||
padding: 64px 16px;
|
||||
}
|
||||
@ -3250,7 +3250,7 @@
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kxp-vhero__title {
|
||||
font-size: clamp(28px, 5.2vw, 46px);
|
||||
@ -3258,7 +3258,7 @@
|
||||
line-height: 1.18;
|
||||
letter-spacing: -0.025em;
|
||||
margin: 8px 0 12px;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kxp-vhero__lead {
|
||||
font-size: clamp(15px, 2.2vw, 19px);
|
||||
@ -3321,7 +3321,7 @@
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--color-ai-accent);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
@ -3347,7 +3347,7 @@
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
@ -3488,7 +3488,7 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
.kxp-ai__evfallback {
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-weight: 800;
|
||||
font-size: 15px;
|
||||
}
|
||||
@ -3907,7 +3907,7 @@ a.kxp-ai__cite:hover {
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@ -210,7 +210,7 @@
|
||||
}
|
||||
.kx-cal__cell.is-today {
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
.kx-cal__cell.is-today.has-event::after {
|
||||
|
||||
@ -244,7 +244,7 @@
|
||||
.kx-sg__toast {
|
||||
display: inline-block;
|
||||
background: var(--color-neutral-900);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-size: var(--fs-body);
|
||||
padding: 10px var(--space-4);
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
@ -63,7 +63,7 @@
|
||||
.kx-uord__wizard li.is-done::before {
|
||||
content: '✓';
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kx-uord__wizard li.is-current {
|
||||
color: var(--color-primary-700);
|
||||
@ -71,7 +71,7 @@
|
||||
}
|
||||
.kx-uord__wizard li.is-current::before {
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
|
||||
.kx-uord__body {
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
.kx-wire__legend-item::before {
|
||||
content: '';
|
||||
|
||||
@ -248,7 +248,7 @@
|
||||
.kx-badgecard__top {
|
||||
width: 100%;
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
@ -280,7 +280,7 @@
|
||||
.kx-badgecard__tag {
|
||||
font-size: 7px;
|
||||
font-weight: 800;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
background: var(--color-ai-accent);
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
@ -490,7 +490,7 @@
|
||||
.kx-gauge__num {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
background: var(--color-primary-600);
|
||||
border-radius: var(--radius-pill);
|
||||
padding: 1px 8px;
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { getAccessToken, setAccessToken } from '../api/client';
|
||||
import type { AuthUser, EventRole, LoginResponse, WorkspaceDto } from '../api/types';
|
||||
import { resolveLandingTrack, trackOf, type Track } from '../lib/roleTrack';
|
||||
|
||||
interface AuthState {
|
||||
user: AuthUser | null;
|
||||
@ -18,6 +19,8 @@ interface AuthState {
|
||||
roleForCurrent: () => EventRole | null;
|
||||
// 전역 역할(app_user.role_code — 'ADMIN' | 'MANAGER' | …). 없으면 null. 관리자 판정(AdminGuard) 등에 사용.
|
||||
globalRole: () => string | null;
|
||||
// 반응형 활성 트랙(메뉴/탭 스코프). 행사 선택 시 해당 역할 트랙, 아니면 랜딩 트랙(plan §1).
|
||||
activeTrack: () => Track;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
@ -50,4 +53,14 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
roleForCurrent: () => get().currentWorkspace()?.myRole ?? 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;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-primary-600);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 600;
|
||||
transition: top 0.15s ease;
|
||||
}
|
||||
.kx-skip-link:focus-visible {
|
||||
top: var(--space-3);
|
||||
color: var(--color-white);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
--color-neutral-100: #f2f4f7;
|
||||
--color-neutral-050: #f9fafb; /* 앱 배경 */
|
||||
--color-white: #ffffff;
|
||||
--color-on-accent: #ffffff; /* 색상(파랑/빨강 등) 배경 위 텍스트 — 다크에서도 항상 흰색 유지(--color-white 는 다크에서 서피스로 반전되므로 버튼/배지 글자엔 이 토큰 사용) */
|
||||
|
||||
--color-canvas-bg: #1c2536; /* 에디터 캔버스 다크 서피스 */
|
||||
--color-canvas-line: #2f3b52; /* 캔버스 위 보조 라인 */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user