diff --git a/app/lib/features/auth/onboarding_data.dart b/app/lib/features/auth/onboarding_data.dart index bf8cd47..a98b721 100644 --- a/app/lib/features/auth/onboarding_data.dart +++ b/app/lib/features/auth/onboarding_data.dart @@ -164,7 +164,13 @@ class SampleOnboardingRepository implements OnboardingRepository { }) async => profile = (nickname: nickname, country: country, locale: locale); @override - Future attributeReferral(String code) async => referral = code; + Future attributeReferral(String code) async { + // 서버 `fnReferralAttribute` 처럼 1인 1회 — 두 번째는 거절(E-POP-03). + if (referral != null) { + throw const BackendException('E-POP-03', '이미 처리된 추천입니다.'); + } + referral = code; + } @override Future> fandoms({String query = ''}) async => [ diff --git a/app/lib/features/auth/onboarding_screens.dart b/app/lib/features/auth/onboarding_screens.dart index 93688b6..7966391 100644 --- a/app/lib/features/auth/onboarding_screens.dart +++ b/app/lib/features/auth/onboarding_screens.dart @@ -165,6 +165,10 @@ class _OnboardingFlowState extends State { /// ② 에서 서버에 기록한 동의 항목. ③ 에서 뒤로 오면 이 상태로 체크해 보여 준다. Set? _agreed; + /// ③ 에서 서버에 귀속한 추천인 코드. ④ 에서 뒤로 와 다시 「다음」이면 또 부르지 않는다 + /// (서버는 1인 1회라 두 번째 호출을 거절한다). + String? _referred; + /// ④ 에서 고른 팬덤 — 고른 순서. 첫 번째가 대표(Primary). final _picked = []; @@ -241,6 +245,8 @@ class _OnboardingFlowState extends State { ), _Step.profile => _ProfileStep( repository: widget.repository, + referred: _referred, + onReferred: (code) => _referred = code, onBack: _back, onDone: () => setState(() => _step = _Step.artist), ), @@ -287,11 +293,17 @@ class _OnboardingFlowState extends State { class _ProfileStep extends StatefulWidget { const _ProfileStep({ required this.repository, + required this.referred, + required this.onReferred, required this.onDone, required this.onBack, }); final OnboardingRepository repository; + + /// 이미 귀속한 추천인 코드(뒤로 왔을 때 채워 보여 준다). + final String? referred; + final ValueChanged onReferred; final VoidCallback onDone; final VoidCallback onBack; @@ -301,7 +313,7 @@ class _ProfileStep extends StatefulWidget { class _ProfileStepState extends State<_ProfileStep> { final _nickname = TextEditingController(); - final _referral = TextEditingController(); + late final _referral = TextEditingController(text: widget.referred); String? _country = _deviceCountry(); Locale? _locale; String? _nicknameError; @@ -360,11 +372,20 @@ class _ProfileStepState extends State<_ProfileStep> { ); if (_locale != null) await scope?.select(_locale!); final code = _referral.text.trim().toUpperCase(); - if (code.isNotEmpty) { + if (code.isNotEmpty && code != widget.referred) { try { await widget.repository.attributeReferral(code); + widget.onReferred(code); } on BackendException catch (e) { - if (mounted) setState(() => _referralError = e.message); + // 거절 사유(형식 · 없는 코드 · 본인 · 상호 · 이미 등록 · 7일 경과)는 Figma `511:86845` + // 문구 하나로 보인다(C-119 「오류 문구 재사용」). 통신 실패만 서버 문구 그대로. + if (mounted) { + setState( + () => _referralError = e.code == 'E-SYS-01' + ? e.message + : l10n.onbReferralInvalid, + ); + } return; } } @@ -440,6 +461,8 @@ class _ProfileStepState extends State<_ProfileStep> { onChanged: (v) => setState(() => _locale = v), ), const SizedBox(height: AppSpace.s3), + // 추천인 코드 입력칸 — Figma 이메일 가입 `333:18620`(입력칸) · `511:86845`(오류) · + // `511:86809`(선택 입력). 10/3 통합회신 7항으로 가입 직후 온보딩에 둔다(C-119). TextField( controller: _referral, textCapitalization: TextCapitalization.characters, @@ -447,7 +470,6 @@ class _ProfileStepState extends State<_ProfileStep> { onChanged: (_) => setState(() => _referralError = null), decoration: InputDecoration( labelText: l10n.onbReferral, - helperText: l10n.onbReferralHint, errorText: _referralError, ), ), diff --git a/app/lib/features/my/my_data.dart b/app/lib/features/my/my_data.dart index 813363c..67a9f90 100644 --- a/app/lib/features/my/my_data.dart +++ b/app/lib/features/my/my_data.dart @@ -322,6 +322,7 @@ class MyProfile { this.userLevel = 1, this.passportTier = PassportTier.silver, required this.balancePop, + this.reservedPop = 0, required this.steps, required this.completedQuests, required this.planetTiers, @@ -346,9 +347,15 @@ class MyProfile { /// 대표 팬덤의 Passport Tier(B-65 · 10/3 #10 사용자 × 팬덤). 팬덤별 값은 [planetTiers]. final PassportTier passportTier; - /// 보유 POP(활동). + /// 총 보유 POP — 일반 + 기여 + 예약(G-05 `총 보유 = 예약 + 사용 가능`). final int balancePop; + /// 예약 — 검토 대기 보상처럼 묶여 있어 아직 쓸 수 없는 몫(서버 `pendingActivity`). + final int reservedPop; + + /// 사용 가능 = 총 보유 − 예약. + int get availablePop => balancePop - reservedPop; + /// 누적 걸음. 명세 `549:22058` 의 프로필 카드 항목. final int steps; final int completedQuests; diff --git a/app/lib/features/my/my_repository_firestore.dart b/app/lib/features/my/my_repository_firestore.dart index d90b42f..6ce0a6e 100644 --- a/app/lib/features/my/my_repository_firestore.dart +++ b/app/lib/features/my/my_repository_firestore.dart @@ -23,7 +23,7 @@ import '../../core/i18n/app_locale.dart'; /// - Passport `users/{uid}/passports/{fandomId}.tier` — **팬덤별**(10/3 #10 · 9/21 5절). 없으면 SILVER. /// 탈퇴한 팬덤의 Passport 도 지우지 않고 보인다(ARCHIVED). /// - Badge `users/{uid}/badges`(상세정의 4장 · 별첨 1 ②-5) -/// - 잔액 `users/{uid}/wallet/main.activityBalance` +/// - 잔액 `users/{uid}/wallet/main` — 사용 가능 = `activityBalance + supportBalance`, 예약 = `pendingActivity`(G-05) /// - 행성별 기여 `fandoms/{fandomId}` + `fandoms/{fandomId}/members/{uid}.contributedPop` /// - POP 원장 `users/{uid}/popTransactions` (최신순 · 커서 `createdAt`) /// - 공지 `notices` @@ -126,7 +126,8 @@ class FirestoreMyRepository extends SampleMyRepository { userExp: user.intOr('userExp'), userLevel: user.intOr('userLevel').clamp(1, 1 << 31), passportTier: primaryTier, - balancePop: wallet.intOr('activityBalance'), + balancePop: wallet.intOr('activityBalance') + wallet.intOr('supportBalance') + wallet.intOr('pendingActivity'), + reservedPop: wallet.intOr('pendingActivity'), steps: user.intOr('steps'), completedQuests: await completedF, planetTiers: tiers, diff --git a/app/lib/features/my/pop_history_screen.dart b/app/lib/features/my/pop_history_screen.dart index 85311fd..be06815 100644 --- a/app/lib/features/my/pop_history_screen.dart +++ b/app/lib/features/my/pop_history_screen.dart @@ -109,7 +109,7 @@ class _PopHistoryScreenState extends State { itemCount: 1 + (_entries.isEmpty ? 1 : _entries.length) + (_hasMore ? 1 : 0), itemBuilder: (context, i) { - if (i == 0) return _Balance(pop: profile.balancePop); + if (i == 0) return _Balance(profile: profile); i -= 1; if (_entries.isEmpty && i == 0) { return ColoredBox( @@ -148,11 +148,14 @@ class _PopHistoryScreenState extends State { } } -/// 보유 POP — 번개 20 + 「POP」 18/500 민트 · 값 36/900. +/// 보유 POP — 번개 20 + 「POP」 18/500 민트 · 값 36/900(명세 `554:23714` 현재 보유 POP = 총 보유). +/// +/// 그 아래 「총 보유 · 예약 · 사용 가능」 세 값(정책 설계서 잔액 구분 · G-05 · QA 2-29). +/// Figma 에는 이 줄이 없어 배치는 해석 확인 대상이다. class _Balance extends StatelessWidget { - const _Balance({required this.pop}); + const _Balance({required this.profile}); - final int pop; + final MyProfile profile; @override Widget build(BuildContext context) => Container( @@ -182,7 +185,7 @@ class _Balance extends StatelessWidget { child: OverflowBox( maxHeight: 48, child: Text( - formatPop(pop), + formatPop(profile.balancePop), style: const TextStyle( fontFamily: AppFonts.body, fontSize: 36, @@ -193,6 +196,25 @@ class _Balance extends StatelessWidget { ), ), ), + const SizedBox(height: 24), + Row( + children: [ + for (final (label, value) in [ + (context.l10n.popTotal, profile.balancePop), + (context.l10n.popReserved, profile.reservedPop), + (context.l10n.popAvailable, profile.availablePop), + ]) + Expanded( + child: Column( + children: [ + Text(label, style: myText(12, FontWeight.w500, color: MyColors.muted)), + const SizedBox(height: 4), + Text(formatPop(value), style: myText(14, FontWeight.w700)), + ], + ), + ), + ], + ), ], ), ); diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index db93f31..e0104a1 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -400,6 +400,9 @@ "refundReason": "Reason for refund", "refundConfirm": "Request a refund?", "myPop": "My POP", + "popTotal": "Total", + "popReserved": "Reserved", + "popAvailable": "Available", "commonNoHistory": "No history yet.", "nicknameEdit": "Edit nickname", "nicknameReserved": "You can't use a name that could be mistaken for staff.", @@ -861,7 +864,7 @@ "onbCountrySheetTitle": "Select your country/region of residence", "onbLanguage": "Display language", "onbReferral": "Referral code (optional)", - "onbReferralHint": "Enter a code if a friend shared one", + "onbReferralInvalid": "Please check the referral code.", "onbArtistTitle": "Choose the artists you follow", "onbArtistBody": "The first artist you pick becomes your main planet. You can join several fandoms.", "onbArtistSearch": "Search artist name", diff --git a/app/lib/l10n/app_es.arb b/app/lib/l10n/app_es.arb index f4a7e84..e7feccd 100644 --- a/app/lib/l10n/app_es.arb +++ b/app/lib/l10n/app_es.arb @@ -400,6 +400,9 @@ "refundReason": "Motivo del reembolso", "refundConfirm": "¿Seguro que quieres solicitar el reembolso?", "myPop": "Mi POP", + "popTotal": "Total", + "popReserved": "Reservado", + "popAvailable": "Disponible", "commonNoHistory": "Sin historial.", "nicknameEdit": "Editar apodo", "nicknameReserved": "No puedes usar un nombre que parezca del equipo.", @@ -861,7 +864,7 @@ "onbCountrySheetTitle": "Selecciona tu país/región de residencia", "onbLanguage": "Idioma de visualización", "onbReferral": "Código de referido (opcional)", - "onbReferralHint": "Introduce el código si un amigo te lo compartió", + "onbReferralInvalid": "Revisa el código de referido.", "onbArtistTitle": "Elige a los artistas que sigues", "onbArtistBody": "El primer artista que elijas será tu planeta principal. Puedes unirte a varios fandoms.", "onbArtistSearch": "Buscar nombre del artista", diff --git a/app/lib/l10n/app_ja.arb b/app/lib/l10n/app_ja.arb index f0825f2..5ab9edb 100644 --- a/app/lib/l10n/app_ja.arb +++ b/app/lib/l10n/app_ja.arb @@ -400,6 +400,9 @@ "refundReason": "返金理由", "refundConfirm": "本当に返金を申請しますか?", "myPop": "マイPOP", + "popTotal": "総保有", + "popReserved": "予約", + "popAvailable": "利用可能", "commonNoHistory": "履歴はありません。", "nicknameEdit": "ニックネーム変更", "nicknameReserved": "運営者と誤解される名前は使用できません。", @@ -861,7 +864,7 @@ "onbCountrySheetTitle": "居住国・地域を選択", "onbLanguage": "表示言語", "onbReferral": "紹介コード(任意)", - "onbReferralHint": "友だちからもらったコードがあれば入力してください", + "onbReferralInvalid": "紹介コードをご確認ください。", "onbArtistTitle": "好きなアーティストを選んでください", "onbArtistBody": "最初に選んだアーティストが基本の惑星になります。複数のファンダムに加入できます。", "onbArtistSearch": "アーティスト名で検索", diff --git a/app/lib/l10n/app_ko.arb b/app/lib/l10n/app_ko.arb index 16bcbf1..f9645de 100644 --- a/app/lib/l10n/app_ko.arb +++ b/app/lib/l10n/app_ko.arb @@ -827,6 +827,9 @@ "refundReason": "환불 사유", "refundConfirm": "정말 환불 신청을 할까요?", "myPop": "내 POP", + "popTotal": "총 보유", + "popReserved": "예약", + "popAvailable": "사용 가능", "commonNoHistory": "내역이 없습니다.", "nicknameEdit": "닉네임 수정", "nicknameReserved": "운영자로 오해될 수 있는 이름은 쓸 수 없습니다.", @@ -1804,7 +1807,7 @@ "onbCountrySheetTitle": "거주 국가/지역 선택", "onbLanguage": "표시 언어", "onbReferral": "추천인 코드 (선택)", - "onbReferralHint": "친구에게 받은 코드가 있으면 입력해 주세요", + "onbReferralInvalid": "추천인 코드를 확인해 주세요.", "onbArtistTitle": "관심 아티스트를 선택해 주세요", "onbArtistBody": "처음 고른 아티스트가 기본 행성이 됩니다. 여러 팬덤에 함께 가입할 수 있어요.", "onbArtistSearch": "아티스트 이름 검색", diff --git a/app/lib/l10n/app_localizations.dart b/app/lib/l10n/app_localizations.dart index 15d8718..ee2d3a3 100644 --- a/app/lib/l10n/app_localizations.dart +++ b/app/lib/l10n/app_localizations.dart @@ -2504,6 +2504,24 @@ abstract class AppLocalizations { /// **'내 POP'** String get myPop; + /// No description provided for @popTotal. + /// + /// In ko, this message translates to: + /// **'총 보유'** + String get popTotal; + + /// No description provided for @popReserved. + /// + /// In ko, this message translates to: + /// **'예약'** + String get popReserved; + + /// No description provided for @popAvailable. + /// + /// In ko, this message translates to: + /// **'사용 가능'** + String get popAvailable; + /// No description provided for @commonNoHistory. /// /// In ko, this message translates to: @@ -5282,11 +5300,11 @@ abstract class AppLocalizations { /// **'추천인 코드 (선택)'** String get onbReferral; - /// No description provided for @onbReferralHint. + /// No description provided for @onbReferralInvalid. /// /// In ko, this message translates to: - /// **'친구에게 받은 코드가 있으면 입력해 주세요'** - String get onbReferralHint; + /// **'추천인 코드를 확인해 주세요.'** + String get onbReferralInvalid; /// No description provided for @onbArtistTitle. /// diff --git a/app/lib/l10n/app_localizations_en.dart b/app/lib/l10n/app_localizations_en.dart index f09c3db..6aa6125 100644 --- a/app/lib/l10n/app_localizations_en.dart +++ b/app/lib/l10n/app_localizations_en.dart @@ -1398,6 +1398,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get myPop => 'My POP'; + @override + String get popTotal => 'Total'; + + @override + String get popReserved => 'Reserved'; + + @override + String get popAvailable => 'Available'; + @override String get commonNoHistory => 'No history yet.'; @@ -3007,7 +3016,7 @@ class AppLocalizationsEn extends AppLocalizations { String get onbReferral => 'Referral code (optional)'; @override - String get onbReferralHint => 'Enter a code if a friend shared one'; + String get onbReferralInvalid => 'Please check the referral code.'; @override String get onbArtistTitle => 'Choose the artists you follow'; diff --git a/app/lib/l10n/app_localizations_es.dart b/app/lib/l10n/app_localizations_es.dart index 40d2b68..a295a2e 100644 --- a/app/lib/l10n/app_localizations_es.dart +++ b/app/lib/l10n/app_localizations_es.dart @@ -1406,6 +1406,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get myPop => 'Mi POP'; + @override + String get popTotal => 'Total'; + + @override + String get popReserved => 'Reservado'; + + @override + String get popAvailable => 'Disponible'; + @override String get commonNoHistory => 'Sin historial.'; @@ -3022,8 +3031,7 @@ class AppLocalizationsEs extends AppLocalizations { String get onbReferral => 'Código de referido (opcional)'; @override - String get onbReferralHint => - 'Introduce el código si un amigo te lo compartió'; + String get onbReferralInvalid => 'Revisa el código de referido.'; @override String get onbArtistTitle => 'Elige a los artistas que sigues'; diff --git a/app/lib/l10n/app_localizations_ja.dart b/app/lib/l10n/app_localizations_ja.dart index 2a11f67..a727201 100644 --- a/app/lib/l10n/app_localizations_ja.dart +++ b/app/lib/l10n/app_localizations_ja.dart @@ -1351,6 +1351,15 @@ class AppLocalizationsJa extends AppLocalizations { @override String get myPop => 'マイPOP'; + @override + String get popTotal => '総保有'; + + @override + String get popReserved => '予約'; + + @override + String get popAvailable => '利用可能'; + @override String get commonNoHistory => '履歴はありません。'; @@ -2906,7 +2915,7 @@ class AppLocalizationsJa extends AppLocalizations { String get onbReferral => '紹介コード(任意)'; @override - String get onbReferralHint => '友だちからもらったコードがあれば入力してください'; + String get onbReferralInvalid => '紹介コードをご確認ください。'; @override String get onbArtistTitle => '好きなアーティストを選んでください'; diff --git a/app/lib/l10n/app_localizations_ko.dart b/app/lib/l10n/app_localizations_ko.dart index 73aa0fc..db7595c 100644 --- a/app/lib/l10n/app_localizations_ko.dart +++ b/app/lib/l10n/app_localizations_ko.dart @@ -1350,6 +1350,15 @@ class AppLocalizationsKo extends AppLocalizations { @override String get myPop => '내 POP'; + @override + String get popTotal => '총 보유'; + + @override + String get popReserved => '예약'; + + @override + String get popAvailable => '사용 가능'; + @override String get commonNoHistory => '내역이 없습니다.'; @@ -2909,7 +2918,7 @@ class AppLocalizationsKo extends AppLocalizations { String get onbReferral => '추천인 코드 (선택)'; @override - String get onbReferralHint => '친구에게 받은 코드가 있으면 입력해 주세요'; + String get onbReferralInvalid => '추천인 코드를 확인해 주세요.'; @override String get onbArtistTitle => '관심 아티스트를 선택해 주세요'; diff --git a/app/lib/l10n/app_localizations_zh.dart b/app/lib/l10n/app_localizations_zh.dart index 6d869ef..715f9cc 100644 --- a/app/lib/l10n/app_localizations_zh.dart +++ b/app/lib/l10n/app_localizations_zh.dart @@ -1349,6 +1349,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get myPop => '我的 POP'; + @override + String get popTotal => '总持有'; + + @override + String get popReserved => '预留'; + + @override + String get popAvailable => '可用'; + @override String get commonNoHistory => '暂无记录。'; @@ -2901,7 +2910,7 @@ class AppLocalizationsZh extends AppLocalizations { String get onbReferral => '推荐码(选填)'; @override - String get onbReferralHint => '如有朋友分享的推荐码请输入'; + String get onbReferralInvalid => '请确认推荐码。'; @override String get onbArtistTitle => '请选择关注的艺人'; diff --git a/app/lib/l10n/app_zh.arb b/app/lib/l10n/app_zh.arb index 15071ef..c9779fd 100644 --- a/app/lib/l10n/app_zh.arb +++ b/app/lib/l10n/app_zh.arb @@ -400,6 +400,9 @@ "refundReason": "退款原因", "refundConfirm": "确定要申请退款吗?", "myPop": "我的 POP", + "popTotal": "总持有", + "popReserved": "预留", + "popAvailable": "可用", "commonNoHistory": "暂无记录。", "nicknameEdit": "修改昵称", "nicknameReserved": "不能使用可能被误认为运营方的名称。", @@ -861,7 +864,7 @@ "onbCountrySheetTitle": "选择居住国家/地区", "onbLanguage": "显示语言", "onbReferral": "推荐码(选填)", - "onbReferralHint": "如有朋友分享的推荐码请输入", + "onbReferralInvalid": "请确认推荐码。", "onbArtistTitle": "请选择关注的艺人", "onbArtistBody": "第一个选择的艺人将成为默认星球。可以同时加入多个粉丝团。", "onbArtistSearch": "搜索艺人名称", diff --git a/app/lib/l10n/remote_app_localizations.dart b/app/lib/l10n/remote_app_localizations.dart index 019a674..631f14d 100644 --- a/app/lib/l10n/remote_app_localizations.dart +++ b/app/lib/l10n/remote_app_localizations.dart @@ -1466,7 +1466,7 @@ class RemoteAppLocalizations extends AppLocalizations { String get onbReferral => _remote['onbReferral'] ?? _base.onbReferral; @override - String get onbReferralHint => _remote['onbReferralHint'] ?? _base.onbReferralHint; + String get onbReferralInvalid => _remote['onbReferralInvalid'] ?? _base.onbReferralInvalid; @override String get onbSkip => _remote['onbSkip'] ?? _base.onbSkip; @@ -1687,6 +1687,9 @@ class RemoteAppLocalizations extends AppLocalizations { @override String get playNow => _remote['playNow'] ?? _base.playNow; + @override + String get popAvailable => _remote['popAvailable'] ?? _base.popAvailable; + @override String get popKindContribution => _remote['popKindContribution'] ?? _base.popKindContribution; @@ -1783,6 +1786,12 @@ class RemoteAppLocalizations extends AppLocalizations { @override String popRepeat(String title, String count) => _fill('popRepeat', {'title': title, 'count': count}) ?? _base.popRepeat(title, count); + @override + String get popReserved => _remote['popReserved'] ?? _base.popReserved; + + @override + String get popTotal => _remote['popTotal'] ?? _base.popTotal; + @override String get popTxGrant => _remote['popTxGrant'] ?? _base.popTxGrant; diff --git a/app/test/backend_repository_test.dart b/app/test/backend_repository_test.dart index 578c7c2..24b39dc 100644 --- a/app/test/backend_repository_test.dart +++ b/app/test/backend_repository_test.dart @@ -805,7 +805,17 @@ void main() { expect([left.joined, left.passportTier, left.contributedPop, left.fandomName], [false, PassportTier.diamond, 700, '떠난팬덤']); expect(profile.badges.single.label, 'HEALTH · 2/7'); - expect(profile.balancePop, 12540); + // 총 보유 = 일반 12,540 + 기여 300 + 예약 0(G-05). + expect(profile.balancePop, 12840); + }); + + test('잔액 3구분 — 총 보유 = 예약 + 사용 가능(G-05 · QA 2-29)', () async { + final db = await _seed(); + await db.doc('users/$_uid/wallet/main').set({ + 'activityBalance': 1000, 'supportBalance': 200, 'pendingActivity': 50, + }); + final profile = await FirestoreMyRepository(_backend(db)).profile(); + expect([profile.balancePop, profile.reservedPop, profile.availablePop], [1250, 50, 1200]); }); test('레벨 필드가 없으면 Lv.1 · SILVER 로 둔다', () async { diff --git a/app/test/f6_screens_test.dart b/app/test/f6_screens_test.dart index 39d37ec..a3b4177 100644 --- a/app/test/f6_screens_test.dart +++ b/app/test/f6_screens_test.dart @@ -656,6 +656,10 @@ void main() { expect(find.text('12,540'), findsWidgets); expect(find.text('+50'), findsOneWidget); expect(find.text('-500'), findsOneWidget); + // 총 보유 · 예약 · 사용 가능 세 값(G-05). + expect(find.text('총 보유'), findsOneWidget); + expect(find.text('예약'), findsOneWidget); + expect(find.text('사용 가능'), findsOneWidget); }); testWidgets('Zap 은 건별이 아니라 당일 합산 한 줄이다', (tester) async { diff --git a/app/test/onboarding_test.dart b/app/test/onboarding_test.dart index 9f77c5b..6dbf7de 100644 --- a/app/test/onboarding_test.dart +++ b/app/test/onboarding_test.dart @@ -206,6 +206,54 @@ void main() { expect(find.byType(BackButton), findsOneWidget); // ③ 프로필로 돌아왔다. }); + testWidgets('추천인 코드(C-119) — 귀속 뒤 돌아와도 다시 부르지 않고, 다른 코드는 511:86845 문구로 거절', ( + tester, + ) async { + final (auth, repo) = await boot(tester, needs: true); + await auth.signIn(SocialProvider.google); + await tester.pumpAndSettle(); + for (final label in [ + '서비스 이용약관 (필수)', + '개인정보 수집·이용 및 처리방침 동의 (필수)', + '위치기반 서비스 이용약관 (필수)', + '만 14세 이상입니다. (필수)', + ]) { + await tester.tap(find.text(label)); + } + await tester.pump(); + await tester.tap(find.text('완료')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField).first, '우주팬'); + await tester.enterText(find.byType(TextField).last, 'abcdefghjk'); + await tester.pump(); + await tester.tap(find.text('다음')); + await tester.pumpAndSettle(); + expect(repo.referral, 'ABCDEFGHJK'); + + // ④ → ③ : 귀속한 코드가 채워져 있고 그대로 「다음」이면 통과(두 번째 호출 없음). + await tester.tap(find.byType(BackButton)); + await tester.pumpAndSettle(); + expect(find.text('ABCDEFGHJK'), findsOneWidget); + // 목 저장소는 저장한 닉네임을 돌려주지 않는다 — 다시 넣는다. + await tester.enterText(find.byType(TextField).first, '우주팬'); + await tester.pump(); + await tester.tap(find.text('다음')); + await tester.pumpAndSettle(); + expect(find.text('NewJeans'), findsOneWidget); // ④ 로 넘어갔다. + + // 다른 코드로 바꾸면 서버가 1인 1회로 거절 → Figma 오류 문구, 단계는 그대로. + await tester.tap(find.byType(BackButton)); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField).first, '우주팬'); + await tester.enterText(find.byType(TextField).last, 'ZZZZZZZZZZ'); + await tester.pump(); + await tester.tap(find.text('다음')); + await tester.pumpAndSettle(); + expect(find.text('추천인 코드를 확인해 주세요.'), findsOneWidget); + expect(repo.referral, 'ABCDEFGHJK'); + }); + testWidgets('가입 모드에서 받은 약관은 다시 묻지 않고 기록만 한다', (tester) async { final (auth, repo) = await boot(tester, needs: true); auth.pendingConsent = {