/// 회사 폼 컨트롤러 /// /// 회사 폼 화면의 비즈니스 로직을 담당하는 컨트롤러 클래스 /// 주요 기능: /// - 회사 데이터 로드 및 저장 /// - 자동완성 처리 /// - 지점 정보 관리 (추가, 삭제, 수정) /// - 전화번호 처리 /// - 중복 회사명 체크 /// - 회사 유형 관리 library; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:superport/models/address_model.dart'; 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 'dart:async'; import 'branch_form_controller.dart'; // 분리된 지점 컨트롤러 import import 'package:superport/data/models/zipcode_dto.dart'; import 'package:superport/data/repositories/zipcode_repository.dart'; /// 회사 폼 컨트롤러 - 비즈니스 로직 처리 class CompanyFormController { // final MockDataService? dataService; // Mock 서비스 제거 final CompanyService _companyService = GetIt.instance(); final int? companyId; // Feature flag for API usage final bool _useApi; final TextEditingController nameController = TextEditingController(); Address companyAddress = const Address(); final TextEditingController zipcodeController = TextEditingController(); ZipcodeDto? selectedZipcode; final TextEditingController contactNameController = TextEditingController(); final TextEditingController contactPositionController = TextEditingController(); final TextEditingController contactPhoneController = TextEditingController(); final TextEditingController contactEmailController = TextEditingController(); final TextEditingController remarkController = TextEditingController(); final FocusNode nameFocusNode = FocusNode(); final GlobalKey formKey = GlobalKey(); final ScrollController scrollController = ScrollController(); // 회사 유형 선택값 (복수) List selectedCompanyTypes = [CompanyType.customer]; // 부모 회사 선택 int? selectedParentCompanyId; List availableParentCompanies = []; List companyNames = []; List filteredCompanyNames = []; bool showCompanyNameDropdown = false; // 분리된 BranchFormController 리스트로 관리 List branchControllers = []; // 직책 목록 및 전화번호 접두사 목록(공통 상수) final List positions = [ '대표이사', '사장', '부사장', '전무', '상무', '이사', '부장', '차장', '팀장', '과장', '대리', '사원', '주임', '기타', ]; final List phonePrefixes = getCommonPhonePrefixes(); String selectedPhonePrefix = '010'; final List phonePrefixesForMain = getCommonPhonePrefixes(); ValueNotifier showPositionDropdownNotifier = ValueNotifier(false); Timer? debounceTimer; bool preventAutoFocus = false; final Map isNewlyAddedBranch = {}; CompanyFormController({this.companyId, bool useApi = true}) : _useApi = useApi { _setupFocusNodes(); _setupControllerListeners(); // 비동기 초기화는 별도로 호출해야 함 Future.microtask(() => _initializeAsync()); } Future _initializeAsync() async { await _loadCompanyNames(); // loadCompanyData는 별도로 호출됨 (company_form.dart에서) } // 회사명 목록 로드 (자동완성용) Future _loadCompanyNames() async { try { List 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; // 부모 회사 목록도 설정 (자기 자신은 제외) if (companyId != null) { availableParentCompanies = companies.where((c) => c.id != companyId).toList(); } else { availableParentCompanies = companies; } } catch (e) { debugPrint('❌ 회사명 목록 로드 실패: $e'); companyNames = []; filteredCompanyNames = []; availableParentCompanies = []; } } // 회사 데이터 로드 (수정 모드) Future loadCompanyData() async { if (companyId == null) { debugPrint('❌ companyId가 null입니다'); return; } debugPrint('📝 loadCompanyData 시작 - ID: $companyId'); try { Company? company; if (_useApi) { debugPrint('📝 API에서 회사 정보 로드 중...'); company = await _companyService.getCompanyDetail(companyId!); debugPrint('📝 API 응답 받음: ${company != null ? "성공" : "null"}'); } else { debugPrint('📝 API만 사용 가능'); throw Exception('API를 통해만 데이터를 로드할 수 있습니다'); } debugPrint('📝 로드된 회사 정보:'); debugPrint(' - ID: ${company.id}'); debugPrint(' - 이름: ${company.name}'); debugPrint(' - 담당자: ${company.contactName}'); debugPrint(' - 연락처: ${company.contactPhone}'); debugPrint(' - 이메일: ${company.contactEmail}'); // 폼 필드에 데이터 설정 debugPrint('📝 회사명 설정 전: "${nameController.text}"'); nameController.text = company.name; debugPrint('📝 회사명 설정 후: "${nameController.text}"'); companyAddress = company.address; debugPrint('📝 주소 설정: $companyAddress'); contactNameController.text = company.contactName ?? ''; debugPrint('📝 담당자명 설정: "${contactNameController.text}"'); contactPositionController.text = company.contactPosition ?? ''; debugPrint('📝 직급 설정: "${contactPositionController.text}"'); contactEmailController.text = company.contactEmail ?? ''; debugPrint('📝 이메일 설정: "${contactEmailController.text}"'); remarkController.text = company.remark ?? ''; debugPrint('📝 비고 설정: "${remarkController.text}"'); // 전화번호 처리 if (company.contactPhone != null && company.contactPhone!.isNotEmpty) { selectedPhonePrefix = extractPhonePrefix( company.contactPhone!, phonePrefixes, ); contactPhoneController.text = extractPhoneNumberWithoutPrefix( company.contactPhone!, phonePrefixes, ); debugPrint('📝 전화번호 설정: $selectedPhonePrefix-${contactPhoneController.text}'); } // 회사 유형 설정 selectedCompanyTypes = List.from(company.companyTypes); debugPrint('📝 회사 유형 설정: $selectedCompanyTypes'); // 부모 회사 설정 selectedParentCompanyId = company.parentCompanyId; debugPrint('📝 부모 회사 설정: $selectedParentCompanyId'); // 지점 정보 설정 if (company.branches != null && company.branches!.isNotEmpty) { branchControllers.clear(); for (final branch in company.branches!) { branchControllers.add( BranchFormController( branch: branch, positions: positions, phonePrefixes: phonePrefixes, ), ); } debugPrint('📝 지점 설정 완료: ${branchControllers.length}개'); } debugPrint('📝 폼 필드 설정 완료:'); debugPrint(' - 회사명: "${nameController.text}"'); debugPrint(' - 담당자: "${contactNameController.text}"'); debugPrint(' - 이메일: "${contactEmailController.text}"'); debugPrint(' - 전화번호: "$selectedPhonePrefix-${contactPhoneController.text}"'); debugPrint(' - 지점 수: ${branchControllers.length}'); debugPrint(' - 회사 유형: $selectedCompanyTypes'); // TextEditingController는 text 설정 시 자동으로 리스너 트리거됨 // notifyListeners() 직접 호출은 불필요하고 부적절함 debugPrint('✅ 폼 데이터 로드 완료'); } catch (e, stackTrace) { debugPrint('❌ 회사 정보 로드 실패: $e'); debugPrint('❌ 스택 트레이스: $stackTrace'); rethrow; } } void dispose() { debounceTimer?.cancel(); scrollController.dispose(); showPositionDropdownNotifier.dispose(); for (final branchController in branchControllers) { branchController.dispose(); } nameController.dispose(); contactNameController.dispose(); contactPositionController.dispose(); contactPhoneController.dispose(); contactEmailController.dispose(); remarkController.dispose(); nameFocusNode.dispose(); } void _setupFocusNodes() { nameFocusNode.addListener(() { if (nameFocusNode.hasFocus) { showCompanyNameDropdown = filteredCompanyNames.isNotEmpty; } else { showCompanyNameDropdown = false; } }); } void _setupControllerListeners() { nameController.addListener(_onCompanyNameTextChanged); } void updateCompanyAddress(Address address) { companyAddress = address; } void updateBranchAddress(int index, Address address) { if (index >= 0 && index < branchControllers.length) { branchControllers[index].updateAddress(address); } } void _onCompanyNameTextChanged() { final query = nameController.text.toLowerCase(); if (query.isEmpty) { filteredCompanyNames = List.from(companyNames); } else { filteredCompanyNames = companyNames .where((name) => name.toLowerCase().contains(query)) .toList(); } showCompanyNameDropdown = nameFocusNode.hasFocus && filteredCompanyNames.isNotEmpty; } void selectCompanyName(String name) { nameController.text = name; showCompanyNameDropdown = false; } // 지점 추가 void addBranch() { final newBranch = Branch( companyId: companyId ?? 0, name: '지점 {branchControllers.length + 1}', address: const Address(), ); branchControllers.add( BranchFormController( branch: newBranch, positions: positions, phonePrefixes: phonePrefixes, ), ); isNewlyAddedBranch[branchControllers.length - 1] = true; } // 지점 삭제 void removeBranch(int index) { if (index < 0 || index >= branchControllers.length) return; branchControllers[index].dispose(); branchControllers.removeAt(index); isNewlyAddedBranch.remove(index); } // 회사명 중복 검사 (저장 시점에만 수행) Future checkDuplicateName(String name) async { try { // 수정 모드일 때는 자기 자신을 제외하고 검사 final response = await _companyService.getCompanies(search: name); for (final company in response.items) { // 정확히 일치하는 회사명이 있는지 확인 (대소문자 구분 없이) if (company.name.toLowerCase() == name.toLowerCase()) { // 수정 모드일 때는 자기 자신은 제외 if (companyId != null && company.id == companyId) { continue; } return true; // 중복 발견 } } return false; // 중복 없음 } catch (e) { debugPrint('회사명 중복 검사 실패: $e'); // 네트워크 오류 시 중복 없음으로 처리 (저장 진행) return false; } } @Deprecated('checkDuplicateName을 사용하세요') Future checkDuplicateCompany() async { if (companyId != null) return null; // 수정 모드에서는 체크하지 않음 final name = nameController.text.trim(); if (name.isEmpty) return null; if (_useApi) { try { // 회사명 목록을 조회하여 중복 확인 final response = await _companyService.getCompanies(search: name); // 정확히 일치하는 회사명이 있는지 확인 for (final company in response.items) { if (company.name.toLowerCase() == name.toLowerCase()) { return company; } } return null; } on Failure catch (e) { debugPrint('Failed to check duplicate company: ${e.message}'); // 오류 발생 시 중복 없음으로 처리 return null; } } else { // API만 사용 return null; } } Future saveCompany() async { if (!formKey.currentState!.validate()) { return false; } // 저장 직전, remarkController의 값을 branch.remark에 동기화 for (final c in branchControllers) { c.updateField('remark', c.remarkController.text); } final company = Company( id: companyId, name: nameController.text.trim(), address: companyAddress, contactName: contactNameController.text.trim(), contactPosition: contactPositionController.text.trim(), contactPhone: getFullPhoneNumber( selectedPhonePrefix, contactPhoneController.text.trim(), ), contactEmail: contactEmailController.text.trim(), remark: remarkController.text.trim(), branches: branchControllers.isEmpty ? null : branchControllers.map((c) => c.branch).toList(), companyTypes: List.from(selectedCompanyTypes), // 복수 유형 저장 isPartner: selectedCompanyTypes.contains(CompanyType.partner), isCustomer: selectedCompanyTypes.contains(CompanyType.customer), parentCompanyId: selectedParentCompanyId, // 부모 회사 ID 추가 ); if (_useApi) { try { Company savedCompany; if (companyId == null) { // 새 회사 생성 savedCompany = await _companyService.createCompany(company); debugPrint( 'Company created successfully with ID: ${savedCompany.id}', ); // 지점이 있으면 별도로 생성 if (branchControllers.isNotEmpty && savedCompany.id != null) { for (final branchController in branchControllers) { try { // TODO: Branch 생성 대신 자회사 Company 생성으로 변경 필요 // final branch = branchController.branch.copyWith( // companyId: savedCompany.id!, // ); // await _companyService.createBranch(savedCompany.id!, branch); debugPrint('Branch creation is deprecated. Use hierarchical Company structure instead.'); // debugPrint('Branch created successfully: ${branch.name}'); } catch (e) { debugPrint('Failed to create branch: $e'); // 지점 생성 실패는 경고만 하고 계속 진행 } } } } else { // 기존 회사 수정 savedCompany = await _companyService.updateCompany( companyId!, company, ); debugPrint('Company updated successfully'); // DEPRECATED: 지점 업데이트 처리 (계층형 Company 구조로 대체) if (branchControllers.isNotEmpty) { debugPrint('Branch management is deprecated. Use hierarchical Company structure instead.'); // TODO: 자회사 관리로 마이그레이션 필요 /* // 기존 지점 목록 가져오기 final currentCompany = await _companyService.getCompanyDetail(companyId!); final existingBranchIds = currentCompany.branches ?.where((b) => b.id != null) .map((b) => b.id!) .toSet() ?? {}; final newBranchIds = branchControllers .where((bc) => bc.branch.id != null && bc.branch.id! > 0) .map((bc) => bc.branch.id!) .toSet(); // 삭제할 지점 처리 (기존에 있었지만 새 목록에 없는 지점) final branchesToDelete = existingBranchIds.difference(newBranchIds); for (final branchId in branchesToDelete) { try { await _companyService.deleteBranch(companyId!, branchId); debugPrint('Branch deleted successfully: $branchId'); } catch (e) { debugPrint('Failed to delete branch: $e'); } } // 지점 추가 또는 수정 for (final branchController in branchControllers) { try { final branch = branchController.branch.copyWith( companyId: companyId!, ); if (branch.id == null || branch.id == 0) { // 새 지점 추가 await _companyService.createBranch(companyId!, branch); debugPrint('Branch created successfully: ${branch.name}'); } else if (existingBranchIds.contains(branch.id)) { // 기존 지점 수정 await _companyService.updateBranch(companyId!, branch.id!, branch); debugPrint('Branch updated successfully: ${branch.name}'); } } catch (e) { debugPrint('Failed to save branch: $e'); // 지점 처리 실패는 경고만 하고 계속 진행 } } */ } } return true; } on Failure catch (e) { debugPrint('Failed to save company: ${e.message}'); return false; } catch (e) { debugPrint('Unexpected error saving company: $e'); return false; } } else { // API만 사용 throw Exception('API를 통해만 데이터를 저장할 수 있습니다'); } } // DEPRECATED: 지점 저장 (계층형 Company 구조로 대체) @Deprecated('계층형 Company 구조로 대체되었습니다. Company 관리로 자회사를 생성하세요.') Future saveBranch(int branchId) async { debugPrint('saveBranch is deprecated. Use hierarchical Company structure instead.'); return false; /* if (!formKey.currentState!.validate()) { return false; } formKey.currentState!.save(); // 지점 정보 생성 final branch = Branch( id: branchId, companyId: companyId!, name: nameController.text.trim(), address: companyAddress, contactName: contactNameController.text.trim(), contactPosition: contactPositionController.text.trim(), contactPhone: getFullPhoneNumber( selectedPhonePrefix, contactPhoneController.text.trim(), ), contactEmail: contactEmailController.text.trim(), remark: remarkController.text.trim(), ); if (_useApi) { try { // API를 사용하여 지점 업데이트 await _companyService.updateBranch(companyId!, branchId, branch); return true; } on Failure catch (e) { debugPrint('Failed to save branch: ${e.message}'); return false; } } else { // API만 사용 return false; } */ } // 회사 유형 체크박스 토글 함수 void toggleCompanyType(CompanyType type, bool checked) { if (checked) { if (!selectedCompanyTypes.contains(type)) { selectedCompanyTypes.add(type); } } else { selectedCompanyTypes.remove(type); if (selectedCompanyTypes.isEmpty) { // 최소 1개는 선택되도록 강제 selectedCompanyTypes.add(CompanyType.customer); } } } // 우편번호 선택 void selectZipcode(ZipcodeDto zipcode) { selectedZipcode = zipcode; zipcodeController.text = zipcode.zipcode; // 주소를 Address 객체로 변환 companyAddress = Address( zipCode: zipcode.zipcode, region: '${zipcode.sido} ${zipcode.gu}'.trim(), detailAddress: zipcode.etc ?? '', ); } } // 전화번호 관련 유틸리티 메서드 // 전화번호 접두사 추출 String extractPhonePrefix(String phoneNumber, List prefixes) { if (phoneNumber.isEmpty) return '010'; // 하이픈 제거 String cleanNumber = phoneNumber.replaceAll('-', ''); // 접두사 확인 for (String prefix in prefixes) { if (cleanNumber.startsWith(prefix)) { return prefix; } } return '010'; // 기본값 } // 접두사 제외 전화번호 추출 String extractPhoneNumberWithoutPrefix( String phoneNumber, List prefixes, ) { if (phoneNumber.isEmpty) return ''; // 하이픈 제거 String cleanNumber = phoneNumber.replaceAll('-', ''); // 접두사 제거 for (String prefix in prefixes) { if (cleanNumber.startsWith(prefix)) { return cleanNumber.substring(prefix.length); } } return cleanNumber; } // 전체 전화번호 생성 (접두사 + 번호) String getFullPhoneNumber(String prefix, String number) { if (number.isEmpty) return ''; // 하이픈 제거 String cleanNumber = number.replaceAll('-', ''); return '$prefix-$cleanNumber'; } // 일반적인 전화번호 접두사 목록 List getCommonPhonePrefixes() { return [ '010', '011', '016', '017', '018', '019', // 휴대폰 '02', '031', '032', '033', '041', '042', '043', '044', '051', '052', '053', '054', '055', '061', '062', '063', '064', // 지역번호 '070', '080', '1588', '1566', '1544', '1644', '1661', '1599', // 기타 ]; }