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
29 changes: 28 additions & 1 deletion docs/DESIGN-SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,34 @@ src/pages/
"이 색을 언제 써야 하는가"는 정의되어 있지 않다. 그래서 새 컴포넌트를 만들 때 참조할
기준이 없고, 결국 눈대중으로 `rgba`를 적게 된다.

## 현재 정의된 토큰
## 토큰 체계 (2계층)

컴포넌트는 **의미 토큰만** 참조한다. 원시 토큰을 직접 쓰지 않는다.

| 파일 | 역할 |
| ------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `src/styles/tokens.js` | **원시 토큰**. 팔레트·간격·반경·그림자·흐림·전환. 값 그 자체이며 "언제 쓰는지"는 말하지 않는다 |
| `src/styles/semantic.js` | **의미 토큰**. `surface`·`text`·`border`·`action`·`feedback`·`rank`·`glass`·`layer`. 이름이 용도를 말한다 |
| `src/styles/theme.js` | 호환 레이어. 기존 컴포넌트가 쓰던 이름(`colors`, `glassmorphism` 등)을 유지하되 값은 전부 의미 토큰을 참조한다 |

### 왜 나눴나

`palette.lilac90`은 무슨 색인지만 알려주지만 `action.primary`는 **어디에 써야 하는지**를 알려준다.
이름에 용도가 없으면 새 컴포넌트를 만들 때 참조할 기준이 없어 결국 눈대중으로 `rgba`를 적게 된다.

### 어떻게 쓰나

```js
// 새 코드 — 의미 토큰을 직접 import
import { action, surface, text } from "../styles/semantic";

// 기존 코드 — theme.js 그대로 동작 (값은 의미 토큰을 참조)
import { colors, glassmorphism, media } from "../styles/theme";
```

색을 바꾸려면 `tokens.js`의 팔레트 한 곳만 고치면 의미 토큰과 호환 레이어를 타고 전체에 반영된다.

## 기존 토큰 (호환 레이어)

`src/styles/theme.js` 기준이다.

Expand Down
104 changes: 104 additions & 0 deletions src/styles/semantic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { blur, palette, radius, shadow, transition } from "./tokens";

/**
* 의미 토큰 (semantic tokens)
*
* **컴포넌트는 이것만 참조한다.** 원시 토큰(tokens.js)을 직접 쓰지 않는다.
*
* 이름이 "언제 쓰는지"를 말한다. `palette.lilac90`은 무슨 색인지만 알려주지만
* `action.primary`는 어디에 써야 하는지 알려준다. 그래야 새 컴포넌트를 만들 때
* 눈대중으로 색을 고르지 않게 된다.
*/

/** 표면 — 배경으로 깔리는 것 */
export const surface = {
glass: palette.black40,
glassHover: palette.black50,
raised: palette.white10,
raisedHover: palette.white15,
};

/** 텍스트 */
export const text = {
primary: palette.white,
secondary: palette.white70,
tertiary: palette.white60,
accent: palette.lilac90,
onAction: palette.white,
};

/** 테두리 */
export const border = {
subtle: palette.white15,
accent: palette.lilac30,
accentStrong: palette.lilac50,
danger: palette.red30,
};

/** 액션 — 버튼처럼 누를 수 있는 것 */
export const action = {
primary: palette.lilac90,
primaryHover: palette.lilac100,
secondary: palette.white15,
secondaryHover: palette.lilac30,
};

/** 피드백 — 상태 알림 */
export const feedback = {
error: palette.red90,
errorSurface: palette.red10,
errorBorder: palette.red30,
warning: palette.amber90,
success: palette.green90,
};

/** 순위 표시 */
export const rank = {
first: palette.gold,
second: palette.silver,
third: palette.bronze,
};

/**
* 이 프로젝트의 시각적 정체성.
* 카드·모달의 기본 표면 처리다.
*/
export const glass = {
background: surface.glass,
backdropFilter: blur.card,
border: `1px solid ${border.subtle}`,
boxShadow: shadow.card,
borderRadius: radius.lg,
};

export const glassHover = {
background: surface.glassHover,
borderColor: border.accent,
boxShadow: `${shadow.cardHover}, 0 0 0 1px ${palette.lilac20}, inset 0 1px 0 ${palette.white10}`,
transform: "translateY(-4px)",
};

/** 레이어 순서 */
export const layer = {
base: 1,
dropdown: 10,
sticky: 100,
scrollButton: 100,
modal: 1000,
progressBar: 1000,
};

const semantic = {
surface,
text,
border,
action,
feedback,
rank,
glass,
glassHover,
layer,
transition,
};

export default semantic;
153 changes: 75 additions & 78 deletions src/styles/theme.js
Original file line number Diff line number Diff line change
@@ -1,86 +1,92 @@
// 공통 테마 스타일 시스템
/**
* 테마 (호환 레이어)
*
* 토큰 체계는 2계층이다:
* tokens.js — 원시 토큰. 값 그 자체
* semantic.js — 의미 토큰. "언제 쓰는지"를 이름이 말한다
*
* **새 코드는 semantic.js를 직접 import한다.** 이 파일은 기존 컴포넌트가 쓰던
* 이름(colors, glassmorphism 등)을 유지하기 위한 호환 레이어이며,
* 값은 전부 의미 토큰을 참조하므로 tokens.js 한 곳만 고치면 전체가 따라 바뀐다.
*/
import {
action,
border,
feedback,
glass,
glassHover,
layer,
rank,
surface,
text,
} from "./semantic";
import { palette, radius, shadow, spacing, transition } from "./tokens";

export const colors = {
primary: "rgba(200, 182, 226, 0.9)",
glassBg: "rgba(0, 0, 0, 0.4)",
glassBorder: "rgba(255, 255, 255, 0.15)",
textPrimary: "white",
textSecondary: "rgba(255, 255, 255, 0.7)",
textTertiary: "rgba(255, 255, 255, 0.6)",
error: "rgba(220, 53, 69, 0.9)",
errorBg: "rgba(220, 53, 69, 0.1)",
errorBorder: "rgba(220, 53, 69, 0.3)",
warning: "rgba(255, 193, 7, 0.9)",
success: "rgba(40, 167, 69, 0.9)",

// 랭킹 컬러
gold: "#FFD700",
silver: "#C0C0C0",
bronze: "#CD7F32",
primary: action.primary,
glassBg: surface.glass,
glassBorder: border.subtle,
textPrimary: text.primary,
textSecondary: text.secondary,
textTertiary: text.tertiary,
error: feedback.error,
errorBg: feedback.errorSurface,
errorBorder: feedback.errorBorder,
warning: feedback.warning,
success: feedback.success,

gold: rank.first,
silver: rank.second,
bronze: rank.third,
};

export const glassmorphism = {
background: colors.glassBg,
backdropFilter: "blur(15px)",
border: `1px solid ${colors.glassBorder}`,
boxShadow: "0 8px 32px 0 rgba(0, 0, 0, 0.5)",
borderRadius: "20px",
};
export const glassmorphism = glass;

export const glassCard = {
...glassmorphism,
transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)",

hover: {
background: "rgba(0, 0, 0, 0.5)",
borderColor: "rgba(200, 182, 226, 0.4)",
boxShadow: `
0 12px 40px rgba(0, 0, 0, 0.6),
0 0 0 1px rgba(200, 182, 226, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.1)
`,
transform: "translateY(-4px)",
},
...glass,
transition: transition.smooth,
hover: glassHover,
};

export const buttons = {
primary: {
background: colors.primary,
border: `1px solid ${colors.glassBorder}`,
color: colors.textPrimary,
borderRadius: "12px",
transition: "all 0.3s ease",
background: action.primary,
border: `1px solid ${border.subtle}`,
color: text.onAction,
borderRadius: radius.md,
transition: transition.fast,

hover: {
background: "rgba(200, 182, 226, 1)",
background: action.primaryHover,
transform: "translateY(-2px)",
boxShadow: "0 6px 20px rgba(200, 182, 226, 0.3)",
boxShadow: shadow.buttonHover,
},
},

glass: {
...glassmorphism,
borderRadius: "12px",
color: colors.textPrimary,
transition: "all 0.3s ease",
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
...glass,
borderRadius: radius.md,
color: text.onAction,
transition: transition.fast,
boxShadow: shadow.button,

hover: {
background: "rgba(200, 182, 226, 0.3)",
borderColor: "rgba(200, 182, 226, 0.5)",
background: action.secondaryHover,
borderColor: border.accentStrong,
transform: "translateY(-2px)",
boxShadow: "0 6px 20px rgba(200, 182, 226, 0.3)",
boxShadow: shadow.buttonHover,
},
},

retry: {
background: "rgba(200, 182, 226, 0.8)",
border: `1px solid ${colors.glassBorder}`,
color: colors.textPrimary,
borderRadius: "12px",
transition: "all 0.3s ease",
border: `1px solid ${border.subtle}`,
color: text.onAction,
borderRadius: radius.md,
transition: transition.fast,

hover: {
background: "rgba(200, 182, 226, 1)",
background: action.primaryHover,
transform: "translateY(-2px)",
},
},
Expand Down Expand Up @@ -122,45 +128,37 @@ export const breakpoints = {
large: "1440px",
};

export const zIndex = {
base: 1,
dropdown: 10,
sticky: 100,
modal: 1000,
progressBar: 1000,
scrollButton: 100,
};
export const zIndex = layer;

// 미디어 쿼리 헬퍼
// 미디어 쿼리 헬퍼 — 브레이크포인트를 컴포넌트에 하드코딩하지 않기 위한 것
export const media = {
mobile: `@media (max-width: ${breakpoints.mobile})`,
tablet: `@media (max-width: ${breakpoints.tablet})`,
desktop: `@media (min-width: ${breakpoints.desktop})`,
large: `@media (min-width: ${breakpoints.large})`,
};

// 공통 컴포넌트 스타일
export const componentStyles = {
detailRow: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
padding: "0.8rem 1.2rem",
background: "rgba(255, 255, 255, 0.1)",
padding: `${spacing.md} ${spacing.lg}`,
background: surface.raised,
backdropFilter: "blur(10px)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "12px",
transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)",
border: `1px solid ${palette.white10}`,
borderRadius: radius.md,
transition: transition.smooth,
cursor: "pointer",
overflow: "hidden",
position: "relative",

hover: {
background: "rgba(255, 255, 255, 0.15)",
borderColor: "rgba(200, 182, 226, 0.4)",
background: surface.raisedHover,
borderColor: border.accent,
transform: "translateX(3px) scale(1.01)",
boxShadow: "0 4px 15px rgba(200, 182, 226, 0.15)",
boxShadow: shadow.subtle,
},
},

Expand All @@ -169,11 +167,11 @@ export const componentStyles = {
width: "8px",
},
"&::-webkit-scrollbar-track": {
background: "rgba(255, 255, 255, 0.1)",
background: surface.raised,
borderRadius: "4px",
},
"&::-webkit-scrollbar-thumb": {
background: "rgba(200, 182, 226, 0.5)",
background: border.accentStrong,
borderRadius: "4px",
},
"&::-webkit-scrollbar-thumb:hover": {
Expand All @@ -182,7 +180,6 @@ export const componentStyles = {
},
};

// 익명 객체 export는 import 측에서 이름 추적이 어렵다
const theme = {
colors,
glassmorphism,
Expand Down
Loading
Loading