10 Commits

Author SHA1 Message Date
JiWoong Sul
37e797f6c1 chore: 버전 1.0.8+10 업데이트 2026-01-30 15:33:08 +09:00
JiWoong Sul
903906c880 fix(billing): 월별 비용 계산 시 연도 무시 버그 수정
- hasBillingInMonth()에서 targetYear를 실제로 사용하도록 수정
- 연간 구독 수정 시 잘못된 월에 비용이 포함되던 문제 해결
- 연도+월을 포함한 개월 차이 계산으로 정확한 결제 발생 여부 판단
2026-01-29 23:55:57 +09:00
JiWoong Sul
5de33992a2 fix(sms-scan): 에러 메시지를 토스트로 변경
- 구독 정보를 찾지 못한 경우 상단 붉은색 텍스트 대신 하단 토스트 메시지 출력
- ScanInitialWidget에서 errorMessage 파라미터 제거
- SmsScanController에서 AppSnackBar.showError() 사용
- 불필요한 _errorMessage 상태 변수 제거
2026-01-29 23:55:49 +09:00
JiWoong Sul
cc8bcc7b54 refactor(home): 구독 개수 표시에서 화살표 아이콘 제거
- 메인 화면의 구독 개수 표시 우측 ">" 아이콘 삭제
- 불필요한 UI 요소 정리
2026-01-29 23:55:39 +09:00
JiWoong Sul
9a950ee6c7 chore: 버전 1.0.7+9 업데이트 2026-01-21 17:01:32 +09:00
JiWoong Sul
88569a57bf chore: 스플래시 화면 저작권 텍스트에 cclabs 추가 2026-01-21 17:01:22 +09:00
JiWoong Sul
7125a4745a feat(settings): 앱 버전 자동 표시 기능 추가
- package_info_plus 패키지 추가
- settings_screen에서 pubspec.yaml 버전을 자동으로 표시
2026-01-17 00:31:41 +09:00
JiWoong Sul
8d6b24ed6f chore: 버전 1.0.6+8 업데이트 2026-01-17 00:15:52 +09:00
JiWoong Sul
0db1f12b40 feat: Android 15 edge-to-edge 모드 지원
- immersiveSticky → edgeToEdge 모드 변경
- deprecated된 네비게이션바 색상 API 제거
- 시스템이 네비게이션바 색상 자동 처리
2026-01-14 19:12:35 +09:00
JiWoong Sul
595513b2e6 refactor: MainActivity 불필요한 주석 제거 2026-01-14 19:12:28 +09:00
13 changed files with 91 additions and 90 deletions

View File

@@ -2,7 +2,4 @@ package com.naturebridgeai.digitalrentmanager
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
// flutter_sms_inbox 패키지가 SMS 처리를 담당하므로
// 기존 MethodChannel 코드는 제거되었습니다
}
class MainActivity: FlutterActivity()

View File

@@ -17,15 +17,13 @@ import '../providers/category_provider.dart';
import '../providers/payment_card_provider.dart';
import '../l10n/app_localizations.dart';
import '../utils/logger.dart';
import '../widgets/common/snackbar/app_snackbar.dart';
class SmsScanController extends ChangeNotifier {
// 상태 관리
bool _isLoading = false;
bool get isLoading => _isLoading;
String? _errorMessage;
String? get errorMessage => _errorMessage;
List<Subscription> _scannedSubscriptions = [];
List<Subscription> get scannedSubscriptions => _scannedSubscriptions;
PaymentCardSuggestion? _currentSuggestion;
@@ -109,7 +107,6 @@ class SmsScanController extends ChangeNotifier {
Future<void> scanSms(BuildContext context) async {
_isLoading = true;
_errorMessage = null;
_scannedSubscriptions = [];
_currentIndex = 0;
notifyListeners();
@@ -137,9 +134,12 @@ class SmsScanController extends ChangeNotifier {
final req = await permission.Permission.sms.request();
if (!ctx.mounted) return;
if (!req.isGranted) {
// 거부됨: 안내 후 종료
// 거부됨: 토스트 표시 후 종료
if (!ctx.mounted) return;
_errorMessage = AppLocalizations.of(ctx).smsPermissionRequired;
AppSnackBar.showError(
context: ctx,
message: AppLocalizations.of(ctx).smsPermissionRequired,
);
_isLoading = false;
notifyListeners();
return;
@@ -162,7 +162,10 @@ class SmsScanController extends ChangeNotifier {
if (scanResults.isEmpty) {
Log.i('스캔된 구독이 없음');
_errorMessage = AppLocalizations.of(context).subscriptionNotFound;
AppSnackBar.showError(
context: context,
message: AppLocalizations.of(context).subscriptionNotFound,
);
_isLoading = false;
notifyListeners();
return;
@@ -184,7 +187,10 @@ class SmsScanController extends ChangeNotifier {
if (repeatSubscriptions.isEmpty) {
Log.i('반복 결제된 구독이 없음');
_errorMessage = AppLocalizations.of(context).repeatSubscriptionNotFound;
AppSnackBar.showError(
context: context,
message: AppLocalizations.of(context).repeatSubscriptionNotFound,
);
_isLoading = false;
notifyListeners();
return;
@@ -223,8 +229,10 @@ class SmsScanController extends ChangeNotifier {
} catch (e) {
Log.e('SMS 스캔 중 오류 발생', e);
if (context.mounted) {
_errorMessage =
AppLocalizations.of(context).smsScanErrorWithMessage(e.toString());
AppSnackBar.showError(
context: context,
message: AppLocalizations.of(context).smsScanErrorWithMessage(e.toString()),
);
_isLoading = false;
notifyListeners();
}
@@ -343,7 +351,6 @@ class SmsScanController extends ChangeNotifier {
void resetState() {
_scannedSubscriptions = [];
_currentIndex = 0;
_errorMessage = null;
_selectedPaymentCardId = null;
_currentSuggestion = null;
_shouldSuggestCardCreation = false;

View File

@@ -35,12 +35,9 @@ const bool enableAdMob = true;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 시스템 네비게이션 바 숨김 (Immersive Sticky 모드)
// 스와이프 시 일시적으로 나타났다가 자동으로 사라짐
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
overlays: [SystemUiOverlay.top], // 상태바만 유지
);
// Android 15 edge-to-edge 모드 활성화
// 콘텐츠가 시스템 바 영역까지 확장됨
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
// 구글 모바일 광고 SDK 초기화 (웹이 아니고, Android/iOS에서만)
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS) && enableAdMob) {

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode;
import 'package:provider/provider.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../providers/notification_provider.dart';
import 'dart:io';
import '../services/notification_service.dart';
@@ -884,22 +885,28 @@ class SettingsScreen extends StatelessWidget {
.withValues(alpha: 0.5),
),
),
child: ListTile(
contentPadding: const EdgeInsets.all(8),
title: Text(
AppLocalizations.of(context).appInfo,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface),
),
subtitle: Text(
'${AppLocalizations.of(context).version} 1.0.0',
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurfaceVariant),
),
leading: Icon(Icons.info,
color: Theme.of(context).colorScheme.onSurfaceVariant),
onTap: null,
child: FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
final version = snapshot.data?.version ?? '-';
return ListTile(
contentPadding: const EdgeInsets.all(8),
title: Text(
AppLocalizations.of(context).appInfo,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface),
),
subtitle: Text(
'${AppLocalizations.of(context).version} $version',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant),
),
leading: Icon(Icons.info,
color:
Theme.of(context).colorScheme.onSurfaceVariant),
onTap: null,
// onTap: () async {
// // 항상 앱 내 About 다이얼로그를 우선 표시 (현재 미사용)
// showAboutDialog(
@@ -954,6 +961,8 @@ class SettingsScreen extends StatelessWidget {
// ],
// );
// },
);
},
),
),
// FloatingNavigationBar를 위한 충분한 하단 여백

View File

@@ -58,7 +58,6 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
if (_controller.scannedSubscriptions.isEmpty) {
return ScanInitialWidget(
onScanPressed: () => _controller.startScan(context),
errorMessage: _controller.errorMessage,
);
}
@@ -77,7 +76,6 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
});
return ScanInitialWidget(
onScanPressed: () => _controller.startScan(context),
errorMessage: _controller.errorMessage,
);
}

View File

@@ -384,7 +384,7 @@ class _SplashScreenState extends State<SplashScreen>
child: FadeTransition(
opacity: _fadeAnimation,
child: Text(
'© 2025 NatureBridgeAI. All rights reserved.',
'© 2025 NatureBridgeAI @ cclabs. All rights reserved.',
style: TextStyle(
fontSize: 12,
color: Theme.of(context)

View File

@@ -305,9 +305,9 @@ class AdaptiveTheme {
}
/// 시스템 테마에 따른 상태바 스타일 적용
/// Android 15+ edge-to-edge 호환: deprecated된 네비게이션바 색상 API 제거
static void applySystemUIOverlay(BuildContext context) {
final brightness = Theme.of(context).brightness;
final isOled = Theme.of(context).scaffoldBackgroundColor == Colors.black;
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
@@ -315,13 +315,8 @@ class AdaptiveTheme {
brightness == Brightness.dark ? Brightness.light : Brightness.dark,
statusBarBrightness:
brightness == Brightness.dark ? Brightness.light : Brightness.dark,
systemNavigationBarColor: isOled
? Colors.black
: (brightness == Brightness.dark
? const Color(0xFF121212)
: Colors.white),
systemNavigationBarIconBrightness:
brightness == Brightness.dark ? Brightness.light : Brightness.dark,
// Android 15+: 네비게이션바 색상은 시스템이 자동 처리
systemNavigationBarContrastEnforced: false,
));
}

View File

@@ -214,20 +214,20 @@ class BillingCostUtil {
// 결제 주기에 따른 개월 수
final cycleMonths = _getCycleMonths(normalizedCycle);
// 결제 발생 월 계산 (nextBillingDate 기준으로 역산)
final billingMonth = nextBillingDate.month;
// 연도+월을 포함한 개월 차이 계산
// nextBillingDate와 target 월 사이의 차이가 cycleMonths의 배수인지 확인
final targetStart = DateTime(targetYear, targetMonth, 1);
final billingStart =
DateTime(nextBillingDate.year, nextBillingDate.month, 1);
// 대상 월이 결제 발생 월과 일치하는지 확인
// 예: 연간 결제(1월), targetMonth = 1 → true
// 예: 연간 결제(1월), targetMonth = 2 → false
for (int i = 0; i < 12; i += cycleMonths) {
final checkMonth = ((billingMonth - 1 + i) % 12) + 1;
if (checkMonth == targetMonth) {
return true;
}
}
final monthDiff = (billingStart.year - targetStart.year) * 12 +
(billingStart.month - targetStart.month);
return false;
// monthDiff가 cycleMonths의 배수이면 해당 월에 결제 발생
// 예: 연간 결제(2027-01), target=2026-01 → monthDiff=12, 12%12=0 → true (이전 결제)
// 예: 연간 결제(2027-01), target=2026-02 → monthDiff=11, 11%12≠0 → false
// 예: 연간 결제(2027-01), target=2027-01 → monthDiff=0, 0%12=0 → true
return monthDiff % cycleMonths == 0;
}
/// 결제 주기별 개월 수 반환

View File

@@ -161,24 +161,14 @@ class _HomeContentState extends State<HomeContent> {
).animate(CurvedAnimation(
parent: widget.slideController,
curve: Curves.easeOutCubic)),
child: Row(
children: [
Text(
AppLocalizations.of(context).subscriptionCount(
subscriptionProvider.subscriptions.length),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 4),
Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).colorScheme.primary,
),
],
child: Text(
AppLocalizations.of(context).subscriptionCount(
subscriptionProvider.subscriptions.length),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
),
],

View File

@@ -7,12 +7,10 @@ import '../../l10n/app_localizations.dart';
class ScanInitialWidget extends StatelessWidget {
final VoidCallback onScanPressed;
final String? errorMessage;
const ScanInitialWidget({
super.key,
required this.onScanPressed,
this.errorMessage,
});
@override
@@ -24,15 +22,6 @@ class ScanInitialWidget extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (errorMessage != null)
Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: ThemedText(
errorMessage!,
color: Theme.of(context).colorScheme.error,
textAlign: TextAlign.center,
),
),
ThemedText(
AppLocalizations.of(context).findRepeatSubscriptions,
fontSize: 20,

View File

@@ -8,6 +8,7 @@ import Foundation
import flutter_local_notifications
import flutter_secure_storage_darwin
import local_auth_darwin
import package_info_plus
import path_provider_foundation
import share_plus
import shared_preferences_foundation
@@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View File

@@ -733,6 +733,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
url: "https://pub.dev"
source: hosted
version: "8.3.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path:
dependency: transitive
description:

View File

@@ -1,7 +1,7 @@
name: submanager
description: A new Flutter project.
publish_to: 'none'
version: 1.0.5+7
version: 1.0.8+10
environment:
sdk: '>=3.0.0 <4.0.0'
@@ -42,6 +42,7 @@ dependencies:
crypto: ^3.0.6
image: ^4.5.4
google_mobile_ads: ^6.0.0
package_info_plus: ^8.3.0
dev_dependencies:
flutter_test: