Skip to content

Commit

Permalink
Merge pull request #238 from Team-Weather/feat/#227-apply-firestore-t…
Browse files Browse the repository at this point in the history
…ransaction

Feat/#227 apply firestore transaction
  • Loading branch information
KimDonghyeok authored Jun 6, 2024
2 parents 9f756f2 + 6aa82df commit 1c51ce9
Show file tree
Hide file tree
Showing 25 changed files with 325 additions and 227 deletions.
3 changes: 3 additions & 0 deletions lib/core/db/transaction_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
abstract interface class TransactionService {
Future<bool> run(Function callBack);
}
7 changes: 7 additions & 0 deletions lib/core/di/common/common_di_setup.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:dio/dio.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:weaco/core/db/transaction_service.dart';
import 'package:weaco/core/di/di_setup.dart';
import 'package:weaco/core/dio/base_dio.dart';
import 'package:weaco/core/exception/handler/exception_message_handler.dart';
import 'package:weaco/core/firebase/firebase_auth_service.dart';
import 'package:weaco/core/firebase/firestore_service.dart';
import 'package:weaco/core/gps/gps_helper.dart';
import 'package:weaco/core/hive/hive_wrapper.dart';
import 'package:weaco/core/path_provider/path_provider_service.dart';
Expand All @@ -15,6 +17,11 @@ void commonDiSetup() {
() => FirebaseFirestore.instance);
getIt.registerLazySingleton<FirebaseStorage>(() => FirebaseStorage.instance);
getIt.registerLazySingleton<FirebaseAuthService>(() => FirebaseAuthService());
getIt.registerLazySingleton<TransactionService>(
() => FirestoreService(
fireStore: getIt<FirebaseFirestore>(),
),
);

// PathProvider
getIt.registerLazySingleton<PathProviderService>(() => PathProviderService());
Expand Down
13 changes: 9 additions & 4 deletions lib/core/di/feed/feed_di_setup.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:weaco/core/db/transaction_service.dart';
import 'package:weaco/core/di/di_setup.dart';
import 'package:weaco/data/feed/data_source/remote_feed_data_source.dart';
import 'package:weaco/data/feed/data_source/remote_feed_data_source_impl.dart';
import 'package:weaco/data/feed/repository/feed_repository_impl.dart';
import 'package:weaco/data/feed/repository/ootd_feed_repository_impl.dart';
import 'package:weaco/data/user/data_source/remote_user_profile_data_source.dart';
import 'package:weaco/domain/feed/repository/feed_repository.dart';
import 'package:weaco/domain/feed/repository/ootd_feed_repository.dart';
import 'package:weaco/domain/feed/use_case/get_detail_feed_detail_use_case.dart';
Expand All @@ -15,7 +17,6 @@ import 'package:weaco/domain/feed/use_case/get_user_page_feeds_use_case.dart';
import 'package:weaco/domain/feed/use_case/remove_my_page_feed_use_case.dart';
import 'package:weaco/domain/feed/use_case/save_edit_feed_use_case.dart';
import 'package:weaco/domain/file/repository/file_repository.dart';
import 'package:weaco/domain/user/repository/user_profile_repository.dart';
import 'package:weaco/domain/user/use_case/get_user_profile_use_case.dart';
import 'package:weaco/domain/weather/use_case/get_daily_location_weather_use_case.dart';
import 'package:weaco/presentation/ootd_feed/view_model/ootd_feed_view_model.dart';
Expand All @@ -30,10 +31,14 @@ void feedDiSetup() {
// Repository
getIt.registerLazySingleton<FeedRepository>(() =>
FeedRepositoryImpl(remoteFeedDataSource: getIt<RemoteFeedDataSource>()));
getIt.registerLazySingleton<OotdFeedRepository>(() => OotdFeedRepositoryImpl(
getIt.registerLazySingleton<OotdFeedRepository>(
() => OotdFeedRepositoryImpl(
fileRepository: getIt<FileRepository>(),
feedRepository: getIt<FeedRepository>(),
userProfileRepository: getIt<UserProfileRepository>()));
remoteFeedDataSource: getIt<RemoteFeedDataSource>(),
remoteUserProfileDataSource: getIt<RemoteUserProfileDataSource>(),
firestoreService: getIt<TransactionService>(),
),
);

// UseCase
getIt.registerLazySingleton<GetDetailFeedDetailUseCase>(() =>
Expand Down
22 changes: 22 additions & 0 deletions lib/core/firebase/firestore_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:weaco/core/db/transaction_service.dart';

class FirestoreService implements TransactionService {
final FirebaseFirestore _fireStore;

const FirestoreService({
required FirebaseFirestore fireStore,
}) : _fireStore = fireStore;

@override
Future<bool> run(Function callBack) async {
return _fireStore.runTransaction<bool>((transaction) async {
return await callBack(transaction);
}).then(
(value) => true,
onError: (e) {
throw Exception('피드 업로드에 실패 하였습니다.');
},
);
}
}
18 changes: 12 additions & 6 deletions lib/data/feed/data_source/remote_feed_data_source.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:weaco/domain/feed/model/feed.dart';
import 'package:weaco/domain/weather/model/daily_location_weather.dart';

abstract interface class RemoteFeedDataSource {
/// OOTD 피드 작성 성공 시 : 피드 업로드 요청(Feed) -> / 업로드 완료(bool) ← 파베
/// OOTD 편집 완료 후 [상세 페이지]: 위와 동일.
/// OOTD 편집 완료 후 [마이 페이지]: 위와 동일.*피드 업데이트
Future<bool> saveFeed({required Feed feed});
Future<bool> saveFeed({
required Transaction transaction,
required Feed feed,
});

/// OOTD 피드 [상세 페이지] : 피드 데이터 요청 (id) -> 파베 / 피드 데이터 반환(json) ← 파베
Future<Feed> getFeed({required String id});
Expand All @@ -18,17 +22,19 @@ abstract interface class RemoteFeedDataSource {
});

/// [마이페이지] 피드 삭제: 피드 삭제 요청(id) -> 파베/ 삭제 완료 (bool) from FB
Future<bool> deleteFeed({required String id});
Future<bool> deleteFeed({
required Transaction transaction,
required String id,
});

/// [홈 화면] 하단 OOTD 추천 목록:
///
/// 유저의 위치와 기온을 기반으로 피드 목록을 불러옵니다.
/// @param city: 유저 위치의 도시명
/// @param temperature: 날씨 온도
Future<List<Feed>> getRecommendedFeedList({
required DailyLocationWeather dailyLocationWeather,
DateTime? createdAt
});
Future<List<Feed>> getRecommendedFeedList(
{required DailyLocationWeather dailyLocationWeather,
DateTime? createdAt});

/// [검색 페이지] 피드 검색:
///
Expand Down
47 changes: 29 additions & 18 deletions lib/data/feed/data_source/remote_feed_data_source_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,22 @@ class RemoteFeedDataSourceImpl implements RemoteFeedDataSource {

/// OOTD 피드 작성 또는 편집 후 저장
@override
Future<bool> saveFeed({required Feed feed}) async {
Future<bool> saveFeed({
required Transaction transaction,
required Feed feed,
}) async {
final feedDto = toFeedDto(feed: feed);

// 피드를 수정 할 경우
if (feed.id != null) {
return await _fireStore
.collection('feeds')
.doc(feed.id)
.set(feedDto)
.then((value) => true);
// 피드를 수정할 경우
final feedDocRef = _fireStore.collection('feeds').doc(feed.id);
transaction.set(feedDocRef, feedDto);
} else {
// 새 피드를 추가할 경우
final feedDocRef = _fireStore.collection('feeds').doc();
transaction.set(feedDocRef, feedDto);
}

// 새 피드를 저장 할 경우
return await _fireStore
.collection('feeds')
.add(feedDto)
.then((value) => true);
return true;
}

/// [OOTD 피드 상세 페이지]:
Expand Down Expand Up @@ -75,11 +74,23 @@ class RemoteFeedDataSourceImpl implements RemoteFeedDataSource {
/// [마이페이지] 피드 삭제
/// soft delete 처리
@override
Future<bool> deleteFeed({required String id}) async {
await _fireStore
.collection('feeds')
.doc(id)
.update({'deleted_at': Timestamp.fromDate(DateTime.now())});
Future<bool> deleteFeed({
required Transaction transaction,
required String id,
}) async {
final originalFeedDocRef = _fireStore.collection('feeds').doc(id);
final originalFeedDoc = await originalFeedDocRef.get();

if (originalFeedDoc.exists) {
final feed = toFeed(json: originalFeedDoc.data()!, id: id);
final deletedFeed = feed.copyWith(deletedAt: DateTime.now());

transaction.update(
originalFeedDocRef,
toFeedDto(feed: deletedFeed),
);
}

return true;
}

Expand Down
12 changes: 1 addition & 11 deletions lib/data/feed/repository/feed_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,6 @@ class FeedRepositoryImpl implements FeedRepository {
required this.remoteFeedDataSource,
});

@override
Future<bool> saveFeed({required Feed editedFeed}) {
return remoteFeedDataSource.saveFeed(feed: editedFeed);
}

@override
Future<bool> deleteFeed({required String id}) async {
return await remoteFeedDataSource.deleteFeed(id: id);
}

/// 피드의 id 값을 전달 하여 Firebase 내의 해당 피드를 가져 와야 한다.
/// GetDetailFeedDetailUseCase에서 사용
/// @param id: 피드 id
Expand Down Expand Up @@ -59,7 +49,7 @@ class FeedRepositoryImpl implements FeedRepository {
}) async {
return await remoteFeedDataSource.getRecommendedFeedList(
dailyLocationWeather: dailyLocationWeather,
createdAt: createdAt
createdAt: createdAt,
);
}

Expand Down
87 changes: 61 additions & 26 deletions lib/data/feed/repository/ootd_feed_repository_impl.dart
Original file line number Diff line number Diff line change
@@ -1,65 +1,100 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:weaco/core/db/transaction_service.dart';
import 'package:weaco/data/feed/data_source/remote_feed_data_source.dart';
import 'package:weaco/data/user/data_source/remote_user_profile_data_source.dart';
import 'package:weaco/domain/feed/model/feed.dart';
import 'package:weaco/domain/feed/repository/feed_repository.dart';
import 'package:weaco/domain/feed/repository/ootd_feed_repository.dart';
import 'package:weaco/domain/file/repository/file_repository.dart';
import 'package:weaco/domain/user/repository/user_profile_repository.dart';

class OotdFeedRepositoryImpl implements OotdFeedRepository {
final FileRepository _fileRepository;
final FeedRepository _feedRepository;
final UserProfileRepository _userProfileRepository;

final RemoteFeedDataSource _remoteFeedDataSource;
final RemoteUserProfileDataSource _remoteUserProfileDataSource;

final TransactionService _firestoreService;

const OotdFeedRepositoryImpl({
required FileRepository fileRepository,
required FeedRepository feedRepository,
required UserProfileRepository userProfileRepository,
required RemoteFeedDataSource remoteFeedDataSource,
required RemoteUserProfileDataSource remoteUserProfileDataSource,
required TransactionService firestoreService,
}) : _fileRepository = fileRepository,
_feedRepository = feedRepository,
_userProfileRepository = userProfileRepository;
_remoteFeedDataSource = remoteFeedDataSource,
_remoteUserProfileDataSource = remoteUserProfileDataSource,
_firestoreService = firestoreService;

/// 피드 저장 및 수정
@override
Future<bool> saveOotdFeed({required Feed feed}) async {
return feed.id == null
? await _save(feed: feed)
: await _update(feed: feed);
return _firestoreService.run((Transaction transaction) async {
return feed.id == null
? await _save(transaction: transaction, feed: feed)
: await _update(transaction: transaction, feed: feed);
});
}

/// 피드 저장
Future<bool> _save({required Feed feed}) async {
Future<bool> _save({
required Transaction transaction,
required Feed feed,
}) async {
final List<String> path = await _fileRepository.saveOotdImage();

final saveResult = await _feedRepository.saveFeed(
editedFeed: feed.copyWith(imagePath: path[0], thumbnailImagePath: path[1]));
await _remoteFeedDataSource.saveFeed(
transaction: transaction,
feed: feed.copyWith(imagePath: path[0], thumbnailImagePath: path[1]),
);

await _updateMyFeedCount(1);
await _updateMyFeedCount(
transaction: transaction,
count: 1,
);

return saveResult;
return true;
}

/// 피드 수정
Future<bool> _update({required Feed feed}) async {
final updateResult = await _feedRepository.saveFeed(editedFeed: feed);
Future<bool> _update({
required Transaction transaction,
required Feed feed,
}) async {
final updateResult = await _remoteFeedDataSource.saveFeed(
transaction: transaction,
feed: feed,
);

return updateResult;
}

/// 피드 삭제
@override
Future<bool> removeOotdFeed({required String id}) async {
await _feedRepository.deleteFeed(id: id);
return _firestoreService.run((Transaction transaction) async {
await _remoteFeedDataSource.deleteFeed(
transaction: transaction,
id: id,
);

await _updateMyFeedCount(-1);
await _updateMyFeedCount(
transaction: transaction,
count: -1,
);

return true;
return true;
});
}

/// 유저 피드 카운트 업데이트
Future<void> _updateMyFeedCount(int count) async {
final myProfile = await _userProfileRepository.getMyProfile();
Future<void> _updateMyFeedCount({
required Transaction transaction,
required int count,
}) async {
final myProfile = await _remoteUserProfileDataSource.getUserProfile();

_userProfileRepository.updateUserProfile(
userProfile:
myProfile!.copyWith(feedCount: myProfile.feedCount + count));
await _remoteUserProfileDataSource.updateUserProfile(
transaction: transaction,
userProfile: myProfile.copyWith(feedCount: myProfile.feedCount + count),
);
}
}
26 changes: 15 additions & 11 deletions lib/data/file/data_source/remote/remote_file_data_source_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,29 @@ class RemoteFileDataSourceImpl implements RemoteFileDataSource {
final FirebaseAuthService _firebaseAuthService;

RemoteFileDataSourceImpl(
{required FirebaseStorage firebaseStorage, required FirebaseAuthService firebaseAuthService})
{required FirebaseStorage firebaseStorage,
required FirebaseAuthService firebaseAuthService})
: _firebaseStorage = firebaseStorage,
_firebaseAuthService = firebaseAuthService;

@override
Future<List<String>> saveImage({required File croppedImage, required File compressedImage}) async {
Future<List<String>> saveImage(
{required File croppedImage, required File compressedImage}) async {
final String? email = _firebaseAuthService.firebaseAuth.currentUser?.email;
if (email == null) throw Exception();
final feedOriginImageRef = _firebaseStorage.ref().child(
'feed_origin_images/${email}_${DateTime
.now()
.microsecondsSinceEpoch}.png');
await feedOriginImageRef.putFile(croppedImage);
'feed_origin_images/${email}_${DateTime.now().microsecondsSinceEpoch}.png');
final feedThumbnailImageRef = _firebaseStorage.ref().child(
'feed_thumbnail_images/${email}_${DateTime
.now()
.microsecondsSinceEpoch}.png');
await feedThumbnailImageRef.putFile(compressedImage);
'feed_thumbnail_images/${email}_${DateTime.now().microsecondsSinceEpoch}.png');

return [await feedOriginImageRef.getDownloadURL(), await feedThumbnailImageRef.getDownloadURL()];
await Future.wait([
feedOriginImageRef.putFile(croppedImage),
feedThumbnailImageRef.putFile(compressedImage),
]);

return await Future.wait([
feedOriginImageRef.getDownloadURL(),
feedThumbnailImageRef.getDownloadURL()
]);
}
}
Loading

0 comments on commit 1c51ce9

Please sign in to comment.