140 lines
4.8 KiB
Dart
140 lines
4.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../core/theme/tokens.dart';
|
|
import '../../core/i18n/app_locale.dart';
|
|
|
|
/// 위치 선택 — 화면 대조 원장 F4 · Figma `371:356705`.
|
|
///
|
|
/// 명세 `567:19531` — *분류 특정 위치를 선택했을 경우 목표 수치(특정 위치) 필드 활성화*.
|
|
///
|
|
/// **지도를 붙이지 않았다.** 지오펜스 반경(D-08)과 대상 장소 등록이 모두 Admin
|
|
/// 운영값이고 아직 확정되지 않았다. 지금 지도 SDK 를 넣으면 최소 지원 OS 가
|
|
/// 다시 움직인다(플러그인 선검증 §3.5). **장소 선택 동선은 여기서 완결**되고,
|
|
/// 지도 표시는 대상 장소가 확정된 뒤 이 화면 안에서 바뀐다.
|
|
class PlacePickerScreen extends StatefulWidget {
|
|
const PlacePickerScreen({super.key, this.places = defaultPlaces});
|
|
|
|
final List<String> places;
|
|
|
|
/// 어드민에 등록될 장소의 표본. **정책값이 아니다.**
|
|
static const defaultPlaces = [
|
|
'홍대 팝업스토어',
|
|
'HYBE 사옥',
|
|
'SM 코엑스 아티움',
|
|
'올림픽공원 체조경기장',
|
|
'고척 스카이돔',
|
|
];
|
|
|
|
@override
|
|
State<PlacePickerScreen> createState() => _PlacePickerScreenState();
|
|
}
|
|
|
|
class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
|
String _query = '';
|
|
String? _selected;
|
|
|
|
List<String> get _visible => widget.places
|
|
.where((p) => _query.isEmpty || p.contains(_query))
|
|
.toList();
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Scaffold(
|
|
appBar: AppBar(leading: const CloseButton()),
|
|
body: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// 시안 `371:356705` 머리 — 안내 문구 + 고른 장소. 지도 자리는 아래 검색 목록이다.
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 20, 16, 20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
context.l10n.placePickPrompt,
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
height: 1.4,
|
|
letterSpacing: -0.45,
|
|
color: AppColors.onCosmic,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
_selected ?? '',
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
height: 1.4,
|
|
color: Color(0xFFCDCDCD),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4),
|
|
child: TextField(
|
|
decoration: InputDecoration(
|
|
labelText: context.l10n.placeSearch,
|
|
prefixIcon: Icon(Icons.search),
|
|
),
|
|
onChanged: (v) => setState(() => _query = v.trim()),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _visible.isEmpty
|
|
? Center(
|
|
child: Text(
|
|
context.l10n.commonNoResults,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
)
|
|
// 디폴트 미선택 · 단수 선택.
|
|
: RadioGroup<String>(
|
|
groupValue: _selected,
|
|
onChanged: (v) => setState(() => _selected = v),
|
|
child: ListView.builder(
|
|
itemCount: _visible.length,
|
|
itemBuilder: (context, i) {
|
|
final place = _visible[i];
|
|
return RadioListTile<String>(
|
|
value: place,
|
|
title: Text(place),
|
|
secondary: const Icon(Icons.place_outlined),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
SafeArea(
|
|
top: false,
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
|
child: FilledButton(
|
|
onPressed: _selected == null
|
|
? null
|
|
: () => Navigator.of(context).pop(_selected),
|
|
style: FilledButton.styleFrom(
|
|
minimumSize: const Size.fromHeight(56),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.sm),
|
|
),
|
|
backgroundColor: AppColors.cosmicAccent,
|
|
foregroundColor: Colors.black,
|
|
textStyle: const TextStyle(
|
|
fontFamily: AppFonts.body,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
height: 24 / 16,
|
|
),
|
|
),
|
|
child: Text(context.l10n.commonSelectDone),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|