Files
superport/CLAUDE_old.md

1818 lines
58 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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