# 모바일 반응형 패턴 가이드

> 작성일: 2026-01-10
> 적용 범위: SAM 프로젝트 전체
> 주요 대상 기기: Galaxy Z Fold 5 (접힌 상태 344px)

---

## 1. 브레이크포인트 정의

### 1.1 Tailwind 기본 브레이크포인트

| 접두사 | 최소 너비 | 대상 기기 |
|--------|----------|----------|
| (기본) | 0px | Galaxy Fold 접힌 (344px) |
| `xs` | 375px | iPhone SE, 소형 모바일 |
| `sm` | 640px | 대형 모바일, 소형 태블릿 |
| `md` | 768px | 태블릿 |
| `lg` | 1024px | 소형 데스크탑 |
| `xl` | 1280px | 데스크탑 |
| `2xl` | 1536px | 대형 데스크탑 |

### 1.2 커스텀 브레이크포인트 (tailwind.config.js)

```javascript
// tailwind.config.js
module.exports = {
  theme: {
    screens: {
      'xs': '375px',   // iPhone SE
      'sm': '640px',
      'md': '768px',
      'lg': '1024px',
      'xl': '1280px',
      '2xl': '1536px',
      // Galaxy Fold 전용 (선택적)
      'fold': '344px',
    },
  },
}
```

### 1.3 주요 테스트 뷰포트

| 기기 | 너비 | 높이 | 우선순위 |
|------|------|------|----------|
| Galaxy Z Fold 5 (접힌) | **344px** | 882px | 🔴 필수 |
| iPhone SE | 375px | 667px | 🔴 필수 |
| iPhone 14 Pro | 393px | 852px | 🟡 권장 |
| iPad Mini | 768px | 1024px | 🟡 권장 |
| Desktop | 1280px+ | - | 🟢 기본 |

---

## 2. 공통 패턴별 해결책

### 2.1 그리드 레이아웃

#### 문제
344px에서 `grid-cols-2`는 각 항목이 ~160px로 좁아져 텍스트 오버플로우 발생

#### 해결 패턴

**패턴 A: 1열 → 2열 → 4열 (권장)**
```tsx
// Before
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">

// After - 344px에서 1열
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-4">
```

**패턴 B: 최소 너비 보장**
```tsx
// 카드 최소 너비 보장 + 자동 열 조정
<div className="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-4">
```

**패턴 C: Flex Wrap (항목 수 가변적일 때)**
```tsx
<div className="flex flex-wrap gap-4">
  <div className="w-full xs:w-[calc(50%-0.5rem)] md:w-[calc(25%-0.75rem)]">
    {/* 카드 내용 */}
  </div>
</div>
```

#### 적용 기준
| 카드 개수 | 권장 패턴 |
|-----------|----------|
| 1-2개 | `grid-cols-1 xs:grid-cols-2` |
| 3-4개 | `grid-cols-1 xs:grid-cols-2 md:grid-cols-4` |
| 5개+ | `grid-cols-1 xs:grid-cols-2 md:grid-cols-3 lg:grid-cols-4` |

---

### 2.2 테이블 반응형

#### 문제
테이블이 344px 화면에서 가로 스크롤 없이 표시 불가

#### 해결 패턴

**패턴 A: 가로 스크롤 (기본)**
```tsx
<div className="overflow-x-auto -mx-4 px-4 md:mx-0 md:px-0">
  <table className="min-w-[600px] w-full">
    {/* 테이블 내용 */}
  </table>
</div>
```

**패턴 B: 카드형 변환 (복잡한 데이터)**
```tsx
{/* 데스크탑: 테이블 */}
<table className="hidden md:table">
  {/* 테이블 내용 */}
</table>

{/* 모바일: 카드 리스트 */}
<div className="md:hidden space-y-3">
  {data.map((item) => (
    <Card key={item.id}>
      <CardContent className="p-4">
        <div className="flex justify-between">
          <span className="text-sm text-muted-foreground">거래처</span>
          <span className="font-medium">{item.vendor}</span>
        </div>
        {/* 추가 필드 */}
      </CardContent>
    </Card>
  ))}
</div>
```

**패턴 C: 컬럼 숨김 (우선순위 기반)**
```tsx
<th className="hidden sm:table-cell">등록일</th>
<th className="hidden md:table-cell">수정일</th>
<th>필수 컬럼</th>

<td className="hidden sm:table-cell">{item.createdAt}</td>
<td className="hidden md:table-cell">{item.updatedAt}</td>
<td>{item.essential}</td>
```

---

### 2.3 카드 컴포넌트

#### 문제
카드 내 금액, 라벨이 좁은 화면에서 잘림

#### 해결 패턴

**패턴 A: 텍스트 크기 반응형**
```tsx
// Before
<p className="text-3xl font-bold">30,500,000,000원</p>

// After
<p className="text-xl xs:text-2xl md:text-3xl font-bold">30.5억원</p>
```

**패턴 B: 금액 포맷 함수 개선**
```typescript
// utils/format.ts
export const formatAmountResponsive = (amount: number, compact = false): string => {
  if (compact || amount >= 100000000) {
    // 억 단위
    const billion = amount / 100000000;
    return billion >= 1 ? `${billion.toFixed(1)}억원` : formatAmount(amount);
  }
  if (amount >= 10000) {
    // 만 단위
    const man = amount / 10000;
    return `${man.toFixed(0)}만원`;
  }
  return new Intl.NumberFormat('ko-KR').format(amount) + '원';
};
```

**패턴 C: 라벨 줄바꿈 허용**
```tsx
// Before
<p className="text-sm whitespace-nowrap">현금성 자산 합계</p>

// After
<p className="text-sm break-keep">현금성 자산 합계</p>
```

**패턴 D: Truncate + Tooltip**
```tsx
<p className="text-sm truncate max-w-full" title={longLabel}>
  {longLabel}
</p>
```

---

### 2.4 모달/다이얼로그

#### 문제
모달이 344px 화면에서 좌우 여백 없이 꽉 차거나 넘침

#### 해결 패턴

**패턴 A: 최대 너비 반응형**
```tsx
// Before
<DialogContent className="max-w-2xl">

// After
<DialogContent className="max-w-[calc(100vw-2rem)] sm:max-w-lg md:max-w-2xl">
```

**패턴 B: 전체 화면 모달 (복잡한 내용)**
```tsx
<DialogContent className="w-full h-full max-w-none sm:max-w-2xl sm:h-auto sm:max-h-[90vh]">
```

**패턴 C: 모달 내부 스크롤**
```tsx
<DialogContent className="max-h-[90vh] flex flex-col">
  <DialogHeader className="flex-shrink-0">
    {/* 헤더 */}
  </DialogHeader>
  <div className="flex-1 overflow-y-auto">
    {/* 스크롤 가능한 내용 */}
  </div>
  <DialogFooter className="flex-shrink-0">
    {/* 푸터 */}
  </DialogFooter>
</DialogContent>
```

---

### 2.5 버튼 그룹

#### 문제
여러 버튼이 가로로 나열될 때 344px에서 넘침

#### 해결 패턴

**패턴 A: Flex Wrap**
```tsx
// Before
<div className="flex gap-2">
  <Button>저장</Button>
  <Button>취소</Button>
  <Button>삭제</Button>
</div>

// After
<div className="flex flex-wrap gap-2">
  <Button className="flex-1 min-w-[80px]">저장</Button>
  <Button className="flex-1 min-w-[80px]">취소</Button>
  <Button className="flex-1 min-w-[80px]">삭제</Button>
</div>
```

**패턴 B: 세로 배치 (모바일)**
```tsx
<div className="flex flex-col xs:flex-row gap-2">
  <Button className="w-full xs:w-auto">저장</Button>
  <Button className="w-full xs:w-auto">취소</Button>
</div>
```

**패턴 C: 아이콘 전용 (극소 화면)**
```tsx
<Button className="gap-2">
  <SaveIcon className="h-4 w-4" />
  <span className="hidden xs:inline">저장</span>
</Button>
```

---

### 2.6 긴 텍스트 처리

#### 문제
긴 제목, 설명, 메시지가 좁은 화면에서 레이아웃 깨짐

#### 해결 패턴

**패턴 A: Truncate (한 줄)**
```tsx
<h3 className="truncate max-w-full" title={title}>
  {title}
</h3>
```

**패턴 B: Line Clamp (여러 줄)**
```tsx
<p className="line-clamp-2 text-sm text-muted-foreground">
  {description}
</p>
```

**패턴 C: Break Keep (한글 단어 단위)**
```tsx
<p className="break-keep">
  가지급금 인정이자 4.6%, 법인세 및 연말정산 시 대표자 종합세 가중 주의
</p>
```

**패턴 D: 반응형 텍스트 크기**
```tsx
<h1 className="text-lg xs:text-xl md:text-2xl font-bold break-keep">
  {title}
</h1>
```

---

### 2.7 헤더/네비게이션

#### 문제
페이지 헤더의 타이틀과 액션 버튼이 충돌

#### 해결 패턴

**패턴 A: 세로 배치 (모바일)**
```tsx
<div className="flex flex-col xs:flex-row xs:items-center xs:justify-between gap-2">
  <div>
    <h1 className="text-xl font-bold">{title}</h1>
    <p className="text-sm text-muted-foreground">{description}</p>
  </div>
  <div className="flex gap-2">
    <Button size="sm">액션</Button>
  </div>
</div>
```

**패턴 B: 아이콘 버튼 (극소 화면)**
```tsx
<Button size="sm" className="gap-1.5">
  <SettingsIcon className="h-4 w-4" />
  <span className="hidden xs:inline">항목 설정</span>
</Button>
```

---

### 2.8 패딩/마진 반응형

#### 문제
데스크탑용 패딩이 모바일에서 공간 낭비

#### 해결 패턴

```tsx
// Before
<div className="p-6">

// After
<div className="p-3 xs:p-4 md:p-6">

// 카드 내부
<CardContent className="p-3 xs:p-4 md:p-6">
```

---

## 3. Tailwind 유틸리티 클래스 모음

### 3.1 자주 사용하는 반응형 패턴

```css
/* 그리드 */
.grid-responsive-1-2-4: grid-cols-1 xs:grid-cols-2 md:grid-cols-4
.grid-responsive-1-2-3: grid-cols-1 xs:grid-cols-2 md:grid-cols-3
.grid-responsive-1-3: grid-cols-1 md:grid-cols-3

/* 텍스트 */
.text-responsive-sm: text-xs xs:text-sm
.text-responsive-base: text-sm xs:text-base
.text-responsive-lg: text-base xs:text-lg md:text-xl
.text-responsive-xl: text-lg xs:text-xl md:text-2xl
.text-responsive-2xl: text-xl xs:text-2xl md:text-3xl

/* 패딩 */
.p-responsive: p-3 xs:p-4 md:p-6
.px-responsive: px-3 xs:px-4 md:px-6
.py-responsive: py-3 xs:py-4 md:py-6

/* 갭 */
.gap-responsive: gap-2 xs:gap-3 md:gap-4

/* Flex 방향 */
.flex-col-to-row: flex-col xs:flex-row
```

### 3.2 커스텀 유틸리티 (globals.css)

```css
/* globals.css */
@layer utilities {
  .grid-responsive-cards {
    @apply grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4;
  }

  .text-amount {
    @apply text-xl xs:text-2xl md:text-3xl font-bold;
  }

  .card-padding {
    @apply p-3 xs:p-4 md:p-6;
  }

  .section-padding {
    @apply p-4 xs:p-5 md:p-6;
  }
}
```

---

## 4. 적용 체크리스트

### 4.1 페이지 단위 체크리스트

```markdown
## 페이지: [페이지명]
테스트 뷰포트: 344px (Galaxy Fold)

### 레이아웃
- [ ] 헤더 타이틀/액션 버튼 충돌 없음
- [ ] 그리드 카드 오버플로우 없음
- [ ] 사이드바 접힘 상태 정상

### 텍스트
- [ ] 제목 텍스트 잘림/줄바꿈 정상
- [ ] 금액 표시 가독성 확보
- [ ] 라벨 텍스트 truncate 또는 줄바꿈

### 테이블
- [ ] 가로 스크롤 정상 동작
- [ ] 필수 컬럼 표시 확인
- [ ] 체크박스/액션 버튼 접근 가능

### 카드
- [ ] 카드 내용 오버플로우 없음
- [ ] 터치 영역 충분 (최소 44px)
- [ ] 카드 간 간격 적절

### 모달
- [ ] 화면 내 완전히 표시
- [ ] 닫기 버튼 접근 가능
- [ ] 내부 스크롤 정상

### 버튼
- [ ] 버튼 그룹 wrap 정상
- [ ] 터치 영역 충분
- [ ] 아이콘/텍스트 가독성
```

### 4.2 컴포넌트 단위 체크리스트

```markdown
## 컴포넌트: [컴포넌트명]

### 필수 확인
- [ ] min-width 고정값 없음 또는 반응형 처리
- [ ] whitespace-nowrap 사용 시 truncate 동반
- [ ] grid-cols-N 사용 시 모바일 breakpoint 추가
- [ ] 패딩/마진 반응형 적용

### 권장 확인
- [ ] 텍스트 크기 반응형
- [ ] 버튼 크기 반응형
- [ ] 아이콘 크기 반응형
```

---

## 5. 적용 사례

### 5.1 CEO 대시보드 적용 예정

**현재 문제점**:
- `grid-cols-2 md:grid-cols-4` → 344px에서 카드당 ~160px
- 금액 "3,050,000,000원" 표시 → 잘림
- "현금성 자산 합계" 라벨 → 잘림

**적용 계획**:
1. 그리드: `grid-cols-1 xs:grid-cols-2 md:grid-cols-4`
2. 금액: `formatAmountResponsive()` 함수 사용 (억 단위)
3. 라벨: `break-keep` 또는 `truncate`
4. 카드 패딩: `p-3 xs:p-4 md:p-6`
5. 헤더 버튼: 아이콘 전용 옵션

**상세 계획**: `[PLAN] ceo-dashboard-refactoring.md` 참조

---

## 6. 테스트 방법

### 6.1 Chrome DevTools 설정

1. DevTools 열기 (F12)
2. Device Toolbar (Ctrl+Shift+M)
3. Edit → Add custom device:
   - Name: `Galaxy Z Fold 5 (Folded)`
   - Width: `344`
   - Height: `882`
   - Device pixel ratio: `3`
   - User agent: Mobile

### 6.2 권장 테스트 순서

1. **344px**: 최소 지원 너비 (Galaxy Fold)
2. **375px**: iPhone SE
3. **768px**: 태블릿
4. **1280px**: 데스크탑

### 6.3 자동화 테스트 (Playwright)

```typescript
// playwright.config.ts
const devices = [
  { name: 'Galaxy Fold', viewport: { width: 344, height: 882 } },
  { name: 'iPhone SE', viewport: { width: 375, height: 667 } },
  { name: 'iPad', viewport: { width: 768, height: 1024 } },
  { name: 'Desktop', viewport: { width: 1280, height: 800 } },
];
```

---

## 변경 이력

| 날짜 | 버전 | 변경 내용 |
|------|------|----------|
| 2026-01-10 | 1.0 | 초기 작성 |