feat(app): add vworld geocoding and native ads placeholders
This commit is contained in:
@@ -9,6 +9,7 @@ import '../../../domain/entities/visit_record.dart';
|
||||
import '../../providers/recommendation_provider.dart';
|
||||
import '../../providers/debug_test_data_provider.dart';
|
||||
import '../../providers/visit_provider.dart';
|
||||
import '../../widgets/native_ad_placeholder.dart';
|
||||
import 'widgets/visit_record_card.dart';
|
||||
import 'widgets/recommendation_record_card.dart';
|
||||
import 'widgets/visit_statistics.dart';
|
||||
@@ -106,129 +107,144 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen>
|
||||
_events = _buildEvents(visits, recommendations);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (kDebugMode)
|
||||
const DebugTestDataBanner(
|
||||
margin: EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
),
|
||||
// 캘린더
|
||||
Card(
|
||||
margin: const EdgeInsets.all(16),
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: TableCalendar(
|
||||
firstDay: DateTime.utc(2025, 1, 1),
|
||||
lastDay: DateTime.utc(2030, 12, 31),
|
||||
focusedDay: _focusedDay,
|
||||
calendarFormat: _calendarFormat,
|
||||
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = focusedDay;
|
||||
});
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
setState(() {
|
||||
_calendarFormat = format;
|
||||
});
|
||||
},
|
||||
eventLoader: _getEventsForDay,
|
||||
calendarBuilders: CalendarBuilders(
|
||||
markerBuilder: (context, day, events) {
|
||||
if (events.isEmpty) return null;
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: Column(
|
||||
children: [
|
||||
if (kDebugMode)
|
||||
const DebugTestDataBanner(
|
||||
margin: EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
),
|
||||
Card(
|
||||
margin: const EdgeInsets.all(16),
|
||||
color: isDark
|
||||
? AppColors.darkSurface
|
||||
: AppColors.lightSurface,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: TableCalendar(
|
||||
firstDay: DateTime.utc(2025, 1, 1),
|
||||
lastDay: DateTime.utc(2030, 12, 31),
|
||||
focusedDay: _focusedDay,
|
||||
calendarFormat: _calendarFormat,
|
||||
selectedDayPredicate: (day) =>
|
||||
isSameDay(_selectedDay, day),
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = focusedDay;
|
||||
});
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
setState(() {
|
||||
_calendarFormat = format;
|
||||
});
|
||||
},
|
||||
eventLoader: _getEventsForDay,
|
||||
calendarBuilders: CalendarBuilders(
|
||||
markerBuilder: (context, day, events) {
|
||||
if (events.isEmpty) return null;
|
||||
|
||||
final calendarEvents = events.cast<_CalendarEvent>();
|
||||
final confirmedVisits = calendarEvents.where(
|
||||
(e) => e.visitRecord?.isConfirmed == true,
|
||||
);
|
||||
final recommendedOnly = calendarEvents.where(
|
||||
(e) => e.recommendationRecord != null,
|
||||
);
|
||||
final calendarEvents = events
|
||||
.cast<_CalendarEvent>();
|
||||
final confirmedVisits = calendarEvents.where(
|
||||
(e) => e.visitRecord?.isConfirmed == true,
|
||||
);
|
||||
final recommendedOnly = calendarEvents.where(
|
||||
(e) => e.recommendationRecord != null,
|
||||
);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (confirmedVisits.isNotEmpty)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.lightPrimary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (confirmedVisits.isNotEmpty)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 1,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.lightPrimary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
if (recommendedOnly.isNotEmpty)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 1,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
calendarStyle: CalendarStyle(
|
||||
outsideDaysVisible: false,
|
||||
selectedDecoration: const BoxDecoration(
|
||||
color: AppColors.lightPrimary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
if (recommendedOnly.isNotEmpty)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
todayDecoration: BoxDecoration(
|
||||
color: AppColors.lightPrimary.withOpacity(0.5),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
calendarStyle: CalendarStyle(
|
||||
outsideDaysVisible: false,
|
||||
selectedDecoration: const BoxDecoration(
|
||||
color: AppColors.lightPrimary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
todayDecoration: BoxDecoration(
|
||||
color: AppColors.lightPrimary.withOpacity(0.5),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
markersMaxCount: 2,
|
||||
markerDecoration: const BoxDecoration(
|
||||
color: AppColors.lightSecondary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
weekendTextStyle: const TextStyle(
|
||||
color: AppColors.lightError,
|
||||
),
|
||||
),
|
||||
headerStyle: HeaderStyle(
|
||||
formatButtonVisible: true,
|
||||
titleCentered: true,
|
||||
formatButtonShowsNext: false,
|
||||
formatButtonDecoration: BoxDecoration(
|
||||
color: AppColors.lightPrimary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
formatButtonTextStyle: const TextStyle(
|
||||
color: AppColors.lightPrimary,
|
||||
),
|
||||
markersMaxCount: 2,
|
||||
markerDecoration: const BoxDecoration(
|
||||
color: AppColors.lightSecondary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
weekendTextStyle: const TextStyle(
|
||||
color: AppColors.lightError,
|
||||
),
|
||||
),
|
||||
headerStyle: HeaderStyle(
|
||||
formatButtonVisible: true,
|
||||
titleCentered: true,
|
||||
formatButtonShowsNext: false,
|
||||
formatButtonDecoration: BoxDecoration(
|
||||
color: AppColors.lightPrimary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
formatButtonTextStyle: const TextStyle(
|
||||
color: AppColors.lightPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildLegend('추천받음', Colors.orange, isDark),
|
||||
const SizedBox(width: 24),
|
||||
_buildLegend('방문완료', Colors.green, isDark),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const NativeAdPlaceholder(
|
||||
margin: EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
),
|
||||
_buildDayRecords(_selectedDay, isDark),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 범례
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildLegend('추천받음', Colors.orange, isDark),
|
||||
const SizedBox(width: 24),
|
||||
_buildLegend('방문완료', Colors.green, isDark),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 선택된 날짜의 기록
|
||||
Expanded(child: _buildDayRecords(_selectedDay, isDark)),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -302,30 +318,35 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen>
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: events.length,
|
||||
itemBuilder: (context, index) {
|
||||
final event = events[index];
|
||||
if (event.visitRecord != null) {
|
||||
return VisitRecordCard(
|
||||
visitRecord: event.visitRecord!,
|
||||
onTap: () {},
|
||||
);
|
||||
}
|
||||
if (event.recommendationRecord != null) {
|
||||
return RecommendationRecordCard(
|
||||
recommendation: event.recommendationRecord!,
|
||||
onConfirmVisit: () async {
|
||||
await ref
|
||||
.read(recommendationNotifierProvider.notifier)
|
||||
.confirmVisit(event.recommendationRecord!.id);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: events.length,
|
||||
itemBuilder: (context, index) {
|
||||
final event = events[index];
|
||||
if (event.visitRecord != null) {
|
||||
return VisitRecordCard(
|
||||
visitRecord: event.visitRecord!,
|
||||
onTap: () {},
|
||||
);
|
||||
}
|
||||
if (event.recommendationRecord != null) {
|
||||
return RecommendationRecordCard(
|
||||
recommendation: event.recommendationRecord!,
|
||||
onConfirmVisit: () async {
|
||||
await ref
|
||||
.read(recommendationNotifierProvider.notifier)
|
||||
.confirmVisit(event.recommendationRecord!.id);
|
||||
},
|
||||
onDelete: () async {
|
||||
await ref
|
||||
.read(recommendationNotifierProvider.notifier)
|
||||
.deleteRecommendation(event.recommendationRecord!.id);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user