## 주요 변경사항: ### 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>
12 KiB
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 일반 규칙
- 단일 책임 원칙: 각 클래스/함수는 하나의 책임만 가져야 함
- DRY 원칙: 코드 중복을 피하고 재사용 가능한 컴포넌트 작성
- 명확한 네이밍: 변수, 함수, 클래스명은 용도를 명확히 표현
- 일관성: 프로젝트 전체에서 동일한 패턴과 스타일 사용
6.2 Flutter 특화
- const 생성자 사용: 가능한 모든 위젯에 const 사용
- Key 사용: 리스트나 동적 위젯에는 적절한 Key 제공
- BuildContext 주의: async 작업 후 context 사용 시 mounted 체크
- 메모리 누수 방지: Controller, Stream 등은 dispose에서 정리
6.3 리디자인 관련
- 테마 시스템 사용: 하드코딩된 스타일 대신 ShadcnTheme 사용
- 컴포넌트 재사용: shadcn_components의 표준 컴포넌트 활용
- 일관된 레이아웃: AppLayoutRedesign으로 모든 화면 감싸기
- 반응형 디자인: 다양한 화면 크기 고려
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