feat: 대규모 코드베이스 개선 - 백엔드 통합성 강화 및 UI 일관성 완성
- CLAUDE.md 대폭 개선: 개발 가이드라인 및 프로젝트 상태 문서화 - 백엔드 API 통합: 모든 엔티티 간 Foreign Key 관계 완벽 구현 - UI 일관성 강화: shadcn_ui 컴포넌트 표준화 적용 - 데이터 모델 개선: DTO 및 모델 클래스 백엔드 스키마와 100% 일치 - 사용자 관리: 회사 연결, 중복 검사, 입력 검증 기능 추가 - 창고 관리: 우편번호 연결, 중복 검사 기능 강화 - 회사 관리: 우편번호 연결, 중복 검사 로직 구현 - 장비 관리: 불필요한 카테고리 필드 제거, 벤더-모델 관계 정리 - 우편번호 시스템: 검색 다이얼로그 Provider 버그 수정 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:provider/provider.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/screens/common/templates/form_layout_template.dart';
|
||||
import 'package:superport/screens/company/controllers/company_form_controller.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
import 'package:superport/utils/formatters/korean_phone_formatter.dart';
|
||||
import 'package:superport/data/models/zipcode_dto.dart';
|
||||
import 'package:superport/screens/zipcode/zipcode_search_screen.dart';
|
||||
import 'package:superport/screens/zipcode/controllers/zipcode_controller.dart';
|
||||
import 'package:superport/domain/usecases/zipcode_usecase.dart';
|
||||
|
||||
/// 회사 등록/수정 화면
|
||||
/// User/Warehouse Location 화면과 동일한 FormFieldWrapper 패턴 사용
|
||||
@@ -23,6 +29,11 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
final TextEditingController _phoneNumberController = TextEditingController();
|
||||
int? companyId;
|
||||
bool isBranch = false;
|
||||
|
||||
// 중복 검사 상태 관리
|
||||
bool _isCheckingDuplicate = false;
|
||||
String _duplicateCheckMessage = '';
|
||||
Color _messageColor = Colors.transparent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -69,12 +80,78 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 우편번호 검색 다이얼로그
|
||||
Future<ZipcodeDto?> _showZipcodeSearchDialog() async {
|
||||
return await showDialog<ZipcodeDto>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext dialogContext) => Dialog(
|
||||
clipBehavior: Clip.none,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24),
|
||||
child: SizedBox(
|
||||
width: 800,
|
||||
height: 600,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ShadTheme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ChangeNotifierProvider(
|
||||
create: (_) => ZipcodeController(
|
||||
GetIt.instance<ZipcodeUseCase>(),
|
||||
),
|
||||
child: ZipcodeSearchScreen(
|
||||
onSelect: (zipcode) {
|
||||
Navigator.of(dialogContext).pop(zipcode);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 저장
|
||||
Future<void> _saveCompany() async {
|
||||
if (!_controller.formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 저장 시점에 중복 검사 수행
|
||||
final companyName = _controller.nameController.text.trim();
|
||||
if (companyName.isEmpty) {
|
||||
setState(() {
|
||||
_duplicateCheckMessage = '회사명을 입력하세요';
|
||||
_messageColor = Colors.red;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 중복 검사 시작
|
||||
setState(() {
|
||||
_isCheckingDuplicate = true;
|
||||
_duplicateCheckMessage = '회사명 중복 확인 중...';
|
||||
_messageColor = Colors.blue;
|
||||
});
|
||||
|
||||
final isDuplicate = await _controller.checkDuplicateName(companyName);
|
||||
|
||||
if (isDuplicate) {
|
||||
setState(() {
|
||||
_isCheckingDuplicate = false;
|
||||
_duplicateCheckMessage = '이미 존재하는 회사명입니다';
|
||||
_messageColor = Colors.red;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isCheckingDuplicate = false;
|
||||
_duplicateCheckMessage = '사용 가능한 회사명입니다';
|
||||
_messageColor = Colors.green;
|
||||
});
|
||||
|
||||
// 주소 업데이트
|
||||
_controller.updateCompanyAddress(
|
||||
Address.fromFullAddress(_addressController.text)
|
||||
@@ -235,29 +312,79 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
// 회사명 (필수)
|
||||
FormFieldWrapper(
|
||||
label: "회사명 *",
|
||||
child: ShadInputFormField(
|
||||
controller: _controller.nameController,
|
||||
placeholder: const Text('회사명을 입력하세요'),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
return '회사명을 입력하세요';
|
||||
}
|
||||
if (value.trim().length < 2) {
|
||||
return '회사명은 2자 이상 입력하세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShadInputFormField(
|
||||
controller: _controller.nameController,
|
||||
placeholder: const Text('회사명을 입력하세요'),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
return '회사명을 입력하세요';
|
||||
}
|
||||
if (value.trim().length < 2) {
|
||||
return '회사명은 2자 이상 입력하세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
// 중복 검사 메시지 영역 (고정 높이)
|
||||
SizedBox(
|
||||
height: 24,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
_duplicateCheckMessage,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _messageColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 우편번호 검색
|
||||
FormFieldWrapper(
|
||||
label: "우편번호",
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ShadInputFormField(
|
||||
controller: _controller.zipcodeController,
|
||||
placeholder: const Text('우편번호'),
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadButton(
|
||||
onPressed: () async {
|
||||
// 우편번호 검색 다이얼로그 호출
|
||||
final result = await _showZipcodeSearchDialog();
|
||||
if (result != null) {
|
||||
_controller.selectZipcode(result);
|
||||
// 주소 필드도 업데이트
|
||||
_addressController.text = '${result.sido} ${result.gu} ${result.etc ?? ''}'.trim();
|
||||
}
|
||||
},
|
||||
child: const Text('검색'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 주소 (선택)
|
||||
FormFieldWrapper(
|
||||
label: "주소",
|
||||
child: ShadInputFormField(
|
||||
controller: _addressController,
|
||||
placeholder: const Text('회사 주소를 입력하세요'),
|
||||
placeholder: const Text('상세 주소를 입력하세요'),
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
@@ -340,7 +467,7 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
|
||||
// 저장 버튼
|
||||
ShadButton(
|
||||
onPressed: _saveCompany,
|
||||
onPressed: _isCheckingDuplicate ? null : _saveCompany,
|
||||
size: ShadButtonSize.lg,
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
|
||||
@@ -517,14 +517,14 @@ class _CompanyListState extends State<CompanyList> {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/company/edit',
|
||||
arguments: int.parse(nodeId),
|
||||
arguments: {'companyId': int.parse(nodeId)},
|
||||
);
|
||||
},
|
||||
onEdit: (nodeId) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/company/edit',
|
||||
arguments: int.parse(nodeId),
|
||||
arguments: {'companyId': int.parse(nodeId)},
|
||||
);
|
||||
},
|
||||
onDelete: (nodeId) async {
|
||||
|
||||
@@ -18,6 +18,8 @@ import 'package:superport/services/company_service.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'dart:async';
|
||||
import 'branch_form_controller.dart'; // 분리된 지점 컨트롤러 import
|
||||
import 'package:superport/data/models/zipcode_dto.dart';
|
||||
import 'package:superport/data/repositories/zipcode_repository.dart';
|
||||
|
||||
/// 회사 폼 컨트롤러 - 비즈니스 로직 처리
|
||||
class CompanyFormController {
|
||||
@@ -30,6 +32,8 @@ class CompanyFormController {
|
||||
|
||||
final TextEditingController nameController = TextEditingController();
|
||||
Address companyAddress = const Address();
|
||||
final TextEditingController zipcodeController = TextEditingController();
|
||||
ZipcodeDto? selectedZipcode;
|
||||
final TextEditingController contactNameController = TextEditingController();
|
||||
final TextEditingController contactPositionController =
|
||||
TextEditingController();
|
||||
@@ -309,6 +313,31 @@ class CompanyFormController {
|
||||
isNewlyAddedBranch.remove(index);
|
||||
}
|
||||
|
||||
// 회사명 중복 검사 (저장 시점에만 수행)
|
||||
Future<bool> checkDuplicateName(String name) async {
|
||||
try {
|
||||
// 수정 모드일 때는 자기 자신을 제외하고 검사
|
||||
final response = await _companyService.getCompanies(search: name);
|
||||
|
||||
for (final company in response.items) {
|
||||
// 정확히 일치하는 회사명이 있는지 확인 (대소문자 구분 없이)
|
||||
if (company.name.toLowerCase() == name.toLowerCase()) {
|
||||
// 수정 모드일 때는 자기 자신은 제외
|
||||
if (companyId != null && company.id == companyId) {
|
||||
continue;
|
||||
}
|
||||
return true; // 중복 발견
|
||||
}
|
||||
}
|
||||
return false; // 중복 없음
|
||||
} catch (e) {
|
||||
debugPrint('회사명 중복 검사 실패: $e');
|
||||
// 네트워크 오류 시 중복 없음으로 처리 (저장 진행)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('checkDuplicateName을 사용하세요')
|
||||
Future<Company?> checkDuplicateCompany() async {
|
||||
if (companyId != null) return null; // 수정 모드에서는 체크하지 않음
|
||||
final name = nameController.text.trim();
|
||||
@@ -525,6 +554,18 @@ class CompanyFormController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 우편번호 선택
|
||||
void selectZipcode(ZipcodeDto zipcode) {
|
||||
selectedZipcode = zipcode;
|
||||
zipcodeController.text = zipcode.zipcode;
|
||||
// 주소를 Address 객체로 변환
|
||||
companyAddress = Address(
|
||||
zipCode: zipcode.zipcode,
|
||||
region: '${zipcode.sido} ${zipcode.gu}'.trim(),
|
||||
detailAddress: zipcode.etc ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 전화번호 관련 유틸리티 메서드
|
||||
|
||||
Reference in New Issue
Block a user