From 7278c6e06e6d0eb191bbb22196c809a240773beb Mon Sep 17 00:00:00 2001 From: eld_master Date: Wed, 22 Jan 2025 19:51:36 +0900 Subject: [PATCH] =?UTF-8?q?1=EC=B0=A8=20=EC=B5=9C=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 4 +- lib/config/config.dart | 1 + lib/dialogs/score_edit_dialog.dart | 32 +- lib/dialogs/team_name_edit_dialog.dart | 4 +- lib/dialogs/user_info_basic_dialog.dart | 2 +- lib/dialogs/user_info_private_dialog.dart | 61 +- lib/dialogs/user_info_team_dialog.dart | 71 +- lib/plugins/admob.dart | 85 ++ lib/survey/survey_page.dart | 2 +- lib/views/login/id_finding_page.dart | 36 +- lib/views/login/login_page.dart | 2 +- lib/views/login/pw_finding_page.dart | 36 +- lib/views/login/signup_page.dart | 3 +- lib/views/room/main_page.dart | 43 +- lib/views/room/playing_private_page.dart | 65 +- lib/views/room/playing_team_page.dart | 60 +- lib/views/room/room_search_home_page.dart | 56 +- lib/views/room/room_search_list_page.dart | 42 +- lib/views/room/waiting_room_private_page.dart | 92 +- lib/views/room/waiting_room_team_page.dart | 94 +- lib/views/user/my_page.dart | 1200 +++++++++-------- lib/views/user/withdrawal_page.dart | 201 +-- pubspec.lock | 8 + pubspec.yaml | 3 +- 24 files changed, 1143 insertions(+), 1060 deletions(-) create mode 100644 lib/plugins/admob.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 9b7d7c8..6deef19 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId "com.allscore_app" minSdkVersion 23 targetSdkVersion 34 - versionCode 1 - versionName "1.0" + versionCode 4 + versionName "1.0.3" } // ... diff --git a/lib/config/config.dart b/lib/config/config.dart index a177ef3..7c80335 100644 --- a/lib/config/config.dart +++ b/lib/config/config.dart @@ -5,6 +5,7 @@ class Config { static const String realAdUnitId = 'ca-app-pub-6461991944599918~9492697896'; // 테스트 광고 단위 ID static const String adUnitId = 'ca-app-pub-6461991944599918~9492697896'; + // static const String adUnitId = 'ca-app-pub-3940256099942544/6300978111'; // 서버 주소 static const String baseUrl = 'https://eldsoft.com:8097'; // 이미지 업로드 주소 diff --git a/lib/dialogs/score_edit_dialog.dart b/lib/dialogs/score_edit_dialog.dart index 8437d1e..3052eb6 100644 --- a/lib/dialogs/score_edit_dialog.dart +++ b/lib/dialogs/score_edit_dialog.dart @@ -64,8 +64,9 @@ class _ScoreEditDialogState extends State { void _onDelta(int delta) { setState(() { newScore += delta; - if (newScore < 0) newScore = 0; // 최소 0점이라고 가정 + // if (newScore < 0) newScore = 0; // 최소 0점이라고 가정 if (newScore > 999999) newScore = 999999; // 임의 최대치 + if (newScore < -999999) newScore = -999999; // 임의 최소치 }); } @@ -122,17 +123,24 @@ class _ScoreEditDialogState extends State { ], ), const SizedBox(height: 8), - Wrap( - spacing: 4, - runSpacing: 4, - children: [ - _buildDeltaButton(-100), - _buildDeltaButton(-10), - _buildDeltaButton(-1), - _buildDeltaButton(1), - _buildDeltaButton(10), - _buildDeltaButton(100), - ], + SizedBox( + // height: 120, // 그리드 높이 지정 + child: GridView.count( + crossAxisCount: 2, // 열 3개 + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 2.0, // 가로:세로 비율 + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + _buildDeltaButton(-100), + _buildDeltaButton(100), + _buildDeltaButton(-10), + _buildDeltaButton(10), + _buildDeltaButton(-1), + _buildDeltaButton(1), + ], + ), ), const SizedBox(height: 12), diff --git a/lib/dialogs/team_name_edit_dialog.dart b/lib/dialogs/team_name_edit_dialog.dart index 9db08b9..087a404 100644 --- a/lib/dialogs/team_name_edit_dialog.dart +++ b/lib/dialogs/team_name_edit_dialog.dart @@ -121,7 +121,7 @@ class _TeamNameEditModalState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ SizedBox( - width: 100, + width: 80, child: ElevatedButton( onPressed: _onUpdateTeamName, style: ElevatedButton.styleFrom(backgroundColor: Colors.black), @@ -130,7 +130,7 @@ class _TeamNameEditModalState extends State { ), ), SizedBox( - width: 100, + width: 80, child: ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom(backgroundColor: Colors.black), diff --git a/lib/dialogs/user_info_basic_dialog.dart b/lib/dialogs/user_info_basic_dialog.dart index 90ee8cf..d51296f 100644 --- a/lib/dialogs/user_info_basic_dialog.dart +++ b/lib/dialogs/user_info_basic_dialog.dart @@ -23,7 +23,7 @@ class UserInfoBasicDialog extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text('유저 정보 (진행중-개인전)', + const Text('유저 정보', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 12), diff --git a/lib/dialogs/user_info_private_dialog.dart b/lib/dialogs/user_info_private_dialog.dart index 5a73474..b571972 100644 --- a/lib/dialogs/user_info_private_dialog.dart +++ b/lib/dialogs/user_info_private_dialog.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'response_dialog.dart'; import '../../plugins/api.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:auto_size_text/auto_size_text.dart'; class UserInfoPrivateDialog extends StatefulWidget { final Map userData; @@ -24,6 +26,10 @@ class _UserInfoPrivateDialogState extends State { late String participantType; // 'ADMIN' or 'PLAYER' late String introduceMyself; + // 화면 크기에 따라 폰트 크기 조절 + double scaleFactor = 1.0; + double buttonScaleFactor = 1.0; + @override void initState() { super.initState(); @@ -32,6 +38,22 @@ class _UserInfoPrivateDialogState extends State { introduceMyself = widget.userData['introduce_myself'] ?? ''; } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateScaleFactor(); + } + + // 화면 크기에 따라 폰트 크기 조절 + void _updateScaleFactor() { + final screenWidth = MediaQuery.of(context).size.width; + const baseWidth = 450.0; + setState(() { + scaleFactor = (screenWidth / baseWidth).clamp(0.8, 1.2); + buttonScaleFactor = (screenWidth / baseWidth).clamp(0.6, 1.2); + }); + } + /// 역할 변경 API (방장 전용) Future _onUpdateUserInfo() async { // 방장만 수정하기 가능 @@ -71,6 +93,13 @@ class _UserInfoPrivateDialogState extends State { Future _onKickParticipant() async { // 방장이 아닌데 추방 시도 -> 그냥 return if (!widget.isRoomMaster) return; + // 나 자신은 추방 불가 + final prefs = await SharedPreferences.getInstance(); + final mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0'; + if (widget.userData['user_seq'] == mySeq) { + await showResponseDialog(context, '오류', '방장은 추방할 수 없습니다.'); + return; + } final reqBody = { "room_seq": "${widget.roomSeq}", @@ -116,7 +145,7 @@ class _UserInfoPrivateDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ const Text( - '유저 정보 (개인전)', + '유저 정보', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 12), @@ -224,34 +253,40 @@ class _UserInfoPrivateDialogState extends State { children: [ // (D-1) 역할 수정하기 SizedBox( - width: 90, + width: scaleFactor==0.8 ? 50 : 90, child: ElevatedButton( onPressed: _onUpdateUserInfo, style: ElevatedButton.styleFrom(backgroundColor: Colors.black), - child: FittedBox( - child: Text('수정하기', style: TextStyle(color: Colors.white)), + child: AutoSizeText( + scaleFactor==0.8 ? '수정' : '수정하기', + maxLines: 1, + style: TextStyle(color: Colors.white), ), ), ), // (D-2) 추방하기 SizedBox( - width: 90, + width: scaleFactor==0.8 ? 50 : 90, child: ElevatedButton( onPressed: _onKickParticipant, style: ElevatedButton.styleFrom(backgroundColor: Colors.black), - child: FittedBox( - child: Text('추방하기', style: TextStyle(color: Colors.white)), + child: AutoSizeText( + scaleFactor==0.8 ? '추방' : '추방하기', + maxLines: 1, + style: TextStyle(color: Colors.white), ), ), ), // (D-3) 확인 SizedBox( - width: 90, + width: scaleFactor==0.8 ? 50 : 90, child: ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom(backgroundColor: Colors.black), - child: FittedBox( - child: Text('확인', style: TextStyle(color: Colors.white)), + child: AutoSizeText( + '확인', + maxLines: 1, + style: TextStyle(color: Colors.white), ), ), ), @@ -261,7 +296,11 @@ class _UserInfoPrivateDialogState extends State { ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom(backgroundColor: Colors.black), - child: const Text('확인', style: TextStyle(color: Colors.white)), + child: AutoSizeText( + '확인', + maxLines: 1, + style: TextStyle(color: Colors.white), + ), ), ], ], diff --git a/lib/dialogs/user_info_team_dialog.dart b/lib/dialogs/user_info_team_dialog.dart index c65d197..ce6e376 100644 --- a/lib/dialogs/user_info_team_dialog.dart +++ b/lib/dialogs/user_info_team_dialog.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'response_dialog.dart'; import '../../plugins/api.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:auto_size_text/auto_size_text.dart'; class UserInfoTeamDialog extends StatefulWidget { final Map userData; @@ -27,6 +29,10 @@ class _UserInfoTeamDialogState extends State { late String teamName; // 'A'/'B'/'WAIT' late String introduceMyself; // 유저 소개 + // 화면 크기에 따라 폰트 크기 조절 + double scaleFactor = 1.0; + double buttonScaleFactor = 1.0; + @override void initState() { super.initState(); @@ -45,6 +51,22 @@ class _UserInfoTeamDialogState extends State { } } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateScaleFactor(); + } + + // 화면 크기에 따라 폰트 크기 조절 + void _updateScaleFactor() { + final screenWidth = MediaQuery.of(context).size.width; + const baseWidth = 450.0; + setState(() { + scaleFactor = (screenWidth / baseWidth).clamp(0.8, 1.2); + buttonScaleFactor = (screenWidth / baseWidth).clamp(0.6, 1.2); + }); + } + // (1) 역할/팀 수정 API (기존 코드) Future _onUpdateUserInfo() async { // 방장이 아닌데 수정 시도 -> 그냥 return @@ -85,6 +107,13 @@ class _UserInfoTeamDialogState extends State { Future _onKickParticipant() async { // 방장이 아니면 리턴 if (!widget.isRoomMaster) return; + // 나 자신은 추방 불가 + final prefs = await SharedPreferences.getInstance(); + final mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0'; + if (widget.userData['user_seq'] == mySeq) { + await showResponseDialog(context, '오류', '방장은 추방할 수 없습니다.'); + return; + } final reqBody = { "room_seq": "${widget.roomSeq}", @@ -124,12 +153,13 @@ class _UserInfoTeamDialogState extends State { return Dialog( backgroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + insetPadding: const EdgeInsets.symmetric(horizontal: 20), child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text('유저 정보 (팀전)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Text('유저 정보', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 12), // (A) 프로필 영역 @@ -257,44 +287,41 @@ class _UserInfoTeamDialogState extends State { children: [ // 수정하기 SizedBox( - width: 90, + width: scaleFactor==0.8 ? 75 : 90, child: ElevatedButton( onPressed: _onUpdateUserInfo, style: ElevatedButton.styleFrom(backgroundColor: Colors.black), // (★) FittedBox 감싸기 - child: FittedBox( - child: Text( - '수정하기', - style: TextStyle(color: Colors.white), - ), + child: AutoSizeText( + scaleFactor==0.8 ? '수정' : '수정하기', + maxLines: 1, + style: TextStyle(color: Colors.white), ), ), ), // 추방하기 SizedBox( - width: 90, + width: scaleFactor==0.8 ? 75 : 90, child: ElevatedButton( onPressed: _onKickParticipant, style: ElevatedButton.styleFrom(backgroundColor: Colors.black), - child: FittedBox( - child: Text( - '추방하기', - style: TextStyle(color: Colors.white), - ), + child: AutoSizeText( + scaleFactor==0.8 ? '추방' : '추방하기', + maxLines: 1, + style: TextStyle(color: Colors.white), ), ), ), // 확인 SizedBox( - width: 90, + width: scaleFactor==0.8 ? 75 : 90, child: ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom(backgroundColor: Colors.black), - child: FittedBox( - child: Text( - '확인', - style: TextStyle(color: Colors.white), - ), + child: AutoSizeText( + '확인', + maxLines: 1, + style: TextStyle(color: Colors.white), ), ), ), @@ -305,7 +332,11 @@ class _UserInfoTeamDialogState extends State { ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom(backgroundColor: Colors.black), - child: const Text('확인', style: TextStyle(color: Colors.white)), + child: AutoSizeText( + '확인', + maxLines: 1, + style: TextStyle(color: Colors.white), + ), ), ], ], diff --git a/lib/plugins/admob.dart b/lib/plugins/admob.dart new file mode 100644 index 0000000..7f0ead9 --- /dev/null +++ b/lib/plugins/admob.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +// import 'package:google_mobile_ads/google_mobile_ads.dart'; +// import '../config/config.dart'; + +/* +// ================== 기존 코드 (전체 주석 처리) ================== + +// class AdBannerWidget extends StatefulWidget { +// const AdBannerWidget({Key? key}) : super(key: key); + +// @override +// State createState() => _AdBannerWidgetState(); +// } + +// class _AdBannerWidgetState extends State { +// BannerAd? _bannerAd; // 광고 객체 + +// @override +// void initState() { +// super.initState(); +// _initBannerAd(); +// } + +// @override +// void dispose() { +// _bannerAd?.dispose(); +// super.dispose(); +// } + +// /// 배너 광고를 초기화 & 로드 +// void _initBannerAd() { +// _bannerAd = BannerAd( +// size: AdSize.banner, // 배너 사이즈(고정) +// adUnitId: Config.adUnitId, // 광고 단위 ID (Config에서 직접 가져옴) +// listener: BannerAdListener( +// onAdLoaded: (Ad ad) { +// // 로드 성공 시: _bannerAd를 그대로 두면 됨. +// setState(() {/* 굳이 아무 것도 안 해도 됨 */}); +// }, +// onAdFailedToLoad: (Ad ad, LoadAdError error) { +// // 실패 시: 자원 정리 & null 처리 +// ad.dispose(); +// setState(() { +// _bannerAd = null; +// }); +// }, +// ), +// request: const AdRequest(), +// ); + +// // 실제 로드 시작 +// _bannerAd!.load(); +// } + +// @override +// Widget build(BuildContext context) { +// // 로딩 전/실패 시 _bannerAd가 null → 빈 위젯 반환 +// if (_bannerAd == null) { +// return const SizedBox.shrink(); +// } +// // 로딩 완료 시 Container에 AdWidget으로 표시 +// return Container( +// color: Colors.white, // 배경색 흰색으로 고정 +// width: _bannerAd!.size.width.toDouble(), +// height: _bannerAd!.size.height.toDouble(), +// child: AdWidget(ad: _bannerAd!), +// ); +// } +// } +*/ + +// ================== 임시/빈 컴포넌트 ================== +class AdBannerWidget extends StatelessWidget { + const AdBannerWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + // (예시) 50dp 높이의 빈 영역을 차지 + // or return SizedBox.shrink() 로 아예 공간을 안 쓰는 방법도 가능 + return const SizedBox( + height: 50, // 원하는 높이 + child: ColoredBox(color: Colors.white), // 배경 흰색 + ); + } +} diff --git a/lib/survey/survey_page.dart b/lib/survey/survey_page.dart index 74d28f7..16a452f 100644 --- a/lib/survey/survey_page.dart +++ b/lib/survey/survey_page.dart @@ -130,7 +130,7 @@ class _SurveyPageState extends State { } final requestBody = { - "QNA": qnaList.toString(), + "QNA": qnaList, }; try { diff --git a/lib/views/login/id_finding_page.dart b/lib/views/login/id_finding_page.dart index 6e68b8e..db7dd5d 100644 --- a/lib/views/login/id_finding_page.dart +++ b/lib/views/login/id_finding_page.dart @@ -7,7 +7,7 @@ import 'pw_finding_page.dart'; import 'signup_page.dart'; // 모바일 광고 -import 'package:google_mobile_ads/google_mobile_ads.dart'; +import '../../plugins/admob.dart'; // 설정 import '../../config/config.dart'; @@ -27,11 +27,6 @@ class _IdFindingPageState extends State { String foundIdMessage = ''; String authId = ''; - /// (1) 광고 배너 관련 변수 - BannerAd? _bannerAd; - bool _isBannerReady = false; // 광고 로드 완료 여부 - String adUnitId = Config.adUnitId; - Future _findId(String nickname, String email) async { @@ -100,30 +95,10 @@ class _IdFindingPageState extends State { @override void initState() { super.initState(); - _initBannerAd(); - } - - void _initBannerAd() { - _bannerAd = BannerAd( - // 실제/테스트 배너 광고 단위 ID - adUnitId: adUnitId, - size: AdSize.banner, - request: const AdRequest(), - listener: BannerAdListener( - onAdLoaded: (ad) { - setState(() => _isBannerReady = true); - }, - onAdFailedToLoad: (ad, error) { - ad.dispose(); - }, - ), - ); - _bannerAd?.load(); } @override void dispose() { - _bannerAd?.dispose(); super.dispose(); } @@ -349,14 +324,7 @@ class _IdFindingPageState extends State { ), // (3) 하단 광고 영역 - bottomNavigationBar: _isBannerReady && _bannerAd != null - ? Container( - color: Colors.white, - width: _bannerAd!.size.width.toDouble(), - height: _bannerAd!.size.height.toDouble(), - child: AdWidget(ad: _bannerAd!), - ) - : SizedBox.shrink(), // 로딩 전엔 빈 공간 or 원하는 위젯 + bottomNavigationBar: AdBannerWidget(), ); } } \ No newline at end of file diff --git a/lib/views/login/login_page.dart b/lib/views/login/login_page.dart index 2950894..b6d415c 100644 --- a/lib/views/login/login_page.dart +++ b/lib/views/login/login_page.dart @@ -369,7 +369,7 @@ class _LoginPageState extends State { 그러나 필수 항목에 대한 동의를 거부하실 경우 서비스 이용이 제한될 수 있습니다. 7. 개인정보 보호책임자 -연락처: eld_yeojh@naver.com +연락처: eldyeojh@gmail.com 8. 개인정보의 안전성 확보 조치 회사는 개인정보의 안전한 처리를 위하여 기술적, 관리적 보호조치를 시행하고 있습니다. diff --git a/lib/views/login/pw_finding_page.dart b/lib/views/login/pw_finding_page.dart index fc15b93..ef19448 100644 --- a/lib/views/login/pw_finding_page.dart +++ b/lib/views/login/pw_finding_page.dart @@ -7,7 +7,7 @@ import 'signup_page.dart'; // 회원가입 페이지 임포트 추가 import 'id_finding_page.dart'; // ID 찾기 페이지 임포트 추가 // 모바일 광고 -import 'package:google_mobile_ads/google_mobile_ads.dart'; +import '../../plugins/admob.dart'; // 설정 import '../../config/config.dart'; @@ -25,11 +25,6 @@ class _PwFindingPageState extends State { String emailErrorMessage = ''; // 이메일 오류 메시지 String idErrorMessage = ''; // ID 오류 메시지 - /// (1) 광고 배너 관련 변수 - BannerAd? _bannerAd; - bool _isBannerReady = false; // 광고 로드 완료 여부 - String adUnitId = Config.adUnitId; - Future _findPassword(String id, String email) async { // PW 찾기 요청 처리 @@ -125,30 +120,10 @@ class _PwFindingPageState extends State { @override void initState() { super.initState(); - _initBannerAd(); - } - - void _initBannerAd() { - _bannerAd = BannerAd( - // 실제/테스트 배너 광고 단위 ID - adUnitId: adUnitId, - size: AdSize.banner, - request: const AdRequest(), - listener: BannerAdListener( - onAdLoaded: (ad) { - setState(() => _isBannerReady = true); - }, - onAdFailedToLoad: (ad, error) { - ad.dispose(); - }, - ), - ); - _bannerAd?.load(); } @override void dispose() { - _bannerAd?.dispose(); super.dispose(); } @@ -254,14 +229,7 @@ class _PwFindingPageState extends State { ), // (3) 하단 광고 영역 - bottomNavigationBar: _isBannerReady && _bannerAd != null - ? Container( - color: Colors.white, - width: _bannerAd!.size.width.toDouble(), - height: _bannerAd!.size.height.toDouble(), - child: AdWidget(ad: _bannerAd!), - ) - : SizedBox.shrink(), // 로딩 전엔 빈 공간 or 원하는 위젯 + bottomNavigationBar: AdBannerWidget(), ); } } \ No newline at end of file diff --git a/lib/views/login/signup_page.dart b/lib/views/login/signup_page.dart index 945bbbb..4116107 100644 --- a/lib/views/login/signup_page.dart +++ b/lib/views/login/signup_page.dart @@ -253,8 +253,7 @@ class _SignUpPageState extends State { '이용자는 개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있습니다.\n' '그러나 필수 항목에 대한 동의를 거부하실 경우 서비스 이용이 제한될 수 있습니다.\n\n' '7. 개인정보 보호책임자\n' - '이름: 여정훈\n' - '연락처: eld_yeojh@naver.com\n\n' + '연락처: eldyeojh@gmail.com\n\n' '8. 개인정보의 안전성 확보 조치\n' '회사는 개인정보의 안전한 처리를 위하여 기술적, 관리적 보호조치를 시행하고 있습니다.\n' '개인정보의 암호화\n' diff --git a/lib/views/room/main_page.dart b/lib/views/room/main_page.dart index ca657fc..3c2d4f8 100644 --- a/lib/views/room/main_page.dart +++ b/lib/views/room/main_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:google_mobile_ads/google_mobile_ads.dart'; // ★ AdMob 패키지 // 기타 import... import '../../dialogs/settings_dialog.dart'; @@ -25,6 +24,9 @@ import '../../config/config.dart'; // 설문조사 import '../../dialogs/survey_dialog.dart'; +// 광고 +import '../../plugins/admob.dart'; + class MainPage extends StatefulWidget { const MainPage({Key? key}) : super(key: key); @@ -33,11 +35,6 @@ class MainPage extends StatefulWidget { } class _MainPageState extends State { - /// (1) 광고 배너 관련 변수 - BannerAd? _bannerAd; - bool _isBannerReady = false; // 광고 로드 완료 여부 - String adUnitId = Config.adUnitId; - // 뒤로가기 처리 DateTime? _lastPressedTime; @@ -55,38 +52,13 @@ class _MainPageState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { _checkLandingMainPageInfo(); }); - - // (C) 배너 광고 초기화 - _initBannerAd(); } @override void dispose() { - _bannerAd?.dispose(); super.dispose(); } - /// 배너 광고 초기화 - void _initBannerAd() { - _bannerAd = BannerAd( - size: AdSize.banner, // 일반 배너 사이즈 - // adUnitId: 'ca-app-pub-3940256099942544/6300978111' (테스트용) - adUnitId: adUnitId, // 실제/테스트 배너 광고 단위 ID - listener: BannerAdListener( - onAdLoaded: (Ad ad) { - setState(() => _isBannerReady = true); - }, - onAdFailedToLoad: (Ad ad, LoadAdError err) { - ad.dispose(); - }, - ), - request: const AdRequest(), - ); - - // load() 호출로 광고 요청 - _bannerAd?.load(); - } - Future _onWillPop() async { final now = DateTime.now(); if (_lastPressedTime == null || @@ -202,14 +174,7 @@ class _MainPageState extends State { ], ), - bottomNavigationBar: _isBannerReady && _bannerAd != null - ? Container( - color: Colors.white, - width: _bannerAd!.size.width.toDouble(), - height: _bannerAd!.size.height.toDouble(), - child: AdWidget(ad: _bannerAd!), - ) - : SizedBox.shrink(), // 로딩 전엔 빈 공간 or 원하는 위젯 + bottomNavigationBar: AdBannerWidget(), body: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/views/room/playing_private_page.dart b/lib/views/room/playing_private_page.dart index f1cc4be..49e708d 100644 --- a/lib/views/room/playing_private_page.dart +++ b/lib/views/room/playing_private_page.dart @@ -9,8 +9,8 @@ import '../../plugins/api.dart'; import '../../dialogs/response_dialog.dart'; import '../../dialogs/score_edit_dialog.dart'; // 점수 수정 모달 import '../../dialogs/user_info_basic_dialog.dart'; // 일반 유저 정보 모달 +import '../../plugins/admob.dart'; -import 'package:google_mobile_ads/google_mobile_ads.dart'; import '../../config/config.dart'; class PlayingPrivatePage extends StatefulWidget { @@ -58,14 +58,12 @@ class _PlayingPrivatePageState extends State { String mySeq = '0'; + // 나의 참가 타입 + String myParticipantType = 'PLAYER'; + // userListMap: { userSeq: true/false } Map _userListMap = {}; - /// (1) 광고 배너 관련 변수 - BannerAd? _bannerAd; - bool _isBannerReady = false; // 광고 로드 완료 여부 - String adUnitId = Config.adUnitId; - @override void initState() { super.initState(); @@ -73,8 +71,6 @@ class _PlayingPrivatePageState extends State { FirebaseDatabase.instance.goOnline(); roomTitle = widget.roomTitle; - // (C) 배너 광고 초기화 - _initBannerAd(); // (D) 방 정보 초기화 _initFirebase(); } @@ -86,27 +82,6 @@ class _PlayingPrivatePageState extends State { super.dispose(); } - /// 배너 광고 초기화 - void _initBannerAd() { - _bannerAd = BannerAd( - size: AdSize.banner, // 일반 배너 사이즈 - // adUnitId: 'ca-app-pub-3940256099942544/6300978111' (테스트용) - adUnitId: adUnitId, // 실제/테스트 배너 광고 단위 ID - listener: BannerAdListener( - onAdLoaded: (Ad ad) { - setState(() => _isBannerReady = true); - }, - onAdFailedToLoad: (Ad ad, LoadAdError err) { - ad.dispose(); - }, - ), - request: const AdRequest(), - ); - - // load() 호출로 광고 요청 - _bannerAd?.load(); - } - Future _initFirebase() async { final prefs = await SharedPreferences.getInstance(); mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0'; @@ -192,6 +167,10 @@ class _PlayingPrivatePageState extends State { 'introduce_myself': uData['introduce_myself'] ?? '', 'is_my_score': (uSeq.toString() == mySeq) ? 'Y' : 'N', }); + + if (uSeq.toString() == mySeq) { + myParticipantType = (uData['participant_type'] ?? '').toString().toUpperCase(); + } }); // 내 점수 @@ -386,11 +365,13 @@ class _PlayingPrivatePageState extends State { }, ); - if (confirm != true) return false; + if (confirm != true) { + return false; + } } // userList => false - final userRef = _roomRef.child('userList').child(mySeq); + final userRef = _roomRef.child('userList').child('_$mySeq'); await userRef.set(false); if (!mounted) return false; @@ -449,8 +430,7 @@ class _PlayingPrivatePageState extends State { } Future _onTapUser(Map userData) async { - final pType = (userData['participant_type'] ?? '').toString().toUpperCase(); - if (pType == 'ADMIN') { + if (myParticipantType == 'ADMIN') { // 점수수정 await showDialog( context: context, @@ -460,16 +440,6 @@ class _PlayingPrivatePageState extends State { userData: userData, ), ); - } else if (roomMasterYn == 'Y') { - // 방장(PLAYER)도 수정 가능 - await showDialog( - context: context, - builder: (_) => ScoreEditDialog( - roomSeq: widget.roomSeq, - roomType: 'PRIVATE', - userData: userData, - ), - ); } else { // 일반 유저 정보 await showDialog( @@ -552,14 +522,7 @@ class _PlayingPrivatePageState extends State { ), ], ), - bottomNavigationBar: _isBannerReady && _bannerAd != null - ? Container( - color: Colors.white, - width: _bannerAd!.size.width.toDouble(), - height: _bannerAd!.size.height.toDouble(), - child: AdWidget(ad: _bannerAd!), - ) - : SizedBox.shrink(), // 로딩 전엔 빈 공간 or 원하는 위젯 + bottomNavigationBar: AdBannerWidget(), ), ); } diff --git a/lib/views/room/playing_team_page.dart b/lib/views/room/playing_team_page.dart index 1d7d66e..bdc9712 100644 --- a/lib/views/room/playing_team_page.dart +++ b/lib/views/room/playing_team_page.dart @@ -9,8 +9,7 @@ import '../../plugins/api.dart'; import '../../dialogs/response_dialog.dart'; import '../../dialogs/score_edit_dialog.dart'; import '../../dialogs/user_info_basic_dialog.dart'; - -import 'package:google_mobile_ads/google_mobile_ads.dart'; +import '../../plugins/admob.dart'; import '../../config/config.dart'; class PlayingTeamPage extends StatefulWidget { @@ -60,14 +59,12 @@ class _PlayingTeamPageState extends State { // userListMap: { seq: true/false } Map _userListMap = {}; + // 나의 참가 타입 + String myParticipantType = 'PLAYER'; + // 점수 공개 범위 String scoreOpenRange = 'ALL'; - /// (1) 광고 배너 관련 변수 - BannerAd? _bannerAd; - bool _isBannerReady = false; // 광고 로드 완료 여부 - String adUnitId = Config.adUnitId; - @override void initState() { super.initState(); @@ -75,8 +72,6 @@ class _PlayingTeamPageState extends State { FirebaseDatabase.instance.goOnline(); roomTitle = widget.roomTitle; - // (C) 배너 광고 초기화 - _initBannerAd(); // (D) 방 정보 초기화 _initFirebase(); } @@ -88,27 +83,6 @@ class _PlayingTeamPageState extends State { super.dispose(); } - /// 배너 광고 초기화 - void _initBannerAd() { - _bannerAd = BannerAd( - size: AdSize.banner, // 일반 배너 사이즈 - // adUnitId: 'ca-app-pub-3940256099942544/6300978111' (테스트용) - adUnitId: adUnitId, // 실제/테스트 배너 광고 단위 ID - listener: BannerAdListener( - onAdLoaded: (Ad ad) { - setState(() => _isBannerReady = true); - }, - onAdFailedToLoad: (Ad ad, LoadAdError err) { - ad.dispose(); - }, - ), - request: const AdRequest(), - ); - - // load() 호출로 광고 요청 - _bannerAd?.load(); - } - Future _initFirebase() async { final prefs = await SharedPreferences.getInstance(); mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0'; @@ -191,6 +165,9 @@ class _PlayingTeamPageState extends State { 'team_name': (uData['team_name'] ?? '').toString().toUpperCase(), 'score': uData['score'] ?? 0, }); + if (uSeq.toString() == mySeq) { + myParticipantType = (uData['participant_type'] ?? '').toString().toUpperCase(); + } }); // 내 점수 & 팀 점수 @@ -415,7 +392,7 @@ class _PlayingTeamPageState extends State { } // userList => false - final userRef = _roomRef.child('userList').child(mySeq); + final userRef = _roomRef.child('userList').child('_$mySeq'); await userRef.set(false); if (!mounted) return false; @@ -510,14 +487,7 @@ class _PlayingTeamPageState extends State { ), ], ), - bottomNavigationBar: _isBannerReady && _bannerAd != null - ? Container( - color: Colors.white, - width: _bannerAd!.size.width.toDouble(), - height: _bannerAd!.size.height.toDouble(), - child: AdWidget(ad: _bannerAd!), - ) - : SizedBox.shrink(), // 로딩 전엔 빈 공간 or 원하는 위젯 + bottomNavigationBar: AdBannerWidget(), ), ); } @@ -616,17 +586,7 @@ class _PlayingTeamPageState extends State { } Future _onTapUser(Map userData) async { - final pType = (userData['participant_type'] ?? '').toString().toUpperCase(); - if (pType == 'ADMIN') { - await showDialog( - context: context, - builder: (_) => ScoreEditDialog( - roomSeq: widget.roomSeq, - roomType: 'TEAM', - userData: userData, - ), - ); - } else if (roomMasterYn == 'Y') { + if (myParticipantType == 'ADMIN') { await showDialog( context: context, builder: (_) => ScoreEditDialog( diff --git a/lib/views/room/room_search_home_page.dart b/lib/views/room/room_search_home_page.dart index 38ea858..2557ae4 100644 --- a/lib/views/room/room_search_home_page.dart +++ b/lib/views/room/room_search_home_page.dart @@ -5,7 +5,7 @@ import 'room_search_list_page.dart'; import '../../config/config.dart'; // 광고 -import 'package:google_mobile_ads/google_mobile_ads.dart'; // ★ AdMob 패키지 +import '../../plugins/admob.dart'; class RoomSearchHomePage extends StatefulWidget { const RoomSearchHomePage({Key? key}) : super(key: key); @@ -15,43 +15,32 @@ class RoomSearchHomePage extends StatefulWidget { } class _RoomSearchHomePageState extends State { - BannerAd? _bannerAd; - bool _isBannerReady = false; // 광고 로드 완료 여부 - String adUnitId = Config.adUnitId; + // 화면 크기에 따라 폰트 크기 조절 + double scaleFactor = 1.0; @override void initState() { super.initState(); - - // (C) 배너 광고 초기화 - _initBannerAd(); } @override void dispose() { - _bannerAd?.dispose(); super.dispose(); } - /// 배너 광고 초기화 - void _initBannerAd() { - _bannerAd = BannerAd( - size: AdSize.banner, // 일반 배너 사이즈 - // adUnitId: 'ca-app-pub-3940256099942544/6300978111' (테스트용) - adUnitId: adUnitId, // 실제/테스트 배너 광고 단위 ID - listener: BannerAdListener( - onAdLoaded: (Ad ad) { - setState(() => _isBannerReady = true); - }, - onAdFailedToLoad: (Ad ad, LoadAdError err) { - // 광고 로드 실패 시 처리 - }, - ), - request: const AdRequest(), - ); - - // load() 호출로 광고 요청 - _bannerAd?.load(); + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateScaleFactor(); + } + + // 화면 크기에 따라 폰트 크기 조절 + void _updateScaleFactor() { + final screenWidth = MediaQuery.of(context).size.width; + const baseWidth = 450.0; + setState(() { + scaleFactor = (screenWidth / baseWidth).clamp(0.8, 1.2); + }); } @override @@ -70,14 +59,7 @@ class _RoomSearchHomePageState extends State { ), // 화면 하단 광고 영역 - bottomNavigationBar: _isBannerReady && _bannerAd != null - ? Container( - color: Colors.white, - width: _bannerAd!.size.width.toDouble(), - height: _bannerAd!.size.height.toDouble(), - child: AdWidget(ad: _bannerAd!), - ) - : SizedBox.shrink(), // 로딩 전엔 빈 공간 or 원하는 위젯 + bottomNavigationBar: AdBannerWidget(), // 본문: 중앙에 3개 버튼 (대기중 / 진행중 / 종료) body: Center( @@ -102,8 +84,8 @@ class _RoomSearchHomePageState extends State { required String status, }) { return SizedBox( - width: 100, - height: 100, + width: 100 * scaleFactor, + height: 100 * scaleFactor, child: ElevatedButton( onPressed: () { // RoomSearchListPage로 이동하며, roomStatus 전달 diff --git a/lib/views/room/room_search_list_page.dart b/lib/views/room/room_search_list_page.dart index c32ec8d..b5dcc32 100644 --- a/lib/views/room/room_search_list_page.dart +++ b/lib/views/room/room_search_list_page.dart @@ -14,7 +14,7 @@ import '../room/finish_team_page.dart'; import '../../config/config.dart'; // 광고 -import 'package:google_mobile_ads/google_mobile_ads.dart'; // ★ AdMob 패키지 +import '../../plugins/admob.dart'; class RoomSearchListPage extends StatefulWidget { final String roomStatus; // WAIT / RUNNING / FINISH @@ -36,11 +36,6 @@ class _RoomSearchListPageState extends State { final int _pageSize = 10; late ScrollController _scrollController; - - // 배너 광고 관련 변수 - BannerAd? _bannerAd; - bool _isBannerReady = false; // 광고 로드 완료 여부 - String adUnitId = Config.adUnitId; @override void initState() { @@ -48,9 +43,6 @@ class _RoomSearchListPageState extends State { _scrollController = ScrollController()..addListener(_onScroll); _fetchRoomList(isRefresh: true); - - // (C) 배너 광고 초기화 - _initBannerAd(); } @override @@ -58,31 +50,9 @@ class _RoomSearchListPageState extends State { _scrollController.removeListener(_onScroll); _scrollController.dispose(); _searchController.dispose(); - _bannerAd?.dispose(); super.dispose(); } - /// 배너 광고 초기화 - void _initBannerAd() { - _bannerAd = BannerAd( - size: AdSize.banner, // 일반 배너 사이즈 - // adUnitId: 'ca-app-pub-3940256099942544/6300978111' (테스트용) - adUnitId: adUnitId, // 실제/테스트 배너 광고 단위 ID - listener: BannerAdListener( - onAdLoaded: (Ad ad) { - setState(() => _isBannerReady = true); - }, - onAdFailedToLoad: (Ad ad, LoadAdError err) { - ad.dispose(); - }, - ), - request: const AdRequest(), - ); - - // load() 호출로 광고 요청 - _bannerAd?.load(); - } - void _onScroll() { if (!_scrollController.hasClients) return; final thresholdPixels = 200; @@ -236,14 +206,8 @@ class _RoomSearchListPageState extends State { ), ), // 화면 하단 광고 영역 - bottomNavigationBar: _isBannerReady && _bannerAd != null - ? Container( - color: Colors.white, - width: _bannerAd!.size.width.toDouble(), - height: _bannerAd!.size.height.toDouble(), - child: AdWidget(ad: _bannerAd!), - ) - : SizedBox.shrink(), // 로딩 전엔 빈 공간 or 원하는 위젯 + bottomNavigationBar: AdBannerWidget(), + body: Column( children: [ // 검색창 diff --git a/lib/views/room/waiting_room_private_page.dart b/lib/views/room/waiting_room_private_page.dart index 9072ea4..94c223f 100644 --- a/lib/views/room/waiting_room_private_page.dart +++ b/lib/views/room/waiting_room_private_page.dart @@ -12,10 +12,12 @@ import '../../dialogs/user_info_private_dialog.dart'; import 'playing_private_page.dart'; // 광고 -import 'package:google_mobile_ads/google_mobile_ads.dart'; +import '../../plugins/admob.dart'; // 설정 import '../../config/config.dart'; +// 폰트 크기 조절 +import 'package:auto_size_text/auto_size_text.dart'; class WaitingRoomPrivatePage extends StatefulWidget { final int roomSeq; @@ -74,14 +76,13 @@ class _WaitingRoomPrivatePageState extends State { bool _roomTimeOut = false; String _roomExitYn = 'N'; - /// (1) 광고 배너 관련 변수 - BannerAd? _bannerAd; - bool _isBannerReady = false; // 광고 로드 완료 여부 - String adUnitId = Config.adUnitId; - // 방장 SEQ 저장 String _masterSeqString = '0'; + // 화면 크기에 따라 폰트 크기 조절 + double scaleFactor = 1.0; + double buttonScaleFactor = 1.0; + @override void initState() { super.initState(); @@ -89,8 +90,6 @@ class _WaitingRoomPrivatePageState extends State { FirebaseDatabase.instance.goOnline(); // (B) 방 정보 초기화 _initRoomRef(); - // (C) 배너 광고 초기화 - _initBannerAd(); } @override @@ -100,25 +99,20 @@ class _WaitingRoomPrivatePageState extends State { super.dispose(); } - /// 배너 광고 초기화 - void _initBannerAd() { - _bannerAd = BannerAd( - size: AdSize.banner, // 일반 배너 사이즈 - // adUnitId: 'ca-app-pub-3940256099942544/6300978111' (테스트용) - adUnitId: adUnitId, // 실제/테스트 배너 광고 단위 ID - listener: BannerAdListener( - onAdLoaded: (Ad ad) { - setState(() => _isBannerReady = true); - }, - onAdFailedToLoad: (Ad ad, LoadAdError err) { - ad.dispose(); - }, - ), - request: const AdRequest(), - ); - - // load() 호출로 광고 요청 - _bannerAd?.load(); + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateScaleFactor(); + } + + // 화면 크기에 따라 폰트 크기 조절 + void _updateScaleFactor() { + final screenWidth = MediaQuery.of(context).size.width; + const baseWidth = 450.0; + setState(() { + scaleFactor = (screenWidth / baseWidth).clamp(0.8, 1.2); + buttonScaleFactor = (screenWidth / baseWidth).clamp(0.6, 1.2); + }); } Future _initRoomRef() async { @@ -328,7 +322,7 @@ class _WaitingRoomPrivatePageState extends State { final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody); // result ok -> 메인 } catch (e) { - // + await showResponseDialog(context, '오류', '방 나가기 처리 실패'); } if (mounted) { Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const MainPage()), (route) => false); @@ -410,7 +404,7 @@ class _WaitingRoomPrivatePageState extends State { final me = _userList.firstWhere((u) => (u['user_seq'].toString() ?? '0') == mySeq, orElse: () => {}); final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase(); final bool isReady = (myReadyYn == 'Y'); - final readyLabel = isReady ? '준비완료' : '준비'; + final readyLabel = '준비'; final btnStyle = ElevatedButton.styleFrom( backgroundColor: Colors.white, @@ -428,7 +422,11 @@ class _WaitingRoomPrivatePageState extends State { child: ElevatedButton( style: btnStyle, onPressed: _onOpenRoomSetting, - child: const Text('방 설정'), + child: AutoSizeText( + '방 설정', + maxLines: 1, + style: TextStyle(fontSize: 14 * scaleFactor), + ), ), ), ), @@ -438,7 +436,11 @@ class _WaitingRoomPrivatePageState extends State { child: ElevatedButton( style: btnStyle, onPressed: _readyButtonEnabled ? _onToggleReady : null, - child: Text(readyLabel), + child: AutoSizeText( + readyLabel, + maxLines: 1, + style: TextStyle(fontSize: 14 * scaleFactor, color: isReady ? Colors.red : Colors.black), + ), ), ), ), @@ -448,7 +450,11 @@ class _WaitingRoomPrivatePageState extends State { child: ElevatedButton( style: btnStyle, onPressed: _onGameStart, - child: const Text('게임 시작'), + child: AutoSizeText( + scaleFactor==0.8 ? '시작' : '게임 시작', + maxLines: 1, + style: TextStyle(fontSize: 14 * scaleFactor), + ), ), ), ), @@ -464,7 +470,11 @@ class _WaitingRoomPrivatePageState extends State { child: ElevatedButton( style: btnStyle, onPressed: _onOpenRoomSetting, - child: const Text('방 설정'), + child: AutoSizeText( + '방 설정', + maxLines: 1, + style: TextStyle(fontSize: 14 * scaleFactor), + ), ), ), ), @@ -474,7 +484,11 @@ class _WaitingRoomPrivatePageState extends State { child: ElevatedButton( style: btnStyle, onPressed: _readyButtonEnabled ? _onToggleReady : null, - child: Text(readyLabel), + child: AutoSizeText( + readyLabel, + maxLines: 1, + style: TextStyle(fontSize: 14 * scaleFactor, color: isReady ? Colors.red : Colors.black), + ), ), ), ), @@ -619,14 +633,8 @@ class _WaitingRoomPrivatePageState extends State { onPressed: () => _onLeaveRoom(), ), ), - bottomNavigationBar: _isBannerReady && _bannerAd != null - ? Container( - color: Colors.white, - width: _bannerAd!.size.width.toDouble(), - height: _bannerAd!.size.height.toDouble(), - child: AdWidget(ad: _bannerAd!), - ) - : SizedBox.shrink(), // 로딩 전엔 빈 공간 or 원하는 위젯 + bottomNavigationBar: AdBannerWidget(), + body: _isLoading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( diff --git a/lib/views/room/waiting_room_team_page.dart b/lib/views/room/waiting_room_team_page.dart index 22c933c..f150a7d 100644 --- a/lib/views/room/waiting_room_team_page.dart +++ b/lib/views/room/waiting_room_team_page.dart @@ -18,10 +18,12 @@ import '../../dialogs/team_name_edit_dialog.dart'; import 'playing_team_page.dart'; // 광고 -import 'package:google_mobile_ads/google_mobile_ads.dart'; +import '../../plugins/admob.dart'; // 설정 import '../../config/config.dart'; +// 폰트 크기 조절 +import 'package:auto_size_text/auto_size_text.dart'; class WaitingRoomTeamPage extends StatefulWidget { final int roomSeq; @@ -76,24 +78,37 @@ class _WaitingRoomTeamPageState extends State { bool _roomTimeOut = false; String _roomExitYn = 'N'; - /// (1) 광고 배너 관련 변수 - BannerAd? _bannerAd; - bool _isBannerReady = false; // 광고 로드 완료 여부 - String adUnitId = Config.adUnitId; - // 방장 SEQ 저장 String _masterSeqString = '0'; + // 화면 크기에 따라 폰트 크기 조절 + double scaleFactor = 1.0; + double buttonScaleFactor = 1.0; + @override void initState() { super.initState(); FirebaseDatabase.instance.goOnline(); - // (C) 배너 광고 초기화 - _initBannerAd(); // (D) 방 정보 초기화 _initRoomRef(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateScaleFactor(); + } + + // 화면 크기에 따라 폰트 크기 조절 + void _updateScaleFactor() { + final screenWidth = MediaQuery.of(context).size.width; + const baseWidth = 450.0; + setState(() { + scaleFactor = (screenWidth / baseWidth).clamp(0.8, 1.2); + buttonScaleFactor = (screenWidth / baseWidth).clamp(0.6, 1.2); + }); + } + Future _initRoomRef() async { final prefs = await SharedPreferences.getInstance(); mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0'; @@ -118,27 +133,6 @@ class _WaitingRoomTeamPageState extends State { super.dispose(); } - /// 배너 광고 초기화 - void _initBannerAd() { - _bannerAd = BannerAd( - size: AdSize.banner, // 일반 배너 사이즈 - // adUnitId: 'ca-app-pub-3940256099942544/6300978111' (테스트용) - adUnitId: adUnitId, // 실제/테스트 배너 광고 단위 ID - listener: BannerAdListener( - onAdLoaded: (Ad ad) { - setState(() => _isBannerReady = true); - }, - onAdFailedToLoad: (Ad ad, LoadAdError err) { - ad.dispose(); - }, - ), - request: const AdRequest(), - ); - - // load() 호출로 광고 요청 - _bannerAd?.load(); - } - void _listenRoomData() { _roomStream = _roomRef.onValue; _roomStreamSubscription = _roomStream?.listen((event) { @@ -416,7 +410,7 @@ class _WaitingRoomTeamPageState extends State { final me = _userList.firstWhere((u) => (u['user_seq'].toString() ?? '0') == mySeq, orElse: () => {}); final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase(); final bool isReady = (myReadyYn == 'Y'); - final readyLabel = isReady ? '준비완료' : '준비'; + final readyLabel = '준비'; final btnStyle = ElevatedButton.styleFrom( backgroundColor: Colors.white, @@ -433,7 +427,11 @@ class _WaitingRoomTeamPageState extends State { child: ElevatedButton( style: btnStyle, onPressed: _onOpenRoomSetting, - child: const Text('방 설정'), + child: AutoSizeText( + '방 설정', + maxLines: 1, + style: TextStyle(fontSize: 14 * scaleFactor), + ), ), ), ), @@ -443,7 +441,11 @@ class _WaitingRoomTeamPageState extends State { child: ElevatedButton( style: btnStyle, onPressed: _readyButtonEnabled ? _onToggleReady : null, - child: Text(readyLabel), + child: AutoSizeText( + readyLabel, + maxLines: 1, + style: TextStyle(fontSize: 14 * scaleFactor, color: isReady ? Colors.red : Colors.black), + ), ), ), ), @@ -455,7 +457,11 @@ class _WaitingRoomTeamPageState extends State { onPressed: _isServerRequestLoading ? null : _onGameStart, child: _isServerRequestLoading ? const CircularProgressIndicator() - : const Text('게임 시작'), + : AutoSizeText( + scaleFactor==0.8 ? '시작' : '게임 시작', + maxLines: 1, + style: TextStyle(fontSize: 14 * scaleFactor), + ), ), ), ), @@ -470,7 +476,11 @@ class _WaitingRoomTeamPageState extends State { child: ElevatedButton( style: btnStyle, onPressed: _onOpenRoomSetting, - child: const Text('방 설정'), + child: AutoSizeText( + '방 설정', + maxLines: 1, + style: TextStyle(fontSize: 14 * scaleFactor), + ), ), ), ), @@ -480,7 +490,11 @@ class _WaitingRoomTeamPageState extends State { child: ElevatedButton( style: btnStyle, onPressed: _readyButtonEnabled ? _onToggleReady : null, - child: Text(readyLabel), + child: AutoSizeText( + readyLabel, + maxLines: 1, + style: TextStyle(fontSize: 14 * scaleFactor, color: isReady ? Colors.red : Colors.black), + ), ), ), ), @@ -637,14 +651,8 @@ class _WaitingRoomTeamPageState extends State { onPressed: () => _onLeaveRoom(), ), ), - bottomNavigationBar: _isBannerReady && _bannerAd != null - ? Container( - color: Colors.white, - width: _bannerAd!.size.width.toDouble(), - height: _bannerAd!.size.height.toDouble(), - child: AdWidget(ad: _bannerAd!), - ) - : SizedBox.shrink(), // 로딩 전엔 빈 공간 or 원하는 위젯 + bottomNavigationBar: AdBannerWidget(), + body: _isLoading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( diff --git a/lib/views/user/my_page.dart b/lib/views/user/my_page.dart index f99aa09..9046c7b 100644 --- a/lib/views/user/my_page.dart +++ b/lib/views/user/my_page.dart @@ -9,6 +9,7 @@ import '../../plugins/utils.dart'; import 'withdrawal_page.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; +import '../room/main_page.dart'; // import 'package:path/path.dart'; class MyPage extends StatefulWidget { @@ -47,13 +48,27 @@ class _MyPageState extends State { final TextEditingController _introduceController = TextEditingController(); String? _nicknameError; XFile? _image; // 선택된 이미지 파일 + String _oauthType = 'idpw'; // 로그인 방법 + + // 화면 크기에 따라 폰트 크기 조절 + double scaleFactor = 1.0; + double buttonScaleFactor = 1.0; @override void initState() { super.initState(); + // 자기소개 초기화 + _introduceController.text = ''; + _fetchUserInfo(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateScaleFactor(); + } + @override void dispose() { _nicknameController.dispose(); @@ -69,525 +84,600 @@ class _MyPageState extends State { _introduceController.dispose(); super.dispose(); } + + // 화면 크기에 따라 폰트 크기 조절 + void _updateScaleFactor() { + final screenWidth = MediaQuery.of(context).size.width; + const baseWidth = 400.0; + setState(() { + scaleFactor = (screenWidth / baseWidth).clamp(0.8, 1.2); + buttonScaleFactor = (screenWidth / baseWidth).clamp(0.6, 1.2); + }); + } @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - if (isEditingNickname) { - setState(() { - user_nickname = _nicknameController.text; - isEditingNickname = false; - _nicknameFocusNode.unfocus(); - }); - } - if (isEditingPassword) { - setState(() { - new_user_pw = _passwordController.text; - isEditingPassword = false; - _passwordFocusNode.unfocus(); - }); - } - if (isConfirmingPassword) { - setState(() { - isConfirmingPassword = false; - _confirmPasswordFocusNode.unfocus(); - }); - } - if (isEditingEmail) { - setState(() { - user_email = _emailController.text; - isEditingEmail = false; - _emailFocusNode.unfocus(); - }); - } - if (isEditingDepartment) { - setState(() { - user_department = _departmentController.text; - isEditingDepartment = false; - _departmentFocusNode.unfocus(); - }); - } + return WillPopScope( + onWillPop: () async { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const MainPage()), + (route) => false, + ); + return false; }, - child: Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - title: const Text('MY PAGE', style: TextStyle(color: Colors.black)), - backgroundColor: Colors.transparent, - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, color: Colors.black), - onPressed: () { - Navigator.pop(context); - Navigator.pop(context); - }, + child: GestureDetector( + onTap: () { + // 화면 아무 곳이나 탭 했을 때, 편집 모드 해제 등을 처리 + if (isEditingNickname) { + setState(() { + user_nickname = _nicknameController.text; + isEditingNickname = false; + _nicknameFocusNode.unfocus(); + }); + } + if (isEditingPassword) { + setState(() { + new_user_pw = _passwordController.text; + isEditingPassword = false; + _passwordFocusNode.unfocus(); + }); + } + if (isConfirmingPassword) { + setState(() { + isConfirmingPassword = false; + _confirmPasswordFocusNode.unfocus(); + }); + } + if (isEditingEmail) { + setState(() { + user_email = _emailController.text; + isEditingEmail = false; + _emailFocusNode.unfocus(); + }); + } + if (isEditingDepartment) { + setState(() { + user_department = _departmentController.text; + isEditingDepartment = false; + _departmentFocusNode.unfocus(); + }); + } + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + title: const Text('MY PAGE', style: TextStyle(color: Colors.black)), + backgroundColor: Colors.transparent, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.black), + onPressed: () { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const MainPage()), + (route) => false, + ); + }, + ), ), - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Card( - elevation: 4, - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '닉네임:', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Expanded( - child: GestureDetector( - onTap: () { - setState(() { - isEditingNickname = true; - _nicknameController.text = user_nickname; - _nicknameFocusNode.requestFocus(); - }); - }, - child: isEditingNickname - ? TextField( - controller: _nicknameController, - focusNode: _nicknameFocusNode, - onChanged: (newNickname) { - setState(() { - user_nickname = newNickname; - // 닉네임 패턴 검증 - if (!_isNicknameValidPattern(user_nickname)) { - // 패턴이 일치하지 않을 경우 안내 문구 표시 - _nicknameError = '닉네임은 2~20자 영문, 한글, 숫자만 사용할 수 있습니다.'; - } else { - _nicknameError = null; // 패턴이 일치하면 오류 메시지 초기화 - } - }); - }, - onSubmitted: (newNickname) { - setState(() { - user_nickname = newNickname; - isEditingNickname = false; - _nicknameFocusNode.unfocus(); - }); - }, - decoration: InputDecoration( - hintText: '닉네임을 입력하세요', - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), - ), - ) - : Container( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - user_nickname, - style: const TextStyle(fontSize: 18, color: Colors.black54), - ), - ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '닉네임:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), - ), - ], - ), - ), - ), - // 닉네임 오류 메시지 추가 - if (_nicknameError != null) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - _nicknameError!, - style: const TextStyle(color: Colors.red, fontSize: 12), - ), - ), - const SizedBox(height: 16), - Card( - elevation: 4, - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '비밀번호:', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Expanded( - child: GestureDetector( - onTap: () { - setState(() { - isEditingPassword = true; - _passwordController.text = new_user_pw.replaceAll(RegExp(r'.'), '*'); - _passwordFocusNode.requestFocus(); - }); - }, - child: isEditingPassword - ? TextField( - controller: _passwordController, - focusNode: _passwordFocusNode, - obscureText: true, - onChanged: (newPassword) { - setState(() { - new_user_pw = newPassword; - _passwordError = _isPasswordValidPattern(new_user_pw) ? null : '비밀번호는 8~20자 영문과 숫자가 반드시 포함되어야 합니다.'; - }); - }, - onSubmitted: (newPassword) { - setState(() { - isEditingPassword = false; - _passwordFocusNode.unfocus(); - }); - }, - decoration: InputDecoration( - hintText: '비밀번호를 입력하세요', - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), - ), - ) - : Container( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - '**********', - style: const TextStyle(fontSize: 18, color: Colors.black54), - ), - ), - ), - ), - ], - ), - ), - ), - // 비밀번호 오류 메시지 추가 - if (_passwordError != null) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - _passwordError!, - style: const TextStyle(color: Colors.red, fontSize: 12), - ), - ), - const SizedBox(height: 16), - Card( - elevation: 4, - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '비밀번호 확인:', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Expanded( - child: GestureDetector( - onTap: () { - setState(() { - isConfirmingPassword = true; - _confirmPasswordController.text = ''; - _confirmPasswordFocusNode.requestFocus(); - }); - }, - child: isConfirmingPassword - ? TextField( - controller: _confirmPasswordController, - focusNode: _confirmPasswordFocusNode, - obscureText: true, - onChanged: (value) { - setState(() { - confirmPassword = value; - }); - }, - onSubmitted: (newPassword) { - setState(() { - isConfirmingPassword = false; - _confirmPasswordFocusNode.unfocus(); - }); - }, - decoration: InputDecoration( - hintText: '비밀번호를 다시 입력해주세요', - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), - ), - ) - : Container( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - '*' * confirmPassword.length, - style: const TextStyle(fontSize: 18, color: Colors.black54), - ), - ), - ), - ), - ], - ), - ), - ), - // 비밀번호 일치 여부 안내 텍스트 추가 - const SizedBox(height: 8), - Text( - (new_user_pw == confirmPassword) - ? '비밀번호가 일치합니다.' - : (confirmPassword.isNotEmpty ? '비밀번호가 일치하지 않습니다.' : ''), - style: TextStyle( - fontSize: 16, - color: (new_user_pw == confirmPassword) ? Colors.green : Colors.red, - ), - ), - Card( - elevation: 4, - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '이메일:', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Expanded( - child: GestureDetector( - onTap: () { - setState(() { - isEditingEmail = true; - _emailController.text = user_email; - _emailFocusNode.requestFocus(); - }); - }, - child: isEditingEmail - ? TextField( - controller: _emailController, - focusNode: _emailFocusNode, - onChanged: (value) { - setState(() { - user_email = value; - _emailError = _isEmailValid(user_email) ? null : '올바른 이메일 형식을 입력해주세요.'; - }); - }, - onSubmitted: (newEmail) { - setState(() { - user_email = newEmail; - isEditingEmail = false; - _emailFocusNode.unfocus(); - }); - }, - decoration: InputDecoration( - hintText: '이메일을 입력하세요', - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), - ), - ) - : Container( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - user_email, - style: const TextStyle(fontSize: 18, color: Colors.black54), - ), - ), - ), - ), - ], - ), - ), - ), - // 이메일 오류 메시지 추가 - if (_emailError != null) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - _emailError!, - style: const TextStyle(color: Colors.red, fontSize: 12), - ), - ), - const SizedBox(height: 8), - Card( - elevation: 4, - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '소속:', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Expanded( - child: GestureDetector( - onTap: () { - setState(() { - isEditingDepartment = true; - _departmentController.text = user_department; - _departmentFocusNode.requestFocus(); - }); - }, - child: isEditingDepartment - ? TextField( - controller: _departmentController, - focusNode: _departmentFocusNode, - onChanged: (value) { - setState(() { - user_department = value; - }); - }, - onSubmitted: (newDepartment) { - setState(() { - user_department = newDepartment; - isEditingDepartment = false; - _departmentFocusNode.unfocus(); - }); - }, - decoration: InputDecoration( - hintText: '소속을 입력하세요', - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), - ), - ) - : Container( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - user_department, - style: const TextStyle(fontSize: 18, color: Colors.black54), - ), - ), - ), - ), - ], - ), - ), - ), - const SizedBox(height: 8), - // 소속 박스 아래에 텍스트 추가 - const SizedBox(height: 8), - const Text( - '프로필 이미지를 설정해주세요.', - style: TextStyle(fontSize: 16, color: Colors.black), - ), - const SizedBox(height: 16), // 여백 추가 - Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - GestureDetector( - onTap: () { - _selectImage(); - }, - child: Container( - width: 100, // 정사각형 가로 길이 - height: 100, // 정사각형 세로 길이 - decoration: BoxDecoration( - color: Colors.white, // 내부 색상 - border: Border.all(color: Colors.black), // 테두리 색상 - borderRadius: BorderRadius.circular(20), // 모서리 둥글게 - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), // 모서리 둥글게 - child: Image.network( - 'https://eldsoft.com:8097/images${user_profile_image}', // 프로필 이미지 URL - fit: BoxFit.cover, // 이미지 크기 조정 - errorBuilder: (context, error, stackTrace) { - return const Center(child: Text('이미지를 불러올 수 없습니다.')); // 오류 처리 + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isEditingNickname = true; + _nicknameController.text = user_nickname; + _nicknameFocusNode.requestFocus(); + }); }, + child: isEditingNickname + ? TextField( + controller: _nicknameController, + focusNode: _nicknameFocusNode, + onChanged: (newNickname) { + setState(() { + user_nickname = newNickname; + // 닉네임 패턴 검증 + if (!_isNicknameValidPattern(user_nickname)) { + // 패턴이 일치하지 않을 경우 안내 문구 표시 + _nicknameError = '닉네임은 2~20자 영문, 한글, 숫자만 사용할 수 있습니다.'; + } else { + _nicknameError = null; // 패턴이 일치하면 오류 메시지 초기화 + } + }); + }, + onSubmitted: (newNickname) { + setState(() { + user_nickname = newNickname; + isEditingNickname = false; + _nicknameFocusNode.unfocus(); + }); + }, + decoration: InputDecoration( + hintText: '닉네임을 입력하세요', + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + user_nickname, + style: const TextStyle(fontSize: 18, color: Colors.black54), + ), + ), + ), + ), + ], + ), + ), + ), + // 닉네임 오류 메시지 추가 + if (_nicknameError != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + _nicknameError!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), + const SizedBox(height: 16), + // 비밀번호 수정 카드 + _oauthType == 'idpw' ? Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '비밀번호:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isEditingPassword = true; + _passwordController.text = new_user_pw.replaceAll(RegExp(r'.'), '*'); + _passwordFocusNode.requestFocus(); + }); + }, + child: isEditingPassword + ? TextField( + controller: _passwordController, + focusNode: _passwordFocusNode, + obscureText: true, + onChanged: (newPassword) { + setState(() { + new_user_pw = newPassword; + _passwordError = _isPasswordValidPattern(new_user_pw) ? null : '비밀번호는 8~20자 영문과 숫자가 반드시 포함되어야 합니다.'; + }); + }, + onSubmitted: (newPassword) { + setState(() { + isEditingPassword = false; + _passwordFocusNode.unfocus(); + }); + }, + decoration: InputDecoration( + hintText: '비밀번호를 입력하세요', + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + '**********', + style: const TextStyle(fontSize: 18, color: Colors.black54), + ), + ), + ), + ), + ], + ), + ), + ) : Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + '비밀번호: 소셜 로그인 계정은 비밀번호를 수정할 수 없습니다.', + style: TextStyle(fontSize: 16, color: Colors.black54), + ), + ), + ), + // 비밀번호 오류 메시지 + if (_oauthType == 'idpw' && _passwordError != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + _passwordError!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), + // 비밀번호 확인 카드 + _oauthType == 'idpw' ? + const SizedBox(height: 16) + : const SizedBox(), + _oauthType == 'idpw' ? + Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '비밀번호 확인:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isConfirmingPassword = true; + _confirmPasswordController.text = ''; + _confirmPasswordFocusNode.requestFocus(); + }); + }, + child: isConfirmingPassword + ? TextField( + controller: _confirmPasswordController, + focusNode: _confirmPasswordFocusNode, + obscureText: true, + onChanged: (value) { + setState(() { + confirmPassword = value; + }); + }, + onSubmitted: (newPassword) { + setState(() { + isConfirmingPassword = false; + _confirmPasswordFocusNode.unfocus(); + }); + }, + decoration: InputDecoration( + hintText: '비밀번호를 다시 입력해주세요', + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + '*' * confirmPassword.length, + style: const TextStyle(fontSize: 18, color: Colors.black54), + ), + ), + ), + ), + ], + ), + ), + ) + : const SizedBox(), // 소셜 로그인에서는 비밀번호 확인 칸을 아예 숨김 + // 비밀번호 일치 여부 안내 텍스트 추가 + _oauthType == 'idpw' ? + const SizedBox(height: 8) + : const SizedBox(), + _oauthType == 'idpw' ? + Text( + (new_user_pw == confirmPassword) + ? '비밀번호가 일치합니다.' + : (confirmPassword.isNotEmpty ? '비밀번호가 일치하지 않습니다.' : ''), + style: TextStyle( + fontSize: 16, + color: (new_user_pw == confirmPassword) ? Colors.green : Colors.red, + ), + ) + : const SizedBox(), + // 이메일 카드 + _oauthType == 'idpw' ? + Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '이메일:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isEditingEmail = true; + _emailController.text = user_email; + _emailFocusNode.requestFocus(); + }); + }, + child: isEditingEmail + ? TextField( + controller: _emailController, + focusNode: _emailFocusNode, + onChanged: (value) { + setState(() { + user_email = value; + _emailError = _isEmailValid(user_email) ? null : '올바른 이메일 형식을 입력해주세요.'; + }); + }, + onSubmitted: (newEmail) { + setState(() { + user_email = newEmail; + isEditingEmail = false; + _emailFocusNode.unfocus(); + }); + }, + decoration: InputDecoration( + hintText: '이메일을 입력하세요', + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + user_email, + style: const TextStyle(fontSize: 18, color: Colors.black54), + ), + ), + ), + ), + ], + ), + ), + ) + : Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '이메일:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Expanded( + child: Text( + user_email, // 읽기 전용 + style: const TextStyle(fontSize: 18, color: Colors.black54), + ), + ), + ], + ), + ), + ), + // 이메일 오류 메시지 + if (_oauthType == 'idpw' && _emailError != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + _emailError!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), + const SizedBox(height: 8), + Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '소속:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isEditingDepartment = true; + _departmentController.text = user_department; + _departmentFocusNode.requestFocus(); + }); + }, + child: isEditingDepartment + ? TextField( + controller: _departmentController, + focusNode: _departmentFocusNode, + onChanged: (value) { + setState(() { + user_department = value; + }); + }, + onSubmitted: (newDepartment) { + setState(() { + user_department = newDepartment; + isEditingDepartment = false; + _departmentFocusNode.unfocus(); + }); + }, + decoration: InputDecoration( + hintText: '소속을 입력하세요', + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + user_department, + style: const TextStyle(fontSize: 18, color: Colors.black54), + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + // 소속 박스 아래에 텍스트 추가 + const SizedBox(height: 8), + const Text( + '프로필 이미지를 설정해주세요.', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + const SizedBox(height: 16), // 여백 추가 + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + _selectImage(); + }, + child: Container( + width: 100, // 정사각형 가로 길이 + height: 100, // 정사각형 세로 길이 + decoration: BoxDecoration( + color: Colors.white, // 내부 색상 + border: Border.all(color: Colors.black), // 테두리 색상 + borderRadius: BorderRadius.circular(20), // 모서리 둥글게 + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), // 모서리 둥글게 + child: Image.network( + 'https://eldsoft.com:8097/images${user_profile_image}', // 프로필 이미지 URL + fit: BoxFit.cover, // 이미지 크기 조정 + errorBuilder: (context, error, stackTrace) { + return const Center(child: Text('이미지를 불러올 수 없습니다.')); // 오류 처리 + }, + ), ), ), ), - ), - const SizedBox(height: 8), - const Text( - '이미지를 클릭하시면 새로 등록할 수 있습니다.', - textAlign: TextAlign.center, // 가운데 정렬 - style: TextStyle(fontSize: 12, color: Colors.blueGrey), // 색상 변경 - ), - ], - ), - ), - const SizedBox(height: 8), - const Text( - '다른 유저에게 자신을 소개해주세요.', - style: TextStyle(fontSize: 16, color: Colors.black), - ), - const SizedBox(height: 8), // 여백 추가 - TextField( - controller: _introduceController..text = user_introduce_myself, // 기본값 설정 - maxLines: 5, // 여러 줄 입력 가능 - decoration: InputDecoration( - hintText: '자신을 소개하는 내용을 입력하세요...', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), // 모서리 둥글게 - borderSide: const BorderSide(color: Colors.black), // 테두리 색상 + const SizedBox(height: 8), + const Text( + '이미지를 클릭하시면 새로 등록할 수 있습니다.', + textAlign: TextAlign.center, // 가운데 정렬 + style: TextStyle(fontSize: 12, color: Colors.blueGrey), // 색상 변경 + ), + ], ), - contentPadding: const EdgeInsets.all(10), // 패딩 추가 ), - onChanged: (value) { - setState(() { - user_introduce_myself = value; // 자기소개 내용 업데이트 - }); - }, - ), - const SizedBox(height: 30), // 여백 추가 - Center( - child: OutlinedButton( - onPressed: () { - // 수정하기 버튼 클릭 시 모달창 열기 - _showEditDialog(); // 모달창을 여는 메서드 호출 + const SizedBox(height: 8), + const Text( + '다른 유저에게 자신을 소개해주세요.', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + const SizedBox(height: 8), // 여백 추가 + TextField( + controller: _introduceController, // 기본값 설정 + maxLines: 5, // 여러 줄 입력 가능 + decoration: InputDecoration( + hintText: '자신을 소개하는 내용을 입력하세요...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), // 모서리 둥글게 + borderSide: const BorderSide(color: Colors.black), // 테두리 색상 + ), + contentPadding: const EdgeInsets.all(10), // 패딩 추가 + ), + onChanged: (value) { + setState(() { + user_introduce_myself = value; // 자기소개 내용 업데이트 + }); }, - style: OutlinedButton.styleFrom( - backgroundColor: Colors.white, // 버튼 배경색 - side: const BorderSide(color: Colors.black54, width: 1), // 테두리 색상 및 두께 - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40), // 모서리 둥글게 조정 + ), + const SizedBox(height: 30), // 여백 추가 + Center( + child: OutlinedButton( + onPressed: () { + // 수정하기 버튼 클릭 시 모달창 열기 + _showEditDialog(); // 모달창을 여는 메서드 호출 + }, + style: OutlinedButton.styleFrom( + backgroundColor: Colors.white, // 버튼 배경색 + side: const BorderSide(color: Colors.black54, width: 1), // 테두리 색상 및 두께 + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), // 모서리 둥글게 조정 + ), + padding: EdgeInsets.symmetric(vertical: 12 * scaleFactor, horizontal: 60 * scaleFactor), // 버튼 패딩 + ), + child: const Text( + '수정하기', + style: TextStyle(color: Colors.black, fontSize: 16), // 텍스트 색상 및 크기 ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60), // 버튼 패딩 - ), - child: const Text( - '수정하기', - style: TextStyle(color: Colors.black, fontSize: 16), // 텍스트 색상 및 크기 ), ), - ), - const SizedBox(height: 10), // 여백 추가 - Center( - child: OutlinedButton( - onPressed: () { - // 회원탈퇴 버튼 클릭 시 WithdrawalPage로 이동 - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const WithdrawalPage()), - ); - }, - style: OutlinedButton.styleFrom( - backgroundColor: Colors.white, // 버튼 배경색 - side: const BorderSide(color: Colors.black54, width: 1), // 테두리 색상 및 두께 - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40), // 모서리 둥글게 조정 + const SizedBox(height: 10), // 여백 추가 + Center( + child: OutlinedButton( + onPressed: () { + // 회원탈퇴 버튼 클릭 시 WithdrawalPage로 이동 + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const WithdrawalPage()), + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Colors.white, // 버튼 배경색 + side: const BorderSide(color: Colors.black54, width: 1), // 테두리 색상 및 두께 + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), // 모서리 둥글게 조정 + ), + padding: EdgeInsets.symmetric(vertical: 12 * scaleFactor, horizontal: 60 * scaleFactor), // 버튼 패딩 + ), + child: const Text( + '회원탈퇴', + style: TextStyle(color: Colors.black, fontSize: 16), // 텍스트 색상 및 크기 ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60), // 버튼 패딩 - ), - child: const Text( - '회원탈퇴', - style: TextStyle(color: Colors.black, fontSize: 16), // 텍스트 색상 및 크기 ), ), - ), - const SizedBox(height: 30), // 여백 추가 - ], + const SizedBox(height: 30), // 여백 추가 + ], + ), ), ), ), @@ -598,6 +688,9 @@ class _MyPageState extends State { Future _fetchUserInfo() async { SharedPreferences prefs = await SharedPreferences.getInstance(); String? authToken = prefs.getString('auth_token'); + setState(() { + _oauthType = prefs.getString('oauth_type') ?? 'idpw'; + }); final response = await Api.serverRequest( uri: '/user/myinfo', @@ -616,6 +709,8 @@ class _MyPageState extends State { user_department = jsonResponse['data']['department']; user_introduce_myself = jsonResponse['data']['introduce_myself']; user_profile_image = jsonResponse['data']['profile_img']; + // 자기소개 값 세팅 + _introduceController.text = user_introduce_myself; }); } else { showResponseDialog(context, '${jsonResponse['response_info']['msg_title']}', '${jsonResponse['response_info']['msg_content']}'); @@ -716,17 +811,22 @@ class _MyPageState extends State { // 모달창을 여는 메서드 추가 void _showEditDialog() { - // 현재 비밀번호와 confirmPassword 비교 - if (new_user_pw != '**********') { - if (!_isPasswordValidPattern(new_user_pw)) { - showResponseDialog(context, '수정하기 실패', '비밀번호 패턴을 확인해주세요.'); - return; - } - if (new_user_pw != confirmPassword) { - showResponseDialog(context, '수정하기 실패', '비밀번호가 일치하지 않습니다.'); - return; // 비밀번호가 일치하지 않으면 모달창을 열지 않음 + // 1) 비밀번호 패턴 검증은 'idpw'일 때만 수행 + if (_oauthType == 'idpw') { + // 새 비밀번호가 바뀌었을 경우에만 확인 + if (new_user_pw != '**********') { + if (!_isPasswordValidPattern(new_user_pw)) { + showResponseDialog(context, '수정하기 실패', '비밀번호 패턴을 확인해주세요.'); + return; + } + if (new_user_pw != confirmPassword) { + showResponseDialog(context, '수정하기 실패', '비밀번호가 일치하지 않습니다.'); + return; // 비밀번호가 일치하지 않으면 모달창 열지 않음 + } } } + + // 2) 이메일/닉네임 패턴 검증은 모든 경우에 대해 수행 if (!_isEmailValid(user_email)) { showResponseDialog(context, '수정하기 실패', '이메일 형식을 확인해주세요.'); return; @@ -741,43 +841,47 @@ class _MyPageState extends State { builder: (BuildContext context) { return AlertDialog( backgroundColor: Colors.white, // 모달창 배경색을 흰색으로 설정 - title: const Center( // 제목을 가운데 정렬 + title: Center( // 제목을 가운데 정렬 child: Text( '회원정보 수정', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), // 제목 스타일 + style: TextStyle(fontSize: 20 * scaleFactor, fontWeight: FontWeight.bold), // 제목 스타일 ), ), content: Column( mainAxisSize: MainAxisSize.min, // 내용의 크기를 최소화 children: [ - const Center( // 문구를 가운데 정렬 + Center( // 문구를 가운데 정렬 child: Text( '회원정보를 수정합니다.', - style: TextStyle(fontSize: 16), // 문구 스타일 + style: TextStyle(fontSize: 16 * scaleFactor), // 문구 스타일 ), ), const SizedBox(height: 8), // 여백 추가 - const Center( // 문구를 가운데 정렬 - child: Text( - '현재 비밀번호를 입력해주세요.', - style: TextStyle(fontSize: 16), // 문구 스타일 - ), - ), - const SizedBox(height: 8), // 여백 추가 - TextField( - obscureText: true, // 비밀번호 입력 시 텍스트 숨기기 - decoration: InputDecoration( - hintText: '현재 비밀번호', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), // 모서리 둥글게 - borderSide: const BorderSide(color: Colors.black), // 테두리 색상 + // --- 분기점 --- + if (_oauthType == 'idpw') ...[ + Center( + child: Text( + '현재 비밀번호를 입력해주세요.', + style: TextStyle(fontSize: 16 * scaleFactor), ), - contentPadding: const EdgeInsets.all(10), // 패딩 추가 ), - onChanged: (value) { - user_pw = value; // 입력한 비밀번호를 user_pw에 저장 - }, - ), + const SizedBox(height: 8), + TextField( + obscureText: true, + decoration: InputDecoration( + hintText: '현재 비밀번호', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.black), + ), + contentPadding: const EdgeInsets.all(10), + ), + onChanged: (value) { + user_pw = value; // 입력한 비밀번호를 user_pw에 저장 + }, + ), + ], + // --- 분기 끝 --- ], ), actions: [ @@ -785,7 +889,7 @@ class _MyPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 버튼을 양쪽 끝으로 배치 children: [ SizedBox( - width: 100, // 버튼의 너비를 고정 + width: 100 * buttonScaleFactor, // 버튼의 너비를 고정 child: ElevatedButton( onPressed: () async { // 수정하기 버튼 클릭 시 서버에 요청 @@ -795,17 +899,16 @@ class _MyPageState extends State { if (serverResponse['result'] == 'OK') { final serverResponse1 = serverResponse['response']; if (serverResponse1['result'] == 'OK') { - showResponseDialog(context, '수정하기 성공', '회원정보가 성공적으로 수정되었습니다.'); - Navigator.of(context).pop(); // 모달창 닫기 - Navigator.of(context).pop(); // 모달창 닫기 + await showResponseDialog(context, '수정하기 성공', '회원정보가 성공적으로 수정되었습니다.'); + Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (context) => const MyPage()), (route) => false); } else { - showResponseDialog(context, '${serverResponse1['response_info']['msg_title']}', '${serverResponse1['response_info']['msg_content']}'); + await showResponseDialog(context, '${serverResponse1['response_info']['msg_title']}', '${serverResponse1['response_info']['msg_content']}'); } } else { - showResponseDialog(context, '수정하기 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.'); + await showResponseDialog(context, '수정하기 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.'); } } catch (e) { - showResponseDialog(context, '수정하기 실패', e.toString()); + await showResponseDialog(context, '수정하기 실패', e.toString()); } }, style: ElevatedButton.styleFrom( @@ -815,14 +918,14 @@ class _MyPageState extends State { borderRadius: BorderRadius.circular(40), // 모서리 둥글게 조정 ), ), - child: const Text( - '수정하기', - style: TextStyle(color: Colors.black, fontSize: 14), // 텍스트 색상 및 크기 + child: Text( + '수정', + style: TextStyle(color: Colors.black, fontSize: 14 * buttonScaleFactor), // 텍스트 색상 및 크기 ), ), ), SizedBox( - width: 100, // 버튼의 너비를 고정 + width: 100 * buttonScaleFactor, // 버튼의 너비를 고정 child: ElevatedButton( onPressed: () { Navigator.of(context).pop(); // 모달창 닫기 @@ -834,9 +937,9 @@ class _MyPageState extends State { borderRadius: BorderRadius.circular(40), // 모서리 둥글게 조정 ), ), - child: const Text( + child: Text( '취소', - style: TextStyle(color: Colors.black, fontSize: 14), // 텍스트 색상 및 크기 + style: TextStyle(color: Colors.black, fontSize: 14 * buttonScaleFactor), // 텍스트 색상 및 크기 ), ), ), @@ -862,6 +965,7 @@ class _MyPageState extends State { "department": user_department, // 소속 "profile_img": user_profile_image, // 프로필 이미지 "introduce_myself": user_introduce_myself, // 자기소개 + "oauth_type": _oauthType, // 로그인 방법 }, ); @@ -905,17 +1009,17 @@ class _MyPageState extends State { if (serverResponse['result'] == 'OK') { final serverResponse1 = serverResponse['response']; if (serverResponse1['result'] == 'OK') { - showResponseDialog(context, '업로드 성공', '프로필 이미지가 성공적으로 업로드되었습니다.'); + await showResponseDialog(context, '업로드 성공', '프로필 이미지가 성공적으로 업로드되었습니다.'); // user_profile_image 값을 업데이트 (앞의 '/images' 제거) setState(() { user_profile_image = serverResponse1['data']['img_src'].replaceFirst('/images', ''); // 앞의 '/images' 제거 }); } else { - showResponseDialog(context, '${serverResponse1['response_info']['msg_title']}', '${serverResponse1['response_info']['msg_content']}'); + await showResponseDialog(context, '${serverResponse1['response_info']['msg_title']}', '${serverResponse1['response_info']['msg_content']}'); } } else { - showResponseDialog(context, '업로드 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.'); + await showResponseDialog(context, '업로드 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.'); } } } diff --git a/lib/views/user/withdrawal_page.dart b/lib/views/user/withdrawal_page.dart index 77d301f..77a5397 100644 --- a/lib/views/user/withdrawal_page.dart +++ b/lib/views/user/withdrawal_page.dart @@ -15,9 +15,8 @@ class WithdrawalPage extends StatefulWidget { } class _WithdrawalPageState extends State { - bool _isAgreed = false; // 체크박스 상태를 관리하는 변수 + bool _isAgreed = false; // 체크박스 상태 final TextEditingController _passwordController = TextEditingController(); // 비밀번호 입력 컨트롤러 - String _oauthType = 'idpw'; @override @@ -36,7 +35,10 @@ class _WithdrawalPageState extends State { @override Widget build(BuildContext context) { - // oauth_type == 'google' 인 경우 => 비밀번호 입력란 스킵 + // 화면 가로 길이 가져오기 + final screenWidth = MediaQuery.of(context).size.width; + + // oauth_type == 'google' 등인 경우 비밀번호 입력 무시 final isGoogleUser = (_oauthType.toLowerCase() == 'google'); return Scaffold( @@ -49,87 +51,105 @@ class _WithdrawalPageState extends State { onPressed: () => Navigator.pop(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - '회원탈퇴를 진행합니다.\n현재 비밀번호를 입력해주세요.', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 18), - ), - const SizedBox(height: 20), - // 구글 로그인 사용자면 비밀번호 입력 생략 - if (!isGoogleUser) ...[ - TextField( - controller: _passwordController, - obscureText: true, - decoration: InputDecoration( - hintText: '비밀번호 입력', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: Colors.black), + // 작은 화면에서도 스크롤 가능하도록 SingleChildScrollView 사용 + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + // Column이 화면을 넘어가면 스크롤되도록 + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + const Center( + child: Text( + '회원탈퇴를 진행합니다.\n현재 비밀번호를 입력해주세요.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), + ), + ), + const SizedBox(height: 20), + + // 구글 로그인 사용자는 비밀번호 입력란 생략 + if (!isGoogleUser) ...[ + TextField( + controller: _passwordController, + obscureText: true, + decoration: InputDecoration( + hintText: '비밀번호 입력', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.black), + ), + contentPadding: const EdgeInsets.all(10), + ), + ), + const SizedBox(height: 20), + ], + + Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black54, width: 1), + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.all(16.0), + child: const Text( + '[회원 탈퇴 안내]\n' + '회원 탈퇴를 진행하시겠습니까?\n' + ' - 회원 탈퇴 시 등록하신 모든 개인정보(ID, 비밀번호, 닉네임, 이메일 주소, 소속, 자기소개 등)는 즉시 삭제되며 복구가 불가능합니다.\n' + ' - 탈퇴 후 동일한 아이디로 재가입이 불가능할 수 있습니다.\n' + ' - 관련 법령에 따라 일정 기간 보관이 필요한 경우 해당 기간 동안 법령이 허용하는 범위 내에서만 보관됩니다.\n' + '탈퇴를 원하시면 아래의 "동의" 버튼을 눌러주시기 바랍니다.', + textAlign: TextAlign.left, + style: TextStyle(fontSize: 12), + ), + ), + const SizedBox(height: 20), + + Row( + children: [ + Checkbox( + value: _isAgreed, + activeColor: Colors.black, + checkColor: Colors.white, + onChanged: (value) { + setState(() { + _isAgreed = value ?? false; + }); + }, + ), + const Expanded( + child: Text( + '회원탈퇴에 동의합니다.', + style: TextStyle(fontSize: 16), + ), + ), + ], + ), + const SizedBox(height: 20), + + // "탈퇴하기" 버튼 + Center( + child: ElevatedButton( + onPressed: () { + _requestWithdrawal(_passwordController.text, _isAgreed); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black54, + // 화면 너비에 따라 버튼의 좌우 패딩 동적 조정 + padding: EdgeInsets.symmetric( + vertical: 12, + horizontal: screenWidth * 0.15, + ), + ), + child: const Text( + '탈퇴하기', + style: TextStyle(color: Colors.white, fontSize: 16), ), - contentPadding: const EdgeInsets.all(10), ), ), const SizedBox(height: 20), ], - Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black54, width: 1), // 테두리 설정 - borderRadius: BorderRadius.circular(10), // 모서리 둥글게 - ), - padding: const EdgeInsets.all(16.0), // 내부 여백 - child: const Text( - '[회원 탈퇴 안내]\n' - '회원 탈퇴를 진행하시겠습니까?\n' - ' - 회원 탈퇴 시 등록하신 모든 개인정보(ID, 비밀번호, 닉네임, 이메일 주소, 소속, 자기소개 등)는 즉시 삭제되며 복구가 불가능합니다.\n' - ' - 탈퇴 후 동일한 아이디로 재가입이 불가능할 수 있습니다.\n' - ' - 관련 법령에 따라 일정 기간 보관이 필요한 경우 해당 기간 동안 법령이 허용하는 범위 내에서만 보관됩니다.\n' - '탈퇴를 원하시면 아래의 "동의" 버튼을 눌러주시기 바랍니다.', - textAlign: TextAlign.left, - style: TextStyle(fontSize: 12), - ), - ), - const SizedBox(height: 20), - Row( - children: [ - Checkbox( - value: _isAgreed, // 체크박스 상태 - activeColor: Colors.black, // 체크박스가 활성화될 때의 색상 - checkColor: Colors.white, // 체크 표시 색상 - onChanged: (value) { - setState(() { - _isAgreed = value ?? false; // 체크박스 상태 변경 - }); - }, - ), - const Expanded( - child: Text( - '회원탈퇴에 동의합니다.', - style: TextStyle(fontSize: 16), - ), - ), - ], - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () { - // 탈퇴하기 버튼 클릭 시의 동작을 여기에 추가 - _requestWithdrawal(_passwordController.text, _isAgreed); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.black54, // 버튼 배경색 - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60), // 버튼 패딩 - ), - child: const Text( - '탈퇴하기', - style: TextStyle(color: Colors.white, fontSize: 16), // 텍스트 색상 및 크기 - ), - ), - ], + ), ), ), ); @@ -137,19 +157,17 @@ class _WithdrawalPageState extends State { Future _requestWithdrawal(String password, bool isAgreed) async { if (!isAgreed) { - // 체크박스가 체크되지 않은 경우 showResponseDialog(context, '회원탈퇴 동의 확인', '회원탈퇴 동의 체크가 필요합니다.'); return; } - // 구글 로그인 사용자면 비밀번호 입력 생략 + // 구글 로그인 사용자가 아닌데 비밀번호를 입력 안 했다면 if (_oauthType != 'google' && password.isEmpty) { - // 비밀번호가 입력되지 않은 경우 showResponseDialog(context, '비밀번호 확인', '비밀번호를 입력해야 합니다.'); return; } - // 구글 로그인으로 비밀번호값이 비어있다면 빈값으로 채워주기 + // 구글 로그인으로 비밀번호 값이 비어있다면 그냥 ''로 if (password.isEmpty) { password = ''; } @@ -157,7 +175,7 @@ class _WithdrawalPageState extends State { final response = await Api.serverRequest( uri: '/user/withdraw/user', body: { - 'user_pw': Utils.hashPassword(password), // 입력한 비밀번호 + 'user_pw': Utils.hashPassword(password), 'oauth_type': _oauthType, }, ); @@ -165,20 +183,23 @@ class _WithdrawalPageState extends State { if (response['result'] == 'OK') { final serverResponse = response['response']; if (serverResponse['result'] == 'OK') { - // 회원탈퇴 완료 후 로그인 페이지로 이동 + // 회원탈퇴 완료 시 토큰 제거 & 로그인 페이지로 이동 SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.remove('auth_token'); // 토큰 초기화 - + await prefs.remove('auth_token'); await showResponseDialog(context, '회원탈퇴 완료', '회원탈퇴가 완료되었습니다.'); Navigator.pushReplacement( context, - MaterialPageRoute(builder: (context) => const LoginPage()), // 로그인 페이지로 이동 + MaterialPageRoute(builder: (context) => const LoginPage()), ); } else { - showResponseDialog(context, serverResponse['response_info']['msg_title'], serverResponse['response_info']['msg_content']); + showResponseDialog( + context, + serverResponse['response_info']['msg_title'], + serverResponse['response_info']['msg_content'], + ); } } else { showResponseDialog(context, '회원탈퇴 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.'); } } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 03c8918..3c8aa76 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 77d4b13..959527a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: allscore_app description: "A new Flutter project." publish_to: 'none' -version: 1.0.0+1 +version: 1.0.3+4 environment: sdk: ^3.6.0 @@ -26,6 +26,7 @@ dependencies: fluttertoast: ^8.0.9 flutter_launcher_icons: ^0.12.0 flutter_native_splash: ^2.2.15 + auto_size_text: ^3.0.0 dev_dependencies: flutter_test: