Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/app/chat/KnowledgePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export default function KnowledgePanel() {
<div className="border-t border-line pt-4">
<button
onClick={rebuild}
className="btn-ghost flex w-full items-center justify-center gap-1.5 px-3 py-2 text-xs"
className="btn-ghost min-h-0 flex w-full items-center justify-center gap-1.5 px-3 py-2 text-xs"
>
<Sparkle size={14} />
지식그래프 반영 (재빌드)
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ function AssistantAnswer({ content }: { content: string }) {
<button
onClick={copyAnswer}
aria-label="답변 복사"
className="btn-ghost gap-1.5 rounded-full px-3.5 py-1.5 text-xs sm:text-[13px] text-muted hover:text-fg"
className="btn-ghost min-h-0 gap-1.5 rounded-full px-3.5 py-1.5 text-xs sm:text-[13px] text-muted hover:text-fg"
>
{copied ? (
<>
Expand Down Expand Up @@ -427,7 +427,7 @@ function ChatClient() {
<button
onClick={() => setMobileSidebarOpen((v) => !v)}
aria-label="대화 목록 및 지식베이스 열기"
className="md:hidden btn-ghost flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-mono-spec shrink-0"
className="md:!hidden btn-ghost min-h-0 flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-mono-spec shrink-0"
>
<SidebarSimple size={15} />
<span>{tab === "chats" ? "대화 목록" : "지식베이스"}</span>
Expand All @@ -446,7 +446,7 @@ function ChatClient() {
<span className="font-display text-sm font-semibold text-fg">대화 및 지식베이스</span>
<button
onClick={() => setMobileSidebarOpen(false)}
className="btn-ghost btn-icon h-7 w-7 shrink-0"
className="btn-ghost btn-icon min-h-0 h-7 w-7 shrink-0"
aria-label="사이드바 닫기"
>
<X size={15} />
Expand Down
230 changes: 129 additions & 101 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -167,102 +167,104 @@ html.dark .glass {
backdrop-filter: blur(12px);
}

/* 버튼 — pill 라운드, 최소 높이 46px.
.btn-accent 가 주 CTA(인디고 그라디언트 + 호버 글로우),
.btn-ghost 는 외곽선. 배지·탭도 pill. */
.btn-accent,
.btn-ghost {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 46px;
padding: 0.65rem 1.4rem;
border-radius: var(--r-pill);
font-size: 0.9375rem;
font-weight: 600;
letter-spacing: 0.01em;
/* press 피드백은 100~160ms — 눌렀다는 응답은 즉각적이어야 한다 */
transition:
background 0.15s var(--ease-soft),
border-color 0.15s ease,
box-shadow 0.2s var(--ease-soft),
color 0.15s ease,
opacity 0.15s ease,
transform 0.12s var(--ease-out);
}
/* 키보드 포커스 링 — 필드(.field:focus)와 같은 인디고 링으로 통일 */
.btn-accent:focus-visible,
.btn-ghost:focus-visible {
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
@layer components {
/* 버튼 — pill 라운드, 최소 높이 46px.
.btn-accent 가 주 CTA(인디고 그라디언트 + 호버 글로우),
.btn-ghost 는 외곽선. 배지/탭도 pill. */
.btn-accent,
.btn-ghost {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 46px;
padding: 0.65rem 1.4rem;
border-radius: var(--r-pill);
font-size: 0.9375rem;
font-weight: 600;
letter-spacing: 0.01em;
/* press 피드백은 100~160ms — 눌렀다는 응답은 즉각적이어야 한다 */
transition:
background 0.15s var(--ease-soft),
border-color 0.15s ease,
box-shadow 0.2s var(--ease-soft),
color 0.15s ease,
opacity 0.15s ease,
transform 0.12s var(--ease-out);
}
/* 키보드 포커스 링 — 필드(.field:focus)와 같은 인디고 링으로 통일 */
.btn-accent:focus-visible,
.btn-ghost:focus-visible {
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

.btn-accent {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-soft) 100%);
color: #fff;
border: none;
box-shadow: 0 0 0 0 transparent;
}
/* 호버 이동은 포인팅 디바이스에서만 — 터치 탭에 false hover가 발생한다 */
@media (hover: hover) and (pointer: fine) {
.btn-accent:hover:not(:disabled) {
box-shadow: 0 6px 24px -6px var(--glow);
transform: translateY(-1px);
.btn-accent {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-soft) 100%);
color: #fff;
border: none;
box-shadow: 0 0 0 0 transparent;
}
/* 호버 이동은 포인팅 디바이스에서만 — 터치 탭에 false hover가 발생한다 */
@media (hover: hover) and (pointer: fine) {
.btn-accent:hover:not(:disabled) {
box-shadow: 0 6px 24px -6px var(--glow);
transform: translateY(-1px);
}
}
.btn-accent:active:not(:disabled),
.btn-ghost:active:not(:disabled) {
transform: scale(0.97); /* press 피드백 — 인터페이스가 입력을 듣고 있다는 확인 */
}
.btn-accent:disabled {
opacity: 0.35;
}
}
.btn-accent:active:not(:disabled),
.btn-ghost:active:not(:disabled) {
transform: scale(0.97); /* press 피드백 — 인터페이스가 입력을 듣고 있다는 확인 */
}
.btn-accent:disabled {
opacity: 0.35;
}

.btn-ghost {
background: transparent;
color: var(--fg);
border: 1px solid var(--line);
}
@media (hover: hover) and (pointer: fine) {
.btn-ghost:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
background: var(--surface);
.btn-ghost {
background: transparent;
color: var(--fg);
border: 1px solid var(--line);
}
@media (hover: hover) and (pointer: fine) {
.btn-ghost:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
background: var(--surface);
}
}
.btn-ghost:disabled {
opacity: 0.4;
}
}
.btn-ghost:disabled {
opacity: 0.4;
}

/* 아이콘 정사각 버튼 — 라벨 없는 것들 전용. */
.btn-icon {
min-height: 0;
padding: 0;
width: 40px;
height: 40px;
border-radius: var(--r-md);
}
/* 아이콘 정사각 버튼 — 라벨 없는 것들 전용. */
.btn-icon {
min-height: 0;
padding: 0;
width: 40px;
height: 40px;
border-radius: var(--r-md);
}

/* 입력 필드 — 인셋 표면 + 헤어라인, 포커스는 인디고 링. */
.field {
background: var(--ink-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
min-height: 46px;
font-size: 0.95rem;
color: var(--fg);
font-family: var(--font-sans);
outline: none;
transition:
border-color 0.15s ease,
box-shadow 0.2s var(--ease-soft);
}
.field::placeholder {
color: var(--stone);
}
.field:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
/* 입력 필드 — 인셋 표면 + 헤어라인, 포커스는 인디고 링. */
.field {
background: var(--ink-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
min-height: 46px;
font-size: 0.95rem;
color: var(--fg);
font-family: var(--font-sans);
outline: none;
transition:
border-color 0.15s ease,
box-shadow 0.2s var(--ease-soft);
}
.field::placeholder {
color: var(--stone);
}
.field:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
}

/* 스켈레톤 시머 — 중립 표면 명도로 깜빡인다. */
Expand Down Expand Up @@ -458,21 +460,47 @@ html.dark .glass {
.a11y-large-text .font-display {
letter-spacing: -0.01em;
}
.a11y-large-text .btn-accent,
.a11y-large-text .btn-ghost {
min-height: 48px;
font-size: 1rem;
@layer components {
.a11y-large-text .btn-accent,
.a11y-large-text .btn-ghost {
min-height: 48px;
font-size: 1rem;
}
.a11y-large-text .field {
min-height: 48px;
font-size: 1rem;
}
}

/* 아이콘 버튼 규격 절대 보호 (정사각 아이콘 버튼 및 인라인 제어는 늘어나지 않음) */
.btn-icon,
.a11y-large-text .btn-icon,
.a11y-large-text .btn-ghost.btn-icon,
.a11y-large-text .btn-accent.btn-icon {
min-height: 0 !important;
padding: 0 !important;
}

/* 네비게이션 바 내부 요소 규격 고정 — a11y 큰 글씨 모드에서도 캡슐 알약 형태를 온전히 유지 */
.a11y-large-text nav {
font-size: 13px;
}
/* 네비게이션 바 내부 버튼은 알약 캡슐 레이아웃을 유지하도록 컴팩트 타깃 보정 */
.a11y-large-text nav .btn-ghost,
.a11y-large-text nav .btn-icon {
min-height: 36px;
font-size: 0.875rem;
.a11y-large-text nav .btn-accent {
min-height: 36px !important;
height: 36px !important;
padding: 0 0.65rem !important;
font-size: 12px !important;
}
.a11y-large-text .field {
min-height: 48px;
font-size: 1rem;
.a11y-large-text nav .btn-icon,
.a11y-large-text nav .btn-ghost.btn-icon,
.a11y-large-text nav .btn-accent.btn-icon {
min-height: 38px !important;
height: 38px !important;
width: 38px !important;
padding: 0 !important;
}

/* 답변 말풍선은 줄간격과 글자 크기를 넓혀 시니어 가독성을 보강한다 */
.a11y-large-text [data-chat-answer] {
font-size: 1.1rem;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/graph/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export default function GraphPage() {
<button
onClick={loadSnapshot}
disabled={snapLoading}
className="btn-ghost px-4 py-2 text-sm font-semibold disabled:opacity-40"
className="btn-ghost min-h-0 px-4 py-2 text-sm font-semibold disabled:opacity-40"
>
{snapLoading ? "불러오는 중…" : snapshot ? "새로고침" : "스냅샷 불러오기"}
</button>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export default function LoginPage() {
type="button"
onClick={() => setShowPassword((s) => !s)}
aria-label={showPassword ? "비밀번호 숨기기" : "비밀번호 표시"}
className="btn-ghost btn-icon absolute right-1.5 top-1/2 -translate-y-1/2 h-7 w-7 text-muted hover:text-fg"
className="btn-ghost btn-icon min-h-0 absolute right-1.5 top-1/2 -translate-y-1/2 h-7 w-7 text-muted hover:text-fg"
>
{showPassword ? <EyeSlash size={16} /> : <Eye size={16} />}
</button>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,13 @@ export default function HomePage() {
<div className="shrink-0 flex items-center gap-2">
<Link
href="/security"
className="btn-ghost text-xs font-semibold px-3 py-1.5"
className="btn-ghost min-h-0 text-xs font-semibold px-3 py-1.5"
>
5겹 보안 체험
</Link>
<Link
href="/chat"
className="btn-accent text-xs font-semibold px-3 py-1.5"
className="btn-accent min-h-0 text-xs font-semibold px-3 py-1.5"
>
챗봇 열기 →
</Link>
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/app/simulator/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,16 +207,16 @@ export default function SimulatorPage() {
<button
type="button"
onClick={() => applyPreset(20_000_000, 600_000, 40_000_000)}
className="btn-ghost text-xs px-3 py-1 rounded-full text-fg/90 hover:text-accent border-line/60"
className="btn-ghost min-h-0 text-xs px-3 py-1 rounded-full text-fg/90 hover:text-accent border-line/60"
>
🐣 사회초년생 (자산 2천 · 월 60만 ➔ 계약금 4천만)
🐣 사회초년생 (자산 2천, 월 60만 ➔ 계약금 4천만)
</button>
<button
type="button"
onClick={() => applyPreset(50_000_000, 1_500_000, 100_000_000)}
className="btn-ghost text-xs px-3 py-1 rounded-full text-fg/90 hover:text-accent border-line/60"
className="btn-ghost min-h-0 text-xs px-3 py-1 rounded-full text-fg/90 hover:text-accent border-line/60"
>
💍 신혼부부 (자산 5천 · 월 150만 ➔ 1억 목표)
💍 신혼부부 (자산 5천, 월 150만 ➔ 1억 목표)
</button>
<button
type="button"
Expand All @@ -228,7 +228,7 @@ export default function SimulatorPage() {
setMonthlySaving(300_000);
toast("서울 85㎡ 예치금 프리셋이 적용되었습니다.", "info");
}}
className="btn-ghost text-xs px-3 py-1 rounded-full text-fg/90 hover:text-accent border-line/60"
className="btn-ghost min-h-0 text-xs px-3 py-1 rounded-full text-fg/90 hover:text-accent border-line/60"
>
🏠 서울 85㎡ 청약 예치금 (300만)
</button>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/stocks/MemoryStatsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default function MemoryStatsCard({
<button
onClick={validate}
disabled={busy}
className="btn-ghost flex items-center gap-1.5 px-3 py-1.5 text-sm disabled:opacity-50"
className="btn-ghost min-h-0 flex items-center gap-1.5 px-3 py-1.5 text-sm disabled:opacity-50"
title="7일 이상 지난 분석을 현재가와 비교해 적중 여부 채우기"
>
<ArrowsClockwise size={14} className={busy ? "animate-spin" : ""} />
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/stocks/WatchlistCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default function WatchlistCard({
<button
onClick={() => mutate(() => addWatchlist(userUuid, symbol))}
disabled={busy || !symbol || alreadyIn}
className="btn-ghost flex items-center gap-1.5 px-3 py-1.5 text-sm disabled:opacity-40"
className="btn-ghost min-h-0 flex items-center gap-1.5 px-3 py-1.5 text-sm disabled:opacity-40"
title={alreadyIn ? "이미 관심종목에 있습니다" : `${symbol} 담기`}
>
<Plus size={14} />
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/stocks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,7 @@ export default function StocksPage() {
<button
onClick={optimize}
disabled={gridBusy}
className="btn-ghost flex items-center gap-1.5 px-4 py-2 text-sm disabled:opacity-50"
className="btn-ghost min-h-0 flex items-center gap-1.5 px-4 py-2 text-sm disabled:opacity-50"
>
{gridBusy ? <Spinner className="h-3.5 w-3.5" /> : <Target size={14} />}
{gridBusy ? "탐색 중…" : "최적 파라미터 탐색"}
Expand Down Expand Up @@ -1115,7 +1115,7 @@ export default function StocksPage() {
</SectionLabel>
<button
onClick={consult}
className="btn-ghost flex items-center gap-1.5 px-3 py-1.5 text-sm"
className="btn-ghost min-h-0 flex items-center gap-1.5 px-3 py-1.5 text-sm"
>
<ChatCircleText size={14} /> 이 결과로 상담받기
</button>
Expand Down Expand Up @@ -1217,7 +1217,7 @@ export default function StocksPage() {
<button
onClick={() => analyze(m)}
disabled={reportBusy}
className="btn-ghost flex items-center gap-1.5 px-3 py-1.5 text-sm disabled:opacity-50"
className="btn-ghost min-h-0 flex items-center gap-1.5 px-3 py-1.5 text-sm disabled:opacity-50"
>
{reportBusy ? <Spinner className="h-3.5 w-3.5" /> : <Sparkle size={14} />}
{reportBusy ? "생성 중…" : report ? "다시 생성" : "리포트 생성"}
Expand Down
Loading
Loading