가입 약관 동의 기록 · 서비스 탈퇴 · 온보딩 6단계 · Coach Mark

This commit is contained in:
Sn-Kinos
2026-10-05 22:48:25 +09:00
parent e7010010bb
commit 55424bd4a2
36 changed files with 2884 additions and 77 deletions
+28 -8
View File
@@ -5,19 +5,34 @@ import '../../core/theme/tokens.dart';
/// 회원가입 약관 — Figma `179:24230`(미동의) · `179:24324`(전체 동의).
///
/// 필수 3항(서비스 · 위치기반 · 만 14세)을 모두 체크해야 「완료」가 켜진다. 완료하면 `true` 를
/// 돌려주고, 부른 쪽([SimpleLoginScreen] 가입 모드)이 제공자 인증으로 넘어간다.
/// 필수 4항을 모두 체크해야 「완료」가 켜진다. 완료하면 동의한 항목 키([signupTermKeys] 중)를
/// 돌려준다 — 가입 모드([SimpleLoginScreen])는 `pop` 결과로, 온보딩은 [onAgreed] 로 받는다.
/// 서버(`fnSignupConsent`)가 같은 필수 목록을 다시 검사하고 항목별 동의 행을 남긴다.
///
/// 약관 본문은 시안의 자리 표시 문구다. 확정 약관이 오면 [_body] 만 바꾼다.
/// 필수 항목은 Figma 의 서비스 · 위치기반 · 만 14세에 개인정보 처리방침을 더했다(10/3 통합회신
/// 3절 4 「이용약관 · 개인정보처리방침 문구 제공」). 구성은 해석 확인 요청 E-Q1 회신 대기다.
/// 약관 본문은 발주사 실서비스 문구를 받기 전까지 시안의 자리 표시 문구다 — 받으면 [_body] 와
/// [signupTermsVersion] 을 함께 바꾼다(동의 기록에 버전이 남는다).
class TermsScreen extends StatefulWidget {
const TermsScreen({super.key});
const TermsScreen({super.key, this.onAgreed, this.onBack});
/// 온보딩 안에서 쓸 때 — 「완료」가 화면을 닫지 않고 이걸 부른다.
final ValueChanged<Set<String>>? onAgreed;
final VoidCallback? onBack;
@override
State<TermsScreen> createState() => _TermsScreenState();
}
/// 동의 기록에 남는 약관 버전. 🟡 임시 — 본문이 자리 표시 문구인 동안은 `draft` 다.
const signupTermsVersion = 'draft-1';
/// 서버 `SIGNUP_REQUIRED_TERMS` + 선택 `marketing` 과 같은 키.
List<String> get signupTermKeys => [for (final t in _Term.values) t.name];
enum _Term {
service(required: true, body: true),
privacy(required: true, body: true),
location(required: true, body: true),
age(required: true, body: false),
marketing(required: false, body: true);
@@ -29,6 +44,7 @@ enum _Term {
String label(AppLocalizations l10n) => switch (this) {
service => l10n.termsService,
privacy => l10n.termsPrivacy,
location => l10n.termsLocation,
age => l10n.termsAge,
marketing => l10n.termsMarketing,
@@ -48,6 +64,12 @@ class _TermsScreenState extends State<TermsScreen> {
bool get _ready =>
_Term.values.where((t) => t.required).every(_agreed.contains);
void _done() {
final agreed = {for (final t in _agreed) t.name};
final cb = widget.onAgreed;
cb != null ? cb(agreed) : Navigator.of(context).pop(agreed);
}
void _toggle(_Term t) =>
setState(() => _agreed.contains(t) ? _agreed.remove(t) : _agreed.add(t));
@@ -120,7 +142,7 @@ class _TermsScreenState extends State<TermsScreen> {
children: [
Expanded(
child: FilledButton(
onPressed: () => Navigator.of(context).pop(false),
onPressed: widget.onBack ?? () => Navigator.of(context).pop(),
style: FilledButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: AppColors.onCosmic,
@@ -131,9 +153,7 @@ class _TermsScreenState extends State<TermsScreen> {
const SizedBox(width: 10),
Expanded(
child: FilledButton(
onPressed: _ready
? () => Navigator.of(context).pop(true)
: null,
onPressed: _ready ? _done : null,
style: FilledButton.styleFrom(
backgroundColor: AppColors.cosmicAccent,
foregroundColor: AppColors.onCosmicAccent,