Merge remote-tracking branch 'origin/master'
# Conflicts: # src/components/production/WorkOrders/WorkOrderCreate.tsx # src/components/production/WorkOrders/WorkOrderDetail.tsx # src/components/production/WorkOrders/WorkOrderList.tsx
This commit is contained in:
331
claudedocs/[PLAN] ceo-dashboard-refactoring.md
Normal file
331
claudedocs/[PLAN] ceo-dashboard-refactoring.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# CEO 대시보드 리팩토링 계획
|
||||
|
||||
> 작성일: 2026-01-10
|
||||
> 대상 파일: `src/components/business/CEODashboard/`
|
||||
> 목표: 파일 분리 + 모바일(344px) 대응
|
||||
|
||||
---
|
||||
|
||||
## 1. 현재 상태 분석
|
||||
|
||||
### 1.1 파일 구조
|
||||
|
||||
```
|
||||
CEODashboard/
|
||||
├── CEODashboard.tsx # 1,648줄 ⚠️ 분리 필요
|
||||
├── components.tsx # 312줄 ✅ 적정
|
||||
├── types.ts # ~100줄 ✅ 적정
|
||||
├── sections/
|
||||
│ ├── index.ts
|
||||
│ ├── TodayIssueSection.tsx # 73줄 ✅
|
||||
│ ├── DailyReportSection.tsx # 37줄 ✅
|
||||
│ ├── MonthlyExpenseSection.tsx # 38줄 ✅
|
||||
│ ├── CardManagementSection.tsx # ~50줄 ✅
|
||||
│ ├── EntertainmentSection.tsx # ~50줄 ✅
|
||||
│ ├── WelfareSection.tsx # ~50줄 ✅
|
||||
│ ├── ReceivableSection.tsx # ~50줄 ✅
|
||||
│ ├── DebtCollectionSection.tsx # ~50줄 ✅
|
||||
│ ├── VatSection.tsx # ~50줄 ✅
|
||||
│ └── CalendarSection.tsx # ~100줄 ✅
|
||||
├── modals/
|
||||
│ ├── ScheduleDetailModal.tsx # ~200줄 ✅
|
||||
│ └── DetailModal.tsx # ~300줄 ✅
|
||||
└── dialogs/
|
||||
└── DashboardSettingsDialog.tsx # ~200줄 ✅
|
||||
```
|
||||
|
||||
### 1.2 CEODashboard.tsx 내부 분석 (1,648줄)
|
||||
|
||||
| 줄 범위 | 내용 | 줄 수 | 분리 대상 |
|
||||
|---------|------|-------|----------|
|
||||
| 1-26 | imports | 26 | - |
|
||||
| 27-370 | mockData 객체 | **344** | ✅ 분리 |
|
||||
| 371-748 | handleMonthlyExpenseCardClick (모달 config) | **378** | ✅ 분리 |
|
||||
| 749-1019 | handleCardManagementCardClick (모달 config) | **271** | ✅ 분리 |
|
||||
| 1020-1247 | handleEntertainmentCardClick (모달 config) | **228** | ✅ 분리 |
|
||||
| 1248-1375 | handleWelfareCardClick (모달 config) | **128** | ✅ 분리 |
|
||||
| 1376-1465 | handleVatClick (모달 config) | **90** | ✅ 분리 |
|
||||
| 1466-1509 | 캘린더 관련 핸들러 | 44 | - |
|
||||
| 1510-1648 | 컴포넌트 렌더링 | 139 | - |
|
||||
|
||||
**분리 대상 총합**: ~1,439줄 (87%)
|
||||
**분리 후 예상**: ~210줄
|
||||
|
||||
---
|
||||
|
||||
## 2. 분리 계획
|
||||
|
||||
### 2.1 목표 구조
|
||||
|
||||
```
|
||||
CEODashboard/
|
||||
├── CEODashboard.tsx # ~250줄 (컴포넌트 + 핸들러)
|
||||
├── components.tsx # 312줄 (유지)
|
||||
├── types.ts # ~100줄 (유지)
|
||||
├── mockData.ts # 🆕 ~350줄 (목데이터)
|
||||
├── modalConfigs/ # 🆕 모달 설정 분리
|
||||
│ ├── index.ts
|
||||
│ ├── monthlyExpenseConfigs.ts # ~380줄
|
||||
│ ├── cardManagementConfigs.ts # ~280줄
|
||||
│ ├── entertainmentConfigs.ts # ~230줄
|
||||
│ ├── welfareConfigs.ts # ~130줄
|
||||
│ └── vatConfigs.ts # ~100줄
|
||||
├── sections/ # (유지)
|
||||
├── modals/ # (유지)
|
||||
└── dialogs/ # (유지)
|
||||
```
|
||||
|
||||
### 2.2 분리 파일 상세
|
||||
|
||||
#### A. mockData.ts (신규)
|
||||
|
||||
```typescript
|
||||
// mockData.ts
|
||||
import type { CEODashboardData } from './types';
|
||||
|
||||
export const mockData: CEODashboardData = {
|
||||
todayIssue: [...],
|
||||
dailyReport: {...},
|
||||
monthlyExpense: {...},
|
||||
cardManagement: {...},
|
||||
entertainment: {...},
|
||||
welfare: {...},
|
||||
receivable: {...},
|
||||
debtCollection: {...},
|
||||
vat: {...},
|
||||
calendarSchedules: [...],
|
||||
};
|
||||
```
|
||||
|
||||
#### B. modalConfigs/index.ts (신규)
|
||||
|
||||
```typescript
|
||||
// modalConfigs/index.ts
|
||||
export { getMonthlyExpenseModalConfig } from './monthlyExpenseConfigs';
|
||||
export { getCardManagementModalConfig } from './cardManagementConfigs';
|
||||
export { getEntertainmentModalConfig } from './entertainmentConfigs';
|
||||
export { getWelfareModalConfig } from './welfareConfigs';
|
||||
export { getVatModalConfig } from './vatConfigs';
|
||||
```
|
||||
|
||||
#### C. 개별 모달 config 파일 예시
|
||||
|
||||
```typescript
|
||||
// modalConfigs/monthlyExpenseConfigs.ts
|
||||
import type { DetailModalConfig } from '../types';
|
||||
|
||||
export function getMonthlyExpenseModalConfig(cardId: string): DetailModalConfig | null {
|
||||
const configs: Record<string, DetailModalConfig> = {
|
||||
me1: { title: '당월 매입 상세', ... },
|
||||
me2: { title: '당월 카드 상세', ... },
|
||||
me3: { title: '당월 발행어음 상세', ... },
|
||||
me4: { title: '당월 지출 예상 상세', ... },
|
||||
};
|
||||
return configs[cardId] || null;
|
||||
}
|
||||
```
|
||||
|
||||
#### D. CEODashboard.tsx (리팩토링 후)
|
||||
|
||||
```typescript
|
||||
// CEODashboard.tsx (리팩토링 후 ~250줄)
|
||||
import { mockData } from './mockData';
|
||||
import {
|
||||
getMonthlyExpenseModalConfig,
|
||||
getCardManagementModalConfig,
|
||||
getEntertainmentModalConfig,
|
||||
getWelfareModalConfig,
|
||||
getVatModalConfig,
|
||||
} from './modalConfigs';
|
||||
|
||||
export function CEODashboard() {
|
||||
// 상태 관리
|
||||
const [data] = useState<CEODashboardData>(mockData);
|
||||
const [detailModalConfig, setDetailModalConfig] = useState<DetailModalConfig | null>(null);
|
||||
// ...
|
||||
|
||||
// 간소화된 핸들러
|
||||
const handleMonthlyExpenseCardClick = useCallback((cardId: string) => {
|
||||
const config = getMonthlyExpenseModalConfig(cardId);
|
||||
if (config) {
|
||||
setDetailModalConfig(config);
|
||||
setIsDetailModalOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 렌더링
|
||||
return (...);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 모바일 대응 계획
|
||||
|
||||
### 3.1 적용 대상 컴포넌트
|
||||
|
||||
| 컴포넌트 | 현재 상태 | 변경 필요 |
|
||||
|----------|----------|----------|
|
||||
| TodayIssueSection | `grid-cols-2 md:grid-cols-4` | ✅ `grid-cols-1 xs:grid-cols-2 md:grid-cols-4` |
|
||||
| DailyReportSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| MonthlyExpenseSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| CardManagementSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| EntertainmentSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| WelfareSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| ReceivableSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| DebtCollectionSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| VatSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| AmountCardItem (공통) | 고정 텍스트 크기 | ✅ 반응형 텍스트 |
|
||||
| IssueCardItem (공통) | 고정 텍스트 크기 | ✅ 반응형 텍스트 |
|
||||
| PageHeader | 가로 배치 | ✅ 세로/가로 반응형 |
|
||||
|
||||
### 3.2 components.tsx 변경 사항
|
||||
|
||||
#### AmountCardItem
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
<p className="text-2xl md:text-3xl font-bold">
|
||||
{formatCardAmount(card.amount)}
|
||||
</p>
|
||||
|
||||
// After
|
||||
<p className="text-lg xs:text-xl md:text-2xl lg:text-3xl font-bold truncate">
|
||||
{formatCardAmount(card.amount)}
|
||||
</p>
|
||||
<p className="text-xs xs:text-sm font-medium mb-1 xs:mb-2 break-keep">
|
||||
{card.label}
|
||||
</p>
|
||||
```
|
||||
|
||||
#### IssueCardItem
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
<p className="text-2xl md:text-3xl font-bold">
|
||||
{typeof count === 'number' ? `${count}건` : count}
|
||||
</p>
|
||||
|
||||
// After
|
||||
<p className="text-lg xs:text-xl md:text-2xl lg:text-3xl font-bold">
|
||||
{typeof count === 'number' ? `${count}건` : count}
|
||||
</p>
|
||||
```
|
||||
|
||||
### 3.3 섹션 공통 변경
|
||||
|
||||
```tsx
|
||||
// Before (모든 섹션)
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
|
||||
// After
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4">
|
||||
```
|
||||
|
||||
### 3.4 CardContent 패딩
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
<CardContent className="p-6">
|
||||
|
||||
// After
|
||||
<CardContent className="p-3 xs:p-4 md:p-6">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 실행 계획
|
||||
|
||||
### Phase 1: 파일 분리 (예상 30분)
|
||||
|
||||
- [ ] **1.1** `mockData.ts` 생성 및 데이터 이동
|
||||
- [ ] **1.2** `modalConfigs/` 폴더 생성
|
||||
- [ ] **1.3** `monthlyExpenseConfigs.ts` 생성
|
||||
- [ ] **1.4** `cardManagementConfigs.ts` 생성
|
||||
- [ ] **1.5** `entertainmentConfigs.ts` 생성
|
||||
- [ ] **1.6** `welfareConfigs.ts` 생성
|
||||
- [ ] **1.7** `vatConfigs.ts` 생성
|
||||
- [ ] **1.8** `modalConfigs/index.ts` 생성
|
||||
- [ ] **1.9** `CEODashboard.tsx` 리팩토링
|
||||
- [ ] **1.10** import 정리 및 동작 확인
|
||||
|
||||
### Phase 2: 모바일 대응 (예상 30분)
|
||||
|
||||
- [ ] **2.1** `components.tsx` - AmountCardItem 반응형 적용
|
||||
- [ ] **2.2** `components.tsx` - IssueCardItem 반응형 적용
|
||||
- [ ] **2.3** `sections/*.tsx` - 그리드 반응형 적용 (일괄)
|
||||
- [ ] **2.4** `sections/*.tsx` - CardContent 패딩 반응형 적용
|
||||
- [ ] **2.5** PageHeader 반응형 확인
|
||||
- [ ] **2.6** 344px 테스트 및 미세 조정
|
||||
|
||||
### Phase 3: 검증 (예상 15분)
|
||||
|
||||
- [ ] **3.1** 빌드 확인 요청
|
||||
- [ ] **3.2** 데스크탑(1280px) 동작 확인
|
||||
- [ ] **3.3** 태블릿(768px) 동작 확인
|
||||
- [ ] **3.4** 모바일(375px) 동작 확인
|
||||
- [ ] **3.5** Galaxy Fold(344px) 동작 확인
|
||||
|
||||
---
|
||||
|
||||
## 5. 예상 결과
|
||||
|
||||
### 5.1 파일 크기 변화
|
||||
|
||||
| 파일 | Before | After |
|
||||
|------|--------|-------|
|
||||
| CEODashboard.tsx | 1,648줄 | ~250줄 |
|
||||
| mockData.ts | - | ~350줄 |
|
||||
| modalConfigs/*.ts | - | ~1,100줄 (5개 파일) |
|
||||
|
||||
### 5.2 장점
|
||||
|
||||
1. **유지보수성**: 각 파일이 단일 책임 원칙 준수
|
||||
2. **재사용성**: 모달 config를 다른 곳에서 재사용 가능
|
||||
3. **확장성**: 새 모달 추가 시 별도 파일로 분리
|
||||
4. **가독성**: 핵심 로직만 CEODashboard.tsx에 유지
|
||||
5. **API 전환 용이**: mockData.ts만 교체하면 됨
|
||||
|
||||
### 5.3 모바일 개선 효과
|
||||
|
||||
| 항목 | Before (344px) | After (344px) |
|
||||
|------|----------------|---------------|
|
||||
| 카드 배치 | 2열 (160px/카드) | 1열 (320px/카드) |
|
||||
| 금액 표시 | 잘림 가능 | 완전 표시 |
|
||||
| 라벨 표시 | 잘림 가능 | 줄바꿈/truncate |
|
||||
| 패딩 | 과다 (24px) | 적정 (12px) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 참고 문서
|
||||
|
||||
- **모바일 대응 가이드**: `claudedocs/guides/[GUIDE] mobile-responsive-patterns.md`
|
||||
- **기존 테스트 계획**: `claudedocs/[PLAN] mobile-overflow-testing.md`
|
||||
|
||||
---
|
||||
|
||||
## 7. 의사결정 사항
|
||||
|
||||
### Q1: mockData를 별도 파일로?
|
||||
- **결정**: ✅ 분리
|
||||
- **이유**: 향후 API 연동 시 교체 용이
|
||||
|
||||
### Q2: 모달 config를 폴더로?
|
||||
- **결정**: ✅ 폴더로 분리
|
||||
- **이유**: 각 config가 100줄 이상, 단일 파일은 여전히 큼
|
||||
|
||||
### Q3: 모바일에서 1열 vs 2열?
|
||||
- **결정**: 344px 이하 1열, 375px 이상 2열
|
||||
- **이유**: Galaxy Fold 160px 카드는 너무 좁음
|
||||
|
||||
---
|
||||
|
||||
## 8. 시작 조건
|
||||
|
||||
- [x] 계획서 작성 완료
|
||||
- [x] 모바일 가이드 작성 완료
|
||||
- [ ] 사용자 승인
|
||||
|
||||
---
|
||||
|
||||
> **다음 단계**: 계획 승인 후 Phase 1 (파일 분리) 시작
|
||||
538
claudedocs/guides/[GUIDE] mobile-responsive-patterns.md
Normal file
538
claudedocs/guides/[GUIDE] mobile-responsive-patterns.md
Normal file
@@ -0,0 +1,538 @@
|
||||
# 모바일 반응형 패턴 가이드
|
||||
|
||||
> 작성일: 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 | 초기 작성 |
|
||||
Reference in New Issue
Block a user