guricode

[flutter-sns-project - 11]flutter 앱 배포 준비,CheckboxListTile,WebView 본문

앱/Flutter&Dart

[flutter-sns-project - 11]flutter 앱 배포 준비,CheckboxListTile,WebView

agentrakugaki 2025. 9. 8. 19:02

구글에 배포하기 위해 몇가지 추가가 필요했다

 

일단 회원가입할떄 이용약관과 개인정보 처리방침에 동의를 받아야한다.

 

그런데 이미 이메일 회원가입과 구글 로그인/회원가입이 다 짜여져있어서 분기처리를 해야한다.

 

그래도 이용약관 페이지를 만들어야하기때문에 유저에게 보이는 signup_agreement.dart파일을 만들어줬다

// signup_agreement.dart
import 'package:flutter/material.dart';
import 'package:flutter_princess/presentation/policy/policy_web_view.dart';

const termsUrl =
    'https://fate-friend-339.notion.site/Goal-Mate-268e795aec4c806fa80ef443feb6eac1';

class SignupAgreement extends StatefulWidget {
  final VoidCallback onAgreed;
  const SignupAgreement({super.key, required this.onAgreed});

  @override
  State<SignupAgreement> createState() => _SignupAgreementState();
}

class _SignupAgreementState extends State<SignupAgreement> {
  bool agreeTerms = false;

  void _open(BuildContext ctx, String url, String title) {
    Navigator.push(
      ctx,
      MaterialPageRoute(
        builder: (_) => PolicyWebViewPage(url: url, title: title),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(automaticallyImplyLeading: false),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Column(
              children: [
                Align(
                  alignment: Alignment.centerLeft,
                  child: TextButton(
                    onPressed: () =>
                        _open(context, termsUrl, '이용약관 · 개인정보 처리방침'),
                    child: const Text('전체 화면으로 보기'),
                  ),
                ),
                SizedBox(
                  height: 400,
                  child: PolicyWebViewPage(
                    url: termsUrl,
                    title: '이용약관 · 개인정보 처리방침',
                  ),
                ),
                // TextButton(
                //   onPressed: () => _open(context, termsUrl, '이용약관,개인정보 처리방침'),
                //   child: const Text('이용약관 보기'),
                // ),
              ],
            ),
            CheckboxListTile(
              dense: true,
              contentPadding: EdgeInsets.zero,
              title: const Text('[필수] 이용약관,개인정보 처리방침에 동의합니다'),
              value: agreeTerms,
              onChanged: (v) => setState(() => agreeTerms = v ?? false),
            ),
            const SizedBox(height: 12),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {
                  if (!agreeTerms) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('필수 약관에 모두 동의해 주세요')),
                    );
                    return;
                  }
                  widget.onAgreed();
                },
                child: const Text('동의하고 회원가입'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

CheckboxListTile = ListTile + Checkbox. 리스트에서 동의·설정 토글에 최적. Material 위젯 트리(Scaffold 등) 안에서만 사용

원래는 스크롤러( SingleChildScrollView나 ListView 같은)로 감싸야하지만 화면을 넘어가지 않아서 그냥 사용했다

자주 쓰는 옵션

  • value: 현재 체크 상태. bool.
  • onChanged: 변경 콜백. null이면 비활성화.
  • title: 주 텍스트. Widget(보통 Text).
  • subtitle: 보조 텍스트. 작은 설명 붙일 때.
  • secondary: 왼쪽(또는 오른쪽) 아이콘 영역. 예: Icon(Icons.lock).
  • controlAffinity: 체크박스 위치.
    • ListTileControlAffinity.leading(왼쪽) / trailing(오른쪽) / platform.
  • contentPadding: 좌우 여백. EdgeInsets.zero로 딱 붙이기 가능.
  • dense: 높이를 낮춰 조밀하게 표시. 목록을 컴팩트하게.
  • isThreeLine: 세 줄 높이 강제. subtitle 길 때.
  • selected: 선택 스타일 강조. 텍스트 색이 테마에 따라 바뀜.
  • activeColor: 체크박스 활성 색상(테마 우선).
  • checkColor: 체크마크 색.
  • tileColor / selectedTileColor: 타일 배경색(평상시/선택시).
  • shape / side: 타일 테두리와 모양.
  • visualDensity: 높이·가로 밀도 미세 조정.
  • autofocus, enableFeedback: 포커스, 햅틱/사운드 피드백 제어.

 

그리고 policy_web_view로 노션에 작성한 이용약관을 보여주도록했다

// policy_webview_page.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class PolicyWebViewPage extends StatefulWidget {
  final String url;
  final String title;
  const PolicyWebViewPage({super.key, required this.url, required this.title});

  @override
  State<PolicyWebViewPage> createState() => _PolicyWebViewPageState();
}

class _PolicyWebViewPageState extends State<PolicyWebViewPage> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(const Color(0x00000000))
      ..loadRequest(Uri.parse(widget.url));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: WebViewWidget(controller: _controller),
    );
  }
}

 

웹뷰에 대해서 포스팅을 안했는데 

웹뷰는 URL을 냅 내부 위젯으로 띄우는 화면이다

WebViewController로 설정해서 WebViewWidget에 연결한다.

WebViewWidget은 스크롤이 자동 제공된다..얼마나 좋은가

 

initstate에서 초기화해서 사용하는데 사용된 옵션은 이렇다

_controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted) // JS 허용(필요 시 only)
  ..setBackgroundColor(const Color(0x00000000))    // 배경 투명(디폴트 흰색)
  ..loadRequest(Uri.parse(widget.url));            // 페이지 로드

JavaScriptMode.unrestricted: Notion 같은 사이트는 JS 필요. 보안상 최소화하려면 가능하면 disabled.

loadRequest: 문자열 URL → Uri 파싱 후 로드.

 

그리고 네트워크 권한을 추가해준다

android/ app /src /main /AndroidManifest에  
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> 밑에
<uses-permission android:name="android.permission.INTERNET"/> 추가해주면된다

 

그리고 이메일 로그인과 구글 로그인 분기처리가 필요해서 Gorouter를 두개를 작성했따

   GoRoute(
      path: '/policy', // 결과 반환용(구글로그인에서 사용)
      name: 'policy',
      builder: (context, state) => SignupAgreement(
        onAgreed: () => Navigator.pop(context, true), // bool 반환
      ),
    ),
    GoRoute(
      path: '/policyToSignup', // 회원가입 버튼에서 사용
      name: 'policyToSignup',
      builder: (context, state) => SignupAgreement(
        onAgreed: () => context.go('/signup'), // 동의 후 회원가입 화면으로
      ),
    ),

 

 

 

구글 로그인은 구글 회원가입 후 약관동의 여부를 bool값으로 되돌려받아 동의하면 true를 받아와서 

firebase에 agreeat의 날짜를 추가하도록 구성했다.

동의하지않으면 로그아웃되게 구성해서 이전에 만든 로그인/회원가입 코드를 건들지 않도록했다

 

Future<bool> googleLogin(BuildContext context) async {
    final usecase = ref.read(googleLoginUsecaseProvider);

    try {
      // 1) 구글 로그인
      final user = await usecase.execute();
      if (user == null) return false;

      final uid = user.uid;

      // 2) 동의 이력 확인
      final users = FirebaseFirestore.instance.collection('user');
      final snap = await users.doc(uid).get();
      final hasAgreement = snap.exists && (snap.data()?['agreedAt'] != null);

      // 3) 약관 동의 요구
      if (!hasAgreement) {
        final agreed =
            await GoRouter.of(context).push<bool>('/policy') ?? false;
        if (agreed != true) {
          await FirebaseAuth.instance.signOut();
          return false;
        }
        await users.doc(uid).set({
          'uid': uid,
          'agreedAt': FieldValue.serverTimestamp(),
        }, SetOptions(merge: true));
      }

      // 4) 상태 갱신
      state = UserState(
        uid: user.uid,
        email: user.email,
        profileImgUrl: user.profileImgUrl ?? '',
        userNickname: user.userNickname,
      );
      return true;
    } catch (e) {
      debugPrint('구글 로그인 실패: $e');
      return false;
    }
  }

 

 

이제 패키지명을 배포용으로 바꾸고  앱 아이콘을 만들어야한다

안드로이드 배포할떄 패키지명 바꿔야하는데 위치들 참고링크
https://itwise.tistory.com/47

 

앱아이콘 만들어야하는데  flutter_launcher_icons로 추가해서 사용하거나
다른방법은 여기링크를 이용한다
https://blog.dglee.co.kr/entry/Flutter-%EC%95%B1-%EC%95%84%EC%9D%B4%EC%BD%98%EC%9D%84-%EB%82%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%B4%EB%B3%B4%EC%9E%90

패키지명을 바꾸면 앱등록 새로 해야한다, 이때 키스토어는그대로써도된다

 

플러터로 개발한 앱 플레이스토어 배포용 APK 생성하기 (패키지 이름 변경, 키 서명, 프로가드, 앱

미소닭갈비 가게를 소개하는 앱을 플러터로 개발했다. 이제 개발한 앱을 구글 플레이스토어에 등록하기 위해 배포용 APK를 만들어야 한다. ​ ​ 패키지(Package) 이름 변경 ​ 우선 패키지 이름부

itwise.tistory.com

 

설정이 끝났으니 flutter build aab로 빌드해서 구글개발계정 가진 팀원에게 전달하자...