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>
This commit is contained in:
502
.claude/code_patterns_guide.md
Normal file
502
.claude/code_patterns_guide.md
Normal file
@@ -0,0 +1,502 @@
|
||||
# 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*
|
||||
Reference in New Issue
Block a user