Skip to content

20260223 26 프로필 조회 수정 설정 화면 필요#41

Merged
Cassiiopeia merged 20 commits intomainfrom
20260223_26_프로필_조회_수정_설정_화면_필요
Feb 23, 2026

Hidden character warning

The head ref may contain hidden characters: "20260223_26_\ud504\ub85c\ud544_\uc870\ud68c_\uc218\uc815_\uc124\uc815_\ud654\uba74_\ud544\uc694"
Merged

20260223 26 프로필 조회 수정 설정 화면 필요#41
Cassiiopeia merged 20 commits intomainfrom
20260223_26_프로필_조회_수정_설정_화면_필요

Conversation

@Cassiiopeia
Copy link
Contributor

@Cassiiopeia Cassiiopeia commented Feb 23, 2026

- #26

Summary by CodeRabbit

  • 새로운 기능

    • MyPage 추가: 프로필 표시, 닉네임 수정(하단 시트), 약관·개인정보처리방침 페이지, 로그아웃/탈퇴, 앱 버전 표시, 알림 설정 토글
    • 프로필 카드·설정 타일 등 UI 컴포넌트 및 라우팅 확장
    • 닉네임 검증·수정 흐름 및 관련 사용자 피드백 처리
  • 데이터 모델

    • Place 모델 및 AI 분석 요청/응답 등 신규/생성된 직렬화 가능한 모델들 추가
    • 프로필 업데이트 및 닉네임 검사 응답 모델 추가
  • 문서화

    • MyPage 설계 문서 및 구현 계획 문서 추가

@github-actions
Copy link
Contributor

github-actions bot commented Feb 23, 2026

❌ Flutter CI 실패

검사 항목 상태 소요 시간
🔍 Analyze ❌ 실패 1분 23초
🤖 Android 빌드 ❌ 실패 9분 4초
🍎 iOS 빌드 ❌ 실패 10분 36초
항목
브랜치 20260223_26_프로필_조회_수정_설정_화면_필요
커밋 d0a41e1
총 소요 시간 10분 57초

💡 확인 사항

Analyze 실패 시:

  • flutter analyze 로컬에서 실행하여 lint 오류 확인
  • 코드 스타일 및 타입 오류 수정

Android 빌드 실패 시:

  • Gradle 빌드 로그에서 에러 확인
  • 의존성 버전 호환성 확인

iOS 빌드 실패 시:

  • Xcode 빌드 로그에서 에러 확인
  • CocoaPods 의존성 확인

📋 워크플로우 로그

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Warning

Rate limit exceeded

@Cassiiopeia has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 34 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

Walkthrough

MyPage 설계·구현 문서 추가, MyPage 원격 데이터소스/레포지토리/모델 및 Riverpod provider 구현, 관련 UI(프로필 카드·설정 타일·닉네임 편집 바텀시트·약관/개인정보 페이지) 추가, AI 추출 및 PlaceModel Freezed/JSON/Provider 생성과 홈 모듈 import 정리.

Changes

Cohort / File(s) Summary
문서: MyPage 설계·구현 계획
docs/plans/2026-02-23-mypage-design.md, docs/plans/2026-02-23-mypage-implementation.md
MyPage 기능 설계 문서 및 단계별 구현 계획 추가(아키텍처·라우팅·작업 순서 포함).
공통 모델: PlaceModel 생성
lib/common/models/place_model.freezed.dart, lib/common/models/place_model.g.dart
PlaceModel의 Freezed 불변 타입 및 json_serializable 직렬화 코드(필드: placeId, placeName, address, latitude, longitude, category, tags, imageUrl, contentId).
AI 추출: 모델·provider 추가
lib/features/ai_extraction/data/models/*.{freezed,g.dart}, lib/features/ai_extraction/data/ai_extraction_remote_datasource.g.dart, lib/features/ai_extraction/data/ai_extraction_repository_impl.g.dart, lib/features/ai_extraction/presentation/ai_extraction_provider.*
AnalyzeRequest/AnalyzeResponse/ContentDetailResponse Freezed·JSON 생성 및 Riverpod provider 보일러플레이트 추가. 일부 클로저 파라미터명 정리 및 navigation 안전성 체크 강화.
MyPage: 데이터 모델·원격소스·레포지토리
lib/features/mypage/data/check_name_response.*, lib/features/mypage/data/profile_update_request.*, lib/features/mypage/data/mypage_remote_datasource.dart, lib/features/mypage/data/mypage_remote_datasource.g.dart, lib/features/mypage/data/mypage_repository*.dart, lib/features/mypage/data/mypage_repository_impl.g.dart
닉네임 체크/프로필 수정 모델 추가. MypageRemoteDataSource(Dio) 구현과 Riverpod provider, MypageRepository 인터페이스 및 구현과 provider 추가(체크·업데이트 흐름).
MyPage: presentation / provider / widgets / pages
lib/features/mypage/presentation/mypage_provider.dart, lib/features/mypage/presentation/mypage_provider.g.dart, lib/features/mypage/presentation/pages/mypage_page.dart, lib/features/mypage/presentation/pages/terms_page.dart, lib/features/mypage/presentation/pages/privacy_policy_page.dart, lib/features/mypage/presentation/widgets/{profile_card.dart,setting_tile.dart,nickname_edit_bottom_sheet.dart}
앱 버전·알림 설정·닉네임 에디터 Riverpod providers 추가. MypagePage를 ConsumerWidget으로 재구성하고 프로필/설정/정보/계정 섹션 구현, 닉네임 편집 바텀시트·약관·개인정보 페이지·재사용 위젯 추가.
라우팅: MyPage 하위 경로 추가
lib/routing/app_router.dart, lib/routing/route_paths.dart
/mypage/terms, /mypage/privacy-policy 경로 및 이름 상수 추가 및 MyPage 라우트의 자식 라우트로 등록.
홈 모듈: PlaceModel import 경로 정리
lib/features/home/data/models/content_response.dart, lib/features/home/presentation/home_provider.dart, lib/features/home/presentation/widgets/place_card.dart
PlaceModel 참조 import 경로를 상대경로로 정리(경로 변경 및 일부 가독성 파라미터명 정리).
생성 코드(Freezed/JSON/Riverpod)
다수의 *.freezed.dart, *.g.dart 파일
Freezed/json_serializable/riverpod 생성 파일 다수 추가 — 주요 변경은 mypage 및 ai_extraction 관련 생성 코드와 PlaceModel 생성물. 일부 불필요 import 제거 및 클로저 이름 통일.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경사항의 주요 기능(MyPage 기능 구현)을 명확하게 설명하고 있습니다. 프로필 조회, 수정, 설정 화면 필요라는 의도가 잘 드러나 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 20260223_26_프로필_조회_수정_설정_화면_필요

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

github-actions bot and others added 2 commits February 23, 2026 04:51
…ory) #26

- MypageRemoteDataSource: 닉네임 중복 확인(GET), 프로필 업데이트(POST) API 호출
- MypageRepository 인터페이스 및 MypageRepositoryImpl 구현체
- Riverpod @riverpod Provider 코드 생성 (.g.dart)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.placestoJson 직렬화 문제 확인

생성된 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 범위 확인 권장

현재 ProfileUpdateRequestname 단일 필드만 정의되어 있습니다. 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.placesList<PlaceModel> 인스턴스를 그대로 Map에 저장합니다. dart:convertjsonEncode는 내부적으로 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: errorMessagenull 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개 섹션:

```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

펜스 코드 블록에 언어 식별자를 추가해 주세요.

마크다운 린터(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 형태로 수정해 주세요.

Comment on lines +119 to +122
### 알림 설정

- 토글 ON: 시스템 알림 권한 확인 → 권한 없으면 설정 앱 안내
- 토글 OFF: SharedPreferences에 즉시 저장
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

알림 토글 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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +120 to +126

final result = CheckNameResponse.fromJson(
response.data as Map<String, dynamic>,
);
debugPrint('✅ Name check result: isAvailable=${result.isAvailable}');
return result;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +1026 to +1029
// 사용자 정보 (Firebase User에서)
final user = authState.valueOrNull;
final nickname = user?.displayName ?? '사용자';
final email = user?.email ?? '';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

닉네임 데이터 소스 불일치: Firebase displayName vs. 백엔드 프로필.

nicknameuser?.displayName(Firebase Auth) 에서 읽지만, updateNickname은 백엔드 /api/members/profile API를 통해 서버 측 데이터를 업데이트합니다. 백엔드가 Firebase Auth의 displayName을 동기적으로 업데이트하지 않는 한, ref.invalidate(authNotifierProvider) (line 1159)로 Firebase 상태를 재조회해도 UI에는 변경 이전 닉네임이 그대로 표시됩니다.

해결 방법을 두 가지 중 하나로 통일해야 합니다:

  1. 백엔드에서 Firebase displayName도 함께 업데이트 (서버 측 Firebase Admin SDK 사용)
  2. 닉네임 소스를 백엔드 전용으로 분리 — 별도의 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.

Comment on lines +1182 to +1186

if (confirmed == true) {
await ref.read(authNotifierProvider.notifier).signOut();
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 23, 2026

❌ Flutter CI 실패

검사 항목 상태 소요 시간
🔍 Analyze ✅ 성공 1분 29초
🤖 Android 빌드 ❌ 실패 4분 34초
🍎 iOS 빌드 ❌ 실패 4분 31초
항목
브랜치 20260223_26_프로필_조회_수정_설정_화면_필요
커밋 37e430e
총 소요 시간 4분 55초

💡 확인 사항

Android 빌드 실패 시:

  • Gradle 빌드 로그에서 에러 확인
  • 의존성 버전 호환성 확인

iOS 빌드 실패 시:

  • Xcode 빌드 로그에서 에러 확인
  • CocoaPods 의존성 확인

📋 워크플로우 로그

github-actions bot and others added 3 commits February 23, 2026 05:03
- appVersionProvider: 앱 버전 조회 (PackageInfo)
- NotificationSetting: 알림 설정 토글 (SharedPreferences)
- NicknameEditor: 닉네임 유효성 검증 + 중복확인 + 변경
- build_runner 네트워크 이슈로 .g.dart 수동 생성

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Feb 23, 2026

❌ Flutter CI 실패

검사 항목 상태 소요 시간
🔍 Analyze ❌ 실패 1분 30초
🤖 Android 빌드 ❌ 실패 9분 12초
🍎 iOS 빌드 ✅ 성공 4분 56초
항목
브랜치 20260223_26_프로필_조회_수정_설정_화면_필요
커밋 8806cd2
총 소요 시간 9분 28초

💡 확인 사항

Analyze 실패 시:

  • flutter analyze 로컬에서 실행하여 lint 오류 확인
  • 코드 스타일 및 타입 오류 수정

Android 빌드 실패 시:

  • Gradle 빌드 로그에서 에러 확인
  • 의존성 버전 호환성 확인

📋 워크플로우 로그

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.mountedthis.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: TermsPagePrivacyPolicyPage의 빌드 구조가 완전히 중복됩니다.

두 위젯은 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,
          ),
        ),
      ),
    );
  }
}

그런 다음 TermsPagePrivacyPolicyPage를 아래처럼 단순화할 수 있습니다:

 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.

borderRadiusInkWell에 올바르게 설정되어 있어 리플 효과가 의도대로 클리핑됩니다. 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: InkWellborderRadius가 없어 리플이 부모 컨테이너 경계를 초과할 수 있습니다.

SettingTile이 모서리가 둥근 CardContainer 안에서 사용될 경우, borderRadius가 없으면 잉크 리플 효과가 시각적 경계 밖으로 넘쳐 보입니다. ProfileCardborderRadius: 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와 같은 클리핑을 적용해 확실히 경계
내에 머무르도록 조치하세요.

Comment on lines +26 to +38
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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +26 to +52
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');
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -20

Repository: 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' | sort

Repository: 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.dart

Repository: 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.dart

Repository: 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 1

Repository: MapSee-Lab/MapSy-FE

Length of output: 45


에러 핸들링 누락 - 모든 레이어에서 미구현

checkNameupdateProfile 메서드가 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');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +39 to +70
const _termsText = '''
제1조 (목적)
이 약관은 맵시(이하 "회사")가 제공하는 서비스의 이용과 관련하여 회사와 이용자 간의 권리, 의무 및 책임사항을 규정함을 목적으로 합니다.

제2조 (정의)
1. "서비스"란 회사가 제공하는 AI 기반 장소 추출 플랫폼을 말합니다.
2. "이용자"란 이 약관에 따라 회사가 제공하는 서비스를 받는 자를 말합니다.
3. "콘텐츠"란 이용자가 서비스를 이용하면서 생성하거나 저장한 모든 정보를 말합니다.

제3조 (약관의 효력 및 변경)
1. 이 약관은 서비스 화면에 게시하거나 기타의 방법으로 이용자에게 공지함으로써 효력이 발생합니다.
2. 회사는 필요한 경우 관련 법령에 위배되지 않는 범위 안에서 이 약관을 개정할 수 있습니다.

제4조 (서비스의 제공)
1. 회사는 다음과 같은 서비스를 제공합니다.
- SNS URL 기반 AI 장소 추출 서비스
- 장소 정보 저장 및 관리 서비스
- 기타 회사가 정하는 서비스

제5조 (이용자의 의무)
1. 이용자는 서비스를 이용할 때 다음 행위를 하여서는 안 됩니다.
- 타인의 정보를 도용하는 행위
- 서비스의 운영을 방해하는 행위
- 기타 관련 법령에 위반되는 행위

제6조 (개인정보 보호)
회사는 이용자의 개인정보를 보호하기 위해 개인정보처리방침을 수립하고 이를 준수합니다.

제7조 (면책사항)
1. 회사는 천재지변 등 불가항력으로 서비스를 제공할 수 없는 경우 책임이 면제됩니다.
2. 회사는 이용자의 귀책사유로 인한 서비스 이용 장애에 대해 책임을 지지 않습니다.
''';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

약관 텍스트 하드코딩은 PIPA 컴플라이언스 리스크를 유발합니다.

법률 텍스트가 컴파일 타임 상수(const)로 고정되어 있어, 내용 변경 시 반드시 앱 업데이트를 배포해야 합니다. 한국 개인정보보호법(PIPA)은 개인정보처리방침이 수집 데이터 유형·수집 목적·데이터 공유 방식을 투명하게 명시해야 한다고 규정하며, 2025년 3월 13일부터 데이터 이동권이 발효됨에 따라 기업들은 개인정보처리방침 및 계약서를 업데이트할 것을 요구하고 있습니다. 현재 방침에는 이 권리에 대한 내용이 누락되어 있으며, 앱 재배포 없이는 수정이 불가능합니다.

이 문제는 privacy_policy_page.dart에도 동일하게 적용됩니다.

권장 접근 방식 (중요도 순):

  1. (권장) API 엔드포인트에서 약관/방침을 서버에서 불러오고, 로컬 캐시를 폴백으로 사용
  2. Flutter 앱 번들의 assets/에 텍스트 파일로 분리하여 WebView로 렌더링(url_launcher + 회사 웹사이트)
  3. 최소한 const 문자열 대신 동적 변수로 교체하여 런타임 주입이 가능하도록 구조 변경

github-actions bot and others added 6 commits February 23, 2026 05:08
- 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>
@github-actions
Copy link
Contributor

github-actions bot commented Feb 23, 2026

❌ Flutter CI 실패

검사 항목 상태 소요 시간
🔍 Analyze ❌ 실패 1분 28초
🤖 Android 빌드 ❌ 실패 7분 12초
🍎 iOS 빌드 ❌ 실패 4분 51초
항목
브랜치 20260223_26_프로필_조회_수정_설정_화면_필요
커밋 93d5f19
총 소요 시간 7분 29초

💡 확인 사항

Analyze 실패 시:

  • flutter analyze 로컬에서 실행하여 lint 오류 확인
  • 코드 스타일 및 타입 오류 수정

Android 빌드 실패 시:

  • Gradle 빌드 로그에서 에러 확인
  • 의존성 버전 호환성 확인

iOS 빌드 실패 시:

  • Xcode 빌드 로그에서 에러 확인
  • CocoaPods 의존성 확인

📋 워크플로우 로그

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
lib/features/mypage/presentation/widgets/nickname_edit_bottom_sheet.dart (1)

58-71: _isChangedbuild_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.

Comment on lines +57 to +65
SettingTile(
title: '알림 설정',
trailing: Switch.adaptive(
value: notificationSetting.valueOrNull ?? true,
onChanged: (_) =>
ref.read(notificationSettingProvider.notifier).toggle(),
activeColor: AppColors.primary,
),
),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

알림 설정 로딩 중 기본값 true로 인한 UI 깜빡임 가능성.

Line 60에서 notificationSetting.valueOrNull ?? true를 사용하여 로딩 중 스위치가 켜진 상태로 표시됩니다. 실제 저장된 값이 false인 경우 로딩 완료 후 스위치가 꺼지면서 깜빡임이 발생할 수 있습니다. AsyncValuewhen/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.

Comment on lines +161 to +210
/// 로그아웃 확인 다이얼로그
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();
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

로그아웃/회원탈퇴 실패 시 에러 처리 누락.

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.

Suggested change
/// 로그아웃 확인 다이얼로그
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.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 23, 2026

❌ Flutter CI 실패

검사 항목 상태 소요 시간
🔍 Analyze ❌ 실패 1분 25초
🤖 Android 빌드 ❌ 실패 4분 35초
🍎 iOS 빌드 ❌ 실패 4분 43초
항목
브랜치 20260223_26_프로필_조회_수정_설정_화면_필요
커밋 ee88d1a
총 소요 시간 5분 2초

💡 확인 사항

Analyze 실패 시:

  • flutter analyze 로컬에서 실행하여 lint 오류 확인
  • 코드 스타일 및 타입 오류 수정

Android 빌드 실패 시:

  • Gradle 빌드 로그에서 에러 확인
  • 의존성 버전 호환성 확인

iOS 빌드 실패 시:

  • Xcode 빌드 로그에서 에러 확인
  • CocoaPods 의존성 확인

📋 워크플로우 로그

@github-actions
Copy link
Contributor

github-actions bot commented Feb 23, 2026

❌ Flutter CI 실패

검사 항목 상태 소요 시간
🔍 Analyze ✅ 성공 1분 36초
🤖 Android 빌드 ❌ 실패 7분 33초
🍎 iOS 빌드 ✅ 성공 6분 6초
항목
브랜치 20260223_26_프로필_조회_수정_설정_화면_필요
커밋 dc421df
총 소요 시간 7분 50초

💡 확인 사항

Android 빌드 실패 시:

  • Gradle 빌드 로그에서 에러 확인
  • 의존성 버전 호환성 확인

📋 워크플로우 로그

@Cassiiopeia Cassiiopeia merged commit 22f3516 into main Feb 23, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant