220 lines
6.3 KiB
Markdown
220 lines
6.3 KiB
Markdown
# Frontend Design Patterns
|
|
|
|
라온누리 프로젝트의 프론트엔드 디자인 패턴 가이드입니다.
|
|
`STYLE_GUIDE.md`(컬러, 아이콘, 테마 규칙)와 함께, **어떻게 느끼게 할 것인가**를 정의합니다.
|
|
|
|
---
|
|
|
|
## 1. 원칙
|
|
|
|
### 콘텐츠가 주인공이다
|
|
|
|
UI 크롬(타이틀 바, 보더, 패딩)은 최소화한다.
|
|
미디어 중심 UI(카메라, 이미지 뷰어, 크로퍼)는 풀블리드로, 컨트롤은 콘텐츠 위에 오버레이한다.
|
|
폼/설정 같은 도구 UI에서만 일반 크롬을 유지한다.
|
|
|
|
### 상태는 뚝 바뀌지 않는다
|
|
|
|
조건부 렌더링(`{ready && <UI />}`)은 레이아웃이 변하는 경우에만 쓴다.
|
|
그 외에는 `opacity + transition`으로 부드럽게 드러낸다.
|
|
|
|
```tsx
|
|
// ✅ opacity 전환
|
|
<Box css={{opacity: ready ? 1 : 0, transition: "opacity 0.4s ease"}}>
|
|
|
|
// ❌ 갑자기 나타남
|
|
{ready && <Overlay />}
|
|
```
|
|
|
|
### 아이콘은 맥락에 맞게
|
|
|
|
로딩/에러 상태에 제네릭 Spinner 대신, 해당 기능의 아이콘을 사용한다.
|
|
카메라 로딩엔 `LuCamera`, 업로드 로딩엔 `LuUpload`, 에러엔 해당 아이콘의 Off 변형(`LuCameraOff`).
|
|
|
|
### 비슷한 아이콘은 나란히 두지 않는다
|
|
|
|
같은 도구 모음 안에서 시각적으로 유사한 아이콘이 인접하면 혼란을 준다.
|
|
의미가 다른 아이콘이 비슷하게 보이면 아이콘 자체를 바꾼다.
|
|
|
|
```
|
|
✅ 회전 LuRotateCw | 초기화 LuUndo2 — 형태가 다름
|
|
❌ 회전 LuRotateCw | 초기화 LuRefreshCw — 둘 다 회전 화살표
|
|
```
|
|
|
|
---
|
|
|
|
## 2. 애니메이션
|
|
|
|
모든 애니메이션은 CSS `@keyframes` + Chakra `css` prop으로 구현한다.
|
|
|
|
### 수치 기준
|
|
|
|
| 용도 | duration | easing |
|
|
|------|----------|--------|
|
|
| 등장 (fadeIn) | 0.3s | ease-out |
|
|
| 호버 전환 | 0.2s | — |
|
|
| 상태 전환 (색상, 크기) | 0.3s | — |
|
|
| 오버레이 reveal | 0.4s | ease |
|
|
| 호흡 (pulse) | 2s | ease-in-out |
|
|
|
|
### 등장 — scale + fade
|
|
|
|
상태 변화(에러/성공/새 요소)에 사용. 0.3초, 살짝 작게 시작.
|
|
|
|
```tsx
|
|
css={{
|
|
animation: "fadeIn 0.3s ease-out",
|
|
"@keyframes fadeIn": {
|
|
from: {opacity: 0, transform: "scale(0.9)"},
|
|
to: {opacity: 1, transform: "scale(1)"},
|
|
},
|
|
}}
|
|
```
|
|
|
|
순차 등장이 필요하면 `animation-delay`를 `0.1s` 간격으로 준다.
|
|
|
|
### 호흡 — 준비 상태 표현
|
|
|
|
"사용 가능"을 은은하게 알린다. 2초 주기, 미세한 변화.
|
|
|
|
```tsx
|
|
// boxShadow 펄스 (버튼 주변 링)
|
|
"@keyframes pulse": {
|
|
"0%, 100%": {boxShadow: "0 0 0 0px {colors.brand.solid/30}"},
|
|
"50%": {boxShadow: "0 0 0 6px {colors.brand.solid/0}"},
|
|
}
|
|
|
|
// opacity + scale (아이콘)
|
|
"@keyframes pulse": {
|
|
"0%, 100%": {opacity: 0.4, transform: "scale(1)"},
|
|
"50%": {opacity: 1, transform: "scale(1.1)"},
|
|
}
|
|
```
|
|
|
|
### 촉각 피드백
|
|
|
|
모든 커스텀 버튼(`Box as="button"`)에 적용한다.
|
|
|
|
```tsx
|
|
css={{
|
|
"&:hover": { /* 배경/테두리 변화 */ },
|
|
"&:active": { transform: "scale(0.9)" }, // 눌림 느낌
|
|
}}
|
|
transition="all 0.2s"
|
|
```
|
|
|
|
### keyframe 네이밍
|
|
|
|
`{컴포넌트}{동작}` camelCase. 범용은 접두사 없이.
|
|
예: `cameraPulse`, `shutterReady`, `fadeIn`
|
|
|
|
---
|
|
|
|
## 3. 컴포넌트 패턴
|
|
|
|
### Frosted Glass Pill
|
|
|
|
미디어 위에 텍스트를 올릴 때 사용. 반투명 배경 + 블러.
|
|
|
|
```tsx
|
|
<Text fontSize="xs" color="white" px={3} py={1} borderRadius="full"
|
|
css={{background: "rgba(0,0,0,0.5)", backdropFilter: "blur(8px)"}}
|
|
/>
|
|
```
|
|
|
|
### 플로팅 툴바
|
|
|
|
미디어 위 도구 버튼을 frosted glass bar로 그룹화. 아이콘 전용, `title`로 레이블.
|
|
**성격이 다른 도구 그룹 사이에는 세로 디바이더**를 넣는다.
|
|
|
|
```tsx
|
|
<Box display="flex" alignItems="center" gap={1} px={3} py={1.5}
|
|
borderRadius="full"
|
|
css={{background: "rgba(0,0,0,0.5)", backdropFilter: "blur(8px)"}}
|
|
>
|
|
{/* 편집 도구 */}
|
|
<ToolButton icon={<LuRotateCw />} />
|
|
<ToolButton icon={<LuZoomIn />} />
|
|
<ToolButton icon={<LuZoomOut />} />
|
|
{/* 디바이더 — 파괴적 도구 분리 */}
|
|
<Box w="1px" h={5} mx={0.5} css={{background: "rgba(255,255,255,0.3)"}} />
|
|
<ToolButton icon={<LuUndo2 />} />
|
|
</Box>
|
|
```
|
|
|
|
ToolButton 스펙: `w={9} h={9}`, `borderRadius="full"`, hover `rgba(255,255,255,0.2)`, active `scale(0.9)`
|
|
|
|
### 은은한 글로우
|
|
|
|
어두운 배경 위 흰색 요소에 빛 번짐. `boxShadow="0 0 6px rgba(255,255,255,0.4)"`
|
|
|
|
### 안정적 비율 유지
|
|
|
|
비동기 콘텐츠(카메라, 이미지 로딩)는 `aspectRatio`를 고정해 레이아웃 시프트를 방지한다.
|
|
|
|
```tsx
|
|
<Box css={{aspectRatio: "4 / 3"}} bg="black">
|
|
<video style={{width: "100%", height: "100%", objectFit: "cover"}} />
|
|
</Box>
|
|
```
|
|
|
|
### 절대 위치 3단 레이아웃
|
|
|
|
컨트롤 바에서 좌/중앙/우 배치. 중앙은 자연 중앙, 좌우는 `position: absolute`.
|
|
|
|
```tsx
|
|
<Box display="flex" justifyContent="center" position="relative">
|
|
<Box position="absolute" left={8}>{/* 보조 버튼 */}</Box>
|
|
<Box>{/* 주요 버튼 — 항상 정중앙 */}</Box>
|
|
<Box position="absolute" right={8} w={11} />{/* 균형 */}
|
|
</Box>
|
|
```
|
|
|
|
---
|
|
|
|
## 4. 상태 표현
|
|
|
|
### 색상으로 구분
|
|
|
|
| 상태 | 색상 | cursor |
|
|
|------|------|--------|
|
|
| 비활성/로딩 | `border.muted` | `not-allowed` |
|
|
| 활성/준비 | `brand.solid` | `pointer` |
|
|
| 에러 | `red.subtle` / `red.fg` | — |
|
|
|
|
비활성 → 활성 전환에 `transition: "all 0.3s"`를 적용한다.
|
|
|
|
### 에러 상태
|
|
|
|
관련 아이콘을 `subtle` 배경 원 안에 배치 + fadeIn 등장. 아래에 설명 텍스트.
|
|
|
|
```tsx
|
|
<VStack gap={4} py={16}>
|
|
<Box p={4} borderRadius="full" bg="red.subtle" color="red.fg"
|
|
css={{animation: "fadeIn 0.3s ease-out", ...}}>
|
|
<LuCameraOff size={32} />
|
|
</Box>
|
|
<Text color="fg.muted" textAlign="center" fontSize="sm">
|
|
{errorMessage}
|
|
</Text>
|
|
</VStack>
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 체크리스트
|
|
|
|
새 컴포넌트를 만들 때:
|
|
|
|
- [ ] 비동기 콘텐츠 영역에 `aspectRatio` 고정했는가?
|
|
- [ ] 로딩/에러 상태에 맥락 부합 아이콘을 사용했는가?
|
|
- [ ] 커스텀 버튼에 hover 전환 + active `scale(0.9)` 피드백이 있는가?
|
|
- [ ] 상태 전환에 `transition` 또는 `animation`을 적용했는가?
|
|
- [ ] 미디어 위 텍스트에 frosted glass pill을 사용했는가?
|
|
- [ ] 도구 모음에서 성격이 다른 그룹을 디바이더로 분리했는가?
|
|
- [ ] 인접 아이콘이 시각적으로 혼동되지 않는가?
|
|
|
|
---
|
|
|
|
© 2024 BlueNovaLab. All rights reserved.
|