diff --git a/CHANGELOG.json b/CHANGELOG.json index c5b5590..3bf9f41 100644 --- a/CHANGELOG.json +++ b/CHANGELOG.json @@ -1,11 +1,37 @@ { "metadata": { - "lastUpdated": "2026-02-23T08:10:23Z", - "currentVersion": "1.0.45", + "lastUpdated": "2026-02-23T09:58:25Z", + "currentVersion": "1.0.49", "projectType": "flutter", - "totalReleases": 15 + "totalReleases": 16 }, "releases": [ + { + "version": "1.0.49", + "project_type": "flutter", + "date": "2026-02-23", + "pr_number": 51, + "raw_summary": "## Summary by CodeRabbit\n\n## 릴리스 노트 (v1.0.49)\n\n* **새로운 기능**\n * 저장된 장소를 폴더로 정리하는 기능 추가\n * 장소 평점 및 사진 갤러리 표시\n * 콘텐츠 피드 개선 (최신/내 콘텐츠 탭)\n * 로그인 및 스플래시 화면 애니메이션 적용\n\n* **개선사항**\n * 홈 화면 UI 레이아웃 최적화\n * 장소 정보 표시 개선", + "parsed_changes": { + "새로운_기능": { + "title": "새로운 기능", + "items": [ + "저장된 장소를 폴더로 정리하는 기능 추가", + "장소 평점 및 사진 갤러리 표시", + "콘텐츠 피드 개선 (최신/내 콘텐츠 탭)", + "로그인 및 스플래시 화면 애니메이션 적용" + ] + }, + "개선사항": { + "title": "개선사항", + "items": [ + "홈 화면 UI 레이아웃 최적화", + "장소 정보 표시 개선" + ] + } + }, + "parse_method": "markdown" + }, { "version": "1.0.45", "project_type": "flutter", diff --git a/CHANGELOG.md b/CHANGELOG.md index 052c5d2..be057ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,23 @@ # Changelog -**현재 버전:** 1.0.45 -**마지막 업데이트:** 2026-02-23T08:10:23Z +**현재 버전:** 1.0.49 +**마지막 업데이트:** 2026-02-23T09:58:25Z + +--- + +## [1.0.49] - 2026-02-23 + +**PR:** #51 + +**새로운 기능** +- 저장된 장소를 폴더로 정리하는 기능 추가 +- 장소 평점 및 사진 갤러리 표시 +- 콘텐츠 피드 개선 (최신/내 콘텐츠 탭) +- 로그인 및 스플래시 화면 애니메이션 적용 + +**개선사항** +- 홈 화면 UI 레이아웃 최적화 +- 장소 정보 표시 개선 --- diff --git a/README.md b/README.md index e055983..3534f79 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,6 @@ samples, guidance on mobile development, and a full API reference. --- -## 최신 버전 : v1.0.45 (2026-02-23) +## 최신 버전 : v1.0.46 (2026-02-23) [전체 버전 기록 보기](CHANGELOG.md) diff --git a/assets/mapsy_logo_transparent.png b/assets/mapsy_logo_transparent.png new file mode 100644 index 0000000..34b2cb1 Binary files /dev/null and b/assets/mapsy_logo_transparent.png differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0858cae..f1ced56 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -232,12 +232,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f - device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d - firebase_auth: e7aec07fcada64e296cf237a61df9660e52842c2 - firebase_core: 8d5e24676350f15dd111aa59a88a1ae26605f9ba - firebase_crashlytics: a14ae83fe2d4738b6b5a7bebdf9dad9ccc747e70 - firebase_messaging: 834cfc0887393d3108cdb19da8e57655c54fd0e4 + firebase_auth: e9031a1dbe04a90d98e8d11ff2302352a1c6d9e8 + firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398 + firebase_crashlytics: 28b8f39df8104131376393e6af658b8b77dd120f + firebase_messaging: 343de01a8d3e18b60df0c6d37f7174c44ae38e02 FirebaseAppCheckInterop: ba3dc604a89815379e61ec2365101608d365cf7d FirebaseAuth: 4c289b1a43f5955283244a55cf6bd616de344be5 FirebaseAuthInterop: 95363fe96493cb4f106656666a0768b420cba090 @@ -250,22 +250,22 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: 869ddca16614f979e5c931ece11fbb0b8729ed41 FirebaseSessions: d614ca154c63dbbc6c10d6c38259c2162c4e7c9b Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f - flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - google_sign_in_ios: 7411fab6948df90490dc4620ecbcabdc3ca04017 + flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba - shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 - sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 PODFILE CHECKSUM: 85d318c08613be190fccc1abd43524ac3b83a41b diff --git a/lib/common/constants/api_endpoints.dart b/lib/common/constants/api_endpoints.dart index 8dbed87..7661eac 100644 --- a/lib/common/constants/api_endpoints.dart +++ b/lib/common/constants/api_endpoints.dart @@ -107,4 +107,22 @@ class ApiEndpoints { /// 저장된 장소 (콘텐츠에서) static const String contentSavedPlaces = '/api/content/place/saved'; + + // ============================================ + // Folder API Endpoints + // ============================================ + + /// 폴더 목록 조회 / 폴더 생성 + static const String folders = '/api/folders'; + + /// 폴더 수정 / 삭제 + static String folderDetail(String folderId) => '/api/folders/$folderId'; + + /// 폴더 내 장소 목록 조회 / 장소 추가 + static String folderPlaces(String folderId) => + '/api/folders/$folderId/places'; + + /// 폴더에서 장소 제거 + static String folderPlaceDetail(String folderId, String placeId) => + '/api/folders/$folderId/places/$placeId'; } diff --git a/lib/common/constants/home_colors.dart b/lib/common/constants/home_colors.dart index bea3f68..d47fc5d 100644 --- a/lib/common/constants/home_colors.dart +++ b/lib/common/constants/home_colors.dart @@ -90,4 +90,11 @@ class HomeColors { /// 재시도 버튼 색상 static const Color retryButton = Color(0xFF1A1A1A); + + // ============================================ + // 평점 색상 (Rating Colors) + // ============================================ + + /// 별점 아이콘 색상 + static const Color starRating = Color(0xFFFFC107); } diff --git a/lib/common/models/place_model.dart b/lib/common/models/place_model.dart index 477ca87..77b10c0 100644 --- a/lib/common/models/place_model.dart +++ b/lib/common/models/place_model.dart @@ -3,36 +3,34 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'place_model.freezed.dart'; part 'place_model.g.dart'; -/// 장소 모델 (공통) +/// 장소 모델 (공통) - 백엔드 PlaceDto 매칭 +/// +/// 백엔드에 두 개의 PlaceDto가 존재: +/// - MS-Place/PlaceDto: placeId 필드 사용 (폴더, 저장 장소) → 그대로 역직렬화 +/// - MS-SNS/PlaceDto: id 필드 사용 (콘텐츠 상세) → ContentDetailResponse.fromSnsJson에서 매핑 @freezed class PlaceModel with _$PlaceModel { const factory PlaceModel({ - /// 장소 ID - required int placeId, + /// 장소 ID (UUID) + required String placeId, /// 장소명 - required String placeName, + required String name, /// 주소 String? address, - /// 위도 - double? latitude, + /// 평점 (0.0 ~ 5.0) + double? rating, - /// 경도 - double? longitude, + /// 리뷰 수 + int? userRatingsTotal, - /// 카테고리 - String? category, + /// 사진 URL 배열 (최대 10개) + @Default([]) List photoUrls, - /// 태그 목록 - @Default([]) List tags, - - /// 대표 이미지 URL - String? imageUrl, - - /// 콘텐츠 ID (상위 콘텐츠) - int? contentId, + /// 장소 요약 설명 + String? description, }) = _PlaceModel; factory PlaceModel.fromJson(Map json) => diff --git a/lib/common/models/place_model.freezed.dart b/lib/common/models/place_model.freezed.dart index 92f65e2..8f90245 100644 --- a/lib/common/models/place_model.freezed.dart +++ b/lib/common/models/place_model.freezed.dart @@ -21,32 +21,26 @@ PlaceModel _$PlaceModelFromJson(Map json) { /// @nodoc mixin _$PlaceModel { - /// 장소 ID - int get placeId => throw _privateConstructorUsedError; + /// 장소 ID (UUID) + String get placeId => throw _privateConstructorUsedError; /// 장소명 - String get placeName => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; /// 주소 String? get address => throw _privateConstructorUsedError; - /// 위도 - double? get latitude => throw _privateConstructorUsedError; + /// 평점 (0.0 ~ 5.0) + double? get rating => throw _privateConstructorUsedError; - /// 경도 - double? get longitude => throw _privateConstructorUsedError; + /// 리뷰 수 + int? get userRatingsTotal => throw _privateConstructorUsedError; - /// 카테고리 - String? get category => throw _privateConstructorUsedError; + /// 사진 URL 배열 (최대 10개) + List get photoUrls => throw _privateConstructorUsedError; - /// 태그 목록 - List get tags => throw _privateConstructorUsedError; - - /// 대표 이미지 URL - String? get imageUrl => throw _privateConstructorUsedError; - - /// 콘텐츠 ID (상위 콘텐츠) - int? get contentId => throw _privateConstructorUsedError; + /// 장소 요약 설명 + String? get description => throw _privateConstructorUsedError; /// Serializes this PlaceModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -66,15 +60,13 @@ abstract class $PlaceModelCopyWith<$Res> { ) = _$PlaceModelCopyWithImpl<$Res, PlaceModel>; @useResult $Res call({ - int placeId, - String placeName, + String placeId, + String name, String? address, - double? latitude, - double? longitude, - String? category, - List tags, - String? imageUrl, - int? contentId, + double? rating, + int? userRatingsTotal, + List photoUrls, + String? description, }); } @@ -94,53 +86,43 @@ class _$PlaceModelCopyWithImpl<$Res, $Val extends PlaceModel> @override $Res call({ Object? placeId = null, - Object? placeName = null, + Object? name = null, Object? address = freezed, - Object? latitude = freezed, - Object? longitude = freezed, - Object? category = freezed, - Object? tags = null, - Object? imageUrl = freezed, - Object? contentId = freezed, + Object? rating = freezed, + Object? userRatingsTotal = freezed, + Object? photoUrls = null, + Object? description = freezed, }) { return _then( _value.copyWith( placeId: null == placeId ? _value.placeId : placeId // ignore: cast_nullable_to_non_nullable - as int, - placeName: null == placeName - ? _value.placeName - : placeName // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable as String, address: freezed == address ? _value.address : address // ignore: cast_nullable_to_non_nullable as String?, - latitude: freezed == latitude - ? _value.latitude - : latitude // ignore: cast_nullable_to_non_nullable + rating: freezed == rating + ? _value.rating + : rating // ignore: cast_nullable_to_non_nullable as double?, - longitude: freezed == longitude - ? _value.longitude - : longitude // ignore: cast_nullable_to_non_nullable - as double?, - category: freezed == category - ? _value.category - : category // ignore: cast_nullable_to_non_nullable - as String?, - tags: null == tags - ? _value.tags - : tags // ignore: cast_nullable_to_non_nullable + userRatingsTotal: freezed == userRatingsTotal + ? _value.userRatingsTotal + : userRatingsTotal // ignore: cast_nullable_to_non_nullable + as int?, + photoUrls: null == photoUrls + ? _value.photoUrls + : photoUrls // ignore: cast_nullable_to_non_nullable as List, - imageUrl: freezed == imageUrl - ? _value.imageUrl - : imageUrl // ignore: cast_nullable_to_non_nullable + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable as String?, - contentId: freezed == contentId - ? _value.contentId - : contentId // ignore: cast_nullable_to_non_nullable - as int?, ) as $Val, ); @@ -157,15 +139,13 @@ abstract class _$$PlaceModelImplCopyWith<$Res> @override @useResult $Res call({ - int placeId, - String placeName, + String placeId, + String name, String? address, - double? latitude, - double? longitude, - String? category, - List tags, - String? imageUrl, - int? contentId, + double? rating, + int? userRatingsTotal, + List photoUrls, + String? description, }); } @@ -184,53 +164,43 @@ class __$$PlaceModelImplCopyWithImpl<$Res> @override $Res call({ Object? placeId = null, - Object? placeName = null, + Object? name = null, Object? address = freezed, - Object? latitude = freezed, - Object? longitude = freezed, - Object? category = freezed, - Object? tags = null, - Object? imageUrl = freezed, - Object? contentId = freezed, + Object? rating = freezed, + Object? userRatingsTotal = freezed, + Object? photoUrls = null, + Object? description = freezed, }) { return _then( _$PlaceModelImpl( placeId: null == placeId ? _value.placeId : placeId // ignore: cast_nullable_to_non_nullable - as int, - placeName: null == placeName - ? _value.placeName - : placeName // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable as String, address: freezed == address ? _value.address : address // ignore: cast_nullable_to_non_nullable as String?, - latitude: freezed == latitude - ? _value.latitude - : latitude // ignore: cast_nullable_to_non_nullable + rating: freezed == rating + ? _value.rating + : rating // ignore: cast_nullable_to_non_nullable as double?, - longitude: freezed == longitude - ? _value.longitude - : longitude // ignore: cast_nullable_to_non_nullable - as double?, - category: freezed == category - ? _value.category - : category // ignore: cast_nullable_to_non_nullable - as String?, - tags: null == tags - ? _value._tags - : tags // ignore: cast_nullable_to_non_nullable + userRatingsTotal: freezed == userRatingsTotal + ? _value.userRatingsTotal + : userRatingsTotal // ignore: cast_nullable_to_non_nullable + as int?, + photoUrls: null == photoUrls + ? _value._photoUrls + : photoUrls // ignore: cast_nullable_to_non_nullable as List, - imageUrl: freezed == imageUrl - ? _value.imageUrl - : imageUrl // ignore: cast_nullable_to_non_nullable + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable as String?, - contentId: freezed == contentId - ? _value.contentId - : contentId // ignore: cast_nullable_to_non_nullable - as int?, ), ); } @@ -241,66 +211,56 @@ class __$$PlaceModelImplCopyWithImpl<$Res> class _$PlaceModelImpl implements _PlaceModel { const _$PlaceModelImpl({ required this.placeId, - required this.placeName, + required this.name, this.address, - this.latitude, - this.longitude, - this.category, - final List tags = const [], - this.imageUrl, - this.contentId, - }) : _tags = tags; + this.rating, + this.userRatingsTotal, + final List photoUrls = const [], + this.description, + }) : _photoUrls = photoUrls; factory _$PlaceModelImpl.fromJson(Map json) => _$$PlaceModelImplFromJson(json); - /// 장소 ID + /// 장소 ID (UUID) @override - final int placeId; + final String placeId; /// 장소명 @override - final String placeName; + final String name; /// 주소 @override final String? address; - /// 위도 + /// 평점 (0.0 ~ 5.0) @override - final double? latitude; + final double? rating; - /// 경도 + /// 리뷰 수 @override - final double? longitude; + final int? userRatingsTotal; - /// 카테고리 - @override - final String? category; + /// 사진 URL 배열 (최대 10개) + final List _photoUrls; - /// 태그 목록 - final List _tags; - - /// 태그 목록 + /// 사진 URL 배열 (최대 10개) @override @JsonKey() - List get tags { - if (_tags is EqualUnmodifiableListView) return _tags; + List get photoUrls { + if (_photoUrls is EqualUnmodifiableListView) return _photoUrls; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_tags); + return EqualUnmodifiableListView(_photoUrls); } - /// 대표 이미지 URL - @override - final String? imageUrl; - - /// 콘텐츠 ID (상위 콘텐츠) + /// 장소 요약 설명 @override - final int? contentId; + final String? description; @override String toString() { - return 'PlaceModel(placeId: $placeId, placeName: $placeName, address: $address, latitude: $latitude, longitude: $longitude, category: $category, tags: $tags, imageUrl: $imageUrl, contentId: $contentId)'; + return 'PlaceModel(placeId: $placeId, name: $name, address: $address, rating: $rating, userRatingsTotal: $userRatingsTotal, photoUrls: $photoUrls, description: $description)'; } @override @@ -309,20 +269,17 @@ class _$PlaceModelImpl implements _PlaceModel { (other.runtimeType == runtimeType && other is _$PlaceModelImpl && (identical(other.placeId, placeId) || other.placeId == placeId) && - (identical(other.placeName, placeName) || - other.placeName == placeName) && + (identical(other.name, name) || other.name == name) && (identical(other.address, address) || other.address == address) && - (identical(other.latitude, latitude) || - other.latitude == latitude) && - (identical(other.longitude, longitude) || - other.longitude == longitude) && - (identical(other.category, category) || - other.category == category) && - const DeepCollectionEquality().equals(other._tags, _tags) && - (identical(other.imageUrl, imageUrl) || - other.imageUrl == imageUrl) && - (identical(other.contentId, contentId) || - other.contentId == contentId)); + (identical(other.rating, rating) || other.rating == rating) && + (identical(other.userRatingsTotal, userRatingsTotal) || + other.userRatingsTotal == userRatingsTotal) && + const DeepCollectionEquality().equals( + other._photoUrls, + _photoUrls, + ) && + (identical(other.description, description) || + other.description == description)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -330,14 +287,12 @@ class _$PlaceModelImpl implements _PlaceModel { int get hashCode => Object.hash( runtimeType, placeId, - placeName, + name, address, - latitude, - longitude, - category, - const DeepCollectionEquality().hash(_tags), - imageUrl, - contentId, + rating, + userRatingsTotal, + const DeepCollectionEquality().hash(_photoUrls), + description, ); /// Create a copy of PlaceModel @@ -356,55 +311,45 @@ class _$PlaceModelImpl implements _PlaceModel { abstract class _PlaceModel implements PlaceModel { const factory _PlaceModel({ - required final int placeId, - required final String placeName, + required final String placeId, + required final String name, final String? address, - final double? latitude, - final double? longitude, - final String? category, - final List tags, - final String? imageUrl, - final int? contentId, + final double? rating, + final int? userRatingsTotal, + final List photoUrls, + final String? description, }) = _$PlaceModelImpl; factory _PlaceModel.fromJson(Map json) = _$PlaceModelImpl.fromJson; - /// 장소 ID + /// 장소 ID (UUID) @override - int get placeId; + String get placeId; /// 장소명 @override - String get placeName; + String get name; /// 주소 @override String? get address; - /// 위도 - @override - double? get latitude; - - /// 경도 - @override - double? get longitude; - - /// 카테고리 + /// 평점 (0.0 ~ 5.0) @override - String? get category; + double? get rating; - /// 태그 목록 + /// 리뷰 수 @override - List get tags; + int? get userRatingsTotal; - /// 대표 이미지 URL + /// 사진 URL 배열 (최대 10개) @override - String? get imageUrl; + List get photoUrls; - /// 콘텐츠 ID (상위 콘텐츠) + /// 장소 요약 설명 @override - int? get contentId; + String? get description; /// Create a copy of PlaceModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/common/models/place_model.g.dart b/lib/common/models/place_model.g.dart index bf5ad60..50a721a 100644 --- a/lib/common/models/place_model.g.dart +++ b/lib/common/models/place_model.g.dart @@ -8,28 +8,26 @@ part of 'place_model.dart'; _$PlaceModelImpl _$$PlaceModelImplFromJson(Map json) => _$PlaceModelImpl( - placeId: (json['placeId'] as num).toInt(), - placeName: json['placeName'] as String, + placeId: json['placeId'] as String, + name: json['name'] as String, address: json['address'] as String?, - latitude: (json['latitude'] as num?)?.toDouble(), - longitude: (json['longitude'] as num?)?.toDouble(), - category: json['category'] as String?, - tags: - (json['tags'] as List?)?.map((e) => e as String).toList() ?? + rating: (json['rating'] as num?)?.toDouble(), + userRatingsTotal: (json['userRatingsTotal'] as num?)?.toInt(), + photoUrls: + (json['photoUrls'] as List?) + ?.map((e) => e as String) + .toList() ?? const [], - imageUrl: json['imageUrl'] as String?, - contentId: (json['contentId'] as num?)?.toInt(), + description: json['description'] as String?, ); Map _$$PlaceModelImplToJson(_$PlaceModelImpl instance) => { 'placeId': instance.placeId, - 'placeName': instance.placeName, + 'name': instance.name, 'address': instance.address, - 'latitude': instance.latitude, - 'longitude': instance.longitude, - 'category': instance.category, - 'tags': instance.tags, - 'imageUrl': instance.imageUrl, - 'contentId': instance.contentId, + 'rating': instance.rating, + 'userRatingsTotal': instance.userRatingsTotal, + 'photoUrls': instance.photoUrls, + 'description': instance.description, }; diff --git a/lib/features/ai_extraction/data/ai_extraction_remote_datasource.dart b/lib/features/ai_extraction/data/ai_extraction_remote_datasource.dart index 77fd887..7ef7018 100644 --- a/lib/features/ai_extraction/data/ai_extraction_remote_datasource.dart +++ b/lib/features/ai_extraction/data/ai_extraction_remote_datasource.dart @@ -26,7 +26,7 @@ class AiExtractionRemoteDataSource { /// AI 분석 요청 /// POST /api/content/analyze Future analyze(AnalyzeRequest request) async { - debugPrint('📤 AiExtraction: Analyzing URL: ${request.sourceUrl}'); + debugPrint('📤 AiExtraction: Analyzing URL: ${request.snsUrl}'); final response = await _dio.post( ApiEndpoints.contentAnalyze, @@ -42,27 +42,27 @@ class AiExtractionRemoteDataSource { /// 콘텐츠 상세 조회 (폴링용) /// GET /api/content/{contentId} - Future getContentDetail(int contentId) async { + Future getContentDetail(String contentId) async { debugPrint('📤 AiExtraction: Polling contentId=$contentId'); final response = await _dio.get( - ApiEndpoints.contentDetail(contentId.toString()), + ApiEndpoints.contentDetail(contentId), ); - final result = ContentDetailResponse.fromJson( + final result = ContentDetailResponse.fromSnsJson( response.data as Map, ); - debugPrint('✅ Content status: ${result.status}'); + debugPrint('✅ Content status: ${result.content.status}'); return result; } /// 장소 저장 /// POST /api/place/{placeId}/save - Future savePlace(int placeId) async { + Future savePlace(String placeId) async { debugPrint('📤 AiExtraction: Saving placeId=$placeId'); await _dio.post( - ApiEndpoints.savePlace(placeId.toString()), + ApiEndpoints.savePlace(placeId), ); debugPrint('✅ Place saved: placeId=$placeId'); diff --git a/lib/features/ai_extraction/data/ai_extraction_repository.dart b/lib/features/ai_extraction/data/ai_extraction_repository.dart index 13dd8e1..55ce4b4 100644 --- a/lib/features/ai_extraction/data/ai_extraction_repository.dart +++ b/lib/features/ai_extraction/data/ai_extraction_repository.dart @@ -8,8 +8,8 @@ abstract class AiExtractionRepository { Future analyze(AnalyzeRequest request); /// 콘텐츠 상세 조회 (폴링용) - Future getContentDetail(int contentId); + Future getContentDetail(String contentId); /// 장소 저장 - Future savePlace(int placeId); + Future savePlace(String placeId); } diff --git a/lib/features/ai_extraction/data/ai_extraction_repository_impl.dart b/lib/features/ai_extraction/data/ai_extraction_repository_impl.dart index 0ad9c16..697ad93 100644 --- a/lib/features/ai_extraction/data/ai_extraction_repository_impl.dart +++ b/lib/features/ai_extraction/data/ai_extraction_repository_impl.dart @@ -29,13 +29,13 @@ class AiExtractionRepositoryImpl implements AiExtractionRepository { } @override - Future getContentDetail(int contentId) async { + Future getContentDetail(String contentId) async { debugPrint('📝 AiExtractionRepo: Getting content detail...'); return await _remoteDataSource.getContentDetail(contentId); } @override - Future savePlace(int placeId) async { + Future savePlace(String placeId) async { debugPrint('📝 AiExtractionRepo: Saving place...'); return await _remoteDataSource.savePlace(placeId); } diff --git a/lib/features/ai_extraction/data/models/analyze_request.dart b/lib/features/ai_extraction/data/models/analyze_request.dart index 1838b6b..4041e62 100644 --- a/lib/features/ai_extraction/data/models/analyze_request.dart +++ b/lib/features/ai_extraction/data/models/analyze_request.dart @@ -3,12 +3,12 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'analyze_request.freezed.dart'; part 'analyze_request.g.dart'; -/// AI 분석 요청 DTO +/// AI 분석 요청 DTO - 백엔드 RequestPlaceExtractionRequest 매칭 @freezed class AnalyzeRequest with _$AnalyzeRequest { const factory AnalyzeRequest({ /// SNS URL (Instagram, YouTube) - @JsonKey(name: 'snsUrl') required String sourceUrl, + required String snsUrl, }) = _AnalyzeRequest; factory AnalyzeRequest.fromJson(Map json) => diff --git a/lib/features/ai_extraction/data/models/analyze_request.freezed.dart b/lib/features/ai_extraction/data/models/analyze_request.freezed.dart index 94a759f..ffa0e0f 100644 --- a/lib/features/ai_extraction/data/models/analyze_request.freezed.dart +++ b/lib/features/ai_extraction/data/models/analyze_request.freezed.dart @@ -22,7 +22,7 @@ AnalyzeRequest _$AnalyzeRequestFromJson(Map json) { /// @nodoc mixin _$AnalyzeRequest { /// SNS URL (Instagram, YouTube) - String get sourceUrl => throw _privateConstructorUsedError; + String get snsUrl => throw _privateConstructorUsedError; /// Serializes this AnalyzeRequest to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -41,7 +41,7 @@ abstract class $AnalyzeRequestCopyWith<$Res> { $Res Function(AnalyzeRequest) then, ) = _$AnalyzeRequestCopyWithImpl<$Res, AnalyzeRequest>; @useResult - $Res call({String sourceUrl}); + $Res call({String snsUrl}); } /// @nodoc @@ -58,12 +58,12 @@ class _$AnalyzeRequestCopyWithImpl<$Res, $Val extends AnalyzeRequest> /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({Object? sourceUrl = null}) { + $Res call({Object? snsUrl = null}) { return _then( _value.copyWith( - sourceUrl: null == sourceUrl - ? _value.sourceUrl - : sourceUrl // ignore: cast_nullable_to_non_nullable + snsUrl: null == snsUrl + ? _value.snsUrl + : snsUrl // ignore: cast_nullable_to_non_nullable as String, ) as $Val, @@ -80,7 +80,7 @@ abstract class _$$AnalyzeRequestImplCopyWith<$Res> ) = __$$AnalyzeRequestImplCopyWithImpl<$Res>; @override @useResult - $Res call({String sourceUrl}); + $Res call({String snsUrl}); } /// @nodoc @@ -96,12 +96,12 @@ class __$$AnalyzeRequestImplCopyWithImpl<$Res> /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({Object? sourceUrl = null}) { + $Res call({Object? snsUrl = null}) { return _then( _$AnalyzeRequestImpl( - sourceUrl: null == sourceUrl - ? _value.sourceUrl - : sourceUrl // ignore: cast_nullable_to_non_nullable + snsUrl: null == snsUrl + ? _value.snsUrl + : snsUrl // ignore: cast_nullable_to_non_nullable as String, ), ); @@ -111,18 +111,18 @@ class __$$AnalyzeRequestImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$AnalyzeRequestImpl implements _AnalyzeRequest { - const _$AnalyzeRequestImpl({required this.sourceUrl}); + const _$AnalyzeRequestImpl({required this.snsUrl}); factory _$AnalyzeRequestImpl.fromJson(Map json) => _$$AnalyzeRequestImplFromJson(json); /// SNS URL (Instagram, YouTube) @override - final String sourceUrl; + final String snsUrl; @override String toString() { - return 'AnalyzeRequest(sourceUrl: $sourceUrl)'; + return 'AnalyzeRequest(snsUrl: $snsUrl)'; } @override @@ -130,13 +130,12 @@ class _$AnalyzeRequestImpl implements _AnalyzeRequest { return identical(this, other) || (other.runtimeType == runtimeType && other is _$AnalyzeRequestImpl && - (identical(other.sourceUrl, sourceUrl) || - other.sourceUrl == sourceUrl)); + (identical(other.snsUrl, snsUrl) || other.snsUrl == snsUrl)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, sourceUrl); + int get hashCode => Object.hash(runtimeType, snsUrl); /// Create a copy of AnalyzeRequest /// with the given fields replaced by the non-null parameter values. @@ -156,7 +155,7 @@ class _$AnalyzeRequestImpl implements _AnalyzeRequest { } abstract class _AnalyzeRequest implements AnalyzeRequest { - const factory _AnalyzeRequest({required final String sourceUrl}) = + const factory _AnalyzeRequest({required final String snsUrl}) = _$AnalyzeRequestImpl; factory _AnalyzeRequest.fromJson(Map json) = @@ -164,7 +163,7 @@ abstract class _AnalyzeRequest implements AnalyzeRequest { /// SNS URL (Instagram, YouTube) @override - String get sourceUrl; + String get snsUrl; /// Create a copy of AnalyzeRequest /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/ai_extraction/data/models/analyze_request.g.dart b/lib/features/ai_extraction/data/models/analyze_request.g.dart index c1b4d7f..f847710 100644 --- a/lib/features/ai_extraction/data/models/analyze_request.g.dart +++ b/lib/features/ai_extraction/data/models/analyze_request.g.dart @@ -7,8 +7,8 @@ part of 'analyze_request.dart'; // ************************************************************************** _$AnalyzeRequestImpl _$$AnalyzeRequestImplFromJson(Map json) => - _$AnalyzeRequestImpl(sourceUrl: json['snsUrl'] as String); + _$AnalyzeRequestImpl(snsUrl: json['snsUrl'] as String); Map _$$AnalyzeRequestImplToJson( _$AnalyzeRequestImpl instance, -) => {'snsUrl': instance.sourceUrl}; +) => {'snsUrl': instance.snsUrl}; diff --git a/lib/features/ai_extraction/data/models/analyze_response.dart b/lib/features/ai_extraction/data/models/analyze_response.dart index ad3c9ae..5196fc2 100644 --- a/lib/features/ai_extraction/data/models/analyze_response.dart +++ b/lib/features/ai_extraction/data/models/analyze_response.dart @@ -3,12 +3,18 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'analyze_response.freezed.dart'; part 'analyze_response.g.dart'; -/// AI 분석 응답 DTO +/// AI 분석 응답 DTO - 백엔드 RequestPlaceExtractionResponse 매칭 @freezed class AnalyzeResponse with _$AnalyzeResponse { const factory AnalyzeResponse({ - /// 폴링용 콘텐츠 ID - required int contentId, + /// 폴링용 콘텐츠 ID (UUID) + required String contentId, + + /// 회원 ID (UUID) + String? memberId, + + /// 장소 추출 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) + String? status, }) = _AnalyzeResponse; factory AnalyzeResponse.fromJson(Map json) => diff --git a/lib/features/ai_extraction/data/models/analyze_response.freezed.dart b/lib/features/ai_extraction/data/models/analyze_response.freezed.dart index e20d8db..c798bca 100644 --- a/lib/features/ai_extraction/data/models/analyze_response.freezed.dart +++ b/lib/features/ai_extraction/data/models/analyze_response.freezed.dart @@ -21,8 +21,14 @@ AnalyzeResponse _$AnalyzeResponseFromJson(Map json) { /// @nodoc mixin _$AnalyzeResponse { - /// 폴링용 콘텐츠 ID - int get contentId => throw _privateConstructorUsedError; + /// 폴링용 콘텐츠 ID (UUID) + String get contentId => throw _privateConstructorUsedError; + + /// 회원 ID (UUID) + String? get memberId => throw _privateConstructorUsedError; + + /// 장소 추출 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) + String? get status => throw _privateConstructorUsedError; /// Serializes this AnalyzeResponse to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -41,7 +47,7 @@ abstract class $AnalyzeResponseCopyWith<$Res> { $Res Function(AnalyzeResponse) then, ) = _$AnalyzeResponseCopyWithImpl<$Res, AnalyzeResponse>; @useResult - $Res call({int contentId}); + $Res call({String contentId, String? memberId, String? status}); } /// @nodoc @@ -58,13 +64,25 @@ class _$AnalyzeResponseCopyWithImpl<$Res, $Val extends AnalyzeResponse> /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({Object? contentId = null}) { + $Res call({ + Object? contentId = null, + Object? memberId = freezed, + Object? status = freezed, + }) { return _then( _value.copyWith( contentId: null == contentId ? _value.contentId : contentId // ignore: cast_nullable_to_non_nullable - as int, + as String, + memberId: freezed == memberId + ? _value.memberId + : memberId // ignore: cast_nullable_to_non_nullable + as String?, + status: freezed == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String?, ) as $Val, ); @@ -80,7 +98,7 @@ abstract class _$$AnalyzeResponseImplCopyWith<$Res> ) = __$$AnalyzeResponseImplCopyWithImpl<$Res>; @override @useResult - $Res call({int contentId}); + $Res call({String contentId, String? memberId, String? status}); } /// @nodoc @@ -96,13 +114,25 @@ class __$$AnalyzeResponseImplCopyWithImpl<$Res> /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({Object? contentId = null}) { + $Res call({ + Object? contentId = null, + Object? memberId = freezed, + Object? status = freezed, + }) { return _then( _$AnalyzeResponseImpl( contentId: null == contentId ? _value.contentId : contentId // ignore: cast_nullable_to_non_nullable - as int, + as String, + memberId: freezed == memberId + ? _value.memberId + : memberId // ignore: cast_nullable_to_non_nullable + as String?, + status: freezed == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String?, ), ); } @@ -111,18 +141,30 @@ class __$$AnalyzeResponseImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$AnalyzeResponseImpl implements _AnalyzeResponse { - const _$AnalyzeResponseImpl({required this.contentId}); + const _$AnalyzeResponseImpl({ + required this.contentId, + this.memberId, + this.status, + }); factory _$AnalyzeResponseImpl.fromJson(Map json) => _$$AnalyzeResponseImplFromJson(json); - /// 폴링용 콘텐츠 ID + /// 폴링용 콘텐츠 ID (UUID) + @override + final String contentId; + + /// 회원 ID (UUID) + @override + final String? memberId; + + /// 장소 추출 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) @override - final int contentId; + final String? status; @override String toString() { - return 'AnalyzeResponse(contentId: $contentId)'; + return 'AnalyzeResponse(contentId: $contentId, memberId: $memberId, status: $status)'; } @override @@ -131,12 +173,15 @@ class _$AnalyzeResponseImpl implements _AnalyzeResponse { (other.runtimeType == runtimeType && other is _$AnalyzeResponseImpl && (identical(other.contentId, contentId) || - other.contentId == contentId)); + other.contentId == contentId) && + (identical(other.memberId, memberId) || + other.memberId == memberId) && + (identical(other.status, status) || other.status == status)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, contentId); + int get hashCode => Object.hash(runtimeType, contentId, memberId, status); /// Create a copy of AnalyzeResponse /// with the given fields replaced by the non-null parameter values. @@ -156,15 +201,26 @@ class _$AnalyzeResponseImpl implements _AnalyzeResponse { } abstract class _AnalyzeResponse implements AnalyzeResponse { - const factory _AnalyzeResponse({required final int contentId}) = - _$AnalyzeResponseImpl; + const factory _AnalyzeResponse({ + required final String contentId, + final String? memberId, + final String? status, + }) = _$AnalyzeResponseImpl; factory _AnalyzeResponse.fromJson(Map json) = _$AnalyzeResponseImpl.fromJson; - /// 폴링용 콘텐츠 ID + /// 폴링용 콘텐츠 ID (UUID) + @override + String get contentId; + + /// 회원 ID (UUID) + @override + String? get memberId; + + /// 장소 추출 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) @override - int get contentId; + String? get status; /// Create a copy of AnalyzeResponse /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/ai_extraction/data/models/analyze_response.g.dart b/lib/features/ai_extraction/data/models/analyze_response.g.dart index f7d934b..791b081 100644 --- a/lib/features/ai_extraction/data/models/analyze_response.g.dart +++ b/lib/features/ai_extraction/data/models/analyze_response.g.dart @@ -8,8 +8,16 @@ part of 'analyze_response.dart'; _$AnalyzeResponseImpl _$$AnalyzeResponseImplFromJson( Map json, -) => _$AnalyzeResponseImpl(contentId: (json['contentId'] as num).toInt()); +) => _$AnalyzeResponseImpl( + contentId: json['contentId'] as String, + memberId: json['memberId'] as String?, + status: json['status'] as String?, +); Map _$$AnalyzeResponseImplToJson( _$AnalyzeResponseImpl instance, -) => {'contentId': instance.contentId}; +) => { + 'contentId': instance.contentId, + 'memberId': instance.memberId, + 'status': instance.status, +}; diff --git a/lib/features/ai_extraction/data/models/content_detail_response.dart b/lib/features/ai_extraction/data/models/content_detail_response.dart index ad2fbb0..cf38cb1 100644 --- a/lib/features/ai_extraction/data/models/content_detail_response.dart +++ b/lib/features/ai_extraction/data/models/content_detail_response.dart @@ -5,17 +5,70 @@ import '../../../../common/models/place_model.dart'; part 'content_detail_response.freezed.dart'; part 'content_detail_response.g.dart'; -/// 콘텐츠 상세 응답 DTO (폴링용) +/// 콘텐츠 상세 응답 DTO - 백엔드 GetContentInfoResponse 매칭 @freezed class ContentDetailResponse with _$ContentDetailResponse { const factory ContentDetailResponse({ - required int contentId, - /// PENDING, PROCESSING, COMPLETED, FAILED - required String status, - String? sourceUrl, + /// 콘텐츠 상세 정보 + required ContentInfo content, + + /// 연관된 장소 목록 (position 순서) @Default([]) List places, }) = _ContentDetailResponse; factory ContentDetailResponse.fromJson(Map json) => _$ContentDetailResponseFromJson(json); + + /// SNS PlaceDto의 id → placeId 매핑이 필요한 경우 사용 + static ContentDetailResponse fromSnsJson(Map json) { + final normalized = Map.from(json); + if (normalized['places'] is List) { + normalized['places'] = (normalized['places'] as List).map((p) { + if (p is Map && p.containsKey('id') && !p.containsKey('placeId')) { + return Map.from(p)..['placeId'] = p['id']; + } + return p; + }).toList(); + } + return _$ContentDetailResponseFromJson(normalized); + } +} + +/// 콘텐츠 정보 DTO - 백엔드 ContentDto 매칭 +@freezed +class ContentInfo with _$ContentInfo { + const factory ContentInfo({ + /// 콘텐츠 ID (UUID) + required String id, + + /// 플랫폼 유형 (INSTAGRAM, YOUTUBE 등) + String? platform, + + /// 처리 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) + required String status, + + /// 업로더 이름 + String? platformUploader, + + /// 캡션 + String? caption, + + /// 썸네일 URL + String? thumbnailUrl, + + /// 원본 SNS URL + String? originalUrl, + + /// 제목 + String? title, + + /// 요약 설명 + String? summary, + + /// 마지막 확인 시각 + String? lastCheckedAt, + }) = _ContentInfo; + + factory ContentInfo.fromJson(Map json) => + _$ContentInfoFromJson(json); } diff --git a/lib/features/ai_extraction/data/models/content_detail_response.freezed.dart b/lib/features/ai_extraction/data/models/content_detail_response.freezed.dart index a29041e..d83aa08 100644 --- a/lib/features/ai_extraction/data/models/content_detail_response.freezed.dart +++ b/lib/features/ai_extraction/data/models/content_detail_response.freezed.dart @@ -23,11 +23,10 @@ ContentDetailResponse _$ContentDetailResponseFromJson( /// @nodoc mixin _$ContentDetailResponse { - int get contentId => throw _privateConstructorUsedError; + /// 콘텐츠 상세 정보 + ContentInfo get content => throw _privateConstructorUsedError; - /// PENDING, PROCESSING, COMPLETED, FAILED - String get status => throw _privateConstructorUsedError; - String? get sourceUrl => throw _privateConstructorUsedError; + /// 연관된 장소 목록 (position 순서) List get places => throw _privateConstructorUsedError; /// Serializes this ContentDetailResponse to a JSON map. @@ -47,12 +46,9 @@ abstract class $ContentDetailResponseCopyWith<$Res> { $Res Function(ContentDetailResponse) then, ) = _$ContentDetailResponseCopyWithImpl<$Res, ContentDetailResponse>; @useResult - $Res call({ - int contentId, - String status, - String? sourceUrl, - List places, - }); + $Res call({ContentInfo content, List places}); + + $ContentInfoCopyWith<$Res> get content; } /// @nodoc @@ -72,26 +68,13 @@ class _$ContentDetailResponseCopyWithImpl< /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({ - Object? contentId = null, - Object? status = null, - Object? sourceUrl = freezed, - Object? places = null, - }) { + $Res call({Object? content = null, Object? places = null}) { return _then( _value.copyWith( - contentId: null == contentId - ? _value.contentId - : contentId // ignore: cast_nullable_to_non_nullable - as int, - status: null == status - ? _value.status - : status // ignore: cast_nullable_to_non_nullable - as String, - sourceUrl: freezed == sourceUrl - ? _value.sourceUrl - : sourceUrl // ignore: cast_nullable_to_non_nullable - as String?, + content: null == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as ContentInfo, places: null == places ? _value.places : places // ignore: cast_nullable_to_non_nullable @@ -100,6 +83,16 @@ class _$ContentDetailResponseCopyWithImpl< as $Val, ); } + + /// Create a copy of ContentDetailResponse + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ContentInfoCopyWith<$Res> get content { + return $ContentInfoCopyWith<$Res>(_value.content, (value) { + return _then(_value.copyWith(content: value) as $Val); + }); + } } /// @nodoc @@ -111,12 +104,10 @@ abstract class _$$ContentDetailResponseImplCopyWith<$Res> ) = __$$ContentDetailResponseImplCopyWithImpl<$Res>; @override @useResult - $Res call({ - int contentId, - String status, - String? sourceUrl, - List places, - }); + $Res call({ContentInfo content, List places}); + + @override + $ContentInfoCopyWith<$Res> get content; } /// @nodoc @@ -133,26 +124,13 @@ class __$$ContentDetailResponseImplCopyWithImpl<$Res> /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({ - Object? contentId = null, - Object? status = null, - Object? sourceUrl = freezed, - Object? places = null, - }) { + $Res call({Object? content = null, Object? places = null}) { return _then( _$ContentDetailResponseImpl( - contentId: null == contentId - ? _value.contentId - : contentId // ignore: cast_nullable_to_non_nullable - as int, - status: null == status - ? _value.status - : status // ignore: cast_nullable_to_non_nullable - as String, - sourceUrl: freezed == sourceUrl - ? _value.sourceUrl - : sourceUrl // ignore: cast_nullable_to_non_nullable - as String?, + content: null == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as ContentInfo, places: null == places ? _value._places : places // ignore: cast_nullable_to_non_nullable @@ -166,24 +144,21 @@ class __$$ContentDetailResponseImplCopyWithImpl<$Res> @JsonSerializable() class _$ContentDetailResponseImpl implements _ContentDetailResponse { const _$ContentDetailResponseImpl({ - required this.contentId, - required this.status, - this.sourceUrl, + required this.content, final List places = const [], }) : _places = places; factory _$ContentDetailResponseImpl.fromJson(Map json) => _$$ContentDetailResponseImplFromJson(json); + /// 콘텐츠 상세 정보 @override - final int contentId; + final ContentInfo content; - /// PENDING, PROCESSING, COMPLETED, FAILED - @override - final String status; - @override - final String? sourceUrl; + /// 연관된 장소 목록 (position 순서) final List _places; + + /// 연관된 장소 목록 (position 순서) @override @JsonKey() List get places { @@ -194,7 +169,7 @@ class _$ContentDetailResponseImpl implements _ContentDetailResponse { @override String toString() { - return 'ContentDetailResponse(contentId: $contentId, status: $status, sourceUrl: $sourceUrl, places: $places)'; + return 'ContentDetailResponse(content: $content, places: $places)'; } @override @@ -202,11 +177,7 @@ class _$ContentDetailResponseImpl implements _ContentDetailResponse { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ContentDetailResponseImpl && - (identical(other.contentId, contentId) || - other.contentId == contentId) && - (identical(other.status, status) || other.status == status) && - (identical(other.sourceUrl, sourceUrl) || - other.sourceUrl == sourceUrl) && + (identical(other.content, content) || other.content == content) && const DeepCollectionEquality().equals(other._places, _places)); } @@ -214,9 +185,7 @@ class _$ContentDetailResponseImpl implements _ContentDetailResponse { @override int get hashCode => Object.hash( runtimeType, - contentId, - status, - sourceUrl, + content, const DeepCollectionEquality().hash(_places), ); @@ -240,23 +209,18 @@ class _$ContentDetailResponseImpl implements _ContentDetailResponse { abstract class _ContentDetailResponse implements ContentDetailResponse { const factory _ContentDetailResponse({ - required final int contentId, - required final String status, - final String? sourceUrl, + required final ContentInfo content, final List places, }) = _$ContentDetailResponseImpl; factory _ContentDetailResponse.fromJson(Map json) = _$ContentDetailResponseImpl.fromJson; + /// 콘텐츠 상세 정보 @override - int get contentId; + ContentInfo get content; - /// PENDING, PROCESSING, COMPLETED, FAILED - @override - String get status; - @override - String? get sourceUrl; + /// 연관된 장소 목록 (position 순서) @override List get places; @@ -267,3 +231,420 @@ abstract class _ContentDetailResponse implements ContentDetailResponse { _$$ContentDetailResponseImplCopyWith<_$ContentDetailResponseImpl> get copyWith => throw _privateConstructorUsedError; } + +ContentInfo _$ContentInfoFromJson(Map json) { + return _ContentInfo.fromJson(json); +} + +/// @nodoc +mixin _$ContentInfo { + /// 콘텐츠 ID (UUID) + String get id => throw _privateConstructorUsedError; + + /// 플랫폼 유형 (INSTAGRAM, YOUTUBE 등) + String? get platform => throw _privateConstructorUsedError; + + /// 처리 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) + String get status => throw _privateConstructorUsedError; + + /// 업로더 이름 + String? get platformUploader => throw _privateConstructorUsedError; + + /// 캡션 + String? get caption => throw _privateConstructorUsedError; + + /// 썸네일 URL + String? get thumbnailUrl => throw _privateConstructorUsedError; + + /// 원본 SNS URL + String? get originalUrl => throw _privateConstructorUsedError; + + /// 제목 + String? get title => throw _privateConstructorUsedError; + + /// 요약 설명 + String? get summary => throw _privateConstructorUsedError; + + /// 마지막 확인 시각 + String? get lastCheckedAt => throw _privateConstructorUsedError; + + /// Serializes this ContentInfo to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ContentInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ContentInfoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ContentInfoCopyWith<$Res> { + factory $ContentInfoCopyWith( + ContentInfo value, + $Res Function(ContentInfo) then, + ) = _$ContentInfoCopyWithImpl<$Res, ContentInfo>; + @useResult + $Res call({ + String id, + String? platform, + String status, + String? platformUploader, + String? caption, + String? thumbnailUrl, + String? originalUrl, + String? title, + String? summary, + String? lastCheckedAt, + }); +} + +/// @nodoc +class _$ContentInfoCopyWithImpl<$Res, $Val extends ContentInfo> + implements $ContentInfoCopyWith<$Res> { + _$ContentInfoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ContentInfo + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? platform = freezed, + Object? status = null, + Object? platformUploader = freezed, + Object? caption = freezed, + Object? thumbnailUrl = freezed, + Object? originalUrl = freezed, + Object? title = freezed, + Object? summary = freezed, + Object? lastCheckedAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + platform: freezed == platform + ? _value.platform + : platform // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + platformUploader: freezed == platformUploader + ? _value.platformUploader + : platformUploader // ignore: cast_nullable_to_non_nullable + as String?, + caption: freezed == caption + ? _value.caption + : caption // ignore: cast_nullable_to_non_nullable + as String?, + thumbnailUrl: freezed == thumbnailUrl + ? _value.thumbnailUrl + : thumbnailUrl // ignore: cast_nullable_to_non_nullable + as String?, + originalUrl: freezed == originalUrl + ? _value.originalUrl + : originalUrl // ignore: cast_nullable_to_non_nullable + as String?, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + summary: freezed == summary + ? _value.summary + : summary // ignore: cast_nullable_to_non_nullable + as String?, + lastCheckedAt: freezed == lastCheckedAt + ? _value.lastCheckedAt + : lastCheckedAt // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ContentInfoImplCopyWith<$Res> + implements $ContentInfoCopyWith<$Res> { + factory _$$ContentInfoImplCopyWith( + _$ContentInfoImpl value, + $Res Function(_$ContentInfoImpl) then, + ) = __$$ContentInfoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String? platform, + String status, + String? platformUploader, + String? caption, + String? thumbnailUrl, + String? originalUrl, + String? title, + String? summary, + String? lastCheckedAt, + }); +} + +/// @nodoc +class __$$ContentInfoImplCopyWithImpl<$Res> + extends _$ContentInfoCopyWithImpl<$Res, _$ContentInfoImpl> + implements _$$ContentInfoImplCopyWith<$Res> { + __$$ContentInfoImplCopyWithImpl( + _$ContentInfoImpl _value, + $Res Function(_$ContentInfoImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ContentInfo + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? platform = freezed, + Object? status = null, + Object? platformUploader = freezed, + Object? caption = freezed, + Object? thumbnailUrl = freezed, + Object? originalUrl = freezed, + Object? title = freezed, + Object? summary = freezed, + Object? lastCheckedAt = freezed, + }) { + return _then( + _$ContentInfoImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + platform: freezed == platform + ? _value.platform + : platform // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + platformUploader: freezed == platformUploader + ? _value.platformUploader + : platformUploader // ignore: cast_nullable_to_non_nullable + as String?, + caption: freezed == caption + ? _value.caption + : caption // ignore: cast_nullable_to_non_nullable + as String?, + thumbnailUrl: freezed == thumbnailUrl + ? _value.thumbnailUrl + : thumbnailUrl // ignore: cast_nullable_to_non_nullable + as String?, + originalUrl: freezed == originalUrl + ? _value.originalUrl + : originalUrl // ignore: cast_nullable_to_non_nullable + as String?, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + summary: freezed == summary + ? _value.summary + : summary // ignore: cast_nullable_to_non_nullable + as String?, + lastCheckedAt: freezed == lastCheckedAt + ? _value.lastCheckedAt + : lastCheckedAt // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ContentInfoImpl implements _ContentInfo { + const _$ContentInfoImpl({ + required this.id, + this.platform, + required this.status, + this.platformUploader, + this.caption, + this.thumbnailUrl, + this.originalUrl, + this.title, + this.summary, + this.lastCheckedAt, + }); + + factory _$ContentInfoImpl.fromJson(Map json) => + _$$ContentInfoImplFromJson(json); + + /// 콘텐츠 ID (UUID) + @override + final String id; + + /// 플랫폼 유형 (INSTAGRAM, YOUTUBE 등) + @override + final String? platform; + + /// 처리 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) + @override + final String status; + + /// 업로더 이름 + @override + final String? platformUploader; + + /// 캡션 + @override + final String? caption; + + /// 썸네일 URL + @override + final String? thumbnailUrl; + + /// 원본 SNS URL + @override + final String? originalUrl; + + /// 제목 + @override + final String? title; + + /// 요약 설명 + @override + final String? summary; + + /// 마지막 확인 시각 + @override + final String? lastCheckedAt; + + @override + String toString() { + return 'ContentInfo(id: $id, platform: $platform, status: $status, platformUploader: $platformUploader, caption: $caption, thumbnailUrl: $thumbnailUrl, originalUrl: $originalUrl, title: $title, summary: $summary, lastCheckedAt: $lastCheckedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ContentInfoImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.platform, platform) || + other.platform == platform) && + (identical(other.status, status) || other.status == status) && + (identical(other.platformUploader, platformUploader) || + other.platformUploader == platformUploader) && + (identical(other.caption, caption) || other.caption == caption) && + (identical(other.thumbnailUrl, thumbnailUrl) || + other.thumbnailUrl == thumbnailUrl) && + (identical(other.originalUrl, originalUrl) || + other.originalUrl == originalUrl) && + (identical(other.title, title) || other.title == title) && + (identical(other.summary, summary) || other.summary == summary) && + (identical(other.lastCheckedAt, lastCheckedAt) || + other.lastCheckedAt == lastCheckedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + platform, + status, + platformUploader, + caption, + thumbnailUrl, + originalUrl, + title, + summary, + lastCheckedAt, + ); + + /// Create a copy of ContentInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ContentInfoImplCopyWith<_$ContentInfoImpl> get copyWith => + __$$ContentInfoImplCopyWithImpl<_$ContentInfoImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ContentInfoImplToJson(this); + } +} + +abstract class _ContentInfo implements ContentInfo { + const factory _ContentInfo({ + required final String id, + final String? platform, + required final String status, + final String? platformUploader, + final String? caption, + final String? thumbnailUrl, + final String? originalUrl, + final String? title, + final String? summary, + final String? lastCheckedAt, + }) = _$ContentInfoImpl; + + factory _ContentInfo.fromJson(Map json) = + _$ContentInfoImpl.fromJson; + + /// 콘텐츠 ID (UUID) + @override + String get id; + + /// 플랫폼 유형 (INSTAGRAM, YOUTUBE 등) + @override + String? get platform; + + /// 처리 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) + @override + String get status; + + /// 업로더 이름 + @override + String? get platformUploader; + + /// 캡션 + @override + String? get caption; + + /// 썸네일 URL + @override + String? get thumbnailUrl; + + /// 원본 SNS URL + @override + String? get originalUrl; + + /// 제목 + @override + String? get title; + + /// 요약 설명 + @override + String? get summary; + + /// 마지막 확인 시각 + @override + String? get lastCheckedAt; + + /// Create a copy of ContentInfo + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ContentInfoImplCopyWith<_$ContentInfoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/ai_extraction/data/models/content_detail_response.g.dart b/lib/features/ai_extraction/data/models/content_detail_response.g.dart index 9f4a281..f1f1469 100644 --- a/lib/features/ai_extraction/data/models/content_detail_response.g.dart +++ b/lib/features/ai_extraction/data/models/content_detail_response.g.dart @@ -9,9 +9,7 @@ part of 'content_detail_response.dart'; _$ContentDetailResponseImpl _$$ContentDetailResponseImplFromJson( Map json, ) => _$ContentDetailResponseImpl( - contentId: (json['contentId'] as num).toInt(), - status: json['status'] as String, - sourceUrl: json['sourceUrl'] as String?, + content: ContentInfo.fromJson(json['content'] as Map), places: (json['places'] as List?) ?.map((e) => PlaceModel.fromJson(e as Map)) @@ -21,9 +19,32 @@ _$ContentDetailResponseImpl _$$ContentDetailResponseImplFromJson( Map _$$ContentDetailResponseImplToJson( _$ContentDetailResponseImpl instance, -) => { - 'contentId': instance.contentId, - 'status': instance.status, - 'sourceUrl': instance.sourceUrl, - 'places': instance.places, -}; +) => {'content': instance.content, 'places': instance.places}; + +_$ContentInfoImpl _$$ContentInfoImplFromJson(Map json) => + _$ContentInfoImpl( + id: json['id'] as String, + platform: json['platform'] as String?, + status: json['status'] as String, + platformUploader: json['platformUploader'] as String?, + caption: json['caption'] as String?, + thumbnailUrl: json['thumbnailUrl'] as String?, + originalUrl: json['originalUrl'] as String?, + title: json['title'] as String?, + summary: json['summary'] as String?, + lastCheckedAt: json['lastCheckedAt'] as String?, + ); + +Map _$$ContentInfoImplToJson(_$ContentInfoImpl instance) => + { + 'id': instance.id, + 'platform': instance.platform, + 'status': instance.status, + 'platformUploader': instance.platformUploader, + 'caption': instance.caption, + 'thumbnailUrl': instance.thumbnailUrl, + 'originalUrl': instance.originalUrl, + 'title': instance.title, + 'summary': instance.summary, + 'lastCheckedAt': instance.lastCheckedAt, + }; diff --git a/lib/features/ai_extraction/presentation/ai_extraction_provider.dart b/lib/features/ai_extraction/presentation/ai_extraction_provider.dart index 158839a..73fe32f 100644 --- a/lib/features/ai_extraction/presentation/ai_extraction_provider.dart +++ b/lib/features/ai_extraction/presentation/ai_extraction_provider.dart @@ -27,9 +27,9 @@ class AiExtractionState with _$AiExtractionState { const factory AiExtractionState({ @Default(AiExtractionStep.input) AiExtractionStep step, @Default('') String url, - int? contentId, + String? contentId, @Default([]) List places, - @Default({}) Set selectedPlaceIds, + @Default({}) Set selectedPlaceIds, String? errorMessage, @Default(0.0) double saveProgress, }) = _AiExtractionState; @@ -81,7 +81,7 @@ class AiExtractionNotifier extends _$AiExtractionNotifier { try { final repository = ref.read(aiExtractionRepositoryProvider); final response = await repository.analyze( - AnalyzeRequest(sourceUrl: trimmedUrl), + AnalyzeRequest(snsUrl: trimmedUrl), ); if (_disposed) return; @@ -97,7 +97,7 @@ class AiExtractionNotifier extends _$AiExtractionNotifier { } } - void _startPolling(int contentId) { + void _startPolling(String contentId) { int attempts = 0; int consecutiveFailures = 0; const maxAttempts = 60; @@ -135,7 +135,7 @@ class AiExtractionNotifier extends _$AiExtractionNotifier { consecutiveFailures = 0; - if (detail.status.toUpperCase() == 'COMPLETED') { + if (detail.content.status.toUpperCase() == 'COMPLETED') { timer.cancel(); final allPlaceIds = detail.places.map((p) => p.placeId).toSet(); state = state.copyWith( @@ -143,7 +143,7 @@ class AiExtractionNotifier extends _$AiExtractionNotifier { places: detail.places, selectedPlaceIds: allPlaceIds, ); - } else if (detail.status.toUpperCase() == 'FAILED') { + } else if (detail.content.status.toUpperCase() == 'FAILED') { timer.cancel(); state = state.copyWith( step: AiExtractionStep.error, @@ -166,8 +166,8 @@ class AiExtractionNotifier extends _$AiExtractionNotifier { ); } - void togglePlace(int placeId) { - final selected = Set.from(state.selectedPlaceIds); + void togglePlace(String placeId) { + final selected = Set.from(state.selectedPlaceIds); if (selected.contains(placeId)) { selected.remove(placeId); } else { @@ -196,7 +196,7 @@ class AiExtractionNotifier extends _$AiExtractionNotifier { try { final repository = ref.read(aiExtractionRepositoryProvider); final placeIds = state.selectedPlaceIds.toList(); - final failedIds = []; + final failedIds = []; for (int i = 0; i < placeIds.length; i++) { try { diff --git a/lib/features/ai_extraction/presentation/ai_extraction_provider.freezed.dart b/lib/features/ai_extraction/presentation/ai_extraction_provider.freezed.dart index aa087cc..34bc646 100644 --- a/lib/features/ai_extraction/presentation/ai_extraction_provider.freezed.dart +++ b/lib/features/ai_extraction/presentation/ai_extraction_provider.freezed.dart @@ -19,9 +19,9 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$AiExtractionState { AiExtractionStep get step => throw _privateConstructorUsedError; String get url => throw _privateConstructorUsedError; - int? get contentId => throw _privateConstructorUsedError; + String? get contentId => throw _privateConstructorUsedError; List get places => throw _privateConstructorUsedError; - Set get selectedPlaceIds => throw _privateConstructorUsedError; + Set get selectedPlaceIds => throw _privateConstructorUsedError; String? get errorMessage => throw _privateConstructorUsedError; double get saveProgress => throw _privateConstructorUsedError; @@ -42,9 +42,9 @@ abstract class $AiExtractionStateCopyWith<$Res> { $Res call({ AiExtractionStep step, String url, - int? contentId, + String? contentId, List places, - Set selectedPlaceIds, + Set selectedPlaceIds, String? errorMessage, double saveProgress, }); @@ -86,7 +86,7 @@ class _$AiExtractionStateCopyWithImpl<$Res, $Val extends AiExtractionState> contentId: freezed == contentId ? _value.contentId : contentId // ignore: cast_nullable_to_non_nullable - as int?, + as String?, places: null == places ? _value.places : places // ignore: cast_nullable_to_non_nullable @@ -94,7 +94,7 @@ class _$AiExtractionStateCopyWithImpl<$Res, $Val extends AiExtractionState> selectedPlaceIds: null == selectedPlaceIds ? _value.selectedPlaceIds : selectedPlaceIds // ignore: cast_nullable_to_non_nullable - as Set, + as Set, errorMessage: freezed == errorMessage ? _value.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable @@ -121,9 +121,9 @@ abstract class _$$AiExtractionStateImplCopyWith<$Res> $Res call({ AiExtractionStep step, String url, - int? contentId, + String? contentId, List places, - Set selectedPlaceIds, + Set selectedPlaceIds, String? errorMessage, double saveProgress, }); @@ -164,7 +164,7 @@ class __$$AiExtractionStateImplCopyWithImpl<$Res> contentId: freezed == contentId ? _value.contentId : contentId // ignore: cast_nullable_to_non_nullable - as int?, + as String?, places: null == places ? _value._places : places // ignore: cast_nullable_to_non_nullable @@ -172,7 +172,7 @@ class __$$AiExtractionStateImplCopyWithImpl<$Res> selectedPlaceIds: null == selectedPlaceIds ? _value._selectedPlaceIds : selectedPlaceIds // ignore: cast_nullable_to_non_nullable - as Set, + as Set, errorMessage: freezed == errorMessage ? _value.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable @@ -196,7 +196,7 @@ class _$AiExtractionStateImpl this.url = '', this.contentId, final List places = const [], - final Set selectedPlaceIds = const {}, + final Set selectedPlaceIds = const {}, this.errorMessage, this.saveProgress = 0.0, }) : _places = places, @@ -209,7 +209,7 @@ class _$AiExtractionStateImpl @JsonKey() final String url; @override - final int? contentId; + final String? contentId; final List _places; @override @JsonKey() @@ -219,10 +219,10 @@ class _$AiExtractionStateImpl return EqualUnmodifiableListView(_places); } - final Set _selectedPlaceIds; + final Set _selectedPlaceIds; @override @JsonKey() - Set get selectedPlaceIds { + Set get selectedPlaceIds { if (_selectedPlaceIds is EqualUnmodifiableSetView) return _selectedPlaceIds; // ignore: implicit_dynamic_type return EqualUnmodifiableSetView(_selectedPlaceIds); @@ -301,9 +301,9 @@ abstract class _AiExtractionState implements AiExtractionState { const factory _AiExtractionState({ final AiExtractionStep step, final String url, - final int? contentId, + final String? contentId, final List places, - final Set selectedPlaceIds, + final Set selectedPlaceIds, final String? errorMessage, final double saveProgress, }) = _$AiExtractionStateImpl; @@ -313,11 +313,11 @@ abstract class _AiExtractionState implements AiExtractionState { @override String get url; @override - int? get contentId; + String? get contentId; @override List get places; @override - Set get selectedPlaceIds; + Set get selectedPlaceIds; @override String? get errorMessage; @override diff --git a/lib/features/ai_extraction/presentation/ai_extraction_provider.g.dart b/lib/features/ai_extraction/presentation/ai_extraction_provider.g.dart index b88a5c9..ac7f1c6 100644 --- a/lib/features/ai_extraction/presentation/ai_extraction_provider.g.dart +++ b/lib/features/ai_extraction/presentation/ai_extraction_provider.g.dart @@ -7,7 +7,7 @@ part of 'ai_extraction_provider.dart'; // ************************************************************************** String _$aiExtractionNotifierHash() => - r'd294d9e09204287c142dc5f7892db33f4ce50b32'; + r'61cc42f75470f2b444aee6dd8202a1608fd23510'; /// AI 추출 화면 Notifier /// diff --git a/lib/features/ai_extraction/presentation/widgets/place_result_section.dart b/lib/features/ai_extraction/presentation/widgets/place_result_section.dart index 23b6959..6e7f939 100644 --- a/lib/features/ai_extraction/presentation/widgets/place_result_section.dart +++ b/lib/features/ai_extraction/presentation/widgets/place_result_section.dart @@ -20,8 +20,8 @@ class PlaceResultSection extends StatelessWidget { }); final List places; - final Set selectedPlaceIds; - final ValueChanged onTogglePlace; + final Set selectedPlaceIds; + final ValueChanged onTogglePlace; final VoidCallback onToggleAll; final VoidCallback onSave; final bool isSaving; @@ -203,9 +203,9 @@ class _PlaceSelectItem extends StatelessWidget { width: 56.w, height: 56.w, color: HomeColors.shimmerBase, - child: place.imageUrl != null + child: place.photoUrls.isNotEmpty ? Image.network( - place.imageUrl!, + place.photoUrls.first, fit: BoxFit.cover, errorBuilder: (_, _, _) => Icon( Icons.place, @@ -226,7 +226,7 @@ class _PlaceSelectItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - place.placeName, + place.name, style: AppTextStyles.label, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -242,13 +242,15 @@ class _PlaceSelectItem extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ], - if (place.category != null) ...[ + if (place.description != null) ...[ SizedBox(height: 2.h), Text( - place.category!, + place.description!, style: AppTextStyles.callout.copyWith( color: HomeColors.textDisabled, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ], diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index e342021..39a87ed 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -2,8 +2,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; +import '../../../../common/constants/app_colors.dart'; import '../../../../common/constants/spacing_and_radius.dart'; import '../../../../common/constants/text_styles.dart'; import '../../../../common/exceptions/app_exception.dart'; @@ -39,7 +41,7 @@ class LoginPage extends ConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(errorMessage, style: AppTextStyles.toast), - backgroundColor: Colors.red, + backgroundColor: AppColors.error, behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 3), ), @@ -75,7 +77,7 @@ class LoginPage extends ConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(errorMessage, style: AppTextStyles.toast), - backgroundColor: Colors.red, + backgroundColor: AppColors.error, behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 3), ), @@ -124,70 +126,98 @@ class LoginPage extends ConsumerWidget { final authState = ref.watch(authNotifierProvider); return Scaffold( - appBar: AppBar(title: Text('Login', style: AppTextStyles.subHeading)), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('로그인', style: AppTextStyles.heading01), - SizedBox(height: AppSpacing.vertical40), - - // TODO: Google 로그인 버튼 디자인 만들어지면 바뀌어야함 - ElevatedButton.icon( - onPressed: authState.isLoading - ? null // 로딩 중에는 버튼 비활성화 - : () => _handleGoogleSignIn(context, ref), - icon: const Icon(Icons.login), - label: const Text('Google Login'), - style: ElevatedButton.styleFrom( - padding: AppPadding.buttonPadding, - textStyle: AppTextStyles.label, + backgroundColor: Colors.white, + body: SafeArea( + child: Padding( + padding: AppPadding.horizontal20, + child: Column( + children: [ + const Spacer(flex: 3), + + Image.asset( + 'assets/mapsy_logo_transparent.png', + width: 160.w, ), - ), - // iOS에서만 Apple 로그인 버튼 표시 - if (Platform.isIOS) ...[ - SizedBox(height: AppSpacing.vertical16), - // TODO: Apple 로그인 버튼 디자인 만들어지면 바뀌어야함 - ElevatedButton.icon( - onPressed: authState.isLoading - ? null // 로딩 중에는 버튼 비활성화 - : () => _handleAppleSignIn(context, ref), - icon: const Icon(Icons.apple), - label: const Text('Apple Login'), - style: ElevatedButton.styleFrom( - padding: AppPadding.buttonPadding, - textStyle: AppTextStyles.label, - backgroundColor: Colors.black, - foregroundColor: Colors.white, + const Spacer(flex: 2), + + // Google 로그인 버튼 + SizedBox( + width: double.infinity, + height: 52.h, + child: OutlinedButton( + onPressed: authState.isLoading + ? null + : () => _handleGoogleSignIn(context, ref), + style: OutlinedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: AppColors.textPrimary, + side: const BorderSide(color: AppColors.gray300), + shape: RoundedRectangleBorder( + borderRadius: AppRadius.medium, + ), + ), + child: Text('Google로 계속하기', style: AppTextStyles.label), ), ), - ], - // 로딩 인디케이터 - if (authState.isLoading) - Padding( - padding: EdgeInsets.only(top: AppSpacing.vertical20), - child: const CircularProgressIndicator(), - ), + // iOS에서만 Apple 로그인 버튼 표시 + if (Platform.isIOS) ...[ + SizedBox(height: AppSpacing.vertical12), + SizedBox( + width: double.infinity, + height: 52.h, + child: ElevatedButton.icon( + onPressed: authState.isLoading + ? null + : () => _handleAppleSignIn(context, ref), + icon: const Icon(Icons.apple, size: 22), + label: Text('Apple로 계속하기', style: AppTextStyles.label), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: AppRadius.medium, + ), + elevation: 0, + ), + ), + ), + ], - // 에러 메시지 (선택사항 - SnackBar와 중복이므로 간단하게 표시) - if (authState.hasError && !authState.isLoading) - Padding( - padding: EdgeInsets.only( - top: AppSpacing.vertical20, - left: AppSpacing.horizontal20, - right: AppSpacing.horizontal20, + if (authState.isLoading) + Padding( + padding: EdgeInsets.only(top: AppSpacing.vertical20), + child: const CircularProgressIndicator(), ), - child: Text( - authState.error is AuthException - ? (authState.error as AuthException).message - : '로그인에 실패했습니다.', - style: AppTextStyles.paragraph.copyWith(color: Colors.red), - textAlign: TextAlign.center, + + if (authState.hasError && !authState.isLoading) + Padding( + padding: EdgeInsets.only(top: AppSpacing.vertical12), + child: Text( + authState.error is AuthException + ? (authState.error as AuthException).message + : '로그인에 실패했습니다.', + style: AppTextStyles.callout.copyWith( + color: AppColors.error, + ), + textAlign: TextAlign.center, + ), ), + + SizedBox(height: AppSpacing.vertical16), + + Text( + '계속 진행하면 서비스 이용약관 및\n개인정보 처리방침에 동의하는 것으로 간주합니다.', + style: AppTextStyles.calloutSmall.copyWith( + color: AppColors.textDisabled, + ), + textAlign: TextAlign.center, ), - ], + + SizedBox(height: AppSpacing.vertical32), + ], + ), ), ), ); diff --git a/lib/features/auth/presentation/pages/splash_page.dart b/lib/features/auth/presentation/pages/splash_page.dart index 2e66b05..69f1cac 100644 --- a/lib/features/auth/presentation/pages/splash_page.dart +++ b/lib/features/auth/presentation/pages/splash_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import '../../../../routing/route_paths.dart'; @@ -19,13 +20,43 @@ class SplashPage extends ConsumerStatefulWidget { ConsumerState createState() => _SplashPageState(); } -class _SplashPageState extends ConsumerState { +class _SplashPageState extends ConsumerState + with SingleTickerProviderStateMixin { + late final AnimationController _animationController; + late final Animation _fadeAnimation; + late final Animation _scaleAnimation; + @override void initState() { super.initState(); + + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); + + _fadeAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutCubic, + ); + + _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutCubic, + ), + ); + + _animationController.forward(); _navigateToNextScreen(); } + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + /// 인증 상태를 확인하고 적절한 화면으로 이동 Future _navigateToNextScreen() async { // 최소 2초간 스플래시 표시 @@ -90,6 +121,20 @@ class _SplashPageState extends ConsumerState { @override Widget build(BuildContext context) { - return const Scaffold(body: Center(child: Text('Splash'))); + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: FadeTransition( + opacity: _fadeAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: Image.asset( + 'assets/mapsy_logo_transparent.png', + width: 160.w, + ), + ), + ), + ), + ); } } diff --git a/lib/features/home/data/home_remote_datasource.dart b/lib/features/home/data/home_remote_datasource.dart index 7d59383..a1f5462 100644 --- a/lib/features/home/data/home_remote_datasource.dart +++ b/lib/features/home/data/home_remote_datasource.dart @@ -17,58 +17,47 @@ HomeRemoteDataSource homeRemoteDataSource(Ref ref) { } /// 홈 화면 Remote DataSource -/// -/// 홈 피드에 필요한 백엔드 API를 호출합니다. class HomeRemoteDataSource { final Dio _dio; HomeRemoteDataSource(this._dio); - /// 최근 콘텐츠 목록 조회 (커서 기반 페이지네이션) + /// 최근 콘텐츠 목록 조회 /// - /// GET /api/content/recent?cursor={cursor}&size={size} - Future fetchRecentContent({ - int? cursor, - int size = 20, - }) async { + /// GET /api/content/recent + Future fetchRecentContent() async { debugPrint('📤 HomeRemoteDataSource: Fetching recent content...'); - final response = await _dio.get( - ApiEndpoints.recentContent, - queryParameters: { - if (cursor != null) 'cursor': cursor, - 'size': size, - }, - ); + final response = await _dio.get(ApiEndpoints.recentContent); - final result = ContentListResponse.fromJson( + final result = RecentContentResponse.fromJson( response.data as Map, ); - debugPrint('✅ Recent content fetched: ${result.content.length} items'); + debugPrint('✅ Recent content fetched: ${result.contents.length} items'); return result; } - /// 회원 콘텐츠 목록 조회 (커서 기반 페이지네이션) + /// 회원 콘텐츠 목록 조회 (Spring Page 기반 페이지네이션) /// - /// GET /api/content/member?cursor={cursor}&size={size} - Future fetchMemberContent({ - int? cursor, - int size = 30, + /// GET /api/content/member?pageSize={pageSize} + Future fetchMemberContent({ + int pageSize = 10, }) async { debugPrint('📤 HomeRemoteDataSource: Fetching member content...'); final response = await _dio.get( ApiEndpoints.memberContent, queryParameters: { - if (cursor != null) 'cursor': cursor, - 'size': size, + 'pageSize': pageSize, }, ); - final result = ContentListResponse.fromJson( + final result = MemberContentPageResponse.fromJson( response.data as Map, ); - debugPrint('✅ Member content fetched: ${result.content.length} items'); + debugPrint( + '✅ Member content fetched: ${result.contentPage.content.length} items', + ); return result; } } diff --git a/lib/features/home/data/home_repository.dart b/lib/features/home/data/home_repository.dart index 6ee3165..cc56006 100644 --- a/lib/features/home/data/home_repository.dart +++ b/lib/features/home/data/home_repository.dart @@ -3,8 +3,8 @@ import 'models/content_response.dart'; /// 홈 화면 Repository 인터페이스 abstract class HomeRepository { /// 최근 콘텐츠 목록 조회 - Future getRecentContent({int? cursor, int size = 20}); + Future getRecentContent(); - /// 회원 콘텐츠 목록 조회 (인기 장소) - Future getMemberContent({int? cursor, int size = 30}); + /// 회원 콘텐츠 목록 조회 + Future getMemberContent({int pageSize = 10}); } diff --git a/lib/features/home/data/home_repository_impl.dart b/lib/features/home/data/home_repository_impl.dart index 5ee6340..43d889e 100644 --- a/lib/features/home/data/home_repository_impl.dart +++ b/lib/features/home/data/home_repository_impl.dart @@ -22,26 +22,16 @@ class HomeRepositoryImpl implements HomeRepository { HomeRepositoryImpl(this._remoteDataSource); @override - Future getRecentContent({ - int? cursor, - int size = 20, - }) async { + Future getRecentContent() async { debugPrint('📝 HomeRepository: Getting recent content...'); - return await _remoteDataSource.fetchRecentContent( - cursor: cursor, - size: size, - ); + return await _remoteDataSource.fetchRecentContent(); } @override - Future getMemberContent({ - int? cursor, - int size = 30, + Future getMemberContent({ + int pageSize = 10, }) async { debugPrint('📝 HomeRepository: Getting member content...'); - return await _remoteDataSource.fetchMemberContent( - cursor: cursor, - size: size, - ); + return await _remoteDataSource.fetchMemberContent(pageSize: pageSize); } } diff --git a/lib/features/home/data/models/content_response.dart b/lib/features/home/data/models/content_response.dart index 961a185..1eec801 100644 --- a/lib/features/home/data/models/content_response.dart +++ b/lib/features/home/data/models/content_response.dart @@ -1,46 +1,84 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'cursor_model.dart'; -import '../../../../common/models/place_model.dart'; - part 'content_response.freezed.dart'; part 'content_response.g.dart'; -/// 콘텐츠 아이템 (API 응답 단위) +/// 콘텐츠 아이템 - 백엔드 ContentDto 매칭 @freezed -class ContentItem with _$ContentItem { - const factory ContentItem({ - /// 콘텐츠 ID - required int contentId, +class ContentItemModel with _$ContentItemModel { + const factory ContentItemModel({ + /// 콘텐츠 ID (UUID) + required String id, - /// 원본 URL - String? sourceUrl, + /// 플랫폼 유형 (INSTAGRAM, YOUTUBE 등) + String? platform, - /// 콘텐츠 상태 + /// 처리 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) String? status, - /// 생성일시 - String? createdAt, + /// 업로더 이름 + String? platformUploader, + + /// 캡션 + String? caption, + + /// 썸네일 URL + String? thumbnailUrl, + + /// 원본 SNS URL + String? originalUrl, - /// 콘텐츠에 포함된 장소 목록 - @Default([]) List places, - }) = _ContentItem; + /// 제목 + String? title, - factory ContentItem.fromJson(Map json) => - _$ContentItemFromJson(json); + /// 요약 설명 + String? summary, + + /// 마지막 확인 시각 + String? lastCheckedAt, + }) = _ContentItemModel; + + factory ContentItemModel.fromJson(Map json) => + _$ContentItemModelFromJson(json); } -/// 콘텐츠 목록 응답 (페이지네이션 포함) +/// 최근 콘텐츠 목록 응답 - 백엔드 GetRecentContentResponse 매칭 @freezed -class ContentListResponse with _$ContentListResponse { - const factory ContentListResponse({ - /// 콘텐츠 아이템 목록 - @Default([]) List content, +class RecentContentResponse with _$RecentContentResponse { + const factory RecentContentResponse({ + @Default([]) List contents, + }) = _RecentContentResponse; - /// 페이지네이션 정보 - CursorModel? cursor, - }) = _ContentListResponse; + factory RecentContentResponse.fromJson(Map json) => + _$RecentContentResponseFromJson(json); +} + +/// 회원 콘텐츠 목록 응답 - 백엔드 GetMemberContentPageResponse 매칭 +/// Spring `Page`를 contentPage 필드로 감싸는 구조 +@freezed +class MemberContentPageResponse with _$MemberContentPageResponse { + const factory MemberContentPageResponse({ + required ContentPage contentPage, + }) = _MemberContentPageResponse; + + factory MemberContentPageResponse.fromJson(Map json) => + _$MemberContentPageResponseFromJson(json); +} + +/// Spring Page 구조 매칭 +@freezed +class ContentPage with _$ContentPage { + const factory ContentPage({ + @Default([]) List content, + @Default(0) int totalElements, + @Default(0) int totalPages, + @Default(0) int size, + @Default(0) int number, + @Default(true) bool first, + @Default(true) bool last, + @Default(true) bool empty, + }) = _ContentPage; - factory ContentListResponse.fromJson(Map json) => - _$ContentListResponseFromJson(json); + factory ContentPage.fromJson(Map json) => + _$ContentPageFromJson(json); } diff --git a/lib/features/home/data/models/content_response.freezed.dart b/lib/features/home/data/models/content_response.freezed.dart index 22965ed..54c613b 100644 --- a/lib/features/home/data/models/content_response.freezed.dart +++ b/lib/features/home/data/models/content_response.freezed.dart @@ -15,96 +15,141 @@ final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', ); -ContentItem _$ContentItemFromJson(Map json) { - return _ContentItem.fromJson(json); +ContentItemModel _$ContentItemModelFromJson(Map json) { + return _ContentItemModel.fromJson(json); } /// @nodoc -mixin _$ContentItem { - /// 콘텐츠 ID - int get contentId => throw _privateConstructorUsedError; +mixin _$ContentItemModel { + /// 콘텐츠 ID (UUID) + String get id => throw _privateConstructorUsedError; - /// 원본 URL - String? get sourceUrl => throw _privateConstructorUsedError; + /// 플랫폼 유형 (INSTAGRAM, YOUTUBE 등) + String? get platform => throw _privateConstructorUsedError; - /// 콘텐츠 상태 + /// 처리 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) String? get status => throw _privateConstructorUsedError; - /// 생성일시 - String? get createdAt => throw _privateConstructorUsedError; + /// 업로더 이름 + String? get platformUploader => throw _privateConstructorUsedError; - /// 콘텐츠에 포함된 장소 목록 - List get places => throw _privateConstructorUsedError; + /// 캡션 + String? get caption => throw _privateConstructorUsedError; - /// Serializes this ContentItem to a JSON map. + /// 썸네일 URL + String? get thumbnailUrl => throw _privateConstructorUsedError; + + /// 원본 SNS URL + String? get originalUrl => throw _privateConstructorUsedError; + + /// 제목 + String? get title => throw _privateConstructorUsedError; + + /// 요약 설명 + String? get summary => throw _privateConstructorUsedError; + + /// 마지막 확인 시각 + String? get lastCheckedAt => throw _privateConstructorUsedError; + + /// Serializes this ContentItemModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; - /// Create a copy of ContentItem + /// Create a copy of ContentItemModel /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) - $ContentItemCopyWith get copyWith => + $ContentItemModelCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class $ContentItemCopyWith<$Res> { - factory $ContentItemCopyWith( - ContentItem value, - $Res Function(ContentItem) then, - ) = _$ContentItemCopyWithImpl<$Res, ContentItem>; +abstract class $ContentItemModelCopyWith<$Res> { + factory $ContentItemModelCopyWith( + ContentItemModel value, + $Res Function(ContentItemModel) then, + ) = _$ContentItemModelCopyWithImpl<$Res, ContentItemModel>; @useResult $Res call({ - int contentId, - String? sourceUrl, + String id, + String? platform, String? status, - String? createdAt, - List places, + String? platformUploader, + String? caption, + String? thumbnailUrl, + String? originalUrl, + String? title, + String? summary, + String? lastCheckedAt, }); } /// @nodoc -class _$ContentItemCopyWithImpl<$Res, $Val extends ContentItem> - implements $ContentItemCopyWith<$Res> { - _$ContentItemCopyWithImpl(this._value, this._then); +class _$ContentItemModelCopyWithImpl<$Res, $Val extends ContentItemModel> + implements $ContentItemModelCopyWith<$Res> { + _$ContentItemModelCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of ContentItem + /// Create a copy of ContentItemModel /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ - Object? contentId = null, - Object? sourceUrl = freezed, + Object? id = null, + Object? platform = freezed, Object? status = freezed, - Object? createdAt = freezed, - Object? places = null, + Object? platformUploader = freezed, + Object? caption = freezed, + Object? thumbnailUrl = freezed, + Object? originalUrl = freezed, + Object? title = freezed, + Object? summary = freezed, + Object? lastCheckedAt = freezed, }) { return _then( _value.copyWith( - contentId: null == contentId - ? _value.contentId - : contentId // ignore: cast_nullable_to_non_nullable - as int, - sourceUrl: freezed == sourceUrl - ? _value.sourceUrl - : sourceUrl // ignore: cast_nullable_to_non_nullable + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + platform: freezed == platform + ? _value.platform + : platform // ignore: cast_nullable_to_non_nullable as String?, status: freezed == status ? _value.status : status // ignore: cast_nullable_to_non_nullable as String?, - createdAt: freezed == createdAt - ? _value.createdAt - : createdAt // ignore: cast_nullable_to_non_nullable + platformUploader: freezed == platformUploader + ? _value.platformUploader + : platformUploader // ignore: cast_nullable_to_non_nullable + as String?, + caption: freezed == caption + ? _value.caption + : caption // ignore: cast_nullable_to_non_nullable + as String?, + thumbnailUrl: freezed == thumbnailUrl + ? _value.thumbnailUrl + : thumbnailUrl // ignore: cast_nullable_to_non_nullable + as String?, + originalUrl: freezed == originalUrl + ? _value.originalUrl + : originalUrl // ignore: cast_nullable_to_non_nullable + as String?, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + summary: freezed == summary + ? _value.summary + : summary // ignore: cast_nullable_to_non_nullable + as String?, + lastCheckedAt: freezed == lastCheckedAt + ? _value.lastCheckedAt + : lastCheckedAt // ignore: cast_nullable_to_non_nullable as String?, - places: null == places - ? _value.places - : places // ignore: cast_nullable_to_non_nullable - as List, ) as $Val, ); @@ -112,65 +157,95 @@ class _$ContentItemCopyWithImpl<$Res, $Val extends ContentItem> } /// @nodoc -abstract class _$$ContentItemImplCopyWith<$Res> - implements $ContentItemCopyWith<$Res> { - factory _$$ContentItemImplCopyWith( - _$ContentItemImpl value, - $Res Function(_$ContentItemImpl) then, - ) = __$$ContentItemImplCopyWithImpl<$Res>; +abstract class _$$ContentItemModelImplCopyWith<$Res> + implements $ContentItemModelCopyWith<$Res> { + factory _$$ContentItemModelImplCopyWith( + _$ContentItemModelImpl value, + $Res Function(_$ContentItemModelImpl) then, + ) = __$$ContentItemModelImplCopyWithImpl<$Res>; @override @useResult $Res call({ - int contentId, - String? sourceUrl, + String id, + String? platform, String? status, - String? createdAt, - List places, + String? platformUploader, + String? caption, + String? thumbnailUrl, + String? originalUrl, + String? title, + String? summary, + String? lastCheckedAt, }); } /// @nodoc -class __$$ContentItemImplCopyWithImpl<$Res> - extends _$ContentItemCopyWithImpl<$Res, _$ContentItemImpl> - implements _$$ContentItemImplCopyWith<$Res> { - __$$ContentItemImplCopyWithImpl( - _$ContentItemImpl _value, - $Res Function(_$ContentItemImpl) _then, +class __$$ContentItemModelImplCopyWithImpl<$Res> + extends _$ContentItemModelCopyWithImpl<$Res, _$ContentItemModelImpl> + implements _$$ContentItemModelImplCopyWith<$Res> { + __$$ContentItemModelImplCopyWithImpl( + _$ContentItemModelImpl _value, + $Res Function(_$ContentItemModelImpl) _then, ) : super(_value, _then); - /// Create a copy of ContentItem + /// Create a copy of ContentItemModel /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ - Object? contentId = null, - Object? sourceUrl = freezed, + Object? id = null, + Object? platform = freezed, Object? status = freezed, - Object? createdAt = freezed, - Object? places = null, + Object? platformUploader = freezed, + Object? caption = freezed, + Object? thumbnailUrl = freezed, + Object? originalUrl = freezed, + Object? title = freezed, + Object? summary = freezed, + Object? lastCheckedAt = freezed, }) { return _then( - _$ContentItemImpl( - contentId: null == contentId - ? _value.contentId - : contentId // ignore: cast_nullable_to_non_nullable - as int, - sourceUrl: freezed == sourceUrl - ? _value.sourceUrl - : sourceUrl // ignore: cast_nullable_to_non_nullable + _$ContentItemModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + platform: freezed == platform + ? _value.platform + : platform // ignore: cast_nullable_to_non_nullable as String?, status: freezed == status ? _value.status : status // ignore: cast_nullable_to_non_nullable as String?, - createdAt: freezed == createdAt - ? _value.createdAt - : createdAt // ignore: cast_nullable_to_non_nullable + platformUploader: freezed == platformUploader + ? _value.platformUploader + : platformUploader // ignore: cast_nullable_to_non_nullable + as String?, + caption: freezed == caption + ? _value.caption + : caption // ignore: cast_nullable_to_non_nullable + as String?, + thumbnailUrl: freezed == thumbnailUrl + ? _value.thumbnailUrl + : thumbnailUrl // ignore: cast_nullable_to_non_nullable + as String?, + originalUrl: freezed == originalUrl + ? _value.originalUrl + : originalUrl // ignore: cast_nullable_to_non_nullable + as String?, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + summary: freezed == summary + ? _value.summary + : summary // ignore: cast_nullable_to_non_nullable + as String?, + lastCheckedAt: freezed == lastCheckedAt + ? _value.lastCheckedAt + : lastCheckedAt // ignore: cast_nullable_to_non_nullable as String?, - places: null == places - ? _value._places - : places // ignore: cast_nullable_to_non_nullable - as List, ), ); } @@ -178,249 +253,727 @@ class __$$ContentItemImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$ContentItemImpl implements _ContentItem { - const _$ContentItemImpl({ - required this.contentId, - this.sourceUrl, +class _$ContentItemModelImpl implements _ContentItemModel { + const _$ContentItemModelImpl({ + required this.id, + this.platform, this.status, - this.createdAt, - final List places = const [], - }) : _places = places; + this.platformUploader, + this.caption, + this.thumbnailUrl, + this.originalUrl, + this.title, + this.summary, + this.lastCheckedAt, + }); - factory _$ContentItemImpl.fromJson(Map json) => - _$$ContentItemImplFromJson(json); + factory _$ContentItemModelImpl.fromJson(Map json) => + _$$ContentItemModelImplFromJson(json); - /// 콘텐츠 ID + /// 콘텐츠 ID (UUID) @override - final int contentId; + final String id; - /// 원본 URL + /// 플랫폼 유형 (INSTAGRAM, YOUTUBE 등) @override - final String? sourceUrl; + final String? platform; - /// 콘텐츠 상태 + /// 처리 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) @override final String? status; - /// 생성일시 + /// 업로더 이름 @override - final String? createdAt; + final String? platformUploader; - /// 콘텐츠에 포함된 장소 목록 - final List _places; + /// 캡션 + @override + final String? caption; - /// 콘텐츠에 포함된 장소 목록 + /// 썸네일 URL @override - @JsonKey() - List get places { - if (_places is EqualUnmodifiableListView) return _places; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_places); - } + final String? thumbnailUrl; + + /// 원본 SNS URL + @override + final String? originalUrl; + + /// 제목 + @override + final String? title; + + /// 요약 설명 + @override + final String? summary; + + /// 마지막 확인 시각 + @override + final String? lastCheckedAt; @override String toString() { - return 'ContentItem(contentId: $contentId, sourceUrl: $sourceUrl, status: $status, createdAt: $createdAt, places: $places)'; + return 'ContentItemModel(id: $id, platform: $platform, status: $status, platformUploader: $platformUploader, caption: $caption, thumbnailUrl: $thumbnailUrl, originalUrl: $originalUrl, title: $title, summary: $summary, lastCheckedAt: $lastCheckedAt)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$ContentItemImpl && - (identical(other.contentId, contentId) || - other.contentId == contentId) && - (identical(other.sourceUrl, sourceUrl) || - other.sourceUrl == sourceUrl) && + other is _$ContentItemModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.platform, platform) || + other.platform == platform) && (identical(other.status, status) || other.status == status) && - (identical(other.createdAt, createdAt) || - other.createdAt == createdAt) && - const DeepCollectionEquality().equals(other._places, _places)); + (identical(other.platformUploader, platformUploader) || + other.platformUploader == platformUploader) && + (identical(other.caption, caption) || other.caption == caption) && + (identical(other.thumbnailUrl, thumbnailUrl) || + other.thumbnailUrl == thumbnailUrl) && + (identical(other.originalUrl, originalUrl) || + other.originalUrl == originalUrl) && + (identical(other.title, title) || other.title == title) && + (identical(other.summary, summary) || other.summary == summary) && + (identical(other.lastCheckedAt, lastCheckedAt) || + other.lastCheckedAt == lastCheckedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, - contentId, - sourceUrl, + id, + platform, status, - createdAt, - const DeepCollectionEquality().hash(_places), + platformUploader, + caption, + thumbnailUrl, + originalUrl, + title, + summary, + lastCheckedAt, ); - /// Create a copy of ContentItem + /// Create a copy of ContentItemModel /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$ContentItemImplCopyWith<_$ContentItemImpl> get copyWith => - __$$ContentItemImplCopyWithImpl<_$ContentItemImpl>(this, _$identity); + _$$ContentItemModelImplCopyWith<_$ContentItemModelImpl> get copyWith => + __$$ContentItemModelImplCopyWithImpl<_$ContentItemModelImpl>( + this, + _$identity, + ); @override Map toJson() { - return _$$ContentItemImplToJson(this); + return _$$ContentItemModelImplToJson(this); } } -abstract class _ContentItem implements ContentItem { - const factory _ContentItem({ - required final int contentId, - final String? sourceUrl, +abstract class _ContentItemModel implements ContentItemModel { + const factory _ContentItemModel({ + required final String id, + final String? platform, final String? status, - final String? createdAt, - final List places, - }) = _$ContentItemImpl; + final String? platformUploader, + final String? caption, + final String? thumbnailUrl, + final String? originalUrl, + final String? title, + final String? summary, + final String? lastCheckedAt, + }) = _$ContentItemModelImpl; + + factory _ContentItemModel.fromJson(Map json) = + _$ContentItemModelImpl.fromJson; + + /// 콘텐츠 ID (UUID) + @override + String get id; - factory _ContentItem.fromJson(Map json) = - _$ContentItemImpl.fromJson; + /// 플랫폼 유형 (INSTAGRAM, YOUTUBE 등) + @override + String? get platform; - /// 콘텐츠 ID + /// 처리 상태 (PENDING, ANALYZING, COMPLETED, FAILED, DELETED) @override - int get contentId; + String? get status; - /// 원본 URL + /// 업로더 이름 @override - String? get sourceUrl; + String? get platformUploader; - /// 콘텐츠 상태 + /// 캡션 @override - String? get status; + String? get caption; - /// 생성일시 + /// 썸네일 URL @override - String? get createdAt; + String? get thumbnailUrl; - /// 콘텐츠에 포함된 장소 목록 + /// 원본 SNS URL @override - List get places; + String? get originalUrl; - /// Create a copy of ContentItem + /// 제목 + @override + String? get title; + + /// 요약 설명 + @override + String? get summary; + + /// 마지막 확인 시각 + @override + String? get lastCheckedAt; + + /// Create a copy of ContentItemModel /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$ContentItemImplCopyWith<_$ContentItemImpl> get copyWith => + _$$ContentItemModelImplCopyWith<_$ContentItemModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +RecentContentResponse _$RecentContentResponseFromJson( + Map json, +) { + return _RecentContentResponse.fromJson(json); +} + +/// @nodoc +mixin _$RecentContentResponse { + List get contents => throw _privateConstructorUsedError; + + /// Serializes this RecentContentResponse to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of RecentContentResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RecentContentResponseCopyWith get copyWith => throw _privateConstructorUsedError; } -ContentListResponse _$ContentListResponseFromJson(Map json) { - return _ContentListResponse.fromJson(json); +/// @nodoc +abstract class $RecentContentResponseCopyWith<$Res> { + factory $RecentContentResponseCopyWith( + RecentContentResponse value, + $Res Function(RecentContentResponse) then, + ) = _$RecentContentResponseCopyWithImpl<$Res, RecentContentResponse>; + @useResult + $Res call({List contents}); } /// @nodoc -mixin _$ContentListResponse { - /// 콘텐츠 아이템 목록 - List get content => throw _privateConstructorUsedError; +class _$RecentContentResponseCopyWithImpl< + $Res, + $Val extends RecentContentResponse +> + implements $RecentContentResponseCopyWith<$Res> { + _$RecentContentResponseCopyWithImpl(this._value, this._then); - /// 페이지네이션 정보 - CursorModel? get cursor => throw _privateConstructorUsedError; + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; - /// Serializes this ContentListResponse to a JSON map. + /// Create a copy of RecentContentResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? contents = null}) { + return _then( + _value.copyWith( + contents: null == contents + ? _value.contents + : contents // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$RecentContentResponseImplCopyWith<$Res> + implements $RecentContentResponseCopyWith<$Res> { + factory _$$RecentContentResponseImplCopyWith( + _$RecentContentResponseImpl value, + $Res Function(_$RecentContentResponseImpl) then, + ) = __$$RecentContentResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List contents}); +} + +/// @nodoc +class __$$RecentContentResponseImplCopyWithImpl<$Res> + extends + _$RecentContentResponseCopyWithImpl<$Res, _$RecentContentResponseImpl> + implements _$$RecentContentResponseImplCopyWith<$Res> { + __$$RecentContentResponseImplCopyWithImpl( + _$RecentContentResponseImpl _value, + $Res Function(_$RecentContentResponseImpl) _then, + ) : super(_value, _then); + + /// Create a copy of RecentContentResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? contents = null}) { + return _then( + _$RecentContentResponseImpl( + contents: null == contents + ? _value._contents + : contents // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$RecentContentResponseImpl implements _RecentContentResponse { + const _$RecentContentResponseImpl({ + final List contents = const [], + }) : _contents = contents; + + factory _$RecentContentResponseImpl.fromJson(Map json) => + _$$RecentContentResponseImplFromJson(json); + + final List _contents; + @override + @JsonKey() + List get contents { + if (_contents is EqualUnmodifiableListView) return _contents; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_contents); + } + + @override + String toString() { + return 'RecentContentResponse(contents: $contents)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RecentContentResponseImpl && + const DeepCollectionEquality().equals(other._contents, _contents)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(_contents)); + + /// Create a copy of RecentContentResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RecentContentResponseImplCopyWith<_$RecentContentResponseImpl> + get copyWith => + __$$RecentContentResponseImplCopyWithImpl<_$RecentContentResponseImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$RecentContentResponseImplToJson(this); + } +} + +abstract class _RecentContentResponse implements RecentContentResponse { + const factory _RecentContentResponse({ + final List contents, + }) = _$RecentContentResponseImpl; + + factory _RecentContentResponse.fromJson(Map json) = + _$RecentContentResponseImpl.fromJson; + + @override + List get contents; + + /// Create a copy of RecentContentResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RecentContentResponseImplCopyWith<_$RecentContentResponseImpl> + get copyWith => throw _privateConstructorUsedError; +} + +MemberContentPageResponse _$MemberContentPageResponseFromJson( + Map json, +) { + return _MemberContentPageResponse.fromJson(json); +} + +/// @nodoc +mixin _$MemberContentPageResponse { + ContentPage get contentPage => throw _privateConstructorUsedError; + + /// Serializes this MemberContentPageResponse to a JSON map. Map toJson() => throw _privateConstructorUsedError; - /// Create a copy of ContentListResponse + /// Create a copy of MemberContentPageResponse /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) - $ContentListResponseCopyWith get copyWith => + $MemberContentPageResponseCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class $ContentListResponseCopyWith<$Res> { - factory $ContentListResponseCopyWith( - ContentListResponse value, - $Res Function(ContentListResponse) then, - ) = _$ContentListResponseCopyWithImpl<$Res, ContentListResponse>; +abstract class $MemberContentPageResponseCopyWith<$Res> { + factory $MemberContentPageResponseCopyWith( + MemberContentPageResponse value, + $Res Function(MemberContentPageResponse) then, + ) = _$MemberContentPageResponseCopyWithImpl<$Res, MemberContentPageResponse>; @useResult - $Res call({List content, CursorModel? cursor}); + $Res call({ContentPage contentPage}); - $CursorModelCopyWith<$Res>? get cursor; + $ContentPageCopyWith<$Res> get contentPage; } /// @nodoc -class _$ContentListResponseCopyWithImpl<$Res, $Val extends ContentListResponse> - implements $ContentListResponseCopyWith<$Res> { - _$ContentListResponseCopyWithImpl(this._value, this._then); +class _$MemberContentPageResponseCopyWithImpl< + $Res, + $Val extends MemberContentPageResponse +> + implements $MemberContentPageResponseCopyWith<$Res> { + _$MemberContentPageResponseCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of ContentListResponse + /// Create a copy of MemberContentPageResponse /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({Object? content = null, Object? cursor = freezed}) { + $Res call({Object? contentPage = null}) { return _then( _value.copyWith( - content: null == content - ? _value.content - : content // ignore: cast_nullable_to_non_nullable - as List, - cursor: freezed == cursor - ? _value.cursor - : cursor // ignore: cast_nullable_to_non_nullable - as CursorModel?, + contentPage: null == contentPage + ? _value.contentPage + : contentPage // ignore: cast_nullable_to_non_nullable + as ContentPage, ) as $Val, ); } - /// Create a copy of ContentListResponse + /// Create a copy of MemberContentPageResponse /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') - $CursorModelCopyWith<$Res>? get cursor { - if (_value.cursor == null) { - return null; - } - - return $CursorModelCopyWith<$Res>(_value.cursor!, (value) { - return _then(_value.copyWith(cursor: value) as $Val); + $ContentPageCopyWith<$Res> get contentPage { + return $ContentPageCopyWith<$Res>(_value.contentPage, (value) { + return _then(_value.copyWith(contentPage: value) as $Val); }); } } /// @nodoc -abstract class _$$ContentListResponseImplCopyWith<$Res> - implements $ContentListResponseCopyWith<$Res> { - factory _$$ContentListResponseImplCopyWith( - _$ContentListResponseImpl value, - $Res Function(_$ContentListResponseImpl) then, - ) = __$$ContentListResponseImplCopyWithImpl<$Res>; +abstract class _$$MemberContentPageResponseImplCopyWith<$Res> + implements $MemberContentPageResponseCopyWith<$Res> { + factory _$$MemberContentPageResponseImplCopyWith( + _$MemberContentPageResponseImpl value, + $Res Function(_$MemberContentPageResponseImpl) then, + ) = __$$MemberContentPageResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ContentPage contentPage}); + + @override + $ContentPageCopyWith<$Res> get contentPage; +} + +/// @nodoc +class __$$MemberContentPageResponseImplCopyWithImpl<$Res> + extends + _$MemberContentPageResponseCopyWithImpl< + $Res, + _$MemberContentPageResponseImpl + > + implements _$$MemberContentPageResponseImplCopyWith<$Res> { + __$$MemberContentPageResponseImplCopyWithImpl( + _$MemberContentPageResponseImpl _value, + $Res Function(_$MemberContentPageResponseImpl) _then, + ) : super(_value, _then); + + /// Create a copy of MemberContentPageResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? contentPage = null}) { + return _then( + _$MemberContentPageResponseImpl( + contentPage: null == contentPage + ? _value.contentPage + : contentPage // ignore: cast_nullable_to_non_nullable + as ContentPage, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$MemberContentPageResponseImpl implements _MemberContentPageResponse { + const _$MemberContentPageResponseImpl({required this.contentPage}); + + factory _$MemberContentPageResponseImpl.fromJson(Map json) => + _$$MemberContentPageResponseImplFromJson(json); + + @override + final ContentPage contentPage; + + @override + String toString() { + return 'MemberContentPageResponse(contentPage: $contentPage)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MemberContentPageResponseImpl && + (identical(other.contentPage, contentPage) || + other.contentPage == contentPage)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, contentPage); + + /// Create a copy of MemberContentPageResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$MemberContentPageResponseImplCopyWith<_$MemberContentPageResponseImpl> + get copyWith => + __$$MemberContentPageResponseImplCopyWithImpl< + _$MemberContentPageResponseImpl + >(this, _$identity); + + @override + Map toJson() { + return _$$MemberContentPageResponseImplToJson(this); + } +} + +abstract class _MemberContentPageResponse implements MemberContentPageResponse { + const factory _MemberContentPageResponse({ + required final ContentPage contentPage, + }) = _$MemberContentPageResponseImpl; + + factory _MemberContentPageResponse.fromJson(Map json) = + _$MemberContentPageResponseImpl.fromJson; + @override + ContentPage get contentPage; + + /// Create a copy of MemberContentPageResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$MemberContentPageResponseImplCopyWith<_$MemberContentPageResponseImpl> + get copyWith => throw _privateConstructorUsedError; +} + +ContentPage _$ContentPageFromJson(Map json) { + return _ContentPage.fromJson(json); +} + +/// @nodoc +mixin _$ContentPage { + List get content => throw _privateConstructorUsedError; + int get totalElements => throw _privateConstructorUsedError; + int get totalPages => throw _privateConstructorUsedError; + int get size => throw _privateConstructorUsedError; + int get number => throw _privateConstructorUsedError; + bool get first => throw _privateConstructorUsedError; + bool get last => throw _privateConstructorUsedError; + bool get empty => throw _privateConstructorUsedError; + + /// Serializes this ContentPage to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ContentPage + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ContentPageCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ContentPageCopyWith<$Res> { + factory $ContentPageCopyWith( + ContentPage value, + $Res Function(ContentPage) then, + ) = _$ContentPageCopyWithImpl<$Res, ContentPage>; @useResult - $Res call({List content, CursorModel? cursor}); + $Res call({ + List content, + int totalElements, + int totalPages, + int size, + int number, + bool first, + bool last, + bool empty, + }); +} + +/// @nodoc +class _$ContentPageCopyWithImpl<$Res, $Val extends ContentPage> + implements $ContentPageCopyWith<$Res> { + _$ContentPageCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ContentPage + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? content = null, + Object? totalElements = null, + Object? totalPages = null, + Object? size = null, + Object? number = null, + Object? first = null, + Object? last = null, + Object? empty = null, + }) { + return _then( + _value.copyWith( + content: null == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as List, + totalElements: null == totalElements + ? _value.totalElements + : totalElements // ignore: cast_nullable_to_non_nullable + as int, + totalPages: null == totalPages + ? _value.totalPages + : totalPages // ignore: cast_nullable_to_non_nullable + as int, + size: null == size + ? _value.size + : size // ignore: cast_nullable_to_non_nullable + as int, + number: null == number + ? _value.number + : number // ignore: cast_nullable_to_non_nullable + as int, + first: null == first + ? _value.first + : first // ignore: cast_nullable_to_non_nullable + as bool, + last: null == last + ? _value.last + : last // ignore: cast_nullable_to_non_nullable + as bool, + empty: null == empty + ? _value.empty + : empty // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} +/// @nodoc +abstract class _$$ContentPageImplCopyWith<$Res> + implements $ContentPageCopyWith<$Res> { + factory _$$ContentPageImplCopyWith( + _$ContentPageImpl value, + $Res Function(_$ContentPageImpl) then, + ) = __$$ContentPageImplCopyWithImpl<$Res>; @override - $CursorModelCopyWith<$Res>? get cursor; + @useResult + $Res call({ + List content, + int totalElements, + int totalPages, + int size, + int number, + bool first, + bool last, + bool empty, + }); } /// @nodoc -class __$$ContentListResponseImplCopyWithImpl<$Res> - extends _$ContentListResponseCopyWithImpl<$Res, _$ContentListResponseImpl> - implements _$$ContentListResponseImplCopyWith<$Res> { - __$$ContentListResponseImplCopyWithImpl( - _$ContentListResponseImpl _value, - $Res Function(_$ContentListResponseImpl) _then, +class __$$ContentPageImplCopyWithImpl<$Res> + extends _$ContentPageCopyWithImpl<$Res, _$ContentPageImpl> + implements _$$ContentPageImplCopyWith<$Res> { + __$$ContentPageImplCopyWithImpl( + _$ContentPageImpl _value, + $Res Function(_$ContentPageImpl) _then, ) : super(_value, _then); - /// Create a copy of ContentListResponse + /// Create a copy of ContentPage /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({Object? content = null, Object? cursor = freezed}) { + $Res call({ + Object? content = null, + Object? totalElements = null, + Object? totalPages = null, + Object? size = null, + Object? number = null, + Object? first = null, + Object? last = null, + Object? empty = null, + }) { return _then( - _$ContentListResponseImpl( + _$ContentPageImpl( content: null == content ? _value._content : content // ignore: cast_nullable_to_non_nullable - as List, - cursor: freezed == cursor - ? _value.cursor - : cursor // ignore: cast_nullable_to_non_nullable - as CursorModel?, + as List, + totalElements: null == totalElements + ? _value.totalElements + : totalElements // ignore: cast_nullable_to_non_nullable + as int, + totalPages: null == totalPages + ? _value.totalPages + : totalPages // ignore: cast_nullable_to_non_nullable + as int, + size: null == size + ? _value.size + : size // ignore: cast_nullable_to_non_nullable + as int, + number: null == number + ? _value.number + : number // ignore: cast_nullable_to_non_nullable + as int, + first: null == first + ? _value.first + : first // ignore: cast_nullable_to_non_nullable + as bool, + last: null == last + ? _value.last + : last // ignore: cast_nullable_to_non_nullable + as bool, + empty: null == empty + ? _value.empty + : empty // ignore: cast_nullable_to_non_nullable + as bool, ), ); } @@ -428,43 +981,72 @@ class __$$ContentListResponseImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$ContentListResponseImpl implements _ContentListResponse { - const _$ContentListResponseImpl({ - final List content = const [], - this.cursor, +class _$ContentPageImpl implements _ContentPage { + const _$ContentPageImpl({ + final List content = const [], + this.totalElements = 0, + this.totalPages = 0, + this.size = 0, + this.number = 0, + this.first = true, + this.last = true, + this.empty = true, }) : _content = content; - factory _$ContentListResponseImpl.fromJson(Map json) => - _$$ContentListResponseImplFromJson(json); + factory _$ContentPageImpl.fromJson(Map json) => + _$$ContentPageImplFromJson(json); - /// 콘텐츠 아이템 목록 - final List _content; - - /// 콘텐츠 아이템 목록 + final List _content; @override @JsonKey() - List get content { + List get content { if (_content is EqualUnmodifiableListView) return _content; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_content); } - /// 페이지네이션 정보 @override - final CursorModel? cursor; + @JsonKey() + final int totalElements; + @override + @JsonKey() + final int totalPages; + @override + @JsonKey() + final int size; + @override + @JsonKey() + final int number; + @override + @JsonKey() + final bool first; + @override + @JsonKey() + final bool last; + @override + @JsonKey() + final bool empty; @override String toString() { - return 'ContentListResponse(content: $content, cursor: $cursor)'; + return 'ContentPage(content: $content, totalElements: $totalElements, totalPages: $totalPages, size: $size, number: $number, first: $first, last: $last, empty: $empty)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$ContentListResponseImpl && + other is _$ContentPageImpl && const DeepCollectionEquality().equals(other._content, _content) && - (identical(other.cursor, cursor) || other.cursor == cursor)); + (identical(other.totalElements, totalElements) || + other.totalElements == totalElements) && + (identical(other.totalPages, totalPages) || + other.totalPages == totalPages) && + (identical(other.size, size) || other.size == size) && + (identical(other.number, number) || other.number == number) && + (identical(other.first, first) || other.first == first) && + (identical(other.last, last) || other.last == last) && + (identical(other.empty, empty) || other.empty == empty)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -472,47 +1054,65 @@ class _$ContentListResponseImpl implements _ContentListResponse { int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(_content), - cursor, + totalElements, + totalPages, + size, + number, + first, + last, + empty, ); - /// Create a copy of ContentListResponse + /// Create a copy of ContentPage /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$ContentListResponseImplCopyWith<_$ContentListResponseImpl> get copyWith => - __$$ContentListResponseImplCopyWithImpl<_$ContentListResponseImpl>( - this, - _$identity, - ); + _$$ContentPageImplCopyWith<_$ContentPageImpl> get copyWith => + __$$ContentPageImplCopyWithImpl<_$ContentPageImpl>(this, _$identity); @override Map toJson() { - return _$$ContentListResponseImplToJson(this); + return _$$ContentPageImplToJson(this); } } -abstract class _ContentListResponse implements ContentListResponse { - const factory _ContentListResponse({ - final List content, - final CursorModel? cursor, - }) = _$ContentListResponseImpl; +abstract class _ContentPage implements ContentPage { + const factory _ContentPage({ + final List content, + final int totalElements, + final int totalPages, + final int size, + final int number, + final bool first, + final bool last, + final bool empty, + }) = _$ContentPageImpl; + + factory _ContentPage.fromJson(Map json) = + _$ContentPageImpl.fromJson; - factory _ContentListResponse.fromJson(Map json) = - _$ContentListResponseImpl.fromJson; - - /// 콘텐츠 아이템 목록 @override - List get content; - - /// 페이지네이션 정보 + List get content; + @override + int get totalElements; + @override + int get totalPages; + @override + int get size; + @override + int get number; + @override + bool get first; + @override + bool get last; @override - CursorModel? get cursor; + bool get empty; - /// Create a copy of ContentListResponse + /// Create a copy of ContentPage /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$ContentListResponseImplCopyWith<_$ContentListResponseImpl> get copyWith => + _$$ContentPageImplCopyWith<_$ContentPageImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/features/home/data/models/content_response.g.dart b/lib/features/home/data/models/content_response.g.dart index d3287db..79012f5 100644 --- a/lib/features/home/data/models/content_response.g.dart +++ b/lib/features/home/data/models/content_response.g.dart @@ -6,41 +6,86 @@ part of 'content_response.dart'; // JsonSerializableGenerator // ************************************************************************** -_$ContentItemImpl _$$ContentItemImplFromJson(Map json) => - _$ContentItemImpl( - contentId: (json['contentId'] as num).toInt(), - sourceUrl: json['sourceUrl'] as String?, - status: json['status'] as String?, - createdAt: json['createdAt'] as String?, - places: - (json['places'] as List?) - ?.map((e) => PlaceModel.fromJson(e as Map)) - .toList() ?? - const [], - ); +_$ContentItemModelImpl _$$ContentItemModelImplFromJson( + Map json, +) => _$ContentItemModelImpl( + id: json['id'] as String, + platform: json['platform'] as String?, + status: json['status'] as String?, + platformUploader: json['platformUploader'] as String?, + caption: json['caption'] as String?, + thumbnailUrl: json['thumbnailUrl'] as String?, + originalUrl: json['originalUrl'] as String?, + title: json['title'] as String?, + summary: json['summary'] as String?, + lastCheckedAt: json['lastCheckedAt'] as String?, +); -Map _$$ContentItemImplToJson(_$ContentItemImpl instance) => - { - 'contentId': instance.contentId, - 'sourceUrl': instance.sourceUrl, - 'status': instance.status, - 'createdAt': instance.createdAt, - 'places': instance.places, - }; +Map _$$ContentItemModelImplToJson( + _$ContentItemModelImpl instance, +) => { + 'id': instance.id, + 'platform': instance.platform, + 'status': instance.status, + 'platformUploader': instance.platformUploader, + 'caption': instance.caption, + 'thumbnailUrl': instance.thumbnailUrl, + 'originalUrl': instance.originalUrl, + 'title': instance.title, + 'summary': instance.summary, + 'lastCheckedAt': instance.lastCheckedAt, +}; -_$ContentListResponseImpl _$$ContentListResponseImplFromJson( +_$RecentContentResponseImpl _$$RecentContentResponseImplFromJson( Map json, -) => _$ContentListResponseImpl( - content: - (json['content'] as List?) - ?.map((e) => ContentItem.fromJson(e as Map)) +) => _$RecentContentResponseImpl( + contents: + (json['contents'] as List?) + ?.map((e) => ContentItemModel.fromJson(e as Map)) .toList() ?? const [], - cursor: json['cursor'] == null - ? null - : CursorModel.fromJson(json['cursor'] as Map), ); -Map _$$ContentListResponseImplToJson( - _$ContentListResponseImpl instance, -) => {'content': instance.content, 'cursor': instance.cursor}; +Map _$$RecentContentResponseImplToJson( + _$RecentContentResponseImpl instance, +) => {'contents': instance.contents}; + +_$MemberContentPageResponseImpl _$$MemberContentPageResponseImplFromJson( + Map json, +) => _$MemberContentPageResponseImpl( + contentPage: ContentPage.fromJson( + json['contentPage'] as Map, + ), +); + +Map _$$MemberContentPageResponseImplToJson( + _$MemberContentPageResponseImpl instance, +) => {'contentPage': instance.contentPage}; + +_$ContentPageImpl _$$ContentPageImplFromJson(Map json) => + _$ContentPageImpl( + content: + (json['content'] as List?) + ?.map((e) => ContentItemModel.fromJson(e as Map)) + .toList() ?? + const [], + totalElements: (json['totalElements'] as num?)?.toInt() ?? 0, + totalPages: (json['totalPages'] as num?)?.toInt() ?? 0, + size: (json['size'] as num?)?.toInt() ?? 0, + number: (json['number'] as num?)?.toInt() ?? 0, + first: json['first'] as bool? ?? true, + last: json['last'] as bool? ?? true, + empty: json['empty'] as bool? ?? true, + ); + +Map _$$ContentPageImplToJson(_$ContentPageImpl instance) => + { + 'content': instance.content, + 'totalElements': instance.totalElements, + 'totalPages': instance.totalPages, + 'size': instance.size, + 'number': instance.number, + 'first': instance.first, + 'last': instance.last, + 'empty': instance.empty, + }; diff --git a/lib/features/home/data/models/cursor_model.dart b/lib/features/home/data/models/cursor_model.dart index afd0025..f2161ad 100644 --- a/lib/features/home/data/models/cursor_model.dart +++ b/lib/features/home/data/models/cursor_model.dart @@ -3,17 +3,12 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'cursor_model.freezed.dart'; part 'cursor_model.g.dart'; -/// 커서 기반 페이지네이션 모델 +/// 커서 기반 페이지네이션 모델 (예비용) @freezed class CursorModel with _$CursorModel { const factory CursorModel({ - /// 다음 페이지 존재 여부 @Default(false) bool hasNext, - - /// 다음 커서 값 (다음 페이지 요청 시 사용) int? nextCursor, - - /// 현재 페이지 아이템 수 @Default(0) int size, }) = _CursorModel; diff --git a/lib/features/home/data/models/cursor_model.freezed.dart b/lib/features/home/data/models/cursor_model.freezed.dart index 3022ec5..5751c74 100644 --- a/lib/features/home/data/models/cursor_model.freezed.dart +++ b/lib/features/home/data/models/cursor_model.freezed.dart @@ -21,13 +21,8 @@ CursorModel _$CursorModelFromJson(Map json) { /// @nodoc mixin _$CursorModel { - /// 다음 페이지 존재 여부 bool get hasNext => throw _privateConstructorUsedError; - - /// 다음 커서 값 (다음 페이지 요청 시 사용) int? get nextCursor => throw _privateConstructorUsedError; - - /// 현재 페이지 아이템 수 int get size => throw _privateConstructorUsedError; /// Serializes this CursorModel to a JSON map. @@ -150,16 +145,11 @@ class _$CursorModelImpl implements _CursorModel { factory _$CursorModelImpl.fromJson(Map json) => _$$CursorModelImplFromJson(json); - /// 다음 페이지 존재 여부 @override @JsonKey() final bool hasNext; - - /// 다음 커서 값 (다음 페이지 요청 시 사용) @override final int? nextCursor; - - /// 현재 페이지 아이템 수 @override @JsonKey() final int size; @@ -208,15 +198,10 @@ abstract class _CursorModel implements CursorModel { factory _CursorModel.fromJson(Map json) = _$CursorModelImpl.fromJson; - /// 다음 페이지 존재 여부 @override bool get hasNext; - - /// 다음 커서 값 (다음 페이지 요청 시 사용) @override int? get nextCursor; - - /// 현재 페이지 아이템 수 @override int get size; diff --git a/lib/features/home/presentation/home_provider.dart b/lib/features/home/presentation/home_provider.dart index bb4d210..5e0aa9b 100644 --- a/lib/features/home/presentation/home_provider.dart +++ b/lib/features/home/presentation/home_provider.dart @@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../../../common/models/place_model.dart'; +import '../data/models/content_response.dart'; import '../data/home_repository_impl.dart'; part 'home_provider.freezed.dart'; @@ -12,26 +12,17 @@ part 'home_provider.g.dart'; @freezed class HomeState with _$HomeState { const factory HomeState({ - /// 최신 장소 목록 - @Default([]) List recentPlaces, + /// 최근 콘텐츠 목록 + @Default([]) List recentContents, - /// 인기 장소 목록 - @Default([]) List popularPlaces, + /// 회원 콘텐츠 목록 + @Default([]) List memberContents, - /// 최신 장소 로딩 중 + /// 최근 콘텐츠 로딩 중 @Default(false) bool isLoadingRecent, - /// 인기 장소 로딩 중 - @Default(false) bool isLoadingPopular, - - /// 추가 로딩 중 (무한 스크롤) - @Default(false) bool isLoadingMore, - - /// 다음 페이지 커서 (최신 장소) - int? recentNextCursor, - - /// 다음 페이지 존재 여부 (최신 장소) - @Default(true) bool recentHasNext, + /// 회원 콘텐츠 로딩 중 + @Default(false) bool isLoadingMember, /// 에러 메시지 String? errorMessage, @@ -52,85 +43,50 @@ class HomeNotifier extends _$HomeNotifier { Future _initialize() async { await Future.wait([ - fetchRecentPlaces(), - fetchPopularPlaces(), + fetchRecentContents(), + fetchMemberContents(), ]); state = state.copyWith(isInitialized: true); } - /// 최신 장소 목록 조회 (초기 로드) - Future fetchRecentPlaces() async { + /// 최근 콘텐츠 목록 조회 + Future fetchRecentContents() async { state = state.copyWith(isLoadingRecent: true, errorMessage: null); try { final repository = ref.read(homeRepositoryProvider); final response = await repository.getRecentContent(); - final places = - response.content.expand((item) => item.places).toList(); - state = state.copyWith( - recentPlaces: places, + recentContents: response.contents, isLoadingRecent: false, - recentNextCursor: response.cursor?.nextCursor, - recentHasNext: response.cursor?.hasNext ?? false, ); } catch (e) { - debugPrint('❌ HomeNotifier: Failed to fetch recent places: $e'); + debugPrint('❌ HomeNotifier: Failed to fetch recent contents: $e'); state = state.copyWith( isLoadingRecent: false, - errorMessage: '최신 장소를 불러올 수 없습니다', - ); - } - } - - /// 최신 장소 추가 로드 (무한 스크롤) - Future fetchMoreRecentPlaces() async { - if (!state.recentHasNext || state.isLoadingMore) return; - - state = state.copyWith(isLoadingMore: true); - - try { - final repository = ref.read(homeRepositoryProvider); - final response = await repository.getRecentContent( - cursor: state.recentNextCursor, + errorMessage: '최신 콘텐츠를 불러올 수 없습니다', ); - - final newPlaces = - response.content.expand((item) => item.places).toList(); - - state = state.copyWith( - recentPlaces: [...state.recentPlaces, ...newPlaces], - isLoadingMore: false, - recentNextCursor: response.cursor?.nextCursor, - recentHasNext: response.cursor?.hasNext ?? false, - ); - } catch (e) { - debugPrint('❌ HomeNotifier: Failed to fetch more places: $e'); - state = state.copyWith(isLoadingMore: false); } } - /// 인기 장소 목록 조회 - Future fetchPopularPlaces() async { - state = state.copyWith(isLoadingPopular: true, errorMessage: null); + /// 회원 콘텐츠 목록 조회 + Future fetchMemberContents() async { + state = state.copyWith(isLoadingMember: true, errorMessage: null); try { final repository = ref.read(homeRepositoryProvider); final response = await repository.getMemberContent(); - final places = - response.content.expand((item) => item.places).toList(); - state = state.copyWith( - popularPlaces: places, - isLoadingPopular: false, + memberContents: response.contentPage.content, + isLoadingMember: false, ); } catch (e) { - debugPrint('❌ HomeNotifier: Failed to fetch popular places: $e'); + debugPrint('❌ HomeNotifier: Failed to fetch member contents: $e'); state = state.copyWith( - isLoadingPopular: false, - errorMessage: '인기 장소를 불러올 수 없습니다', + isLoadingMember: false, + errorMessage: '내 콘텐츠를 불러올 수 없습니다', ); } } diff --git a/lib/features/home/presentation/home_provider.freezed.dart b/lib/features/home/presentation/home_provider.freezed.dart index 8f95fc3..34661aa 100644 --- a/lib/features/home/presentation/home_provider.freezed.dart +++ b/lib/features/home/presentation/home_provider.freezed.dart @@ -17,26 +17,19 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$HomeState { - /// 최신 장소 목록 - List get recentPlaces => throw _privateConstructorUsedError; + /// 최근 콘텐츠 목록 + List get recentContents => + throw _privateConstructorUsedError; - /// 인기 장소 목록 - List get popularPlaces => throw _privateConstructorUsedError; + /// 회원 콘텐츠 목록 + List get memberContents => + throw _privateConstructorUsedError; - /// 최신 장소 로딩 중 + /// 최근 콘텐츠 로딩 중 bool get isLoadingRecent => throw _privateConstructorUsedError; - /// 인기 장소 로딩 중 - bool get isLoadingPopular => throw _privateConstructorUsedError; - - /// 추가 로딩 중 (무한 스크롤) - bool get isLoadingMore => throw _privateConstructorUsedError; - - /// 다음 페이지 커서 (최신 장소) - int? get recentNextCursor => throw _privateConstructorUsedError; - - /// 다음 페이지 존재 여부 (최신 장소) - bool get recentHasNext => throw _privateConstructorUsedError; + /// 회원 콘텐츠 로딩 중 + bool get isLoadingMember => throw _privateConstructorUsedError; /// 에러 메시지 String? get errorMessage => throw _privateConstructorUsedError; @@ -57,13 +50,10 @@ abstract class $HomeStateCopyWith<$Res> { _$HomeStateCopyWithImpl<$Res, HomeState>; @useResult $Res call({ - List recentPlaces, - List popularPlaces, + List recentContents, + List memberContents, bool isLoadingRecent, - bool isLoadingPopular, - bool isLoadingMore, - int? recentNextCursor, - bool recentHasNext, + bool isLoadingMember, String? errorMessage, bool isInitialized, }); @@ -84,45 +74,30 @@ class _$HomeStateCopyWithImpl<$Res, $Val extends HomeState> @pragma('vm:prefer-inline') @override $Res call({ - Object? recentPlaces = null, - Object? popularPlaces = null, + Object? recentContents = null, + Object? memberContents = null, Object? isLoadingRecent = null, - Object? isLoadingPopular = null, - Object? isLoadingMore = null, - Object? recentNextCursor = freezed, - Object? recentHasNext = null, + Object? isLoadingMember = null, Object? errorMessage = freezed, Object? isInitialized = null, }) { return _then( _value.copyWith( - recentPlaces: null == recentPlaces - ? _value.recentPlaces - : recentPlaces // ignore: cast_nullable_to_non_nullable - as List, - popularPlaces: null == popularPlaces - ? _value.popularPlaces - : popularPlaces // ignore: cast_nullable_to_non_nullable - as List, + recentContents: null == recentContents + ? _value.recentContents + : recentContents // ignore: cast_nullable_to_non_nullable + as List, + memberContents: null == memberContents + ? _value.memberContents + : memberContents // ignore: cast_nullable_to_non_nullable + as List, isLoadingRecent: null == isLoadingRecent ? _value.isLoadingRecent : isLoadingRecent // ignore: cast_nullable_to_non_nullable as bool, - isLoadingPopular: null == isLoadingPopular - ? _value.isLoadingPopular - : isLoadingPopular // ignore: cast_nullable_to_non_nullable - as bool, - isLoadingMore: null == isLoadingMore - ? _value.isLoadingMore - : isLoadingMore // ignore: cast_nullable_to_non_nullable - as bool, - recentNextCursor: freezed == recentNextCursor - ? _value.recentNextCursor - : recentNextCursor // ignore: cast_nullable_to_non_nullable - as int?, - recentHasNext: null == recentHasNext - ? _value.recentHasNext - : recentHasNext // ignore: cast_nullable_to_non_nullable + isLoadingMember: null == isLoadingMember + ? _value.isLoadingMember + : isLoadingMember // ignore: cast_nullable_to_non_nullable as bool, errorMessage: freezed == errorMessage ? _value.errorMessage @@ -148,13 +123,10 @@ abstract class _$$HomeStateImplCopyWith<$Res> @override @useResult $Res call({ - List recentPlaces, - List popularPlaces, + List recentContents, + List memberContents, bool isLoadingRecent, - bool isLoadingPopular, - bool isLoadingMore, - int? recentNextCursor, - bool recentHasNext, + bool isLoadingMember, String? errorMessage, bool isInitialized, }); @@ -174,45 +146,30 @@ class __$$HomeStateImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? recentPlaces = null, - Object? popularPlaces = null, + Object? recentContents = null, + Object? memberContents = null, Object? isLoadingRecent = null, - Object? isLoadingPopular = null, - Object? isLoadingMore = null, - Object? recentNextCursor = freezed, - Object? recentHasNext = null, + Object? isLoadingMember = null, Object? errorMessage = freezed, Object? isInitialized = null, }) { return _then( _$HomeStateImpl( - recentPlaces: null == recentPlaces - ? _value._recentPlaces - : recentPlaces // ignore: cast_nullable_to_non_nullable - as List, - popularPlaces: null == popularPlaces - ? _value._popularPlaces - : popularPlaces // ignore: cast_nullable_to_non_nullable - as List, + recentContents: null == recentContents + ? _value._recentContents + : recentContents // ignore: cast_nullable_to_non_nullable + as List, + memberContents: null == memberContents + ? _value._memberContents + : memberContents // ignore: cast_nullable_to_non_nullable + as List, isLoadingRecent: null == isLoadingRecent ? _value.isLoadingRecent : isLoadingRecent // ignore: cast_nullable_to_non_nullable as bool, - isLoadingPopular: null == isLoadingPopular - ? _value.isLoadingPopular - : isLoadingPopular // ignore: cast_nullable_to_non_nullable - as bool, - isLoadingMore: null == isLoadingMore - ? _value.isLoadingMore - : isLoadingMore // ignore: cast_nullable_to_non_nullable - as bool, - recentNextCursor: freezed == recentNextCursor - ? _value.recentNextCursor - : recentNextCursor // ignore: cast_nullable_to_non_nullable - as int?, - recentHasNext: null == recentHasNext - ? _value.recentHasNext - : recentHasNext // ignore: cast_nullable_to_non_nullable + isLoadingMember: null == isLoadingMember + ? _value.isLoadingMember + : isLoadingMember // ignore: cast_nullable_to_non_nullable as bool, errorMessage: freezed == errorMessage ? _value.errorMessage @@ -231,65 +188,48 @@ class __$$HomeStateImplCopyWithImpl<$Res> class _$HomeStateImpl with DiagnosticableTreeMixin implements _HomeState { const _$HomeStateImpl({ - final List recentPlaces = const [], - final List popularPlaces = const [], + final List recentContents = const [], + final List memberContents = const [], this.isLoadingRecent = false, - this.isLoadingPopular = false, - this.isLoadingMore = false, - this.recentNextCursor, - this.recentHasNext = true, + this.isLoadingMember = false, this.errorMessage, this.isInitialized = false, - }) : _recentPlaces = recentPlaces, - _popularPlaces = popularPlaces; + }) : _recentContents = recentContents, + _memberContents = memberContents; - /// 최신 장소 목록 - final List _recentPlaces; + /// 최근 콘텐츠 목록 + final List _recentContents; - /// 최신 장소 목록 + /// 최근 콘텐츠 목록 @override @JsonKey() - List get recentPlaces { - if (_recentPlaces is EqualUnmodifiableListView) return _recentPlaces; + List get recentContents { + if (_recentContents is EqualUnmodifiableListView) return _recentContents; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_recentPlaces); + return EqualUnmodifiableListView(_recentContents); } - /// 인기 장소 목록 - final List _popularPlaces; + /// 회원 콘텐츠 목록 + final List _memberContents; - /// 인기 장소 목록 + /// 회원 콘텐츠 목록 @override @JsonKey() - List get popularPlaces { - if (_popularPlaces is EqualUnmodifiableListView) return _popularPlaces; + List get memberContents { + if (_memberContents is EqualUnmodifiableListView) return _memberContents; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_popularPlaces); + return EqualUnmodifiableListView(_memberContents); } - /// 최신 장소 로딩 중 + /// 최근 콘텐츠 로딩 중 @override @JsonKey() final bool isLoadingRecent; - /// 인기 장소 로딩 중 - @override - @JsonKey() - final bool isLoadingPopular; - - /// 추가 로딩 중 (무한 스크롤) + /// 회원 콘텐츠 로딩 중 @override @JsonKey() - final bool isLoadingMore; - - /// 다음 페이지 커서 (최신 장소) - @override - final int? recentNextCursor; - - /// 다음 페이지 존재 여부 (최신 장소) - @override - @JsonKey() - final bool recentHasNext; + final bool isLoadingMember; /// 에러 메시지 @override @@ -302,7 +242,7 @@ class _$HomeStateImpl with DiagnosticableTreeMixin implements _HomeState { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'HomeState(recentPlaces: $recentPlaces, popularPlaces: $popularPlaces, isLoadingRecent: $isLoadingRecent, isLoadingPopular: $isLoadingPopular, isLoadingMore: $isLoadingMore, recentNextCursor: $recentNextCursor, recentHasNext: $recentHasNext, errorMessage: $errorMessage, isInitialized: $isInitialized)'; + return 'HomeState(recentContents: $recentContents, memberContents: $memberContents, isLoadingRecent: $isLoadingRecent, isLoadingMember: $isLoadingMember, errorMessage: $errorMessage, isInitialized: $isInitialized)'; } @override @@ -310,13 +250,10 @@ class _$HomeStateImpl with DiagnosticableTreeMixin implements _HomeState { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('type', 'HomeState')) - ..add(DiagnosticsProperty('recentPlaces', recentPlaces)) - ..add(DiagnosticsProperty('popularPlaces', popularPlaces)) + ..add(DiagnosticsProperty('recentContents', recentContents)) + ..add(DiagnosticsProperty('memberContents', memberContents)) ..add(DiagnosticsProperty('isLoadingRecent', isLoadingRecent)) - ..add(DiagnosticsProperty('isLoadingPopular', isLoadingPopular)) - ..add(DiagnosticsProperty('isLoadingMore', isLoadingMore)) - ..add(DiagnosticsProperty('recentNextCursor', recentNextCursor)) - ..add(DiagnosticsProperty('recentHasNext', recentHasNext)) + ..add(DiagnosticsProperty('isLoadingMember', isLoadingMember)) ..add(DiagnosticsProperty('errorMessage', errorMessage)) ..add(DiagnosticsProperty('isInitialized', isInitialized)); } @@ -327,23 +264,17 @@ class _$HomeStateImpl with DiagnosticableTreeMixin implements _HomeState { (other.runtimeType == runtimeType && other is _$HomeStateImpl && const DeepCollectionEquality().equals( - other._recentPlaces, - _recentPlaces, + other._recentContents, + _recentContents, ) && const DeepCollectionEquality().equals( - other._popularPlaces, - _popularPlaces, + other._memberContents, + _memberContents, ) && (identical(other.isLoadingRecent, isLoadingRecent) || other.isLoadingRecent == isLoadingRecent) && - (identical(other.isLoadingPopular, isLoadingPopular) || - other.isLoadingPopular == isLoadingPopular) && - (identical(other.isLoadingMore, isLoadingMore) || - other.isLoadingMore == isLoadingMore) && - (identical(other.recentNextCursor, recentNextCursor) || - other.recentNextCursor == recentNextCursor) && - (identical(other.recentHasNext, recentHasNext) || - other.recentHasNext == recentHasNext) && + (identical(other.isLoadingMember, isLoadingMember) || + other.isLoadingMember == isLoadingMember) && (identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage) && (identical(other.isInitialized, isInitialized) || @@ -353,13 +284,10 @@ class _$HomeStateImpl with DiagnosticableTreeMixin implements _HomeState { @override int get hashCode => Object.hash( runtimeType, - const DeepCollectionEquality().hash(_recentPlaces), - const DeepCollectionEquality().hash(_popularPlaces), + const DeepCollectionEquality().hash(_recentContents), + const DeepCollectionEquality().hash(_memberContents), isLoadingRecent, - isLoadingPopular, - isLoadingMore, - recentNextCursor, - recentHasNext, + isLoadingMember, errorMessage, isInitialized, ); @@ -375,44 +303,29 @@ class _$HomeStateImpl with DiagnosticableTreeMixin implements _HomeState { abstract class _HomeState implements HomeState { const factory _HomeState({ - final List recentPlaces, - final List popularPlaces, + final List recentContents, + final List memberContents, final bool isLoadingRecent, - final bool isLoadingPopular, - final bool isLoadingMore, - final int? recentNextCursor, - final bool recentHasNext, + final bool isLoadingMember, final String? errorMessage, final bool isInitialized, }) = _$HomeStateImpl; - /// 최신 장소 목록 + /// 최근 콘텐츠 목록 @override - List get recentPlaces; + List get recentContents; - /// 인기 장소 목록 + /// 회원 콘텐츠 목록 @override - List get popularPlaces; + List get memberContents; - /// 최신 장소 로딩 중 + /// 최근 콘텐츠 로딩 중 @override bool get isLoadingRecent; - /// 인기 장소 로딩 중 - @override - bool get isLoadingPopular; - - /// 추가 로딩 중 (무한 스크롤) - @override - bool get isLoadingMore; - - /// 다음 페이지 커서 (최신 장소) - @override - int? get recentNextCursor; - - /// 다음 페이지 존재 여부 (최신 장소) + /// 회원 콘텐츠 로딩 중 @override - bool get recentHasNext; + bool get isLoadingMember; /// 에러 메시지 @override diff --git a/lib/features/home/presentation/home_provider.g.dart b/lib/features/home/presentation/home_provider.g.dart index ca1ed4a..12e3e20 100644 --- a/lib/features/home/presentation/home_provider.g.dart +++ b/lib/features/home/presentation/home_provider.g.dart @@ -6,7 +6,7 @@ part of 'home_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$homeNotifierHash() => r'b8dc0b493c3601ec697650b1450d1f7e12983150'; +String _$homeNotifierHash() => r'037bfa7aff735ac06bfedbf79c34b4f6a508a466'; /// 홈 화면 Notifier /// diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index b883b89..79dbb60 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -4,15 +4,16 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import '../../../../common/constants/home_colors.dart'; +import '../../../../common/constants/text_styles.dart'; import '../../../../routing/route_paths.dart'; import '../../../auth/presentation/auth_provider.dart'; import '../home_provider.dart'; +import '../widgets/content_card.dart'; import '../widgets/home_empty_state.dart'; import '../widgets/home_error_state.dart'; import '../widgets/home_loading_shimmer.dart'; -import '../widgets/place_card.dart'; -/// 홈 화면 (씀 스타일: 탭 전환 + 미니멀 카드 리스트) +/// 홈 화면 (콘텐츠 피드: 최신/내 콘텐츠 탭) class HomePage extends ConsumerStatefulWidget { const HomePage({super.key}); @@ -23,30 +24,19 @@ class HomePage extends ConsumerStatefulWidget { class _HomePageState extends ConsumerState with SingleTickerProviderStateMixin { late final TabController _tabController; - final _recentScrollController = ScrollController(); @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); - _recentScrollController.addListener(_onRecentScroll); } @override void dispose() { _tabController.dispose(); - _recentScrollController.removeListener(_onRecentScroll); - _recentScrollController.dispose(); super.dispose(); } - void _onRecentScroll() { - if (_recentScrollController.position.pixels >= - _recentScrollController.position.maxScrollExtent - 200) { - ref.read(homeNotifierProvider.notifier).fetchMoreRecentPlaces(); - } - } - @override Widget build(BuildContext context) { final homeState = ref.watch(homeNotifierProvider); @@ -59,10 +49,8 @@ class _HomePageState extends ConsumerState scrolledUnderElevation: 0, title: Text( 'MapSy', - style: TextStyle( + style: AppTextStyles.heading02.copyWith( color: HomeColors.textPrimary, - fontSize: 20.sp, - fontWeight: FontWeight.w600, ), ), actions: [ @@ -85,17 +73,16 @@ class _HomePageState extends ConsumerState controller: _tabController, labelColor: HomeColors.textPrimary, unselectedLabelColor: HomeColors.iconSecondary, - labelStyle: TextStyle(fontSize: 15.sp, fontWeight: FontWeight.w600), - unselectedLabelStyle: TextStyle( - fontSize: 15.sp, + labelStyle: AppTextStyles.label, + unselectedLabelStyle: AppTextStyles.paragraph.copyWith( fontWeight: FontWeight.w400, ), indicatorColor: HomeColors.textPrimary, indicatorWeight: 2, dividerColor: HomeColors.divider, tabs: const [ - Tab(text: '최신순'), - Tab(text: '인기순'), + Tab(text: '최신'), + Tab(text: '내 콘텐츠'), ], ), ), @@ -104,16 +91,14 @@ class _HomePageState extends ConsumerState } Widget _buildBody(HomeState state) { - // 초기 로딩 if (!state.isInitialized && - (state.isLoadingRecent || state.isLoadingPopular)) { + (state.isLoadingRecent || state.isLoadingMember)) { return const SingleChildScrollView(child: HomeLoadingShimmer()); } - // 에러 상태 (데이터 없이 에러) if (state.errorMessage != null && - state.recentPlaces.isEmpty && - state.popularPlaces.isEmpty) { + state.recentContents.isEmpty && + state.memberContents.isEmpty) { return HomeErrorState( message: state.errorMessage!, onRetry: () => ref.read(homeNotifierProvider.notifier).refresh(), @@ -122,14 +107,14 @@ class _HomePageState extends ConsumerState return TabBarView( controller: _tabController, - children: [_buildRecentTab(state), _buildPopularTab(state)], + children: [_buildRecentTab(state), _buildMemberTab(state)], ); } - /// 최신순 탭 + /// 최신 콘텐츠 탭 Widget _buildRecentTab(HomeState state) { - if (state.recentPlaces.isEmpty && !state.isLoadingRecent) { - return const HomeEmptyState(message: '아직 등록된 장소가 없습니다'); + if (state.recentContents.isEmpty && !state.isLoadingRecent) { + return const HomeEmptyState(message: '아직 등록된 콘텐츠가 없습니다'); } return RefreshIndicator( @@ -137,36 +122,19 @@ class _HomePageState extends ConsumerState color: HomeColors.textPrimary, backgroundColor: HomeColors.background, child: ListView.builder( - controller: _recentScrollController, padding: EdgeInsets.symmetric(vertical: 8.h), - itemCount: state.recentPlaces.length + (state.isLoadingMore ? 1 : 0), + itemCount: state.recentContents.length, itemBuilder: (context, index) { - if (index < state.recentPlaces.length) { - return PlaceCard(place: state.recentPlaces[index]); - } - // 무한 스크롤 로딩 - return Padding( - padding: EdgeInsets.all(16.w), - child: Center( - child: SizedBox( - width: 20.w, - height: 20.w, - child: CircularProgressIndicator( - color: HomeColors.textPrimary, - strokeWidth: 2, - ), - ), - ), - ); + return ContentCard(content: state.recentContents[index]); }, ), ); } - /// 인기순 탭 - Widget _buildPopularTab(HomeState state) { - if (state.popularPlaces.isEmpty && !state.isLoadingPopular) { - return const HomeEmptyState(message: '아직 인기 장소가 없습니다'); + /// 내 콘텐츠 탭 + Widget _buildMemberTab(HomeState state) { + if (state.memberContents.isEmpty && !state.isLoadingMember) { + return const HomeEmptyState(message: '아직 분석한 콘텐츠가 없습니다'); } return RefreshIndicator( @@ -175,9 +143,9 @@ class _HomePageState extends ConsumerState backgroundColor: HomeColors.background, child: ListView.builder( padding: EdgeInsets.symmetric(vertical: 8.h), - itemCount: state.popularPlaces.length, + itemCount: state.memberContents.length, itemBuilder: (context, index) { - return PlaceCard(place: state.popularPlaces[index]); + return ContentCard(content: state.memberContents[index]); }, ), ); diff --git a/lib/features/home/presentation/widgets/content_card.dart b/lib/features/home/presentation/widgets/content_card.dart new file mode 100644 index 0000000..2cdb18d --- /dev/null +++ b/lib/features/home/presentation/widgets/content_card.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../common/constants/home_colors.dart'; +import '../../../../common/constants/text_styles.dart'; +import '../../data/models/content_response.dart'; + +/// 콘텐츠 카드 (홈 피드용) +class ContentCard extends StatelessWidget { + final ContentItemModel content; + final VoidCallback? onTap; + + const ContentCard({super.key, required this.content, this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h), + decoration: BoxDecoration( + color: HomeColors.cardBackground, + border: Border.all(color: HomeColors.cardBorder, width: 1), + ), + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 썸네일 (3:2) + if (content.thumbnailUrl != null) + AspectRatio(aspectRatio: 3 / 2, child: _buildThumbnail()), + + // 정보 영역 + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 플랫폼 + 상태 배지 + Row( + children: [ + if (content.platform != null) + _buildPlatformBadge(content.platform!), + if (content.status != null) ...[ + SizedBox(width: 6.w), + _buildStatusBadge(content.status!), + ], + ], + ), + SizedBox(height: 8.h), + + // 제목 또는 캡션 + Text( + content.title ?? content.caption ?? '제목 없음', + style: AppTextStyles.label.copyWith( + color: HomeColors.textPrimary, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + // 요약 + if (content.summary != null) ...[ + SizedBox(height: 4.h), + Text( + content.summary!, + style: AppTextStyles.paragraph.copyWith( + color: HomeColors.textSecondary, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + + // 업로더 + if (content.platformUploader != null) ...[ + SizedBox(height: 8.h), + Text( + '@${content.platformUploader}', + style: AppTextStyles.callout.copyWith( + color: HomeColors.textSecondary, + ), + ), + ], + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildThumbnail() { + return Image.network( + content.thumbnailUrl!, + fit: BoxFit.cover, + errorBuilder: (_, _, _) => _buildPlaceholder(), + ); + } + + Widget _buildPlaceholder() { + return Container( + color: HomeColors.surfaceLight, + child: Center( + child: Icon( + Icons.article_outlined, + color: HomeColors.iconSecondary, + size: 48.sp, + ), + ), + ); + } + + Widget _buildPlatformBadge(String platform) { + final icon = switch (platform.toUpperCase()) { + 'INSTAGRAM' => Icons.camera_alt_outlined, + 'YOUTUBE' => Icons.play_circle_outline, + _ => Icons.language, + }; + + return Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: HomeColors.tagBackground, + borderRadius: BorderRadius.circular(4.r), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 12.sp, color: HomeColors.tagText), + SizedBox(width: 4.w), + Text( + platform, + style: AppTextStyles.calloutSmall.copyWith( + color: HomeColors.tagText, + ), + ), + ], + ), + ); + } + + Widget _buildStatusBadge(String status) { + final (label, color) = switch (status.toUpperCase()) { + 'COMPLETED' => ('완료', HomeColors.textPrimary), + 'ANALYZING' => ('분석 중', HomeColors.textSecondary), + 'PENDING' => ('대기 중', HomeColors.textSecondary), + 'FAILED' => ('실패', HomeColors.error), + _ => (status, HomeColors.textSecondary), + }; + + return Text( + label, + style: AppTextStyles.calloutSmall.copyWith(color: color), + ); + } +} diff --git a/lib/features/home/presentation/widgets/place_card.dart b/lib/features/home/presentation/widgets/place_card.dart index 7945d84..1be3090 100644 --- a/lib/features/home/presentation/widgets/place_card.dart +++ b/lib/features/home/presentation/widgets/place_card.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../../common/constants/home_colors.dart'; +import '../../../../common/constants/text_styles.dart'; import '../../../../common/models/place_model.dart'; /// 장소 카드 (씀 스타일: 직각 보더, 넓은 여백) @@ -35,53 +36,51 @@ class PlaceCard extends StatelessWidget { children: [ // 장소명 Text( - place.placeName, - style: TextStyle( + place.name, + style: AppTextStyles.label.copyWith( color: HomeColors.textPrimary, - fontSize: 16.sp, - fontWeight: FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (place.address != null) ...[ SizedBox(height: 4.h), - // 주소 Text( place.address!, - style: TextStyle( + style: AppTextStyles.paragraph.copyWith( color: HomeColors.textSecondary, - fontSize: 13.sp, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], - if (place.tags.isNotEmpty) ...[ + if (place.rating != null) ...[ SizedBox(height: 8.h), - // 태그 - Wrap( - spacing: 6.w, - runSpacing: 4.h, - children: place.tags.take(3).map((tag) { - return Container( - padding: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 4.h, + Row( + children: [ + Icon( + Icons.star_rounded, + size: 14.sp, + color: HomeColors.starRating, + ), + SizedBox(width: 2.w), + Text( + place.rating!.toStringAsFixed(1), + style: AppTextStyles.callout.copyWith( + color: HomeColors.textPrimary, + fontWeight: FontWeight.w500, ), - decoration: BoxDecoration( - color: HomeColors.tagBackground, - borderRadius: BorderRadius.circular(100.r), - ), - child: Text( - '#$tag', - style: TextStyle( - color: HomeColors.tagText, - fontSize: 12.sp, + ), + if (place.userRatingsTotal != null) ...[ + SizedBox(width: 4.w), + Text( + '(${place.userRatingsTotal})', + style: AppTextStyles.calloutSmall.copyWith( + color: HomeColors.textSecondary, ), ), - ); - }).toList(), + ], + ], ), ], ], @@ -94,9 +93,9 @@ class PlaceCard extends StatelessWidget { } Widget _buildThumbnail() { - if (place.imageUrl != null && place.imageUrl!.isNotEmpty) { + if (place.photoUrls.isNotEmpty) { return Image.network( - place.imageUrl!, + place.photoUrls.first, fit: BoxFit.cover, errorBuilder: (_, _, _) => _buildPlaceholder(), ); diff --git a/lib/features/mypage/presentation/pages/mypage_page.dart b/lib/features/mypage/presentation/pages/mypage_page.dart index b4b2f22..ee5818d 100644 --- a/lib/features/mypage/presentation/pages/mypage_page.dart +++ b/lib/features/mypage/presentation/pages/mypage_page.dart @@ -13,6 +13,7 @@ import '../mypage_provider.dart'; import '../widgets/nickname_edit_bottom_sheet.dart'; import '../widgets/profile_card.dart'; import '../widgets/setting_tile.dart'; +import '../../../saved_places/presentation/widgets/folder_preview_section.dart'; class MypagePage extends ConsumerWidget { const MypagePage({super.key}); @@ -52,6 +53,11 @@ class MypagePage extends ConsumerWidget { _buildDivider(), + // ─── 내 폴더 미리보기 ─── + const FolderPreviewSection(), + + _buildDivider(), + // ─── 앱 설정 섹션 ─── _buildSectionTitle('앱 설정'), SettingTile( diff --git a/lib/features/saved_places/data/models/create_folder_request.dart b/lib/features/saved_places/data/models/create_folder_request.dart new file mode 100644 index 0000000..5b3b68c --- /dev/null +++ b/lib/features/saved_places/data/models/create_folder_request.dart @@ -0,0 +1,34 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'create_folder_request.freezed.dart'; +part 'create_folder_request.g.dart'; + +/// 폴더 생성 요청 DTO +@freezed +class CreateFolderRequest with _$CreateFolderRequest { + const factory CreateFolderRequest({ + /// 폴더 이름 (최대 100자) + required String name, + + /// 공개 설정 (PRIVATE / SHARED) + @Default('PRIVATE') String visibility, + }) = _CreateFolderRequest; + + factory CreateFolderRequest.fromJson(Map json) => + _$CreateFolderRequestFromJson(json); +} + +/// 폴더 생성 응답 DTO +@freezed +class CreateFolderResponse with _$CreateFolderResponse { + const factory CreateFolderResponse({ + required String id, + required String name, + required String visibility, + @Default(false) bool isDefault, + String? createdAt, + }) = _CreateFolderResponse; + + factory CreateFolderResponse.fromJson(Map json) => + _$CreateFolderResponseFromJson(json); +} diff --git a/lib/features/saved_places/data/models/create_folder_request.freezed.dart b/lib/features/saved_places/data/models/create_folder_request.freezed.dart new file mode 100644 index 0000000..6cf6578 --- /dev/null +++ b/lib/features/saved_places/data/models/create_folder_request.freezed.dart @@ -0,0 +1,456 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'create_folder_request.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +CreateFolderRequest _$CreateFolderRequestFromJson(Map json) { + return _CreateFolderRequest.fromJson(json); +} + +/// @nodoc +mixin _$CreateFolderRequest { + /// 폴더 이름 (최대 100자) + String get name => throw _privateConstructorUsedError; + + /// 공개 설정 (PRIVATE / SHARED) + String get visibility => throw _privateConstructorUsedError; + + /// Serializes this CreateFolderRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CreateFolderRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CreateFolderRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CreateFolderRequestCopyWith<$Res> { + factory $CreateFolderRequestCopyWith( + CreateFolderRequest value, + $Res Function(CreateFolderRequest) then, + ) = _$CreateFolderRequestCopyWithImpl<$Res, CreateFolderRequest>; + @useResult + $Res call({String name, String visibility}); +} + +/// @nodoc +class _$CreateFolderRequestCopyWithImpl<$Res, $Val extends CreateFolderRequest> + implements $CreateFolderRequestCopyWith<$Res> { + _$CreateFolderRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CreateFolderRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? name = null, Object? visibility = null}) { + return _then( + _value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + visibility: null == visibility + ? _value.visibility + : visibility // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CreateFolderRequestImplCopyWith<$Res> + implements $CreateFolderRequestCopyWith<$Res> { + factory _$$CreateFolderRequestImplCopyWith( + _$CreateFolderRequestImpl value, + $Res Function(_$CreateFolderRequestImpl) then, + ) = __$$CreateFolderRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name, String visibility}); +} + +/// @nodoc +class __$$CreateFolderRequestImplCopyWithImpl<$Res> + extends _$CreateFolderRequestCopyWithImpl<$Res, _$CreateFolderRequestImpl> + implements _$$CreateFolderRequestImplCopyWith<$Res> { + __$$CreateFolderRequestImplCopyWithImpl( + _$CreateFolderRequestImpl _value, + $Res Function(_$CreateFolderRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CreateFolderRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? name = null, Object? visibility = null}) { + return _then( + _$CreateFolderRequestImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + visibility: null == visibility + ? _value.visibility + : visibility // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$CreateFolderRequestImpl implements _CreateFolderRequest { + const _$CreateFolderRequestImpl({ + required this.name, + this.visibility = 'PRIVATE', + }); + + factory _$CreateFolderRequestImpl.fromJson(Map json) => + _$$CreateFolderRequestImplFromJson(json); + + /// 폴더 이름 (최대 100자) + @override + final String name; + + /// 공개 설정 (PRIVATE / SHARED) + @override + @JsonKey() + final String visibility; + + @override + String toString() { + return 'CreateFolderRequest(name: $name, visibility: $visibility)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreateFolderRequestImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.visibility, visibility) || + other.visibility == visibility)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, name, visibility); + + /// Create a copy of CreateFolderRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CreateFolderRequestImplCopyWith<_$CreateFolderRequestImpl> get copyWith => + __$$CreateFolderRequestImplCopyWithImpl<_$CreateFolderRequestImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$CreateFolderRequestImplToJson(this); + } +} + +abstract class _CreateFolderRequest implements CreateFolderRequest { + const factory _CreateFolderRequest({ + required final String name, + final String visibility, + }) = _$CreateFolderRequestImpl; + + factory _CreateFolderRequest.fromJson(Map json) = + _$CreateFolderRequestImpl.fromJson; + + /// 폴더 이름 (최대 100자) + @override + String get name; + + /// 공개 설정 (PRIVATE / SHARED) + @override + String get visibility; + + /// Create a copy of CreateFolderRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CreateFolderRequestImplCopyWith<_$CreateFolderRequestImpl> get copyWith => + throw _privateConstructorUsedError; +} + +CreateFolderResponse _$CreateFolderResponseFromJson(Map json) { + return _CreateFolderResponse.fromJson(json); +} + +/// @nodoc +mixin _$CreateFolderResponse { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get visibility => throw _privateConstructorUsedError; + bool get isDefault => throw _privateConstructorUsedError; + String? get createdAt => throw _privateConstructorUsedError; + + /// Serializes this CreateFolderResponse to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CreateFolderResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CreateFolderResponseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CreateFolderResponseCopyWith<$Res> { + factory $CreateFolderResponseCopyWith( + CreateFolderResponse value, + $Res Function(CreateFolderResponse) then, + ) = _$CreateFolderResponseCopyWithImpl<$Res, CreateFolderResponse>; + @useResult + $Res call({ + String id, + String name, + String visibility, + bool isDefault, + String? createdAt, + }); +} + +/// @nodoc +class _$CreateFolderResponseCopyWithImpl< + $Res, + $Val extends CreateFolderResponse +> + implements $CreateFolderResponseCopyWith<$Res> { + _$CreateFolderResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CreateFolderResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? visibility = null, + Object? isDefault = null, + Object? createdAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + visibility: null == visibility + ? _value.visibility + : visibility // ignore: cast_nullable_to_non_nullable + as String, + isDefault: null == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CreateFolderResponseImplCopyWith<$Res> + implements $CreateFolderResponseCopyWith<$Res> { + factory _$$CreateFolderResponseImplCopyWith( + _$CreateFolderResponseImpl value, + $Res Function(_$CreateFolderResponseImpl) then, + ) = __$$CreateFolderResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String name, + String visibility, + bool isDefault, + String? createdAt, + }); +} + +/// @nodoc +class __$$CreateFolderResponseImplCopyWithImpl<$Res> + extends _$CreateFolderResponseCopyWithImpl<$Res, _$CreateFolderResponseImpl> + implements _$$CreateFolderResponseImplCopyWith<$Res> { + __$$CreateFolderResponseImplCopyWithImpl( + _$CreateFolderResponseImpl _value, + $Res Function(_$CreateFolderResponseImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CreateFolderResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? visibility = null, + Object? isDefault = null, + Object? createdAt = freezed, + }) { + return _then( + _$CreateFolderResponseImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + visibility: null == visibility + ? _value.visibility + : visibility // ignore: cast_nullable_to_non_nullable + as String, + isDefault: null == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$CreateFolderResponseImpl implements _CreateFolderResponse { + const _$CreateFolderResponseImpl({ + required this.id, + required this.name, + required this.visibility, + this.isDefault = false, + this.createdAt, + }); + + factory _$CreateFolderResponseImpl.fromJson(Map json) => + _$$CreateFolderResponseImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + final String visibility; + @override + @JsonKey() + final bool isDefault; + @override + final String? createdAt; + + @override + String toString() { + return 'CreateFolderResponse(id: $id, name: $name, visibility: $visibility, isDefault: $isDefault, createdAt: $createdAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreateFolderResponseImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.visibility, visibility) || + other.visibility == visibility) && + (identical(other.isDefault, isDefault) || + other.isDefault == isDefault) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, id, name, visibility, isDefault, createdAt); + + /// Create a copy of CreateFolderResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CreateFolderResponseImplCopyWith<_$CreateFolderResponseImpl> + get copyWith => + __$$CreateFolderResponseImplCopyWithImpl<_$CreateFolderResponseImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$CreateFolderResponseImplToJson(this); + } +} + +abstract class _CreateFolderResponse implements CreateFolderResponse { + const factory _CreateFolderResponse({ + required final String id, + required final String name, + required final String visibility, + final bool isDefault, + final String? createdAt, + }) = _$CreateFolderResponseImpl; + + factory _CreateFolderResponse.fromJson(Map json) = + _$CreateFolderResponseImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + String get visibility; + @override + bool get isDefault; + @override + String? get createdAt; + + /// Create a copy of CreateFolderResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CreateFolderResponseImplCopyWith<_$CreateFolderResponseImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/features/saved_places/data/models/create_folder_request.g.dart b/lib/features/saved_places/data/models/create_folder_request.g.dart new file mode 100644 index 0000000..54f9fcd --- /dev/null +++ b/lib/features/saved_places/data/models/create_folder_request.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'create_folder_request.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$CreateFolderRequestImpl _$$CreateFolderRequestImplFromJson( + Map json, +) => _$CreateFolderRequestImpl( + name: json['name'] as String, + visibility: json['visibility'] as String? ?? 'PRIVATE', +); + +Map _$$CreateFolderRequestImplToJson( + _$CreateFolderRequestImpl instance, +) => { + 'name': instance.name, + 'visibility': instance.visibility, +}; + +_$CreateFolderResponseImpl _$$CreateFolderResponseImplFromJson( + Map json, +) => _$CreateFolderResponseImpl( + id: json['id'] as String, + name: json['name'] as String, + visibility: json['visibility'] as String, + isDefault: json['isDefault'] as bool? ?? false, + createdAt: json['createdAt'] as String?, +); + +Map _$$CreateFolderResponseImplToJson( + _$CreateFolderResponseImpl instance, +) => { + 'id': instance.id, + 'name': instance.name, + 'visibility': instance.visibility, + 'isDefault': instance.isDefault, + 'createdAt': instance.createdAt, +}; diff --git a/lib/features/saved_places/data/models/folder_model.dart b/lib/features/saved_places/data/models/folder_model.dart new file mode 100644 index 0000000..2e44b2a --- /dev/null +++ b/lib/features/saved_places/data/models/folder_model.dart @@ -0,0 +1,45 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'folder_model.freezed.dart'; +part 'folder_model.g.dart'; + +/// 폴더 DTO (목록 조회용) +@freezed +class FolderModel with _$FolderModel { + const factory FolderModel({ + /// 폴더 ID + required String id, + + /// 폴더 이름 + required String name, + + /// 공개 설정 (PRIVATE / SHARED) + required String visibility, + + /// 썸네일 URL + String? thumbnailUrl, + + /// 기본 폴더 여부 + @Default(false) bool isDefault, + + /// 폴더 내 장소 수 + @Default(0) int placeCount, + + /// 생성일시 + String? createdAt, + }) = _FolderModel; + + factory FolderModel.fromJson(Map json) => + _$FolderModelFromJson(json); +} + +/// 폴더 목록 응답 +@freezed +class GetFoldersResponse with _$GetFoldersResponse { + const factory GetFoldersResponse({ + @Default([]) List folders, + }) = _GetFoldersResponse; + + factory GetFoldersResponse.fromJson(Map json) => + _$GetFoldersResponseFromJson(json); +} diff --git a/lib/features/saved_places/data/models/folder_model.freezed.dart b/lib/features/saved_places/data/models/folder_model.freezed.dart new file mode 100644 index 0000000..23b6c5c --- /dev/null +++ b/lib/features/saved_places/data/models/folder_model.freezed.dart @@ -0,0 +1,517 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'folder_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +FolderModel _$FolderModelFromJson(Map json) { + return _FolderModel.fromJson(json); +} + +/// @nodoc +mixin _$FolderModel { + /// 폴더 ID + String get id => throw _privateConstructorUsedError; + + /// 폴더 이름 + String get name => throw _privateConstructorUsedError; + + /// 공개 설정 (PRIVATE / SHARED) + String get visibility => throw _privateConstructorUsedError; + + /// 썸네일 URL + String? get thumbnailUrl => throw _privateConstructorUsedError; + + /// 기본 폴더 여부 + bool get isDefault => throw _privateConstructorUsedError; + + /// 폴더 내 장소 수 + int get placeCount => throw _privateConstructorUsedError; + + /// 생성일시 + String? get createdAt => throw _privateConstructorUsedError; + + /// Serializes this FolderModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of FolderModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $FolderModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $FolderModelCopyWith<$Res> { + factory $FolderModelCopyWith( + FolderModel value, + $Res Function(FolderModel) then, + ) = _$FolderModelCopyWithImpl<$Res, FolderModel>; + @useResult + $Res call({ + String id, + String name, + String visibility, + String? thumbnailUrl, + bool isDefault, + int placeCount, + String? createdAt, + }); +} + +/// @nodoc +class _$FolderModelCopyWithImpl<$Res, $Val extends FolderModel> + implements $FolderModelCopyWith<$Res> { + _$FolderModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of FolderModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? visibility = null, + Object? thumbnailUrl = freezed, + Object? isDefault = null, + Object? placeCount = null, + Object? createdAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + visibility: null == visibility + ? _value.visibility + : visibility // ignore: cast_nullable_to_non_nullable + as String, + thumbnailUrl: freezed == thumbnailUrl + ? _value.thumbnailUrl + : thumbnailUrl // ignore: cast_nullable_to_non_nullable + as String?, + isDefault: null == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool, + placeCount: null == placeCount + ? _value.placeCount + : placeCount // ignore: cast_nullable_to_non_nullable + as int, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$FolderModelImplCopyWith<$Res> + implements $FolderModelCopyWith<$Res> { + factory _$$FolderModelImplCopyWith( + _$FolderModelImpl value, + $Res Function(_$FolderModelImpl) then, + ) = __$$FolderModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String name, + String visibility, + String? thumbnailUrl, + bool isDefault, + int placeCount, + String? createdAt, + }); +} + +/// @nodoc +class __$$FolderModelImplCopyWithImpl<$Res> + extends _$FolderModelCopyWithImpl<$Res, _$FolderModelImpl> + implements _$$FolderModelImplCopyWith<$Res> { + __$$FolderModelImplCopyWithImpl( + _$FolderModelImpl _value, + $Res Function(_$FolderModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of FolderModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? visibility = null, + Object? thumbnailUrl = freezed, + Object? isDefault = null, + Object? placeCount = null, + Object? createdAt = freezed, + }) { + return _then( + _$FolderModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + visibility: null == visibility + ? _value.visibility + : visibility // ignore: cast_nullable_to_non_nullable + as String, + thumbnailUrl: freezed == thumbnailUrl + ? _value.thumbnailUrl + : thumbnailUrl // ignore: cast_nullable_to_non_nullable + as String?, + isDefault: null == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool, + placeCount: null == placeCount + ? _value.placeCount + : placeCount // ignore: cast_nullable_to_non_nullable + as int, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$FolderModelImpl implements _FolderModel { + const _$FolderModelImpl({ + required this.id, + required this.name, + required this.visibility, + this.thumbnailUrl, + this.isDefault = false, + this.placeCount = 0, + this.createdAt, + }); + + factory _$FolderModelImpl.fromJson(Map json) => + _$$FolderModelImplFromJson(json); + + /// 폴더 ID + @override + final String id; + + /// 폴더 이름 + @override + final String name; + + /// 공개 설정 (PRIVATE / SHARED) + @override + final String visibility; + + /// 썸네일 URL + @override + final String? thumbnailUrl; + + /// 기본 폴더 여부 + @override + @JsonKey() + final bool isDefault; + + /// 폴더 내 장소 수 + @override + @JsonKey() + final int placeCount; + + /// 생성일시 + @override + final String? createdAt; + + @override + String toString() { + return 'FolderModel(id: $id, name: $name, visibility: $visibility, thumbnailUrl: $thumbnailUrl, isDefault: $isDefault, placeCount: $placeCount, createdAt: $createdAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FolderModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.visibility, visibility) || + other.visibility == visibility) && + (identical(other.thumbnailUrl, thumbnailUrl) || + other.thumbnailUrl == thumbnailUrl) && + (identical(other.isDefault, isDefault) || + other.isDefault == isDefault) && + (identical(other.placeCount, placeCount) || + other.placeCount == placeCount) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + name, + visibility, + thumbnailUrl, + isDefault, + placeCount, + createdAt, + ); + + /// Create a copy of FolderModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$FolderModelImplCopyWith<_$FolderModelImpl> get copyWith => + __$$FolderModelImplCopyWithImpl<_$FolderModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$FolderModelImplToJson(this); + } +} + +abstract class _FolderModel implements FolderModel { + const factory _FolderModel({ + required final String id, + required final String name, + required final String visibility, + final String? thumbnailUrl, + final bool isDefault, + final int placeCount, + final String? createdAt, + }) = _$FolderModelImpl; + + factory _FolderModel.fromJson(Map json) = + _$FolderModelImpl.fromJson; + + /// 폴더 ID + @override + String get id; + + /// 폴더 이름 + @override + String get name; + + /// 공개 설정 (PRIVATE / SHARED) + @override + String get visibility; + + /// 썸네일 URL + @override + String? get thumbnailUrl; + + /// 기본 폴더 여부 + @override + bool get isDefault; + + /// 폴더 내 장소 수 + @override + int get placeCount; + + /// 생성일시 + @override + String? get createdAt; + + /// Create a copy of FolderModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$FolderModelImplCopyWith<_$FolderModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +GetFoldersResponse _$GetFoldersResponseFromJson(Map json) { + return _GetFoldersResponse.fromJson(json); +} + +/// @nodoc +mixin _$GetFoldersResponse { + List get folders => throw _privateConstructorUsedError; + + /// Serializes this GetFoldersResponse to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of GetFoldersResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $GetFoldersResponseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GetFoldersResponseCopyWith<$Res> { + factory $GetFoldersResponseCopyWith( + GetFoldersResponse value, + $Res Function(GetFoldersResponse) then, + ) = _$GetFoldersResponseCopyWithImpl<$Res, GetFoldersResponse>; + @useResult + $Res call({List folders}); +} + +/// @nodoc +class _$GetFoldersResponseCopyWithImpl<$Res, $Val extends GetFoldersResponse> + implements $GetFoldersResponseCopyWith<$Res> { + _$GetFoldersResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of GetFoldersResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? folders = null}) { + return _then( + _value.copyWith( + folders: null == folders + ? _value.folders + : folders // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$GetFoldersResponseImplCopyWith<$Res> + implements $GetFoldersResponseCopyWith<$Res> { + factory _$$GetFoldersResponseImplCopyWith( + _$GetFoldersResponseImpl value, + $Res Function(_$GetFoldersResponseImpl) then, + ) = __$$GetFoldersResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List folders}); +} + +/// @nodoc +class __$$GetFoldersResponseImplCopyWithImpl<$Res> + extends _$GetFoldersResponseCopyWithImpl<$Res, _$GetFoldersResponseImpl> + implements _$$GetFoldersResponseImplCopyWith<$Res> { + __$$GetFoldersResponseImplCopyWithImpl( + _$GetFoldersResponseImpl _value, + $Res Function(_$GetFoldersResponseImpl) _then, + ) : super(_value, _then); + + /// Create a copy of GetFoldersResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? folders = null}) { + return _then( + _$GetFoldersResponseImpl( + folders: null == folders + ? _value._folders + : folders // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$GetFoldersResponseImpl implements _GetFoldersResponse { + const _$GetFoldersResponseImpl({final List folders = const []}) + : _folders = folders; + + factory _$GetFoldersResponseImpl.fromJson(Map json) => + _$$GetFoldersResponseImplFromJson(json); + + final List _folders; + @override + @JsonKey() + List get folders { + if (_folders is EqualUnmodifiableListView) return _folders; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_folders); + } + + @override + String toString() { + return 'GetFoldersResponse(folders: $folders)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GetFoldersResponseImpl && + const DeepCollectionEquality().equals(other._folders, _folders)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(_folders)); + + /// Create a copy of GetFoldersResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GetFoldersResponseImplCopyWith<_$GetFoldersResponseImpl> get copyWith => + __$$GetFoldersResponseImplCopyWithImpl<_$GetFoldersResponseImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$GetFoldersResponseImplToJson(this); + } +} + +abstract class _GetFoldersResponse implements GetFoldersResponse { + const factory _GetFoldersResponse({final List folders}) = + _$GetFoldersResponseImpl; + + factory _GetFoldersResponse.fromJson(Map json) = + _$GetFoldersResponseImpl.fromJson; + + @override + List get folders; + + /// Create a copy of GetFoldersResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GetFoldersResponseImplCopyWith<_$GetFoldersResponseImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/saved_places/data/models/folder_model.g.dart b/lib/features/saved_places/data/models/folder_model.g.dart new file mode 100644 index 0000000..6350c84 --- /dev/null +++ b/lib/features/saved_places/data/models/folder_model.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'folder_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$FolderModelImpl _$$FolderModelImplFromJson(Map json) => + _$FolderModelImpl( + id: json['id'] as String, + name: json['name'] as String, + visibility: json['visibility'] as String, + thumbnailUrl: json['thumbnailUrl'] as String?, + isDefault: json['isDefault'] as bool? ?? false, + placeCount: (json['placeCount'] as num?)?.toInt() ?? 0, + createdAt: json['createdAt'] as String?, + ); + +Map _$$FolderModelImplToJson(_$FolderModelImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'visibility': instance.visibility, + 'thumbnailUrl': instance.thumbnailUrl, + 'isDefault': instance.isDefault, + 'placeCount': instance.placeCount, + 'createdAt': instance.createdAt, + }; + +_$GetFoldersResponseImpl _$$GetFoldersResponseImplFromJson( + Map json, +) => _$GetFoldersResponseImpl( + folders: + (json['folders'] as List?) + ?.map((e) => FolderModel.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$$GetFoldersResponseImplToJson( + _$GetFoldersResponseImpl instance, +) => {'folders': instance.folders}; diff --git a/lib/features/saved_places/data/models/folder_place_model.dart b/lib/features/saved_places/data/models/folder_place_model.dart new file mode 100644 index 0000000..c155694 --- /dev/null +++ b/lib/features/saved_places/data/models/folder_place_model.dart @@ -0,0 +1,45 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../../../common/models/place_model.dart'; + +part 'folder_place_model.freezed.dart'; +part 'folder_place_model.g.dart'; + +/// 폴더 내 장소 목록 응답 +@freezed +class GetFolderPlacesResponse with _$GetFolderPlacesResponse { + const factory GetFolderPlacesResponse({ + required String folderId, + required String folderName, + @Default([]) List places, + }) = _GetFolderPlacesResponse; + + factory GetFolderPlacesResponse.fromJson(Map json) => + _$GetFolderPlacesResponseFromJson(json); +} + +/// 폴더에 장소 추가 요청 DTO +@freezed +class AddFolderPlaceRequest with _$AddFolderPlaceRequest { + const factory AddFolderPlaceRequest({ + required String placeId, + }) = _AddFolderPlaceRequest; + + factory AddFolderPlaceRequest.fromJson(Map json) => + _$AddFolderPlaceRequestFromJson(json); +} + +/// 폴더에 장소 추가 응답 DTO +@freezed +class AddFolderPlaceResponse with _$AddFolderPlaceResponse { + const factory AddFolderPlaceResponse({ + required String id, + required String folderId, + required String placeId, + int? position, + String? createdAt, + }) = _AddFolderPlaceResponse; + + factory AddFolderPlaceResponse.fromJson(Map json) => + _$AddFolderPlaceResponseFromJson(json); +} diff --git a/lib/features/saved_places/data/models/folder_place_model.freezed.dart b/lib/features/saved_places/data/models/folder_place_model.freezed.dart new file mode 100644 index 0000000..65c3036 --- /dev/null +++ b/lib/features/saved_places/data/models/folder_place_model.freezed.dart @@ -0,0 +1,655 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'folder_place_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +GetFolderPlacesResponse _$GetFolderPlacesResponseFromJson( + Map json, +) { + return _GetFolderPlacesResponse.fromJson(json); +} + +/// @nodoc +mixin _$GetFolderPlacesResponse { + String get folderId => throw _privateConstructorUsedError; + String get folderName => throw _privateConstructorUsedError; + List get places => throw _privateConstructorUsedError; + + /// Serializes this GetFolderPlacesResponse to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of GetFolderPlacesResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $GetFolderPlacesResponseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GetFolderPlacesResponseCopyWith<$Res> { + factory $GetFolderPlacesResponseCopyWith( + GetFolderPlacesResponse value, + $Res Function(GetFolderPlacesResponse) then, + ) = _$GetFolderPlacesResponseCopyWithImpl<$Res, GetFolderPlacesResponse>; + @useResult + $Res call({String folderId, String folderName, List places}); +} + +/// @nodoc +class _$GetFolderPlacesResponseCopyWithImpl< + $Res, + $Val extends GetFolderPlacesResponse +> + implements $GetFolderPlacesResponseCopyWith<$Res> { + _$GetFolderPlacesResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of GetFolderPlacesResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? folderId = null, + Object? folderName = null, + Object? places = null, + }) { + return _then( + _value.copyWith( + folderId: null == folderId + ? _value.folderId + : folderId // ignore: cast_nullable_to_non_nullable + as String, + folderName: null == folderName + ? _value.folderName + : folderName // ignore: cast_nullable_to_non_nullable + as String, + places: null == places + ? _value.places + : places // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$GetFolderPlacesResponseImplCopyWith<$Res> + implements $GetFolderPlacesResponseCopyWith<$Res> { + factory _$$GetFolderPlacesResponseImplCopyWith( + _$GetFolderPlacesResponseImpl value, + $Res Function(_$GetFolderPlacesResponseImpl) then, + ) = __$$GetFolderPlacesResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String folderId, String folderName, List places}); +} + +/// @nodoc +class __$$GetFolderPlacesResponseImplCopyWithImpl<$Res> + extends + _$GetFolderPlacesResponseCopyWithImpl< + $Res, + _$GetFolderPlacesResponseImpl + > + implements _$$GetFolderPlacesResponseImplCopyWith<$Res> { + __$$GetFolderPlacesResponseImplCopyWithImpl( + _$GetFolderPlacesResponseImpl _value, + $Res Function(_$GetFolderPlacesResponseImpl) _then, + ) : super(_value, _then); + + /// Create a copy of GetFolderPlacesResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? folderId = null, + Object? folderName = null, + Object? places = null, + }) { + return _then( + _$GetFolderPlacesResponseImpl( + folderId: null == folderId + ? _value.folderId + : folderId // ignore: cast_nullable_to_non_nullable + as String, + folderName: null == folderName + ? _value.folderName + : folderName // ignore: cast_nullable_to_non_nullable + as String, + places: null == places + ? _value._places + : places // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$GetFolderPlacesResponseImpl implements _GetFolderPlacesResponse { + const _$GetFolderPlacesResponseImpl({ + required this.folderId, + required this.folderName, + final List places = const [], + }) : _places = places; + + factory _$GetFolderPlacesResponseImpl.fromJson(Map json) => + _$$GetFolderPlacesResponseImplFromJson(json); + + @override + final String folderId; + @override + final String folderName; + final List _places; + @override + @JsonKey() + List get places { + if (_places is EqualUnmodifiableListView) return _places; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_places); + } + + @override + String toString() { + return 'GetFolderPlacesResponse(folderId: $folderId, folderName: $folderName, places: $places)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GetFolderPlacesResponseImpl && + (identical(other.folderId, folderId) || + other.folderId == folderId) && + (identical(other.folderName, folderName) || + other.folderName == folderName) && + const DeepCollectionEquality().equals(other._places, _places)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + folderId, + folderName, + const DeepCollectionEquality().hash(_places), + ); + + /// Create a copy of GetFolderPlacesResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GetFolderPlacesResponseImplCopyWith<_$GetFolderPlacesResponseImpl> + get copyWith => + __$$GetFolderPlacesResponseImplCopyWithImpl< + _$GetFolderPlacesResponseImpl + >(this, _$identity); + + @override + Map toJson() { + return _$$GetFolderPlacesResponseImplToJson(this); + } +} + +abstract class _GetFolderPlacesResponse implements GetFolderPlacesResponse { + const factory _GetFolderPlacesResponse({ + required final String folderId, + required final String folderName, + final List places, + }) = _$GetFolderPlacesResponseImpl; + + factory _GetFolderPlacesResponse.fromJson(Map json) = + _$GetFolderPlacesResponseImpl.fromJson; + + @override + String get folderId; + @override + String get folderName; + @override + List get places; + + /// Create a copy of GetFolderPlacesResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GetFolderPlacesResponseImplCopyWith<_$GetFolderPlacesResponseImpl> + get copyWith => throw _privateConstructorUsedError; +} + +AddFolderPlaceRequest _$AddFolderPlaceRequestFromJson( + Map json, +) { + return _AddFolderPlaceRequest.fromJson(json); +} + +/// @nodoc +mixin _$AddFolderPlaceRequest { + String get placeId => throw _privateConstructorUsedError; + + /// Serializes this AddFolderPlaceRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AddFolderPlaceRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AddFolderPlaceRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AddFolderPlaceRequestCopyWith<$Res> { + factory $AddFolderPlaceRequestCopyWith( + AddFolderPlaceRequest value, + $Res Function(AddFolderPlaceRequest) then, + ) = _$AddFolderPlaceRequestCopyWithImpl<$Res, AddFolderPlaceRequest>; + @useResult + $Res call({String placeId}); +} + +/// @nodoc +class _$AddFolderPlaceRequestCopyWithImpl< + $Res, + $Val extends AddFolderPlaceRequest +> + implements $AddFolderPlaceRequestCopyWith<$Res> { + _$AddFolderPlaceRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AddFolderPlaceRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? placeId = null}) { + return _then( + _value.copyWith( + placeId: null == placeId + ? _value.placeId + : placeId // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AddFolderPlaceRequestImplCopyWith<$Res> + implements $AddFolderPlaceRequestCopyWith<$Res> { + factory _$$AddFolderPlaceRequestImplCopyWith( + _$AddFolderPlaceRequestImpl value, + $Res Function(_$AddFolderPlaceRequestImpl) then, + ) = __$$AddFolderPlaceRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String placeId}); +} + +/// @nodoc +class __$$AddFolderPlaceRequestImplCopyWithImpl<$Res> + extends + _$AddFolderPlaceRequestCopyWithImpl<$Res, _$AddFolderPlaceRequestImpl> + implements _$$AddFolderPlaceRequestImplCopyWith<$Res> { + __$$AddFolderPlaceRequestImplCopyWithImpl( + _$AddFolderPlaceRequestImpl _value, + $Res Function(_$AddFolderPlaceRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AddFolderPlaceRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? placeId = null}) { + return _then( + _$AddFolderPlaceRequestImpl( + placeId: null == placeId + ? _value.placeId + : placeId // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AddFolderPlaceRequestImpl implements _AddFolderPlaceRequest { + const _$AddFolderPlaceRequestImpl({required this.placeId}); + + factory _$AddFolderPlaceRequestImpl.fromJson(Map json) => + _$$AddFolderPlaceRequestImplFromJson(json); + + @override + final String placeId; + + @override + String toString() { + return 'AddFolderPlaceRequest(placeId: $placeId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AddFolderPlaceRequestImpl && + (identical(other.placeId, placeId) || other.placeId == placeId)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, placeId); + + /// Create a copy of AddFolderPlaceRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AddFolderPlaceRequestImplCopyWith<_$AddFolderPlaceRequestImpl> + get copyWith => + __$$AddFolderPlaceRequestImplCopyWithImpl<_$AddFolderPlaceRequestImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AddFolderPlaceRequestImplToJson(this); + } +} + +abstract class _AddFolderPlaceRequest implements AddFolderPlaceRequest { + const factory _AddFolderPlaceRequest({required final String placeId}) = + _$AddFolderPlaceRequestImpl; + + factory _AddFolderPlaceRequest.fromJson(Map json) = + _$AddFolderPlaceRequestImpl.fromJson; + + @override + String get placeId; + + /// Create a copy of AddFolderPlaceRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AddFolderPlaceRequestImplCopyWith<_$AddFolderPlaceRequestImpl> + get copyWith => throw _privateConstructorUsedError; +} + +AddFolderPlaceResponse _$AddFolderPlaceResponseFromJson( + Map json, +) { + return _AddFolderPlaceResponse.fromJson(json); +} + +/// @nodoc +mixin _$AddFolderPlaceResponse { + String get id => throw _privateConstructorUsedError; + String get folderId => throw _privateConstructorUsedError; + String get placeId => throw _privateConstructorUsedError; + int? get position => throw _privateConstructorUsedError; + String? get createdAt => throw _privateConstructorUsedError; + + /// Serializes this AddFolderPlaceResponse to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AddFolderPlaceResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AddFolderPlaceResponseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AddFolderPlaceResponseCopyWith<$Res> { + factory $AddFolderPlaceResponseCopyWith( + AddFolderPlaceResponse value, + $Res Function(AddFolderPlaceResponse) then, + ) = _$AddFolderPlaceResponseCopyWithImpl<$Res, AddFolderPlaceResponse>; + @useResult + $Res call({ + String id, + String folderId, + String placeId, + int? position, + String? createdAt, + }); +} + +/// @nodoc +class _$AddFolderPlaceResponseCopyWithImpl< + $Res, + $Val extends AddFolderPlaceResponse +> + implements $AddFolderPlaceResponseCopyWith<$Res> { + _$AddFolderPlaceResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AddFolderPlaceResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? folderId = null, + Object? placeId = null, + Object? position = freezed, + Object? createdAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + folderId: null == folderId + ? _value.folderId + : folderId // ignore: cast_nullable_to_non_nullable + as String, + placeId: null == placeId + ? _value.placeId + : placeId // ignore: cast_nullable_to_non_nullable + as String, + position: freezed == position + ? _value.position + : position // ignore: cast_nullable_to_non_nullable + as int?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AddFolderPlaceResponseImplCopyWith<$Res> + implements $AddFolderPlaceResponseCopyWith<$Res> { + factory _$$AddFolderPlaceResponseImplCopyWith( + _$AddFolderPlaceResponseImpl value, + $Res Function(_$AddFolderPlaceResponseImpl) then, + ) = __$$AddFolderPlaceResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String folderId, + String placeId, + int? position, + String? createdAt, + }); +} + +/// @nodoc +class __$$AddFolderPlaceResponseImplCopyWithImpl<$Res> + extends + _$AddFolderPlaceResponseCopyWithImpl<$Res, _$AddFolderPlaceResponseImpl> + implements _$$AddFolderPlaceResponseImplCopyWith<$Res> { + __$$AddFolderPlaceResponseImplCopyWithImpl( + _$AddFolderPlaceResponseImpl _value, + $Res Function(_$AddFolderPlaceResponseImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AddFolderPlaceResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? folderId = null, + Object? placeId = null, + Object? position = freezed, + Object? createdAt = freezed, + }) { + return _then( + _$AddFolderPlaceResponseImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + folderId: null == folderId + ? _value.folderId + : folderId // ignore: cast_nullable_to_non_nullable + as String, + placeId: null == placeId + ? _value.placeId + : placeId // ignore: cast_nullable_to_non_nullable + as String, + position: freezed == position + ? _value.position + : position // ignore: cast_nullable_to_non_nullable + as int?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AddFolderPlaceResponseImpl implements _AddFolderPlaceResponse { + const _$AddFolderPlaceResponseImpl({ + required this.id, + required this.folderId, + required this.placeId, + this.position, + this.createdAt, + }); + + factory _$AddFolderPlaceResponseImpl.fromJson(Map json) => + _$$AddFolderPlaceResponseImplFromJson(json); + + @override + final String id; + @override + final String folderId; + @override + final String placeId; + @override + final int? position; + @override + final String? createdAt; + + @override + String toString() { + return 'AddFolderPlaceResponse(id: $id, folderId: $folderId, placeId: $placeId, position: $position, createdAt: $createdAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AddFolderPlaceResponseImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.folderId, folderId) || + other.folderId == folderId) && + (identical(other.placeId, placeId) || other.placeId == placeId) && + (identical(other.position, position) || + other.position == position) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, id, folderId, placeId, position, createdAt); + + /// Create a copy of AddFolderPlaceResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AddFolderPlaceResponseImplCopyWith<_$AddFolderPlaceResponseImpl> + get copyWith => + __$$AddFolderPlaceResponseImplCopyWithImpl<_$AddFolderPlaceResponseImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AddFolderPlaceResponseImplToJson(this); + } +} + +abstract class _AddFolderPlaceResponse implements AddFolderPlaceResponse { + const factory _AddFolderPlaceResponse({ + required final String id, + required final String folderId, + required final String placeId, + final int? position, + final String? createdAt, + }) = _$AddFolderPlaceResponseImpl; + + factory _AddFolderPlaceResponse.fromJson(Map json) = + _$AddFolderPlaceResponseImpl.fromJson; + + @override + String get id; + @override + String get folderId; + @override + String get placeId; + @override + int? get position; + @override + String? get createdAt; + + /// Create a copy of AddFolderPlaceResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AddFolderPlaceResponseImplCopyWith<_$AddFolderPlaceResponseImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/features/saved_places/data/models/folder_place_model.g.dart b/lib/features/saved_places/data/models/folder_place_model.g.dart new file mode 100644 index 0000000..a491294 --- /dev/null +++ b/lib/features/saved_places/data/models/folder_place_model.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'folder_place_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$GetFolderPlacesResponseImpl _$$GetFolderPlacesResponseImplFromJson( + Map json, +) => _$GetFolderPlacesResponseImpl( + folderId: json['folderId'] as String, + folderName: json['folderName'] as String, + places: + (json['places'] as List?) + ?.map((e) => PlaceModel.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$$GetFolderPlacesResponseImplToJson( + _$GetFolderPlacesResponseImpl instance, +) => { + 'folderId': instance.folderId, + 'folderName': instance.folderName, + 'places': instance.places, +}; + +_$AddFolderPlaceRequestImpl _$$AddFolderPlaceRequestImplFromJson( + Map json, +) => _$AddFolderPlaceRequestImpl(placeId: json['placeId'] as String); + +Map _$$AddFolderPlaceRequestImplToJson( + _$AddFolderPlaceRequestImpl instance, +) => {'placeId': instance.placeId}; + +_$AddFolderPlaceResponseImpl _$$AddFolderPlaceResponseImplFromJson( + Map json, +) => _$AddFolderPlaceResponseImpl( + id: json['id'] as String, + folderId: json['folderId'] as String, + placeId: json['placeId'] as String, + position: (json['position'] as num?)?.toInt(), + createdAt: json['createdAt'] as String?, +); + +Map _$$AddFolderPlaceResponseImplToJson( + _$AddFolderPlaceResponseImpl instance, +) => { + 'id': instance.id, + 'folderId': instance.folderId, + 'placeId': instance.placeId, + 'position': instance.position, + 'createdAt': instance.createdAt, +}; diff --git a/lib/features/saved_places/data/models/update_folder_request.dart b/lib/features/saved_places/data/models/update_folder_request.dart new file mode 100644 index 0000000..ef3e2c0 --- /dev/null +++ b/lib/features/saved_places/data/models/update_folder_request.dart @@ -0,0 +1,34 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'update_folder_request.freezed.dart'; +part 'update_folder_request.g.dart'; + +/// 폴더 수정 요청 DTO +@freezed +class UpdateFolderRequest with _$UpdateFolderRequest { + const factory UpdateFolderRequest({ + /// 폴더 이름 (선택) + String? name, + + /// 공개 설정 (선택) + String? visibility, + }) = _UpdateFolderRequest; + + factory UpdateFolderRequest.fromJson(Map json) => + _$UpdateFolderRequestFromJson(json); +} + +/// 폴더 수정 응답 DTO +@freezed +class UpdateFolderResponse with _$UpdateFolderResponse { + const factory UpdateFolderResponse({ + required String id, + required String name, + required String visibility, + @Default(false) bool isDefault, + String? updatedAt, + }) = _UpdateFolderResponse; + + factory UpdateFolderResponse.fromJson(Map json) => + _$UpdateFolderResponseFromJson(json); +} diff --git a/lib/features/saved_places/data/models/update_folder_request.freezed.dart b/lib/features/saved_places/data/models/update_folder_request.freezed.dart new file mode 100644 index 0000000..596823d --- /dev/null +++ b/lib/features/saved_places/data/models/update_folder_request.freezed.dart @@ -0,0 +1,452 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'update_folder_request.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +UpdateFolderRequest _$UpdateFolderRequestFromJson(Map json) { + return _UpdateFolderRequest.fromJson(json); +} + +/// @nodoc +mixin _$UpdateFolderRequest { + /// 폴더 이름 (선택) + String? get name => throw _privateConstructorUsedError; + + /// 공개 설정 (선택) + String? get visibility => throw _privateConstructorUsedError; + + /// Serializes this UpdateFolderRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UpdateFolderRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UpdateFolderRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UpdateFolderRequestCopyWith<$Res> { + factory $UpdateFolderRequestCopyWith( + UpdateFolderRequest value, + $Res Function(UpdateFolderRequest) then, + ) = _$UpdateFolderRequestCopyWithImpl<$Res, UpdateFolderRequest>; + @useResult + $Res call({String? name, String? visibility}); +} + +/// @nodoc +class _$UpdateFolderRequestCopyWithImpl<$Res, $Val extends UpdateFolderRequest> + implements $UpdateFolderRequestCopyWith<$Res> { + _$UpdateFolderRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UpdateFolderRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? name = freezed, Object? visibility = freezed}) { + return _then( + _value.copyWith( + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + visibility: freezed == visibility + ? _value.visibility + : visibility // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UpdateFolderRequestImplCopyWith<$Res> + implements $UpdateFolderRequestCopyWith<$Res> { + factory _$$UpdateFolderRequestImplCopyWith( + _$UpdateFolderRequestImpl value, + $Res Function(_$UpdateFolderRequestImpl) then, + ) = __$$UpdateFolderRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String? name, String? visibility}); +} + +/// @nodoc +class __$$UpdateFolderRequestImplCopyWithImpl<$Res> + extends _$UpdateFolderRequestCopyWithImpl<$Res, _$UpdateFolderRequestImpl> + implements _$$UpdateFolderRequestImplCopyWith<$Res> { + __$$UpdateFolderRequestImplCopyWithImpl( + _$UpdateFolderRequestImpl _value, + $Res Function(_$UpdateFolderRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of UpdateFolderRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? name = freezed, Object? visibility = freezed}) { + return _then( + _$UpdateFolderRequestImpl( + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + visibility: freezed == visibility + ? _value.visibility + : visibility // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$UpdateFolderRequestImpl implements _UpdateFolderRequest { + const _$UpdateFolderRequestImpl({this.name, this.visibility}); + + factory _$UpdateFolderRequestImpl.fromJson(Map json) => + _$$UpdateFolderRequestImplFromJson(json); + + /// 폴더 이름 (선택) + @override + final String? name; + + /// 공개 설정 (선택) + @override + final String? visibility; + + @override + String toString() { + return 'UpdateFolderRequest(name: $name, visibility: $visibility)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UpdateFolderRequestImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.visibility, visibility) || + other.visibility == visibility)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, name, visibility); + + /// Create a copy of UpdateFolderRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UpdateFolderRequestImplCopyWith<_$UpdateFolderRequestImpl> get copyWith => + __$$UpdateFolderRequestImplCopyWithImpl<_$UpdateFolderRequestImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$UpdateFolderRequestImplToJson(this); + } +} + +abstract class _UpdateFolderRequest implements UpdateFolderRequest { + const factory _UpdateFolderRequest({ + final String? name, + final String? visibility, + }) = _$UpdateFolderRequestImpl; + + factory _UpdateFolderRequest.fromJson(Map json) = + _$UpdateFolderRequestImpl.fromJson; + + /// 폴더 이름 (선택) + @override + String? get name; + + /// 공개 설정 (선택) + @override + String? get visibility; + + /// Create a copy of UpdateFolderRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UpdateFolderRequestImplCopyWith<_$UpdateFolderRequestImpl> get copyWith => + throw _privateConstructorUsedError; +} + +UpdateFolderResponse _$UpdateFolderResponseFromJson(Map json) { + return _UpdateFolderResponse.fromJson(json); +} + +/// @nodoc +mixin _$UpdateFolderResponse { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get visibility => throw _privateConstructorUsedError; + bool get isDefault => throw _privateConstructorUsedError; + String? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this UpdateFolderResponse to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UpdateFolderResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UpdateFolderResponseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UpdateFolderResponseCopyWith<$Res> { + factory $UpdateFolderResponseCopyWith( + UpdateFolderResponse value, + $Res Function(UpdateFolderResponse) then, + ) = _$UpdateFolderResponseCopyWithImpl<$Res, UpdateFolderResponse>; + @useResult + $Res call({ + String id, + String name, + String visibility, + bool isDefault, + String? updatedAt, + }); +} + +/// @nodoc +class _$UpdateFolderResponseCopyWithImpl< + $Res, + $Val extends UpdateFolderResponse +> + implements $UpdateFolderResponseCopyWith<$Res> { + _$UpdateFolderResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UpdateFolderResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? visibility = null, + Object? isDefault = null, + Object? updatedAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + visibility: null == visibility + ? _value.visibility + : visibility // ignore: cast_nullable_to_non_nullable + as String, + isDefault: null == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UpdateFolderResponseImplCopyWith<$Res> + implements $UpdateFolderResponseCopyWith<$Res> { + factory _$$UpdateFolderResponseImplCopyWith( + _$UpdateFolderResponseImpl value, + $Res Function(_$UpdateFolderResponseImpl) then, + ) = __$$UpdateFolderResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String name, + String visibility, + bool isDefault, + String? updatedAt, + }); +} + +/// @nodoc +class __$$UpdateFolderResponseImplCopyWithImpl<$Res> + extends _$UpdateFolderResponseCopyWithImpl<$Res, _$UpdateFolderResponseImpl> + implements _$$UpdateFolderResponseImplCopyWith<$Res> { + __$$UpdateFolderResponseImplCopyWithImpl( + _$UpdateFolderResponseImpl _value, + $Res Function(_$UpdateFolderResponseImpl) _then, + ) : super(_value, _then); + + /// Create a copy of UpdateFolderResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? visibility = null, + Object? isDefault = null, + Object? updatedAt = freezed, + }) { + return _then( + _$UpdateFolderResponseImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + visibility: null == visibility + ? _value.visibility + : visibility // ignore: cast_nullable_to_non_nullable + as String, + isDefault: null == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$UpdateFolderResponseImpl implements _UpdateFolderResponse { + const _$UpdateFolderResponseImpl({ + required this.id, + required this.name, + required this.visibility, + this.isDefault = false, + this.updatedAt, + }); + + factory _$UpdateFolderResponseImpl.fromJson(Map json) => + _$$UpdateFolderResponseImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + final String visibility; + @override + @JsonKey() + final bool isDefault; + @override + final String? updatedAt; + + @override + String toString() { + return 'UpdateFolderResponse(id: $id, name: $name, visibility: $visibility, isDefault: $isDefault, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UpdateFolderResponseImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.visibility, visibility) || + other.visibility == visibility) && + (identical(other.isDefault, isDefault) || + other.isDefault == isDefault) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, id, name, visibility, isDefault, updatedAt); + + /// Create a copy of UpdateFolderResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UpdateFolderResponseImplCopyWith<_$UpdateFolderResponseImpl> + get copyWith => + __$$UpdateFolderResponseImplCopyWithImpl<_$UpdateFolderResponseImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$UpdateFolderResponseImplToJson(this); + } +} + +abstract class _UpdateFolderResponse implements UpdateFolderResponse { + const factory _UpdateFolderResponse({ + required final String id, + required final String name, + required final String visibility, + final bool isDefault, + final String? updatedAt, + }) = _$UpdateFolderResponseImpl; + + factory _UpdateFolderResponse.fromJson(Map json) = + _$UpdateFolderResponseImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + String get visibility; + @override + bool get isDefault; + @override + String? get updatedAt; + + /// Create a copy of UpdateFolderResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UpdateFolderResponseImplCopyWith<_$UpdateFolderResponseImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/features/saved_places/data/models/update_folder_request.g.dart b/lib/features/saved_places/data/models/update_folder_request.g.dart new file mode 100644 index 0000000..1151872 --- /dev/null +++ b/lib/features/saved_places/data/models/update_folder_request.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'update_folder_request.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$UpdateFolderRequestImpl _$$UpdateFolderRequestImplFromJson( + Map json, +) => _$UpdateFolderRequestImpl( + name: json['name'] as String?, + visibility: json['visibility'] as String?, +); + +Map _$$UpdateFolderRequestImplToJson( + _$UpdateFolderRequestImpl instance, +) => { + 'name': instance.name, + 'visibility': instance.visibility, +}; + +_$UpdateFolderResponseImpl _$$UpdateFolderResponseImplFromJson( + Map json, +) => _$UpdateFolderResponseImpl( + id: json['id'] as String, + name: json['name'] as String, + visibility: json['visibility'] as String, + isDefault: json['isDefault'] as bool? ?? false, + updatedAt: json['updatedAt'] as String?, +); + +Map _$$UpdateFolderResponseImplToJson( + _$UpdateFolderResponseImpl instance, +) => { + 'id': instance.id, + 'name': instance.name, + 'visibility': instance.visibility, + 'isDefault': instance.isDefault, + 'updatedAt': instance.updatedAt, +}; diff --git a/lib/features/saved_places/data/saved_places_remote_datasource.dart b/lib/features/saved_places/data/saved_places_remote_datasource.dart new file mode 100644 index 0000000..7190aa6 --- /dev/null +++ b/lib/features/saved_places/data/saved_places_remote_datasource.dart @@ -0,0 +1,132 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../../common/constants/api_endpoints.dart'; +import '../../../common/services/api_client.dart'; +import 'models/create_folder_request.dart'; +import 'models/folder_model.dart'; +import 'models/folder_place_model.dart'; +import 'models/update_folder_request.dart'; + +part 'saved_places_remote_datasource.g.dart'; + +@riverpod +SavedPlacesRemoteDataSource savedPlacesRemoteDataSource(Ref ref) { + final dio = ref.watch(dioProvider); + return SavedPlacesRemoteDataSource(dio); +} + +/// 저장 장소 / 폴더 Remote DataSource +class SavedPlacesRemoteDataSource { + final Dio _dio; + + SavedPlacesRemoteDataSource(this._dio); + + /// 폴더 목록 조회 + /// GET /api/folders + Future getFolders() async { + debugPrint('📤 SavedPlaces: Fetching folders'); + + final response = await _dio.get(ApiEndpoints.folders); + + final result = GetFoldersResponse.fromJson( + response.data as Map, + ); + debugPrint('✅ Folders fetched: ${result.folders.length}개'); + return result; + } + + /// 폴더 생성 + /// POST /api/folders + Future createFolder(CreateFolderRequest request) async { + debugPrint('📤 SavedPlaces: Creating folder: ${request.name}'); + + final response = await _dio.post( + ApiEndpoints.folders, + data: request.toJson(), + ); + + final result = CreateFolderResponse.fromJson( + response.data as Map, + ); + debugPrint('✅ Folder created: id=${result.id}'); + return result; + } + + /// 폴더 수정 + /// PUT /api/folders/{folderId} + Future updateFolder( + String folderId, + UpdateFolderRequest request, + ) async { + debugPrint('📤 SavedPlaces: Updating folder: $folderId'); + + final response = await _dio.put( + ApiEndpoints.folderDetail(folderId), + data: request.toJson(), + ); + + final result = UpdateFolderResponse.fromJson( + response.data as Map, + ); + debugPrint('✅ Folder updated: id=${result.id}'); + return result; + } + + /// 폴더 삭제 + /// DELETE /api/folders/{folderId} + Future deleteFolder(String folderId) async { + debugPrint('📤 SavedPlaces: Deleting folder: $folderId'); + + await _dio.delete(ApiEndpoints.folderDetail(folderId)); + + debugPrint('✅ Folder deleted: $folderId'); + } + + /// 폴더 내 장소 목록 조회 + /// GET /api/folders/{folderId}/places + Future getFolderPlaces(String folderId) async { + debugPrint('📤 SavedPlaces: Fetching places for folder: $folderId'); + + final response = await _dio.get(ApiEndpoints.folderPlaces(folderId)); + + final result = GetFolderPlacesResponse.fromJson( + response.data as Map, + ); + debugPrint('✅ Folder places fetched: ${result.places.length}개'); + return result; + } + + /// 폴더에 장소 추가 + /// POST /api/folders/{folderId}/places + Future addPlaceToFolder( + String folderId, + String placeId, + ) async { + debugPrint('📤 SavedPlaces: Adding place $placeId to folder $folderId'); + + final request = AddFolderPlaceRequest(placeId: placeId); + final response = await _dio.post( + ApiEndpoints.folderPlaces(folderId), + data: request.toJson(), + ); + + final result = AddFolderPlaceResponse.fromJson( + response.data as Map, + ); + debugPrint('✅ Place added to folder: ${result.id}'); + return result; + } + + /// 폴더에서 장소 제거 + /// DELETE /api/folders/{folderId}/places/{placeId} + Future removePlaceFromFolder(String folderId, String placeId) async { + debugPrint('📤 SavedPlaces: Removing place $placeId from folder $folderId'); + + await _dio.delete(ApiEndpoints.folderPlaceDetail(folderId, placeId)); + + debugPrint('✅ Place removed from folder'); + } +} diff --git a/lib/features/saved_places/data/saved_places_remote_datasource.g.dart b/lib/features/saved_places/data/saved_places_remote_datasource.g.dart new file mode 100644 index 0000000..f0dd229 --- /dev/null +++ b/lib/features/saved_places/data/saved_places_remote_datasource.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'saved_places_remote_datasource.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$savedPlacesRemoteDataSourceHash() => + r'28f153f44bfe47df788d7f5a9a1134bedc36184c'; + +/// See also [savedPlacesRemoteDataSource]. +@ProviderFor(savedPlacesRemoteDataSource) +final savedPlacesRemoteDataSourceProvider = + AutoDisposeProvider.internal( + savedPlacesRemoteDataSource, + name: r'savedPlacesRemoteDataSourceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$savedPlacesRemoteDataSourceHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef SavedPlacesRemoteDataSourceRef = + AutoDisposeProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/saved_places/data/saved_places_repository.dart b/lib/features/saved_places/data/saved_places_repository.dart new file mode 100644 index 0000000..a5688ef --- /dev/null +++ b/lib/features/saved_places/data/saved_places_repository.dart @@ -0,0 +1,34 @@ +import 'models/create_folder_request.dart'; +import 'models/folder_model.dart'; +import 'models/folder_place_model.dart'; +import 'models/update_folder_request.dart'; + +/// 저장 장소 / 폴더 Repository 인터페이스 +abstract class SavedPlacesRepository { + /// 폴더 목록 조회 + Future getFolders(); + + /// 폴더 생성 + Future createFolder(CreateFolderRequest request); + + /// 폴더 수정 + Future updateFolder( + String folderId, + UpdateFolderRequest request, + ); + + /// 폴더 삭제 + Future deleteFolder(String folderId); + + /// 폴더 내 장소 목록 조회 + Future getFolderPlaces(String folderId); + + /// 폴더에 장소 추가 + Future addPlaceToFolder( + String folderId, + String placeId, + ); + + /// 폴더에서 장소 제거 + Future removePlaceFromFolder(String folderId, String placeId); +} diff --git a/lib/features/saved_places/data/saved_places_repository_impl.dart b/lib/features/saved_places/data/saved_places_repository_impl.dart new file mode 100644 index 0000000..df5c4a1 --- /dev/null +++ b/lib/features/saved_places/data/saved_places_repository_impl.dart @@ -0,0 +1,75 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'saved_places_repository.dart'; +import 'saved_places_remote_datasource.dart'; +import 'models/create_folder_request.dart'; +import 'models/folder_model.dart'; +import 'models/folder_place_model.dart'; +import 'models/update_folder_request.dart'; + +part 'saved_places_repository_impl.g.dart'; + +@riverpod +SavedPlacesRepository savedPlacesRepository(Ref ref) { + final remoteDataSource = ref.watch(savedPlacesRemoteDataSourceProvider); + return SavedPlacesRepositoryImpl(remoteDataSource); +} + +/// 저장 장소 / 폴더 Repository 구현체 +class SavedPlacesRepositoryImpl implements SavedPlacesRepository { + final SavedPlacesRemoteDataSource _remoteDataSource; + + SavedPlacesRepositoryImpl(this._remoteDataSource); + + @override + Future getFolders() async { + debugPrint('📝 SavedPlacesRepo: Getting folders...'); + return await _remoteDataSource.getFolders(); + } + + @override + Future createFolder( + CreateFolderRequest request, + ) async { + debugPrint('📝 SavedPlacesRepo: Creating folder...'); + return await _remoteDataSource.createFolder(request); + } + + @override + Future updateFolder( + String folderId, + UpdateFolderRequest request, + ) async { + debugPrint('📝 SavedPlacesRepo: Updating folder...'); + return await _remoteDataSource.updateFolder(folderId, request); + } + + @override + Future deleteFolder(String folderId) async { + debugPrint('📝 SavedPlacesRepo: Deleting folder...'); + return await _remoteDataSource.deleteFolder(folderId); + } + + @override + Future getFolderPlaces(String folderId) async { + debugPrint('📝 SavedPlacesRepo: Getting folder places...'); + return await _remoteDataSource.getFolderPlaces(folderId); + } + + @override + Future addPlaceToFolder( + String folderId, + String placeId, + ) async { + debugPrint('📝 SavedPlacesRepo: Adding place to folder...'); + return await _remoteDataSource.addPlaceToFolder(folderId, placeId); + } + + @override + Future removePlaceFromFolder(String folderId, String placeId) async { + debugPrint('📝 SavedPlacesRepo: Removing place from folder...'); + return await _remoteDataSource.removePlaceFromFolder(folderId, placeId); + } +} diff --git a/lib/features/saved_places/data/saved_places_repository_impl.g.dart b/lib/features/saved_places/data/saved_places_repository_impl.g.dart new file mode 100644 index 0000000..274340a --- /dev/null +++ b/lib/features/saved_places/data/saved_places_repository_impl.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'saved_places_repository_impl.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$savedPlacesRepositoryHash() => + r'932dd9b16d8e3583cb0cc5557b6a29fb5e1393a8'; + +/// See also [savedPlacesRepository]. +@ProviderFor(savedPlacesRepository) +final savedPlacesRepositoryProvider = + AutoDisposeProvider.internal( + savedPlacesRepository, + name: r'savedPlacesRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$savedPlacesRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef SavedPlacesRepositoryRef = + AutoDisposeProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/saved_places/presentation/pages/saved_places_page.dart b/lib/features/saved_places/presentation/pages/saved_places_page.dart new file mode 100644 index 0000000..a22b31b --- /dev/null +++ b/lib/features/saved_places/presentation/pages/saved_places_page.dart @@ -0,0 +1,277 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../common/constants/home_colors.dart'; +import '../../../../common/constants/text_styles.dart'; +import '../../data/models/folder_model.dart'; +import '../saved_places_provider.dart'; +import '../widgets/create_folder_bottom_sheet.dart'; +import '../widgets/edit_folder_bottom_sheet.dart'; +import '../widgets/empty_folder_state.dart'; +import '../widgets/folder_place_card.dart'; +import '../widgets/folder_tab_bar.dart'; + +class SavedPlacesPage extends ConsumerStatefulWidget { + const SavedPlacesPage({super.key}); + + @override + ConsumerState createState() => _SavedPlacesPageState(); +} + +class _SavedPlacesPageState extends ConsumerState { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(savedPlacesNotifierProvider.notifier).loadFolders(); + }); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(savedPlacesNotifierProvider); + + return Scaffold( + backgroundColor: HomeColors.background, + appBar: AppBar( + backgroundColor: HomeColors.background, + elevation: 0, + scrolledUnderElevation: 0, + title: Text( + '내 저장 장소', + style: AppTextStyles.heading02.copyWith( + color: HomeColors.textPrimary, + ), + ), + actions: [ + // 폴더 추가 버튼 + IconButton( + onPressed: () => _onCreateFolder(), + icon: Icon( + Icons.create_new_folder_outlined, + color: HomeColors.iconPrimary, + size: 24.sp, + ), + ), + // 현재 폴더 편집 버튼 (기본 폴더 제외) + Builder(builder: (_) { + final folder = _selectedFolder(state); + if (folder == null || folder.isDefault) { + return const SizedBox.shrink(); + } + return IconButton( + onPressed: () => _onEditFolder(), + icon: Icon( + Icons.settings_outlined, + color: HomeColors.iconPrimary, + size: 24.sp, + ), + ); + }), + ], + ), + body: state.isFoldersLoading + ? const Center(child: CircularProgressIndicator()) + : state.foldersError != null + ? _buildErrorState(state.foldersError!) + : _buildContent(state), + ); + } + + Widget _buildContent(SavedPlacesState state) { + return Column( + children: [ + // 폴더 탭 바 + if (state.folders.isNotEmpty) + Padding( + padding: EdgeInsets.only(top: 8.h, bottom: 12.h), + child: FolderTabBar( + folders: state.folders, + selectedFolderId: state.selectedFolderId, + onFolderSelected: (folderId) { + ref + .read(savedPlacesNotifierProvider.notifier) + .selectFolder(folderId); + }, + ), + ), + + // 구분선 + Divider(height: 1, color: HomeColors.divider), + + // 장소 목록 + Expanded( + child: state.isPlacesLoading + ? const Center(child: CircularProgressIndicator()) + : state.placesError != null + ? _buildErrorState(state.placesError!) + : state.places.isEmpty + ? const EmptyFolderState() + : _buildPlaceList(state), + ), + ], + ); + } + + Widget _buildPlaceList(SavedPlacesState state) { + return ListView.builder( + padding: EdgeInsets.symmetric(vertical: 8.h), + itemCount: state.places.length, + itemBuilder: (context, index) { + final place = state.places[index]; + return FolderPlaceCard( + place: place, + onRemove: () => _onRemovePlace(place.placeId), + ); + }, + ); + } + + Widget _buildErrorState(String message) { + return Center( + child: Padding( + padding: EdgeInsets.all(20.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48.sp, + color: HomeColors.error, + ), + SizedBox(height: 16.h), + Text( + message, + style: AppTextStyles.paragraph.copyWith( + color: HomeColors.textSecondary, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 16.h), + TextButton( + onPressed: () { + ref.read(savedPlacesNotifierProvider.notifier).loadFolders(); + }, + child: Text( + '다시 시도', + style: AppTextStyles.label.copyWith( + color: HomeColors.retryButton, + ), + ), + ), + ], + ), + ), + ); + } + + /// 현재 선택된 폴더 객체 + FolderModel? _selectedFolder(SavedPlacesState state) { + if (state.selectedFolderId == null) return null; + try { + return state.folders.firstWhere((f) => f.id == state.selectedFolderId); + } catch (_) { + return null; + } + } + + /// 폴더 생성 + Future _onCreateFolder() async { + final result = await showCreateFolderBottomSheet(context); + if (result == null) return; + + final success = await ref + .read(savedPlacesNotifierProvider.notifier) + .createFolder( + name: result['name']!, + visibility: result['visibility']!, + ); + + if (success && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('폴더가 생성되었습니다.'), + duration: Duration(seconds: 2), + ), + ); + } + } + + /// 폴더 편집 + Future _onEditFolder() async { + final state = ref.read(savedPlacesNotifierProvider); + final folder = _selectedFolder(state); + if (folder == null) return; + + final result = await showEditFolderBottomSheet(context, folder: folder); + if (result == null) return; + + final notifier = ref.read(savedPlacesNotifierProvider.notifier); + + if (result.action == EditFolderAction.delete) { + final success = await notifier.deleteFolder(folder.id); + if (success && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('폴더가 삭제되었습니다.'), + duration: Duration(seconds: 2), + ), + ); + } + } else { + final success = await notifier.updateFolder( + folderId: folder.id, + name: result.name, + visibility: result.visibility, + ); + if (success && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('폴더가 수정되었습니다.'), + duration: Duration(seconds: 2), + ), + ); + } + } + } + + /// 장소 제거 + Future _onRemovePlace(String placeId) async { + final state = ref.read(savedPlacesNotifierProvider); + if (state.selectedFolderId == null) return; + + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('장소 제거'), + content: const Text('이 장소를 폴더에서 제거하시겠습니까?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('취소'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('제거'), + ), + ], + ), + ); + + if (confirmed == true) { + final success = await ref + .read(savedPlacesNotifierProvider.notifier) + .removePlaceFromFolder(state.selectedFolderId!, placeId); + + if (success && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('장소가 제거되었습니다.'), + duration: Duration(seconds: 2), + ), + ); + } + } + } +} diff --git a/lib/features/saved_places/presentation/saved_places_provider.dart b/lib/features/saved_places/presentation/saved_places_provider.dart new file mode 100644 index 0000000..407d773 --- /dev/null +++ b/lib/features/saved_places/presentation/saved_places_provider.dart @@ -0,0 +1,175 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../data/models/create_folder_request.dart'; +import '../data/models/folder_model.dart'; +import '../data/models/update_folder_request.dart'; +import '../../../../common/models/place_model.dart'; +import '../data/saved_places_repository_impl.dart'; + +part 'saved_places_provider.freezed.dart'; +part 'saved_places_provider.g.dart'; + +/// 저장 장소 화면 상태 +@freezed +class SavedPlacesState with _$SavedPlacesState { + const factory SavedPlacesState({ + @Default([]) List folders, + @Default(false) bool isFoldersLoading, + String? foldersError, + + String? selectedFolderId, + @Default([]) List places, + @Default(false) bool isPlacesLoading, + String? placesError, + }) = _SavedPlacesState; +} + +/// 저장 장소 Notifier +@riverpod +class SavedPlacesNotifier extends _$SavedPlacesNotifier { + @override + SavedPlacesState build() { + return const SavedPlacesState(); + } + + /// 폴더 목록 로드 + Future loadFolders() async { + state = state.copyWith(isFoldersLoading: true, foldersError: null); + + try { + final repository = ref.read(savedPlacesRepositoryProvider); + final response = await repository.getFolders(); + + state = state.copyWith( + folders: response.folders, + isFoldersLoading: false, + ); + + if (state.selectedFolderId == null && response.folders.isNotEmpty) { + final defaultFolder = response.folders.firstWhere( + (f) => f.isDefault, + orElse: () => response.folders.first, + ); + selectFolder(defaultFolder.id); + } + } catch (e) { + debugPrint('❌ SavedPlaces: Failed to load folders: $e'); + state = state.copyWith( + isFoldersLoading: false, + foldersError: '폴더 목록을 불러올 수 없습니다.', + ); + } + } + + /// 폴더 선택 및 해당 폴더의 장소 로드 + Future selectFolder(String folderId) async { + state = state.copyWith( + selectedFolderId: folderId, + isPlacesLoading: true, + placesError: null, + ); + + try { + final repository = ref.read(savedPlacesRepositoryProvider); + final response = await repository.getFolderPlaces(folderId); + + state = state.copyWith( + places: response.places, + isPlacesLoading: false, + ); + } catch (e) { + debugPrint('❌ SavedPlaces: Failed to load folder places: $e'); + state = state.copyWith( + isPlacesLoading: false, + placesError: '장소 목록을 불러올 수 없습니다.', + ); + } + } + + /// 폴더 생성 + Future createFolder({ + required String name, + String visibility = 'PRIVATE', + }) async { + try { + final repository = ref.read(savedPlacesRepositoryProvider); + await repository.createFolder( + CreateFolderRequest(name: name, visibility: visibility), + ); + await loadFolders(); + return true; + } catch (e) { + debugPrint('❌ SavedPlaces: Failed to create folder: $e'); + return false; + } + } + + /// 폴더 수정 + Future updateFolder({ + required String folderId, + String? name, + String? visibility, + }) async { + try { + final repository = ref.read(savedPlacesRepositoryProvider); + await repository.updateFolder( + folderId, + UpdateFolderRequest(name: name, visibility: visibility), + ); + await loadFolders(); + return true; + } catch (e) { + debugPrint('❌ SavedPlaces: Failed to update folder: $e'); + return false; + } + } + + /// 폴더 삭제 + Future deleteFolder(String folderId) async { + try { + final repository = ref.read(savedPlacesRepositoryProvider); + await repository.deleteFolder(folderId); + await loadFolders(); + return true; + } catch (e) { + debugPrint('❌ SavedPlaces: Failed to delete folder: $e'); + return false; + } + } + + /// 폴더에서 장소 제거 + Future removePlaceFromFolder(String folderId, String placeId) async { + try { + final repository = ref.read(savedPlacesRepositoryProvider); + await repository.removePlaceFromFolder(folderId, placeId); + + if (state.selectedFolderId == folderId) { + await selectFolder(folderId); + } + await loadFolders(); + return true; + } catch (e) { + debugPrint('❌ SavedPlaces: Failed to remove place: $e'); + return false; + } + } + + /// 폴더에 장소 추가 + Future addPlaceToFolder(String folderId, String placeId) async { + try { + final repository = ref.read(savedPlacesRepositoryProvider); + await repository.addPlaceToFolder(folderId, placeId); + + if (state.selectedFolderId == folderId) { + await selectFolder(folderId); + } + await loadFolders(); + return true; + } catch (e) { + debugPrint('❌ SavedPlaces: Failed to add place to folder: $e'); + return false; + } + } +} diff --git a/lib/features/saved_places/presentation/saved_places_provider.freezed.dart b/lib/features/saved_places/presentation/saved_places_provider.freezed.dart new file mode 100644 index 0000000..9a1089d --- /dev/null +++ b/lib/features/saved_places/presentation/saved_places_provider.freezed.dart @@ -0,0 +1,330 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'saved_places_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +/// @nodoc +mixin _$SavedPlacesState { + List get folders => throw _privateConstructorUsedError; + bool get isFoldersLoading => throw _privateConstructorUsedError; + String? get foldersError => throw _privateConstructorUsedError; + String? get selectedFolderId => throw _privateConstructorUsedError; + List get places => throw _privateConstructorUsedError; + bool get isPlacesLoading => throw _privateConstructorUsedError; + String? get placesError => throw _privateConstructorUsedError; + + /// Create a copy of SavedPlacesState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SavedPlacesStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SavedPlacesStateCopyWith<$Res> { + factory $SavedPlacesStateCopyWith( + SavedPlacesState value, + $Res Function(SavedPlacesState) then, + ) = _$SavedPlacesStateCopyWithImpl<$Res, SavedPlacesState>; + @useResult + $Res call({ + List folders, + bool isFoldersLoading, + String? foldersError, + String? selectedFolderId, + List places, + bool isPlacesLoading, + String? placesError, + }); +} + +/// @nodoc +class _$SavedPlacesStateCopyWithImpl<$Res, $Val extends SavedPlacesState> + implements $SavedPlacesStateCopyWith<$Res> { + _$SavedPlacesStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SavedPlacesState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? folders = null, + Object? isFoldersLoading = null, + Object? foldersError = freezed, + Object? selectedFolderId = freezed, + Object? places = null, + Object? isPlacesLoading = null, + Object? placesError = freezed, + }) { + return _then( + _value.copyWith( + folders: null == folders + ? _value.folders + : folders // ignore: cast_nullable_to_non_nullable + as List, + isFoldersLoading: null == isFoldersLoading + ? _value.isFoldersLoading + : isFoldersLoading // ignore: cast_nullable_to_non_nullable + as bool, + foldersError: freezed == foldersError + ? _value.foldersError + : foldersError // ignore: cast_nullable_to_non_nullable + as String?, + selectedFolderId: freezed == selectedFolderId + ? _value.selectedFolderId + : selectedFolderId // ignore: cast_nullable_to_non_nullable + as String?, + places: null == places + ? _value.places + : places // ignore: cast_nullable_to_non_nullable + as List, + isPlacesLoading: null == isPlacesLoading + ? _value.isPlacesLoading + : isPlacesLoading // ignore: cast_nullable_to_non_nullable + as bool, + placesError: freezed == placesError + ? _value.placesError + : placesError // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$SavedPlacesStateImplCopyWith<$Res> + implements $SavedPlacesStateCopyWith<$Res> { + factory _$$SavedPlacesStateImplCopyWith( + _$SavedPlacesStateImpl value, + $Res Function(_$SavedPlacesStateImpl) then, + ) = __$$SavedPlacesStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + List folders, + bool isFoldersLoading, + String? foldersError, + String? selectedFolderId, + List places, + bool isPlacesLoading, + String? placesError, + }); +} + +/// @nodoc +class __$$SavedPlacesStateImplCopyWithImpl<$Res> + extends _$SavedPlacesStateCopyWithImpl<$Res, _$SavedPlacesStateImpl> + implements _$$SavedPlacesStateImplCopyWith<$Res> { + __$$SavedPlacesStateImplCopyWithImpl( + _$SavedPlacesStateImpl _value, + $Res Function(_$SavedPlacesStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of SavedPlacesState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? folders = null, + Object? isFoldersLoading = null, + Object? foldersError = freezed, + Object? selectedFolderId = freezed, + Object? places = null, + Object? isPlacesLoading = null, + Object? placesError = freezed, + }) { + return _then( + _$SavedPlacesStateImpl( + folders: null == folders + ? _value._folders + : folders // ignore: cast_nullable_to_non_nullable + as List, + isFoldersLoading: null == isFoldersLoading + ? _value.isFoldersLoading + : isFoldersLoading // ignore: cast_nullable_to_non_nullable + as bool, + foldersError: freezed == foldersError + ? _value.foldersError + : foldersError // ignore: cast_nullable_to_non_nullable + as String?, + selectedFolderId: freezed == selectedFolderId + ? _value.selectedFolderId + : selectedFolderId // ignore: cast_nullable_to_non_nullable + as String?, + places: null == places + ? _value._places + : places // ignore: cast_nullable_to_non_nullable + as List, + isPlacesLoading: null == isPlacesLoading + ? _value.isPlacesLoading + : isPlacesLoading // ignore: cast_nullable_to_non_nullable + as bool, + placesError: freezed == placesError + ? _value.placesError + : placesError // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc + +class _$SavedPlacesStateImpl + with DiagnosticableTreeMixin + implements _SavedPlacesState { + const _$SavedPlacesStateImpl({ + final List folders = const [], + this.isFoldersLoading = false, + this.foldersError, + this.selectedFolderId, + final List places = const [], + this.isPlacesLoading = false, + this.placesError, + }) : _folders = folders, + _places = places; + + final List _folders; + @override + @JsonKey() + List get folders { + if (_folders is EqualUnmodifiableListView) return _folders; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_folders); + } + + @override + @JsonKey() + final bool isFoldersLoading; + @override + final String? foldersError; + @override + final String? selectedFolderId; + final List _places; + @override + @JsonKey() + List get places { + if (_places is EqualUnmodifiableListView) return _places; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_places); + } + + @override + @JsonKey() + final bool isPlacesLoading; + @override + final String? placesError; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'SavedPlacesState(folders: $folders, isFoldersLoading: $isFoldersLoading, foldersError: $foldersError, selectedFolderId: $selectedFolderId, places: $places, isPlacesLoading: $isPlacesLoading, placesError: $placesError)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'SavedPlacesState')) + ..add(DiagnosticsProperty('folders', folders)) + ..add(DiagnosticsProperty('isFoldersLoading', isFoldersLoading)) + ..add(DiagnosticsProperty('foldersError', foldersError)) + ..add(DiagnosticsProperty('selectedFolderId', selectedFolderId)) + ..add(DiagnosticsProperty('places', places)) + ..add(DiagnosticsProperty('isPlacesLoading', isPlacesLoading)) + ..add(DiagnosticsProperty('placesError', placesError)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SavedPlacesStateImpl && + const DeepCollectionEquality().equals(other._folders, _folders) && + (identical(other.isFoldersLoading, isFoldersLoading) || + other.isFoldersLoading == isFoldersLoading) && + (identical(other.foldersError, foldersError) || + other.foldersError == foldersError) && + (identical(other.selectedFolderId, selectedFolderId) || + other.selectedFolderId == selectedFolderId) && + const DeepCollectionEquality().equals(other._places, _places) && + (identical(other.isPlacesLoading, isPlacesLoading) || + other.isPlacesLoading == isPlacesLoading) && + (identical(other.placesError, placesError) || + other.placesError == placesError)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_folders), + isFoldersLoading, + foldersError, + selectedFolderId, + const DeepCollectionEquality().hash(_places), + isPlacesLoading, + placesError, + ); + + /// Create a copy of SavedPlacesState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SavedPlacesStateImplCopyWith<_$SavedPlacesStateImpl> get copyWith => + __$$SavedPlacesStateImplCopyWithImpl<_$SavedPlacesStateImpl>( + this, + _$identity, + ); +} + +abstract class _SavedPlacesState implements SavedPlacesState { + const factory _SavedPlacesState({ + final List folders, + final bool isFoldersLoading, + final String? foldersError, + final String? selectedFolderId, + final List places, + final bool isPlacesLoading, + final String? placesError, + }) = _$SavedPlacesStateImpl; + + @override + List get folders; + @override + bool get isFoldersLoading; + @override + String? get foldersError; + @override + String? get selectedFolderId; + @override + List get places; + @override + bool get isPlacesLoading; + @override + String? get placesError; + + /// Create a copy of SavedPlacesState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SavedPlacesStateImplCopyWith<_$SavedPlacesStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/saved_places/presentation/saved_places_provider.g.dart b/lib/features/saved_places/presentation/saved_places_provider.g.dart new file mode 100644 index 0000000..ac85ced --- /dev/null +++ b/lib/features/saved_places/presentation/saved_places_provider.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'saved_places_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$savedPlacesNotifierHash() => + r'426fa0d1ccd06a47d99d9939f784d551a2ca0c3d'; + +/// 저장 장소 Notifier +/// +/// Copied from [SavedPlacesNotifier]. +@ProviderFor(SavedPlacesNotifier) +final savedPlacesNotifierProvider = + AutoDisposeNotifierProvider.internal( + SavedPlacesNotifier.new, + name: r'savedPlacesNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$savedPlacesNotifierHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$SavedPlacesNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/saved_places/presentation/widgets/create_folder_bottom_sheet.dart b/lib/features/saved_places/presentation/widgets/create_folder_bottom_sheet.dart new file mode 100644 index 0000000..5baf21a --- /dev/null +++ b/lib/features/saved_places/presentation/widgets/create_folder_bottom_sheet.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../common/constants/home_colors.dart'; +import '../../../../common/constants/text_styles.dart'; + +/// 폴더 생성 바텀시트 +Future?> showCreateFolderBottomSheet( + BuildContext context, +) async { + return showModalBottomSheet>( + context: context, + isScrollControlled: true, + backgroundColor: HomeColors.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), + ), + builder: (context) => const _CreateFolderSheet(), + ); +} + +class _CreateFolderSheet extends StatefulWidget { + const _CreateFolderSheet(); + + @override + State<_CreateFolderSheet> createState() => _CreateFolderSheetState(); +} + +class _CreateFolderSheetState extends State<_CreateFolderSheet> { + final _nameController = TextEditingController(); + String _visibility = 'PRIVATE'; + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + bool get _isValid => _nameController.text.trim().isNotEmpty; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + left: 20.w, + right: 20.w, + top: 24.h, + bottom: MediaQuery.of(context).viewInsets.bottom + 24.h, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 핸들 + Center( + child: Container( + width: 40.w, + height: 4.h, + decoration: BoxDecoration( + color: HomeColors.divider, + borderRadius: BorderRadius.circular(2.r), + ), + ), + ), + SizedBox(height: 20.h), + + // 제목 + Text( + '새 폴더 만들기', + style: AppTextStyles.heading02.copyWith( + color: HomeColors.textPrimary, + ), + ), + SizedBox(height: 20.h), + + // 폴더 이름 입력 + Text( + '폴더 이름', + style: AppTextStyles.callout.copyWith( + color: HomeColors.textSecondary, + ), + ), + SizedBox(height: 8.h), + TextField( + controller: _nameController, + maxLength: 100, + autofocus: true, + style: AppTextStyles.paragraph.copyWith( + color: HomeColors.textPrimary, + ), + decoration: InputDecoration( + hintText: '폴더 이름을 입력하세요', + hintStyle: AppTextStyles.paragraph.copyWith( + color: HomeColors.textDisabled, + ), + counterText: '', + contentPadding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 12.h, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + borderSide: const BorderSide(color: HomeColors.cardBorder), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + borderSide: const BorderSide(color: HomeColors.cardBorder), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + borderSide: const BorderSide( + color: HomeColors.textPrimary, + width: 1.5, + ), + ), + ), + onChanged: (_) => setState(() {}), + ), + SizedBox(height: 16.h), + + // 공개 설정 + Text( + '공개 설정', + style: AppTextStyles.callout.copyWith( + color: HomeColors.textSecondary, + ), + ), + SizedBox(height: 8.h), + _buildVisibilityOption('PRIVATE', '나만 보기'), + SizedBox(height: 4.h), + _buildVisibilityOption('SHARED', '공유 가능'), + SizedBox(height: 24.h), + + // 만들기 버튼 + SizedBox( + width: double.infinity, + height: 48.h, + child: ElevatedButton( + onPressed: _isValid + ? () => Navigator.of(context).pop({ + 'name': _nameController.text.trim(), + 'visibility': _visibility, + }) + : null, + style: ElevatedButton.styleFrom( + backgroundColor: HomeColors.textPrimary, + disabledBackgroundColor: HomeColors.surfaceLight, + foregroundColor: HomeColors.background, + disabledForegroundColor: HomeColors.textDisabled, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + elevation: 0, + ), + child: Text('만들기', style: AppTextStyles.label), + ), + ), + ], + ), + ); + } + + Widget _buildVisibilityOption(String value, String label) { + final isSelected = _visibility == value; + + return GestureDetector( + onTap: () => setState(() => _visibility = value), + behavior: HitTestBehavior.opaque, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 6.h), + child: Row( + children: [ + Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 20.sp, + color: isSelected + ? HomeColors.textPrimary + : HomeColors.iconSecondary, + ), + SizedBox(width: 8.w), + Text( + label, + style: AppTextStyles.paragraph.copyWith( + color: HomeColors.textPrimary, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/saved_places/presentation/widgets/edit_folder_bottom_sheet.dart b/lib/features/saved_places/presentation/widgets/edit_folder_bottom_sheet.dart new file mode 100644 index 0000000..4659e21 --- /dev/null +++ b/lib/features/saved_places/presentation/widgets/edit_folder_bottom_sheet.dart @@ -0,0 +1,279 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../common/constants/app_colors.dart'; +import '../../../../common/constants/home_colors.dart'; +import '../../../../common/constants/text_styles.dart'; +import '../../data/models/folder_model.dart'; + +/// 폴더 편집 바텀시트 (수정 + 삭제) +Future showEditFolderBottomSheet( + BuildContext context, { + required FolderModel folder, +}) async { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: HomeColors.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), + ), + builder: (context) => _EditFolderSheet(folder: folder), + ); +} + +/// 폴더 편집 결과 +class EditFolderResult { + final EditFolderAction action; + final String? name; + final String? visibility; + + EditFolderResult.update({this.name, this.visibility}) + : action = EditFolderAction.update; + EditFolderResult.delete() + : action = EditFolderAction.delete, + name = null, + visibility = null; +} + +enum EditFolderAction { update, delete } + +class _EditFolderSheet extends StatefulWidget { + const _EditFolderSheet({required this.folder}); + + final FolderModel folder; + + @override + State<_EditFolderSheet> createState() => _EditFolderSheetState(); +} + +class _EditFolderSheetState extends State<_EditFolderSheet> { + late final TextEditingController _nameController; + late String _visibility; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.folder.name); + _visibility = widget.folder.visibility; + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + bool get _isValid => _nameController.text.trim().isNotEmpty; + + bool get _hasChanges => + _nameController.text.trim() != widget.folder.name || + _visibility != widget.folder.visibility; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + left: 20.w, + right: 20.w, + top: 24.h, + bottom: MediaQuery.of(context).viewInsets.bottom + 24.h, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 핸들 + Center( + child: Container( + width: 40.w, + height: 4.h, + decoration: BoxDecoration( + color: HomeColors.divider, + borderRadius: BorderRadius.circular(2.r), + ), + ), + ), + SizedBox(height: 20.h), + + // 제목 + Text( + '폴더 수정', + style: AppTextStyles.heading02.copyWith( + color: HomeColors.textPrimary, + ), + ), + SizedBox(height: 20.h), + + // 폴더 이름 입력 + Text( + '폴더 이름', + style: AppTextStyles.callout.copyWith( + color: HomeColors.textSecondary, + ), + ), + SizedBox(height: 8.h), + TextField( + controller: _nameController, + maxLength: 100, + style: AppTextStyles.paragraph.copyWith( + color: HomeColors.textPrimary, + ), + decoration: InputDecoration( + hintText: '폴더 이름을 입력하세요', + hintStyle: AppTextStyles.paragraph.copyWith( + color: HomeColors.textDisabled, + ), + counterText: '', + contentPadding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 12.h, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + borderSide: const BorderSide(color: HomeColors.cardBorder), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + borderSide: const BorderSide(color: HomeColors.cardBorder), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + borderSide: const BorderSide( + color: HomeColors.textPrimary, + width: 1.5, + ), + ), + ), + onChanged: (_) => setState(() {}), + ), + SizedBox(height: 16.h), + + // 공개 설정 + Text( + '공개 설정', + style: AppTextStyles.callout.copyWith( + color: HomeColors.textSecondary, + ), + ), + SizedBox(height: 8.h), + _buildVisibilityOption('PRIVATE', '나만 보기'), + SizedBox(height: 4.h), + _buildVisibilityOption('SHARED', '공유 가능'), + SizedBox(height: 24.h), + + // 수정 버튼 + SizedBox( + width: double.infinity, + height: 48.h, + child: ElevatedButton( + onPressed: _isValid && _hasChanges + ? () { + final result = EditFolderResult.update( + name: _nameController.text.trim() != + widget.folder.name + ? _nameController.text.trim() + : null, + visibility: _visibility != widget.folder.visibility + ? _visibility + : null, + ); + Navigator.of(context).pop(result); + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: HomeColors.textPrimary, + disabledBackgroundColor: HomeColors.surfaceLight, + foregroundColor: HomeColors.background, + disabledForegroundColor: HomeColors.textDisabled, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + elevation: 0, + ), + child: Text('수정하기', style: AppTextStyles.label), + ), + ), + + // 삭제 버튼 (기본 폴더가 아닌 경우만) + if (!widget.folder.isDefault) ...[ + SizedBox(height: 12.h), + SizedBox( + width: double.infinity, + height: 48.h, + child: TextButton( + onPressed: () => _onDelete(context), + style: TextButton.styleFrom( + foregroundColor: AppColors.error, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + child: Text('폴더 삭제', style: AppTextStyles.label), + ), + ), + ], + ], + ), + ); + } + + Widget _buildVisibilityOption(String value, String label) { + final isSelected = _visibility == value; + + return GestureDetector( + onTap: () => setState(() => _visibility = value), + behavior: HitTestBehavior.opaque, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 6.h), + child: Row( + children: [ + Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 20.sp, + color: isSelected + ? HomeColors.textPrimary + : HomeColors.iconSecondary, + ), + SizedBox(width: 8.w), + Text( + label, + style: AppTextStyles.paragraph.copyWith( + color: HomeColors.textPrimary, + ), + ), + ], + ), + ), + ); + } + + Future _onDelete(BuildContext context) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('폴더 삭제'), + content: Text( + '\'${widget.folder.name}\' 폴더를 삭제하시겠습니까?\n폴더 안의 장소 연결도 함께 삭제됩니다.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('취소'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: AppColors.error), + child: const Text('삭제'), + ), + ], + ), + ); + + if (confirmed == true && context.mounted) { + Navigator.of(context).pop(EditFolderResult.delete()); + } + } +} diff --git a/lib/features/saved_places/presentation/widgets/empty_folder_state.dart b/lib/features/saved_places/presentation/widgets/empty_folder_state.dart new file mode 100644 index 0000000..8ac2603 --- /dev/null +++ b/lib/features/saved_places/presentation/widgets/empty_folder_state.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../common/constants/home_colors.dart'; +import '../../../../common/constants/text_styles.dart'; + +/// 빈 폴더 상태 위젯 +class EmptyFolderState extends StatelessWidget { + const EmptyFolderState({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 60.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.folder_open_outlined, + size: 56.sp, + color: HomeColors.iconSecondary, + ), + SizedBox(height: 16.h), + Text( + '아직 저장된 장소가 없어요', + style: AppTextStyles.label.copyWith( + color: HomeColors.textSecondary, + ), + ), + SizedBox(height: 8.h), + Text( + 'AI 추출로 장소를 저장해보세요', + style: AppTextStyles.callout.copyWith( + color: HomeColors.textDisabled, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/saved_places/presentation/widgets/folder_place_card.dart b/lib/features/saved_places/presentation/widgets/folder_place_card.dart new file mode 100644 index 0000000..394db1d --- /dev/null +++ b/lib/features/saved_places/presentation/widgets/folder_place_card.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../common/constants/home_colors.dart'; +import '../../../../common/constants/text_styles.dart'; +import '../../../../common/models/place_model.dart'; + +/// 폴더 내 장소 카드 +class FolderPlaceCard extends StatelessWidget { + const FolderPlaceCard({ + super.key, + required this.place, + this.onTap, + this.onRemove, + }); + + final PlaceModel place; + final VoidCallback? onTap; + final VoidCallback? onRemove; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 20.w, vertical: 4.h), + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: HomeColors.cardBackground, + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: HomeColors.cardBorder, width: 1), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 장소 이미지 + _buildThumbnail(), + SizedBox(width: 12.w), + + // 장소 정보 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + place.name, + style: AppTextStyles.label.copyWith( + color: HomeColors.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (place.address != null) ...[ + SizedBox(height: 4.h), + Text( + place.address!, + style: AppTextStyles.callout.copyWith( + color: HomeColors.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + if (place.rating != null) ...[ + SizedBox(height: 6.h), + Row( + children: [ + Icon( + Icons.star_rounded, + size: 14.sp, + color: HomeColors.starRating, + ), + SizedBox(width: 2.w), + Text( + place.rating!.toStringAsFixed(1), + style: AppTextStyles.callout.copyWith( + color: HomeColors.textPrimary, + ), + ), + if (place.userRatingsTotal != null) ...[ + SizedBox(width: 4.w), + Text( + '(${place.userRatingsTotal})', + style: AppTextStyles.calloutSmall.copyWith( + color: HomeColors.textSecondary, + ), + ), + ], + ], + ), + ], + ], + ), + ), + + // 삭제 버튼 + if (onRemove != null) + GestureDetector( + onTap: onRemove, + behavior: HitTestBehavior.opaque, + child: Padding( + padding: EdgeInsets.all(4.w), + child: Icon( + Icons.close, + size: 18.sp, + color: HomeColors.iconSecondary, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildThumbnail() { + final hasPhoto = place.photoUrls.isNotEmpty; + + return ClipRRect( + borderRadius: BorderRadius.circular(8.r), + child: Container( + width: 64.w, + height: 64.w, + color: HomeColors.surfaceLight, + child: hasPhoto + ? Image.network( + place.photoUrls.first, + fit: BoxFit.cover, + errorBuilder: (_, _, _) => _buildPlaceholder(), + ) + : _buildPlaceholder(), + ), + ); + } + + Widget _buildPlaceholder() { + return Center( + child: Icon( + Icons.place_outlined, + size: 28.sp, + color: HomeColors.iconSecondary, + ), + ); + } +} diff --git a/lib/features/saved_places/presentation/widgets/folder_preview_section.dart b/lib/features/saved_places/presentation/widgets/folder_preview_section.dart new file mode 100644 index 0000000..518dec0 --- /dev/null +++ b/lib/features/saved_places/presentation/widgets/folder_preview_section.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../common/constants/home_colors.dart'; +import '../../../../common/constants/text_styles.dart'; +import '../../../../routing/route_paths.dart'; +import '../../data/models/folder_model.dart'; +import '../saved_places_provider.dart'; + +/// 마이페이지용 폴더 미리보기 섹션 +class FolderPreviewSection extends ConsumerStatefulWidget { + const FolderPreviewSection({super.key}); + + @override + ConsumerState createState() => + _FolderPreviewSectionState(); +} + +class _FolderPreviewSectionState extends ConsumerState { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final state = ref.read(savedPlacesNotifierProvider); + if (state.folders.isEmpty && !state.isFoldersLoading) { + ref.read(savedPlacesNotifierProvider.notifier).loadFolders(); + } + }); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(savedPlacesNotifierProvider); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 섹션 헤더 + Padding( + padding: EdgeInsets.only(left: 20.w, right: 12.w, top: 16.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '내 폴더', + style: AppTextStyles.callout.copyWith( + color: HomeColors.textSecondary, + ), + ), + GestureDetector( + onTap: () => context.push(RoutePaths.mypageSavedPlaces), + behavior: HitTestBehavior.opaque, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 8.w, + vertical: 4.h, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '전체보기', + style: AppTextStyles.callout.copyWith( + color: HomeColors.textSecondary, + ), + ), + SizedBox(width: 2.w), + Icon( + Icons.chevron_right, + size: 16.sp, + color: HomeColors.iconSecondary, + ), + ], + ), + ), + ), + ], + ), + ), + SizedBox(height: 8.h), + + // 폴더 칩 목록 + if (state.isFoldersLoading) + _buildLoading() + else if (state.folders.isEmpty) + _buildEmpty() + else + _buildFolderChips(state.folders), + + SizedBox(height: 8.h), + ], + ); + } + + Widget _buildLoading() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), + child: Row( + children: List.generate( + 3, + (index) => Padding( + padding: EdgeInsets.only(right: 8.w), + child: Container( + width: 80.w, + height: 36.h, + decoration: BoxDecoration( + color: HomeColors.shimmerBase, + borderRadius: BorderRadius.circular(18.r), + ), + ), + ), + ), + ), + ); + } + + Widget _buildEmpty() { + return GestureDetector( + onTap: () => context.push(RoutePaths.mypageSavedPlaces), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), + child: Text( + 'AI 추출로 장소를 저장해보세요', + style: AppTextStyles.callout.copyWith( + color: HomeColors.textDisabled, + ), + ), + ), + ); + } + + Widget _buildFolderChips(List folders) { + return SizedBox( + height: 36.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 20.w), + itemCount: folders.length, + separatorBuilder: (_, _) => SizedBox(width: 8.w), + itemBuilder: (context, index) { + final folder = folders[index]; + return GestureDetector( + onTap: () => context.push(RoutePaths.mypageSavedPlaces), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h), + decoration: BoxDecoration( + color: HomeColors.surfaceLight, + borderRadius: BorderRadius.circular(18.r), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (folder.isDefault) + Padding( + padding: EdgeInsets.only(right: 4.w), + child: Icon( + Icons.folder_special_outlined, + size: 14.sp, + color: HomeColors.textSecondary, + ), + ), + Text( + folder.name, + style: AppTextStyles.callout.copyWith( + color: HomeColors.textPrimary, + ), + ), + SizedBox(width: 4.w), + Text( + '${folder.placeCount}', + style: AppTextStyles.calloutSmall.copyWith( + color: HomeColors.textSecondary, + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/saved_places/presentation/widgets/folder_tab_bar.dart b/lib/features/saved_places/presentation/widgets/folder_tab_bar.dart new file mode 100644 index 0000000..0067d03 --- /dev/null +++ b/lib/features/saved_places/presentation/widgets/folder_tab_bar.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../common/constants/home_colors.dart'; +import '../../../../common/constants/text_styles.dart'; +import '../../data/models/folder_model.dart'; + +/// 폴더 탭 바 (가로 스크롤 칩 목록) +class FolderTabBar extends StatelessWidget { + const FolderTabBar({ + super.key, + required this.folders, + required this.selectedFolderId, + required this.onFolderSelected, + }); + + final List folders; + final String? selectedFolderId; + final ValueChanged onFolderSelected; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 20.w), + itemCount: folders.length, + separatorBuilder: (_, _) => SizedBox(width: 8.w), + itemBuilder: (context, index) { + final folder = folders[index]; + final isSelected = folder.id == selectedFolderId; + + return GestureDetector( + onTap: () => onFolderSelected(folder.id), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + decoration: BoxDecoration( + color: isSelected + ? HomeColors.textPrimary + : HomeColors.surfaceLight, + borderRadius: BorderRadius.circular(20.r), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + folder.name, + style: AppTextStyles.callout.copyWith( + color: isSelected + ? HomeColors.background + : HomeColors.textSecondary, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + SizedBox(width: 4.w), + Text( + '${folder.placeCount}', + style: AppTextStyles.calloutSmall.copyWith( + color: isSelected + ? HomeColors.background.withValues(alpha: 0.7) + : HomeColors.textDisabled, + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/routing/app_router.dart b/lib/routing/app_router.dart index 4f282fb..564fe8f 100644 --- a/lib/routing/app_router.dart +++ b/lib/routing/app_router.dart @@ -27,6 +27,7 @@ import '../features/onboarding/presentation/pages/birth_date_step_page.dart'; import '../features/onboarding/presentation/pages/gender_step_page.dart'; import '../features/onboarding/presentation/pages/nickname_step_page.dart'; import '../features/ai_extraction/presentation/pages/ai_extraction_page.dart'; +import '../features/saved_places/presentation/pages/saved_places_page.dart'; /// GoRouter 인스턴스를 제공하는 Riverpod Provider /// @@ -247,6 +248,11 @@ final routerProvider = Provider((ref) { name: RoutePaths.mypagePrivacyPolicyName, builder: (context, state) => const PrivacyPolicyPage(), ), + GoRoute( + path: 'saved-places', + name: RoutePaths.mypageSavedPlacesName, + builder: (context, state) => const SavedPlacesPage(), + ), ], ), ], diff --git a/lib/routing/route_paths.dart b/lib/routing/route_paths.dart index a1d7013..e7e21f1 100644 --- a/lib/routing/route_paths.dart +++ b/lib/routing/route_paths.dart @@ -75,6 +75,9 @@ class RoutePaths { /// 마이페이지 - 개인정보처리방침 static const String mypagePrivacyPolicy = '/mypage/privacy-policy'; + /// 마이페이지 - 저장 장소 (폴더 관리) + static const String mypageSavedPlaces = '/mypage/saved-places'; + // ============================================================================ // AI Extraction Routes // ============================================================================ @@ -101,4 +104,5 @@ class RoutePaths { static const String mypageTermsName = 'mypage-terms'; static const String mypagePrivacyPolicyName = 'mypage-privacy-policy'; static const String aiExtractionName = 'ai-extraction'; + static const String mypageSavedPlacesName = 'mypage-saved-places'; } diff --git a/pubspec.lock b/pubspec.lock index 01a3f44..ef1e61b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -756,10 +756,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1169,10 +1169,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" timezone: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b23e97e..9b9b19c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: mapsy description: "MapSy - Flutter Application" publish_to: "none" -version: 1.0.46+46 +version: 1.0.49+49 environment: sdk: ^3.9.2 dependencies: diff --git a/version.yml b/version.yml index 137b7b4..bd9d046 100644 --- a/version.yml +++ b/version.yml @@ -34,11 +34,11 @@ # - 버전은 항상 높은 버전으로 자동 동기화됩니다 # =================================================================== -version: "1.0.46" -version_code: 47 # app build number +version: "1.0.49" +version_code: 50 # app build number project_type: "flutter" # spring, flutter, react, react-native, react-native-expo, node, python, basic metadata: - last_updated: "2026-02-23 08:11:34" + last_updated: "2026-02-23 09:56:01" last_updated_by: "Cassiiopeia" default_branch: "main" integrated_from: "SUH-DEVOPS-TEMPLATE"