Figma 설명 프레임 반영 — 로그인·온보딩·스플래시 · HOME · GALAXY · 퀘스트·이클립스·알림
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -8,10 +9,13 @@ import 'package:latlong2/latlong.dart';
|
||||
import '../../core/theme/tokens.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import 'quest_data.dart';
|
||||
import 'quest_evidence.dart';
|
||||
|
||||
/// 위치 선택 — 화면 대조 원장 F4 · Figma `371:356705`(지도 + 핀 주소 + 「선택 완료」).
|
||||
///
|
||||
/// 지도를 누르면 그 자리에 핀을 꽂고 주소를 보여 준다. 운영이 등록한 장소는 지도 위 표시와
|
||||
/// 명세 `523:18207` — 지도는 *디폴트 사용자 현재 위치*, 위치 마크는 *지도 영역 중간 정렬*, 지도를
|
||||
/// 움직이면 가운데 좌표의 주소를 *실시간* 으로 보이고, *선택 완료* 는 위치 정보를 정상 호출한 뒤 켜진다.
|
||||
/// 운영이 등록한 장소는 지도 위 표시와
|
||||
/// 아래 목록으로 함께 보이고, 고르면 그 장소를 쓴다(해석 확인 요청 영역 B — 장소 등록 주체 권고안:
|
||||
/// 사용자 핀 → 장소 생성 · 운영 장소 우선 노출). 보상 인증 반경은 서버가 B-27(기본 100m)로 판정한다.
|
||||
///
|
||||
@@ -42,6 +46,10 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
||||
String _query = '';
|
||||
QuestPlace? _selected;
|
||||
|
||||
/// 가운데 좌표의 주소를 찾는 중 — 이 동안은 *선택 완료* 를 잠근다(명세 「정상 호출 후 활성화」).
|
||||
bool _resolving = false;
|
||||
Timer? _settle;
|
||||
|
||||
List<QuestPlace> get _visible => widget.places
|
||||
.where((p) => _query.isEmpty || p.name.contains(_query))
|
||||
.toList();
|
||||
@@ -53,25 +61,65 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
||||
return PlacePickerScreen.fallbackCenter;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_locate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_settle?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 디폴트 사용자 현재 위치. 권한이 없거나 꺼져 있으면 지금 화면(등록 장소 · 서울시청)에 둔다 —
|
||||
/// 탐색은 권한 없이 열린다(B-27). 이때는 사용자가 지도를 움직여야 위치가 정해진다.
|
||||
Future<void> _locate() async {
|
||||
try {
|
||||
final fix = await devicePlaceFix();
|
||||
if (!mounted || _selected != null) return;
|
||||
final here = LatLng(fix.lat, fix.lng);
|
||||
_map.move(here, _map.camera.zoom);
|
||||
await _drop(here);
|
||||
} catch (_) {
|
||||
// 권한 거부 · 위치 꺼짐 · 플랫폼 구현 없음(테스트) — 기본 화면 유지.
|
||||
}
|
||||
}
|
||||
|
||||
void _choose(QuestPlace place) {
|
||||
setState(() => _selected = place);
|
||||
_settle?.cancel();
|
||||
setState(() {
|
||||
_selected = place;
|
||||
_resolving = false;
|
||||
});
|
||||
if (place.lat != null && place.lng != null) {
|
||||
_map.move(LatLng(place.lat!, place.lng!), _map.camera.zoom);
|
||||
}
|
||||
}
|
||||
|
||||
/// 지도를 누른 자리에 핀 — 주소는 단말 지오코더로 찾고, 못 찾으면 좌표를 그대로 보인다.
|
||||
/// 사용자가 지도를 움직이면 멈춘 뒤 가운데 좌표로 위치를 정한다(제스처가 아닌 이동 — 등록 장소
|
||||
/// 선택 · 현재 위치 이동 — 은 무시).
|
||||
void _moved(MapCamera camera, bool hasGesture) {
|
||||
if (!hasGesture) return;
|
||||
_settle?.cancel();
|
||||
setState(() => _resolving = true);
|
||||
_settle = Timer(const Duration(milliseconds: 400), () => _drop(camera.center));
|
||||
}
|
||||
|
||||
/// 가운데 좌표의 주소 — 단말 지오코더로 찾고, 못 찾으면 좌표를 그대로 보인다.
|
||||
Future<void> _drop(LatLng point) async {
|
||||
final coords =
|
||||
'${point.latitude.toStringAsFixed(5)}, ${point.longitude.toStringAsFixed(5)}';
|
||||
setState(
|
||||
() => _selected = QuestPlace(
|
||||
setState(() {
|
||||
_resolving = true;
|
||||
_selected = QuestPlace(
|
||||
id: '',
|
||||
name: coords,
|
||||
lat: point.latitude,
|
||||
lng: point.longitude,
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
var address = coords;
|
||||
try {
|
||||
final marks = await Geocoding().placemarkFromCoordinates(
|
||||
@@ -86,14 +134,15 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
||||
// 지오코더가 없거나(에뮬레이터 · 테스트 — 플랫폼 구현 없음) 실패 — 좌표 표시로 둔다.
|
||||
}
|
||||
if (!mounted || _selected?.lat != point.latitude) return;
|
||||
setState(
|
||||
() => _selected = QuestPlace(
|
||||
setState(() {
|
||||
_resolving = false;
|
||||
_selected = QuestPlace(
|
||||
id: '',
|
||||
name: address,
|
||||
lat: point.latitude,
|
||||
lng: point.longitude,
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -140,7 +189,7 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
||||
options: MapOptions(
|
||||
initialCenter: _center,
|
||||
initialZoom: 15,
|
||||
onTap: (_, point) => _drop(point),
|
||||
onPositionChanged: _moved,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
@@ -163,17 +212,20 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (pinned != null &&
|
||||
pinned.lat != null &&
|
||||
pinned.lng != null)
|
||||
Marker(
|
||||
point: LatLng(pinned.lat!, pinned.lng!),
|
||||
],
|
||||
),
|
||||
// 위치 마크 — *지도 영역 중간 정렬*. 핀 끝이 지도 가운데(선택 좌표)를 가리킨다.
|
||||
const IgnorePointer(
|
||||
child: Align(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 44),
|
||||
child: SizedBox(
|
||||
width: 38,
|
||||
height: 44,
|
||||
alignment: Alignment.topCenter,
|
||||
child: const CustomPaint(painter: _PinPainter()),
|
||||
child: CustomPaint(painter: _PinPainter()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// OSM 타일 이용 조건 — 출처 표기.
|
||||
const Align(
|
||||
@@ -227,7 +279,7 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: FilledButton(
|
||||
onPressed: pinned == null
|
||||
onPressed: pinned == null || _resolving
|
||||
? null
|
||||
: () => Navigator.of(context).pop(pinned),
|
||||
style: FilledButton.styleFrom(
|
||||
|
||||
Reference in New Issue
Block a user