Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0f5cd30
[refactor] : "Phase 1 - 리스트 페이지네이션 및 검색/UX 버그 수정"
eum018 Jun 21, 2026
bf6bff5
[feat] : "Phase 2 - 로딩/에러/빈 상태 UI 및 화면별 메시지 적용"
eum018 Jun 21, 2026
3606cdf
[feat] : "Phase 3 - 게시물 삭제/수정 이벤트 전파 및 바인딩 구조 정비"
eum018 Jun 21, 2026
685874a
[fix] : "좋아요 목록→상세 동기화 및 상태 관리 버그 수정"
eum018 Jun 21, 2026
226094e
[feat] : "AppSnackbar 유틸리티 추가 및 에러 피드백 적용"
eum018 Jun 21, 2026
b418b5d
[fix] : "제미나이 코드 리뷰 반영 - 스크롤 트리거·필터 유지·댓글 재조회 개선"
eum018 Jun 22, 2026
0e26467
[fix] : "스크롤 페이지네이션 depth 조건 누락 수정 - home_recommend_profile_list"
eum018 Jun 22, 2026
464af98
Merge branch 'develop' into feature/#262-async-error-handling
eum018 Jun 22, 2026
d1dc8d7
[feat] : "NotificationController 에러 스낵바 피드백 추가"
eum018 Jun 22, 2026
d54f3be
refactor : "datasource 에러 전파 일관성 수정 - null 반환 대신 rethrow"
eum018 Jun 22, 2026
2a87120
feat : "에러 UI 추가 - 커피챗 추천 리스트, 알림, 상대 프로필"
eum018 Jun 22, 2026
b5a3abf
feat : "에러 UI 추가 - 내 프로필 화면 로드 실패 시 에러 아이콘 표시"
eum018 Jun 22, 2026
eb6ca67
fix : "프로필 이미지 URL 실패 시 전체 에러 화면 전환되는 버그 수정"
eum018 Jun 22, 2026
3bdbb25
feat : "프로필 이미지 presign URL 만료 시 자동 재발급 로직 추가"
eum018 Jun 22, 2026
2592ffe
fix : "커뮤니티 에러/로딩 상태 화면 중앙 정렬 수정"
eum018 Jun 22, 2026
c11b911
feat : "실시간 인기 게시물 좋아요 동기화 - 추천 게시물/포스트 상세 연동"
eum018 Jun 22, 2026
f16ace3
feat : "검색 에러 전파 및 검색 결과 UI 개선"
eum018 Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions lib/core/design_system/components/app_empty_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:ondo/core/design_system/app_colors.dart';
import 'package:ondo/core/design_system/app_icon.dart';
import 'package:ondo/core/design_system/app_text_styles.dart';

class AppEmptyState extends StatelessWidget {
final String message;
final AppIcon? icon;

const AppEmptyState({
super.key,
required this.message,
this.icon,
});

@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
_buildIcon(icon!),
const SizedBox(height: 12),
],
Text(
message,
style: AppTextStyles.textMedium(textColor: AppColors.gray60),
textAlign: TextAlign.center,
),
],
),
);
}

Widget _buildIcon(AppIcon appIcon) {
const size = 48.0;
final path = appIcon.path;
if (path.endsWith('.svg')) {
return SvgPicture.asset(
path,
width: size,
height: size,
colorFilter: const ColorFilter.mode(AppColors.gray60, BlendMode.srcIn),
);
}
return Image.asset(
path,
width: size,
height: size,
color: AppColors.gray60,
);
}
}
42 changes: 42 additions & 0 deletions lib/core/design_system/components/app_error_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:ondo/core/design_system/app_colors.dart';
import 'package:ondo/core/design_system/app_text_styles.dart';

class AppErrorState extends StatelessWidget {
final String message;
final VoidCallback? onRetry;

const AppErrorState({
super.key,
this.message = '데이터를 불러오지 못했어요.',
this.onRetry,
});

@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: AppColors.gray60),
const SizedBox(height: 12),
Text(
message,
style: AppTextStyles.textMedium(textColor: AppColors.gray60),
textAlign: TextAlign.center,
),
if (onRetry != null) ...[
const SizedBox(height: 16),
TextButton(
onPressed: onRetry,
child: Text(
'다시 시도',
style: AppTextStyles.textMedium(textColor: AppColors.primary),
),
),
],
],
),
);
}
}
38 changes: 38 additions & 0 deletions lib/core/design_system/components/app_loading_indicator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:ondo/core/design_system/app_colors.dart';

class AppLoadingIndicator extends StatelessWidget {
const AppLoadingIndicator({super.key});

@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.primary,
strokeWidth: 2.5,
),
);
}
}

/// 리스트 하단 무한스크롤 로딩 인디케이터
class AppBottomLoadingIndicator extends StatelessWidget {
const AppBottomLoadingIndicator({super.key});

@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: AppColors.primary,
strokeWidth: 2,
),
),
),
);
}
}
10 changes: 10 additions & 0 deletions lib/core/router/bindings/post_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import 'package:ondo/data/datasource/post/post_remote_datasource.dart';
import 'package:ondo/data/network/clients/auth_client.dart';
import 'package:ondo/data/repositories/post/post_repository_impl.dart';
import 'package:ondo/domain/usecases/post/bookmark_post_usecase.dart';
import 'package:ondo/domain/usecases/post/create_post_usecase.dart';
import 'package:ondo/domain/usecases/post/like_post_usecase.dart';
import 'package:ondo/domain/usecases/post/update_post_usecase.dart';
import 'package:ondo/domain/usecases/post/liked_post_use_case.dart'; // ← 추가
import 'package:ondo/domain/usecases/post/load_recent_popular_post_list_use_case.dart';
import 'package:ondo/domain/usecases/post/load_recommend_post_list_use_case.dart';
Expand Down Expand Up @@ -44,6 +46,14 @@ class PostBinding extends Bindings {
() => UnlikePostUseCase(Get.find<PostRepositoryImpl>()),
);

Get.lazyPut<CreatePostUseCase>(
() => CreatePostUseCase(Get.find<PostRepositoryImpl>()),
);

Get.lazyPut<UpdatePostUseCase>(
() => UpdatePostUseCase(Get.find<PostRepositoryImpl>()),
);

Get.lazyPut<BookmarkPostUseCase>(
() => BookmarkPostUseCase(Get.find<PostRepositoryImpl>()),
);
Expand Down
9 changes: 0 additions & 9 deletions lib/core/router/bindings/post_view_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import '../../../data/repositories/comment/comment_repository_impl.dart';
import '../../../data/repositories/post/post_repository_impl.dart';

import '../../../domain/usecases/post/bookmark_post_usecase.dart';
import '../../../domain/usecases/post/create_post_usecase.dart';
import '../../../domain/usecases/post/delete_post_usecase.dart';
import '../../../domain/usecases/post/get_post_detail_usecase.dart';
import '../../../domain/usecases/post/like_post_usecase.dart';
Expand All @@ -33,14 +32,6 @@ class PostViewBinding extends Bindings {
() => GetPostDetailUseCase(Get.find<PostRepositoryImpl>()),
);

Get.lazyPut<CreatePostUseCase>(
() => CreatePostUseCase(Get.find<PostRepositoryImpl>()),
);

Get.lazyPut<UpdatePostUseCase>(
() => UpdatePostUseCase(Get.find<PostRepositoryImpl>()),
);

Get.lazyPut<DeletePostUseCase>(
() => DeletePostUseCase(Get.find<PostRepositoryImpl>()),
);
Expand Down
28 changes: 28 additions & 0 deletions lib/core/utils/app_snackbar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:ondo/core/design_system/app_colors.dart';
import 'package:ondo/core/design_system/app_text_styles.dart';

final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();

class AppSnackbar {
static void showError(String message) {
scaffoldMessengerKey.currentState
?..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(
message,
style: AppTextStyles.caption(textColor: AppColors.white),
),
backgroundColor: AppColors.gray90,
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
);
}
}
18 changes: 13 additions & 5 deletions lib/data/datasource/comment/comment_remote_datasource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import 'dart:convert';
import 'package:ondo/data/network/clients/auth_client.dart';
import 'package:ondo/data/network/constants/api_constants.dart';
import 'package:ondo/data/models/comment/response/comment_model.dart';
import 'package:ondo/domain/entities/base/listable_wrapper.dart';

abstract class CommentRemoteDataSource {
Future<List<CommentModel>> getComments(int postId);
Future<ListableWrapper<CommentModel>> getComments(int postId, {int page = 0, int size = 10});
Future<bool> createComment({
required int postId,
required String content,
Expand All @@ -28,11 +29,11 @@ class CommentRemoteDataSourceImpl implements CommentRemoteDataSource {
}

@override
Future<List<CommentModel>> getComments(int postId) async {
Future<ListableWrapper<CommentModel>> getComments(int postId, {int page = 0, int size = 10}) async {
final log = ApiConstants(logName: '댓글 조회');
try {
final response = await _client.get(
Uri.parse('${ApiConstants.comment}/$postId'),
Uri.parse('${ApiConstants.comment}/$postId?page=$page&size=$size'),
);
log.statusLog(response.statusCode);

Expand All @@ -49,10 +50,17 @@ class CommentRemoteDataSourceImpl implements CommentRemoteDataSource {
}

final data = body['data'];
if (data == null) return [];
if (data == null) return ListableWrapper.none();

final list = (data['content'] as List?) ?? [];
return list.map((e) => CommentModel.fromJson(e)).toList();
return ListableWrapper<CommentModel>(
content: list.map((e) => CommentModel.fromJson(e)).toList(),
page: data['page'] ?? 0,
size: data['size'] ?? size,
totalElements: data['totalElements'] ?? 0,
totalPages: data['totalPages'] ?? 0,
last: data['last'],
);
} catch (e) {
log.errorLog(e);
rethrow;
Expand Down
5 changes: 3 additions & 2 deletions lib/data/repositories/comment/comment_repository_impl.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:ondo/data/datasource/comment/comment_remote_datasource.dart';
import 'package:ondo/data/models/comment/response/comment_model.dart';
import 'package:ondo/domain/entities/base/listable_wrapper.dart';
import 'package:ondo/domain/repositories/comment/comment_repository.dart';

class CommentRepositoryImpl implements CommentRepository {
Expand All @@ -10,9 +11,9 @@ class CommentRepositoryImpl implements CommentRepository {
});

@override
Future<List<CommentModel>> getComments(int postId) async {
Future<ListableWrapper<CommentModel>> getComments(int postId, {int page = 0, int size = 10}) async {
try {
return await remoteDataSource.getComments(postId);
return await remoteDataSource.getComments(postId, page: page, size: size);
} catch (e) {
throw Exception('댓글 조회 실패: $e');
}
Expand Down
33 changes: 15 additions & 18 deletions lib/data/repositories/rating/rating_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:ondo/data/datasource/rating/rating_remote_datasource.dart';
import 'package:ondo/data/models/base/request/base_list_request_model.dart';
import 'package:ondo/data/models/rating/request/rating_chat_request_model.dart';
import 'package:ondo/data/models/rating/response/rating_model.dart';
import 'package:ondo/domain/entities/base/pageable_wrapper.dart';
import 'package:ondo/domain/entities/rating/rating_entity.dart';
import 'package:ondo/domain/repositories/rating/rating_repository.dart';

Expand Down Expand Up @@ -30,40 +31,36 @@ class RatingRepositoryImpl extends RatingRepository {
}

@override
Future<List<RatingEntity>> loadOtherRatingList(
Future<PageableWrapper<RatingEntity>> loadOtherRatingList(
String userPublicId,
int cursor,
int size,
) async {
final model = ListRequestModelBaseCursor(size: size, cursor: cursor);
final json = await remoteDatasource.getOtherRatingList(userPublicId, model);

//TODO : PageableModel 적용
if (json.isEmpty) return [];
if (json.isEmpty) return PageableWrapper(pages: [], nextCursor: null, hasNext: false);

final data = RatingDataModel.fromJson(json);

return data.items
.map(
(e) => RatingEntity.fromRatingModel(e),
)
.toList();
return PageableWrapper(
pages: data.items.map((e) => RatingEntity.fromRatingModel(e)).toList(),
nextCursor: data.nextCursor,
hasNext: data.hasNext,
);
}

@override
Future<List<RatingEntity>> loadMyRatingList(int cursor, int size) async {
Future<PageableWrapper<RatingEntity>> loadMyRatingList(int cursor, int size) async {
final model = ListRequestModelBaseCursor(size: size, cursor: cursor);
final json = await remoteDatasource.getMyRatingList(model);

//TODO : Pageable 객체로 관리
if (json.isEmpty) return [];
if (json.isEmpty) return PageableWrapper(pages: [], nextCursor: null, hasNext: false);

final data = RatingDataModel.fromJson(json);

return data.items
.map(
(e) => RatingEntity.fromRatingModel(e),
)
.toList();
return PageableWrapper(
pages: data.items.map((e) => RatingEntity.fromRatingModel(e)).toList(),
nextCursor: data.nextCursor,
hasNext: data.hasNext,
);
}
}
Loading
Loading