feat: 라이선스 및 창고 관리 API 연동 구현

- 라이선스 관리 API 연동 완료
  - LicenseRemoteDataSource, LicenseService 구현
  - LicenseListController, LicenseFormController API 연동
  - 페이지네이션, 검색, 필터링 기능 추가
  - 라이선스 할당/해제 기능 구현

- 창고 관리 API 연동 완료
  - WarehouseRemoteDataSource, WarehouseService 구현
  - WarehouseLocationListController, WarehouseLocationFormController API 연동
  - 창고별 장비 조회 및 용량 관리 기능 추가

- DI 컨테이너에 새로운 서비스 등록
- API 통합 문서 업데이트 (전체 진행률 100% 달성)
This commit is contained in:
JiWoong Sul
2025-07-25 00:18:49 +09:00
parent 37f35ca68b
commit 8384423cf2
23 changed files with 7591 additions and 926 deletions

View File

@@ -1,10 +1,16 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:superport/models/warehouse_location_model.dart';
import 'package:superport/models/address_model.dart';
import 'package:superport/services/warehouse_service.dart';
import 'package:superport/services/mock_data_service.dart';
/// 입고지 폼 상태 및 저장/수정 로직을 담당하는 컨트롤러
class WarehouseLocationFormController {
class WarehouseLocationFormController extends ChangeNotifier {
final bool useApi;
final MockDataService? mockDataService;
late final WarehouseService _warehouseService;
/// 폼 키
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
@@ -15,74 +21,157 @@ class WarehouseLocationFormController {
final TextEditingController remarkController = TextEditingController();
/// 주소 정보
Address address = const Address();
Address _address = const Address();
/// 저장 중 여부
bool isSaving = false;
bool _isSaving = false;
/// 수정 모드 여부
bool isEditMode = false;
bool _isEditMode = false;
/// 입고지 id (수정 모드)
int? id;
int? _id;
/// 로딩 상태
bool _isLoading = false;
/// 에러 메시지
String? _error;
/// 원본 창고 위치 (수정 모드)
WarehouseLocation? _originalLocation;
WarehouseLocationFormController({
this.useApi = true,
this.mockDataService,
int? locationId,
}) {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
_warehouseService = GetIt.instance<WarehouseService>();
}
if (locationId != null) {
initialize(locationId);
}
}
// Getters
Address get address => _address;
bool get isSaving => _isSaving;
bool get isEditMode => _isEditMode;
int? get id => _id;
bool get isLoading => _isLoading;
String? get error => _error;
WarehouseLocation? get originalLocation => _originalLocation;
/// 기존 데이터 세팅 (수정 모드)
void initialize(int? locationId) {
id = locationId;
if (id != null) {
final location = MockDataService().getWarehouseLocationById(id!);
if (location != null) {
isEditMode = true;
nameController.text = location.name;
address = location.address;
remarkController.text = location.remark ?? '';
Future<void> initialize(int locationId) async {
_id = locationId;
_isEditMode = true;
_isLoading = true;
_error = null;
notifyListeners();
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
_originalLocation = await _warehouseService.getWarehouseLocationById(locationId);
} else {
_originalLocation = mockDataService?.getWarehouseLocationById(locationId);
}
if (_originalLocation != null) {
nameController.text = _originalLocation!.name;
_address = _originalLocation!.address;
remarkController.text = _originalLocation!.remark ?? '';
}
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
/// 주소 변경 처리
void updateAddress(Address newAddress) {
address = newAddress;
_address = newAddress;
notifyListeners();
}
/// 저장 처리 (추가/수정)
Future<bool> save(BuildContext context) async {
Future<bool> save() async {
if (!formKey.currentState!.validate()) return false;
isSaving = true;
if (isEditMode) {
// 수정
MockDataService().updateWarehouseLocation(
WarehouseLocation(
id: id!,
name: nameController.text.trim(),
address: address,
remark: remarkController.text.trim(),
),
);
} else {
// 추가
MockDataService().addWarehouseLocation(
WarehouseLocation(
id: 0,
name: nameController.text.trim(),
address: address,
remark: remarkController.text.trim(),
),
_isSaving = true;
_error = null;
notifyListeners();
try {
final location = WarehouseLocation(
id: _isEditMode ? _id! : 0,
name: nameController.text.trim(),
address: _address,
remark: remarkController.text.trim().isEmpty ? null : remarkController.text.trim(),
);
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
if (_isEditMode) {
await _warehouseService.updateWarehouseLocation(location);
} else {
await _warehouseService.createWarehouseLocation(location);
}
} else {
if (_isEditMode) {
mockDataService?.updateWarehouseLocation(location);
} else {
mockDataService?.addWarehouseLocation(location);
}
}
return true;
} catch (e) {
_error = e.toString();
notifyListeners();
return false;
} finally {
_isSaving = false;
notifyListeners();
}
isSaving = false;
Navigator.pop(context, true);
return true;
}
/// 취소 처리
void cancel(BuildContext context) {
Navigator.pop(context, false);
/// 폼 초기화
void resetForm() {
nameController.clear();
remarkController.clear();
_address = const Address();
_error = null;
formKey.currentState?.reset();
notifyListeners();
}
/// 유효성 검사
String? validateName(String? value) {
if (value == null || value.isEmpty) {
return '입고지명을 입력해주세요';
}
if (value.length < 2) {
return '입고지명은 2자 이상이어야 합니다';
}
return null;
}
String? validateAddress() {
if (_address.isEmpty) {
return '주소를 입력해주세요';
}
return null;
}
/// 컨트롤러 해제
@override
void dispose() {
nameController.dispose();
remarkController.dispose();
super.dispose();
}
}

View File

@@ -1,36 +1,246 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:superport/models/warehouse_location_model.dart';
import 'package:superport/services/warehouse_service.dart';
import 'package:superport/services/mock_data_service.dart';
/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (SRP 적용)
/// UI, 네비게이션, 다이얼로그 등은 포함하지 않음
/// 향후 서비스/리포지토리 DI 구조로 확장 가능
class WarehouseLocationListController {
/// 입고지 데이터 서비스 (mock)
final MockDataService _dataService = MockDataService();
class WarehouseLocationListController extends ChangeNotifier {
final bool useApi;
final MockDataService? mockDataService;
late final WarehouseService _warehouseService;
List<WarehouseLocation> _warehouseLocations = [];
List<WarehouseLocation> _filteredLocations = [];
bool _isLoading = false;
String? _error;
String _searchQuery = '';
int _currentPage = 1;
final int _pageSize = 20;
bool _hasMore = true;
int _total = 0;
/// 전체 입고지 목록
List<WarehouseLocation> warehouseLocations = [];
// 필터 옵션
bool? _isActive;
WarehouseLocationListController({this.useApi = true, this.mockDataService}) {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
_warehouseService = GetIt.instance<WarehouseService>();
}
}
// Getters
List<WarehouseLocation> get warehouseLocations => _filteredLocations;
bool get isLoading => _isLoading;
String? get error => _error;
String get searchQuery => _searchQuery;
int get currentPage => _currentPage;
bool get hasMore => _hasMore;
int get total => _total;
bool? get isActive => _isActive;
/// 데이터 로드
void loadWarehouseLocations() {
warehouseLocations = _dataService.getAllWarehouseLocations();
Future<void> loadWarehouseLocations({bool isInitialLoad = true}) async {
if (_isLoading) return;
_isLoading = true;
_error = null;
if (isInitialLoad) {
_currentPage = 1;
_warehouseLocations.clear();
_hasMore = true;
}
notifyListeners();
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
// API 사용
final fetchedLocations = await _warehouseService.getWarehouseLocations(
page: _currentPage,
perPage: _pageSize,
isActive: _isActive,
);
if (isInitialLoad) {
_warehouseLocations = fetchedLocations;
} else {
_warehouseLocations.addAll(fetchedLocations);
}
_hasMore = fetchedLocations.length >= _pageSize;
// 전체 개수 조회
_total = await _warehouseService.getTotalWarehouseLocations(
isActive: _isActive,
);
} else {
// Mock 데이터 사용
final allLocations = mockDataService?.getAllWarehouseLocations() ?? [];
// 필터링 적용
var filtered = allLocations;
if (_isActive != null) {
// Mock 데이터에는 isActive 필드가 없으므로 모두 활성으로 처리
filtered = _isActive! ? allLocations : [];
}
// 페이지네이션 적용
final startIndex = (_currentPage - 1) * _pageSize;
final endIndex = startIndex + _pageSize;
if (startIndex < filtered.length) {
final pageLocations = filtered.sublist(
startIndex,
endIndex > filtered.length ? filtered.length : endIndex,
);
if (isInitialLoad) {
_warehouseLocations = pageLocations;
} else {
_warehouseLocations.addAll(pageLocations);
}
_hasMore = endIndex < filtered.length;
} else {
_hasMore = false;
}
_total = filtered.length;
}
_applySearchFilter();
if (!isInitialLoad) {
_currentPage++;
}
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
// 다음 페이지 로드
Future<void> loadNextPage() async {
if (!_hasMore || _isLoading) return;
_currentPage++;
await loadWarehouseLocations(isInitialLoad: false);
}
// 검색
void search(String query) {
_searchQuery = query;
_applySearchFilter();
notifyListeners();
}
// 검색 필터 적용
void _applySearchFilter() {
if (_searchQuery.isEmpty) {
_filteredLocations = List.from(_warehouseLocations);
} else {
_filteredLocations = _warehouseLocations.where((location) {
return location.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
location.address.toString().toLowerCase().contains(_searchQuery.toLowerCase());
}).toList();
}
}
// 필터 설정
void setFilters({bool? isActive}) {
_isActive = isActive;
loadWarehouseLocations();
}
// 필터 초기화
void clearFilters() {
_isActive = null;
_searchQuery = '';
loadWarehouseLocations();
}
/// 입고지 추가
void addWarehouseLocation(WarehouseLocation location) {
_dataService.addWarehouseLocation(location);
loadWarehouseLocations();
Future<void> addWarehouseLocation(WarehouseLocation location) async {
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
await _warehouseService.createWarehouseLocation(location);
} else {
mockDataService?.addWarehouseLocation(location);
}
// 목록 새로고침
await loadWarehouseLocations();
} catch (e) {
_error = e.toString();
notifyListeners();
}
}
/// 입고지 수정
void updateWarehouseLocation(WarehouseLocation location) {
_dataService.updateWarehouseLocation(location);
loadWarehouseLocations();
Future<void> updateWarehouseLocation(WarehouseLocation location) async {
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
await _warehouseService.updateWarehouseLocation(location);
} else {
mockDataService?.updateWarehouseLocation(location);
}
// 목록에서 업데이트
final index = _warehouseLocations.indexWhere((l) => l.id == location.id);
if (index != -1) {
_warehouseLocations[index] = location;
_applySearchFilter();
notifyListeners();
}
} catch (e) {
_error = e.toString();
notifyListeners();
}
}
/// 입고지 삭제
void deleteWarehouseLocation(int id) {
_dataService.deleteWarehouseLocation(id);
loadWarehouseLocations();
Future<void> deleteWarehouseLocation(int id) async {
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
await _warehouseService.deleteWarehouseLocation(id);
} else {
mockDataService?.deleteWarehouseLocation(id);
}
// 목록에서 제거
_warehouseLocations.removeWhere((l) => l.id == id);
_applySearchFilter();
_total--;
notifyListeners();
} catch (e) {
_error = e.toString();
notifyListeners();
}
}
// 새로고침
Future<void> refresh() async {
await loadWarehouseLocations();
}
// 사용 중인 창고 위치 조회
Future<List<WarehouseLocation>> getInUseWarehouseLocations() async {
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
return await _warehouseService.getInUseWarehouseLocations();
} else {
// Mock 데이터에서는 모든 창고가 사용 중으로 간주
return mockDataService?.getAllWarehouseLocations() ?? [];
}
} catch (e) {
_error = e.toString();
notifyListeners();
return [];
}
}
}