tdd 진행
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../config/api_config.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../screens/auth/login_screen.dart';
|
||||
@@ -23,8 +24,10 @@ class ApiService {
|
||||
contentType: 'application/json',
|
||||
));
|
||||
|
||||
// 쿠키 관리자 설정
|
||||
_dio.interceptors.add(CookieManager(_cookieJar));
|
||||
// 쿠키 관리자 설정 (웹 환경에서는 사용하지 않음)
|
||||
if (!kIsWeb) {
|
||||
_dio.interceptors.add(CookieManager(_cookieJar));
|
||||
}
|
||||
|
||||
// 로그 인터셉터 (디버깅용)
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
|
||||
@@ -12,7 +12,13 @@ class ClubService with ChangeNotifier {
|
||||
Club? _currentClub;
|
||||
bool _isLoading = false;
|
||||
String? _token;
|
||||
final ApiService _apiService = ApiService();
|
||||
ApiService _apiService;
|
||||
|
||||
// 기본 생성자
|
||||
ClubService() : _apiService = ApiService();
|
||||
|
||||
// 테스트용 생성자
|
||||
ClubService.forTest(this._apiService);
|
||||
|
||||
List<Club> get clubs => [..._clubs];
|
||||
Club? get currentClub => _currentClub;
|
||||
|
||||
@@ -15,6 +15,11 @@ class EventBus {
|
||||
_streamController.add(event);
|
||||
}
|
||||
|
||||
// 특정 타입의 이벤트를 구독하는 메서드
|
||||
Stream<T> on<T>() {
|
||||
return stream.where((event) => event is T).cast<T>();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_streamController.close();
|
||||
}
|
||||
|
||||
@@ -3,17 +3,33 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../config/api_config.dart';
|
||||
import '../models/event_model.dart';
|
||||
import '../models/participant_model.dart';
|
||||
import '../models/score_model.dart';
|
||||
import '../models/team_model.dart';
|
||||
import './api_service.dart';
|
||||
|
||||
class EventService with ChangeNotifier {
|
||||
List<Event> _events = [];
|
||||
Event? _currentEvent;
|
||||
List<Participant> _participants = [];
|
||||
List<Score> _scores = [];
|
||||
List<Team> _teams = [];
|
||||
bool _isLoading = false;
|
||||
String? _token;
|
||||
final ApiService _apiService = ApiService();
|
||||
ApiService _apiService;
|
||||
String? _clubId;
|
||||
|
||||
// 기본 생성자
|
||||
EventService() : _apiService = ApiService();
|
||||
|
||||
// 테스트용 생성자
|
||||
EventService.forTest(this._apiService);
|
||||
|
||||
List<Event> get events => [..._events];
|
||||
Event? get currentEvent => _currentEvent;
|
||||
List<Participant> get participants => [..._participants];
|
||||
List<Score> get scores => [..._scores];
|
||||
List<Team> get teams => [..._teams];
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
// 초기화 함수
|
||||
@@ -182,4 +198,464 @@ class EventService with ChangeNotifier {
|
||||
throw Exception('이벤트 삭제에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 참가자 관리 기능
|
||||
// 이벤트 참가자 목록 가져오기
|
||||
Future<List<Participant>> fetchEventParticipants(String eventId) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.clubs}/events/participants',
|
||||
data: {'eventId': eventId},
|
||||
);
|
||||
|
||||
final List<dynamic> participantsData = data['participants'] ?? [];
|
||||
_participants = participantsData
|
||||
.map((participantData) => Participant.fromJson(participantData))
|
||||
.toList();
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return _participants;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('참가자 목록을 불러오는데 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 참가자 추가
|
||||
Future<Participant> addParticipant(
|
||||
String eventId,
|
||||
Map<String, dynamic> participantData,
|
||||
) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.clubs}/events/$eventId/participants',
|
||||
data: participantData,
|
||||
);
|
||||
|
||||
final newParticipant = Participant.fromJson(data['participant']);
|
||||
|
||||
_participants.add(newParticipant);
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return newParticipant;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('참가자 추가에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 참가자 정보 업데이트
|
||||
Future<Participant> updateParticipant(
|
||||
String eventId,
|
||||
String participantId,
|
||||
Map<String, dynamic> updates,
|
||||
) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final data = await _apiService.put(
|
||||
'${ApiConfig.clubs}/events/$eventId/participants/$participantId',
|
||||
data: updates,
|
||||
);
|
||||
|
||||
final updatedParticipant = Participant.fromJson(data['participant']);
|
||||
|
||||
// 참가자 목록 업데이트
|
||||
final index = _participants.indexWhere(
|
||||
(participant) => participant.id == participantId,
|
||||
);
|
||||
if (index >= 0) {
|
||||
_participants[index] = updatedParticipant;
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return updatedParticipant;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('참가자 정보 업데이트에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 참가자 삭제
|
||||
Future<bool> removeParticipant(String eventId, String participantId) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _apiService.delete(
|
||||
'${ApiConfig.clubs}/events/$eventId/participants/$participantId',
|
||||
);
|
||||
|
||||
// 참가자 목록에서 삭제
|
||||
_participants.removeWhere(
|
||||
(participant) => participant.id == participantId,
|
||||
);
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('참가자 삭제에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 참가자 상태 업데이트
|
||||
Future<Participant> updateParticipantStatus(
|
||||
String eventId,
|
||||
String participantId,
|
||||
String status,
|
||||
) async {
|
||||
return updateParticipant(eventId, participantId, {'status': status});
|
||||
}
|
||||
|
||||
// 점수 관리 기능
|
||||
// 이벤트 점수 목록 가져오기
|
||||
Future<List<Score>> fetchEventScores(String eventId) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.clubs}/events/scores',
|
||||
data: {'eventId': eventId},
|
||||
);
|
||||
|
||||
final List<dynamic> scoresData = data['scores'] ?? [];
|
||||
_scores = scoresData
|
||||
.map((scoreData) => Score.fromJson(scoreData))
|
||||
.toList();
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return _scores;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('점수 목록을 불러오는데 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 점수 추가
|
||||
Future<Score> addScore(String eventId, Map<String, dynamic> scoreData) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.clubs}/events/$eventId/scores',
|
||||
data: scoreData,
|
||||
);
|
||||
|
||||
final newScore = Score.fromJson(data['score']);
|
||||
|
||||
_scores.add(newScore);
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return newScore;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('점수 추가에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 점수 업데이트
|
||||
Future<Score> updateScore(
|
||||
String eventId,
|
||||
String scoreId,
|
||||
Map<String, dynamic> updates,
|
||||
) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final data = await _apiService.put(
|
||||
'${ApiConfig.clubs}/events/$eventId/scores/$scoreId',
|
||||
data: updates,
|
||||
);
|
||||
|
||||
final updatedScore = Score.fromJson(data['score']);
|
||||
|
||||
// 점수 목록 업데이트
|
||||
final index = _scores.indexWhere((score) => score.id == scoreId);
|
||||
if (index >= 0) {
|
||||
_scores[index] = updatedScore;
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return updatedScore;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('점수 업데이트에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 점수 삭제
|
||||
Future<bool> deleteScore(String eventId, String scoreId) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _apiService.delete(
|
||||
'${ApiConfig.clubs}/events/$eventId/scores/$scoreId',
|
||||
);
|
||||
|
||||
// 점수 목록에서 삭제
|
||||
_scores.removeWhere((score) => score.id == scoreId);
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('점수 삭제에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 팀 관리 기능
|
||||
// 이벤트 팀 목록 가져오기
|
||||
Future<List<Team>> fetchEventTeams(String eventId) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.clubs}/events/teams',
|
||||
data: {'eventId': eventId},
|
||||
);
|
||||
|
||||
final List<dynamic> teamsData = data['teams'] ?? [];
|
||||
_teams = teamsData.map((teamData) => Team.fromJson(teamData)).toList();
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return _teams;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('팀 목록을 불러오는데 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 팀 생성
|
||||
Future<List<Team>> generateTeams(
|
||||
String eventId,
|
||||
Map<String, dynamic> teamConfig,
|
||||
) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.clubs}/events/$eventId/teams/generate',
|
||||
data: teamConfig,
|
||||
);
|
||||
|
||||
final List<dynamic> teamsData = data['teams'] ?? [];
|
||||
_teams = teamsData.map((teamData) => Team.fromJson(teamData)).toList();
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return _teams;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('팀 생성에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 팀 저장
|
||||
Future<bool> saveTeams(String eventId, List<Team> teams) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _apiService.post(
|
||||
'${ApiConfig.clubs}/events/$eventId/teams/save',
|
||||
data: {'teams': teams.map((team) => team.toJson()).toList()},
|
||||
);
|
||||
|
||||
_teams = teams;
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('팀 저장에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 파일에서 이벤트 일괄 생성
|
||||
Future<List<Event>> createEventsFromFile(
|
||||
List<Map<String, dynamic>> eventsData,
|
||||
) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final List<Event> createdEvents = [];
|
||||
|
||||
// 각 이벤트 데이터에 클럽 ID 추가
|
||||
for (var eventData in eventsData) {
|
||||
eventData['clubId'] = _clubId;
|
||||
|
||||
// null 값 처리 - 빈 문자열을 명시적 null로 변환
|
||||
eventData.forEach((key, value) {
|
||||
if (value is String && value.isEmpty) {
|
||||
eventData[key] = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 이벤트 생성 API 호출
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.clubs}/events',
|
||||
data: eventData,
|
||||
);
|
||||
|
||||
final newEvent = Event.fromJson(data['event']);
|
||||
createdEvents.add(newEvent);
|
||||
_events.add(newEvent);
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return createdEvents;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('파일에서 이벤트 생성에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 이벤트 복제
|
||||
Future<Event> cloneEvent(String eventId, {String? newName}) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// 원본 이벤트 정보 가져오기
|
||||
final originalEvent = await fetchEventById(eventId);
|
||||
|
||||
// 복제할 이벤트 데이터 준비
|
||||
final Map<String, dynamic> cloneData = {
|
||||
'clubId': _clubId,
|
||||
'title': newName ?? '${originalEvent.title} (복사본)',
|
||||
'description': originalEvent.description,
|
||||
'location': originalEvent.location,
|
||||
'type': originalEvent.type,
|
||||
'status': 'draft', // 복제된 이벤트는 초안 상태로 시작
|
||||
'startDate': originalEvent.startDate.toIso8601String(),
|
||||
'endDate': originalEvent.endDate?.toIso8601String(),
|
||||
'maxParticipants': originalEvent.maxParticipants,
|
||||
'participantFee': originalEvent.participantFee,
|
||||
'gameCount': originalEvent.gameCount,
|
||||
'publicHash': originalEvent.publicHash,
|
||||
'accessPassword': originalEvent.accessPassword,
|
||||
'isActive': true,
|
||||
};
|
||||
|
||||
// 이벤트 생성 API 호출
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.clubs}/events',
|
||||
data: cloneData,
|
||||
);
|
||||
|
||||
final newEvent = Event.fromJson(data['event']);
|
||||
_events.add(newEvent);
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return newEvent;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('이벤트 복제에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,16 @@ class InAppPurchaseService extends ChangeNotifier {
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
|
||||
// 구매 검증 관련 게터/세터
|
||||
bool get isPurchaseVerified => _purchaseVerified;
|
||||
set isPurchaseVerified(bool value) => _purchaseVerified = value;
|
||||
PurchaseDetails? get lastVerifiedPurchase => _purchases.isNotEmpty ? _purchases.last : null;
|
||||
set lastVerifiedPurchase(PurchaseDetails? value) {
|
||||
if (value != null && !_purchases.contains(value)) {
|
||||
_purchases.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
InAppPurchaseService() {
|
||||
_initialize();
|
||||
}
|
||||
@@ -195,9 +205,8 @@ class InAppPurchaseService extends ChangeNotifier {
|
||||
String platform = Platform.isIOS ? 'ios' : 'android';
|
||||
|
||||
if (Platform.isIOS) {
|
||||
final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
|
||||
_inAppPurchase.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
|
||||
receiptData = await iosPlatformAddition.retrieveReceiptData();
|
||||
// iOS의 경우 구매 정보에서 직접 가져오기
|
||||
receiptData = purchaseDetails.verificationData.serverVerificationData;
|
||||
} else if (Platform.isAndroid) {
|
||||
if (purchaseDetails is GooglePlayPurchaseDetails) {
|
||||
receiptData = purchaseDetails.billingClientPurchase.originalJson;
|
||||
|
||||
@@ -13,8 +13,14 @@ class MemberService with ChangeNotifier {
|
||||
bool _isLoading = false;
|
||||
String? _token;
|
||||
String? _clubId;
|
||||
final ApiService _apiService = ApiService();
|
||||
final ApiService _apiService;
|
||||
StreamSubscription? _eventSubscription;
|
||||
|
||||
// 기본 생성자
|
||||
MemberService() : _apiService = ApiService();
|
||||
|
||||
// 테스트용 생성자
|
||||
MemberService.forTest(this._apiService);
|
||||
|
||||
List<Member> get members => [..._members];
|
||||
bool get isLoading => _isLoading;
|
||||
@@ -60,9 +66,14 @@ class MemberService with ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// 클럽 ID 가져오기
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final clubId = prefs.getString(ApiConfig.clubIdKey);
|
||||
// 클럽 ID 가져오기 - 메모리에 저장된 값을 우선 사용
|
||||
String? clubId = _clubId;
|
||||
|
||||
// 메모리에 없으면 SharedPreferences에서 가져오기
|
||||
if (clubId == null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
clubId = prefs.getString(ApiConfig.clubIdKey);
|
||||
}
|
||||
|
||||
if (clubId == null) {
|
||||
throw Exception('선택된 클럽이 없습니다');
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
import 'package:timezone/data/latest.dart' as tz_data;
|
||||
|
||||
import '../models/event_model.dart';
|
||||
|
||||
class NotificationService {
|
||||
static NotificationService _instance = NotificationService._internal();
|
||||
factory NotificationService() => _instance;
|
||||
NotificationService._internal();
|
||||
|
||||
// 테스트용 인스턴스 설정 메서드
|
||||
static void setTestInstance(NotificationService instance) {
|
||||
_instance = instance;
|
||||
}
|
||||
|
||||
final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
bool _isInitialized = false;
|
||||
|
||||
// 알림 서비스 초기화
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
// 타임존 초기화
|
||||
tz_data.initializeTimeZones();
|
||||
|
||||
// 안드로이드 설정
|
||||
const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
|
||||
// iOS 설정
|
||||
const DarwinInitializationSettings iOSSettings = DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
);
|
||||
|
||||
// 초기화 설정
|
||||
const InitializationSettings initSettings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: iOSSettings,
|
||||
);
|
||||
|
||||
// 알림 플러그인 초기화
|
||||
await _notificationsPlugin.initialize(
|
||||
initSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
||||
// 알림 클릭 시 처리
|
||||
debugPrint('알림 클릭: ${response.payload}');
|
||||
},
|
||||
);
|
||||
|
||||
_isInitialized = true;
|
||||
debugPrint('알림 서비스 초기화 완료');
|
||||
}
|
||||
|
||||
// 권한 요청
|
||||
Future<bool> requestPermission() async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
// iOS에서 권한 요청
|
||||
final bool? result = await _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
// 즉시 알림 표시
|
||||
Future<void> showNotification({
|
||||
required int id,
|
||||
required String title,
|
||||
required String body,
|
||||
String? payload,
|
||||
}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
|
||||
'event_channel',
|
||||
'이벤트 알림',
|
||||
channelDescription: '이벤트 관련 알림을 표시합니다',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
showWhen: true,
|
||||
);
|
||||
|
||||
const DarwinNotificationDetails iOSDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
const NotificationDetails notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iOSDetails,
|
||||
);
|
||||
|
||||
await _notificationsPlugin.show(
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
notificationDetails,
|
||||
payload: payload,
|
||||
);
|
||||
}
|
||||
|
||||
// 예약 알림 설정
|
||||
Future<void> scheduleNotification({
|
||||
required int id,
|
||||
required String title,
|
||||
required String body,
|
||||
required DateTime scheduledDate,
|
||||
String? payload,
|
||||
}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
final tz.TZDateTime scheduledTZDate = tz.TZDateTime.from(
|
||||
scheduledDate,
|
||||
tz.local,
|
||||
);
|
||||
|
||||
const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
|
||||
'event_reminder_channel',
|
||||
'이벤트 리마인더',
|
||||
channelDescription: '예정된 이벤트에 대한 알림을 표시합니다',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
);
|
||||
|
||||
const DarwinNotificationDetails iOSDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
const NotificationDetails notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iOSDetails,
|
||||
);
|
||||
|
||||
await _notificationsPlugin.zonedSchedule(
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
scheduledTZDate,
|
||||
notificationDetails,
|
||||
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
|
||||
matchDateTimeComponents: DateTimeComponents.time,
|
||||
payload: payload,
|
||||
);
|
||||
}
|
||||
|
||||
// 이벤트 시작 알림 설정
|
||||
Future<void> scheduleEventStartReminder(Event event) async {
|
||||
if (event.id.isEmpty) return;
|
||||
|
||||
// 이벤트 시작 1시간 전 알림
|
||||
final DateTime reminderTime = event.startDate.subtract(const Duration(hours: 1));
|
||||
|
||||
// 현재 시간이 알림 시간보다 이후라면 알림을 설정하지 않음
|
||||
if (reminderTime.isBefore(DateTime.now())) return;
|
||||
|
||||
await scheduleNotification(
|
||||
id: event.id.hashCode,
|
||||
title: '이벤트 시작 알림',
|
||||
body: '${event.title} 이벤트가 1시간 후에 시작됩니다.',
|
||||
scheduledDate: reminderTime,
|
||||
payload: event.id,
|
||||
);
|
||||
}
|
||||
|
||||
// 이벤트 등록 마감 알림 설정
|
||||
Future<void> scheduleRegistrationDeadlineReminder(Event event) async {
|
||||
if (event.id.isEmpty || event.registrationDeadline == null) return;
|
||||
|
||||
// 등록 마감 1일 전 알림
|
||||
final DateTime reminderTime = event.registrationDeadline!.subtract(const Duration(days: 1));
|
||||
|
||||
// 현재 시간이 알림 시간보다 이후라면 알림을 설정하지 않음
|
||||
if (reminderTime.isBefore(DateTime.now())) return;
|
||||
|
||||
await scheduleNotification(
|
||||
id: event.id.hashCode + 1, // 이벤트 시작 알림과 ID가 겹치지 않도록 함
|
||||
title: '이벤트 등록 마감 알림',
|
||||
body: '${event.title} 이벤트의 등록이 내일 마감됩니다.',
|
||||
scheduledDate: reminderTime,
|
||||
payload: event.id,
|
||||
);
|
||||
}
|
||||
|
||||
// 특정 이벤트의 모든 알림 취소
|
||||
Future<void> cancelEventNotifications(String eventId) async {
|
||||
await _notificationsPlugin.cancel(eventId.hashCode);
|
||||
await _notificationsPlugin.cancel(eventId.hashCode + 1);
|
||||
}
|
||||
|
||||
// 모든 알림 취소
|
||||
Future<void> cancelAllNotifications() async {
|
||||
await _notificationsPlugin.cancelAll();
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,30 @@ class ScoreService with ChangeNotifier {
|
||||
bool _isLoading = false;
|
||||
String? _token;
|
||||
String? _clubId;
|
||||
final ApiService _apiService = ApiService();
|
||||
ApiService _apiService = ApiService();
|
||||
String? _memberId;
|
||||
String? _eventId;
|
||||
|
||||
// 테스트용 생성자
|
||||
ScoreService.forTest(ApiService apiService, {String? clubId, String? memberId, String? eventId}) {
|
||||
_apiService = apiService;
|
||||
if (clubId != null) {
|
||||
_clubId = clubId;
|
||||
}
|
||||
if (memberId != null) {
|
||||
_memberId = memberId;
|
||||
}
|
||||
if (eventId != null) {
|
||||
_eventId = eventId;
|
||||
}
|
||||
}
|
||||
|
||||
// 테스트용 getter
|
||||
String? get testMemberId => _memberId;
|
||||
String? get testEventId => _eventId;
|
||||
|
||||
// 기본 생성자
|
||||
ScoreService();
|
||||
|
||||
List<Score> get scores => [..._scores];
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
@@ -10,6 +10,8 @@ import 'in_app_purchase_service.dart';
|
||||
class SubscriptionService extends ChangeNotifier {
|
||||
final String _baseUrl = ApiConfig.baseUrl;
|
||||
final String _token;
|
||||
final String _clubId;
|
||||
final String _userId;
|
||||
|
||||
Subscription? _currentSubscription;
|
||||
List<SubscriptionPlan> _availablePlans = [];
|
||||
@@ -17,9 +19,42 @@ class SubscriptionService extends ChangeNotifier {
|
||||
String? _error;
|
||||
|
||||
// 인앱 결제 서비스
|
||||
final InAppPurchaseService _purchaseService = InAppPurchaseService();
|
||||
final InAppPurchaseService _purchaseService;
|
||||
final http.Client _httpClient;
|
||||
|
||||
SubscriptionService(this._token) {
|
||||
// 기본 생성자
|
||||
SubscriptionService(this._token, [this._clubId = '', this._userId = '']) :
|
||||
_purchaseService = InAppPurchaseService(),
|
||||
_httpClient = http.Client() {
|
||||
_loadAvailablePlans();
|
||||
_initializeInAppPurchase();
|
||||
}
|
||||
|
||||
// 테스트용 생성자
|
||||
factory SubscriptionService.forTest({
|
||||
required http.Client client,
|
||||
required InAppPurchaseService purchaseService,
|
||||
required String userId,
|
||||
required String token,
|
||||
required String clubId,
|
||||
}) {
|
||||
return SubscriptionService._internal(
|
||||
token,
|
||||
clubId,
|
||||
userId,
|
||||
client,
|
||||
purchaseService,
|
||||
);
|
||||
}
|
||||
|
||||
// 내부 생성자
|
||||
SubscriptionService._internal(
|
||||
this._token,
|
||||
this._clubId,
|
||||
this._userId,
|
||||
this._httpClient,
|
||||
this._purchaseService,
|
||||
) {
|
||||
_loadAvailablePlans();
|
||||
_initializeInAppPurchase();
|
||||
}
|
||||
@@ -34,7 +69,7 @@ class SubscriptionService extends ChangeNotifier {
|
||||
|
||||
// 인앱 결제 관련 게터
|
||||
InAppPurchaseService get purchaseService => _purchaseService;
|
||||
List<ProductDetails> get products => _purchaseService.products;
|
||||
List<dynamic> get products => _purchaseService.products;
|
||||
|
||||
/// 현재 구독 정보 로드
|
||||
Future<void> loadCurrentSubscription() async {
|
||||
@@ -43,7 +78,7 @@ class SubscriptionService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
final response = await _httpClient.post(
|
||||
Uri.parse('$_baseUrl${ApiConfig.currentSubscription}'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -66,9 +101,11 @@ class SubscriptionService extends ChangeNotifier {
|
||||
status: SubscriptionStatus.active,
|
||||
startDate: DateTime.now(),
|
||||
endDate: DateTime.now().add(const Duration(days: 36500)), // 100년
|
||||
price: 0,
|
||||
currency: 'KRW',
|
||||
autoRenew: false,
|
||||
lastPaymentDate: null,
|
||||
nextPaymentDate: null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
} else {
|
||||
_error = '구독 정보를 불러오는데 실패했습니다. (${response.statusCode})';
|
||||
@@ -88,7 +125,7 @@ class SubscriptionService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
final response = await _httpClient.get(
|
||||
Uri.parse('$_baseUrl${ApiConfig.subscriptionPlans}'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -125,20 +162,21 @@ class SubscriptionService extends ChangeNotifier {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
|
||||
try {
|
||||
// 1. 인앱 결제 복원 요청
|
||||
final restoreSuccess = await _purchaseService.restorePurchases();
|
||||
if (!restoreSuccess) {
|
||||
_error = '구독 복원에 실패했습니다.';
|
||||
// 인앱 구매 복원 시도
|
||||
final restored = await _purchaseService.restorePurchases();
|
||||
|
||||
if (restored) {
|
||||
// 서버에서 현재 구독 정보 가져오기
|
||||
await loadCurrentSubscription();
|
||||
return true;
|
||||
} else {
|
||||
_error = '구독을 복원할 수 없습니다.';
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 서버에서 최신 구독 정보 조회
|
||||
await fetchCurrentSubscription();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = '구독 복원에 실패했습니다: $e';
|
||||
_error = '구독 복원 중 오류가 발생했습니다: $e';
|
||||
return false;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
@@ -153,8 +191,8 @@ class SubscriptionService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/subscriptions/verify'),
|
||||
final response = await _httpClient.get(
|
||||
Uri.parse('$_baseUrl${ApiConfig.currentSubscription}'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $_token',
|
||||
@@ -204,7 +242,7 @@ class SubscriptionService extends ChangeNotifier {
|
||||
// 구매 검증이 완료될 때까지 최대 10회 확인
|
||||
while (!verified && attempts < maxAttempts) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
verified = _purchaseService._purchaseVerified;
|
||||
verified = _purchaseService.isPurchaseVerified;
|
||||
attempts++;
|
||||
|
||||
// 오류가 발생한 경우
|
||||
@@ -224,7 +262,7 @@ class SubscriptionService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// 검증된 구매 정보 가져오기
|
||||
final verifiedPurchase = _purchaseService._verifiedPurchase;
|
||||
final verifiedPurchase = _purchaseService.lastVerifiedPurchase;
|
||||
if (verifiedPurchase == null) {
|
||||
_error = '검증된 구매 정보를 찾을 수 없습니다.';
|
||||
_isLoading = false;
|
||||
@@ -233,7 +271,7 @@ class SubscriptionService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// 서버에 구독 생성 요청
|
||||
final response = await http.post(
|
||||
final response = await _httpClient.post(
|
||||
Uri.parse('$_baseUrl${ApiConfig.subscribeClub}'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -243,9 +281,9 @@ class SubscriptionService extends ChangeNotifier {
|
||||
'planType': planType.toString().split('.').last,
|
||||
'isYearly': isYearly,
|
||||
'clubId': _clubId,
|
||||
'transactionId': verifiedPurchase['transactionId'],
|
||||
'productId': verifiedPurchase['productId'],
|
||||
'verificationData': verifiedPurchase['verificationData'],
|
||||
'transactionId': verifiedPurchase.purchaseID,
|
||||
'productId': verifiedPurchase.productID,
|
||||
'verificationData': verifiedPurchase.verificationData.serverVerificationData,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -254,8 +292,8 @@ class SubscriptionService extends ChangeNotifier {
|
||||
_currentSubscription = Subscription.fromJson(data);
|
||||
|
||||
// 구매 검증 상태 초기화
|
||||
_purchaseService._purchaseVerified = false;
|
||||
_purchaseService._verifiedPurchase = null;
|
||||
_purchaseService.isPurchaseVerified = false;
|
||||
_purchaseService.lastVerifiedPurchase = null;
|
||||
|
||||
notifyListeners();
|
||||
return true;
|
||||
@@ -285,8 +323,8 @@ class SubscriptionService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/subscriptions/${_currentSubscription!.id}/cancel'),
|
||||
final response = await _httpClient.post(
|
||||
Uri.parse('$_baseUrl${ApiConfig.subscriptions}/${_currentSubscription!.id}/cancel'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $_token',
|
||||
@@ -324,7 +362,7 @@ class SubscriptionService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
final response = await _httpClient.post(
|
||||
Uri.parse('$_baseUrl${ApiConfig.changePlan}'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -369,7 +407,7 @@ class SubscriptionService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
final response = await _httpClient.post(
|
||||
Uri.parse('$_baseUrl${ApiConfig.subscriptions}/autorenew'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
Reference in New Issue
Block a user