From abb1b21d179f646d3e45bec9b9384f06c8e8ea59 Mon Sep 17 00:00:00 2001 From: hyejj19 Date: Tue, 20 Jan 2026 03:12:59 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20Android=20=EC=B9=B4=EB=A9=94?= =?UTF-8?q?=EB=9D=BC=20=ED=98=B8=EC=B6=9C=20=ED=9B=84=20=EC=95=B1=20kill?= =?UTF-8?q?=20=EC=8B=9C=20WebView=20URL=20=EB=B3=B5=EC=9B=90=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 카메라 호출 전 현재 URL을 SharedPreferences에 저장하고, 앱이 메모리 부족으로 kill되었다 재시작될 때 저장된 URL로 복원합니다. iOS는 이 문제가 없어 Android 전용 기능입니다. Co-Authored-By: Claude Haiku 4.5 --- lib/bridge/web_view_bridge_handler.dart | 13 ++++++ lib/main.dart | 46 +++++++++++++------- lib/utils/url_restore_manager.dart | 56 +++++++++++++++++++++++++ lib/web_view/web_view.dart | 8 +++- pubspec.lock | 2 +- pubspec.yaml | 1 + 6 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 lib/utils/url_restore_manager.dart diff --git a/lib/bridge/web_view_bridge_handler.dart b/lib/bridge/web_view_bridge_handler.dart index bde01a9..487aef8 100644 --- a/lib/bridge/web_view_bridge_handler.dart +++ b/lib/bridge/web_view_bridge_handler.dart @@ -11,6 +11,7 @@ import 'package:logger/logger.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:app/utils/url_restore_manager.dart'; class WebViewBridgeHandler { final BuildContext context; @@ -219,6 +220,13 @@ class WebViewBridgeHandler { } try { + // Android에서 카메라 호출 전 현재 URL 저장 (앱이 kill될 경우 복원용) + final currentUrl = await controller.getUrl(); + if (currentUrl != null) { + await UrlRestoreManager.saveUrlBeforeCamera(currentUrl.toString()); + logger.d('카메라 호출 전 URL 저장: $currentUrl'); + } + onShowLoading?.call('사진 촬영 중...'); final ImagePicker picker = ImagePicker(); final XFile? image = await picker.pickImage( @@ -235,9 +243,14 @@ class WebViewBridgeHandler { source: "openAlbum('data:image/png;base64,$base64Image')", ); } + + // 정상 복귀 시 저장된 URL 삭제 (앱이 kill되지 않았으므로) + await UrlRestoreManager.clearSavedUrl(); onHideLoading?.call(); } catch (e) { logger.e('Error taking image: $e'); + // 에러 시에도 저장된 URL 삭제 + await UrlRestoreManager.clearSavedUrl(); onHideLoading?.call(); } } diff --git a/lib/main.dart b/lib/main.dart index 3969bcd..d5efe7f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:app/permissions/FirebaseConfig.dart'; import 'package:app/ui/loading_widget.dart'; import 'package:app/utils/env/env.dart'; +import 'package:app/utils/url_restore_manager.dart'; import 'package:app/web_view/web_view.dart'; import 'package:flutter/material.dart'; import 'package:kakao_flutter_sdk/kakao_flutter_sdk_template.dart'; @@ -142,25 +143,40 @@ class SplashScreen extends StatefulWidget { } class _SplashScreenState extends State { + String? _restoredUrl; + @override void initState() { super.initState(); + _initializeAndNavigate(); + } - Future.delayed(const Duration(seconds: 3), () { - Navigator.of(context).pushReplacement( - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => - const BottleNoteWebView(), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return FadeTransition( - opacity: animation, - child: child, - ); - }, - transitionDuration: const Duration(milliseconds: 1000), - ), - ); - }); + Future _initializeAndNavigate() async { + // Android에서 카메라 호출 후 앱이 kill된 경우 복원할 URL 확인 + final restoredUrl = await UrlRestoreManager.consumeRestoredUrl(); + if (restoredUrl != null) { + logger.d('복원할 URL 발견: $restoredUrl'); + _restoredUrl = restoredUrl; + } + + // 최소 3초 대기 (기존 동작 유지) + await Future.delayed(const Duration(seconds: 3)); + + if (!mounted) return; + + Navigator.of(context).pushReplacement( + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => + BottleNoteWebView(initialUrl: _restoredUrl), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + transitionDuration: const Duration(milliseconds: 1000), + ), + ); } @override diff --git a/lib/utils/url_restore_manager.dart b/lib/utils/url_restore_manager.dart new file mode 100644 index 0000000..1ef63a7 --- /dev/null +++ b/lib/utils/url_restore_manager.dart @@ -0,0 +1,56 @@ +import 'dart:io' show Platform; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Android에서 카메라 호출 후 앱이 kill되었을 때 URL을 복원하기 위한 매니저 +class UrlRestoreManager { + static const String _pendingUrlKey = 'pending_restore_url'; + static const String _timestampKey = 'pending_restore_timestamp'; + + /// URL 복원 유효 시간 (5분) + static const int _validDurationMinutes = 5; + + /// 카메라 호출 전 현재 URL 저장 (Android 전용) + static Future saveUrlBeforeCamera(String url) async { + if (!Platform.isAndroid) return; + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_pendingUrlKey, url); + await prefs.setInt(_timestampKey, DateTime.now().millisecondsSinceEpoch); + } + + /// 저장된 URL 가져오기 및 삭제 (Android 전용) + /// 유효 시간이 지났거나 URL이 없으면 null 반환 + static Future consumeRestoredUrl() async { + if (!Platform.isAndroid) return null; + + final prefs = await SharedPreferences.getInstance(); + final savedUrl = prefs.getString(_pendingUrlKey); + final savedTimestamp = prefs.getInt(_timestampKey); + + // 저장된 URL이 없으면 null + if (savedUrl == null || savedTimestamp == null) { + return null; + } + + // 저장 후 사용했으면 바로 삭제 + await clearSavedUrl(); + + // 유효 시간 체크 (5분 초과시 무효) + final now = DateTime.now().millisecondsSinceEpoch; + final elapsed = now - savedTimestamp; + if (elapsed > _validDurationMinutes * 60 * 1000) { + return null; + } + + return savedUrl; + } + + /// 저장된 URL 삭제 (정상 카메라 복귀 시 호출) + static Future clearSavedUrl() async { + if (!Platform.isAndroid) return; + + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_pendingUrlKey); + await prefs.remove(_timestampKey); + } +} diff --git a/lib/web_view/web_view.dart b/lib/web_view/web_view.dart index 9fa4950..03970d2 100644 --- a/lib/web_view/web_view.dart +++ b/lib/web_view/web_view.dart @@ -17,7 +17,9 @@ import '../actions/back_action_handler.dart'; class BottleNoteWebView extends StatefulWidget { final VoidCallback? onLoaded; - const BottleNoteWebView({super.key, this.onLoaded}); + final String? initialUrl; + + const BottleNoteWebView({super.key, this.onLoaded, this.initialUrl}); @override State createState() => BottleNoteWebViewState(); @@ -113,7 +115,9 @@ class BottleNoteWebViewState extends State final content = Stack( children: [ InAppWebView( - initialUrlRequest: URLRequest(url: WebUri(Env.webViewUrl)), + initialUrlRequest: URLRequest( + url: WebUri(widget.initialUrl ?? Env.webViewUrl), + ), initialSettings: InAppWebViewSettings( javaScriptEnabled: true, useShouldOverrideUrlLoading: true, diff --git a/pubspec.lock b/pubspec.lock index 96932e8..0bae6c9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1017,7 +1017,7 @@ packages: source: hosted version: "4.1.0" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" diff --git a/pubspec.yaml b/pubspec.yaml index 9750997..c8b65b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: flutter_svg: ^2.0.10+1 svg_path_parser: ^1.1.2 flutter_native_splash: ^2.4.6 + shared_preferences: ^2.5.2 dev_dependencies: flutter_test: From 4b9ccd9decb0ed4aee0e6210f8690cf166765dd8 Mon Sep 17 00:00:00 2001 From: hyejj19 Date: Thu, 22 Jan 2026 12:31:18 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20PR=20=EB=A6=AC=EB=B7=B0=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - consumeRestoredUrl()에서 유효 시간 체크 순서 수정 - SplashScreen 초기화 시 try-catch 에러 처리 추가 Co-Authored-By: Claude Haiku 4.5 --- lib/main.dart | 20 ++++++++++++-------- lib/utils/url_restore_manager.dart | 7 ++++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index d5efe7f..7237042 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -152,16 +152,20 @@ class _SplashScreenState extends State { } Future _initializeAndNavigate() async { - // Android에서 카메라 호출 후 앱이 kill된 경우 복원할 URL 확인 - final restoredUrl = await UrlRestoreManager.consumeRestoredUrl(); - if (restoredUrl != null) { - logger.d('복원할 URL 발견: $restoredUrl'); - _restoredUrl = restoredUrl; + try { + // Android에서 카메라 호출 후 앱이 kill된 경우 복원할 URL 확인 + final restoredUrl = await UrlRestoreManager.consumeRestoredUrl(); + if (restoredUrl != null) { + logger.d('복원할 URL 발견: $restoredUrl'); + _restoredUrl = restoredUrl; + } + + // 최소 3초 대기 (기존 동작 유지) + await Future.delayed(const Duration(seconds: 3)); + } catch (e) { + logger.e('스플래시 초기화 중 오류 발생: $e'); } - // 최소 3초 대기 (기존 동작 유지) - await Future.delayed(const Duration(seconds: 3)); - if (!mounted) return; Navigator.of(context).pushReplacement( diff --git a/lib/utils/url_restore_manager.dart b/lib/utils/url_restore_manager.dart index 1ef63a7..ee45956 100644 --- a/lib/utils/url_restore_manager.dart +++ b/lib/utils/url_restore_manager.dart @@ -32,16 +32,17 @@ class UrlRestoreManager { return null; } - // 저장 후 사용했으면 바로 삭제 - await clearSavedUrl(); - // 유효 시간 체크 (5분 초과시 무효) final now = DateTime.now().millisecondsSinceEpoch; final elapsed = now - savedTimestamp; if (elapsed > _validDurationMinutes * 60 * 1000) { + // 만료된 데이터도 정리 + await clearSavedUrl(); return null; } + // 정상적으로 복원된 경우 사용 후 삭제 + await clearSavedUrl(); return savedUrl; }