## 주요 변경사항: ### 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>
502 lines
12 KiB
Markdown
502 lines
12 KiB
Markdown
# 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<String, dynamic> 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<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 컨트롤러 패턴
|
|
```dart
|
|
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 리스트 화면 패턴 (리디자인 버전)
|
|
```dart
|
|
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 폼 화면 패턴
|
|
```dart
|
|
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 컴포넌트 사용 (리디자인)
|
|
```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<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
|
|
```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<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* |