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:
JiWoong Sul
2025-07-09 14:29:53 +09:00
commit 8619e96739
177 changed files with 23085 additions and 0 deletions

View 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,
});
}

View 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;
}

View 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;
}

View 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

View 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;
}