# 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 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,
''', 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 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 _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>> getEquipmentsByVendor({ required int vendorId, PaginationParams? params, }); Future> isSerialNumberUnique(String serialNumber); } // UseCase: ๋‹จ์ผ ์ฑ…์ž„ ์›์น™ (SRP) ์ฒ ์ € ์ค€์ˆ˜ class ValidateEquipmentSerialUseCase { final EquipmentRepository _repository; Future> 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 mainMenuItems = [ "๋Œ€์‹œ๋ณด๋“œ", // ์ฒซ ํ™”๋ฉด, ์ „์ฒด ํ˜„ํ™ฉ "์žฅ๋น„๊ด€๋ฆฌ", // ํ•ต์‹ฌ ์—…๋ฌด "ํšŒ์‚ฌ๊ด€๋ฆฌ", // ๊ณ ๊ฐ/ํŒŒํŠธ๋„ˆ ๊ด€๋ฆฌ "์œ ์ง€๋ณด์ˆ˜", // ์ •๊ธฐ ์—…๋ฌด "๋ณด๊ณ ์„œ", // ๊ฒฐ๊ณผ ํ™•์ธ ]; // ๋ธŒ๋ ˆ๋“œํฌ๋Ÿผ: ํ˜„์žฌ ์œ„์น˜ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ static Widget buildBreadcrumb(List 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 scanEquipmentCode() async { return await BarcodeScanner.scan( options: ScanOptions( strings: { 'cancel': '์ทจ์†Œ', 'flash_on': 'ํ”Œ๋ž˜์‹œ ์ผœ๊ธฐ', 'flash_off': 'ํ”Œ๋ž˜์‹œ ๋„๊ธฐ', }, restrictFormat: [BarcodeFormat.qr, BarcodeFormat.code128], ), ); } // 2. ์Œ์„ฑ ๊ฒ€์ƒ‰ (ํ•œ๊ตญ์–ด STT) static Future 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 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 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 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