카테고리 없음

📌 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()
}

💡 정리

이번 구현을 통해:

  1. UI에서 반복 주기 선택
  2. 데이터 모델에 반복 여부/주기 저장
  3. 앱 실행 및 데이터 변경 시 반복 거래 자동 생성
  4. 이미 생성된 거래는 중복 생성 방지
  5. 사진 제외, 나머지 필드 그대로 복사 가능
728x90
LIST