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:
@@ -30,12 +30,16 @@ class ZipcodeSearchFilter extends StatefulWidget {
|
||||
|
||||
class _ZipcodeSearchFilterState extends State<ZipcodeSearchFilter> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ScrollController _sidoScrollController = ScrollController();
|
||||
final ScrollController _guScrollController = ScrollController();
|
||||
Timer? _debounceTimer;
|
||||
bool _hasFilters = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_sidoScrollController.dispose();
|
||||
_guScrollController.dispose();
|
||||
_debounceTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -51,12 +55,16 @@ class _ZipcodeSearchFilterState extends State<ZipcodeSearchFilter> {
|
||||
}
|
||||
|
||||
void _onSidoChanged(String? value) {
|
||||
widget.onSidoChanged(value);
|
||||
// 빈 문자열을 null로 변환
|
||||
final actualValue = (value == '') ? null : value;
|
||||
widget.onSidoChanged(actualValue);
|
||||
_updateHasFilters();
|
||||
}
|
||||
|
||||
void _onGuChanged(String? value) {
|
||||
widget.onGuChanged(value);
|
||||
// 빈 문자열을 null로 변환
|
||||
final actualValue = (value == '') ? null : value;
|
||||
widget.onGuChanged(actualValue);
|
||||
_updateHasFilters();
|
||||
}
|
||||
|
||||
@@ -157,36 +165,45 @@ class _ZipcodeSearchFilterState extends State<ZipcodeSearchFilter> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ShadSelect<String>(
|
||||
placeholder: const Text('시도 선택'),
|
||||
onChanged: _onSidoChanged,
|
||||
options: [
|
||||
const ShadOption(
|
||||
value: null,
|
||||
child: Text('전체'),
|
||||
widget.sidoList.isEmpty
|
||||
? Container(
|
||||
height: 38,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.border),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
...widget.sidoList.map((sido) => ShadOption(
|
||||
value: sido,
|
||||
child: Text(sido),
|
||||
)),
|
||||
],
|
||||
selectedOptionBuilder: (context, value) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_city,
|
||||
size: 16,
|
||||
color: theme.colorScheme.primary,
|
||||
child: Text('로딩 중...', style: theme.textTheme.muted),
|
||||
)
|
||||
: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ShadSelect<String>(
|
||||
placeholder: const Text('시도 선택'),
|
||||
maxHeight: 400,
|
||||
shrinkWrap: true,
|
||||
showScrollToBottomChevron: true,
|
||||
showScrollToTopChevron: true,
|
||||
scrollController: _sidoScrollController,
|
||||
onChanged: (value) => _onSidoChanged(value),
|
||||
options: [
|
||||
const ShadOption(
|
||||
value: '',
|
||||
child: Text('전체'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(value ?? '전체'),
|
||||
...widget.sidoList.map((sido) => ShadOption(
|
||||
value: sido,
|
||||
child: Text(sido),
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
if (value == '') {
|
||||
return const Text('전체');
|
||||
}
|
||||
return Text(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -204,42 +221,45 @@ class _ZipcodeSearchFilterState extends State<ZipcodeSearchFilter> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ShadSelect<String>(
|
||||
placeholder: Text(
|
||||
widget.selectedSido == null
|
||||
? '시도를 먼저 선택하세요'
|
||||
: '구/군 선택'
|
||||
),
|
||||
onChanged: widget.selectedSido != null ? _onGuChanged : null,
|
||||
options: [
|
||||
const ShadOption(
|
||||
value: null,
|
||||
child: Text('전체'),
|
||||
widget.selectedSido == null
|
||||
? Container(
|
||||
height: 38,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.border),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
...widget.guList.map((gu) => ShadOption(
|
||||
value: gu,
|
||||
child: Text(gu),
|
||||
)),
|
||||
],
|
||||
selectedOptionBuilder: (context, value) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 16,
|
||||
color: widget.selectedSido != null
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.mutedForeground,
|
||||
child: Text('시도를 먼저 선택하세요', style: theme.textTheme.muted),
|
||||
)
|
||||
: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ShadSelect<String>(
|
||||
placeholder: const Text('구/군 선택'),
|
||||
maxHeight: 400,
|
||||
shrinkWrap: true,
|
||||
showScrollToBottomChevron: true,
|
||||
showScrollToTopChevron: true,
|
||||
scrollController: _guScrollController,
|
||||
onChanged: (value) => _onGuChanged(value),
|
||||
options: [
|
||||
const ShadOption(
|
||||
value: '',
|
||||
child: Text('전체'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(value ?? '전체'),
|
||||
...widget.guList.map((gu) => ShadOption(
|
||||
value: gu,
|
||||
child: Text(gu),
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
if (value == '') {
|
||||
return const Text('전체');
|
||||
}
|
||||
return Text(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -128,9 +128,12 @@ class ZipcodeTable extends StatelessWidget {
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
zipcode.sido,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
Flexible(
|
||||
child: Text(
|
||||
zipcode.sido,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -146,9 +149,12 @@ class ZipcodeTable extends StatelessWidget {
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
zipcode.gu,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
Flexible(
|
||||
child: Text(
|
||||
zipcode.gu,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -190,28 +196,10 @@ class ZipcodeTable extends StatelessWidget {
|
||||
|
||||
// 작업
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton(
|
||||
onPressed: () => onSelect(zipcode),
|
||||
size: ShadButtonSize.sm,
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check, size: 14),
|
||||
SizedBox(width: 4),
|
||||
Text('선택'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
ShadButton.outline(
|
||||
onPressed: () => _showAddressDetails(context, zipcode),
|
||||
size: ShadButtonSize.sm,
|
||||
child: const Icon(Icons.info_outline, size: 14),
|
||||
),
|
||||
],
|
||||
ShadButton(
|
||||
onPressed: () => onSelect(zipcode),
|
||||
size: ShadButtonSize.sm,
|
||||
child: const Text('선택', style: TextStyle(fontSize: 11)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -53,14 +53,23 @@ class ZipcodeController extends ChangeNotifier {
|
||||
|
||||
// 초기 데이터 로드
|
||||
Future<void> initialize() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
// 시도 목록 로드
|
||||
await _loadSidoList();
|
||||
|
||||
// 초기 우편번호 목록 로드 (첫 페이지)
|
||||
await searchZipcodes();
|
||||
try {
|
||||
_isLoading = true;
|
||||
_zipcodes = [];
|
||||
_selectedZipcode = null;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
// 시도 목록 로드
|
||||
await _loadSidoList();
|
||||
|
||||
// 초기 우편번호 목록 로드 (첫 페이지)
|
||||
await searchZipcodes();
|
||||
} catch (e) {
|
||||
_errorMessage = '초기화 중 오류가 발생했습니다.';
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 우편번호 검색
|
||||
@@ -141,27 +150,35 @@ class ZipcodeController extends ChangeNotifier {
|
||||
|
||||
// 시도 선택
|
||||
Future<void> setSido(String? sido) async {
|
||||
_selectedSido = sido;
|
||||
_selectedGu = null; // 시도 변경 시 구 초기화
|
||||
_guList = []; // 구 목록 초기화
|
||||
notifyListeners();
|
||||
|
||||
// 선택된 시도에 따른 구 목록 로드
|
||||
if (sido != null) {
|
||||
await _loadGuListBySido(sido);
|
||||
try {
|
||||
_selectedSido = sido;
|
||||
_selectedGu = null; // 시도 변경 시 구 초기화
|
||||
_guList = []; // 구 목록 초기화
|
||||
notifyListeners();
|
||||
|
||||
// 선택된 시도에 따른 구 목록 로드
|
||||
if (sido != null && sido.isNotEmpty) {
|
||||
await _loadGuListBySido(sido);
|
||||
}
|
||||
|
||||
// 검색 새로고침
|
||||
await searchZipcodes(refresh: true);
|
||||
} catch (e) {
|
||||
debugPrint('시도 선택 오류: $e');
|
||||
}
|
||||
|
||||
// 검색 새로고침
|
||||
await searchZipcodes(refresh: true);
|
||||
}
|
||||
|
||||
// 구 선택
|
||||
Future<void> setGu(String? gu) async {
|
||||
_selectedGu = gu;
|
||||
notifyListeners();
|
||||
|
||||
// 검색 새로고침
|
||||
await searchZipcodes(refresh: true);
|
||||
try {
|
||||
_selectedGu = gu;
|
||||
notifyListeners();
|
||||
|
||||
// 검색 새로고침
|
||||
await searchZipcodes(refresh: true);
|
||||
} catch (e) {
|
||||
debugPrint('구 선택 오류: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 필터 초기화
|
||||
@@ -202,6 +219,9 @@ class ZipcodeController extends ChangeNotifier {
|
||||
Future<void> _loadSidoList() async {
|
||||
try {
|
||||
_sidoList = await _zipcodeUseCase.getAllSidoList();
|
||||
debugPrint('=== 시도 목록 로드 완료 ===');
|
||||
debugPrint('총 시도 개수: ${_sidoList.length}');
|
||||
debugPrint('시도 목록: $_sidoList');
|
||||
} catch (e) {
|
||||
debugPrint('시도 목록 로드 실패: $e');
|
||||
_sidoList = [];
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/data/models/zipcode_dto.dart';
|
||||
import 'package:superport/screens/zipcode/controllers/zipcode_controller.dart';
|
||||
import 'package:superport/screens/zipcode/components/zipcode_search_filter.dart';
|
||||
import 'package:superport/screens/zipcode/components/zipcode_table.dart';
|
||||
|
||||
class ZipcodeSearchScreen extends StatefulWidget {
|
||||
const ZipcodeSearchScreen({super.key});
|
||||
final Function(ZipcodeDto)? onSelect;
|
||||
const ZipcodeSearchScreen({super.key, this.onSelect});
|
||||
|
||||
@override
|
||||
State<ZipcodeSearchScreen> createState() => _ZipcodeSearchScreenState();
|
||||
@@ -62,9 +64,9 @@ class _ZipcodeSearchScreenState extends State<ZipcodeSearchScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.colorScheme.background,
|
||||
body: Column(
|
||||
return Material(
|
||||
color: theme.colorScheme.background,
|
||||
child: Column(
|
||||
children: [
|
||||
// 헤더 섹션
|
||||
Container(
|
||||
@@ -227,7 +229,13 @@ class _ZipcodeSearchScreenState extends State<ZipcodeSearchScreen> {
|
||||
onPageChanged: controller.goToPage,
|
||||
onSelect: (zipcode) {
|
||||
controller.selectZipcode(zipcode);
|
||||
_showSuccessToast('우편번호 ${zipcode.zipcode}를 선택했습니다');
|
||||
if (widget.onSelect != null) {
|
||||
// 다이얼로그로 사용될 때
|
||||
widget.onSelect!(zipcode);
|
||||
} else {
|
||||
// 일반 화면으로 사용될 때
|
||||
_showSuccessToast('우편번호 ${zipcode.zipcode}를 선택했습니다');
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user