From b989981464746115d11e4949046e1815c683f630 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 28 Jan 2026 18:54:51 +0900 Subject: [PATCH] =?UTF-8?q?fix(url):=20URL=20=EC=97=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=8B=9C=20canLaunchUrl=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20try?= =?UTF-8?q?-catch=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetched_restaurant_json_view.dart: _launchNaverUrl try-catch 패턴 적용 - restaurant_card.dart: 카드 클릭 시 URL 열기, 상세보기 팝업 제거 - 웹 환경 호환성 개선 (platformDefault 폴백) --- .../widgets/fetched_restaurant_json_view.dart | 136 +++++++++--------- .../widgets/restaurant_card.dart | 61 +++----- 2 files changed, 88 insertions(+), 109 deletions(-) diff --git a/lib/presentation/pages/restaurant_list/widgets/fetched_restaurant_json_view.dart b/lib/presentation/pages/restaurant_list/widgets/fetched_restaurant_json_view.dart index 0e5a559..b844d18 100644 --- a/lib/presentation/pages/restaurant_list/widgets/fetched_restaurant_json_view.dart +++ b/lib/presentation/pages/restaurant_list/widgets/fetched_restaurant_json_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../../../core/constants/app_colors.dart'; import '../../../../core/constants/app_typography.dart'; @@ -17,6 +18,7 @@ class FetchedRestaurantJsonView extends StatefulWidget { final TextEditingController longitudeController; final TextEditingController naverUrlController; final ValueChanged onFieldChanged; + final String? naverUrl; // 순수 URL (클릭 가능한 링크용) const FetchedRestaurantJsonView({ super.key, @@ -32,6 +34,7 @@ class FetchedRestaurantJsonView extends StatefulWidget { required this.longitudeController, required this.naverUrlController, required this.onFieldChanged, + this.naverUrl, }); @override @@ -126,7 +129,7 @@ class _FetchedRestaurantJsonViewState extends State { controller: widget.jibunAddressController, icon: Icons.map, ), - _buildCoordinateFields(context), + _buildNaverUrlField(context), _buildJsonField( context, label: '전화번호', @@ -154,78 +157,79 @@ class _FetchedRestaurantJsonViewState extends State { ); } - Widget _buildCoordinateFields(BuildContext context) { - final border = OutlineInputBorder(borderRadius: BorderRadius.circular(8)); + Widget _buildNaverUrlField(BuildContext context) { + final url = widget.naverUrl ?? ''; + if (url.isEmpty) return const SizedBox.shrink(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: const [ - Icon(Icons.my_location, size: 16), - SizedBox(width: 8), - Text('좌표'), - ], - ), - const SizedBox(height: 6), - Row( - children: [ - Expanded( - child: TextFormField( - controller: widget.latitudeController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: const [ + Icon(Icons.link, size: 16), + SizedBox(width: 8), + Text('네이버 지도:'), + ], + ), + const SizedBox(height: 6), + InkWell( + onTap: () => _launchNaverUrl(url), + borderRadius: BorderRadius.circular(8), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration( + border: Border.all( + color: widget.isDark + ? AppColors.darkDivider + : AppColors.lightDivider, ), - decoration: InputDecoration( - labelText: '위도', - border: border, - isDense: true, - ), - onChanged: widget.onFieldChanged, - validator: (value) { - if (value != null && value.isNotEmpty) { - final latitude = double.tryParse(value); - if (latitude == null || latitude < -90 || latitude > 90) { - return '올바른 위도값을 입력해주세요'; - } - } - return null; - }, + borderRadius: BorderRadius.circular(8), + color: widget.isDark + ? AppColors.darkSurface + : AppColors.lightSurface, + ), + child: Row( + children: [ + Expanded( + child: Text( + url, + style: TextStyle( + color: Colors.blue[700], + decoration: TextDecoration.underline, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.open_in_new, + size: 18, + color: Colors.blue[700], + ), + ], ), ), - const SizedBox(width: 12), - Expanded( - child: TextFormField( - controller: widget.longitudeController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), - decoration: InputDecoration( - labelText: '경도', - border: border, - isDense: true, - ), - onChanged: widget.onFieldChanged, - validator: (value) { - if (value != null && value.isNotEmpty) { - final longitude = double.tryParse(value); - if (longitude == null || - longitude < -180 || - longitude > 180) { - return '올바른 경도값을 입력해주세요'; - } - } - return null; - }, - ), - ), - ], - ), - const SizedBox(height: 12), - ], + ), + ], + ), ); } + Future _launchNaverUrl(String url) async { + final uri = Uri.tryParse(url); + if (uri == null) return; + + try { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (_) { + // 웹 환경에서 실패 시 platformDefault 모드로 재시도 + await launchUrl(uri, mode: LaunchMode.platformDefault); + } + } + Widget _buildCategoryField(BuildContext context) { return RawAutocomplete( textEditingController: widget.categoryController, diff --git a/lib/presentation/pages/restaurant_list/widgets/restaurant_card.dart b/lib/presentation/pages/restaurant_list/widgets/restaurant_card.dart index 3fe483f..f1603e6 100644 --- a/lib/presentation/pages/restaurant_list/widgets/restaurant_card.dart +++ b/lib/presentation/pages/restaurant_list/widgets/restaurant_card.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:url_launcher/url_launcher.dart'; + import 'package:lunchpick/core/constants/app_colors.dart'; import 'package:lunchpick/core/constants/app_typography.dart'; -import 'package:lunchpick/core/widgets/info_row.dart'; import 'package:lunchpick/domain/entities/restaurant.dart'; import 'package:lunchpick/presentation/providers/restaurant_provider.dart'; import 'edit_restaurant_dialog.dart'; @@ -25,10 +26,13 @@ class RestaurantCard extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isDark = Theme.of(context).brightness == Brightness.dark; + final hasNaverUrl = restaurant.naverUrl != null && + restaurant.naverUrl!.isNotEmpty; + return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: InkWell( - onTap: () => _showRestaurantDetail(context, isDark), + onTap: hasNaverUrl ? () => _openNaverUrl(restaurant.naverUrl!) : null, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), @@ -211,6 +215,18 @@ class RestaurantCard extends ConsumerWidget { ); } + Future _openNaverUrl(String url) async { + final uri = Uri.tryParse(url); + if (uri == null) return; + + try { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (_) { + // 웹 환경에서 실패 시 platformDefault 모드로 재시도 + await launchUrl(uri, mode: LaunchMode.platformDefault); + } + } + IconData _getCategoryIcon(String category) { switch (category) { case '한식': @@ -239,47 +255,6 @@ class RestaurantCard extends ConsumerWidget { return daysSinceVisit == 0 ? '오늘 방문' : '$daysSinceVisit일 전 방문'; } - void _showRestaurantDetail(BuildContext context, bool isDark) { - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: isDark - ? AppColors.darkSurface - : AppColors.lightSurface, - title: Text(restaurant.name), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InfoRow( - label: '카테고리', - value: '${restaurant.category} > ${restaurant.subCategory}', - isDark: isDark, - ), - if (restaurant.description != null) - InfoRow(label: '설명', value: restaurant.description!, isDark: isDark), - if (restaurant.phoneNumber != null) - InfoRow(label: '전화번호', value: restaurant.phoneNumber!, isDark: isDark), - InfoRow(label: '도로명 주소', value: restaurant.roadAddress, isDark: isDark), - InfoRow(label: '지번 주소', value: restaurant.jibunAddress, isDark: isDark), - if (restaurant.lastVisitDate != null) - InfoRow( - label: '마지막 방문', - value: '${restaurant.lastVisitDate!.year}년 ${restaurant.lastVisitDate!.month}월 ${restaurant.lastVisitDate!.day}일', - isDark: isDark, - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('닫기'), - ), - ], - ), - ); - } - void _handleMenuAction( _RestaurantMenuAction action, BuildContext context,