diff --git a/.gitignore b/.gitignore
index 79c113f..044af3b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
+
+# 민감한 정보가 포함된 파일
+lib/config/secrets.dart
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index d4deff8..b67e975 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -8,7 +8,7 @@ plugins {
android {
namespace = "com.example.heafit"
compileSdk = flutter.compileSdkVersion
- ndkVersion = flutter.ndkVersion
+ ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index f2c3c28..22765fd 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,4 +1,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/images/category/Bibimbap .png b/assets/images/category/Bibimbap .png
new file mode 100644
index 0000000..36654da
Binary files /dev/null and b/assets/images/category/Bibimbap .png differ
diff --git a/assets/images/category/Book.png b/assets/images/category/Book.png
new file mode 100644
index 0000000..38d37e2
Binary files /dev/null and b/assets/images/category/Book.png differ
diff --git a/assets/images/category/Bus.png b/assets/images/category/Bus.png
new file mode 100644
index 0000000..90e5179
Binary files /dev/null and b/assets/images/category/Bus.png differ
diff --git a/assets/images/category/Clothes .png b/assets/images/category/Clothes .png
new file mode 100644
index 0000000..f117fb0
Binary files /dev/null and b/assets/images/category/Clothes .png differ
diff --git a/assets/images/category/Coffee.png b/assets/images/category/Coffee.png
new file mode 100644
index 0000000..9274c33
Binary files /dev/null and b/assets/images/category/Coffee.png differ
diff --git a/assets/images/category/Exhibition .png b/assets/images/category/Exhibition .png
new file mode 100644
index 0000000..f8fa75b
Binary files /dev/null and b/assets/images/category/Exhibition .png differ
diff --git a/assets/images/category/Jjajangmyeon .png b/assets/images/category/Jjajangmyeon .png
new file mode 100644
index 0000000..bbbc277
Binary files /dev/null and b/assets/images/category/Jjajangmyeon .png differ
diff --git a/assets/images/category/Laptop.png b/assets/images/category/Laptop.png
new file mode 100644
index 0000000..0303113
Binary files /dev/null and b/assets/images/category/Laptop.png differ
diff --git a/assets/images/category/Makeup.png b/assets/images/category/Makeup.png
new file mode 100644
index 0000000..9ebeef8
Binary files /dev/null and b/assets/images/category/Makeup.png differ
diff --git a/assets/images/category/Movie.png b/assets/images/category/Movie.png
new file mode 100644
index 0000000..764a803
Binary files /dev/null and b/assets/images/category/Movie.png differ
diff --git a/assets/images/category/Performance .png b/assets/images/category/Performance .png
new file mode 100644
index 0000000..0c628c5
Binary files /dev/null and b/assets/images/category/Performance .png differ
diff --git a/assets/images/category/Spaghetti.png b/assets/images/category/Spaghetti.png
new file mode 100644
index 0000000..c1b9c4a
Binary files /dev/null and b/assets/images/category/Spaghetti.png differ
diff --git a/assets/images/category/Sushi.png b/assets/images/category/Sushi.png
new file mode 100644
index 0000000..5edc73a
Binary files /dev/null and b/assets/images/category/Sushi.png differ
diff --git a/assets/images/category/Tteokbokki.png b/assets/images/category/Tteokbokki.png
new file mode 100644
index 0000000..de13de5
Binary files /dev/null and b/assets/images/category/Tteokbokki.png differ
diff --git a/assets/images/category/subway.png b/assets/images/category/subway.png
new file mode 100644
index 0000000..12675f9
Binary files /dev/null and b/assets/images/category/subway.png differ
diff --git a/assets/images/change-cal.png b/assets/images/change-cal.png
new file mode 100644
index 0000000..8857d11
Binary files /dev/null and b/assets/images/change-cal.png differ
diff --git a/assets/images/like-category.png b/assets/images/like-category.png
new file mode 100644
index 0000000..ed39a77
Binary files /dev/null and b/assets/images/like-category.png differ
diff --git a/assets/images/use-al.png b/assets/images/use-al.png
new file mode 100644
index 0000000..9eb9329
Binary files /dev/null and b/assets/images/use-al.png differ
diff --git a/assets/logo/heafit-logo2.png b/assets/logo/heafit-logo2.png
new file mode 100644
index 0000000..bde7414
Binary files /dev/null and b/assets/logo/heafit-logo2.png differ
diff --git a/lib/config/secrets.dart.example b/lib/config/secrets.dart.example
new file mode 100644
index 0000000..e219077
--- /dev/null
+++ b/lib/config/secrets.dart.example
@@ -0,0 +1,7 @@
+/// 비밀 API 키 및 클라이언트 ID를 관리하는 클래스
+/// 이 파일은 예시 템플릿입니다. 실제 사용을 위해 secrets.dart로 복사한 후 실제 값을 입력하세요.
+class Secrets {
+ /// 구글 OAuth 클라이언트 ID
+ /// GCP 콘솔에서 발급받은 Android 클라이언트 ID를 여기에 입력
+ static const String googleClientId = 'YOUR_OAUTH_CLIENT_ID_HERE.apps.googleusercontent.com';
+}
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
index c101432..5b8f611 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:heafit/screens/splash_screen.dart';
import 'package:heafit/constants/theme.dart';
+import 'package:heafit/services/google_auth_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -10,6 +11,10 @@ void main() async {
// Hive 초기화
await Hive.initFlutter();
+ // Google Auth Service 초기화
+ final googleAuthService = GoogleAuthService();
+ await googleAuthService.init();
+
runApp(const MyApp());
}
@@ -23,7 +28,7 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
- themeMode: ThemeMode.system, // 시스템 설정에 따라 테마 적용
+ themeMode: ThemeMode.light, // 항상 라이트 테마 적용
home: const SplashScreen(),
);
}
diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart
index 78f1da8..9029352 100644
--- a/lib/screens/calendar_screen.dart
+++ b/lib/screens/calendar_screen.dart
@@ -1,90 +1,668 @@
import 'package:flutter/material.dart';
import 'package:heafit/constants/theme.dart';
import 'package:table_calendar/table_calendar.dart';
+import 'package:heafit/services/google_auth_service.dart';
+import 'package:google_sign_in/google_sign_in.dart';
+import 'package:googleapis/calendar/v3.dart' as calendar;
class CalendarScreen extends StatefulWidget {
- const CalendarScreen({Key? key}) : super(key: key);
+ final List? highlightDates;
+
+ const CalendarScreen({Key? key, this.highlightDates}) : super(key: key);
@override
State createState() => _CalendarScreenState();
}
class _CalendarScreenState extends State {
+ final GoogleAuthService _googleAuthService = GoogleAuthService();
+ bool _isLoadingAuth = true;
+ bool _isCalendarConnected = false;
+ bool _isLoadingEvents = false;
+
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
- // 임시 일정 데이터
- final Map>> _events = {};
+ // 이벤트 데이터
+ Map> _events = {};
@override
void initState() {
super.initState();
_selectedDay = _focusedDay;
- _loadEvents();
+ _checkCalendarConnection();
}
- // 임시 이벤트 데이터 로드
- void _loadEvents() {
- final now = DateTime.now();
-
- // 오늘 일정
- final today = DateTime(now.year, now.month, now.day);
- _events[today] = [
- {
- 'title': '스타벅스 방문',
- 'time': '15:00',
- 'benefit': '신한카드 50% 할인',
- 'isUsed': true,
- },
- {
- 'title': 'CGV 영화 관람',
- 'time': '19:30',
- 'benefit': '토스 결제 시 1+1',
- 'isUsed': false,
- },
- ];
-
- // 내일 일정
- final tomorrow = DateTime(now.year, now.month, now.day + 1);
- _events[tomorrow] = [
- {
- 'title': '배달의민족 주문',
- 'time': '12:00',
- 'benefit': '네이버페이 3,000원 할인',
- 'isUsed': null,
+ // 구글 캘린더 연결 상태 확인
+ Future _checkCalendarConnection() async {
+ await _googleAuthService.init();
+ setState(() {
+ _isCalendarConnected = _googleAuthService.isCalendarConnected;
+ _isLoadingAuth = false;
+ });
+
+ if (_isCalendarConnected) {
+ // 먼저 이벤트를 로드
+ await _loadEvents();
+
+ // 동기화 설정이 있는 경우에만 자동 동기화 수행
+ final syncSourceCalendarIds = _googleAuthService.syncSourceCalendarIds;
+ if (syncSourceCalendarIds.isNotEmpty) {
+ // 자동 동기화 로직 실행
+ await _syncSelectedCalendars();
+ } else {
+ setState(() {
+ _isLoadingEvents = false;
+ });
+ }
+ }
+ }
+
+ // 동기화 설정에 따라 캘린더 동기화 수행
+ Future _syncSelectedCalendars() async {
+ if (!_isCalendarConnected) return;
+
+ // 저장된 동기화 설정 가져오기
+ final syncSourceCalendarIds = _googleAuthService.syncSourceCalendarIds;
+
+ if (syncSourceCalendarIds.isEmpty) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('캘린더 동기화가 해제되어 있습니다. 프로필에서 동기화 설정을 확인해주세요.'),
+ backgroundColor: Colors.blue,
+ duration: Duration(seconds: 3),
+ ),
+ );
+ setState(() {
+ _isLoadingEvents = false;
+ });
+ return;
+ }
+
+ setState(() {
+ _isLoadingEvents = true;
+ });
+
+ try {
+ // 동기화 기간 설정: 현재 월 및 이전 월의 일정만 동기화
+ final now = DateTime.now();
+ // 현재 년도의 1월 1일 (1월이면 전년도의 12월 1일)
+ final startMonth = now.month > 1 ? 1 : 12;
+ final startYear = now.month > 1 ? now.year : now.year - 1;
+ final startTime = DateTime(startYear, startMonth, 1);
+
+ // 현재 월의 마지막 날
+ final endTime = DateTime(now.year, now.month + 1, 0);
+
+ debugPrint(
+ '캘린더 동기화 시작 - 범위: ${startTime.toString()} ~ ${endTime.toString()}',
+ );
+
+ final syncedCount = await _googleAuthService.syncCalendarToHeafit(
+ sourceCalendarIds: syncSourceCalendarIds,
+ startTime: startTime,
+ endTime: endTime,
+ );
+
+ if (syncedCount > 0) {
+ // 동기화 후 이벤트 다시 로드
+ await _loadEvents();
+
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ '$syncedCount개의 일정이 동기화되었습니다.\n현재 월과 이전 월의 일정만 동기화됩니다.',
+ ),
+ backgroundColor: AppTheme.primaryColor,
+ duration: const Duration(seconds: 3),
+ ),
+ );
+ } else if (syncedCount == 0) {
+ // 동기화할 내용이 없는 경우
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('새로운 동기화 대상이 없습니다. 모든 일정이 최신 상태입니다.'),
+ backgroundColor: Colors.orange,
+ duration: const Duration(seconds: 2),
+ ),
+ );
+ }
+ } catch (e) {
+ debugPrint('자동 동기화 오류: $e');
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('동기화 중 오류가 발생했습니다: $e'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ } finally {
+ setState(() {
+ _isLoadingEvents = false;
+ });
+ }
+ }
+
+ // 구글 계정으로 로그인하고 캘린더 연결
+ Future _connectGoogleCalendar() async {
+ setState(() {
+ _isLoadingAuth = true;
+ });
+
+ final success = await _googleAuthService.signIn();
+
+ setState(() {
+ _isCalendarConnected = success;
+ _isLoadingAuth = false;
+ });
+
+ if (success) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('구글 캘린더가 성공적으로 연결되었습니다!'),
+ backgroundColor: AppTheme.primaryColor,
+ ),
+ );
+ await _loadEvents();
+ } else {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('구글 캘린더 연결에 실패했습니다. 다시 시도해주세요.'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ }
+
+ // 캘린더 설정 다이얼로그 표시
+ void _showCalendarSettings() {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return StatefulBuilder(
+ builder: (context, setState) {
+ return AlertDialog(
+ title: const Text('캘린더 설정'),
+ content: SizedBox(
+ width: double.maxFinite,
+ height: 300,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '표시할 캘린더',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Expanded(
+ child: ListView.builder(
+ itemCount: _googleAuthService.userCalendars.length,
+ itemBuilder: (context, index) {
+ final calendar =
+ _googleAuthService.userCalendars[index];
+ final calendarId = calendar.id ?? '';
+ final isVisible = _googleAuthService
+ .isCalendarVisible(calendarId);
+
+ return CheckboxListTile(
+ title: Text(calendar.summary ?? '이름 없음'),
+ subtitle: Text(
+ calendar.id == _googleAuthService.heafitCalendarId
+ ? 'Heafit 캘린더'
+ : calendar.id == 'primary'
+ ? '기본 캘린더'
+ : '',
+ ),
+ value: isVisible,
+ activeColor: AppTheme.primaryColor,
+ onChanged: (value) async {
+ if (value == true) {
+ await _googleAuthService.addVisibleCalendar(
+ calendarId,
+ );
+ } else {
+ await _googleAuthService.removeVisibleCalendar(
+ calendarId,
+ );
+ }
+
+ setState(() {});
+
+ // 메인 상태 업데이트를 위한 이벤트 다시 로드
+ if (mounted) {
+ await _loadEvents();
+ this.setState(() {});
+ }
+ },
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('확인'),
+ ),
+ ],
+ );
+ },
+ );
},
- ];
-
- // 일주일 후 일정
- final nextWeek = DateTime(now.year, now.month, now.day + 7);
- _events[nextWeek] = [
- {
- 'title': '올리브영 방문',
- 'time': '14:00',
- 'benefit': '현대카드 20% 할인',
- 'isUsed': null,
+ );
+ }
+
+ // 일정 추가 다이얼로그 표시
+ void _showAddEventDialog() {
+ final titleController = TextEditingController();
+ final descriptionController = TextEditingController();
+ final locationController = TextEditingController();
+
+ DateTime selectedDate = _selectedDay ?? DateTime.now();
+ TimeOfDay startTime = TimeOfDay.now();
+ TimeOfDay endTime = TimeOfDay(
+ hour: TimeOfDay.now().hour + 1,
+ minute: TimeOfDay.now().minute,
+ );
+
+ showDialog(
+ context: context,
+ builder: (context) {
+ return StatefulBuilder(
+ builder: (context, setState) {
+ return AlertDialog(
+ title: const Text('혜택 일정 추가'),
+ content: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ TextField(
+ controller: titleController,
+ decoration: const InputDecoration(
+ labelText: '제목',
+ hintText: '예: 스타벅스 방문',
+ ),
+ ),
+ const SizedBox(height: 12),
+ TextField(
+ controller: descriptionController,
+ decoration: const InputDecoration(
+ labelText: '혜택 설명',
+ hintText: '예: 신한카드 50% 할인',
+ ),
+ maxLines: 2,
+ ),
+ const SizedBox(height: 12),
+ TextField(
+ controller: locationController,
+ decoration: const InputDecoration(
+ labelText: '위치 (선택)',
+ hintText: '예: 강남역점',
+ ),
+ ),
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ const Text(
+ '날짜: ',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ TextButton(
+ onPressed: () async {
+ final pickedDate = await showDatePicker(
+ context: context,
+ initialDate: selectedDate,
+ firstDate: DateTime.now(),
+ lastDate: DateTime.now().add(
+ const Duration(days: 365),
+ ),
+ );
+ if (pickedDate != null) {
+ setState(() {
+ selectedDate = pickedDate;
+ });
+ }
+ },
+ child: Text(
+ '${selectedDate.year}년 ${selectedDate.month}월 ${selectedDate.day}일',
+ ),
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ const Text(
+ '시작 시간: ',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ TextButton(
+ onPressed: () async {
+ final pickedTime = await showTimePicker(
+ context: context,
+ initialTime: startTime,
+ );
+ if (pickedTime != null) {
+ setState(() {
+ startTime = pickedTime;
+ // 종료 시간이 시작 시간보다 이전이면 조정
+ if (startTime.hour > endTime.hour ||
+ (startTime.hour == endTime.hour &&
+ startTime.minute >= endTime.minute)) {
+ endTime = TimeOfDay(
+ hour: startTime.hour + 1,
+ minute: startTime.minute,
+ );
+ }
+ });
+ }
+ },
+ child: Text(
+ '${startTime.hour}:${startTime.minute.toString().padLeft(2, '0')}',
+ ),
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ const Text(
+ '종료 시간: ',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ TextButton(
+ onPressed: () async {
+ final pickedTime = await showTimePicker(
+ context: context,
+ initialTime: endTime,
+ );
+ if (pickedTime != null) {
+ setState(() {
+ endTime = pickedTime;
+ });
+ }
+ },
+ child: Text(
+ '${endTime.hour}:${endTime.minute.toString().padLeft(2, '0')}',
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('취소'),
+ ),
+ ElevatedButton(
+ onPressed: () async {
+ if (titleController.text.isEmpty) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('제목을 입력해주세요.')),
+ );
+ return;
+ }
+
+ // 일정 추가
+ final startDateTime = DateTime(
+ selectedDate.year,
+ selectedDate.month,
+ selectedDate.day,
+ startTime.hour,
+ startTime.minute,
+ );
+
+ final endDateTime = DateTime(
+ selectedDate.year,
+ selectedDate.month,
+ selectedDate.day,
+ endTime.hour,
+ endTime.minute,
+ );
+
+ final success = await _googleAuthService.addEventToCalendar(
+ title: titleController.text,
+ description: descriptionController.text,
+ startTime: startDateTime,
+ endTime: endDateTime,
+ location:
+ locationController.text.isEmpty
+ ? null
+ : locationController.text,
+ calendarId: _googleAuthService.heafitCalendarId,
+ );
+
+ Navigator.pop(context);
+
+ if (success) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('일정이 추가되었습니다.'),
+ backgroundColor: AppTheme.primaryColor,
+ ),
+ );
+
+ // 이벤트 목록 새로고침
+ await _loadEvents();
+ setState(() {});
+ } else {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('일정 추가에 실패했습니다.'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppTheme.primaryColor,
+ ),
+ child: const Text('추가'),
+ ),
+ ],
+ );
+ },
+ );
},
- ];
+ );
}
- List