Screen: Notifications (알림)
푸시 알림 설정 화면. App 전용 (Web 없음 — 웹은 비로그인 피드라 알림/워치리스트 개념 없음). bottom nav "알림" 탭 진입.
- Flutter (App):
mobile/lib/features/notifications/presentation/pages/notifications_page.dart - 상태:
notifications_controller.dart(OS 권한) +watchlist_controller.dart(종목별 토글)
💡 시각 미리보기: design site / notifications-app
플랫폼별 차이
| 항목 | App (Flutter) | Web (Next.js) |
|---|---|---|
| 화면 존재 | ✅ | ❌ (없음 — 비로그인 피드) |
| 인증 | 비로그인 진입 가능 (lazy auth — GoRouter.redirect 는 인증 기준 분기 없음, 첫 실행 온보딩 게이트 redirect 만 존재). 워치리스트 조회/토글은 인증 필요 → 미인증 시 빈 워치리스트로 렌더, OS 권한 카드는 동작 | — |
App — 화면 구성
| 영역 | 컴포넌트 | 조건 |
|---|---|---|
| AppBar | (시스템, title="알림") | 항상 |
| 푸시 권한 카드 | _PushPermissionCard (bell 아이콘 + 상태 + iOS 스위치) | 항상 |
| 섹션 라벨 | "종목별 알림 설정" | 항상 |
| 종목별 토글 리스트 | _StockList / _StockRow (종목명 + 코드 + 스위치) | 워치리스트 ≥ 1 |
| 빈 상태 | _EmptyWatchlist | 워치리스트 = 0 |
| 안내 문구 | "알림을 끄려면 기기의 설정 → 공시한입에서 변경해주세요." | 항상 (하단) |
App — 푸시 권한 카드 (_PushPermissionCard)
OS 알림 권한(permission_handler)을 토글. 앱 자체 설정이 아니라 시스템 권한이라, 끄기는 OS 설정에서만 가능.
- ON — 상태 카피 "알림이 활성화되어 있습니다", 스위치 on + disabled (끄려면 OS 설정으로)
- OFF — 상태 카피 "새 공시가 등록되면 알림을 받습니다", 스위치 off → 탭 시 권한 요청
provider 최초 생성 시 1회 (build() → _init()) — 분기 조건 isGranted / !isPermanentlyDenied:
granted→ ON 유지permanentlyDenied가 아닌 그 외 (notDetermined·denied) →requestPermission()자동 호출 (시스템 다이얼로그)permanentlyDenied→ OFF 유지, 스위치 탭 시openAppSettings()로 설정 앱 이동- 요청 중 → 스위치 자리에 spinner (
requesting)
자동 요청은 provider 최초 생성 시 1회만.
notificationsControllerProvider는 (autoDispose 아닌)NotifierProvider라 탭을 나갔다 다시 들어와도 재생성되지 않고,auth_controller._invalidateAuthScoped()도 이 provider 를 의도적으로 invalidate 에서 제외(권한 팝업 반복 노출 방지). 즉 매 탭 진입마다 재요청되지 않음 — 최초 1회 이후엔 스위치 탭으로만 요청.
App — 종목별 토글 리스트
워치리스트 각 종목의 notify_enabled 를 개별 on/off. 행 = 종목명 + (종목코드 ?? corp_code) + 스위치.
- 스위치는 OS 권한 OFF 거나 해당 종목 토글 요청 진행 중(
inFlightToggles)이면 disabled - 토글 실패 시 토스트 "알림 설정 변경에 실패했습니다." (
ToastType.error), 상태 롤백은 컨트롤러가 처리 - 워치리스트 로딩 중 → 스피너
App — 상태 분기
- N1. 권한 ON + 종목 있음 — 권한 카드 ON + 종목 리스트 (각 스위치 활성)
- N2. 권한 OFF — 권한 카드 OFF + 종목 스위치 전부 disabled (권한 먼저 켜야 함)
- N3. 빈 워치리스트 — 권한 카드 +
_EmptyWatchlist("등록된 관심 종목이 없습니다." / "홈에서 종목을 추가하면 여기에 표시됩니다.") - N4. 워치리스트 로딩 — 권한 카드 + 스피너
App — 액션 (API)
| 액션 | endpoint | method | request | response |
|---|---|---|---|---|
| 워치리스트 조회 | /api/v1/watchlist | GET | — | [WatchStock] (notify_enabled 포함) |
| 종목 알림 토글 | /api/v1/watchlist/{corpCode}/notify | PATCH | {enabled} (bool, 필수) | {notify_enabled} |
OS 푸시 권한은 API 가 아니라 단말 권한(
permission_handler). FCM 토큰 등록/해제는 별도push/*흐름 (앱 부트스트랩 시). 이 화면은 권한 토글 + 종목별 on/off 만 담당.
Edge cases
- 권한 permanentlyDenied: 앱 내 토글 불가 → 스위치 탭 시 OS 설정 앱으로 이동 (
openAppSettings) - 권한 OFF 상태에서 종목 토글 시도: 스위치 disabled 라 불가 — 권한 카드로 먼저 유도
- 토글 race / 실패:
inFlightToggles로 중복 탭 차단, 실패 시 토스트 + 롤백
관련 docs
- domain-watchlist.md — 워치리스트 / notify 비즈니스 룰
- screen-dashboard.md — 워치리스트 등록(홈)