Skip to content

Latest commit

Β 

History

History
445 lines (366 loc) Β· 14.2 KB

File metadata and controls

445 lines (366 loc) Β· 14.2 KB

PrayAnswer 개발 λ‘œλ“œλ§΅

μž‘μ„±μΌ: 2025-01-25 브랜치: feature/ux-improvements

κ°œμš”

PrayAnswer μ•±μ˜ μΆ”κ°€ κΈ°λŠ₯ 개발 κ³„νšμ„œμž…λ‹ˆλ‹€.

개발 μ˜ˆμ • κΈ°λŠ₯

  1. Phase 1: μ•ŒλžŒ μ„ΈλΆ€μ„€μ • (λ‚ μ§œ, μ‹œκ°„, 반볡 횟수 μ»€μŠ€ν„°λ§ˆμ΄μ§•)
  2. Phase 2: D-Day μΊ˜λ¦°λ” 연동 (iOS μΊ˜λ¦°λ” 앱에 이벀트 μΆ”κ°€)
  3. Phase 3: AI μŒμ„± μš”μ•½ (Apple Foundation Models ν™œμš©) - iOS 26+ ν•„μš”

ν˜„μž¬ μƒνƒœ 뢄석

κΈ°μ‘΄ κ΅¬ν˜„ ν˜„ν™©

κΈ°λŠ₯ μƒνƒœ κ΅¬ν˜„ 파일 μ„€λͺ…
D-Day μ•Œλ¦Ό βœ… μ™„λ£Œ NotificationManager.swift D-7, D-3, D-1, D-Day κ³ μ • μ•Œλ¦Ό (μ˜€μ „ 9μ‹œ)
D-Day 좔적 βœ… μ™„λ£Œ Prayer.swift targetDate 속성, μ•± λ‚΄λΆ€ μ €μž₯만
μŒμ„± λ…ΉμŒ βœ… μ™„λ£Œ SpeechRecognitionManager.swift μ‹€μ‹œκ°„ Speech-to-Text (ν•œκ΅­μ–΄)
μœ„μ ― βœ… μ™„λ£Œ WidgetDataManager.swift 즐겨찾기 기도 ν‘œμ‹œ

기술 μŠ€νƒ

  • UI: SwiftUI
  • 데이터: SwiftData
  • μ•Œλ¦Ό: UserNotifications
  • μŒμ„±: Speech Framework, AVFoundation
  • μœ„μ ―: WidgetKit + App Groups

Phase 1: μ•ŒλžŒ μ„ΈλΆ€μ„€μ • κΈ°λŠ₯

λͺ©ν‘œ

μ‚¬μš©μžκ°€ μ•Œλ¦Ό λ‚ μ§œ, μ‹œκ°„, 반볡 횟수λ₯Ό 자유둭게 μ„€μ •ν•  수 μžˆλ„λ‘ κ°œμ„ 

ν˜„μž¬ ν•œκ³„

  • μ•Œλ¦Ό 일정 κ³ μ •: D-7, D-3, D-1, D-Day
  • μ•Œλ¦Ό μ‹œκ°„ κ³ μ •: μ˜€μ „ 9μ‹œ
  • 반볡 μ•Œλ¦Ό 미지원

κ΅¬ν˜„ ν•­λͺ©

1.1 NotificationSettings λͺ¨λΈ μΆ”κ°€

// μƒˆ 파일: Models/NotificationSettings.swift
struct NotificationSettings: Codable {
    var isEnabled: Bool = false
    var notificationTime: Date = Calendar.current.date(from: DateComponents(hour: 9, minute: 0))!
    var reminderDays: [Int] = [7, 3, 1, 0]  // D-n일 λͺ©λ‘
    var repeatType: RepeatType = .none
    var repeatCount: Int? = nil

    enum RepeatType: String, Codable, CaseIterable {
        case none = "μ—†μŒ"
        case daily = "맀일"
        case weekly = "λ§€μ£Ό"
        case custom = "μ‚¬μš©μž μ§€μ •"
    }
}

1.2 Prayer λͺ¨λΈ ν™•μž₯

// Prayer.swift에 μΆ”κ°€
var notificationSettingsData: Data?  // NotificationSettings JSON μ €μž₯

var notificationSettings: NotificationSettings {
    get {
        guard let data = notificationSettingsData else { return NotificationSettings() }
        return (try? JSONDecoder().decode(NotificationSettings.self, from: data)) ?? NotificationSettings()
    }
    set {
        notificationSettingsData = try? JSONEncoder().encode(newValue)
    }
}

1.3 NotificationSettingsView ν™”λ©΄ 생성

// μƒˆ 파일: Views/NotificationSettingsView.swift
struct NotificationSettingsView: View {
    @Binding var settings: NotificationSettings

    var body: some View {
        Form {
            // μ•Œλ¦Ό ν™œμ„±ν™” ν† κΈ€
            Section("μ•Œλ¦Ό μ„€μ •") {
                Toggle("μ•Œλ¦Ό λ°›κΈ°", isOn: $settings.isEnabled)
            }

            // μ•Œλ¦Ό μ‹œκ°„ 선택
            Section("μ•Œλ¦Ό μ‹œκ°„") {
                DatePicker("μ‹œκ°„", selection: $settings.notificationTime, displayedComponents: .hourAndMinute)
            }

            // D-n일 선택 (λ©€ν‹° 선택)
            Section("μ•Œλ¦Ό 일정") {
                // D-30 ~ D-Day μ²΄ν¬λ°•μŠ€
            }

            // 반볡 μ„€μ •
            Section("반볡") {
                Picker("반볡 μœ ν˜•", selection: $settings.repeatType) { ... }
            }
        }
    }
}

1.4 NotificationManager ν™•μž₯

// NotificationManager.swift μˆ˜μ •
func scheduleCustomNotifications(for prayer: Prayer) {
    let settings = prayer.notificationSettings
    guard settings.isEnabled, let targetDate = prayer.targetDate else { return }

    cancelNotifications(for: prayer)

    for daysBefore in settings.reminderDays {
        // μ»€μŠ€ν…€ μ‹œκ°„μœΌλ‘œ μ•Œλ¦Ό 생성
        scheduleNotification(
            for: prayer,
            daysBefore: daysBefore,
            time: settings.notificationTime
        )
    }

    // 반볡 μ•Œλ¦Ό 처리
    if settings.repeatType != .none {
        scheduleRepeatingNotifications(for: prayer, settings: settings)
    }
}

1.5 UI 톡합

  • AddPrayerView: μ•Œλ¦Ό μ„€μ • μ„Ήμ…˜μ— "μ„ΈλΆ€ μ„€μ •" λ²„νŠΌ μΆ”κ°€
  • PrayerDetailView: μ•Œλ¦Ό μ„€μ • νŽΈμ§‘ κΈ°λŠ₯ μΆ”κ°€

파일 λ³€κ²½ λͺ©λ‘

파일 μž‘μ—…
Models/NotificationSettings.swift πŸ†• μƒˆλ‘œ 생성
Models/Prayer.swift πŸ“ notificationSettings 속성 μΆ”κ°€
Views/NotificationSettingsView.swift πŸ†• μƒˆλ‘œ 생성
Utils/NotificationManager.swift πŸ“ μ»€μŠ€ν…€ μŠ€μΌ€μ€„λ§ 둜직 μΆ”κ°€
Views/AddPrayerView.swift πŸ“ μ„ΈλΆ€ μ„€μ • λ²„νŠΌ μΆ”κ°€
Views/PrayerDetailView.swift πŸ“ μ•Œλ¦Ό μ„€μ • νŽΈμ§‘ UI μΆ”κ°€
Utils/LocalizationKeys.swift πŸ“ μƒˆ λ‘œμ»¬λΌμ΄μ œμ΄μ…˜ ν‚€ μΆ”κ°€

Phase 2: D-Day μΊ˜λ¦°λ” 연동

λͺ©ν‘œ

D-Dayλ₯Ό iOS μΊ˜λ¦°λ” 앱에 이벀트둜 μΆ”κ°€ν•˜μ—¬ μ‹œμŠ€ν…œ μΊ˜λ¦°λ”μ™€ 톡합

ν•„μš” ν”„λ ˆμž„μ›Œν¬

  • EventKit: μΊ˜λ¦°λ” μ ‘κ·Ό 및 이벀트 생성
  • EventKitUI (선택): λ„€μ΄ν‹°λΈŒ 이벀트 νŽΈμ§‘ UI

κ΅¬ν˜„ ν•­λͺ©

2.1 CalendarManager μœ ν‹Έλ¦¬ν‹° 생성

// μƒˆ 파일: Utils/CalendarManager.swift
import EventKit

final class CalendarManager {
    static let shared = CalendarManager()
    private let eventStore = EKEventStore()

    // MARK: - Permission
    func requestAccess() async -> Bool {
        do {
            return try await eventStore.requestFullAccessToEvents()
        } catch {
            return false
        }
    }

    // MARK: - Calendar Operations
    func availableCalendars() -> [EKCalendar] {
        return eventStore.calendars(for: .event)
    }

    // MARK: - Event Operations
    func addEvent(for prayer: Prayer, to calendar: EKCalendar) throws -> String {
        let event = EKEvent(eventStore: eventStore)
        event.title = "πŸ™ \(prayer.title)"
        event.notes = prayer.content
        event.startDate = prayer.targetDate
        event.endDate = prayer.targetDate
        event.isAllDay = true
        event.calendar = calendar

        // μ•Œλ¦Ό μΆ”κ°€ (D-1, D-Day)
        event.addAlarm(EKAlarm(relativeOffset: -86400)) // 1일 μ „
        event.addAlarm(EKAlarm(relativeOffset: 0))       // 당일

        try eventStore.save(event, span: .thisEvent)
        return event.eventIdentifier
    }

    func removeEvent(identifier: String) throws { ... }
    func updateEvent(identifier: String, with prayer: Prayer) throws { ... }
}

2.2 Prayer λͺ¨λΈ ν™•μž₯

// Prayer.swift에 μΆ”κ°€
var calendarEventIdentifier: String?  // μΊ˜λ¦°λ” 이벀트 ID μ €μž₯
var isAddedToCalendar: Bool { calendarEventIdentifier != nil }

2.3 Info.plist κΆŒν•œ μΆ”κ°€

<key>NSCalendarsFullAccessUsageDescription</key>
<string>D-Dayλ₯Ό μΊ˜λ¦°λ”μ— μΆ”κ°€ν•˜κΈ° μœ„ν•΄ μΊ˜λ¦°λ” μ ‘κ·Ό κΆŒν•œμ΄ ν•„μš”ν•©λ‹ˆλ‹€.</string>

2.4 CalendarPickerView 생성

// μƒˆ 파일: Views/Components/CalendarPickerView.swift
struct CalendarPickerView: View {
    @State private var calendars: [EKCalendar] = []
    @State private var selectedCalendar: EKCalendar?
    var onSelect: (EKCalendar) -> Void

    var body: some View {
        List(calendars, id: \.calendarIdentifier) { calendar in
            Button {
                onSelect(calendar)
            } label: {
                HStack {
                    Circle().fill(Color(cgColor: calendar.cgColor)).frame(width: 12)
                    Text(calendar.title)
                }
            }
        }
    }
}

2.5 PrayerDetailView 톡합

// PrayerDetailView.swift에 μΆ”κ°€
Button {
    showCalendarPicker = true
} label: {
    Label(
        prayer.isAddedToCalendar ? "μΊ˜λ¦°λ”μ—μ„œ 보기" : "μΊ˜λ¦°λ”μ— μΆ”κ°€",
        systemImage: "calendar.badge.plus"
    )
}
.sheet(isPresented: $showCalendarPicker) {
    CalendarPickerView { calendar in
        addToCalendar(calendar: calendar)
    }
}

파일 λ³€κ²½ λͺ©λ‘

파일 μž‘μ—…
Utils/CalendarManager.swift πŸ†• μƒˆλ‘œ 생성
Models/Prayer.swift πŸ“ calendarEventIdentifier μΆ”κ°€
Views/Components/CalendarPickerView.swift πŸ†• μƒˆλ‘œ 생성
Views/PrayerDetailView.swift πŸ“ μΊ˜λ¦°λ” μΆ”κ°€ λ²„νŠΌ
Info.plist πŸ“ μΊ˜λ¦°λ” κΆŒν•œ μ„€λͺ… μΆ”κ°€
Utils/LocalizationKeys.swift πŸ“ μƒˆ λ‘œμ»¬λΌμ΄μ œμ΄μ…˜ ν‚€

Phase 3: AI μŒμ„± μš”μ•½ (Apple Foundation Models)

⚠️ μ€‘μš” μš”κ΅¬μ‚¬ν•­

πŸ”΄ iOS 26.0+ ν•„μš” (2025λ…„ 가을 μΆœμ‹œ μ˜ˆμ •)
πŸ”΄ Apple Intelligence 지원 κΈ°κΈ°: A17 Pro, M1 이상
πŸ”΄ ν˜„μž¬(2025λ…„ 1μ›”) 개발 λΆˆκ°€ - iOS 26 베타 μΆœμ‹œ ν›„ μ§„ν–‰

λͺ©ν‘œ

μŒμ„±μœΌλ‘œ λ…ΉμŒν•œ λ‚΄μš©μ„ AIκ°€ 기도문 ν˜•μ‹μœΌλ‘œ μžλ™ 정리

기술 μŠ€νƒ

  • Foundation Models Framework: μ˜¨λ””λ°”μ΄μŠ€ LLM
  • LanguageModelSession: ν…μŠ€νŠΈ 생성 API

κ΅¬ν˜„ ν•­λͺ© (iOS 26 μΆœμ‹œ ν›„)

3.1 AISummarizationManager 생성

// μƒˆ 파일: Utils/AISummarizationManager.swift
import FoundationModels

@available(iOS 26.0, *)
final class AISummarizationManager {
    static let shared = AISummarizationManager()

    private let instructions = """
    λ‹€μŒ μŒμ„± λ…ΉμŒ λ‚΄μš©μ„ κΈ°λ„λ¬ΈμœΌλ‘œ μ •λ¦¬ν•΄μ£Όμ„Έμš”:
    1. 핡심 기도 λ‚΄μš©μ„ μΆ”μΆœν•©λ‹ˆλ‹€
    2. λΆˆν•„μš”ν•œ 말(μ–΄, 음, κ·Έ...)을 μ œκ±°ν•©λ‹ˆλ‹€
    3. λ¬Έμž₯을 μžμ—°μŠ€λŸ½κ²Œ λ‹€λ“¬μŠ΅λ‹ˆλ‹€
    4. 기도문 ν˜•μ‹μœΌλ‘œ κ΅¬μ„±ν•©λ‹ˆλ‹€ (감사/간ꡬ/결심)
    """

    var isAvailable: Bool {
        SystemLanguageModel.default.availability == .available
    }

    func summarize(text: String) async throws -> String {
        let session = LanguageModelSession(instructions: instructions)
        let response = try await session.respond(to: text)
        return response.content
    }
}

3.2 VoiceRecordingOverlay ν™•μž₯

// AddPrayerView.swift의 VoiceRecordingOverlay μˆ˜μ •
if #available(iOS 26.0, *), AISummarizationManager.shared.isAvailable {
    Button("AI둜 μ •λ¦¬ν•˜κΈ°") {
        Task {
            isProcessing = true
            let summarized = try await AISummarizationManager.shared.summarize(text: recognizedText)
            showSummaryPreview = true
            summarizedText = summarized
            isProcessing = false
        }
    }
}

3.3 μš”μ•½ κ²°κ³Ό 미리보기 UI

// μƒˆ 파일: Views/Components/AISummaryPreviewView.swift
struct AISummaryPreviewView: View {
    let originalText: String
    @Binding var summarizedText: String
    var onApply: () -> Void
    var onCancel: () -> Void

    var body: some View {
        VStack {
            Text("AI 정리 κ²°κ³Ό").font(.headline)

            // 원본 vs μš”μ•½ 비ꡐ
            HStack {
                VStack { Text("원본"); Text(originalText) }
                VStack { Text("정리됨"); TextEditor(text: $summarizedText) }
            }

            HStack {
                Button("μ·¨μ†Œ", action: onCancel)
                Button("적용", action: onApply)
            }
        }
    }
}

iOS 26 미만 λŒ€μ‘

// κΈ°λŠ₯ λΉ„ν™œμ„±ν™” λ˜λŠ” μ•ˆλ‚΄ λ©”μ‹œμ§€ ν‘œμ‹œ
if #unavailable(iOS 26.0) {
    Text("AI 정리 κΈ°λŠ₯은 iOS 26 μ΄μƒμ—μ„œ μ‚¬μš© κ°€λŠ₯ν•©λ‹ˆλ‹€")
        .foregroundColor(.secondary)
}

파일 λ³€κ²½ λͺ©λ‘ (iOS 26 μΆœμ‹œ ν›„)

파일 μž‘μ—…
Utils/AISummarizationManager.swift πŸ†• μƒˆλ‘œ 생성
Views/Components/AISummaryPreviewView.swift πŸ†• μƒˆλ‘œ 생성
Views/AddPrayerView.swift πŸ“ AI 정리 λ²„νŠΌ μΆ”κ°€
Utils/LocalizationKeys.swift πŸ“ μƒˆ λ‘œμ»¬λΌμ΄μ œμ΄μ…˜ ν‚€

개발 일정

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Phase 1: μ•ŒλžŒ μ„ΈλΆ€μ„€μ •                                         β”‚
β”‚  β”œβ”€ μ˜ˆμƒ μ†Œμš”: 2-3일                                           β”‚
β”‚  β”œβ”€ λ‚œμ΄λ„: β­β­β˜†β˜†β˜†                                           β”‚
β”‚  └─ μƒνƒœ: βœ… μ™„λ£Œ (2025-01-25)                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Phase 2: μΊ˜λ¦°λ” 연동                                          β”‚
β”‚  β”œβ”€ μ˜ˆμƒ μ†Œμš”: 2-3일                                           β”‚
β”‚  β”œβ”€ λ‚œμ΄λ„: β­β­β­β˜†β˜†                                          β”‚
β”‚  └─ μƒνƒœ: βœ… μ™„λ£Œ (2025-01-25)                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Phase 3: AI μŒμ„± μš”μ•½                                         β”‚
β”‚  β”œβ”€ μ˜ˆμƒ μ†Œμš”: 3-4일                                           β”‚
β”‚  β”œβ”€ λ‚œμ΄λ„: β­β­β­β­β˜†                                         β”‚
β”‚  └─ μƒνƒœ: βœ… μ™„λ£Œ (2025-01-26) - iOS 26+ 쑰건뢀 컴파일 적용    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

ν…ŒμŠ€νŠΈ 체크리슀트

Phase 1 ν…ŒμŠ€νŠΈ

  • μ•Œλ¦Ό μ‹œκ°„ λ³€κ²½ ν›„ 정상 λ°œμ†‘ 확인
  • μ»€μŠ€ν…€ D-n일 μ„€μ • λ™μž‘ 확인
  • 반볡 μ•Œλ¦Ό 정상 λ™μž‘ 확인
  • κΈ°μ‘΄ μ•Œλ¦Ό μ„€μ •κ³Όμ˜ ν˜Έν™˜μ„±

Phase 2 ν…ŒμŠ€νŠΈ

  • μΊ˜λ¦°λ” κΆŒν•œ μš”μ²­ 정상 λ™μž‘
  • 이벀트 생성/μˆ˜μ •/μ‚­μ œ 확인
  • μ—¬λŸ¬ μΊ˜λ¦°λ”μ— μΆ”κ°€ ν…ŒμŠ€νŠΈ
  • 기도 μ‚­μ œ μ‹œ μΊ˜λ¦°λ” 이벀트 μ‚­μ œ

Phase 3 ν…ŒμŠ€νŠΈ

  • AI λͺ¨λΈ κ°€μš©μ„± 체크
  • μš”μ•½ ν’ˆμ§ˆ 검증 (ν•œκ΅­μ–΄)
  • iOS 26 미만 κΈ°κΈ° λŒ€μ‘
  • μ˜€ν”„λΌμΈ μƒνƒœ 처리

μ°Έκ³  λ¬Έμ„œ


Made with Claude Code