최신 인기 장소 목록 UI#30
Hidden character warning
Conversation
|
Caution Review failedThe pull request is closed. Walkthrough홈 피드 기능을 새로 구현하면서 콘텐츠 모델, 데이터 소스, 레포지토리, 상태 관리를 추가했습니다. 다양한 페이지와 서비스의 import 경로를 재구성하고, 생성된 Freezed/Riverpod 파일들을 정리했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant HomePage
participant ScrollController as ScrollController
participant HomeNotifier
participant HomeRepository
participant HomeRemoteDataSource
participant API as API Server
User->>HomePage: 페이지 진입
HomePage->>HomeNotifier: fetchRecentPlaces() 초기화
HomeNotifier->>HomeRepository: getRecentContent()
HomeRepository->>HomeRemoteDataSource: fetchRecentContent()
HomeRemoteDataSource->>API: GET /api/content/recent
API-->>HomeRemoteDataSource: ContentListResponse
HomeRemoteDataSource-->>HomeRepository: ContentListResponse
HomeRepository-->>HomeNotifier: ContentListResponse
HomeNotifier->>HomeNotifier: state.copyWith(recentPlaces, cursor 업데이트)
HomeNotifier-->>HomePage: 상태 업데이트
User->>ScrollController: 스크롤 (바닥 도달)
ScrollController->>HomePage: onScroll 콜백
HomePage->>HomeNotifier: fetchMoreRecentPlaces()
HomeNotifier->>HomeRepository: getRecentContent(cursor=nextCursor)
HomeRepository->>HomeRemoteDataSource: fetchRecentContent(cursor)
HomeRemoteDataSource->>API: GET /api/content/recent?cursor=...
API-->>HomeRemoteDataSource: ContentListResponse
HomeRemoteDataSource-->>HomeRepository: ContentListResponse
HomeRepository-->>HomeNotifier: ContentListResponse
HomeNotifier->>HomeNotifier: state.copyWith(기존 리스트 + 새 항목)
HomeNotifier-->>HomePage: 상태 업데이트 (페이지네이션)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50분 Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@lib/features/home/data/datasources/home_remote_datasource.dart`:
- Around line 36-48: The code unsafely casts response.data to Map<String,
dynamic> causing TypeError on null or unexpected responses; update the logic in
the method that calls ContentListResponse.fromJson (and likewise in
fetchMemberContent) to first verify response.data is non-null and is a
Map<String, dynamic> (e.g., check response.data is Map or use a safe cast), and
handle the failure path by logging the response/error and either throwing a
descriptive exception or returning an appropriate fallback (empty
ContentListResponse or error result) before calling ContentListResponse.fromJson
so deserialization is never invoked with an invalid type.
In `@lib/features/home/data/models/cursor_model.dart`:
- Around line 1-22: The CI is failing because the Freezed-generated files for
CursorModel (cursor_model.freezed.dart and cursor_model.g.dart) are missing; run
the Dart build runner to generate them (dart run build_runner build
--delete-conflicting-outputs), then either commit the generated files or update
CI to run build_runner before tests/builds so models like CursorModel (and
others mentioned such as ContentResponse, HomeRemoteDatasource,
HomeRepositoryImpl) have their corresponding *.freezed.dart and *.g.dart
artifacts available at runtime.
In `@lib/features/home/data/models/place_model.dart`:
- Around line 1-39: The build artifacts for the Freezed/JsonSerializable parts
referenced by PlaceModel (place_model.freezed.dart, place_model.g.dart providing
_$PlaceModel mixin and _$PlaceModelFromJson) are missing; fix by either running
code generation locally with `dart run build_runner build
--delete-conflicting-outputs` and committing the generated files, or update the
CI workflow to run a build step (after flutter pub get) that executes `dart run
build_runner build` so the part files are generated during CI. Ensure generated
files for PlaceModel are present in the repo or produced in CI so _$PlaceModel
and _$PlaceModelFromJson resolve.
In `@lib/features/home/presentation/providers/home_provider.dart`:
- Around line 47-51: The `@override` warning means the generated Riverpod
signature doesn't match this method; first run the generator (dart run
build_runner build) to ensure the provider code is up-to-date and that the
method signature (build / HomeState) matches the generated code; if generation
is intentionally not used, remove the `@override` annotation so it stops reporting
as not overriding. Also avoid silent fire-and-forget for initialization: change
Future.microtask(() => _initialize()) to a form that surface errors (either
await the call in an async build or attach error handling like
Future.microtask(() => _initialize()).catchError(...)) so exceptions from
_initialize (not just fetchRecentPlaces/fetchPopularPlaces) are handled;
references: HomeState, build(), _initialize(), fetchRecentPlaces(),
fetchPopularPlaces().
🧹 Nitpick comments (11)
lib/features/home/domain/entities/content_status.dart (1)
16-21: 알 수 없는 상태값에 대한 무조건적pending폴백은 데이터 문제를 숨길 수 있습니다.서버에서 예상치 못한 상태값이 내려올 경우, 오류 없이
pending으로 처리되어 디버깅이 어려워질 수 있습니다. 최소한 로그를 남기거나,assert를 추가하는 것을 권장합니다.💡 로그 추가 제안
static ContentStatus fromString(String value) { return ContentStatus.values.firstWhere( (e) => e.name.toUpperCase() == value.toUpperCase(), - orElse: () => ContentStatus.pending, + orElse: () { + debugPrint('Unknown ContentStatus: $value, defaulting to pending'); + return ContentStatus.pending; + }, ); }lib/features/home/presentation/widgets/home_loading_shimmer.dart (1)
7-25: "Shimmer"라고 명명되었지만 실제 애니메이션이 없는 정적 스켈레톤입니다.현재 구현은 색상 블록만 표시하고 있어 로딩 중이라는 시각적 피드백이 부족합니다.
shimmer패키지를 활용하거나AnimationController로 그라데이션 애니메이션을 추가하면 사용자 경험이 개선됩니다. 지금 당장 필요하지 않다면 클래스 이름을HomeLoadingSkeleton으로 변경하는 것도 방법입니다.lib/features/home/presentation/widgets/place_card.dart (2)
103-112: 네트워크 이미지 로딩 시 사용자 피드백 및 캐싱이 없습니다.
Image.network는 로딩 중 빈 영역을 표시하며, 이미지 캐싱도 지원하지 않습니다.cached_network_image패키지를 사용하면 로딩 플레이스홀더와 캐싱을 모두 처리할 수 있습니다.💡 CachedNetworkImage 적용 제안
+import 'package:cached_network_image/cached_network_image.dart'; ... Widget _buildThumbnail() { if (place.imageUrl != null && place.imageUrl!.isNotEmpty) { - return Image.network( - place.imageUrl!, + return CachedNetworkImage( + imageUrl: place.imageUrl!, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _buildPlaceholder(), + placeholder: (_, __) => _buildPlaceholder(), + errorWidget: (_, __, ___) => _buildPlaceholder(), ); } return _buildPlaceholder(); }
20-21:GestureDetector대신InkWell을 사용하면 탭 시 시각적 피드백(리플 효과)을 제공할 수 있습니다.현재 탭 시 사용자에게 아무런 시각적 반응이 없어 터치가 인식되었는지 알기 어렵습니다.
Material+InkWell조합으로 변경하면 리플 이펙트를 통해 UX가 개선됩니다.lib/features/home/presentation/widgets/popular_place_tile.dart (1)
68-77: 네트워크 이미지 캐싱 및 로딩 상태 고려
Image.network는 이미지 캐싱을 제공하지 않아 스크롤 시마다 재다운로드가 발생할 수 있습니다. 그리드에서 다수의 이미지를 동시에 표시하는 경우 성능 및 사용자 경험에 영향을 줄 수 있습니다.
cached_network_image패키지 사용을 권장합니다. 또한loadingBuilder나frameBuilder가 없어 이미지 로딩 중 빈 영역이 표시됩니다.♻️ CachedNetworkImage 적용 제안
+import 'package:cached_network_image/cached_network_image.dart'; + Widget _buildThumbnail() { if (place.imageUrl != null && place.imageUrl!.isNotEmpty) { - return Image.network( - place.imageUrl!, + return CachedNetworkImage( + imageUrl: place.imageUrl!, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _buildPlaceholder(), + errorWidget: (_, __, ___) => _buildPlaceholder(), + placeholder: (_, __) => _buildPlaceholder(), ); } return _buildPlaceholder(); }lib/features/home/domain/repositories/home_repository.dart (1)
1-10: Domain 레이어에서 Data 레이어 모델을 직접 참조하고 있습니다.
HomeRepository는 domain 레이어에 위치하지만ContentListResponse를 data 레이어(../../data/models/content_response.dart)에서 import하고 있습니다. Clean Architecture 원칙에 따르면 의존성 방향은 외부 → 내부(data → domain)여야 합니다.프로젝트 규모와 팀 컨벤션에 따라 허용될 수 있지만, 향후 확장성을 고려한다면 domain 레이어에 별도의 entity를 정의하고 repository impl에서 매핑하는 구조를 권장합니다.
lib/features/home/data/datasources/home_remote_datasource.dart (1)
30-73: API 호출에 에러 핸들링이 없습니다.
fetchRecentContent와fetchMemberContent모두try-catch없이 Dio 호출을 수행합니다. 네트워크 오류, 타임아웃, 서버 에러(4xx/5xx) 시DioException이 그대로 상위 레이어로 전파됩니다.데이터소스 또는 리포지토리 레이어에서
DioException을 캐치하여 앱 내부의 도메인 예외로 변환하면, 상위 레이어의 에러 처리가 일관되고 명확해집니다.lib/features/home/presentation/pages/home_page.dart (1)
178-180: 인기 장소 표시 개수가 30개로 하드코딩되어 있습니다.
childCount를min(30, state.popularPlaces.length)로 제한하고 있습니다.getMemberContent의 기본size도 30이므로 현재는 동작하지만, 향후 페이지 크기가 변경되면 동기화가 깨질 수 있습니다. 상수로 추출하여 데이터소스와 UI에서 공유하는 것을 권장합니다.♻️ 상수 추출 제안
+// home_constants.dart 또는 적절한 위치에 정의 +const int kPopularPlacesMaxCount = 30;childCount: state.popularPlaces.length > 30 - ? 30 + ? kPopularPlacesMaxCount : state.popularPlaces.length,lib/features/home/data/models/content_response.dart (1)
19-23:status필드에ContentStatusenum 사용을 권장합니다.
ContentStatusenum이 명확히 정의되어 있으며,fromString()과toJson()메서드를 통해 JSON 변환을 지원합니다. 이를 사용하면 타입 안전성을 개선할 수 있습니다.반면,
createdAt을DateTime?으로 변경하는 제안은 현재 코드베이스에서 데이터 모델의 JSON 역직렬화 시 날짜 변환을 위한 인프라가 명확히 보이지 않아, 추가 설정(예:@JsonKey(fromJson: ...)와 같은 커스텀 변환기) 없이는 구현이 복잡할 수 있습니다. API 응답 형식이 문자열이므로 현재의String?타입이 의도적일 가능성이 있습니다.♻️ status 필드 타입 강화 제안
+import '../../domain/entities/content_status.dart'; + const factory ContentItem({ required int contentId, String? sourceUrl, - String? status, + ContentStatus? status, String? createdAt, `@Default`([]) List<PlaceModel> places, }) = _ContentItem;lib/features/home/presentation/providers/home_provider.dart (2)
53-59: 동시 fetch 시errorMessage경쟁 상태 가능성.
Future.wait으로fetchRecentPlaces()와fetchPopularPlaces()를 동시에 실행하는데, 두 메서드 모두 시작 시errorMessage: null로 초기화하고 실패 시 단일errorMessage필드에 쓰기합니다. 한쪽만 실패하면 문제없지만, 양쪽 모두 실패할 경우 마지막에 완료된 쪽의 에러 메시지만 남게 됩니다.현재로서는 큰 문제는 아니지만, 향후 에러 메시지를
recentErrorMessage/popularErrorMessage로 분리하면 각 섹션별 에러를 독립적으로 표시할 수 있습니다.
108-111: 페이지네이션 실패 시 사용자에게 피드백이 없습니다.
fetchMoreRecentPlacescatch 블록에서isLoadingMore만false로 설정하고 에러 메시지를 표시하지 않습니다. 네트워크 오류 시 사용자가 더 이상 데이터가 없는 것으로 오해할 수 있습니다. 스낵바 표시나 재시도 버튼 노출 등 최소한의 피드백을 고려해주세요.
| final response = await _dio.get( | ||
| ApiEndpoints.recentContent, | ||
| queryParameters: { | ||
| if (cursor != null) 'cursor': cursor, | ||
| 'size': size, | ||
| }, | ||
| ); | ||
|
|
||
| final result = ContentListResponse.fromJson( | ||
| response.data as Map<String, dynamic>, | ||
| ); | ||
| debugPrint('✅ Recent content fetched: ${result.content.length} items'); | ||
| return result; |
There was a problem hiding this comment.
response.data의 안전하지 않은 타입 캐스팅
response.data as Map<String, dynamic> 캐스팅은 응답이 null이거나 예상과 다른 타입일 경우 TypeError를 발생시킵니다. 서버 오류나 비정상 응답 시 디버깅이 어려운 런타임 예외로 이어질 수 있습니다.
🛡️ 안전한 캐스팅 제안
final response = await _dio.get(
ApiEndpoints.recentContent,
queryParameters: {
if (cursor != null) 'cursor': cursor,
'size': size,
},
);
+ final data = response.data;
+ if (data is! Map<String, dynamic>) {
+ throw FormatException('Unexpected response format: ${data.runtimeType}');
+ }
final result = ContentListResponse.fromJson(
- response.data as Map<String, dynamic>,
+ data,
);동일한 패턴이 fetchMemberContent (Line 68-69)에도 적용되어야 합니다.
🧰 Tools
🪛 GitHub Actions: Project Flutter CI
[error] 44-44: The getter 'content' isn't defined for the type 'ContentListResponse'
🤖 Prompt for AI Agents
In `@lib/features/home/data/datasources/home_remote_datasource.dart` around lines
36 - 48, The code unsafely casts response.data to Map<String, dynamic> causing
TypeError on null or unexpected responses; update the logic in the method that
calls ContentListResponse.fromJson (and likewise in fetchMemberContent) to first
verify response.data is non-null and is a Map<String, dynamic> (e.g., check
response.data is Map or use a safe cast), and handle the failure path by logging
the response/error and either throwing a descriptive exception or returning an
appropriate fallback (empty ContentListResponse or error result) before calling
ContentListResponse.fromJson so deserialization is never invoked with an invalid
type.
| 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; | ||
|
|
||
| factory CursorModel.fromJson(Map<String, dynamic> json) => | ||
| _$CursorModelFromJson(json); | ||
| } |
There was a problem hiding this comment.
코드 생성(build_runner) 실행이 필요합니다.
파이프라인에서 cursor_model.freezed.dart와 cursor_model.g.dart가 존재하지 않는다는 에러가 발생하고 있습니다. Freezed 모델을 추가한 후 반드시 코드 생성을 실행해야 합니다:
dart run build_runner build --delete-conflicting-outputs생성된 *.freezed.dart와 *.g.dart 파일을 커밋에 포함시키거나, CI 파이프라인에서 build_runner가 자동 실행되도록 설정해 주세요. 현재 이 파일뿐 아니라 content_response.dart, home_remote_datasource.dart, home_repository_impl.dart 등 다른 파일의 파이프라인 에러도 모두 동일한 원인입니다.
🧰 Tools
🪛 GitHub Actions: Project Flutter CI
[error] 3-3: Target of URI doesn't exist: 'package:mapsy/features/home/data/models/cursor_model.freezed.dart'
[error] 4-4: Target of URI hasn't been generated: 'package:mapsy/features/home/data/models/cursor_model.g.dart'
🤖 Prompt for AI Agents
In `@lib/features/home/data/models/cursor_model.dart` around lines 1 - 22, The CI
is failing because the Freezed-generated files for CursorModel
(cursor_model.freezed.dart and cursor_model.g.dart) are missing; run the Dart
build runner to generate them (dart run build_runner build
--delete-conflicting-outputs), then either commit the generated files or update
CI to run build_runner before tests/builds so models like CursorModel (and
others mentioned such as ContentResponse, HomeRemoteDatasource,
HomeRepositoryImpl) have their corresponding *.freezed.dart and *.g.dart
artifacts available at runtime.
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||
|
|
||
| part 'place_model.freezed.dart'; | ||
| part 'place_model.g.dart'; | ||
|
|
||
| /// 장소 모델 | ||
| @freezed | ||
| class PlaceModel with _$PlaceModel { | ||
| const factory PlaceModel({ | ||
| /// 장소 ID | ||
| required int placeId, | ||
|
|
||
| /// 장소명 | ||
| required String placeName, | ||
|
|
||
| /// 주소 | ||
| String? address, | ||
|
|
||
| /// 위도 | ||
| double? latitude, | ||
|
|
||
| /// 경도 | ||
| double? longitude, | ||
|
|
||
| /// 카테고리 | ||
| String? category, | ||
|
|
||
| /// 태그 목록 | ||
| @Default([]) List<String> tags, | ||
|
|
||
| /// 대표 이미지 URL | ||
| String? imageUrl, | ||
|
|
||
| /// 콘텐츠 ID (상위 콘텐츠) | ||
| int? contentId, | ||
| }) = _PlaceModel; | ||
|
|
||
| factory PlaceModel.fromJson(Map<String, dynamic> json) => | ||
| _$PlaceModelFromJson(json); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check for CI configuration files
find .github -type f -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -20Repository: MapSee-Lab/MapSy-FE
Length of output: 1159
🏁 Script executed:
# Look for pubspec.yaml to check dependencies
find . -name "pubspec.yaml" -type f | head -5Repository: MapSee-Lab/MapSy-FE
Length of output: 77
🏁 Script executed:
# Check if generated files exist
find . -name "place_model.freezed.dart" -o -name "place_model.g.dart" 2>/dev/nullRepository: MapSee-Lab/MapSy-FE
Length of output: 45
🏁 Script executed:
# Check for build_runner references in the codebase
rg "build_runner" --type yaml --type toml --type jsonRepository: MapSee-Lab/MapSy-FE
Length of output: 112
🏁 Script executed:
# Check the Flutter CI workflow configuration
cat .github/workflows/PROJECT-FLUTTER-CI.yamlRepository: MapSee-Lab/MapSy-FE
Length of output: 5899
🏁 Script executed:
# Also check if there are any generated file references in place_model.dart
cat -n lib/features/home/data/models/place_model.dartRepository: MapSee-Lab/MapSy-FE
Length of output: 1027
🏁 Script executed:
# Check pubspec.yaml to see the full freezed configuration
cat pubspec.yaml | grep -A 10 -B 5 "freezed"Repository: MapSee-Lab/MapSy-FE
Length of output: 1148
CI 실패: Freezed 코드 생성 파일이 누락되었습니다.
파일 시스템에서 place_model.freezed.dart와 place_model.g.dart가 존재하지 않습니다. place_model.dart는 이 파일들을 part 지시문으로 참조하고 있으며 (_$PlaceModel mixin, _$PlaceModelFromJson 함수), 이들 파일이 없으면 코드가 컴파일되지 않습니다.
현재 CI 파이프라인에는 build_runner build 단계가 없으므로 자동 생성도 이루어지지 않습니다.
다음 중 하나를 선택해 주세요:
- 로컬에서
dart run build_runner build --delete-conflicting-outputs실행 후 생성된 파일을 커밋하거나 - CI 파이프라인
.github/workflows/PROJECT-FLUTTER-CI.yaml에서flutter pub get이후dart run build_runner build단계를 추가해 주세요.
🧰 Tools
🪛 GitHub Actions: Project Flutter CI
[error] 3-3: Target of URI doesn't exist: 'package:mapsy/features/home/data/models/place_model.freezed.dart'
[error] 4-4: Target of URI hasn't been generated: 'package:mapsy/features/home/data/models/place_model.g.dart'
🤖 Prompt for AI Agents
In `@lib/features/home/data/models/place_model.dart` around lines 1 - 39, The
build artifacts for the Freezed/JsonSerializable parts referenced by PlaceModel
(place_model.freezed.dart, place_model.g.dart providing _$PlaceModel mixin and
_$PlaceModelFromJson) are missing; fix by either running code generation locally
with `dart run build_runner build --delete-conflicting-outputs` and committing
the generated files, or update the CI workflow to run a build step (after
flutter pub get) that executes `dart run build_runner build` so the part files
are generated during CI. Ensure generated files for PlaceModel are present in
the repo or produced in CI so _$PlaceModel and _$PlaceModelFromJson resolve.
| @override | ||
| HomeState build() { | ||
| Future.microtask(() => _initialize()); | ||
| return const HomeState(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd 'home_provider' -e 'dart' | head -20Repository: MapSee-Lab/MapSy-FE
Length of output: 122
🏁 Script executed:
ls -la lib/features/home/presentation/providers/ | grep -E "home_provider|\.g\.dart|\.freezed"Repository: MapSee-Lab/MapSy-FE
Length of output: 130
🏁 Script executed:
cat -n lib/features/home/presentation/providers/home_provider.dartRepository: MapSee-Lab/MapSy-FE
Length of output: 4836
파이프라인 경고: @override가 상속된 메서드를 오버라이드하지 않습니다.
CI에서 Line 48에 The method doesn't override an inherited method 경고가 발생하는 것으로 확인됩니다. @riverpod 코드 생성이 올바르게 실행되었는지 확인하고, dart run build_runner build를 다시 실행해주세요.
또한, Future.microtask(() => _initialize())는 fire-and-forget 방식입니다. 현재는 fetchRecentPlaces()와 fetchPopularPlaces() 모두 try-catch로 보호되어 있어 괜찮지만, _initialize() 자체에서 발생하는 예외는 상위에서 포착되지 않습니다. 향후 _initialize()에 보호되지 않은 로직이 추가될 경우 주의가 필요합니다.
🧰 Tools
🪛 GitHub Actions: Project Flutter CI
[warning] 48-48: The method doesn't override an inherited method
🤖 Prompt for AI Agents
In `@lib/features/home/presentation/providers/home_provider.dart` around lines 47
- 51, The `@override` warning means the generated Riverpod signature doesn't match
this method; first run the generator (dart run build_runner build) to ensure the
provider code is up-to-date and that the method signature (build / HomeState)
matches the generated code; if generation is intentionally not used, remove the
`@override` annotation so it stops reporting as not overriding. Also avoid silent
fire-and-forget for initialization: change Future.microtask(() => _initialize())
to a form that surface errors (either await the call in an async build or attach
error handling like Future.microtask(() => _initialize()).catchError(...)) so
exceptions from _initialize (not just fetchRecentPlaces/fetchPopularPlaces) are
handled; references: HomeState, build(), _initialize(), fetchRecentPlaces(),
fetchPopularPlaces().
❌ Flutter CI 실패
💡 확인 사항Analyze 실패 시:
Android 빌드 실패 시:
iOS 빌드 실패 시:
|
❌ Flutter CI 실패
💡 확인 사항Analyze 실패 시:
Android 빌드 실패 시:
iOS 빌드 실패 시:
|
Summary by CodeRabbit
출시 노트
새로운 기능
사용성 개선