refactor: 회사 폼 UI 개선 및 코드 정리
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled

- 담당자 연락처 필드를 드롭다운 + 입력 방식으로 분리
- 사용자 폼과 동일한 전화번호 UI 패턴 적용
- 미사용 위젯 파일 4개 정리 (branch_card, contact_info_* 등)
- 파일명 통일성 확보 (branch_edit_screen → branch_form, company_form_simplified → company_form)
- 네이밍 일관성 개선으로 유지보수성 향상
This commit is contained in:
JiWoong Sul
2025-08-18 17:57:16 +09:00
parent 93bceb8a6c
commit 6d745051b5
37 changed files with 2743 additions and 2446 deletions

View File

@@ -0,0 +1,167 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:superport/models/company_model.dart';
import 'package:superport/models/address_model.dart';
import 'package:superport/services/company_service.dart';
import 'package:superport/core/utils/error_handler.dart';
/// 지점 정보 수정 컨트롤러 (단일 지점 전용)
/// 지점의 기본 정보만 수정할 수 있도록 단순화
class BranchEditFormController extends ChangeNotifier {
final CompanyService _companyService = GetIt.instance<CompanyService>();
// 식별 정보
final int companyId;
final int branchId;
final String parentCompanyName;
// 폼 관련
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
// 텍스트 컨트롤러들
final TextEditingController nameController = TextEditingController();
final TextEditingController addressController = TextEditingController();
final TextEditingController managerNameController = TextEditingController();
final TextEditingController managerPhoneController = TextEditingController();
final TextEditingController remarkController = TextEditingController();
// 상태 관리
bool _isLoading = false;
String? _error;
Branch? _originalBranch;
// Getters
bool get isLoading => _isLoading;
String? get error => _error;
Branch? get originalBranch => _originalBranch;
BranchEditFormController({
required this.companyId,
required this.branchId,
required this.parentCompanyName,
});
@override
void dispose() {
nameController.dispose();
addressController.dispose();
managerNameController.dispose();
managerPhoneController.dispose();
remarkController.dispose();
super.dispose();
}
/// 지점 데이터 로드
Future<void> loadBranchData() async {
_setLoading(true);
_clearError();
try {
final branch = await ErrorHandler.handleApiCall(
() => _companyService.getBranchDetail(companyId, branchId),
onError: (failure) {
throw failure;
},
);
if (branch != null) {
_originalBranch = branch;
_populateForm(branch);
} else {
_setError('지점 데이터를 불러올 수 없습니다');
}
} catch (e) {
_setError('지점 정보 로드 실패: ${e.toString()}');
} finally {
_setLoading(false);
}
}
/// 폼에 데이터 설정
void _populateForm(Branch branch) {
nameController.text = branch.name;
addressController.text = branch.address.toString();
managerNameController.text = branch.contactName ?? '';
managerPhoneController.text = branch.contactPhone ?? '';
remarkController.text = branch.remark ?? '';
}
/// 지점 정보 저장
Future<bool> saveBranch() async {
if (!formKey.currentState!.validate()) {
return false;
}
_setLoading(true);
_clearError();
try {
// Branch 객체 생성
final updatedBranch = Branch(
id: branchId,
companyId: companyId,
name: nameController.text.trim(),
address: Address.fromFullAddress(addressController.text.trim()),
contactName: managerNameController.text.trim().isEmpty
? null
: managerNameController.text.trim(),
contactPhone: managerPhoneController.text.trim().isEmpty
? null
: managerPhoneController.text.trim(),
remark: remarkController.text.trim().isEmpty
? null
: remarkController.text.trim(),
);
// API 호출
await ErrorHandler.handleApiCall(
() => _companyService.updateBranch(companyId, branchId, updatedBranch),
onError: (failure) {
throw failure;
},
);
return true;
} catch (e) {
_setError('지점 저장 실패: ${e.toString()}');
return false;
} finally {
_setLoading(false);
}
}
/// 입력 데이터 유효성 검증
bool hasChanges() {
if (_originalBranch == null) return false;
return nameController.text.trim() != _originalBranch!.name ||
addressController.text.trim() != _originalBranch!.address.toString() ||
managerNameController.text.trim() != (_originalBranch!.contactName ?? '') ||
managerPhoneController.text.trim() != (_originalBranch!.contactPhone ?? '') ||
remarkController.text.trim() != (_originalBranch!.remark ?? '');
}
/// 폼 리셋
void resetForm() {
if (_originalBranch != null) {
_populateForm(_originalBranch!);
notifyListeners();
}
}
// Private helper methods
void _setLoading(bool loading) {
_isLoading = loading;
notifyListeners();
}
void _setError(String error) {
_error = error;
notifyListeners();
}
void _clearError() {
_error = null;
notifyListeners();
}
}

View File

@@ -1,111 +1,90 @@
import 'package:flutter/material.dart';
import 'package:superport/models/address_model.dart';
import 'package:superport/models/company_model.dart';
import 'package:superport/utils/phone_utils.dart';
import 'package:superport/models/address_model.dart';
/// 지점(Branch) 폼 컨트롤러
///
/// 각 지점의 상태, 컨트롤러, 포커스, 드롭다운, 전화번호 등 관리를 담당
/// 회사 폼에서 사용되는 지점 관리 컨트롤러
/// 각 지점의 정보를 개별적으로 관리
class BranchFormController {
// 지점 데이터
Branch branch;
// 입력 컨트롤러
final TextEditingController nameController;
final TextEditingController contactNameController;
final TextEditingController contactPositionController;
final TextEditingController contactPhoneController;
final TextEditingController contactEmailController;
final TextEditingController remarkController;
// 포커스 노드
final FocusNode focusNode;
// 카드 키(위젯 식별용)
final GlobalKey cardKey;
// 직책 드롭다운 상태
final ValueNotifier<bool> positionDropdownNotifier;
// 전화번호 접두사
String selectedPhonePrefix;
// 직책 목록(공통 상수로 관리 권장)
Branch _branch;
final List<String> positions;
// 전화번호 접두사 목록(공통 상수로 관리 권장)
final List<String> phonePrefixes;
// 컨트롤러들
final TextEditingController nameController = TextEditingController();
final TextEditingController contactNameController = TextEditingController();
final TextEditingController contactPositionController = TextEditingController();
final TextEditingController contactPhoneController = TextEditingController();
final TextEditingController contactEmailController = TextEditingController();
final TextEditingController remarkController = TextEditingController();
final FocusNode focusNode = FocusNode();
// 전화번호 관련
String selectedPhonePrefix = '010';
BranchFormController({
required this.branch,
required Branch branch,
required this.positions,
required this.phonePrefixes,
}) : nameController = TextEditingController(text: branch.name),
contactNameController = TextEditingController(
text: branch.contactName ?? '',
),
contactPositionController = TextEditingController(
text: branch.contactPosition ?? '',
),
contactPhoneController = TextEditingController(
text: PhoneUtils.extractPhoneNumberWithoutPrefix(
branch.contactPhone ?? '',
phonePrefixes,
),
),
contactEmailController = TextEditingController(
text: branch.contactEmail ?? '',
),
remarkController = TextEditingController(text: branch.remark ?? ''),
focusNode = FocusNode(),
cardKey = GlobalKey(),
positionDropdownNotifier = ValueNotifier<bool>(false),
selectedPhonePrefix = PhoneUtils.extractPhonePrefix(
branch.contactPhone ?? '',
phonePrefixes,
);
}) : _branch = branch {
// 초기값 설정
nameController.text = branch.name;
contactNameController.text = branch.contactName ?? '';
contactPositionController.text = branch.contactPosition ?? '';
contactPhoneController.text = branch.contactPhone ?? '';
contactEmailController.text = branch.contactEmail ?? '';
remarkController.text = branch.remark ?? '';
// 전화번호 접두사 추출
if (branch.contactPhone != null && branch.contactPhone!.isNotEmpty) {
for (String prefix in phonePrefixes) {
if (branch.contactPhone!.startsWith(prefix)) {
selectedPhonePrefix = prefix;
contactPhoneController.text = branch.contactPhone!.substring(prefix.length);
break;
}
}
}
}
/// Branch 객체 getter
Branch get branch => _branch.copyWith(
name: nameController.text.trim(),
contactName: contactNameController.text.trim().isEmpty ? null : contactNameController.text.trim(),
contactPosition: contactPositionController.text.trim().isEmpty ? null : contactPositionController.text.trim(),
contactPhone: contactPhoneController.text.trim().isEmpty ? null : '$selectedPhonePrefix${contactPhoneController.text.trim()}',
contactEmail: contactEmailController.text.trim().isEmpty ? null : contactEmailController.text.trim(),
remark: remarkController.text.trim().isEmpty ? null : remarkController.text.trim(),
);
/// 주소 업데이트
void updateAddress(Address address) {
branch = branch.copyWith(address: address);
_branch = _branch.copyWith(address: address);
}
/// 필드별 값 업데이트
void updateField(String fieldName, String value) {
switch (fieldName) {
/// 필드 업데이트
void updateField(String field, String value) {
switch (field) {
case 'name':
branch = branch.copyWith(name: value);
nameController.text = value;
break;
case 'contactName':
branch = branch.copyWith(contactName: value);
contactNameController.text = value;
break;
case 'contactPosition':
branch = branch.copyWith(contactPosition: value);
contactPositionController.text = value;
break;
case 'contactPhone':
branch = branch.copyWith(
contactPhone: PhoneUtils.getFullPhoneNumber(
selectedPhonePrefix,
value,
),
);
contactPhoneController.text = value;
break;
case 'contactEmail':
branch = branch.copyWith(contactEmail: value);
contactEmailController.text = value;
break;
case 'remark':
branch = branch.copyWith(remark: value);
remarkController.text = value;
break;
}
}
/// 전화번호 접두사 변경
void updatePhonePrefix(String prefix) {
selectedPhonePrefix = prefix;
branch = branch.copyWith(
contactPhone: PhoneUtils.getFullPhoneNumber(
prefix,
contactPhoneController.text,
),
);
}
/// 리소스 해제
void dispose() {
nameController.dispose();
@@ -115,7 +94,5 @@ class BranchFormController {
contactEmailController.dispose();
remarkController.dispose();
focusNode.dispose();
positionDropdownNotifier.dispose();
// cardKey는 위젯에서 자동 관리
}
}
}

View File

@@ -15,6 +15,7 @@ import 'package:superport/models/company_model.dart';
// import 'package:superport/services/mock_data_service.dart'; // Mock 서비스 제거
import 'package:superport/services/company_service.dart';
import 'package:superport/core/errors/failures.dart';
import 'package:superport/utils/phone_utils.dart';
import 'dart:async';
import 'branch_form_controller.dart'; // 분리된 지점 컨트롤러 import
@@ -109,7 +110,10 @@ class CompanyFormController {
// 회사 데이터 로드 (수정 모드)
Future<void> loadCompanyData() async {
if (companyId == null) return;
if (companyId == null) {
debugPrint('❌ companyId가 null입니다');
return;
}
debugPrint('📝 loadCompanyData 시작 - ID: $companyId');
@@ -119,12 +123,18 @@ class CompanyFormController {
if (_useApi) {
debugPrint('📝 API에서 회사 정보 로드 중...');
company = await _companyService.getCompanyDetail(companyId!);
debugPrint('📝 API 응답 받음: ${company != null ? "성공" : "null"}');
} else {
debugPrint('📝 API만 사용 가능');
throw Exception('API를 통해만 데이터를 로드할 수 있습니다');
}
debugPrint('📝 로드된 회사: $company');
debugPrint('📝 로드된 회사 정보:');
debugPrint(' - ID: ${company?.id}');
debugPrint(' - 이름: ${company?.name}');
debugPrint(' - 담당자: ${company?.contactName}');
debugPrint(' - 연락처: ${company?.contactPhone}');
debugPrint(' - 이메일: ${company?.contactEmail}');
if (company != null) {
// 폼 필드에 데이터 설정
@@ -157,10 +167,12 @@ class CompanyFormController {
company.contactPhone!,
phonePrefixes,
);
debugPrint('📝 전화번호 설정: $selectedPhonePrefix-${contactPhoneController.text}');
}
// 회사 유형 설정
selectedCompanyTypes = company.companyTypes;
selectedCompanyTypes = List.from(company.companyTypes);
debugPrint('📝 회사 유형 설정: $selectedCompanyTypes');
// 지점 정보 설정
if (company.branches != null && company.branches!.isNotEmpty) {
@@ -174,16 +186,33 @@ class CompanyFormController {
),
);
}
debugPrint('📝 지점 설정 완료: ${branchControllers.length}');
}
debugPrint('📝 폼 필드 설정 완료:');
debugPrint(' - 회사명: ${nameController.text}');
debugPrint(' - 담당자: ${contactNameController.text}');
debugPrint(' - 이메일: ${contactEmailController.text}');
debugPrint(' - 회사명: "${nameController.text}"');
debugPrint(' - 담당자: "${contactNameController.text}"');
debugPrint(' - 이메일: "${contactEmailController.text}"');
debugPrint(' - 전화번호: "$selectedPhonePrefix-${contactPhoneController.text}"');
debugPrint(' - 지점 수: ${branchControllers.length}');
debugPrint(' - 회사 유형: $selectedCompanyTypes');
// 강제로 TextEditingController 리스너 트리거
nameController.notifyListeners();
contactNameController.notifyListeners();
contactPositionController.notifyListeners();
contactEmailController.notifyListeners();
contactPhoneController.notifyListeners();
remarkController.notifyListeners();
debugPrint('✅ 폼 데이터 로드 완료');
} else {
debugPrint('❌ 회사 정보가 null입니다');
}
} catch (e) {
} catch (e, stackTrace) {
debugPrint('❌ 회사 정보 로드 실패: $e');
debugPrint('❌ 스택 트레이스: $stackTrace');
rethrow;
}
}
@@ -325,6 +354,8 @@ class CompanyFormController {
? null
: branchControllers.map((c) => c.branch).toList(),
companyTypes: List.from(selectedCompanyTypes), // 복수 유형 저장
isPartner: selectedCompanyTypes.contains(CompanyType.partner),
isCustomer: selectedCompanyTypes.contains(CompanyType.customer),
);
if (_useApi) {

View File

@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
import 'package:superport/models/company_model.dart';
import 'package:superport/models/company_item_model.dart';
import 'package:superport/services/company_service.dart';
import 'package:superport/core/utils/error_handler.dart';
import 'package:superport/core/controllers/base_list_controller.dart';
@@ -8,7 +9,8 @@ import 'package:superport/data/models/common/pagination_params.dart';
/// 회사 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
/// BaseListController를 상속받아 공통 기능을 재사용
class CompanyListController extends BaseListController<Company> {
/// CompanyItem 모델을 사용하여 회사와 지점을 통합 관리
class CompanyListController extends BaseListController<CompanyItem> {
late final CompanyService _companyService;
// 추가 상태 관리
@@ -20,8 +22,14 @@ class CompanyListController extends BaseListController<Company> {
bool _includeInactive = false; // 비활성 회사 포함 여부
// Getters
List<Company> get companies => items;
List<Company> get filteredCompanies => items;
List<CompanyItem> get companyItems => items;
List<CompanyItem> get filteredCompanyItems => items;
// 호환성을 위한 기존 getter (deprecated, 사용하지 말 것)
@deprecated
List<Company> get companies => items.where((item) => !item.isBranch).map((item) => item.company!).toList();
@deprecated
List<Company> get filteredCompanies => companies;
bool? get isActiveFilter => _isActiveFilter;
CompanyType? get typeFilter => _typeFilter;
bool get includeInactive => _includeInactive;
@@ -46,17 +54,16 @@ class CompanyListController extends BaseListController<Company> {
}
@override
Future<PagedResult<Company>> fetchData({
Future<PagedResult<CompanyItem>> fetchData({
required PaginationParams params,
Map<String, dynamic>? additionalFilters,
}) async {
// API 호출 - 회사 목록 조회 (PaginatedResponse 반환)
// API 호출 - 회사 목록 조회 (모든 필드 포함)
final response = await ErrorHandler.handleApiCall(
() => _companyService.getCompanies(
page: params.page,
perPage: params.perPage,
search: params.search,
isActive: _isActiveFilter,
includeInactive: _includeInactive,
),
onError: (failure) {
@@ -78,7 +85,10 @@ class CompanyListController extends BaseListController<Company> {
);
}
// PaginatedResponse를 PagedResult로 변환
// Company 리스트를 CompanyItem 리스트로 변환 (본사만, 지점은 제외)
final companyItems = response.items.map((company) => CompanyItem.headquarters(company)).toList();
// 서버에서 이미 페이지네이션 및 필터링이 완료된 데이터 사용
final meta = PaginationMeta(
currentPage: response.page,
perPage: response.size,
@@ -88,17 +98,40 @@ class CompanyListController extends BaseListController<Company> {
hasPrevious: !response.first,
);
return PagedResult(items: response.items, meta: meta);
return PagedResult(items: companyItems, meta: meta);
}
// 더 이상 사용하지 않는 메서드 - getCompanies() API는 지점 정보를 포함하지 않음
// /// Company 리스트를 CompanyItem 리스트로 확장 (본사 + 지점)
// List<CompanyItem> _expandCompaniesAndBranches(List<Company> companies) {
// final List<CompanyItem> items = [];
//
// for (final company in companies) {
// // 1. 본사 추가
// items.add(CompanyItem.headquarters(company));
//
// // 2. 지점들 추가
// if (company.branches != null && company.branches!.isNotEmpty) {
// for (final branch in company.branches!) {
// items.add(CompanyItem.branch(branch, company.name, company.id!));
// }
// }
// }
//
// return items;
// }
@override
bool filterItem(Company item, String query) {
bool filterItem(CompanyItem item, String query) {
final q = query.toLowerCase();
return item.name.toLowerCase().contains(q) ||
item.displayName.toLowerCase().contains(q) ||
(item.contactPhone?.toLowerCase().contains(q) ?? false) ||
(item.contactEmail?.toLowerCase().contains(q) ?? false) ||
(item.companyTypes.any((type) => type.name.toLowerCase().contains(q))) ||
(item.address.toString().toLowerCase().contains(q));
(item.address.toLowerCase().contains(q)) ||
(item.contactName?.toLowerCase().contains(q) ?? false) ||
(item.parentCompanyName?.toLowerCase().contains(q) ?? false);
}
// 회사 선택/선택 해제
@@ -157,7 +190,45 @@ class CompanyListController extends BaseListController<Company> {
},
);
updateItemLocally(company, (c) => c.id == company.id);
// CompanyItem에서 해당 회사 업데이트
// TODO: 지역적 업데이트 대신 전체 새로고침 사용
await refresh();
}
// 지점 추가
Future<void> addBranch(int companyId, Branch branch) async {
await ErrorHandler.handleApiCall<void>(
() => _companyService.createBranch(companyId, branch),
onError: (failure) {
throw failure;
},
);
await refresh();
}
// 지점 수정
Future<void> updateBranch(int companyId, int branchId, Branch branch) async {
await ErrorHandler.handleApiCall<void>(
() => _companyService.updateBranch(companyId, branchId, branch),
onError: (failure) {
throw failure;
},
);
await refresh();
}
// 지점 삭제
Future<void> deleteBranch(int companyId, int branchId) async {
await ErrorHandler.handleApiCall<void>(
() => _companyService.deleteBranch(companyId, branchId),
onError: (failure) {
throw failure;
},
);
await refresh();
}
// 회사 삭제