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:
zio 2026-07-12 20:34:30 +09:00
parent 87f310f40a
commit 55e27b3309
34 changed files with 202 additions and 85 deletions

View File

@ -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 />;
} }

View File

@ -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">

View File

@ -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;

View File

@ -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 {

View File

@ -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;
} }

View 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';
}
}

View File

@ -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); }

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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;
} }

View File

@ -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);

View File

@ -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 {

View File

@ -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 {

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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) */

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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;
} }

View File

@ -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 {

View File

@ -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);

View File

@ -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 {

View File

@ -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: '';

View File

@ -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;

View File

@ -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);
},
})); }));

View File

@ -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);
} }

View File

@ -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; /* 캔버스 위 보조 라인 */