# 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 모델 클래스 패턴 ```dart 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 toJson() => { 'id': id, 'name': name, 'createdAt': createdAt?.toIso8601String(), }; } ``` ### 2.2 화면(Screen) 패턴 ```dart class FeatureScreen extends StatefulWidget { final String? id; // 선택적 파라미터 const FeatureScreen({Key? key, this.id}) : super(key: key); @override State createState() => _FeatureScreenState(); } class _FeatureScreenState extends State { 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 컨트롤러 패턴 ```dart class FeatureController extends ChangeNotifier { final MockDataService _dataService = MockDataService(); bool _isLoading = false; String? _error; List _items = []; // Getters bool get isLoading => _isLoading; String? get error => _error; List get items => _items; // 초기화 Future 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 리스트 화면 패턴 (리디자인 버전) ```dart class EntityListRedesign extends StatefulWidget { const EntityListRedesign({Key? key}) : super(key: key); @override State createState() => _EntityListRedesignState(); } class _EntityListRedesignState extends State { 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 폼 화면 패턴 ```dart class EntityFormScreen extends StatefulWidget { final String? id; const EntityFormScreen({Key? key, this.id}) : super(key: key); @override State createState() => _EntityFormScreenState(); } class _EntityFormScreenState extends State { final _formKey = GlobalKey(); late final EntityFormController _controller; @override void initState() { super.initState(); _controller = EntityFormController(id: widget.id); _controller.loadData(); } Future _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 컴포넌트 사용 (리디자인) ```dart // 카드 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 테이블/리스트 패턴 ```dart // 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 데이터 서비스 ```dart class MockDataService { static final MockDataService _instance = MockDataService._internal(); factory MockDataService() => _instance; MockDataService._internal(); // 데이터 저장소 final List _items = []; // CRUD 메서드 Future> getItems() async { await Future.delayed(Duration(milliseconds: 300)); // 네트워크 지연 시뮬레이션 return List.from(_items); } Future addItem(Model item) async { await Future.delayed(Duration(milliseconds: 300)); _items.add(item); return item; } Future 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 deleteItem(String id) async { await Future.delayed(Duration(milliseconds: 300)); _items.removeWhere((i) => i.id == id); } } ``` ## 5. 유틸리티 패턴 ### 5.1 Validator ```dart 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 상수 정의 ```dart 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 완전한 리스트 화면 예제 ```dart // user_list_redesign.dart class UserListRedesign extends StatefulWidget { const UserListRedesign({Key? key}) : super(key: key); @override State createState() => _UserListRedesignState(); } class _UserListRedesignState extends State { final UserListController _controller = UserListController(); @override void initState() { super.initState(); _loadData(); } Future _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 _navigateToAdd() async { final result = await Navigator.pushNamed(context, Routes.userAdd); if (result == true) { _loadData(); } } } ``` --- *이 가이드는 Superport 프로젝트의 코드 일관성을 위해 작성되었습니다.* *마지막 업데이트: 2025-07-07*