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
Binary file added docs/images/design-consistency-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/design-consistency-browse.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/design-consistency-form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/design-consistency-home-picker.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/design-consistency-intro-header.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/design-consistency-login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/design-consistency-mobile-home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/design-polish-browse.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/design-polish-disclosure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions docs/ui-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,47 @@
- 모바일 공개 페이지의 메뉴는 공통 모달에서 열립니다. 현재 위치를 표시하고, Escape로 닫으면 메뉴 버튼으로 초점이 돌아옵니다. 타입 변경도 메뉴 안에서 가능합니다.
- 새 프로젝트의 입력 화면에서는 비어 있는 기록 목록을 생략합니다. 이용 횟수와 분석 가능 상태는 폼 위에, 로그인 혜택은 폼 아래에 표시합니다.
- 모바일 배너는 시작 버튼을 이미지보다 먼저 보여줍니다. 프로젝트 점검의 장식 이미지는 모바일에서 생략해 입력 공간을 확보합니다.

## 화면 일관성 점검 — 2026-09-30

로컬 개발 서버(격리 SQLite, AI 키 없음)의 14개 경로를 1440×900, 390×844 라이트 테마에서 캡처해 비교했다. 공통 보정은 마지막에 불러오는 `src/styles/design-consistency.css`에 모았다.

| 발견한 문제 | 변경 |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| 홈 기능 배너의 이미지가 모든 슬라이드에서 보이지 않음. `ThemedImage`의 `<picture>` 높이가 0px이라 절대 위치 이미지가 사라졌다. | 배너 이미지 영역에서 `<picture>`가 프레임 크기를 채운다. |
| 페이지 소개 영역이 아래 테두리만 있고 양 끝이 둥글어 잘린 카드처럼 보임. | 전체 테두리와 패널 배경을 적용한다. |
| 홈·문제 풀이는 `CODE:FIT / 홈`, 나머지 작업 공간은 `작업 공간 / …` 경로와 안쪽으로 들어간 구분선을 사용. 모바일 메뉴 버튼 위치도 화면마다 좌우가 달랐다. | 모든 작업 공간 헤더가 `CODE:FIT / 페이지` 경로와 전체 너비 구분선을 쓰고, 모바일 메뉴 버튼을 왼쪽에 둔다. |
| 로그인·계정·개인정보 안내는 데스크톱에서 사이드바 없이 9개 링크가 두 줄로 줄바꿈됨. | 세 경로를 데스크톱 프레임에 포함해 같은 사이드바를 사용한다. |
| 홈 첫 방문 화면에 제목 두 개가 연달아 표시되고 프로젝트 카드와 붙어 있음. 공간 카드의 ‘이 공간으로 시작’은 행동으로 읽히지 않았다. | 제목을 하나로 합치고 48px 간격을 두었다. 시작 문구를 강조색과 화살표로 표시한다. |
| 전체 문제 탐색의 페이지 제목과 목록 제목이 같음. `EXPLORE YOUR DOMAIN`, `25 min`, `VIEW SOLUTION` 등 영어 표기가 섞임. | 목록 제목을 ‘문제 목록’으로 바꾸고 화면 문구와 대화상자 이름을 한국어로 통일했다. 로그인 카드의 터미널 장식 문구는 유지한다. |
| 내 프로젝트가 없을 때 검색창과 ‘새 프로젝트 분석’ 버튼이 빈 상태 안내의 버튼과 중복됨. | 첫 프로젝트 전에는 검색 도구를 숨긴다. |
| 프로젝트 점검 폼의 ‘(선택)’이 다음 줄로 떨어지고 안내 문장 사이 간격이 44px로 벌어짐. 보안 점검 동의 체크박스가 13px였다. | 라벨과 같은 줄에 표시하고 안내 간격을 12px로 맞췄다. 체크박스를 20px로 키웠다. |

![기능 배너 이미지](images/design-consistency-banner.png)
![소개 카드와 헤더](images/design-consistency-intro-header.png)
![로그인 화면 프레임](images/design-consistency-login.png)
![홈 공간 선택](images/design-consistency-home-picker.png)
![문제 목록](images/design-consistency-browse.png)
![프로젝트 점검 폼](images/design-consistency-form.png)
![모바일 홈](images/design-consistency-mobile-home.png)

검증: Prettier, ESLint, `tsc --noEmit`, knip 통과. 개발 서버 대상 Chromium에서 `desktop-navigation`, `navigation`, `design-refresh`, `theme-artwork`, `project-classes`, `select-login` 37개 중 30개 통과. 실패 7개(`navigation` 3개, `design-refresh`의 profile·dialogs, `select-login` 2개)는 변경 전 코드에서도 같은 오류로 실패했다. 다크 테마는 소개 카드와 배너를 화면으로만 확인했다. Firefox·WebKit, 프로덕션 빌드와 실제 운영 사이트는 이번에 확인하지 않았다.

## 표면과 조작 피드백 정리 — 2026-09-30

공통 보정은 `src/styles/design-polish.css`에 있으며 `design-consistency.css` 다음에 불러온다. 다크 테마의 선 색은 바꾸지 않았다.

| 발견한 문제 | 변경 |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| 라이트 테마에서 카드, 표, 버튼, 입력이 모두 같은 `#bac6d5` 선을 써 화면 전체의 테두리가 무겁고 입력칸이 구분되지 않음. | 컨테이너 선(`--line`)만 `#d5dde8`로 낮추고, 버튼과 입력칸은 기존 선을 유지한다. 카드에 1px 그림자를 더한다. |
| 문제 유형 필터는 테두리 있는 버튼 네 개가 테두리 있는 틀 안에 들어 있어 선택 상태가 테두리 굵기 차이로만 보임. | 회색 트랙 위에서 선택한 항목만 흰 배경, 그림자, 굵은 글씨로 올라오는 분절 컨트롤로 바꾼다. 문제 생성 대화상자에도 적용된다. |
| 문제 행의 태그가 모서리 없는 고정폭 테두리 상자로 표시되고, 행마다 북마크 버튼 테두리가 반복됨. 헤더의 새로고침·도움말 버튼만 테두리가 있고 테마 버튼은 없었다. | 태그는 채운 둥근 칩으로, 북마크와 헤더 아이콘 버튼은 테두리 없이 표시하고 마우스를 올리면 배경을 채운다. |
| 비활성 주요 버튼이 흐린 파란색이라 로딩 중인 버튼처럼 보임. | 비활성 주요 버튼을 회색 배경과 회색 글씨로 표시한다. |
| 보안 점검과 프로젝트 점검의 펼침 카드가 브라우저 기본 삼각형을 쓰고, 카드 여백과 제목 여백이 겹쳐 제목 아래가 넓게 비었다. | 기본 삼각형 대신 오른쪽 끝에 펼치면 뒤집히는 꺾쇠를 둔다. 여백을 20px 24px 하나로 합친다. |
| 버튼을 누를 때 1px 아래로만 움직였다. | 누르는 동안 98%로 줄어든다. 동작 줄이기 설정에서는 움직이지 않는다. |
| 문제 수, 쪽 번호, 예상 시간 숫자의 폭이 달라 값이 바뀔 때 흔들림. | 고정폭 숫자(`tabular-nums`)를 쓴다. |

![문제 목록 전후](images/design-polish-browse.png)
![펼침 카드 전후](images/design-polish-disclosure.png)

검증: Prettier 통과. 개발 서버 대상 Chromium에서 `beginner`, `security-records`, `theme`, `experience-ui`, `project-class-actions`, `control-spacing`, `design-refresh`, `terminal-system`, `interaction-refinements`, `personalized-security`, `project-check` 111개 중 94개 통과. 실패 17개 중 15개는 변경 전 코드에서도 같은 오류로 실패했다. 나머지 `theme`의 `/profile` 대비 검사 2개는 페이지 이동 중 실행 문맥이 사라지는 오류로, 변경 전 코드에서도 라이트·다크 중 하나가 번갈아 실패했다. 문제 목록, 프로젝트 점검, 보안 점검의 라이트·다크 대비 검사는 통과했다. Firefox·WebKit과 프로덕션 빌드는 확인하지 않았다.
4 changes: 4 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,7 @@
@import "../styles/desktop-layout.css";

@import "../styles/workspace-spacing.css";

@import "../styles/design-consistency.css";

@import "../styles/design-polish.css";
2 changes: 1 addition & 1 deletion src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default async function LoginPage({
</div>
<Card as="div" className="auth-card-content">
<SectionArtwork topic="progress" className="auth-artwork" eager />
<span className="eyebrow">YOUR NEXT REP</span>
<span className="eyebrow">계정 연결</span>
<h1>
로그인하고
<br />
Expand Down
23 changes: 18 additions & 5 deletions src/components/library/learning-preference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@
import { FieldLabel, Input, Status, Button } from "@/components/ui/primitives";
import { ThemedImage } from "@/components/theme/themed-image";
import { useId, useRef, useState } from "react";
import { Check, SlidersHorizontal } from "lucide-react";
import { ArrowRight, Check, SlidersHorizontal } from "lucide-react";
import { useLearningPreference } from "@/hooks/use-learning-preference";
import { learnerTypes } from "@/lib/learner-types";
import { Modal } from "@/components/ui/modal";

export function LearningPreferencePicker({ onSelect }: { onSelect?: () => void }) {
export function LearningPreferencePicker({
heading = "오늘은 무엇을 해볼까요?",
onSelect,
}: {
heading?: string;
onSelect?: () => void;
}) {
const { type, setType, storageError, hasChosen } = useLearningPreference();
const group = useId();
return (
<section className="persona-picker" aria-label="나에게 맞는 시작점">
<h2>오늘은 무엇을 해볼까요?</h2>
<h2>{heading}</h2>
<p className="persona-intro">
서비스 점검, AI 활용, 코딩 연습. 고른 작업에 맞춰 홈과 메뉴가 달라집니다.
</p>
Expand Down Expand Up @@ -52,8 +58,15 @@ export function LearningPreferencePicker({ onSelect }: { onSelect?: () => void }
<span>{item.description}</span>
</span>
<span className="persona-selected">
<Check size={14} aria-hidden="true" />
{hasChosen && type === item.id ? "선택됨" : "이 공간으로 시작"}
{hasChosen && type === item.id ? (
<>
<Check size={14} aria-hidden="true" /> 선택됨
</>
) : (
<>
이 공간으로 시작 <ArrowRight size={14} aria-hidden="true" />
</>
)}
</span>
</FieldLabel>
))}
Expand Down
2 changes: 1 addition & 1 deletion src/components/library/problem-generator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export function Generator({
<div className="terminal-icon">
<Sparkles size={25} />
</div>
<span className="eyebrow">MAKE YOUR NEXT CHALLENGE</span>
<span className="eyebrow">AI 문제 생성</span>
<h2>지금 필요한 문제를 만드세요.</h2>
<p>분야, 난이도와 주제를 고르면 AI가 문제와 힌트, 참고 정답을 만듭니다.</p>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/library/problem-library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function ProblemLibrary({
) : (
<div className="page-title">
<span className="eyebrow">
{initialView === "bookmarks" ? "SAVED FOR LATER" : "EXPLORE YOUR DOMAIN"}
{initialView === "bookmarks" ? "나중에 풀 문제" : "분야별 코딩 연습"}
</span>
<h1>
{initialView === "bookmarks"
Expand Down Expand Up @@ -163,7 +163,7 @@ export function ProblemLibrary({
<div className="section-heading">
<div>
<h2>
{title}
{isHome ? title : "문제 목록"}
<span>{total}</span>
</h2>
<p>연습할 분야와 난이도에 맞는 문제를 골라보세요.</p>
Expand Down
2 changes: 1 addition & 1 deletion src/components/library/problem-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function ProblemTable({ library, initialView, bookmarking, bookmark }: Pr
<DifficultyBadge level={p.difficulty} />
<span className="row-time mono">
{p.minutes}
<small> min</small>
<small>분</small>
</span>
<Button
className={`bookmark-button icon-button ${progress?.bookmarked ? "active" : ""}`}
Expand Down
2 changes: 1 addition & 1 deletion src/components/library/training-hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ export function TrainingHero({
<span className="eyebrow">나의 CODE:FIT</span>
<h1>내 프로젝트로 배우고, 직접 확인하세요</h1>
<ProjectQuickStart key={scope} scope={scope} />
<h2>관심 있는 학습 공간도 골라보세요</h2>
<LearningPreferencePicker
heading="관심 있는 학습 공간도 골라보세요"
onSelect={() =>
requestAnimationFrame(() => home.current?.querySelector<HTMLElement>("h1")?.focus())
}
Expand Down
3 changes: 3 additions & 0 deletions src/components/navigation/desktop-frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const workspaceRoutes = [
"/security-check",
"/learn",
"/handoff",
"/login",
"/profile",
"/privacy",
];

/** Keep product navigation stable while each page owns its content and mobile menu. */
Expand Down
12 changes: 10 additions & 2 deletions src/components/navigation/site-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import { Modal } from "@/components/ui/modal";
import { Menu, ArrowUpRight } from "lucide-react";
import { useRef, useState } from "react";

const pageLabels: Record<string, string> = {
"/login": "로그인",
"/profile": "내 계정과 기록",
"/privacy": "개인정보 안내",
};

/** Shared destinations outside the editor; learning stages keep their own local navigation. */
export function SiteHeader() {
const pathname = usePathname();
Expand Down Expand Up @@ -52,15 +58,17 @@ export function SiteHeader() {
CODE:FIT_
</Link>
<div className="desktop-location" aria-label="현재 위치">
<span>작업 공간</span>
<Link href="/">CODE:FIT</Link>
<span aria-hidden="true">/</span>
<strong>
{items.find(
(item) =>
item.href !== "/" &&
!item.href.includes("?") &&
(pathname === item.href || pathname.startsWith(item.href + "/")),
)?.label || "프로젝트 실습"}
)?.label ||
pageLabels[pathname] ||
"프로젝트 실습"}
</strong>
</div>
<div className="site-preference">
Expand Down
36 changes: 20 additions & 16 deletions src/components/projects/project-classes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,25 +230,29 @@ function ClassList({
(!searching || task.label.toLowerCase().includes(query.trim().toLowerCase())),
);
const pending = searching ? search.busy : busy;
// Search and a second "new analysis" button add nothing before the first project exists.
const empty = !overview.checks.length && !active.length && !searching;
return (
<>
<div className="class-toolbar">
<div>
<FieldLabel htmlFor="class-search">내 프로젝트 찾기</FieldLabel>
<Input
ref={searchInput}
id="class-search"
type="search"
maxLength={200}
value={query}
onChange={(e) => changeQuery(e.target.value)}
placeholder="클래스 이름, 학습 목표 또는 주소"
/>
{!empty && (
<div className="class-toolbar">
<div>
<FieldLabel htmlFor="class-search">내 프로젝트 찾기</FieldLabel>
<Input
ref={searchInput}
id="class-search"
type="search"
maxLength={200}
value={query}
onChange={(e) => changeQuery(e.target.value)}
placeholder="클래스 이름, 학습 목표 또는 주소"
/>
</div>
<AppLink className="primary-button" href="/project-check">
새 프로젝트 분석 <ArrowRight size={18} />
</AppLink>
</div>
<AppLink className="primary-button" href="/project-check">
새 프로젝트 분석 <ArrowRight size={18} />
</AppLink>
</div>
)}
{searching && (
<div className="class-search-status">
<p role="status" aria-live="polite">
Expand Down
4 changes: 2 additions & 2 deletions src/components/shell/help-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ interface HelpDialogProps {
}
export function HelpDialog({ helpOpen, setHelpOpen }: HelpDialogProps) {
return (
<Modal open={helpOpen} onClose={() => setHelpOpen(false)} title="QUICK START">
<Modal open={helpOpen} onClose={() => setHelpOpen(false)} title="사용 안내">
<div className="help-content">
<span className="eyebrow">WELCOME TO CODE:FIT</span>
<span className="eyebrow">CODE:FIT</span>
<h2>코드핏 시작하기</h2>
<ol>
<li>
Expand Down
2 changes: 1 addition & 1 deletion src/components/workspace/problem-workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export function ProblemWorkspace({
<ChevronRight size={13} />
<span>{domainLabel(problem.domain)}</span>
<span className="workspace-id mono">
{problem.id.startsWith("ai-") ? "AI CHALLENGE" : "CURATED CHALLENGE"}
{problem.id.startsWith("ai-") ? "AI 생성 문제" : "기본 문제"}
</span>
</div>
<div className="workspace-heading">
Expand Down
6 changes: 3 additions & 3 deletions src/components/workspace/workspace-confirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ export function WorkspaceConfirmation({
onClose={() => setConfirm(null)}
title={
confirm === "solution"
? "VIEW SOLUTION"
? "참고 정답 보기"
: confirm === "restore"
? "RESTORE SUBMISSION"
: "RESET CODE"
? "제출본 복원"
: "코드 초기화"
}
className="confirm-modal"
>
Expand Down
Loading
Loading