| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
- 자바스크립트
- firebase
- npm
- println
- ListView
- JS
- Clean Architecture
- printf
- react
- LLM
- java 콘솔 출력 차이
- java
- 앱심사
- java 출력
- UI/UX
- lifecycle
- Flutter
- DART
- unity
- nodejs
- 엡
- 단축키
- 자바 포맷 출력
- abap
- 배포
- 자바 출력 방식
- JQ
- riverpod
- scss
- develop
- Today
- Total
guricode
[flutter-sns-project - 11]flutter 앱 배포 준비,CheckboxListTile,WebView 본문
[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로 빌드해서 구글개발계정 가진 팀원에게 전달하자...
'앱 > Flutter&Dart' 카테고리의 다른 글
| [Flutter]자취의 정석 1 - 기획 및 컨셉(클린아키텍쳐MVVM, Jira) (0) | 2025.09.10 |
|---|---|
| [flutter]Hero 애니메이션 (0) | 2025.09.09 |
| [flutter-sns-project - 10]안드로이드 릴리즈 단계 요약 (0) | 2025.09.05 |
| [flutter-sns-project - 9] 트러블슈팅 - 구글로그인 중간에 화면 나갈시 그대로 홈 화면으로 진입되는 문제 (0) | 2025.09.04 |
| [flutter-sns-project - 8] Flutter 앱에 Sentry 연동하기: 설치부터 초기화까지 (0) | 2025.09.04 |