grep

iOS

๐Ÿ—“๏ธ์—ฌ๊ธฐ์–ด๋•Œ Dateยท์ธ์› ์„ ํƒ UI ๋งŒ๋“ค๊ธฐ-SwiftUI Calendar Core & DateTravelerSelector ๊ตฌ์กฐ ์žฌ์„ค๊ณ„

๋ ˆ์ด๋„ˆRaynor(์ตœ์„ฑ์šฑ) / iOS ๊ฐœ๋ฐœํŒ€์—ฌ๊ธฐ์–ด๋•Œ

2025๋…„ 12์›” 17์ผ

์›๋ฌธ์—์„œ ๋ณด๊ธฐ โ†—

์•ˆ๋…•ํ•˜์„ธ์š”. ์—ฌ๊ธฐ์–ด๋•Œ์ปดํผ๋‹ˆ iOS ๊ฐœ๋ฐœํŒ€ ๋ ˆ์ด๋„ˆ์ž…๋‹ˆ๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์‹ค์ œ ์„œ๋น„์Šค์—์„œ ๊ฐ€์žฅ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” UI ์ค‘ ํ•˜๋‚˜์ธ

โ€œ๋‚ ์งœ + ์ธ์› ์„ ํƒ(DateTravelerSelector)โ€ ํ™”๋ฉด์„ ์–ด๋–ป๊ฒŒ SwiftUI ๊ธฐ๋ฐ˜์œผ๋กœ ์žฌ์„ค๊ณ„ํ–ˆ๋Š”์ง€ ๊ณต์œ ํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์–ด๋•Œ ์•ฑ์—๋Š” ํŒจํ‚ค์ง€ยท๊ตญ๋‚ด์ˆ™์†Œยทํ•ด์™ธ์ˆ™์†Œยท์ฐœ๋ชฉ๋ก ๋“ฑ ๋‹ค์–‘ํ•œ ๋„๋ฉ”์ธ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

๊ฒ‰์œผ๋กœ ๋ณด๊ธฐ์—” ๋น„์Šทํ•œ UI์ง€๋งŒ, ์ •์ฑ…์ด ์ „๋ถ€ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ์™„์ „ํžˆ ๊ณตํ†ตํ™”ํ•˜๊ธฐ ์–ด๋ ค์šด ์˜์—ญ์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด:

์ด์ฒ˜๋Ÿผ ํ™”๋ฉด ํ˜•ํƒœ๋Š” ๋น„์Šทํ•ด ๋ณด์—ฌ๋„ ๋กœ์ง์€ ๋ชจ๋‘ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์—, ๋‹จ์ˆœํžˆ ํ•˜๋‚˜์˜ ํ†ตํ•ฉ ๋‹ฌ๋ ฅ์œผ๋กœ ๋งŒ๋“ค๊ธฐ์—๋Š” ๋ฌด๋ฆฌ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๐ŸŽฏ ํ•ด๊ฒฐํ•ด์•ผ ํ–ˆ๋˜ ๋ฌธ์ œ๋“ค

SwiftUI๋กœ Dateยท์ธ์› ์„ ํƒ UI๋ฅผ ์žฌ์„ค๊ณ„ํ•˜๋ฉด์„œ, ๊ฒ‰์œผ๋กœ ๋ณด๊ธฐ์—๋Š” ๋‹จ์ˆœํ•œ ํ™”๋ฉด ๊ฐ™์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” ๋„๋ฉ”์ธ๋ณ„๋กœ ์ •์ฑ…๋„ ๋‹ค๋ฅด๊ณ  ๊ธฐ์ˆ ์ ์œผ๋กœ๋„ ํ•ด๊ฒฐํ•ด์•ผ ํ•  ๋‚œ๊ด€์ด ๋งŽ์•˜์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ์•„๋ž˜์™€ ๊ฐ™์€ ๋ถ€๋ถ„๋“ค์€ ์‹ค์ œ ๊ฐœ๋ฐœ ๊ณผ์ •์—์„œ ๊นŠ์€ ๊ณ ๋ฏผ์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

1. ํ•˜๋‚˜๋กœ ํ†ต์ผํ•˜๋ ค ํ–ˆ์ง€๋งŒโ€ฆ ๋„๋ฉ”์ธ๋ณ„ ์ •์ฑ… ์ฐจ์ด๊ฐ€ ๋„ˆ๋ฌด ์ปธ๋˜ ๋ฌธ์ œ

์ฒ˜์Œ์—๋Š” ๋ชจ๋“  ๋„๋ฉ”์ธ์„ ํ•˜๋‚˜์˜ โ€œํ†ตํ•ฉ Dateยท์ธ์› ์„ ํƒ ํ™”๋ฉดโ€์œผ๋กœ ๋ฌถ์–ด๋ณด๋ ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ ํŒจํ‚ค์ง€ยท๊ตญ๋‚ด์ˆ™์†Œยทํ•ด์™ธ์ˆ™์†Œยท์ฐœ ํ™”๋ฉด์€ UI์™€ ์ •์ฑ…์ด ๋ชจ๋‘ ๋‹ฌ๋ž์Šต๋‹ˆ๋‹ค.

์ด ๋ชจ๋“  ์ •์ฑ…์„ ํ•˜๋‚˜์˜ ๋‹ฌ๋ ฅ์—์„œ ๋ถ„๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด

๋กœ์ง์ด ๋„ˆ๋ฌด ๋ณต์žกํ•ด์ง€๊ณ  ์œ ์ง€๋ณด์ˆ˜ ๋‚œ์ด๋„๊ฐ€ ํฌ๊ฒŒ ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ‘‰ ๊ฒฐ๋ก : ์ „์ฒด ํ™”๋ฉด(DateTravelerSelector)์€ ๋„๋ฉ”์ธ๋ณ„ ์ •์ฑ… ์ฐจ์ด ๋•Œ๋ฌธ์— ๋ถ„๋ฆฌํ•˜๋˜ ๋‹ฌ๋ ฅ ์…€ยท์Šคํƒ€์ผ ๋“ฑ UI ์ปดํฌ๋„ŒํŠธ๋Š” YDS ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ณตํ†ตํ™”ํ•˜ ๊ณ , ๊ทธ ์œ„์—์„œ ๋™์ž‘ํ•˜๋Š” ๋‚ ์งœ ๊ณ„์‚ฐ Core(์›” ์ƒ์„ฑยท์…€ ์ƒ์„ฑยทRange ๊ณ„์‚ฐ ๋“ฑ ๋กœ์ง)๋งŒ ๋ณ„๋„๋กœ ์ถ”์ƒํ™”ํ•ด ๊ฐ ๋„๋ฉ”์ธ์—์„œ ์žฌ์‚ฌ์šฉํ•˜๋Š” ๊ตฌ์กฐ๋กœ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

2. Range ๋ชจ๋“œ ๋ฐ consecutiveDays ์ •์ฑ…์˜ ๋ณต์žก์„ฑ

๊ตญ๋‚ดยทํ•ด์™ธยท์ฐœ ํ™”๋ฉด์€ ๋ชจ๋‘ โ€œRange(์ฒดํฌ์ธ/์ฒดํฌ์•„์›ƒ)โ€์„ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์„ธ๋ถ€ ์ •์ฑ…์€ ๋‹ค๋ฅด๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

์ฆ‰, ๋‹จ์ˆœํ•œ range ๊ณ„์‚ฐ์ด ์•„๋‹ˆ๋ผ ๋„๋ฉ”์ธ๋ณ„ ์ •์ฑ…์ด ํ•จ๊ป˜ ์ž‘๋™ํ•˜๋Š” ๊ตฌ์กฐ ์—ฌ์„œ

Core์™€ Selector๊ฐ€ ๋ฐ˜๋“œ์‹œ ๋ถ„๋ฆฌ๋˜์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ‘‰ ๊ฒฐ๋ก : DateViewData ์ƒ์„ฑ ๋กœ์ง์„ Calendar Core๋กœ ๋ถ„๋ฆฌํ•˜๊ณ ,

๋„๋ฉ”์ธ ์ •์ฑ…์€ DateTravelerSelector์—์„œ๋งŒ ์ œ์–ดํ•˜๋Š” ์•„ํ‚คํ…์ฒ˜๊ฐ€ ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

3. LazyVGrid์˜ iOS 16 ๋ฒ„๊ทธ ๋ฐ ์„ฑ๋Šฅ ์ด์Šˆ๋กœ ์ธํ•œ Grid ๊ตฌ์กฐ ์žฌ์„ค๊ณ„

์ดˆ๊ธฐ ๋‹ฌ๋ ฅ ์‹œ๋„์—์„œ๋Š” LazyVGrid๋ฅผ ์‚ฌ์šฉํ–ˆ์ง€๋งŒ,

๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ ๋‹ฌ๋ ฅ์€ 180~365์ผ ๋ถ„๋Ÿ‰์„ ํ•œ ๋ฒˆ์— ๊ทธ๋ฆฌ๋Š” ๊ตฌ์กฐ๋ผ

LazyVGrid + GeometryReader ์กฐํ•ฉ์€ ์„ฑ๋Šฅ์ด ๋” ์•…ํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ‘‰ ์ตœ์ข… ์„ ํƒ์€ ์•„๋ž˜ ์„ธ ๊ฐ€์ง€ ์ „๋žต์ด์—ˆ์Šต๋‹ˆ๋‹ค.

โœ” ์ง์ ‘ Grid ๊ตฌ์„ฑ

โœ” GeometryReader ์ตœ์†Œํ™”

โœ” ์ดˆ๊ธฐ ๋ Œ๋”๋ง ์ตœ์ ํ™” โ€” Precompute & Cache Strategy

์ด ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝํ•œ ๋’ค iOS 16์—์„œ๋„ ์Šคํฌ๋กค ๋ฒ„๋ฒ…์ž„์ด ํฌ๊ฒŒ ๊ฐ์†Œํ–ˆ์Šต๋‹ˆ๋‹ค.

โญ Calendar Core ๊ตฌ์กฐ๋กœ ํ•ด๊ฒฐํ•œ ๋ฐฉ์‹

์•ž์„œ ๋งํ•œ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์šฐ๋ฆฌ๋Š” ์บ˜๋ฆฐ๋” ๊ธฐ๋Šฅ์„ ๋‹ค์Œ ๋‘ ์ถ•์œผ๋กœ ๋‚˜๋ˆ„์–ด ์žฌ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ๋‘ ๊ตฌ์กฐ๋ฅผ ํ†ตํ•ด ํŒจํ‚ค์ง€ยท๊ตญ๋‚ด์ˆ™์†Œยทํ•ด์™ธ์ˆ™์†Œยท์ฐœ ํ™”๋ฉด์ด ๋ชจ๋‘ ๊ฐ™์€ Calendar Core๋ฅผ ๊ณต์œ ํ•˜๋ฉด์„œ๋„, ๊ฐ ๋„๋ฉ”์ธ์˜ ์ •์ฑ…์€ ๋…๋ฆฝ์ ์œผ๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

1. Calendar Core: ์›” ๋‹จ์œ„๋กœ โ€œ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ๋œ ๋ Œ๋”๋ง ๋ฐ์ดํ„ฐโ€๋ฅผ ๋งŒ๋“œ๋Š” ๊ตฌ์กฐ

Calendar Core(DateTravelerSelectorUtils)๋Š” ๋‹ค์Œ ์—ญํ• ๋“ค์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

โ‘  Month ๋ฆฌ์ŠคํŠธ ๊ณ„์‚ฐ (generateMonths)

Calendar Core์—์„œ๋Š” ๋จผ์ € ์‹œ์ž‘์ผ ~ ์ข…๋ฃŒ์ผ ์‚ฌ์ด์— ํฌํ•จ๋œ ๋ชจ๋“  (YYYYMM) ์„ ๋งŒ๋“ค์–ด ๋‘ก๋‹ˆ๋‹ค.

์ด Month ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋‚˜์ค‘์— Dictionary<YearMonth, [DateViewData]> ํ˜•ํƒœ์˜ ์บ์‹œ ํ‚ค๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด:

๋ผ๋ฉด, Core์—์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ Month ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

[202501, 202502, 202503]

์‹ค์ œ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

typealias YearMonth = Int //์˜ˆ: 202501

static func generateMonths(from startDate: Date, to endDate: Date) -> [YearMonth] {
    var months: [YearMonth] = []
    var current = startDate
    let calendar = Calendar(identifier: .gregorian)
    // endDate๊ฐ€ ์†ํ•œ ๋‹ฌ๊นŒ์ง€ ํฌํ•จ๋˜๋„๋ก ๋งˆ์ง€๋ง‰ ๋‹ฌ์˜ ๋ง์ผ๋กœ ๋ณด์ •
    let endOfMonth = calendar.date(
        byAdding: DateComponents(month: 1, day: -1),
        to: endDate
    )!
    while current <= endOfMonth {
        let year = calendar.component(.year, from: current)
        let month = calendar.component(.month, from: current)
        let yearMonth = year * 100 + month   // 2025๋…„ 1์›” -> 202501
        if !months.contains(yearMonth) {
            months.append(yearMonth)
        }
        // ๋‹ค์Œ ๋‹ฌ๋กœ ์ด๋™
        current = calendar.date(byAdding: .month, value: 1, to: current)!
    }
    return months
}

์ด๋ ‡๊ฒŒ ํ•ด๋‘๋ฉด:

์ฒ˜๋Ÿผ ๋„๋ฉ”์ธ๋ณ„๋กœ ๊ธฐ๊ฐ„์ด ๋‹ฌ๋ผ๋„ , ์œ„ generateMonths๋งŒ ํ˜ธ์ถœํ•˜๋ฉด ํ•ญ์ƒ ๊ณตํ†ต๋œ Month ๋ฆฌ์ŠคํŠธ ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์–ด์„œ ๋’ค์—์„œ ์„ค๋ช…ํ•  initDateViewData / updateMonthDateViewData ๋กœ์ง์ด ๋ชจ๋‘ ๊ฐ™์€ ํ๋ฆ„์„ ํƒ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

โ‘ก ๊ฐ ์›”์˜ Grid ์ƒ์„ฑ (makeDateGridArray / makeCalendarGridItems)

Calendar Core์—์„œ๋Š” ํ•œ ๋‹ฌ(month)์„ ๊ฐ€๋กœ 7์—ด(์ผ~ํ† ), ์ตœ๋Œ€ 6ํ–‰(์ด 42์นธ) ์˜ ๊ณ ์ •๋œ Grid๋กœ ์ •๊ทœํ™”ํ•ด์„œ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

SwiftUI์—์„œ ์–ด๋–ค ๋ ˆ์ด์•„์›ƒ์„ ์‚ฌ์šฉํ•˜๋”๋ผ๋„, ํ•ญ์ƒ ์ด 42์นธ์งœ๋ฆฌ ๋ฐฐ์—ด๋งŒ ๋ฐ›์•„์„œ ๊ทธ๋ฆฌ๋ฉด ๋˜๋„๋ก ๋งŒ๋“  ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์•„๋ž˜์™€ ๊ฐ™์ด ์›” ์‹œ์ž‘ ์š”์ผ์— ๋”ฐ๋ผ ์•ž๋ถ€๋ถ„์€ nil๋กœ ์ฑ„์šฐ๊ณ ,

์‹ค์ œ ๋‚ ์งœ๋Š” 1 ~ 31๊นŒ์ง€ ์ˆซ์ž๋กœ ์ฑ„์›๋‹ˆ๋‹ค.

[ nil, nil, 1, 2, 3, 4, 5,
  6, 7, 8, 9, 10, 11, 12,
  ...]

์ด ๋ฐฐ์—ด์„ ๋งŒ๋“œ๋Š” ์ฝ”๋“œ์˜ ํ•ต์‹ฌ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

static func makeDateGridArray(for startOfMonth: Date) -> [Int?] {
    let calendar = Calendar(identifier: .gregorian)
    
    // ํ•ด๋‹น ๋‹ฌ์˜ 1์ผ์ด ๋ฌด์Šจ ์š”์ผ์ธ์ง€ (1 = ์ผ์š”์ผ)
    let startWeekday = calendar.component(.weekday, from: startOfMonth)
    
    // ์ด ๋‹ฌ์ด ๋ฉฐ์น ๊นŒ์ง€ ์žˆ๋Š”์ง€ (์˜ˆ: 1...31)
    let range = calendar.range(of: .day, in: .month, for: startOfMonth)!
    
    // ์›” ์‹œ์ž‘ ์š”์ผ ์•ž์ชฝ์„ nil ๋กœ ์ฑ„์›€
    let leadingEmptyCount = (startWeekday - 1) % 7
    var array: [Int?] = Array(repeating: nil, count: leadingEmptyCount)
    
    // ์‹ค์ œ ๋‚ ์งœ ์ฑ„์šฐ๊ธฐ
    array.append(contentsOf: range.map { Optional($0) })
    
    // ๋งˆ์ง€๋ง‰ ์ค„๋„ 7์˜ ๋ฐฐ์ˆ˜๊ฐ€ ๋˜๋„๋ก ๋’ค์ชฝ์„ nil ๋กœ ์ฑ„์›€
    let remaining = array.count % 7
    if remaining > 0 {
        array.append(contentsOf: Array(repeating: nil, count: 7 - remaining))
    }
    
    return array
}

์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด์ง„ [Int?] ๋ฐฐ์—ด์„ ๊ธฐ๋ฐ˜์œผ๋กœ,

makeCalendarGridItems์—์„œ ๊ฐ ์…€์„ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ๊ฐ์‹ธ์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

struct CalendarGridItem {
 let index: Int // 0 ~ 41
 let day: Int? // nil ์ด๋ฉด ๋นˆ ์นธ, ์•„๋‹ˆ๋ฉด ์‹ค์ œ ๋‚ ์งœ
 let dateViewData: DateViewData?
}

๊ฒฐ๊ณผ์ ์œผ๋กœ MonthView ์ž…์žฅ์—์„œ๋Š”:

๋ผ๋Š” ์ „์ œ๊ฐ€ ๊น”๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์—,

๋„๋ฉ”์ธ์— ์ƒ๊ด€์—†์ด ๊ฐ™์€ MonthView ์ปดํฌ๋„ŒํŠธ๋ฅผ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

โ‘ข ๋‚ ์งœ ์…€(DateViewData)์„ ๋งŒ๋“œ๋Š” ํ•ต์‹ฌ ๋กœ์ง โ€” buildDateViewData

โ‘ ์—์„œ Month ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค๊ณ , โ‘ก์—์„œ ๊ฐ ์›”์„ 7ร—6 ๊ทธ๋ฆฌ๋“œ๋กœ ์ •๋ฆฌํ–ˆ๋‹ค๋ฉด,

๋‹ค์Œ ๋‹จ๊ณ„๋Š” โ€œ๊ฐ ์นธ์ด ์–ด๋–ค ์ƒํƒœ์˜ ๋‚ ์งœ ์…€์ธ์ง€ โ€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ์ผ์ž…๋‹ˆ๋‹ค.

์ด ์—ญํ• ์„ ํ•˜๋Š” ํ•จ์ˆ˜๊ฐ€ buildDateViewData์ž…๋‹ˆ๋‹ค.

์ด ๋‹จ๊ณ„์—์„œ ์‹ค์ œ๋กœ ๋‹ฌ๋ ฅ UI์˜ ๊ฑฐ์˜ ๋ชจ๋“  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.

์‹ค์ œ ์„œ๋น„์Šค ์ฝ”๋“œ๋Š” ๋„๋ฉ”์ธ๋ณ„ ์ •์ฑ…(๊ตญ๋‚ด/ํ•ด์™ธ/์ฐœ, ๊ฐ€๊ฒฉ/์žฌ๊ณ , ์ตœ๋Œ€ ์—ฐ๋ฐ• ๋“ฑ)์ด ๋” ๋งŽ์ด ํฌํ•จ๋˜์–ด ์žˆ์ง€๋งŒ, ํ•ต์‹ฌ ํ๋ฆ„๋งŒ ์š”์•ฝํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค.

/// ๋‚ ์งœ ์…€(DateViewData)์„ ๋งŒ๋“œ๋Š” ํ•ต์‹ฌ ํ๋ฆ„ ์š”์•ฝ๋ณธ
private static func buildDateViewData(
    date: Date?,
    context: CalendarRenderContext,
    previous: DateViewData?
) -> DateViewData {

    // ๋นˆ ์นธ(๋‚ ์งœ ์—†์Œ) ์ฒ˜๋ฆฌ
    guard let date else { return handleEmptyDate(context, previous) }

    // ๊ธฐ๋ณธ ์ƒํƒœ ๊ณ„์‚ฐ
    let isSelected = context.isSelected(date)
    let isHoliday  = context.isHoliday(date)
    let priceInfo  = context.priceInfo(for: date)

    // ๋„๋ฉ”์ธ ์ •์ฑ…์„ ๋ฐ˜์˜ํ•œ ๋น„ํ™œ์„ฑํ™” ์—ฌ๋ถ€
    let isDisabled = context.isDisabled(date, priceInfo: priceInfo)

    // Range ๋ชจ๋“œ์—์„œ ์ฒดํฌ์ธ/์ฒดํฌ์•„์›ƒ edge ๊ณ„์‚ฐ
    let rangeType  = context.rangeType(for: date)

    // diff ๊ธฐ๋ฐ˜ ์—…๋ฐ์ดํŠธ (๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง ๋ฐฉ์ง€)
    return previous?.updated(
        isSelected: isSelected,
        isDisabled: isDisabled,
        rangeType: rangeType,
        priceText: priceInfo?.priceText
    ) ?? DateViewData(
        date: date,
        isSelected: isSelected,
        isHoliday: isHoliday,
        isDisabled: isDisabled,
        rangeType: rangeType,
        subText: priceInfo?.priceText ?? ""
    )
}

ํ•ต์‹ฌ ์•„์ด๋””์–ด๋Š” ๋ชจ๋“  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ CalendarRenderContext ๋‚ด๋ถ€๋กœ ์บก์Аํ™” ํ•˜๊ณ , buildDateViewData์—์„œ๋Š” โ€œ์ด ๋‚ ์งœ๋ฅผ ์–ด๋–ค ์ƒํƒœ๋กœ ํ‘œํ˜„ํ• ์ง€โ€๋งŒ ๊ฒฐ์ •ํ•˜๋„๋ก ๋งŒ๋“  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•ด๋‘๋ฉด SwiftUI View ์ชฝ์—์„œ๋Š” DateViewData ๋ฐฐ์—ด๋งŒ ๋ฐ›์•„์„œ:

๋งŒ ์‹ ๊ฒฝ ์“ฐ๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์—, ๋„๋ฉ”์ธ๋ณ„ ์ •์ฑ… ๋ณ€๊ฒฝ์ด ์ƒ๊ฒจ๋„ Core ๋กœ์ง์—์„œ๋งŒ ์ˆ˜์ •ํ•˜๊ณ  ๊ฐ ๋„๋ฉ”์ธ์˜ UI ์ฝ”๋“œ๋Š” ๊ทธ๋Œ€๋กœ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

โ‘ฃ ๋ณ€ํ™”๋œ ์ƒํƒœ๋งŒ ๊ฐฑ์‹ ํ•˜๋Š” ๊ตฌ์กฐ โ€” updateMonthDateViewData

๋‹ฌ๋ ฅ ํ™”๋ฉด์€ ๋‚ ์งœ ์„ ํƒยท๊ฐ€๊ฒฉยท๊ณตํœด์ผยท์žฌ๊ณ  ๋“ฑ์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์ƒํƒœ๊ฐ€ ๋ณ€ํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์›” ์ „์ฒด๋ฅผ ๋‹ค์‹œ ๊ณ„์‚ฐํ•˜๋ฉด ์„ฑ๋Šฅ์ด ๊ธ‰๊ฒฉํžˆ ์ €ํ•˜๋˜๋ฏ€๋กœ, โ€œ๋ณ€๊ฒฝ๋œ ์…€๋งŒ ๊ฐฑ์‹ ํ•˜๊ณ , ๋‚˜๋จธ์ง€๋Š” ์žฌ์‚ฌ์šฉํ•˜๋Š” ๊ตฌ์กฐโ€๋กœ ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ์•„์ด๋””์–ด๋Š” ๋”ฑ ๋‘ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.

(a) ์ด์ „ ์…€(DateViewData)์„ ๊ทธ๋Œ€๋กœ ๋„˜๊ฒจ์„œ ๋น„๊ต ํ›„ ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ์—…๋ฐ์ดํŠธ

let newData = calendarGridItems.map { cell in
    buildDateViewData(
        date: date,
        context: context,
        previous: cell.dateViewData   // โ† ์ด์ „ ๊ฐ’ ์ „๋‹ฌ
    )
}

buildDateViewData๋Š” ๋‚ด๋ถ€์—์„œ:

์ด๋ ‡๊ฒŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

(b) ๊ณ„์‚ฐ ๋น„์šฉ์ด ํฐ ์š”์†Œ๋Š” ํ•œ ๋ฒˆ๋งŒ ๋ฌถ์–ด์„œ Context๋กœ ์ „๋‹ฌ

let context = CalendarRenderContext(
    selectedDates: selectedDates ?? [],
    holidaysSet: holidaysSet,
    openDaysSet: openDaysSet,
    priceDataMap: priceDataMap,
    model: model
)

date๋งˆ๋‹ค:

๊ฐ™์€ ๊ณ„์‚ฐ์„ ๋งค๋ฒˆ ํ•˜๋ฉด ์„ฑ๋Šฅ์ด ๊ธ‰๊ฒฉํžˆ ๋–จ์–ด์ง‘๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ํ•„์š”ํ•œ ๊ฐ’์„ ํ•œ ๋ฒˆ์— ๋ฌถ์–ด ๋‘” CalendarRenderContext ๋ฅผ ๋งŒ๋“ค๊ณ , ๊ฐ ์…€์€ ์ด ๊ฐ’์„ ๋น ๋ฅด๊ฒŒ ์ฐธ์กฐํ•˜๋„๋ก ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

SwiftUI๋Š” ๋™์ผํ•œ Identifiable ์…€์€ ์žฌ๋ Œ๋”๋งํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ์ด ๊ตฌ์กฐ๋Š” ๋ณ€๊ฒฝ๋œ ์…€๋งŒ ์ตœ์†Œ๋กœ ๊ฐฑ์‹ ๋˜๋Š” ์‹ค์ œ ์„œ๋น„์Šค ์„ฑ๋Šฅ ์ตœ์ ํ™”์— ํฐ ํšจ๊ณผ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

2. ScrollableCalendarView: UI๋ฅผ ๋‹ด๋‹นํ•˜๋Š” SwiftUI ๋ ˆ์ด์–ด

ScrollableCalendarView๋Š” ์—ฌ๊ธฐ์–ด๋•Œ ์•ฑ์˜ ๋ชจ๋“  Dateยท์ธ์› ์„ ํƒ ํ™”๋ฉด์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” SwiftUI ์บ˜๋ฆฐ๋” UI ๋ ˆ์ด์–ด ์ž…๋‹ˆ๋‹ค.

์•ž์—์„œ ์„ค๋ช…ํ•œ Calendar Core๊ฐ€ ์›”๋ณ„ DateViewData๋ฅผ ๊ณ„์‚ฐํ•ด ๋„˜๊ฒจ์ฃผ๋ฉด, ์ด ๋ทฐ๋Š” ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ๊ทธ๋ฆฌ๊ธฐ๋งŒ ํ•ฉ๋‹ˆ๋‹ค.

์ฆ‰, UI๋Š” ๋ Œ๋”๋ง + ์ด๋ฒคํŠธ ์ „๋‹ฌ ,

์ •์ฑ…ยท๊ณ„์‚ฐยท๊ฒ€์ฆ์€ Core๊ฐ€ ๋‹ด๋‹นํ•˜๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

โ‘  ScrollableCalendarView

์ „์ฒด ๊ตฌํ˜„์€ ๊ธธ์ง€๋งŒ, ํ•ต์‹ฌ ๊ตฌ์กฐ๋งŒ ๋ณด๋ฉด ๊ต‰์žฅํžˆ ๋‹จ์ˆœํ•ฉ๋‹ˆ๋‹ค.

struct ScrollableCalendarView: View {
    @Binding var monthDateViewData: [YearMonth: [DateViewData]]
    @Binding var selectedDates: [Date]

    let model: DateSelectorModel
    let openDaysSet: Set<Date>

    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                ForEach(monthDateViewData.keys.sorted(), id: \.self) { month in
                    DTSMonthView(
                        month: month,
                        dateViewData: monthDateViewData[month] ?? [],
                        onDateSelected: { date in
                            handleDateSelection(date)
                        }
                    )
                    .id(month)
                    .equatable()
                }
            }
        }
    }

    private func handleDateSelection(_ date: Date) {
        DateTravelerSelectorUtils.handleDateSelection(
            date: date,
            selectedDates: &selectedDates,
            dateSelectorModel: model,
            selectionMode: model.selectionMode,
            openDaysSet: openDaysSet
        )
    }
}

UI๋Š” ์…€ ์ƒํƒœ๋ฅผ ์ „ํ˜€ ๊ณ„์‚ฐํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Core์—์„œ ๋งŒ๋“ค์–ด์ค€ DateViewData๋งŒ ๊ทธ๋Œ€๋กœ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

โ‘ก Core๊ฐ€ ๊ณ„์‚ฐํ•œ DateViewData๋ฅผ ๊ทธ๋Œ€๋กœ ๊ทธ๋ฆฐ๋‹ค

DTSMonthView(
    month: month,
    dateViewData: monthDateViewData[month] ?? [],
    onDateSelected: { date in handleDateSelection(date) }
)

SwiftUI๋Š” Equatable ์ ์šฉ ๋•๋ถ„์— โ€œ๋ณ€๊ฒฝ๋œ ์…€๋งŒ ์ตœ์†Œ๋กœ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐโ€๊ฐ€ ๊ฐ€๋Šฅํ•ด ์„ฑ๋Šฅ์ด ๋งค์šฐ ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

โ‘ข ์‹ค์ œ ๋„๋ฉ”์ธ ํ™”๋ฉด์—์„œ๋Š” ์ด๋ ‡๊ฒŒ ์žฌ์‚ฌ์šฉ๋œ๋‹ค

Calendar Core๋Š” ๋„๋ฉ”์ธ๋ณ„๋กœ ๋‹ฌ๋ ฅ ์ •์ฑ…์ด ๋‹ฌ๋ผ๋„, DateSelectorModel๋งŒ ๋‹ค๋ฅด๊ฒŒ ์ฃผ์ž…ํ•˜๋ฉด ๊ทธ๋Œ€๋กœ ๋™์ผํ•œ ScrollableCalendarView๋ฅผ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

let model = DateSelectorModel(
    startDate: today,
    endDate: today.adding(days: 180),
    consecutiveDays: 30,
    selectionMode: .range
)

ScrollableCalendarView(
    dateSelectorModel: model,
    openDaysSet: openDates,
    monthDateViewData: $monthData,
    selectedDates: $selectedDates
)
let model = DateSelectorModel(
    startDate: today,
    endDate: today.adding(days: 365),
    maxSelectableDays: 31,
    selectionMode: .multi
)

ScrollableCalendarView(
    dateSelectorModel: model,
    openDaysSet: openDates,
    monthDateViewData: $monthData,
    selectedDates: $selectedDates
)

Calendar Core๋ฅผ ์ค‘์‹ฌ์œผ๋กœ UI ยท ์ •์ฑ…์„ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ•˜๋ฉด์„œ ์—ฌ๊ธฐ์–ด๋•Œ์˜ ํŒจํ‚ค์ง€ยท๊ตญ๋‚ด์ˆ™์†Œยทํ•ด์™ธ์ˆ™์†Œยท์ฐœ ๋“ฑ ๋‹ค์–‘ํ•œ ํ™”๋ฉด์„ ํ•˜๋‚˜์˜ SwiftUI ์บ˜๋ฆฐ๋” ์ปดํฌ๋„ŒํŠธ๋กœ ์œ ์—ฐํ•˜๊ฒŒ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

[ DateTravelerSelector (๋„๋ฉ”์ธ๋ณ„) ]
          โ†“ ๋ชจ๋ธ ์ฃผ์ž…
[ Calendar Core ] โ€” ๋‚ ์งœ ์ƒํƒœ ๊ณ„์‚ฐ
          โ†“ DateViewData
[ ScrollableCalendarView ] โ€” ๊ทธ๋ฆฌ๊ธฐ + ์ด๋ฒคํŠธ ์ „๋‹ฌ

UI๋Š” ์–‡๊ณ  ๋‹จ์ˆœํ•˜๊ฒŒ ์œ ์ง€๋˜๊ณ , ์ •์ฑ… ๋ณ€๊ฒฝ์€ Core์—๋งŒ ์†๋Œ€๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์—

ํ™•์žฅ์„ฑยท์žฌ์‚ฌ์šฉ์„ฑยท์œ ์ง€๋ณด์ˆ˜์„ฑ ๋ชจ๋‘ ํฌ๊ฒŒ ๊ฐœ์„ ๋œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

๐Ÿ‘ฅ ์ธ์› ์„ ํƒ(Traveler Selector) ๊ตฌ์กฐ

Calendar Core์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, ์ธ์› ์„ ํƒ๋„ ๋„๋ฉ”์ธ๋ณ„ ์ •์ฑ… ์ฐจ์ด๊ฐ€ ์ปธ๊ธฐ ๋•Œ๋ฌธ์— ์™„์ „ ๊ณตํ†ตํ™”๊ฐ€ ์‰ฝ์ง€ ์•Š์€ ์˜์—ญ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋‚ ์งœ ์„ ํƒ๊ณผ ๋™์ผํ•œ ์ฒ ํ•™์œผ๋กœ, UI๋Š” ์–‡๊ฒŒยท์ •์ฑ…์€ ๋ฐ–์—์„œ ์ฃผ์ž…ํ•˜๋Š” ๊ตฌ์กฐ๋กœ ์žฌ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

1. TravelerView๋Š” โ€œ๊ณตํ†ต UI ์ปดํฌ๋„ŒํŠธโ€, ์ •์ฑ…์€ ์™ธ๋ถ€์—์„œ ์ฃผ์ž…ํ•˜๋Š” ๋ฐฉ์‹

TravelerView๋Š” ์•„๋ž˜ ์š”์†Œ๋“ค์ด ๋ชจ๋‘ ์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋˜๋Š” ๊ตฌ์กฐ๋กœ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค

์ฆ‰, ์ธ์› ์ •์ฑ…์€ ๋ทฐ ๋‚ด๋ถ€์— ๋ฐ•ํ˜€์žˆ์ง€ ์•Š๊ณ , ๋ชจ๋‘ TravelerViewData๋กœ ์ฃผ์ž…๋ฉ๋‹ˆ๋‹ค.

struct TravelerViewData {
    let layoutType: TravelerView.TravelerLayoutType
    let travelers: [Traveler]
    let minTravelerCounts: [TravelerCategory: Int]
    let maxTravelerCounts: [TravelerCategory: Int]
    let minRoomCount: Int?
    let maxRoomCount: Int?
    let messages: [TravelerMessage]
}

ํ•ด์™ธ ์ˆ™์†Œ ์˜ˆ์‹œ๋ฅผ ํ•˜๋‚˜๋งŒ ๋ณด๋ฉด ํ๋ฆ„์ด ๋” ์ž˜ ๋ณด์ž…๋‹ˆ๋‹ค.

// ์˜ˆ: ํ•ด์™ธ ์ˆ™์†Œ ์ธ์› ์„ ํƒ์šฉ ViewData ๊ตฌ์„ฑ ์˜ˆ์‹œ (์ถ•์•ฝ ๋ฒ„์ „)
let viewData = TravelerViewData(
    layoutType: .overseas,
    travelers: [
        .init(category: .adult, subTitle: "์ด ์„ฑ์ธ ์ˆ˜"),
        .init(category: .child, subTitle: "์ด ์•„๋™ ์ˆ˜(๋งŒ 0์„ธ~17์„ธ)")
    ],
    messages: [
        .init(text: "์ธ์› ๊ด€๋ จ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ 1 ..."),
        .init(text: "์ธ์› ๊ด€๋ จ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ 2 ...")
    ],
    minTravelerCounts: [.adult: 1, .child: 0],
    maxTravelerCounts: [.adult: 4, .child: 3],
    minRoomCount: 1,
    maxRoomCount: 3
)

TravelerView(
    viewData: viewData,
    selectedTravelerCounts: $selectedTravelerCounts,
    selectedRoomCounts: $selectedRoomCount,
    selectedChildAges: $selectedChildAges
)

TravelerView๋Š” ๋‹จ์ˆœํžˆ ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ์—ญํ• ๋งŒ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

๋“ฑ UI ์ž‘์—…๋งŒ ๋‹ด๋‹นํ•˜๊ณ , ์ •์ฑ… ํŒ๋‹จ์€ ๋‹จ ํ•˜๋‚˜๋„ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ์—ฌ๊ธฐ์–ด๋•Œ์—์„œ๋Š” ๋„๋ฉ”์ธ๋ณ„๋กœ ์ธ์› ๊ตฌ์„ฑ์ด ์•„๋ž˜์ฒ˜๋Ÿผ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ๋„๋ฉ”์ธ๋ณ„ ์ฐจ์ด๋Š” ๋ชจ๋‘ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ(TravelerViewData)์—์„œ ์ •์˜ํ•˜๊ณ ,

TravelerView๋Š” ๊ทธ ์ •๋ณด๋ฅผ ๋ฐ›์•„ ๋™์ผํ•œ ๋ ˆ์ด์•„์›ƒ ๊ทœ์น™์— ๋งž์ถฐ ๊ทธ๋ฆฌ๊ธฐ๋งŒ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ ํ•œ ์ค„ ์ˆ˜์ • ์—†์ด๋„ ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ์—์„œ ๊ณตํ†ต์œผ๋กœ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2. DateTravelerSelector์—์„œ โ€œ๋‹ฌ๋ ฅ + ์ธ์›โ€์„ ์กฐ๋ฆฝํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋

์ตœ์ข… DateTravelerSelector ํ™”๋ฉด์—์„œ๋Š” ์•„๋ž˜์ฒ˜๋Ÿผ ์กฐ๋ฆฝ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค:

[ ScrollableCalendarView ]     โ† ๋‚ ์งœ ์„ ํƒ (๊ณตํ†ต)
[ TravelerView ]               โ† ์ธ์› ์„ ํƒ (๊ณตํ†ต)

๋ชจ๋“  ๋„๋ฉ”์ธ์€ ์ž์‹ ๋“ค์˜ ์ •์ฑ…๋Œ€๋กœ TravelerViewData๋งŒ ๋งŒ๋“ค์–ด์„œ ์ „๋‹ฌํ•˜๋ฉด ๋˜๊ณ , TravelerView ๋‚ด๋ถ€ ์ฝ”๋“œ๋‚˜ ๋ ˆ์ด์•„์›ƒ ๋ถ„๊ธฐ์—๋Š” ์†๋Œˆ ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.

์ด ๋ฐฉ์‹์˜ ์žฅ์ ์€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

โœจ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์ด๋ฒˆ ๊ตฌ์กฐ ๊ฐœํŽธ์„ ํ†ตํ•ด Dateยท์ธ์› ์„ ํƒ UI๋Š” ๋„๋ฉ”์ธ๋ณ„ ์ •์ฑ… ์ฐจ์ด๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ๋„, SwiftUI ๊ธฐ๋ฐ˜์˜ ์ผ๊ด€๋œ Calendar Core + ๊ฐ€๋ฒผ์šด UI ๋ ˆ์ด์–ด ๊ตฌ์กฐ๋กœ ์žฌ์ •๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋•๋ถ„์— ํŒจํ‚ค์ง€ยท๊ตญ๋‚ด์ˆ™์†Œยทํ•ด์™ธ์ˆ™์†Œยท์ฐœ ๋“ฑ ๋‹ค์–‘ํ•œ ํ™”๋ฉด์—์„œ๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์•ˆ์ •์ ์œผ๋กœ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๊ณ , ์•ž์œผ๋กœ ์ƒˆ๋กœ์šด ์ •์ฑ…์ด ์ถ”๊ฐ€๋˜๋”๋ผ๋„ Core ํ™•์žฅ๋งŒ์œผ๋กœ ๋Œ€์‘ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋ฅผ ๊ฐ–์ถ”๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ธด ๊ธ€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

๋” ๋‚˜์€ ๊ตฌ์กฐ๋ฅผ ์œ„ํ•œ ๊ณ ๋ฏผ๊ณผ ๊ฒฝํ—˜์„ ๊ณ„์† ๊ณต์œ ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!