- 52개 이상의 컴포넌트에 isNextRedirectError 처리 추가 - Server Action의 redirect() 에러가 catch 블록에서 삼켜지는 문제 해결 - access_token + refresh_token 모두 만료 시 정상적으로 로그인 페이지로 리다이렉트 수정된 영역: - accounting: 10개 컴포넌트 - production: 12개 컴포넌트 - hr: 5개 컴포넌트 - settings: 8개 컴포넌트 - approval: 5개 컴포넌트 - items: 20개+ 컴포넌트 - board: 5개 컴포넌트 - quality: 4개 컴포넌트 - material, outbound, quotes 등 기타 컴포넌트 Co-Authored-By: Claude <noreply@anthropic.com>
12 KiB
모바일 반응형 패턴 가이드
작성일: 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)
// 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열 (권장)
// 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: 최소 너비 보장
// 카드 최소 너비 보장 + 자동 열 조정
<div className="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-4">
패턴 C: Flex Wrap (항목 수 가변적일 때)
<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: 가로 스크롤 (기본)
<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: 카드형 변환 (복잡한 데이터)
{/* 데스크탑: 테이블 */}
<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: 컬럼 숨김 (우선순위 기반)
<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: 텍스트 크기 반응형
// 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: 금액 포맷 함수 개선
// 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: 라벨 줄바꿈 허용
// Before
<p className="text-sm whitespace-nowrap">현금성 자산 합계</p>
// After
<p className="text-sm break-keep">현금성 자산 합계</p>
패턴 D: Truncate + Tooltip
<p className="text-sm truncate max-w-full" title={longLabel}>
{longLabel}
</p>
2.4 모달/다이얼로그
문제
모달이 344px 화면에서 좌우 여백 없이 꽉 차거나 넘침
해결 패턴
패턴 A: 최대 너비 반응형
// Before
<DialogContent className="max-w-2xl">
// After
<DialogContent className="max-w-[calc(100vw-2rem)] sm:max-w-lg md:max-w-2xl">
패턴 B: 전체 화면 모달 (복잡한 내용)
<DialogContent className="w-full h-full max-w-none sm:max-w-2xl sm:h-auto sm:max-h-[90vh]">
패턴 C: 모달 내부 스크롤
<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
// 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: 세로 배치 (모바일)
<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: 아이콘 전용 (극소 화면)
<Button className="gap-2">
<SaveIcon className="h-4 w-4" />
<span className="hidden xs:inline">저장</span>
</Button>
2.6 긴 텍스트 처리
문제
긴 제목, 설명, 메시지가 좁은 화면에서 레이아웃 깨짐
해결 패턴
패턴 A: Truncate (한 줄)
<h3 className="truncate max-w-full" title={title}>
{title}
</h3>
패턴 B: Line Clamp (여러 줄)
<p className="line-clamp-2 text-sm text-muted-foreground">
{description}
</p>
패턴 C: Break Keep (한글 단어 단위)
<p className="break-keep">
가지급금 인정이자 4.6%, 법인세 및 연말정산 시 대표자 종합세 가중 주의
</p>
패턴 D: 반응형 텍스트 크기
<h1 className="text-lg xs:text-xl md:text-2xl font-bold break-keep">
{title}
</h1>
2.7 헤더/네비게이션
문제
페이지 헤더의 타이틀과 액션 버튼이 충돌
해결 패턴
패턴 A: 세로 배치 (모바일)
<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: 아이콘 버튼 (극소 화면)
<Button size="sm" className="gap-1.5">
<SettingsIcon className="h-4 w-4" />
<span className="hidden xs:inline">항목 설정</span>
</Button>
2.8 패딩/마진 반응형
문제
데스크탑용 패딩이 모바일에서 공간 낭비
해결 패턴
// 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 자주 사용하는 반응형 패턴
/* 그리드 */
.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)
/* 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 페이지 단위 체크리스트
## 페이지: [페이지명]
테스트 뷰포트: 344px (Galaxy Fold)
### 레이아웃
- [ ] 헤더 타이틀/액션 버튼 충돌 없음
- [ ] 그리드 카드 오버플로우 없음
- [ ] 사이드바 접힘 상태 정상
### 텍스트
- [ ] 제목 텍스트 잘림/줄바꿈 정상
- [ ] 금액 표시 가독성 확보
- [ ] 라벨 텍스트 truncate 또는 줄바꿈
### 테이블
- [ ] 가로 스크롤 정상 동작
- [ ] 필수 컬럼 표시 확인
- [ ] 체크박스/액션 버튼 접근 가능
### 카드
- [ ] 카드 내용 오버플로우 없음
- [ ] 터치 영역 충분 (최소 44px)
- [ ] 카드 간 간격 적절
### 모달
- [ ] 화면 내 완전히 표시
- [ ] 닫기 버튼 접근 가능
- [ ] 내부 스크롤 정상
### 버튼
- [ ] 버튼 그룹 wrap 정상
- [ ] 터치 영역 충분
- [ ] 아이콘/텍스트 가독성
4.2 컴포넌트 단위 체크리스트
## 컴포넌트: [컴포넌트명]
### 필수 확인
- [ ] 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원" 표시 → 잘림
- "현금성 자산 합계" 라벨 → 잘림
적용 계획:
- 그리드:
grid-cols-1 xs:grid-cols-2 md:grid-cols-4 - 금액:
formatAmountResponsive()함수 사용 (억 단위) - 라벨:
break-keep또는truncate - 카드 패딩:
p-3 xs:p-4 md:p-6 - 헤더 버튼: 아이콘 전용 옵션
상세 계획: [PLAN] ceo-dashboard-refactoring.md 참조
6. 테스트 방법
6.1 Chrome DevTools 설정
- DevTools 열기 (F12)
- Device Toolbar (Ctrl+Shift+M)
- Edit → Add custom device:
- Name:
Galaxy Z Fold 5 (Folded) - Width:
344 - Height:
882 - Device pixel ratio:
3 - User agent: Mobile
- Name:
6.2 권장 테스트 순서
- 344px: 최소 지원 너비 (Galaxy Fold)
- 375px: iPhone SE
- 768px: 태블릿
- 1280px: 데스크탑
6.3 자동화 테스트 (Playwright)
// 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 | 초기 작성 |