Files
sam-react-prod/claudedocs/guides/[GUIDE] mobile-responsive-patterns.md
byeongcheolryu e56b7d53a4 fix(WEB): 토큰 만료 시 무한 로딩 대신 로그인 리다이렉트 처리
- 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>
2026-01-11 17:19:11 +09:00

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원" 표시 → 잘림
  • "현금성 자산 합계" 라벨 → 잘림

적용 계획:

  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)

// 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 초기 작성