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
3 changes: 1 addition & 2 deletions fastlane/metadata/ko/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
์ด๋ฒˆ ์—…๋ฐ์ดํŠธ์—์„œ ๋‹ค์Œ ๋‚ด์šฉ์ด ๊ฐœ์„ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

โ€ข ์„ธํƒ ์ง„ํ–‰ ์ƒํƒœ์™€ ์•Œ๋ฆผ ๋ชฉ๋ก์ด ๋” ์ •ํ™•ํ•˜๊ฒŒ ๊ฐฑ์‹ ๋˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
โ€ข ์˜ˆ์•ฝ ์ค‘ ๋‹ค๋ฅธ ๊ธฐ๊ธฐ๋ฅผ ๋ˆŒ๋ €์„ ๋•Œ ์—‰๋šฑํ•œ ๊ธฐ๊ธฐ๊ฐ€ ์˜ˆ์•ฝ๋˜๋˜ ๋ฌธ์ œ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
โ€ข ์˜ˆ์•ฝ ์‹คํŒจ ์‹œ ์•ฑ์ด ๋น„์ •์ƒ์ ์œผ๋กœ ์ข…๋ฃŒ๋  ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ๋ฅผ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
15 changes: 13 additions & 2 deletions lib/core/errors/app_exception.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import 'package:dio/dio.dart';

abstract interface class UserFacingException implements Exception {
String get userMessage;
}

class AppException {
AppException({
required this.message,
Expand All @@ -12,6 +16,13 @@ class AppException {
final String? debugMessage;

factory AppException.from(Object? error) {
if (error is UserFacingException) {
return AppException(
message: error.userMessage,
debugMessage: error.toString(),
);
}

if (error is DioException) {
return AppException._fromDioException(error);
}
Expand Down Expand Up @@ -44,7 +55,7 @@ class AppException {
type == DioExceptionType.receiveTimeout ||
type == DioExceptionType.sendTimeout ||
type == DioExceptionType.connectionTimeout ||
statusCode == null) {
exception.response == null) {
return AppException(
message: '๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.',
statusCode: statusCode,
Expand Down Expand Up @@ -85,7 +96,7 @@ class AppException {
);
}

if (statusCode >= 500) {
if (statusCode != null && statusCode >= 500) {
return AppException(
message: '์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
statusCode: statusCode,
Expand Down
17 changes: 0 additions & 17 deletions lib/features/report/presentation/providers/report_provider.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:washer/core/utils/app_logger.dart';
import 'package:washer/features/report/data/data_sources/remote/report_remote_data_source.dart';
Expand Down Expand Up @@ -52,22 +51,6 @@ class ReportNotifier extends AsyncNotifier<void> {
}
}

String reportErrorMessage(Object? error) {
const fallback = '๊ณ ์žฅ ์‹ ๊ณ ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.';
if (error is! DioException || error.response?.data == null) {
return fallback;
}

final response = error.response!.data;
if (response is Map<String, dynamic> &&
response['message'] is String &&
(response['message'] as String).isNotEmpty) {
return response['message'] as String;
}

return fallback;
}

final reportProvider = AsyncNotifierProvider<ReportNotifier, void>(
ReportNotifier.new,
);
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'dart:async';

import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:washer/core/constants/durations.dart';
import 'package:washer/core/utils/app_logger.dart';
Expand All @@ -10,27 +9,17 @@ import 'package:washer/features/reservation/data/data_sources/remote/reservation
import 'package:washer/features/reservation/data/data_sources/remote/reservation_status_remote_data_source.dart';
import 'package:washer/features/reservation/data/models/local/active_reservation_model.dart';
import 'package:washer/features/reservation/data/models/local/laundry_machine_model.dart';
import 'package:washer/features/reservation/presentation/providers/reservation_exceptions.dart';
import 'package:washer/features/reservation/presentation/providers/reservation_penalty_provider.dart';
import 'package:washer/features/reservation/presentation/providers/reservation_status_provider.dart';
import 'package:washer/features/reservation/presentation/providers/reservation_sync_controller.dart';

/// ์˜ˆ์•ฝ ์š”์ฒญ ์ง์ „ GET์œผ๋กœ ํ™•์ธํ•œ ๊ฒฐ๊ณผ, ์ด๋ฏธ ์˜ˆ์•ฝ/์‚ฌ์šฉ ์ค‘์ด๋ผ ์˜ˆ์•ฝํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ.
class AlreadyReservedException implements Exception {
const AlreadyReservedException();
}

/// ์ทจ์†Œ ํŒจ๋„ํ‹ฐ ๊ธฐ๊ฐ„์ด๋ผ ์„œ๋ฒ„ ์š”์ฒญ ์—†์ด ํด๋ผ์ด์–ธํŠธ์—์„œ ์˜ˆ์•ฝ์„ ๋ง‰์€ ๊ฒฝ์šฐ.
class ReservationPenaltyException implements Exception {
const ReservationPenaltyException(this.expiresAt);

final DateTime expiresAt;
}

class ReservationActionNotifier extends AsyncNotifier<ActiveReservationModel?> {
/// ์ง„ํ–‰ ์ค‘์ธ ์š”์ฒญ์„ ๋Œ€์ƒ(๊ธฐ๊ธฐ/์˜ˆ์•ฝ) ๋‹จ์œ„๋กœ ๋ณด๊ด€ํ•œ๋‹ค.
/// ํ‚ค ์—†์ด ๋‹จ์ผ ์Šฌ๋กฏ์— ๋‹ด์œผ๋ฉด ๋‹ค๋ฅธ ๋Œ€์ƒ์˜ ์š”์ฒญ์— ํ•ฉ๋ฅ˜ํ•ด ๊ทธ ๊ฒฐ๊ณผ๊ฐ€
/// ์ด ํ˜ธ์ถœ์˜ ๋ฐ˜ํ™˜๊ฐ’์ด ๋œ๋‹ค(#261).
final Map<String, Future<Object?>> _inflight = {};
bool _didWaitForBuild = false;

@override
Future<ActiveReservationModel?> build() async => null;
Expand All @@ -45,12 +34,12 @@ class ReservationActionNotifier extends AsyncNotifier<ActiveReservationModel?> {
Future<ActiveReservationModel?> _reserveInternal({
required int machineId,
}) async {
// build()์ด ๋๋‚œ ๋’ค ์ƒํƒœ๋ฅผ ๋ฐ”๊ฟ”์•ผ, ๋™๊ธฐ ๊ฒฝ๋กœ์—์„œ ๋˜์ง„ ์˜ˆ์™ธ ์ƒํƒœ๊ฐ€
// ๋’ค๋Šฆ๊ฒŒ ๋๋‚œ build ๊ฒฐ๊ณผ(null)๋กœ ๋ฎ์ด์ง€ ์•Š๋Š”๋‹ค.
await future;
state = const AsyncLoading();

try {
// build()์ด ๋๋‚œ ๋’ค ์ƒํƒœ๋ฅผ ๋ฐ”๊ฟ”์•ผ, ๋™๊ธฐ ๊ฒฝ๋กœ์—์„œ ๋˜์ง„ ์˜ˆ์™ธ ์ƒํƒœ๊ฐ€
// ๋’ค๋Šฆ๊ฒŒ ๋๋‚œ build ๊ฒฐ๊ณผ(null)๋กœ ๋ฎ์ด์ง€ ์•Š๋Š”๋‹ค.
await _waitForInitialBuild();
state = const AsyncLoading();

// ์ทจ์†Œ ํŒจ๋„ํ‹ฐ ๊ธฐ๊ฐ„์ด๋ฉด ์„œ๋ฒ„๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด์ง€ ์•Š๊ณ  ํด๋ผ์ด์–ธํŠธ์—์„œ ๊ณง๋ฐ”๋กœ ๋ง‰๋Š”๋‹ค.
final penaltyExpiry = ref.read(reservationPenaltyProvider);
if (penaltyExpiry != null) {
Expand Down Expand Up @@ -99,6 +88,15 @@ class ReservationActionNotifier extends AsyncNotifier<ActiveReservationModel?> {
}
}

Future<void> _waitForInitialBuild() async {
if (_didWaitForBuild) {
return;
}

await future;
_didWaitForBuild = true;
}

Future<MachineModel?> _findMachine(int machineId) async {
final latestStatus = await ref
.read(homeRemoteDataSourceProvider)
Expand Down Expand Up @@ -169,47 +167,18 @@ class ReservationActionNotifier extends AsyncNotifier<ActiveReservationModel?> {

final request = action();
_inflight[key] = request;
request.whenComplete(() => _inflight.remove(key));
unawaited(
request.then<void>(
(_) => _inflight.remove(key),
onError: (Object _, StackTrace __) {
_inflight.remove(key);
},
),
);
return request;
}
}

String reservationActionErrorMessage(
Object? error, {
required String fallback,
}) {
if (error is! DioException || error.response?.data == null) {
return fallback;
}

final response = error.response!.data;
if (response is Map<String, dynamic> &&
response['message'] is String &&
(response['message'] as String).isNotEmpty) {
return response['message'] as String;
}

return fallback;
}

/// ์˜ˆ์•ฝ ์‹œ๋„ ์‹คํŒจ๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ๋ฌธ๊ตฌ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
///
/// ์‚ฌ์ „ ์กฐํšŒ ๋‹จ๊ณ„์—์„œ ์ด๋ฏธ ์˜ˆ์•ฝ/์‚ฌ์šฉ ์ค‘์œผ๋กœ ํ™•์ธ๋œ ๊ฒฝ์šฐ๋Š” ๋ณ„๋„ ์•ˆ๋‚ด๋กœ,
/// ๊ทธ ์™ธ์—๋Š” ์„œ๋ฒ„ ๋ฉ”์‹œ์ง€(์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ฌธ๊ตฌ)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
String reserveFailureMessage(Object? error) {
if (error is ReservationPenaltyException) {
final remaining = error.expiresAt.difference(DateTime.now());
final minutes = remaining.inMinutes;
return minutes >= 1
? '์˜ˆ์•ฝ์ด ์ œํ•œ๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ์•ฝ $minutes๋ถ„ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'
: '์˜ˆ์•ฝ์ด ์ œํ•œ๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.';
}
if (error is AlreadyReservedException) {
return '์ด๋ฏธ ์˜ˆ์•ฝ๋œ ๊ธฐ๊ธฐ์ž…๋‹ˆ๋‹ค.';
}
return '์˜ˆ์•ฝ ์‹คํŒจ: ${reservationActionErrorMessage(error, fallback: '์˜ˆ์•ฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.')}';
}

final reservationActionProvider =
AsyncNotifierProvider<ReservationActionNotifier, ActiveReservationModel?>(
ReservationActionNotifier.new,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:washer/core/errors/app_exception.dart';

/// ์˜ˆ์•ฝ ์š”์ฒญ ์ง์ „ ์กฐํšŒ์—์„œ ์ด๋ฏธ ์˜ˆ์•ฝ ๋˜๋Š” ์‚ฌ์šฉ ์ค‘์ธ ๊ธฐ๊ธฐ๋กœ ํ™•์ธ๋œ ๊ฒฝ์šฐ.
class AlreadyReservedException implements UserFacingException {
const AlreadyReservedException();

@override
String get userMessage => '์ด๋ฏธ ์˜ˆ์•ฝ๋œ ๊ธฐ๊ธฐ์ž…๋‹ˆ๋‹ค.';
}

/// ์ทจ์†Œ ํŒจ๋„ํ‹ฐ ๊ธฐ๊ฐ„์ด๋ผ ์„œ๋ฒ„ ์š”์ฒญ ์—†์ด ์˜ˆ์•ฝ์„ ๋ง‰์€ ๊ฒฝ์šฐ.
class ReservationPenaltyException implements UserFacingException {
const ReservationPenaltyException(this.expiresAt);

final DateTime expiresAt;

@override
String get userMessage {
final remaining = expiresAt.difference(DateTime.now());
final minutes = remaining.inMinutes;
return minutes >= 1
? '์˜ˆ์•ฝ์ด ์ œํ•œ๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ์•ฝ $minutes๋ถ„ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'
: '์˜ˆ์•ฝ์ด ์ œํ•œ๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.';
}
}
12 changes: 6 additions & 6 deletions lib/shared/ui/dialog/dialog_action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import 'package:washer/core/utils/app_logger.dart';
/// ์•ก์…˜ ๊ณ ์œ  ์ •๋ณด๋งŒ ๋‹ด๋Š”๋‹ค. ์•ก์…˜ ์ข…๋ฅ˜๋ณ„ ์ •์˜๋Š” `DialogActions` ํŒฉํ† ๋ฆฌ ํ•œ ๊ณณ์—
/// ๋ชจ์•„ ๋‘๊ณ , ํ˜ธ์ถœ๋ถ€๋Š” ๊ทธ๊ฒƒ์„ [runDialogAction]์— ๋„˜๊ธฐ๊ธฐ๋งŒ ํ•œ๋‹ค.
///
/// [run]ยท[failureMessage]๋Š” ์œ„์ ฏ์˜ `ref`๊ฐ€ ์•„๋‹ˆ๋ผ lifecycle๊ณผ ๋ฌด๊ด€ํ•œ
/// [ProviderContainer]๋ฅผ ๋ฐ›๋Š”๋‹ค. ๋”ฐ๋ผ์„œ pop(=widget dispose) ์ดํ›„ ์‹คํ–‰๋ผ๋„
/// "bad state" ์˜ค๋ฅ˜๊ฐ€ ๋‚˜์ง€ ์•Š์œผ๋ฉฐ, ํ˜ธ์ถœ๋ถ€๊ฐ€ notifier๋ฅผ ๋ฏธ๋ฆฌ ์บก์ฒ˜ํ•  ํ•„์š”๋„ ์—†๋‹ค.
/// [run]์€ ์œ„์ ฏ์˜ `ref`๊ฐ€ ์•„๋‹ˆ๋ผ lifecycle๊ณผ ๋ฌด๊ด€ํ•œ [ProviderContainer]๋ฅผ
/// ๋ฐ›๋Š”๋‹ค. ๋”ฐ๋ผ์„œ pop(=widget dispose) ์ดํ›„ ์‹คํ–‰๋ผ๋„ "bad state" ์˜ค๋ฅ˜๊ฐ€ ๋‚˜์ง€
/// ์•Š์œผ๋ฉฐ, ํ˜ธ์ถœ๋ถ€๊ฐ€ notifier๋ฅผ ๋ฏธ๋ฆฌ ์บก์ฒ˜ํ•  ํ•„์š”๋„ ์—†๋‹ค.
class DialogAction<R> {
const DialogAction({
required this.run,
required this.isSuccess,
required this.successMessage,
required this.failureMessage,
required this.fallbackMessage,
required this.logName,
this.failureError,
this.showLoading = false,
Expand All @@ -27,7 +27,7 @@ class DialogAction<R> {
final Future<R> Function(ProviderContainer container) run;
final bool Function(R result) isSuccess;
final String successMessage;
final String Function(ProviderContainer container) failureMessage;
final String fallbackMessage;
final String logName;
final Object? Function(ProviderContainer container)? failureError;

Expand Down Expand Up @@ -78,7 +78,7 @@ Future<void> runDialogAction<R>(
messenger.showErrorSnackBar(failureError);
} else {
messenger.showSnackBar(
SnackBar(content: Text(action.failureMessage(container))),
SnackBar(content: Text(action.fallbackMessage)),
);
}
}
Expand Down
15 changes: 6 additions & 9 deletions lib/shared/ui/dialog/dialog_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ abstract final class DialogActions {
successMessage:
'$machineName ์˜ˆ์•ฝ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค\n'
'$reservationExpiryMinutes๋ถ„ ๋™์•ˆ ๊ธฐ๊ธฐ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค',
failureMessage: (container) => reserveFailureMessage(
container.read(reservationActionProvider).error,
),
fallbackMessage: '์˜ˆ์•ฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
failureError: (container) =>
container.read(reservationActionProvider).error,
logName: 'ReserveAction',
showLoading: true,
);
Expand All @@ -42,10 +42,7 @@ abstract final class DialogActions {
),
isSuccess: (didCancel) => didCancel,
successMessage: '์˜ˆ์•ฝ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
failureMessage: (container) => reservationActionErrorMessage(
container.read(reservationActionProvider).error,
fallback: '์˜ˆ์•ฝ ์ทจ์†Œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.',
),
fallbackMessage: '์˜ˆ์•ฝ ์ทจ์†Œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.',
failureError: (container) =>
container.read(reservationActionProvider).error,
logName: 'CancelReservationAction',
Expand All @@ -66,8 +63,8 @@ abstract final class DialogActions {
),
isSuccess: (didReport) => didReport,
successMessage: '์‹ ๊ณ ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
failureMessage: (container) =>
reportErrorMessage(container.read(reportProvider).error),
fallbackMessage: '๊ณ ์žฅ ์‹ ๊ณ ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.',
failureError: (container) => container.read(reportProvider).error,
logName: 'ReportBrokenAction',
);
}
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.1.6+8
version: 1.1.6+9

environment:
sdk: ^3.9.2
Expand Down
3 changes: 2 additions & 1 deletion test/features/report/report_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:washer/core/errors/app_exception.dart';
import 'package:washer/features/report/data/data_sources/remote/report_remote_data_source.dart';
import 'package:washer/features/report/presentation/providers/report_provider.dart';

Expand Down Expand Up @@ -68,7 +69,7 @@ void main() {

expect(result, isFalse);
expect(
reportErrorMessage(container.read(reportProvider).error),
AppException.from(container.read(reportProvider).error).message,
'์ด๋ฏธ ์ ‘์ˆ˜๋œ ์‹ ๊ณ ์ž…๋‹ˆ๋‹ค.',
);
});
Expand Down
Loading
Loading