Files
superport/task.md
JiWoong Sul 6b5d126990
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled
refactor: UI 일관성 개선 및 회사 타입 배지 통일
- 회사 리스트 화면의 배지를 ShadcnBadge 컴포넌트로 통일
- 본사(Blue)와 지점(Purple) 색상 차별화로 시각적 구분 강화
- 고객사(Orange), 파트너사(Green) 색상 체계 개선
- 장비/라이선스 관리 화면과 동일한 배지 스타일 적용
- 불필요한 문서 파일 정리
- 라이선스 만료 요약 모델 업데이트
- 리스트 화면들의 페이지네이션 및 필터링 로직 개선

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-09 23:45:28 +09:00

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. 근본 원인

  1. _calculateTableWidth() 함수가 Container의 padding (좌우 각 16px = 총 32px)을 고려하지 않음
  2. Border 두께 (2px)도 계산에서 누락
  3. 고정 너비 방식으로 인한 유연성 부족
  4. 각 컬럼의 고정 너비 합계가 실제 사용 가능한 공간을 초과

🛠️ 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. 제거해야 할 코드

  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