diff --git a/lib/presentation/pages/restaurant_list/widgets/add_restaurant_dialog.dart b/lib/presentation/pages/restaurant_list/widgets/add_restaurant_dialog.dart index b6155b6..8757d84 100644 --- a/lib/presentation/pages/restaurant_list/widgets/add_restaurant_dialog.dart +++ b/lib/presentation/pages/restaurant_list/widgets/add_restaurant_dialog.dart @@ -124,7 +124,7 @@ class _AddRestaurantDialogState extends ConsumerState { _jibunAddressController.text = formData.jibunAddress; _latitudeController.text = formData.latitude; _longitudeController.text = formData.longitude; - _naverUrlController.text = formData.naverUrl; + // naverUrlController는 사용자 입력을 그대로 유지 } Future _saveRestaurant() async { @@ -234,6 +234,7 @@ class _AddRestaurantDialogState extends ConsumerState { longitudeController: _longitudeController, naverUrlController: _naverUrlController, onFieldChanged: _onFormDataChanged, + naverUrl: state.formData.naverUrl, ), ), const SizedBox(height: 24), diff --git a/lib/presentation/pages/restaurant_list/widgets/add_restaurant_form.dart b/lib/presentation/pages/restaurant_list/widgets/add_restaurant_form.dart index d5e1a63..f7f87ba 100644 --- a/lib/presentation/pages/restaurant_list/widgets/add_restaurant_form.dart +++ b/lib/presentation/pages/restaurant_list/widgets/add_restaurant_form.dart @@ -1,5 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + import '../../../services/restaurant_form_validator.dart'; /// 식당 추가 폼 위젯 @@ -18,6 +20,7 @@ class AddRestaurantForm extends StatefulWidget { final List categories; final List subCategories; final String geocodingStatus; + final TextEditingController? naverUrlController; // 네이버 지도 URL 입력 const AddRestaurantForm({ super.key, @@ -35,6 +38,7 @@ class AddRestaurantForm extends StatefulWidget { this.categories = const [], this.subCategories = const [], this.geocodingStatus = '', + this.naverUrlController, }); @override @@ -196,90 +200,22 @@ class _AddRestaurantFormState extends State { ), const SizedBox(height: 16), - // 위도/경도 입력 - Row( - children: [ - Expanded( - child: TextFormField( - controller: widget.latitudeController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), - decoration: InputDecoration( - labelText: '위도', - hintText: '37.5665', - prefixIcon: const Icon(Icons.explore), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - 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; - }, + // 네이버 지도 URL (컨트롤러가 있는 경우 항상 표시) + if (widget.naverUrlController != null) + _buildNaverUrlField(context), + + if (widget.geocodingStatus.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + widget.geocodingStatus, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.blueGrey, + fontWeight: FontWeight.w600, ), - ), - const SizedBox(width: 8), - Expanded( - child: TextFormField( - controller: widget.longitudeController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), - decoration: InputDecoration( - labelText: '경도', - hintText: '126.9780', - prefixIcon: const Icon(Icons.explore), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - 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: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - '주소가 정확하지 않을 경우 위도/경도를 현재 위치로 입력합니다.', - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: Colors.grey), textAlign: TextAlign.center, ), - if (widget.geocodingStatus.isNotEmpty) ...[ - const SizedBox(height: 4), - Text( - widget.geocodingStatus, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.blueGrey, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - ], - ], - ), + ), ], ), ); @@ -459,4 +395,66 @@ class _AddRestaurantFormState extends State { }, ); } + + Widget _buildNaverUrlField(BuildContext context) { + final url = widget.naverUrlController!.text.trim(); + final hasUrl = url.isNotEmpty && url.startsWith('http'); + + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: widget.naverUrlController, + keyboardType: TextInputType.multiline, + minLines: 1, + maxLines: 4, + decoration: InputDecoration( + labelText: '네이버 지도 링크', + hintText: '네이버 지도 공유 링크를 붙여넣으세요', + prefixIcon: const Icon(Icons.link), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + suffixIcon: hasUrl + ? IconButton( + icon: Icon(Icons.open_in_new, color: Colors.blue[700]), + onPressed: () => _launchNaverUrl(url), + tooltip: '네이버 지도에서 열기', + ) + : null, + ), + onChanged: widget.onFieldChanged, + ), + const SizedBox(height: 4), + Text( + '공유 텍스트 전체를 붙여넣으면 URL만 자동 추출됩니다.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.grey, + ), + ), + ], + ), + ); + } + + Future _launchNaverUrl(String text) async { + // URL 추출 + final urlRegex = RegExp( + r'(https?://(?:map\.naver\.com|naver\.me)[^\s]+)', + caseSensitive: false, + ); + final match = urlRegex.firstMatch(text); + final url = match?.group(0) ?? text; + + final uri = Uri.tryParse(url); + if (uri == null) return; + + try { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (_) { + await launchUrl(uri, mode: LaunchMode.platformDefault); + } + } } diff --git a/lib/presentation/pages/restaurant_list/widgets/add_restaurant_url_tab.dart b/lib/presentation/pages/restaurant_list/widgets/add_restaurant_url_tab.dart index 1c717d5..a6de8ec 100644 --- a/lib/presentation/pages/restaurant_list/widgets/add_restaurant_url_tab.dart +++ b/lib/presentation/pages/restaurant_list/widgets/add_restaurant_url_tab.dart @@ -75,7 +75,7 @@ class AddRestaurantUrlTab extends StatelessWidget { minLines: 1, maxLines: 6, decoration: InputDecoration( - labelText: '네이버 지도 URL', + labelText: '네이버 지도 링크', hintText: kIsWeb ? 'https://map.naver.com/...' : 'https://naver.me/...', diff --git a/lib/presentation/pages/restaurant_list/widgets/edit_restaurant_dialog.dart b/lib/presentation/pages/restaurant_list/widgets/edit_restaurant_dialog.dart index 99fde92..2d65e9b 100644 --- a/lib/presentation/pages/restaurant_list/widgets/edit_restaurant_dialog.dart +++ b/lib/presentation/pages/restaurant_list/widgets/edit_restaurant_dialog.dart @@ -32,6 +32,7 @@ class _EditRestaurantDialogState extends ConsumerState { late final TextEditingController _jibunAddressController; late final TextEditingController _latitudeController; late final TextEditingController _longitudeController; + late final TextEditingController _naverUrlController; bool _isSaving = false; late final String _originalRoadAddress; @@ -64,6 +65,9 @@ class _EditRestaurantDialogState extends ConsumerState { _longitudeController = TextEditingController( text: restaurant.longitude.toString(), ); + _naverUrlController = TextEditingController( + text: restaurant.naverUrl ?? '', + ); _originalRoadAddress = restaurant.roadAddress; _originalJibunAddress = restaurant.jibunAddress; } @@ -79,6 +83,7 @@ class _EditRestaurantDialogState extends ConsumerState { _jibunAddressController.dispose(); _latitudeController.dispose(); _longitudeController.dispose(); + _naverUrlController.dispose(); super.dispose(); } @@ -107,6 +112,9 @@ class _EditRestaurantDialogState extends ConsumerState { _latitudeController.text = coords.latitude.toString(); _longitudeController.text = coords.longitude.toString(); + // URL 추출: 공유 텍스트에서 URL만 추출 + final extractedUrl = _extractNaverUrl(_naverUrlController.text.trim()); + final updatedRestaurant = widget.restaurant.copyWith( name: _nameController.text.trim(), category: _categoryController.text.trim(), @@ -125,6 +133,7 @@ class _EditRestaurantDialogState extends ConsumerState { : _jibunAddressController.text.trim(), latitude: coords.latitude, longitude: coords.longitude, + naverUrl: extractedUrl.isEmpty ? null : extractedUrl, updatedAt: DateTime.now(), needsAddressVerification: coords.usedCurrentLocation, ); @@ -201,6 +210,7 @@ class _EditRestaurantDialogState extends ConsumerState { onFieldChanged: _onFieldChanged, categories: categories, subCategories: subCategories, + naverUrlController: _naverUrlController, ), const SizedBox(height: 24), Row( @@ -288,4 +298,17 @@ class _EditRestaurantDialogState extends ConsumerState { usedCurrentLocation: true, ); } + + /// 공유 텍스트에서 네이버 지도 URL만 추출 + String _extractNaverUrl(String text) { + if (text.isEmpty) return ''; + + // URL 패턴 추출 + final urlRegex = RegExp( + r'(https?://(?:map\.naver\.com|naver\.me)[^\s]+)', + caseSensitive: false, + ); + final match = urlRegex.firstMatch(text); + return match?.group(0) ?? text; + } }