20260223 26 프로필 조회 수정 설정 화면 필요#41
Hidden character warning
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…CheckNameResponse) #26 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
❌ Flutter CI 실패
💡 확인 사항Analyze 실패 시:
Android 빌드 실패 시:
iOS 빌드 실패 시:
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. WalkthroughMyPage 설계·구현 문서 추가, MyPage 원격 데이터소스/레포지토리/모델 및 Riverpod provider 구현, 관련 UI(프로필 카드·설정 타일·닉네임 편집 바텀시트·약관/개인정보 페이지) 추가, AI 추출 및 PlaceModel Freezed/JSON/Provider 생성과 홈 모듈 import 정리. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant UI as "UI (BottomSheet / MypagePage)"
participant Editor as "NicknameEditor (Notifier)"
participant Repo as "MypageRepository"
participant Remote as "MypageRemoteDataSource"
participant API as "Server API"
rect rgba(200,200,255,0.5)
UI->>Editor: 입력 후 저장 요청 (newNickname)
Editor->>Editor: 클라이언트 유효성 검사
Editor->>Repo: checkName(newNickname)
Repo->>Remote: checkName(name)
Remote->>API: GET /check-name?name={name}
API-->>Remote: 200 {isAvailable, name}
Remote-->>Repo: CheckNameResponse
Repo-->>Editor: 가용성 결과
alt 사용 가능
Editor->>Repo: updateNickname(newNickname)
Repo->>Remote: updateProfile({name})
Remote->>API: POST /member/profile {name}
API-->>Remote: 200 OK
Remote-->>Repo: 성공
Repo-->>Editor: 성공 응답
Editor->>UI: 성공 → 닫기(true) / 상태 갱신
else 사용 불가 or 오류
Editor-->>UI: 에러 메시지 갱신
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 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)
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: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/features/home/data/models/content_response.dart (1)
26-26:⚠️ Potential issue | 🔴 Critical
ContentItem.places의toJson직렬화 문제 확인생성된
content_response.g.dart의_$$ContentItemImplToJson메서드에서places필드가'places': instance.places,로 생성되어 있습니다. 이는List<PlaceModel>내의 각PlaceModel객체를 JSON으로 직렬화하지 않고 그대로 전달하는 것으로,PlaceModel의 복잡한 속성들(placeId, placeName, address, latitude, longitude, category, tags, imageUrl 등)이 제대로 직렬화되지 않습니다.
fromJson에서는PlaceModel.fromJson()으로 올바르게 역직렬화되지만,toJson에서는 대응하는 직렬화가 누락되어 있습니다.@JsonSerializable(explicitToJson: true)사용을 검토하거나 수동 직렬화를 추가해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/home/data/models/content_response.dart` at line 26, The generated toJson for ContentItem is serializing places as a raw List<PlaceModel> which prevents each PlaceModel from being converted to JSON; update the ContentItem/freezed/@JsonSerializable configuration so places is serialized properly—either add `@JsonSerializable`(explicitToJson: true) to the ContentItem class or change the places serialization to map each PlaceModel via place.toJson() (ensure PlaceModel has toJson) so that ContentItem.places is emitted as a List of JSON maps rather than raw objects.
🧹 Nitpick comments (5)
lib/features/mypage/data/models/profile_update_request.dart (1)
7-13:name필드만 포함된 DTO 범위 확인 권장현재
ProfileUpdateRequest는name단일 필드만 정의되어 있습니다. PR 목표(프로필 조회·수정·설정)에서 프로필 이미지, 소개글 등 추가 수정 가능한 필드가 존재한다면, 해당 필드도 이 DTO에 포함되어야 합니다. 의도적으로 이름만 수정하는 범위라면 무시해도 됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/mypage/data/models/profile_update_request.dart` around lines 7 - 13, The DTO ProfileUpdateRequest currently exposes only the name field; if the PR intends to support updating other profile attributes (e.g., profile image/avatar, bio/introduction, maybe visibility flags), extend the const factory ProfileUpdateRequest to include those additional required/nullable fields (e.g., String? avatarUrl, String? bio, bool? isPublic) and regenerate the JSON serialization (the ProfileUpdateRequest.fromJson/_$ProfileUpdateRequestFromJson) so the new fields are serialized/deserialized, and then update any callers that construct or consume ProfileUpdateRequest to pass or handle the new fields accordingly; if the intent is truly to only allow name changes, add a clarifying comment on ProfileUpdateRequest to indicate that scope.docs/plans/2026-02-23-mypage-design.md (1)
91-91:MypageRepository에서 직접AuthNotifier상태를 갱신하는 것은 레이어 경계 위반입니다.Data 레이어인 Repository가 Presentation 레이어인
AuthNotifier를 직접 참조하면 의존성 방향이 역전됩니다. 닉네임 수정 성공 후AuthNotifier상태 갱신은 Presentation 레이어(예:MypageNotifier)에서 Repository 호출 결과를 받아AuthNotifier.refresh()또는 유사 메서드를 호출하는 방식으로 조율하는 것이 좋습니다.♻️ 권장 흐름 예시
// Presentation 레이어 (MypageNotifier)에서 조율 await mypageRepository.updateProfile(request); // Data 레이어 await authNotifier.refresh(); // Presentation 레이어 간 협력🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-02-23-mypage-design.md` at line 91, MypageRepository should not directly update AuthNotifier; remove any direct references from MypageRepository and instead have updateProfile (or the relevant method on MypageRepository) return a success/result object; then call AuthNotifier.refresh() (or equivalent) from the presentation orchestrator (e.g., MypageNotifier) after awaiting mypageRepository.updateProfile(...). Ensure MypageNotifier handles errors from updateProfile and triggers AuthNotifier only on success so layering remains correct.lib/features/home/presentation/home_provider.dart (1)
5-6: [선택적 개선] 내부 파일 임포트 스타일 통일 권장Line 5는
package:mapsy/...절대 임포트, Line 6은'../...'상대 임포트로 혼용되고 있습니다. 동일한 패키지 내부 파일들은 Dart 스타일 가이드 권고에 따라 상대 임포트로 통일하는 것이 일관성 있습니다.place_card.dart,content_response.dart에도 동일한 패턴이 있습니다.♻️ 제안 변경
-import 'package:mapsy/common/models/place_model.dart'; +import '../../../common/models/place_model.dart';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/home/presentation/home_provider.dart` around lines 5 - 6, Imports in home_provider.dart mix package and relative styles (e.g., import 'package:mapsy/common/models/place_model.dart'; vs import '../data/home_repository_impl.dart'), which is inconsistent with the Dart style suggestion to use relative imports for files inside the same package; update the import of PlaceModel (and other same-package imports like place_card.dart and content_response.dart) to use relative paths instead of package: paths so all internal imports in this module are relative and consistent (locate the import statements in home_provider.dart and similarly update occurrences in place_card.dart and content_response.dart).lib/features/ai_extraction/data/models/content_detail_response.g.dart (1)
22-29: 중첩PlaceModel객체의toJson()미호출 —explicitToJson: true권장
'places': instance.places는List<PlaceModel>인스턴스를 그대로 Map에 저장합니다.dart:convert의jsonEncode는 내부적으로toJson()을 호출하므로 일반적인 경우엔 동작하지만,explicitToJson: false(기본값) 상태의toJson출력은 JSON 명세(List/Map/num/bool/String만 허용)를 준수하지 않으며,dart:convert를 거치지 않는 인코더는 예외를 던집니다. 예를 들어 Hive, Isar 등의 로컬 캐시, 일부 HTTP 인터셉터, 또는 테스트의 직접 Map 비교 시 문제가 생길 수 있습니다.
explicitToJson: true를 설정하면 중첩 클래스에서toJson()을 강제로 호출하도록 생성 코드가 변경됩니다.소스 파일(
content_detail_response.dart)의@freezed클래스에 아래와 같이 애노테이션을 추가한 뒤build_runner를 재실행하면'places': instance.places.map((e) => e.toJson()).toList()로 생성됩니다.🔧 소스 파일에 적용할 변경 (content_detail_response.dart)
+@JsonSerializable(explicitToJson: true) `@freezed` class ContentDetailResponse with _$ContentDetailResponse {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/ai_extraction/data/models/content_detail_response.g.dart` around lines 22 - 29, The generated toJson for _$ContentDetailResponseImpl leaves nested PlaceModel objects un-serialized (`'places': instance.places`); update the `@freezed` annotation on the ContentDetailResponse class (in content_detail_response.dart) to set explicitToJson: true (e.g., `@Freezed`(explicitToJson: true)) and re-run build_runner so the generator emits a proper nested serialization (places mapped via e.toJson()). Ensure the class name ContentDetailResponse and the places field remain unchanged so the regenerated _$ContentDetailResponseImplToJson uses instance.places.map((e) => e.toJson()).toList().docs/plans/2026-02-23-mypage-implementation.md (1)
351-360:copyWith에서 nullable 필드errorMessage가 보존되지 않습니다.현재 구현에서
errorMessage: errorMessage는null coalescing을 사용하지 않아서,errorMessage를 명시하지 않고state.copyWith(isLoading: false)를 호출하면 기존 에러 메시지가 항상null로 초기화됩니다. 현재 코드 흐름에서는 우연히 동작하지만(line 317은 성공 경로), 표준copyWith계약을 위반하여 향후 호출자에게 잠재적 버그를 유발합니다.Dart에서 nullable 필드의
copyWith처리는Object()센티넬 패턴을 사용하거나, Freezed를 사용하면 자동으로 해결됩니다.♻️ 센티넬 패턴을 사용한 수정 방안
+ static const _unset = Object(); + NicknameEditState copyWith({ bool? isLoading, - String? errorMessage, + Object? errorMessage = _unset, }) { return NicknameEditState( isLoading: isLoading ?? this.isLoading, - errorMessage: errorMessage, + errorMessage: errorMessage == _unset + ? this.errorMessage + : errorMessage as String?, ); }기존에
null로 명시적으로 초기화하는 호출(state.copyWith(errorMessage: null))은 그대로 동작하며,errorMessage를 생략한 호출은 기존 값을 유지합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-02-23-mypage-implementation.md` around lines 351 - 360, The copyWith in NicknameEditState currently overwrites the existing nullable errorMessage when the caller omits that parameter; update NicknameEditState.copyWith to use a sentinel pattern so omitted parameters keep their current values while callers can still explicitly pass null: add a private sentinel (e.g. const _undefined = Object()), change the copyWith signature to accept Object? errorMessage = _undefined, and when constructing the new NicknameEditState choose (if identical(errorMessage, _undefined) then this.errorMessage else errorMessage as String?). Keep the isLoading handling as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/plans/2026-02-23-mypage-design.md`:
- Around line 119-122: 문서의 알림 토글 동작에 AppLifecycleState.resumed 처리 누락이 있으므로, 앱이
포그라운드로 돌아올 때(AppLifecycleState.resumed) 시스템 권한을 재확인하고 토글 상태를 동기화하도록 설계에 추가하세요;
구체적으로는 onResume 또는 WidgetsBindingObserver의 didChangeAppLifecycleState에서 권한 체크를
호출하고(예: checkNotificationPermission), 권한이 거부되어 있으면 SharedPreferences와 UI 토글
상태(알림 토글 위젯)를 OFF로 롤백하고 사용자에게 재안내 또는 설명 토스트를 표시하도록 명시하세요.
- Line 15: 마크다운의 펜스 코드 블록들이 언어 식별자 없이 작성되어 MD040 경고가 발생하므로, ASCII 아트 블록(예: 블록
시작에 "┌─────────────────────────────────┐"가 있는 블록), 파일 트리 블록(예:
"lib/features/mypage/"), 라우팅 목록 블록(예: "/mypage"로 시작하는 블록) 등 모든 ``` 코드 블록에 언어 식별자
text 또는 plaintext를 추가하여 ```text 또는 ```plaintext 형태로 수정해 주세요.
In `@docs/plans/2026-02-23-mypage-implementation.md`:
- Around line 1026-1029: The code reads nickname from Firebase Auth
(user?.displayName) while updateNickname updates the backend
(/api/members/profile), causing stale UI; pick one strategy—preferably backend
single source: introduce a currentUserProfileProvider that fetches the member
profile and use its nickname instead of user?.displayName, update updateNickname
to call the backend and on success call
ref.invalidate(currentUserProfileProvider) (instead of
ref.invalidate(authNotifierProvider)); alternatively, if you choose Firebase as
source, modify the backend update to also update Firebase displayName
(server-side Firebase Admin) so user and backend stay in sync.
- Around line 120-126: The code unsafely force-casts response.data to
Map<String,dynamic> before calling CheckNameResponse.fromJson, which can throw a
_CastError for null, arrays, or error bodies; update the parsing in the function
that calls CheckNameResponse.fromJson to first validate and coerce response.data
(the variable named response.data) is a Map<String,dynamic> — e.g. check
response.data is Map, attempt a safe cast or Map<String,dynamic>.from with
try/catch, handle null/empty bodies and non-map payloads by returning a sensible
fallback or throwing a descriptive exception, and then pass the validated map to
CheckNameResponse.fromJson so runtime cast errors are avoided.
- Around line 1182-1186: After calling await on signOut()/withdraw() in
_onLogout and _onWithdraw, add a guard checking context.mounted before doing any
further UI work (e.g., snackbars, navigation) to avoid using a disposed context;
specifically, after await ref.read(authNotifierProvider.notifier).signOut() (and
the corresponding withdraw call) return early or wrap subsequent context usage
in if (!context.mounted) return; so the pattern matches the existing check used
in _onProfileTap.
- Line 3: Remove the AI meta-instruction line that begins with "**For Claude:**
REQUIRED SUB-SKILL: Use superpowers:executing-plans" from the document; locate
the exact string and delete it, then scan the same file for any other
agent/runtime prompt fragments and remove them as well (ensure the document
contains only user-facing content), and commit the cleaned document with a brief
message indicating removal of embedded AI runtime prompts.
---
Outside diff comments:
In `@lib/features/home/data/models/content_response.dart`:
- Line 26: The generated toJson for ContentItem is serializing places as a raw
List<PlaceModel> which prevents each PlaceModel from being converted to JSON;
update the ContentItem/freezed/@JsonSerializable configuration so places is
serialized properly—either add `@JsonSerializable`(explicitToJson: true) to the
ContentItem class or change the places serialization to map each PlaceModel via
place.toJson() (ensure PlaceModel has toJson) so that ContentItem.places is
emitted as a List of JSON maps rather than raw objects.
---
Duplicate comments:
In `@lib/features/home/data/models/content_response.dart`:
- Around line 3-4: The imports in content_response.dart are mixed (relative
import for cursor_model.dart and package import for place_model.dart); update
the cursor import to use the same package-style import pattern used in
home_provider.dart so all imports are consistent (replace the relative
"cursor_model.dart" import with the equivalent package import used elsewhere).
In `@lib/features/home/presentation/widgets/place_card.dart`:
- Around line 4-5: Imports in place_card.dart are using a relative path for
home_colors.dart while other files (like home_provider.dart) use package-style
imports; change the relative import import
'../../../../common/constants/home_colors.dart'; to the package import format
(e.g., import 'package:mapsy/common/constants/home_colors.dart';), keep the
existing package import for PlaceModel, and ensure import ordering/grouping
matches the project's convention.
---
Nitpick comments:
In `@docs/plans/2026-02-23-mypage-design.md`:
- Line 91: MypageRepository should not directly update AuthNotifier; remove any
direct references from MypageRepository and instead have updateProfile (or the
relevant method on MypageRepository) return a success/result object; then call
AuthNotifier.refresh() (or equivalent) from the presentation orchestrator (e.g.,
MypageNotifier) after awaiting mypageRepository.updateProfile(...). Ensure
MypageNotifier handles errors from updateProfile and triggers AuthNotifier only
on success so layering remains correct.
In `@docs/plans/2026-02-23-mypage-implementation.md`:
- Around line 351-360: The copyWith in NicknameEditState currently overwrites
the existing nullable errorMessage when the caller omits that parameter; update
NicknameEditState.copyWith to use a sentinel pattern so omitted parameters keep
their current values while callers can still explicitly pass null: add a private
sentinel (e.g. const _undefined = Object()), change the copyWith signature to
accept Object? errorMessage = _undefined, and when constructing the new
NicknameEditState choose (if identical(errorMessage, _undefined) then
this.errorMessage else errorMessage as String?). Keep the isLoading handling
as-is.
In `@lib/features/ai_extraction/data/models/content_detail_response.g.dart`:
- Around line 22-29: The generated toJson for _$ContentDetailResponseImpl leaves
nested PlaceModel objects un-serialized (`'places': instance.places`); update
the `@freezed` annotation on the ContentDetailResponse class (in
content_detail_response.dart) to set explicitToJson: true (e.g.,
`@Freezed`(explicitToJson: true)) and re-run build_runner so the generator emits a
proper nested serialization (places mapped via e.toJson()). Ensure the class
name ContentDetailResponse and the places field remain unchanged so the
regenerated _$ContentDetailResponseImplToJson uses instance.places.map((e) =>
e.toJson()).toList().
In `@lib/features/home/presentation/home_provider.dart`:
- Around line 5-6: Imports in home_provider.dart mix package and relative styles
(e.g., import 'package:mapsy/common/models/place_model.dart'; vs import
'../data/home_repository_impl.dart'), which is inconsistent with the Dart style
suggestion to use relative imports for files inside the same package; update the
import of PlaceModel (and other same-package imports like place_card.dart and
content_response.dart) to use relative paths instead of package: paths so all
internal imports in this module are relative and consistent (locate the import
statements in home_provider.dart and similarly update occurrences in
place_card.dart and content_response.dart).
In `@lib/features/mypage/data/models/profile_update_request.dart`:
- Around line 7-13: The DTO ProfileUpdateRequest currently exposes only the name
field; if the PR intends to support updating other profile attributes (e.g.,
profile image/avatar, bio/introduction, maybe visibility flags), extend the
const factory ProfileUpdateRequest to include those additional required/nullable
fields (e.g., String? avatarUrl, String? bio, bool? isPublic) and regenerate the
JSON serialization (the
ProfileUpdateRequest.fromJson/_$ProfileUpdateRequestFromJson) so the new fields
are serialized/deserialized, and then update any callers that construct or
consume ProfileUpdateRequest to pass or handle the new fields accordingly; if
the intent is truly to only allow name changes, add a clarifying comment on
ProfileUpdateRequest to indicate that scope.
|
|
||
| 단일 ScrollView, 3개 섹션: | ||
|
|
||
| ``` |
There was a problem hiding this comment.
펜스 코드 블록에 언어 식별자를 추가해 주세요.
마크다운 린터(MD040)가 언어 미지정 경고를 발생시키고 있습니다. ASCII 아트 다이어그램, 파일 트리, 라우팅 목록 모두 text 또는 plaintext로 지정하면 됩니다.
📝 제안 수정
- ```
+ ```text
┌─────────────────────────────────┐
...- ```
+ ```text
lib/features/mypage/
...- ```
+ ```text
/mypage
...Also applies to: 60-60, 96-96
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 15-15: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-02-23-mypage-design.md` at line 15, 마크다운의 펜스 코드 블록들이 언어 식별자
없이 작성되어 MD040 경고가 발생하므로, ASCII 아트 블록(예: 블록 시작에
"┌─────────────────────────────────┐"가 있는 블록), 파일 트리 블록(예:
"lib/features/mypage/"), 라우팅 목록 블록(예: "/mypage"로 시작하는 블록) 등 모든 ``` 코드 블록에 언어 식별자
text 또는 plaintext를 추가하여 ```text 또는 ```plaintext 형태로 수정해 주세요.
| ### 알림 설정 | ||
|
|
||
| - 토글 ON: 시스템 알림 권한 확인 → 권한 없으면 설정 앱 안내 | ||
| - 토글 OFF: SharedPreferences에 즉시 저장 |
There was a problem hiding this comment.
알림 토글 ON → 설정 앱 이동 후 권한 미허용 복귀 시 상태 처리가 누락되어 있습니다.
현재 문서는 토글 ON 시 "권한 없으면 설정 앱 안내"까지만 명시하고 있습니다. 사용자가 시스템 설정 앱으로 이동했다가 권한을 허용하지 않고 앱으로 복귀했을 때, 토글은 ON 상태로 남지만 실제 권한은 거부된 상황이 발생합니다.
구현 전에 아래 케이스를 설계에 포함해야 합니다:
AppLifecycleState.resumed시점에 권한 상태 재확인 후 토글 상태 동기화- 권한이 여전히 거부된 경우 토글을 OFF로 되돌리고 사용자에게 재안내하거나, 토글을 비활성 상태로 유지
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-02-23-mypage-design.md` around lines 119 - 122, 문서의 알림 토글 동작에
AppLifecycleState.resumed 처리 누락이 있으므로, 앱이 포그라운드로 돌아올
때(AppLifecycleState.resumed) 시스템 권한을 재확인하고 토글 상태를 동기화하도록 설계에 추가하세요; 구체적으로는
onResume 또는 WidgetsBindingObserver의 didChangeAppLifecycleState에서 권한 체크를 호출하고(예:
checkNotificationPermission), 권한이 거부되어 있으면 SharedPreferences와 UI 토글 상태(알림 토글
위젯)를 OFF로 롤백하고 사용자에게 재안내 또는 설명 토스트를 표시하도록 명시하세요.
| @@ -0,0 +1,1266 @@ | |||
| # 마이페이지 구현 계획 | |||
|
|
|||
| > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. | |||
There was a problem hiding this comment.
AI 메타 지시문을 문서에서 제거하세요.
> **For Claude:** REQUIRED SUB-SKILL: ... 구문은 AI 에이전트용 런타임 프롬프트 지시문으로, 소스 코드 저장소에 커밋될 문서에 포함될 내용이 아닙니다. 버전 히스토리에 영구적으로 남으며, AI 코드 리뷰 도구나 자동화된 파이프라인에서 프롬프트 인젝션으로 잘못 해석될 수 있습니다.
🛠️ 수정 방안
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-02-23-mypage-implementation.md` at line 3, Remove the AI
meta-instruction line that begins with "**For Claude:** REQUIRED SUB-SKILL: Use
superpowers:executing-plans" from the document; locate the exact string and
delete it, then scan the same file for any other agent/runtime prompt fragments
and remove them as well (ensure the document contains only user-facing content),
and commit the cleaned document with a brief message indicating removal of
embedded AI runtime prompts.
|
|
||
| final result = CheckNameResponse.fromJson( | ||
| response.data as Map<String, dynamic>, | ||
| ); | ||
| debugPrint('✅ Name check result: isAvailable=${result.isAvailable}'); | ||
| return result; | ||
| } |
There was a problem hiding this comment.
API 응답 데이터에 대한 안전하지 않은 타입 캐스팅.
response.data as Map<String, dynamic> 는 강제 캐스팅(hard cast)이므로, 서버가 빈 바디, 배열, 혹은 오류 객체를 반환할 경우 _CastError가 발생하여 처리되지 않는 런타임 예외로 이어집니다.
🛡️ 수정 방안
- final result = CheckNameResponse.fromJson(
- response.data as Map<String, dynamic>,
- );
+ final data = response.data;
+ if (data is! Map<String, dynamic>) {
+ throw FormatException('Unexpected response format for checkName: $data');
+ }
+ final result = CheckNameResponse.fromJson(data);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| final result = CheckNameResponse.fromJson( | |
| response.data as Map<String, dynamic>, | |
| ); | |
| debugPrint('✅ Name check result: isAvailable=${result.isAvailable}'); | |
| return result; | |
| } | |
| final data = response.data; | |
| if (data is! Map<String, dynamic>) { | |
| throw FormatException('Unexpected response format for checkName: $data'); | |
| } | |
| final result = CheckNameResponse.fromJson(data); | |
| debugPrint('✅ Name check result: isAvailable=${result.isAvailable}'); | |
| return result; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-02-23-mypage-implementation.md` around lines 120 - 126, The
code unsafely force-casts response.data to Map<String,dynamic> before calling
CheckNameResponse.fromJson, which can throw a _CastError for null, arrays, or
error bodies; update the parsing in the function that calls
CheckNameResponse.fromJson to first validate and coerce response.data (the
variable named response.data) is a Map<String,dynamic> — e.g. check
response.data is Map, attempt a safe cast or Map<String,dynamic>.from with
try/catch, handle null/empty bodies and non-map payloads by returning a sensible
fallback or throwing a descriptive exception, and then pass the validated map to
CheckNameResponse.fromJson so runtime cast errors are avoided.
| // 사용자 정보 (Firebase User에서) | ||
| final user = authState.valueOrNull; | ||
| final nickname = user?.displayName ?? '사용자'; | ||
| final email = user?.email ?? ''; |
There was a problem hiding this comment.
닉네임 데이터 소스 불일치: Firebase displayName vs. 백엔드 프로필.
nickname을 user?.displayName(Firebase Auth) 에서 읽지만, updateNickname은 백엔드 /api/members/profile API를 통해 서버 측 데이터를 업데이트합니다. 백엔드가 Firebase Auth의 displayName을 동기적으로 업데이트하지 않는 한, ref.invalidate(authNotifierProvider) (line 1159)로 Firebase 상태를 재조회해도 UI에는 변경 이전 닉네임이 그대로 표시됩니다.
해결 방법을 두 가지 중 하나로 통일해야 합니다:
- 백엔드에서 Firebase
displayName도 함께 업데이트 (서버 측 Firebase Admin SDK 사용) - 닉네임 소스를 백엔드 전용으로 분리 — 별도의
currentUserProfileProvider를 두고, 닉네임 변경 성공 시 해당 provider를invalidate하여 서버에서 최신 닉네임을 재조회
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-02-23-mypage-implementation.md` around lines 1026 - 1029, The
code reads nickname from Firebase Auth (user?.displayName) while updateNickname
updates the backend (/api/members/profile), causing stale UI; pick one
strategy—preferably backend single source: introduce a
currentUserProfileProvider that fetches the member profile and use its nickname
instead of user?.displayName, update updateNickname to call the backend and on
success call ref.invalidate(currentUserProfileProvider) (instead of
ref.invalidate(authNotifierProvider)); alternatively, if you choose Firebase as
source, modify the backend update to also update Firebase displayName
(server-side Firebase Admin) so user and backend stay in sync.
|
|
||
| if (confirmed == true) { | ||
| await ref.read(authNotifierProvider.notifier).signOut(); | ||
| } | ||
| } |
There was a problem hiding this comment.
signOut/withdraw 완료 후 context.mounted 검사 누락.
_onLogout(line 1183)과 _onWithdraw(line 1209)에서 await signOut()/await withdraw() 완료 후 별도 코드 실행은 없지만, 이 메서드들이 향후 확장될 때(예: 스낵바 표시, 라우팅 처리) context.mounted 검사가 없으면 위젯이 이미 dispose된 상태에서 context를 사용하는 문제가 발생합니다. 이미 _onProfileTap(line 1151)에서 context.mounted를 올바르게 확인하고 있으므로, 동일한 패턴을 적용하는 것이 일관성 있습니다.
🛡️ 수정 방안 (두 메서드 공통 패턴)
if (confirmed == true) {
await ref.read(authNotifierProvider.notifier).signOut();
+ // 향후 signOut 후 처리 추가 시 mounted 검사 필요
+ // if (!context.mounted) return;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (confirmed == true) { | |
| await ref.read(authNotifierProvider.notifier).signOut(); | |
| } | |
| } | |
| if (confirmed == true) { | |
| await ref.read(authNotifierProvider.notifier).signOut(); | |
| // 향후 signOut 후 처리 추가 시 mounted 검사 필요 | |
| // if (!context.mounted) return; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-02-23-mypage-implementation.md` around lines 1182 - 1186,
After calling await on signOut()/withdraw() in _onLogout and _onWithdraw, add a
guard checking context.mounted before doing any further UI work (e.g.,
snackbars, navigation) to avoid using a disposed context; specifically, after
await ref.read(authNotifierProvider.notifier).signOut() (and the corresponding
withdraw call) return early or wrap subsequent context usage in if
(!context.mounted) return; so the pattern matches the existing check used in
_onProfileTap.
…apSee-Lab/MapSy-FE into 20260223_26_프로필_조회_수정_설정_화면_필요
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
❌ Flutter CI 실패
💡 확인 사항Android 빌드 실패 시:
iOS 빌드 실패 시:
|
- appVersionProvider: 앱 버전 조회 (PackageInfo) - NotificationSetting: 알림 설정 토글 (SharedPreferences) - NicknameEditor: 닉네임 유효성 검증 + 중복확인 + 변경 - build_runner 네트워크 이슈로 .g.dart 수동 생성 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…apSee-Lab/MapSy-FE into 20260223_26_프로필_조회_수정_설정_화면_필요
❌ Flutter CI 실패
💡 확인 사항Analyze 실패 시:
Android 빌드 실패 시:
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (6)
lib/features/ai_extraction/presentation/pages/ai_extraction_page.dart (1)
59-61:mounted체크가context.mounted와 중복됩니다.
ConsumerState(=State서브클래스) 내부에서는this.mounted와this.context.mounted가 항상 동일한 조건을 반영합니다. 따라서 두 조건을 중첩하는 것은 불필요하며, Flutter 3.7+ 에서의 권장 패턴은 비동기 갭(async gap) 이후context.mounted단독 체크입니다.♻️ 중복 체크 제거 제안
Future.delayed(const Duration(seconds: 1), () { - if (mounted) { - if (context.mounted) context.pop(); - } + if (context.mounted) context.pop(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/ai_extraction/presentation/pages/ai_extraction_page.dart` around lines 59 - 61, Remove the redundant mounted check inside the ConsumerState: replace the nested "if (mounted) { if (context.mounted) context.pop(); }" with a single post-async-gap check using context.mounted and then call context.pop(); keep references to ConsumerState, mounted, context.mounted, and context.pop() so you locate the block to change.lib/features/mypage/data/mypage_repository_impl.dart (2)
23-35: Repository 레이어에서 에러 변환(error translation) 미구현일반적으로 Repository 레이어는 데이터 소스의 인프라 예외(예:
DioException)를 도메인 수준 예외로 변환하는 역할을 합니다. 현재는 datasource의 예외가 그대로 상위 레이어로 전파됩니다.지금 당장 필수는 아니지만, 추후 에러 핸들링 고도화 시 이 레이어에서 처리하는 것이 적절합니다.
♻️ 참고: 에러 변환 패턴 예시
`@override` Future<CheckNameResponse> checkName(String name) async { debugPrint('📝 MypageRepo: Checking name...'); - return await _remoteDataSource.checkName(name); + try { + return await _remoteDataSource.checkName(name); + } on DioException catch (e) { + throw MypageException('닉네임 확인 실패: ${e.message}'); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/mypage/data/mypage_repository_impl.dart` around lines 23 - 35, The repository methods checkName and updateNickname currently let datasource exceptions (e.g., DioException) bubble up; wrap calls to _remoteDataSource.checkName and _remoteDataSource.updateProfile in try/catch blocks, catch transport/infrastructure exceptions (DioException or generic Exception), map them to a domain-level exception (create or use a MyPageRepositoryException/RepositoryException or specific domain errors) and rethrow that mapped exception so callers receive translated domain errors instead of raw infra errors; ensure you convert both checkName (returning CheckNameResponse) and updateNickname (using ProfileUpdateRequest) consistently and preserve original error details when constructing the domain exception.
25-25: Datasource와 중복되는 debugPrint 로깅Remote datasource 레이어에서 이미 동일한 작업에 대한 로그를 출력하고 있어, repository 레이어의
debugPrint가 중복됩니다. 한 레이어에서만 로깅하면 로그 노이즈를 줄일 수 있습니다.Also applies to: 31-31
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/mypage/data/mypage_repository_impl.dart` at line 25, Repository-level debugPrint calls in MypageRepositoryImpl (e.g., the '📝 MypageRepo: Checking name...' and the similar print at the other occurrence) are duplicating logs already emitted by the remote datasource; remove these debugPrint statements from MypageRepositoryImpl so only the datasource emits that log, or replace them with a single non-verbose logger call if you need a repository-level trace. Locate the debugPrint calls in MypageRepositoryImpl and delete them (or downgrade them) so logging is not duplicated across layers.lib/features/mypage/presentation/pages/terms_page.dart (1)
7-36:TermsPage와PrivacyPolicyPage의 빌드 구조가 완전히 중복됩니다.두 위젯은 AppBar 타이틀 문자열과 본문 텍스트 상수만 다르고, Scaffold/AppBar/SingleChildScrollView/Text 구조가 동일합니다. 내부 공유 위젯(
_PolicyPage)으로 추출하여 중복을 제거하세요.♻️ 공통 위젯 추출 제안
새 파일 또는 두 파일 중 한 곳에 다음 내부 위젯을 정의하세요:
// lib/features/mypage/presentation/pages/_policy_page.dart class _PolicyPage extends StatelessWidget { final String title; final String content; const _PolicyPage({required this.title, required this.content}); `@override` Widget build(BuildContext context) { return Scaffold( backgroundColor: HomeColors.background, appBar: AppBar( backgroundColor: HomeColors.background, elevation: 0, scrolledUnderElevation: 0, title: Text( title, style: AppTextStyles.heading02.copyWith( color: HomeColors.textPrimary, ), ), ), body: SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), child: Text( content, style: AppTextStyles.paragraph.copyWith( color: HomeColors.textPrimary, height: 1.6, ), ), ), ); } }그런 다음
TermsPage와PrivacyPolicyPage를 아래처럼 단순화할 수 있습니다:class TermsPage extends StatelessWidget { const TermsPage({super.key}); `@override` Widget build(BuildContext context) { - return Scaffold( - backgroundColor: HomeColors.background, - appBar: AppBar( - backgroundColor: HomeColors.background, - elevation: 0, - scrolledUnderElevation: 0, - title: Text( - '이용약관', - style: AppTextStyles.heading02.copyWith( - color: HomeColors.textPrimary, - ), - ), - ), - body: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), - child: Text( - _termsText, - style: AppTextStyles.paragraph.copyWith( - color: HomeColors.textPrimary, - height: 1.6, - ), - ), - ), - ); + return const _PolicyPage(title: '이용약관', content: _termsText); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/mypage/presentation/pages/terms_page.dart` around lines 7 - 36, The TermsPage and PrivacyPolicyPage build bodies are duplicated; extract a shared StatelessWidget (e.g., _PolicyPage) that takes final String title and final String content, implements the Scaffold/AppBar/SingleChildScrollView/Text structure using AppTextStyles.heading02 and AppTextStyles.paragraph with HomeColors, and then have TermsPage and PrivacyPolicyPage simply return _PolicyPage(title: '이용약관', content: _termsText) and the privacy equivalent; update imports if you place _PolicyPage in a new file.lib/features/mypage/presentation/widgets/profile_card.dart (1)
1-72: LGTM.
borderRadius가InkWell에 올바르게 설정되어 있어 리플 효과가 의도대로 클리핑됩니다.height: 48.w는 원형 아바타의 1:1 비율을 보장하기 위한 의도적인 ScreenUtil 패턴으로 정상입니다.단,
AppColors(app_colors.dart)와HomeColors(home_colors.dart) 두 컬러 시스템이 혼용되고 있습니다. 장기적으로는 컬러 시스템을 단일화하는 것이 유지보수에 유리합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/mypage/presentation/widgets/profile_card.dart` around lines 1 - 72, This file mixes two color systems (AppColors and HomeColors) which should be unified; update ProfileCard to use a single color source throughout (choose either AppColors or HomeColors), e.g. replace AppColors.gray100 and AppColors.gray400 with the equivalent values from the chosen system (or replace HomeColors.textPrimary/textSecondary/iconSecondary with AppColors equivalents) so all color references inside the ProfileCard widget (container background, person Icon, nickname Text, email Text, chevron Icon) come from the same color class.lib/features/mypage/presentation/widgets/setting_tile.dart (1)
23-48:InkWell에borderRadius가 없어 리플이 부모 컨테이너 경계를 초과할 수 있습니다.
SettingTile이 모서리가 둥근Card나Container안에서 사용될 경우,borderRadius가 없으면 잉크 리플 효과가 시각적 경계 밖으로 넘쳐 보입니다.ProfileCard는borderRadius: BorderRadius.circular(12.r)를 지정하고 있으므로,SettingTile에도 동일하게 적용하는 것을 고려하세요.♻️ borderRadius 추가 제안
return InkWell( onTap: onTap, + borderRadius: BorderRadius.circular(12.r), child: Padding(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/mypage/presentation/widgets/setting_tile.dart` around lines 23 - 48, InkWell의 리플이 부모의 둥근 모서리를 넘어가는 문제는 SettingTile의 InkWell에 borderRadius를 지정해 해결하세요: SettingTile 내 InkWell에 borderRadius: BorderRadius.circular(12.r) (ProfileCard과 동일한 반경) 를 추가하고, 리플이 여전히 넘칠 경우 InkWell을 감싼 Material에 clipBehavior: Clip.hardEdge 또는 Clip.antiAlias와 같은 클리핑을 적용해 확실히 경계 내에 머무르도록 조치하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/features/mypage/data/mypage_remote_datasource.dart`:
- Line 27: Remove direct logging of the user nickname in the debugPrint calls
that use the variable name (currently printing "📤 Mypage: Checking name
availability: $name" and the similar call at line 44); instead either remove the
PII from the message or redact/mask it (e.g., log only that a name check
occurred, or log a hashed/partial value or length) so the variable name is not
printed; update the debugPrint usages that reference the identifier name to use
a non-PII message (or maskedName) in the functions in
mypage_remote_datasource.dart.
- Around line 26-38: In checkName, avoid the unsafe cast response.data as
Map<String, dynamic>; first validate response.data is non-null and of type Map
(or parse if it's a JSON string) before passing to CheckNameResponse.fromJson,
and if it's not a Map<String, dynamic> throw or return a handled error (with a
clear message) so callers don't hit a TypeError — update the checkName method to
guard on response.data's runtime type and handle null/List/String cases
explicitly when constructing a CheckNameResponse.
- Around line 26-52: The checkName and updateProfile methods currently call Dio
without error handling; wrap the network calls in try-catch inside
MypageRemoteDatasource (methods checkName and updateProfile) to catch
DioException (and other exceptions), convert them into a clear domain error or a
Result/Failure type (or rethrow a mapped custom exception) and return that to
the repository; if you prefer handling at repository level instead, document in
the datasource method comments that errors are propagated and implement
try-catch and mapping in MypageRepositoryImpl to produce a Result<T> (e.g.,
Success/Failure) so presentation can handle errors safely.
---
Duplicate comments:
In `@lib/features/mypage/presentation/pages/privacy_policy_page.dart`:
- Around line 39-70: The privacy policy is hardcoded in the const
_privacyPolicyText in privacy_policy_page.dart (same issue as terms_page.dart)
and omits two PIPA 2025-required items plus uses a hardcoded contact email;
externalize the policy text and contact address to a configurable source (e.g.,
remote config / backend endpoint / localization file loaded at runtime or reuse
the terms_page.dart fetching approach), remove the const literal, and update the
policy content to explicitly add sections for "데이터 이동권 (data portability)" and
"AI 자동화 의사결정 투명성 (algorithm/process profiling and cross‑border transfer
transparency)"; ensure the email (privacy@mapsee.com) is injected from config
rather than hardcoded and add a fallback/localized copy for offline use.
- Around line 7-36: PrivacyPolicyPage duplicates the build structure from
TermsPage; extract and reuse the same _PolicyPage common widget: create or reuse
_PolicyPage that accepts a title string and a child/body text widget, move the
Scaffold/AppBar/background/text style and SingleChildScrollView boilerplate into
_PolicyPage, then modify PrivacyPolicyPage to simply return _PolicyPage(title:
'개인정보처리방침', body: Text(_privacyPolicyText, style: ...)) or equivalent; ensure
you reference the existing _privacyPolicyText and match the AppTextStyles and
HomeColors used in PrivacyPolicyPage so styling remains identical to TermsPage.
---
Nitpick comments:
In `@lib/features/ai_extraction/presentation/pages/ai_extraction_page.dart`:
- Around line 59-61: Remove the redundant mounted check inside the
ConsumerState: replace the nested "if (mounted) { if (context.mounted)
context.pop(); }" with a single post-async-gap check using context.mounted and
then call context.pop(); keep references to ConsumerState, mounted,
context.mounted, and context.pop() so you locate the block to change.
In `@lib/features/mypage/data/mypage_repository_impl.dart`:
- Around line 23-35: The repository methods checkName and updateNickname
currently let datasource exceptions (e.g., DioException) bubble up; wrap calls
to _remoteDataSource.checkName and _remoteDataSource.updateProfile in try/catch
blocks, catch transport/infrastructure exceptions (DioException or generic
Exception), map them to a domain-level exception (create or use a
MyPageRepositoryException/RepositoryException or specific domain errors) and
rethrow that mapped exception so callers receive translated domain errors
instead of raw infra errors; ensure you convert both checkName (returning
CheckNameResponse) and updateNickname (using ProfileUpdateRequest) consistently
and preserve original error details when constructing the domain exception.
- Line 25: Repository-level debugPrint calls in MypageRepositoryImpl (e.g., the
'📝 MypageRepo: Checking name...' and the similar print at the other occurrence)
are duplicating logs already emitted by the remote datasource; remove these
debugPrint statements from MypageRepositoryImpl so only the datasource emits
that log, or replace them with a single non-verbose logger call if you need a
repository-level trace. Locate the debugPrint calls in MypageRepositoryImpl and
delete them (or downgrade them) so logging is not duplicated across layers.
In `@lib/features/mypage/presentation/pages/terms_page.dart`:
- Around line 7-36: The TermsPage and PrivacyPolicyPage build bodies are
duplicated; extract a shared StatelessWidget (e.g., _PolicyPage) that takes
final String title and final String content, implements the
Scaffold/AppBar/SingleChildScrollView/Text structure using
AppTextStyles.heading02 and AppTextStyles.paragraph with HomeColors, and then
have TermsPage and PrivacyPolicyPage simply return _PolicyPage(title: '이용약관',
content: _termsText) and the privacy equivalent; update imports if you place
_PolicyPage in a new file.
In `@lib/features/mypage/presentation/widgets/profile_card.dart`:
- Around line 1-72: This file mixes two color systems (AppColors and HomeColors)
which should be unified; update ProfileCard to use a single color source
throughout (choose either AppColors or HomeColors), e.g. replace
AppColors.gray100 and AppColors.gray400 with the equivalent values from the
chosen system (or replace HomeColors.textPrimary/textSecondary/iconSecondary
with AppColors equivalents) so all color references inside the ProfileCard
widget (container background, person Icon, nickname Text, email Text, chevron
Icon) come from the same color class.
In `@lib/features/mypage/presentation/widgets/setting_tile.dart`:
- Around line 23-48: InkWell의 리플이 부모의 둥근 모서리를 넘어가는 문제는 SettingTile의 InkWell에
borderRadius를 지정해 해결하세요: SettingTile 내 InkWell에 borderRadius:
BorderRadius.circular(12.r) (ProfileCard과 동일한 반경) 를 추가하고, 리플이 여전히 넘칠 경우 InkWell을
감싼 Material에 clipBehavior: Clip.hardEdge 또는 Clip.antiAlias와 같은 클리핑을 적용해 확실히 경계
내에 머무르도록 조치하세요.
| Future<CheckNameResponse> checkName(String name) async { | ||
| debugPrint('📤 Mypage: Checking name availability: $name'); | ||
|
|
||
| final response = await _dio.get( | ||
| ApiEndpoints.checkName, | ||
| queryParameters: {'name': name}, | ||
| ); | ||
|
|
||
| final result = CheckNameResponse.fromJson( | ||
| response.data as Map<String, dynamic>, | ||
| ); | ||
| debugPrint('✅ Name check result: isAvailable=${result.isAvailable}'); | ||
| return result; |
There was a problem hiding this comment.
response.data 캐스팅 시 안전하지 않은 타입 변환
Line 35에서 response.data as Map<String, dynamic>은 API가 예상과 다른 형식(null, List 등)을 반환할 경우 TypeError가 발생합니다. 방어적 처리를 권장합니다.
🛡️ 제안 수정
- final result = CheckNameResponse.fromJson(
- response.data as Map<String, dynamic>,
- );
+ final data = response.data;
+ if (data is! Map<String, dynamic>) {
+ throw FormatException('Unexpected response format: ${data.runtimeType}');
+ }
+ final result = CheckNameResponse.fromJson(data);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Future<CheckNameResponse> checkName(String name) async { | |
| debugPrint('📤 Mypage: Checking name availability: $name'); | |
| final response = await _dio.get( | |
| ApiEndpoints.checkName, | |
| queryParameters: {'name': name}, | |
| ); | |
| final result = CheckNameResponse.fromJson( | |
| response.data as Map<String, dynamic>, | |
| ); | |
| debugPrint('✅ Name check result: isAvailable=${result.isAvailable}'); | |
| return result; | |
| Future<CheckNameResponse> checkName(String name) async { | |
| debugPrint('📤 Mypage: Checking name availability: $name'); | |
| final response = await _dio.get( | |
| ApiEndpoints.checkName, | |
| queryParameters: {'name': name}, | |
| ); | |
| final data = response.data; | |
| if (data is! Map<String, dynamic>) { | |
| throw FormatException('Unexpected response format: ${data.runtimeType}'); | |
| } | |
| final result = CheckNameResponse.fromJson(data); | |
| debugPrint('✅ Name check result: isAvailable=${result.isAvailable}'); | |
| return result; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/features/mypage/data/mypage_remote_datasource.dart` around lines 26 - 38,
In checkName, avoid the unsafe cast response.data as Map<String, dynamic>; first
validate response.data is non-null and of type Map (or parse if it's a JSON
string) before passing to CheckNameResponse.fromJson, and if it's not a
Map<String, dynamic> throw or return a handled error (with a clear message) so
callers don't hit a TypeError — update the checkName method to guard on
response.data's runtime type and handle null/List/String cases explicitly when
constructing a CheckNameResponse.
| Future<CheckNameResponse> checkName(String name) async { | ||
| debugPrint('📤 Mypage: Checking name availability: $name'); | ||
|
|
||
| final response = await _dio.get( | ||
| ApiEndpoints.checkName, | ||
| queryParameters: {'name': name}, | ||
| ); | ||
|
|
||
| final result = CheckNameResponse.fromJson( | ||
| response.data as Map<String, dynamic>, | ||
| ); | ||
| debugPrint('✅ Name check result: isAvailable=${result.isAvailable}'); | ||
| return result; | ||
| } | ||
|
|
||
| /// 프로필 업데이트 (닉네임 변경) | ||
| /// POST /api/members/profile | ||
| Future<void> updateProfile(ProfileUpdateRequest request) async { | ||
| debugPrint('📤 Mypage: Updating profile: ${request.name}'); | ||
|
|
||
| await _dio.post( | ||
| ApiEndpoints.memberProfile, | ||
| data: request.toJson(), | ||
| ); | ||
|
|
||
| debugPrint('✅ Profile updated successfully'); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for error handling in mypage-related files
rg -n 'try|catch|DioException|onError' --type=dart -g '*mypage*'Repository: MapSee-Lab/MapSy-FE
Length of output: 192
🏁 Script executed:
# Find repository and viewmodel files in mypage feature
fd -t f '\.(dart)$' -path '*mypage*' | grep -E '(repository|viewmodel|controller)' | head -20Repository: MapSee-Lab/MapSy-FE
Length of output: 231
🏁 Script executed:
# List all dart files in mypage directory to understand structure
find lib/features/mypage -type f -name '*.dart' | sortRepository: MapSee-Lab/MapSy-FE
Length of output: 994
🏁 Script executed:
# Read repository implementation to check error handling
cat -n lib/features/mypage/data/mypage_repository_impl.dartRepository: MapSee-Lab/MapSy-FE
Length of output: 1404
🏁 Script executed:
# Read the page file to see error handling at UI layer
cat -n lib/features/mypage/presentation/pages/mypage_page.dartRepository: MapSee-Lab/MapSy-FE
Length of output: 1649
🏁 Script executed:
# Search for try-catch blocks in dart files without type filter
rg -n 'try\s*\{|catch\s*\(' lib/features/mypage/ -A 2 -B 1Repository: MapSee-Lab/MapSy-FE
Length of output: 45
에러 핸들링 누락 - 모든 레이어에서 미구현
checkName과 updateProfile 메서드가 datasource와 repository 모두에서 try-catch 없이 Dio 호출을 수행합니다. 상위 레이어인 MypageRepositoryImpl에서도 에러 핸들링이 없어 DioException 발생 시 예외가 그대로 전파됩니다. 현재 페이지가 placeholder 상태이므로 UI 레이어에서의 에러 처리도 없습니다.
다음 중 하나를 구현해야 합니다:
- datasource나 repository에서 try-catch로 에러를 처리하고 적절한 Result 타입 반환
- 또는 presentation 레이어에서 에러를 명시적으로 처리하되, 이 경우 datasource 메서드 설명에 에러 처리 책임이 상위 레이어에 있음을 명기
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/features/mypage/data/mypage_remote_datasource.dart` around lines 26 - 52,
The checkName and updateProfile methods currently call Dio without error
handling; wrap the network calls in try-catch inside MypageRemoteDatasource
(methods checkName and updateProfile) to catch DioException (and other
exceptions), convert them into a clear domain error or a Result/Failure type (or
rethrow a mapped custom exception) and return that to the repository; if you
prefer handling at repository level instead, document in the datasource method
comments that errors are propagated and implement try-catch and mapping in
MypageRepositoryImpl to produce a Result<T> (e.g., Success/Failure) so
presentation can handle errors safely.
| /// 닉네임 중복 확인 | ||
| /// GET /api/members/check-name?name={name} | ||
| Future<CheckNameResponse> checkName(String name) async { | ||
| debugPrint('📤 Mypage: Checking name availability: $name'); |
There was a problem hiding this comment.
debugPrint에 사용자 닉네임(PII) 직접 노출
Line 27과 44에서 사용자가 입력한 닉네임을 로그에 그대로 출력하고 있습니다. debugPrint는 디버그 모드에서만 동작하지만, 로그 수집 도구에 의해 캡처될 수 있으므로 PII를 직접 로깅하지 않는 것이 좋습니다.
🛡️ 제안 수정
- debugPrint('📤 Mypage: Checking name availability: $name');
+ debugPrint('📤 Mypage: Checking name availability');- debugPrint('📤 Mypage: Updating profile: ${request.name}');
+ debugPrint('📤 Mypage: Updating profile');Also applies to: 44-44
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/features/mypage/data/mypage_remote_datasource.dart` at line 27, Remove
direct logging of the user nickname in the debugPrint calls that use the
variable name (currently printing "📤 Mypage: Checking name availability: $name"
and the similar call at line 44); instead either remove the PII from the message
or redact/mask it (e.g., log only that a name check occurred, or log a
hashed/partial value or length) so the variable name is not printed; update the
debugPrint usages that reference the identifier name to use a non-PII message
(or maskedName) in the functions in mypage_remote_datasource.dart.
| const _termsText = ''' | ||
| 제1조 (목적) | ||
| 이 약관은 맵시(이하 "회사")가 제공하는 서비스의 이용과 관련하여 회사와 이용자 간의 권리, 의무 및 책임사항을 규정함을 목적으로 합니다. | ||
|
|
||
| 제2조 (정의) | ||
| 1. "서비스"란 회사가 제공하는 AI 기반 장소 추출 플랫폼을 말합니다. | ||
| 2. "이용자"란 이 약관에 따라 회사가 제공하는 서비스를 받는 자를 말합니다. | ||
| 3. "콘텐츠"란 이용자가 서비스를 이용하면서 생성하거나 저장한 모든 정보를 말합니다. | ||
|
|
||
| 제3조 (약관의 효력 및 변경) | ||
| 1. 이 약관은 서비스 화면에 게시하거나 기타의 방법으로 이용자에게 공지함으로써 효력이 발생합니다. | ||
| 2. 회사는 필요한 경우 관련 법령에 위배되지 않는 범위 안에서 이 약관을 개정할 수 있습니다. | ||
|
|
||
| 제4조 (서비스의 제공) | ||
| 1. 회사는 다음과 같은 서비스를 제공합니다. | ||
| - SNS URL 기반 AI 장소 추출 서비스 | ||
| - 장소 정보 저장 및 관리 서비스 | ||
| - 기타 회사가 정하는 서비스 | ||
|
|
||
| 제5조 (이용자의 의무) | ||
| 1. 이용자는 서비스를 이용할 때 다음 행위를 하여서는 안 됩니다. | ||
| - 타인의 정보를 도용하는 행위 | ||
| - 서비스의 운영을 방해하는 행위 | ||
| - 기타 관련 법령에 위반되는 행위 | ||
|
|
||
| 제6조 (개인정보 보호) | ||
| 회사는 이용자의 개인정보를 보호하기 위해 개인정보처리방침을 수립하고 이를 준수합니다. | ||
|
|
||
| 제7조 (면책사항) | ||
| 1. 회사는 천재지변 등 불가항력으로 서비스를 제공할 수 없는 경우 책임이 면제됩니다. | ||
| 2. 회사는 이용자의 귀책사유로 인한 서비스 이용 장애에 대해 책임을 지지 않습니다. | ||
| '''; |
There was a problem hiding this comment.
약관 텍스트 하드코딩은 PIPA 컴플라이언스 리스크를 유발합니다.
법률 텍스트가 컴파일 타임 상수(const)로 고정되어 있어, 내용 변경 시 반드시 앱 업데이트를 배포해야 합니다. 한국 개인정보보호법(PIPA)은 개인정보처리방침이 수집 데이터 유형·수집 목적·데이터 공유 방식을 투명하게 명시해야 한다고 규정하며, 2025년 3월 13일부터 데이터 이동권이 발효됨에 따라 기업들은 개인정보처리방침 및 계약서를 업데이트할 것을 요구하고 있습니다. 현재 방침에는 이 권리에 대한 내용이 누락되어 있으며, 앱 재배포 없이는 수정이 불가능합니다.
이 문제는 privacy_policy_page.dart에도 동일하게 적용됩니다.
권장 접근 방식 (중요도 순):
- (권장) API 엔드포인트에서 약관/방침을 서버에서 불러오고, 로컬 캐시를 폴백으로 사용
- Flutter 앱 번들의
assets/에 텍스트 파일로 분리하여 WebView로 렌더링(url_launcher+ 회사 웹사이트) - 최소한
const문자열 대신 동적 변수로 교체하여 런타임 주입이 가능하도록 구조 변경
- route_paths.dart에 mypageTerms, mypagePrivacyPolicy 경로 상수 추가 - route_paths.dart에 mypageTermsName, mypagePrivacyPolicyName 이름 상수 추가 - app_router.dart에 TermsPage, PrivacyPolicyPage import 추가 - 마이페이지 GoRoute에 하위 라우트(terms, privacy-policy) 등록 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
플레이스홀더를 실제 마이페이지로 교체: - 프로필 카드 (닉네임/이메일 표시, 닉네임 수정 바텀시트 연동) - 앱 설정 (알림 토글) - 정보 (이용약관, 개인정보처리방침, 오픈소스 라이선스, 앱 버전) - 계정 (로그아웃/회원탈퇴 확인 다이얼로그) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…apSee-Lab/MapSy-FE into 20260223_26_프로필_조회_수정_설정_화면_필요
❌ Flutter CI 실패
💡 확인 사항Analyze 실패 시:
Android 빌드 실패 시:
iOS 빌드 실패 시:
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
lib/features/mypage/presentation/widgets/nickname_edit_bottom_sheet.dart (1)
58-71:_isChanged가build중_controller.text를 직접 참조하는 패턴은 동작하나, 유효성 검사 피드백 타이밍 고려 필요.현재
_isChanged만으로 버튼 활성화를 제어하고, 유효성 검사는 제출 시에만 수행됩니다. 사용자가 특수문자나 공백이 포함된 닉네임을 입력해도 버튼이 활성화되어 제출 후에야 에러를 볼 수 있습니다.onChanged에서 실시간 유효성 검사를 추가하면 UX가 개선됩니다.♻️ 실시간 유효성 검사 추가 제안
onChanged: (_) => setState(() {}),를 다음과 같이 변경:
+ onChanged: (_) { + ref.read(nicknameEditorProvider.notifier).clearError(); + final error = ref.read(nicknameEditorProvider.notifier).validate(_controller.text.trim()); + // 필요시 에러 상태 업데이트 + setState(() {}); + },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/mypage/presentation/widgets/nickname_edit_bottom_sheet.dart` around lines 58 - 71, The _isChanged getter currently drives button state but lacks real-time validation; add an onChanged handler on the TextField tied to _controller that runs a validation function (e.g., validateNickname) on _controller.text.trim(), storing the result in a local state field like _isValid or _validationError (update via setState) and use that field together with _isChanged to enable the submit button; keep _onSubmit using notifier.updateNickname(...) but short-circuit submission if _isValid is false (show the same error message kept in _validationError) so invalid input (spaces/special chars) disables the button and provides immediate feedback.lib/features/mypage/presentation/mypage_provider.dart (1)
121-129:copyWith에서errorMessage의 "미지정"과 "명시적 null"을 구분할 수 없습니다.
copyWith(isLoading: false)를 호출하면errorMessage가 의도치 않게null로 초기화됩니다. 현재 사용 패턴에서는 문제가 발생하지 않지만, 향후isLoading만 변경하고 기존 에러 메시지를 유지하려는 경우 버그가 발생합니다.isLoading과 달리errorMessage만 null-coalescing 패턴이 적용되지 않아 비대칭적입니다.♻️ sentinel 값을 사용한 수정 제안
+const _sentinel = Object(); + NicknameEditState copyWith({ bool? isLoading, - String? errorMessage, + Object? errorMessage = _sentinel, }) { return NicknameEditState( isLoading: isLoading ?? this.isLoading, - errorMessage: errorMessage, + errorMessage: errorMessage == _sentinel + ? this.errorMessage + : errorMessage as String?, ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/mypage/presentation/mypage_provider.dart` around lines 121 - 129, The copyWith currently treats a missing errorMessage and an explicit null the same, causing calls like NicknameEditState.copyWith(isLoading: false) to wipe the existing error; change copyWith to accept a sentinel default for errorMessage (e.g. Object _errorMessageSentinel = Object(); signature: copyWith({bool? isLoading, Object? errorMessage = _errorMessageSentinel}) ) and when building the new NicknameEditState use errorMessage == _errorMessageSentinel ? this.errorMessage : errorMessage as String? so an explicit null clears the message but omission preserves it; update any related tests/usages accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/features/mypage/presentation/pages/mypage_page.dart`:
- Around line 57-65: The switch currently uses notificationSetting.valueOrNull
?? true which shows ON during loading and can cause a UI flash; update the
widget to handle AsyncValue loading explicitly (using notificationSetting.when
or maybeWhen) so you render a loading placeholder or disabled switch while
notificationSetting is loading and only read notificationSetting.data.value for
the Switch.adaptive value; locate SettingTile / Switch.adaptive and change the
on-build logic to branch on notificationSetting (or use
ref.watch(notificationSettingProvider).when/maybeWhen) so the switch reflects
the persisted value only after loading completes and avoid defaulting to true.
- Around line 161-210: Wrap the await calls to
ref.read(authNotifierProvider.notifier).signOut() in _onLogout and
ref.read(authNotifierProvider.notifier).withdraw() in _onWithdraw with
try/catch, handle exceptions by showing user-facing feedback (e.g.,
ScaffoldMessenger.of(context).showSnackBar or an AlertDialog with the error
message) and optionally log the error; ensure the dialog is dismissed before
showing feedback and keep the confirmed == true check, so failures surface to
the user instead of failing silently.
---
Nitpick comments:
In `@lib/features/mypage/presentation/mypage_provider.dart`:
- Around line 121-129: The copyWith currently treats a missing errorMessage and
an explicit null the same, causing calls like
NicknameEditState.copyWith(isLoading: false) to wipe the existing error; change
copyWith to accept a sentinel default for errorMessage (e.g. Object
_errorMessageSentinel = Object(); signature: copyWith({bool? isLoading, Object?
errorMessage = _errorMessageSentinel}) ) and when building the new
NicknameEditState use errorMessage == _errorMessageSentinel ? this.errorMessage
: errorMessage as String? so an explicit null clears the message but omission
preserves it; update any related tests/usages accordingly.
In `@lib/features/mypage/presentation/widgets/nickname_edit_bottom_sheet.dart`:
- Around line 58-71: The _isChanged getter currently drives button state but
lacks real-time validation; add an onChanged handler on the TextField tied to
_controller that runs a validation function (e.g., validateNickname) on
_controller.text.trim(), storing the result in a local state field like _isValid
or _validationError (update via setState) and use that field together with
_isChanged to enable the submit button; keep _onSubmit using
notifier.updateNickname(...) but short-circuit submission if _isValid is false
(show the same error message kept in _validationError) so invalid input
(spaces/special chars) disables the button and provides immediate feedback.
| SettingTile( | ||
| title: '알림 설정', | ||
| trailing: Switch.adaptive( | ||
| value: notificationSetting.valueOrNull ?? true, | ||
| onChanged: (_) => | ||
| ref.read(notificationSettingProvider.notifier).toggle(), | ||
| activeColor: AppColors.primary, | ||
| ), | ||
| ), |
There was a problem hiding this comment.
알림 설정 로딩 중 기본값 true로 인한 UI 깜빡임 가능성.
Line 60에서 notificationSetting.valueOrNull ?? true를 사용하여 로딩 중 스위치가 켜진 상태로 표시됩니다. 실제 저장된 값이 false인 경우 로딩 완료 후 스위치가 꺼지면서 깜빡임이 발생할 수 있습니다. AsyncValue의 when/maybeWhen을 사용하여 로딩 상태를 별도로 처리하는 것을 고려해보세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/features/mypage/presentation/pages/mypage_page.dart` around lines 57 -
65, The switch currently uses notificationSetting.valueOrNull ?? true which
shows ON during loading and can cause a UI flash; update the widget to handle
AsyncValue loading explicitly (using notificationSetting.when or maybeWhen) so
you render a loading placeholder or disabled switch while notificationSetting is
loading and only read notificationSetting.data.value for the Switch.adaptive
value; locate SettingTile / Switch.adaptive and change the on-build logic to
branch on notificationSetting (or use
ref.watch(notificationSettingProvider).when/maybeWhen) so the switch reflects
the persisted value only after loading completes and avoid defaulting to true.
| /// 로그아웃 확인 다이얼로그 | ||
| Future<void> _onLogout(BuildContext context, WidgetRef ref) async { | ||
| final confirmed = await showDialog<bool>( | ||
| 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) { | ||
| await ref.read(authNotifierProvider.notifier).signOut(); | ||
| } | ||
| } | ||
|
|
||
| /// 회원탈퇴 확인 다이얼로그 | ||
| Future<void> _onWithdraw(BuildContext context, WidgetRef ref) async { | ||
| final confirmed = await showDialog<bool>( | ||
| context: context, | ||
| builder: (context) => AlertDialog( | ||
| title: const Text('회원탈퇴'), | ||
| content: const Text('정말 탈퇴하시겠습니까?\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) { | ||
| await ref.read(authNotifierProvider.notifier).withdraw(); | ||
| } | ||
| } |
There was a problem hiding this comment.
로그아웃/회원탈퇴 실패 시 에러 처리 누락.
signOut()과 withdraw() 호출 시 예외가 발생하면 사용자에게 아무런 피드백이 없습니다. 특히 회원탈퇴는 네트워크 에러 등으로 실패할 가능성이 높으므로 에러 처리가 필요합니다.
🛡️ 에러 처리 추가 제안 (회원탈퇴 예시)
if (confirmed == true) {
- await ref.read(authNotifierProvider.notifier).withdraw();
+ try {
+ await ref.read(authNotifierProvider.notifier).withdraw();
+ } catch (e) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('회원탈퇴에 실패했습니다. 다시 시도해주세요.')),
+ );
+ }
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// 로그아웃 확인 다이얼로그 | |
| Future<void> _onLogout(BuildContext context, WidgetRef ref) async { | |
| final confirmed = await showDialog<bool>( | |
| 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) { | |
| await ref.read(authNotifierProvider.notifier).signOut(); | |
| } | |
| } | |
| /// 회원탈퇴 확인 다이얼로그 | |
| Future<void> _onWithdraw(BuildContext context, WidgetRef ref) async { | |
| final confirmed = await showDialog<bool>( | |
| context: context, | |
| builder: (context) => AlertDialog( | |
| title: const Text('회원탈퇴'), | |
| content: const Text('정말 탈퇴하시겠습니까?\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) { | |
| await ref.read(authNotifierProvider.notifier).withdraw(); | |
| } | |
| } | |
| /// 로그아웃 확인 다이얼로그 | |
| Future<void> _onLogout(BuildContext context, WidgetRef ref) async { | |
| final confirmed = await showDialog<bool>( | |
| 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) { | |
| await ref.read(authNotifierProvider.notifier).signOut(); | |
| } | |
| } | |
| /// 회원탈퇴 확인 다이얼로그 | |
| Future<void> _onWithdraw(BuildContext context, WidgetRef ref) async { | |
| final confirmed = await showDialog<bool>( | |
| context: context, | |
| builder: (context) => AlertDialog( | |
| title: const Text('회원탈퇴'), | |
| content: const Text('정말 탈퇴하시겠습니까?\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) { | |
| try { | |
| await ref.read(authNotifierProvider.notifier).withdraw(); | |
| } catch (e) { | |
| if (context.mounted) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar(content: Text('회원탈퇴에 실패했습니다. 다시 시도해주세요.')), | |
| ); | |
| } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/features/mypage/presentation/pages/mypage_page.dart` around lines 161 -
210, Wrap the await calls to ref.read(authNotifierProvider.notifier).signOut()
in _onLogout and ref.read(authNotifierProvider.notifier).withdraw() in
_onWithdraw with try/catch, handle exceptions by showing user-facing feedback
(e.g., ScaffoldMessenger.of(context).showSnackBar or an AlertDialog with the
error message) and optionally log the error; ensure the dialog is dismissed
before showing feedback and keep the confirmed == true check, so failures
surface to the user instead of failing silently.
…티널 패턴) #26 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
❌ Flutter CI 실패
💡 확인 사항Analyze 실패 시:
Android 빌드 실패 시:
iOS 빌드 실패 시:
|
❌ Flutter CI 실패
💡 확인 사항Android 빌드 실패 시:
|
- #26
Summary by CodeRabbit
새로운 기능
데이터 모델
문서화