refactor: 회사 폼 UI 개선 및 코드 정리
- 담당자 연락처 필드를 드롭다운 + 입력 방식으로 분리 - 사용자 폼과 동일한 전화번호 UI 패턴 적용 - 미사용 위젯 파일 4개 정리 (branch_card, contact_info_* 등) - 파일명 통일성 확보 (branch_edit_screen → branch_form, company_form_simplified → company_form) - 네이밍 일관성 개선으로 유지보수성 향상
This commit is contained in:
324
lib/screens/company/branch_form.dart
Normal file
324
lib/screens/company/branch_form.dart
Normal file
@@ -0,0 +1,324 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
import 'package:superport/screens/company/controllers/branch_edit_form_controller.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
|
||||
/// 지점 정보 관리 화면 (등록/수정)
|
||||
/// User/Warehouse Location 화면과 동일한 패턴으로 구현
|
||||
class BranchFormScreen extends StatefulWidget {
|
||||
final Map<String, dynamic> arguments;
|
||||
|
||||
const BranchFormScreen({Key? key, required this.arguments}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<BranchFormScreen> createState() => _BranchFormScreenState();
|
||||
}
|
||||
|
||||
class _BranchFormScreenState extends State<BranchFormScreen> {
|
||||
late final BranchEditFormController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// arguments에서 정보 추출
|
||||
final companyId = widget.arguments['companyId'] as int;
|
||||
final branchId = widget.arguments['branchId'] as int;
|
||||
final parentCompanyName = widget.arguments['parentCompanyName'] as String;
|
||||
|
||||
_controller = BranchEditFormController(
|
||||
companyId: companyId,
|
||||
branchId: branchId,
|
||||
parentCompanyName: parentCompanyName,
|
||||
);
|
||||
|
||||
// 데이터 로드
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_controller.loadBranchData();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 저장 처리
|
||||
Future<void> _onSave() async {
|
||||
if (_controller.isLoading) return;
|
||||
|
||||
final success = await _controller.saveBranch();
|
||||
|
||||
if (success && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('지점 정보가 수정되었습니다.'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
} else if (_controller.error != null && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_controller.error!),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 취소 처리 (변경사항 확인)
|
||||
void _onCancel() {
|
||||
if (_controller.hasChanges()) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('변경사항 확인'),
|
||||
content: const Text('변경된 내용이 있습니다. 저장하지 않고 나가시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('계속 수정'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context); // 다이얼로그 닫기
|
||||
Navigator.pop(context); // 화면 닫기
|
||||
},
|
||||
child: const Text('나가기'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('${_controller.parentCompanyName} 지점 수정'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: _onCancel,
|
||||
),
|
||||
),
|
||||
body: ListenableBuilder(
|
||||
listenable: _controller,
|
||||
builder: (context, child) {
|
||||
// 로딩 상태
|
||||
if (_controller.isLoading && _controller.originalBranch == null) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('지점 정보를 불러오는 중...'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 에러 상태
|
||||
if (_controller.error != null && _controller.originalBranch == null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(_controller.error!),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _controller.loadBranchData,
|
||||
child: const Text('다시 시도'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 폼 화면
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _controller.formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 지점명 (필수)
|
||||
FormFieldWrapper(
|
||||
label: "지점명 *",
|
||||
child: TextFormField(
|
||||
controller: _controller.nameController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '지점명을 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '지점명을 입력하세요';
|
||||
}
|
||||
if (value.trim().length < 2) {
|
||||
return '지점명은 2자 이상 입력하세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 주소 (선택)
|
||||
FormFieldWrapper(
|
||||
label: "주소",
|
||||
child: TextFormField(
|
||||
controller: _controller.addressController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '지점 주소를 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 2,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 담당자명 (선택)
|
||||
FormFieldWrapper(
|
||||
label: "담당자명",
|
||||
child: TextFormField(
|
||||
controller: _controller.managerNameController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '담당자명을 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 담당자 연락처 (선택)
|
||||
FormFieldWrapper(
|
||||
label: "담당자 연락처",
|
||||
child: TextFormField(
|
||||
controller: _controller.managerPhoneController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '010-0000-0000',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9-]')),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value != null && value.trim().isNotEmpty) {
|
||||
return validatePhoneNumber(value);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 비고 (선택)
|
||||
FormFieldWrapper(
|
||||
label: "비고",
|
||||
child: TextFormField(
|
||||
controller: _controller.remarkController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '추가 정보나 메모를 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
textInputAction: TextInputAction.done,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 버튼들
|
||||
Row(
|
||||
children: [
|
||||
// 리셋 버튼
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: OutlinedButton(
|
||||
onPressed: _controller.hasChanges()
|
||||
? _controller.resetForm
|
||||
: null,
|
||||
child: const Text('초기화'),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 취소 버튼
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: OutlinedButton(
|
||||
onPressed: _onCancel,
|
||||
child: const Text('취소'),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 저장 버튼
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton(
|
||||
onPressed: _controller.isLoading ? null : _onSave,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ShadcnTheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
),
|
||||
child: _controller.isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'수정 완료',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,299 +1,116 @@
|
||||
/// 회사 등록 및 수정 화면
|
||||
///
|
||||
/// SRP(단일 책임 원칙)에 따라 컴포넌트를 분리하여 구현한 리팩토링 버전
|
||||
/// - 컨트롤러: CompanyFormController - 비즈니스 로직 담당
|
||||
/// - 위젯:
|
||||
/// - CompanyFormHeader: 회사명 및 주소 입력
|
||||
/// - ContactInfoForm: 담당자 정보 입력
|
||||
/// - BranchCard: 지점 정보 카드
|
||||
/// - CompanyNameAutocomplete: 회사명 자동완성
|
||||
/// - MapDialog: 지도 다이얼로그
|
||||
/// - DuplicateCompanyDialog: 중복 회사 확인 다이얼로그
|
||||
/// - CompanyTypeSelector: 회사 유형 선택 라디오 버튼
|
||||
/// - 유틸리티:
|
||||
/// - PhoneUtils: 전화번호 관련 유틸리티
|
||||
import 'package:flutter/material.dart';
|
||||
// import 'package:superport/models/address_model.dart'; // 사용되지 않는 import
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
// import 'package:superport/screens/common/custom_widgets.dart'; // 사용되지 않는 import
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
import 'package:superport/screens/company/controllers/company_form_controller.dart';
|
||||
// import 'package:superport/screens/company/widgets/branch_card.dart'; // 사용되지 않는 import
|
||||
import 'package:superport/screens/company/widgets/company_form_header.dart';
|
||||
import 'package:superport/screens/company/widgets/contact_info_form.dart';
|
||||
import 'package:superport/screens/company/widgets/duplicate_company_dialog.dart';
|
||||
import 'package:superport/screens/company/widgets/map_dialog.dart';
|
||||
import 'package:superport/screens/company/widgets/branch_form_widget.dart';
|
||||
// import 'package:superport/services/mock_data_service.dart'; // Mock 서비스 제거
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:superport/screens/company/controllers/branch_form_controller.dart';
|
||||
import 'package:superport/core/config/environment.dart' as env;
|
||||
|
||||
/// 회사 유형 선택 위젯 (체크박스)
|
||||
class CompanyTypeSelector extends StatelessWidget {
|
||||
final List<CompanyType> selectedTypes;
|
||||
final Function(CompanyType, bool) onTypeChanged;
|
||||
|
||||
const CompanyTypeSelector({
|
||||
Key? key,
|
||||
required this.selectedTypes,
|
||||
required this.onTypeChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('회사 유형', style: ShadcnTheme.labelMedium),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
// 고객사 체크박스
|
||||
Checkbox(
|
||||
value: selectedTypes.contains(CompanyType.customer),
|
||||
onChanged: (checked) {
|
||||
onTypeChanged(CompanyType.customer, checked ?? false);
|
||||
},
|
||||
),
|
||||
const Text('고객사'),
|
||||
const SizedBox(width: 24),
|
||||
// 파트너사 체크박스
|
||||
Checkbox(
|
||||
value: selectedTypes.contains(CompanyType.partner),
|
||||
onChanged: (checked) {
|
||||
onTypeChanged(CompanyType.partner, checked ?? false);
|
||||
},
|
||||
),
|
||||
const Text('파트너사'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:superport/utils/validators.dart';
|
||||
import 'package:superport/utils/phone_utils.dart';
|
||||
|
||||
/// 회사 등록/수정 화면
|
||||
/// User/Warehouse Location 화면과 동일한 FormFieldWrapper 패턴 사용
|
||||
class CompanyFormScreen extends StatefulWidget {
|
||||
final Map? args;
|
||||
const CompanyFormScreen({Key? key, this.args}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CompanyFormScreenState createState() => _CompanyFormScreenState();
|
||||
State<CompanyFormScreen> createState() => _CompanyFormScreenState();
|
||||
}
|
||||
|
||||
class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
late CompanyFormController _controller;
|
||||
bool isBranch = false;
|
||||
String? mainCompanyName;
|
||||
final TextEditingController _addressController = TextEditingController();
|
||||
final TextEditingController _phoneNumberController = TextEditingController();
|
||||
int? companyId;
|
||||
int? branchId;
|
||||
bool isBranch = false;
|
||||
|
||||
// 전화번호 관련 변수
|
||||
String _selectedPhonePrefix = '010';
|
||||
List<String> _phonePrefixes = PhoneUtils.getCommonPhonePrefixes();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// controller는 didChangeDependencies에서 초기화
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
// arguments 처리
|
||||
final args = widget.args;
|
||||
if (args != null) {
|
||||
isBranch = args['isBranch'] ?? false;
|
||||
mainCompanyName = args['mainCompanyName'];
|
||||
companyId = args['companyId'];
|
||||
branchId = args['branchId'];
|
||||
isBranch = args['isBranch'] ?? false;
|
||||
}
|
||||
|
||||
// API 모드 확인
|
||||
final useApi = env.Environment.useApi;
|
||||
debugPrint('📌 회사 폼 초기화 - API 모드: $useApi, companyId: $companyId');
|
||||
|
||||
_controller = CompanyFormController(
|
||||
companyId: companyId,
|
||||
useApi: true, // 항상 API 사용
|
||||
useApi: true,
|
||||
);
|
||||
|
||||
// 일반 회사 수정 모드일 때 데이터 로드
|
||||
if (!isBranch && companyId != null) {
|
||||
debugPrint('📌 회사 데이터 로드 시작...');
|
||||
// 수정 모드일 때 데이터 로드
|
||||
if (companyId != null && !isBranch) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_controller.loadCompanyData().then((_) {
|
||||
debugPrint('📌 회사 데이터 로드 완료, UI 갱신');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
debugPrint('📌 setState 호출됨');
|
||||
debugPrint('📌 nameController.text: "${_controller.nameController.text}"');
|
||||
debugPrint('📌 contactNameController.text: "${_controller.contactNameController.text}"');
|
||||
});
|
||||
// 주소 필드 초기화
|
||||
_addressController.text = _controller.companyAddress.toString();
|
||||
|
||||
// 전화번호 분리 초기화
|
||||
final fullPhone = _controller.contactPhoneController.text;
|
||||
if (fullPhone.isNotEmpty) {
|
||||
_selectedPhonePrefix = PhoneUtils.extractPhonePrefix(fullPhone, _phonePrefixes);
|
||||
_phoneNumberController.text = PhoneUtils.extractPhoneNumberWithoutPrefix(fullPhone, _phonePrefixes);
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
}).catchError((error) {
|
||||
debugPrint('❌ 회사 데이터 로드 실패: $error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 지점 수정 모드일 때 branchId로 branch 정보 세팅
|
||||
if (isBranch && branchId != null) {
|
||||
// Mock 서비스 제거 - API를 통해 데이터 로드
|
||||
// 디버그: 진입 시 companyId, branchId 정보 출력
|
||||
print('[DEBUG] 지점 수정 진입: companyId=$companyId, branchId=$branchId');
|
||||
// TODO: API를 통해 회사 데이터 로드 필요
|
||||
// 아래 코드는 Mock 서비스 제거로 인해 주석 처리됨
|
||||
/*
|
||||
if (false) { // 임시로 비활성화
|
||||
print(
|
||||
'[DEBUG] 불러온 company.name=${company.name}, branches=${company.branches!.map((b) => 'id:${b.id}, name:${b.name}, remark:${b.remark}').toList()}',
|
||||
);
|
||||
final branch = company.branches!.firstWhere(
|
||||
(b) => b.id == branchId,
|
||||
orElse: () => company.branches!.first,
|
||||
);
|
||||
print(
|
||||
'[DEBUG] 선택된 branch: id=${branch.id}, name=${branch.name}, remark=${branch.remark}',
|
||||
);
|
||||
// 폼 컨트롤러의 각 필드에 branch 정보 세팅
|
||||
_controller.nameController.text = branch.name;
|
||||
_controller.companyAddress = branch.address;
|
||||
_controller.contactNameController.text = branch.contactName ?? '';
|
||||
_controller.contactPositionController.text =
|
||||
branch.contactPosition ?? '';
|
||||
_controller.selectedPhonePrefix = extractPhonePrefix(
|
||||
branch.contactPhone ?? '',
|
||||
_controller.phonePrefixes,
|
||||
);
|
||||
_controller
|
||||
.contactPhoneController
|
||||
.text = extractPhoneNumberWithoutPrefix(
|
||||
branch.contactPhone ?? '',
|
||||
_controller.phonePrefixes,
|
||||
);
|
||||
_controller.contactEmailController.text = branch.contactEmail ?? '';
|
||||
// 지점 단일 입력만 허용 (branchControllers 초기화)
|
||||
_controller.branchControllers.clear();
|
||||
_controller.branchControllers.add(
|
||||
BranchFormController(
|
||||
branch: branch,
|
||||
positions: _controller.positions,
|
||||
phonePrefixes: _controller.phonePrefixes,
|
||||
),
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_addressController.dispose();
|
||||
_phoneNumberController.dispose();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 지점 추가 후 스크롤 처리 (branchControllers 기반)
|
||||
void _scrollToAddedBranchCard() {
|
||||
if (_controller.branchControllers.isEmpty ||
|
||||
!_controller.scrollController.hasClients) {
|
||||
return;
|
||||
}
|
||||
// 추가 버튼 위치까지 스크롤 - 지점 추가 버튼이 있는 위치를 계산하여 그 위치로 스크롤
|
||||
final double additionalOffset = 80.0;
|
||||
final maxPos = _controller.scrollController.position.maxScrollExtent;
|
||||
final currentPos = _controller.scrollController.position.pixels;
|
||||
final targetPos = math.min(currentPos + additionalOffset, maxPos - 20.0);
|
||||
_controller.scrollController.animateTo(
|
||||
targetPos,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutQuad,
|
||||
);
|
||||
}
|
||||
|
||||
// 지점 추가
|
||||
void _addBranch() {
|
||||
setState(() {
|
||||
_controller.addBranch();
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_scrollToAddedBranchCard();
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
// 마지막 지점의 포커스 노드로 포커스 이동
|
||||
if (_controller.branchControllers.isNotEmpty) {
|
||||
_controller.branchControllers.last.focusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 회사 저장
|
||||
/// 회사 저장
|
||||
Future<void> _saveCompany() async {
|
||||
// 지점 수정 모드일 때의 처리
|
||||
if (isBranch && branchId != null) {
|
||||
// 로딩 표시
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final success = await _controller.saveBranch(branchId!);
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // 로딩 다이얼로그 닫기
|
||||
if (success) {
|
||||
Navigator.pop(context, true);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('지점 저장에 실패했습니다.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // 로딩 다이얼로그 닫기
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('오류가 발생했습니다: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!_controller.formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 회사 저장 로직
|
||||
final duplicateCompany = await _controller.checkDuplicateCompany();
|
||||
if (duplicateCompany != null) {
|
||||
DuplicateCompanyDialog.show(context, duplicateCompany);
|
||||
return;
|
||||
}
|
||||
// 주소 업데이트
|
||||
_controller.updateCompanyAddress(
|
||||
Address.fromFullAddress(_addressController.text)
|
||||
);
|
||||
|
||||
// 전화번호 합치기
|
||||
final fullPhoneNumber = PhoneUtils.getFullPhoneNumber(_selectedPhonePrefix, _phoneNumberController.text);
|
||||
_controller.contactPhoneController.text = fullPhoneNumber;
|
||||
|
||||
// 로딩 표시
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
builder: (context) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
|
||||
try {
|
||||
final success = await _controller.saveCompany();
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // 로딩 다이얼로그 닫기
|
||||
|
||||
if (success) {
|
||||
// 성공 메시지 표시
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(companyId != null ? '회사 정보가 수정되었습니다.' : '회사가 등록되었습니다.'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
// 리스트 화면으로 돌아가기
|
||||
Navigator.pop(context, true);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -320,215 +137,264 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEditMode = companyId != null;
|
||||
final String title =
|
||||
isBranch
|
||||
? '${mainCompanyName ?? ''} 지점 정보 수정'
|
||||
: (isEditMode ? '회사 정보 수정' : '회사 등록');
|
||||
final String nameLabel = isBranch ? '지점명' : '회사명';
|
||||
final String nameHint = isBranch ? '지점명을 입력하세요' : '회사명을 입력하세요';
|
||||
final title = isEditMode ? '회사 정보 수정' : '회사 등록';
|
||||
|
||||
// 지점 수정 모드일 때는 BranchFormWidget만 단독 노출
|
||||
if (isBranch && branchId != null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _controller.formKey,
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _controller.formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: BranchFormWidget(
|
||||
controller: _controller.branchControllers[0],
|
||||
index: 0,
|
||||
onRemove: null,
|
||||
onAddressChanged: (address) {
|
||||
setState(() {
|
||||
_controller.updateBranchAddress(0, address);
|
||||
});
|
||||
},
|
||||
),
|
||||
// 회사 유형 선택
|
||||
FormFieldWrapper(
|
||||
label: "회사 유형",
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
title: const Text('고객사'),
|
||||
value: _controller.selectedCompanyTypes.contains(CompanyType.customer),
|
||||
onChanged: (checked) {
|
||||
setState(() {
|
||||
_controller.toggleCompanyType(CompanyType.customer, checked ?? false);
|
||||
});
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text('파트너사'),
|
||||
value: _controller.selectedCompanyTypes.contains(CompanyType.partner),
|
||||
onChanged: (checked) {
|
||||
setState(() {
|
||||
_controller.toggleCompanyType(CompanyType.partner, checked ?? false);
|
||||
});
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 저장 버튼
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: _saveCompany,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ShadcnTheme.primary,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'수정 완료',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 회사명 (필수)
|
||||
FormFieldWrapper(
|
||||
label: "회사명 *",
|
||||
child: TextFormField(
|
||||
controller: _controller.nameController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '회사명을 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '회사명을 입력하세요';
|
||||
}
|
||||
if (value.trim().length < 2) {
|
||||
return '회사명은 2자 이상 입력하세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// ... 기존 본사/신규 등록 모드 렌더링
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (_controller.showCompanyNameDropdown) {
|
||||
_controller.showCompanyNameDropdown = false;
|
||||
}
|
||||
});
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _controller.formKey,
|
||||
child: SingleChildScrollView(
|
||||
controller: _controller.scrollController,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 회사 유형 선택 (체크박스)
|
||||
CompanyTypeSelector(
|
||||
selectedTypes: _controller.selectedCompanyTypes,
|
||||
onTypeChanged: (type, checked) {
|
||||
setState(() {
|
||||
_controller.toggleCompanyType(type, checked);
|
||||
});
|
||||
},
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 주소 (선택)
|
||||
FormFieldWrapper(
|
||||
label: "주소",
|
||||
child: TextFormField(
|
||||
controller: _addressController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '회사 주소를 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 2,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
// 회사 기본 정보 헤더 (회사명/지점명 + 주소)
|
||||
CompanyFormHeader(
|
||||
nameController: _controller.nameController,
|
||||
nameFocusNode: _controller.nameFocusNode,
|
||||
companyNames: _controller.companyNames,
|
||||
filteredCompanyNames: _controller.filteredCompanyNames,
|
||||
showCompanyNameDropdown:
|
||||
_controller.showCompanyNameDropdown,
|
||||
onCompanyNameSelected: (name) {
|
||||
setState(() {
|
||||
_controller.selectCompanyName(name);
|
||||
});
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 담당자명 (필수)
|
||||
FormFieldWrapper(
|
||||
label: "담당자명 *",
|
||||
child: TextFormField(
|
||||
controller: _controller.contactNameController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '담당자명을 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '담당자명을 입력하세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onShowMapPressed: () {
|
||||
final fullAddress = _controller.companyAddress.toString();
|
||||
MapDialog.show(context, fullAddress);
|
||||
},
|
||||
onNameSaved: (value) {},
|
||||
onAddressChanged: (address) {
|
||||
setState(() {
|
||||
_controller.updateCompanyAddress(address);
|
||||
});
|
||||
},
|
||||
initialAddress: _controller.companyAddress,
|
||||
nameLabel: nameLabel,
|
||||
nameHint: nameHint,
|
||||
remarkController: _controller.remarkController,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
// 담당자 정보
|
||||
ContactInfoForm(
|
||||
contactNameController: _controller.contactNameController,
|
||||
contactPositionController:
|
||||
_controller.contactPositionController,
|
||||
contactPhoneController: _controller.contactPhoneController,
|
||||
contactEmailController: _controller.contactEmailController,
|
||||
positions: _controller.positions,
|
||||
selectedPhonePrefix: _controller.selectedPhonePrefix,
|
||||
phonePrefixes: _controller.phonePrefixes,
|
||||
onPhonePrefixChanged: (value) {
|
||||
setState(() {
|
||||
_controller.selectedPhonePrefix = value;
|
||||
});
|
||||
},
|
||||
onNameSaved: (value) {},
|
||||
onPositionSaved: (value) {},
|
||||
onPhoneSaved: (value) {},
|
||||
onEmailSaved: (value) {},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 담당자 직급 (선택)
|
||||
FormFieldWrapper(
|
||||
label: "담당자 직급",
|
||||
child: TextFormField(
|
||||
controller: _controller.contactPositionController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '담당자 직급을 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
// 지점 정보(하단) 및 +지점추가 버튼은 본사/신규 등록일 때만 노출
|
||||
if (!(isBranch && branchId != null)) ...[
|
||||
if (_controller.branchControllers.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0),
|
||||
child: Text(
|
||||
'지점 정보',
|
||||
style: ShadcnTheme.headingH6,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 담당자 연락처 (필수) - 사용자 폼과 동일한 패턴
|
||||
FormFieldWrapper(
|
||||
label: "담당자 연락처 *",
|
||||
child: Row(
|
||||
children: [
|
||||
// 접두사 드롭다운 (010, 02, 031 등)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: DropdownButton<String>(
|
||||
value: _selectedPhonePrefix,
|
||||
items: _phonePrefixes.map((prefix) {
|
||||
return DropdownMenuItem(
|
||||
value: prefix,
|
||||
child: Text(prefix),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedPhonePrefix = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
underline: Container(), // 밑줄 제거
|
||||
),
|
||||
),
|
||||
if (_controller.branchControllers.isNotEmpty)
|
||||
for (
|
||||
int i = 0;
|
||||
i < _controller.branchControllers.length;
|
||||
i++
|
||||
)
|
||||
BranchFormWidget(
|
||||
controller: _controller.branchControllers[i],
|
||||
index: i,
|
||||
onRemove: () {
|
||||
setState(() {
|
||||
_controller.removeBranch(i);
|
||||
});
|
||||
},
|
||||
onAddressChanged: (address) {
|
||||
setState(() {
|
||||
_controller.updateBranchAddress(i, address);
|
||||
});
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _addBranch,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('지점 추가'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
const SizedBox(width: 8),
|
||||
const Text('-', style: TextStyle(fontSize: 16)),
|
||||
const SizedBox(width: 8),
|
||||
// 전화번호 입력 (7-8자리)
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _phoneNumberController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '1234-5678',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
TextInputFormatter.withFunction((oldValue, newValue) {
|
||||
final formatted = PhoneUtils.formatPhoneNumberByPrefix(
|
||||
_selectedPhonePrefix,
|
||||
newValue.text,
|
||||
);
|
||||
return TextEditingValue(
|
||||
text: formatted,
|
||||
selection: TextSelection.collapsed(offset: formatted.length),
|
||||
);
|
||||
}),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '전화번호를 입력하세요';
|
||||
}
|
||||
final digitsOnly = value.replaceAll(RegExp(r'[^\d]'), '');
|
||||
if (digitsOnly.length < 7) {
|
||||
return '전화번호는 7-8자리 숫자를 입력해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 담당자 이메일 (필수)
|
||||
FormFieldWrapper(
|
||||
label: "담당자 이메일 *",
|
||||
child: TextFormField(
|
||||
controller: _controller.contactEmailController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'example@company.com',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
],
|
||||
// 저장 버튼 추가
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0, bottom: 16.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: _saveCompany,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ShadcnTheme.primary,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isEditMode ? '수정 완료' : '등록 완료',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '담당자 이메일을 입력하세요';
|
||||
}
|
||||
return validateEmail(value);
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 비고 (선택)
|
||||
FormFieldWrapper(
|
||||
label: "비고",
|
||||
child: TextFormField(
|
||||
controller: _controller.remarkController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '추가 정보나 메모를 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
textInputAction: TextInputAction.done,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 저장 버튼
|
||||
ElevatedButton(
|
||||
onPressed: _saveCompany,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ShadcnTheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
isEditMode ? '수정 완료' : '등록 완료',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
import 'dart:async';
|
||||
import 'package:superport/core/constants/app_constants.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/company_item_model.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
import 'package:superport/screens/common/widgets/pagination.dart';
|
||||
@@ -104,18 +105,38 @@ class _CompanyListState extends State<CompanyList> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Branch 객체를 Company 객체로 변환
|
||||
Company _convertBranchToCompany(Branch branch) {
|
||||
return Company(
|
||||
id: branch.id,
|
||||
name: branch.name,
|
||||
address: branch.address,
|
||||
contactName: branch.contactName,
|
||||
contactPosition: branch.contactPosition,
|
||||
contactPhone: branch.contactPhone,
|
||||
contactEmail: branch.contactEmail,
|
||||
companyTypes: [],
|
||||
remark: branch.remark,
|
||||
/// 지점 삭제 처리
|
||||
void _deleteBranch(int companyId, int branchId) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('지점 삭제 확인'),
|
||||
content: const Text('이 지점 정보를 삭제하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
try {
|
||||
await _controller.deleteBranch(companyId, branchId);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(e.toString()),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('삭제'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,23 +198,161 @@ class _CompanyListState extends State<CompanyList> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 이름 표시 (지점인 경우 본사명 포함)
|
||||
Widget _buildCompanyNameText(
|
||||
Company company,
|
||||
bool isBranch, {
|
||||
String? mainCompanyName,
|
||||
}) {
|
||||
if (isBranch && mainCompanyName != null) {
|
||||
/// CompanyItem의 계층적 이름 표시
|
||||
Widget _buildDisplayNameText(CompanyItem item) {
|
||||
if (item.isBranch) {
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: '$mainCompanyName > ', style: ShadcnTheme.bodyMuted),
|
||||
TextSpan(text: company.name, style: ShadcnTheme.bodyMedium),
|
||||
TextSpan(text: '${item.parentCompanyName} > ', style: ShadcnTheme.bodyMuted),
|
||||
TextSpan(text: item.name, style: ShadcnTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Text(company.name, style: ShadcnTheme.bodyMedium);
|
||||
return Text(item.name, style: ShadcnTheme.bodyMedium);
|
||||
}
|
||||
}
|
||||
|
||||
/// 활성 상태 배지 생성
|
||||
Widget _buildStatusBadge(bool isActive) {
|
||||
return ShadcnBadge(
|
||||
text: isActive ? '활성' : '비활성',
|
||||
variant: isActive
|
||||
? ShadcnBadgeVariant.success
|
||||
: ShadcnBadgeVariant.secondary,
|
||||
size: ShadcnBadgeSize.small,
|
||||
);
|
||||
}
|
||||
|
||||
/// 날짜 포맷팅
|
||||
String _formatDate(DateTime? date) {
|
||||
if (date == null) return '-';
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 담당자 정보 통합 표시 (이름 + 직책)
|
||||
Widget _buildContactInfo(CompanyItem item) {
|
||||
final name = item.contactName ?? '-';
|
||||
final position = item.contactPosition;
|
||||
|
||||
if (position != null && position.isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: ShadcnTheme.bodySmall.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
position,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.muted,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Text(name, style: ShadcnTheme.bodySmall);
|
||||
}
|
||||
}
|
||||
|
||||
/// 연락처 정보 통합 표시 (전화 + 이메일)
|
||||
Widget _buildContactDetails(CompanyItem item) {
|
||||
final phone = item.contactPhone ?? '-';
|
||||
final email = item.contactEmail;
|
||||
|
||||
if (email != null && email.isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
phone,
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
email,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.muted,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Text(phone, style: ShadcnTheme.bodySmall);
|
||||
}
|
||||
}
|
||||
|
||||
/// 파트너/고객 플래그 표시
|
||||
Widget _buildPartnerCustomerFlags(CompanyItem item) {
|
||||
if (item.isBranch) {
|
||||
return Text('-', style: ShadcnTheme.bodySmall);
|
||||
}
|
||||
|
||||
final flags = <Widget>[];
|
||||
|
||||
if (item.isPartner) {
|
||||
flags.add(ShadcnBadge(
|
||||
text: '파트너',
|
||||
variant: ShadcnBadgeVariant.companyPartner,
|
||||
size: ShadcnBadgeSize.small,
|
||||
));
|
||||
}
|
||||
|
||||
if (item.isCustomer) {
|
||||
flags.add(ShadcnBadge(
|
||||
text: '고객',
|
||||
variant: ShadcnBadgeVariant.companyCustomer,
|
||||
size: ShadcnBadgeSize.small,
|
||||
));
|
||||
}
|
||||
|
||||
if (flags.isEmpty) {
|
||||
return Text('-', style: ShadcnTheme.bodySmall);
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 2,
|
||||
children: flags,
|
||||
);
|
||||
}
|
||||
|
||||
/// 등록일/수정일 표시
|
||||
Widget _buildDateInfo(CompanyItem item) {
|
||||
final createdAt = item.createdAt;
|
||||
final updatedAt = item.updatedAt;
|
||||
|
||||
if (createdAt == null) {
|
||||
return Text('-', style: ShadcnTheme.bodySmall);
|
||||
}
|
||||
|
||||
final created = _formatDate(createdAt);
|
||||
|
||||
if (updatedAt != null && updatedAt != createdAt) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'등록: $created',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
'수정: ${_formatDate(updatedAt)}',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.muted,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Text(created, style: ShadcnTheme.bodySmall);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,38 +362,18 @@ class _CompanyListState extends State<CompanyList> {
|
||||
value: _controller,
|
||||
child: Consumer<CompanyListController>(
|
||||
builder: (context, controller, child) {
|
||||
// 본사와 지점 구분하기 위한 데이터 준비
|
||||
final List<Map<String, dynamic>> displayCompanies = [];
|
||||
for (final company in controller.filteredCompanies) {
|
||||
displayCompanies.add({
|
||||
'company': company,
|
||||
'isBranch': false,
|
||||
'mainCompanyName': null,
|
||||
});
|
||||
if (company.branches != null) {
|
||||
for (final branch in company.branches!) {
|
||||
displayCompanies.add({
|
||||
'branch': branch,
|
||||
'companyId': company.id,
|
||||
'isBranch': true,
|
||||
'mainCompanyName': company.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Controller가 이미 페이지크된 데이터를 제공
|
||||
final List<Map<String, dynamic>> pagedCompanies = displayCompanies;
|
||||
final int totalCount = controller.total; // 실제 전체 개수 사용
|
||||
// CompanyItem 데이터 직접 사용 (복잡한 변환 로직 제거)
|
||||
final companyItems = controller.companyItems;
|
||||
final int totalCount = controller.total;
|
||||
|
||||
print('🔍 [VIEW DEBUG] 페이지네이션 상태');
|
||||
print(' • Controller items: ${controller.companies.length}개');
|
||||
print('🔍 [VIEW DEBUG] CompanyItem 페이지네이션 상태');
|
||||
print(' • CompanyItem items: ${controller.companyItems.length}개');
|
||||
print(' • 전체 개수: ${controller.total}개');
|
||||
print(' • 현재 페이지: ${controller.currentPage}');
|
||||
print(' • 페이지 크기: ${controller.pageSize}');
|
||||
|
||||
// 로딩 상태
|
||||
if (controller.isLoading && controller.companies.isEmpty) {
|
||||
if (controller.isLoading && controller.companyItems.isEmpty) {
|
||||
return const StandardLoadingState(message: '회사 데이터를 불러오는 중...');
|
||||
}
|
||||
|
||||
@@ -306,7 +445,7 @@ class _CompanyListState extends State<CompanyList> {
|
||||
|
||||
// 데이터 테이블
|
||||
dataTable:
|
||||
displayCompanies.isEmpty
|
||||
companyItems.isEmpty
|
||||
? StandardEmptyState(
|
||||
title:
|
||||
controller.searchQuery.isNotEmpty
|
||||
@@ -326,23 +465,19 @@ class _CompanyListState extends State<CompanyList> {
|
||||
std_table.DataColumn(label: '번호', flex: 1),
|
||||
std_table.DataColumn(label: '회사명', flex: 3),
|
||||
std_table.DataColumn(label: '구분', flex: 1),
|
||||
std_table.DataColumn(label: '유형', flex: 2),
|
||||
std_table.DataColumn(label: '연락처', flex: 2),
|
||||
std_table.DataColumn(label: '주소', flex: 3),
|
||||
std_table.DataColumn(label: '담당자', flex: 2),
|
||||
std_table.DataColumn(label: '연락처', flex: 3),
|
||||
std_table.DataColumn(label: '파트너/고객', flex: 2),
|
||||
std_table.DataColumn(label: '상태', flex: 1),
|
||||
std_table.DataColumn(label: '등록/수정일', flex: 2),
|
||||
std_table.DataColumn(label: '비고', flex: 2),
|
||||
std_table.DataColumn(label: '관리', flex: 2),
|
||||
],
|
||||
rows: [
|
||||
...pagedCompanies.asMap().entries.map((entry) {
|
||||
...companyItems.asMap().entries.map((entry) {
|
||||
final int index = ((controller.currentPage - 1) * controller.pageSize) + entry.key;
|
||||
final companyData = entry.value;
|
||||
final bool isBranch = companyData['isBranch'] as bool;
|
||||
final Company company =
|
||||
isBranch
|
||||
? _convertBranchToCompany(
|
||||
companyData['branch'] as Branch,
|
||||
)
|
||||
: companyData['company'] as Company;
|
||||
final String? mainCompanyName =
|
||||
companyData['mainCompanyName'] as String?;
|
||||
final CompanyItem item = entry.value;
|
||||
|
||||
return std_table.StandardDataRow(
|
||||
index: index,
|
||||
@@ -350,8 +485,13 @@ class _CompanyListState extends State<CompanyList> {
|
||||
std_table.DataColumn(label: '번호', flex: 1),
|
||||
std_table.DataColumn(label: '회사명', flex: 3),
|
||||
std_table.DataColumn(label: '구분', flex: 1),
|
||||
std_table.DataColumn(label: '유형', flex: 2),
|
||||
std_table.DataColumn(label: '연락처', flex: 2),
|
||||
std_table.DataColumn(label: '주소', flex: 3),
|
||||
std_table.DataColumn(label: '담당자', flex: 2),
|
||||
std_table.DataColumn(label: '연락처', flex: 3),
|
||||
std_table.DataColumn(label: '파트너/고객', flex: 2),
|
||||
std_table.DataColumn(label: '상태', flex: 1),
|
||||
std_table.DataColumn(label: '등록/수정일', flex: 2),
|
||||
std_table.DataColumn(label: '비고', flex: 2),
|
||||
std_table.DataColumn(label: '관리', flex: 2),
|
||||
],
|
||||
cells: [
|
||||
@@ -360,82 +500,89 @@ class _CompanyListState extends State<CompanyList> {
|
||||
'${index + 1}',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
// 회사명
|
||||
_buildCompanyNameText(
|
||||
company,
|
||||
isBranch,
|
||||
mainCompanyName: mainCompanyName,
|
||||
),
|
||||
// 구분
|
||||
// 회사명 (계층적 표시)
|
||||
_buildDisplayNameText(item),
|
||||
// 구분 (본사/지점 배지)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: _buildCompanyTypeLabel(isBranch),
|
||||
child: _buildCompanyTypeLabel(item.isBranch),
|
||||
),
|
||||
// 유형
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: _buildCompanyTypeChips(company.companyTypes),
|
||||
),
|
||||
// 연락처
|
||||
// 주소
|
||||
Text(
|
||||
company.contactPhone ?? '-',
|
||||
item.address.isNotEmpty ? item.address : '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
// 관리
|
||||
// 담당자 (이름 + 직책)
|
||||
_buildContactInfo(item),
|
||||
// 연락처 (전화 + 이메일)
|
||||
_buildContactDetails(item),
|
||||
// 파트너/고객 플래그
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: _buildPartnerCustomerFlags(item),
|
||||
),
|
||||
// 상태
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: _buildStatusBadge(item.isActive),
|
||||
),
|
||||
// 등록/수정일
|
||||
_buildDateInfo(item),
|
||||
// 비고
|
||||
Text(
|
||||
item.remark ?? '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
// 관리 (액션 버튼들)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isBranch &&
|
||||
company.branches != null &&
|
||||
company.branches!.isNotEmpty) ...[
|
||||
ShadcnButton(
|
||||
text: '지점',
|
||||
onPressed:
|
||||
() => _showBranchDialog(company),
|
||||
variant: ShadcnButtonVariant.ghost,
|
||||
size: ShadcnButtonSize.small,
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
],
|
||||
std_table.StandardActionButtons(
|
||||
onEdit:
|
||||
company.id != null
|
||||
? () {
|
||||
if (isBranch) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/company/edit',
|
||||
arguments: {
|
||||
'companyId':
|
||||
companyData['companyId'],
|
||||
'isBranch': true,
|
||||
'mainCompanyName':
|
||||
mainCompanyName,
|
||||
'branchId': company.id,
|
||||
},
|
||||
).then((result) {
|
||||
if (result == true)
|
||||
controller.refresh();
|
||||
});
|
||||
} else {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/company/edit',
|
||||
arguments: {
|
||||
'companyId': company.id,
|
||||
'isBranch': false,
|
||||
},
|
||||
).then((result) {
|
||||
if (result == true)
|
||||
controller.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
onDelete:
|
||||
(!isBranch && company.id != null)
|
||||
? () => _deleteCompany(company.id!)
|
||||
: null,
|
||||
onEdit: item.id != null
|
||||
? () {
|
||||
if (item.isBranch) {
|
||||
// 지점 수정 - 별도 화면으로 이동 (Phase 3에서 구현)
|
||||
// TODO: Phase 3에서 별도 지점 수정 화면 구현
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/company/branch/edit',
|
||||
arguments: {
|
||||
'companyId': item.parentCompanyId,
|
||||
'branchId': item.id,
|
||||
'parentCompanyName': item.parentCompanyName,
|
||||
},
|
||||
).then((result) {
|
||||
if (result == true) controller.refresh();
|
||||
});
|
||||
} else {
|
||||
// 본사 수정
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/company/edit',
|
||||
arguments: {
|
||||
'companyId': item.id,
|
||||
'isBranch': false,
|
||||
},
|
||||
).then((result) {
|
||||
if (result == true) controller.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
onDelete: item.id != null
|
||||
? () {
|
||||
if (item.isBranch) {
|
||||
// 지점 삭제
|
||||
_deleteBranch(item.parentCompanyId!, item.id!);
|
||||
} else {
|
||||
// 본사 삭제
|
||||
_deleteCompany(item.id!);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
167
lib/screens/company/controllers/branch_edit_form_controller.dart
Normal file
167
lib/screens/company/controllers/branch_edit_form_controller.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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는 위젯에서 자동 관리
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
// 회사 삭제
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/widgets/address_input.dart';
|
||||
import 'package:superport/screens/company/widgets/contact_info_widget.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
import 'package:superport/utils/phone_utils.dart';
|
||||
|
||||
class BranchCard extends StatefulWidget {
|
||||
final GlobalKey cardKey;
|
||||
final int index;
|
||||
final Branch branch;
|
||||
final TextEditingController nameController;
|
||||
final TextEditingController contactNameController;
|
||||
final TextEditingController contactPositionController;
|
||||
final TextEditingController contactPhoneController;
|
||||
final TextEditingController contactEmailController;
|
||||
final FocusNode focusNode;
|
||||
final List<String> positions;
|
||||
final List<String> phonePrefixes;
|
||||
final String selectedPhonePrefix;
|
||||
final ValueChanged<String> onNameChanged;
|
||||
final ValueChanged<Address> onAddressChanged;
|
||||
final ValueChanged<String> onContactNameChanged;
|
||||
final ValueChanged<String> onContactPositionChanged;
|
||||
final ValueChanged<String> onContactPhoneChanged;
|
||||
final ValueChanged<String> onContactEmailChanged;
|
||||
final ValueChanged<String> onPhonePrefixChanged;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const BranchCard({
|
||||
Key? key,
|
||||
required this.cardKey,
|
||||
required this.index,
|
||||
required this.branch,
|
||||
required this.nameController,
|
||||
required this.contactNameController,
|
||||
required this.contactPositionController,
|
||||
required this.contactPhoneController,
|
||||
required this.contactEmailController,
|
||||
required this.focusNode,
|
||||
required this.positions,
|
||||
required this.phonePrefixes,
|
||||
required this.selectedPhonePrefix,
|
||||
required this.onNameChanged,
|
||||
required this.onAddressChanged,
|
||||
required this.onContactNameChanged,
|
||||
required this.onContactPositionChanged,
|
||||
required this.onContactPhoneChanged,
|
||||
required this.onContactEmailChanged,
|
||||
required this.onPhonePrefixChanged,
|
||||
required this.onDelete,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_BranchCardState createState() => _BranchCardState();
|
||||
}
|
||||
|
||||
class _BranchCardState extends State<BranchCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// 화면의 빈 공간 터치 시 포커스 해제
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: Card(
|
||||
key: widget.cardKey,
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'지점 #${widget.index + 1}',
|
||||
style: ShadcnTheme.headingH6,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: widget.onDelete,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FormFieldWrapper(
|
||||
label: '지점명',
|
||||
isRequired: true,
|
||||
child: TextFormField(
|
||||
controller: widget.nameController,
|
||||
focusNode: widget.focusNode,
|
||||
decoration: const InputDecoration(hintText: '지점명을 입력하세요'),
|
||||
onChanged: widget.onNameChanged,
|
||||
validator: FormValidator.required('지점명은 필수입니다'),
|
||||
),
|
||||
),
|
||||
AddressInput(
|
||||
initialZipCode: widget.branch.address.zipCode,
|
||||
initialRegion: widget.branch.address.region,
|
||||
initialDetailAddress: widget.branch.address.detailAddress,
|
||||
onAddressChanged: (zipCode, region, detailAddress) {
|
||||
final address = Address(
|
||||
zipCode: zipCode,
|
||||
region: region,
|
||||
detailAddress: detailAddress,
|
||||
);
|
||||
widget.onAddressChanged(address);
|
||||
},
|
||||
),
|
||||
|
||||
// 담당자 정보 - ContactInfoWidget 사용
|
||||
ContactInfoWidget(
|
||||
title: '담당자 정보',
|
||||
contactNameController: widget.contactNameController,
|
||||
contactPositionController: widget.contactPositionController,
|
||||
contactPhoneController: widget.contactPhoneController,
|
||||
contactEmailController: widget.contactEmailController,
|
||||
positions: widget.positions,
|
||||
selectedPhonePrefix: widget.selectedPhonePrefix,
|
||||
phonePrefixes: widget.phonePrefixes,
|
||||
onPhonePrefixChanged: widget.onPhonePrefixChanged,
|
||||
onContactNameChanged: widget.onContactNameChanged,
|
||||
onContactPositionChanged: widget.onContactPositionChanged,
|
||||
onContactPhoneChanged: widget.onContactPhoneChanged,
|
||||
onContactEmailChanged: widget.onContactEmailChanged,
|
||||
compactMode: false, // compactMode를 false로 변경하여 한 줄로 표시
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../controllers/branch_form_controller.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/screens/company/widgets/contact_info_form.dart';
|
||||
import 'package:superport/screens/common/widgets/address_input.dart';
|
||||
import 'package:superport/screens/common/widgets/remark_input.dart';
|
||||
|
||||
/// 지점 입력 폼 위젯
|
||||
///
|
||||
/// BranchFormController를 받아서 입력 필드, 드롭다운, 포커스, 전화번호 등 UI/상태를 관리한다.
|
||||
class BranchFormWidget extends StatelessWidget {
|
||||
final BranchFormController controller;
|
||||
final int index;
|
||||
final void Function()? onRemove;
|
||||
final void Function(Address)? onAddressChanged;
|
||||
|
||||
const BranchFormWidget({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
required this.index,
|
||||
this.onRemove,
|
||||
this.onAddressChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
key: controller.cardKey,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller.nameController,
|
||||
focusNode: controller.focusNode,
|
||||
decoration: const InputDecoration(labelText: '지점명'),
|
||||
onChanged: (value) => controller.updateField('name', value),
|
||||
),
|
||||
),
|
||||
if (onRemove != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: onRemove,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 주소 입력: 회사와 동일한 AddressInput 위젯 사용
|
||||
AddressInput(
|
||||
initialZipCode: controller.branch.address.zipCode,
|
||||
initialRegion: controller.branch.address.region,
|
||||
initialDetailAddress: controller.branch.address.detailAddress,
|
||||
isRequired: false,
|
||||
onAddressChanged: (zipCode, region, detailAddress) {
|
||||
controller.updateAddress(
|
||||
Address(
|
||||
zipCode: zipCode,
|
||||
region: region,
|
||||
detailAddress: detailAddress,
|
||||
),
|
||||
);
|
||||
if (onAddressChanged != null) {
|
||||
onAddressChanged!(
|
||||
Address(
|
||||
zipCode: zipCode,
|
||||
region: region,
|
||||
detailAddress: detailAddress,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 담당자 정보 입력: ContactInfoForm 위젯으로 대체 (회사 담당자와 동일 UI)
|
||||
ContactInfoForm(
|
||||
contactNameController: controller.contactNameController,
|
||||
contactPositionController: controller.contactPositionController,
|
||||
contactPhoneController: controller.contactPhoneController,
|
||||
contactEmailController: controller.contactEmailController,
|
||||
positions: controller.positions,
|
||||
selectedPhonePrefix: controller.selectedPhonePrefix,
|
||||
phonePrefixes: controller.phonePrefixes,
|
||||
onPhonePrefixChanged: (value) {
|
||||
controller.updatePhonePrefix(value);
|
||||
},
|
||||
onNameSaved: (value) {
|
||||
controller.updateField('contactName', value ?? '');
|
||||
},
|
||||
onPositionSaved: (value) {
|
||||
controller.updateField('contactPosition', value ?? '');
|
||||
},
|
||||
onPhoneSaved: (value) {
|
||||
controller.updateField('contactPhone', value ?? '');
|
||||
},
|
||||
onEmailSaved: (value) {
|
||||
controller.updateField('contactEmail', value ?? '');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 비고 입력란
|
||||
RemarkInput(controller: controller.remarkController),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/company/widgets/contact_info_widget.dart';
|
||||
|
||||
/// 담당자 정보 폼
|
||||
///
|
||||
/// 회사 등록 및 수정 화면에서 사용되는 담당자 정보 입력 폼
|
||||
/// 내부적으로 공통 ContactInfoWidget을 사용하여 코드 재사용성 확보
|
||||
class ContactInfoForm extends StatelessWidget {
|
||||
final TextEditingController contactNameController;
|
||||
final TextEditingController contactPositionController;
|
||||
final TextEditingController contactPhoneController;
|
||||
final TextEditingController contactEmailController;
|
||||
final List<String> positions;
|
||||
final String selectedPhonePrefix;
|
||||
final List<String> phonePrefixes;
|
||||
final ValueChanged<String> onPhonePrefixChanged;
|
||||
final ValueChanged<String?> onNameSaved;
|
||||
final ValueChanged<String?> onPositionSaved;
|
||||
final ValueChanged<String?> onPhoneSaved;
|
||||
final ValueChanged<String?> onEmailSaved;
|
||||
|
||||
const ContactInfoForm({
|
||||
Key? key,
|
||||
required this.contactNameController,
|
||||
required this.contactPositionController,
|
||||
required this.contactPhoneController,
|
||||
required this.contactEmailController,
|
||||
required this.positions,
|
||||
required this.selectedPhonePrefix,
|
||||
required this.phonePrefixes,
|
||||
required this.onPhonePrefixChanged,
|
||||
required this.onNameSaved,
|
||||
required this.onPositionSaved,
|
||||
required this.onPhoneSaved,
|
||||
required this.onEmailSaved,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// ContactInfoWidget을 사용하여 담당자 정보 UI 구성
|
||||
return ContactInfoWidget(
|
||||
contactNameController: contactNameController,
|
||||
contactPositionController: contactPositionController,
|
||||
contactPhoneController: contactPhoneController,
|
||||
contactEmailController: contactEmailController,
|
||||
positions: positions,
|
||||
selectedPhonePrefix: selectedPhonePrefix,
|
||||
phonePrefixes: phonePrefixes,
|
||||
onPhonePrefixChanged: onPhonePrefixChanged,
|
||||
|
||||
// 각 콜백 함수를 ContactInfoWidget의 onChanged 콜백과 연결
|
||||
onContactNameChanged: (value) => onNameSaved?.call(value),
|
||||
onContactPositionChanged: (value) => onPositionSaved?.call(value),
|
||||
onContactPhoneChanged: (value) => onPhoneSaved?.call(value),
|
||||
onContactEmailChanged: (value) => onEmailSaved?.call(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,722 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:developer' as developer;
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
import 'package:superport/utils/phone_utils.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// 담당자 정보 위젯
|
||||
///
|
||||
/// 회사 및 지점의 담당자 정보를 입력받는 공통 위젯
|
||||
/// SRP(단일 책임 원칙)에 따라 담당자 정보 입력 로직을 분리
|
||||
class ContactInfoWidget extends StatefulWidget {
|
||||
/// 위젯 제목
|
||||
final String title;
|
||||
|
||||
/// 담당자 이름 컨트롤러
|
||||
final TextEditingController contactNameController;
|
||||
|
||||
/// 담당자 직책 컨트롤러
|
||||
final TextEditingController contactPositionController;
|
||||
|
||||
/// 담당자 전화번호 컨트롤러
|
||||
final TextEditingController contactPhoneController;
|
||||
|
||||
/// 담당자 이메일 컨트롤러
|
||||
final TextEditingController contactEmailController;
|
||||
|
||||
/// 직책 목록
|
||||
final List<String> positions;
|
||||
|
||||
/// 선택된 전화번호 접두사
|
||||
final String selectedPhonePrefix;
|
||||
|
||||
/// 전화번호 접두사 목록
|
||||
final List<String> phonePrefixes;
|
||||
|
||||
/// 직책 컴팩트 모드 (Row 또는 Column 레이아웃 결정)
|
||||
final bool compactMode;
|
||||
|
||||
/// 전화번호 접두사 변경 콜백
|
||||
final ValueChanged<String> onPhonePrefixChanged;
|
||||
|
||||
/// 담당자 이름 변경 콜백
|
||||
final ValueChanged<String> onContactNameChanged;
|
||||
|
||||
/// 담당자 직책 변경 콜백
|
||||
final ValueChanged<String> onContactPositionChanged;
|
||||
|
||||
/// 담당자 전화번호 변경 콜백
|
||||
final ValueChanged<String> onContactPhoneChanged;
|
||||
|
||||
/// 담당자 이메일 변경 콜백
|
||||
final ValueChanged<String> onContactEmailChanged;
|
||||
|
||||
const ContactInfoWidget({
|
||||
Key? key,
|
||||
this.title = '담당자 정보',
|
||||
required this.contactNameController,
|
||||
required this.contactPositionController,
|
||||
required this.contactPhoneController,
|
||||
required this.contactEmailController,
|
||||
required this.positions,
|
||||
required this.selectedPhonePrefix,
|
||||
required this.phonePrefixes,
|
||||
required this.onPhonePrefixChanged,
|
||||
required this.onContactNameChanged,
|
||||
required this.onContactPositionChanged,
|
||||
required this.onContactPhoneChanged,
|
||||
required this.onContactEmailChanged,
|
||||
this.compactMode = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ContactInfoWidget> createState() => _ContactInfoWidgetState();
|
||||
}
|
||||
|
||||
class _ContactInfoWidgetState extends State<ContactInfoWidget> {
|
||||
bool _showPositionDropdown = false;
|
||||
bool _showPhonePrefixDropdown = false;
|
||||
final LayerLink _positionLayerLink = LayerLink();
|
||||
final LayerLink _phonePrefixLayerLink = LayerLink();
|
||||
|
||||
OverlayEntry? _positionOverlayEntry;
|
||||
OverlayEntry? _phonePrefixOverlayEntry;
|
||||
|
||||
final FocusNode _positionFocusNode = FocusNode();
|
||||
final FocusNode _phonePrefixFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
developer.log('ContactInfoWidget 초기화 완료', name: 'ContactInfoWidget');
|
||||
|
||||
_positionFocusNode.addListener(() {
|
||||
if (_positionFocusNode.hasFocus) {
|
||||
developer.log('직책 필드 포커스 얻음', name: 'ContactInfoWidget');
|
||||
} else {
|
||||
developer.log('직책 필드 포커스 잃음', name: 'ContactInfoWidget');
|
||||
}
|
||||
});
|
||||
|
||||
_phonePrefixFocusNode.addListener(() {
|
||||
if (_phonePrefixFocusNode.hasFocus) {
|
||||
developer.log('전화번호 접두사 필드 포커스 얻음', name: 'ContactInfoWidget');
|
||||
} else {
|
||||
developer.log('전화번호 접두사 필드 포커스 잃음', name: 'ContactInfoWidget');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_removeAllOverlays();
|
||||
_positionFocusNode.dispose();
|
||||
_phonePrefixFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _togglePositionDropdown() {
|
||||
developer.log(
|
||||
'직책 드롭다운 토글: $_showPositionDropdown -> ${!_showPositionDropdown}',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
setState(() {
|
||||
if (_showPositionDropdown) {
|
||||
_removePositionOverlay();
|
||||
} else {
|
||||
_showPositionDropdown = true;
|
||||
_showPhonePrefixDropdown = false;
|
||||
_removePhonePrefixOverlay();
|
||||
_showPositionOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _togglePhonePrefixDropdown() {
|
||||
developer.log(
|
||||
'전화번호 접두사 드롭다운 토글: $_showPhonePrefixDropdown -> ${!_showPhonePrefixDropdown}',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
setState(() {
|
||||
if (_showPhonePrefixDropdown) {
|
||||
_removePhonePrefixOverlay();
|
||||
} else {
|
||||
_showPhonePrefixDropdown = true;
|
||||
_showPositionDropdown = false;
|
||||
_removePositionOverlay();
|
||||
_showPhonePrefixOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _removePositionOverlay() {
|
||||
_positionOverlayEntry?.remove();
|
||||
_positionOverlayEntry = null;
|
||||
_showPositionDropdown = false;
|
||||
}
|
||||
|
||||
void _removePhonePrefixOverlay() {
|
||||
_phonePrefixOverlayEntry?.remove();
|
||||
_phonePrefixOverlayEntry = null;
|
||||
_showPhonePrefixDropdown = false;
|
||||
}
|
||||
|
||||
void _removeAllOverlays() {
|
||||
_removePositionOverlay();
|
||||
_removePhonePrefixOverlay();
|
||||
}
|
||||
|
||||
void _closeAllDropdowns() {
|
||||
if (_showPositionDropdown || _showPhonePrefixDropdown) {
|
||||
developer.log('모든 드롭다운 닫기', name: 'ContactInfoWidget');
|
||||
setState(() {
|
||||
_removeAllOverlays();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _showPositionOverlay() {
|
||||
final RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
final availableHeight =
|
||||
MediaQuery.of(context).size.height - offset.dy - 100;
|
||||
final maxHeight = math.min(300.0, availableHeight);
|
||||
|
||||
_positionOverlayEntry = OverlayEntry(
|
||||
builder:
|
||||
(context) => Positioned(
|
||||
width: 200,
|
||||
child: CompositedTransformFollower(
|
||||
link: _positionLayerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: const Offset(0, 45),
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...widget.positions.map(
|
||||
(position) => InkWell(
|
||||
onTap: () {
|
||||
developer.log(
|
||||
'직책 선택됨: $position',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
setState(() {
|
||||
widget.contactPositionController.text =
|
||||
position;
|
||||
widget.onContactPositionChanged(position);
|
||||
_removePositionOverlay();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Text(position),
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
developer.log(
|
||||
'직책 기타(직접 입력) 선택됨',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
_removePositionOverlay();
|
||||
widget.contactPositionController.clear();
|
||||
widget.onContactPositionChanged('');
|
||||
_positionFocusNode.requestFocus();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: const Text('기타 (직접 입력)'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_positionOverlayEntry!);
|
||||
}
|
||||
|
||||
void _showPhonePrefixOverlay() {
|
||||
final RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
final availableHeight =
|
||||
MediaQuery.of(context).size.height - offset.dy - 100;
|
||||
final maxHeight = math.min(300.0, availableHeight);
|
||||
|
||||
_phonePrefixOverlayEntry = OverlayEntry(
|
||||
builder:
|
||||
(context) => Positioned(
|
||||
width: 200,
|
||||
child: CompositedTransformFollower(
|
||||
link: _phonePrefixLayerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: const Offset(0, 45),
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...widget.phonePrefixes.map(
|
||||
(prefix) => InkWell(
|
||||
onTap: () {
|
||||
developer.log(
|
||||
'전화번호 접두사 선택됨: $prefix',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
widget.onPhonePrefixChanged(prefix);
|
||||
setState(() {
|
||||
_removePhonePrefixOverlay();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Text(prefix),
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
developer.log(
|
||||
'전화번호 접두사 직접 입력 선택됨',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
_removePhonePrefixOverlay();
|
||||
_phonePrefixFocusNode.requestFocus();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: const Text('기타 (직접 입력)'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_phonePrefixOverlayEntry!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
developer.log(
|
||||
'ContactInfoWidget 빌드 시작: 직책 드롭다운=$_showPositionDropdown, 전화번호 접두사 드롭다운=$_showPhonePrefixDropdown',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
|
||||
// 컴팩트 모드에 따라 다른 레이아웃 생성
|
||||
return FormFieldWrapper(
|
||||
label: widget.title,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children:
|
||||
widget.compactMode ? _buildCompactLayout() : _buildDefaultLayout(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 기본 레이아웃 (한 줄에 모든 필드 표시)
|
||||
List<Widget> _buildDefaultLayout() {
|
||||
return [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 담당자 이름
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextFormField(
|
||||
controller: widget.contactNameController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '이름',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
developer.log('이름 필드 터치됨', name: 'ContactInfoWidget');
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
onChanged: widget.onContactNameChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 담당자 직책
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: CompositedTransformTarget(
|
||||
link: _positionLayerLink,
|
||||
child: Stack(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: widget.contactPositionController,
|
||||
focusNode: _positionFocusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: '직책',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_down, size: 20),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
developer.log(
|
||||
'직책 드롭다운 버튼 클릭됨',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
_togglePositionDropdown();
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
// 필드를 터치했을 때는 드롭다운을 열지 않고 직접 입력 모드로 진입
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
onChanged: widget.onContactPositionChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 전화번호 접두사
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: CompositedTransformTarget(
|
||||
link: _phonePrefixLayerLink,
|
||||
child: Stack(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: TextEditingController(
|
||||
text: widget.selectedPhonePrefix,
|
||||
),
|
||||
focusNode: _phonePrefixFocusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: '국가번호',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_down, size: 20),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
developer.log(
|
||||
'전화번호 접두사 드롭다운 버튼 클릭됨',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
_togglePhonePrefixDropdown();
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
// 필드를 터치했을 때는 드롭다운을 열지 않고 직접 입력 모드로 진입
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
onChanged: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
widget.onPhonePrefixChanged(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 담당자 전화번호
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextFormField(
|
||||
controller: widget.contactPhoneController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '전화번호',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
// 접두사에 따른 동적 포맷팅
|
||||
TextInputFormatter.withFunction((oldValue, newValue) {
|
||||
final formatted = PhoneUtils.formatPhoneNumberByPrefix(
|
||||
widget.selectedPhonePrefix,
|
||||
newValue.text,
|
||||
);
|
||||
return TextEditingValue(
|
||||
text: formatted,
|
||||
selection: TextSelection.collapsed(offset: formatted.length),
|
||||
);
|
||||
}),
|
||||
],
|
||||
onTap: () {
|
||||
developer.log('전화번호 필드 터치됨', name: 'ContactInfoWidget');
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
validator: validatePhoneNumber,
|
||||
onChanged: widget.onContactPhoneChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 담당자 이메일
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: TextFormField(
|
||||
controller: widget.contactEmailController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '이메일',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onTap: () {
|
||||
developer.log('이메일 필드 터치됨', name: 'ContactInfoWidget');
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
validator: FormValidator.email(),
|
||||
onChanged: widget.onContactEmailChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// 컴팩트 레이아웃 (여러 줄에 필드 표시)
|
||||
List<Widget> _buildCompactLayout() {
|
||||
return [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 담당자 이름
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.contactNameController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '담당자 이름',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
developer.log('이름 필드 터치됨', name: 'ContactInfoWidget');
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
onChanged: widget.onContactNameChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 담당자 직책
|
||||
Expanded(
|
||||
child: CompositedTransformTarget(
|
||||
link: _positionLayerLink,
|
||||
child: InkWell(
|
||||
onTap: _togglePositionDropdown,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 15,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.contactPositionController.text.isEmpty
|
||||
? '직책 선택'
|
||||
: widget.contactPositionController.text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color:
|
||||
widget.contactPositionController.text.isEmpty
|
||||
? Colors.grey.shade600
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 전화번호 (접두사 + 번호)
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// 전화번호 접두사
|
||||
CompositedTransformTarget(
|
||||
link: _phonePrefixLayerLink,
|
||||
child: InkWell(
|
||||
onTap: _togglePhonePrefixDropdown,
|
||||
child: Container(
|
||||
width: 70,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 14,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: const BorderRadius.horizontal(
|
||||
left: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.selectedPhonePrefix,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 전화번호
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.contactPhoneController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '전화번호',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.horizontal(
|
||||
left: Radius.zero,
|
||||
right: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
// 접두사에 따른 동적 포맷팅
|
||||
TextInputFormatter.withFunction((oldValue, newValue) {
|
||||
final formatted = PhoneUtils.formatPhoneNumberByPrefix(
|
||||
widget.selectedPhonePrefix,
|
||||
newValue.text,
|
||||
);
|
||||
return TextEditingValue(
|
||||
text: formatted,
|
||||
selection: TextSelection.collapsed(offset: formatted.length),
|
||||
);
|
||||
}),
|
||||
],
|
||||
keyboardType: TextInputType.phone,
|
||||
onTap: _closeAllDropdowns,
|
||||
onChanged: widget.onContactPhoneChanged,
|
||||
validator: validatePhoneNumber,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 이메일
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.contactEmailController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '이메일 주소',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onTap: _closeAllDropdowns,
|
||||
onChanged: widget.onContactEmailChanged,
|
||||
validator: FormValidator.email(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,13 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
List<String> warehouseLocations = [];
|
||||
List<String> partnerCompanies = [];
|
||||
|
||||
// 새로운 필드들 (백엔드 API 구조 변경 대응)
|
||||
int? currentCompanyId;
|
||||
int? currentBranchId;
|
||||
DateTime? lastInspectionDate;
|
||||
DateTime? nextInspectionDate;
|
||||
String? equipmentStatus;
|
||||
|
||||
final TextEditingController remarkController = TextEditingController();
|
||||
|
||||
EquipmentInFormController({this.equipmentInId}) {
|
||||
@@ -234,6 +241,13 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
warrantyStartDate = equipment.warrantyStartDate ?? DateTime.now();
|
||||
warrantyEndDate = equipment.warrantyEndDate ?? DateTime.now().add(const Duration(days: 365));
|
||||
|
||||
// 새로운 필드들 설정 (백엔드 API에서 제공되면 사용, 아니면 기본값)
|
||||
currentCompanyId = equipment.currentCompanyId;
|
||||
currentBranchId = equipment.currentBranchId;
|
||||
lastInspectionDate = equipment.lastInspectionDate;
|
||||
nextInspectionDate = equipment.nextInspectionDate;
|
||||
equipmentStatus = equipment.equipmentStatus ?? 'available'; // 기본값: 사용 가능
|
||||
|
||||
// 입고 관련 정보는 현재 API에서 제공하지 않으므로 기본값 사용
|
||||
inDate = equipment.inDate ?? DateTime.now();
|
||||
equipmentType = EquipmentType.new_;
|
||||
@@ -337,6 +351,12 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
warrantyLicense: warrantyLicense,
|
||||
warrantyStartDate: warrantyStartDate,
|
||||
warrantyEndDate: warrantyEndDate,
|
||||
// 새로운 필드들 추가
|
||||
currentCompanyId: currentCompanyId,
|
||||
currentBranchId: currentBranchId,
|
||||
lastInspectionDate: lastInspectionDate,
|
||||
nextInspectionDate: nextInspectionDate,
|
||||
equipmentStatus: equipmentStatus,
|
||||
// 워런티 코드 저장 필요시 여기에 추가
|
||||
);
|
||||
|
||||
|
||||
@@ -2374,6 +2374,184 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// 현재 위치 및 상태 정보 섹션
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 현재 회사 및 지점 정보
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FormFieldWrapper(
|
||||
label: '현재 회사',
|
||||
required: false,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _controller.currentCompanyId?.toString(),
|
||||
decoration: const InputDecoration(
|
||||
hintText: '현재 배치된 회사를 선택하세요',
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: null, child: Text('선택하지 않음')),
|
||||
// TODO: 실제 회사 목록으로 대체 필요
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.currentCompanyId = value != null ? int.tryParse(value) : null;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FormFieldWrapper(
|
||||
label: '현재 지점',
|
||||
required: false,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _controller.currentBranchId?.toString(),
|
||||
decoration: const InputDecoration(
|
||||
hintText: '현재 배치된 지점을 선택하세요',
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: null, child: Text('선택하지 않음')),
|
||||
// TODO: 실제 지점 목록으로 대체 필요
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.currentBranchId = value != null ? int.tryParse(value) : null;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 점검 날짜 정보
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FormFieldWrapper(
|
||||
label: '최근 점검일',
|
||||
required: false,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _controller.lastInspectionDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_controller.lastInspectionDate = picked;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 15,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_controller.lastInspectionDate != null
|
||||
? '${_controller.lastInspectionDate!.year}-${_controller.lastInspectionDate!.month.toString().padLeft(2, '0')}-${_controller.lastInspectionDate!.day.toString().padLeft(2, '0')}'
|
||||
: '날짜를 선택하세요',
|
||||
style: TextStyle(
|
||||
color: _controller.lastInspectionDate != null
|
||||
? Colors.black87
|
||||
: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FormFieldWrapper(
|
||||
label: '다음 점검일',
|
||||
required: false,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _controller.nextInspectionDate ?? DateTime.now().add(const Duration(days: 365)),
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_controller.nextInspectionDate = picked;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 15,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_controller.nextInspectionDate != null
|
||||
? '${_controller.nextInspectionDate!.year}-${_controller.nextInspectionDate!.month.toString().padLeft(2, '0')}-${_controller.nextInspectionDate!.day.toString().padLeft(2, '0')}'
|
||||
: '날짜를 선택하세요',
|
||||
style: TextStyle(
|
||||
color: _controller.nextInspectionDate != null
|
||||
? Colors.black87
|
||||
: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 장비 상태
|
||||
const SizedBox(height: 16),
|
||||
FormFieldWrapper(
|
||||
label: '장비 상태',
|
||||
required: false,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _controller.equipmentStatus,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '장비 상태를 선택하세요',
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'available', child: Text('사용 가능')),
|
||||
DropdownMenuItem(value: 'inuse', child: Text('사용 중')),
|
||||
DropdownMenuItem(value: 'maintenance', child: Text('유지보수')),
|
||||
DropdownMenuItem(value: 'disposed', child: Text('폐기')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.equipmentStatus = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 비고 입력란 추가
|
||||
const SizedBox(height: 16),
|
||||
FormFieldWrapper(
|
||||
|
||||
@@ -759,12 +759,9 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
if (_showDetailedColumns) {
|
||||
totalWidth += 120; // 시리얼번호
|
||||
totalWidth += 120; // 바코드
|
||||
|
||||
// 출고 정보 (조건부)
|
||||
if (pagedEquipments.any((e) => e.status == EquipmentStatus.out || e.status == EquipmentStatus.rent)) {
|
||||
totalWidth += 120; // 회사
|
||||
totalWidth += 80; // 담당자
|
||||
}
|
||||
totalWidth += 120; // 현재 위치
|
||||
totalWidth += 100; // 창고 위치
|
||||
totalWidth += 100; // 점검일
|
||||
}
|
||||
|
||||
// padding 추가 (좌우 각 16px)
|
||||
@@ -865,10 +862,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
_buildHeaderCell('상태', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
// 날짜
|
||||
_buildHeaderCell('날짜', flex: 2, useExpanded: useExpanded, minWidth: 80),
|
||||
// 출고 정보 (조건부)
|
||||
if (_showDetailedColumns && hasOutOrRent) ...[
|
||||
_buildHeaderCell('회사', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
_buildHeaderCell('담당자', flex: 2, useExpanded: useExpanded, minWidth: 80),
|
||||
// 상세 정보 (조건부)
|
||||
if (_showDetailedColumns) ...[
|
||||
_buildHeaderCell('현재 위치', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
_buildHeaderCell('창고 위치', flex: 2, useExpanded: useExpanded, minWidth: 100),
|
||||
_buildHeaderCell('점검일', flex: 2, useExpanded: useExpanded, minWidth: 100),
|
||||
],
|
||||
// 관리
|
||||
_buildHeaderCell('관리', flex: 2, useExpanded: useExpanded, minWidth: 90),
|
||||
@@ -989,25 +987,34 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 80,
|
||||
),
|
||||
// 출고 정보 (조건부)
|
||||
if (_showDetailedColumns && hasOutOrRent) ...[
|
||||
// 상세 정보 (조건부)
|
||||
if (_showDetailedColumns) ...[
|
||||
// 현재 위치 (회사 + 지점)
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
'-', // TODO: 출고 정보 추가 필요
|
||||
'-',
|
||||
_buildCurrentLocationText(equipment),
|
||||
_buildCurrentLocationText(equipment),
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
// 창고 위치
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'-', // TODO: 담당자 정보 추가 필요
|
||||
equipment.warehouseLocation ?? '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 80,
|
||||
minWidth: 100,
|
||||
),
|
||||
// 점검일 (최근/다음)
|
||||
_buildDataCell(
|
||||
_buildInspectionDateWidget(equipment),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 100,
|
||||
),
|
||||
],
|
||||
// 관리
|
||||
@@ -1325,4 +1332,50 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/// 현재 위치 텍스트 생성 (회사명 + 지점명)
|
||||
String _buildCurrentLocationText(UnifiedEquipment equipment) {
|
||||
final currentCompany = equipment.currentCompany ?? '-';
|
||||
final currentBranch = equipment.currentBranch ?? '';
|
||||
|
||||
if (currentBranch.isNotEmpty) {
|
||||
return '$currentCompany ($currentBranch)';
|
||||
} else {
|
||||
return currentCompany;
|
||||
}
|
||||
}
|
||||
|
||||
/// 점검일 위젯 생성 (최근 점검일/다음 점검일)
|
||||
Widget _buildInspectionDateWidget(UnifiedEquipment equipment) {
|
||||
final lastInspection = equipment.lastInspectionDate;
|
||||
final nextInspection = equipment.nextInspectionDate;
|
||||
|
||||
String displayText = '-';
|
||||
Color? textColor;
|
||||
|
||||
if (nextInspection != null) {
|
||||
final now = DateTime.now();
|
||||
final difference = nextInspection.difference(now).inDays;
|
||||
|
||||
if (difference < 0) {
|
||||
displayText = '점검 필요';
|
||||
textColor = Colors.red;
|
||||
} else if (difference <= 30) {
|
||||
displayText = '${difference}일 후';
|
||||
textColor = Colors.orange;
|
||||
} else {
|
||||
displayText = '${nextInspection.month}/${nextInspection.day}';
|
||||
textColor = Colors.green;
|
||||
}
|
||||
} else if (lastInspection != null) {
|
||||
displayText = '${lastInspection.month}/${lastInspection.day}';
|
||||
}
|
||||
|
||||
return Text(
|
||||
displayText,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: textColor ?? ShadcnTheme.bodySmall.color,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,11 +467,36 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
},
|
||||
),
|
||||
|
||||
// 장비 상태 변경 (출고 시 'inuse'로 자동 설정)
|
||||
const Text('장비 상태 설정', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
DropdownButtonFormField<String>(
|
||||
value: 'inuse', // 출고 시 기본값
|
||||
decoration: const InputDecoration(
|
||||
hintText: '출고 후 장비 상태',
|
||||
labelText: '출고 후 상태 *',
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'inuse', child: Text('사용 중')),
|
||||
DropdownMenuItem(value: 'maintenance', child: Text('유지보수')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
// controller.equipmentStatus = value; // TODO: 컨트롤러에 추가 필요
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '출고 후 상태를 선택해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 출고 회사 영역 헤더
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('출고 회사', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const Text('출고 회사 *', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
controller.addCompany();
|
||||
|
||||
@@ -17,8 +17,19 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
/// 비고 입력 컨트롤러
|
||||
final TextEditingController remarkController = TextEditingController();
|
||||
|
||||
/// 주소 정보
|
||||
Address _address = const Address();
|
||||
/// 담당자명 입력 컨트롤러
|
||||
final TextEditingController managerNameController = TextEditingController();
|
||||
|
||||
/// 담당자 연락처 입력 컨트롤러
|
||||
final TextEditingController managerPhoneController = TextEditingController();
|
||||
|
||||
/// 수용량 입력 컨트롤러
|
||||
final TextEditingController capacityController = TextEditingController();
|
||||
|
||||
/// 주소 입력 컨트롤러 (단일 필드)
|
||||
final TextEditingController addressController = TextEditingController();
|
||||
|
||||
/// 백엔드 API에 맞는 단순 필드들 (주소는 단일 String)
|
||||
|
||||
/// 저장 중 여부
|
||||
bool _isSaving = false;
|
||||
@@ -53,7 +64,6 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Getters
|
||||
Address get address => _address;
|
||||
bool get isSaving => _isSaving;
|
||||
bool get isEditMode => _isEditMode;
|
||||
int? get id => _id;
|
||||
@@ -74,8 +84,11 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
|
||||
if (_originalLocation != null) {
|
||||
nameController.text = _originalLocation!.name;
|
||||
_address = _originalLocation!.address;
|
||||
addressController.text = _originalLocation!.address ?? '';
|
||||
remarkController.text = _originalLocation!.remark ?? '';
|
||||
managerNameController.text = _originalLocation!.managerName ?? '';
|
||||
managerPhoneController.text = _originalLocation!.managerPhone ?? '';
|
||||
capacityController.text = _originalLocation!.capacity?.toString() ?? '';
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
@@ -85,11 +98,6 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 주소 변경 처리
|
||||
void updateAddress(Address newAddress) {
|
||||
_address = newAddress;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 저장 처리 (추가/수정)
|
||||
Future<bool> save() async {
|
||||
@@ -103,8 +111,13 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
final location = WarehouseLocation(
|
||||
id: _isEditMode ? _id! : 0,
|
||||
name: nameController.text.trim(),
|
||||
address: _address,
|
||||
address: addressController.text.trim().isEmpty ? null : addressController.text.trim(),
|
||||
remark: remarkController.text.trim().isEmpty ? null : remarkController.text.trim(),
|
||||
managerName: managerNameController.text.trim().isEmpty ? null : managerNameController.text.trim(),
|
||||
managerPhone: managerPhoneController.text.trim().isEmpty ? null : managerPhoneController.text.trim(),
|
||||
capacity: capacityController.text.trim().isEmpty ? null : int.tryParse(capacityController.text.trim()),
|
||||
isActive: true, // 새로 생성 시 항상 활성화
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
if (_isEditMode) {
|
||||
@@ -127,8 +140,11 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
/// 폼 초기화
|
||||
void resetForm() {
|
||||
nameController.clear();
|
||||
addressController.clear();
|
||||
remarkController.clear();
|
||||
_address = const Address();
|
||||
managerNameController.clear();
|
||||
managerPhoneController.clear();
|
||||
capacityController.clear();
|
||||
_error = null;
|
||||
formKey.currentState?.reset();
|
||||
notifyListeners();
|
||||
@@ -145,9 +161,28 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateAddress() {
|
||||
if (_address.isEmpty) {
|
||||
return '주소를 입력해주세요';
|
||||
|
||||
/// 수용량 유효성 검사
|
||||
String? validateCapacity(String? value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final capacity = int.tryParse(value);
|
||||
if (capacity == null) {
|
||||
return '올바른 숫자를 입력해주세요';
|
||||
}
|
||||
if (capacity < 0) {
|
||||
return '수용량은 0 이상이어야 합니다';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 전화번호 유효성 검사
|
||||
String? validatePhoneNumber(String? value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
// 기본적인 전화번호 형식 검사 (숫자, 하이픈 허용)
|
||||
if (!RegExp(r'^[0-9-]+$').hasMatch(value)) {
|
||||
return '올바른 전화번호 형식을 입력해주세요';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -156,7 +191,11 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
addressController.dispose();
|
||||
remarkController.dispose();
|
||||
managerNameController.dispose();
|
||||
managerPhoneController.dispose();
|
||||
capacityController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/screens/common/widgets/address_input.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:superport/screens/common/widgets/remark_input.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
@@ -81,47 +80,78 @@ class _WarehouseLocationFormScreenState
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UIConstants.formPadding),
|
||||
child: FormSection(
|
||||
title: '입고지 정보',
|
||||
subtitle: '입고지의 기본 정보를 입력하세요',
|
||||
title: '창고 정보',
|
||||
subtitle: '창고의 기본 정보를 입력하세요',
|
||||
children: [
|
||||
// 입고지명 입력
|
||||
FormFieldWrapper(
|
||||
label: '입고지명',
|
||||
label: '창고명',
|
||||
required: true,
|
||||
child: TextFormField(
|
||||
controller: _controller.nameController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '입고지명을 입력하세요',
|
||||
hintText: '창고명을 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '입고지명을 입력하세요';
|
||||
return '창고명을 입력하세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
// 주소 입력 (공통 위젯)
|
||||
// 주소 입력 (단일 필드)
|
||||
FormFieldWrapper(
|
||||
label: '주소',
|
||||
required: true,
|
||||
child: AddressInput(
|
||||
initialZipCode: _controller.address.zipCode,
|
||||
initialRegion: _controller.address.region,
|
||||
initialDetailAddress: _controller.address.detailAddress,
|
||||
isRequired: true,
|
||||
onAddressChanged: (zip, region, detail) {
|
||||
setState(() {
|
||||
_controller.updateAddress(
|
||||
Address(
|
||||
zipCode: zip,
|
||||
region: region,
|
||||
detailAddress: detail,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
child: TextFormField(
|
||||
controller: _controller.addressController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '주소를 입력하세요 (예: 경기도 용인시 기흥구 동백로 123)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
// 담당자명 입력
|
||||
FormFieldWrapper(
|
||||
label: '담당자명',
|
||||
child: TextFormField(
|
||||
controller: _controller.managerNameController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '담당자명을 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 담당자 연락처 입력
|
||||
FormFieldWrapper(
|
||||
label: '담당자 연락처',
|
||||
child: TextFormField(
|
||||
controller: _controller.managerPhoneController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '연락처를 입력하세요 (예: 02-1234-5678)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: _controller.validatePhoneNumber,
|
||||
),
|
||||
),
|
||||
// 수용량 입력
|
||||
FormFieldWrapper(
|
||||
label: '수용량',
|
||||
child: TextFormField(
|
||||
controller: _controller.capacityController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '수용량을 입력하세요 (개)',
|
||||
border: OutlineInputBorder(),
|
||||
suffixText: '개',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
validator: _controller.validateCapacity,
|
||||
),
|
||||
),
|
||||
// 비고 입력
|
||||
|
||||
@@ -15,7 +15,7 @@ import 'package:superport/services/auth_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/core/widgets/auth_guard.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 입고지 관리 화면
|
||||
/// shadcn/ui 스타일로 재설계된 창고 관리 화면
|
||||
class WarehouseLocationList extends StatefulWidget {
|
||||
const WarehouseLocationList({Key? key}) : super(key: key);
|
||||
|
||||
@@ -62,7 +62,7 @@ class _WarehouseLocationListState
|
||||
_controller.refresh(); // Controller에서 페이지 리셋 처리
|
||||
}
|
||||
|
||||
/// 입고지 추가 폼으로 이동
|
||||
/// 창고 추가 폼으로 이동
|
||||
void _navigateToAdd() async {
|
||||
final result = await Navigator.pushNamed(
|
||||
context,
|
||||
@@ -73,7 +73,7 @@ class _WarehouseLocationListState
|
||||
}
|
||||
}
|
||||
|
||||
/// 입고지 수정 폼으로 이동
|
||||
/// 창고 수정 폼으로 이동
|
||||
void _navigateToEdit(WarehouseLocation location) async {
|
||||
final result = await Navigator.pushNamed(
|
||||
context,
|
||||
@@ -91,7 +91,7 @@ class _WarehouseLocationListState
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('입고지 삭제'),
|
||||
title: const Text('창고 삭제'),
|
||||
content: const Text('정말로 삭제하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -110,6 +110,11 @@ class _WarehouseLocationListState
|
||||
);
|
||||
}
|
||||
|
||||
/// 날짜 포맷팅 함수
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Admin과 Manager만 접근 가능
|
||||
@@ -130,13 +135,13 @@ class _WarehouseLocationListState
|
||||
emptyMessage:
|
||||
controller.searchQuery.isNotEmpty
|
||||
? '검색 결과가 없습니다'
|
||||
: '등록된 입고지가 없습니다',
|
||||
: '등록된 창고가 없습니다',
|
||||
emptyIcon: Icons.warehouse_outlined,
|
||||
|
||||
// 검색바
|
||||
searchBar: UnifiedSearchBar(
|
||||
controller: _searchController,
|
||||
placeholder: '창고명, 주소로 검색',
|
||||
placeholder: '창고명, 주소, 담당자로 검색',
|
||||
onChanged: (value) => _controller.search(value),
|
||||
onSearch: () => _controller.search(_searchController.text),
|
||||
onClear: () {
|
||||
@@ -149,7 +154,7 @@ class _WarehouseLocationListState
|
||||
actionBar: StandardActionBar(
|
||||
leftActions: [
|
||||
ShadcnButton(
|
||||
text: '입고지 추가',
|
||||
text: '창고 추가',
|
||||
onPressed: _navigateToAdd,
|
||||
variant: ShadcnButtonVariant.primary,
|
||||
textColor: Colors.white,
|
||||
@@ -211,7 +216,7 @@ class _WarehouseLocationListState
|
||||
action:
|
||||
_controller.searchQuery.isEmpty
|
||||
? StandardActionButtons.addButton(
|
||||
text: '첫 입고지 추가하기',
|
||||
text: '첫 창고 추가하기',
|
||||
onPressed: _navigateToAdd,
|
||||
)
|
||||
: null,
|
||||
@@ -246,16 +251,32 @@ class _WarehouseLocationListState
|
||||
child: Text('번호', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text('입고지명', style: ShadcnTheme.bodyMedium),
|
||||
flex: 2,
|
||||
child: Text('창고명', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
flex: 3,
|
||||
child: Text('주소', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text('비고', style: ShadcnTheme.bodyMedium),
|
||||
child: Text('담당자', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text('연락처', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text('수용량', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text('상태', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text('생성일', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
@@ -290,28 +311,85 @@ class _WarehouseLocationListState
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// 입고지명
|
||||
// 창고명
|
||||
Expanded(
|
||||
flex: 3,
|
||||
flex: 2,
|
||||
child: Text(
|
||||
location.name,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// 주소
|
||||
Expanded(
|
||||
flex: 4,
|
||||
flex: 3,
|
||||
child: Text(
|
||||
'${location.address.region} ${location.address.detailAddress}',
|
||||
location.address ?? '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// 비고
|
||||
// 담당자
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
location.remark ?? '-',
|
||||
location.managerName ?? '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// 연락처
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
location.managerPhone ?? '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// 수용량
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
location.capacity?.toString() ?? '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
// 상태
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: location.isActive
|
||||
? ShadcnTheme.success.withOpacity(0.1)
|
||||
: ShadcnTheme.muted.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: location.isActive
|
||||
? ShadcnTheme.success.withOpacity(0.3)
|
||||
: ShadcnTheme.muted.withOpacity(0.3)
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
location.isActive ? '활성' : '비활성',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: location.isActive
|
||||
? ShadcnTheme.success
|
||||
: ShadcnTheme.muted,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 생성일
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
_formatDate(location.createdAt),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user