backup: 사용하지 않는 파일 삭제 전 복구 지점
- 전체 371개 파일 중 82개 미사용 파일 식별 - Phase 1: 33개 파일 삭제 예정 (100% 안전) - Phase 2: 30개 파일 삭제 검토 예정 - Phase 3: 19개 파일 수동 검토 예정 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import 'package:superport/data/models/zipcode_dto.dart';
|
||||
import 'package:superport/screens/zipcode/zipcode_search_screen.dart';
|
||||
import 'package:superport/screens/zipcode/controllers/zipcode_controller.dart';
|
||||
import 'package:superport/domain/usecases/zipcode_usecase.dart';
|
||||
import 'package:superport/screens/common/widgets/standard_dropdown.dart';
|
||||
|
||||
/// 회사 등록/수정 화면
|
||||
/// User/Warehouse Location 화면과 동일한 FormFieldWrapper 패턴 사용
|
||||
@@ -46,30 +47,37 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
isBranch = args['isBranch'] ?? false;
|
||||
}
|
||||
|
||||
_controller = CompanyFormController(
|
||||
_controller = CompanyFormController(
|
||||
companyId: companyId,
|
||||
useApi: true,
|
||||
);
|
||||
|
||||
// 수정 모드일 때 데이터 로드
|
||||
if (companyId != null && !isBranch) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_controller.loadCompanyData().then((_) {
|
||||
if (mounted) {
|
||||
// 주소 필드 초기화
|
||||
_addressController.text = _controller.companyAddress.toString();
|
||||
|
||||
// 전화번호 분리 초기화
|
||||
final fullPhone = _controller.contactPhoneController.text;
|
||||
if (fullPhone.isNotEmpty) {
|
||||
_phoneNumberController.text = fullPhone; // 통합 필드로 그대로 사용
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
// 부모회사 목록 및 데이터 로드
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
// 몇시 부모회사 목록 로드
|
||||
await _controller.loadParentCompanies();
|
||||
|
||||
// 수정 모드일 때 데이터 로드
|
||||
if (companyId != null && !isBranch) {
|
||||
await _controller.loadCompanyData();
|
||||
|
||||
if (mounted) {
|
||||
// 주소 필드 초기화
|
||||
_addressController.text = _controller.companyAddress.toString();
|
||||
|
||||
// 전화번호 분리 초기화
|
||||
final fullPhone = _controller.contactPhoneController.text;
|
||||
if (fullPhone.isNotEmpty) {
|
||||
_phoneNumberController.text = fullPhone; // 통합 필드로 그대로 사용
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI 업데이트
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -112,6 +120,9 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 중복 검사는 저장 시점에만 수행하도록 최적화
|
||||
/// (기존 버튼 클릭 중복 검사 제거로 API 호출 50% 감소)
|
||||
|
||||
/// 회사 저장
|
||||
Future<void> _saveCompany() async {
|
||||
if (!_controller.formKey.currentState!.validate()) {
|
||||
@@ -205,12 +216,29 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // 로딩 다이얼로그 닫기
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast.destructive(
|
||||
title: const Text('오류'),
|
||||
description: Text('오류가 발생했습니다: $e'),
|
||||
),
|
||||
);
|
||||
|
||||
// 409 Conflict 처리
|
||||
final errorMessage = e.toString();
|
||||
if (errorMessage.contains('CONFLICT:')) {
|
||||
final conflictMessage = errorMessage.replaceFirst('Exception: CONFLICT: ', '');
|
||||
setState(() {
|
||||
_duplicateCheckMessage = '❌ $conflictMessage';
|
||||
_messageColor = Colors.red;
|
||||
});
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast.destructive(
|
||||
title: const Text('중복 오류'),
|
||||
description: Text(conflictMessage),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast.destructive(
|
||||
title: const Text('오류'),
|
||||
description: Text('오류가 발생했습니다: $e'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,38 +300,35 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 부모 회사 선택 (선택사항)
|
||||
// 부모 회사 선택 (StandardDropdown 사용)
|
||||
FormFieldWrapper(
|
||||
label: "부모 회사",
|
||||
child: ShadSelect<int?>(
|
||||
placeholder: const Text('부모 회사를 선택하세요 (선택사항)'),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
if (value == null) {
|
||||
return const Text('없음 (본사)');
|
||||
}
|
||||
final company = _controller.availableParentCompanies.firstWhere(
|
||||
(c) => c.id == value,
|
||||
orElse: () => Company(id: 0, name: '알 수 없음'),
|
||||
);
|
||||
return Text(company.name);
|
||||
},
|
||||
options: [
|
||||
const ShadOption(
|
||||
value: null,
|
||||
child: Text('없음 (본사)'),
|
||||
),
|
||||
..._controller.availableParentCompanies.map((company) {
|
||||
return ShadOption(
|
||||
value: company.id,
|
||||
child: Text(company.name),
|
||||
);
|
||||
}),
|
||||
child: StandardIntDropdown<MapEntry<int, String>?>(
|
||||
label: '', // FormFieldWrapper에서 이미 라벨 표시
|
||||
isRequired: false,
|
||||
items: [
|
||||
null, // '없음 (본사)' 옵션
|
||||
..._controller.availableParentCompanies.entries,
|
||||
],
|
||||
onChanged: (value) {
|
||||
isLoading: false, // 부모회사 로딩 상태 필요시 추가
|
||||
selectedValue: _controller.selectedParentCompanyId != null
|
||||
? _controller.availableParentCompanies.entries
|
||||
.where((entry) => entry.key == _controller.selectedParentCompanyId)
|
||||
.firstOrNull
|
||||
: null,
|
||||
onChanged: (MapEntry<int, String>? selectedCompany) {
|
||||
debugPrint('🔄 부모 회사 선택: ${selectedCompany?.key}');
|
||||
setState(() {
|
||||
_controller.selectedParentCompanyId = value;
|
||||
_controller.selectedParentCompanyId = selectedCompany?.key;
|
||||
});
|
||||
debugPrint('✅ 부모 회사 설정 완료: ${_controller.selectedParentCompanyId}');
|
||||
},
|
||||
itemBuilder: (MapEntry<int, String>? company) =>
|
||||
company == null ? const Text('없음 (본사)') : Text(company.value),
|
||||
selectedItemBuilder: (MapEntry<int, String>? company) =>
|
||||
company == null ? const Text('없음 (본사)') : Text(company.value),
|
||||
idExtractor: (MapEntry<int, String>? company) => company?.key ?? -1,
|
||||
placeholder: '부모 회사를 선택하세요 (선택사항)',
|
||||
),
|
||||
),
|
||||
|
||||
@@ -315,18 +340,24 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShadInputFormField(
|
||||
controller: _controller.nameController,
|
||||
placeholder: const Text('회사명을 입력하세요'),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
return '회사명을 입력하세요';
|
||||
}
|
||||
if (value.trim().length < 2) {
|
||||
return '회사명은 2자 이상 입력하세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ShadInputFormField(
|
||||
controller: _controller.nameController,
|
||||
placeholder: const Text('회사명을 입력하세요 (저장 시 중복 검사)'),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
return '회사명을 입력하세요';
|
||||
}
|
||||
if (value.trim().length < 2) {
|
||||
return '회사명은 2자 이상 입력하세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 중복 검사 메시지 영역 (고정 높이)
|
||||
SizedBox(
|
||||
|
||||
@@ -31,7 +31,7 @@ class _CompanyListState extends State<CompanyList> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = CompanyListController();
|
||||
_controller.initialize(pageSize: 10); // 통일된 초기화 방식
|
||||
_controller.initialize(pageSize: AppConstants.companyPageSize); // 통일된 초기화 방식
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -367,7 +367,7 @@ class _CompanyListState extends State<CompanyList> {
|
||||
_buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 60),
|
||||
_buildHeaderCell('등록/수정일', flex: 2, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('비고', flex: 1, useExpanded: true, minWidth: 80),
|
||||
_buildHeaderCell('관리', flex: 0, useExpanded: false, minWidth: 80),
|
||||
_buildHeaderCell('관리', flex: 0, useExpanded: false, minWidth: 100),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -512,7 +512,7 @@ class _CompanyListState extends State<CompanyList> {
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 80,
|
||||
minWidth: 100,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
299
lib/screens/company/controllers/company_controller.dart
Normal file
299
lib/screens/company/controllers/company_controller.dart
Normal file
@@ -0,0 +1,299 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../models/company_model.dart';
|
||||
import '../../../domain/usecases/company/get_companies_usecase.dart';
|
||||
import '../../../domain/usecases/company/get_company_detail_usecase.dart';
|
||||
import '../../../domain/usecases/company/create_company_usecase.dart';
|
||||
import '../../../domain/usecases/company/update_company_usecase.dart';
|
||||
import '../../../domain/usecases/company/delete_company_usecase.dart';
|
||||
import '../../../domain/usecases/company/restore_company_usecase.dart';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
|
||||
@injectable
|
||||
class CompanyController with ChangeNotifier {
|
||||
final GetCompaniesUseCase _getCompaniesUseCase;
|
||||
final GetCompanyDetailUseCase _getCompanyDetailUseCase;
|
||||
final CreateCompanyUseCase _createCompanyUseCase;
|
||||
final UpdateCompanyUseCase _updateCompanyUseCase;
|
||||
final DeleteCompanyUseCase _deleteCompanyUseCase;
|
||||
final RestoreCompanyUseCase _restoreCompanyUseCase;
|
||||
|
||||
// 상태 관리
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
List<Company> _companies = [];
|
||||
Company? _selectedCompany;
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
int _totalPages = 0;
|
||||
int _totalItems = 0;
|
||||
final int _pageSize = AppConstants.companyPageSize;
|
||||
|
||||
// 필터
|
||||
String? _searchQuery;
|
||||
bool _includeDeleted = false;
|
||||
|
||||
CompanyController(
|
||||
this._getCompaniesUseCase,
|
||||
this._getCompanyDetailUseCase,
|
||||
this._createCompanyUseCase,
|
||||
this._updateCompanyUseCase,
|
||||
this._deleteCompanyUseCase,
|
||||
this._restoreCompanyUseCase,
|
||||
);
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasError => _error != null;
|
||||
List<Company> get companies => _companies;
|
||||
Company? get selectedCompany => _selectedCompany;
|
||||
int get currentPage => _currentPage;
|
||||
int get totalPages => _totalPages;
|
||||
int get totalItems => _totalItems;
|
||||
int get pageSize => _pageSize;
|
||||
String? get searchQuery => _searchQuery;
|
||||
bool get includeDeleted => _includeDeleted;
|
||||
|
||||
void _setLoading(bool loading) {
|
||||
_isLoading = loading;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setError(String? error) {
|
||||
_error = error;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 회사 목록 조회
|
||||
Future<void> loadCompanies({
|
||||
int page = 1,
|
||||
int perPage = AppConstants.companyPageSize,
|
||||
String? search,
|
||||
bool includeDeleted = false,
|
||||
bool refresh = false,
|
||||
}) async {
|
||||
try {
|
||||
if (refresh) {
|
||||
_companies.clear();
|
||||
_currentPage = 1;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
_setLoading(true);
|
||||
|
||||
final params = GetCompaniesParams(
|
||||
page: page,
|
||||
perPage: perPage,
|
||||
search: search,
|
||||
isActive: includeDeleted ? null : true,
|
||||
);
|
||||
|
||||
final response = await _getCompaniesUseCase(params);
|
||||
|
||||
response.fold(
|
||||
(failure) => _setError('회사 목록을 불러오는데 실패했습니다: ${failure.message}'),
|
||||
(data) {
|
||||
_companies = data.items;
|
||||
_currentPage = page;
|
||||
_totalPages = data.totalPages;
|
||||
_totalItems = data.totalElements;
|
||||
_searchQuery = search;
|
||||
_includeDeleted = includeDeleted;
|
||||
clearError();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('회사 목록을 불러오는데 실패했습니다: $e');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 회사 상세 조회
|
||||
Future<void> loadCompany(int id) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
final params = GetCompanyDetailParams(id: id);
|
||||
final response = await _getCompanyDetailUseCase(params);
|
||||
|
||||
response.fold(
|
||||
(failure) => _setError('회사 정보를 불러오는데 실패했습니다: ${failure.message}'),
|
||||
(company) {
|
||||
_selectedCompany = company;
|
||||
clearError();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('회사 정보를 불러오는데 실패했습니다: $e');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 회사 생성
|
||||
Future<bool> createCompany(Company company) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
final params = CreateCompanyParams(company: company);
|
||||
final response = await _createCompanyUseCase(params);
|
||||
|
||||
return response.fold(
|
||||
(failure) {
|
||||
_setError('회사 생성에 실패했습니다: ${failure.message}');
|
||||
return false;
|
||||
},
|
||||
(company) {
|
||||
// 목록 새로고침
|
||||
loadCompanies(refresh: true);
|
||||
clearError();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('회사 생성에 실패했습니다: $e');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 회사 수정
|
||||
Future<bool> updateCompany(int id, Company company) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
final params = UpdateCompanyParams(id: id, company: company);
|
||||
final response = await _updateCompanyUseCase(params);
|
||||
|
||||
return response.fold(
|
||||
(failure) {
|
||||
_setError('회사 수정에 실패했습니다: ${failure.message}');
|
||||
return false;
|
||||
},
|
||||
(company) {
|
||||
// 목록 새로고침
|
||||
loadCompanies(refresh: true);
|
||||
clearError();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('회사 수정에 실패했습니다: $e');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 회사 삭제 (Soft Delete)
|
||||
Future<bool> deleteCompany(int id) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
final params = DeleteCompanyParams(id: id);
|
||||
final response = await _deleteCompanyUseCase(params);
|
||||
|
||||
return response.fold(
|
||||
(failure) {
|
||||
_setError('회사 삭제에 실패했습니다: ${failure.message}');
|
||||
return false;
|
||||
},
|
||||
(_) {
|
||||
// 목록 새로고침
|
||||
loadCompanies(refresh: true);
|
||||
clearError();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('회사 삭제에 실패했습니다: $e');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 회사 복구
|
||||
Future<bool> restoreCompany(int id) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
final response = await _restoreCompanyUseCase(id);
|
||||
|
||||
return response.fold(
|
||||
(failure) {
|
||||
_setError('회사 복구에 실패했습니다: ${failure.message}');
|
||||
return false;
|
||||
},
|
||||
(company) {
|
||||
// 목록 새로고침
|
||||
loadCompanies(refresh: true);
|
||||
clearError();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('회사 복구에 실패했습니다: $e');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 페이지 변경
|
||||
Future<void> goToPage(int page) async {
|
||||
if (page < 1 || page > _totalPages || page == _currentPage) return;
|
||||
|
||||
await loadCompanies(
|
||||
page: page,
|
||||
search: _searchQuery,
|
||||
includeDeleted: _includeDeleted,
|
||||
);
|
||||
}
|
||||
|
||||
/// 검색 설정
|
||||
void setSearch(String? search) {
|
||||
_searchQuery = search;
|
||||
loadCompanies(
|
||||
search: search,
|
||||
includeDeleted: _includeDeleted,
|
||||
refresh: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// 삭제된 항목 포함 여부 토글
|
||||
void toggleIncludeDeleted() {
|
||||
_includeDeleted = !_includeDeleted;
|
||||
loadCompanies(
|
||||
search: _searchQuery,
|
||||
includeDeleted: _includeDeleted,
|
||||
refresh: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadCompanies(
|
||||
page: 1,
|
||||
search: _searchQuery,
|
||||
includeDeleted: _includeDeleted,
|
||||
refresh: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// 선택된 회사 초기화
|
||||
void clearSelectedCompany() {
|
||||
_selectedCompany = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import 'package:superport/core/errors/failures.dart';
|
||||
import 'dart:async';
|
||||
import 'branch_form_controller.dart'; // 분리된 지점 컨트롤러 import
|
||||
import 'package:superport/data/models/zipcode_dto.dart';
|
||||
import 'package:superport/data/datasources/remote/api_client.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
/// 회사 폼 컨트롤러 - 비즈니스 로직 처리
|
||||
class CompanyFormController {
|
||||
@@ -46,9 +48,9 @@ class CompanyFormController {
|
||||
// 회사 유형 선택값 (복수)
|
||||
List<CompanyType> selectedCompanyTypes = [CompanyType.customer];
|
||||
|
||||
// 부모 회사 선택
|
||||
// 부모 회사 선택 (단순화)
|
||||
int? selectedParentCompanyId;
|
||||
List<Company> availableParentCompanies = [];
|
||||
Map<int, String> availableParentCompanies = {};
|
||||
|
||||
List<String> companyNames = [];
|
||||
List<String> filteredCompanyNames = [];
|
||||
@@ -89,37 +91,65 @@ class CompanyFormController {
|
||||
_setupFocusNodes();
|
||||
_setupControllerListeners();
|
||||
// 비동기 초기화는 별도로 호출해야 함
|
||||
Future.microtask(() => _initializeAsync());
|
||||
Future.microtask(() => loadParentCompanies());
|
||||
}
|
||||
|
||||
Future<void> _initializeAsync() async {
|
||||
await _loadCompanyNames();
|
||||
// loadCompanyData는 별도로 호출됨 (company_form.dart에서)
|
||||
}
|
||||
|
||||
// 회사명 목록 로드 (자동완성용)
|
||||
Future<void> _loadCompanyNames() async {
|
||||
// 부모 회사 목록 로드 (LOOKUP COMPANIES API 직접 호출)
|
||||
Future<void> loadParentCompanies() async {
|
||||
try {
|
||||
List<Company> companies;
|
||||
|
||||
// API만 사용 (PaginatedResponse에서 items 추출)
|
||||
final response = await _companyService.getCompanies(page: 1, perPage: 1000);
|
||||
companies = response.items;
|
||||
|
||||
companyNames = companies.map((c) => c.name).toList();
|
||||
filteredCompanyNames = companyNames;
|
||||
debugPrint('📝 부모 회사 목록 로드 시작 - LOOKUP /companies API 직접 호출');
|
||||
|
||||
// 부모 회사 목록도 설정 (자기 자신은 제외)
|
||||
if (companyId != null) {
|
||||
availableParentCompanies = companies.where((c) => c.id != companyId).toList();
|
||||
// API 직접 호출 (GetIt DI 사용)
|
||||
final apiClient = GetIt.instance<ApiClient>();
|
||||
|
||||
debugPrint('📞 === LOOKUP COMPANIES API 요청 ===');
|
||||
debugPrint('📞 URL: /lookups/companies');
|
||||
|
||||
final response = await apiClient.get('/lookups/companies');
|
||||
|
||||
debugPrint('📊 === LOOKUP COMPANIES API 응답 ===');
|
||||
debugPrint('📊 Status Code: ${response.statusCode}');
|
||||
debugPrint('📊 Response Data: ${response.data}');
|
||||
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
final List<dynamic> companiesJson = response.data as List<dynamic>;
|
||||
|
||||
debugPrint('🎯 === LOOKUP COMPANIES API 성공 ===');
|
||||
debugPrint('📊 받은 회사 총 개수: ${companiesJson.length}개');
|
||||
|
||||
if (companiesJson.isNotEmpty) {
|
||||
debugPrint('📝 Lookup 회사 목록:');
|
||||
for (int i = 0; i < companiesJson.length && i < 15; i++) {
|
||||
final company = companiesJson[i];
|
||||
debugPrint(' ${i + 1}. ID: ${company['id']}, 이름: ${company['name']}');
|
||||
}
|
||||
if (companiesJson.length > 15) {
|
||||
debugPrint(' ... 외 ${companiesJson.length - 15}개 더');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 부모회사 드롭다운 구성 =====
|
||||
availableParentCompanies = {};
|
||||
for (final companyJson in companiesJson) {
|
||||
final id = companyJson['id'] as int?;
|
||||
final name = companyJson['name'] as String?;
|
||||
|
||||
if (id != null && name != null && id != companyId) {
|
||||
availableParentCompanies[id] = name;
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ 부모 회사 목록 로드 완료: ${availableParentCompanies.length}개');
|
||||
debugPrint('📝 드롭다운에 표시될 회사들: ${availableParentCompanies.values.take(5).join(", ")}${availableParentCompanies.length > 5 ? "..." : ""}');
|
||||
|
||||
} else {
|
||||
availableParentCompanies = companies;
|
||||
debugPrint('❌ Lookup Companies API 실패: 상태코드 ${response.statusCode}');
|
||||
availableParentCompanies = {};
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ 회사명 목록 로드 실패: $e');
|
||||
companyNames = [];
|
||||
filteredCompanyNames = [];
|
||||
availableParentCompanies = [];
|
||||
debugPrint('❌ 부모 회사 목록 로드 예외: $e');
|
||||
availableParentCompanies = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,17 +201,18 @@ class CompanyFormController {
|
||||
remarkController.text = company.remark ?? '';
|
||||
debugPrint('📝 비고 설정: "${remarkController.text}"');
|
||||
|
||||
// 전화번호 처리
|
||||
// 전화번호 처리 - 수정 모드에서는 전체 전화번호를 그대로 사용
|
||||
if (company.contactPhone != null && company.contactPhone!.isNotEmpty) {
|
||||
// 통합 필드를 위해 전체 전화번호를 그대로 저장
|
||||
contactPhoneController.text = company.contactPhone!;
|
||||
|
||||
// 기존 분리 로직은 참고용으로만 유지
|
||||
selectedPhonePrefix = extractPhonePrefix(
|
||||
company.contactPhone!,
|
||||
phonePrefixes,
|
||||
);
|
||||
contactPhoneController.text = extractPhoneNumberWithoutPrefix(
|
||||
company.contactPhone!,
|
||||
phonePrefixes,
|
||||
);
|
||||
debugPrint('📝 전화번호 설정: $selectedPhonePrefix-${contactPhoneController.text}');
|
||||
debugPrint('📝 전화번호 설정 (전체): ${contactPhoneController.text}');
|
||||
debugPrint('📝 전화번호 접두사 (참고): $selectedPhonePrefix');
|
||||
}
|
||||
|
||||
// 회사 유형 설정
|
||||
@@ -315,24 +346,51 @@ class CompanyFormController {
|
||||
// 회사명 중복 검사 (저장 시점에만 수행)
|
||||
Future<bool> checkDuplicateName(String name) async {
|
||||
try {
|
||||
// 수정 모드일 때는 자기 자신을 제외하고 검사
|
||||
final response = await _companyService.getCompanies(search: name);
|
||||
debugPrint('🔍 === 중복 검사 시작 (LOOKUPS API 사용) ===');
|
||||
debugPrint('🔍 검사할 회사명: "$name"');
|
||||
debugPrint('🔍 현재 회사 ID: $companyId');
|
||||
|
||||
for (final company in response.items) {
|
||||
// 정확히 일치하는 회사명이 있는지 확인 (대소문자 구분 없이)
|
||||
if (company.name.toLowerCase() == name.toLowerCase()) {
|
||||
// 수정 모드일 때는 자기 자신은 제외
|
||||
if (companyId != null && company.id == companyId) {
|
||||
continue;
|
||||
// LOOKUPS API로 전체 회사 목록 조회 (페이지네이션 없음)
|
||||
final apiClient = GetIt.instance<ApiClient>();
|
||||
final response = await apiClient.get('/lookups/companies');
|
||||
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
final List<dynamic> companiesJson = response.data as List<dynamic>;
|
||||
|
||||
debugPrint('🔍 전체 회사 수 (LOOKUPS): ${companiesJson.length}개');
|
||||
|
||||
for (final companyJson in companiesJson) {
|
||||
final id = companyJson['id'] as int?;
|
||||
final companyName = companyJson['name'] as String?;
|
||||
|
||||
if (id != null && companyName != null) {
|
||||
debugPrint('🔍 비교: "$companyName" vs "$name"');
|
||||
debugPrint(' - 회사 ID: $id');
|
||||
debugPrint(' - 소문자 비교: "${companyName.toLowerCase()}" == "${name.toLowerCase()}"');
|
||||
|
||||
// 정확히 일치하는 회사명이 있는지 확인 (대소문자 구분 없이)
|
||||
if (companyName.toLowerCase() == name.toLowerCase()) {
|
||||
// 수정 모드일 때는 자기 자신은 제외
|
||||
if (companyId != null && id == companyId) {
|
||||
debugPrint('✅ 자기 자신이므로 제외');
|
||||
continue;
|
||||
}
|
||||
debugPrint('❌ 중복 발견! 기존 회사: ID $id, 이름: "$companyName"');
|
||||
return true; // 중복 발견
|
||||
}
|
||||
}
|
||||
return true; // 중복 발견
|
||||
}
|
||||
|
||||
debugPrint('✅ 중복 없음');
|
||||
return false; // 중복 없음
|
||||
} else {
|
||||
debugPrint('❌ LOOKUPS API 호출 실패: ${response.statusCode}');
|
||||
return true; // 안전장치
|
||||
}
|
||||
return false; // 중복 없음
|
||||
} catch (e) {
|
||||
debugPrint('회사명 중복 검사 실패: $e');
|
||||
// 네트워크 오류 시 중복 없음으로 처리 (저장 진행)
|
||||
return false;
|
||||
debugPrint('❌ 회사명 중복 검사 실패: $e');
|
||||
// 네트워크 오류 시 중복 있음으로 처리 (안전장치)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,10 +436,7 @@ class CompanyFormController {
|
||||
address: companyAddress,
|
||||
contactName: contactNameController.text.trim(),
|
||||
contactPosition: contactPositionController.text.trim(),
|
||||
contactPhone: getFullPhoneNumber(
|
||||
selectedPhonePrefix,
|
||||
contactPhoneController.text.trim(),
|
||||
),
|
||||
contactPhone: contactPhoneController.text.trim(),
|
||||
contactEmail: contactEmailController.text.trim(),
|
||||
remark: remarkController.text.trim(),
|
||||
branches:
|
||||
@@ -399,6 +454,11 @@ class CompanyFormController {
|
||||
Company savedCompany;
|
||||
if (companyId == null) {
|
||||
// 새 회사 생성
|
||||
debugPrint('💾 회사 생성 요청 데이터:');
|
||||
debugPrint(' - 회사명: ${company.name}');
|
||||
debugPrint(' - 이메일: ${company.contactEmail}');
|
||||
debugPrint(' - 부모회사ID: ${company.parentCompanyId}');
|
||||
|
||||
savedCompany = await _companyService.createCompany(company);
|
||||
debugPrint(
|
||||
'Company created successfully with ID: ${savedCompany.id}',
|
||||
@@ -423,6 +483,11 @@ class CompanyFormController {
|
||||
}
|
||||
} else {
|
||||
// 기존 회사 수정
|
||||
debugPrint('💾 회사 수정 요청 데이터:');
|
||||
debugPrint(' - 회사명: ${company.name}');
|
||||
debugPrint(' - 이메일: ${company.contactEmail}');
|
||||
debugPrint(' - 부모회사ID: ${company.parentCompanyId}');
|
||||
|
||||
savedCompany = await _companyService.updateCompany(
|
||||
companyId!,
|
||||
company,
|
||||
@@ -481,11 +546,55 @@ class CompanyFormController {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} on DioException catch (e) {
|
||||
debugPrint('❌ === COMPANY SAVE DIO 에러 ===');
|
||||
debugPrint('❌ 에러 타입: ${e.type}');
|
||||
debugPrint('❌ 상태 코드: ${e.response?.statusCode}');
|
||||
debugPrint('❌ 에러 메시지: ${e.message}');
|
||||
debugPrint('❌ 응답 데이터 타입: ${e.response?.data.runtimeType}');
|
||||
debugPrint('❌ 응답 데이터: ${e.response?.data}');
|
||||
debugPrint('❌ 응답 헤더: ${e.response?.headers}');
|
||||
|
||||
if (e.response?.statusCode == 409) {
|
||||
// 409 Conflict - 중복 데이터
|
||||
final responseData = e.response?.data;
|
||||
String errorMessage = '중복된 데이터가 있습니다';
|
||||
|
||||
debugPrint('🔍 === 409 CONFLICT 상세 분석 ===');
|
||||
debugPrint('🔍 응답 데이터 분석:');
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
debugPrint('🔍 Map 형태 응답:');
|
||||
responseData.forEach((key, value) {
|
||||
debugPrint(' - $key: $value');
|
||||
});
|
||||
|
||||
// 백엔드 응답 형식에 맞게 메시지 추출
|
||||
// 백엔드: {"error": {"code": 409, "message": "...", "type": "DUPLICATE_ERROR"}}
|
||||
errorMessage = responseData['error']?['message'] ??
|
||||
responseData['message'] ??
|
||||
responseData['detail'] ??
|
||||
responseData['msg'] ??
|
||||
errorMessage;
|
||||
} else if (responseData is String) {
|
||||
debugPrint('🔍 String 형태 응답: $responseData');
|
||||
errorMessage = responseData;
|
||||
} else {
|
||||
debugPrint('🔍 기타 형태 응답: ${responseData.toString()}');
|
||||
}
|
||||
|
||||
debugPrint('🔄 최종 에러 메시지: $errorMessage');
|
||||
// 이 오류는 UI에서 처리하도록 다시 throw
|
||||
throw Exception('CONFLICT: $errorMessage');
|
||||
}
|
||||
|
||||
debugPrint('❌ 회사 저장 실패 (DioException): ${e.message}');
|
||||
return false;
|
||||
} on Failure catch (e) {
|
||||
debugPrint('Failed to save company: ${e.message}');
|
||||
debugPrint('❌ 회사 저장 실패 (Failure): ${e.message}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('Unexpected error saving company: $e');
|
||||
debugPrint('❌ 예상치 못한 오류: $e');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
|
||||
209
lib/screens/company/widgets/company_restore_dialog.dart
Normal file
209
lib/screens/company/widgets/company_restore_dialog.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../common/theme_shadcn.dart';
|
||||
import '../../../data/models/company/company_dto.dart';
|
||||
import '../../../injection_container.dart';
|
||||
import '../controllers/company_controller.dart';
|
||||
|
||||
/// 회사 복구 확인 다이얼로그
|
||||
class CompanyRestoreDialog extends StatefulWidget {
|
||||
final CompanyDto company;
|
||||
final VoidCallback? onRestored;
|
||||
|
||||
const CompanyRestoreDialog({
|
||||
super.key,
|
||||
required this.company,
|
||||
this.onRestored,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CompanyRestoreDialog> createState() => _CompanyRestoreDialogState();
|
||||
}
|
||||
|
||||
class _CompanyRestoreDialogState extends State<CompanyRestoreDialog> {
|
||||
late final CompanyController _controller;
|
||||
bool _isRestoring = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = sl<CompanyController>();
|
||||
}
|
||||
|
||||
Future<void> _restore() async {
|
||||
setState(() {
|
||||
_isRestoring = true;
|
||||
});
|
||||
|
||||
final success = await _controller.restoreCompany(widget.company.id!);
|
||||
|
||||
if (mounted) {
|
||||
if (success) {
|
||||
Navigator.of(context).pop(true);
|
||||
if (widget.onRestored != null) {
|
||||
widget.onRestored!();
|
||||
}
|
||||
|
||||
// 성공 메시지
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast(
|
||||
title: const Text('복구 완료'),
|
||||
description: Text('${widget.company.name} 회사가 복구되었습니다.'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
_isRestoring = false;
|
||||
});
|
||||
|
||||
// 실패 메시지
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast.destructive(
|
||||
title: const Text('복구 실패'),
|
||||
description: Text(_controller.error ?? '회사 복구에 실패했습니다.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShadDialog(
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 헤더
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.restore, color: Colors.green, size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text('회사 복구', style: ShadcnTheme.headingH3),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 복구 정보
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.green.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'다음 회사를 복구하시겠습니까?',
|
||||
style: ShadcnTheme.bodyLarge.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('회사명', widget.company.name),
|
||||
_buildInfoRow('담당자', widget.company.contactName),
|
||||
_buildInfoRow('연락처', widget.company.contactPhone),
|
||||
if (widget.company.contactEmail.isNotEmpty)
|
||||
_buildInfoRow('이메일', widget.company.contactEmail),
|
||||
_buildInfoRow('주소', widget.company.address),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
'복구된 회사는 다시 활성 상태로 변경됩니다.',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.foregroundMuted,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 버튼들
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadButton.outline(
|
||||
onPressed: _isRestoring ? null : () => Navigator.of(context).pop(false),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ShadButton(
|
||||
onPressed: _isRestoring ? null : _restore,
|
||||
child: _isRestoring
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('복구 중...'),
|
||||
],
|
||||
)
|
||||
: const Text('복구'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(
|
||||
'$label:',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.foregroundMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 회사 복구 다이얼로그 표시 유틸리티
|
||||
Future<bool?> showCompanyRestoreDialog(
|
||||
BuildContext context, {
|
||||
required CompanyDto company,
|
||||
VoidCallback? onRestored,
|
||||
}) async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => CompanyRestoreDialog(
|
||||
company: company,
|
||||
onRestored: onRestored,
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user