feat(ui): 네이버 지도 링크 필드 UI 개선

- 레이블 "네이버 지도 URL" → "네이버 지도 링크" 변경
- add_restaurant_form: URL 입력 필드 추가 (공유 텍스트 붙여넣기 지원)
- edit_restaurant_dialog: URL 필드 항상 표시, URL 추출 로직 추가
- add_restaurant_dialog: naverUrl을 FetchedRestaurantJsonView에 전달
This commit is contained in:
JiWoong Sul
2026-01-28 18:54:59 +09:00
parent b989981464
commit 6426d14336
4 changed files with 105 additions and 83 deletions

View File

@@ -124,7 +124,7 @@ class _AddRestaurantDialogState extends ConsumerState<AddRestaurantDialog> {
_jibunAddressController.text = formData.jibunAddress; _jibunAddressController.text = formData.jibunAddress;
_latitudeController.text = formData.latitude; _latitudeController.text = formData.latitude;
_longitudeController.text = formData.longitude; _longitudeController.text = formData.longitude;
_naverUrlController.text = formData.naverUrl; // naverUrlController는 사용자 입력을 그대로 유지
} }
Future<void> _saveRestaurant() async { Future<void> _saveRestaurant() async {
@@ -234,6 +234,7 @@ class _AddRestaurantDialogState extends ConsumerState<AddRestaurantDialog> {
longitudeController: _longitudeController, longitudeController: _longitudeController,
naverUrlController: _naverUrlController, naverUrlController: _naverUrlController,
onFieldChanged: _onFormDataChanged, onFieldChanged: _onFormDataChanged,
naverUrl: state.formData.naverUrl,
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),

View File

@@ -1,5 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../services/restaurant_form_validator.dart'; import '../../../services/restaurant_form_validator.dart';
/// 식당 추가 폼 위젯 /// 식당 추가 폼 위젯
@@ -18,6 +20,7 @@ class AddRestaurantForm extends StatefulWidget {
final List<String> categories; final List<String> categories;
final List<String> subCategories; final List<String> subCategories;
final String geocodingStatus; final String geocodingStatus;
final TextEditingController? naverUrlController; // 네이버 지도 URL 입력
const AddRestaurantForm({ const AddRestaurantForm({
super.key, super.key,
@@ -35,6 +38,7 @@ class AddRestaurantForm extends StatefulWidget {
this.categories = const <String>[], this.categories = const <String>[],
this.subCategories = const <String>[], this.subCategories = const <String>[],
this.geocodingStatus = '', this.geocodingStatus = '',
this.naverUrlController,
}); });
@override @override
@@ -196,80 +200,14 @@ class _AddRestaurantFormState extends State<AddRestaurantForm> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// 위도/경도 입력 // 네이버 지도 URL (컨트롤러가 있는 경우 항상 표시)
Row( if (widget.naverUrlController != null)
children: [ _buildNaverUrlField(context),
Expanded(
child: TextFormField( if (widget.geocodingStatus.isNotEmpty)
controller: widget.latitudeController, Padding(
keyboardType: const TextInputType.numberWithOptions( padding: const EdgeInsets.only(top: 8),
decimal: true, child: Text(
),
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;
},
),
),
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, widget.geocodingStatus,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.blueGrey, color: Colors.blueGrey,
@@ -277,8 +215,6 @@ class _AddRestaurantFormState extends State<AddRestaurantForm> {
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
],
],
), ),
], ],
), ),
@@ -459,4 +395,66 @@ class _AddRestaurantFormState extends State<AddRestaurantForm> {
}, },
); );
} }
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<void> _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);
}
}
} }

View File

@@ -75,7 +75,7 @@ class AddRestaurantUrlTab extends StatelessWidget {
minLines: 1, minLines: 1,
maxLines: 6, maxLines: 6,
decoration: InputDecoration( decoration: InputDecoration(
labelText: '네이버 지도 URL', labelText: '네이버 지도 링크',
hintText: kIsWeb hintText: kIsWeb
? 'https://map.naver.com/...' ? 'https://map.naver.com/...'
: 'https://naver.me/...', : 'https://naver.me/...',

View File

@@ -32,6 +32,7 @@ class _EditRestaurantDialogState extends ConsumerState<EditRestaurantDialog> {
late final TextEditingController _jibunAddressController; late final TextEditingController _jibunAddressController;
late final TextEditingController _latitudeController; late final TextEditingController _latitudeController;
late final TextEditingController _longitudeController; late final TextEditingController _longitudeController;
late final TextEditingController _naverUrlController;
bool _isSaving = false; bool _isSaving = false;
late final String _originalRoadAddress; late final String _originalRoadAddress;
@@ -64,6 +65,9 @@ class _EditRestaurantDialogState extends ConsumerState<EditRestaurantDialog> {
_longitudeController = TextEditingController( _longitudeController = TextEditingController(
text: restaurant.longitude.toString(), text: restaurant.longitude.toString(),
); );
_naverUrlController = TextEditingController(
text: restaurant.naverUrl ?? '',
);
_originalRoadAddress = restaurant.roadAddress; _originalRoadAddress = restaurant.roadAddress;
_originalJibunAddress = restaurant.jibunAddress; _originalJibunAddress = restaurant.jibunAddress;
} }
@@ -79,6 +83,7 @@ class _EditRestaurantDialogState extends ConsumerState<EditRestaurantDialog> {
_jibunAddressController.dispose(); _jibunAddressController.dispose();
_latitudeController.dispose(); _latitudeController.dispose();
_longitudeController.dispose(); _longitudeController.dispose();
_naverUrlController.dispose();
super.dispose(); super.dispose();
} }
@@ -107,6 +112,9 @@ class _EditRestaurantDialogState extends ConsumerState<EditRestaurantDialog> {
_latitudeController.text = coords.latitude.toString(); _latitudeController.text = coords.latitude.toString();
_longitudeController.text = coords.longitude.toString(); _longitudeController.text = coords.longitude.toString();
// URL 추출: 공유 텍스트에서 URL만 추출
final extractedUrl = _extractNaverUrl(_naverUrlController.text.trim());
final updatedRestaurant = widget.restaurant.copyWith( final updatedRestaurant = widget.restaurant.copyWith(
name: _nameController.text.trim(), name: _nameController.text.trim(),
category: _categoryController.text.trim(), category: _categoryController.text.trim(),
@@ -125,6 +133,7 @@ class _EditRestaurantDialogState extends ConsumerState<EditRestaurantDialog> {
: _jibunAddressController.text.trim(), : _jibunAddressController.text.trim(),
latitude: coords.latitude, latitude: coords.latitude,
longitude: coords.longitude, longitude: coords.longitude,
naverUrl: extractedUrl.isEmpty ? null : extractedUrl,
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
needsAddressVerification: coords.usedCurrentLocation, needsAddressVerification: coords.usedCurrentLocation,
); );
@@ -201,6 +210,7 @@ class _EditRestaurantDialogState extends ConsumerState<EditRestaurantDialog> {
onFieldChanged: _onFieldChanged, onFieldChanged: _onFieldChanged,
categories: categories, categories: categories,
subCategories: subCategories, subCategories: subCategories,
naverUrlController: _naverUrlController,
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Row( Row(
@@ -288,4 +298,17 @@ class _EditRestaurantDialogState extends ConsumerState<EditRestaurantDialog> {
usedCurrentLocation: true, 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;
}
} }