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
@@ -1,9 +1,7 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:tma_app/shared/widgets/photo_image.dart';
/// 기기에 저장된 반려동물 사진 — 웹 미리보기에서는 브라우저 임시 주소를 쓴다
/// 반려동물·다이어리 사진 — 서버 파일 또는 기기 사진. 읽을 수 없으면 빈 칸으로 둔다
class PetPhotoImage extends StatelessWidget {
const PetPhotoImage({super.key, required this.path, this.fit = BoxFit.cover});
@@ -11,12 +9,5 @@ class PetPhotoImage extends StatelessWidget {
final BoxFit fit;
@override
Widget build(BuildContext context) {
// 사진 파일이 지워졌거나 읽을 수 없으면 빈 칸으로 둔다
Widget error(BuildContext context, Object e, StackTrace? s) => const SizedBox.shrink();
if (kIsWeb) {
return Image.network(path, fit: fit, errorBuilder: error);
}
return Image.file(File(path), fit: fit, errorBuilder: error);
}
Widget build(BuildContext context) => PhotoImage(path: path, fit: fit);
}