diff --git a/analysis_options.yaml b/analysis_options.yaml index 5f77254..7ea0814 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -14,6 +14,9 @@ analyzer: # iOS 빌드 시 SwiftPM 이 체크아웃한 서드파티 예제 dart 파일이 # 로컬 analyze 에 오류로 잡히는 것을 제외(CI 는 build/ 가 없어 무관). - build/** + language: + strict-casts: true + strict-raw-types: true linter: # The lint rules applied to this project can be customized in the @@ -27,8 +30,13 @@ linter: # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + # flutter_lints 기본 세트에 더해 실질 버그를 잡는 규칙 위주로 활성화. + unawaited_futures: true # await 누락(무시 의도면 unawaited() 명시) + cancel_subscriptions: true # StreamSubscription 해제 누락 + avoid_slow_async_io: true # 동기 IO 사용 + only_throw_errors: true # 문자열 throw 금지 + prefer_single_quotes: true + directives_ordering: true # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/lib/main.dart b/lib/main.dart index ba4a650..dbf5cfe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,34 +1,36 @@ import 'dart:async'; -import 'env.dart'; -import 'motion/motion.dart'; -import 'firebase_options.dart'; + +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:firebase_core/firebase_core.dart'; import 'package:flutter_naver_map/flutter_naver_map.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'theme/app_theme.dart'; -import 'screen/welcome_screen.dart'; -import 'screen/main_screen.dart'; -import 'screen/admin/admin_home_screen.dart'; + +import 'env.dart'; +import 'firebase_options.dart'; +import 'motion/motion.dart'; import 'screen/activity_screens.dart' show MyReviewsScreen; +import 'screen/admin/admin_home_screen.dart'; import 'screen/chat_room_screen.dart'; import 'screen/guardian_invites_screen.dart'; +import 'screen/main_screen.dart'; import 'screen/notifications_screen.dart'; import 'screen/post_detail_screen.dart'; import 'screen/review_detail_screen.dart'; import 'screen/user_profile_screen.dart'; +import 'screen/welcome_screen.dart'; import 'services/chat_repository.dart'; import 'services/community_repository.dart'; import 'services/facility_review_repository.dart'; -import 'services/session.dart'; -import 'widgets/review_cards.dart'; +import 'services/keyboard_barrier.dart'; import 'services/local_notice_service.dart'; import 'services/push_service.dart'; import 'services/realtime_service.dart'; -import 'services/keyboard_barrier.dart'; +import 'services/session.dart'; import 'services/theme_controller.dart'; +import 'theme/app_theme.dart'; import 'widgets/app_toast.dart'; +import 'widgets/review_cards.dart'; /// 강제 로그아웃(세션 무효화) 시 라우팅·안내를 위한 전역 키. final GlobalKey navigatorKey = GlobalKey(); diff --git a/lib/motion/motion.dart b/lib/motion/motion.dart index 06a635e..1988dd3 100644 --- a/lib/motion/motion.dart +++ b/lib/motion/motion.dart @@ -8,9 +8,9 @@ library; export 'app_motion.dart'; -export 'pressable.dart'; -export 'entrance.dart'; -export 'spring_page_route.dart'; export 'collapse_route.dart'; +export 'entrance.dart'; export 'heart_burst.dart'; +export 'pressable.dart'; +export 'spring_page_route.dart'; export 'springy_nav_bar.dart'; diff --git a/lib/motion/springy_nav_bar.dart b/lib/motion/springy_nav_bar.dart index e7489e0..90561c4 100644 --- a/lib/motion/springy_nav_bar.dart +++ b/lib/motion/springy_nav_bar.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'app_motion.dart'; + import '../theme/app_palette.dart'; +import 'app_motion.dart'; class SpringyNavItem { const SpringyNavItem({required this.icon, IconData? activeIcon}) diff --git a/lib/screen/activity_screens.dart b/lib/screen/activity_screens.dart index 0725b09..9f69b19 100644 --- a/lib/screen/activity_screens.dart +++ b/lib/screen/activity_screens.dart @@ -1,10 +1,12 @@ +import 'dart:async'; import 'package:flutter/material.dart'; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show categoryLabel, timeAgo; import '../data/review_categories.dart'; +import '../motion/motion.dart'; import '../services/activity_repository.dart'; import '../services/session.dart'; +import '../theme/app_palette.dart'; import 'review_write_screen.dart'; DateTime? _date(dynamic v) => @@ -345,7 +347,7 @@ class _MyAppointmentsScreenState extends State { ), ), ); - if (done == true) _load(); + if (done == true) unawaited(_load()); } void _toast(String m) { diff --git a/lib/screen/admin/admin_broadcast_screen.dart b/lib/screen/admin/admin_broadcast_screen.dart index fd82e44..21505a2 100644 --- a/lib/screen/admin/admin_broadcast_screen.dart +++ b/lib/screen/admin/admin_broadcast_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../../theme/app_palette.dart'; + import '../../services/admin_repository.dart'; +import '../../theme/app_palette.dart'; import 'admin_theme.dart'; /// 전체 공지 발송 — 탈퇴자를 제외한 전 회원에게 system_notice 알림(인앱+푸시)을 보낸다. diff --git a/lib/screen/admin/admin_business_screen.dart b/lib/screen/admin/admin_business_screen.dart index e0db97c..5c31f9b 100644 --- a/lib/screen/admin/admin_business_screen.dart +++ b/lib/screen/admin/admin_business_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -181,7 +182,7 @@ class _AdminBusinessScreenState extends State { ), builder: (_) => _DetailSheet(app: a), ); - if (changed == true) _load(); + if (changed == true) unawaited(_load()); } void _toast(String msg) { diff --git a/lib/screen/admin/admin_chat_history_screen.dart b/lib/screen/admin/admin_chat_history_screen.dart index 8603e90..9b0ae0c 100644 --- a/lib/screen/admin/admin_chat_history_screen.dart +++ b/lib/screen/admin/admin_chat_history_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../../theme/app_palette.dart'; + import '../../services/admin_repository.dart'; +import '../../theme/app_palette.dart'; import 'admin_theme.dart'; /// 관리자 — 채팅방 대화 내역(삭제된 메시지 포함) 조회. diff --git a/lib/screen/admin/admin_home_screen.dart b/lib/screen/admin/admin_home_screen.dart index 8c81925..fd811be 100644 --- a/lib/screen/admin/admin_home_screen.dart +++ b/lib/screen/admin/admin_home_screen.dart @@ -1,20 +1,21 @@ import 'package:flutter/material.dart'; + import '../../motion/motion.dart'; -import '../../theme/app_palette.dart'; import '../../services/admin_repository.dart'; import '../../services/auth_service.dart'; import '../../services/session.dart'; -import '../welcome_screen.dart'; +import '../../theme/app_palette.dart'; import '../change_password_screen.dart'; -import 'admin_theme.dart'; -import 'admin_business_screen.dart'; -import 'admin_users_screen.dart'; -import 'admin_reports_screen.dart'; -import 'admin_posts_screen.dart'; +import '../welcome_screen.dart'; import 'admin_broadcast_screen.dart'; +import 'admin_business_screen.dart'; import 'admin_inquiries_screen.dart'; import 'admin_logs_screen.dart'; import 'admin_metrics_screen.dart'; +import 'admin_posts_screen.dart'; +import 'admin_reports_screen.dart'; +import 'admin_theme.dart'; +import 'admin_users_screen.dart'; /// 관리자 홈 — 대시보드 + 관리 메뉴. /// 관리자(user_type='admin') 로 로그인하면 일반 화면 대신 이 화면으로 진입한다. diff --git a/lib/screen/admin/admin_inquiries_screen.dart b/lib/screen/admin/admin_inquiries_screen.dart index 3b6e2e8..721a023 100644 --- a/lib/screen/admin/admin_inquiries_screen.dart +++ b/lib/screen/admin/admin_inquiries_screen.dart @@ -1,9 +1,11 @@ +import 'dart:async'; import 'package:flutter/material.dart'; -import '../../motion/motion.dart'; -import '../../theme/app_palette.dart'; + import '../../data/mock_data.dart' show timeAgo; +import '../../motion/motion.dart'; import '../../services/admin_repository.dart'; import '../../services/chat_repository.dart'; +import '../../theme/app_palette.dart'; import '../chat_room_screen.dart'; import 'admin_theme.dart'; @@ -69,7 +71,7 @@ class _AdminInquiriesScreenState extends State { context, AppPageRoute(builder: (_) => ChatRoomScreen(room: room)), ); - if (mounted) _load(); + if (mounted) unawaited(_load()); } catch (_) { _toast('문의방을 열지 못했어요'); } finally { diff --git a/lib/screen/admin/admin_logs_screen.dart b/lib/screen/admin/admin_logs_screen.dart index 8fea6d9..6df180a 100644 --- a/lib/screen/admin/admin_logs_screen.dart +++ b/lib/screen/admin/admin_logs_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import '../../theme/app_palette.dart'; + import '../../data/mock_data.dart' show timeAgo; import '../../services/admin_repository.dart'; +import '../../theme/app_palette.dart'; import 'admin_theme.dart'; /// 감사 로그 — 관리자 조치 이력(정지·숨김·삭제·신고처리 등). diff --git a/lib/screen/admin/admin_metrics_screen.dart b/lib/screen/admin/admin_metrics_screen.dart index 24d134a..14a3e2e 100644 --- a/lib/screen/admin/admin_metrics_screen.dart +++ b/lib/screen/admin/admin_metrics_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../../theme/app_palette.dart'; + import '../../services/admin_repository.dart'; +import '../../theme/app_palette.dart'; import 'admin_theme.dart'; /// 관리자 운영 지표·비용 화면. diff --git a/lib/screen/admin/admin_posts_screen.dart b/lib/screen/admin/admin_posts_screen.dart index 8da78b8..12641ae 100644 --- a/lib/screen/admin/admin_posts_screen.dart +++ b/lib/screen/admin/admin_posts_screen.dart @@ -1,9 +1,11 @@ import 'dart:async'; -import '../../motion/motion.dart'; + import 'package:flutter/material.dart'; -import '../../theme/app_palette.dart'; + import '../../data/mock_data.dart' show timeAgo, categoryLabel; +import '../../motion/motion.dart'; import '../../services/admin_repository.dart'; +import '../../theme/app_palette.dart'; import 'admin_theme.dart'; /// 게시글/댓글 관리 — 게시글 검색/조회 + 숨김·삭제, 댓글 관리로 이동. diff --git a/lib/screen/admin/admin_report_detail_screen.dart b/lib/screen/admin/admin_report_detail_screen.dart index 3c92dd8..33833f6 100644 --- a/lib/screen/admin/admin_report_detail_screen.dart +++ b/lib/screen/admin/admin_report_detail_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import '../../theme/app_palette.dart'; + import '../../data/mock_data.dart' show timeAgo; import '../../services/admin_repository.dart'; +import '../../theme/app_palette.dart'; import 'admin_chat_history_screen.dart'; import 'admin_theme.dart'; diff --git a/lib/screen/admin/admin_reports_screen.dart b/lib/screen/admin/admin_reports_screen.dart index c1b76a8..4b8897a 100644 --- a/lib/screen/admin/admin_reports_screen.dart +++ b/lib/screen/admin/admin_reports_screen.dart @@ -1,10 +1,12 @@ +import 'dart:async'; import 'package:flutter/material.dart'; -import '../../motion/motion.dart'; -import '../../theme/app_palette.dart'; + import '../../data/mock_data.dart' show timeAgo; +import '../../motion/motion.dart'; import '../../services/admin_repository.dart'; -import 'admin_theme.dart'; +import '../../theme/app_palette.dart'; import 'admin_report_detail_screen.dart'; +import 'admin_theme.dart'; /// 신고 처리 — 미처리/전체 신고 조회 + 상태 변경(검토중/처리완료/반려). class AdminReportsScreen extends StatefulWidget { @@ -163,7 +165,7 @@ class _AdminReportsScreenState extends State { builder: (_) => AdminReportDetailScreen(report: _items[i]), ), ); - if (mounted) _load(); + if (mounted) unawaited(_load()); }, child: _ReportCard( report: _items[i], diff --git a/lib/screen/admin/admin_users_screen.dart b/lib/screen/admin/admin_users_screen.dart index cc74dd7..ec28087 100644 --- a/lib/screen/admin/admin_users_screen.dart +++ b/lib/screen/admin/admin_users_screen.dart @@ -1,9 +1,11 @@ import 'dart:async'; + import 'package:flutter/material.dart'; -import '../../theme/app_palette.dart'; + import '../../data/mock_data.dart' show timeAgo; import '../../services/admin_repository.dart'; import '../../services/session.dart'; +import '../../theme/app_palette.dart'; import 'admin_theme.dart'; /// 회원 관리 — 검색/조회 + 상태 변경(정지/휴면/활성). diff --git a/lib/screen/applicants_screen.dart b/lib/screen/applicants_screen.dart index 72d9968..ddf8872 100644 --- a/lib/screen/applicants_screen.dart +++ b/lib/screen/applicants_screen.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show timeAgo; import '../models/community.dart'; import '../services/activity_repository.dart'; import '../services/chat_launcher.dart'; +import '../theme/app_palette.dart'; /// 지원자 목록 — 글 작성자 또는 공동보호자가 지원자를 확인하고 1명을 수락(선택)한다. /// 수락 시 DB 트리거가 약속을 만들고 나머지 지원자는 자동 거절된다. diff --git a/lib/screen/auth/login_screen.dart b/lib/screen/auth/login_screen.dart index afa05bc..ae65686 100644 --- a/lib/screen/auth/login_screen.dart +++ b/lib/screen/auth/login_screen.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; + import '../../motion/motion.dart'; -import '../../theme/app_palette.dart'; import '../../services/auth_service.dart'; import '../../services/session.dart'; -import '../main_screen.dart'; +import '../../theme/app_palette.dart'; import '../admin/admin_home_screen.dart'; -import 'signup_phone_screen.dart'; +import '../main_screen.dart'; import 'reset_password_screen.dart'; +import 'signup_phone_screen.dart'; /// 로그인 — 아이디/비밀번호 기반. class LoginScreen extends StatefulWidget { diff --git a/lib/screen/auth/reset_password_screen.dart b/lib/screen/auth/reset_password_screen.dart index ccdb0f0..2d4e6b1 100644 --- a/lib/screen/auth/reset_password_screen.dart +++ b/lib/screen/auth/reset_password_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import '../../theme/app_palette.dart'; + import '../../services/phone_auth_service.dart'; +import '../../theme/app_palette.dart'; /// 비밀번호 재설정 — 전화 OTP 기반. /// 1) 전화번호 입력 → SMS 코드 발송 (phone_verifications.purpose='password_reset') diff --git a/lib/screen/auth/signup_phone_screen.dart b/lib/screen/auth/signup_phone_screen.dart index 529c669..48f83bd 100644 --- a/lib/screen/auth/signup_phone_screen.dart +++ b/lib/screen/auth/signup_phone_screen.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; -import '../../motion/motion.dart'; import 'package:flutter/services.dart'; -import '../../theme/app_palette.dart'; -import '../../services/phone_auth_service.dart'; + +import '../../main.dart' show navigatorKey; +import '../../motion/motion.dart'; import '../../services/auth_service.dart'; +import '../../services/phone_auth_service.dart'; +import '../../theme/app_palette.dart'; +import '../business_register_screen.dart'; import '../main_screen.dart'; import '../pet_edit_screen.dart'; -import '../business_register_screen.dart'; import '../terms_screen.dart'; import 'login_screen.dart'; -import '../../main.dart' show navigatorKey; /// 회원가입 — 전화 OTP 기반 다단계 흐름 (0025 §1.3). /// 1) 약관 동의 (필수 전부 동의해야 다음 진행 가능) diff --git a/lib/screen/blocked_users_screen.dart b/lib/screen/blocked_users_screen.dart index 7b57db1..c53e36a 100644 --- a/lib/screen/blocked_users_screen.dart +++ b/lib/screen/blocked_users_screen.dart @@ -1,6 +1,8 @@ +import 'dart:async'; import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; + import '../services/activity_repository.dart'; +import '../theme/app_palette.dart'; /// 차단 사용자 관리 — 목록 + 차단 해제. class BlockedUsersScreen extends StatefulWidget { @@ -39,7 +41,7 @@ class _BlockedUsersScreenState extends State { Future _unblock(Map item) async { try { await _repo.unblock(item['blocked_id'] as String); - _load(); + unawaited(_load()); } catch (_) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/screen/change_password_screen.dart b/lib/screen/change_password_screen.dart index f069ac3..61eaa83 100644 --- a/lib/screen/change_password_screen.dart +++ b/lib/screen/change_password_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; + import '../services/auth_service.dart'; +import '../theme/app_palette.dart'; /// 비밀번호 변경 — 현재 비밀번호 확인 후 새 비밀번호로 변경. class ChangePasswordScreen extends StatefulWidget { diff --git a/lib/screen/chat_room_screen.dart b/lib/screen/chat_room_screen.dart index 766c77d..e1e59b3 100644 --- a/lib/screen/chat_room_screen.dart +++ b/lib/screen/chat_room_screen.dart @@ -1,14 +1,16 @@ import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show SystemUiOverlayStyle; import 'package:supabase_flutter/supabase_flutter.dart'; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show timeAgo; import '../models/chat.dart'; +import '../motion/motion.dart'; import '../services/chat_repository.dart'; import '../services/report_repository.dart'; import '../services/storage_service.dart'; +import '../theme/app_palette.dart'; import '../widgets/overlay_icon_button.dart'; import '../widgets/report_sheet.dart'; import 'user_profile_screen.dart'; diff --git a/lib/screen/connections_screen.dart b/lib/screen/connections_screen.dart index 6102c78..340b497 100644 --- a/lib/screen/connections_screen.dart +++ b/lib/screen/connections_screen.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; + import '../models/social.dart'; +import '../motion/motion.dart'; import '../services/social_repository.dart'; +import '../theme/app_palette.dart'; import '../widgets/user_tile.dart'; import 'user_profile_screen.dart'; diff --git a/lib/screen/facility_review_screen.dart b/lib/screen/facility_review_screen.dart index 38271ef..ea6fc4e 100644 --- a/lib/screen/facility_review_screen.dart +++ b/lib/screen/facility_review_screen.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; + import '../models/facility_review.dart'; import '../services/facility_repository.dart'; import '../services/facility_review_repository.dart'; import '../services/storage_service.dart'; +import '../theme/app_palette.dart'; /// 시설 후기 작성/수정 (0022). 갤러리 다중 사진 허용. 카페는 작성 시 승격. /// 저장 성공 시 true 를 pop. diff --git a/lib/screen/guardian_invites_screen.dart b/lib/screen/guardian_invites_screen.dart index a3b0d45..dea487f 100644 --- a/lib/screen/guardian_invites_screen.dart +++ b/lib/screen/guardian_invites_screen.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart' show PostgrestException; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show timeAgo; import '../services/pet_repository.dart'; +import '../theme/app_palette.dart'; /// 받은 공동보호자 초대 — 수락/거절. /// 수락 시 트리거가 보호자로 등록. 단, 진행 중 약속의 지원자면 차단되어 안내한다. diff --git a/lib/screen/image_crop_screen.dart b/lib/screen/image_crop_screen.dart index 1753d06..f646552 100644 --- a/lib/screen/image_crop_screen.dart +++ b/lib/screen/image_crop_screen.dart @@ -2,8 +2,9 @@ import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; + import '../models/community.dart' show kPostImageAspectRatio; +import '../theme/app_palette.dart'; /// 갤러리 사진을 게시 표시 비율(3:4)에 맞게 "보여질 영역"을 고르는 화면. /// diff --git a/lib/screen/location_verify_screen.dart b/lib/screen/location_verify_screen.dart index 2bd323e..a34164d 100644 --- a/lib/screen/location_verify_screen.dart +++ b/lib/screen/location_verify_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; import '../services/location_repository.dart'; import '../services/location_service.dart'; +import '../theme/app_palette.dart'; /// 동네 인증 화면 — 현재 위치(GPS)로 활동 지역(행정동)을 인증한다. /// diff --git a/lib/screen/main_screen.dart b/lib/screen/main_screen.dart index cfc2605..7defa41 100644 --- a/lib/screen/main_screen.dart +++ b/lib/screen/main_screen.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; + import '../motion/motion.dart'; import '../services/keyboard_barrier.dart'; import '../theme/app_palette.dart'; +import 'tabs/chat_tab.dart'; import 'tabs/community_tab.dart'; import 'tabs/map_tab.dart'; -import 'tabs/user_search_tab.dart'; -import 'tabs/chat_tab.dart'; import 'tabs/my_info_tab.dart'; +import 'tabs/user_search_tab.dart'; /// 메인 화면 — 바텀 네비게이션 5탭. /// 탭 순서: 지도(개발중) / 사용자검색 / 커뮤니티(중앙=기본) / 채팅 / 내정보 diff --git a/lib/screen/my_posts_screen.dart b/lib/screen/my_posts_screen.dart index add101c..dafe9b0 100644 --- a/lib/screen/my_posts_screen.dart +++ b/lib/screen/my_posts_screen.dart @@ -1,9 +1,11 @@ +import 'dart:async'; import 'package:flutter/material.dart'; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; + import '../models/community.dart'; +import '../motion/motion.dart'; import '../services/community_repository.dart'; import '../services/session.dart'; +import '../theme/app_palette.dart'; import '../widgets/post_card.dart'; import 'post_detail_screen.dart'; @@ -93,7 +95,7 @@ class _MyPostsScreenState extends State { ), ), ); - _load(); + unawaited(_load()); } Future _confirmDelete() async { diff --git a/lib/screen/notification_panel.dart b/lib/screen/notification_panel.dart index 0f7e1a5..1aaf333 100644 --- a/lib/screen/notification_panel.dart +++ b/lib/screen/notification_panel.dart @@ -1,12 +1,14 @@ import 'dart:async' show unawaited; import 'dart:math' as math; + import 'package:flutter/material.dart'; -import '../main.dart' show openFromPush; -import '../motion/motion.dart' show SpringCurve; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show timeAgo; +import '../main.dart' show openFromPush; import '../models/notification.dart'; +import '../motion/motion.dart' show SpringCurve; import '../services/notification_repository.dart'; +import '../theme/app_palette.dart'; /// 알림 벨을 탭하면 그 자리에서 **아래로 확장되는 알림 패널**을 연다(Slack 채널 헤더가 /// 메뉴로 펼쳐지는 스펠 차용). 패널 높이는 알림 수에 비례해 커지고, 화면을 넘으면 스크롤. diff --git a/lib/screen/notification_settings_screen.dart b/lib/screen/notification_settings_screen.dart index f457e87..8b61cbb 100644 --- a/lib/screen/notification_settings_screen.dart +++ b/lib/screen/notification_settings_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; + import '../services/activity_repository.dart'; +import '../theme/app_palette.dart'; /// 알림 설정 — notification_preferences 토글. class NotificationSettingsScreen extends StatefulWidget { diff --git a/lib/screen/notifications_screen.dart b/lib/screen/notifications_screen.dart index 8241144..2fe6278 100644 --- a/lib/screen/notifications_screen.dart +++ b/lib/screen/notifications_screen.dart @@ -1,9 +1,11 @@ +import 'dart:async'; import 'package:flutter/material.dart'; -import '../main.dart' show openFromPush; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show timeAgo; +import '../main.dart' show openFromPush; import '../models/notification.dart'; import '../services/notification_repository.dart'; +import '../theme/app_palette.dart'; /// 알림함 — 내 알림 목록 / 읽음 처리 / 관련 화면 이동. class NotificationsScreen extends StatefulWidget { @@ -48,7 +50,7 @@ class _NotificationsScreenState extends State { Future _onTap(AppNotification n) async { if (!n.isRead) { - _repo.markRead(n.id); + unawaited(_repo.markRead(n.id)); final idx = _items.indexWhere((e) => e.id == n.id); if (idx >= 0) { setState( @@ -83,7 +85,7 @@ class _NotificationsScreenState extends State { Future _markAll() async { await _repo.markAllRead(); - _load(); + unawaited(_load()); } @override diff --git a/lib/screen/pet_detail_screen.dart b/lib/screen/pet_detail_screen.dart index 6db5db5..be1f456 100644 --- a/lib/screen/pet_detail_screen.dart +++ b/lib/screen/pet_detail_screen.dart @@ -1,10 +1,12 @@ +import 'dart:async'; import 'package:flutter/material.dart'; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show MockPet; +import '../motion/motion.dart'; import '../services/pet_repository.dart'; -import '../widgets/role_badge.dart'; +import '../theme/app_palette.dart'; import '../widgets/pet_trust_badge.dart'; +import '../widgets/role_badge.dart'; import 'pet_edit_screen.dart'; /// 펫 상세 — 펫 정보 + 보호자 목록(실데이터). owner 는 수정/삭제/초대 가능. @@ -80,7 +82,7 @@ class _PetDetailScreenState extends State { return; } setState(() => _pet = fresh); - _loadGuardians(); + unawaited(_loadGuardians()); } void _toast(String m) { @@ -167,7 +169,7 @@ class _PetDetailScreenState extends State { context, AppPageRoute(builder: (_) => PetEditScreen(pet: _pet)), ); - if (changed == true) _reloadPet(); + if (changed == true) unawaited(_reloadPet()); }, ), ListTile( @@ -221,7 +223,7 @@ class _PetDetailScreenState extends State { Future _removeGuardian(Guardian g) async { try { await _repo.removeGuardian(_pet.id, g.userId); - _loadGuardians(); + unawaited(_loadGuardians()); } catch (_) { _toast('보호자 제거에 실패했어요'); } diff --git a/lib/screen/pet_edit_screen.dart b/lib/screen/pet_edit_screen.dart index b383768..cbbddc4 100644 --- a/lib/screen/pet_edit_screen.dart +++ b/lib/screen/pet_edit_screen.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show MockPet; +import '../motion/motion.dart'; import '../services/pet_repository.dart'; import '../services/storage_service.dart'; +import '../theme/app_palette.dart'; import 'pet_identity_enroll_screen.dart'; /// 가입 단계 등에서 미리 받아온 펫 정보 초안 — 신규 등록 폼을 프리필한다. diff --git a/lib/screen/pet_identity_enroll_screen.dart b/lib/screen/pet_identity_enroll_screen.dart index 18270be..b9aba43 100644 --- a/lib/screen/pet_identity_enroll_screen.dart +++ b/lib/screen/pet_identity_enroll_screen.dart @@ -5,9 +5,9 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart' show XFile; import 'package:video_thumbnail/video_thumbnail.dart'; -import '../theme/app_palette.dart'; import '../services/pet_enroll_repository.dart'; import '../services/storage_service.dart'; +import '../theme/app_palette.dart'; /// 펫 신원 인증(0020) — 반려동물 영상 촬영 → 프레임 추출 → 서버 AI 검증(실물·동일개체). /// diff --git a/lib/screen/pet_profile_screen.dart b/lib/screen/pet_profile_screen.dart index 631f89a..22a85e0 100644 --- a/lib/screen/pet_profile_screen.dart +++ b/lib/screen/pet_profile_screen.dart @@ -1,11 +1,14 @@ +import 'dart:async'; import 'dart:ui' as ui; + import 'package:flutter/material.dart'; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; + import '../models/community.dart'; import '../models/pet_search.dart'; +import '../motion/motion.dart'; import '../services/community_repository.dart'; import '../services/pet_repository.dart'; +import '../theme/app_palette.dart'; import '../widgets/pet_trust_badge.dart'; import '../widgets/post_card.dart'; import 'post_detail_screen.dart'; @@ -130,7 +133,7 @@ class _PetProfileScreenState extends State { ); if (!mounted) return; setState(() => _openedPostId = null); - _load(silent: true); // 하트/댓글 변동 반영 + unawaited(_load(silent: true)); // 하트/댓글 변동 반영 } Rect? _guardianRect(String userId) { @@ -230,8 +233,8 @@ class _PetProfileScreenState extends State { }; final subtitle = [ if (pet.species.isNotEmpty) pet.species, - if (age != null) age, - if (genderKo != null) genderKo, + ?age, + ?genderKo, ].join(' · '); return Padding( diff --git a/lib/screen/post_detail_screen.dart b/lib/screen/post_detail_screen.dart index 4463ecc..76c3826 100644 --- a/lib/screen/post_detail_screen.dart +++ b/lib/screen/post_detail_screen.dart @@ -1,23 +1,24 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show SystemUiOverlayStyle; -import '../motion/motion.dart'; -import '../services/business_repository.dart'; -import 'user_profile_screen.dart'; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show categoryLabel, timeAgo; import '../models/community.dart'; -import '../services/community_repository.dart'; -import '../services/social_repository.dart'; +import '../motion/motion.dart'; +import '../services/business_repository.dart'; import '../services/chat_launcher.dart'; +import '../services/community_repository.dart'; import '../services/report_repository.dart'; import '../services/session.dart'; +import '../services/social_repository.dart'; +import '../theme/app_palette.dart'; import '../widgets/blob_background.dart'; import '../widgets/overlay_icon_button.dart'; -import '../widgets/role_badge.dart'; import '../widgets/report_sheet.dart'; -import 'auth/auth_wall_dialog.dart'; +import '../widgets/role_badge.dart'; import 'applicants_screen.dart'; +import 'auth/auth_wall_dialog.dart'; import 'post_edit_screen.dart'; +import 'user_profile_screen.dart'; /// 신고 액션 시트의 한 항목. class _ReportAction { diff --git a/lib/screen/post_edit_screen.dart b/lib/screen/post_edit_screen.dart index 961b384..9729f2e 100644 --- a/lib/screen/post_edit_screen.dart +++ b/lib/screen/post_edit_screen.dart @@ -1,11 +1,13 @@ import 'dart:typed_data'; + import 'package:flutter/material.dart'; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show categoryLabel; import '../models/community.dart'; +import '../motion/motion.dart'; import '../services/community_repository.dart'; import '../services/storage_service.dart'; +import '../theme/app_palette.dart'; import 'image_crop_screen.dart'; /// 내 게시글 수정 — 제목·내용, 일정 게시글이면 약속 일정, 자유/입양이면 사진까지 편집. diff --git a/lib/screen/profile_edit_screen.dart b/lib/screen/profile_edit_screen.dart index 05c3084..7a4f92a 100644 --- a/lib/screen/profile_edit_screen.dart +++ b/lib/screen/profile_edit_screen.dart @@ -1,24 +1,26 @@ +import 'dart:async'; import 'package:flutter/material.dart'; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; + import '../models/profile.dart'; +import '../motion/motion.dart'; +import '../services/auth_service.dart'; import '../services/business_repository.dart'; import '../services/profile_repository.dart'; -import '../services/storage_service.dart'; import '../services/session.dart'; -import '../services/auth_service.dart'; +import '../services/storage_service.dart'; import '../services/theme_controller.dart'; -import 'location_verify_screen.dart'; -import 'connections_screen.dart'; -import 'my_posts_screen.dart'; +import '../theme/app_palette.dart'; import 'activity_screens.dart'; +import 'blocked_users_screen.dart'; +import 'business_register_screen.dart'; import 'change_password_screen.dart'; +import 'connections_screen.dart'; import 'guardian_invites_screen.dart'; +import 'location_verify_screen.dart'; +import 'my_posts_screen.dart'; import 'notification_settings_screen.dart'; -import 'blocked_users_screen.dart'; -import 'welcome_screen.dart'; import 'terms_screen.dart'; -import 'business_register_screen.dart'; +import 'welcome_screen.dart'; /// 화면 이동 공용 헬퍼. void _push(BuildContext context, Widget screen) { @@ -760,7 +762,7 @@ class _BusinessSectionState extends State<_BusinessSection> { context, AppPageRoute(builder: (_) => const BusinessRegisterScreen()), ); - _load(); // 신청/재신청 결과 반영 + unawaited(_load()); // 신청/재신청 결과 반영 } /// 일반 ↔ 업체 모드 토글 — 서버(switch_account_mode)가 approved 게이트. diff --git a/lib/screen/review_write_screen.dart b/lib/screen/review_write_screen.dart index 6520b25..a3668cb 100644 --- a/lib/screen/review_write_screen.dart +++ b/lib/screen/review_write_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; + import '../data/review_categories.dart'; import '../services/activity_repository.dart'; +import '../theme/app_palette.dart'; /// 평가 작성 — 완료된 약속의 상대에게 8개 카테고리 중 최대 4개 선택해 평가. /// 상반되는 카테고리(예: 친절해요 ↔ 불친절해요)는 동시 선택 불가. diff --git a/lib/screen/tabs/chat_tab.dart b/lib/screen/tabs/chat_tab.dart index 256a05d..8a7b326 100644 --- a/lib/screen/tabs/chat_tab.dart +++ b/lib/screen/tabs/chat_tab.dart @@ -1,11 +1,14 @@ +import 'dart:async'; import 'dart:ui' as ui; + import 'package:flutter/material.dart'; -import '../../motion/motion.dart'; -import '../../theme/app_palette.dart'; + import '../../data/mock_data.dart' show timeAgo; import '../../models/chat.dart'; -import '../../services/chat_repository.dart'; +import '../../motion/motion.dart'; import '../../services/app_events.dart'; +import '../../services/chat_repository.dart'; +import '../../theme/app_palette.dart'; import '../../widgets/gradient_header.dart'; import '../auth/auth_wall_dialog.dart'; import '../chat_room_screen.dart'; @@ -54,7 +57,7 @@ class _ChatTabState extends State { ); if (!mounted) return; setState(() => _openedRoomId = null); // 타일 복원 - _load(silent: true); // 읽음/새 메시지 반영 + unawaited(_load(silent: true)); // 읽음/새 메시지 반영 } @override diff --git a/lib/screen/tabs/community_tab.dart b/lib/screen/tabs/community_tab.dart index 1102c4f..498885c 100644 --- a/lib/screen/tabs/community_tab.dart +++ b/lib/screen/tabs/community_tab.dart @@ -1,24 +1,26 @@ import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show ScrollDirection; -import '../../theme/app_palette.dart'; + import '../../models/community.dart'; -import '../../services/business_repository.dart'; -import '../../services/community_repository.dart'; -import '../../widgets/app_search_field.dart'; -import '../../widgets/post_card.dart'; -import '../../widgets/role_badge.dart'; import '../../motion/motion.dart'; import '../../services/app_events.dart'; +import '../../services/business_repository.dart'; +import '../../services/community_repository.dart'; import '../../services/keyboard_barrier.dart'; import '../../services/notification_repository.dart'; import '../../services/profile_repository.dart'; +import '../../theme/app_palette.dart'; +import '../../widgets/app_search_field.dart'; +import '../../widgets/post_card.dart'; +import '../../widgets/role_badge.dart'; import '../auth/auth_wall_dialog.dart'; import '../location_verify_screen.dart'; -import '../post_detail_screen.dart'; -import '../post_create_screen.dart'; -import '../notifications_screen.dart'; import '../notification_panel.dart'; +import '../notifications_screen.dart'; +import '../post_create_screen.dart'; +import '../post_detail_screen.dart'; /// 커뮤니티 탭 — 게시글 목록(실데이터) + 카테고리 필터 + 검색. class CommunityTab extends StatefulWidget { @@ -259,7 +261,7 @@ class _CommunityTabState extends State if (!mounted) return; setState(() => _openedPostId = null); // 상세 닫힘 → 원본 카드 복원 _revealHeaderIfAtTop(); // 최상단이면 헤더 복귀(흰 공백 방지) - _load(silent: true); // 스크롤 유지한 채 하트/댓글 변동만 반영 + unawaited(_load(silent: true)); // 스크롤 유지한 채 하트/댓글 변동만 반영 } void _selectCategory(String? c) { @@ -269,7 +271,7 @@ class _CommunityTabState extends State Future _toggleHeart(int index) async { if (widget.isGuest) { - AuthWallDialog.show(context, message: '하트는 로그인 후 누를 수 있어요'); + unawaited(AuthWallDialog.show(context, message: '하트는 로그인 후 누를 수 있어요')); return; } final post = _posts[index]; @@ -297,7 +299,7 @@ class _CommunityTabState extends State Future _openCreate() async { if (widget.isGuest) { - AuthWallDialog.show(context, message: '게시글은 로그인 후 작성할 수 있어요'); + unawaited(AuthWallDialog.show(context, message: '게시글은 로그인 후 작성할 수 있어요')); return; } // 동네 인증 게이트 — 미인증/만료(30일)면 작성 화면 대신 인증 안내. @@ -330,7 +332,7 @@ class _CommunityTabState extends State origin: (_) => const _FabGhost(), ), ); - if (created == true) _load(); + if (created == true) unawaited(_load()); } /// 동네 인증이 없거나 만료된 사용자에게 인증 화면으로 안내. @@ -726,7 +728,7 @@ class _NotificationBellState extends State<_NotificationBell> { Future _open() async { if (widget.isGuest) { - AuthWallDialog.show(context); + unawaited(AuthWallDialog.show(context)); return; } // 벨 위치를 앵커로 그 아래로 펼쳐지는 알림 패널을 연다(Slack 헤더-메뉴 스펠). @@ -740,7 +742,7 @@ class _NotificationBellState extends State<_NotificationBell> { final items = await NotificationRepository.instance.fetch(); if (!mounted) return; await showNotificationPanel(context, anchor, items); - _loadCount(); + unawaited(_loadCount()); return; } catch (_) { /* 실패 시 전체화면으로 폴백 */ @@ -751,7 +753,7 @@ class _NotificationBellState extends State<_NotificationBell> { context, AppPageRoute(builder: (_) => const NotificationsScreen()), ); - _loadCount(); + unawaited(_loadCount()); } @override diff --git a/lib/screen/tabs/map_tab.dart b/lib/screen/tabs/map_tab.dart index 550cc1f..6d2a7d4 100644 --- a/lib/screen/tabs/map_tab.dart +++ b/lib/screen/tabs/map_tab.dart @@ -1,16 +1,18 @@ import 'dart:async'; import 'dart:math' as math; import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_naver_map/flutter_naver_map.dart'; -import 'package:http/http.dart' as http; import 'package:geolocator/geolocator.dart'; -import '../../theme/app_palette.dart'; +import 'package:http/http.dart' as http; + import '../../motion/motion.dart'; -import '../../services/facility_repository.dart'; import '../../services/community_repository.dart'; +import '../../services/facility_repository.dart'; import '../../services/location_service.dart'; +import '../../theme/app_palette.dart'; import '../../widgets/app_search_field.dart'; import '../../widgets/facility_sheet.dart'; import '../../widgets/map_bottom_sheet.dart'; diff --git a/lib/screen/tabs/my_info_tab.dart b/lib/screen/tabs/my_info_tab.dart index e62df9a..6bf5da5 100644 --- a/lib/screen/tabs/my_info_tab.dart +++ b/lib/screen/tabs/my_info_tab.dart @@ -1,32 +1,35 @@ +import 'dart:async'; import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show ScrollDirection; -import '../../motion/motion.dart'; -import '../../theme/app_palette.dart'; + import '../../data/mock_data.dart' show MockPet; import '../../models/community.dart'; import '../../models/profile.dart'; -import '../../services/profile_repository.dart'; -import '../../services/pet_repository.dart'; +import '../../motion/motion.dart'; +import '../../services/app_events.dart'; import '../../services/business_repository.dart'; import '../../services/community_repository.dart'; -import '../../services/app_events.dart'; +import '../../services/pet_repository.dart'; +import '../../services/profile_repository.dart'; import '../../services/session.dart'; +import '../../theme/app_palette.dart'; +import '../../widgets/gradient_header.dart'; +import '../../widgets/overlay_icon_button.dart'; import '../../widgets/pet_card.dart'; import '../../widgets/review_cards.dart'; import '../../widgets/role_badge.dart'; -import '../../widgets/gradient_header.dart'; -import '../../widgets/overlay_icon_button.dart'; import '../activity_screens.dart' show MyAppointmentsScreen; +import '../auth/login_screen.dart'; +import '../auth/signup_phone_screen.dart'; import '../connections_screen.dart'; import '../pet_detail_screen.dart'; import '../pet_edit_screen.dart'; import '../post_detail_screen.dart'; import '../profile_edit_screen.dart'; -import '../user_profile_screen.dart' show PostPhotoTile; -import '../auth/login_screen.dart'; -import '../auth/signup_phone_screen.dart'; import '../terms_screen.dart'; +import '../user_profile_screen.dart' show PostPhotoTile; /// 화면 이동 공용 헬퍼. void _push(BuildContext context, Widget screen) { @@ -130,7 +133,7 @@ class _MyInfoTabState extends State ), ), ); - if (mounted) _load(silent: true); // 닉네임 등 수정 반영 + if (mounted) unawaited(_load(silent: true)); // 닉네임 등 수정 반영 } // ── 내 게시글: 대표사진 2열 그리드 (공개 프로필과 동일) ── @@ -160,7 +163,7 @@ class _MyInfoTabState extends State ); if (!mounted) return; setState(() => _openedPostId = null); - _load(silent: true); // 하트/댓글/삭제 변동 반영 + unawaited(_load(silent: true)); // 하트/댓글/삭제 변동 반영 } Widget _myPostsSection({bool isBusinessMode = false}) { @@ -866,11 +869,7 @@ class _PetHeroCard extends StatelessWidget { 'female' => '여아', _ => null, }; - final subtitle = [ - pet.species, - if (age != null) age, - if (genderKo != null) genderKo, - ].join(' · '); + final subtitle = [pet.species, ?age, ?genderKo].join(' · '); // Pressable — 피드 카드·검색 타일과 동일한 스프링 눌림 피드백으로 통일. return Pressable( diff --git a/lib/screen/tabs/user_search_tab.dart b/lib/screen/tabs/user_search_tab.dart index 4769f7a..35a8cf9 100644 --- a/lib/screen/tabs/user_search_tab.dart +++ b/lib/screen/tabs/user_search_tab.dart @@ -1,13 +1,15 @@ import 'dart:async'; -import '../../motion/motion.dart'; + import 'package:flutter/material.dart'; -import '../../theme/app_palette.dart'; -import '../../models/social.dart'; + import '../../models/pet_search.dart'; +import '../../models/social.dart'; +import '../../motion/motion.dart'; import '../../services/social_repository.dart'; +import '../../theme/app_palette.dart'; import '../../widgets/app_search_field.dart'; -import '../../widgets/user_tile.dart'; import '../../widgets/gradient_header.dart'; +import '../../widgets/user_tile.dart'; import '../pet_profile_screen.dart'; import '../user_profile_screen.dart'; diff --git a/lib/screen/user_profile_screen.dart b/lib/screen/user_profile_screen.dart index 0452ca5..28db370 100644 --- a/lib/screen/user_profile_screen.dart +++ b/lib/screen/user_profile_screen.dart @@ -1,25 +1,28 @@ +import 'dart:async'; import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show RenderAbstractViewport; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../data/mock_data.dart' show MockPet; import '../models/community.dart'; import '../models/profile.dart'; -import '../services/community_repository.dart'; -import '../services/profile_repository.dart'; -import '../services/social_repository.dart'; -import 'package:url_launcher/url_launcher.dart'; +import '../motion/motion.dart'; import '../services/business_repository.dart'; import '../services/chat_launcher.dart'; +import '../services/community_repository.dart'; import '../services/facility_repository.dart' show Facility; +import '../services/profile_repository.dart'; import '../services/session.dart'; -import '../data/mock_data.dart' show MockPet; +import '../services/social_repository.dart'; +import '../theme/app_palette.dart'; +import '../utils/phone_format.dart'; +import '../widgets/blob_background.dart'; import '../widgets/review_cards.dart'; import '../widgets/role_badge.dart' show categoryColor; -import '../widgets/blob_background.dart'; import 'facility_review_screen.dart'; import 'pet_profile_screen.dart'; -import '../utils/phone_format.dart'; import 'post_detail_screen.dart'; /// 타 사용자 공개 프로필 — 사용자 검색/연결 목록에서 진입. @@ -157,7 +160,7 @@ class _UserProfileScreenState extends State { _bizReviews = bizReviews; _loading = false; }); - if (!_isMe) _loadFollowing(); + if (!_isMe) unawaited(_loadFollowing()); } catch (_) { if (!mounted) return; setState(() { @@ -257,7 +260,7 @@ class _UserProfileScreenState extends State { ); if (!mounted) return; setState(() => _openedPostId = null); - _load(silent: true); // 하트/댓글 변동 반영 + unawaited(_load(silent: true)); // 하트/댓글 변동 반영 } Future _openPet(MockPet pet, Rect? rect) async { @@ -1079,7 +1082,7 @@ class _UserProfileScreenState extends State { ), ), ); - if (ok == true && mounted) _load(silent: true); // 후기·평점 즉시 반영 + if (ok == true && mounted) unawaited(_load(silent: true)); // 후기·평점 즉시 반영 } Widget _bizReviewWriteButton() => Padding( @@ -1475,8 +1478,8 @@ class PetPosterCard extends StatelessWidget { }; final subtitle = [ if (pet.species.isNotEmpty) pet.species, - if (age != null) age, - if (genderKo != null) genderKo, + ?age, + ?genderKo, ].join(' · '); return Pressable( diff --git a/lib/screen/welcome_screen.dart b/lib/screen/welcome_screen.dart index d612e56..75348a8 100644 --- a/lib/screen/welcome_screen.dart +++ b/lib/screen/welcome_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../motion/motion.dart'; import 'package:google_fonts/google_fonts.dart'; + +import '../motion/motion.dart'; import '../theme/app_palette.dart'; import 'auth/login_screen.dart'; import 'auth/signup_phone_screen.dart'; diff --git a/lib/services/admin_repository.dart b/lib/services/admin_repository.dart index 54cf4bf..2ff52bf 100644 --- a/lib/services/admin_repository.dart +++ b/lib/services/admin_repository.dart @@ -16,7 +16,7 @@ class AdminStats { required this.reportsOpen, }); - factory AdminStats.fromJson(Map j) => AdminStats( + factory AdminStats.fromJson(Map j) => AdminStats( users: (j['users'] as num?)?.toInt() ?? 0, usersSuspended: (j['users_suspended'] as num?)?.toInt() ?? 0, posts: (j['posts'] as num?)?.toInt() ?? 0, @@ -47,7 +47,7 @@ class AdminUser { bool get isAdmin => userType == 'admin'; - factory AdminUser.fromJson(Map j) => AdminUser( + factory AdminUser.fromJson(Map j) => AdminUser( id: j['id'] as String, username: (j['username'] ?? '') as String, nickname: (j['nickname'] ?? '') as String, @@ -82,7 +82,7 @@ class AdminReport { required this.reporterNickname, }); - factory AdminReport.fromJson(Map j) => AdminReport( + factory AdminReport.fromJson(Map j) => AdminReport( id: j['id'] as String, targetType: (j['target_type'] ?? '') as String, targetId: j['target_id'] as String?, @@ -125,7 +125,7 @@ class AdminPost { required this.createdAt, }); - factory AdminPost.fromJson(Map j) => AdminPost( + factory AdminPost.fromJson(Map j) => AdminPost( id: j['id'] as String, title: (j['title'] ?? '') as String, content: (j['content'] ?? '') as String, @@ -155,7 +155,7 @@ class AdminComment { required this.createdAt, }); - factory AdminComment.fromJson(Map j) => AdminComment( + factory AdminComment.fromJson(Map j) => AdminComment( id: j['id'] as String, content: (j['content'] ?? '') as String, authorNickname: (j['author_nickname'] ?? '알 수 없음') as String, @@ -180,7 +180,7 @@ class AdminInquiry { required this.lastMessageAt, }); - factory AdminInquiry.fromJson(Map j) => AdminInquiry( + factory AdminInquiry.fromJson(Map j) => AdminInquiry( roomId: j['room_id'] as String, userId: j['user_id'] as String?, userNickname: (j['user_nickname'] ?? '알 수 없음') as String, @@ -240,7 +240,7 @@ class ReportTarget { required this.data, }); - factory ReportTarget.fromJson(Map j) => ReportTarget( + factory ReportTarget.fromJson(Map j) => ReportTarget( kind: (j['kind'] ?? '') as String, exists: j['exists'] == true, data: j.cast(), @@ -267,7 +267,7 @@ class AdminLog { required this.createdAt, }); - factory AdminLog.fromJson(Map j) => AdminLog( + factory AdminLog.fromJson(Map j) => AdminLog( id: j['id'] as String, adminNickname: (j['admin_nickname'] ?? '알 수 없음') as String, actionType: (j['action_type'] ?? '') as String, @@ -331,9 +331,10 @@ class AdminOpsMetrics { /// AI 인증 성공률 0~1. double get aiSuccessRate => aiTotal == 0 ? 0 : aiPass / aiTotal; - static int _gi(Map m, String k) => (m[k] as num?)?.toInt() ?? 0; + static int _gi(Map m, String k) => + (m[k] as num?)?.toInt() ?? 0; - factory AdminOpsMetrics.fromJson(Map j) { + factory AdminOpsMetrics.fromJson(Map j) { final uc = (j['unit_cost'] as Map?) ?? const {}; final ai = (j['ai'] as Map?) ?? const {}; final sms = (j['sms'] as Map?) ?? const {}; @@ -418,16 +419,17 @@ class PhotoVerifyFailure { } } - factory PhotoVerifyFailure.fromJson(Map j) => PhotoVerifyFailure( - id: j['id'] as String, - createdAt: DateTime.parse(j['created_at'] as String).toLocal(), - failReason: j['fail_reason'] as String?, - aiReason: j['ai_reason'] as String?, - regionMatched: j['region_matched'] == true, - aiMatchScore: (j['ai_match_score'] as num?)?.toDouble(), - purpose: (j['purpose'] ?? '') as String, - nickname: (j['nickname'] ?? '알 수 없음') as String, - ); + factory PhotoVerifyFailure.fromJson(Map j) => + PhotoVerifyFailure( + id: j['id'] as String, + createdAt: DateTime.parse(j['created_at'] as String).toLocal(), + failReason: j['fail_reason'] as String?, + aiReason: j['ai_reason'] as String?, + regionMatched: j['region_matched'] == true, + aiMatchScore: (j['ai_match_score'] as num?)?.toDouble(), + purpose: (j['purpose'] ?? '') as String, + nickname: (j['nickname'] ?? '알 수 없음') as String, + ); } /// 관리자가 보는 업체 인증 신청 1건 (0025 §6). @@ -487,33 +489,34 @@ class AdminBusinessApplication { /// 자동승인 트랙이 아니면 승인이 override(사유 필수)로 처리된다 (0025 §6-2). bool get approveNeedsReason => reviewTrack != 'auto'; - factory AdminBusinessApplication.fromJson(Map j) => AdminBusinessApplication( - userId: j['user_id'] as String, - nickname: (j['nickname'] ?? '') as String, - businessRegNo: (j['business_reg_no'] ?? '') as String, - declaredCategory: (j['declared_category'] ?? '') as String, - businessName: (j['business_name'] ?? '') as String, - storefrontName: j['storefront_name'] as String?, - prevBusinessName: j['prev_business_name'] as String?, - businessAddress: (j['business_address'] ?? '') as String, - businessAddressJibun: j['business_address_jibun'] as String?, - businessPhone: j['business_phone'] as String?, - representativeName: j['representative_name'] as String?, - contactEmail: (j['contact_email'] ?? '') as String, - licenseImagePath: (j['license_image_path'] ?? '') as String, - extraDocPath: j['extra_doc_path'] as String?, - matchedFacilityName: j['matched_facility_name'] as String?, - matchScore: (j['match_score'] as num?)?.toInt(), - matchDetail: - (j['match_detail'] as Map?)?.cast() ?? const {}, - reviewTrack: (j['review_track'] ?? 'review') as String, - autoApproved: j['auto_approved'] == true, - status: (j['status'] ?? 'pending') as String, - rejectedReason: j['rejected_reason'] as String?, - reviewNote: j['review_note'] as String?, - createdAt: DateTime.parse(j['created_at'] as String).toLocal(), - updatedAt: DateTime.parse(j['updated_at'] as String).toLocal(), - ); + factory AdminBusinessApplication.fromJson(Map j) => + AdminBusinessApplication( + userId: j['user_id'] as String, + nickname: (j['nickname'] ?? '') as String, + businessRegNo: (j['business_reg_no'] ?? '') as String, + declaredCategory: (j['declared_category'] ?? '') as String, + businessName: (j['business_name'] ?? '') as String, + storefrontName: j['storefront_name'] as String?, + prevBusinessName: j['prev_business_name'] as String?, + businessAddress: (j['business_address'] ?? '') as String, + businessAddressJibun: j['business_address_jibun'] as String?, + businessPhone: j['business_phone'] as String?, + representativeName: j['representative_name'] as String?, + contactEmail: (j['contact_email'] ?? '') as String, + licenseImagePath: (j['license_image_path'] ?? '') as String, + extraDocPath: j['extra_doc_path'] as String?, + matchedFacilityName: j['matched_facility_name'] as String?, + matchScore: (j['match_score'] as num?)?.toInt(), + matchDetail: + (j['match_detail'] as Map?)?.cast() ?? const {}, + reviewTrack: (j['review_track'] ?? 'review') as String, + autoApproved: j['auto_approved'] == true, + status: (j['status'] ?? 'pending') as String, + rejectedReason: j['rejected_reason'] as String?, + reviewNote: j['review_note'] as String?, + createdAt: DateTime.parse(j['created_at'] as String).toLocal(), + updatedAt: DateTime.parse(j['updated_at'] as String).toLocal(), + ); } /// 관리자 전용 데이터 접근. 모든 호출은 DB 에서 app.is_admin() 으로 검증된다. diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 2b8821e..bd9be88 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,7 +1,8 @@ import 'package:supabase_flutter/supabase_flutter.dart' hide AuthUser; -import 'session.dart'; + import 'push_service.dart'; import 'realtime_service.dart'; +import 'session.dart'; /// 로그인/로그아웃. 성공 시 [SessionManager] 에 JWT 세션을 저장한다. class AuthService { diff --git a/lib/services/chat_launcher.dart b/lib/services/chat_launcher.dart index 1fab4df..e515283 100644 --- a/lib/services/chat_launcher.dart +++ b/lib/services/chat_launcher.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import '../motion/motion.dart'; import '../screen/chat_room_screen.dart'; @@ -11,10 +13,12 @@ Future openDirectChat( String otherUserId, { bool business = false, }) async { - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const Center(child: CircularProgressIndicator()), + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const Center(child: CircularProgressIndicator()), + ), ); try { final room = await ChatRepository.instance.startDirectChat( diff --git a/lib/services/push_service.dart b/lib/services/push_service.dart index d3dc393..a1e5c95 100644 --- a/lib/services/push_service.dart +++ b/lib/services/push_service.dart @@ -1,8 +1,10 @@ import 'dart:io'; + +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show MethodChannel; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; + import 'session.dart'; /// 백그라운드/종료 상태 수신 핸들러(반드시 top-level, vm:entry-point). diff --git a/lib/services/realtime_service.dart b/lib/services/realtime_service.dart index 6b7274f..ef420f2 100644 --- a/lib/services/realtime_service.dart +++ b/lib/services/realtime_service.dart @@ -1,6 +1,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; -import 'session.dart'; + import 'app_events.dart'; +import 'session.dart'; /// 앱 전역 Realtime — 로그인 세션의 커스텀 JWT 로 realtime 을 인증하고, /// 내 알림(notifications) insert 를 구독해 화면 갱신 이벤트를 발화한다. diff --git a/lib/services/session.dart b/lib/services/session.dart index 87c6121..a1906b3 100644 --- a/lib/services/session.dart +++ b/lib/services/session.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -18,7 +19,7 @@ class AuthUser { required this.userType, }); - factory AuthUser.fromJson(Map json) => AuthUser( + factory AuthUser.fromJson(Map json) => AuthUser( id: json['id'] as String, username: (json['username'] ?? '') as String, nickname: (json['nickname'] ?? '') as String, @@ -172,7 +173,7 @@ class SessionManager extends ChangeNotifier { await _secure.write(key: _kAccess, value: _access!); // realtime(채팅) 연결도 새 토큰으로 재인증 — 안 하면 8h 후 만료로 끊길 수 있음. try { - Supabase.instance.client.realtime.setAuth(_access); + unawaited(Supabase.instance.client.realtime.setAuth(_access)); } catch (_) { /* realtime 미연결 등 */ } diff --git a/lib/services/social_repository.dart b/lib/services/social_repository.dart index 539b631..edf3b98 100644 --- a/lib/services/social_repository.dart +++ b/lib/services/social_repository.dart @@ -1,6 +1,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; -import '../models/social.dart'; + import '../models/pet_search.dart'; +import '../models/social.dart'; import 'app_events.dart'; import 'session.dart'; diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index e57cf0c..93bb63e 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'app_palette.dart'; + import '../motion/spring_page_route.dart'; +import 'app_palette.dart'; /// 앱 테마 — 라이트/다크 모두 [AppPalette] 시맨틱 토큰에서 파생된다. /// 컴포넌트 테마(버튼·입력창·스낵바 등)는 팔레트만 갈아끼우면 양쪽에서 동일하게 diff --git a/lib/widgets/facility_sheet.dart b/lib/widgets/facility_sheet.dart index 421005e..4b55e1b 100644 --- a/lib/widgets/facility_sheet.dart +++ b/lib/widgets/facility_sheet.dart @@ -1,17 +1,19 @@ +import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import '../motion/motion.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../theme/app_palette.dart'; + import '../models/facility_review.dart'; +import '../motion/motion.dart'; +import '../screen/facility_review_screen.dart'; +import '../screen/user_profile_screen.dart'; import '../services/facility_repository.dart'; import '../services/facility_review_repository.dart'; import '../services/session.dart'; -import '../screen/facility_review_screen.dart'; -import '../screen/user_profile_screen.dart'; -import 'review_cards.dart'; +import '../theme/app_palette.dart'; import '../utils/phone_format.dart'; +import 'review_cards.dart'; /// 시설 상세 콘텐츠(정보 + 후기/사진 + 후기 작성 + 네이버 지도 링크). /// @@ -69,9 +71,11 @@ class _FacilityDetailContentState extends State { final fac = widget.facility; // 같은 업체의 전체 카테고리(DB 시설만; Naver 카페는 단일). 리뷰와 병렬로. if (!fac.isNaver) { - FacilityRepository.instance.allCategories(fac.id).then((cats) { - if (mounted && cats.isNotEmpty) setState(() => _categories = cats); - }); + unawaited( + FacilityRepository.instance.allCategories(fac.id).then((cats) { + if (mounted && cats.isNotEmpty) setState(() => _categories = cats); + }), + ); } try { final f = widget.facility; @@ -116,7 +120,7 @@ class _FacilityDetailContentState extends State { builder: (_) => FacilityReviewScreen(facility: widget.facility), ), ); - if (ok == true) _load(); + if (ok == true) unawaited(_load()); } Future _openInNaverMap() async { diff --git a/lib/widgets/pet_card.dart b/lib/widgets/pet_card.dart index b7a38a0..601291b 100644 --- a/lib/widgets/pet_card.dart +++ b/lib/widgets/pet_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart'; +import '../theme/app_palette.dart'; import 'role_badge.dart'; /// 반려동물 카드 — 내정보 탭의 펫 목록에 사용. diff --git a/lib/widgets/post_card.dart b/lib/widgets/post_card.dart index ecedf62..60cfafb 100644 --- a/lib/widgets/post_card.dart +++ b/lib/widgets/post_card.dart @@ -1,12 +1,14 @@ import 'dart:ui' as ui; + import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show categoryLabel, timeAgo; import '../models/community.dart'; import '../motion/motion.dart'; import '../screen/user_profile_screen.dart'; -import 'role_badge.dart'; +import '../theme/app_palette.dart'; import 'blob_background.dart'; +import 'role_badge.dart'; /// 커뮤니티 게시글 카드 — 애플뮤직 아티스트 카드 스타일. /// 대표사진이 카드 전체를 채우고, 하단은 사진을 이어받아 이음새 없이 점점 diff --git a/lib/widgets/region_posts_sheet.dart b/lib/widgets/region_posts_sheet.dart index 37a16b1..91c4cf4 100644 --- a/lib/widgets/region_posts_sheet.dart +++ b/lib/widgets/region_posts_sheet.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart' show categoryLabel, timeAgo; import '../models/community.dart'; +import '../motion/motion.dart'; +import '../screen/post_detail_screen.dart'; import '../services/community_repository.dart'; +import '../theme/app_palette.dart'; import '../widgets/role_badge.dart' show categoryColor; -import '../screen/post_detail_screen.dart'; /// 동네(행정동) 게시글 목록 콘텐츠 — 지도 위 바텀시트([MapBottomSheet]) 안에 올라간다. /// diff --git a/lib/widgets/report_sheet.dart b/lib/widgets/report_sheet.dart index 52d9cc0..4a02ea3 100644 --- a/lib/widgets/report_sheet.dart +++ b/lib/widgets/report_sheet.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; + import '../services/report_repository.dart'; +import '../theme/app_palette.dart'; /// 신고 바텀시트를 띄운다. 접수 성공 시 true 를 반환한다. /// diff --git a/lib/widgets/role_badge.dart b/lib/widgets/role_badge.dart index 98b97ac..ace4ea6 100644 --- a/lib/widgets/role_badge.dart +++ b/lib/widgets/role_badge.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../theme/app_palette.dart'; + import '../data/mock_data.dart'; +import '../theme/app_palette.dart'; /// 보호자 역할 배지 — owner / co_guardian 구분. class RoleBadge extends StatelessWidget { diff --git a/lib/widgets/user_tile.dart b/lib/widgets/user_tile.dart index 340e8c3..b799190 100644 --- a/lib/widgets/user_tile.dart +++ b/lib/widgets/user_tile.dart @@ -1,9 +1,11 @@ import 'dart:ui' as ui; + import 'package:flutter/material.dart'; -import '../motion/motion.dart'; -import '../theme/app_palette.dart'; + import '../models/social.dart'; +import '../motion/motion.dart'; import '../screen/user_profile_screen.dart'; +import '../theme/app_palette.dart'; /// 사용자 한 명을 표시하는 공통 타일 — 프로필 사진 블러 배경 + 중앙 닉네임 /// (채팅 목록과 동일한 프로스트 문법). 채팅·팔로우 동작은 프로필 상세에서. diff --git a/test/facility_sheet_test.dart b/test/facility_sheet_test.dart index 6f48889..3ee3008 100644 --- a/test/facility_sheet_test.dart +++ b/test/facility_sheet_test.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pawmate/screen/user_profile_screen.dart'; -import 'package:pawmate/theme/app_theme.dart'; import 'package:pawmate/services/facility_repository.dart'; +import 'package:pawmate/theme/app_theme.dart'; import 'package:pawmate/widgets/facility_sheet.dart'; import 'package:pawmate/widgets/map_bottom_sheet.dart'; diff --git a/test/helpers/fake_supabase.dart b/test/helpers/fake_supabase.dart index 52cef34..0dd4c1d 100644 --- a/test/helpers/fake_supabase.dart +++ b/test/helpers/fake_supabase.dart @@ -27,7 +27,7 @@ class FakeSupabase { SharedPreferences.setMockInitialValues({}); await Supabase.initialize( url: 'http://fake.supabase.test', - anonKey: 'sb_publishable_fake', + publishableKey: 'sb_publishable_fake', httpClient: MockClient(_handle), authOptions: FlutterAuthClientOptions( localStorage: EmptyLocalStorage(), diff --git a/test/region_posts_sheet_test.dart b/test/region_posts_sheet_test.dart index 61a9acd..d919986 100644 --- a/test/region_posts_sheet_test.dart +++ b/test/region_posts_sheet_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pawmate/theme/app_theme.dart'; import 'package:pawmate/services/community_repository.dart'; +import 'package:pawmate/theme/app_theme.dart'; import 'package:pawmate/widgets/region_posts_sheet.dart'; /// 동네 게시글 시트 콘텐츠가 예외 없이 렌더되는지 스모크 검증.