refactor: UI 일관성 개선 및 회사 타입 배지 통일
- 회사 리스트 화면의 배지를 ShadcnBadge 컴포넌트로 통일 - 본사(Blue)와 지점(Purple) 색상 차별화로 시각적 구분 강화 - 고객사(Orange), 파트너사(Green) 색상 체계 개선 - 장비/라이선스 관리 화면과 동일한 배지 스타일 적용 - 불필요한 문서 파일 정리 - 라이선스 만료 요약 모델 업데이트 - 리스트 화면들의 페이지네이션 및 필터링 로직 개선 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
381
task.md
Normal file
381
task.md
Normal file
@@ -0,0 +1,381 @@
|
||||
# Equipment Management RenderFlex Overflow 수정 작업
|
||||
|
||||
## 📋 작업 개요
|
||||
|
||||
**목적**: Equipment Management 화면의 RenderFlex overflow 오류를 근본적으로 해결하기 위한 구조적 개선
|
||||
|
||||
**문제 상황**:
|
||||
- 장비 관리 화면 우측에 32 픽셀의 RenderFlex overflow 발생
|
||||
- 노란색/검은색 줄무늬 패턴으로 렌더링 오류 시각화
|
||||
- 고정 너비 계산 방식이 padding과 border를 제대로 고려하지 못함
|
||||
|
||||
**해결 방안**: Option B - Expanded 위젯 기반 유연한 레이아웃 구조로 전환
|
||||
|
||||
## 🔍 현재 문제 분석
|
||||
|
||||
### 1. 오류 발생 위치
|
||||
```
|
||||
파일: /Users/maximilian.j.sul/Documents/flutter/superport/lib/screens/equipment/equipment_list_redesign.dart
|
||||
라인: 755번 줄의 Row 위젯
|
||||
```
|
||||
|
||||
### 2. 현재 구조의 문제점
|
||||
```dart
|
||||
// 현재 문제가 있는 구조
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black), // 2px border
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _horizontalScrollController,
|
||||
child: SizedBox(
|
||||
width: _calculateTableWidth(pagedEquipments), // 고정 너비 계산
|
||||
child: Column(
|
||||
children: [
|
||||
// 테이블 헤더
|
||||
Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16), // 32px padding
|
||||
child: Row(...) // ← 여기서 오버플로우 발생
|
||||
),
|
||||
// 테이블 바디
|
||||
...
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 근본 원인
|
||||
1. `_calculateTableWidth()` 함수가 Container의 padding (좌우 각 16px = 총 32px)을 고려하지 않음
|
||||
2. Border 두께 (2px)도 계산에서 누락
|
||||
3. 고정 너비 방식으로 인한 유연성 부족
|
||||
4. 각 컬럼의 고정 너비 합계가 실제 사용 가능한 공간을 초과
|
||||
|
||||
## 🛠️ Option B 구현 상세
|
||||
|
||||
### 1. 핵심 변경 사항
|
||||
|
||||
#### A. 고정 너비 제거
|
||||
```dart
|
||||
// 변경 전 - 고정 너비 사용
|
||||
Container(
|
||||
width: 60, // 고정 너비
|
||||
child: Text('번호')
|
||||
)
|
||||
|
||||
// 변경 후 - Expanded 사용
|
||||
Expanded(
|
||||
flex: 1, // 비율로 너비 결정
|
||||
child: Text('번호')
|
||||
)
|
||||
```
|
||||
|
||||
#### B. 테이블 구조 개선
|
||||
```dart
|
||||
// 새로운 구조
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableWidth = constraints.maxWidth;
|
||||
final needsHorizontalScroll = _getMinimumTableWidth() > availableWidth;
|
||||
|
||||
if (needsHorizontalScroll) {
|
||||
// 최소 너비보다 작을 때만 스크롤 활성화
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _horizontalScrollController,
|
||||
child: SizedBox(
|
||||
width: _getMinimumTableWidth(),
|
||||
child: _buildTable(pagedEquipments, useExpanded: false),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 충분한 공간이 있을 때는 Expanded 사용
|
||||
return _buildTable(pagedEquipments, useExpanded: true);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### 2. 구현 단계
|
||||
|
||||
#### Step 1: 테이블 빌더 함수 분리
|
||||
```dart
|
||||
Widget _buildTable(List<Equipment> equipments, {required bool useExpanded}) {
|
||||
return Column(
|
||||
children: [
|
||||
// 테이블 헤더
|
||||
Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF8F9FA),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFE5E7EB)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildHeaderCell('번호', flex: 1, useExpanded: useExpanded, minWidth: 60),
|
||||
_buildHeaderCell('장비 타입', flex: 2, useExpanded: useExpanded, minWidth: 120),
|
||||
_buildHeaderCell('모델명', flex: 2, useExpanded: useExpanded, minWidth: 150),
|
||||
_buildHeaderCell('제조사', flex: 2, useExpanded: useExpanded, minWidth: 120),
|
||||
_buildHeaderCell('시리얼 번호', flex: 2, useExpanded: useExpanded, minWidth: 150),
|
||||
_buildHeaderCell('상태', flex: 1, useExpanded: useExpanded, minWidth: 100),
|
||||
_buildHeaderCell('입고일', flex: 2, useExpanded: useExpanded, minWidth: 120),
|
||||
_buildHeaderCell('입고지', flex: 2, useExpanded: useExpanded, minWidth: 150),
|
||||
_buildHeaderCell('액션', flex: 1, useExpanded: useExpanded, minWidth: 100),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 테이블 바디
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: equipments.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildDataRow(equipments[index], index, useExpanded);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2: 헤더 셀 빌더
|
||||
```dart
|
||||
Widget _buildHeaderCell(
|
||||
String text, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final child = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: child);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: child);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 3: 데이터 행 빌더
|
||||
```dart
|
||||
Widget _buildDataRow(Equipment equipment, int index, bool useExpanded) {
|
||||
return Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFE5E7EB)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildDataCell(
|
||||
'${index + 1}',
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 60,
|
||||
),
|
||||
_buildDataCell(
|
||||
equipment.equipmentType ?? '-',
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
_buildDataCell(
|
||||
equipment.model ?? '-',
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 150,
|
||||
),
|
||||
_buildDataCell(
|
||||
equipment.manufacturer ?? '-',
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
_buildDataCell(
|
||||
equipment.serialNumber ?? '-',
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 150,
|
||||
),
|
||||
_buildStatusCell(
|
||||
equipment.status ?? '-',
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
equipment.warehousingDate != null
|
||||
? DateFormat('yyyy-MM-dd').format(equipment.warehousingDate!)
|
||||
: '-',
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
_buildDataCell(
|
||||
equipment.warehouseLocationName ?? '-',
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 150,
|
||||
),
|
||||
_buildActionCell(
|
||||
equipment,
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 100,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 4: 데이터 셀 빌더
|
||||
```dart
|
||||
Widget _buildDataCell(
|
||||
String text, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final child = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(color: Color(0xFF6B7280)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: child);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: child);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 5: 최소 테이블 너비 계산
|
||||
```dart
|
||||
double _getMinimumTableWidth() {
|
||||
// 각 컬럼의 최소 너비 합계
|
||||
const columnWidths = [
|
||||
60, // 번호
|
||||
120, // 장비 타입
|
||||
150, // 모델명
|
||||
120, // 제조사
|
||||
150, // 시리얼 번호
|
||||
100, // 상태
|
||||
120, // 입고일
|
||||
150, // 입고지
|
||||
100, // 액션
|
||||
];
|
||||
|
||||
final totalWidth = columnWidths.reduce((a, b) => a + b);
|
||||
const padding = 32; // 좌우 padding
|
||||
|
||||
return totalWidth + padding;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 제거해야 할 코드
|
||||
|
||||
1. `_calculateTableWidth()` 함수 전체 제거
|
||||
2. 모든 고정 너비 Container 제거
|
||||
3. 중첩된 SingleChildScrollView 구조 제거
|
||||
|
||||
## ✅ 테스트 체크리스트
|
||||
|
||||
### 1. 기능 테스트
|
||||
- [ ] 페이지네이션이 정상 작동하는가?
|
||||
- [ ] 검색 기능이 정상 작동하는가?
|
||||
- [ ] 정렬 기능이 정상 작동하는가?
|
||||
- [ ] 장비 추가/수정/삭제가 정상 작동하는가?
|
||||
- [ ] 장비 이력 조회가 정상 작동하는가?
|
||||
|
||||
### 2. 레이아웃 테스트
|
||||
- [ ] RenderFlex overflow 오류가 해결되었는가?
|
||||
- [ ] 다양한 화면 크기에서 정상 표시되는가?
|
||||
- [ ] 스크롤이 필요한 경우에만 표시되는가?
|
||||
- [ ] 테이블 컬럼이 적절한 비율로 표시되는가?
|
||||
- [ ] 텍스트가 잘리지 않고 적절히 표시되는가?
|
||||
|
||||
### 3. 일관성 테스트
|
||||
- [ ] 다른 관리 화면과 동일한 위치에 페이지네이션이 표시되는가?
|
||||
- [ ] BaseListScreen을 통한 레이아웃이 일관되게 적용되는가?
|
||||
- [ ] 스타일과 여백이 일관되게 적용되는가?
|
||||
|
||||
## 📝 추가 고려사항
|
||||
|
||||
### 1. 반응형 디자인
|
||||
- 1200px 이상: Expanded 위젯 사용 (유연한 레이아웃)
|
||||
- 1200px 미만: 고정 최소 너비 + 수평 스크롤
|
||||
|
||||
### 2. 성능 최적화
|
||||
- ListView.builder 사용으로 가상 스크롤링 유지
|
||||
- 불필요한 rebuild 방지를 위한 const 생성자 활용
|
||||
|
||||
### 3. 접근성
|
||||
- 적절한 최소 터치 영역 유지 (48x48 dp)
|
||||
- 텍스트 가독성을 위한 적절한 padding 유지
|
||||
|
||||
## 🚀 구현 순서
|
||||
|
||||
1. **백업**: 현재 equipment_list_redesign.dart 파일 백업
|
||||
2. **테이블 구조 분리**: _buildTable 함수 생성
|
||||
3. **셀 빌더 구현**: 헤더와 데이터 셀 빌더 함수 구현
|
||||
4. **LayoutBuilder 적용**: 반응형 레이아웃 구현
|
||||
5. **테스트**: 모든 체크리스트 항목 확인
|
||||
6. **동일 패턴 적용**: license_list_redesign.dart에도 동일하게 적용
|
||||
|
||||
## 📌 예상 결과
|
||||
|
||||
- RenderFlex overflow 오류 완전 해결
|
||||
- 화면 크기에 따른 유연한 레이아웃
|
||||
- 필요한 경우에만 수평 스크롤 표시
|
||||
- 모든 관리 화면에서 일관된 페이지네이션 위치
|
||||
- 향후 유지보수가 용이한 구조
|
||||
|
||||
## 🔗 관련 파일
|
||||
|
||||
1. **수정 대상 파일**:
|
||||
- `/Users/maximilian.j.sul/Documents/flutter/superport/lib/screens/equipment/equipment_list_redesign.dart`
|
||||
- `/Users/maximilian.j.sul/Documents/flutter/superport/lib/screens/license/license_list_redesign.dart` (동일 패턴 적용)
|
||||
|
||||
2. **참조 파일**:
|
||||
- `/Users/maximilian.j.sul/Documents/flutter/superport/lib/screens/common/layouts/base_list_screen.dart`
|
||||
- `/Users/maximilian.j.sul/Documents/flutter/superport/lib/screens/company/company_list_redesign.dart` (정상 작동 예시)
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-01-09
|
||||
**작성자**: Claude Code Assistant
|
||||
**버전**: 1.0
|
||||
Reference in New Issue
Block a user