diff --git a/README.md b/README.md
index 8a0f2c4c..2a5b25f2 100644
--- a/README.md
+++ b/README.md
@@ -7,22 +7,14 @@
-## 너의 날씨는 팀 소개
-
-|[송윤지](https://github.com/somarok)|[장날아](https://github.com/NalaJang)|[전종현](https://github.com/hoogom88)|
-|:----:|:----:|:----:|
-||||
-
-|[성종호](https://github.com/SeongJongHo)|[김동혁](https://github.com/KimDonghyeok)|[김민규](https://github.com/gyubit)|
-|:----:|:----:|:----:|
-||||
-
-
+
## 앱 기획 의도
매일 날씨 확인, 옷 고민 하는데 시간 쏟기 귀찮았던 사람들이 모여서 ‘WeaCo’를 만들었습니다.
+
+
## 서비스 소개
유저의 현재 위치에 대한 날씨 정보 제공과 날씨를 기반으로 OOTD를 공유하는 서비스
@@ -31,6 +23,7 @@
+
## Configuration
@@ -39,6 +32,8 @@
- 라이브러리
+
+
## 앱 전체 구조 / 아키텍처 이미지
![image](https://github.com/Team-Weather/ci_test/assets/90754590/cfa21382-ffa8-4015-b18e-3120aa640002)
@@ -48,3 +43,15 @@
## 전체 플로우 차트
![image](https://github.com/Team-Weather/ci_test/assets/73895803/e532d7cd-999f-4451-9f18-83f9e7248f77)
+
+
+
+## 너의 날씨는 팀 소개
+
+|[송윤지](https://github.com/somarok)|[장날아](https://github.com/NalaJang)|[전종현](https://github.com/hoogom88)|
+|:----:|:----:|:----:|
+||||
+
+|[성종호](https://github.com/SeongJongHo)|[김동혁](https://github.com/KimDonghyeok)|[김민규](https://github.com/gyubit)|
+|:----:|:----:|:----:|
+||||
diff --git a/lib/core/db/transaction_service.dart b/lib/core/db/transaction_service.dart
new file mode 100644
index 00000000..42f92096
--- /dev/null
+++ b/lib/core/db/transaction_service.dart
@@ -0,0 +1,3 @@
+abstract interface class TransactionService {
+ Future run(Function callBack);
+}
diff --git a/lib/core/di/common/common_di_setup.dart b/lib/core/di/common/common_di_setup.dart
index 95105f1e..3d3a9840 100644
--- a/lib/core/di/common/common_di_setup.dart
+++ b/lib/core/di/common/common_di_setup.dart
@@ -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';
@@ -15,6 +17,11 @@ void commonDiSetup() {
() => FirebaseFirestore.instance);
getIt.registerLazySingleton(() => FirebaseStorage.instance);
getIt.registerLazySingleton(() => FirebaseAuthService());
+ getIt.registerLazySingleton(
+ () => FirestoreService(
+ fireStore: getIt(),
+ ),
+ );
// PathProvider
getIt.registerLazySingleton(() => PathProviderService());
diff --git a/lib/core/di/di_setup.dart b/lib/core/di/di_setup.dart
index 3a778569..30a51fe1 100644
--- a/lib/core/di/di_setup.dart
+++ b/lib/core/di/di_setup.dart
@@ -7,6 +7,7 @@ import 'package:weaco/core/di/user/user_di_setup.dart';
import 'package:weaco/core/di/weather/weather_di_setup.dart';
import 'package:weaco/domain/feed/use_case/get_my_page_feeds_use_case.dart';
import 'package:weaco/domain/feed/use_case/get_recommended_feeds_use_case.dart';
+import 'package:weaco/domain/feed/use_case/get_search_feeds_use_case.dart';
import 'package:weaco/domain/feed/use_case/remove_my_page_feed_use_case.dart';
import 'package:weaco/domain/user/repository/user_auth_repository.dart';
import 'package:weaco/domain/user/use_case/get_my_profile_use_case.dart';
@@ -61,6 +62,7 @@ void diSetup() {
getDailyLocationWeatherUseCase: getIt(),
getBackgroundImageListUseCase: getIt(),
getRecommendedFeedsUseCase: getIt(),
+ getSearchFeedsUseCase: getIt(),
),
);
diff --git a/lib/core/di/feed/feed_di_setup.dart b/lib/core/di/feed/feed_di_setup.dart
index c1f51616..33f7fafa 100644
--- a/lib/core/di/feed/feed_di_setup.dart
+++ b/lib/core/di/feed/feed_di_setup.dart
@@ -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';
@@ -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';
@@ -30,10 +31,14 @@ void feedDiSetup() {
// Repository
getIt.registerLazySingleton(() =>
FeedRepositoryImpl(remoteFeedDataSource: getIt()));
- getIt.registerLazySingleton(() => OotdFeedRepositoryImpl(
+ getIt.registerLazySingleton(
+ () => OotdFeedRepositoryImpl(
fileRepository: getIt(),
- feedRepository: getIt(),
- userProfileRepository: getIt()));
+ remoteFeedDataSource: getIt(),
+ remoteUserProfileDataSource: getIt(),
+ firestoreService: getIt(),
+ ),
+ );
// UseCase
getIt.registerLazySingleton(() =>
diff --git a/lib/core/di/file/file_di_setup.dart b/lib/core/di/file/file_di_setup.dart
index 088d7e26..c947e3af 100644
--- a/lib/core/di/file/file_di_setup.dart
+++ b/lib/core/di/file/file_di_setup.dart
@@ -2,6 +2,7 @@ import 'package:firebase_storage/firebase_storage.dart';
import 'package:weaco/core/di/di_setup.dart';
import 'package:weaco/core/firebase/firebase_auth_service.dart';
import 'package:weaco/core/path_provider/path_provider_service.dart';
+import 'package:weaco/core/util/image_compressor_util.dart';
import 'package:weaco/data/file/data_source/local/local_file_data_source.dart';
import 'package:weaco/data/file/data_source/local/local_file_data_source_impl.dart';
import 'package:weaco/data/file/data_source/remote/remote_file_data_source.dart';
@@ -27,7 +28,7 @@ void fileDiSetup() {
// UseCase
getIt.registerLazySingleton(
- () => SaveImageUseCase(fileRepository: getIt()));
+ () => SaveImageUseCase(fileRepository: getIt(), imageCompressor: ImageCompressorImpl()));
getIt.registerLazySingleton(
() => GetImageUseCase(fileRepository: getIt()));
}
diff --git a/lib/core/enum/image_type.dart b/lib/core/enum/image_type.dart
new file mode 100644
index 00000000..b48afe61
--- /dev/null
+++ b/lib/core/enum/image_type.dart
@@ -0,0 +1,5 @@
+enum ImageType {
+ origin,
+ cropped,
+ compressed
+}
\ No newline at end of file
diff --git a/lib/core/firebase/firestore_dto_mapper.dart b/lib/core/firebase/firestore_dto_mapper.dart
index ccf65f00..e5bf3cce 100644
--- a/lib/core/firebase/firestore_dto_mapper.dart
+++ b/lib/core/firebase/firestore_dto_mapper.dart
@@ -8,6 +8,7 @@ Feed toFeed({required Map json, required String id}) {
return Feed(
id: id,
imagePath: json['image_path'],
+ thumbnailImagePath: json['thumbnail_image_path'] ?? json['image_path'],
userEmail: json['user_email'],
description: json['description'],
weather: Weather.fromJson(json['weather']),
@@ -23,6 +24,7 @@ Feed toFeed({required Map json, required String id}) {
Map toFeedDto({required Feed feed}) {
return {
'image_path': feed.imagePath,
+ 'thumbnail_image_path': feed.thumbnailImagePath,
'user_email': feed.userEmail,
'description': feed.description,
'weather': feed.weather.toJson(),
diff --git a/lib/core/firebase/firestore_service.dart b/lib/core/firebase/firestore_service.dart
new file mode 100644
index 00000000..5e518ff1
--- /dev/null
+++ b/lib/core/firebase/firestore_service.dart
@@ -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 run(Function callBack) async {
+ return _fireStore.runTransaction((transaction) async {
+ return await callBack(transaction);
+ }).then(
+ (value) => true,
+ onError: (e) {
+ throw Exception('피드 업로드에 실패 하였습니다.');
+ },
+ );
+ }
+}
diff --git a/lib/core/path_provider/path_provider_service.dart b/lib/core/path_provider/path_provider_service.dart
index 9ae85153..36eb6ac3 100644
--- a/lib/core/path_provider/path_provider_service.dart
+++ b/lib/core/path_provider/path_provider_service.dart
@@ -2,6 +2,6 @@ import 'package:path_provider/path_provider.dart';
class PathProviderService {
Future getCacheDirectory() async {
- return (await getApplicationCacheDirectory()).path;
+ return (await getApplicationDocumentsDirectory()).path;
}
}
diff --git a/lib/core/util/image_compressor_util.dart b/lib/core/util/image_compressor_util.dart
new file mode 100644
index 00000000..7b37fb51
--- /dev/null
+++ b/lib/core/util/image_compressor_util.dart
@@ -0,0 +1,16 @@
+import 'dart:io';
+import 'package:image/image.dart' as image;
+import 'package:weaco/domain/common/util/image_compressor.dart';
+
+class ImageCompressorImpl implements ImageCompressor {
+ int width= 1000;
+ int height = 1000 ~/ 9 * 16;
+
+ @override
+ Future> compressImage({required File file}) async {
+ image.Image originImage = (await image.decodeImageFile(file.path))!;
+ image.Image resizedImage = image.copyResize(originImage, width: width, height: height);
+ List compressedResizedImage = image.encodeJpg(resizedImage, quality: 80);
+ return compressedResizedImage;
+ }
+}
\ No newline at end of file
diff --git a/lib/data/feed/data_source/remote_feed_data_source.dart b/lib/data/feed/data_source/remote_feed_data_source.dart
index f6e8bf2b..81572826 100644
--- a/lib/data/feed/data_source/remote_feed_data_source.dart
+++ b/lib/data/feed/data_source/remote_feed_data_source.dart
@@ -1,3 +1,4 @@
+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';
@@ -5,7 +6,10 @@ abstract interface class RemoteFeedDataSource {
/// OOTD 피드 작성 성공 시 : 피드 업로드 요청(Feed) -> / 업로드 완료(bool) ← 파베
/// OOTD 편집 완료 후 [상세 페이지]: 위와 동일.
/// OOTD 편집 완료 후 [마이 페이지]: 위와 동일.*피드 업데이트
- Future saveFeed({required Feed feed});
+ Future saveFeed({
+ required Transaction transaction,
+ required Feed feed,
+ });
/// OOTD 피드 [상세 페이지] : 피드 데이터 요청 (id) -> 파베 / 피드 데이터 반환(json) ← 파베
Future getFeed({required String id});
@@ -18,17 +22,19 @@ abstract interface class RemoteFeedDataSource {
});
/// [마이페이지] 피드 삭제: 피드 삭제 요청(id) -> 파베/ 삭제 완료 (bool) from FB
- Future deleteFeed({required String id});
+ Future deleteFeed({
+ required Transaction transaction,
+ required String id,
+ });
/// [홈 화면] 하단 OOTD 추천 목록:
///
/// 유저의 위치와 기온을 기반으로 피드 목록을 불러옵니다.
/// @param city: 유저 위치의 도시명
/// @param temperature: 날씨 온도
- Future> getRecommendedFeedList({
- required DailyLocationWeather dailyLocationWeather,
- DateTime? createdAt
- });
+ Future> getRecommendedFeedList(
+ {required DailyLocationWeather dailyLocationWeather,
+ DateTime? createdAt});
/// [검색 페이지] 피드 검색:
///
diff --git a/lib/data/feed/data_source/remote_feed_data_source_impl.dart b/lib/data/feed/data_source/remote_feed_data_source_impl.dart
index c2b8d9a9..3e7763fa 100644
--- a/lib/data/feed/data_source/remote_feed_data_source_impl.dart
+++ b/lib/data/feed/data_source/remote_feed_data_source_impl.dart
@@ -14,23 +14,22 @@ class RemoteFeedDataSourceImpl implements RemoteFeedDataSource {
/// OOTD 피드 작성 또는 편집 후 저장
@override
- Future saveFeed({required Feed feed}) async {
+ Future 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 피드 상세 페이지]:
@@ -75,11 +74,23 @@ class RemoteFeedDataSourceImpl implements RemoteFeedDataSource {
/// [마이페이지] 피드 삭제
/// soft delete 처리
@override
- Future deleteFeed({required String id}) async {
- await _fireStore
- .collection('feeds')
- .doc(id)
- .update({'deleted_at': Timestamp.fromDate(DateTime.now())});
+ Future 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;
}
diff --git a/lib/data/feed/repository/feed_repository_impl.dart b/lib/data/feed/repository/feed_repository_impl.dart
index 0c7b0f9a..a4e24e72 100644
--- a/lib/data/feed/repository/feed_repository_impl.dart
+++ b/lib/data/feed/repository/feed_repository_impl.dart
@@ -10,16 +10,6 @@ class FeedRepositoryImpl implements FeedRepository {
required this.remoteFeedDataSource,
});
- @override
- Future saveFeed({required Feed editedFeed}) {
- return remoteFeedDataSource.saveFeed(feed: editedFeed);
- }
-
- @override
- Future deleteFeed({required String id}) async {
- return await remoteFeedDataSource.deleteFeed(id: id);
- }
-
/// 피드의 id 값을 전달 하여 Firebase 내의 해당 피드를 가져 와야 한다.
/// GetDetailFeedDetailUseCase에서 사용
/// @param id: 피드 id
@@ -59,7 +49,7 @@ class FeedRepositoryImpl implements FeedRepository {
}) async {
return await remoteFeedDataSource.getRecommendedFeedList(
dailyLocationWeather: dailyLocationWeather,
- createdAt: createdAt
+ createdAt: createdAt,
);
}
diff --git a/lib/data/feed/repository/ootd_feed_repository_impl.dart b/lib/data/feed/repository/ootd_feed_repository_impl.dart
index 69b3c4a8..59c9a393 100644
--- a/lib/data/feed/repository/ootd_feed_repository_impl.dart
+++ b/lib/data/feed/repository/ootd_feed_repository_impl.dart
@@ -1,45 +1,68 @@
+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 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 _save({required Feed feed}) async {
- final String path = await _fileRepository.saveOotdImage();
+ Future _save({
+ required Transaction transaction,
+ required Feed feed,
+ }) async {
+ final List path = await _fileRepository.saveOotdImage();
- final saveResult = await _feedRepository.saveFeed(
- editedFeed: feed.copyWith(imagePath: path));
+ 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 _update({required Feed feed}) async {
- final updateResult = await _feedRepository.saveFeed(editedFeed: feed);
+ Future _update({
+ required Transaction transaction,
+ required Feed feed,
+ }) async {
+ final updateResult = await _remoteFeedDataSource.saveFeed(
+ transaction: transaction,
+ feed: feed,
+ );
return updateResult;
}
@@ -47,19 +70,31 @@ class OotdFeedRepositoryImpl implements OotdFeedRepository {
/// 피드 삭제
@override
Future 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 _updateMyFeedCount(int count) async {
- final myProfile = await _userProfileRepository.getMyProfile();
+ Future _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),
+ );
}
}
diff --git a/lib/data/file/data_source/local/local_file_data_source.dart b/lib/data/file/data_source/local/local_file_data_source.dart
index 17aa9e27..f58a75bf 100644
--- a/lib/data/file/data_source/local/local_file_data_source.dart
+++ b/lib/data/file/data_source/local/local_file_data_source.dart
@@ -1,7 +1,15 @@
import 'dart:io';
+import 'package:weaco/core/enum/image_type.dart';
+
abstract interface class LocalFileDataSource {
- Future getImage({required bool isOrigin});
+ Future getImage({required ImageType imageType});
Future saveImage({required bool isOrigin, required File file});
+
+ Future getCompressedImage();
+
+ Future saveCompressedImage({required List image});
+
+
}
diff --git a/lib/data/file/data_source/local/local_file_data_source_impl.dart b/lib/data/file/data_source/local/local_file_data_source_impl.dart
index 588e71a4..d5471963 100644
--- a/lib/data/file/data_source/local/local_file_data_source_impl.dart
+++ b/lib/data/file/data_source/local/local_file_data_source_impl.dart
@@ -1,21 +1,27 @@
import 'dart:developer';
import 'dart:io';
+import 'package:weaco/core/enum/image_type.dart';
import 'package:weaco/core/path_provider/path_provider_service.dart';
import 'local_file_data_source.dart';
class LocalFileDataSourceImpl implements LocalFileDataSource {
final _originImageFileName = 'origin.png';
final _croppedImageFileName = 'cropped.png';
+ final _compressedImageFileName = 'compressed.png';
final PathProviderService _pathProvider;
LocalFileDataSourceImpl({required PathProviderService pathProvider})
: _pathProvider = pathProvider;
@override
- Future getImage({required bool isOrigin}) async {
+ Future getImage({required ImageType imageType}) async {
try {
final directory = await _pathProvider.getCacheDirectory();
- String fileName = isOrigin ? _originImageFileName : _croppedImageFileName;
+ String fileName = switch(imageType) {
+ ImageType.origin => _originImageFileName ,
+ ImageType.cropped => _croppedImageFileName,
+ ImageType.compressed => _compressedImageFileName,
+ };
return (await File('$directory/$fileName').exists())
? File('$directory/$fileName')
: null;
@@ -40,4 +46,30 @@ class LocalFileDataSourceImpl implements LocalFileDataSource {
return false;
}
}
+
+ @override
+ Future getCompressedImage() async {
+ try {
+ final directory = await _pathProvider.getCacheDirectory();
+ return (await File('$directory/$_compressedImageFileName').exists())
+ ? File('$directory/$_compressedImageFileName')
+ : null;
+ } catch (e) {
+ log(e.toString(), name: 'LocalFileDataSourceImpl.getCompressedImage()');
+ return null;
+ }
+ }
+
+ @override
+ Future saveCompressedImage({required List image}) async {
+ try {
+ final directory = await _pathProvider.getCacheDirectory();
+ if (await File('$directory/$_compressedImageFileName').exists()) {
+ await File('$directory/$_compressedImageFileName').delete();
+ }
+ await File('$directory/$_compressedImageFileName').writeAsBytes(image);
+ } catch (e) {
+ log(e.toString(), name: 'LocalFileDataSourceImpl.saveCompressedImage()');
+ }
+ }
}
diff --git a/lib/data/file/data_source/remote/remote_file_data_source.dart b/lib/data/file/data_source/remote/remote_file_data_source.dart
index e9ba0cca..d06b7c04 100644
--- a/lib/data/file/data_source/remote/remote_file_data_source.dart
+++ b/lib/data/file/data_source/remote/remote_file_data_source.dart
@@ -1,5 +1,5 @@
import 'dart:io';
abstract interface class RemoteFileDataSource {
- Future saveImage({required File image});
+ Future> saveImage({required File croppedImage, required File compressedImage});
}
diff --git a/lib/data/file/data_source/remote/remote_file_data_source_impl.dart b/lib/data/file/data_source/remote/remote_file_data_source_impl.dart
index d0aa7677..511bc7c8 100644
--- a/lib/data/file/data_source/remote/remote_file_data_source_impl.dart
+++ b/lib/data/file/data_source/remote/remote_file_data_source_impl.dart
@@ -9,20 +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 saveImage({required File image}) async {
+ Future> saveImage(
+ {required File croppedImage, required File compressedImage}) async {
final String? email = _firebaseAuthService.firebaseAuth.currentUser?.email;
if (email == null) throw Exception();
- final feedImageRef = _firebaseStorage.ref().child(
- 'feed_images/${email}_${DateTime
- .now()
- .microsecondsSinceEpoch}.png');
- await feedImageRef.putFile(image);
+ final feedOriginImageRef = _firebaseStorage.ref().child(
+ 'feed_origin_images/${email}_${DateTime.now().microsecondsSinceEpoch}.png');
+ final feedThumbnailImageRef = _firebaseStorage.ref().child(
+ 'feed_thumbnail_images/${email}_${DateTime.now().microsecondsSinceEpoch}.png');
- return await feedImageRef.getDownloadURL();
+ await Future.wait([
+ feedOriginImageRef.putFile(croppedImage),
+ feedThumbnailImageRef.putFile(compressedImage),
+ ]);
+
+ return await Future.wait([
+ feedOriginImageRef.getDownloadURL(),
+ feedThumbnailImageRef.getDownloadURL()
+ ]);
}
}
diff --git a/lib/data/file/repository/file_repository_impl.dart b/lib/data/file/repository/file_repository_impl.dart
index 2643126d..69e9c71c 100644
--- a/lib/data/file/repository/file_repository_impl.dart
+++ b/lib/data/file/repository/file_repository_impl.dart
@@ -1,4 +1,5 @@
import 'dart:io';
+import 'package:weaco/core/enum/image_type.dart';
import 'package:weaco/data/file/data_source/local/local_file_data_source.dart';
import 'package:weaco/data/file/data_source/remote/remote_file_data_source.dart';
import 'package:weaco/domain/file/repository/file_repository.dart';
@@ -14,19 +15,22 @@ class FileRepositoryImpl implements FileRepository {
_remoteFileDataSource = remoteFileDataSource;
@override
- Future getImage({required bool isOrigin}) async {
- return await _localFileDataSource.getImage(isOrigin: isOrigin);
+ Future getImage({required ImageType imageType}) async {
+ return await _localFileDataSource.getImage(imageType: imageType);
}
@override
- Future saveImage({required bool isOrigin, required File file}) async {
+ Future saveImage({required bool isOrigin, required File file, required List compressedImage}) async {
+ await _localFileDataSource.saveCompressedImage(image: compressedImage);
return await _localFileDataSource.saveImage(isOrigin: isOrigin, file: file);
}
+
@override
- Future saveOotdImage() async {
- final File? image = await _localFileDataSource.getImage(isOrigin: false);
- if (image == null) throw Exception();
- return await _remoteFileDataSource.saveImage(image: image);
+ Future> saveOotdImage() async {
+ final File? croppedImage = await _localFileDataSource.getImage(imageType: ImageType.cropped);
+ final File? compressedImage = await _localFileDataSource.getImage(imageType: ImageType.compressed);
+ if (croppedImage == null || compressedImage == null) throw Exception();
+ return await _remoteFileDataSource.saveImage(croppedImage: croppedImage, compressedImage: compressedImage);
}
}
diff --git a/lib/data/user/data_source/remote_user_profile_data_source.dart b/lib/data/user/data_source/remote_user_profile_data_source.dart
index dad87273..db66618a 100644
--- a/lib/data/user/data_source/remote_user_profile_data_source.dart
+++ b/lib/data/user/data_source/remote_user_profile_data_source.dart
@@ -1,3 +1,4 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:weaco/domain/user/model/user_profile.dart';
/// firebase에서 유저 프로필 정보를 읽고 쓰기 위한 데이터 소스
@@ -6,7 +7,10 @@ abstract interface class RemoteUserProfileDataSource {
Future getUserProfile({String email});
/// 유저 프로필 업데이트 (피드 갯수 수정 할 때 주로 쓰임)
- Future updateUserProfile({required UserProfile userProfile});
+ Future updateUserProfile({
+ required Transaction transaction,
+ required UserProfile userProfile,
+ });
/// 유저 프로필 저장
Future saveUserProfile({required UserProfile userProfile});
diff --git a/lib/data/user/data_source/remote_user_profile_date_source_impl.dart b/lib/data/user/data_source/remote_user_profile_date_source_impl.dart
index 7b342f3a..09f42ec1 100644
--- a/lib/data/user/data_source/remote_user_profile_date_source_impl.dart
+++ b/lib/data/user/data_source/remote_user_profile_date_source_impl.dart
@@ -46,21 +46,26 @@ class RemoteUserProfileDataSourceImpl implements RemoteUserProfileDataSource {
}
@override
- Future updateUserProfile({required UserProfile userProfile}) async {
+ Future updateUserProfile({
+ required Transaction transaction,
+ required UserProfile userProfile,
+ }) async {
try {
- final originProfileDocument = await _firestore
+ // 기존 프로필 문서를 검색
+ final originProfileQuery = await _firestore
.collection('user_profiles')
.where('email', isEqualTo: userProfile.email)
.get();
- return await _firestore
- .collection('user_profiles')
- .doc(originProfileDocument.docs[0].reference.id)
- .set(toUserProfileDto(userProfile: userProfile))
- .then((value) => true)
- .catchError(
- (e) => false,
- );
+ if (originProfileQuery.docs.isEmpty) {
+ throw Exception('User profile not found');
+ }
+
+ final originProfileDocRef = originProfileQuery.docs[0].reference;
+ transaction.set(
+ originProfileDocRef, toUserProfileDto(userProfile: userProfile));
+
+ return true;
} catch (e) {
throw Exception(e);
}
diff --git a/lib/data/user/repository/user_profile_repository_impl.dart b/lib/data/user/repository/user_profile_repository_impl.dart
index b3e3d1d6..6344a51f 100644
--- a/lib/data/user/repository/user_profile_repository_impl.dart
+++ b/lib/data/user/repository/user_profile_repository_impl.dart
@@ -18,9 +18,4 @@ class UserProfileRepositoryImpl implements UserProfileRepository {
Future getUserProfile({required String email}) async {
return await _remoteUserProfileDataSource.getUserProfile(email: email);
}
-
- @override
- Future updateUserProfile({required UserProfile userProfile}) async {
- return await _remoteUserProfileDataSource.updateUserProfile(userProfile: userProfile);
- }
}
diff --git a/lib/domain/common/util/image_compressor.dart b/lib/domain/common/util/image_compressor.dart
new file mode 100644
index 00000000..51938c56
--- /dev/null
+++ b/lib/domain/common/util/image_compressor.dart
@@ -0,0 +1,5 @@
+import 'dart:io';
+
+abstract interface class ImageCompressor {
+ Future> compressImage({required File file});
+}
diff --git a/lib/domain/feed/model/feed.dart b/lib/domain/feed/model/feed.dart
index d3734f25..7d53c691 100644
--- a/lib/domain/feed/model/feed.dart
+++ b/lib/domain/feed/model/feed.dart
@@ -4,6 +4,7 @@ import 'package:weaco/domain/weather/model/weather.dart';
class Feed {
final String? id;
final String imagePath;
+ final String thumbnailImagePath;
final String userEmail;
final String description;
final Weather weather;
@@ -15,6 +16,7 @@ class Feed {
Feed({
required this.id,
required this.imagePath,
+ required this.thumbnailImagePath,
required this.userEmail,
required this.description,
required this.weather,
@@ -26,7 +28,7 @@ class Feed {
@override
String toString() {
- return 'Feed(id: $id, imagePath: $imagePath, userEmail: $userEmail, description: $description, weather: $weather, seasonCode: $seasonCode, location: $location, createdAt: $createdAt, deletedAt: $deletedAt)';
+ return 'Feed(id: $id, imagePath: $imagePath, thumbnailImagePath: $thumbnailImagePath, userEmail: $userEmail, description: $description, weather: $weather, seasonCode: $seasonCode, location: $location, createdAt: $createdAt, deletedAt: $deletedAt)';
}
@override
@@ -35,6 +37,7 @@ class Feed {
return other.id == id &&
other.imagePath == imagePath &&
+ other.thumbnailImagePath == thumbnailImagePath &&
other.userEmail == userEmail &&
other.description == description &&
other.weather == weather &&
@@ -48,6 +51,7 @@ class Feed {
int get hashCode {
return id.hashCode ^
imagePath.hashCode ^
+ thumbnailImagePath.hashCode ^
userEmail.hashCode ^
description.hashCode ^
weather.hashCode ^
@@ -60,6 +64,7 @@ class Feed {
Feed copyWith({
String? id,
String? imagePath,
+ String? thumbnailImagePath,
String? userEmail,
String? description,
Weather? weather,
@@ -71,6 +76,7 @@ class Feed {
return Feed(
id: id ?? this.id,
imagePath: imagePath ?? this.imagePath,
+ thumbnailImagePath: thumbnailImagePath ?? this.thumbnailImagePath,
userEmail: userEmail ?? this.userEmail,
description: description ?? this.description,
weather: weather ?? this.weather,
@@ -85,6 +91,7 @@ class Feed {
return {
'id': id,
'image_path': imagePath,
+ 'thumbnail_image_path': thumbnailImagePath,
'user_email': userEmail,
'description': description,
'weather': weather.toJson(),
@@ -99,6 +106,7 @@ class Feed {
return Feed(
id: map['id'] as String,
imagePath: map['image_path'] as String,
+ thumbnailImagePath: map['thumbnail_image_path'] as String,
userEmail: map['user_email'] as String,
description: map['description'] as String,
weather: Weather.fromJson(map['weather']),
diff --git a/lib/domain/feed/repository/feed_repository.dart b/lib/domain/feed/repository/feed_repository.dart
index 9181701f..2aee6428 100644
--- a/lib/domain/feed/repository/feed_repository.dart
+++ b/lib/domain/feed/repository/feed_repository.dart
@@ -13,12 +13,6 @@ abstract interface class FeedRepository {
/// 피드의 상세 정보를 가져옵니다.
Future getFeed({required String id});
- /// 피드를 삭제합니다.
- Future deleteFeed({required String id});
-
- /// 새 피드를 저장하거나 편집된 피드를 업데이트 합니다.
- Future saveFeed({required Feed editedFeed});
-
/// [홈 하단]
/// 추천 OOTD 목록을 불러옵니다.
Future> getRecommendedFeedList({
diff --git a/lib/domain/file/repository/file_repository.dart b/lib/domain/file/repository/file_repository.dart
index 6d2052a7..1e6537c2 100644
--- a/lib/domain/file/repository/file_repository.dart
+++ b/lib/domain/file/repository/file_repository.dart
@@ -1,9 +1,11 @@
import 'dart:io';
+import 'package:weaco/core/enum/image_type.dart';
+
abstract interface class FileRepository {
- Future getImage({required bool isOrigin});
+ Future getImage({required ImageType imageType});
- Future saveImage({required bool isOrigin, required File file});
+ Future saveImage({required bool isOrigin, required File file, required List compressedImage});
- Future saveOotdImage();
+ Future> saveOotdImage();
}
diff --git a/lib/domain/file/use_case/get_image_use_case.dart b/lib/domain/file/use_case/get_image_use_case.dart
index 5c222d6b..fc0ff760 100644
--- a/lib/domain/file/use_case/get_image_use_case.dart
+++ b/lib/domain/file/use_case/get_image_use_case.dart
@@ -1,5 +1,6 @@
import 'dart:io';
+import 'package:weaco/core/enum/image_type.dart';
import 'package:weaco/domain/file/repository/file_repository.dart';
class GetImageUseCase {
@@ -11,7 +12,7 @@ class GetImageUseCase {
/// 레포지토리에 크롭 전 이미지를 요청
/// @param isOrigin: 원본 이미지 = true, 크롭된 이미지 = false
/// @return: 요청한 이미지 파일 반환
- Future execute({required bool isOrigin}) async {
- return await _fileRepository.getImage(isOrigin: isOrigin);
+ Future execute({required ImageType imageType}) async {
+ return await _fileRepository.getImage(imageType: imageType);
}
}
diff --git a/lib/domain/file/use_case/save_image_use_case.dart b/lib/domain/file/use_case/save_image_use_case.dart
index 107dceea..0c6c3a6a 100644
--- a/lib/domain/file/use_case/save_image_use_case.dart
+++ b/lib/domain/file/use_case/save_image_use_case.dart
@@ -1,17 +1,23 @@
import 'dart:io';
+import 'package:weaco/domain/common/util/image_compressor.dart';
import 'package:weaco/domain/file/repository/file_repository.dart';
class SaveImageUseCase {
final FileRepository _fileRepository;
+ final ImageCompressor _imageCompressor;
- SaveImageUseCase({required FileRepository fileRepository})
- : _fileRepository = fileRepository;
+ SaveImageUseCase(
+ {required FileRepository fileRepository,
+ required ImageCompressor imageCompressor})
+ : _fileRepository = fileRepository,
+ _imageCompressor = imageCompressor;
/// 레포지토리에 이미지 파일 데이터를 전달하여 저장을 요청
/// @param isOrigin: 원본 이미지 = true, 크롭된 이미지 = false
/// @param file: 저장할 이미지 데이터
/// @return: 데이터 저장 성공 여부 반환
Future execute({required bool isOrigin, required File file}) async {
- return await _fileRepository.saveImage(isOrigin: isOrigin, file: file);
+ final List compressedImage = await _imageCompressor.compressImage(file: file);
+ return await _fileRepository.saveImage(isOrigin: isOrigin, file: file, compressedImage: compressedImage);
}
}
diff --git a/lib/domain/user/repository/user_profile_repository.dart b/lib/domain/user/repository/user_profile_repository.dart
index 25644f6f..af768752 100644
--- a/lib/domain/user/repository/user_profile_repository.dart
+++ b/lib/domain/user/repository/user_profile_repository.dart
@@ -4,6 +4,4 @@ abstract interface class UserProfileRepository {
Future getUserProfile({required String email});
Future getMyProfile();
-
- Future updateUserProfile({required UserProfile userProfile});
-}
\ No newline at end of file
+}
diff --git a/lib/presentation/common/component/cached_image_widget.dart b/lib/presentation/common/component/cached_image_widget.dart
index 7498c7cb..b5e23eb8 100644
--- a/lib/presentation/common/component/cached_image_widget.dart
+++ b/lib/presentation/common/component/cached_image_widget.dart
@@ -24,6 +24,7 @@ class CachedImageWidget extends StatelessWidget {
return CachedNetworkImage(
fit: _boxFit ?? BoxFit.cover,
imageUrl: _imageUrl,
+ // Flip Card 위젯 높이와 동일 = 16 * 35
memCacheHeight: (16 * 35).cacheSize(context),
progressIndicatorBuilder: (context, url, downloadProgress) =>
_progressIndicatorBuilder ??
diff --git a/lib/presentation/common/style/image_path.dart b/lib/presentation/common/style/image_path.dart
index d4d68842..b09b219a 100644
--- a/lib/presentation/common/style/image_path.dart
+++ b/lib/presentation/common/style/image_path.dart
@@ -1,3 +1,5 @@
+import 'package:flutter_dotenv/flutter_dotenv.dart';
+
class ImagePath {
static const String _iconPath = 'asset/icon';
static const String _imagePath = 'asset/image';
@@ -34,7 +36,7 @@ class ImagePath {
static const String weatherThunderStormHail =
'$_iconPath/weather_thunder_storm_hail_icon.png';
- static const String homeBackgroundSunny = '$_imagePath/home_bg_sunny.jpg';
+ static final String homeBackgroundSunny = ImageConfig.defaultBackgroundImage;
static const String imageIconHome = '$_iconPath/weaco_home_weather_icon.png';
@@ -62,3 +64,7 @@ class ImagePath {
/// 주황 구름 + 타이포 로고 이미지
static const String weacoLogoWithTypo = '$_imagePath/logo_with_typo.png';
}
+
+class ImageConfig {
+ static final String defaultBackgroundImage = dotenv.env['DEFAULT_BACKGROUND_IMAGE'] ?? 'undefined';
+}
\ No newline at end of file
diff --git a/lib/presentation/home/component/recommend_ootd_list_widget.dart b/lib/presentation/home/component/recommend_ootd_list_widget.dart
index c66f21aa..551cbe54 100644
--- a/lib/presentation/home/component/recommend_ootd_list_widget.dart
+++ b/lib/presentation/home/component/recommend_ootd_list_widget.dart
@@ -9,10 +9,12 @@ class RecommendOotdListWidget extends StatelessWidget {
super.key,
required this.dailyLocationWeather,
required this.feedList,
+ required this.isLoading,
});
final DailyLocationWeather? dailyLocationWeather;
final List feedList;
+ final bool isLoading;
@override
Widget build(BuildContext context) {
@@ -32,34 +34,223 @@ class RecommendOotdListWidget extends StatelessWidget {
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
- feedList.isEmpty
- ? const Center(
- child: Text(
- '아직 등록된 코디가 없어요 :( \n 가장 먼저 OOTD를 올려보세요! :D',
- textAlign: TextAlign.center,
- ),
- )
- : Expanded(
- child: ListView.builder(
+ isLoading
+ ? Expanded(
+ child: ListView(
scrollDirection: Axis.horizontal,
- itemCount: feedList.length,
- itemBuilder: (context, index) {
- return GestureDetector(
- onTap: () => RouterStatic.pushToOotdDetail(
- context,
- feed: feedList[index]
- ),
- child: RecommendOotdWidget(
- feedList: feedList,
- index: index,
+ children: List.generate(
+ 20,
+ (index) => Shimmer(
+ linearGradient: _shimmerGradient,
+ child: ShimmerLoading(
+ isLoading: isLoading,
+ child: const RecommendOotdWidget(),
),
- );
- },
+ ),
+ ).toList(),
),
- ),
+ )
+ : feedList.isEmpty
+ ? const Center(
+ child: Text(
+ '아직 등록된 코디가 없어요 :( \n 가장 먼저 OOTD를 올려보세요! :D',
+ textAlign: TextAlign.center,
+ ),
+ )
+ : Expanded(
+ child: ListView(
+ scrollDirection: Axis.horizontal,
+ children: feedList.indexed
+ .map(
+ (e) => GestureDetector(
+ onTap: () => RouterStatic.pushToOotdDetail(
+ context,
+ feed: e.$2,
+ ),
+ child: RecommendOotdWidget(
+ feedImagePath: e.$2.thumbnailImagePath,
+ ),
+ ),
+ )
+ .toList(),
+ ),
+ ),
],
),
),
);
}
}
+
+const _shimmerGradient = LinearGradient(
+ colors: [
+ Color(0x305F5F5F),
+ Color(0x30FFFFFF),
+ Color(0x305F5F5F),
+ ],
+ stops: [
+ 0.1,
+ 0.3,
+ 0.4,
+ ],
+ begin: Alignment(-1.0, -0.3),
+ end: Alignment(1.0, 0.3),
+ tileMode: TileMode.clamp,
+);
+
+class Shimmer extends StatefulWidget {
+ static ShimmerState? of(BuildContext context) {
+ return context.findAncestorStateOfType();
+ }
+
+ const Shimmer({
+ super.key,
+ required this.linearGradient,
+ this.child,
+ });
+
+ final LinearGradient linearGradient;
+ final Widget? child;
+
+ @override
+ ShimmerState createState() => ShimmerState();
+}
+
+class ShimmerState extends State with SingleTickerProviderStateMixin {
+ late AnimationController _shimmerController;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _shimmerController = AnimationController.unbounded(vsync: this)
+ ..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000));
+ }
+
+ @override
+ void dispose() {
+ _shimmerController.dispose();
+ super.dispose();
+ }
+
+ LinearGradient get gradient => LinearGradient(
+ colors: widget.linearGradient.colors,
+ stops: widget.linearGradient.stops,
+ begin: widget.linearGradient.begin,
+ end: widget.linearGradient.end,
+ transform:
+ _SlidingGradientTransform(slidePercent: _shimmerController.value),
+ );
+
+ bool get isSized =>
+ (context.findRenderObject() as RenderBox?)?.hasSize ?? false;
+
+ Size get size => (context.findRenderObject() as RenderBox).size;
+
+ Offset getDescendantOffset({
+ required RenderBox descendant,
+ Offset offset = Offset.zero,
+ }) {
+ final shimmerBox = context.findRenderObject() as RenderBox?;
+ return descendant.localToGlobal(offset, ancestor: shimmerBox);
+ }
+
+ Listenable get shimmerChanges => _shimmerController;
+
+ @override
+ Widget build(BuildContext context) {
+ return widget.child ?? const SizedBox();
+ }
+}
+
+class _SlidingGradientTransform extends GradientTransform {
+ const _SlidingGradientTransform({
+ required this.slidePercent,
+ });
+
+ final double slidePercent;
+
+ @override
+ Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
+ return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0);
+ }
+}
+
+class ShimmerLoading extends StatefulWidget {
+ const ShimmerLoading({
+ super.key,
+ required this.isLoading,
+ required this.child,
+ });
+
+ final bool isLoading;
+ final Widget child;
+
+ @override
+ State createState() => _ShimmerLoadingState();
+}
+
+class _ShimmerLoadingState extends State {
+ Listenable? _shimmerChanges;
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ if (_shimmerChanges != null) {
+ _shimmerChanges!.removeListener(_onShimmerChange);
+ }
+ _shimmerChanges = Shimmer.of(context)?.shimmerChanges;
+ if (_shimmerChanges != null) {
+ _shimmerChanges!.addListener(_onShimmerChange);
+ }
+ }
+
+ @override
+ void dispose() {
+ _shimmerChanges?.removeListener(_onShimmerChange);
+ super.dispose();
+ }
+
+ void _onShimmerChange() {
+ if (widget.isLoading) {
+ setState(() {
+ // Update the shimmer painting.
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (!widget.isLoading) {
+ return widget.child;
+ }
+
+ // Collect ancestor shimmer info.
+ final shimmer = Shimmer.of(context)!;
+ if (!shimmer.isSized) {
+ // The ancestor Shimmer widget has not laid
+ // itself out yet. Return an empty box.
+ return const SizedBox();
+ }
+ final shimmerSize = shimmer.size;
+ final gradient = shimmer.gradient;
+ final offsetWithinShimmer = shimmer.getDescendantOffset(
+ descendant: context.findRenderObject() as RenderBox,
+ );
+
+ return ShaderMask(
+ blendMode: BlendMode.srcATop,
+ shaderCallback: (bounds) {
+ return gradient.createShader(
+ Rect.fromLTWH(
+ -offsetWithinShimmer.dx,
+ -offsetWithinShimmer.dy,
+ shimmerSize.width,
+ shimmerSize.height,
+ ),
+ );
+ },
+ child: widget.child,
+ );
+ }
+}
diff --git a/lib/presentation/home/component/recommend_ootd_widget.dart b/lib/presentation/home/component/recommend_ootd_widget.dart
index 131988f9..71dc49b6 100644
--- a/lib/presentation/home/component/recommend_ootd_widget.dart
+++ b/lib/presentation/home/component/recommend_ootd_widget.dart
@@ -1,16 +1,13 @@
import 'package:flutter/material.dart';
-import 'package:weaco/domain/feed/model/feed.dart';
import 'package:weaco/presentation/common/component/cached_image_widget.dart';
class RecommendOotdWidget extends StatelessWidget {
const RecommendOotdWidget({
super.key,
- required this.feedList,
- required this.index,
+ this.feedImagePath,
});
- final List feedList;
- final int index;
+ final String? feedImagePath;
@override
Widget build(BuildContext context) {
@@ -28,10 +25,11 @@ class RecommendOotdWidget extends StatelessWidget {
]),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
- child: CachedImageWidget(
- feedList[index].imagePath,
- ),
-
+ child: feedImagePath != null
+ ? CachedImageWidget(
+ feedImagePath!,
+ )
+ : null,
),
);
}
diff --git a/lib/presentation/home/screen/home_screen.dart b/lib/presentation/home/screen/home_screen.dart
index 1ec19ca4..a1df91d3 100644
--- a/lib/presentation/home/screen/home_screen.dart
+++ b/lib/presentation/home/screen/home_screen.dart
@@ -1,9 +1,11 @@
+import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
-import 'package:weaco/core/util/reaction_util.dart';
-import 'package:weaco/presentation/common/style/image_path.dart';
import 'package:weaco/core/enum/weather_code.dart';
+import 'package:weaco/core/util/reaction_util.dart';
+import 'package:weaco/domain/feed/model/feed.dart';
import 'package:weaco/presentation/common/enum/exception_alert.dart';
+import 'package:weaco/presentation/common/style/image_path.dart';
import 'package:weaco/presentation/common/util/alert_util.dart';
import 'package:weaco/presentation/home/component/recommend_ootd_list_widget.dart';
import 'package:weaco/presentation/home/component/weather_by_time_list_widget.dart';
@@ -25,7 +27,14 @@ class _HomeScreenState extends State {
Future.microtask(
() async {
- await context.read().initHomeScreen();
+ context.read().initHomeScreen();
+ final tmp = context.read().precacheList;
+ for (Feed e in tmp) {
+ if (mounted) {
+ await precacheImage(
+ CachedNetworkImageProvider(e.thumbnailImagePath), context);
+ }
+ }
},
);
}
@@ -59,14 +68,13 @@ class _HomeScreenState extends State {
return Scaffold(
body: switch (viewModel.status) {
HomeScreenStatus.error => const Center(child: Text('데이터를 불러올 수 없습니다.')),
- HomeScreenStatus.loading =>
- const Center(child: CircularProgressIndicator()),
HomeScreenStatus.idle => const SizedBox(),
- HomeScreenStatus.success => Container(
+ HomeScreenStatus.loading || HomeScreenStatus.success => Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fill,
- image: NetworkImage(viewModel.backgroundImagePath),
+ image:
+ CachedNetworkImageProvider(viewModel.backgroundImagePath),
),
),
child: SafeArea(
@@ -88,7 +96,8 @@ class _HomeScreenState extends State {
)),
// city
Text(
- viewModel.dailyLocationWeather!.location.city,
+ viewModel.dailyLocationWeather?.location.city ??
+ '-',
style: const TextStyle(
fontSize: 15,
color: Colors.white,
@@ -97,9 +106,11 @@ class _HomeScreenState extends State {
const SizedBox(height: 4),
// weather code description
Text(
- WeatherCode.fromValue(
- viewModel.currentWeather!.code)
- .description,
+ viewModel.currentWeather != null
+ ? WeatherCode.fromValue(
+ viewModel.currentWeather!.code)
+ .description
+ : '-',
style: const TextStyle(
fontSize: 15,
color: Colors.white,
@@ -112,7 +123,7 @@ class _HomeScreenState extends State {
)),
// current temperature
Text(
- '${viewModel.currentWeather!.temperature}℃',
+ '${viewModel.currentWeather?.temperature ?? '-'}℃',
style: const TextStyle(
color: Colors.white,
fontSize: 60,
@@ -130,7 +141,7 @@ class _HomeScreenState extends State {
Column(
children: [
Text(
- '최고 ${viewModel.dailyLocationWeather!.highTemperature}℃',
+ '최고 ${viewModel.dailyLocationWeather?.highTemperature ?? '-'}℃',
style: const TextStyle(
fontSize: 15,
color: Colors.white,
@@ -143,7 +154,7 @@ class _HomeScreenState extends State {
),
),
Text(
- '최저 ${viewModel.dailyLocationWeather!.lowTemperature}℃',
+ '최저 ${viewModel.dailyLocationWeather?.lowTemperature ?? '-'}℃',
style: const TextStyle(
fontSize: 15,
color: Colors.white,
@@ -229,6 +240,7 @@ class _HomeScreenState extends State {
// ootd list
RecommendOotdListWidget(
+ isLoading: viewModel.isRecommendOotdLoading,
dailyLocationWeather: viewModel.dailyLocationWeather,
feedList: viewModel.feedList,
),
diff --git a/lib/presentation/home/view_model/home_screen_view_model.dart b/lib/presentation/home/view_model/home_screen_view_model.dart
index fcb5ab3f..2a708cbc 100644
--- a/lib/presentation/home/view_model/home_screen_view_model.dart
+++ b/lib/presentation/home/view_model/home_screen_view_model.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:weaco/domain/feed/use_case/get_search_feeds_use_case.dart';
import 'package:weaco/presentation/common/style/image_path.dart';
import 'package:weaco/core/enum/weather_code.dart';
import 'package:weaco/domain/feed/model/feed.dart';
@@ -26,42 +27,58 @@ class HomeScreenViewModel with ChangeNotifier {
required this.getDailyLocationWeatherUseCase,
required this.getBackgroundImageListUseCase,
required this.getRecommendedFeedsUseCase,
+ required this.getSearchFeedsUseCase,
});
final GetDailyLocationWeatherUseCase getDailyLocationWeatherUseCase;
final GetBackgroundImageListUseCase getBackgroundImageListUseCase;
final GetRecommendedFeedsUseCase getRecommendedFeedsUseCase;
+ final GetSearchFeedsUseCase getSearchFeedsUseCase;
DailyLocationWeather? _dailyLocationWeather;
Weather? _currentWeather;
List _weatherByTimeList = [];
List _feedList = [];
+ List _precacheList = [];
HomeScreenStatus _status = HomeScreenStatus.idle;
// 전일 대비 온도차
double? _temperatureGap;
String? _weatherBackgroundImage;
+ bool _isRecommendOotdLoading = false;
DailyLocationWeather? get dailyLocationWeather => _dailyLocationWeather;
Weather? get currentWeather => _currentWeather;
double? get temperatureGap => _temperatureGap ?? 0;
List get feedList => _feedList;
+ List get precacheList => _precacheList;
HomeScreenStatus get status => _status;
String get backgroundImagePath =>
_weatherBackgroundImage ?? ImagePath.homeBackgroundSunny;
List get weatherByTimeList => _weatherByTimeList;
String get errorMesasge => _errorMessage;
String _errorMessage = '';
+ bool get isRecommendOotdLoading => _isRecommendOotdLoading;
Future initHomeScreen() async {
_status = HomeScreenStatus.loading;
+ _isRecommendOotdLoading = true;
notifyListeners();
try {
_dailyLocationWeather = await getDailyLocationWeatherUseCase.execute();
+ notifyListeners();
+
+ final [futureFeedList, futurePrecacheList] = await Future.wait([
+ getRecommendedFeedsUseCase.execute(
+ dailyLocationWeather: _dailyLocationWeather!,
+ ),
+ getSearchFeedsUseCase.execute(),
+ ]);
- _feedList = await getRecommendedFeedsUseCase.execute(
- dailyLocationWeather: _dailyLocationWeather!,
- );
+ _feedList = futureFeedList;
+ _precacheList = futurePrecacheList;
+ _isRecommendOotdLoading = false;
+ notifyListeners();
if (_dailyLocationWeather != null) {
// 현재 시간에 맞는 날씨 예보 빼내기
diff --git a/lib/presentation/my_page/screen/my_page_screen.dart b/lib/presentation/my_page/screen/my_page_screen.dart
index 151bab65..22955ed6 100644
--- a/lib/presentation/my_page/screen/my_page_screen.dart
+++ b/lib/presentation/my_page/screen/my_page_screen.dart
@@ -245,7 +245,7 @@ class _MyPageScreenState extends State {
},
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
- child: CachedImageWidget(currentFeed.imagePath),
+ child: CachedImageWidget(currentFeed.thumbnailImagePath),
),
);
}
diff --git a/lib/presentation/ootd_feed/component/flip_card_widget.dart b/lib/presentation/ootd_feed/component/flip_card_widget.dart
index 173f9b48..ff1a0f0c 100644
--- a/lib/presentation/ootd_feed/component/flip_card_widget.dart
+++ b/lib/presentation/ootd_feed/component/flip_card_widget.dart
@@ -204,7 +204,7 @@ class _FlipCardState extends State
child: ClipRRect(
borderRadius: BorderRadius.circular(30),
child: CachedImageWidget(
- _data.feed.imagePath,
+ _data.feed.thumbnailImagePath,
),
),
);
diff --git a/lib/presentation/ootd_post/view_model/ootd_post_view_model.dart b/lib/presentation/ootd_post/view_model/ootd_post_view_model.dart
index 9e70dd7e..bc80e11a 100644
--- a/lib/presentation/ootd_post/view_model/ootd_post_view_model.dart
+++ b/lib/presentation/ootd_post/view_model/ootd_post_view_model.dart
@@ -1,7 +1,7 @@
import 'dart:developer';
import 'dart:io';
-
import 'package:flutter/cupertino.dart';
+import 'package:weaco/core/enum/image_type.dart';
import 'package:weaco/domain/feed/model/feed.dart';
import 'package:weaco/domain/feed/use_case/save_edit_feed_use_case.dart';
import 'package:weaco/domain/file/use_case/get_image_use_case.dart';
@@ -38,7 +38,7 @@ class OotdPostViewModel with ChangeNotifier {
notifyListeners();
try {
- _croppedImage = await _getImageUseCase.execute(isOrigin: false);
+ _croppedImage = await _getImageUseCase.execute(imageType: ImageType.cropped);
_dailyLocationWeather = await _getDailyLocationWeatherUseCase.execute();
// 현재 시간 날씨
_weather = _dailyLocationWeather!.weatherList.firstWhere((element) {
@@ -59,7 +59,8 @@ class OotdPostViewModel with ChangeNotifier {
final now = DateTime.now();
final Feed feed = Feed(
id: null,
- imagePath: _croppedImage!.path,
+ imagePath: '',
+ thumbnailImagePath: '',
userEmail: email,
description: description,
weather: Weather(
@@ -91,6 +92,7 @@ class OotdPostViewModel with ChangeNotifier {
final editedFeed = Feed(
id: feed.id,
imagePath: feed.imagePath,
+ thumbnailImagePath: feed.thumbnailImagePath,
userEmail: feed.userEmail,
description: description,
weather: feed.weather,
@@ -106,7 +108,7 @@ class OotdPostViewModel with ChangeNotifier {
}
Future getOriginImage() async {
- _originImage = await _getImageUseCase.execute(isOrigin: true);
+ _originImage = await _getImageUseCase.execute(imageType: ImageType.origin);
if (_originImage == null) return;
}
diff --git a/lib/presentation/ootd_search/screen/ootd_search_screen.dart b/lib/presentation/ootd_search/screen/ootd_search_screen.dart
index 235339e1..4d1bf8c6 100644
--- a/lib/presentation/ootd_search/screen/ootd_search_screen.dart
+++ b/lib/presentation/ootd_search/screen/ootd_search_screen.dart
@@ -196,7 +196,7 @@ class _OotdSearchScreenState extends State {
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: CachedImageWidget(
- searchFeedList[index].imagePath,
+ searchFeedList[index].thumbnailImagePath,
)),
);
},
diff --git a/lib/presentation/user_page/screen/user_page_screen.dart b/lib/presentation/user_page/screen/user_page_screen.dart
index 6294077d..e191e32d 100644
--- a/lib/presentation/user_page/screen/user_page_screen.dart
+++ b/lib/presentation/user_page/screen/user_page_screen.dart
@@ -109,7 +109,7 @@ class _UserPageScreenState extends State {
},
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
- child: CachedImageWidget(currentFeed.imagePath),
+ child: CachedImageWidget(currentFeed.thumbnailImagePath),
),
);
}
diff --git a/pubspec.yaml b/pubspec.yaml
index c0933378..ca194304 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
-version: 1.0.0+1
+version: 1.1.0+1
environment:
sdk: '>=3.3.3 <4.0.0'
@@ -54,6 +54,7 @@ dependencies:
firebase_storage_mocks: ^0.6.1
hive_test: ^1.0.1
image_picker: ^1.1.1
+ image: ^4.2.0
permission_handler: ^11.3.1
intl: ^0.18.1
flutter_localizations:
diff --git a/test/data/feed/data_source/remote_feed_data_source_impl_test.dart b/test/data/feed/data_source/remote_feed_data_source_impl_test.dart
index ee74312c..5156c097 100644
--- a/test/data/feed/data_source/remote_feed_data_source_impl_test.dart
+++ b/test/data/feed/data_source/remote_feed_data_source_impl_test.dart
@@ -41,6 +41,7 @@ void main() {
final mockFeed = Feed(
id: 'id',
imagePath: 'imagePath',
+ thumbnailImagePath: 'thumbnailImagePath',
userEmail: 'test@email.com',
description: 'This is a test feed',
weather: mockWeather,
@@ -50,7 +51,13 @@ void main() {
);
// When
- final result = await dataSource.saveFeed(feed: mockFeed);
+ final result =
+ await fakeFirestore.runTransaction((transaction) async {
+ return await dataSource.saveFeed(
+ transaction: transaction,
+ feed: mockFeed,
+ );
+ });
// Then
expect(result, true);
@@ -76,6 +83,7 @@ void main() {
final mockFeed = Feed(
id: 'gyubro',
imagePath: 'imagePath',
+ thumbnailImagePath: 'thumbnailImagePath',
userEmail: 'test@email.com',
description: 'This is a test feed',
weather: mockWeather,
@@ -83,7 +91,12 @@ void main() {
location: mockLocation,
createdAt: dateTime,
);
- await dataSource.saveFeed(feed: mockFeed);
+ await fakeFirestore.runTransaction((transaction) async {
+ return await dataSource.saveFeed(
+ transaction: transaction,
+ feed: mockFeed,
+ );
+ });
// When
final snapshot = await fakeFirestore.collection('feeds').get();
@@ -119,6 +132,7 @@ void main() {
final mockFeed = Feed(
id: testId,
imagePath: 'imagePath',
+ thumbnailImagePath: 'thumbnailImagePath',
userEmail: 'test@email.com',
description: 'This is a test feed',
weather: mockWeather,
@@ -147,19 +161,24 @@ void main() {
'weather': {
'code': i, // 날씨 코드
'temperature': 22.0, // 온도
- 'time_temperature': DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
- 'created_at': DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
+ 'time_temperature':
+ DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
+ 'created_at':
+ DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
},
'location': {
'lat': 35.234,
'lng': 131.1,
'city': '서울시 구로구',
- 'created_at': DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
+ 'created_at':
+ DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
},
'created_at': DateTime.parse('2024-05-01 13:27:00'),
'description': 'desc',
'image_path':
'https://health.chosun.com/site/data/img_dir/2024/01/22/2024012201607_0.jpg',
+ 'thumbnail_image_path':
+ 'https://health.chosun.com/site/data/img_dir/2024/01/22/2024012201607_0.jpg',
'season_code': 0,
'user_email': 'hoogom87@gmail.com',
});
@@ -185,25 +204,36 @@ void main() {
'weather': {
'code': 0, // 날씨 코드
'temperature': 22.0, // 온도
- 'time_temperature': DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
- 'created_at': DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
+ 'time_temperature':
+ DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
+ 'created_at':
+ DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
},
'location': {
'lat': 35.234,
'lng': 131.1,
'city': '서울시 구로구',
- 'created_at': DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
+ 'created_at':
+ DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
},
'created_at': DateTime.parse('2024-05-01 13:27:00'),
'description': 'desc',
'image_path':
'https://health.chosun.com/site/data/img_dir/2024/01/22/2024012201607_0.jpg',
+ 'thumbnail_image_path':
+ 'https://health.chosun.com/site/data/img_dir/2024/01/22/2024012201607_0.jpg',
'season_code': 0,
'user_email': 'hoogom87@gmail.com',
});
// When
- final result = await dataSource.deleteFeed(id: testId);
+ final result =
+ await fakeFirestore.runTransaction((transaction) async {
+ return await dataSource.deleteFeed(
+ transaction: transaction,
+ id: testId,
+ );
+ });
final docResult =
await fakeFirestore.collection('feeds').doc(testId).get();
@@ -222,19 +252,24 @@ void main() {
'weather': {
'code': i, // 날씨 코드
'temperature': 22.0, // 온도
- 'time_temperature': DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
- 'created_at': DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
+ 'time_temperature':
+ DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
+ 'created_at':
+ DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
},
'location': {
'lat': 35.234,
'lng': 131.1,
'city': '서울시 구로구',
- 'created_at': DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
+ 'created_at':
+ DateTime.parse('2024-05-01 13:27:00').toIso8601String(),
},
'created_at': DateTime.parse('2024-05-01 13:27:00'),
'description': 'desc',
'image_path':
'https://health.chosun.com/site/data/img_dir/2024/01/22/2024012201607_0.jpg',
+ 'thumbnail_image_path':
+ 'https://health.chosun.com/site/data/img_dir/2024/01/22/2024012201607_0.jpg',
'season_code': 0,
'user_email': 'hoogom87@gmail.com',
});
@@ -287,10 +322,13 @@ void main() {
await fakeFirestore.collection('feeds').add({
'weather': mockWeather.toJson(),
'location': mockLocation.toJson(),
- 'created_at': Timestamp.fromDate(DateTime.parse('2024-05-01 13:27:00')),
+ 'created_at':
+ Timestamp.fromDate(DateTime.parse('2024-05-01 13:27:00')),
'description': 'desc',
'image_path':
'https://health.chosun.com/site/data/img_dir/2024/01/22/2024012201607_0.jpg',
+ 'thumbnail_image_path':
+ 'https://health.chosun.com/site/data/img_dir/2024/01/22/2024012201607_0.jpg',
'season_code': 0,
'user_email': 'hoogom87@gmail.com',
'deleted_at': null,
diff --git a/test/data/feed/repository/feed_repository_impl_test.dart b/test/data/feed/repository/feed_repository_impl_test.dart
index 0af3f0c1..fe165fb1 100644
--- a/test/data/feed/repository/feed_repository_impl_test.dart
+++ b/test/data/feed/repository/feed_repository_impl_test.dart
@@ -16,6 +16,7 @@ void main() {
final mockFeed = Feed(
id: 'id',
imagePath: 'imagePath',
+ thumbnailImagePath: 'thumbnailImagePath',
userEmail: 'userEmail',
description: 'description',
weather: Weather(
@@ -41,52 +42,6 @@ void main() {
tearDown(() => mockFeedDataSource.cleanUpMockData());
- test('saveFeed는', () async {
- // given
- final mockFeed = Feed(
- id: 'id',
- imagePath: 'imagePath',
- userEmail: 'userEmail',
- description: 'description',
- weather: Weather(
- temperature: 1,
- timeTemperature: DateTime.now(),
- code: 1,
- createdAt: DateTime.now(),
- ),
- seasonCode: 1,
- location: Location(
- lat: 1,
- lng: 1,
- city: 'city',
- createdAt: DateTime.now(),
- ),
- createdAt: DateTime.now(),
- deletedAt: null,
- );
-
- mockFeedDataSource.saveFeedReturnValue = true;
- // when
- final actual = await feedRepository.saveFeed(editedFeed: mockFeed);
-
- // then
- expect(actual, true);
- expect(mockFeedDataSource.feedList.last, mockFeed);
- });
- test('deleteFeed는', () async {
- // Given
- const expectedId = 'id';
- const expectedBool = true;
-
- mockFeedDataSource.deleteFeedReturnValue = expectedBool;
-
- // When
- final actual = await feedRepository.deleteFeed(id: expectedId);
-
- // Then
- expect(mockFeedDataSource.deleteFeedParamId, expectedId);
- expect(actual, expectedBool);
- });
test('getUserFeedList는', () async {
// Given
final expectedList = mockFeedDataSource.feedList;
@@ -188,6 +143,7 @@ void main() {
final mockFeed = Feed(
id: 'id',
imagePath: 'imagePath',
+ thumbnailImagePath: 'thumbnailImagePath',
userEmail: 'userEmail',
description: 'description',
weather: Weather(
diff --git a/test/data/feed/repository/ootd_feed_repository_impl_test.dart b/test/data/feed/repository/ootd_feed_repository_impl_test.dart
index d0d7bd6e..518a063d 100644
--- a/test/data/feed/repository/ootd_feed_repository_impl_test.dart
+++ b/test/data/feed/repository/ootd_feed_repository_impl_test.dart
@@ -5,19 +5,23 @@ import 'package:weaco/domain/location/model/location.dart';
import 'package:weaco/domain/user/model/user_profile.dart';
import 'package:weaco/domain/weather/model/weather.dart';
-import '../../../mock/data/feed/repository/mock_feed_repository_impl.dart';
+import '../../../mock/core/firebase/mock_firestore_service_impl.dart';
+import '../../../mock/data/feed/data_source/mock_remote_feed_data_source.dart';
import '../../../mock/data/file/repository/mock_file_repository_impl.dart';
-import '../../../mock/data/user/repository/mock_user_profile_repository_impl.dart';
+import '../../../mock/data/user/data_source/mock_remote_user_profile_data_source.dart';
void main() {
group('OotdFeedRepositoryImpl 클래스', () {
final fileRepository = MockFileRepositoryImpl();
- final feedRepository = MockFeedRepositoryImpl();
- final userProfileRepository = MockUserProfileRepositoryImpl();
+ final remoteFeedDataSource = MockRemoteFeedDataSource();
+ final remoteUserProfileDatSource = MockRemoteUserProfileDataSourceImpl();
+ final mockTransactionService = MockFirestoreServiceImpl();
+
final ootdFeedRepository = OotdFeedRepositoryImpl(
fileRepository: fileRepository,
- feedRepository: feedRepository,
- userProfileRepository: userProfileRepository,
+ remoteFeedDataSource: remoteFeedDataSource,
+ remoteUserProfileDataSource: remoteUserProfileDatSource,
+ firestoreService: mockTransactionService,
);
const String feedId = '1';
@@ -35,7 +39,8 @@ void main() {
);
final mockFeed = Feed(
id: null,
- imagePath: 'imagePath',
+ imagePath: '',
+ thumbnailImagePath: '',
userEmail: 'test@email.com',
description: 'This is a test feed',
weather: mockWeather,
@@ -55,10 +60,10 @@ void main() {
setUp(() {
fileRepository.initMockData();
- feedRepository.initMockData();
- userProfileRepository.resetCallCount();
- userProfileRepository.resetProfile();
- userProfileRepository.addMyProfile(profile: mockUserProfile);
+ remoteFeedDataSource.cleanUpMockData();
+ remoteUserProfileDatSource.initMockData();
+
+ remoteUserProfileDatSource.getUserProfileResult = mockUserProfile;
});
group('saveOotdFeed 메서드는', () {
@@ -73,7 +78,8 @@ void main() {
expect(fileRepository.saveOotdImageCallCount, expectedCallCount);
});
- test('피드를 저장하기 위해 FeedRepository.saveFeed()를 한 번 호출한다.', () async {
+ test('피드를 저장하기 위해 RemoteFeedDataSourceImpl.saveFeed()를 한 번 호출한다.',
+ () async {
// Given
const expectedCallCount = 1;
@@ -81,15 +87,15 @@ void main() {
await ootdFeedRepository.saveOotdFeed(feed: mockFeed);
// Then
- expect(feedRepository.saveFeedCallCount, expectedCallCount);
+ expect(remoteFeedDataSource.saveFeedMethodCallCount, expectedCallCount);
});
test(
'파라미터로 받은 feed 에 FileRepository.saveOotdImage()로 받은 path 를 '
- '추가한 새로운 피드를 FeedRepository.saveFeed()에 전달한다.', () async {
+ '추가한 새로운 피드를 RemoteFeedDataSourceImpl.saveFeed()에 전달한다.', () async {
// Given
- const String path = 'abc.jpg';
- fileRepository.saveOotdImageResult = path;
+ const String path = '';
+ fileRepository.saveOotdImageResult = [path, path];
// 예상 값
final expectedFeed = mockFeed.copyWith(imagePath: path);
@@ -98,11 +104,11 @@ void main() {
await ootdFeedRepository.saveOotdFeed(feed: mockFeed);
// Then
- expect(feedRepository.feed, expectedFeed);
+ expect(remoteFeedDataSource.paramMap['saveFeedParam'], expectedFeed);
});
test(
- '유저 피드 카운트를 업데이트하기 위해 UserProfileRepository.getMyProfile()를 한 번 호출한다.',
+ '유저 피드 카운트를 업데이트하기 위해 RemoteUserProfileDataSourceImpl.getUserProfile()를 한 번 호출한다.',
() async {
// Given
const expectedCallCount = 1;
@@ -111,11 +117,12 @@ void main() {
await ootdFeedRepository.saveOotdFeed(feed: mockFeed);
// Then
- expect(userProfileRepository.getMyProfileCallCount, expectedCallCount);
+ expect(remoteUserProfileDatSource.getUserProfileMethodCallCount,
+ expectedCallCount);
});
test(
- '유저 피드 카운트를 업데이트하기 위해 UserProfileRepository.updateUserProfile()를 한 번 호출한다.',
+ '유저 피드 카운트를 업데이트하기 위해 RemoteUserProfileDataSourceImpl.updateUserProfile()를 한 번 호출한다.',
() async {
// Given
const expectedCallCount = 1;
@@ -124,13 +131,14 @@ void main() {
await ootdFeedRepository.saveOotdFeed(feed: mockFeed);
// Then
- expect(userProfileRepository.updateUserProfileCallCount,
+ expect(remoteUserProfileDatSource.updateUserProfileMethodCallCount,
expectedCallCount);
});
test(
'가져온 유저 피드 카운트에 1을 더한 userProfile 을 '
- 'UserProfileRepository.updateUserProfile()에 전달한다.', () async {
+ 'RemoteUserProfileDataSourceImpl.updateUserProfile()에 전달한다.',
+ () async {
// Given
final expectedUseProfile =
mockUserProfile.copyWith(feedCount: mockUserProfile.feedCount + 1);
@@ -139,13 +147,12 @@ void main() {
await ootdFeedRepository.saveOotdFeed(feed: mockFeed);
// Then
- expect(userProfileRepository.methodParameterMap['updateUserProfile'],
+ expect(remoteUserProfileDatSource.methodUserProfileParameter,
expectedUseProfile);
});
test(
- ''
- 'FeedRepository.saveFeed, UserProfileRepository.updateMyFeedCount()를 '
+ 'RemoteFeedDataSourceImpl.saveFeed, RemoteUserProfileDataSourceImpl.updateUserProfile()를 '
'정상적으로 호출한 뒤, true 를 반환한다.', () async {
// Given
const expected = true;
@@ -157,7 +164,9 @@ void main() {
expect(actual, expected);
});
- test('파라미터로 전달받은 Feed의 id값이 있으면 수정하는 Feed로써 FeedRepository.saveFeed를 한번 호출한다.', () async {
+ test(
+ '파라미터로 전달받은 Feed의 id값이 있으면 수정하는 Feed로써'
+ 'RemoteFeedDataSourceImpl.saveFeed()를 한번 호출한다.', () async {
// Given
const int expected = 1;
@@ -165,10 +174,12 @@ void main() {
await ootdFeedRepository.saveOotdFeed(feed: mockFeed.copyWith(id: '1'));
// Then
- expect(feedRepository.saveFeedCallCount, expected);
+ expect(remoteFeedDataSource.saveFeedMethodCallCount, expected);
});
- test('파라미터로 전달받은 Feed의 id값이 있으면 수정하는 Feed로써 FileRepository.saveOotdImage()를 호출하지 않는다.', () async {
+ test(
+ '파라미터로 전달받은 Feed의 id값이 있으면 수정하는 Feed로써 FileRepository.saveOotdImage()를 호출하지 않는다.',
+ () async {
// Given
const int expected = 0;
@@ -179,7 +190,9 @@ void main() {
expect(fileRepository.saveImageCallCount, expected);
});
- test('파라미터로 전달받은 Feed의 id값이 있으면 수정하는 Feed로써 UserProfileRepository.getMyProfile()를 호출하지 않는다.', () async {
+ test(
+ '파라미터로 전달받은 Feed의 id값이 있으면 수정하는 Feed로써 RemoteUserProfileDataSourceImpl.getUserProfile()를 호출하지 않는다.',
+ () async {
// Given
const int expected = 0;
@@ -187,12 +200,14 @@ void main() {
await ootdFeedRepository.saveOotdFeed(feed: mockFeed.copyWith(id: '1'));
// Then
- expect(userProfileRepository.getMyProfileCallCount, expected);
+ expect(
+ remoteUserProfileDatSource.getUserProfileMethodCallCount, expected);
});
});
group('removeOotdFeed 메서드는', () {
- test('피드를 삭제하기 위해 FeedRepository.deleteFeed()를 한 번 호출한다.', () async {
+ test('피드를 삭제하기 위해 RemoteFeedDataSourceImpl.deleteFeed()를 한 번 호출한다.',
+ () async {
// Given
const expectedCallCount = 1;
@@ -200,11 +215,12 @@ void main() {
await ootdFeedRepository.removeOotdFeed(id: feedId);
// Then
- expect(feedRepository.getDeleteFeedCallCount, expectedCallCount);
+ expect(
+ remoteFeedDataSource.deleteFeedMethodCallCount, expectedCallCount);
});
test(
- '유저 피드 카운트를 업데이트하기 위해 UserProfileRepository.getMyProfile()를 한 번 호출한다.',
+ '유저 피드 카운트를 업데이트하기 위해 RemoteUserProfileDataSourceImpl.getUserProfile()를 한 번 호출한다.',
() async {
// Given
const expectedCallCount = 1;
@@ -213,11 +229,12 @@ void main() {
await ootdFeedRepository.removeOotdFeed(id: feedId);
// Then
- expect(userProfileRepository.getMyProfileCallCount, expectedCallCount);
+ expect(remoteUserProfileDatSource.getUserProfileMethodCallCount,
+ expectedCallCount);
});
test(
- '유저 피드 카운트를 업데이트하기 위해 UserProfileRepository.updateUserProfile()를 한 번 호출한다.',
+ '유저 피드 카운트를 업데이트하기 위해 RemoteUserProfileDataSourceImpl.updateUserProfile()를 한 번 호출한다.',
() async {
// Given
const expectedCallCount = 1;
@@ -226,12 +243,12 @@ void main() {
await ootdFeedRepository.removeOotdFeed(id: feedId);
// Then
- expect(userProfileRepository.updateUserProfileCallCount,
+ expect(remoteUserProfileDatSource.updateUserProfileMethodCallCount,
expectedCallCount);
});
test(
- '가져온 유저 피드 카운트에 1을 뺀 값을 UserProfileRepository.updateUserProfile()에 전달한다.',
+ '가져온 유저 피드 카운트에 1을 뺀 값을 RemoteUserProfileDataSourceImpl.updateUserProfile()에 전달한다.',
() async {
// Given
final expectedUserProfile =
@@ -241,12 +258,12 @@ void main() {
await ootdFeedRepository.removeOotdFeed(id: feedId);
// Then
- expect(userProfileRepository.methodParameterMap['updateUserProfile'],
+ expect(remoteUserProfileDatSource.methodUserProfileParameter,
expectedUserProfile);
});
test(
- 'FeedRepository.deleteFeed(), OotdFeedRepositoryImpl.updateMyFeedCount()를 '
+ 'RemoteFeedDataSourceImpl.deleteFeed(), OotdFeedRepositoryImpl.updateMyFeedCount()를 '
'정상적으로 호출한 뒤, true 를 반환한다.', () async {
// Given
const expected = true;
diff --git a/test/data/file/data_source/local_file_data_source_test.dart b/test/data/file/data_source/local_file_data_source_test.dart
index 4d81197a..779a5472 100644
--- a/test/data/file/data_source/local_file_data_source_test.dart
+++ b/test/data/file/data_source/local_file_data_source_test.dart
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
+import 'package:weaco/core/enum/image_type.dart';
import 'package:weaco/core/path_provider/path_provider_service.dart';
import 'package:weaco/data/file/data_source/local/local_file_data_source.dart';
import 'package:weaco/data/file/data_source/local/local_file_data_source_impl.dart';
@@ -24,28 +25,28 @@ void main() {
}
});
- test('isOrigin 인자가 true일 경우, origin.png를 반환한다.', () async {
+ test('imageType 인자가 ImageType.origin일 경우, origin.png를 반환한다.', () async {
// Given
- const isOrigin = true;
+ const imageType = ImageType.origin;
File('test/mock/assets/origin.png').writeAsBytesSync(
File('test/mock/assets/test_image.png').readAsBytesSync());
// When
- File? file = await dataSource.getImage(isOrigin: isOrigin);
+ File? file = await dataSource.getImage(imageType: imageType);
// Then
expect(file?.readAsBytesSync(),
File('test/mock/assets/origin.png').readAsBytesSync());
});
- test('isOrigin 인자가 false일 경우, cropped.png를 반환한다.', () async {
+ test('imageType 인자가 ImageType.cropped일 경우, cropped.png를 반환한다.', () async {
// Given
- const isOrigin = false;
+ const imageType = ImageType.cropped;
File('test/mock/assets/cropped.png').writeAsBytesSync(
File('test/mock/assets/test_image.png').readAsBytesSync());
// When
- File? file = await dataSource.getImage(isOrigin: isOrigin);
+ File? file = await dataSource.getImage(imageType: imageType);
// Then
expect(file?.readAsBytesSync(),
@@ -54,10 +55,10 @@ void main() {
test('찾으려는 파일이 없는 경우, null을 반환한다.', () async {
// Given
- const isOrigin = false;
+ const imageType = ImageType.cropped;
// When
- File? file = await dataSource.getImage(isOrigin: isOrigin);
+ File? file = await dataSource.getImage(imageType: imageType);
// Then
expect(file, null);
diff --git a/test/data/file/data_source/remote_file_data_source_test.dart b/test/data/file/data_source/remote_file_data_source_test.dart
index 558a674d..56c3b561 100644
--- a/test/data/file/data_source/remote_file_data_source_test.dart
+++ b/test/data/file/data_source/remote_file_data_source_test.dart
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:firebase_storage_mocks/firebase_storage_mocks.dart';
import 'package:flutter_test/flutter_test.dart';
+import 'package:weaco/core/enum/image_type.dart';
import 'package:weaco/core/path_provider/path_provider_service.dart';
import 'package:weaco/data/file/data_source/local/local_file_data_source.dart';
import 'package:weaco/data/file/data_source/local/local_file_data_source_impl.dart';
@@ -32,21 +33,24 @@ void main() {
group('saveImage() 메소드는', () {
test('File 객체를 인자로 받아 업로드하고 다운로드 경로를 반환한다.', () async {
// Given
- const isOrigin = false;
const email = 'bob@somedomain.com';
File('test/mock/assets/cropped.png').writeAsBytesSync(
File('test/mock/assets/test_image.png').readAsBytesSync());
+ File('test/mock/assets/compressed.png').writeAsBytesSync(
+ File('test/mock/assets/test_image.png').readAsBytesSync());
final bucketPath = await storage.ref().child('').getDownloadURL();
// When
- File? file = await localFileDataSource.getImage(isOrigin: isOrigin);
+ File? croppedImage = await localFileDataSource.getImage(imageType: ImageType.cropped);
+ File? compressedImage = await localFileDataSource.getImage(imageType: ImageType.compressed);
- if (file != null) {
+ if (croppedImage != null && compressedImage != null) {
final path =
- await remoteFileDataSource.saveImage(image: file);
+ await remoteFileDataSource.saveImage(croppedImage: croppedImage, compressedImage: compressedImage);
- expect(path.startsWith('${bucketPath}feed_images/$email'), true);
+ expect(path[0].startsWith('${bucketPath}feed_origin_images/$email'), true);
+ expect(path[1].startsWith('${bucketPath}feed_thumbnail_images/$email'), true);
}
});
});
diff --git a/test/data/file/repository/file_repository_impl_test.dart b/test/data/file/repository/file_repository_impl_test.dart
index a3b20763..d5ed4b18 100644
--- a/test/data/file/repository/file_repository_impl_test.dart
+++ b/test/data/file/repository/file_repository_impl_test.dart
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
+import 'package:weaco/core/enum/image_type.dart';
import 'package:weaco/data/file/repository/file_repository_impl.dart';
import 'package:weaco/domain/file/repository/file_repository.dart';
@@ -22,21 +23,21 @@ void main() {
group('getImage 메서드는', () {
test('인자를 LocalFileDataSource.getImage()에 파라미터로 그대로 전달한다.', () async {
// Given
- const bool isOrigin = true;
+ const ImageType imageType = ImageType.origin;
// When
- await fileRepository.getImage(isOrigin: isOrigin);
+ await fileRepository.getImage(imageType: imageType);
// Then
- expect(mockLocalFileDataSource.methodParameter['isOrigin'], isOrigin);
+ expect(mockLocalFileDataSource.methodParameter['isOrigin'], imageType);
});
test('LocalFileDataSource.getImage()을 한번 호출한다.', () async {
// Given
- const bool isOrigin = true;
+ const ImageType imageType = ImageType.origin;
// When
- await fileRepository.getImage(isOrigin: isOrigin);
+ await fileRepository.getImage(imageType: imageType);
// Then
expect(mockLocalFileDataSource.methodCallCount['getImage'], 1);
@@ -44,12 +45,12 @@ void main() {
test('LocalFileDataSource.getImage()의 반환 값을 그대로 반환한다.', () async {
// Given
- const bool isOrigin = true;
+ const ImageType imageType = ImageType.origin;
final File expectResult = File('test/mock/assets/test_image.png');
mockLocalFileDataSource.methodResult['getImage'] = expectResult;
// When
- await fileRepository.getImage(isOrigin: isOrigin);
+ await fileRepository.getImage(imageType: imageType);
// Then
expect(mockLocalFileDataSource.methodResult['getImage'], expectResult);
@@ -65,7 +66,7 @@ void main() {
mockLocalFileDataSource.methodResult['saveImage'] = true;
// When
- await fileRepository.saveImage(isOrigin: isOrigin, file: file);
+ await fileRepository.saveImage(isOrigin: isOrigin, file: file, compressedImage: file.readAsBytesSync());
// Then
expect(mockLocalFileDataSource.methodParameter['isOrigin'], isOrigin);
@@ -79,7 +80,7 @@ void main() {
mockLocalFileDataSource.methodResult['saveImage'] = true;
// When
- await fileRepository.saveImage(isOrigin: isOrigin, file: file);
+ await fileRepository.saveImage(isOrigin: isOrigin, file: file, compressedImage: file.readAsBytesSync());
// Then
expect(mockLocalFileDataSource.methodCallCount['saveImage'], 1);
@@ -93,7 +94,7 @@ void main() {
mockLocalFileDataSource.methodResult['saveImage'] = expectResult;
// When
- await fileRepository.saveImage(isOrigin: isOrigin, file: file);
+ await fileRepository.saveImage(isOrigin: isOrigin, file: file, compressedImage: file.readAsBytesSync());
// Then
expect(mockLocalFileDataSource.methodResult['saveImage'], expectResult);
@@ -101,12 +102,12 @@ void main() {
});
group('saveOotdImage 메서드는', () {
- test('LocalFileDataSource.getImage()의 isOrigin 파라미터 값으로 false를 전달한다.',
+ test('LocalFileDataSource.getImage()의 imageType 파라미터 값으로 ImageType.compressed를 전달한다.',
() async {
// Given
- const bool expectedParameter = false;
+ const ImageType expectedParameter = ImageType.compressed;
mockLocalFileDataSource.methodResult['getImage'] = File('test/mock/assets/test_image.png');
- mockRemoteFileDataSource.methodResult['saveImage'] = 'test/mock/assets/test_image.png';
+ mockRemoteFileDataSource.methodResult['saveImage'] = ['test/mock/assets/test_image.png', 'test/mock/assets/test_image.png'];
// When
await fileRepository.saveOotdImage();
@@ -116,16 +117,16 @@ void main() {
expectedParameter);
});
- test('LocalFileDataSource.getImage()을 한번 호출한다.', () async {
+ test('LocalFileDataSource.getImage()을 두번 호출한다.', () async {
// Given
mockLocalFileDataSource.methodResult['getImage'] = File('test/mock/assets/test_image.png');
- mockRemoteFileDataSource.methodResult['saveImage'] = 'test/mock/assets/test_image.png';
+ mockRemoteFileDataSource.methodResult['saveImage'] = ['test/mock/assets/test_image.png', 'test/mock/assets/test_image.png'];
// When
await fileRepository.saveOotdImage();
// Then
- expect(mockLocalFileDataSource.methodCallCount['getImage'], 1);
+ expect(mockLocalFileDataSource.methodCallCount['getImage'], 2);
});
test('LocalFileDataSource.getImage()의 반환 값이 null이라면 Exception을 발생시킨다.', () async {
@@ -138,7 +139,7 @@ void main() {
test('RemoteFileDataSource.saveImage()의 반환 값을 그대로 반환한다.', () async {
// Given
- const String expectResult = 'test/mock/assets/test_image.png';
+ const List expectResult = ['test/mock/assets/test_image.png', 'test/mock/assets/test_image.png'];
mockLocalFileDataSource.methodResult['getImage'] = File('test/mock/assets/test_image.png');
mockRemoteFileDataSource.methodResult['saveImage'] = expectResult;
diff --git a/test/data/user/repository/user_auth_repository_impl_test.dart b/test/data/user/repository/user_auth_repository_impl_test.dart
index fa7ab7c3..a42b15ac 100644
--- a/test/data/user/repository/user_auth_repository_impl_test.dart
+++ b/test/data/user/repository/user_auth_repository_impl_test.dart
@@ -68,7 +68,8 @@ void main() {
);
// then
- expect(userProfileDataSource.methodCallCount, expectCallCount);
+ expect(userProfileDataSource.saveUserProfileMethodCallCount,
+ expectCallCount);
});
test(
@@ -87,7 +88,8 @@ void main() {
);
// then
- expect(userProfileDataSource.methodCallCount, expectCallCount);
+ expect(userProfileDataSource.saveUserProfileMethodCallCount,
+ expectCallCount);
});
test(
diff --git a/test/domain/feed/use_case/get_detail_feed_detail_use_case_test.dart b/test/domain/feed/use_case/get_detail_feed_detail_use_case_test.dart
index f03cd878..2795b48d 100644
--- a/test/domain/feed/use_case/get_detail_feed_detail_use_case_test.dart
+++ b/test/domain/feed/use_case/get_detail_feed_detail_use_case_test.dart
@@ -34,6 +34,7 @@ void main() {
final expectedFeed = Feed(
id: 'id',
imagePath: 'imagePath',
+ thumbnailImagePath: 'thumbnailImagePath',
userEmail: 'userEmail',
description: 'description',
weather: Weather(
diff --git a/test/domain/feed/use_case/get_my_page_feeds_use_case_test.dart b/test/domain/feed/use_case/get_my_page_feeds_use_case_test.dart
index 753e003f..20e377c2 100644
--- a/test/domain/feed/use_case/get_my_page_feeds_use_case_test.dart
+++ b/test/domain/feed/use_case/get_my_page_feeds_use_case_test.dart
@@ -39,6 +39,7 @@ void main() {
id: '1',
imagePath:
'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
+ thumbnailImagePath: 'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
userEmail: 'yoonji5809@gmail.com',
description: 'OOTD 설명',
weather: Weather(
@@ -60,6 +61,7 @@ void main() {
id: '2',
imagePath:
'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
+ thumbnailImagePath: 'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
userEmail: 'yoonji5809@gmail.com',
description: 'OOTD 설명',
weather: Weather(
@@ -80,6 +82,7 @@ void main() {
id: '3',
imagePath:
'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
+ thumbnailImagePath: 'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
userEmail: 'yoonji5809@gmail.com',
description: 'OOTD 설명',
weather: Weather(
diff --git a/test/domain/feed/use_case/get_recommended_feeds_use_case_test.dart b/test/domain/feed/use_case/get_recommended_feeds_use_case_test.dart
index 4ff3a6d9..79b92d94 100644
--- a/test/domain/feed/use_case/get_recommended_feeds_use_case_test.dart
+++ b/test/domain/feed/use_case/get_recommended_feeds_use_case_test.dart
@@ -93,6 +93,7 @@ void main() {
final feed1 = Feed(
id: '0',
imagePath: '',
+ thumbnailImagePath: '',
userEmail: 'hoogom87@gmail.com',
description: '오늘의 OOTD',
weather: Weather(
diff --git a/test/domain/feed/use_case/get_search_feeds_use_case_test.dart b/test/domain/feed/use_case/get_search_feeds_use_case_test.dart
index 5dde140e..faf54f34 100644
--- a/test/domain/feed/use_case/get_search_feeds_use_case_test.dart
+++ b/test/domain/feed/use_case/get_search_feeds_use_case_test.dart
@@ -58,6 +58,7 @@ void main() {
final feed1 = Feed(
id: '0',
imagePath: '',
+ thumbnailImagePath: '',
userEmail: 'qoophon@gmail.com',
description: '오늘의 OOTD',
weather: Weather(
diff --git a/test/domain/feed/use_case/get_user_page_feeds_use_case_test.dart b/test/domain/feed/use_case/get_user_page_feeds_use_case_test.dart
index dff0ee70..a34d194a 100644
--- a/test/domain/feed/use_case/get_user_page_feeds_use_case_test.dart
+++ b/test/domain/feed/use_case/get_user_page_feeds_use_case_test.dart
@@ -39,6 +39,8 @@ void main() {
id: '1',
imagePath:
'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
+ thumbnailImagePath:
+ 'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
userEmail: 'yoonji5809@gmail.com',
description: 'OOTD 설명',
weather: Weather(
@@ -60,6 +62,8 @@ void main() {
id: '2',
imagePath:
'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
+ thumbnailImagePath:
+ 'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
userEmail: 'yoonji5809@gmail.com',
description: 'OOTD 설명',
weather: Weather(
@@ -80,6 +84,8 @@ void main() {
id: '3',
imagePath:
'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
+ thumbnailImagePath:
+ 'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
userEmail: 'yoonji5809@gmail.com',
description: 'OOTD 설명',
weather: Weather(
diff --git a/test/domain/feed/use_case/save_edit_feed_use_case_test.dart b/test/domain/feed/use_case/save_edit_feed_use_case_test.dart
index 0a923856..bfdcea11 100644
--- a/test/domain/feed/use_case/save_edit_feed_use_case_test.dart
+++ b/test/domain/feed/use_case/save_edit_feed_use_case_test.dart
@@ -21,6 +21,7 @@ void main() {
final mockFeedNew = Feed(
id: '',
imagePath: 'imagePath',
+ thumbnailImagePath: 'thumbnailImagePath',
userEmail: 'userEmail',
description: 'description',
weather: Weather(
@@ -53,6 +54,7 @@ void main() {
final editedFeed = Feed(
id: 'id',
imagePath: 'imagePath',
+ thumbnailImagePath: 'thumbnailImagePath',
userEmail: 'userEmail',
description: 'description',
weather: Weather(
diff --git a/test/domain/file/use_case/get_image_use_case_test.dart b/test/domain/file/use_case/get_image_use_case_test.dart
index 41b99ec8..6401a1b3 100644
--- a/test/domain/file/use_case/get_image_use_case_test.dart
+++ b/test/domain/file/use_case/get_image_use_case_test.dart
@@ -1,4 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
+import 'package:weaco/core/enum/image_type.dart';
import 'package:weaco/domain/file/use_case/get_image_use_case.dart';
import '../../../mock/data/file/repository/mock_file_repository_impl.dart';
@@ -17,7 +18,7 @@ void main() {
const expectCount = 1;
// When
- await useCase.execute(isOrigin: true);
+ await useCase.execute(imageType: ImageType.origin);
// Then
expect(mockFileRepository.getImageCallCount, expectCount);
diff --git a/test/domain/file/use_case/refresh_user_page_use_case_test.dart b/test/domain/file/use_case/refresh_user_page_use_case_test.dart
index 9a08926e..ef0daa97 100644
--- a/test/domain/file/use_case/refresh_user_page_use_case_test.dart
+++ b/test/domain/file/use_case/refresh_user_page_use_case_test.dart
@@ -41,6 +41,8 @@ void main() {
id: '1',
imagePath:
'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
+ thumbnailImagePath:
+ 'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
userEmail: 'qoophon@gmail.com',
description: 'OOTD 설명',
weather: Weather(
@@ -62,6 +64,8 @@ void main() {
id: '2',
imagePath:
'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
+ thumbnailImagePath:
+ 'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
userEmail: 'qoophon@gmail.com',
description: 'OOTD 설명',
weather: Weather(
@@ -82,6 +86,8 @@ void main() {
id: '3',
imagePath:
'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
+ thumbnailImagePath:
+ 'https://fastly.picsum.photos/id/813/200/300.jpg?hmac=D5xik3d3YUFq2gWtCzrQZs6zuAcmSvgqdZb063ezs4U',
userEmail: 'qoophon@gmail.com',
description: 'OOTD 설명',
weather: Weather(
diff --git a/test/domain/file/use_case/save_image_use_case_test.dart b/test/domain/file/use_case/save_image_use_case_test.dart
index bb4848d4..29dd0c19 100644
--- a/test/domain/file/use_case/save_image_use_case_test.dart
+++ b/test/domain/file/use_case/save_image_use_case_test.dart
@@ -2,13 +2,15 @@ import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:weaco/domain/file/use_case/save_image_use_case.dart';
+import '../../../mock/core/util/mock_image_compressor_impl.dart';
import '../../../mock/data/file/repository/mock_file_repository_impl.dart';
void main() {
group('SaveImageUseCase 클래스', () {
final mockFileRepository = MockFileRepositoryImpl();
- final SaveImageUseCase useCase =
- SaveImageUseCase(fileRepository: mockFileRepository);
+ final SaveImageUseCase useCase = SaveImageUseCase(
+ fileRepository: mockFileRepository,
+ imageCompressor: MockImageCompressorImpl());
setUp(() => mockFileRepository.initMockData());
@@ -43,7 +45,8 @@ void main() {
mockFileRepository.saveImageResult = expectResult;
// When
- final result = await mockFileRepository.saveImage(isOrigin: true, file: data);
+ final result =
+ await mockFileRepository.saveImage(isOrigin: true, file: data);
// Then
expect(result, expectResult);
diff --git a/test/domain/user/data_source/remote_user_profile_data_source_test.dart b/test/domain/user/data_source/remote_user_profile_data_source_test.dart
index ed62ad44..76ca1c37 100644
--- a/test/domain/user/data_source/remote_user_profile_data_source_test.dart
+++ b/test/domain/user/data_source/remote_user_profile_data_source_test.dart
@@ -133,8 +133,12 @@ void main() async {
);
// When
- final bool res = await dataSource.updateUserProfile(
- userProfile: editedUserProfile);
+ final res = await instance.runTransaction((transaction) async {
+ return await dataSource.updateUserProfile(
+ transaction: transaction,
+ userProfile: editedUserProfile,
+ );
+ });
// Then
expect(res, true);
diff --git a/test/domain/user/repository/user_profile_repository_impl_test.dart b/test/domain/user/repository/user_profile_repository_impl_test.dart
index 92f3ceea..cdc0a56d 100644
--- a/test/domain/user/repository/user_profile_repository_impl_test.dart
+++ b/test/domain/user/repository/user_profile_repository_impl_test.dart
@@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
+import 'package:weaco/data/user/repository/user_profile_repository_impl.dart';
import 'package:weaco/domain/user/model/user_profile.dart';
import 'package:weaco/domain/user/repository/user_profile_repository.dart';
-import 'package:weaco/data/user/repository/user_profile_repository_impl.dart';
import '../../../mock/data/user/data_source/mock_remote_user_profile_data_source.dart';
@@ -37,7 +37,8 @@ void main() {
await userProfileRepository.getMyProfile();
// Then
- expect(remoteUserProfileDataSource.methodCallCount, expectedCount);
+ expect(remoteUserProfileDataSource.getUserProfileMethodCallCount,
+ expectedCount);
});
test(
@@ -61,7 +62,8 @@ void main() {
final actualUserProfile = await userProfileRepository.getMyProfile();
// Then
- expect(remoteUserProfileDataSource.methodCallCount, expectedCount);
+ expect(remoteUserProfileDataSource.getUserProfileMethodCallCount,
+ expectedCount);
expect(actualUserProfile, expectedUserProfile);
});
});
@@ -91,7 +93,8 @@ void main() {
await userProfileRepository.getUserProfile(email: userProfile.email);
// Then
- expect(remoteUserProfileDataSource.methodCallCount, expectedCount);
+ expect(remoteUserProfileDataSource.getUserProfileMethodCallCount,
+ expectedCount);
});
test(
@@ -116,7 +119,8 @@ void main() {
email: expectedUserProfile.email);
// Then
- expect(remoteUserProfileDataSource.methodCallCount, expectedCount);
+ expect(remoteUserProfileDataSource.getUserProfileMethodCallCount,
+ expectedCount);
expect(actualUserProfile, expectedUserProfile);
});
});
diff --git a/test/mock/assets/compressed.png b/test/mock/assets/compressed.png
new file mode 100644
index 00000000..50c2e2eb
Binary files /dev/null and b/test/mock/assets/compressed.png differ
diff --git a/test/mock/core/firebase/mock_firestore_service_impl.dart b/test/mock/core/firebase/mock_firestore_service_impl.dart
new file mode 100644
index 00000000..4c06ace9
--- /dev/null
+++ b/test/mock/core/firebase/mock_firestore_service_impl.dart
@@ -0,0 +1,19 @@
+import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
+import 'package:weaco/core/db/transaction_service.dart';
+
+class MockFirestoreServiceImpl implements TransactionService {
+ final _fakeFirestore = FakeFirebaseFirestore();
+
+ @override
+ Future run(Function callBack) async {
+ return await _fakeFirestore.runTransaction((transaction) async {
+ return await callBack(transaction);
+ }).then(
+ (value) => true,
+ onError: (e) {
+ // throw Exception('피드 업로드에 실패 하였습니다.');
+ throw Exception(e);
+ },
+ );
+ }
+}
diff --git a/test/mock/core/util/mock_image_compressor_impl.dart b/test/mock/core/util/mock_image_compressor_impl.dart
new file mode 100644
index 00000000..1d27d758
--- /dev/null
+++ b/test/mock/core/util/mock_image_compressor_impl.dart
@@ -0,0 +1,11 @@
+import 'dart:io';
+
+import 'package:weaco/domain/common/util/image_compressor.dart';
+
+class MockImageCompressorImpl implements ImageCompressor {
+ @override
+ Future> compressImage({required File file}) async {
+ return [];
+ }
+
+}
\ No newline at end of file
diff --git a/test/mock/data/feed/data_source/mock_remote_feed_data_source.dart b/test/mock/data/feed/data_source/mock_remote_feed_data_source.dart
index 4fd68541..766dce0f 100644
--- a/test/mock/data/feed/data_source/mock_remote_feed_data_source.dart
+++ b/test/mock/data/feed/data_source/mock_remote_feed_data_source.dart
@@ -1,4 +1,5 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
+import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:weaco/data/feed/data_source/remote_feed_data_source.dart';
import 'package:weaco/domain/feed/model/feed.dart';
import 'package:weaco/domain/weather/model/daily_location_weather.dart';
@@ -10,6 +11,8 @@ class MockRemoteFeedDataSource implements RemoteFeedDataSource {
String deleteFeedParamId = '';
Map paramMap = {};
String getFeedId = '';
+ int saveFeedMethodCallCount = 0;
+ int deleteFeedMethodCallCount = 0;
void cleanUpMockData() {
feedList = [];
@@ -18,11 +21,17 @@ class MockRemoteFeedDataSource implements RemoteFeedDataSource {
deleteFeedReturnValue = false;
getFeedId = '';
paramMap.clear();
+ saveFeedMethodCallCount = 0;
+ deleteFeedMethodCallCount = 0;
}
@override
- Future deleteFeed({required String id}) async {
+ Future deleteFeed({
+ required Transaction transaction,
+ required String id,
+ }) async {
deleteFeedParamId = id;
+ deleteFeedMethodCallCount++;
return deleteFeedReturnValue;
}
@@ -67,7 +76,14 @@ class MockRemoteFeedDataSource implements RemoteFeedDataSource {
}
@override
- Future saveFeed({required Feed feed}) async {
+ Future saveFeed({
+ required Transaction transaction,
+ required Feed feed,
+ }) async {
+ saveFeedMethodCallCount++;
+
+ paramMap['saveFeedParam'] = feed;
+
if (saveFeedReturnValue) {
feedList.add(feed);
}
diff --git a/test/mock/data/feed/repository/mock_feed_repository_impl.dart b/test/mock/data/feed/repository/mock_feed_repository_impl.dart
index 6ce42160..ed39ae1c 100644
--- a/test/mock/data/feed/repository/mock_feed_repository_impl.dart
+++ b/test/mock/data/feed/repository/mock_feed_repository_impl.dart
@@ -153,19 +153,4 @@ class MockFeedRepositoryImpl implements FeedRepository {
getOotdFeedsListCallCount++;
return getOotdFeedsResult == null ? [] : [getOotdFeedsResult!];
}
-
- @override
- Future deleteFeed({required String id}) async {
- getDeleteFeedCallCount++;
- feedMap.remove(id);
- return deleteFeedReturnValue;
- }
-
- @override
- Future saveFeed({required Feed editedFeed}) async {
- saveFeedCallCount++;
- feed = editedFeed;
-
- return true;
- }
}
diff --git a/test/mock/data/file/data_source/local/mock_local_file_data_source_impl.dart b/test/mock/data/file/data_source/local/mock_local_file_data_source_impl.dart
index dfa16967..f883b1c5 100644
--- a/test/mock/data/file/data_source/local/mock_local_file_data_source_impl.dart
+++ b/test/mock/data/file/data_source/local/mock_local_file_data_source_impl.dart
@@ -1,4 +1,5 @@
import 'dart:io';
+import 'package:weaco/core/enum/image_type.dart';
import 'package:weaco/data/file/data_source/local/local_file_data_source.dart';
class MockLocalFileDataSourceImpl implements LocalFileDataSource {
@@ -13,17 +14,28 @@ class MockLocalFileDataSourceImpl implements LocalFileDataSource {
}
@override
- Future getImage({required bool isOrigin}) async {
+ Future getImage({required ImageType imageType}) async {
methodCallCount['getImage'] = (methodCallCount['getImage'] ?? 0) + 1;
- methodParameter['isOrigin'] = isOrigin;
+ methodParameter['isOrigin'] = imageType;
return methodResult['getImage'];
}
@override
- Future saveImage({required bool isOrigin, required File file}) async {
+ Future saveImage({required bool isOrigin, required File file, List? compressedImage}) async {
methodCallCount['saveImage'] = (methodCallCount['saveImage'] ?? 0) + 1;
methodParameter['isOrigin'] = isOrigin;
methodParameter['file'] = file;
return methodResult['saveImage'];
}
+
+ @override
+ Future getCompressedImage() {
+ methodCallCount['getImage'] = (methodCallCount['getImage'] ?? 0) + 1;
+ return methodResult['getImage'];
+ }
+
+ @override
+ Future saveCompressedImage({required List image}) async{
+ return;
+ }
}
diff --git a/test/mock/data/file/data_source/remote/mock_remote_file_data_source_impl.dart b/test/mock/data/file/data_source/remote/mock_remote_file_data_source_impl.dart
index 2e138bb9..f82aeaa2 100644
--- a/test/mock/data/file/data_source/remote/mock_remote_file_data_source_impl.dart
+++ b/test/mock/data/file/data_source/remote/mock_remote_file_data_source_impl.dart
@@ -14,9 +14,9 @@ class MockRemoteFileDataSourceImpl implements RemoteFileDataSource {
}
@override
- Future saveImage({required File image}) async {
+ Future> saveImage({required File croppedImage, required File compressedImage}) async {
methodCallCount['saveImage'] = (methodCallCount['method'] ?? 0) + 1;
- methodParameter['image'] = image;
+ methodParameter['image'] = croppedImage;
return methodResult['saveImage'];
}
}
diff --git a/test/mock/data/file/repository/mock_file_repository_impl.dart b/test/mock/data/file/repository/mock_file_repository_impl.dart
index 1ea7a604..bcfb508a 100644
--- a/test/mock/data/file/repository/mock_file_repository_impl.dart
+++ b/test/mock/data/file/repository/mock_file_repository_impl.dart
@@ -1,4 +1,5 @@
import 'dart:io';
+import 'package:weaco/core/enum/image_type.dart';
import 'package:weaco/domain/file/repository/file_repository.dart';
class MockFileRepositoryImpl implements FileRepository {
@@ -7,7 +8,7 @@ class MockFileRepositoryImpl implements FileRepository {
int saveOotdImageCallCount = 0;
final Map methodParameterMap = {};
File? getImageResult;
- String saveOotdImageResult = '';
+ List saveOotdImageResult = ['', ''];
bool saveImageResult = false;
void initMockData() {
@@ -15,24 +16,24 @@ class MockFileRepositoryImpl implements FileRepository {
saveImageCallCount = 0;
saveOotdImageCallCount = 0;
methodParameterMap.clear();
- saveOotdImageResult = '';
+ saveOotdImageResult = ['', ''];
}
@override
- Future getImage({required bool isOrigin}) async {
+ Future getImage({required ImageType imageType}) async {
getImageCallCount++;
return getImageResult;
}
@override
- Future saveImage({required bool isOrigin, required File file}) async {
+ Future saveImage({required bool isOrigin, required File file, List? compressedImage}) async {
saveImageCallCount++;
methodParameterMap['data'] = file;
return saveImageResult;
}
@override
- Future saveOotdImage() async {
+ Future> saveOotdImage() async {
saveOotdImageCallCount++;
return saveOotdImageResult;
}
diff --git a/test/mock/data/user/data_source/mock_remote_user_profile_data_source.dart b/test/mock/data/user/data_source/mock_remote_user_profile_data_source.dart
index 50ecb6b4..43c9b390 100644
--- a/test/mock/data/user/data_source/mock_remote_user_profile_data_source.dart
+++ b/test/mock/data/user/data_source/mock_remote_user_profile_data_source.dart
@@ -1,9 +1,13 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:weaco/data/user/data_source/remote_user_profile_data_source.dart';
import 'package:weaco/domain/user/model/user_profile.dart';
class MockRemoteUserProfileDataSourceImpl
implements RemoteUserProfileDataSource {
- int methodCallCount = 0;
+ int getUserProfileMethodCallCount = 0;
+ int updateUserProfileMethodCallCount = 0;
+ int saveUserProfileMethodCallCount = 0;
+ int removeUserProfileMethodCallCount = 0;
UserProfile? getUserProfileResult;
bool isRemoved = false;
bool isSaved = false;
@@ -13,7 +17,10 @@ class MockRemoteUserProfileDataSourceImpl
String? methodEmailParameter;
void initMockData() {
- methodCallCount = 0;
+ getUserProfileMethodCallCount = 0;
+ updateUserProfileMethodCallCount = 0;
+ saveUserProfileMethodCallCount = 0;
+ removeUserProfileMethodCallCount = 0;
getUserProfileResult = null;
isRemoved = false;
isSaved = false;
@@ -24,28 +31,31 @@ class MockRemoteUserProfileDataSourceImpl
@override
Future getUserProfile({String? email}) {
- methodCallCount++;
+ getUserProfileMethodCallCount++;
methodEmailParameter = email;
return Future.value(getUserProfileResult);
}
@override
- Future updateUserProfile({UserProfile? userProfile}) async {
- methodCallCount++;
+ Future updateUserProfile({
+ required Transaction transaction,
+ UserProfile? userProfile,
+ }) async {
+ updateUserProfileMethodCallCount++;
methodUserProfileParameter = userProfile;
return isUpdated;
}
@override
Future saveUserProfile({required UserProfile userProfile}) async {
- methodCallCount++;
+ saveUserProfileMethodCallCount++;
methodUserProfileParameter = userProfile;
return isSaved;
}
@override
Future removeUserProfile({String? email}) async {
- methodCallCount++;
+ removeUserProfileMethodCallCount++;
methodEmailParameter = email;
return isRemoved;
}
diff --git a/test/mock/data/user/repository/mock_user_profile_repository_impl.dart b/test/mock/data/user/repository/mock_user_profile_repository_impl.dart
index 0531ce2f..2ccdec3d 100644
--- a/test/mock/data/user/repository/mock_user_profile_repository_impl.dart
+++ b/test/mock/data/user/repository/mock_user_profile_repository_impl.dart
@@ -51,12 +51,4 @@ class MockUserProfileRepositoryImpl implements UserProfileRepository {
getMyProfileCallCount++;
return _fakeUserProfileMap['my'];
}
-
- @override
- Future updateUserProfile({required UserProfile userProfile}) async {
- ++updateUserProfileCallCount;
- methodParameterMap['updateUserProfile'] = userProfile;
- return updateUserProfileResult;
- }
}
-