generated from ttipo/project-base
docs: 앱 메일 목록의 구현하며 정한 것을 앱 디자인 README · flutter 스킬에 적는다
- 앱 디자인 README: 고르기(줄 누르기 · 바뀌는 단추 · 영구 삭제 · 읽음 뒤 끝내기 · 선택 풀기), 붙잡힌 메일, 알림 두 갈래, 서랍, 30일 줄 · [찾기] 준비 중, 새 메일 확인과 병합 규칙(웹과 다른 점), 크기 맞춤, 메일함 이름 차례 - flutter 스킬 골격 표: 메일 목록 · 앱 틀 줄(틀은 Scaffold 가 아니다 · ShellBarHidden · 알림 두 갈래 · 생명주기 안 프로바이더 · 가짜 서버의 한 프레임 응답) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ description: Flutter 모바일 앱(flutter/) 개발 규칙 — Dart Sound Null S
|
||||
| **세션** | `lib/features/auth/presentation/providers/session_provider.dart` | `AsyncNotifier` 하나(`sessionProvider` — 저절로 다시 하지 않는다: Riverpod 3 의 `retry: null` 은 기본 다시 하기라 늘 null 을 돌려주는 `noAutomaticRetry` 를 준다). 상태 `SignedOut` · `CodeRequired` · `SignedIn(info.restriction)` + 되살리는 중(`AsyncLoading`) · 되살리기 장애(`AsyncError`). 로그인 때 넣은 비밀번호는 상태가 아니라 `LoginSecret`(메모리 · 10분 — 코드 단계 · 제한 단계가 쓴다). 인터셉터와는 `SessionEventBus`(`core/api/session_events.dart`)로만 잇는다 — 세션 끝 · `AUTH_010` 다시 맞추기(`SessionRecheckNeeded` 에 답 `RestrictionCheck` — renewed · restricted · stale · unknown, 웹과 같다) · 갱신 응답의 제한. **시도 세대**: 로그아웃 · [다른 계정으로 로그인] · 세션 끝 때 올라 그 전에 시작한 되살리기 · 로그인 · 코드 · 다시 맞추기의 늦은 결과를 버린다(버린 결과에 토큰이 있으면 조용히 앱 logout). 로그아웃은 토큰 · 상태를 먼저 지우고 서버 logout · 알림 토큰 지우기는 그 뒤. 제한이 풀리면 쥔 비밀번호를 지운다 |
|
||||
| 앱 로그인 저장소 | `lib/features/auth/data/` | 계약 1절 "앱 로그인"(`/auth/app/login` · `refresh` · `logout`(리프레시 토큰 + Bearer) + Bearer `me`, 인터셉터 줄 밖의 세션 확인 `readSessionDirect`). 실패는 늘 `DioException`(`error` 에 `ApiException` — 응답 모양이 틀리면 `SYS_001`, `apiFailure()`). 모델은 손으로 쓴다(`AppLoginResult` · `AuthSessionInfo` · `SessionRestriction` — 모르는 제한은 `unknown`). 로그인 화면이 칸에 보이는 실패(`kLoginInlineCodes`)는 알리지 않는다 |
|
||||
| **로그인 화면 (네 단계 — `App1Login`)** | `lib/features/auth/presentation/pages/`(login · login_code · password_change · two_factor_setup · restricted_login) · `widgets/`(`login_frame` · `login_fields` · `login_parts`) · 글 `core/constants/login_texts.dart`(제목은 `screen_titles.dart`) · `data/`(`two_factor_repository` · `support_contact_repository` · `authenticator_launcher`) | 노티파이어의 제한 단계 API(`attempt` · `passedCodeStep` · `heldLoginListenable` · `changePassword` · `enableTwoFactor` · `finishRestrictedStep` → `RestrictedStepResult { done, needsPassword, stale }`). 로그인 비밀번호는 두 제한 단계에서만 10분 쥔다(모르는 제한 · 제한이 풀리면 지움). 칸 · 상자에 보이는 코드만 알림을 끈다(`kLoginInlineCodes` · `kLoginCodeInlineCodes` · `kPasswordChangeInlineCodes` · `kTwoFactorSetupInlineCodes` — 앱 디자인 README 의 표), `AUTH_002` 는 스낵바 없이 로그인 화면 상자만. 2단계 키 · otpauth 주소는 그 화면 State 에만. **url_launcher**: `canLaunchUrl` 은 쓰지 않는다(debug 판이 주소째 로그를 남긴다) — `launchUrl(externalNonBrowserApplication)` 만, 실패 예외에 주소가 들어 있어 삼킨다, `<queries>` 는 필요 없다. 한글 줄바꿈은 `shared/widgets/keep_all_text.dart`(시험은 `findCopy`). 보안 설정 요청 한도 `kSecuritySettingTimeout` 30초 |
|
||||
| **메일 목록 (`App1MailList`)** · 앱 틀 | `lib/features/mail/` — data(models · `mail_repository.dart`) · domain(`mailbox_tree` · `mail_list_merge` · `mail_bulk` — 순수 함수) · presentation(`pages/mail_list_page.dart` · providers · widgets), 틀 `core/router/app_shell.dart` · `shell_bar.dart`, `core/utils/`(`app_foreground` · `accessible_navigation` · `korean_format`), `shared/widgets/confirm_dialog.dart`, 시험 도구 `test/support/mail_fakes.dart` · `mail_harness.dart` | **틀은 Scaffold 가 아니다**(Column + 탭 막대 — 탭 안 화면마다 자기 Scaffold 를 둔다, 없으면 스낵바 · Material 조상이 없다). 탭 막대는 `MediaQuery.removePadding(removeTop)` 으로 감싸고, 화면이 숨기려면 `ShellBarHidden`(모듈마다 센다 — 보이는지는 TickerMode). Scaffold 는 넓은 단추 자리에 안전 영역을 넘기지 않으니 Scaffold 밖에서 읽어 넘긴다. 목록 상태는 메일함마다(`mailListProvider(String?)` autoDispose), 세션이 바뀌면 `mailAccountProvider` 로 처음부터. 알림은 두 갈래 — 단추 있는 `mailToastProvider`(한 번에 하나 · 화면 낭독기 켠 동안 닫지 않음) · 단추 없는 `mailNoticeSinkProvider`(전역 스낵바). 생명주기 안에서는 프로바이더를 바꾸지 않는다(그린 뒤 · 마이크로태스크), 다른 노티파이어 인스턴스를 든 노티파이어는 세션이 바뀐 뒤 다시 읽는다. 가짜 서버는 응답이 한 프레임 안에 오므로 "받는 중이 끝나는 순간"을 기다리는 위젯을 만들지 않는다 |
|
||||
| HTTP 클라이언트 | `lib/core/api/dio_client.dart` · `dio_provider.dart` | Dio 둘: 화면용(인터셉터 줄 `ResponseInterceptor` → `AuthInterceptor` → `ErrorNotifyInterceptor`)과 갱신 · 다시 보내기용(언래핑만). baseUrl=dotenv `API_URL`(`api_config.dart`), User-Agent `ARASYSTEM-App/<판> (Android <판>; <기종>)`(`user_agent.dart`). `LogInterceptor` 는 넣지 않는다(비밀번호 · 토큰). 시험은 `httpClientAdapterProvider` 로 가짜 서버를 끼운다 |
|
||||
| 언래핑 · 코드 정규화 | `lib/core/api/response_interceptor.dart` · `api_error_mapper.dart` | `{success,data}→data`(스트림 · 바이트는 그대로), 200 + `success:false` 도 실패, 실패는 모두 `DioException.error` 에 `ApiException`. 상태 → 코드 폴백(웹 `resolveCodeByStatus` 와 같다) |
|
||||
| **ApiException** | `lib/core/api/api_exception.dart` | `{code, message, traceId, statusCode}`. 화면은 `apiErrorCodeOf(e)` 또는 `e.error as ApiException` 의 **`code`** 로 가른다 |
|
||||
|
||||
Reference in New Issue
Block a user