Files
superport/.claude/code_patterns_guide.md
JiWoong Sul e0bc5894b2 UI 전체 리디자인 및 개선사항 적용
## 주요 변경사항:

### UI/UX 개선
- shadcn/ui 스타일 기반의 새로운 디자인 시스템 도입
- 모든 주요 화면에 대한 리디자인 구현 완료
  - 로그인 화면: 모던한 카드 스타일 적용
  - 대시보드: 통계 카드와 차트를 활용한 개요 화면
  - 리스트 화면들: 일관된 테이블 디자인과 검색/필터 기능
- 다크모드 지원을 위한 테마 시스템 구축

### 기능 개선
- Equipment List: 고급 필터링 (상태, 담당자별)
- Company List: 검색 및 정렬 기능 강화
- User List: 역할별 필터링 추가
- License List: 만료일 기반 상태 표시
- Warehouse Location: 재고 수준 시각화

### 기술적 개선
- 재사용 가능한 컴포넌트 라이브러리 구축
- 일관된 코드 패턴 가이드라인 작성
- 프로젝트 구조 분석 및 문서화

### 문서화
- 프로젝트 분석 문서 추가
- UI 리디자인 진행 상황 문서
- 코드 패턴 가이드 작성
- Equipment 기능 격차 분석 및 구현 계획

### 삭제/리팩토링
- goods_list.dart 제거 (equipment_list로 통합)
- 불필요한 import 및 코드 정리

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-07 19:45:32 +09:00

12 KiB

Superport 코드 패턴 가이드

1. 파일 구조 및 네이밍 규칙

1.1 디렉토리 구조

lib/
├── models/          # 데이터 모델 (접미사: _model.dart)
├── screens/         # 화면 구성
│   ├── common/      # 공통 컴포넌트 및 레이아웃
│   └── [feature]/   # 기능별 디렉토리
├── services/        # 비즈니스 로직 및 데이터 서비스
└── utils/           # 유틸리티 함수 및 상수

1.2 파일 네이밍 규칙

  • 모델: entity_name_model.dart (예: user_model.dart)
  • 화면: feature_screen.dart (예: login_screen.dart)
  • 리스트: entity_list.dart (예: user_list.dart)
  • : entity_form_screen.dart (예: user_form_screen.dart)
  • 컨트롤러: feature_controller.dart (예: login_controller.dart)
  • 위젯: widget_name.dart (예: custom_button.dart)

2. 코드 패턴

2.1 모델 클래스 패턴

class EntityModel {
  final String id;
  final String name;
  final DateTime? createdAt;
  
  EntityModel({
    required this.id,
    required this.name,
    this.createdAt,
  });
  
  // copyWith 메서드 필수
  EntityModel copyWith({
    String? id,
    String? name,
    DateTime? createdAt,
  }) {
    return EntityModel(
      id: id ?? this.id,
      name: name ?? this.name,
      createdAt: createdAt ?? this.createdAt,
    );
  }
  
  // JSON 직렬화 (선택적)
  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'createdAt': createdAt?.toIso8601String(),
  };
}

2.2 화면(Screen) 패턴

class FeatureScreen extends StatefulWidget {
  final String? id; // 선택적 파라미터
  
  const FeatureScreen({Key? key, this.id}) : super(key: key);
  
  @override
  State<FeatureScreen> createState() => _FeatureScreenState();
}

class _FeatureScreenState extends State<FeatureScreen> {
  late final FeatureController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = FeatureController();
    _controller.initialize(widget.id);
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _buildBody(),
    );
  }
}

2.3 컨트롤러 패턴

class FeatureController extends ChangeNotifier {
  final MockDataService _dataService = MockDataService();
  
  bool _isLoading = false;
  String? _error;
  List<Model> _items = [];
  
  // Getters
  bool get isLoading => _isLoading;
  String? get error => _error;
  List<Model> get items => _items;
  
  // 초기화
  Future<void> initialize() async {
    _setLoading(true);
    try {
      _items = await _dataService.getItems();
    } catch (e) {
      _error = e.toString();
    } finally {
      _setLoading(false);
    }
  }
  
  // 상태 업데이트
  void _setLoading(bool value) {
    _isLoading = value;
    notifyListeners();
  }
  
  @override
  void dispose() {
    // 정리 작업
    super.dispose();
  }
}

2.4 리스트 화면 패턴 (리디자인 버전)

class EntityListRedesign extends StatefulWidget {
  const EntityListRedesign({Key? key}) : super(key: key);
  
  @override
  State<EntityListRedesign> createState() => _EntityListRedesignState();
}

class _EntityListRedesignState extends State<EntityListRedesign> {
  final EntityListController _controller = EntityListController();
  
  @override
  Widget build(BuildContext context) {
    return AppLayoutRedesign(
      currentRoute: Routes.entity,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 헤더
          _buildHeader(),
          SizedBox(height: ShadcnTheme.spacing.lg),
          // 컨텐츠
          Expanded(
            child: ShadcnCard(
              padding: EdgeInsets.zero,
              child: _buildContent(),
            ),
          ),
        ],
      ),
    );
  }
  
  Widget _buildHeader() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(
          '총 ${_controller.items.length}개',
          style: ShadcnTheme.typography.bodyMuted,
        ),
        ShadcnButton(
          onPressed: () => _navigateToForm(),
          icon: Icons.add,
          label: '추가',
        ),
      ],
    );
  }
}

2.5 폼 화면 패턴

class EntityFormScreen extends StatefulWidget {
  final String? id;
  
  const EntityFormScreen({Key? key, this.id}) : super(key: key);
  
  @override
  State<EntityFormScreen> createState() => _EntityFormScreenState();
}

class _EntityFormScreenState extends State<EntityFormScreen> {
  final _formKey = GlobalKey<FormState>();
  late final EntityFormController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = EntityFormController(id: widget.id);
    _controller.loadData();
  }
  
  Future<void> _handleSave() async {
    if (!_formKey.currentState!.validate()) return;
    
    _formKey.currentState!.save();
    
    try {
      await _controller.save();
      if (mounted) {
        Navigator.pop(context, true);
      }
    } catch (e) {
      // 에러 처리
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return MainLayout(
      title: widget.id == null ? '새 항목 추가' : '항목 수정',
      showBackButton: true,
      child: Form(
        key: _formKey,
        child: Column(
          children: [
            // 폼 필드들
          ],
        ),
      ),
    );
  }
}

3. 위젯 사용 패턴

3.1 shadcn 컴포넌트 사용 (리디자인)

// 카드
ShadcnCard(
  child: Column(
    children: [...],
  ),
);

// 버튼
ShadcnButton(
  onPressed: () {},
  label: '저장',
  variant: ShadcnButtonVariant.primary,
);

// 입력 필드
ShadcnInput(
  value: _controller.name,
  onChanged: (value) => _controller.name = value,
  placeholder: '이름을 입력하세요',
);

// 배지
ShadcnBadge(
  label: '활성',
  variant: ShadcnBadgeVariant.success,
);

3.2 테이블/리스트 패턴

// DataTable 사용
DataTable(
  columns: [
    DataColumn(label: Text('이름')),
    DataColumn(label: Text('상태')),
    DataColumn(label: Text('작업')),
  ],
  rows: _controller.items.map((item) => DataRow(
    cells: [
      DataCell(Text(item.name)),
      DataCell(ShadcnBadge(label: item.status)),
      DataCell(Row(
        children: [
          IconButton(
            icon: Icon(Icons.edit),
            onPressed: () => _handleEdit(item),
          ),
          IconButton(
            icon: Icon(Icons.delete),
            onPressed: () => _handleDelete(item),
          ),
        ],
      )),
    ],
  )).toList(),
);

4. 서비스 레이어 패턴

4.1 Mock 데이터 서비스

class MockDataService {
  static final MockDataService _instance = MockDataService._internal();
  factory MockDataService() => _instance;
  MockDataService._internal();
  
  // 데이터 저장소
  final List<Model> _items = [];
  
  // CRUD 메서드
  Future<List<Model>> getItems() async {
    await Future.delayed(Duration(milliseconds: 300)); // 네트워크 지연 시뮬레이션
    return List.from(_items);
  }
  
  Future<Model> addItem(Model item) async {
    await Future.delayed(Duration(milliseconds: 300));
    _items.add(item);
    return item;
  }
  
  Future<void> updateItem(String id, Model item) async {
    await Future.delayed(Duration(milliseconds: 300));
    final index = _items.indexWhere((i) => i.id == id);
    if (index != -1) {
      _items[index] = item;
    }
  }
  
  Future<void> deleteItem(String id) async {
    await Future.delayed(Duration(milliseconds: 300));
    _items.removeWhere((i) => i.id == id);
  }
}

5. 유틸리티 패턴

5.1 Validator

class Validators {
  static String? required(String? value, {String? fieldName}) {
    if (value == null || value.trim().isEmpty) {
      return '${fieldName ?? '이 필드'}는 필수입니다.';
    }
    return null;
  }
  
  static String? email(String? value) {
    if (value == null || value.isEmpty) return null;
    final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
    if (!emailRegex.hasMatch(value)) {
      return '올바른 이메일 형식이 아닙니다.';
    }
    return null;
  }
}

5.2 상수 정의

class Routes {
  static const String home = '/';
  static const String login = '/login';
  static const String equipment = '/equipment';
  // ...
}

class AppColors {
  static const Color primary = Color(0xFF3B82F6);
  static const Color secondary = Color(0xFF64748B);
  // ...
}

6. 베스트 프랙티스

6.1 일반 규칙

  1. 단일 책임 원칙: 각 클래스/함수는 하나의 책임만 가져야 함
  2. DRY 원칙: 코드 중복을 피하고 재사용 가능한 컴포넌트 작성
  3. 명확한 네이밍: 변수, 함수, 클래스명은 용도를 명확히 표현
  4. 일관성: 프로젝트 전체에서 동일한 패턴과 스타일 사용

6.2 Flutter 특화

  1. const 생성자 사용: 가능한 모든 위젯에 const 사용
  2. Key 사용: 리스트나 동적 위젯에는 적절한 Key 제공
  3. BuildContext 주의: async 작업 후 context 사용 시 mounted 체크
  4. 메모리 누수 방지: Controller, Stream 등은 dispose에서 정리

6.3 리디자인 관련

  1. 테마 시스템 사용: 하드코딩된 스타일 대신 ShadcnTheme 사용
  2. 컴포넌트 재사용: shadcn_components의 표준 컴포넌트 활용
  3. 일관된 레이아웃: AppLayoutRedesign으로 모든 화면 감싸기
  4. 반응형 디자인: 다양한 화면 크기 고려

7. 코드 예제

7.1 완전한 리스트 화면 예제

// user_list_redesign.dart
class UserListRedesign extends StatefulWidget {
  const UserListRedesign({Key? key}) : super(key: key);
  
  @override
  State<UserListRedesign> createState() => _UserListRedesignState();
}

class _UserListRedesignState extends State<UserListRedesign> {
  final UserListController _controller = UserListController();
  
  @override
  void initState() {
    super.initState();
    _loadData();
  }
  
  Future<void> _loadData() async {
    await _controller.loadUsers();
    if (mounted) setState(() {});
  }
  
  @override
  Widget build(BuildContext context) {
    return AppLayoutRedesign(
      currentRoute: Routes.user,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 헤더
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(
                '총 ${_controller.users.length}명',
                style: ShadcnTheme.typography.bodyMuted,
              ),
              ShadcnButton(
                onPressed: _navigateToAdd,
                icon: Icons.add,
                label: '사용자 추가',
              ),
            ],
          ),
          SizedBox(height: ShadcnTheme.spacing.lg),
          // 테이블
          Expanded(
            child: ShadcnCard(
              padding: EdgeInsets.zero,
              child: _controller.users.isEmpty
                  ? _buildEmptyState()
                  : _buildDataTable(),
            ),
          ),
        ],
      ),
    );
  }
  
  Widget _buildEmptyState() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.people_outline, size: 64, color: ShadcnTheme.muted),
          SizedBox(height: ShadcnTheme.spacing.md),
          Text('사용자가 없습니다', style: ShadcnTheme.typography.bodyMuted),
        ],
      ),
    );
  }
  
  Widget _buildDataTable() {
    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: DataTable(
        // 테이블 구현
      ),
    );
  }
  
  Future<void> _navigateToAdd() async {
    final result = await Navigator.pushNamed(context, Routes.userAdd);
    if (result == true) {
      _loadData();
    }
  }
}

이 가이드는 Superport 프로젝트의 코드 일관성을 위해 작성되었습니다. 마지막 업데이트: 2025-07-07