- 회사 리스트 화면의 배지를 ShadcnBadge 컴포넌트로 통일 - 본사(Blue)와 지점(Purple) 색상 차별화로 시각적 구분 강화 - 고객사(Orange), 파트너사(Green) 색상 체계 개선 - 장비/라이선스 관리 화면과 동일한 배지 스타일 적용 - 불필요한 문서 파일 정리 - 라이선스 만료 요약 모델 업데이트 - 리스트 화면들의 페이지네이션 및 필터링 로직 개선 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
11 KiB
11 KiB
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. 현재 구조의 문제점
// 현재 문제가 있는 구조
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. 근본 원인
_calculateTableWidth()함수가 Container의 padding (좌우 각 16px = 총 32px)을 고려하지 않음- Border 두께 (2px)도 계산에서 누락
- 고정 너비 방식으로 인한 유연성 부족
- 각 컬럼의 고정 너비 합계가 실제 사용 가능한 공간을 초과
🛠️ Option B 구현 상세
1. 핵심 변경 사항
A. 고정 너비 제거
// 변경 전 - 고정 너비 사용
Container(
width: 60, // 고정 너비
child: Text('번호')
)
// 변경 후 - Expanded 사용
Expanded(
flex: 1, // 비율로 너비 결정
child: Text('번호')
)
B. 테이블 구조 개선
// 새로운 구조
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: 테이블 빌더 함수 분리
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: 헤더 셀 빌더
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: 데이터 행 빌더
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: 데이터 셀 빌더
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: 최소 테이블 너비 계산
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. 제거해야 할 코드
_calculateTableWidth()함수 전체 제거- 모든 고정 너비 Container 제거
- 중첩된 SingleChildScrollView 구조 제거
✅ 테스트 체크리스트
1. 기능 테스트
- 페이지네이션이 정상 작동하는가?
- 검색 기능이 정상 작동하는가?
- 정렬 기능이 정상 작동하는가?
- 장비 추가/수정/삭제가 정상 작동하는가?
- 장비 이력 조회가 정상 작동하는가?
2. 레이아웃 테스트
- RenderFlex overflow 오류가 해결되었는가?
- 다양한 화면 크기에서 정상 표시되는가?
- 스크롤이 필요한 경우에만 표시되는가?
- 테이블 컬럼이 적절한 비율로 표시되는가?
- 텍스트가 잘리지 않고 적절히 표시되는가?
3. 일관성 테스트
- 다른 관리 화면과 동일한 위치에 페이지네이션이 표시되는가?
- BaseListScreen을 통한 레이아웃이 일관되게 적용되는가?
- 스타일과 여백이 일관되게 적용되는가?
📝 추가 고려사항
1. 반응형 디자인
- 1200px 이상: Expanded 위젯 사용 (유연한 레이아웃)
- 1200px 미만: 고정 최소 너비 + 수평 스크롤
2. 성능 최적화
- ListView.builder 사용으로 가상 스크롤링 유지
- 불필요한 rebuild 방지를 위한 const 생성자 활용
3. 접근성
- 적절한 최소 터치 영역 유지 (48x48 dp)
- 텍스트 가독성을 위한 적절한 padding 유지
🚀 구현 순서
- 백업: 현재 equipment_list_redesign.dart 파일 백업
- 테이블 구조 분리: _buildTable 함수 생성
- 셀 빌더 구현: 헤더와 데이터 셀 빌더 함수 구현
- LayoutBuilder 적용: 반응형 레이아웃 구현
- 테스트: 모든 체크리스트 항목 확인
- 동일 패턴 적용: license_list_redesign.dart에도 동일하게 적용
📌 예상 결과
- RenderFlex overflow 오류 완전 해결
- 화면 크기에 따른 유연한 레이아웃
- 필요한 경우에만 수평 스크롤 표시
- 모든 관리 화면에서 일관된 페이지네이션 위치
- 향후 유지보수가 용이한 구조
🔗 관련 파일
-
수정 대상 파일:
/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(동일 패턴 적용)
-
참조 파일:
/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