1차 보완(0.1.1+14) 내용 합치기

This commit is contained in:
Sn-Kinos
2026-10-16 17:58:55 +09:00
62 changed files with 2480 additions and 447 deletions
+3 -5
View File
@@ -14,9 +14,9 @@ import '../../core/ui/bottom_inset.dart';
/// 별첨 B v1.4 — *하나의 FANUZU UID 에 복수 소셜 Provider 연결은 허용합니다.*
/// X(트위터)는 확정 4종에 없어 목록에서 빠졌다([linkableProviders]).
///
/// 명세 `554:22797` — *플랫폼별 연동 상태 반영 · **동일 이메일만 연동 가능***
/// 판정은 [sameAccountIdentity] 가 하고, 실제 연결은 [AccountLinker] 구현체
/// (Firebase `linkWithCredential`)가 한다.
/// 명세 `554:22797` — *플랫폼별 연동 상태 반영*. 명세의 「동일 이메일만 연동 가능」은 통합검토회신
/// v2.3 **Q-E8** 이 대체했다(이메일 일치는 소유 확인이 아니다). 연결 검증은 [AccountLinker] 구현체
/// (Firebase `linkWithCredential` · 카카오 `fnAuthKakao`)가 서버에서 한다.
class AccountLinkScreen extends StatefulWidget {
const AccountLinkScreen({
super.key,
@@ -68,8 +68,6 @@ class _AccountLinkScreenState extends State<AccountLinkScreen> {
],
),
),
const SizedBox(height: AppSpace.s4),
Text(context.l10n.linkSameEmailOnly, style: t.labelSmall),
const SizedBox(height: AppSpace.s5),
FilledButton(
onPressed: _busy ? null : _toggle,
+7
View File
@@ -13,6 +13,7 @@ import 'dart:typed_data';
import '../../core/data/backend.dart';
import '../../core/i18n/app_locale.dart';
import '../quest/location_consent.dart';
/// POP 종류 — 정책 원장 **B-01**.
///
@@ -775,6 +776,9 @@ void checkProfilePhoto(PickedPhoto photo) {
}
abstract interface class MyRepository {
/// 위치기반 서비스 약관 동의 상태 — 설정의 동의 · 철회 토글(Q-E1 · R02).
LocationConsent get locationConsent;
Future<MyProfile> profile();
/// 명세 — *디폴트 최신순으로 20개 노출*.
@@ -836,6 +840,9 @@ abstract interface class MyRepository {
class SampleMyRepository implements MyRepository {
SampleMyRepository();
@override
LocationConsent get locationConsent => SampleLocationConsent.shared;
@override
Future<void> withdraw() async {}
@@ -6,6 +6,7 @@ import 'package:firebase_storage/firebase_storage.dart';
import '../../core/data/backend.dart';
import '../home/home_repository_firestore.dart' show kstDayKey;
import 'my_data.dart';
import '../quest/location_consent.dart';
import '../../core/i18n/app_locale.dart';
/// MY 의 실 서버 구현 — 화면 대조 원장 F6.
@@ -42,6 +43,9 @@ import '../../core/i18n/app_locale.dart';
class FirestoreMyRepository extends SampleMyRepository {
FirestoreMyRepository(this._backend);
@override
late final LocationConsent locationConsent = FirestoreLocationConsent(_backend);
final Backend _backend;
@override
+75 -4
View File
@@ -12,6 +12,7 @@ import '../../core/ui/app_icons.dart';
import '../auth/auth.dart';
import '../auth/auth_repository.dart';
import '../home/home_screen.dart' show formatPop;
import '../quest/location_consent.dart';
import 'my_data.dart';
import 'notice_screens.dart';
import 'payment_screens.dart';
@@ -24,7 +25,7 @@ import '../../core/ui/bottom_inset.dart';
/// 설정 — 화면 대조 원장 F6 · Figma `354:222008`.
///
/// 명세 `554:22634` — 계정 정보(닉네임 · 가입 이메일)
/// 명세 `554:22797` — SNS 연동. *동일 이메일만 연동 가능*. 행의 「연결」에서 바로 제공자
/// 명세 `554:22797` — SNS 연동. 행의 「연결」에서 바로 제공자
/// 시트를 띄운다(시안 F6-05~07 · 대표 결정 — 별도 연동 화면을 거치지 않는다).
/// 명세 `554:22831` — 푸시 알림 · 차단 · 언어
/// 명세 `554:22857` — 공지사항 · 1:1 문의접수 · 앱 버전
@@ -65,6 +66,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
Map<AuthProviderKind, String?> _linked = const {};
({String current, bool latest})? _version;
/// 위치기반 서비스 약관 동의 상태(Q-E1 · R02). 못 읽었으면 null — 토글을 그리지 않는다.
bool? _locationConsent;
@override
void initState() {
super.initState();
@@ -74,8 +78,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
Future<void> _load() async {
final profile = await widget.repository.profile();
final version = await widget.repository.appVersion();
bool? location;
try {
location = await widget.repository.locationConsent.granted();
} catch (_) {
location = null;
}
if (!mounted) return;
setState(() {
_locationConsent = location;
_profile = profile;
_linked = widget.linker.linkedAccounts;
_version = version;
@@ -166,6 +177,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
down: true,
onTap: _pickLanguage,
),
if (_locationConsent case final on?)
_SwitchRow(
label: l10n.settingsLocationConsent,
value: on,
onChanged: _setLocationConsent,
),
],
),
MyGroup(
@@ -254,7 +271,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
/// 설정 행에서 바로 연결 · 해제한다(시안 F6-05~07 — 설정 위에 OS 제공자 시트가 뜬다).
///
/// 연결은 제공자 시트로 곧장 가고, 해제는 확인 팝업(시안 `554:23257`)을 거친다.
/// 이메일이 다르면(E-AUTH-08 · [sameAccountIdentity]) 시안 `554:23103` 안내 팝업.
/// 그 소셜 계정이 다른 FANUZU 계정에 이미 붙어 있으면(E-AUTH-08) 시안 `554:23103` 안내 팝업.
/// 명세의 「동일 이메일만 연동 가능」은 통합검토회신 v2.3 **Q-E8** 이 대체했다 — 이메일은 소유 확인이 아니다.
Future<void> _toggleLink(AuthProviderKind provider) async {
final connected = _linked.containsKey(provider);
if (connected) {
@@ -274,8 +292,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
// 로컬 값을 뒤집으면 서버가 거절한 연결이 붙은 것처럼 보인다.
setState(() => _linked = widget.linker.linkedAccounts);
switch (outcome) {
case AuthFailed(code: 'E-AUTH-08'):
await _notice(context.l10n.settingsSameEmailOnly);
case AuthFailed(code: 'E-AUTH-08', :final message):
await _notice(message);
case AuthFailed(:final code, :final message):
_toast('[$code] $message');
case AuthNotConfigured():
@@ -335,6 +353,24 @@ class _SettingsScreenState extends State<SettingsScreen> {
await LocaleScope.maybeOf(context)?.select(picked);
}
/// 위치기반 서비스 약관 동의 · 철회(Q-E1 · R02). 켜면 동의 창을 거쳐 기록하고, 끄면 철회 행을 남긴다.
/// Figma 설정 시안에 없는 줄이다 — 동의를 철회할 수단이 있어야 해 언어 아래에 토글 하나를 둔다.
Future<void> _setLocationConsent(bool on) async {
final consent = widget.repository.locationConsent;
var now = on;
if (on) {
now = await ensureLocationConsent(context, consent);
} else {
try {
await consent.record(false);
} catch (_) {
now = true;
if (mounted) _toast(context.l10n.commonRequestFailed);
}
}
if (mounted) setState(() => _locationConsent = now);
}
/// 서비스 탈퇴 — 명세 `554:24885` 「연동 상태값이 있을 경우 비정상 탈퇴 처리 · 게시물 보존 ·
/// 탈퇴 처리 후 앱 종료」. 연동이 남아 있으면 비정상 탈퇴 안내(`554:24908` · `374:14699`)만 띄운다.
/// 서버도 같은 조건을 다시 본다(E-AUTH-14).
@@ -400,6 +436,41 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
}
/// 켜고 끄는 줄 — 푸시 알림 설정 화면의 토글과 같은 모양.
class _SwitchRow extends StatelessWidget {
const _SwitchRow({
required this.label,
required this.value,
required this.onChanged,
});
final String label;
final bool value;
final ValueChanged<bool> onChanged;
@override
Widget build(BuildContext context) => SizedBox(
height: 24,
child: Row(
children: [
Expanded(
child: Text(label, style: myText(16, FontWeight.w600, height: 1)),
),
Switch(
value: value,
activeThumbColor: AppColors.onCosmic,
activeTrackColor: AppColors.cosmicAccent,
inactiveThumbColor: const Color(0xFF3A3A3A),
inactiveTrackColor: const Color(0xFF2C2C2C),
trackOutlineColor: const WidgetStatePropertyAll(Colors.transparent),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: onChanged,
),
],
),
);
}
/// 「닉네임 · BTS_Hong」 — 둘 다 16/600, 값은 #bbbbbb.
class _ValueRow extends StatelessWidget {
const _ValueRow({required this.label, required this.value});