Files
superport/lib/screens/company/controllers/company_form_controller.dart
JiWoong Sul c419f8f458 backup: 사용하지 않는 파일 삭제 전 복구 지점
- 전체 371개 파일 중 82개 미사용 파일 식별
- Phase 1: 33개 파일 삭제 예정 (100% 안전)
- Phase 2: 30개 파일 삭제 검토 예정
- Phase 3: 19개 파일 수동 검토 예정

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 19:51:40 +09:00

737 lines
27 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 회사 폼 컨트롤러
///
/// 회사 폼 화면의 비즈니스 로직을 담당하는 컨트롤러 클래스
/// 주요 기능:
/// - 회사 데이터 로드 및 저장
/// - 자동완성 처리
/// - 지점 정보 관리 (추가, 삭제, 수정)
/// - 전화번호 처리
/// - 중복 회사명 체크
/// - 회사 유형 관리
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<CompanyService>();
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<FormState> formKey = GlobalKey<FormState>();
final ScrollController scrollController = ScrollController();
// 회사 유형 선택값 (복수)
List<CompanyType> selectedCompanyTypes = [CompanyType.customer];
// 부모 회사 선택 (단순화)
int? selectedParentCompanyId;
Map<int, String> availableParentCompanies = {};
List<String> companyNames = [];
List<String> filteredCompanyNames = [];
bool showCompanyNameDropdown = false;
// 분리된 BranchFormController 리스트로 관리
List<BranchFormController> branchControllers = [];
// 직책 목록 및 전화번호 접두사 목록(공통 상수)
final List<String> positions = [
'대표이사',
'사장',
'부사장',
'전무',
'상무',
'이사',
'부장',
'차장',
'팀장',
'과장',
'대리',
'사원',
'주임',
'기타',
];
final List<String> phonePrefixes = getCommonPhonePrefixes();
String selectedPhonePrefix = '010';
final List<String> phonePrefixesForMain = getCommonPhonePrefixes();
ValueNotifier<bool> showPositionDropdownNotifier = ValueNotifier<bool>(false);
Timer? debounceTimer;
bool preventAutoFocus = false;
final Map<int, bool> isNewlyAddedBranch = {};
CompanyFormController({this.companyId, bool useApi = true})
: _useApi = useApi {
_setupFocusNodes();
_setupControllerListeners();
// 비동기 초기화는 별도로 호출해야 함
Future.microtask(() => loadParentCompanies());
}
// 부모 회사 목록 로드 (LOOKUP COMPANIES API 직접 호출)
Future<void> loadParentCompanies() async {
try {
debugPrint('📝 부모 회사 목록 로드 시작 - LOOKUP /companies API 직접 호출');
// 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 {
debugPrint('❌ Lookup Companies API 실패: 상태코드 ${response.statusCode}');
availableParentCompanies = {};
}
} catch (e) {
debugPrint('❌ 부모 회사 목록 로드 예외: $e');
availableParentCompanies = {};
}
}
// 회사 데이터 로드 (수정 모드)
Future<void> 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<bool> checkDuplicateName(String name) async {
try {
debugPrint('🔍 === 중복 검사 시작 (LOOKUPS API 사용) ===');
debugPrint('🔍 검사할 회사명: "$name"');
debugPrint('🔍 현재 회사 ID: $companyId');
// 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; // 중복 발견
}
}
}
debugPrint('✅ 중복 없음');
return false; // 중복 없음
} else {
debugPrint('❌ LOOKUPS API 호출 실패: ${response.statusCode}');
return true; // 안전장치
}
} catch (e) {
debugPrint('❌ 회사명 중복 검사 실패: $e');
// 네트워크 오류 시 중복 있음으로 처리 (안전장치)
return true;
}
}
@Deprecated('checkDuplicateName을 사용하세요')
Future<Company?> 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<bool> 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() ?? <int>{};
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<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('❌ 회사 저장 실패 (Failure): ${e.message}');
return false;
} catch (e) {
debugPrint('❌ 예상치 못한 오류: $e');
return false;
}
} else {
// API만 사용
throw Exception('API를 통해만 데이터를 저장할 수 있습니다');
}
}
// DEPRECATED: 지점 저장 (계층형 Company 구조로 대체)
@Deprecated('계층형 Company 구조로 대체되었습니다. Company 관리로 자회사를 생성하세요.')
Future<bool> 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<String> 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<String> 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<String> 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', // 기타
];
}