tdd 진행

This commit is contained in:
2025-08-09 03:09:17 +09:00
parent ed8c7eacba
commit 6033d59590
74 changed files with 17804 additions and 395 deletions
+5 -2
View File
@@ -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(
+7 -1
View File
@@ -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;
+5
View File
@@ -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();
}
+477 -1
View File
@@ -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;
+15 -4
View File
@@ -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();
}
}
+22 -1
View File
@@ -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;
+69 -31
View File
@@ -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',