generated from ttipo/project-base
feat(flutter): 함께 쓰는 기록을 앱을 보는 동안 다시 읽기
다른 멤버가 남긴 글·일정과 내 코드로 새로 연동한 사람이 앱을 다시 켜지 않아도 보이게 한다. 실시간 알림이 붙기 전까지의 대신이다. - 앱으로 돌아올 때와 앱을 보는 동안 3분마다 공유 다이어리·연동 상태(연동 중이면 일정도)를 다시 읽는다 - 뒤에서 읽을 때는 실패해도 알림을 띄우지 않는다 (목록 읽기·저장소 fetch 에 quiet) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -144,12 +144,12 @@ class ApiDiaryRepository implements DiaryRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DiaryBook> fetch() async {
|
||||
Future<DiaryBook> fetch({bool quiet = false}) async {
|
||||
if (!_session.hasSession) return DiaryBook.empty;
|
||||
return DiaryBook(
|
||||
diaries: await fetchAllPages(_dio, _kDiariesPath, _diary),
|
||||
posts: await fetchAllPages(_dio, _kPostsPath, _post),
|
||||
comments: await fetchAllPages(_dio, _kCommentsPath, _comment),
|
||||
diaries: await fetchAllPages(_dio, _kDiariesPath, _diary, quiet: quiet),
|
||||
posts: await fetchAllPages(_dio, _kPostsPath, _post, quiet: quiet),
|
||||
comments: await fetchAllPages(_dio, _kCommentsPath, _comment, quiet: quiet),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ import 'package:tma_app/features/diary/domain/mood.dart';
|
||||
/// 다이어리 저장소 계약 — 서버(ApiDiaryRepository) / 기기(LocalDiaryRepository).
|
||||
/// 바꾼 결과(id 가 붙은 다이어리·기록·댓글)를 돌려주면 알림기가 [DiaryBook] 에 반영한다.
|
||||
abstract interface class DiaryRepository {
|
||||
Future<DiaryBook> fetch();
|
||||
/// [quiet] 면 실패해도 알림을 띄우지 않는다 (뒤에서 다시 읽을 때)
|
||||
Future<DiaryBook> fetch({bool quiet = false});
|
||||
|
||||
/// 공유 다이어리(초대·참여·멤버)를 쓸 수 있는지 — 다른 회원과 함께 쓰므로 서버가 있어야 한다
|
||||
bool get canShare;
|
||||
@@ -114,7 +115,7 @@ class LocalDiaryRepository implements DiaryRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DiaryBook> fetch() async => _read();
|
||||
Future<DiaryBook> fetch({bool quiet = false}) async => _read();
|
||||
|
||||
@override
|
||||
bool get canShare => false;
|
||||
|
||||
@@ -73,10 +73,11 @@ class DiaryNotifier extends AsyncNotifier<DiaryBook> {
|
||||
|
||||
// ── 공유 다이어리 ────────────────────────────────────────
|
||||
/// 다른 멤버가 새로 남긴 기록·댓글을 다시 읽는다 — 불러오는 중 화면 없이 바꿔 끼운다
|
||||
Future<void> refresh() async {
|
||||
/// [quiet] 면 실패해도 알림을 띄우지 않는다 (앱이 뒤에서 다시 읽을 때)
|
||||
Future<void> refresh({bool quiet = false}) async {
|
||||
// 기기 저장 모드는 나만 쓰므로 다시 읽을 것이 없다
|
||||
if (!_repo.canShare) return;
|
||||
_set(await _repo.fetch());
|
||||
_set(await _repo.fetch(quiet: quiet));
|
||||
}
|
||||
|
||||
/// 개인 ↔ 공유 전환 (주인만)
|
||||
|
||||
@@ -105,20 +105,21 @@ class ApiScheduleRepository implements ScheduleRepository {
|
||||
RepeatRule _rule(Map<String, Object?> json) => repeatRuleFromApi(json, myUserId: _myUserId());
|
||||
|
||||
@override
|
||||
Future<ScheduleBook> fetch() async {
|
||||
Future<ScheduleBook> fetch({bool quiet = false}) async {
|
||||
if (!_session.hasSession) return ScheduleBook.empty;
|
||||
|
||||
final done = <String>{};
|
||||
final events = await fetchAllPages(_dio, _kEventsPath, (json) {
|
||||
final events = await fetchAllPages(_dio, _kEventsPath, quiet: quiet, (json) {
|
||||
final event = _event(json);
|
||||
if (json['isDone'] == true) done.add(event.id);
|
||||
return event;
|
||||
});
|
||||
final rules = await fetchAllPages(_dio, _kRulesPath, _rule);
|
||||
final rules = await fetchAllPages(_dio, _kRulesPath, _rule, quiet: quiet);
|
||||
final completions = await fetchAllPages(
|
||||
_dio,
|
||||
_kCompletionsPath,
|
||||
(json) => '${json['ruleId']! as String}@${json['date']! as String}',
|
||||
quiet: quiet,
|
||||
);
|
||||
return ScheduleBook(events: events, rules: rules, doneIds: {...done, ...completions});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:tma_app/core/api/dio_client.dart';
|
||||
import 'package:tma_app/core/api/dio_provider.dart';
|
||||
import 'package:tma_app/core/api/server_file.dart';
|
||||
import 'package:tma_app/core/api/session_store.dart';
|
||||
@@ -17,8 +18,8 @@ abstract interface class CalendarSharingRepository {
|
||||
/// 함께 관리를 쓸 수 있는지 — 기기 저장 모드에서는 쓸 수 없다
|
||||
bool get available;
|
||||
|
||||
/// 내 초대 코드와 연동된 사람들 (기기 저장 모드·로그인 전에는 null)
|
||||
Future<CalendarSharing?> fetch();
|
||||
/// 내 초대 코드와 연동된 사람들 (기기 저장 모드·로그인 전에는 null). [quiet] 면 실패해도 알림을 띄우지 않는다
|
||||
Future<CalendarSharing?> fetch({bool quiet = false});
|
||||
|
||||
/// 받은 초대 코드를 미리 본다
|
||||
Future<CalendarInvitation> previewInvitation(String code);
|
||||
@@ -38,7 +39,7 @@ class UnavailableCalendarSharingRepository implements CalendarSharingRepository
|
||||
bool get available => false;
|
||||
|
||||
@override
|
||||
Future<CalendarSharing?> fetch() async => null;
|
||||
Future<CalendarSharing?> fetch({bool quiet = false}) async => null;
|
||||
|
||||
@override
|
||||
Future<CalendarInvitation> previewInvitation(String code) => throw StateError(_kLocalOnly);
|
||||
@@ -89,9 +90,12 @@ class ApiCalendarSharingRepository implements CalendarSharingRepository {
|
||||
bool get available => true;
|
||||
|
||||
@override
|
||||
Future<CalendarSharing?> fetch() async {
|
||||
Future<CalendarSharing?> fetch({bool quiet = false}) async {
|
||||
if (!_session.hasSession) return null;
|
||||
final response = await _dio.get<Map<String, dynamic>>(_kSharingPath);
|
||||
final response = await _dio.get<Map<String, dynamic>>(
|
||||
_kSharingPath,
|
||||
options: quiet ? Options(extra: {kSilentExtra: true}) : null,
|
||||
);
|
||||
return calendarSharingFromApi(response.data!);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ import 'package:tma_app/features/schedule/domain/schedule_event.dart';
|
||||
/// 일정 저장소 계약 — 서버(ApiScheduleRepository) / 기기(LocalScheduleRepository).
|
||||
/// 바꾼 결과(id 가 붙은 일정·규칙)를 돌려주면 알림기가 [ScheduleBook] 에 반영한다.
|
||||
abstract interface class ScheduleRepository {
|
||||
Future<ScheduleBook> fetch();
|
||||
/// [quiet] 면 실패해도 알림을 띄우지 않는다 (뒤에서 다시 읽을 때)
|
||||
Future<ScheduleBook> fetch({bool quiet = false});
|
||||
|
||||
/// 날짜 일정 여러 건 — id 를 붙여 보낸 순서대로 돌려준다
|
||||
Future<List<ScheduleEvent>> createEvents(List<EventDraft> drafts);
|
||||
@@ -76,7 +77,7 @@ class LocalScheduleRepository implements ScheduleRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ScheduleBook> fetch() async => _read();
|
||||
Future<ScheduleBook> fetch({bool quiet = false}) async => _read();
|
||||
|
||||
@override
|
||||
Future<List<ScheduleEvent>> createEvents(List<EventDraft> drafts) async {
|
||||
|
||||
@@ -25,6 +25,9 @@ class CalendarSharingNotifier extends AsyncNotifier<CalendarSharing?> {
|
||||
/// 함께 관리를 쓸 수 있는지 (서버 모드)
|
||||
bool get available => _repo.available;
|
||||
|
||||
/// 다른 사람이 내 코드로 연동했는지 등을 다시 읽는다. [quiet] 면 실패해도 알림을 띄우지 않는다
|
||||
Future<void> refresh({bool quiet = false}) async => _set(await _repo.fetch(quiet: quiet));
|
||||
|
||||
Future<CalendarInvitation> previewInvitation(String code) => _repo.previewInvitation(code);
|
||||
|
||||
/// 받은 코드로 연동 — 함께 보는 일정이 바뀌므로 일정도 다시 읽는다
|
||||
|
||||
@@ -74,7 +74,8 @@ class ScheduleNotifier extends AsyncNotifier<ScheduleBook> {
|
||||
|
||||
/// 일정 삭제 — 반복 일정이면 규칙 전체를 지운다
|
||||
/// 함께 관리를 연동·해제했을 때 — 보이는 일정이 바뀌므로 다시 읽는다 (불러오는 중 화면 없이 바꿔 끼운다)
|
||||
Future<void> refresh() async => _set(await _repo.fetch());
|
||||
/// [quiet] 면 실패해도 알림을 띄우지 않는다 (앱이 뒤에서 다시 읽을 때)
|
||||
Future<void> refresh({bool quiet = false}) async => _set(await _repo.fetch(quiet: quiet));
|
||||
|
||||
Future<void> delete(ScheduleEvent target) async {
|
||||
await _deleteTarget(target);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:tma_app/features/shell/domain/app_tab.dart';
|
||||
import 'package:tma_app/features/shell/presentation/widgets/rup_tab_bar.dart';
|
||||
import 'package:tma_app/features/shell/presentation/widgets/shared_data_refresher.dart';
|
||||
|
||||
/// 로그인 후 앱 뼈대 — 탭별 화면 상태를 유지한 채 하단 탭으로 오간다.
|
||||
class MainShell extends StatelessWidget {
|
||||
@@ -22,7 +23,8 @@ class MainShell extends StatelessWidget {
|
||||
return Scaffold(
|
||||
// 콘텐츠가 탭바 뒤까지 이어져 흐림 효과가 보이게 한다
|
||||
extendBody: true,
|
||||
body: navigationShell,
|
||||
// 함께 쓰는 기록은 앱을 보는 동안에도 다시 읽는다
|
||||
body: SharedDataRefresher(child: navigationShell),
|
||||
bottomNavigationBar: RupTabBar(
|
||||
current: AppTab.values[navigationShell.currentIndex],
|
||||
onSelect: _select,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:tma_app/core/api/api_guard.dart';
|
||||
import 'package:tma_app/features/diary/presentation/providers/diary_providers.dart';
|
||||
import 'package:tma_app/features/schedule/presentation/providers/calendar_sharing_providers.dart';
|
||||
import 'package:tma_app/features/schedule/presentation/providers/schedule_providers.dart';
|
||||
|
||||
/// 앱을 보고 있는 동안 함께 쓰는 기록을 다시 읽는 간격 — 실시간 알림이 붙기 전까지의 대신
|
||||
const Duration kSharedRefreshInterval = Duration(minutes: 3);
|
||||
|
||||
/// 함께 쓰는 기록(공유 다이어리 · 연동된 달력)을 다시 읽는다 — 앱으로 돌아올 때와, 앱을 보는 동안 일정 간격으로.
|
||||
/// 다른 사람이 남긴 글·일정, 내 코드로 새로 연동한 사람이 앱을 다시 켜지 않아도 보인다.
|
||||
/// 뒤에서 읽는 것이라 실패해도 알리지 않는다(다음 차례에 다시 읽는다).
|
||||
class SharedDataRefresher extends ConsumerStatefulWidget {
|
||||
const SharedDataRefresher({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
ConsumerState<SharedDataRefresher> createState() => _SharedDataRefresherState();
|
||||
}
|
||||
|
||||
class _SharedDataRefresherState extends ConsumerState<SharedDataRefresher> {
|
||||
late final AppLifecycleListener _lifecycle;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_lifecycle = AppLifecycleListener(
|
||||
onResume: () {
|
||||
unawaited(_refresh());
|
||||
_startTimer();
|
||||
},
|
||||
// 화면을 보지 않을 때는 읽지 않는다
|
||||
onHide: () => _timer?.cancel(),
|
||||
);
|
||||
_startTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_lifecycle.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(kSharedRefreshInterval, (_) => unawaited(_refresh()));
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
if (!mounted) return;
|
||||
final shareDiary = ref.read(diaryProvider).value?.diaries.any((d) => d.shared) ?? false;
|
||||
if (shareDiary) unawaited(succeeded(ref.read(diaryProvider.notifier).refresh(quiet: true)));
|
||||
|
||||
// 기기 저장 모드(null)는 함께 관리가 없다. 연동 상태부터 읽고 — 누가 내 코드로 연동했을 수 있다 — 연동 중이면 일정도
|
||||
if (ref.read(calendarSharingProvider).value == null) return;
|
||||
if (!await succeeded(ref.read(calendarSharingProvider.notifier).refresh(quiet: true)) || !mounted) return;
|
||||
if (ref.read(calendarLinkedProvider)) {
|
||||
unawaited(succeeded(ref.read(scheduleProvider.notifier).refresh(quiet: true)));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
Reference in New Issue
Block a user