snp-connection-monitoring/docs/design/code-conventions.md
HYOJIN c2a71c1b77 feat(design): 디자인 시스템 적용 (CSS 토큰, Button/Badge, 차트, 다크모드) (#48)
- 디자인 시스템 가이드 문서 11개 생성 (docs/design/)
- CSS 변수 토큰 시스템 (@theme + :root/.dark 전환)
- cn() 유틸리티 (clsx + tailwind-merge)
- Button/Badge 공통 컴포넌트 (variant/size, 다크모드 대응)
- 하드코딩 Tailwind 색상 → CSS 변수 토큰 리팩토링 (30개 파일)
- 차트 팔레트 다크모드 색상 업데이트 (CHART_COLORS_HEX)
- 버튼 다크모드 채도/대비 강화 (primary-600 기반)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:38:00 +09:00

213 lines
5.8 KiB
Markdown

# 코드 컨벤션
프론트엔드 디자인 시스템 적용을 위한 코드 작성 규칙.
---
## 파일 네이밍
| 유형 | 규칙 | 예시 |
|------|------|------|
| 컴포넌트 | PascalCase | `Button.tsx`, `UserCard.tsx` |
| 훅 | camelCase, `use` 접두사 | `useModal.ts`, `useTheme.ts` |
| 유틸리티 | camelCase | `cn.ts`, `formatDate.ts` |
| 상수 | camelCase | `chart.ts`, `routes.ts` |
| 타입 | camelCase | `user.ts`, `apihub.ts` |
| 스타일 | camelCase | `index.css` |
컴포넌트 파일 당 하나의 `export default` 컴포넌트.
---
## Tailwind 클래스 순서
가독성을 위해 다음 순서로 클래스를 작성한다. Prettier + prettier-plugin-tailwindcss로 자동 정렬 권장.
1. **레이아웃**`flex`, `grid`, `block`, `hidden`, `relative`, `fixed`, `z-*`
2. **크기**`w-*`, `h-*`, `min-w-*`, `max-w-*`, `size-*`
3. **여백**`m-*`, `mx-*`, `my-*`, `p-*`, `px-*`, `py-*`, `gap-*`
4. **테두리**`border*`, `rounded-*`, `ring-*`, `outline-*`
5. **배경**`bg-*`, `shadow-*`, `backdrop-*`
6. **텍스트**`text-*`, `font-*`, `leading-*`, `tracking-*`, `truncate`
7. **색상**`text-[var(*)]`, `bg-[var(*)]`
8. **상호작용**`cursor-*`, `select-*`, `pointer-events-*`
9. **트랜지션**`transition-*`, `duration-*`, `ease-*`, `animate-*`
10. **반응형**`sm:*`, `md:*`, `lg:*`, `xl:*`
11. **다크모드**`dark:*`
12. **상태**`hover:*`, `focus:*`, `active:*`, `disabled:*`, `aria-*:`
```tsx
// 올바른 예
<div className="flex items-center justify-between w-full p-4 border border-[var(--color-border)] rounded-lg bg-[var(--color-bg-surface)] text-sm text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-bg-elevated)]">
```
---
## cn() 유틸리티
`clsx` + `tailwind-merge` 조합. 조건부 클래스와 클래스 충돌 해결에 사용한다.
```ts
// src/utils/cn.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
```
### 사용 패턴
```tsx
// 기본 사용
<div className={cn('p-4 rounded-lg', className)} />
// 조건부 클래스
<button
className={cn(
'px-4 py-2 rounded-lg font-medium transition-colors',
variant === 'primary' && 'bg-[var(--color-primary)] text-white',
variant === 'ghost' && 'text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)]',
disabled && 'opacity-50 cursor-not-allowed',
className,
)}
/>
// 클래스 충돌 해결 (twMerge)
cn('p-4', 'p-6') // → 'p-6'
cn('text-sm', 'text-base') // → 'text-base'
```
### 규칙
- 모든 컴포넌트의 루트 엘리먼트에 `className` prop을 전달할 수 있도록 `cn()`으로 병합.
- variant별 클래스는 객체 맵으로 분리.
```tsx
const variants = {
primary: 'bg-[var(--color-primary)] text-white',
ghost: 'text-[var(--color-text-secondary)]',
} as const;
type Variant = keyof typeof variants;
<div className={cn(baseClass, variants[variant], className)} />
```
---
## Import 순서
ESLint `import/order` 규칙 기준.
```tsx
// 1. React
import { useState, useCallback } from 'react';
// 2. 외부 라이브러리
import { ChevronRight } from 'lucide-react';
// 3. 내부 모듈 (절대 경로)
import { cn } from '@/utils/cn';
import { CHART_COLORS } from '@/constants/chart';
import type { ButtonProps } from '@/types/ui';
// 4. 상대 경로 컴포넌트
import Badge from './Badge';
// 5. 스타일 (필요 시)
import styles from './Button.module.css';
```
---
## forwardRef
외부에서 DOM 요소에 직접 접근이 필요한 컴포넌트(Input, Button 등)에 적용.
```tsx
import { forwardRef } from 'react';
import { cn } from '@/utils/cn';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, className, ...props }, ref) => {
return (
<div className="flex flex-col gap-1.5">
{label && (
<label className="text-sm font-medium text-[var(--color-text-primary)]">
{label}
</label>
)}
<input
ref={ref}
className={cn(
'w-full rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-surface)]',
'px-3 py-2 text-base text-[var(--color-text-primary)]',
'placeholder:text-[var(--color-text-tertiary)]',
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent',
'disabled:opacity-50 disabled:cursor-not-allowed',
error && 'border-[var(--color-danger)] focus:ring-[var(--color-danger)]',
className,
)}
{...props}
/>
{error && (
<p className="text-xs text-[var(--color-danger)]">{error}</p>
)}
</div>
);
},
);
Input.displayName = 'Input';
export default Input;
```
---
## Props 타입 정의
- Props 타입은 `interface`로 정의. 이름은 `{ComponentName}Props`.
- HTML 속성 확장 시 `extends React.HTMLAttributes<HTMLElement>` 사용.
- 선택적 `className` prop 항상 포함.
```tsx
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
title: string;
description?: string;
variant?: 'default' | 'elevated' | 'flat';
}
```
---
## CSS 변수 사용
```tsx
// DO: CSS 변수 토큰 사용
<div className="bg-[var(--color-bg-surface)] text-[var(--color-text-primary)]" />
// DO NOT: HEX 직접 사용
<div className="bg-white text-gray-900" />
// DO NOT: Tailwind 기본 색상으로 브랜드 컬러 표현
<div className="bg-blue-500 text-blue-900" />
```
---
## 컴포넌트 파일 구조
```tsx
// 1. imports
// 2. 타입 정의
// 3. 상수 (variants 맵 등)
// 4. 컴포넌트 함수
// 5. export default
```