로그아웃 SDK 대기 제거 · 온보딩 국가 시트 · 프로필 머리줄

This commit is contained in:
Sn-Kinos
2026-10-06 04:47:37 +09:00
parent 976e569a56
commit 40756d5c86
21 changed files with 277 additions and 43 deletions
+12 -6
View File
@@ -241,13 +241,19 @@ class AuthRepository extends ChangeNotifier {
}
}
// SDK 로그아웃(Google · Kakao 등)은 기다리지 않는다. 로그인 상태를 정하는 것은 Firebase 세션이고,
// SDK 쪽은 다음 로그인의 자동 선택을 지우는 정리일 뿐이다. Android Google 은 Credential Manager
// `clearCredentialStateAsync` 가 응답 없이 멈춰 3초 시간 제한을 다 쓰고, 그 TimeoutException 이
// 처리되지 않은 채 로그에 남았다(10/6 실기기). 실패는 로그로만 남긴다.
if (provider != null) {
unawaited(
provider.signOut().catchError((Object e) {
debugPrint('[Auth] provider sign-out skipped: $e');
}),
);
}
try {
await Future.wait([
// SDK 로그아웃(Google · Kakao 등)이 응답 없이 멈춰도 로컬 로그아웃은 한다.
if (provider != null)
provider.signOut().timeout(const Duration(seconds: 3)),
if (_session != null) _session.signOut(),
], eagerError: false);
await _session?.signOut();
} finally {
// 원격 SDK 정리가 실패해도 로컬 라우터는 로그인 상태에 고착되지 않는다.
_replaceAccount(null);
+72 -16
View File
@@ -381,6 +381,8 @@ class _ProfileStepState extends State<_ProfileStep> {
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF101010),
// 사본 `178:23489` 상단 손잡이.
showDragHandle: true,
builder: (_) => _CountrySheet(current: _country),
);
if (picked != null && mounted) setState(() => _country = picked);
@@ -390,9 +392,12 @@ class _ProfileStepState extends State<_ProfileStep> {
Widget build(BuildContext context) {
final l10n = context.l10n;
final current = _locale ?? LocaleScope.maybeOf(context)?.locale;
// Figma `511:86825` — 머리줄 「<」 좌측 · 타이틀 중앙(사본 `178:23489` 「회원가입」), 본문 큰 제목 없음.
// 입력란은 사본처럼 테두리 상자(`511:86809`).
return _StepScaffold(
title: l10n.onbProfileTitle,
barTitle: l10n.authSignup,
onBack: widget.onBack,
boxedInputs: true,
action: _PrimaryButton(
label: l10n.commonNext,
onPressed: _valid && !_busy ? _save : null,
@@ -475,10 +480,12 @@ class _CountrySheetState extends State<_CountrySheet> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 4, 0),
padding: const EdgeInsets.fromLTRB(16, 0, 4, 0),
child: Row(
children: [
Expanded(child: Text(context.l10n.onbCountry, style: _title)),
Expanded(
child: Text(context.l10n.onbCountrySheetTitle, style: _title),
),
IconButton(
tooltip: context.l10n.commonClose,
onPressed: () => Navigator.of(context).pop(),
@@ -490,7 +497,7 @@ class _CountrySheetState extends State<_CountrySheet> {
Expanded(
child: ListView(
children: [
for (final c in isoCountries)
for (final c in sortedCountries())
ListTile(
onTap: () => setState(() => _picked = c),
leading: Text(
@@ -739,14 +746,18 @@ class _PermissionStepState extends State<_PermissionStep> {
/// 단계 화면 틀 — 사본 가입 화면과 같은 코스믹 다크 · 하단 고정 버튼.
class _StepScaffold extends StatelessWidget {
const _StepScaffold({
required this.title,
this.title,
required this.action,
required this.children,
this.subtitle,
this.onBack,
this.barTitle,
this.boxedInputs = false,
});
final String title;
final String? title;
final String? barTitle;
final bool boxedInputs;
final String? subtitle;
final Widget action;
final List<Widget> children;
@@ -757,6 +768,8 @@ class _StepScaffold extends StatelessWidget {
appBar: AppBar(
automaticallyImplyLeading: false,
leading: onBack == null ? null : BackButton(onPressed: onBack),
title: barTitle == null ? null : Text(barTitle!),
centerTitle: true,
),
body: SafeArea(
top: false,
@@ -764,17 +777,22 @@ class _StepScaffold extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.all(AppSpace.s4),
children: [
Text(title, style: _title),
if (subtitle != null) ...[
const SizedBox(height: AppSpace.s2),
Text(subtitle!, style: _body),
child: _boxed(
context,
ListView(
padding: const EdgeInsets.all(AppSpace.s4),
children: [
if (title != null) ...[
Text(title!, style: _title),
if (subtitle != null) ...[
const SizedBox(height: AppSpace.s2),
Text(subtitle!, style: _body),
],
const SizedBox(height: AppSpace.s5),
],
...children,
],
const SizedBox(height: AppSpace.s5),
...children,
],
),
),
),
Padding(padding: const EdgeInsets.all(AppSpace.s4), child: action),
@@ -782,6 +800,28 @@ class _StepScaffold extends StatelessWidget {
),
),
);
/// 사본 가입 화면 입력란 — 회색 테두리 상자(약관 상자와 같은 선 · 모서리 8).
Widget _boxed(BuildContext context, Widget child) {
if (!boxedInputs) return child;
OutlineInputBorder line(Color c, [double w = 1]) => OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.sm),
borderSide: BorderSide(color: c, width: w),
);
final theme = Theme.of(context);
return Theme(
data: theme.copyWith(
inputDecorationTheme: theme.inputDecorationTheme.copyWith(
border: line(_stroke),
enabledBorder: line(_stroke),
focusedBorder: line(AppColors.cosmicAccent, 2),
errorBorder: line(AppColors.error),
focusedErrorBorder: line(AppColors.error, 2),
),
),
child: child,
);
}
}
class _PrimaryButton extends StatelessWidget {
@@ -803,6 +843,9 @@ class _PrimaryButton extends StatelessWidget {
);
}
/// 사본 입력란 테두리 — 약관 상자(`signup_screens.dart`)와 같은 값.
const _stroke = Color(0xFF545454);
const _title = TextStyle(
fontFamily: AppFonts.body,
fontSize: 20,
@@ -822,6 +865,19 @@ const _body = TextStyle(
String countryFlag(String code) =>
String.fromCharCodes(code.codeUnits.map((c) => c - 0x41 + 0x1F1E6));
/// 국가 시트 순서 — 기기 OS 정렬([platformCountryNames] 순서)을 따르고, OS 가 모르는 코드는 표시명 순으로 뒤에 둔다.
/// 사본 `178:23489` 목록은 (첫 줄 독일을 빼면) 국가명 가나다순이고 검색 칸이 없다. 범위는 Q-E10 회신 대기.
List<String> sortedCountries() {
final rest = isoCountries.toSet();
final ordered = [
for (final c in platformCountryNames.keys)
if (rest.remove(c)) c,
];
return ordered..addAll(
rest.toList()..sort((a, b) => countryName(a).compareTo(countryName(b))),
);
}
/// ISO 3166-1 alpha-2 전체. 랭킹 집계 · Admin 사용자 일람의 「거주 국가/지역」 값이다.
final List<String> isoCountries =
('AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ '
+9 -1
View File
@@ -126,7 +126,15 @@ class _TermsScreenState extends State<TermsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(context.l10n.termsTitle)),
// Figma `179:24230` 머리줄 좌측 「<」 — 「이전」과 같은 동작(온보딩 안에서는 로그아웃 후 로그인 화면).
appBar: AppBar(
automaticallyImplyLeading: false,
leading: BackButton(
onPressed: widget.onBack ?? () => Navigator.of(context).pop(),
),
title: Text(context.l10n.termsTitle),
centerTitle: true,
),
// Figma `179:24230` — 「이전 · 완료」는 목록 끝이 아니라 화면 하단 고정(56 높이 · 간격 10 · 아래 16).
// 시스템 내비게이션 바 위로 올린다(SafeArea) — 10/6 실기기에서 첫 탭이 바에 먹혔다.
bottomNavigationBar: SafeArea(