Files
superport/lib/screens/company/controllers/company_form_controller.dart
JiWoong Sul 162fe08618
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
refactor: Clean Architecture 적용 및 코드베이스 전면 리팩토링
## 주요 변경사항

### 아키텍처 개선
- Clean Architecture 패턴 적용 (Domain, Data, Presentation 레이어 분리)
- Use Case 패턴 도입으로 비즈니스 로직 캡슐화
- Repository 패턴으로 데이터 접근 추상화
- 의존성 주입 구조 개선

### 상태 관리 최적화
- 모든 Controller에서 불필요한 상태 관리 로직 제거
- 페이지네이션 로직 통일 및 간소화
- 에러 처리 로직 개선 (에러 메시지 한글화)
- 로딩 상태 관리 최적화

### Mock 서비스 제거
- MockDataService 완전 제거
- 모든 화면을 실제 API 전용으로 전환
- 불필요한 Mock 관련 코드 정리

### UI/UX 개선
- Overview 화면 대시보드 기능 강화
- 라이선스 만료 알림 위젯 추가
- 사이드바 네비게이션 개선
- 일관된 UI 컴포넌트 사용

### 코드 품질
- 중복 코드 제거 및 함수 추출
- 파일별 책임 분리 명확화
- 테스트 코드 업데이트

## 영향 범위
- 모든 화면의 Controller 리팩토링
- API 통신 레이어 구조 개선
- 에러 처리 및 로깅 시스템 개선

## 향후 계획
- 단위 테스트 커버리지 확대
- 통합 테스트 시나리오 추가
- 성능 모니터링 도구 통합
2025-08-11 00:04:28 +09:00

562 lines
17 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.
/// 회사 폼 컨트롤러
///
/// 회사 폼 화면의 비즈니스 로직을 담당하는 컨트롤러 클래스
/// 주요 기능:
/// - 회사 데이터 로드 및 저장
/// - 자동완성 처리
/// - 지점 정보 관리 (추가, 삭제, 수정)
/// - 전화번호 처리
/// - 중복 회사명 체크
/// - 회사 유형 관리
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 'package:superport/utils/phone_utils.dart';
import 'dart:async';
import 'branch_form_controller.dart'; // 분리된 지점 컨트롤러 import
/// 회사 폼 컨트롤러 - 비즈니스 로직 처리
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 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];
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(() => _initializeAsync());
}
Future<void> _initializeAsync() async {
final isEditMode = companyId != null;
await _loadCompanyNames();
// loadCompanyData는 별도로 호출됨 (company_form.dart에서)
}
// 회사명 목록 로드 (자동완성용)
Future<void> _loadCompanyNames() async {
try {
List<Company> companies;
// API만 사용
companies = await _companyService.getCompanies();
companyNames = companies.map((c) => c.name).toList();
filteredCompanyNames = companyNames;
} catch (e) {
debugPrint('❌ 회사명 목록 로드 실패: $e');
companyNames = [];
filteredCompanyNames = [];
}
}
// 회사 데이터 로드 (수정 모드)
Future<void> loadCompanyData() async {
if (companyId == null) return;
debugPrint('📝 loadCompanyData 시작 - ID: $companyId');
try {
Company? company;
if (_useApi) {
debugPrint('📝 API에서 회사 정보 로드 중...');
company = await _companyService.getCompanyDetail(companyId!);
} else {
debugPrint('📝 API만 사용 가능');
throw Exception('API를 통해만 데이터를 로드할 수 있습니다');
}
debugPrint('📝 로드된 회사: $company');
if (company != null) {
// 폼 필드에 데이터 설정
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,
);
}
// 회사 유형 설정
selectedCompanyTypes = company.companyTypes;
// 지점 정보 설정
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('📝 폼 필드 설정 완료:');
debugPrint(' - 회사명: ${nameController.text}');
debugPrint(' - 담당자: ${contactNameController.text}');
debugPrint(' - 이메일: ${contactEmailController.text}');
debugPrint(' - 지점 수: ${branchControllers.length}');
}
} catch (e) {
debugPrint('❌ 회사 정보 로드 실패: $e');
}
}
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);
}
Future<void> _loadCompanyData() async {
if (companyId == null) return;
Company? company;
if (_useApi) {
try {
company = await _companyService.getCompanyWithBranches(companyId!);
} on Failure catch (e) {
debugPrint('Failed to load company data: ${e.message}');
return;
}
} else {
// API만 사용
debugPrint('API를 통해만 데이터를 로드할 수 있습니다');
}
if (company != null) {
nameController.text = company.name;
companyAddress = company.address;
selectedCompanyTypes = List.from(company.companyTypes); // 복수 유형 지원
contactNameController.text = company.contactName ?? '';
contactPositionController.text = company.contactPosition ?? '';
selectedPhonePrefix = extractPhonePrefix(
company.contactPhone ?? '',
phonePrefixesForMain,
);
contactPhoneController.text = extractPhoneNumberWithoutPrefix(
company.contactPhone ?? '',
phonePrefixesForMain,
);
contactEmailController.text = company.contactEmail ?? '';
remarkController.text = company.remark ?? '';
// 지점 컨트롤러 생성
branchControllers.clear();
final branches = company.branches?.toList() ?? [];
if (branches.isEmpty) {
_addInitialBranch();
} else {
for (final branch in branches) {
branchControllers.add(
BranchFormController(
branch: branch,
positions: positions,
phonePrefixes: phonePrefixes,
),
);
}
}
}
}
void _addInitialBranch() {
final newBranch = Branch(
companyId: companyId ?? 0,
name: '본사',
address: const Address(),
);
branchControllers.add(
BranchFormController(
branch: newBranch,
positions: positions,
phonePrefixes: phonePrefixes,
),
);
isNewlyAddedBranch[branchControllers.length - 1] = true;
}
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<Company?> checkDuplicateCompany() async {
if (companyId != null) return null; // 수정 모드에서는 체크하지 않음
final name = nameController.text.trim();
if (name.isEmpty) return null;
if (_useApi) {
try {
// 회사명 목록을 조회하여 중복 확인
final companies = await _companyService.getCompanies(search: name);
// 정확히 일치하는 회사명이 있는지 확인
for (final company in companies) {
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;
}
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: 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), // 복수 유형 저장
);
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 {
final branch = branchController.branch.copyWith(
companyId: savedCompany.id!,
);
await _companyService.createBranch(savedCompany.id!, branch);
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');
// 지점 업데이트는 별도 처리 필요 (현재는 수정 시 지점 추가/삭제 미지원)
}
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를 통해만 데이터를 저장할 수 있습니다');
return true;
}
return false;
}
// 지점 저장
Future<bool> saveBranch(int branchId) async {
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;
}
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);
}
}
}
}
// 전화번호 관련 유틸리티 메서드
// 전화번호 접두사 추출
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', // 기타
];
}