58 KiB
58 KiB
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
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
# 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: 현재 프론트엔드 구조와 백엔드 실제 스키마 간 심각한 불일치 발견
🔍 주요 불일치 사항
현재_프론트엔드_가정_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)
핵심 엔티티 관계도
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
새로 발견된 필수 엔티티
// 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;
}
기존 엔티티 수정 필요 사항
// 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+ 컴포넌트 구현 완료
🎯 핵심 컴포넌트 활용 계획
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: "리스트 페이지네이션"
🖥️ 웹 우선 반응형 디자인 전략
// 반응형 브레이크포인트 정의
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 구조: [스택형 레이아웃]
- 풀스크린 카드 + 바텀시트
- 리스트뷰 + 탭 네비게이션
📦 필요한 추가 의존성
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 설계 원칙
데이터 흐름 기반 화면 설계
사용자_워크플로우_우선:
Equipment_등록_흐름:
1단계: "제조사 선택 → 모델 자동 필터링"
2단계: "시리얼 번호 입력 → 실시간 중복 검증"
3단계: "워런티 정보 → 만료일 자동 계산"
4단계: "저장 전 최종 검증 → 성공/실패 피드백"
Company_등록_흐름:
1단계: "기본 정보 입력 → 실시간 유효성 검증"
2단계: "주소 검색 → Daum API 웹뷰 호출"
3단계: "연락처 정보 → 전화번호 형식 자동 변환"
4단계: "계층 구조 설정 → 본사/지점 관계 시각화"
실시간_검증_및_피드백:
입력중_검증:
- 시리얼 번호: debounce 500ms 후 중복 검사
- 이메일: 형식 검증 + @ 도메인 검증
- 전화번호: 010-0000-0000 형식 자동 변환
- 사업자번호: 000-00-00000 형식 + 유효성 검증
저장전_검증:
- 필수 항목 누락 시 해당 필드로 자동 스크롤
- 에러 메시지를 필드 하단에 빨간색으로 표시
- 성공 시 ShadToast로 "저장되었습니다" 알림
- 실패 시 구체적인 오류 원인 표시
주소 검색 시스템 (Daum API 연동)
// 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;
}
폼 컴포넌트 표준화
// 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);
}
}
}
에러 처리 및 사용자 피드백 시스템
에러_표시_전략:
폼_레벨_에러:
- 필수 항목 누락: 빨간색 테두리 + 필드별 에러 메시지
- 서버 에러: 폼 상단에 ShadAlert로 전체 에러 표시
- 네트워크 에러: 재시도 버튼 포함된 에러 배너
필드_레벨_에러:
- 실시간 검증: debounce 후 즉시 표시
- 포커스 아웃 검증: 필드를 벗어날 때 검증
- 아이콘으로 상태 표시: ✓(성공), ⚠(경고), ✗(에러)
성공_피드백:
저장_성공: "ShadToast.success('장비가 성공적으로 등록되었습니다')"
수정_성공: "ShadToast.success('정보가 업데이트되었습니다')"
삭제_성공: "ShadToast.success('삭제가 완료되었습니다')"
로딩_상태:
버튼_로딩: "ShadButton에 loading 상태 표시"
폼_로딩: "ShadProgress로 전체 폼 비활성화"
리스트_로딩: "ShadSkeleton으로 로딩 상태 표시"
접근성 및 사용성 개선
키보드_네비게이션:
- Tab 키로 순차적 필드 이동
- Enter 키로 다음 필드 또는 저장 실행
- Esc 키로 모달/다이얼로그 닫기
모바일_최적화:
- 가상 키보드에 맞는 input type 설정
- 화면 회전 시 레이아웃 자동 조정
- 터치 영역 최소 44px 보장
다국어_대응:
- 모든 텍스트 다국어 키로 관리
- 날짜/시간 형식 로케일별 자동 조정
- 숫자 형식(천단위 구분) 로케일 대응
성능_최적화:
- 대용량 리스트 가상화 스크롤
- 이미지 지연 로딩 및 캐싱
- API 호출 debounce/throttling
🏗️ 전면 리팩토링 7단계 계획
📋 "필요한 모든 에이전트를 동원해라. 클린 아키텍쳐와 함께 SRP를 무조건 지켜서 작업해라."
🔧 리팩토링 자유도 및 권한
프로젝트_구조_변경_권한:
디렉토리_구조: "전면 재편 가능"
파일_삭제: "필요없는 파일 완전 제거 허용"
폴더_이동: "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 구현 우선순위
사용자_흐름_기반_구현_순서:
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)
목표: "실제 백엔드 스키마에 맞춘 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)
목표: "통일된 디자인 시스템 및 반응형 레이아웃 기반 구축"
작업_범위:
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)
목표: "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)
목표: "License → MaintenanceHistory 완전 전환"
기능_재정의:
기존: "라이선스 관리 (독립 엔티티)"
신규: "장비 유지보수 이력 관리 (Equipment History 연동)"
새로운_Maintenance_화면:
- Equipment 선택 → History 조회 → Maintenance 등록
- 방문/원격 유지보수 구분
- 주기별 스케줄링 (period_month)
- 만료일 알림 시스템 (기존 License 알림 재활용)
데이터_마이그레이션:
- 기존 License 데이터 → MaintenanceHistory 변환 스크립트
- API 엔드포인트 변경: /licenses → /maintenances
- 알림 로직 재구성
🏢 Phase 5: Company 계층 구조 시각화 (Week 2: Day 11-12)
목표: "본사-지점 계층 관리 + 파트너/고객 구분"
Company_Tree_View:
- 계층형 트리 구조 시각화
- Drag & Drop으로 계층 변경
- 파트너사/고객사 필터링
- 대시보드 통계에 계층별 집계 반영
신규_기능:
- 본사 → 지점 일괄 설정
- 계층별 권한 관리 (상위 회사가 하위 회사 관리)
- 지역별/계층별 장비 현황 보고서
📊 Phase 6: Equipment History & Rent 시스템 (Week 2: Day 13-14)
목표: "완전한 장비 라이프사이클 추적"
Equipment_History_Tracking:
- 입고 (I): 창고 입고 + 수량 관리
- 출고 (O): 회사별 배치 + 상태 변경
- 대여 시작: Rent 레코드 생성
- 대여 종료: 반납 처리 + 상태 복원
Rent_Management_System:
- 대여 기간 관리 (시작일/종료일)
- 연장 승인 프로세스
- 반납 체크리스트
- 연체 알림 시스템
Warehouse_Stock_Dashboard:
- 창고별 실시간 재고 현황
- 장비별 위치 추적
- 입출고 이력 시각화
⚡ Phase 7: 성능 최적화 & 모바일 완성 (Week 3: Day 15-21)
목표: "엔터프라이즈급 성능 + 완벽한 반응형"
성능_최적화:
- 가상화 스크롤링: flutter_staggered_grid_view
- 무한 스크롤: 페이지네이션 자동 로딩
- 이미지 최적화: 바코드/QR코드 캐싱
- 메모리 관리: 대용량 리스트 효율화
캐싱_전략:
- Vendor/Model: 1시간 캐시
- Company 계층: 30분 캐시
- Lookups: 기존 30분 유지
- Equipment History: 실시간 업데이트
모바일_UX_완성:
- 터치 제스처: Swipe to Action
- 오프라인 지원: 핵심 데이터 로컬 저장
- PWA 최적화: 설치 가능한 웹앱
- 푸시 알림: 만료일/연체 알림
🛡️ 작업 안정성 보장 방안
🔒 사이드 이펙트 최소화 전략
0. 구조적 변경 안전성
디렉토리_재구성_안전장치:
백업_생성: "Git 브랜치로 현재 상태 완전 보존"
단계적_변경: "폴더별 순차적 재구성으로 추적 가능"
테스트_검증: "구조 변경 후 빌드 및 테스트 확인"
롤백_가능성: "언제든 이전 구조로 복원 가능"
파일_삭제_안전성:
사전_검토: "삭제 전 의존성 분석 및 영향도 확인"
점진적_제거: "deprecated → warning → 완전 삭제 단계"
복구_대비: "Git history를 통한 완전한 복구 경로"
테스트_확인: "삭제 후 전체 시스템 동작 검증"
1. 점진적 마이그레이션 (Zero-Downtime)
Stage_A: "새로운 모델 병렬 구축"
- 기존 DTO와 신규 DTO 동시 존재
- Feature Flag로 화면별 전환 제어
- A/B 테스트 지원 구조
Stage_B: "화면별 개별 전환"
- 하나씩 새 구조로 마이그레이션
- 각 단계마다 완전한 테스트
- 언제든 이전 버전으로 롤백 가능
Stage_C: "레거시 코드 단계적 제거"
- 새 시스템 안정성 확인 후
- deprecated 경고 → 완전 삭제
- 최종 정리 및 최적화
2. Clean Architecture 철저한 준수
// 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)
Unit_Tests:
- 새로운 UseCase별 100% 커버리지
- DTO 변환 로직 Edge Case 테스트
- Validation 로직 모든 시나리오 테스트
Integration_Tests:
- 백엔드 API 연동 자동화 테스트
- Equipment → Model → Vendor 연쇄 조회 테스트
- 트랜잭션 무결성 테스트
Widget_Tests:
- ShadCN 컴포넌트 모든 상호작용 테스트
- 반응형 레이아웃 모든 브레이크포인트 테스트
- 폼 유효성 검증 시나리오 테스트
E2E_Tests:
- 전체 워크플로우 테스트 (장비 등록 → 출고 → 대여 → 반납)
- 권한별 접근 제어 테스트
- 성능 테스트 (대용량 데이터 처리)
📈 프로젝트 상태 업데이트
🚨 현재 상태 재평가
이전_평가: "Development (99.9% Complete)"
현실_평가: "Major Architecture Gap Discovered (재설계 필요)"
API_호환성: "95% → 40% (심각한 스키마 불일치 발견)"
UI_현대화: "70% → 30% (ShadCN UI 적용 필요)"
기능_완성도: "90% → 60% (핵심 기능 누락 다수)"
전체_완성도: "99.9% → 65% (대규모 리팩토링 필요)"
📊 새로운 성공 지표 (KPI)
기술적_목표:
API_동기화율: "현재 40% → 목표 100%"
UI_일관성: "현재 60% → 목표 95%"
테스트_커버리지: "현재 70% → 목표 90%"
빌드_시간: "25초 유지"
성능: "현재 대비 30% 향상"
사용자_경험:
화면_로딩: "3초 → 1.5초 이하"
모바일_최적화: "70% → 95%"
접근성: "기본 → WCAG 2.1 AA 준수"
유지보수성:
코드_중복률: "15% → 3% 이하"
의존성_결합도: "높음 → 낮음"
SRP_위반: "다수 → 0건"
⏰ 실행 일정
📅 3주 집중 개발 계획
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 사용 패턴 연구
업무_환경_특성:
근무_시간: "09:00-18:00 (주 40시간)"
업무_스타일: "빠른 의사결정, 즉시 처리 선호"
보고_문화: "실시간 현황 파악, 시각적 데이터 선호"
모바일_활용: "업무 시간 외 모바일 접근 빈번"
ERP_사용_패턴:
접근_시점: "출근 직후 (09:00-09:30), 퇴근 직전 (17:30-18:00)"
주요_업무: "일일 현황 확인 → 긴급 처리 → 보고서 작성"
선호_기능: "대시보드 → 검색 → 등록/수정 → 보고서"
처리_속도: "3-Click Rule (최대 3번 클릭으로 목표 달성)"
정보_소비_패턴:
시선_흐름: "좌상단 → 우상단 → 좌하단 → 우하단 (Z패턴)"
중요_정보: "상단 고정, 색상 구분, 숫자 강조"
경고_알림: "빨간색 Badge, 점멸 효과, 소리 알림"
성공_피드백: "파란색/초록색, 체크 아이콘, 간결한 메시지"
🏢 한국 기업 조직문화 반영
의사결정_구조:
상명하달: "관리자 권한 명확한 구분"
보고_라인: "계층별 데이터 접근 권한 차등화"
승인_프로세스: "단계별 승인 절차 시각화"
책임_추적: "작업자 기록 및 이력 관리"
업무_프로세스:
긴급_업무: "빨간색 라벨, 상단 고정 표시"
일반_업무: "우선순위 번호, 마감일 표시"
완료_업무: "회색 처리, 접기 기능"
보류_업무: "노란색 배경, 사유 표시"
커뮤니케이션:
알림_방식: "팝업 → 배지 → 이메일 → SMS 순서"
언어_사용: "존댓말 기본, 업무용 단어 사용"
시간_표기: "24시간제, '오전/오후' 병기"
날짜_형식: "YYYY년 MM월 DD일 (요일)"
🎨 한국형 UI 디자인 원칙
🖥️ 화면 레이아웃 최적화
// 한국어 텍스트 특성 고려 레이아웃
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),
);
}
📱 모바일 우선 반응형 설계
한국_모바일_사용_현황:
스마트폰_보급률: "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 이하"
🎯 사용자 중심 네비게이션
// 한국형 네비게이션 패턴
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,
),
],
);
}
}
📊 한국형 대시보드 설계
🎨 정보 시각화 원칙
대시보드_구성_요소:
상단_KPI_영역: "핵심 지표 4-6개, 큰 숫자로 표시"
좌측_메뉴_영역: "주요 기능 바로가기"
중앙_차트_영역: "트렌드 차트, 상태별 파이차트"
우측_알림_영역: "긴급사항, 만료 예정 항목"
하단_최근_활동: "최근 등록/수정된 항목들"
색상_활용_전략:
상태_표시:
정상: "#28A745 (초록) + ✓ 체크 아이콘"
주의: "#FFC107 (노랑) + ⚠ 경고 아이콘"
위험: "#DC3545 (빨강) + ⚡ 긴급 아이콘"
비활성: "#6C757D (회색) + ○ 원 아이콘"
중요도_구분:
최우선: "빨간 배경, 흰 글자, 굵은 테두리"
높음: "주황 배경, 검은 글자, 점선 테두리"
보통: "파란 배경, 흰 글자, 실선 테두리"
낮음: "회색 배경, 검은 글자, 테두리 없음"
숫자_표현_방식:
큰_숫자: "123,456대 (천단위 구분)"
비율: "85.2% (소수점 1자리)"
금액: "₩1,234,567원 (원화 표시)"
날짜: "2025-08-23 (금) 오후 2:30"
📈 실시간 현황판 설계
// 한국형 실시간 대시보드 위젯
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
⚡ 빠른 입력 시스템
한국_업무_특성_반영:
입력_최소화:
- 자동완성: 회사명, 장비명, 모델명
- 기본값: 오늘 날짜, 현재 사용자
- 복사: 이전 입력값 재사용 버튼
일괄_처리:
- 엑셀 업로드: 대량 데이터 등록
- 템플릿: 미리 정의된 양식
- 복제: 비슷한 항목 빠른 생성
검색_최적화:
- 한글 초성 검색: "ㅅㅁㅅ" → "삼성"
- 띄어쓰기 무시: "삼 성" → "삼성"
- 영문/한글 혼용: "samsung 갤럭시"
실시간_피드백:
- 입력 중 검증: 500ms debounce
- 진행률 표시: 필수 항목 완성도
- 저장 상태: 자동저장 + 수동저장
🎯 상황별 맞춤 UI
// 시간대별 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,
),
],
);
}
}
🔒 보안 및 접근성
🛡️ 한국형 보안 요구사항
개인정보보호법_준수:
데이터_최소화: "필요한 정보만 수집"
동의_관리: "목적별 동의 받기"
보유_기간: "법정 보유기간 준수"
삭제_권리: "사용자 요청 시 즉시 삭제"
접근_제어:
인증: "2단계 인증 (SMS, 앱)"
권한: "역할 기반 접근 제어"
로그: "모든 접근 이력 기록"
세션: "30분 비활성시 자동 로그아웃"
데이터_암호화:
전송: "TLS 1.3 사용"
저장: "AES-256 암호화"
백업: "암호화된 백업 파일"
로그: "민감정보 마스킹"
♿ 접근성 및 사용성
웹_접근성_가이드라인:
키보드_네비게이션: "Tab, Enter, Esc 키 지원"
스크린_리더: "명확한 라벨, 설명 텍스트"
색상_대비: "WCAG 2.1 AA 기준 준수"
폰트_크기: "최소 14px, 확대 200% 지원"
다국어_지원:
기본_언어: "한국어 (ko-KR)"
보조_언어: "영어 (en-US)"
숫자_형식: "1,234,567원"
날짜_형식: "2025년 8월 23일 (금요일)"
성능_최적화:
초기_로딩: "2초 이내"
페이지_전환: "300ms 이내"
검색_응답: "1초 이내"
파일_업로드: "진행률 표시"
📱 모바일 특화 기능
📷 한국 모바일 환경 최적화
// 모바일 전용 기능들
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;
}
}
}
🎨 한국형 아이콘 및 시각 요소
🎯 문화적 친화성
아이콘_선택_기준:
직관성: "한국 사용자가 즉시 이해할 수 있는 아이콘"
일관성: "Material Design 3 기반"
가독성: "24dp 이상, 명확한 선"
주요_아이콘_매핑:
홈: "🏠 house (집 모양)"
설정: "⚙️ settings (톱니바퀴)"
검색: "🔍 search (돋보기)"
등록: "➕ add (플러스)"
수정: "✏️ edit (연필)"
삭제: "🗑️ delete (휴지통)"
다운로드: "⬇️ download (아래 화살표)"
업로드: "⬆️ upload (위 화살표)"
알림: "🔔 notifications (벨)"
즐겨찾기: "⭐ star (별)"
상태_표시_아이콘:
성공: "✅ check_circle (체크 원)"
경고: "⚠️ warning (삼각형 느낌표)"
오류: "❌ error (X 표시)"
정보: "ℹ️ info (원 안에 i)"
로딩: "⏳ hourglass (시계)"
🎨 색상 심리학 활용
// 한국 사용자 선호 색상 시스템
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;
}
}
}