From edf0444b717445a5c949037d124de005dfde1397 Mon Sep 17 00:00:00 2001 From: sihoo Date: Thu, 1 Oct 2026 05:52:57 +0000 Subject: [PATCH] =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EA=BE=B8?= =?UTF-8?q?=EB=AF=B8=EA=B8=B0=20=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=ED=85=8C=EB=91=90=EB=A6=AC/=EC=B2=B4=ED=97=98=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=B6=94=EA=B0=80,=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=97=94=ED=84=B0=20=EC=A7=80=EC=9B=90,?= =?UTF-8?q?=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=ED=99=80=EB=93=9C=20?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로필 꾸미기: 미리보기 중인 큰 타일의 테두리도 테마 색으로 바뀌게 수정, 미구매 템플릿에 "체험하기" 버튼을 따로 둬서 사기 전에도 눌러볼 수 있게 함 - 앱 전반의 확인/삭제/저장 같은 다이얼로그 기본 액션 버튼에 autofocus를 줘서 엔터 키로도 눌리게 함 (텍스트 입력이 있는 다이얼로그는 입력창 쪽에 autofocus + onSubmitted를 달아 입력 중 바로 엔터로 제출되게 처리) - HoldToRevealPasswordField 위젯 추가 - 눈 아이콘을 누르고 있는 동안만 비밀번호가 평문으로 보임. 로그인/간편인증/초기 비밀번호 변경 화면에 적용 --- lib/screens/change_password_screen.dart | 12 +-- lib/ui/admin_dashboard.dart | 1 + lib/ui/board_post_detail_screen.dart | 1 + lib/ui/device_checkout_ledger_page.dart | 2 + lib/ui/friends_screen.dart | 4 + lib/ui/group_detail_screen.dart | 2 + lib/ui/group_list_screen.dart | 3 + lib/ui/hold_to_reveal_password_field.dart | 67 +++++++++++++++ lib/ui/login_screen.dart | 95 ++++++++++----------- lib/ui/main_dashboard.dart | 9 ++ lib/ui/nfc_poccket_checkin_screen.dart | 3 + lib/ui/profile_customize_screen.dart | 58 ++++++++----- lib/ui/study_calendar_screen.dart | 1 + lib/ui/teacher_attendance_page.dart | 16 ++++ lib/ui/teacher_student_management_page.dart | 4 + 15 files changed, 203 insertions(+), 75 deletions(-) create mode 100644 lib/ui/hold_to_reveal_password_field.dart diff --git a/lib/screens/change_password_screen.dart b/lib/screens/change_password_screen.dart index b0d1b66..0837986 100644 --- a/lib/screens/change_password_screen.dart +++ b/lib/screens/change_password_screen.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart' as http; import '../config.dart'; import '../theme/app_palette.dart'; import '../ui/app_notice.dart'; +import '../ui/hold_to_reveal_password_field.dart'; import '../ui/login_screen.dart'; // 💡 main.dart 파일의 최하단(다른 클래스 중괄호 밖)에 붙여넣으세요. @@ -75,13 +76,12 @@ class _ChangePasswordScreenState extends State { style: TextStyle(color: Colors.grey), ), const SizedBox(height: 32), - TextField( + HoldToRevealPasswordField( controller: _pwController, - obscureText: true, - decoration: const InputDecoration( - labelText: '새로운 비밀번호 입력', - border: OutlineInputBorder(), - ), + autofocus: true, + labelText: '새로운 비밀번호 입력', + textInputAction: TextInputAction.done, + onSubmitted: (_) => _updatePassword(), ), const SizedBox(height: 16), SizedBox( diff --git a/lib/ui/admin_dashboard.dart b/lib/ui/admin_dashboard.dart index 4397c05..b307841 100644 --- a/lib/ui/admin_dashboard.dart +++ b/lib/ui/admin_dashboard.dart @@ -53,6 +53,7 @@ class _AdminDashboardState extends State { child: const Text('취소', style: TextStyle(color: Colors.grey)), ), ElevatedButton( + autofocus: true, style: ElevatedButton.styleFrom( backgroundColor: AppPalette.danger, ), diff --git a/lib/ui/board_post_detail_screen.dart b/lib/ui/board_post_detail_screen.dart index efc2bd0..4af2953 100644 --- a/lib/ui/board_post_detail_screen.dart +++ b/lib/ui/board_post_detail_screen.dart @@ -139,6 +139,7 @@ class _BoardPostDetailScreenState extends State { child: const Text('취소', style: TextStyle(color: Colors.grey)), ), TextButton( + autofocus: true, onPressed: () => Navigator.pop(dialogContext, true), child: const Text('삭제', style: TextStyle(color: AppPalette.danger)), ), diff --git a/lib/ui/device_checkout_ledger_page.dart b/lib/ui/device_checkout_ledger_page.dart index 2453ae3..b762e85 100644 --- a/lib/ui/device_checkout_ledger_page.dart +++ b/lib/ui/device_checkout_ledger_page.dart @@ -77,6 +77,7 @@ class _DeviceCheckoutLedgerPageState extends State { child: const Text('취소', style: TextStyle(color: Colors.grey)), ), ElevatedButton( + autofocus: true, style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, foregroundColor: AppPalette.paper, @@ -208,6 +209,7 @@ class _DeviceCheckoutLedgerPageState extends State { ), actions: [ TextButton( + autofocus: true, onPressed: () => Navigator.pop(dialogContext), child: const Text('닫기'), ), diff --git a/lib/ui/friends_screen.dart b/lib/ui/friends_screen.dart index 97878dd..4850169 100644 --- a/lib/ui/friends_screen.dart +++ b/lib/ui/friends_screen.dart @@ -47,7 +47,10 @@ class _FriendsScreenState extends State { title: const Text('친구 추가'), content: TextField( controller: idController, + autofocus: true, keyboardType: TextInputType.number, + textInputAction: TextInputAction.done, + onSubmitted: (_) => Navigator.pop(dialogContext, true), decoration: const InputDecoration( labelText: '친구 학번', border: OutlineInputBorder(), @@ -99,6 +102,7 @@ class _FriendsScreenState extends State { child: const Text('취소', style: TextStyle(color: Colors.grey)), ), TextButton( + autofocus: true, onPressed: () => Navigator.pop(dialogContext, true), child: const Text('삭제', style: TextStyle(color: AppPalette.danger)), ), diff --git a/lib/ui/group_detail_screen.dart b/lib/ui/group_detail_screen.dart index 3fe1bb9..7af9daa 100644 --- a/lib/ui/group_detail_screen.dart +++ b/lib/ui/group_detail_screen.dart @@ -158,6 +158,7 @@ class _GroupDetailScreenState extends State { child: const Text('취소', style: TextStyle(color: Colors.grey)), ), ElevatedButton( + autofocus: true, style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, foregroundColor: AppPalette.paper, @@ -195,6 +196,7 @@ class _GroupDetailScreenState extends State { child: const Text('취소', style: TextStyle(color: Colors.grey)), ), TextButton( + autofocus: true, onPressed: () => Navigator.pop(dialogContext, true), child: const Text( '나가기', diff --git a/lib/ui/group_list_screen.dart b/lib/ui/group_list_screen.dart index d4b2959..ea46fb7 100644 --- a/lib/ui/group_list_screen.dart +++ b/lib/ui/group_list_screen.dart @@ -57,7 +57,10 @@ class _GroupListScreenState extends State { children: [ TextField( controller: nameController, + autofocus: true, maxLength: 20, + textInputAction: TextInputAction.done, + onSubmitted: (_) => Navigator.pop(dialogContext, true), decoration: const InputDecoration( labelText: '그룹 이름', border: OutlineInputBorder(), diff --git a/lib/ui/hold_to_reveal_password_field.dart b/lib/ui/hold_to_reveal_password_field.dart new file mode 100644 index 0000000..15b2e4a --- /dev/null +++ b/lib/ui/hold_to_reveal_password_field.dart @@ -0,0 +1,67 @@ +// 👁️ 비밀번호 입력창 - 오른쪽 눈 아이콘을 누르고 있는 동안만 평문으로 보여준다. +// 토글형(눌러서 계속 보기) 대신 "홀드"로 만든 이유: 손을 떼는 순간 바로 다시 가려져서 +// 누가 옆에서 보더라도 짧게 확인만 하고 끝낼 수 있다. +import 'package:flutter/material.dart'; + +class HoldToRevealPasswordField extends StatefulWidget { + final TextEditingController controller; + final String labelText; + final String? hintText; + final IconData prefixIcon; + final bool autofocus; + final TextInputType? keyboardType; + final TextInputAction? textInputAction; + final ValueChanged? onSubmitted; + + const HoldToRevealPasswordField({ + super.key, + required this.controller, + required this.labelText, + this.hintText, + this.prefixIcon = Icons.lock, + this.autofocus = false, + this.keyboardType, + this.textInputAction, + this.onSubmitted, + }); + + @override + State createState() => + _HoldToRevealPasswordFieldState(); +} + +class _HoldToRevealPasswordFieldState extends State { + bool _revealed = false; + + void _setRevealed(bool value) { + if (_revealed != value) setState(() => _revealed = value); + } + + @override + Widget build(BuildContext context) { + return TextField( + controller: widget.controller, + obscureText: !_revealed, + autofocus: widget.autofocus, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + onSubmitted: widget.onSubmitted, + decoration: InputDecoration( + labelText: widget.labelText, + hintText: widget.hintText, + border: const OutlineInputBorder(), + prefixIcon: Icon(widget.prefixIcon), + suffixIcon: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: (_) => _setRevealed(true), + onTapUp: (_) => _setRevealed(false), + onTapCancel: () => _setRevealed(false), + child: Icon( + _revealed ? Icons.visibility_rounded : Icons.visibility_off_rounded, + color: Colors.grey, + ), + ), + ), + ); + } +} diff --git a/lib/ui/login_screen.dart b/lib/ui/login_screen.dart index 7075f5f..4d14746 100644 --- a/lib/ui/login_screen.dart +++ b/lib/ui/login_screen.dart @@ -6,6 +6,7 @@ import '../function/login_controller.dart'; import '../function/session_store.dart'; import '../theme/app_palette.dart'; import 'app_notice.dart'; +import 'hold_to_reveal_password_field.dart'; import 'main_dashboard.dart'; import 'teacher_register_screen.dart'; @@ -112,6 +113,36 @@ class _LoginScreenState extends State { return StatefulBuilder( builder: (context, setDialogState) { + Future submit() async { + String newPassword = newPwController.text.trim(); + if (newPassword.isEmpty) { + AppNotice.show(context, '새 비밀번호를 입력해 주세요.'); + return; + } + + setDialogState(() => isUpdating = true); + + final (success, message) = await _controller.changePassword( + studentId: studentId, + newPassword: newPassword, + ); + setDialogState(() => isUpdating = false); + + if (success) { + Navigator.pop(dialogContext); + AppNotice.show(context, message); + _navigateBasedOnRole( + role, + studentId, + name, + isDeviceMatched, + grade, + ); + } else { + AppNotice.show(context, message); + } + } + return AlertDialog( title: const Text( '초기 비밀번호 변경', @@ -125,14 +156,13 @@ class _LoginScreenState extends State { style: TextStyle(color: AppPalette.danger), ), const SizedBox(height: 16), - TextField( + HoldToRevealPasswordField( controller: newPwController, - obscureText: true, - decoration: const InputDecoration( - labelText: '새 비밀번호', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.lock_reset), - ), + autofocus: true, + labelText: '새 비밀번호', + prefixIcon: Icons.lock_reset, + textInputAction: TextInputAction.done, + onSubmitted: (_) => submit(), ), ], ), @@ -147,36 +177,7 @@ class _LoginScreenState extends State { backgroundColor: AppPalette.ink, foregroundColor: Colors.white, ), - onPressed: () async { - String newPassword = newPwController.text.trim(); - if (newPassword.isEmpty) { - AppNotice.show(context, '새 비밀번호를 입력해 주세요.'); - return; - } - - setDialogState(() => isUpdating = true); - - final (success, message) = await _controller - .changePassword( - studentId: studentId, - newPassword: newPassword, - ); - setDialogState(() => isUpdating = false); - - if (success) { - Navigator.pop(dialogContext); - AppNotice.show(context, message); - _navigateBasedOnRole( - role, - studentId, - name, - isDeviceMatched, - grade, - ); - } else { - AppNotice.show(context, message); - } - }, + onPressed: submit, child: const Text('변경하고 시작하기'), ), ], @@ -234,6 +235,7 @@ class _LoginScreenState extends State { content: Text(message), actions: [ TextButton( + autofocus: true, onPressed: () => Navigator.pop(ctx), child: const Text('확인'), ), @@ -254,9 +256,11 @@ class _LoginScreenState extends State { builder: (BuildContext dialogContext) { return AlertDialog( title: Text('$title 권한 인증 (기존 방식)'), - content: TextField( + content: HoldToRevealPasswordField( controller: passwordController, - obscureText: true, + autofocus: true, + labelText: '비밀번호', + hintText: '비밀번호 4자리를 입력하세요', keyboardType: TextInputType.number, textInputAction: TextInputAction.done, onSubmitted: (value) { @@ -270,10 +274,6 @@ class _LoginScreenState extends State { AppNotice.show(context, '비밀번호가 올바르지 않습니다.'); } }, - decoration: const InputDecoration( - hintText: '비밀번호 4자리를 입력하세요', - border: OutlineInputBorder(), - ), ), actions: [ TextButton( @@ -341,16 +341,11 @@ class _LoginScreenState extends State { ), ), const SizedBox(height: 16), - TextField( + HoldToRevealPasswordField( controller: _pwController, - obscureText: true, + labelText: '비밀번호', textInputAction: TextInputAction.done, onSubmitted: (_) => _login(), - decoration: const InputDecoration( - labelText: '비밀번호', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.lock), - ), ), const SizedBox(height: 8), InkWell( diff --git a/lib/ui/main_dashboard.dart b/lib/ui/main_dashboard.dart index 7f940a2..8a2aa3c 100644 --- a/lib/ui/main_dashboard.dart +++ b/lib/ui/main_dashboard.dart @@ -157,6 +157,15 @@ class _MainDashboardState extends State { const SizedBox(height: 16), TextField( controller: idController, + autofocus: true, + textInputAction: TextInputAction.done, + onSubmitted: (value) { + final inputId = value.trim(); + if (inputId.isNotEmpty) { + Navigator.pop(context); + _deleteUser(inputId); + } + }, decoration: InputDecoration( labelText: '학번 또는 교직원 번호', hintText: '예: 201101 또는 T1001', diff --git a/lib/ui/nfc_poccket_checkin_screen.dart b/lib/ui/nfc_poccket_checkin_screen.dart index ce57136..55ae3d4 100644 --- a/lib/ui/nfc_poccket_checkin_screen.dart +++ b/lib/ui/nfc_poccket_checkin_screen.dart @@ -86,6 +86,7 @@ class _NfcPocketCheckInScreenState extends State { ), actions: [ TextButton( + autofocus: true, onPressed: () => Navigator.pop(context), child: const Text("확인"), ), @@ -114,6 +115,7 @@ class _NfcPocketCheckInScreenState extends State { ), actions: [ TextButton( + autofocus: true, onPressed: () { Navigator.pop(context); _controller.beginIosProximityWatch(pocketNumber); @@ -139,6 +141,7 @@ class _NfcPocketCheckInScreenState extends State { ), actions: [ TextButton( + autofocus: true, onPressed: () => Navigator.pop(context), child: const Text("확인"), ), diff --git a/lib/ui/profile_customize_screen.dart b/lib/ui/profile_customize_screen.dart index 9105a92..3e36322 100644 --- a/lib/ui/profile_customize_screen.dart +++ b/lib/ui/profile_customize_screen.dart @@ -295,7 +295,10 @@ class _CategoryTile extends StatelessWidget { ) : null, border: Border.all( - color: selected ? AppPalette.ink : AppPalette.sage, + color: hasPreview + ? previewSecondary! + : (selected ? AppPalette.ink : AppPalette.sage), + width: hasPreview ? 2 : 1, ), ), child: Column( @@ -517,23 +520,38 @@ class _TemplateTile extends StatelessWidget { style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), const SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: template.equipped - ? OutlinedButton( - onPressed: null, - style: OutlinedButton.styleFrom( - foregroundColor: AppPalette.approved, - side: const BorderSide(color: AppPalette.approved), - ), - child: const Text('장착중'), - ) - : template.owned - ? OutlinedButton( - onPressed: onEquip, - child: const Text('장착하기'), - ) - : ElevatedButton( + if (template.equipped) + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: null, + style: OutlinedButton.styleFrom( + foregroundColor: AppPalette.approved, + side: const BorderSide(color: AppPalette.approved), + ), + child: const Text('장착중'), + ), + ) + else if (template.owned) + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: onEquip, + child: const Text('장착하기'), + ), + ) + else + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: onPreview, + child: const Text('체험하기'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( onPressed: canAfford ? onPurchase : null, style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, @@ -542,7 +560,9 @@ class _TemplateTile extends StatelessWidget { ), child: const Text('구매하기'), ), - ), + ), + ], + ), ], ), ), diff --git a/lib/ui/study_calendar_screen.dart b/lib/ui/study_calendar_screen.dart index 1ad57f2..4fb54df 100644 --- a/lib/ui/study_calendar_screen.dart +++ b/lib/ui/study_calendar_screen.dart @@ -58,6 +58,7 @@ class _StudyCalendarScreenState extends State { ), actions: [ TextButton( + autofocus: true, onPressed: () => Navigator.pop(dialogContext), child: const Text('닫기'), ), diff --git a/lib/ui/teacher_attendance_page.dart b/lib/ui/teacher_attendance_page.dart index 444964a..c8311bf 100644 --- a/lib/ui/teacher_attendance_page.dart +++ b/lib/ui/teacher_attendance_page.dart @@ -75,6 +75,7 @@ class _TeacherAttendancePageState extends State { child: const Text('취소'), ), ElevatedButton( + autofocus: true, onPressed: () { Navigator.pop(context); _runAction(() => _controller.resetTestPermissions()); @@ -98,7 +99,14 @@ class _TeacherAttendancePageState extends State { title: Text('$studentName 학생 반출 허용'), content: TextField( controller: controller, + autofocus: true, keyboardType: TextInputType.number, + textInputAction: TextInputAction.done, + onSubmitted: (value) { + final minutes = int.tryParse(value.trim()) ?? 5; + Navigator.pop(context); + _runAction(() => _controller.allowRemoval(studentId, minutes)); + }, decoration: const InputDecoration( labelText: '허용 시간 (분)', border: OutlineInputBorder(), @@ -139,7 +147,14 @@ class _TeacherAttendancePageState extends State { const SizedBox(height: 16), TextField( controller: controller, + autofocus: true, keyboardType: TextInputType.number, + textInputAction: TextInputAction.done, + onSubmitted: (value) { + final minutes = int.tryParse(value.trim()) ?? 10; + Navigator.pop(context); + _runAction(() => _controller.setPermissionWindow(minutes)); + }, decoration: const InputDecoration( labelText: '지금부터 허용 시간 (분)', border: OutlineInputBorder(), @@ -180,6 +195,7 @@ class _TeacherAttendancePageState extends State { child: const Text('취소'), ), ElevatedButton( + autofocus: true, onPressed: () { Navigator.pop(context); _runAction(() => _controller.dismissAll()); diff --git a/lib/ui/teacher_student_management_page.dart b/lib/ui/teacher_student_management_page.dart index 520e20b..0913eed 100644 --- a/lib/ui/teacher_student_management_page.dart +++ b/lib/ui/teacher_student_management_page.dart @@ -150,6 +150,7 @@ class _TeacherStudentManagementPageState child: const Text('취소', style: TextStyle(color: Colors.grey)), ), ElevatedButton( + autofocus: true, onPressed: () { Navigator.pop(context); _runBulkImport(rows); @@ -236,6 +237,7 @@ class _TeacherStudentManagementPageState ), actions: [ TextButton( + autofocus: true, onPressed: () => Navigator.pop(context), child: const Text('확인'), ), @@ -296,6 +298,7 @@ class _TeacherStudentManagementPageState child: const Text('취소', style: TextStyle(color: Colors.grey)), ), ElevatedButton( + autofocus: true, style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, foregroundColor: AppPalette.paper, @@ -341,6 +344,7 @@ class _TeacherStudentManagementPageState child: const Text('취소', style: TextStyle(color: Colors.grey)), ), ElevatedButton( + autofocus: true, style: ElevatedButton.styleFrom( backgroundColor: AppPalette.danger, foregroundColor: Colors.white,