fix(web): restore public header login + track-tab icons/tooltips, visitor CTA to /tickets/lookup, accent-button white-text sweep

- PublicShell: track tabs get icons + title tooltips (right-aligned), login link restored at >=768px
- VisitorTrackPage: wayfinding/checkin CTAs point to #guide/#transport//tickets/lookup instead of /login
- accent buttons: enforce --color-on-accent (white text) across approval/docs/home/movein/schedule/work/public

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zio 2026-07-13 00:29:12 +09:00
parent f3b800f9be
commit 0add2cd417
9 changed files with 93 additions and 51 deletions

View File

@ -420,15 +420,15 @@
} }
.kx-rev__step--cur .kx-rev__step-no { .kx-rev__step--cur .kx-rev__step-no {
background: #1f29fc; background: #1f29fc;
color: #fff; color: var(--color-on-accent);
} }
.kx-rev__step--ok .kx-rev__step-no { .kx-rev__step--ok .kx-rev__step-no {
background: #10b981; background: #10b981;
color: #fff; color: var(--color-on-accent);
} }
.kx-rev__step--bad .kx-rev__step-no { .kx-rev__step--bad .kx-rev__step-no {
background: #ef4444; background: #ef4444;
color: #fff; color: var(--color-on-accent);
} }
.kx-rev__step--skip { .kx-rev__step--skip {
opacity: 0.6; opacity: 0.6;

View File

@ -143,7 +143,7 @@
.kx-doc__step.is-done .kx-doc__node { .kx-doc__step.is-done .kx-doc__node {
background: var(--color-primary-600); background: var(--color-primary-600);
border-color: var(--color-primary-600); border-color: var(--color-primary-600);
color: #fff; color: var(--color-on-accent);
} }
.kx-doc__step.is-active .kx-doc__node { .kx-doc__step.is-active .kx-doc__node {
background: var(--color-white); background: var(--color-white);
@ -354,7 +354,7 @@
.kx-doc__stat { .kx-doc__stat {
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
background: var(--color-primary-700); background: var(--color-primary-700);
color: #fff; color: var(--color-on-accent);
padding: var(--space-5); padding: var(--space-5);
} }
.kx-doc__stat-label { .kx-doc__stat-label {
@ -426,7 +426,7 @@
} }
.kx-rep__acc.is-open .kx-rep__acc-head { .kx-rep__acc.is-open .kx-rep__acc-head {
background: var(--color-primary-600); background: var(--color-primary-600);
color: #fff; color: var(--color-on-accent);
} }
.kx-rep__acc-head h3 { .kx-rep__acc-head h3 {
font-size: var(--fs-h3); font-size: var(--fs-h3);
@ -592,7 +592,7 @@
transform: translateX(-50%); transform: translateX(-50%);
z-index: 60; z-index: 60;
background: var(--color-neutral-900); background: var(--color-neutral-900);
color: #fff; color: var(--color-white);
padding: 10px 18px; padding: 10px 18px;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
font-size: var(--fs-body); font-size: var(--fs-body);

View File

@ -153,7 +153,7 @@
background: linear-gradient(120deg, var(--color-primary-600), var(--color-ai-accent)); background: linear-gradient(120deg, var(--color-primary-600), var(--color-ai-accent));
} }
.kx-home__banner-text h2 { .kx-home__banner-text h2 {
color: #fff; color: var(--color-on-accent);
font-size: var(--fs-h1); font-size: var(--fs-h1);
margin: 0 0 8px; margin: 0 0 8px;
} }
@ -167,7 +167,7 @@
left: var(--space-8); left: var(--space-8);
bottom: var(--space-6); bottom: var(--space-6);
right: 30%; right: 30%;
color: #fff; color: var(--color-on-accent);
} }
.kx-home__banner-title { .kx-home__banner-title {
font-size: var(--fs-h1); font-size: var(--fs-h1);
@ -293,7 +293,7 @@
background: linear-gradient(140deg, var(--color-primary-600), var(--color-ai-accent)); background: linear-gradient(140deg, var(--color-primary-600), var(--color-ai-accent));
} }
.kx-poster__fallback-text { .kx-poster__fallback-text {
color: #fff; color: var(--color-on-accent);
font-size: var(--fs-h3); font-size: var(--fs-h3);
font-weight: var(--fw-bold); font-weight: var(--fw-bold);
text-align: center; text-align: center;
@ -539,7 +539,7 @@
} }
.kx-home__cal-cell.is-today .kx-home__cal-daynum { .kx-home__cal-cell.is-today .kx-home__cal-daynum {
background: var(--color-primary-600); background: var(--color-primary-600);
color: #fff; color: var(--color-on-accent);
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
padding: 1px 7px; padding: 1px 7px;
font-weight: var(--fw-bold); font-weight: var(--fw-bold);
@ -564,7 +564,7 @@
border-radius: 3px; border-radius: 3px;
font-size: var(--fs-micro); font-size: var(--fs-micro);
padding: 0 6px; padding: 0 6px;
color: #fff; color: var(--color-on-accent);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -721,7 +721,7 @@
} }
.kx-home__count-badge { .kx-home__count-badge {
background: var(--color-primary-600); background: var(--color-primary-600);
color: #fff; color: var(--color-on-accent);
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
min-width: 20px; min-width: 20px;
height: 20px; height: 20px;
@ -885,7 +885,7 @@
} }
.kx-home__new { .kx-home__new {
background: var(--color-error); background: var(--color-error);
color: #fff; color: var(--color-on-accent);
font-size: var(--fs-nano); font-size: var(--fs-nano);
font-weight: var(--fw-bold); font-weight: var(--fw-bold);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);

View File

@ -159,7 +159,7 @@
} }
.kx-dock__block--booked { .kx-dock__block--booked {
background: var(--color-primary-600); background: var(--color-primary-600);
color: #fff; color: var(--color-on-accent);
} }
.kx-dock__block--priority { .kx-dock__block--priority {
background: var(--color-violation-warn-bg); background: var(--color-violation-warn-bg);
@ -266,7 +266,7 @@
padding: var(--space-4); padding: var(--space-4);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
background: var(--color-primary-700); background: var(--color-primary-700);
color: #fff; color: var(--color-on-accent);
cursor: pointer; cursor: pointer;
border: none; border: none;
text-align: left; text-align: left;
@ -365,7 +365,7 @@
transform: translateX(-50%); transform: translateX(-50%);
z-index: 60; z-index: 60;
background: var(--color-neutral-900); background: var(--color-neutral-900);
color: #fff; color: var(--color-white);
padding: 10px 18px; padding: 10px 18px;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
font-size: var(--fs-body); font-size: var(--fs-body);

View File

@ -3,10 +3,24 @@
* COEX (docs/analysis/coex-website.md): + . * COEX (docs/analysis/coex-website.md): + .
* design.md §3B/§3C-0: 3- (role=tablist· )·GNB· . * design.md §3B/§3C-0: 3- (role=tablist· )·GNB· .
*/ */
import { useRef, useState, type KeyboardEvent, type ReactNode } from 'react'; import {
useRef,
useState,
type ComponentType,
type KeyboardEvent,
type ReactNode,
type SVGProps,
} from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { IconMenu, IconClose, IconSearch } from './publicIcons'; import {
IconMenu,
IconClose,
IconSearch,
IconPerson,
IconBuyer,
IconFactory,
} from './publicIcons';
import { LanguageSwitcher } from '../../components/ui/LanguageSwitcher'; import { LanguageSwitcher } from '../../components/ui/LanguageSwitcher';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '../../store/authStore';
import { usePublicMotion } from './usePublicMotion'; import { usePublicMotion } from './usePublicMotion';
@ -22,10 +36,17 @@ const GNB = [
/** 3-트랙 진입 레이어 키 (§3C). 관문(SCR-T0)은 미선택 → undefined. */ /** 3-트랙 진입 레이어 키 (§3C). 관문(SCR-T0)은 미선택 → undefined. */
export type PublicTrack = 'visitor' | 'business' | 'agency'; export type PublicTrack = 'visitor' | 'business' | 'agency';
const TRACK_TABS: { key: PublicTrack; to: string; i18n: string }[] = [ type TabIcon = ComponentType<SVGProps<SVGSVGElement>>;
{ key: 'visitor', to: '/visitor', i18n: 'track.shell.tab.visitor' },
{ key: 'business', to: '/business', i18n: 'track.shell.tab.business' }, /**
{ key: 'agency', to: '/agency', i18n: 'track.shell.tab.agency' }, * 3- . SVG + (title)·aria-label로
* ( ).
* = · =() · =() .
*/
const TRACK_TABS: { key: PublicTrack; to: string; i18n: string; icon: TabIcon }[] = [
{ key: 'visitor', to: '/visitor', i18n: 'track.shell.tab.visitor', icon: IconPerson },
{ key: 'business', to: '/business', i18n: 'track.shell.tab.business', icon: IconBuyer },
{ key: 'agency', to: '/agency', i18n: 'track.shell.tab.agency', icon: IconFactory },
]; ];
/** /**
@ -58,6 +79,8 @@ function TrackTabs({ track, variant }: { track?: PublicTrack; variant: 'bar' | '
> >
{TRACK_TABS.map((it, i) => { {TRACK_TABS.map((it, i) => {
const isActive = it.key === track; const isActive = it.key === track;
const Icon = it.icon;
const label = t(it.i18n);
return ( return (
<Link <Link
key={it.key} key={it.key}
@ -69,10 +92,13 @@ function TrackTabs({ track, variant }: { track?: PublicTrack; variant: 'bar' | '
className={`kxp-tracktab${isActive ? ' is-active' : ''}`} className={`kxp-tracktab${isActive ? ' is-active' : ''}`}
aria-selected={isActive} aria-selected={isActive}
aria-current={isActive ? 'page' : undefined} aria-current={isActive ? 'page' : undefined}
aria-label={label}
title={label}
tabIndex={i === baseIdx ? 0 : -1} tabIndex={i === baseIdx ? 0 : -1}
onKeyDown={(e) => onKeyDown(e, i)} onKeyDown={(e) => onKeyDown(e, i)}
> >
{t(it.i18n)} <Icon className="kxp-tracktab__ic" aria-hidden />
{variant === 'menu' && <span className="kxp-tracktab__label">{label}</span>}
</Link> </Link>
); );
})} })}
@ -120,10 +146,6 @@ export function PublicShell({
<Link className="kxp-brand" to="/" aria-label={t('shell.homeAria', { brand })}> <Link className="kxp-brand" to="/" aria-label={t('shell.homeAria', { brand })}>
{brand} {brand}
</Link> </Link>
<TrackTabs track={track} variant="bar" />
</div>
<div className="kxp-header__right">
<nav className="kxp-gnb" aria-label={t('shell.primaryMenu')}> <nav className="kxp-gnb" aria-label={t('shell.primaryMenu')}>
{GNB.map((it) => ( {GNB.map((it) => (
<a <a
@ -136,6 +158,10 @@ export function PublicShell({
</a> </a>
))} ))}
</nav> </nav>
</div>
<div className="kxp-header__right">
<TrackTabs track={track} variant="bar" />
{search && ( {search && (
<div className="kxp-search" role="search"> <div className="kxp-search" role="search">

View File

@ -46,7 +46,8 @@ type Ico = ComponentType<SVGProps<SVGSVGElement>>;
const AI_TILES: { key: string; icon: Ico; to: string; soon?: boolean }[] = [ const AI_TILES: { key: string; icon: Ico; to: string; soon?: boolean }[] = [
{ key: 'assistant', icon: IconSparkles, to: '#hero' }, { key: 'assistant', icon: IconSparkles, to: '#hero' },
{ key: 'recommend', icon: IconTarget, to: '#recommend' }, { key: 'recommend', icon: IconTarget, to: '#recommend' },
{ key: 'wayfinding', icon: IconMap, to: '/login' }, // 실내 길찾기는 로그인/앱 기능 → 공개 방문가이드 섹션으로 안내(로그인 유도 금지).
{ key: 'wayfinding', icon: IconMap, to: '#guide' },
{ key: 'schedule', icon: IconCalendar, to: '#briefing' }, { key: 'schedule', icon: IconCalendar, to: '#briefing' },
{ key: 'crowd', icon: IconCloud, to: '#briefing', soon: true }, { key: 'crowd', icon: IconCloud, to: '#briefing', soon: true },
{ key: 'render', icon: IconRobot, to: '/business', soon: true }, { key: 'render', icon: IconRobot, to: '/business', soon: true },
@ -56,7 +57,8 @@ const AI_TILES: { key: string; icon: Ico; to: string; soon?: boolean }[] = [
const GUIDE_CARDS: { key: string; icon: Ico; to: string }[] = [ const GUIDE_CARDS: { key: string; icon: Ico; to: string }[] = [
{ key: 'transport', icon: IconTrain, to: '#transport' }, { key: 'transport', icon: IconTrain, to: '#transport' },
{ key: 'parking', icon: IconParking, to: '#transport' }, { key: 'parking', icon: IconParking, to: '#transport' },
{ key: 'wayfinding', icon: IconMap, to: '/login' }, // 길찾기는 공개 교통·오시는 길 안내로 대체(로그인 유도 금지).
{ key: 'wayfinding', icon: IconMap, to: '#transport' },
{ key: 'amenities', icon: IconInfo, to: '#guide' }, { key: 'amenities', icon: IconInfo, to: '#guide' },
{ key: 'accessibility', icon: IconShield, to: '#guide' }, { key: 'accessibility', icon: IconShield, to: '#guide' },
{ key: 'inquiry', icon: IconChat, to: '/public/exhibit-inquiry' }, { key: 'inquiry', icon: IconChat, to: '/public/exhibit-inquiry' },
@ -65,11 +67,12 @@ const GUIDE_CARDS: { key: string; icon: Ico; to: string }[] = [
/** 관심사 추천 칩(정적 카테고리 라벨). */ /** 관심사 추천 칩(정적 카테고리 라벨). */
const INTEREST_KEYS = ['robot', 'smartfactory', 'healthcare', 'eco', 'mobility', 'content'] as const; const INTEREST_KEYS = ['robot', 'smartfactory', 'healthcare', 'eco', 'mobility', 'content'] as const;
/** 관람객 여정 3-스텝. */ /** 관람객 여정 3-스텝. `linkKey`=기본 link 라벨 대체(로그인 유도 라벨 제거 시). */
const JOURNEY: { key: string; icon: Ico; to: string }[] = [ const JOURNEY: { key: string; icon: Ico; to: string; linkKey?: string }[] = [
{ key: 'register', icon: IconTicket, to: '#showcase' }, { key: 'register', icon: IconTicket, to: '#showcase' },
{ key: 'badge', icon: IconQr, to: '/tickets/lookup' }, { key: 'badge', icon: IconQr, to: '/tickets/lookup' },
{ key: 'checkin', icon: IconCheckCircle, to: '/login' }, // 현장 체크인은 로그인 대신 공개 입장권/QR 조회로 안내(로그인 유도 금지).
{ key: 'checkin', icon: IconCheckCircle, to: '/tickets/lookup', linkKey: 'track.visitor.guideMore' },
]; ];
function LinkOrAnchor({ function LinkOrAnchor({
@ -351,7 +354,7 @@ export function VisitorTrackPage() {
</div> </div>
</div> </div>
<ol className="kxp-journey pub-reveal pub-reveal--stagger"> <ol className="kxp-journey pub-reveal pub-reveal--stagger">
{JOURNEY.map(({ key, icon: Icon, to }, i) => ( {JOURNEY.map(({ key, icon: Icon, to, linkKey }, i) => (
<li key={key} className="kxp-journey__step" style={{ '--i': i } as React.CSSProperties}> <li key={key} className="kxp-journey__step" style={{ '--i': i } as React.CSSProperties}>
<span className="kxp-journey__num" aria-hidden> <span className="kxp-journey__num" aria-hidden>
{i + 1} {i + 1}
@ -363,7 +366,8 @@ export function VisitorTrackPage() {
<div className="kxp-journey__title">{t(`track.visitor.journey.${key}.title`)}</div> <div className="kxp-journey__title">{t(`track.visitor.journey.${key}.title`)}</div>
<p className="kxp-journey__desc">{t(`track.visitor.journey.${key}.desc`)}</p> <p className="kxp-journey__desc">{t(`track.visitor.journey.${key}.desc`)}</p>
<LinkOrAnchor to={to} className="kxp-guide__link"> <LinkOrAnchor to={to} className="kxp-guide__link">
{t(`track.visitor.journey.${key}.link`)} <IconChevronRight width={15} height={15} /> {t(linkKey ?? `track.visitor.journey.${key}.link`)}{' '}
<IconChevronRight width={15} height={15} />
</LinkOrAnchor> </LinkOrAnchor>
</div> </div>
{i < JOURNEY.length - 1 && ( {i < JOURNEY.length - 1 && (

View File

@ -384,14 +384,16 @@
.kxp-header__burger { .kxp-header__burger {
display: none; display: none;
} }
/* 데스크톱(버거 숨김 시점)부터 로그인/ 홈은 항상 헤더 우측에 노출
이전엔 1024px부터만 보여 768~1023 구간에서 로그인이 사라지던 결함 복구. */
.kxp-header__login {
display: inline-flex;
}
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.kxp-header__cta { .kxp-header__cta {
display: inline-flex; display: inline-flex;
} }
.kxp-header__login {
display: inline-flex;
}
} }
/* ═══ 메인 ═══ */ /* ═══ 메인 ═══ */
@ -2805,9 +2807,10 @@
* 신규 토큰 0 (기존 color·radius·kxp-maxw 변수 재사용). WCAG AA. * 신규 토큰 0 (기존 color·radius·kxp-maxw 변수 재사용). WCAG AA.
* =================================================================== */ * =================================================================== */
/* -- 3-트랙 (단일 헤더 내부 흡수 · COEX 통합 top bar) -- /* -- 3-트랙 (단일 헤더 우측 · 아이콘 + 툴팁) --
* 상단 프레임은 1개(.kxp-header). 별도 topbar 제거(이중 프레임 해소). * 상단 프레임은 1개(.kxp-header). 좌측=로고+GNB, 우측=트랙 아이콘 (로그인/언어 ).
* 라이트 헤더 배경 스타일(하단 보더 활성). 모바일은 헤더에서 숨기고 버거 메뉴로. */ * 텍스트 대신 SVG 아이콘 + title 툴팁/aria-label. 활성 트랙=배경 틴트+하단 보더.
* 모바일은 헤더에서 숨기고 버거 메뉴에서 아이콘+텍스트로 노출. */
.kxp-tracktabs { .kxp-tracktabs {
display: none; display: none;
align-items: stretch; align-items: stretch;
@ -2817,20 +2820,29 @@
.kxp-tracktab { .kxp-tracktab {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 8px 12px; gap: 8px;
padding: 8px 10px;
font-size: var(--fs-body); font-size: var(--fs-body);
font-weight: 600; font-weight: 600;
color: var(--color-neutral-600); color: var(--color-neutral-600);
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
white-space: nowrap; white-space: nowrap;
transition: color 0.15s, border-color 0.15s; transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.kxp-tracktab__ic {
width: 20px;
height: 20px;
flex-shrink: 0;
} }
.kxp-tracktab:hover { .kxp-tracktab:hover {
color: var(--color-primary-600); color: var(--color-primary-600);
background: var(--color-primary-050);
} }
.kxp-tracktab.is-active { .kxp-tracktab.is-active {
color: var(--color-primary-700); color: var(--color-primary-700);
font-weight: 800; font-weight: 800;
background: var(--color-primary-050);
border-bottom-color: var(--color-primary-600); border-bottom-color: var(--color-primary-600);
} }
.kxp-tracktab:focus-visible { .kxp-tracktab:focus-visible {
@ -3068,7 +3080,7 @@
height: 24px; height: 24px;
border-radius: 9999px; border-radius: 9999px;
background: var(--color-primary-600, #0066b3); background: var(--color-primary-600, #0066b3);
color: #fff; color: var(--color-on-accent);
font-size: 12px; font-size: 12px;
font-weight: 800; font-weight: 800;
} }

View File

@ -59,7 +59,7 @@
} }
.kx-cv-switch button.is-active { .kx-cv-switch button.is-active {
background: var(--color-primary-600); background: var(--color-primary-600);
color: #fff; color: var(--color-on-accent);
} }
.kx-cv-muted { .kx-cv-muted {
font-size: var(--fs-caption); font-size: var(--fs-caption);

View File

@ -309,7 +309,7 @@
.kx-msg__rail-count { .kx-msg__rail-count {
font-size: var(--fs-caption); font-size: var(--fs-caption);
background: var(--color-primary-600); background: var(--color-primary-600);
color: #fff; color: var(--color-on-accent);
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
padding: 0 7px; padding: 0 7px;
min-width: 18px; min-width: 18px;
@ -479,7 +479,7 @@
} }
.kx-cal__cell.is-today .kx-cal__daynum { .kx-cal__cell.is-today .kx-cal__daynum {
background: var(--color-primary-600); background: var(--color-primary-600);
color: #fff; color: var(--color-on-accent);
border-radius: 50%; border-radius: 50%;
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -492,7 +492,7 @@
line-height: 16px; line-height: 16px;
padding: 1px 6px; padding: 1px 6px;
border-radius: 4px; border-radius: 4px;
color: #fff; color: var(--color-on-accent);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -703,7 +703,7 @@
.kx-chip.is-active { .kx-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: #fff; color: var(--color-on-accent);
} }
.kx-noti__item { .kx-noti__item {
display: flex; display: flex;
@ -795,7 +795,7 @@
height: 64px; height: 64px;
border-radius: 50%; border-radius: 50%;
background: var(--color-primary-600); background: var(--color-primary-600);
color: #fff; color: var(--color-on-accent);
font-size: 26px; font-size: 26px;
font-weight: var(--fw-bold); font-weight: var(--fw-bold);
display: flex; display: flex;
@ -817,7 +817,7 @@
transform: translateX(-50%); transform: translateX(-50%);
z-index: 60; z-index: 60;
background: var(--color-neutral-900); background: var(--color-neutral-900);
color: #fff; color: var(--color-white);
padding: 10px 18px; padding: 10px 18px;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
font-size: var(--fs-body); font-size: var(--fs-body);