Initial commit: SubManager Flutter App
주요 구현 완료 기능: - 구독 관리 (추가/편집/삭제/카테고리 분류) - 이벤트 할인 시스템 (기본값 자동 설정) - SMS 자동 스캔 및 구독 정보 추출 - 알림 시스템 (타임존 처리 안정화) - 환율 변환 지원 (KRW/USD) - 반응형 UI 및 애니메이션 - 다국어 지원 (한국어/영어) 버그 수정: - NotificationService tz.local 초기화 오류 해결 - MainScreenSummaryCard 레이아웃 오버플로우 수정 - 구독 추가 시 LateInitializationError 완전 해결 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
25
lib/models/category_model.dart
Normal file
25
lib/models/category_model.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'category_model.g.dart';
|
||||
|
||||
@HiveType(typeId: 1)
|
||||
class CategoryModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
String id;
|
||||
|
||||
@HiveField(1)
|
||||
String name;
|
||||
|
||||
@HiveField(2)
|
||||
String color;
|
||||
|
||||
@HiveField(3)
|
||||
String icon;
|
||||
|
||||
CategoryModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.color,
|
||||
required this.icon,
|
||||
});
|
||||
}
|
||||
50
lib/models/category_model.g.dart
Normal file
50
lib/models/category_model.g.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'category_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class CategoryModelAdapter extends TypeAdapter<CategoryModel> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
CategoryModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return CategoryModel(
|
||||
id: fields[0] as String,
|
||||
name: fields[1] as String,
|
||||
color: fields[2] as String,
|
||||
icon: fields[3] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, CategoryModel obj) {
|
||||
writer
|
||||
..writeByte(4)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.color)
|
||||
..writeByte(3)
|
||||
..write(obj.icon);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CategoryModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
64
lib/models/subscription.dart
Normal file
64
lib/models/subscription.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
class Subscription {
|
||||
final String id;
|
||||
final String serviceName;
|
||||
final double monthlyCost;
|
||||
final String billingCycle;
|
||||
final DateTime nextBillingDate;
|
||||
final String? category;
|
||||
final String? notes;
|
||||
final int repeatCount;
|
||||
final DateTime? lastPaymentDate;
|
||||
final String? websiteUrl;
|
||||
final String currency;
|
||||
|
||||
Subscription({
|
||||
required this.id,
|
||||
required this.serviceName,
|
||||
required this.monthlyCost,
|
||||
required this.billingCycle,
|
||||
required this.nextBillingDate,
|
||||
this.category,
|
||||
this.notes,
|
||||
this.repeatCount = 1,
|
||||
this.lastPaymentDate,
|
||||
this.websiteUrl,
|
||||
this.currency = 'KRW',
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'serviceName': serviceName,
|
||||
'monthlyCost': monthlyCost,
|
||||
'billingCycle': billingCycle,
|
||||
'nextBillingDate': nextBillingDate.toIso8601String(),
|
||||
'category': category,
|
||||
'notes': notes,
|
||||
'repeatCount': repeatCount,
|
||||
'lastPaymentDate': lastPaymentDate?.toIso8601String(),
|
||||
'websiteUrl': websiteUrl,
|
||||
'currency': currency,
|
||||
};
|
||||
}
|
||||
|
||||
factory Subscription.fromMap(Map<String, dynamic> map) {
|
||||
return Subscription(
|
||||
id: map['id'] as String,
|
||||
serviceName: map['serviceName'] as String,
|
||||
monthlyCost: map['monthlyCost'] as double,
|
||||
billingCycle: map['billingCycle'] as String,
|
||||
nextBillingDate: DateTime.parse(map['nextBillingDate'] as String),
|
||||
category: map['category'] as String?,
|
||||
notes: map['notes'] as String?,
|
||||
repeatCount: (map['repeatCount'] as num?)?.toInt() ?? 1,
|
||||
lastPaymentDate: map['lastPaymentDate'] != null
|
||||
? DateTime.parse(map['lastPaymentDate'] as String)
|
||||
: null,
|
||||
websiteUrl: map['websiteUrl'] as String?,
|
||||
currency: map['currency'] as String? ?? 'KRW',
|
||||
);
|
||||
}
|
||||
|
||||
// 주기적 결제 여부 확인
|
||||
bool get isRecurring => repeatCount > 1;
|
||||
}
|
||||
101
lib/models/subscription_model.dart
Normal file
101
lib/models/subscription_model.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
|
||||
part 'subscription_model.g.dart';
|
||||
|
||||
@HiveType(typeId: 0)
|
||||
class SubscriptionModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
@HiveField(1)
|
||||
String serviceName;
|
||||
|
||||
@HiveField(2)
|
||||
double monthlyCost;
|
||||
|
||||
@HiveField(3)
|
||||
String billingCycle; // '월간', '연간', '주간' 등
|
||||
|
||||
@HiveField(4)
|
||||
DateTime nextBillingDate;
|
||||
|
||||
@HiveField(5)
|
||||
bool isAutoDetected; // SMS로 추가된 경우 true
|
||||
|
||||
@HiveField(6)
|
||||
String? categoryId;
|
||||
|
||||
@HiveField(7)
|
||||
String? websiteUrl; // 홈페이지 URL
|
||||
|
||||
@HiveField(8)
|
||||
int repeatCount; // 반복 결제 횟수
|
||||
|
||||
@HiveField(9)
|
||||
DateTime? lastPaymentDate; // 마지막 결제일
|
||||
|
||||
@HiveField(10)
|
||||
String currency; // 통화 단위: 'KRW' 또는 'USD'
|
||||
|
||||
@HiveField(11)
|
||||
bool isEventActive; // 이벤트 활성화 여부
|
||||
|
||||
@HiveField(12)
|
||||
DateTime? eventStartDate; // 이벤트 시작일
|
||||
|
||||
@HiveField(13)
|
||||
DateTime? eventEndDate; // 이벤트 종료일
|
||||
|
||||
@HiveField(14)
|
||||
double? eventPrice; // 이벤트 기간 중 가격
|
||||
|
||||
SubscriptionModel({
|
||||
required this.id,
|
||||
required this.serviceName,
|
||||
required this.monthlyCost,
|
||||
required this.billingCycle,
|
||||
required this.nextBillingDate,
|
||||
this.isAutoDetected = false,
|
||||
this.categoryId,
|
||||
this.websiteUrl,
|
||||
this.repeatCount = 1,
|
||||
this.lastPaymentDate,
|
||||
this.currency = 'KRW', // 기본값은 KRW
|
||||
this.isEventActive = false, // 기본값은 false
|
||||
this.eventStartDate,
|
||||
this.eventEndDate,
|
||||
this.eventPrice,
|
||||
});
|
||||
|
||||
// 주기적 결제 여부 확인
|
||||
bool get isRecurring => repeatCount > 1;
|
||||
|
||||
// 현재 이벤트 기간인지 확인
|
||||
bool get isCurrentlyInEvent {
|
||||
if (!isEventActive || eventStartDate == null || eventEndDate == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
return now.isAfter(eventStartDate!) && now.isBefore(eventEndDate!);
|
||||
}
|
||||
|
||||
// 현재 적용되는 가격 (이벤트 또는 정상 가격)
|
||||
double get currentPrice {
|
||||
if (isCurrentlyInEvent && eventPrice != null) {
|
||||
return eventPrice!;
|
||||
}
|
||||
return monthlyCost;
|
||||
}
|
||||
|
||||
// 이벤트로 인한 절약액
|
||||
double get eventSavings {
|
||||
if (isCurrentlyInEvent && eventPrice != null) {
|
||||
return monthlyCost - eventPrice!;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Hive TypeAdapter 생성을 위한 명령어
|
||||
// flutter pub run build_runner build
|
||||
83
lib/models/subscription_model.g.dart
Normal file
83
lib/models/subscription_model.g.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'subscription_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class SubscriptionModelAdapter extends TypeAdapter<SubscriptionModel> {
|
||||
@override
|
||||
final int typeId = 0;
|
||||
|
||||
@override
|
||||
SubscriptionModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return SubscriptionModel(
|
||||
id: fields[0] as String,
|
||||
serviceName: fields[1] as String,
|
||||
monthlyCost: fields[2] as double,
|
||||
billingCycle: fields[3] as String,
|
||||
nextBillingDate: fields[4] as DateTime,
|
||||
isAutoDetected: fields[5] as bool,
|
||||
categoryId: fields[6] as String?,
|
||||
websiteUrl: fields[7] as String?,
|
||||
repeatCount: fields[8] as int,
|
||||
lastPaymentDate: fields[9] as DateTime?,
|
||||
currency: fields[10] as String,
|
||||
isEventActive: fields[11] as bool,
|
||||
eventStartDate: fields[12] as DateTime?,
|
||||
eventEndDate: fields[13] as DateTime?,
|
||||
eventPrice: fields[14] as double?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, SubscriptionModel obj) {
|
||||
writer
|
||||
..writeByte(15)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.serviceName)
|
||||
..writeByte(2)
|
||||
..write(obj.monthlyCost)
|
||||
..writeByte(3)
|
||||
..write(obj.billingCycle)
|
||||
..writeByte(4)
|
||||
..write(obj.nextBillingDate)
|
||||
..writeByte(5)
|
||||
..write(obj.isAutoDetected)
|
||||
..writeByte(6)
|
||||
..write(obj.categoryId)
|
||||
..writeByte(7)
|
||||
..write(obj.websiteUrl)
|
||||
..writeByte(8)
|
||||
..write(obj.repeatCount)
|
||||
..writeByte(9)
|
||||
..write(obj.lastPaymentDate)
|
||||
..writeByte(10)
|
||||
..write(obj.currency)
|
||||
..writeByte(11)
|
||||
..write(obj.isEventActive)
|
||||
..writeByte(12)
|
||||
..write(obj.eventStartDate)
|
||||
..writeByte(13)
|
||||
..write(obj.eventEndDate)
|
||||
..writeByte(14)
|
||||
..write(obj.eventPrice);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SubscriptionModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
Reference in New Issue
Block a user