카테고리 없음
📌 iOS 가계부 앱 – 반복 거래(Repeat Transaction) 기능 구현
밤새는 탐험가89
2025. 8. 11. 12:41
728x90
SMALL
가계부 앱을 만들다 보면 "월급, 월세, 구독료"처럼 정기적으로 반복되는 거래를 자동으로 등록해주는 기능이 필요합니다.
이번 글에서는 반복 주기 선택 → 데이터 저장 → 매일 체크 후 생성까지 전 과정을 구현해봤습니다.

1. 데이터 모델 설계
ExpenseModel에 반복 여부와 반복 주기를 저장할 수 있도록 속성을 추가합니다.
(여기서는 언어 현지화에 대응하고자 NSLocalizedString 사용)
// MARK: - Enum 반복 정의
enum RepeatCycle: String, CaseIterable, Codable {
case none
case daily
case weekly
case monthly
case yearly
var title: String {
switch self {
case .none:
return NSLocalizedString("repeat_none", comment: "No repeat option")
case .daily:
return NSLocalizedString("repeat_daily", comment: "Daily repeat option")
case .weekly:
return NSLocalizedString("repeat_weekly", comment: "Weekly repeat option")
case .monthly:
return NSLocalizedString("repeat_monthly", comment: "Monthly repeat option")
case .yearly:
return NSLocalizedString("repeat_yearly", comment: "Yearly repeat option")
}
}
}
2. 반복 주기 선택 UI
AddTransactionViewController에서 반복 주기 섹션을 탭하면 UIAlertController를 띄워서 선택합니다.
private func presentRepeatPicker() {
let alert = UIAlertController(
title: NSLocalizedString("repeat_picker_title", comment: "Title for repeat cycle picker"),
message: nil,
preferredStyle: .actionSheet
)
RepeatCycle.allCases.forEach { cycle in
let action = UIAlertAction(title: cycle.title, style: .default) { [weak self] _ in
self?.transactionViewModel.transaction?.isRepeated = cycle != .none
self?.transactionViewModel.transaction?.repeatCycle = cycle
self?.addTableView.reloadRows(
at: [IndexPath(row: 0, section: AddSection.repeatType.rawValue)],
with: .none
)
}
alert.addAction(action)
}
alert.addAction(UIAlertAction(
title: NSLocalizedString("cancel", comment: "Cancel button"),
style: .cancel,
handler: nil
))
present(alert, animated: true)
}
3. 반복 거래 자동 생성 로직
앱 실행 시 또는 거래 저장/수정 시, 반복 거래 여부(isRepeated)가 true인 항목만 검사하여 새로운 거래를 생성합니다.
전체 실행 메서드
func checkAndGenerateRepeatedTransactionsIfNeeded() {
let today = Date()
let repeatedItems = fetchRepeatableTransactions()
for base in repeatedItems {
generateUpcomingTransactionsIfNeeded(for: base, until: today)
}
}
1) 반복 가능한 거래만 필터링
private func fetchRepeatableTransactions() -> [ExpenseModel] {
return transactions.filter { $0.isRepeated == true && $0.repeatCycle != nil }
}
2) 반복 거래 생성 로직
private func generateUpcomingTransactionsIfNeeded(for base: ExpenseModel, until targetDate: Date) {
guard let cycle = base.repeatCycle else { return }
var nextDate = findNextDate(for: base, cycle: cycle)
while nextDate <= targetDate {
if shouldGenerateTransaction(for: base, on: nextDate) {
createRepeatedTransaction(from: base, on: nextDate)
}
nextDate = nextCycleDate(from: nextDate, cycle: cycle)
}
}
- 역할: 특정 반복 거래를 기준으로 다음 날짜에 해당하는 거래를 생성해야 하는지 판단하고, 필요 시 생성.
- 입력값: 반복 거래 객체 1개.
- 출력값: 없음 (필요 시 Core Data에 새 거래 생성).
- 구현 포인트:
- 먼저 findNextDate()로 다음 발생 날짜 계산.
- shouldGenerateTransaction()로 현재 날짜와 비교해 생성 여부 결정.
- 조건이 맞으면 createRepeatedTransaction() 호출.
- 예시: 8월 12일이 오늘이고, 거래 주기가 매주라면, 8월 19일이 다음 날짜 → 오늘이 8월 19일이면 생성.
3) 최신 날짜 계산
private func findNextDate(for base: ExpenseModel, cycle: RepeatCycle) -> Date {
let latest = transactions
// 1️⃣ base와 동일한 반복 항목들을 찾는 과정이에요.
.filter {
$0.category == base.category &&
$0.amount == base.amount &&
$0.transaction == base.transaction
}
// 2️⃣ 필터된 항목에서 날짜(Date) 값만 꺼냅니다.
.map { $0.date }
// 3️⃣ 가장 최신 날짜를 구합니다. 만약 해당 항목이 한 번도 기록된 적 없다면
// → base.date(반복 항목이 처음 생성된 날짜)를 사용합니다.
.max() ?? base.date
return nextCycleDate(from: latest, cycle: cycle)
}
- 역할: 해당 거래의 반복 주기에 맞춰 다음 발생 날짜를 계산.
- 입력값: 거래 객체.
- 출력값: Date? (다음 반복 날짜, 없으면 nil)
- 구현 포인트:
- repeatCycle이 .daily, .weekly, .monthly, .yearly에 따라 Calendar.date(byAdding:...)로 계산.
- 예를 들어 .weekly면 byAdding: .weekOfYear, value: 1.
- 종료일(repeatEndDate)이 있으면 그 날짜 넘어가면 nil 반환.
4) 생성 여부 판단
private func shouldGenerateTransaction(for base: ExpenseModel, on date: Date) -> Bool {
return !transactions.contains {
$0.date == date &&
$0.amount == base.amount &&
$0.category == base.category &&
!$0.isRepeated
}
}
- 역할: 계산된 다음 날짜가 오늘 또는 이미 지난 날짜인지 확인.
- 입력값: 다음 반복 날짜.
- 출력값: Bool (생성 여부)
- 구현 포인트:
- Calendar.current.isDateInToday(date) 또는 date <= today면 true.
- 이미 같은 날짜에 생성된 거래가 있는 경우 false.
- 매일 체크하더라도 중복 생성 방지 로직 필요.
5) 새로운 반복 거래 생성
private func createRepeatedTransaction(from base: ExpenseModel, on date: Date) {
let new = ExpenseModel(
id: UUID(),
transaction: base.transaction,
category: base.category,
amount: base.amount,
image: nil, // 사진은 복사하지 않음
date: date,
memo: base.memo, // 메모 유지
isRepeated: false,
repeatCycle: nil
)
transactionManager.createTransaction(new)
}
- 역할: 기존 거래를 기반으로 새로운 거래 객체를 생성하고 Core Data에 저장.
- 입력값: 원본 거래, 생성할 날짜.
- 출력값: 없음 (Core Data 저장).
- 구현 포인트:
- 금액, 카테고리, 결제 수단, isRepeated 값은 그대로 복사.
- 메모, 첨부사진은 복사하지 않음 (새 항목이니까).
- date는 지정된 on date로 설정.
- repeatCycle, repeatEndDate 등 반복 속성은 그대로 유지.
6) 다음 날짜 계산
private func nextCycleDate(from date: Date, cycle: RepeatCycle) -> Date {
var component: Calendar.Component
switch cycle {
case .daily: component = .day
case .weekly: component = .weekOfYear
case .monthly: component = .month
default: component = .day
}
return Calendar.current.date(byAdding: component, value: 1, to: date) ?? date
}
✅ 실행
HomeViewController 클래스의 viewWillAppear() 메서드 내에 실행
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
transactionViewModel.readAllTransactions()
transactionViewModel.setAllTransactions()
transactionViewModel.checkAndGenerateRepeatedTransactionsIfNeeded()
bindViewModel()
configureNavigation()
}
💡 정리
이번 구현을 통해:
- UI에서 반복 주기 선택
- 데이터 모델에 반복 여부/주기 저장
- 앱 실행 및 데이터 변경 시 반복 거래 자동 생성
- 이미 생성된 거래는 중복 생성 방지
- 사진 제외, 나머지 필드 그대로 복사 가능
728x90
LIST