Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
984 changes: 643 additions & 341 deletions docs/api-docs.json

Large diffs are not rendered by default.

1,895 changes: 1,895 additions & 0 deletions docs/superpowers/plans/2026-05-29-exploration-backend-integration.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions lib/core/constants/api_endpoints.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,23 @@ abstract class ApiEndpoints {

/// 오늘 공부 통계 (KST)
static const timerSessionsTodayStats = '/api/timer-sessions/today-stats';

// ============================================
// Exploration
// ============================================

/// 전체 행성 목록
static const explorationPlanets = '/api/explorations/planets';

/// 특정 행성 하위 지역 목록
static String explorationRegions(String planetId) =>
'/api/explorations/planets/$planetId/regions';

/// 지역 해금 (연료 소비)
static String explorationUnlockRegion(String regionId) =>
'/api/explorations/regions/$regionId/unlock';

/// 행성 해금 (연료 소비)
static String explorationUnlockPlanet(String planetId) =>
'/api/explorations/planets/$planetId/unlock';
}
35 changes: 17 additions & 18 deletions lib/features/auth/presentation/providers/auth_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import '../../../exploration/presentation/providers/exploration_provider.dart';
import '../../../fuel/presentation/providers/fuel_provider.dart';
import '../../../timer/presentation/providers/timer_provider.dart';
import '../../../timer/presentation/providers/timer_session_provider.dart';
import '../../../timer/presentation/providers/today_stats_provider.dart';
import '../../../todo/presentation/providers/todo_provider.dart';

part 'auth_provider.g.dart';
Expand Down Expand Up @@ -328,10 +327,9 @@ class AuthNotifier extends _$AuthNotifier {
final useCase = ref.read(signOutUseCaseProvider);
await useCase.execute();
// Timer 로컬 캐시 삭제 (인증 → 게스트 전환 시 이전 세션 데이터 노출 방지)
await ref.read(timerSessionRepositoryProvider).clearAll();
// 메모리 캐시 무효화
ref.invalidate(timerSessionListNotifierProvider);
ref.invalidate(todayStatsNotifierProvider);
await ref.read(timerSessionLocalDataSourceProvider).clearAll();
// state=null → isAuthenticated 재계산 → 의존 Provider 자동 리빌드.
// 수동 invalidate 는 authNotifier 자신을 watch 하는 Provider 라 순환 참조.
state = const AsyncValue.data(null);
} on FirebaseAuthException catch (e) {
state = previous;
Expand All @@ -350,9 +348,9 @@ class AuthNotifier extends _$AuthNotifier {
// CircularDependencyError 가 발생한다.
final clearTasks = <Future<void> Function()>[
() => ref.read(localTodoDataSourceProvider).clearAll(),
() => ref.read(timerSessionRepositoryProvider).clearAll(),
() => ref.read(timerSessionLocalDataSourceProvider).clearAll(),
() => ref.read(fuelLocalDataSourceProvider).clearAll(),
() => ref.read(explorationRepositoryProvider).clearAll(),
() => ref.read(explorationLocalDataSourceProvider).clearAll(),
() => ref.read(badgeRepositoryProvider).clearAll(),
];

Expand All @@ -364,13 +362,15 @@ class AuthNotifier extends _$AuthNotifier {
}
}

// 메모리 캐시 무효화 (예외 발생 불가)
ref.invalidate(timerSessionListNotifierProvider);
ref.invalidate(todayStatsNotifierProvider);
ref.invalidate(todoListNotifierProvider);
ref.invalidate(categoryListNotifierProvider);
ref.invalidate(fuelNotifierProvider);
ref.invalidate(explorationNotifierProvider);
// 메모리 캐시 갱신.
// timer/todo/fuel/exploration 등 auth 의존 Provider 는 authNotifier 를 조상으로
// 가지므로 여기서 invalidate(또는 read/watch)하면 CircularDependencyError 가 난다
// (debug assert). 대신:
// - autoDispose Provider(timer/todayStats/todo/category): 화면 재진입 시 비워진
// 로컬 저장소로 자동 리빌드되므로 명시적 무효화 불필요.
// - keepAlive + auth 의존(fuel/exploration): 게스트 진입 시점엔 보통 미사용 상태라
// 다음 watch 때 새로 빌드된다.
// - badge: auth 비의존 + keepAlive 라 명시적 invalidate 가 필요하고 안전하다.
ref.invalidate(badgeNotifierProvider);
}

Expand Down Expand Up @@ -457,10 +457,9 @@ class AuthNotifier extends _$AuthNotifier {
try {
await ref.read(withdrawUseCaseProvider).execute();
// Timer 로컬 캐시 삭제 (인증 → 비로그인 전환 시 이전 세션 데이터 노출 방지)
await ref.read(timerSessionRepositoryProvider).clearAll();
// 메모리 캐시 무효화
ref.invalidate(timerSessionListNotifierProvider);
ref.invalidate(todayStatsNotifierProvider);
await ref.read(timerSessionLocalDataSourceProvider).clearAll();
// state=null → isAuthenticated 재계산 → 의존 Provider 자동 리빌드.
// 수동 invalidate 는 authNotifier 자신을 watch 하는 Provider 라 순환 참조.
state = const AsyncValue.data(null);
} catch (e, stack) {
state = previous;
Expand Down
15 changes: 11 additions & 4 deletions lib/features/badge/presentation/providers/badge_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../../domain/entities/badge_entity.dart';
import '../../domain/repositories/badge_repository.dart';
import '../../../timer/presentation/providers/study_stats_provider.dart';
import '../../../fuel/presentation/providers/fuel_provider.dart';
import '../../../exploration/domain/entities/exploration_node_entity.dart';
import '../../../exploration/presentation/providers/exploration_provider.dart';

part 'badge_provider.g.dart';
Expand Down Expand Up @@ -56,14 +57,20 @@ class BadgeNotifier extends _$BadgeNotifier {
final fuelStateAsync = ref.read(fuelNotifierProvider);
final fuelState = fuelStateAsync.valueOrNull;
if (fuelState == null) return []; // 잔량 아직 로드 전이면 배지 해금 평가 skip
final planets = ref.read(explorationNotifierProvider);
final planets =
ref.read(explorationNotifierProvider).valueOrNull ??
const <ExplorationNodeEntity>[];
final unlockedPlanets = planets.where((p) => p.isUnlocked).length;

// 지역 해금 수 계산
// 지역 해금 수 계산 — 행성 응답에 내장된 progress(=cleared) 합산.
// 지역은 해금 시 즉시 cleared 되므로 clearedChildren == 해금 지역 수.
// regionListNotifierProvider를 행성마다 read하면 인증 모드에서 미방문
// 행성의 지역 API가 연쇄 호출되므로, 캐시된 progress를 사용한다.
final explorationRepo = ref.read(explorationRepositoryProvider);
int unlockedRegions = 0;
for (final planet in planets) {
final regions = ref.read(regionListNotifierProvider(planet.id));
unlockedRegions += regions.where((r) => r.isUnlocked).length;
final progress = await explorationRepo.getProgress(planet.id);
unlockedRegions += progress.clearedChildren;
}

// 히든 배지: 현재 시간 체크
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';

import '../../../../core/constants/api_endpoints.dart';
import '../models/planet_response_model.dart';
import '../models/region_response_model.dart';
import '../models/unlock_response_models.dart';

part 'exploration_remote_datasource.g.dart';

/// Exploration 백엔드 API 클라이언트 (api-docs.json Exploration 태그 4 엔드포인트).
@RestApi()
abstract class ExplorationRemoteDataSource {
factory ExplorationRemoteDataSource(Dio dio) = _ExplorationRemoteDataSource;

/// 전체 행성 목록 — 200
@GET(ApiEndpoints.explorationPlanets)
Future<List<PlanetResponseModel>> getPlanets();

/// 행성 하위 지역 목록 — 200 / 404 PLANET_NOT_FOUND
@GET('/api/explorations/planets/{planetId}/regions')
Future<List<RegionResponseModel>> getRegions(
@Path('planetId') String planetId,
);

/// 지역 해금 — 200 / 400 / 404
@POST('/api/explorations/regions/{regionId}/unlock')
Future<RegionUnlockResponseModel> unlockRegion(
@Path('regionId') String regionId,
);

/// 행성 해금 — 200 / 400 / 404
@POST('/api/explorations/planets/{planetId}/unlock')
Future<PlanetUnlockResponseModel> unlockPlanet(
@Path('planetId') String planetId,
);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading