본문 바로가기
카테고리 없음

📌 iOS 가계부 앱 – 반복 거래(Repeat Transaction) 기능 구현

by 밤새는 탐험가89 2025. 8. 11.
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