feat(app): add manual entry and sharing flows

This commit is contained in:
JiWoong Sul
2025-11-19 16:36:39 +09:00
parent 5ade584370
commit 947fe59486
110 changed files with 5937 additions and 3781 deletions

View File

@@ -3,33 +3,46 @@ 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,
errorMessage: errorMessage ?? this.errorMessage,
fetchedRestaurantData: fetchedRestaurantData ?? this.fetchedRestaurantData,
isSearching: isSearching ?? this.isSearching,
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
fetchedRestaurantData: clearFetchedRestaurant
? null
: (fetchedRestaurantData ?? this.fetchedRestaurantData),
formData: formData ?? this.formData,
searchResults: searchResults ?? this.searchResults,
);
}
}
@@ -156,7 +169,12 @@ class AddRestaurantViewModel extends StateNotifier<AddRestaurantState> {
final Ref _ref;
AddRestaurantViewModel(this._ref)
: super(const AddRestaurantState(formData: RestaurantFormData()));
: super(const AddRestaurantState(formData: RestaurantFormData()));
/// 상태 초기화
void reset() {
state = const AddRestaurantState(formData: RestaurantFormData());
}
/// 네이버 URL로부터 식당 정보 가져오기
Future<void> fetchFromNaverUrl(String url) async {
@@ -165,11 +183,11 @@ class AddRestaurantViewModel extends StateNotifier<AddRestaurantState> {
return;
}
state = state.copyWith(isLoading: true, errorMessage: null);
state = state.copyWith(isLoading: true, clearError: true);
try {
final notifier = _ref.read(restaurantNotifierProvider.notifier);
final restaurant = await notifier.addRestaurantFromUrl(url);
final repository = _ref.read(restaurantRepositoryProvider);
final restaurant = await repository.previewRestaurantFromUrl(url);
state = state.copyWith(
isLoading: false,
@@ -177,42 +195,83 @@ class AddRestaurantViewModel extends StateNotifier<AddRestaurantState> {
formData: RestaurantFormData.fromRestaurant(restaurant),
);
} catch (e) {
state = state.copyWith(
isLoading: false,
errorMessage: e.toString(),
);
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
subCategory: state.formData.subCategory.isEmpty
? state.formData.category
: state.formData.subCategory,
description: state.formData.description.isEmpty
? null
description: state.formData.description.isEmpty
? null
: state.formData.description,
phoneNumber: state.formData.phoneNumber.isEmpty
? null
phoneNumber: state.formData.phoneNumber.isEmpty
? null
: state.formData.phoneNumber,
roadAddress: state.formData.roadAddress,
jibunAddress: state.formData.jibunAddress.isEmpty
? 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,
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 {
@@ -221,9 +280,10 @@ class AddRestaurantViewModel extends StateNotifier<AddRestaurantState> {
}
await notifier.addRestaurantDirect(restaurantToSave);
state = state.copyWith(isLoading: false);
return true;
} catch (e) {
state = state.copyWith(errorMessage: e.toString());
state = state.copyWith(isLoading: false, errorMessage: e.toString());
return false;
}
}
@@ -235,12 +295,13 @@ class AddRestaurantViewModel extends StateNotifier<AddRestaurantState> {
/// 에러 메시지 초기화
void clearError() {
state = state.copyWith(errorMessage: null);
state = state.copyWith(clearError: true);
}
}
/// AddRestaurantViewModel Provider
final addRestaurantViewModelProvider =
StateNotifierProvider.autoDispose<AddRestaurantViewModel, AddRestaurantState>(
(ref) => AddRestaurantViewModel(ref),
);
StateNotifierProvider.autoDispose<
AddRestaurantViewModel,
AddRestaurantState
>((ref) => AddRestaurantViewModel(ref));