Figma 설명 프레임 반영 — 로그인·온보딩·스플래시 · HOME · GALAXY · 퀘스트·이클립스·알림
This commit is contained in:
@@ -4,11 +4,13 @@ import '../../core/data/backend.dart' show BackendException;
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import '../../core/push/push_messaging.dart';
|
||||
import '../../core/theme/tokens.dart';
|
||||
import '../../core/ui/app_icons.dart';
|
||||
import '../home/coach_mark.dart';
|
||||
import '../home/home_data.dart';
|
||||
import '../home/nebula_create_screen.dart';
|
||||
import '../my/my_data.dart'
|
||||
show nicknameMaxLength, nicknameMinLength, nicknameReserved;
|
||||
import '../planet/planet_data.dart' show countryName;
|
||||
import 'auth_repository.dart';
|
||||
import 'onboarding_data.dart';
|
||||
import 'signup_screens.dart';
|
||||
@@ -159,6 +161,9 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
|
||||
_Step _step = _Step.terms;
|
||||
bool _busy = false;
|
||||
|
||||
/// ② 에서 서버에 기록한 동의 항목. ③ 에서 뒤로 오면 이 상태로 체크해 보여 준다.
|
||||
Set<String>? _agreed;
|
||||
|
||||
/// ④ 에서 고른 팬덤 — 고른 순서. 첫 번째가 대표(Primary).
|
||||
final _picked = <OnboardingFandom>[];
|
||||
|
||||
@@ -189,14 +194,25 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
|
||||
|
||||
void _agree(Set<String> agreed) {
|
||||
if (_busy) return;
|
||||
// ③ 에서 뒤로 왔다가 그대로 「완료」면 기록을 다시 남기지 않는다(서버 동의 기록 유지).
|
||||
final prev = _agreed;
|
||||
if (prev != null &&
|
||||
prev.length == agreed.length &&
|
||||
prev.containsAll(agreed)) {
|
||||
setState(() => _step = _Step.profile);
|
||||
return;
|
||||
}
|
||||
_run(() async {
|
||||
await widget.repository.agree(agreed, signupTermsVersion);
|
||||
_agreed = agreed;
|
||||
if (mounted) setState(() => _step = _Step.profile);
|
||||
});
|
||||
}
|
||||
|
||||
void _back() => setState(
|
||||
() => _step = switch (_step) {
|
||||
// 명세 `511:86825` 「상단 뒤로가기 — 이전 화면」. 이전 = ② 약관(동의 체크 유지).
|
||||
_Step.profile => _Step.terms,
|
||||
_Step.artist => _Step.profile,
|
||||
_Step.planet => _Step.artist,
|
||||
final s => s,
|
||||
@@ -216,9 +232,15 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final body = switch (_step) {
|
||||
_Step.terms => TermsScreen(onAgreed: _agree, onBack: widget.auth.signOut),
|
||||
_Step.terms => TermsScreen(
|
||||
key: ValueKey(_agreed),
|
||||
initial: _agreed ?? const {},
|
||||
onAgreed: _agree,
|
||||
onBack: widget.auth.signOut,
|
||||
),
|
||||
_Step.profile => _ProfileStep(
|
||||
repository: widget.repository,
|
||||
onBack: _back,
|
||||
onDone: () => setState(() => _step = _Step.artist),
|
||||
),
|
||||
_Step.artist => _ArtistStep(
|
||||
@@ -262,10 +284,15 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
|
||||
/// 사본 `MY/프로필 설정` 구성을 따른다. 국가 기본값은 기기 지역(Figma `554:24126` 「앱 기동 단계에서
|
||||
/// 조회된 국가/언어를 디폴트」).
|
||||
class _ProfileStep extends StatefulWidget {
|
||||
const _ProfileStep({required this.repository, required this.onDone});
|
||||
const _ProfileStep({
|
||||
required this.repository,
|
||||
required this.onDone,
|
||||
required this.onBack,
|
||||
});
|
||||
|
||||
final OnboardingRepository repository;
|
||||
final VoidCallback onDone;
|
||||
final VoidCallback onBack;
|
||||
|
||||
@override
|
||||
State<_ProfileStep> createState() => _ProfileStepState();
|
||||
@@ -348,12 +375,23 @@ class _ProfileStepState extends State<_ProfileStep> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickCountry() async {
|
||||
final picked = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: const Color(0xFF101010),
|
||||
builder: (_) => _CountrySheet(current: _country),
|
||||
);
|
||||
if (picked != null && mounted) setState(() => _country = picked);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final current = _locale ?? LocaleScope.maybeOf(context)?.locale;
|
||||
return _StepScaffold(
|
||||
title: l10n.onbProfileTitle,
|
||||
onBack: widget.onBack,
|
||||
action: _PrimaryButton(
|
||||
label: l10n.commonNext,
|
||||
onPressed: _valid && !_busy ? _save : null,
|
||||
@@ -369,15 +407,20 @@ class _ProfileStepState extends State<_ProfileStep> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpace.s3),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _country,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(labelText: l10n.onbCountry),
|
||||
items: [
|
||||
for (final c in isoCountries)
|
||||
DropdownMenuItem(value: c, child: Text('${countryFlag(c)} $c')),
|
||||
],
|
||||
onChanged: (v) => setState(() => _country = v ?? _country),
|
||||
// 사본 국가 리스트 모달 `178:23489`(명세 `511:86786` · `511:86796`) — 국기 · 국가명 ·
|
||||
// 단수 선택 체크, 「선택」을 눌러야 적용하고 닫힌다.
|
||||
InkWell(
|
||||
onTap: _pickCountry,
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.onbCountry,
|
||||
suffixIcon: const Icon(Icons.expand_more),
|
||||
),
|
||||
isEmpty: _country == null,
|
||||
child: _country == null
|
||||
? null
|
||||
: Text('${countryFlag(_country!)} ${countryName(_country!)}'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpace.s3),
|
||||
DropdownButtonFormField<Locale>(
|
||||
@@ -407,6 +450,78 @@ class _ProfileStepState extends State<_ProfileStep> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 국가 리스트 모달 — 사본 `178:23489`.
|
||||
///
|
||||
/// 명세 `511:86762` 타이틀 좌측 · 닫기 우측(누르면 모달 종료) / `511:86786` 국기 좌측 · 국가명 ·
|
||||
/// 선택한 국가에만 오른쪽 체크(단수) / `511:86796` 국가를 골라야 「선택」이 켜지고, 누르면 적용 후 닫힘.
|
||||
class _CountrySheet extends StatefulWidget {
|
||||
const _CountrySheet({required this.current});
|
||||
|
||||
final String? current;
|
||||
|
||||
@override
|
||||
State<_CountrySheet> createState() => _CountrySheetState();
|
||||
}
|
||||
|
||||
class _CountrySheetState extends State<_CountrySheet> {
|
||||
late String? _picked = widget.current;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SafeArea(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height * 0.8,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 4, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(context.l10n.onbCountry, style: _title)),
|
||||
IconButton(
|
||||
tooltip: context.l10n.commonClose,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const ImageIcon(AppIcons.x, size: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
for (final c in isoCountries)
|
||||
ListTile(
|
||||
onTap: () => setState(() => _picked = c),
|
||||
leading: Text(
|
||||
countryFlag(c),
|
||||
style: const TextStyle(fontSize: 22),
|
||||
),
|
||||
title: Text(countryName(c)),
|
||||
trailing: c == _picked
|
||||
? const Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: AppColors.cosmicAccent,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(AppSpace.s4),
|
||||
child: _PrimaryButton(
|
||||
label: context.l10n.commonSelect,
|
||||
onPressed: _picked == null
|
||||
? null
|
||||
: () => Navigator.of(context).pop(_picked),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// ④ 관심 아티스트 / Primary Fandom — 최소 1개(FR-CMN-06). 처음 고른 팬덤이 대표가 된다.
|
||||
/// 찾는 아티스트가 없으면 ⑤ 에서 성운을 만든다.
|
||||
class _ArtistStep extends StatefulWidget {
|
||||
|
||||
Reference in New Issue
Block a user