feat(ui): 네이버 지도 링크 필드 UI 개선
- 레이블 "네이버 지도 URL" → "네이버 지도 링크" 변경 - add_restaurant_form: URL 입력 필드 추가 (공유 텍스트 붙여넣기 지원) - edit_restaurant_dialog: URL 필드 항상 표시, URL 추출 로직 추가 - add_restaurant_dialog: naverUrl을 FetchedRestaurantJsonView에 전달
This commit is contained in:
@@ -124,7 +124,7 @@ class _AddRestaurantDialogState extends ConsumerState<AddRestaurantDialog> {
|
||||
_jibunAddressController.text = formData.jibunAddress;
|
||||
_latitudeController.text = formData.latitude;
|
||||
_longitudeController.text = formData.longitude;
|
||||
_naverUrlController.text = formData.naverUrl;
|
||||
// naverUrlController는 사용자 입력을 그대로 유지
|
||||
}
|
||||
|
||||
Future<void> _saveRestaurant() async {
|
||||
@@ -234,6 +234,7 @@ class _AddRestaurantDialogState extends ConsumerState<AddRestaurantDialog> {
|
||||
longitudeController: _longitudeController,
|
||||
naverUrlController: _naverUrlController,
|
||||
onFieldChanged: _onFormDataChanged,
|
||||
naverUrl: state.formData.naverUrl,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -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<String> categories;
|
||||
final List<String> subCategories;
|
||||
final String geocodingStatus;
|
||||
final TextEditingController? naverUrlController; // 네이버 지도 URL 입력
|
||||
|
||||
const AddRestaurantForm({
|
||||
super.key,
|
||||
@@ -35,6 +38,7 @@ class AddRestaurantForm extends StatefulWidget {
|
||||
this.categories = const <String>[],
|
||||
this.subCategories = const <String>[],
|
||||
this.geocodingStatus = '',
|
||||
this.naverUrlController,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -196,80 +200,14 @@ class _AddRestaurantFormState extends State<AddRestaurantForm> {
|
||||
),
|
||||
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;
|
||||
},
|
||||
),
|
||||
),
|
||||
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(
|
||||
// 네이버 지도 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,
|
||||
@@ -277,8 +215,6 @@ class _AddRestaurantFormState extends State<AddRestaurantForm> {
|
||||
),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/...',
|
||||
|
||||
@@ -32,6 +32,7 @@ class _EditRestaurantDialogState extends ConsumerState<EditRestaurantDialog> {
|
||||
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<EditRestaurantDialog> {
|
||||
_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<EditRestaurantDialog> {
|
||||
_jibunAddressController.dispose();
|
||||
_latitudeController.dispose();
|
||||
_longitudeController.dispose();
|
||||
_naverUrlController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -107,6 +112,9 @@ class _EditRestaurantDialogState extends ConsumerState<EditRestaurantDialog> {
|
||||
_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<EditRestaurantDialog> {
|
||||
: _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<EditRestaurantDialog> {
|
||||
onFieldChanged: _onFieldChanged,
|
||||
categories: categories,
|
||||
subCategories: subCategories,
|
||||
naverUrlController: _naverUrlController,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
@@ -288,4 +298,17 @@ class _EditRestaurantDialogState extends ConsumerState<EditRestaurantDialog> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user