From 4e779dedd2032feaacb5f090df397967e02c6d32 Mon Sep 17 00:00:00 2001 From: ttipo Date: Fri, 6 Nov 2026 15:57:39 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=95=B1=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EB=84=A4=20=EB=8B=A8=EA=B3=84=EC=9D=98=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=98=EB=A9=B0=20=EC=A0=95=ED=95=9C=20=EA=B2=83=EC=9D=84=20?= =?UTF-8?q?=EC=95=B1=20=EB=94=94=EC=9E=90=EC=9D=B8=20README=20=C2=B7=20flu?= =?UTF-8?q?tter=20=EC=8A=A4=ED=82=AC=EC=97=90=20=EC=A0=81=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 앱 디자인 README: 단계마다 실패를 보이는 곳(표), 안내와 오류 함께, 세션 끝은 상자만, [다른 계정으로 로그인]을 막는 때, 2단계 켜기의 처음 초점 · 켠 뒤 알림, 다른 곳에서 켠 경우, 주소 칸 자판, 글자 크기 맞춤, 새 글, 알려진 한계(인증 앱에 다녀오는 사이 앱이 정리되면 새 키 · 최근 앱 미리 보기 · 클립보드 · iOS) - flutter 스킬 골격 표에 로그인 화면 줄(노티파이어 제한 단계 API · 알림 끄는 묶음 · url_launcher 쓰는 규칙 · KeepAllText) Co-Authored-By: Claude Opus 5.5 --- .claude/skills/flutter/SKILL.md | 1 + docs/design/app/README.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/.claude/skills/flutter/SKILL.md b/.claude/skills/flutter/SKILL.md index a1b557e..441203f 100644 --- a/.claude/skills/flutter/SKILL.md +++ b/.claude/skills/flutter/SKILL.md @@ -23,6 +23,7 @@ description: Flutter 모바일 앱(flutter/) 개발 규칙 — Dart Sound Null S | 라우터 · 탭 막대 | `lib/core/router/` — `app_router.dart` · `app_shell.dart` · `session_redirect.dart` · `app_routes.dart` | go_router. 로그인 전 · 제한 단계(`/start` · `/login/...`)는 셸 밖, 로그인 뒤는 `StatefulShellRoute.indexedStack`(켜진 모듈마다 탭 하나 — 탭마다 이동 기록을 지킨다). 아래 탭 막대(`NavigationBar`)가 웹의 전역 앱 레일이다. 읽기 · 쓰기 같은 전체 화면은 `parentNavigatorKey: rootNavigatorKey`. `redirect` 하나(`resolveSessionRedirect` — 순수 함수)가 세션 상태로 화면을 고르고, 모르는 · 꺼진 모듈의 주소는 메일 첫 화면(`onException` 도) | | **세션** | `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)` 만, 실패 예외에 주소가 들어 있어 삼킨다, `` 는 필요 없다. 한글 줄바꿈은 `shared/widgets/keep_all_text.dart`(시험은 `findCopy`). 보안 설정 요청 한도 `kSecuritySettingTimeout` 30초 | | 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`** 로 가른다 | diff --git a/docs/design/app/README.md b/docs/design/app/README.md index f1729f2..515e24b 100644 --- a/docs/design/app/README.md +++ b/docs/design/app/README.md @@ -29,6 +29,26 @@ - 2단계 켜기는 같은 휴대폰이라 QR 을 찍을 수 없다 — [인증 앱에 추가](인증 앱을 바로 연다) · 키와 [복사](키를 직접 넣는 길) · 코드 칸. 인증 앱을 찾지 못하면 키를 직접 넣으라고 알린다. - 웹 첫 화면의 메일 프로그램(IMAP) 안내 줄은 넣지 않는다(앱이 메일 앱이다). 문의 연락처 줄은 웹과 같은 자리. +- **구현하며 정한 것**(2026-11-06 — 보드에는 아직 없다) + - 실패를 보이는 곳(웹 로그인 틀과 같은 결 — 화면이 칸 · 상자에 보이는 코드만 알림을 끈다): + + | 단계 | 칸 아래 | 위쪽 상자 | 그 밖 | + | --- | --- | --- | --- | + | 주소 · 비밀번호 | `VAL_001`(주소 칸) | `AUTH_004`(두 칸 빨강 · 비밀번호 비움) | 전역 알림(`REQ_001` 포함 — 웹과 같다) | + | 인증 앱 코드 | `AUTH_006` · `AUTH_007` · `VAL_001`(칸을 비움) | `REQ_001`(코드 그대로) | `AUTH_004` · 10분 지남은 첫 단계로(주소는 남긴다), 나머지는 전역 알림 | + | 새 비밀번호 | `AUTH_005`(임시 비밀번호) · `VAL_001`(새 비밀번호) · `AUTH_006` · `AUTH_007`(코드) | `REQ_001` · 넘기기 실패 + [다시 시도] | 전역 알림 | + | 2단계 켜기 | `AUTH_006` · `AUTH_007`(코드) · `AUTH_005`(지금 비밀번호) | `AUTH_008` + 새 키 · `REQ_001` | 서버의 `VAL_001` 과 나머지는 전역 알림 | + + - 칸 아래에는 안내와 오류를 함께 보인다(오류가 나도 "다음 코드가 뜨면 넣어 주세요" 같은 안내가 남는다). + - 세션이 끝나 로그인 화면으로 오면 위쪽 상자에 `AUTH_002` 문구만 보이고 알림(스낵바)은 띄우지 않는다(웹은 대화상자). + - [다른 계정으로 로그인]은 코드 단계에서는 늘 누를 수 있고(늦은 결과는 버린다), 새 비밀번호 · 2단계 켜기를 보내거나 넘기는 동안은 막는다(서버가 이미 바꿨을 수 있다 — 웹과 같다). + - 2단계 켜기는 처음 초점을 코드 칸에 두지 않는다 — 자판이 [인증 앱에 추가]를 가리지 않게(웹은 둔다). 켠 뒤 알림은 "2단계 인증을 켰습니다. 메일 프로그램은 앱 비밀번호로 연결합니다."이고 웹의 [앱 비밀번호 만들기] 단추는 뺐다(앱에 그 화면이 없다). + - 다른 곳에서 2단계를 켜면 이 세션은 서버가 끊는다(계약 6절) — 다음 요청에서 로그인 화면 + `AUTH_002`. 키를 받을 때 이미 켜져 있으면(`AUTH_008` 뒤 확인) 이 단계를 건너뛴다. + - 첫 단계 주소 칸만 자판 추천을 켠다(끄면 안드로이드 엔진이 비밀번호 보기 형식을 섞어 @ 자판이 나오지 않는다). 자동 고침은 모든 칸에서 끈다. + - 보드의 13.5 · 14.5 · 15.5 글자는 가까운 테마 역할(13 · 15 · 15)로 맞췄다. 한글은 낱말 단위로 줄을 바꾼다(`KeepAllText` — 보드의 `word-break: keep-all`). + - 새 글(보드에 없던 상태): 키를 복사하지 못하면 "키를 복사하지 못했습니다. 키를 보고 인증 앱에 직접 입력해 주세요.", ③ 안내 "지금 비밀번호를 한 번 더 넣어 주세요." — 그 밖은 웹 로그인 틀의 글을 그대로 옮겼다. + - **알려진 한계**: 인증 앱에 다녀오는 사이 안드로이드가 앱을 정리하면 돌아왔을 때 새 키가 나온다(인증 앱에 먼저 넣은 항목은 지우고 다시 넣어야 한다 — 옛 키의 코드는 `AUTH_006`, 웹의 새로고침과 같은 결). + 키 화면은 최근 앱 미리 보기에 찍히고(FLAG_SECURE 없음), 복사한 키는 클립보드에 남으며 안드로이드 13 이상은 복사 미리 보기에 보인다(Flutter 클립보드는 민감 표시를 못 한다 — 네이티브 채널이 든다). iOS 의 [인증 앱에 추가]는 iOS 를 붙일 때 여는 방식을 바꾼다. ### 메일 목록 (`App1MailList`) - 위 막대: 서랍 열기 · 메일함 이름 · 찾기. 서랍은 메일함만(안 읽은 수는 회색 글 — 뱃지 없음), 탭 막대 위까지 화면 전체를 덮는다. - 줄: 동그란 사진(40) · 보낸 사람 · 시각 / 제목 · 첨부 표시 / **미리 보기 한 줄**. 안 읽음은 굵게, 중요는 별.