perf: 파싱/렌더 최적화 다수 적용
- SmsScanner 키워드/정규식 상수화로 반복 컴파일 제거\n- 리스트에 prototypeItem 추가, 카드 RepaintBoundary 적용\n- 차트 영역 RepaintBoundary로 페인트 분리\n- GlassmorphicScaffold 파티클 수를 disableAnimations에 따라 감소\n- 캐시 초기화 플래그를 --dart-define로 제어(CLEAR_CACHE_ON_STARTUP)
This commit is contained in:
@@ -45,8 +45,15 @@ Future<void> main() async {
|
||||
try {
|
||||
// 메모리 이미지 캐시는 유지하지만 필요한 경우 삭제할 수 있도록 준비
|
||||
|
||||
// 오래된 디스크 캐시 파일만 지우기 (새로운 것은 유지)
|
||||
// 캐시 전체 삭제는 큰 I/O 부하를 유발할 수 있어 비활성화
|
||||
// 필요 시 환경 플래그로 제어하거나 주기적 백그라운드 정리로 전환하세요.
|
||||
const bool clearCacheOnStartup = bool.fromEnvironment(
|
||||
'CLEAR_CACHE_ON_STARTUP',
|
||||
defaultValue: false,
|
||||
);
|
||||
if (clearCacheOnStartup) {
|
||||
await DefaultCacheManager().emptyCache();
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
Log.d('이미지 캐시 관리 초기화 완료');
|
||||
|
||||
@@ -93,32 +93,32 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
|
||||
value: '#1976D2',
|
||||
child: Text(
|
||||
AppLocalizations.of(context).colorBlue,
|
||||
style:
|
||||
const TextStyle(color: AppColors.darkNavy))),
|
||||
style: const TextStyle(
|
||||
color: AppColors.darkNavy))),
|
||||
DropdownMenuItem(
|
||||
value: '#4CAF50',
|
||||
child: Text(
|
||||
AppLocalizations.of(context).colorGreen,
|
||||
style:
|
||||
const TextStyle(color: AppColors.darkNavy))),
|
||||
style: const TextStyle(
|
||||
color: AppColors.darkNavy))),
|
||||
DropdownMenuItem(
|
||||
value: '#FF9800',
|
||||
child: Text(
|
||||
AppLocalizations.of(context).colorOrange,
|
||||
style:
|
||||
const TextStyle(color: AppColors.darkNavy))),
|
||||
style: const TextStyle(
|
||||
color: AppColors.darkNavy))),
|
||||
DropdownMenuItem(
|
||||
value: '#F44336',
|
||||
child: Text(
|
||||
AppLocalizations.of(context).colorRed,
|
||||
style:
|
||||
const TextStyle(color: AppColors.darkNavy))),
|
||||
style: const TextStyle(
|
||||
color: AppColors.darkNavy))),
|
||||
DropdownMenuItem(
|
||||
value: '#9C27B0',
|
||||
child: Text(
|
||||
AppLocalizations.of(context).colorPurple,
|
||||
style:
|
||||
const TextStyle(color: AppColors.darkNavy))),
|
||||
style: const TextStyle(
|
||||
color: AppColors.darkNavy))),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
|
||||
@@ -533,8 +533,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
onPressed: () async {
|
||||
await permission.openAppSettings();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)
|
||||
child: Text(AppLocalizations.of(context)
|
||||
.openSettings),
|
||||
)
|
||||
: ElevatedButton(
|
||||
@@ -545,11 +544,13 @@ class SettingsScreen extends StatelessWidget {
|
||||
final newStatus = await permission
|
||||
.Permission.sms.status;
|
||||
if (newStatus.isPermanentlyDenied) {
|
||||
await permission.openAppSettings();
|
||||
await permission
|
||||
.openAppSettings();
|
||||
}
|
||||
}
|
||||
if (context.mounted) {
|
||||
(context as Element).markNeedsBuild();
|
||||
(context as Element)
|
||||
.markNeedsBuild();
|
||||
}
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)
|
||||
|
||||
@@ -7,6 +7,38 @@ import '../services/subscription_url_matcher.dart';
|
||||
import '../utils/platform_helper.dart';
|
||||
|
||||
class SmsScanner {
|
||||
// 반복 사용되는 리소스 상수화로 파싱 성능 최적화
|
||||
static const List<String> _subscriptionKeywords = [
|
||||
'구독',
|
||||
'결제',
|
||||
'정기결제',
|
||||
'자동결제',
|
||||
'월정액',
|
||||
'subscription',
|
||||
'payment',
|
||||
'billing',
|
||||
'charge',
|
||||
'넷플릭스',
|
||||
'Netflix',
|
||||
'유튜브',
|
||||
'YouTube',
|
||||
'Spotify',
|
||||
'멜론',
|
||||
'웨이브',
|
||||
'Disney+',
|
||||
'디즈니플러스',
|
||||
'Apple',
|
||||
'Microsoft',
|
||||
'GitHub',
|
||||
'Adobe',
|
||||
'Amazon'
|
||||
];
|
||||
static final List<RegExp> _amountPatterns = <RegExp>[
|
||||
RegExp(r'(\d{1,3}(?:,\d{3})*)(?:원|₩)'), // 원화
|
||||
RegExp(r'\$(\d+(?:\.\d{2})?)'), // 달러
|
||||
RegExp(r'(\d+(?:\.\d{2})?)\s*USD'), // USD
|
||||
RegExp(r'결제.*?(\d{1,3}(?:,\d{3})*)'), // 결제 금액
|
||||
];
|
||||
final SmsQuery _query = SmsQuery();
|
||||
|
||||
Future<List<SubscriptionModel>> scanForSubscriptions() async {
|
||||
@@ -106,35 +138,8 @@ class SmsScanner {
|
||||
final sender = message.address ?? '';
|
||||
final date = message.date ?? DateTime.now();
|
||||
|
||||
// 구독 서비스 키워드 매칭
|
||||
final subscriptionKeywords = [
|
||||
'구독',
|
||||
'결제',
|
||||
'정기결제',
|
||||
'자동결제',
|
||||
'월정액',
|
||||
'subscription',
|
||||
'payment',
|
||||
'billing',
|
||||
'charge',
|
||||
'넷플릭스',
|
||||
'Netflix',
|
||||
'유튜브',
|
||||
'YouTube',
|
||||
'Spotify',
|
||||
'멜론',
|
||||
'웨이브',
|
||||
'Disney+',
|
||||
'디즈니플러스',
|
||||
'Apple',
|
||||
'Microsoft',
|
||||
'GitHub',
|
||||
'Adobe',
|
||||
'Amazon'
|
||||
];
|
||||
|
||||
// 구독 관련 키워드가 있는지 확인
|
||||
bool isSubscription = subscriptionKeywords.any((keyword) =>
|
||||
bool isSubscription = _subscriptionKeywords.any((keyword) =>
|
||||
body.toLowerCase().contains(keyword.toLowerCase()) ||
|
||||
sender.toLowerCase().contains(keyword.toLowerCase()));
|
||||
|
||||
@@ -208,15 +213,8 @@ class SmsScanner {
|
||||
|
||||
// 금액 추출 로직
|
||||
double? _extractAmount(String body) {
|
||||
// 다양한 금액 패턴 매칭
|
||||
final patterns = [
|
||||
RegExp(r'(\d{1,3}(?:,\d{3})*)(?:원|₩)'), // 원화
|
||||
RegExp(r'\$(\d+(?:\.\d{2})?)'), // 달러
|
||||
RegExp(r'(\d+(?:\.\d{2})?)\s*USD'), // USD
|
||||
RegExp(r'결제.*?(\d{1,3}(?:,\d{3})*)'), // 결제 금액
|
||||
];
|
||||
|
||||
for (final pattern in patterns) {
|
||||
// 다양한 금액 패턴 매칭(사전 컴파일)
|
||||
for (final pattern in _amountPatterns) {
|
||||
final match = pattern.firstMatch(body);
|
||||
if (match != null) {
|
||||
String amountStr = match.group(1) ?? '';
|
||||
|
||||
@@ -154,8 +154,9 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 바 차트
|
||||
AspectRatio(
|
||||
// 바 차트 (RepaintBoundary로 페인트 분리)
|
||||
RepaintBoundary(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.6,
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
@@ -251,6 +252,7 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: ThemedText.caption(
|
||||
|
||||
@@ -177,10 +177,13 @@ class _GlassmorphicScaffoldState extends State<GlassmorphicScaffold>
|
||||
child: AnimatedBuilder(
|
||||
animation: _particleController,
|
||||
builder: (context, child) {
|
||||
final media = MediaQuery.maybeOf(context);
|
||||
final reduce = media?.disableAnimations ?? false;
|
||||
final count = reduce ? 10 : 30;
|
||||
return CustomPaint(
|
||||
painter: ParticlePainter(
|
||||
animation: _particleController,
|
||||
particleCount: 30,
|
||||
particleCount: count,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -71,6 +71,7 @@ class SubscriptionListWidget extends StatelessWidget {
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
prototypeItem: const SizedBox(height: 156),
|
||||
itemCount: subscriptions.length,
|
||||
itemBuilder: (context, subIndex) {
|
||||
// 각 구독의 지연값 계산 (순차적으로 나타나도록)
|
||||
@@ -98,6 +99,7 @@ class SubscriptionListWidget extends StatelessWidget {
|
||||
child: StaggeredAnimationItem(
|
||||
index: subIndex,
|
||||
delay: const Duration(milliseconds: 50),
|
||||
child: RepaintBoundary(
|
||||
child: SwipeableSubscriptionCard(
|
||||
subscription: subscriptions[subIndex],
|
||||
onTap: () {
|
||||
@@ -115,7 +117,8 @@ class SubscriptionListWidget extends StatelessWidget {
|
||||
);
|
||||
final locale =
|
||||
localeProvider.locale.languageCode;
|
||||
final displayName = await SubscriptionUrlMatcher
|
||||
final displayName =
|
||||
await SubscriptionUrlMatcher
|
||||
.getServiceDisplayName(
|
||||
serviceName:
|
||||
subscriptions[subIndex].serviceName,
|
||||
@@ -155,6 +158,7 @@ class SubscriptionListWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user