308 lines
9.8 KiB
Dart
308 lines
9.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
import '../../domain/entities/restaurant.dart';
|
|
import '../providers/di_providers.dart';
|
|
import '../providers/restaurant_provider.dart';
|
|
|
|
/// 식당 추가 화면의 상태 모델
|
|
class AddRestaurantState {
|
|
final bool isLoading;
|
|
final bool isSearching;
|
|
final String? errorMessage;
|
|
final Restaurant? fetchedRestaurantData;
|
|
final RestaurantFormData formData;
|
|
final List<Restaurant> searchResults;
|
|
|
|
const AddRestaurantState({
|
|
this.isLoading = false,
|
|
this.isSearching = false,
|
|
this.errorMessage,
|
|
this.fetchedRestaurantData,
|
|
required this.formData,
|
|
this.searchResults = const [],
|
|
});
|
|
|
|
AddRestaurantState copyWith({
|
|
bool? isLoading,
|
|
bool? isSearching,
|
|
String? errorMessage,
|
|
Restaurant? fetchedRestaurantData,
|
|
RestaurantFormData? formData,
|
|
List<Restaurant>? searchResults,
|
|
bool clearFetchedRestaurant = false,
|
|
bool clearError = false,
|
|
}) {
|
|
return AddRestaurantState(
|
|
isLoading: isLoading ?? this.isLoading,
|
|
isSearching: isSearching ?? this.isSearching,
|
|
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
|
|
fetchedRestaurantData: clearFetchedRestaurant
|
|
? null
|
|
: (fetchedRestaurantData ?? this.fetchedRestaurantData),
|
|
formData: formData ?? this.formData,
|
|
searchResults: searchResults ?? this.searchResults,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 식당 폼 데이터 모델
|
|
class RestaurantFormData {
|
|
final String name;
|
|
final String category;
|
|
final String subCategory;
|
|
final String description;
|
|
final String phoneNumber;
|
|
final String roadAddress;
|
|
final String jibunAddress;
|
|
final String latitude;
|
|
final String longitude;
|
|
final String naverUrl;
|
|
|
|
const RestaurantFormData({
|
|
this.name = '',
|
|
this.category = '',
|
|
this.subCategory = '',
|
|
this.description = '',
|
|
this.phoneNumber = '',
|
|
this.roadAddress = '',
|
|
this.jibunAddress = '',
|
|
this.latitude = '',
|
|
this.longitude = '',
|
|
this.naverUrl = '',
|
|
});
|
|
|
|
RestaurantFormData copyWith({
|
|
String? name,
|
|
String? category,
|
|
String? subCategory,
|
|
String? description,
|
|
String? phoneNumber,
|
|
String? roadAddress,
|
|
String? jibunAddress,
|
|
String? latitude,
|
|
String? longitude,
|
|
String? naverUrl,
|
|
}) {
|
|
return RestaurantFormData(
|
|
name: name ?? this.name,
|
|
category: category ?? this.category,
|
|
subCategory: subCategory ?? this.subCategory,
|
|
description: description ?? this.description,
|
|
phoneNumber: phoneNumber ?? this.phoneNumber,
|
|
roadAddress: roadAddress ?? this.roadAddress,
|
|
jibunAddress: jibunAddress ?? this.jibunAddress,
|
|
latitude: latitude ?? this.latitude,
|
|
longitude: longitude ?? this.longitude,
|
|
naverUrl: naverUrl ?? this.naverUrl,
|
|
);
|
|
}
|
|
|
|
/// TextEditingController로부터 폼 데이터 생성
|
|
factory RestaurantFormData.fromControllers({
|
|
required TextEditingController nameController,
|
|
required TextEditingController categoryController,
|
|
required TextEditingController subCategoryController,
|
|
required TextEditingController descriptionController,
|
|
required TextEditingController phoneController,
|
|
required TextEditingController roadAddressController,
|
|
required TextEditingController jibunAddressController,
|
|
required TextEditingController latitudeController,
|
|
required TextEditingController longitudeController,
|
|
required TextEditingController naverUrlController,
|
|
}) {
|
|
return RestaurantFormData(
|
|
name: nameController.text.trim(),
|
|
category: categoryController.text.trim(),
|
|
subCategory: subCategoryController.text.trim(),
|
|
description: descriptionController.text.trim(),
|
|
phoneNumber: phoneController.text.trim(),
|
|
roadAddress: roadAddressController.text.trim(),
|
|
jibunAddress: jibunAddressController.text.trim(),
|
|
latitude: latitudeController.text.trim(),
|
|
longitude: longitudeController.text.trim(),
|
|
naverUrl: naverUrlController.text.trim(),
|
|
);
|
|
}
|
|
|
|
/// Restaurant 엔티티로부터 폼 데이터 생성
|
|
factory RestaurantFormData.fromRestaurant(Restaurant restaurant) {
|
|
return RestaurantFormData(
|
|
name: restaurant.name,
|
|
category: restaurant.category,
|
|
subCategory: restaurant.subCategory,
|
|
description: restaurant.description ?? '',
|
|
phoneNumber: restaurant.phoneNumber ?? '',
|
|
roadAddress: restaurant.roadAddress,
|
|
jibunAddress: restaurant.jibunAddress,
|
|
latitude: restaurant.latitude.toString(),
|
|
longitude: restaurant.longitude.toString(),
|
|
naverUrl: restaurant.naverUrl ?? '',
|
|
);
|
|
}
|
|
|
|
/// Restaurant 엔티티로 변환
|
|
Restaurant toRestaurant() {
|
|
final uuid = const Uuid();
|
|
return Restaurant(
|
|
id: uuid.v4(),
|
|
name: name,
|
|
category: category,
|
|
subCategory: subCategory.isEmpty ? category : subCategory,
|
|
description: description.isEmpty ? null : description,
|
|
phoneNumber: phoneNumber.isEmpty ? null : phoneNumber,
|
|
roadAddress: roadAddress,
|
|
jibunAddress: jibunAddress.isEmpty ? roadAddress : jibunAddress,
|
|
latitude: double.tryParse(latitude) ?? 37.5665,
|
|
longitude: double.tryParse(longitude) ?? 126.9780,
|
|
naverUrl: naverUrl.isEmpty ? null : naverUrl,
|
|
source: naverUrl.isNotEmpty ? DataSource.NAVER : DataSource.USER_INPUT,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 식당 추가 화면의 ViewModel
|
|
class AddRestaurantViewModel extends StateNotifier<AddRestaurantState> {
|
|
final Ref _ref;
|
|
|
|
AddRestaurantViewModel(this._ref)
|
|
: super(const AddRestaurantState(formData: RestaurantFormData()));
|
|
|
|
/// 상태 초기화
|
|
void reset() {
|
|
state = const AddRestaurantState(formData: RestaurantFormData());
|
|
}
|
|
|
|
/// 네이버 URL로부터 식당 정보 가져오기
|
|
Future<void> fetchFromNaverUrl(String url) async {
|
|
if (url.trim().isEmpty) {
|
|
state = state.copyWith(errorMessage: 'URL을 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
state = state.copyWith(isLoading: true, clearError: true);
|
|
|
|
try {
|
|
final repository = _ref.read(restaurantRepositoryProvider);
|
|
final restaurant = await repository.previewRestaurantFromUrl(url);
|
|
|
|
state = state.copyWith(
|
|
isLoading: false,
|
|
fetchedRestaurantData: restaurant,
|
|
formData: RestaurantFormData.fromRestaurant(restaurant),
|
|
);
|
|
} catch (e) {
|
|
state = state.copyWith(isLoading: false, errorMessage: e.toString());
|
|
}
|
|
}
|
|
|
|
/// 네이버 검색으로 식당 목록 검색
|
|
Future<void> searchRestaurants(
|
|
String query, {
|
|
double? latitude,
|
|
double? longitude,
|
|
}) async {
|
|
if (query.trim().isEmpty) {
|
|
state = state.copyWith(
|
|
errorMessage: '검색어를 입력해주세요.',
|
|
searchResults: const [],
|
|
);
|
|
return;
|
|
}
|
|
|
|
state = state.copyWith(isSearching: true, clearError: true);
|
|
|
|
try {
|
|
final repository = _ref.read(restaurantRepositoryProvider);
|
|
final results = await repository.searchRestaurantsFromNaver(
|
|
query: query,
|
|
latitude: latitude,
|
|
longitude: longitude,
|
|
);
|
|
state = state.copyWith(isSearching: false, searchResults: results);
|
|
} catch (e) {
|
|
state = state.copyWith(isSearching: false, errorMessage: e.toString());
|
|
}
|
|
}
|
|
|
|
/// 검색 결과 선택
|
|
void selectSearchResult(Restaurant restaurant) {
|
|
state = state.copyWith(
|
|
fetchedRestaurantData: restaurant,
|
|
formData: RestaurantFormData.fromRestaurant(restaurant),
|
|
clearError: true,
|
|
);
|
|
}
|
|
|
|
/// 식당 정보 저장
|
|
Future<bool> saveRestaurant() async {
|
|
final notifier = _ref.read(restaurantNotifierProvider.notifier);
|
|
|
|
try {
|
|
state = state.copyWith(isLoading: true, clearError: true);
|
|
Restaurant restaurantToSave;
|
|
|
|
// 네이버에서 가져온 데이터가 있으면 업데이트
|
|
final fetchedData = state.fetchedRestaurantData;
|
|
if (fetchedData != null) {
|
|
restaurantToSave = fetchedData.copyWith(
|
|
name: state.formData.name,
|
|
category: state.formData.category,
|
|
subCategory: state.formData.subCategory.isEmpty
|
|
? state.formData.category
|
|
: state.formData.subCategory,
|
|
description: state.formData.description.isEmpty
|
|
? null
|
|
: state.formData.description,
|
|
phoneNumber: state.formData.phoneNumber.isEmpty
|
|
? null
|
|
: state.formData.phoneNumber,
|
|
roadAddress: state.formData.roadAddress,
|
|
jibunAddress: state.formData.jibunAddress.isEmpty
|
|
? state.formData.roadAddress
|
|
: state.formData.jibunAddress,
|
|
latitude:
|
|
double.tryParse(state.formData.latitude) ?? fetchedData.latitude,
|
|
longitude:
|
|
double.tryParse(state.formData.longitude) ??
|
|
fetchedData.longitude,
|
|
naverUrl: state.formData.naverUrl.isEmpty
|
|
? null
|
|
: state.formData.naverUrl,
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
} else {
|
|
// 직접 입력한 경우
|
|
restaurantToSave = state.formData.toRestaurant();
|
|
}
|
|
|
|
await notifier.addRestaurantDirect(restaurantToSave);
|
|
state = state.copyWith(isLoading: false);
|
|
return true;
|
|
} catch (e) {
|
|
state = state.copyWith(isLoading: false, errorMessage: e.toString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 폼 데이터 업데이트
|
|
void updateFormData(RestaurantFormData formData) {
|
|
state = state.copyWith(formData: formData);
|
|
}
|
|
|
|
/// 에러 메시지 초기화
|
|
void clearError() {
|
|
state = state.copyWith(clearError: true);
|
|
}
|
|
}
|
|
|
|
/// AddRestaurantViewModel Provider
|
|
final addRestaurantViewModelProvider =
|
|
StateNotifierProvider.autoDispose<
|
|
AddRestaurantViewModel,
|
|
AddRestaurantState
|
|
>((ref) => AddRestaurantViewModel(ref));
|