feat(flutter): 서버 연동 기반 — 세션·토큰 갱신·사진 업로드·서버 사진 표시

기기 저장소를 API 로 바꾸기 전에, 모든 도메인이 함께 쓸 네트워크 계층을 먼저 갖춘다.

- DATA_MODE(api|local): 기본은 서버. .env 를 읽지 않는 테스트·화면 확인은 기기 저장소로 돈다
- SessionStore: 액세스·리프레시 토큰을 secure storage 에 두고 앱을 열 때 미리 읽는다
  (모바일에는 쿠키 저장소가 없어 리프레시 토큰도 secure storage 에 둔다)
- DioClient: 401 이면 토큰을 한 번 갱신해 다시 보낸다(동시 요청도 갱신은 한 번).
  서버가 갱신을 거절하면 세션을 끊고 AUTH_002, 서버에 닿지 못하면 세션은 두고 SYS_001
- 실패 알림은 사전 문구로 토스트 한 곳에서 띄우고 같은 코드는 잠깐 한 번만 — 호출부가 처리하는 코드는 끌 수 있다
- 사진: 경로 자리에 server-file:<id> 를 담고, 기기 사진은 저장할 때 형식을 내용으로 확인해 올린다.
  서버 사진은 Dio 로 받아 토큰 갱신을 그대로 쓴다 (PetPhotoImage·PersonAvatar)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:07:49 +09:00
parent 607ff5c474
commit cdd4856300
16 changed files with 836 additions and 67 deletions
+13 -1
View File
@@ -4,7 +4,11 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tma_app/app.dart';
import 'package:tma_app/core/api/dio_provider.dart';
import 'package:tma_app/core/api/session_store.dart';
import 'package:tma_app/core/config/app_config.dart';
import 'package:tma_app/core/storage/preferences_provider.dart';
import 'package:tma_app/shared/widgets/rup_toast.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -21,10 +25,18 @@ Future<void> main() async {
// 진입 화면 결정에 필요한 저장값을 첫 프레임 전에 읽어 둔다
final prefs = await SharedPreferences.getInstance();
// 로그인 세션(토큰) — 서버 모드에서만 secure storage 를 쓴다
final session = readDataMode() == DataMode.api
? await SessionStore.load(const SecureTokenVault())
: SessionStore(MemoryTokenVault());
runApp(
ProviderScope(
overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
sessionStoreProvider.overrideWithValue(session),
apiErrorReporterProvider.overrideWith(apiErrorToastReporter),
],
child: const RupApp(),
),
);