feat: 초기 프로젝트 설정 및 LunchPick 앱 구현
LunchPick(오늘 뭐 먹Z?) Flutter 앱의 초기 구현입니다. 주요 기능: - 네이버 지도 연동 맛집 추가 - 랜덤 메뉴 추천 시스템 - 날씨 기반 거리 조정 - 방문 기록 관리 - Bluetooth 맛집 공유 - 다크모드 지원 기술 스택: - Flutter 3.8.1+ - Riverpod 상태 관리 - Hive 로컬 DB - Clean Architecture 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
246
lib/presentation/view_models/add_restaurant_view_model.dart
Normal file
246
lib/presentation/view_models/add_restaurant_view_model.dart
Normal file
@@ -0,0 +1,246 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../domain/entities/restaurant.dart';
|
||||
import '../providers/restaurant_provider.dart';
|
||||
|
||||
/// 식당 추가 화면의 상태 모델
|
||||
class AddRestaurantState {
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
final Restaurant? fetchedRestaurantData;
|
||||
final RestaurantFormData formData;
|
||||
|
||||
const AddRestaurantState({
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
this.fetchedRestaurantData,
|
||||
required this.formData,
|
||||
});
|
||||
|
||||
AddRestaurantState copyWith({
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
Restaurant? fetchedRestaurantData,
|
||||
RestaurantFormData? formData,
|
||||
}) {
|
||||
return AddRestaurantState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
fetchedRestaurantData: fetchedRestaurantData ?? this.fetchedRestaurantData,
|
||||
formData: formData ?? this.formData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 식당 폼 데이터 모델
|
||||
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()));
|
||||
|
||||
/// 네이버 URL로부터 식당 정보 가져오기
|
||||
Future<void> fetchFromNaverUrl(String url) async {
|
||||
if (url.trim().isEmpty) {
|
||||
state = state.copyWith(errorMessage: 'URL을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(isLoading: true, errorMessage: null);
|
||||
|
||||
try {
|
||||
final notifier = _ref.read(restaurantNotifierProvider.notifier);
|
||||
final restaurant = await notifier.addRestaurantFromUrl(url);
|
||||
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
fetchedRestaurantData: restaurant,
|
||||
formData: RestaurantFormData.fromRestaurant(restaurant),
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 식당 정보 저장
|
||||
Future<bool> saveRestaurant() async {
|
||||
final notifier = _ref.read(restaurantNotifierProvider.notifier);
|
||||
|
||||
try {
|
||||
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);
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(errorMessage: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 폼 데이터 업데이트
|
||||
void updateFormData(RestaurantFormData formData) {
|
||||
state = state.copyWith(formData: formData);
|
||||
}
|
||||
|
||||
/// 에러 메시지 초기화
|
||||
void clearError() {
|
||||
state = state.copyWith(errorMessage: null);
|
||||
}
|
||||
}
|
||||
|
||||
/// AddRestaurantViewModel Provider
|
||||
final addRestaurantViewModelProvider =
|
||||
StateNotifierProvider.autoDispose<AddRestaurantViewModel, AddRestaurantState>(
|
||||
(ref) => AddRestaurantViewModel(ref),
|
||||
);
|
||||
Reference in New Issue
Block a user