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;
|
_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),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/...',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user