1818 lines
58 KiB
Markdown
1818 lines
58 KiB
Markdown
# Superport ERP System
|
||
|
||
> 💡 **Note**: Global Claude Code rules are in `~/.claude/CLAUDE.md`. This document contains project-specific context.
|
||
|
||
## 🎯 Project Overview
|
||
|
||
**Superport**는 기업용 장비 관리 및 유지보수를 위한 클라우드 기반 ERP 시스템입니다.
|
||
|
||
### Business Purpose
|
||
- 장비 입출고 및 재고 관리 자동화
|
||
- 유지보수 라이선스 만료일 추적
|
||
- 고객사별 장비 배치 현황 관리
|
||
- 실시간 대시보드를 통한 경영 인사이트 제공
|
||
|
||
### Target Users
|
||
- **관리자 (Admin)**: 전체 시스템 관리, 장비 입출고, 라이선스 관리, 모든 기능 접근 권한
|
||
|
||
## 🏗️ Technical Architecture
|
||
|
||
### Tech Stack
|
||
```yaml
|
||
Frontend:
|
||
platform: Flutter Web (Mobile ready)
|
||
state_management: Provider + ChangeNotifier
|
||
ui_framework: ShadCN Flutter Port
|
||
api_client: Dio + Retrofit
|
||
code_generation: Freezed + JsonSerializable
|
||
|
||
Backend:
|
||
language: Rust
|
||
framework: Actix-Web
|
||
database: PostgreSQL
|
||
auth: JWT (24시간 만료)
|
||
api_url: http://43.201.34.104:8080/api/v1
|
||
source_path: /Users/maximilian.j.sul/Documents/flutter/superport_api
|
||
|
||
Infrastructure:
|
||
hosting: AWS (예정)
|
||
storage: S3 (예정)
|
||
ci_cd: GitHub Actions (예정)
|
||
```
|
||
|
||
### Project Structure (Clean Architecture)
|
||
```
|
||
/Users/maximilian.j.sul/Documents/flutter/
|
||
├── superport/ # Flutter Frontend (Clean Architecture)
|
||
│ ├── lib/
|
||
│ │ ├── core/ # 핵심 공통 기능
|
||
│ │ │ ├── controllers/ # BaseController 추상화
|
||
│ │ │ ├── errors/ # 에러 처리 체계
|
||
│ │ │ ├── utils/ # 유틸리티 함수
|
||
│ │ │ └── widgets/ # 공통 위젯
|
||
│ │ ├── data/ # Data Layer (외부 인터페이스)
|
||
│ │ │ ├── datasources/ # Remote/Local 데이터소스
|
||
│ │ │ │ ├── remote/ # API 클라이언트 (Retrofit)
|
||
│ │ │ │ └── interceptors/ # Dio 인터셉터
|
||
│ │ │ ├── models/ # DTO (Freezed 불변 객체)
|
||
│ │ │ └── repositories/ # Repository 구현체
|
||
│ │ ├── domain/ # Domain Layer (비즈니스 로직)
|
||
│ │ │ ├── repositories/ # Repository 인터페이스
|
||
│ │ │ └── usecases/ # UseCase (비즈니스 규칙)
|
||
│ │ ├── screens/ # Presentation Layer
|
||
│ │ │ └── [feature]/
|
||
│ │ │ ├── controllers/ # ChangeNotifier 상태 관리
|
||
│ │ │ └── widgets/ # Feature별 UI 컴포넌트
|
||
│ │ └── services/ # 레거시 서비스 (마이그레이션 중)
|
||
│ └── test/
|
||
│ ├── domain/ # UseCase 단위 테스트
|
||
│ ├── integration/ # 통합 테스트
|
||
│ │ ├── automated/ # UI 자동화 테스트
|
||
│ │ └── real_api/ # 실제 API 테스트
|
||
│ └── widget/ # 위젯 테스트
|
||
│
|
||
└── superport_api/ # Rust Backend
|
||
├── src/
|
||
│ ├── handlers/ # API 엔드포인트
|
||
│ ├── services/ # 비즈니스 로직
|
||
│ └── entities/ # DB 모델
|
||
└── migrations/ # DB 마이그레이션
|
||
```
|
||
|
||
## 🚀 Quick Commands
|
||
|
||
### Development
|
||
```bash
|
||
# Start development (Real API)
|
||
flutter run -d chrome
|
||
|
||
# Run tests
|
||
flutter test
|
||
|
||
# Generate code (Freezed, JsonSerializable)
|
||
flutter pub run build_runner build --delete-conflicting-outputs
|
||
|
||
# API integration test
|
||
./test_api_integration.sh
|
||
|
||
# Start backend API (별도 터미널)
|
||
cd /Users/maximilian.j.sul/Documents/flutter/superport_api
|
||
cargo run
|
||
|
||
# View API logs
|
||
cd /Users/maximilian.j.sul/Documents/flutter/superport_api
|
||
tail -f logs/api.log
|
||
```
|
||
|
||
### API Configuration
|
||
```
|
||
Base URL: http://43.201.34.104:8080/api/v1
|
||
Test Account: admin@example.com / password123
|
||
API Source Code: /Users/maximilian.j.sul/Documents/flutter/superport_api
|
||
```
|
||
|
||
|
||
|
||
|
||
# 🚨 2025-08-23 중대 발견: 백엔드 API 스키마 완전 분석 결과
|
||
|
||
## 📊 백엔드 API 문서 분석 완료
|
||
|
||
### ⚠️ **CRITICAL**: 현재 프론트엔드 구조와 백엔드 실제 스키마 간 **심각한 불일치** 발견
|
||
|
||
#### 🔍 **주요 불일치 사항**
|
||
```yaml
|
||
현재_프론트엔드_가정_vs_실제_백엔드:
|
||
Equipment:
|
||
현재: "category1/2/3 필드 직접 사용"
|
||
실제: "models_id FK → models 테이블 → vendors_id FK 구조"
|
||
|
||
License_Management:
|
||
현재: "독립적인 License 엔티티"
|
||
실제: "maintenances 테이블 (equipment_history_id FK 연결)"
|
||
|
||
Company_Structure:
|
||
현재: "단순 Company + Branch 구조"
|
||
실제: "계층형 parent_company_id 지원"
|
||
|
||
Equipment_History:
|
||
현재: "미구현 상태"
|
||
실제: "핵심 트랜잭션 추적 엔티티 (입출고/재고 관리)"
|
||
|
||
Rent_Management:
|
||
현재: "미구현 상태"
|
||
실제: "완전한 대여 관리 시스템 (equipment_history 연동)"
|
||
```
|
||
|
||
#### 🎯 **백엔드 실제 데이터베이스 스키마 (PostgreSQL)**
|
||
|
||
##### **핵심 엔티티 관계도**
|
||
```mermaid
|
||
erDiagram
|
||
vendors ||--o{ models : has
|
||
models ||--o{ equipments : belongs_to
|
||
companies ||--o{ equipments : owns
|
||
companies ||--o{ equipment_history_companies_link : involved_in
|
||
equipments ||--o{ equipment_history : generates
|
||
warehouses ||--o{ equipment_history : stores
|
||
equipment_history ||--|| rents : creates
|
||
equipment_history ||--o{ maintenances : requires
|
||
zipcodes ||--o{ companies : located_at
|
||
zipcodes ||--o{ warehouses : located_at
|
||
```
|
||
|
||
##### **새로 발견된 필수 엔티티**
|
||
```dart
|
||
// 1. 제조사 (vendors) - 완전히 누락됨
|
||
VendorEntity {
|
||
int id;
|
||
String name; // UNIQUE 제약
|
||
bool isDeleted;
|
||
DateTime registeredAt;
|
||
DateTime? updatedAt;
|
||
}
|
||
|
||
// 2. 모델명 (models) - 완전히 누락됨
|
||
ModelEntity {
|
||
int id;
|
||
String name; // UNIQUE 제약
|
||
int vendorsId; // FK to vendors
|
||
bool isDeleted;
|
||
DateTime registeredAt;
|
||
DateTime? updatedAt;
|
||
}
|
||
|
||
// 3. 장비이력 (equipment_history) - 핵심 누락
|
||
EquipmentHistoryEntity {
|
||
int id;
|
||
int equipmentsId; // FK to equipments
|
||
int warehousesId; // FK to warehouses
|
||
String transactionType; // 'I'(입고) | 'O'(출고)
|
||
int quantity;
|
||
DateTime transactedAt;
|
||
String? remark;
|
||
DateTime isDeleted; // 주의: DATETIME 타입
|
||
DateTime createdAt;
|
||
DateTime? updatedAt;
|
||
}
|
||
|
||
// 4. 임대상세 (rents) - 완전히 누락됨
|
||
RentEntity {
|
||
int id;
|
||
DateTime startedAt;
|
||
DateTime endedAt;
|
||
int equipmentHistoryId; // FK to equipment_history
|
||
}
|
||
|
||
// 5. 유지보수이력 (maintenances) - License와 완전히 다름
|
||
MaintenanceEntity {
|
||
int id;
|
||
int equipmentHistoryId; // FK to equipment_history
|
||
DateTime startedAt;
|
||
DateTime endedAt;
|
||
int periodMonth; // 방문 주기 (월)
|
||
String maintenanceType; // 'O'(방문) | 'R'(원격)
|
||
bool isDeleted;
|
||
DateTime registeredAt;
|
||
DateTime? updatedAt;
|
||
}
|
||
```
|
||
|
||
##### **기존 엔티티 수정 필요 사항**
|
||
```dart
|
||
// Equipment 엔티티 - 대폭 수정 필요
|
||
EquipmentEntity {
|
||
int id;
|
||
int companiesId; // 현재: company_id
|
||
int modelsId; // 🚨 누락: models 테이블 FK
|
||
String serialNumber; // UNIQUE 제약
|
||
String? barcode; // UNIQUE 제약
|
||
DateTime purchasedAt;
|
||
int purchasePrice;
|
||
String warrantyNumber;
|
||
DateTime warrantyStartedAt;
|
||
DateTime warrantyEndedAt;
|
||
String? remark;
|
||
bool isDeleted;
|
||
DateTime registeredAt;
|
||
DateTime? updatedAt;
|
||
}
|
||
|
||
// Company 엔티티 - 계층 구조 추가
|
||
CompanyEntity {
|
||
int id;
|
||
String name; // UNIQUE 제약
|
||
String contactName;
|
||
String contactPhone;
|
||
String contactEmail;
|
||
int? parentCompanyId; // 🚨 누락: 계층 구조
|
||
String zipcodeZipcode; // FK to zipcodes
|
||
String address;
|
||
String? remark;
|
||
bool isPartner;
|
||
bool isCustomer;
|
||
bool isActive;
|
||
bool isDeleted;
|
||
DateTime registeredAt;
|
||
DateTime? updatedAt;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
# 🎨 **ShadCN UI 기반 전면 UI/UX 리팩토링 계획**
|
||
|
||
## 📚 **ShadCN Flutter UI 라이브러리 분석**
|
||
|
||
### 🛠️ **라이브러리 개요**
|
||
- **Repository**: https://github.com/nank1ro/flutter-shadcn-ui
|
||
- **Documentation**: https://flutter-shadcn-ui.mariuti.com/
|
||
- **Status**: 활발한 개발 (2.1k stars, 39 contributors)
|
||
- **License**: MIT
|
||
- **Components**: 30+ 컴포넌트 구현 완료
|
||
|
||
### 🎯 **핵심 컴포넌트 활용 계획**
|
||
```yaml
|
||
Form_Components:
|
||
ShadInput: "모든 TextFormField 대체"
|
||
ShadSelect: "Vendor/Model/Company 드롭다운"
|
||
ShadDatePicker: "구매일/만료일/점검일 선택"
|
||
ShadCheckbox: "Boolean 필드 (is_partner, is_customer)"
|
||
ShadButton: "모든 액션 버튼 통일"
|
||
|
||
Layout_Components:
|
||
ShadCard: "정보 카드 및 폼 컨테이너"
|
||
ShadTable: "데이터 테이블 (장비/회사/라이선스 리스트)"
|
||
ShadDialog: "등록/수정 모달"
|
||
ShadSheet: "상세 정보 슬라이드 패널"
|
||
ShadTabs: "화면 내 탭 네비게이션"
|
||
|
||
Data_Display:
|
||
ShadBadge: "상태 표시 (활성/비활성, 장비 상태)"
|
||
ShadAlert: "시스템 알림 및 경고"
|
||
ShadToast: "작업 완료/오류 피드백"
|
||
ShadProgress: "로딩 상태 및 진행률"
|
||
|
||
Navigation:
|
||
ShadBreadcrumb: "페이지 경로 네비게이션"
|
||
ShadPagination: "리스트 페이지네이션"
|
||
```
|
||
|
||
### 🖥️ **웹 우선 반응형 디자인 전략**
|
||
```dart
|
||
// 반응형 브레이크포인트 정의
|
||
class ResponsiveBreakpoints {
|
||
static const double mobile = 640; // 모바일
|
||
static const double tablet = 768; // 태블릿
|
||
static const double desktop = 1024; // 데스크톱
|
||
static const double wide = 1280; // 와이드스크린
|
||
}
|
||
|
||
// 화면별 레이아웃 전략
|
||
Desktop_Layout (1024px+):
|
||
- 3-Column 구조: [필터패널][메인컨텐츠][상세패널]
|
||
- 고정 사이드바 + 동적 메인 영역
|
||
- 테이블 풀사이즈 표시 + 인라인 액션
|
||
|
||
Tablet_Layout (768px~1023px):
|
||
- 2-Column 구조: [메인컨텐츠][접이식 사이드패널]
|
||
- 햄버거 메뉴 + 슬라이딩 필터
|
||
- 테이블 스크롤 + 상세보기 모달
|
||
|
||
Mobile_Layout (~767px):
|
||
- 1-Column 구조: [스택형 레이아웃]
|
||
- 풀스크린 카드 + 바텀시트
|
||
- 리스트뷰 + 탭 네비게이션
|
||
```
|
||
|
||
### 📦 **필요한 추가 의존성**
|
||
```yaml
|
||
dependencies:
|
||
# 기존 의존성들...
|
||
shadcn_ui: ^0.8.0 # ShadCN UI 컴포넌트
|
||
webview_flutter: ^4.4.2 # Daum 주소 API 웹뷰
|
||
flutter_inappwebview: ^6.0.0 # JavaScript 통신 지원
|
||
flutter_staggered_grid_view: ^0.7.0 # 가상화 스크롤링
|
||
intl: ^0.18.1 # 다국어 및 포맷팅
|
||
|
||
dev_dependencies:
|
||
# 기존 dev 의존성들...
|
||
integration_test: ^1.0.0 # 통합 테스트
|
||
flutter_driver: ^0.0.0 # E2E 테스트
|
||
```
|
||
|
||
### 🎯 **사용자 중심 UX/UI 설계 원칙**
|
||
|
||
#### **데이터 흐름 기반 화면 설계**
|
||
```yaml
|
||
사용자_워크플로우_우선:
|
||
Equipment_등록_흐름:
|
||
1단계: "제조사 선택 → 모델 자동 필터링"
|
||
2단계: "시리얼 번호 입력 → 실시간 중복 검증"
|
||
3단계: "워런티 정보 → 만료일 자동 계산"
|
||
4단계: "저장 전 최종 검증 → 성공/실패 피드백"
|
||
|
||
Company_등록_흐름:
|
||
1단계: "기본 정보 입력 → 실시간 유효성 검증"
|
||
2단계: "주소 검색 → Daum API 웹뷰 호출"
|
||
3단계: "연락처 정보 → 전화번호 형식 자동 변환"
|
||
4단계: "계층 구조 설정 → 본사/지점 관계 시각화"
|
||
|
||
실시간_검증_및_피드백:
|
||
입력중_검증:
|
||
- 시리얼 번호: debounce 500ms 후 중복 검사
|
||
- 이메일: 형식 검증 + @ 도메인 검증
|
||
- 전화번호: 010-0000-0000 형식 자동 변환
|
||
- 사업자번호: 000-00-00000 형식 + 유효성 검증
|
||
|
||
저장전_검증:
|
||
- 필수 항목 누락 시 해당 필드로 자동 스크롤
|
||
- 에러 메시지를 필드 하단에 빨간색으로 표시
|
||
- 성공 시 ShadToast로 "저장되었습니다" 알림
|
||
- 실패 시 구체적인 오류 원인 표시
|
||
```
|
||
|
||
#### **주소 검색 시스템 (Daum API 연동)**
|
||
```dart
|
||
// lib/core/services/address_service.dart
|
||
class AddressService {
|
||
static const String daumPostcodeUrl = 'https://postcode.map.daum.net/guide';
|
||
|
||
Future<AddressResult?> searchAddress(BuildContext context) async {
|
||
return await Navigator.push(
|
||
context,
|
||
MaterialPageRoute(
|
||
builder: (context) => AddressSearchWebView(),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// lib/screens/common/widgets/address_search_webview.dart
|
||
class AddressSearchWebView extends StatefulWidget {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: Text('주소 검색')),
|
||
body: WebView(
|
||
initialUrl: '''
|
||
data:text/html;charset=utf-8,
|
||
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
|
||
<div id="layer"></div>
|
||
<script>
|
||
new daum.Postcode({
|
||
oncomplete: function(data) {
|
||
window.flutter_inappwebview.callHandler('onComplete', data);
|
||
}
|
||
}).embed('layer');
|
||
</script>
|
||
''',
|
||
onWebViewCreated: (controller) {
|
||
controller.addJavaScriptHandler(
|
||
handlerName: 'onComplete',
|
||
callback: (args) {
|
||
Navigator.pop(context, AddressResult.fromJson(args[0]));
|
||
},
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 주소 검색 결과 모델
|
||
@freezed
|
||
class AddressResult with _$AddressResult {
|
||
const factory AddressResult({
|
||
required String zonecode, // 우편번호
|
||
required String address, // 기본주소
|
||
required String addressEnglish, // 영문주소
|
||
String? buildingName, // 건물명
|
||
String? addressDetail, // 상세주소 (사용자 입력)
|
||
}) = _AddressResult;
|
||
}
|
||
```
|
||
|
||
#### **폼 컴포넌트 표준화**
|
||
```dart
|
||
// lib/screens/common/widgets/standard_form_components.dart
|
||
|
||
// 1. 실시간 검증이 포함된 입력 필드
|
||
class ValidatedShadInput extends StatefulWidget {
|
||
final String label;
|
||
final String? hintText;
|
||
final bool isRequired;
|
||
final Future<String?> Function(String)? asyncValidator;
|
||
final String? Function(String?)? syncValidator;
|
||
final void Function(String)? onChanged;
|
||
final TextInputType? keyboardType;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 라벨 (필수항목 * 표시)
|
||
RichText(
|
||
text: TextSpan(
|
||
text: label,
|
||
style: Theme.of(context).textTheme.bodyMedium,
|
||
children: isRequired ? [
|
||
TextSpan(
|
||
text: ' *',
|
||
style: TextStyle(color: Colors.red),
|
||
),
|
||
] : [],
|
||
),
|
||
),
|
||
SizedBox(height: 4),
|
||
|
||
// ShadCN Input 필드
|
||
ShadInput(
|
||
hintText: hintText,
|
||
keyboardType: keyboardType,
|
||
onChanged: _handleInputChange,
|
||
decoration: InputDecoration(
|
||
errorText: _errorMessage,
|
||
suffixIcon: _isValidating
|
||
? SizedBox(
|
||
width: 16,
|
||
height: 16,
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
)
|
||
: _isValid
|
||
? Icon(Icons.check_circle, color: Colors.green)
|
||
: null,
|
||
),
|
||
),
|
||
|
||
// 에러 메시지 또는 도움말
|
||
if (_errorMessage != null)
|
||
Padding(
|
||
padding: EdgeInsets.only(top: 4),
|
||
child: Text(
|
||
_errorMessage!,
|
||
style: TextStyle(
|
||
color: Colors.red[600],
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
)
|
||
else if (widget.helpText != null)
|
||
Padding(
|
||
padding: EdgeInsets.only(top: 4),
|
||
child: Text(
|
||
widget.helpText!,
|
||
style: TextStyle(
|
||
color: Colors.grey[600],
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
// 2. 전화번호 자동 포맷팅 입력 필드
|
||
class PhoneNumberInput extends StatelessWidget {
|
||
final String label;
|
||
final bool isRequired;
|
||
final void Function(String)? onChanged;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return ValidatedShadInput(
|
||
label: label,
|
||
isRequired: isRequired,
|
||
hintText: "010-0000-0000",
|
||
keyboardType: TextInputType.phone,
|
||
onChanged: (value) {
|
||
String formatted = _formatPhoneNumber(value);
|
||
onChanged?.call(formatted);
|
||
},
|
||
syncValidator: (value) {
|
||
if (isRequired && (value == null || value.isEmpty)) {
|
||
return '전화번호를 입력해주세요';
|
||
}
|
||
if (value != null && value.isNotEmpty && !_isValidPhoneNumber(value)) {
|
||
return '올바른 전화번호 형식이 아닙니다';
|
||
}
|
||
return null;
|
||
},
|
||
);
|
||
}
|
||
|
||
String _formatPhoneNumber(String value) {
|
||
String digits = value.replaceAll(RegExp(r'[^0-9]'), '');
|
||
if (digits.length <= 3) return digits;
|
||
if (digits.length <= 7) return '${digits.substring(0, 3)}-${digits.substring(3)}';
|
||
return '${digits.substring(0, 3)}-${digits.substring(3, 7)}-${digits.substring(7, min(11, digits.length))}';
|
||
}
|
||
|
||
bool _isValidPhoneNumber(String value) {
|
||
return RegExp(r'^010-\d{4}-\d{4}$').hasMatch(value);
|
||
}
|
||
}
|
||
|
||
// 3. 주소 검색 통합 컴포넌트
|
||
class AddressSearchField extends StatefulWidget {
|
||
final String label;
|
||
final bool isRequired;
|
||
final void Function(AddressResult?)? onAddressSelected;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 기본 주소 표시 (읽기 전용)
|
||
ValidatedShadInput(
|
||
label: label,
|
||
isRequired: isRequired,
|
||
hintText: "주소를 검색하려면 버튼을 클릭하세요",
|
||
readOnly: true,
|
||
controller: _addressController,
|
||
suffixIcon: ShadButton.outline(
|
||
text: "주소 검색",
|
||
onPressed: _searchAddress,
|
||
size: ShadButtonSize.sm,
|
||
),
|
||
),
|
||
|
||
// 상세 주소 입력
|
||
if (_selectedAddress != null)
|
||
Padding(
|
||
padding: EdgeInsets.only(top: 8),
|
||
child: ValidatedShadInput(
|
||
label: "상세 주소",
|
||
hintText: "동, 호수 등 상세 주소를 입력하세요",
|
||
onChanged: (value) {
|
||
_selectedAddress = _selectedAddress!.copyWith(
|
||
addressDetail: value,
|
||
);
|
||
widget.onAddressSelected?.call(_selectedAddress);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Future<void> _searchAddress() async {
|
||
final result = await AddressService.searchAddress(context);
|
||
if (result != null) {
|
||
setState(() {
|
||
_selectedAddress = result;
|
||
_addressController.text = result.address;
|
||
});
|
||
widget.onAddressSelected?.call(result);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### **에러 처리 및 사용자 피드백 시스템**
|
||
```yaml
|
||
에러_표시_전략:
|
||
폼_레벨_에러:
|
||
- 필수 항목 누락: 빨간색 테두리 + 필드별 에러 메시지
|
||
- 서버 에러: 폼 상단에 ShadAlert로 전체 에러 표시
|
||
- 네트워크 에러: 재시도 버튼 포함된 에러 배너
|
||
|
||
필드_레벨_에러:
|
||
- 실시간 검증: debounce 후 즉시 표시
|
||
- 포커스 아웃 검증: 필드를 벗어날 때 검증
|
||
- 아이콘으로 상태 표시: ✓(성공), ⚠(경고), ✗(에러)
|
||
|
||
성공_피드백:
|
||
저장_성공: "ShadToast.success('장비가 성공적으로 등록되었습니다')"
|
||
수정_성공: "ShadToast.success('정보가 업데이트되었습니다')"
|
||
삭제_성공: "ShadToast.success('삭제가 완료되었습니다')"
|
||
|
||
로딩_상태:
|
||
버튼_로딩: "ShadButton에 loading 상태 표시"
|
||
폼_로딩: "ShadProgress로 전체 폼 비활성화"
|
||
리스트_로딩: "ShadSkeleton으로 로딩 상태 표시"
|
||
```
|
||
|
||
#### **접근성 및 사용성 개선**
|
||
```yaml
|
||
키보드_네비게이션:
|
||
- Tab 키로 순차적 필드 이동
|
||
- Enter 키로 다음 필드 또는 저장 실행
|
||
- Esc 키로 모달/다이얼로그 닫기
|
||
|
||
모바일_최적화:
|
||
- 가상 키보드에 맞는 input type 설정
|
||
- 화면 회전 시 레이아웃 자동 조정
|
||
- 터치 영역 최소 44px 보장
|
||
|
||
다국어_대응:
|
||
- 모든 텍스트 다국어 키로 관리
|
||
- 날짜/시간 형식 로케일별 자동 조정
|
||
- 숫자 형식(천단위 구분) 로케일 대응
|
||
|
||
성능_최적화:
|
||
- 대용량 리스트 가상화 스크롤
|
||
- 이미지 지연 로딩 및 캐싱
|
||
- API 호출 debounce/throttling
|
||
```
|
||
|
||
---
|
||
|
||
# 🏗️ **전면 리팩토링 7단계 계획**
|
||
|
||
## 📋 **"필요한 모든 에이전트를 동원해라. 클린 아키텍쳐와 함께 SRP를 무조건 지켜서 작업해라."**
|
||
|
||
### 🔧 **리팩토링 자유도 및 권한**
|
||
```yaml
|
||
프로젝트_구조_변경_권한:
|
||
디렉토리_구조: "전면 재편 가능"
|
||
파일_삭제: "필요없는 파일 완전 제거 허용"
|
||
폴더_이동: "Clean Architecture에 맞게 재구성"
|
||
네이밍_변경: "일관성 있는 명명 규칙으로 통일"
|
||
|
||
허용되는_작업:
|
||
- 기존 폴더 구조 완전 재설계
|
||
- 레거시 파일 및 코드 삭제
|
||
- ShadCN UI에 맞는 새로운 컴포넌트 구조
|
||
- Clean Architecture 원칙에 따른 레이어 재편
|
||
- 중복 코드 및 사용하지 않는 파일 정리
|
||
- 파일명을 역할에 맞게 명확하고 직관적으로 재명명
|
||
- 테스트 폴더 전체 삭제 후 필요시 새로 구축
|
||
|
||
파일명_설계_원칙:
|
||
- "역할과 책임을 파일명에 명확히 표현"
|
||
- "snake_case → PascalCase 일관성 유지"
|
||
- "기능별 그룹핑이 파일명에서 즉시 이해 가능"
|
||
- "Dto, Controller, UseCase, Repository 등 타입 명시"
|
||
- "기존 모호한 파일명은 모두 명확하게 변경"
|
||
|
||
테스트_폴더_관리_정책:
|
||
- "대규모 리팩토링으로 기존 테스트 대부분 무효화 예상"
|
||
- "새로운 구조에 맞지 않는 테스트는 삭제 후 재작성이 효율적"
|
||
- "test/ 폴더 전체 삭제 허용 (사용자 요청 시 새로 구축)"
|
||
- "TDD 원칙에 따라 새 구조에 맞는 테스트 체계 구축"
|
||
|
||
파일_관리_원칙:
|
||
- "AAAA.dart 수정 시 AAAA_simplified.dart 생성 금지"
|
||
- "기존 파일을 직접 수정하여 코드 개선"
|
||
- "새 파일 생성으로 파일 개수 증가 금지"
|
||
- "리팩토링은 기존 파일 내에서 완료"
|
||
|
||
SRP_준수_전략:
|
||
- "현재 코드 대부분이 SRP 위반 상태 (여러 책임 혼재)"
|
||
- "코드 추가 시 별도 위젯/컴포넌트로 분리 설계"
|
||
- "기존 파일 내에서 함수/클래스 단위로 책임 분리"
|
||
- "위젯 트리 구조로 단일 책임 위젯들을 조합하여 사용"
|
||
|
||
위젯_컴포넌트_분리_예시:
|
||
- "복잡한 폼 → 입력 필드별 개별 위젯으로 분리"
|
||
- "긴 함수 → 단일 기능 함수들로 분해"
|
||
- "다중 책임 클래스 → 책임별 별도 클래스로 분리"
|
||
- "UI 로직 혼재 → Presentation과 Business 로직 완전 분리"
|
||
|
||
작업_원칙:
|
||
- "더 나은 구조를 위해서는 기존을 과감히 삭제"
|
||
- "Clean Architecture 위배 요소는 모두 제거"
|
||
- "ShadCN UI 표준에 맞지 않는 컴포넌트 교체"
|
||
- "백엔드 스키마와 맞지 않는 모델 완전 삭제"
|
||
- "파일명이 모호하면 역할에 맞게 명확히 변경"
|
||
- "기존 테스트보다 새 구조에 맞는 테스트 우선"
|
||
- "파일 개수 증가 없이 기존 파일 내에서 개선"
|
||
- "SRP 위반 코드는 위젯/컴포넌트 분리로 해결"
|
||
|
||
코드_품질_관리:
|
||
- "모든 코드 작성 완료 후 반드시 'flutter analyze' 실행"
|
||
- "분석 결과 오류가 0개일 때만 작업 완료로 간주"
|
||
- "오류 발견 시 즉시 수정 후 재분석"
|
||
- "분석 통과 후 상세한 한글 주석으로 코드 정리"
|
||
|
||
한글_주석_작성_원칙:
|
||
- "클래스/함수 상단에 목적과 책임 명시"
|
||
- "복잡한 로직은 단계별로 상세 설명"
|
||
- "매개변수와 반환값의 의미 명확히 기술"
|
||
- "예외 상황과 처리 방법 문서화"
|
||
- "비즈니스 로직의 배경과 이유 설명"
|
||
```
|
||
|
||
### 🖥️ **ShadCN UI 구현 우선순위**
|
||
```yaml
|
||
사용자_흐름_기반_구현_순서:
|
||
1단계_로그인_화면: "사용자 사용흐름의 시작점"
|
||
- ShadInput: 이메일/비밀번호 입력 필드
|
||
- ShadButton: 로그인 버튼 (로딩 상태 포함)
|
||
- ShadCard: 로그인 폼 컨테이너
|
||
- ShadAlert: 로그인 실패 시 에러 메시지
|
||
|
||
2단계_메인_대시보드: "로그인 후 첫 화면"
|
||
- ShadCard: 통계 카드들
|
||
- ShadBadge: 상태 표시 (라이선스 만료 등)
|
||
- ShadTabs: 메뉴 탭 네비게이션
|
||
- ShadTable: 데이터 테이블
|
||
|
||
3단계_핵심_CRUD_화면:
|
||
- Equipment: ShadForm + ShadSelect + ShadDatePicker
|
||
- Company: ShadInput + AddressSearchField
|
||
- Maintenance: ShadDialog + ShadCalendar
|
||
|
||
구현_원칙: "https://github.com/nank1ro/flutter-shadcn-ui 컴포넌트 우선 사용"
|
||
```
|
||
|
||
### 🔥 **Phase 1: 백엔드 API 스키마 동기화** (Week 1: Day 1-2)
|
||
```yaml
|
||
목표: "실제 백엔드 스키마에 맞춘 DTO 모델 완전 재구축"
|
||
|
||
작업_범위:
|
||
신규_DTO_생성:
|
||
- VendorDto + Repository + UseCase
|
||
- ModelDto + Repository + UseCase
|
||
- EquipmentHistoryDto + Repository + UseCase
|
||
- MaintenanceHistoryDto + Repository + UseCase
|
||
- RentDto + Repository + UseCase
|
||
- ZipcodeDto + Repository + UseCase
|
||
|
||
기존_DTO_대폭_수정:
|
||
- EquipmentDto: models_id 필드 추가, category1/2/3 제거
|
||
- CompanyDto: parent_company_id 계층 구조 추가
|
||
- WarehouseDto: zipcode 연동 수정
|
||
|
||
완전_삭제_대상:
|
||
- license_dto.dart → maintenance_history_dto.dart로 대체
|
||
- 모든 Category 관련 하드코딩 로직
|
||
|
||
Clean_Architecture_준수:
|
||
- Domain Layer: 새로운 Repository 인터페이스 6개 추가
|
||
- Data Layer: API 클라이언트 Retrofit 6개 추가
|
||
- UseCase Layer: CRUD UseCase 24개 추가 (각 엔티티당 4개)
|
||
```
|
||
|
||
### 🎨 **Phase 2: ShadCN UI 기반 디자인 시스템 구축** (Week 1: Day 3-4)
|
||
```yaml
|
||
목표: "통일된 디자인 시스템 및 반응형 레이아웃 기반 구축"
|
||
|
||
작업_범위:
|
||
ShadCN_통합:
|
||
- pubspec.yaml: shadcn_ui 의존성 추가
|
||
- main.dart: ShadApp 래퍼 구성
|
||
- theme.dart: 커스텀 테마 (Light/Dark) 설정
|
||
|
||
공통_컴포넌트_개발:
|
||
- ResponsiveLayout: 브레이크포인트 기반 레이아웃
|
||
- StandardFormLayout: 통일된 폼 레이아웃
|
||
- StandardDataTable: ShadTable 기반 데이터 테이블
|
||
- StandardActionBar: CRUD 액션 버튼 바
|
||
|
||
디자인_토큰_정의:
|
||
- 색상 팔레트: Primary, Secondary, Accent
|
||
- 타이포그래피: 제목, 본문, 캡션 스타일
|
||
- 간격: Margin, Padding 표준화
|
||
- 애니메이션: 전환 효과 통일
|
||
```
|
||
|
||
### ⚙️ **Phase 3: Equipment 화면 완전 재구현** (Week 1: Day 5-7)
|
||
```yaml
|
||
목표: "Vendor→Model→Equipment 연쇄 구조 완벽 구현"
|
||
|
||
신규_화면_구조:
|
||
Equipment_Management_Screen:
|
||
Desktop: [VendorFilter + ModelFilter][EquipmentTable][DetailPanel]
|
||
Tablet: [EquipmentTable][SlidePanel]
|
||
Mobile: [EquipmentCards][BottomSheet]
|
||
|
||
핵심_기능:
|
||
- Vendor 선택 → Model 자동 필터링
|
||
- Serial Number 실시간 중복 검증
|
||
- Barcode 스캔 기능 (웹 카메라)
|
||
- 워런티 만료일 자동 계산
|
||
- 장비 이력 추적 (입고→출고→대여→반납)
|
||
|
||
Equipment_Form_Dialog:
|
||
- ShadSelect: Vendor/Model 연쇄 선택
|
||
- ShadInput: Serial Number (실시간 검증)
|
||
- ShadDatePicker: 구매일/워런티 기간
|
||
- 실시간 유효성 검증 + API 호출
|
||
```
|
||
|
||
### 🔧 **Phase 4: Maintenance System 재설계** (Week 2: Day 8-10)
|
||
```yaml
|
||
목표: "License → MaintenanceHistory 완전 전환"
|
||
|
||
기능_재정의:
|
||
기존: "라이선스 관리 (독립 엔티티)"
|
||
신규: "장비 유지보수 이력 관리 (Equipment History 연동)"
|
||
|
||
새로운_Maintenance_화면:
|
||
- Equipment 선택 → History 조회 → Maintenance 등록
|
||
- 방문/원격 유지보수 구분
|
||
- 주기별 스케줄링 (period_month)
|
||
- 만료일 알림 시스템 (기존 License 알림 재활용)
|
||
|
||
데이터_마이그레이션:
|
||
- 기존 License 데이터 → MaintenanceHistory 변환 스크립트
|
||
- API 엔드포인트 변경: /licenses → /maintenances
|
||
- 알림 로직 재구성
|
||
```
|
||
|
||
### 🏢 **Phase 5: Company 계층 구조 시각화** (Week 2: Day 11-12)
|
||
```yaml
|
||
목표: "본사-지점 계층 관리 + 파트너/고객 구분"
|
||
|
||
Company_Tree_View:
|
||
- 계층형 트리 구조 시각화
|
||
- Drag & Drop으로 계층 변경
|
||
- 파트너사/고객사 필터링
|
||
- 대시보드 통계에 계층별 집계 반영
|
||
|
||
신규_기능:
|
||
- 본사 → 지점 일괄 설정
|
||
- 계층별 권한 관리 (상위 회사가 하위 회사 관리)
|
||
- 지역별/계층별 장비 현황 보고서
|
||
```
|
||
|
||
### 📊 **Phase 6: Equipment History & Rent 시스템** (Week 2: Day 13-14)
|
||
```yaml
|
||
목표: "완전한 장비 라이프사이클 추적"
|
||
|
||
Equipment_History_Tracking:
|
||
- 입고 (I): 창고 입고 + 수량 관리
|
||
- 출고 (O): 회사별 배치 + 상태 변경
|
||
- 대여 시작: Rent 레코드 생성
|
||
- 대여 종료: 반납 처리 + 상태 복원
|
||
|
||
Rent_Management_System:
|
||
- 대여 기간 관리 (시작일/종료일)
|
||
- 연장 승인 프로세스
|
||
- 반납 체크리스트
|
||
- 연체 알림 시스템
|
||
|
||
Warehouse_Stock_Dashboard:
|
||
- 창고별 실시간 재고 현황
|
||
- 장비별 위치 추적
|
||
- 입출고 이력 시각화
|
||
```
|
||
|
||
### ⚡ **Phase 7: 성능 최적화 & 모바일 완성** (Week 3: Day 15-21)
|
||
```yaml
|
||
목표: "엔터프라이즈급 성능 + 완벽한 반응형"
|
||
|
||
성능_최적화:
|
||
- 가상화 스크롤링: flutter_staggered_grid_view
|
||
- 무한 스크롤: 페이지네이션 자동 로딩
|
||
- 이미지 최적화: 바코드/QR코드 캐싱
|
||
- 메모리 관리: 대용량 리스트 효율화
|
||
|
||
캐싱_전략:
|
||
- Vendor/Model: 1시간 캐시
|
||
- Company 계층: 30분 캐시
|
||
- Lookups: 기존 30분 유지
|
||
- Equipment History: 실시간 업데이트
|
||
|
||
모바일_UX_완성:
|
||
- 터치 제스처: Swipe to Action
|
||
- 오프라인 지원: 핵심 데이터 로컬 저장
|
||
- PWA 최적화: 설치 가능한 웹앱
|
||
- 푸시 알림: 만료일/연체 알림
|
||
```
|
||
|
||
---
|
||
|
||
# 🛡️ **작업 안정성 보장 방안**
|
||
|
||
## 🔒 **사이드 이펙트 최소화 전략**
|
||
|
||
### **0. 구조적 변경 안전성**
|
||
```yaml
|
||
디렉토리_재구성_안전장치:
|
||
백업_생성: "Git 브랜치로 현재 상태 완전 보존"
|
||
단계적_변경: "폴더별 순차적 재구성으로 추적 가능"
|
||
테스트_검증: "구조 변경 후 빌드 및 테스트 확인"
|
||
롤백_가능성: "언제든 이전 구조로 복원 가능"
|
||
|
||
파일_삭제_안전성:
|
||
사전_검토: "삭제 전 의존성 분석 및 영향도 확인"
|
||
점진적_제거: "deprecated → warning → 완전 삭제 단계"
|
||
복구_대비: "Git history를 통한 완전한 복구 경로"
|
||
테스트_확인: "삭제 후 전체 시스템 동작 검증"
|
||
```
|
||
|
||
### **1. 점진적 마이그레이션 (Zero-Downtime)**
|
||
```yaml
|
||
Stage_A: "새로운 모델 병렬 구축"
|
||
- 기존 DTO와 신규 DTO 동시 존재
|
||
- Feature Flag로 화면별 전환 제어
|
||
- A/B 테스트 지원 구조
|
||
|
||
Stage_B: "화면별 개별 전환"
|
||
- 하나씩 새 구조로 마이그레이션
|
||
- 각 단계마다 완전한 테스트
|
||
- 언제든 이전 버전으로 롤백 가능
|
||
|
||
Stage_C: "레거시 코드 단계적 제거"
|
||
- 새 시스템 안정성 확인 후
|
||
- deprecated 경고 → 완전 삭제
|
||
- 최종 정리 및 최적화
|
||
```
|
||
|
||
### **2. Clean Architecture 철저한 준수**
|
||
```dart
|
||
// Domain Layer: 비즈니스 규칙 완전 분리
|
||
abstract class EquipmentRepository {
|
||
Future<Either<Failure, List<Equipment>>> getEquipmentsByVendor({
|
||
required int vendorId,
|
||
PaginationParams? params,
|
||
});
|
||
|
||
Future<Either<Failure, bool>> isSerialNumberUnique(String serialNumber);
|
||
}
|
||
|
||
// UseCase: 단일 책임 원칙 (SRP) 철저 준수
|
||
class ValidateEquipmentSerialUseCase {
|
||
final EquipmentRepository _repository;
|
||
|
||
Future<Either<Failure, bool>> call(String serialNumber) async {
|
||
if (serialNumber.isEmpty) {
|
||
return Left(ValidationFailure('Serial number is required'));
|
||
}
|
||
|
||
return await _repository.isSerialNumberUnique(serialNumber);
|
||
}
|
||
}
|
||
|
||
// Presentation: 상태 관리 완전 분리
|
||
class EquipmentFormController extends ChangeNotifier {
|
||
final ValidateEquipmentSerialUseCase _validateSerial;
|
||
final CreateEquipmentUseCase _createEquipment;
|
||
|
||
// SRP: 오직 폼 상태 관리만 담당
|
||
}
|
||
```
|
||
|
||
### **3. 테스트 주도 개발 (TDD)**
|
||
```yaml
|
||
Unit_Tests:
|
||
- 새로운 UseCase별 100% 커버리지
|
||
- DTO 변환 로직 Edge Case 테스트
|
||
- Validation 로직 모든 시나리오 테스트
|
||
|
||
Integration_Tests:
|
||
- 백엔드 API 연동 자동화 테스트
|
||
- Equipment → Model → Vendor 연쇄 조회 테스트
|
||
- 트랜잭션 무결성 테스트
|
||
|
||
Widget_Tests:
|
||
- ShadCN 컴포넌트 모든 상호작용 테스트
|
||
- 반응형 레이아웃 모든 브레이크포인트 테스트
|
||
- 폼 유효성 검증 시나리오 테스트
|
||
|
||
E2E_Tests:
|
||
- 전체 워크플로우 테스트 (장비 등록 → 출고 → 대여 → 반납)
|
||
- 권한별 접근 제어 테스트
|
||
- 성능 테스트 (대용량 데이터 처리)
|
||
```
|
||
|
||
---
|
||
|
||
# 📈 **프로젝트 상태 업데이트**
|
||
|
||
## 🚨 **현재 상태 재평가**
|
||
```yaml
|
||
이전_평가: "Development (99.9% Complete)"
|
||
현실_평가: "Major Architecture Gap Discovered (재설계 필요)"
|
||
|
||
API_호환성: "95% → 40% (심각한 스키마 불일치 발견)"
|
||
UI_현대화: "70% → 30% (ShadCN UI 적용 필요)"
|
||
기능_완성도: "90% → 60% (핵심 기능 누락 다수)"
|
||
전체_완성도: "99.9% → 65% (대규모 리팩토링 필요)"
|
||
```
|
||
|
||
## 📊 **새로운 성공 지표 (KPI)**
|
||
```yaml
|
||
기술적_목표:
|
||
API_동기화율: "현재 40% → 목표 100%"
|
||
UI_일관성: "현재 60% → 목표 95%"
|
||
테스트_커버리지: "현재 70% → 목표 90%"
|
||
빌드_시간: "25초 유지"
|
||
성능: "현재 대비 30% 향상"
|
||
|
||
사용자_경험:
|
||
화면_로딩: "3초 → 1.5초 이하"
|
||
모바일_최적화: "70% → 95%"
|
||
접근성: "기본 → WCAG 2.1 AA 준수"
|
||
|
||
유지보수성:
|
||
코드_중복률: "15% → 3% 이하"
|
||
의존성_결합도: "높음 → 낮음"
|
||
SRP_위반: "다수 → 0건"
|
||
```
|
||
|
||
---
|
||
|
||
# ⏰ **실행 일정**
|
||
|
||
## 📅 **3주 집중 개발 계획**
|
||
```yaml
|
||
Week_1: "Foundation & Core (Phase 1-3)"
|
||
Day_1-2: API 스키마 동기화 + 새 DTO 모델
|
||
Day_3-4: ShadCN UI 통합 + 디자인 시스템
|
||
Day_5-7: Equipment 화면 완전 재구현
|
||
|
||
Week_2: "Advanced Features (Phase 4-6)"
|
||
Day_8-10: Maintenance 시스템 재설계
|
||
Day_11-12: Company 계층 구조 구현
|
||
Day_13-14: Equipment History & Rent 시스템
|
||
|
||
Week_3: "Optimization & Completion (Phase 7)"
|
||
Day_15-17: 성능 최적화 + 반응형 완성
|
||
Day_18-19: 전체 테스트 + 품질 보증
|
||
Day_20-21: 배포 준비 + 문서 업데이트
|
||
```
|
||
|
||
---
|
||
|
||
**🚀 Status**: **CRITICAL ARCHITECTURE REDESIGN REQUIRED**
|
||
**⚡ Priority**: **HIGHEST** (모든 다른 작업 중단하고 우선 처리)
|
||
**🎯 Expected Completion**: **2025-09-13** (3주 후)
|
||
**📊 Success Rate**: **85%** (체계적 접근으로 성공 가능성 높음)
|
||
|
||
# 🇰🇷 **한국형 ERP UI/UX 설계 원칙**
|
||
|
||
## 🎯 **한국 비즈니스 환경 특성 분석**
|
||
|
||
### 📋 **한국인 ERP 사용 패턴 연구**
|
||
```yaml
|
||
업무_환경_특성:
|
||
근무_시간: "09:00-18:00 (주 40시간)"
|
||
업무_스타일: "빠른 의사결정, 즉시 처리 선호"
|
||
보고_문화: "실시간 현황 파악, 시각적 데이터 선호"
|
||
모바일_활용: "업무 시간 외 모바일 접근 빈번"
|
||
|
||
ERP_사용_패턴:
|
||
접근_시점: "출근 직후 (09:00-09:30), 퇴근 직전 (17:30-18:00)"
|
||
주요_업무: "일일 현황 확인 → 긴급 처리 → 보고서 작성"
|
||
선호_기능: "대시보드 → 검색 → 등록/수정 → 보고서"
|
||
처리_속도: "3-Click Rule (최대 3번 클릭으로 목표 달성)"
|
||
|
||
정보_소비_패턴:
|
||
시선_흐름: "좌상단 → 우상단 → 좌하단 → 우하단 (Z패턴)"
|
||
중요_정보: "상단 고정, 색상 구분, 숫자 강조"
|
||
경고_알림: "빨간색 Badge, 점멸 효과, 소리 알림"
|
||
성공_피드백: "파란색/초록색, 체크 아이콘, 간결한 메시지"
|
||
```
|
||
|
||
### 🏢 **한국 기업 조직문화 반영**
|
||
```yaml
|
||
의사결정_구조:
|
||
상명하달: "관리자 권한 명확한 구분"
|
||
보고_라인: "계층별 데이터 접근 권한 차등화"
|
||
승인_프로세스: "단계별 승인 절차 시각화"
|
||
책임_추적: "작업자 기록 및 이력 관리"
|
||
|
||
업무_프로세스:
|
||
긴급_업무: "빨간색 라벨, 상단 고정 표시"
|
||
일반_업무: "우선순위 번호, 마감일 표시"
|
||
완료_업무: "회색 처리, 접기 기능"
|
||
보류_업무: "노란색 배경, 사유 표시"
|
||
|
||
커뮤니케이션:
|
||
알림_방식: "팝업 → 배지 → 이메일 → SMS 순서"
|
||
언어_사용: "존댓말 기본, 업무용 단어 사용"
|
||
시간_표기: "24시간제, '오전/오후' 병기"
|
||
날짜_형식: "YYYY년 MM월 DD일 (요일)"
|
||
```
|
||
|
||
## 🎨 **한국형 UI 디자인 원칙**
|
||
|
||
### 🖥️ **화면 레이아웃 최적화**
|
||
```dart
|
||
// 한국어 텍스트 특성 고려 레이아웃
|
||
class KoreanOptimizedLayout {
|
||
// 한글 텍스트는 영문보다 20-30% 더 넓은 공간 필요
|
||
static const double koreanTextPadding = 1.3;
|
||
|
||
// 한국 사용자 선호 색상 팔레트
|
||
static const Color primaryBlue = Color(0xFF1B4F87); // 신뢰감
|
||
static const Color successGreen = Color(0xFF2E8B57); // 성공/완료
|
||
static const Color warningOrange = Color(0xFFFF8C00); // 주의/대기
|
||
static const Color dangerRed = Color(0xFFDC143C); // 위험/긴급
|
||
static const Color neutralGray = Color(0xFF708090); // 일반/비활성
|
||
|
||
// 한국 사용자 선호 여백 (좀 더 넉넉한 공간)
|
||
static const EdgeInsets cardPadding = EdgeInsets.all(20);
|
||
static const EdgeInsets formFieldSpacing = EdgeInsets.symmetric(vertical: 12);
|
||
static const double listItemHeight = 72; // 터치하기 편한 높이
|
||
}
|
||
|
||
// 한국형 폰트 시스템
|
||
class KoreanTypography {
|
||
// 제목: 굵게, 크게 (중요도 강조)
|
||
static const TextStyle heading1 = TextStyle(
|
||
fontSize: 28,
|
||
fontWeight: FontWeight.w700,
|
||
letterSpacing: -0.5,
|
||
height: 1.3,
|
||
);
|
||
|
||
// 본문: 가독성 우선 (긴 텍스트 편안하게)
|
||
static const TextStyle body1 = TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w400,
|
||
letterSpacing: -0.2,
|
||
height: 1.6,
|
||
);
|
||
|
||
// 라벨: 간결하고 명확하게
|
||
static const TextStyle label = TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
letterSpacing: 0,
|
||
height: 1.4,
|
||
);
|
||
|
||
// 캡션: 부가 정보 (작고 연하게)
|
||
static const TextStyle caption = TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w400,
|
||
letterSpacing: 0.1,
|
||
height: 1.2,
|
||
color: Color(0xFF666666),
|
||
);
|
||
}
|
||
```
|
||
|
||
### 📱 **모바일 우선 반응형 설계**
|
||
```yaml
|
||
한국_모바일_사용_현황:
|
||
스마트폰_보급률: "95.1% (세계 1위)"
|
||
주요_기기: "Samsung Galaxy, iPhone"
|
||
화면_크기: "6.1-6.8인치 (주류)"
|
||
OS_점유율: "Android 71%, iOS 29%"
|
||
|
||
모바일_UX_최적화:
|
||
터치_영역:
|
||
최소_크기: "48dp x 48dp"
|
||
선호_크기: "56dp x 56dp"
|
||
간격: "8dp 이상"
|
||
|
||
제스처_패턴:
|
||
스와이프: "좌→우 (뒤로), 우→좌 (삭제)"
|
||
탭: "단일 탭 (선택), 더블 탭 (확대)"
|
||
롱프레스: "컨텍스트 메뉴, 다중 선택"
|
||
|
||
키보드_최적화:
|
||
숫자_입력: "numeric 키패드"
|
||
이메일: "email 키패드 (.com 버튼)"
|
||
검색: "search 버튼, 자동완성"
|
||
|
||
성능_요구사항:
|
||
로딩_시간: "2초 이내 (Wi-Fi), 3초 이내 (4G/5G)"
|
||
스크롤_응답: "60fps 유지"
|
||
메모리_사용: "200MB 이하"
|
||
```
|
||
|
||
### 🎯 **사용자 중심 네비게이션**
|
||
```dart
|
||
// 한국형 네비게이션 패턴
|
||
class KoreanNavigationPattern {
|
||
// 메인 메뉴: 4-5개 주요 기능 (더 많으면 혼란)
|
||
static const List<String> mainMenuItems = [
|
||
"대시보드", // 첫 화면, 전체 현황
|
||
"장비관리", // 핵심 업무
|
||
"회사관리", // 고객/파트너 관리
|
||
"유지보수", // 정기 업무
|
||
"보고서", // 결과 확인
|
||
];
|
||
|
||
// 브레드크럼: 현재 위치 명확히 표시
|
||
static Widget buildBreadcrumb(List<String> path) {
|
||
return Row(
|
||
children: [
|
||
Icon(Icons.home, size: 16, color: Colors.grey[600]),
|
||
...path.map((item) => [
|
||
Text(" > ", style: TextStyle(color: Colors.grey[400])),
|
||
Text(item, style: TextStyle(fontWeight: FontWeight.w500)),
|
||
]).expand((element) => element),
|
||
],
|
||
);
|
||
}
|
||
|
||
// 상단 액션 바: 자주 사용하는 기능 배치
|
||
static Widget buildActionBar() {
|
||
return Row(
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
children: [
|
||
ShadButton.outline(
|
||
icon: Icon(Icons.search),
|
||
text: "검색",
|
||
size: ShadButtonSize.sm,
|
||
),
|
||
SizedBox(width: 8),
|
||
ShadButton(
|
||
icon: Icon(Icons.add),
|
||
text: "등록",
|
||
size: ShadButtonSize.sm,
|
||
),
|
||
SizedBox(width: 8),
|
||
ShadButton.outline(
|
||
icon: Icon(Icons.download),
|
||
text: "엑셀",
|
||
size: ShadButtonSize.sm,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 📊 **한국형 대시보드 설계**
|
||
|
||
### 🎨 **정보 시각화 원칙**
|
||
```yaml
|
||
대시보드_구성_요소:
|
||
상단_KPI_영역: "핵심 지표 4-6개, 큰 숫자로 표시"
|
||
좌측_메뉴_영역: "주요 기능 바로가기"
|
||
중앙_차트_영역: "트렌드 차트, 상태별 파이차트"
|
||
우측_알림_영역: "긴급사항, 만료 예정 항목"
|
||
하단_최근_활동: "최근 등록/수정된 항목들"
|
||
|
||
색상_활용_전략:
|
||
상태_표시:
|
||
정상: "#28A745 (초록) + ✓ 체크 아이콘"
|
||
주의: "#FFC107 (노랑) + ⚠ 경고 아이콘"
|
||
위험: "#DC3545 (빨강) + ⚡ 긴급 아이콘"
|
||
비활성: "#6C757D (회색) + ○ 원 아이콘"
|
||
|
||
중요도_구분:
|
||
최우선: "빨간 배경, 흰 글자, 굵은 테두리"
|
||
높음: "주황 배경, 검은 글자, 점선 테두리"
|
||
보통: "파란 배경, 흰 글자, 실선 테두리"
|
||
낮음: "회색 배경, 검은 글자, 테두리 없음"
|
||
|
||
숫자_표현_방식:
|
||
큰_숫자: "123,456대 (천단위 구분)"
|
||
비율: "85.2% (소수점 1자리)"
|
||
금액: "₩1,234,567원 (원화 표시)"
|
||
날짜: "2025-08-23 (금) 오후 2:30"
|
||
```
|
||
|
||
### 📈 **실시간 현황판 설계**
|
||
```dart
|
||
// 한국형 실시간 대시보드 위젯
|
||
class KoreanDashboardWidget extends StatelessWidget {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return ResponsiveLayout(
|
||
mobile: _buildMobileDashboard(),
|
||
tablet: _buildTabletDashboard(),
|
||
desktop: _buildDesktopDashboard(),
|
||
);
|
||
}
|
||
|
||
Widget _buildDesktopDashboard() {
|
||
return Column(
|
||
children: [
|
||
// 1. 실시간 KPI 카드 (상단)
|
||
_buildKPICards(),
|
||
SizedBox(height: 24),
|
||
|
||
Row(
|
||
children: [
|
||
// 2. 메인 차트 영역 (70%)
|
||
Expanded(
|
||
flex: 7,
|
||
child: Column(
|
||
children: [
|
||
_buildTrendChart(), // 장비 등록 추이
|
||
SizedBox(height: 16),
|
||
_buildStatusPieChart(), // 장비 상태별 분포
|
||
],
|
||
),
|
||
),
|
||
|
||
SizedBox(width: 24),
|
||
|
||
// 3. 알림 및 액션 영역 (30%)
|
||
Expanded(
|
||
flex: 3,
|
||
child: Column(
|
||
children: [
|
||
_buildUrgentAlerts(), // 긴급 알림
|
||
SizedBox(height: 16),
|
||
_buildExpiringItems(), // 만료 예정
|
||
SizedBox(height: 16),
|
||
_buildQuickActions(), // 빠른 작업
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
SizedBox(height: 24),
|
||
|
||
// 4. 최근 활동 및 통계 (하단)
|
||
Row(
|
||
children: [
|
||
Expanded(child: _buildRecentEquipments()),
|
||
SizedBox(width: 16),
|
||
Expanded(child: _buildMaintenanceSchedule()),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildKPICards() {
|
||
return Row(
|
||
children: [
|
||
_buildKPICard(
|
||
title: "총 장비 수",
|
||
value: "1,234",
|
||
unit: "대",
|
||
trend: "+12",
|
||
trendColor: Colors.green,
|
||
icon: Icons.devices,
|
||
backgroundColor: Color(0xFF1B4F87),
|
||
),
|
||
SizedBox(width: 16),
|
||
_buildKPICard(
|
||
title: "가동 중",
|
||
value: "1,156",
|
||
unit: "대",
|
||
percentage: "93.7%",
|
||
icon: Icons.power,
|
||
backgroundColor: Color(0xFF2E8B57),
|
||
),
|
||
SizedBox(width: 16),
|
||
_buildKPICard(
|
||
title: "점검 필요",
|
||
value: "78",
|
||
unit: "대",
|
||
isWarning: true,
|
||
icon: Icons.warning,
|
||
backgroundColor: Color(0xFFFF8C00),
|
||
),
|
||
SizedBox(width: 16),
|
||
_buildKPICard(
|
||
title: "이번 달 수입",
|
||
value: "₩15.8",
|
||
unit: "억원",
|
||
trend: "+8.5%",
|
||
trendColor: Colors.blue,
|
||
icon: Icons.trending_up,
|
||
backgroundColor: Color(0xFF6F42C1),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildKPICard({
|
||
required String title,
|
||
required String value,
|
||
required String unit,
|
||
String? percentage,
|
||
String? trend,
|
||
Color? trendColor,
|
||
bool isWarning = false,
|
||
required IconData icon,
|
||
required Color backgroundColor,
|
||
}) {
|
||
return Expanded(
|
||
child: ShadCard(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(20),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(title, style: KoreanTypography.label),
|
||
Container(
|
||
padding: EdgeInsets.all(8),
|
||
decoration: BoxDecoration(
|
||
color: backgroundColor.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Icon(icon, color: backgroundColor, size: 20),
|
||
),
|
||
],
|
||
),
|
||
|
||
SizedBox(height: 16),
|
||
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||
textBaseline: TextBaseline.alphabetic,
|
||
children: [
|
||
Text(
|
||
value,
|
||
style: KoreanTypography.heading1.copyWith(
|
||
color: isWarning ? Color(0xFFDC143C) : backgroundColor,
|
||
),
|
||
),
|
||
SizedBox(width: 4),
|
||
Text(unit, style: KoreanTypography.body1),
|
||
],
|
||
),
|
||
|
||
if (percentage != null || trend != null) ...[
|
||
SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
if (percentage != null)
|
||
ShadBadge(
|
||
text: percentage,
|
||
backgroundColor: backgroundColor.withOpacity(0.1),
|
||
textColor: backgroundColor,
|
||
),
|
||
if (trend != null) ...[
|
||
if (percentage != null) SizedBox(width: 8),
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
trend.startsWith('+') ? Icons.arrow_upward : Icons.arrow_downward,
|
||
size: 12,
|
||
color: trendColor,
|
||
),
|
||
Text(
|
||
trend,
|
||
style: TextStyle(
|
||
color: trendColor,
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
],
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 🚀 **업무 효율성 극대화 UX**
|
||
|
||
### ⚡ **빠른 입력 시스템**
|
||
```yaml
|
||
한국_업무_특성_반영:
|
||
입력_최소화:
|
||
- 자동완성: 회사명, 장비명, 모델명
|
||
- 기본값: 오늘 날짜, 현재 사용자
|
||
- 복사: 이전 입력값 재사용 버튼
|
||
|
||
일괄_처리:
|
||
- 엑셀 업로드: 대량 데이터 등록
|
||
- 템플릿: 미리 정의된 양식
|
||
- 복제: 비슷한 항목 빠른 생성
|
||
|
||
검색_최적화:
|
||
- 한글 초성 검색: "ㅅㅁㅅ" → "삼성"
|
||
- 띄어쓰기 무시: "삼 성" → "삼성"
|
||
- 영문/한글 혼용: "samsung 갤럭시"
|
||
|
||
실시간_피드백:
|
||
- 입력 중 검증: 500ms debounce
|
||
- 진행률 표시: 필수 항목 완성도
|
||
- 저장 상태: 자동저장 + 수동저장
|
||
```
|
||
|
||
### 🎯 **상황별 맞춤 UI**
|
||
```dart
|
||
// 시간대별 UI 최적화
|
||
class TimeAwareUI {
|
||
static Widget buildDashboard(DateTime currentTime) {
|
||
final hour = currentTime.hour;
|
||
|
||
if (hour >= 9 && hour <= 10) {
|
||
// 출근 시간: 어제 변경사항, 오늘 할 일
|
||
return MorningDashboard();
|
||
} else if (hour >= 12 && hour <= 13) {
|
||
// 점심 시간: 간단한 현황만
|
||
return LunchDashboard();
|
||
} else if (hour >= 17 && hour <= 18) {
|
||
// 퇴근 시간: 오늘 완료 현황, 내일 예정
|
||
return EveningDashboard();
|
||
} else {
|
||
// 일반 시간: 전체 대시보드
|
||
return StandardDashboard();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 모바일 상황별 UI
|
||
class ContextAwareUI {
|
||
static Widget buildMobileInterface(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
// 1. 빠른 액션 바 (상단 고정)
|
||
Container(
|
||
color: Theme.of(context).primaryColor,
|
||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||
child: Row(
|
||
children: [
|
||
// QR 스캔 (카메라 접근)
|
||
ShadButton.ghost(
|
||
icon: Icon(Icons.qr_code_scanner, color: Colors.white),
|
||
onPressed: () => _scanQRCode(context),
|
||
),
|
||
|
||
Spacer(),
|
||
|
||
// 음성 검색 (음성 인식)
|
||
ShadButton.ghost(
|
||
icon: Icon(Icons.mic, color: Colors.white),
|
||
onPressed: () => _voiceSearch(context),
|
||
),
|
||
|
||
// 즐겨찾기 (자주 사용)
|
||
ShadButton.ghost(
|
||
icon: Icon(Icons.star, color: Colors.white),
|
||
onPressed: () => _showFavorites(context),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// 2. 메인 콘텐츠 (스크롤 가능)
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
children: [
|
||
_buildQuickStats(),
|
||
_buildRecentItems(),
|
||
_buildPendingTasks(),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
// 3. 플로팅 액션 버튼 (주요 작업)
|
||
FloatingActionButton.extended(
|
||
onPressed: () => _showQuickActions(context),
|
||
icon: Icon(Icons.add),
|
||
label: Text("빠른 등록"),
|
||
backgroundColor: Theme.of(context).primaryColor,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 🔒 **보안 및 접근성**
|
||
|
||
### 🛡️ **한국형 보안 요구사항**
|
||
```yaml
|
||
개인정보보호법_준수:
|
||
데이터_최소화: "필요한 정보만 수집"
|
||
동의_관리: "목적별 동의 받기"
|
||
보유_기간: "법정 보유기간 준수"
|
||
삭제_권리: "사용자 요청 시 즉시 삭제"
|
||
|
||
접근_제어:
|
||
인증: "2단계 인증 (SMS, 앱)"
|
||
권한: "역할 기반 접근 제어"
|
||
로그: "모든 접근 이력 기록"
|
||
세션: "30분 비활성시 자동 로그아웃"
|
||
|
||
데이터_암호화:
|
||
전송: "TLS 1.3 사용"
|
||
저장: "AES-256 암호화"
|
||
백업: "암호화된 백업 파일"
|
||
로그: "민감정보 마스킹"
|
||
```
|
||
|
||
### ♿ **접근성 및 사용성**
|
||
```yaml
|
||
웹_접근성_가이드라인:
|
||
키보드_네비게이션: "Tab, Enter, Esc 키 지원"
|
||
스크린_리더: "명확한 라벨, 설명 텍스트"
|
||
색상_대비: "WCAG 2.1 AA 기준 준수"
|
||
폰트_크기: "최소 14px, 확대 200% 지원"
|
||
|
||
다국어_지원:
|
||
기본_언어: "한국어 (ko-KR)"
|
||
보조_언어: "영어 (en-US)"
|
||
숫자_형식: "1,234,567원"
|
||
날짜_형식: "2025년 8월 23일 (금요일)"
|
||
|
||
성능_최적화:
|
||
초기_로딩: "2초 이내"
|
||
페이지_전환: "300ms 이내"
|
||
검색_응답: "1초 이내"
|
||
파일_업로드: "진행률 표시"
|
||
```
|
||
|
||
## 📱 **모바일 특화 기능**
|
||
|
||
### 📷 **한국 모바일 환경 최적화**
|
||
```dart
|
||
// 모바일 전용 기능들
|
||
class MobileOptimizedFeatures {
|
||
// 1. QR/바코드 스캔 (장비 등록용)
|
||
static Future<String?> scanEquipmentCode() async {
|
||
return await BarcodeScanner.scan(
|
||
options: ScanOptions(
|
||
strings: {
|
||
'cancel': '취소',
|
||
'flash_on': '플래시 켜기',
|
||
'flash_off': '플래시 끄기',
|
||
},
|
||
restrictFormat: [BarcodeFormat.qr, BarcodeFormat.code128],
|
||
),
|
||
);
|
||
}
|
||
|
||
// 2. 음성 검색 (한국어 STT)
|
||
static Future<String?> voiceSearch() async {
|
||
return await SpeechToText.listen(
|
||
localeId: 'ko-KR',
|
||
onResult: (result) => result.recognizedWords,
|
||
listenOptions: SpeechListenOptions(
|
||
partialResults: true,
|
||
listenMode: ListenMode.confirmation,
|
||
cancelOnError: true,
|
||
),
|
||
);
|
||
}
|
||
|
||
// 3. 오프라인 모드 (핵심 데이터 캐시)
|
||
static Future<void> syncOfflineData() async {
|
||
final box = await Hive.openBox('offline_cache');
|
||
|
||
// 필수 데이터만 오프라인 저장
|
||
await box.put('companies', await CompanyRepository.getAllCompanies());
|
||
await box.put('equipment_types', await EquipmentRepository.getTypes());
|
||
await box.put('recent_equipments', await EquipmentRepository.getRecent(50));
|
||
|
||
// 7일 후 만료
|
||
await box.put('cache_expiry', DateTime.now().add(Duration(days: 7)));
|
||
}
|
||
|
||
// 4. 푸시 알림 (한국어 메시지)
|
||
static Future<void> sendMaintenanceAlert(Equipment equipment) async {
|
||
await FirebaseMessaging.instance.sendMessage(
|
||
to: equipment.managerId,
|
||
data: {
|
||
'title': '유지보수 알림',
|
||
'body': '${equipment.name} 장비의 점검일이 다가왔습니다.',
|
||
'type': 'maintenance_due',
|
||
'equipment_id': equipment.id.toString(),
|
||
},
|
||
);
|
||
}
|
||
|
||
// 5. 생체인증 (지문, Face ID)
|
||
static Future<bool> authenticateWithBiometrics() async {
|
||
final localAuth = LocalAuthentication();
|
||
|
||
try {
|
||
final isAuthenticated = await localAuth.authenticate(
|
||
localizedFallbackTitle: 'PIN으로 인증',
|
||
authMessages: [
|
||
AndroidAuthMessages(
|
||
signInTitle: '생체인증으로 로그인',
|
||
biometricHint: '지문을 터치하세요',
|
||
cancelButton: '취소',
|
||
),
|
||
IOSAuthMessages(
|
||
lockOut: '생체인증이 비활성화되었습니다',
|
||
cancelButton: '취소',
|
||
),
|
||
],
|
||
);
|
||
|
||
return isAuthenticated;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 🎨 **한국형 아이콘 및 시각 요소**
|
||
|
||
### 🎯 **문화적 친화성**
|
||
```yaml
|
||
아이콘_선택_기준:
|
||
직관성: "한국 사용자가 즉시 이해할 수 있는 아이콘"
|
||
일관성: "Material Design 3 기반"
|
||
가독성: "24dp 이상, 명확한 선"
|
||
|
||
주요_아이콘_매핑:
|
||
홈: "🏠 house (집 모양)"
|
||
설정: "⚙️ settings (톱니바퀴)"
|
||
검색: "🔍 search (돋보기)"
|
||
등록: "➕ add (플러스)"
|
||
수정: "✏️ edit (연필)"
|
||
삭제: "🗑️ delete (휴지통)"
|
||
다운로드: "⬇️ download (아래 화살표)"
|
||
업로드: "⬆️ upload (위 화살표)"
|
||
알림: "🔔 notifications (벨)"
|
||
즐겨찾기: "⭐ star (별)"
|
||
|
||
상태_표시_아이콘:
|
||
성공: "✅ check_circle (체크 원)"
|
||
경고: "⚠️ warning (삼각형 느낌표)"
|
||
오류: "❌ error (X 표시)"
|
||
정보: "ℹ️ info (원 안에 i)"
|
||
로딩: "⏳ hourglass (시계)"
|
||
```
|
||
|
||
### 🎨 **색상 심리학 활용**
|
||
```dart
|
||
// 한국 사용자 선호 색상 시스템
|
||
class KoreanColorSystem {
|
||
// 메인 브랜드 컬러 (신뢰감)
|
||
static const Color primaryBlue = Color(0xFF1E40AF);
|
||
static const Color primaryBlueLight = Color(0xFF3B82F6);
|
||
static const Color primaryBlueDark = Color(0xFF1E3A8A);
|
||
|
||
// 보조 컬러 (활동성)
|
||
static const Color secondaryGreen = Color(0xFF059669);
|
||
static const Color secondaryGreenLight = Color(0xFF10B981);
|
||
static const Color secondaryGreenDark = Color(0xFF047857);
|
||
|
||
// 시스템 컬러 (기능성)
|
||
static const Color warningAmber = Color(0xFFD97706); // 주의
|
||
static const Color dangerRed = Color(0xFFDC2626); // 위험
|
||
static const Color infoBlue = Color(0xFF0284C7); // 정보
|
||
static const Color successGreen = Color(0xFF16A34A); // 성공
|
||
|
||
// 중성 컬러 (조화)
|
||
static const Color neutralGray = Color(0xFF6B7280);
|
||
static const Color neutralLightGray = Color(0xFFF3F4F6);
|
||
static const Color neutralDarkGray = Color(0xFF374151);
|
||
|
||
// 한국인 선호 그라데이션
|
||
static const LinearGradient primaryGradient = LinearGradient(
|
||
colors: [Color(0xFF1E40AF), Color(0xFF3B82F6)],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
);
|
||
|
||
// 상태별 배경색 (시각적 구분)
|
||
static Color getStatusColor(String status) {
|
||
switch (status) {
|
||
case '정상': return successGreen.withOpacity(0.1);
|
||
case '주의': return warningAmber.withOpacity(0.1);
|
||
case '위험': return dangerRed.withOpacity(0.1);
|
||
case '점검': return infoBlue.withOpacity(0.1);
|
||
default: return neutralLightGray;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📅 Recent Updates
|