/// 회사 폼 컨트롤러 /// /// 회사 폼 화면의 비즈니스 로직을 담당하는 컨트롤러 클래스 /// 주요 기능: /// - 회사 데이터 로드 및 저장 /// - 자동완성 처리 /// - 지점 정보 관리 (추가, 삭제, 수정) /// - 전화번호 처리 /// - 중복 회사명 체크 /// - 회사 유형 관리 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/datasources/remote/api_client.dart'; import 'package:dio/dio.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; Map 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(() => loadParentCompanies()); } // 부모 회사 목록 로드 (LOOKUP COMPANIES API 직접 호출) Future loadParentCompanies() async { try { debugPrint('📝 부모 회사 목록 로드 시작 - LOOKUP /companies API 직접 호출'); // API 직접 호출 (GetIt DI 사용) final apiClient = GetIt.instance(); 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 companiesJson = response.data as List; 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 { debugPrint('❌ Lookup Companies API 실패: 상태코드 ${response.statusCode}'); availableParentCompanies = {}; } } catch (e) { debugPrint('❌ 부모 회사 목록 로드 예외: $e'); 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) { // 통합 필드를 위해 전체 전화번호를 그대로 저장 contactPhoneController.text = company.contactPhone!; // 기존 분리 로직은 참고용으로만 유지 selectedPhonePrefix = extractPhonePrefix( company.contactPhone!, phonePrefixes, ); debugPrint('📝 전화번호 설정 (전체): ${contactPhoneController.text}'); debugPrint('📝 전화번호 접두사 (참고): $selectedPhonePrefix'); } // 회사 유형 설정 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 { debugPrint('🔍 === 중복 검사 시작 (LOOKUPS API 사용) ==='); debugPrint('🔍 검사할 회사명: "$name"'); debugPrint('🔍 현재 회사 ID: $companyId'); // LOOKUPS API로 전체 회사 목록 조회 (페이지네이션 없음) final apiClient = GetIt.instance(); final response = await apiClient.get('/lookups/companies'); if (response.statusCode == 200 && response.data != null) { final List companiesJson = response.data as List; 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; // 중복 발견 } } } debugPrint('✅ 중복 없음'); return false; // 중복 없음 } else { debugPrint('❌ LOOKUPS API 호출 실패: ${response.statusCode}'); return true; // 안전장치 } } catch (e) { debugPrint('❌ 회사명 중복 검사 실패: $e'); // 네트워크 오류 시 중복 있음으로 처리 (안전장치) return true; } } @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: 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) { // 새 회사 생성 debugPrint('💾 회사 생성 요청 데이터:'); debugPrint(' - 회사명: ${company.name}'); debugPrint(' - 이메일: ${company.contactEmail}'); debugPrint(' - 부모회사ID: ${company.parentCompanyId}'); 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 { // 기존 회사 수정 debugPrint('💾 회사 수정 요청 데이터:'); debugPrint(' - 회사명: ${company.name}'); debugPrint(' - 이메일: ${company.contactEmail}'); debugPrint(' - 부모회사ID: ${company.parentCompanyId}'); 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 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) { 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('❌ 회사 저장 실패 (Failure): ${e.message}'); return false; } catch (e) { debugPrint('❌ 예상치 못한 오류: $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', // 기타 ]; }