Skip to content

[Release] Release 2.1.1 - #186

Merged
oungsi2000 merged 54 commits into
mainfrom
develop
May 8, 2026
Merged

[Release] Release 2.1.1#186
oungsi2000 merged 54 commits into
mainfrom
develop

Conversation

@oungsi2000

Copy link
Copy Markdown
Contributor

#๏ธโƒฃ ์ด์Šˆ ๋ฒˆํ˜ธ

ex) -


๐Ÿ› ๏ธ ์ž‘์—… ๋‚ด์šฉ

  • 2.1.1 ๋ฒ„์ „์„ ๋ฆด๋ฆฌ์ฆˆ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ™‡๐Ÿป ์ค‘์  ๋ฆฌ๋ทฐ ์š”์ฒญ

  • ํŠนํžˆ ํ™•์ธ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„, ๋ฏผํ–ˆ๋˜ ๋ถ€๋ถ„ ๋“ฑ์„ ์ ์–ด์ฃผ์„ธ์š”.

๐Ÿ“ธ ์ด๋ฏธ์ง€ ์ฒจ๋ถ€ (Optional)

oungsi2000 and others added 30 commits March 20, 2026 14:21
[Feat]: ExploreScreen CMP ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
* feat: ๋„ค๋น„๊ฒŒ์ด์…˜ ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๋ฐ `Compose Navigation` ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

* **feat: ์•ฑ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ตฌ์กฐ ๋ฐ ๋ผ์šฐํŒ… ์ •์˜**
    * `FestabookRoute`์™€ `MainTabRoute`๋ฅผ ํ†ตํ•ด ์•ฑ์˜ ์ „์ฒด ํ™”๋ฉด ๊ฒฝ๋กœ(Splash, Explore, Main ๋“ฑ)๋ฅผ `Serializable`ํ•˜๊ฒŒ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookMainTab` ์—ด๊ฑฐํ˜•์„ ์ถ”๊ฐ€ํ•˜์—ฌ ํ™ˆ, ์ผ์ •, ์ง€๋„, ์†Œ์‹, ์„ค์ • ๋“ฑ ํ•˜๋‹จ ํƒญ ๋ฉ”๋‰ด์˜ ์•„์ด์ฝ˜, ๋ผ๋ฒจ ๋ฐ ๋ผ์šฐํŠธ๋ฅผ ํ†ตํ•ฉ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
    * `FestabookNavigator` ํด๋ž˜์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ `NavHostController`๋ฅผ ๋ž˜ํ•‘ํ•˜๊ณ  ํƒญ ์ „ํ™˜ ๋ฐ ํ™”๋ฉด ์ด๋™ ๋กœ์ง์„ ์บก์Аํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ํ™”๋ฉด๋ณ„ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„ ๋ถ„๋ฆฌ ๋ฐ ๊ตฌํ˜„**
    * `splashNavGraph`: ์Šคํ”Œ๋ž˜์‹œ ํ™”๋ฉด ์ง„์ž… ๋ฐ ์•ฑ ๋ฒ„์ „ ์ฒดํฌ ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅธ ๋ถ„๊ธฐ ๋กœ์ง์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
    * `exploreNavGraph`: ๋‘˜๋Ÿฌ๋ณด๊ธฐ ํ™”๋ฉด ์ง„์ž… ๋ฐ ๋ฉ”์ธ ํ™”๋ฉด ์ด๋™ ๋กœ์ง์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
    * `FestabookScreen`: `NavHost`๋ฅผ ์„ค์ •ํ•˜๊ณ  ๊ฐ ๊ธฐ๋Šฅ๋ณ„ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„๋ฅผ ์กฐ๋ฆฝํ•˜๋Š” ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ํ”Œ๋žซํผ ๊ณตํ†ต `AppGraph` ์ ‘๊ทผ ๋กœ์ง ์ถ”๊ฐ€**
    * `expect/actual fun rememberAppGraph()`๋ฅผ ์ •์˜ํ•˜์—ฌ Android์™€ iOS ๊ฐ ํ”Œ๋žซํผ์˜ ์˜์กด์„ฑ ์ฃผ์ž… ๊ฐ์ฒด(`FestabookAppGraph`)์— Compose ์ƒ์—์„œ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **Android**: `LocalContext`๋ฅผ ํ†ตํ•ด `applicationContext`์˜ `appGraph`๋ฅผ ์ฐธ์กฐํ•ฉ๋‹ˆ๋‹ค.
    * **iOS**: `createGraph<IosAppGraph>()`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ธฐ์–ตํ•ฉ๋‹ˆ๋‹ค.

* **build: ํ”„๋กœ์ ํŠธ ์˜์กด์„ฑ ์ถ”๊ฐ€**
    * `build.gradle.kts` ๋ฐ `libs.versions.toml`์— JetBrains Compose Navigation (`navigation-compose`) ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์ „์—ญ ํ…Œ๋งˆ ์„ค์ •์„ ํ†ตํ•œ ์˜์กด์„ฑ ์ฃผ์ž… ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ ๊ณตํ†ต ํ™”๋ฉด ํ†ตํ•ฉ

* **refactor: `FestabookTheme` ๋‚ด ์ „์—ญ ์˜์กด์„ฑ ๋ฐ ์ด๋ฏธ์ง€ ๋กœ๋” ์„ค์ • ํ†ตํ•ฉ**
    * `CompositionLocalProvider`๋ฅผ ํ†ตํ•ด `LocalMetroViewModelFactory`์™€ `LocalCoilImageLoader`๋ฅผ ํ…Œ๋งˆ ์ˆ˜์ค€์—์„œ ์ œ๊ณตํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `setSingletonImageLoaderFactory`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ `crossfade`๊ฐ€ ํ™œ์„ฑํ™”๋œ Coil `ImageLoader` ์„ค์ •์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ํ”Œ๋žซํผ๋ณ„(Android/iOS) ์ง„์ž…์  ๋กœ์ง ๊ฐ„์†Œํ™”**
    * **Android**: `MainActivity`์—์„œ ๊ฐœ๋ณ„์ ์œผ๋กœ ์„ค์ •ํ•˜๋˜ `CompositionLocalProvider`์™€ `Scaffold`๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , ํ†ตํ•ฉ ์ง„์ž…์ ์ธ `FestabookScreen`์„ ํ˜ธ์ถœํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **iOS**: `MainViewController`์—์„œ ๋ถˆํ•„์š”ํ•œ ์ด๋ฏธ์ง€ ๋กœ๋” ๋ฐ ๋ทฐ๋ชจ๋ธ ํŒฉํ† ๋ฆฌ ์ฃผ์ž… ๋กœ์ง์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `FestabookScreen` ๊ตฌ์กฐ ๊ฐœ์„ **
    * `appGraph`๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ฃผ์ž…๋ฐ›๋„๋ก ๋ณ€๊ฒฝํ•˜๊ณ , `SplashViewModel` ์ดˆ๊ธฐํ™” ์‹œ `metroViewModelFactory`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: `CoilImage` ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋Šฅ ๊ฐœ์„ **
    * ์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ•  ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด `fallback` ์ด๋ฏธ์ง€(`Res.drawable.img_fallback`) ์„ค์ •์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `LocalMetroViewModelFactory`๋ฅผ ํ™œ์šฉํ•œ ViewModel ์ฃผ์ž… ๋ฐฉ์‹ ๊ฐœ์„ 

* **refactor: `ExploreNavigation` ๋ฐ `SplashNavigation` ๋‚ด ์˜์กด์„ฑ ์ฃผ์ž… ๋กœ์ง ์ˆ˜์ •**
    * `exploreNavGraph` ๋ฐ `splashNavGraph` ํ•จ์ˆ˜์—์„œ ๋” ์ด์ƒ ํ•„์š”ํ•˜์ง€ ์•Š์€ `FestabookAppGraph` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `viewModel` ์ƒ์„ฑ ์‹œ `appGraph.metroViewModelFactory`๋ฅผ ์ง์ ‘ ์ „๋‹ฌํ•˜๋Š” ๋Œ€์‹ , `LocalMetroViewModelFactory.current`๋ฅผ ์ฐธ์กฐํ•˜์—ฌ ํŒฉํ† ๋ฆฌ๋ฅผ ์ฃผ์ž…๋ฐ›๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `FestabookScreen` ๋‚ด ๋„ค๋น„๊ฒŒ์ด์…˜ ํ˜ธ์ถœ๋ถ€ ์ตœ์ ํ™”**
    * ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„ ๊ตฌ์„ฑ ์‹œ `appGraph` ์ธ์ž๋ฅผ ๋„˜๊ฒจ์ฃผ๋˜ ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ฐ„์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: ๋ฏธ์‚ฌ์šฉ `FestabookAppGraph` ์ž„ํฌํŠธ ์ œ๊ฑฐ

* **chore: ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ด€๋ จ ํŒŒ์ผ ๋‚ด ๋ถˆํ•„์š”ํ•œ ์ž„ํฌํŠธ ์‚ญ์ œ**
    * `ExploreNavigation.kt` ๋ฐ `SplashNavigation.kt`์—์„œ ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `FestabookAppGraph` ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: `composeApp/build.gradle.kts` ๋‚ด ๋ฏธ์‚ฌ์šฉ ๋‚ด๋น„๊ฒŒ์ด์…˜ ์˜์กด์„ฑ ์ œ๊ฑฐ

* **chore: `composeApp/build.gradle.kts` ๋‚ด ๋ฏธ์‚ฌ์šฉ ์˜์กด์„ฑ ์‚ญ์ œ**
    * `commonMain` ์†Œ์Šค์…‹์˜ ์˜์กด์„ฑ ์„ค์ •์—์„œ ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `libs.compose.navigation`์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
* feat: Firebase Crashlytics ๋„์ž…

* **feat: iOS ํ”Œ๋žซํผ Firebase SDK ์—ฐ๋™ ๋ฐ ์ดˆ๊ธฐํ™”**
    * `Podfile` ๋ฐ `Podfile.lock`์— `FirebaseCrashlytics`์™€ `FirebaseAnalytics` ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AppDelegate.swift`๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์•ฑ ์‹คํ–‰ ์‹œ `FirebaseApp.configure()`๊ฐ€ ํ˜ธ์ถœ๋˜๋„๋ก ๊ตฌํ˜„ํ•˜๊ณ , `iOSApp.swift`์—์„œ `UIApplicationDelegateAdaptor`๋ฅผ ํ†ตํ•ด ์ด๋ฅผ ์—ฐ๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `iosApp.xcodeproj`์— ๋นŒ๋“œ ๊ตฌ์„ฑ(Debug/Release)์— ๋”ฐ๋ผ ์ ์ ˆํ•œ `GoogleService-Info.plist`๋ฅผ ๋ณต์‚ฌํ•˜๋Š” ์…ธ ์Šคํฌ๋ฆฝํŠธ์™€ Crashlytics ์ดˆ๊ธฐํ™” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: Android ํ”Œ๋žซํผ Firebase SDK ์—ฐ๋™**
    * `composeApp/build.gradle.kts`์— `firebase-bom`, `firebase-crashlytics-ndk`, `firebase-analytics` ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `libs.versions.toml`์— Firebase ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฐ ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ฒ„์ „์„ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: BuildKonfig ์„ค์ • ํ™•์žฅ**
    * `BUILD_FLAVOR` ๋ฐ `APP_BUNDLE_ID` ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋นŒ๋“œ ํ™˜๊ฒฝ์— ๋”ฐ๋ฅธ ์•ฑ ์‹๋ณ„์ž ๊ด€๋ฆฌ๋ฅผ ์ž๋™ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `local.properties`๋กœ๋ถ€ํ„ฐ `APP_BUNDLE_ID` ๋ฐ `APP_BUNDLE_ID_DEV` ๊ฐ’์„ ์ฝ์–ด์˜ค๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: iOS ํ”„๋กœ์ ํŠธ ์„ค์ • ๋ฐ ๋ณด์•ˆ ๊ด€๋ฆฌ**
    * `.gitignore`์— ํ™˜๊ฒฝ๋ณ„ `GoogleService-Info.plist` ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ฏผ๊ฐ ์ •๋ณด ์œ ์ถœ์„ ๋ฐฉ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `Info.plist` ๋‚ด์˜ ์œ„์น˜ ๊ถŒํ•œ ์„ค๋ช…(`NSLocationWhenInUseUsageDescription`)์„ Xcode ํ”„๋กœ์ ํŠธ ์„ค์ •(`INFOPLIST_KEY`)์œผ๋กœ ์ด๋™ํ•˜์—ฌ ๊ด€๋ฆฌ ํšจ์œจ์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.
    * ๋””๋ฒ„๊ทธ ์‹ฌ๋ณผ ์ƒ์„ฑ์„ ์œ„ํ•ด `DEBUG_INFORMATION_FORMAT`์„ `dwarf-with-dsym`์œผ๋กœ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: `FirebaseCrashlytics` ์ถ”์ƒํ™” ๋ฐ ํ”Œ๋žซํผ๋ณ„ ๊ตฌํ˜„ ์ถ”๊ฐ€

* **feat: `FirebaseCrashlytics` ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ ํ”Œ๋žซํผ๋ณ„ ๊ตฌํ˜„**
    * `commonMain`: ์˜ˆ์™ธ ๊ธฐ๋ก(`recordException`) ๋ฐ ๋ฏธ์ „์†ก ๋ฆฌํฌํŠธ ์ „์†ก(`sendUnsentReports`) ๊ธฐ๋Šฅ์„ ํฌํ•จํ•˜๋Š” `expect object FirebaseCrashlytics`๋ฅผ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **Android**: Firebase Crashlytics SDK๋ฅผ ์—ฐ๋™ํ•˜์—ฌ `actual` ๊ฐ์ฒด๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **iOS**: CocoaPods์˜ `FIRCrashlytics`๋ฅผ ์—ฐ๋™ํ•˜๊ณ , Kotlin `Throwable`์„ `NSError`๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๋ฅผ ํฌํ•จํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `AppVersionManager.ios.kt` ๋‚ด ์„ค์ • ๋กœ์ง ๊ฐœ์„ **
    * ํ•˜๋“œ์ฝ”๋”ฉ๋œ `APP_BUNDLE_ID`๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  `BuildKonfig.APP_BUNDLE_ID`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋นŒ๋“œ ๋ฒ„์ „์ด `dev`์ธ ๊ฒฝ์šฐ ์—…๋ฐ์ดํŠธ ํ™•์ธ ๊ณผ์ •์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ์ฆ‰์‹œ ์„ฑ๊ณต(`false`)์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: `FirebaseAnalytics` ์ถ”์ƒํ™” ๋ฐ ํ”Œ๋žซํผ๋ณ„ ๊ตฌํ˜„ ์ถ”๊ฐ€

* **feat: `FirebaseAnalytics` ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ ํ”Œ๋žซํผ๋ณ„ ๊ตฌํ˜„**
    * `commonMain`: ์ด๋ฒคํŠธ ๋กœ๊ทธ ๊ธฐ๋ก(`logEvent`), ์•ฑ ์ธ์Šคํ„ด์Šค ID ์กฐํšŒ(`getAppInstanceId`), ์„ธ์…˜ ID ์กฐํšŒ(`getSessionId`) ๊ธฐ๋Šฅ์„ ํฌํ•จํ•˜๋Š” `expect object FirebaseAnalytics`๋ฅผ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **Android**: `com.google.firebase.analytics`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ `actual` ๊ฐ์ฒด๋ฅผ ๊ตฌํ˜„ํ–ˆ์œผ๋ฉฐ, `await()`์„ ํ†ตํ•ด ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **iOS**: CocoaPods์˜ `FIRAnalytics`๋ฅผ ์—ฐ๋™ํ•˜์—ฌ `actual` ๊ฐ์ฒด๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. `getSessionId`์˜ ๊ฒฝ์šฐ completion handler๋ฅผ `suspendCancellableCoroutine`์œผ๋กœ ๋ž˜ํ•‘ํ•˜์—ฌ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: iOS ์•ฑ ํ‘œ์‹œ ์ด๋ฆ„(CFBundleDisplayName) ์„ค์ •

* **chore: ๋นŒ๋“œ ๊ตฌ์„ฑ๋ณ„ ์•ฑ ํ‘œ์‹œ ์ด๋ฆ„ ์ถ”๊ฐ€**
    * `iosApp.xcodeproj` ํ”„๋กœ์ ํŠธ ์„ค์ •์— `INFOPLIST_KEY_CFBundleDisplayName`์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์„ค์น˜๋œ ์•ฑ์˜ ์ด๋ฆ„์„ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋””๋ฒ„๊ทธ ๋นŒ๋“œ ํ™˜๊ฒฝ์—์„œ๋Š” `"festabook(Debug)"`, ๋ฆด๋ฆฌ์ฆˆ ๋นŒ๋“œ ํ™˜๊ฒฝ์—์„œ๋Š” `"festabook"`์œผ๋กœ ํ‘œ์‹œ๋˜๋„๋ก ๊ตฌ๋ถ„ํ•˜์—ฌ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: CI ์›Œํฌํ”Œ๋กœ์šฐ ๋‚ด ์•ฑ ๋ฒˆ๋“ค ID ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ถ”๊ฐ€

* **ci: `.github/workflows/cmp-ci.yml` ์ˆ˜์ •**
    * `local.properties` ํŒŒ์ผ ์ƒ์„ฑ ๋‹จ๊ณ„์— `APP_BUNDLE_ID` ๋ฐ `APP_BUNDLE_ID_DEV` ์„ค์ •์„ ์ถ”๊ฐ€ํ•˜์—ฌ GitHub Secrets๋กœ๋ถ€ํ„ฐ ํ•ด๋‹น ๊ฐ’์„ ์ฃผ์ž…๋ฐ›๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: Google Services ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€

* **chore: Google Services ์˜์กด์„ฑ ๋ฐ ํ”Œ๋Ÿฌ๊ทธ์ธ ์„ค์ •**
    * `libs.versions.toml`์— `googleServices` ๋ฒ„์ „์„ ์ •์˜ํ•˜๊ณ  `google-gms-services` ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ฃจํŠธ `build.gradle.kts`์— `google-gms-services` ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `composeApp/build.gradle.kts`์— `google-gms-services` ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: `composeApp/build.gradle.kts` ๋‚ด `buildFlavor` ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •

* **chore: `buildFlavor` ํ”„๋กœํผํ‹ฐ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„ **
    * `project.properties["buildkonfig.flavor"]` ๊ฐ’์ด ์—†์„ ๊ฒฝ์šฐ(null) ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ `"dev"`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ๋นŒ๋“œ ์„ค์ •์˜ ์•ˆ์ •์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* chore: `FirebaseCrashlytics.ios.kt` ๋‚ด `asNSError` ๊ฐ€์‹œ์„ฑ ์ˆ˜์ •

* **chore: `asNSError` ํ™•์žฅ ํ•จ์ˆ˜ ๊ฐ€์‹œ์„ฑ ๋ณ€๊ฒฝ**
    * `FirebaseCrashlytics.ios.kt` ํŒŒ์ผ์—์„œ ์‚ฌ์šฉ๋˜๋Š” `Throwable.asNSError()` ํ™•์žฅ ํ•จ์ˆ˜์˜ ๊ฐ€์‹œ์„ฑ์„ `private`์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์™ธ๋ถ€ ๋…ธ์ถœ์„ ์ œํ•œํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: iOS ํ”Œ๋žซํผ `FirebaseAnalytics` ์ตœ์ ํ™”

* **refactor: `logEvent` ํŒŒ๋ผ๋ฏธํ„ฐ ์บ์ŠคํŒ… ๋กœ์ง ์ตœ์ ํ™”**
    * `logEvent` ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ `Map`์˜ ํ‚ค๋ฅผ ๊ฐœ๋ณ„์ ์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋˜ ๋กœ์ง์„ `as? Map<Any?, Any?>`๋ฅผ ์‚ฌ์šฉํ•œ ์ง์ ‘ ์บ์ŠคํŒ…์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ๋ถˆํ•„์š”ํ•œ ๊ฒฝ๊ณ ๋ฅผ ์–ต์ œํ•˜๊ธฐ ์œ„ํ•ด `@Suppress("UNCHECKED_CAST")` ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: `getSessionId` ๋‚ด ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋กœ์ง ์ถ”๊ฐ€**
    * `sessionIDWithCompletion` ์ฝœ๋ฐฑ์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ, `RuntimeException`์„ ํ†ตํ•ด ์‹คํŒจ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: Android `FirebaseAnalytics` ํŒŒ๋ผ๋ฏธํ„ฐ ํƒ€์ž… ์ฒ˜๋ฆฌ ๊ฐœ์„ 

* **feat: Android ํ”Œ๋žซํผ ๋‚ด Firebase Analytics ์ด๋ฒคํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ ํƒ€์ž… ์ฒ˜๋ฆฌ ๋กœ์ง ์ˆ˜์ •**
    * `FirebaseAnalytics.android.kt`์˜ `logEvent` ๋ฉ”์„œ๋“œ์—์„œ ์ด๋ฒคํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ, ๋ชจ๋“  ๊ฐ’์„ ์ผ๊ด„์ ์œผ๋กœ `String`์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋˜ ๋ฐฉ์‹์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `Long`, `Double`, `String` ํƒ€์ž…์— ๋Œ€ํ•ด ๊ฐ ํƒ€์ž…๋ณ„ ์ „์šฉ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ Firebase SDK์— ์˜ฌ๋ฐ”๋ฅธ ๋ฐ์ดํ„ฐ ํƒ€์ž…์ด ์ „๋‹ฌ๋˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
* [feat]: FestabookTopAppBar์— ์ƒํƒœ๋ฐ” ํŒจ๋”ฉ ์ถ”๊ฐ€

* [Build]: ์•ฑ ๋ฒ„์ „ ๊ด€๋ฆฌ ์ค‘์•™ํ™” ๋ฐ ์ž๋™ํ™”

* [Build]: gradle.properties์— APP_VERSION_NAME, APP_VERSION_CODE ์ถ”๊ฐ€
* [Build]: Android ๋นŒ๋“œ ์„ค์ •(versionCode, versionName)์— Gradle ํ”„๋กœํผํ‹ฐ ์ ์šฉ
* [Build]: iOS Info.plist์˜ ๋ฒ„์ „์„ ์ž๋™ ์—…๋ฐ์ดํŠธํ•˜๋Š” Gradle ํƒœ์Šคํฌ ์ถ”๊ฐ€
* [Build]: BuildConfig์— APP_VERSION_NAME ํ•„๋“œ ์ถ”๊ฐ€

* [Chore]: ic_arrow_forward_right.xml ๋ฆฌ์†Œ์Šค ์ˆ˜์ •

* ๋ฒกํ„ฐ ๋“œ๋กœ์–ด๋ธ”์˜ ์ƒ‰์ƒ ๊ฐ’์„ ํ•˜๋“œ์ฝ”๋”ฉ๋œ hex ๊ฐ’(#FFFFFFFF)์œผ๋กœ ๋ณ€๊ฒฝ
* XML ํŒŒ์ผ ํฌ๋งท ๋ฐ ์ธ๋ดํŠธ ์ •๋ฆฌ

* [Build]: iosApp Info.plist์— CFBundleName ์ถ”๊ฐ€

* [Build]: Android ์•Œ๋ฆผ ๊ถŒํ•œ ์ถ”๊ฐ€

* [Feat]: ContextFactory ์ถ”๊ฐ€

* common: ํ”Œ๋žซํผ๋ณ„ ์ปจํ…์ŠคํŠธ ์ ‘๊ทผ์„ ์œ„ํ•œ expect ํด๋ž˜์Šค ์ •์˜
* android: AssistedInject๋ฅผ ์ด์šฉํ•œ ContextFactory ๊ตฌํ˜„ ์ถ”๊ฐ€
* ios: NSBundle ๋ฐ UIApplication์„ ์ด์šฉํ•œ ContextFactory ๊ตฌํ˜„ ์ถ”๊ฐ€

* [Feat]: NotificationPermissionManager ์ถ”๊ฐ€

* [Feat]: commonMain์— NotificationPermissionManager expect ํด๋ž˜์Šค ์ •์˜
* [Feat]: androidMain์— TIRAMISU ๋Œ€์‘ ๋ฐ Rationale ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ํฌํ•จํ•œ NotificationPermissionManager actual ๊ตฌํ˜„ ์ถ”๊ฐ€
* [Feat]: iosMain์— NotificationPermissionManager actual ๊ตฌํ˜„ ์ถ”๊ฐ€ (์Šคํ…)

* [Feat]: ์•Œ๋ฆผ ๊ถŒํ•œ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ํ”Œ๋žซํผ๋ณ„ Launcher ๋ฐ Manager ์ถ”๊ฐ€

* commonMain์— `rememberNotificationPermissionManager` ๋ฐ `expect` ํ•จ์ˆ˜ ์ •์˜
* androidMain์— `ActivityResultLauncher`๋ฅผ ์ด์šฉํ•œ ๊ถŒํ•œ ์š”์ฒญ ๊ตฌํ˜„
* iosMain์— `UNUserNotificationCenter`๋ฅผ ์ด์šฉํ•œ ๊ถŒํ•œ ์š”์ฒญ ๊ตฌํ˜„

* [Feat]: SettingViewModel ์ถ”๊ฐ€

* [Feat] DI ๊ทธ๋ž˜ํ”„ ๋‚ด NotificationPermissionManager ๋ฐ ContextFactory ์ถ”๊ฐ€

* [Feat]: FestabookAppGraph ๋ฐ AndroidAppGraph์— ํŒฉํ† ๋ฆฌ ์†์„ฑ ์ถ”๊ฐ€
* [Refactor]: FestabookApp ๋‚ด ๊ทธ๋ž˜ํ”„ ๋ณ€์ˆ˜๋ช… ๋ณ€๊ฒฝ (festabookAppGraph -> androidAppGraph)

* [Feat]: ์„ค์ •(Setting) ํ™”๋ฉด ๋ฐ ํ™ˆ(Home) ViewModel ์ถ”๊ฐ€

* [Feat]: `SettingScreen` ๋ฐ `SettingRoute` ๊ตฌํ˜„ (์•Œ๋ฆผ ์„ค์ •, ์•ฑ ์ •๋ณด, ์„œ๋น„์Šค ์•ฝ๊ด€ ๋งํฌ ํฌํ•จ)
* [Feat]: `HomeViewModel` ์ถ”๊ฐ€ ๋ฐ `FestivalUiState`, `LineupUiState`๋ฅผ ํ†ตํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ๊ตฌํ˜„
* [Feat]: `LineupItemUiModel` ๋“ฑ ๋„๋ฉ”์ธ ๋ชจ๋ธ์˜ UI ๋ณ€ํ™˜ ๋กœ์ง ์ถ”๊ฐ€
* [Refactor]: Android/iOS `MainActivity` ๋ฐ `MainViewController`์—์„œ `SettingRoute`๋ฅผ ์ดˆ๊ธฐ ํ™”๋ฉด์œผ๋กœ ์„ค์ • ๋ฐ DI ์˜์กด์„ฑ ์ฃผ์ž… ์ˆ˜์ •
* [Build]: ์„ค์ • ํ™”๋ฉด ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ตฌ์„ฑ์„ ์œ„ํ•œ `SettingNavigation` ํŒŒ์ผ ์ถ”๊ฐ€ (์ฃผ์„ ์ฒ˜๋ฆฌ)

* [Feat]: iOS ContextFactory ์˜์กด์„ฑ ์ฃผ์ž… ์ ์šฉ

* feat: ContextFactory์— @Inject ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€
* feat: IosAppGraph ์ธํ„ฐํŽ˜์ด์Šค์— contextFactory ๋ฉค๋ฒ„ ์ถ”๊ฐ€
* refactor: MainViewController์—์„œ ContextFactory๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜๋Š” ๋Œ€์‹  ์ฃผ์ž…๋œ ์ธ์Šคํ„ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •

* [Refactor]: ContextFactory ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ ์ˆ˜์ • ๋ฐ ๋กœ์ง ๊ฐœ์„ 

* [Refactor]: `ContextFactory`์˜ ๋ฉ”์„œ๋“œ๋ช…์„ `get-`์—์„œ `create-` ํ˜•์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  Android ์ธก ๊ตฌํ˜„์˜ ํƒ€์ž… ์•ˆ์ •์„ฑ ๊ฐœ์„ 
* [Chore]: `NotificationPermissionManager` ๋ฐ `RememberPermissionLauncher`์—์„œ ๊ฐœ์„ ๋œ `ContextFactory` ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •
* [Style]: `SettingScreen` ๋‚ด ๋ถˆํ•„์š”ํ•œ `Unit` ๋ฐ˜ํ™˜ ์ฝ”๋“œ ์ •๋ฆฌ

* [Refactor]: ContextFactory ๋ฉ”์„œ๋“œ ๋„ค์ด๋ฐ ์ˆ˜์ • ๋ฐ ๋กœ์ง ๊ฐœ์„ 

* [Refactor]: `ContextFactory`์˜ ๋ฉ”์„œ๋“œ๋ช…์„ `get-`์—์„œ `create-` ํ˜•์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  Android ์ธก ๊ตฌํ˜„์˜ ํƒ€์ž… ์•ˆ์ •์„ฑ ๊ฐœ์„ 
* [Chore]: `NotificationPermissionManager` ๋ฐ `RememberPermissionLauncher`์—์„œ ๊ฐœ์„ ๋œ `ContextFactory` ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •
* [Style]: `SettingScreen` ๋‚ด ๋ถˆํ•„์š”ํ•œ `Unit` ๋ฐ˜ํ™˜ ์ฝ”๋“œ ์ •๋ฆฌ

* [Refactor]: ํ”Œ๋žซํผ๋ณ„ ์†Œ์Šค ํŒŒ์ผ ์ด๋ฆ„์— .android ๋ฐ .ios ์ ‘๋ฏธ์‚ฌ ์ถ”๊ฐ€

* [Refactor]: ์•Œ๋ฆผ ํ—ˆ์šฉ ์—ฌ๋ถ€ ์ €์žฅ ๋กœ์ง ๊ฐœ์„ 

* [Domain]: FestivalNotificationRepository ๋‚ด ๋ฉ”์„œ๋“œ ๋ช…์นญ ๋ณ€๊ฒฝ(set -> save) ๋ฐ Result ํƒ€์ž… ์ ์šฉ
* [Data]: FestivalNotificationRepositoryImpl ๋‚ด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐ Result ๋ฐ˜ํ™˜ ๋กœ์ง ์ถ”๊ฐ€
* [Presentation]: SettingViewModel์—์„œ ์•Œ๋ฆผ ์„ค์ • ์ €์žฅ ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅธ UI ์—…๋ฐ์ดํŠธ ๋ฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง ํ†ตํ•ฉ

* [Refactor]: ๋ถˆํ•„์š”ํ•œ Greeting ๋ฐ Platform ์ฝ”๋“œ ์‚ญ์ œ

* [Build]: iosX64 ํƒ€๊ฒŸ ์ถ”๊ฐ€

* [Refactor] ContextFactory ์ œ๊ฑฐ

* [Refactor]: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `ContextFactory` ํด๋ž˜์Šค ๋ฐ ํ”Œ๋žซํผ๋ณ„(android, ios) ๊ตฌํ˜„์ฒด ์‚ญ์ œ
* [Refactor]: Dependency Graph(`AndroidAppGraph`, `IosAppGraph`)์—์„œ `ContextFactory` ๊ด€๋ จ ์ฝ”๋“œ ์ œ๊ฑฐ

* [Feat]: NotificationPermissionManager expect/actual ๊ตฌํ˜„ ์ถ”๊ฐ€

* [Refactor]: rememberNotificationPermissionManager๋ฅผ ํ”Œ๋žซํผ๋ณ„ actual ํ•จ์ˆ˜๋กœ ๋ถ„๋ฆฌ
* [Chore]: ContextFactory ์˜์กด์„ฑ ์ œ๊ฑฐ ๋ฐ shouldShowRationale ์ธํ„ฐํŽ˜์ด์Šค ์ˆ˜์ •

* [Feat]: NotificationPermissionManager ๋ฆฌํŒฉํ† ๋ง ๋ฐ ๊ถŒํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ๊ฐœ์„ 

* [Common]: `NotificationPermissionManager`์—์„œ UI ๋กœ์ง(๋‹ค์ด์–ผ๋กœ๊ทธ)์„ ์ œ๊ฑฐํ•˜๊ณ  `checkPermission`, `requestPermission`์œผ๋กœ ๊ธฐ๋Šฅ ๋ถ„๋ฆฌ
* [Common]: ๊ถŒํ•œ ์ƒํƒœ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” `PermissionState` ๋„์ž…์— ๋”ฐ๋ฅธ ์ธํ„ฐํŽ˜์ด์Šค ์ˆ˜์ •
* [Android]: `ContextFactory` ๋Œ€์‹  `Context`๋ฅผ ์ง์ ‘ ์ฃผ์ž…๋ฐ›๋„๋ก ์ˆ˜์ •ํ•˜๊ณ , ๊ธฐ์กด์˜ `AlertDialog` ๋กœ์ง ์ œ๊ฑฐ
* [iOS]: `suspendCancellableCoroutine`์„ ์‚ฌ์šฉํ•˜์—ฌ ๋น„๋™๊ธฐ ๊ถŒํ•œ ์ฒดํฌ(`checkPermission`) ๊ตฌํ˜„ ๋ฐ UI ๋กœ์ง ์ œ๊ฑฐ

* [Feat]: RememberPermissionLauncher ์ถ”๊ฐ€

* [Feat]: ๊ณตํ†ต ๋ชจ๋“ˆ์— ๊ถŒํ•œ ์š”์ฒญ ๋ฐ Rationale ํ™•์ธ์„ ์œ„ํ•œ `expect` ํ•จ์ˆ˜ ์ •์˜
* [Refactor]: `shouldShowRationale`์˜ ๋งค๊ฐœ๋ณ€์ˆ˜ ํƒ€์ž…์„ `ContextFactory`์—์„œ `Any?`๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์˜์กด์„ฑ ์ œ๊ฑฐ
* [Android]: `activity`๋ฅผ `ComponentActivity`๋กœ ์บ์ŠคํŒ…ํ•˜์—ฌ `shouldShowRequestPermissionRationale` ํ˜ธ์ถœํ•˜๋„๋ก ์ˆ˜์ •

* [Refactor]: Platform ๊ด€๋ จ boilerplate ์ฝ”๋“œ ์‚ญ์ œ

* androidMain ๋ฐ iosMain์˜ Platform ๊ตฌํ˜„๋ถ€ ์‚ญ์ œ (Platform.android.kt, Platform.ios.kt)

* [Feat]: ์•Œ๋ฆผ ๊ถŒํ•œ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„  ๋ฐ ๋‹ค์ด์–ผ๋กœ๊ทธ ์ถ”๊ฐ€

* [Feat]: `PermissionState` enum ์ถ”๊ฐ€ ๋ฐ ์•Œ๋ฆผ ๊ถŒํ•œ ์ƒํƒœ ์ฒดํฌ ๋กœ์ง ๊ตฌํ˜„
* [Feat]: `NotificationPermissionDialog` ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€
* [Refactor]: `SettingScreen`์—์„œ ๊ถŒํ•œ ์ƒํƒœ์— ๋”ฐ๋ฅธ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ ๋กœ์ง ์ˆ˜์ •
* [Refactor]: `NotificationPermissionManager`์˜ ๋ถˆํ•„์š”ํ•œ `contextFactory` ์˜์กด์„ฑ ์ œ๊ฑฐ ๋ฐ ํ˜ธ์ถœ ๋ฐฉ์‹ ๋ณ€๊ฒฝ

* [Style]: SettingViewModel ์ฝ”๋“œ ํฌ๋งท ์ˆ˜์ •

* [Refactor]: NotificationPermissionDialog ํ™•์ธ ๋ฐ ์ทจ์†Œ ์ฝœ๋ฐฑ ๋ถ„๋ฆฌ

* NotificationPermissionDialog์— `onConfirm` ๋ฐ `onDismiss` ์ฝœ๋ฐฑ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
* AlertDialog์˜ ํ™•์ธ/์ทจ์†Œ ๋ฒ„ํŠผ์ด ๊ฐ๊ฐ์˜ ์ฝœ๋ฐฑ์„ ํ˜ธ์ถœํ•˜๋„๋ก ๋กœ์ง ์ˆ˜์ •
* `cancelText` ๋ณ€์ˆ˜๋ช…์„ `dismissText`๋กœ ๋ณ€๊ฒฝ

* [Fix]: Android ์•Œ๋ฆผ ๊ถŒํ•œ ๋กœ์ง ์ˆ˜์ • ๋ฐ ๋‹ค์ด์–ผ๋กœ๊ทธ ๋™์ž‘ ๊ฐœ์„ 

* [Fix]: Android 13(TIRAMISU) ๋ฏธ๋งŒ ๋ฒ„์ „์—์„œ ์•Œ๋ฆผ ๊ถŒํ•œ์„ ํ•ญ์ƒ ํ—ˆ์šฉ ์ƒํƒœ๋กœ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •
* [Refactor]: `NotificationPermissionManager`์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ฝœ๋ฐฑ ํŒŒ๋ผ๋ฏธํ„ฐ(`onPermissionGranted`, `onPermissionDenied`) ์ œ๊ฑฐ
* [Feat]: `SettingScreen`์—์„œ ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ๋‹ซ์„ ์ˆ˜ ์žˆ๋Š” `onDismiss` ๋กœ์ง ์ถ”๊ฐ€ ๋ฐ ๊ถŒํ•œ ์ƒํƒœ ์ฒ˜๋ฆฌ ์„ธ๋ถ„ํ™”

* [Refactor]: PermissionState์—์„œ NEED_REQUEST ์ œ๊ฑฐ ๋ฐ ์•Œ๋ฆผ ๊ถŒํ•œ ๋กœ์ง ์ˆ˜์ •

* PermissionState enum์—์„œ `NEED_REQUEST`๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๊ด€๋ จ ๋กœ์ง์„ `DENIED`๋กœ ํ†ตํ•ฉ
* iOS `NotificationPermissionManager`์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์ฝœ๋ฐฑ(`onPermissionGranted`, `onPermissionDenied`) ๋ฐ ๋ถˆํ•„์š”ํ•œ ์ƒํƒœ ์ฒดํฌ ์ œ๊ฑฐ
* Android `NotificationPermissionManager`์—์„œ ๊ธฐ๋ณธ ์ƒํƒœ๋ฅผ `DENIED`๋กœ ๋ณ€๊ฒฝ
* `SettingScreen`์—์„œ `NEED_REQUEST` ์ฒ˜๋ฆฌ ๋กœ์ง์„ `DENIED` ์ผ€์ด์Šค๋กœ ์ด๋™

* [Refactor]: NotificationPermissionManager ๊ด€๋ จ ํŒŒ์ผ ํŒจํ‚ค์ง€ ์œ„์น˜ ๋ณ€๊ฒฝ

* presentation.platform ํŒจํ‚ค์ง€์— ์œ„์น˜ํ•˜๋˜ ์•Œ๋ฆผ ๊ถŒํ•œ ๊ด€๋ จ ํด๋ž˜์Šค๋“ค์„ presentation.setting.component.platform ํŒจํ‚ค์ง€๋กœ ์ด๋™
* ํŒŒ์ผ ์ด๋™์— ๋”ฐ๋ฅธ MainActivity, MainViewController ๋ฐ ๊ด€๋ จ ํŒŒ์ผ๋“ค์˜ import ๊ฒฝ๋กœ ์ˆ˜์ •

* [Feat]: ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ ์„ค์ • ์ด๋™ ์Šค๋‚ต๋ฐ” ๊ตฌํ˜„ ๋ฐ ํ”Œ๋žซํผ๋ณ„ ์—ฐ๋™

* [Feat]: ํ”Œ๋žซํผ๋ณ„ ์•ฑ ์„ค์ • ํ™”๋ฉด ์ด๋™์„ ์œ„ํ•œ `rememberOpenAppSettings` ์ถ”๊ฐ€ (Android, iOS)
* [Feat]: `SnackbarManager`์— ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์•Œ๋ฆผ ๋ฐ ์„ค์ • ์ด๋™ ์•ก์…˜ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
* [Refactor]: `MainActivity` ๋ฐ `MainViewController`์— ์Šค๋‚ต๋ฐ” ํ˜ธ์ŠคํŠธ ์„ค์ • ๋ฐ ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
* [Chore]: `SettingScreen` ๋‚ด ๋ถˆํ•„์š”ํ•œ ๋กœ๊ทธ ์‚ญ์ œ

* [Feat]: ์•Œ๋ฆผ ๊ถŒํ•œ ๋‹ค์ด์–ผ๋กœ๊ทธ UI ์ˆ˜์ •

* NotificationPermissionDialog ์ทจ์†Œ ๋ฒ„ํŠผ ์ œ๊ฑฐ ๋ฐ ์Šคํƒ€์ผ ์ ์šฉ
* NotificationPermissionDialog ๋ฏธ๋ฆฌ๋ณด๊ธฐ(Preview) ์ถ”๊ฐ€

* [Feat]: ์•Œ๋ฆผ ๊ถŒํ•œ ํ—ˆ์šฉ ์‹œ ์•Œ๋ฆผ ID ์ €์žฅ ๋กœ์ง ์ถ”๊ฐ€

* [Refactor]: FestivalNotificationRepositoryImpl ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€

* [Refactor]: ์•Œ๋ฆผ ๊ถŒํ•œ ํ—ˆ์šฉ ์‹œ ์•Œ๋ฆผ ID ์ €์žฅ ๋กœ์ง ์ด๋™

* SettingScreen์—์„œ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋˜ saveNotificationId ๋กœ์ง ์ œ๊ฑฐ
* Android/iOS ํ”Œ๋žซํผ๋ณ„ onPermissionGrant ์ฝœ๋ฐฑ ๋‚ด์—์„œ SettingViewModel์„ ํ†ตํ•ด ์•Œ๋ฆผ ID๋ฅผ ์ €์žฅํ•˜๋„๋ก ๋ณ€๊ฒฝ

* refactor: ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์šฉ BuildKonfig ํ•„๋“œ๋ฅผ commonConfigs๋กœ ์ด๋™

* `composeApp/build.gradle.kts`: `targetConfigs` ์„น์…˜์— ์ •์˜๋˜์–ด ์žˆ๋˜ ๊ฐœ๋ฐœ์šฉ ์„œ๋ฒ„ URL(`FESTABOOK_URL`, `FESTABOOK_IMAGE_URL`) ๋ฐ ์•ฑ ๋ฒˆ๋“ค ID(`APP_BUNDLE_ID`) ์„ค์ •์„ `commonConfigs`๋กœ ์ด๋™ํ•˜์—ฌ ํ”Œ๋žซํผ ๊ณตํ†ต์œผ๋กœ ๊ด€๋ฆฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: Metro ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—…๋ฐ์ดํŠธ์— ๋”ฐ๋ฅธ ๋ทฐ๋ชจ๋ธ ์ฃผ์ž… ๋ฐฉ์‹ ๊ฐœ์„  ๋ฐ ๋ฏธ์‚ฌ์šฉ ์ฝ”๋“œ ์ •๋ฆฌ

* **refactor: ๋ทฐ๋ชจ๋ธ ์ฃผ์ž… ๋ฐฉ์‹ ๋ณ€๊ฒฝ ๋ฐ `CompositionLocalProvider` ์ œ๊ฑฐ**
    * `MainActivity`(Android) ๋ฐ `MainViewController`(iOS)์—์„œ `LocalMetroViewModelFactory`๋ฅผ ํ†ตํ•ด `metroVmf`๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๊ณต๊ธ‰ํ•˜๋˜ `CompositionLocalProvider` ๋กœ์ง์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookScreen`์—์„œ `androidx.lifecycle.viewmodel.compose.viewModel` ๋Œ€์‹  `dev.zacsweers.metrox.viewmodel.metroViewModel`์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐฉ์‹์„ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `metroVmf` ๋ณ€์ˆ˜์™€ ๊ด€๋ จ ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: AndroidAppGraph ์ ‘๊ทผ ํ”„๋กœํผํ‹ฐ ์ด๋ฆ„ ์ˆ˜์ •**
    * `AndroidAppGraph.kt`์—์„œ `Context.appGraph` ํ™•์žฅ ํ”„๋กœํผํ‹ฐ๊ฐ€ ์ฐธ์กฐํ•˜๋Š” ํ•„๋“œ๋ช…์„ `festabookAppGraph`์—์„œ `androidAppGraph`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: iOS `MainViewController` ์ฝ”๋“œ ํฌ๋งทํŒ… ์ •๋ฆฌ**
    * ๋ณ€์ˆ˜ ์„ ์–ธ๋ถ€ ๋ฐ ํด๋กœ์ € ๋‚ด ๋“ค์—ฌ์“ฐ๊ธฐ๋ฅผ ์ •๋ฆฌํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: Android NotificationPermissionManager ๋ฒ„์ „ ๋ถ„๊ธฐ ๋กœ์ง ๊ฐœ์„  ๋ฐ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ

* **refactor: API ๋ ˆ๋ฒจ ์ฒดํฌ ๋กœ์ง ๊ณตํ†ตํ™”**
    * API 33(Tiramisu) ์ด์ƒ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” `runIfAtLeastTiramisu` ํ—ฌํผ ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์กฐ๊ฑด๋ถ€ ์‹คํ–‰ ๋กœ์ง์„ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `checkPermission` ๋ฐ `requestPermission` ๋ฉ”์„œ๋“œ์—์„œ ์œ„ ํ—ฌํผ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๊ตฌ์กฐ๋ฅผ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ ์ •๋ฆฌ**
    * ๋กœ์ง ๊ฐœ์„ ์— ๋”ฐ๋ผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฒŒ ๋œ `androidx.annotation.RequiresApi` ์ž„ํฌํŠธ ๋ฐ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: iOS `rememberPermissionLauncher` ๋‚ด ์ฝœ๋ฐฑ์˜ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์‹คํ–‰ ๋ณด์žฅ ๋ฐ ์•ˆ์ •์„ฑ ๊ฐœ์„ 

* `UNUserNotificationCenter.requestAuthorizationWithOptions`์˜ ๊ฒฐ๊ณผ ์ฝœ๋ฐฑ์„ `Dispatchers.Main`์—์„œ ์‹คํ–‰ํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ UI ์—…๋ฐ์ดํŠธ ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.
* `remember`์™€ `rememberUpdatedState`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ถŒํ•œ ์š”์ฒญ ๋žŒ๋‹ค๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๊ณ , ์ตœ์‹  ์ƒํƒœ์˜ `onResult` ์ฝœ๋ฐœ์„ ์ฐธ์กฐํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: iOS Info.plist ํŒŒ์ผ ๊ตฌ์กฐ ๋ฐ ํฌ๋งท ์ˆ˜์ •

* `Info.plist` ๋‚ด ์ž˜๋ชป ๊ตฌ๋ถ„๋˜์–ด ์žˆ๋˜ `<dict>` ํƒœ๊ทธ๋ฅผ ํ•˜๋‚˜๋กœ ๋ณ‘ํ•ฉํ•˜์—ฌ ํŒŒ์ผ ๊ตฌ์กฐ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
* `CFBundleShortVersionString`, `CFBundleVersion` ๋“ฑ ์ฃผ์š” ํ‚ค์˜ ๋“ค์—ฌ์“ฐ๊ธฐ๋ฅผ ์ˆ˜์ •ํ•˜๊ณ  ํŒŒ์ผ ์ „๋ฐ˜์˜ ๊ณต๋ฐฑ ๋ฌธ์ž๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: composeApp build.gradle.kts ๋‚ด ์ค‘๋ณต๋œ buildConfigField ์ œ๊ฑฐ

* `build.gradle.kts`: ์ค‘๋ณต์œผ๋กœ ์ •์˜๋˜์–ด ์žˆ๋˜ `FESTABOOK_URL` ๋ฐ `FESTABOOK_IMAGE_URL` ๋นŒ๋“œ ์„ค์ • ํ•„๋“œ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: SettingViewModel ๋‚ด ์•Œ๋ฆผ ์„ค์ • ์„ฑ๊ณต ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ์  ์ˆ˜์ •

* `saveFestivalNotification` ํ˜ธ์ถœ ์ „ ์ฆ‰์‹œ ๋ฐœ์ƒํ•˜๋˜ `_success` ์ด๋ฒคํŠธ๋ฅผ API ์š”์ฒญ์ด ์„ฑ๊ณตํ•œ ์‹œ์ (`onSuccess`)์— ๋ฐœ์ƒํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ์™€์˜ ์ผ๊ด€์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: iOS ํ”„๋กœ์ ํŠธ ์„ค์ • ๋ฐ Info.plist ํฌ๋งท ์ˆ˜์ •

* `Info.plist`: `CFBundleShortVersionString` ๋ฐ `CFBundleVersion` ํ•ญ๋ชฉ์˜ ๋“ค์—ฌ์“ฐ๊ธฐ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
* `project.pbxproj`: `Configuration` ๊ทธ๋ฃน์— `exceptions` ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , `Embed Pods Frameworks` ๋นŒ๋“œ ๋‹จ๊ณ„์—์„œ ๋ถˆํ•„์š”ํ•œ ๋นˆ ๊ฒฝ๋กœ ํ•„๋“œ(`inputPaths`, `outputPaths`)๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
* feat: Android ํ”Œ๋žซํผ Firebase Cloud Messaging(FCM) ๋„์ž… ๋ฐ ๋””๋ฐ”์ด์Šค ์‹๋ณ„์ž ๊ด€๋ฆฌ ๊ตฌํ˜„

* **feat: Firebase Cloud Messaging(FCM) ์—ฐ๋™ ๋ฐ ์•Œ๋ฆผ ์„œ๋น„์Šค ๊ตฌํ˜„**
    * `libs.versions.toml` ๋ฐ `build.gradle.kts`์— `firebase-messaging` ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MyFirebaseMessagingService`๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํ‘ธ์‹œ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ (`onMessageReceived`) ๋ฐ ํ† ํฐ ๊ฐฑ์‹ (`onNewToken`) ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  `AndroidManifest.xml`์— ๋“ฑ๋กํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `NotificationHelper`๋ฅผ ์‹ ๊ทœ ์ƒ์„ฑํ•˜์—ฌ ์•Œ๋ฆผ ์ฑ„๋„ ์„ค์ • ๋ฐ ์•Œ๋ฆผ ํด๋ฆญ ์‹œ ํŠน์ • ๊ณต์ง€์‚ฌํ•ญ์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋”ฅ๋งํฌ ํŽœ๋”ฉ ์ธํ…ํŠธ ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ์•ฑ ์ดˆ๊ธฐํ™” ์‹œ ๋””๋ฐ”์ด์Šค ์‹๋ณ„์ž ๋ฐ FCM ํ† ํฐ ๊ด€๋ฆฌ**
    * `FestabookApp` ํด๋ž˜์Šค์— ์ข…์†์„ฑ ์ฃผ์ž…(Metro)์„ ์—ฐ๊ฒฐํ•˜๊ณ  ์•ฑ ์‹œ์ž‘ ์‹œ ํ•„์š”ํ•œ ์ดˆ๊ธฐํ™” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋””๋ฐ”์ด์Šค ๊ณ ์œ  UUID๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜์—ฌ `DeviceLocalDataSource`์— ์ €์žฅํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FirebaseMessaging`์„ ํ†ตํ•ด FCM ํ† ํฐ์„ ์ทจ๋“ํ•˜๊ณ  `FcmDataSource`๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„/๋กœ์ปฌ์— ๊ฐฑ์‹ ํ•˜๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•ฑ ์‹คํ–‰ ์‹œ Crashlytics์˜ ๋ฏธ์ „์†ก ๋ฆฌํฌํŠธ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ์ฆ‰์‹œ ์ „์†กํ•˜๋„๋ก `sendUnsentReports()`๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

* **refactor: Android ์˜์กด์„ฑ ์ฃผ์ž… ๊ทธ๋ž˜ํ”„ ํ™•์žฅ**
    * `AndroidAppGraph` ์ธํ„ฐํŽ˜์ด์Šค์— `FestabookApp` ์ธ์Šคํ„ด์Šค ์ฃผ์ž…์„ ์œ„ํ•œ `inject` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AppUpdateManager`๋ฅผ ์ œ๊ณตํ•˜๋Š” `@Provides` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ด€๋ จ ๊ธฐ๋Šฅ ํ™•์žฅ์„ ์ค€๋น„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `DeviceRepository` ๋‚ด ๊ธฐ๊ธฐ ๋“ฑ๋ก ๋กœ์ง ๊ฐœ์„  ๋ฐ ์ฑ…์ž„ ์ด๋™

* **refactor: `DeviceRepository` ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ ๊ตฌํ˜„์ฒด ๋‚ด `registerDevice` ๋ฉ”์„œ๋“œ ์ˆ˜์ •**
    * ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ `registerDevice(fcmToken: String): Result<Unit>`์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ํ˜ธ์ถœ๋ถ€์˜ ์ฑ…์ž„์„ ์ค„์˜€์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด์— ์™ธ๋ถ€์—์„œ ์ฃผ์ž…๋ฐ›๋˜ `deviceIdentifier`๋ฅผ `DeviceLocalDataSource`์—์„œ ์ง์ ‘ ์กฐํšŒํ•˜๊ฑฐ๋‚˜ ์—†์„ ๊ฒฝ์šฐ `randomUUID()`๋ฅผ ํ†ตํ•ด ์ƒˆ๋กœ ์ƒ์„ฑํ•˜์—ฌ ์ €์žฅํ•˜๋„๋ก ๋‚ด๋ถ€ ๋กœ์ง์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ „๋‹ฌ๋ฐ›์€ `fcmToken`์ด ๋กœ์ปฌ์— ์ €์žฅ๋˜์–ด ์žˆ์ง€ ์•Š์€ ๊ฒฝ์šฐ `FcmDataSource`๋ฅผ ํ†ตํ•ด ์ €์žฅํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋“ฑ๋ก ์„ฑ๊ณต ์‘๋‹ต์„ ๋ฐ›์€ ๊ฒฝ์šฐ, ๋ฐ˜ํ™˜๋œ ๊ธฐ๊ธฐ ID๋ฅผ `saveDeviceId`๋ฅผ ํ†ตํ•ด ์ž๋™์œผ๋กœ ์ €์žฅํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ `Result<Long>`์—์„œ `Result<Unit>`์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ์บก์Аํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ํ”Œ๋žซํผ๋ณ„ ๋”ฅ๋งํฌ ํ•ธ๋“ค๋Ÿฌ ๋ฐ UUID ์ƒ์„ฑ ๊ตฌํ˜„

* **feat: ํ”Œ๋žซํผ๋ณ„ ๊ณตํ†ต `RememberDeepLinkHandler` ๊ตฌํ˜„**
    * `commonMain`: ๋”ฅ๋งํฌ ํ‚ค ์ •์˜(`DeepLinkKeys`) ๋ฐ `RememberDeepLinkHandler` expect ํ•จ์ˆ˜๋ฅผ ์„ ์–ธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **Android**: `ComponentActivity`์˜ `Intent`์™€ `OnNewIntentListener`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋”ฅ๋งํฌ ์ด๋ฒคํŠธ๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **iOS**: `NSNotificationCenter`๋ฅผ ํ†ตํ•ด `OpenURLNotification`์„ ๊ด€์ฐฐํ•˜์—ฌ ์ „๋‹ฌ๋œ `NSURL`์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ํ”Œ๋žซํผ๋ณ„ UUID ์ƒ์„ฑ ํ•จ์ˆ˜ ๊ตฌํ˜„**
    * `commonMain`: ๊ณ ์œ  ์‹๋ณ„์ž ์ƒ์„ฑ์„ ์œ„ํ•œ `randomUUID()` expect ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **Android**: `java.util.UUID`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **iOS**: `platform.Foundation.NSUUID`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: Android ์•Œ๋ฆผ(Notification) ์„ค์ • ๋ฐ ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ**
    * `NotificationHelper`: ์•Œ๋ฆผ ์•„์ด์ฝ˜ ์ƒ‰์ƒ์„ ํ…Œ๋งˆ ์ปฌ๋Ÿฌ(`FestabookColorPalette`)์™€ ์—ฐ๋™ํ•˜๊ณ , `MainActivity` ์ฐธ์กฐ ๊ฒฝ๋กœ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MyFirebaseMessagingService`: ๋ถˆํ•„์š”ํ•œ ๋กœ๊ทธ(Timber) ์ถœ๋ ฅ์„ ์ œ๊ฑฐํ•˜๊ณ , ์•Œ๋ฆผ ์ œ๋ชฉ/๋‚ด์šฉ์ด ์—†์„ ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด `strings.xml`์˜ ๊ธฐ๋ณธ๊ฐ’์„ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **Resource**: ์•Œ๋ฆผ ์ „์šฉ ์•„์ด์ฝ˜๋“ค์„ `commonMain`์—์„œ `androidMain` ๋ฆฌ์†Œ์Šค๋กœ ์ด๋™ํ•˜๊ณ , FCM์šฉ ๊ธฐ๋ณธ ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ๋„คํŠธ์›Œํฌ ํ†ต์‹  ๋กœ์ง ๊ฐœ์„  ๋ฐ Android ๋””๋ฐ”์ด์Šค ๋“ฑ๋ก ํ”„๋กœ์„ธ์Šค ์ตœ์ ํ™”

* **refactor: Ktor HttpClient ์„ค์ • ๋ฐ JSON ์ง๋ ฌํ™” ๋กœ์ง ๊ฐœ์„ **
    * `ContentNegotiation` ์„ค์ • ์‹œ `ContentType.Text.JavaScript` ํƒ€์ž…์— ๋Œ€ํ•ด ๊ณตํ†ต `Json` ์ธ์Šคํ„ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.
    * `DefaultRequest` ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ชจ๋“  ์š”์ฒญ์˜ ๊ธฐ๋ณธ `Content-Type` ํ—ค๋”๋ฅผ `application/json`์œผ๋กœ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: Android ๋””๋ฐ”์ด์Šค ์‹๋ณ„์ž ๋ฐ FCM ํ† ํฐ ๋“ฑ๋ก ๋กœ์ง ๊ฐœ์„ **
    * `FestabookApp`์—์„œ `DeviceLocalDataSource` ๋ฐ `FcmDataSource`๋ฅผ ์ง์ ‘ ์ฐธ์กฐํ•˜๋˜ ๋ฐฉ์‹์„ `DeviceRepository` ์ฃผ์ž…์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๊ณ„์ธต ๊ฐ„ ์˜์กด์„ฑ์„ ์ •์ œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `setupDeviceIdentifiers` ๋ฉ”์„œ๋“œ์—์„œ UUID๋ฅผ ์ˆ˜๋™์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ  ์ €์žฅํ•˜๋˜ ๋กœ์ง์„ `DeviceRepository.registerDevice` ํ˜ธ์ถœ๋กœ ํ†ตํ•ฉํ•˜์—ฌ ๊ด€๋ฆฌ ํšจ์œจ์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.
    * `AndroidAppGraph`์— `FirebaseMessaging` ๊ฐ์ฒด๋ฅผ ์ œ๊ณตํ•˜๋Š” `@Provides` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: Android ํ”„๋กœ์ ํŠธ ์„ค์ • ๋ฐ ๋งค๋‹ˆํŽ˜์ŠคํŠธ ์ˆ˜์ •**
    * `AndroidManifest.xml` ๋‚ด `MyFirebaseMessagingService`์˜ ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ๋ฅผ `.presentation.service`๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `libs.versions.toml`์—์„œ `android-minSdk` ๋ฒ„์ „์„ `24`์—์„œ `28`๋กœ ์ƒํ–ฅ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: Android ํ”Œ๋žซํผ ์„œ๋น„์Šค ํด๋ž˜์Šค ํŒจํ‚ค์ง€ ์œ„์น˜ ๋ณ€๊ฒฝ

* **refactor: `MyFirebaseMessagingService` ๋ฐ `NotificationHelper` ํŒจํ‚ค์ง€ ์ด๋™**
    * ์„œ๋น„์Šค ๊ด€๋ จ ํด๋ž˜์Šค๋“ค์„ `com.daedan.festabook.presentation.service`์—์„œ `com.daedan.festabook.service` ํŒจํ‚ค์ง€๋กœ ์ด๋™ํ•˜์—ฌ ๊ตฌ์กฐ๋ฅผ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํŒจํ‚ค์ง€ ๋ณ€๊ฒฝ์— ๋”ฐ๋ผ `AndroidManifest.xml` ๋‚ด์˜ ์„œ๋น„์Šค ์„ ์–ธ ๊ฒฝ๋กœ๋ฅผ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookApp.kt` ๋“ฑ ๊ด€๋ จ ์ฐธ์กฐ ํŒŒ์ผ์˜ ์ž„ํฌํŠธ ๊ฒฝ๋กœ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: iOS Firebase Cloud Messaging(FCM) ์„ค์ • ๋ฐ ๊ด€๋ จ ์˜์กด์„ฑ ์ถ”๊ฐ€

* **feat: iOS ํ”„๋กœ์ ํŠธ ๋‚ด Firebase Messaging ๋ฐ Core ์ถ”๊ฐ€**
    * `iosApp/Podfile` ๋ฐ `composeApp/build.gradle.kts`์— `FirebaseMessaging`๊ณผ `FirebaseCore` ํฌ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `Podfile.lock`์— ๊ด€๋ จ ์˜์กด์„ฑ ๋ฒ„์ „์„ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: iOS ์•Œ๋ฆผ ๊ถŒํ•œ ๋ฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ชจ๋“œ ์„ค์ •**
    * `iosApp/Info.plist`: `UIBackgroundModes`์— `remote-notification`์„ ์ถ”๊ฐ€ํ•˜๊ณ  `FirebaseAppDelegateProxyEnabled`๋ฅผ `false`๋กœ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `iosApp/iosAppDebug.entitlements`: ๋””๋ฒ„๊ทธ ํ™˜๊ฒฝ์„ ์œ„ํ•œ `aps-environment` ๊ถŒํ•œ ์„ค์ •์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: iOS ๋นŒ๋“œ ์„ค์ • ๋ฐ ๋ถˆํ•„์š”ํ•œ ํŒŒ์ผ ์ œ๊ฑฐ**
    * `NativeBuildKonfig.kt`: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” iOS ์ „์šฉ Konfig ๊ฐ์ฒด๋ฅผ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `iosApp.xcodeproj`: ๋””๋ฒ„๊ทธ ๋นŒ๋“œ ์‹œ `CODE_SIGN_ENTITLEMENTS`๋ฅผ ์ง€์ •ํ•˜๊ณ , ๊ด€๋ จ ๋นŒ๋“œ ๋‹จ๊ณ„(Shell Script) ์ˆœ์„œ๋ฅผ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `NetworkBindings.kt`: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” Ktor headers ๊ด€๋ จ import๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: DI ๊ทธ๋ž˜ํ”„ ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ iOS ํ”Œ๋žซํผ ์•ฑ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๋กœ์ง ํ†ตํ•ฉ

* **refactor: CoroutineScope ์ œ๊ณต ๋กœ์ง์„ ๊ณตํ†ต(commonMain) ์˜์—ญ์œผ๋กœ ์ด๋™**
    * `AndroidAppGraph`์— ์ •์˜๋˜์–ด ์žˆ๋˜ `CoroutineScope` ์ œ๊ณต ๋กœ์ง์„ `FestabookAppGraph`๋กœ ์ด๋™ํ•˜์—ฌ ํ”Œ๋žซํผ ๊ฐ„ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: iOS ํ”Œ๋žซํผ DI ๋ฐ ์›๊ฒฉ ์•Œ๋ฆผ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๊ธฐ๋ฐ˜ ์ž‘์—…**
    * `IosAppGraph`์— `DeviceRepository` ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AppDelegate.swift`์—์„œ ์ง์ ‘ ์ฒ˜๋ฆฌํ•˜๋˜ Firebase ๋ฐ ๋„ค์ด๋ฒ„ ์ง€๋„ ์„ค์ • ๋กœ์ง์„ KMP ๊ณตํ†ต ๋กœ์ง์ธ `DefaultFestabookAppDelegate`๋กœ ์œ„์ž„ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `didRegisterForRemoteNotificationsWithDeviceToken` ๋ฐ `didReceiveRemoteNotification` ์ฝœ๋ฐฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ iOS ์›๊ฒฉ ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์—ฐ๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๋ถˆํ•„์š”ํ•œ ์ž„ํฌํŠธ ์ œ๊ฑฐ**
    * `iOSApp.swift` ๋‚ด ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `FirebaseCore` ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ํ”Œ๋žซํผ๋ณ„ ์ถ•์ œ ์•Œ๋ฆผ ๋“ฑ๋ก API ๊ฒฝ๋กœ ๋™์  ์ฒ˜๋ฆฌ

* **feat: ์ถ•์ œ ์•Œ๋ฆผ API ์—”๋“œํฌ์ธํŠธ ํ”Œ๋žซํผ ํŒŒ๋ผ๋ฏธํ„ฐํ™”**
    * `FestivalNotificationService`: ๊ธฐ์กด์— `android`๋กœ ๊ณ ์ •๋˜์–ด ์žˆ๋˜ API ๊ฒฝ๋กœ๋ฅผ `{platform}` ๊ฐ€๋ณ€ ๊ฒฝ๋กœ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ์š”์ฒญ ์‹œ ํ”Œ๋žซํผ ์ •๋ณด๋ฅผ ์ „๋‹ฌ๋ฐ›๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestivalNotificationRemoteDataSourceImpl`: ์•Œ๋ฆผ ์ €์žฅ ์š”์ฒญ ์‹œ `festivalNotificationPlatform()`์„ ํ˜ธ์ถœํ•˜์—ฌ ํ˜„์žฌ ํ”Œ๋žซํผ์— ๋งž๋Š” ๊ฒฝ๋กœ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ํ”Œ๋žซํผ๋ณ„ ์‹๋ณ„์ž ๋ฐ˜ํ™˜์„ ์œ„ํ•œ expect/actual ํ•จ์ˆ˜ ์ถ”๊ฐ€**
    * `commonMain`: ํ”Œ๋žซํผ๋ณ„ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•˜๋Š” `festivalNotificationPlatform()` expect ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `androidMain` / `iosMain`: ๊ฐ ํ”Œ๋žซํผ์— ๋Œ€์‘ํ•˜๋Š” "android" ๋ฐ "ios" ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก actual ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: iOS ํ”Œ๋žซํผ AppDelegate ๋ฐ Firebase Messaging ๋Œ€๋ฆฌ์ž ๊ตฌํ˜„

* **feat: iOS ์•ฑ ์ƒ๋ช…์ฃผ๊ธฐ ๋ฐ DI ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ `FestabookAppDelegate` ์ถ”๊ฐ€**
    * `DefaultFestabookAppDelegate` ์‹ฑ๊ธ€ํ†ค ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด `IosAppGraph` ๋ฐ ์•ฑ ์ดˆ๊ธฐํ™” ๋กœ์ง(Firebase, Naver Map ์„ค์ •)์„ ํ†ตํ•ฉ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
    * Swift ์ฝ”๋“œ์™€์˜ ์ƒํ˜ธ์šด์šฉ์„ฑ์„ ์œ„ํ•ด `FestabookAppDelegate`, `AppGraphDelegate` ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: iOS ์•Œ๋ฆผ ๋ฐ FCM ์—ฐ๋™ ๊ธฐ๋Šฅ ๊ตฌํ˜„**
    * `DefaultFirebaseMessagingDelegate`: FCM ๋“ฑ๋ก ํ† ํฐ ์ˆ˜์‹  ์‹œ `DeviceRepository`๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„์— ๊ธฐ๊ธฐ๋ฅผ ๋“ฑ๋กํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DefaultUserNotificationDelegate`: ํฌ๊ทธ๋ผ์šด๋“œ ์ƒํƒœ์—์„œ์˜ ์•Œ๋ฆผ ํ‘œ์‹œ ๋ฐ ์†Œ๋ฆฌ ์˜ต์…˜์„ ์ œ์–ดํ•˜๋Š” `UNUserNotificationCenterDelegateProtocol`์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: iOS DI ๊ทธ๋ž˜ํ”„ ์ฐธ์กฐ ๋ฐฉ์‹ ๋ณ€๊ฒฝ**
    * `RememberAppGraph.ios.kt`์—์„œ ๋งค๋ฒˆ `createGraph()`๋ฅผ ํ˜ธ์ถœํ•˜๋˜ ๋ฐฉ์‹์—์„œ `DefaultFestabookAppDelegate`์— ์‹ฑ๊ธ€ํ†ค์œผ๋กœ ์ƒ์„ฑ๋œ ์ธ์Šคํ„ด์Šค๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: Android ๋ฐ iOS ์ง„์ž…์ ์˜ `FestabookScreen` ํ˜ธ์ถœ ์ฃผ์„ ์ฒ˜๋ฆฌ ๋ฐ ๋ฏธ์‚ฌ์šฉ ์ฝ”๋“œ ์ •๋ฆฌ

* **refactor: Android ๋ฐ iOS ํ”Œ๋žซํผ ๊ณตํ†ต UI ํ˜ธ์ถœ ๋กœ์ง ์ˆ˜์ •**
    * `MainActivity` ๋ฐ `MainViewController`์—์„œ `FestabookScreen` ํ˜ธ์ถœ๋ถ€๋ฅผ ์ฃผ์„ ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * iOS `MainViewController`์—์„œ ๋ถˆํ•„์š”ํ•œ `exit(0)` ๊ด€๋ จ ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ํ”Œ๋žซํผ๋ณ„ ์˜์กด์„ฑ ์ฃผ์ž…(DI) ๊ทธ๋ž˜ํ”„ ์ •๋ฆฌ**
    * `AndroidAppGraph.kt`: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `CoroutineScope`, `Dispatchers`, `SingleIn` ๋“ฑ์˜ ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `IosAppGraph.kt`: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `ExploreRepository`, `FestivalNotificationRepository` ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: Android `MainActivity` ์ƒˆ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ๋กœ์ง ์ถ”๊ฐ€**
    * `MainActivity` ๋‚ด๋ถ€์— `FLAG_ACTIVITY_NEW_TASK` ๋ฐ `FLAG_ACTIVITY_CLEAR_TASK` ํ”Œ๋ž˜๊ทธ๋ฅผ ์„ค์ •ํ•˜์—ฌ ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์ƒˆ๋กœ ์‹œ์ž‘ํ•˜๋Š” `newIntent` companion object ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainActivity`์—์„œ ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `metroVmf` ๋ณ€์ˆ˜๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: iOS FCM ์•Œ๋ฆผ ์ˆ˜์‹  ๋ฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ ๋กœ์ง ๋ณด์™„

* **feat: `DefaultFestabookAppDelegate` ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€**
    * ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์•Œ๋ฆผ ์ˆ˜์‹  ์‹œ ํ˜ธ์ถœ๋˜๋Š” `application` ๋ฉ”์„œ๋“œ์— `UIBackgroundFetchResultNewData`๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ์ˆ˜์‹  ์ฒ˜๋ฆฌ๋ฅผ ๋ช…ํ™•ํžˆ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `platform.UIKit.registerForRemoteNotifications` ์ž„ํฌํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **fix: `AppDelegate.swift` ํƒ€์ž… ์บ์ŠคํŒ… ๋ฐ ์•ˆ์ •์„ฑ ๊ฐœ์„ **
    * `fetchCompletionHandler` ํ˜ธ์ถœ ์‹œ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ•์ œ ์บ์ŠคํŒ…(`as!`)ํ•˜๋˜ ๋ฐฉ์‹์„ `UIBackgroundFetchResult` ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•œ ์•ˆ์ „ํ•œ ๋ณ€ํ™˜ ๋ฐฉ์‹์œผ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: iOS ํ”Œ๋žซํผ๋ณ„ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ์ฃผ์„ ์ถ”๊ฐ€**
    * `DefaultFirebaseMessagingDelegate`: FCM ํ† ํฐ ๋“ฑ๋ก ์„น์…˜์— ์ฃผ์„์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DefaultUserNotificationDelegate`: ํฌ๊ทธ๋ผ์šด๋“œ ์•Œ๋ฆผ ์ˆ˜์‹  ์„น์…˜์— ์ฃผ์„์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: FestivalNotificationService ํŒจํ‚ค์ง€ ์ด๋™ ๋ฐ API ์š”์ฒญ ๋กœ์ง ๊ฐœ์„ 

* **refactor: FestivalNotificationService ๊ด€๋ จ ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ ์˜คํƒ€ ์ˆ˜์ •**
    * `FestivalNotificationService` ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ `com.daedan.festabook.data.service.platform`์—์„œ `com.daedan.festabook.data.service` ํŒจํ‚ค์ง€๋กœ ์ด๋™ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `saveFestivalNotification` API ์—”๋“œํฌ์ธํŠธ ๊ฒฝ๋กœ์˜ ์ค‘๊ด„ํ˜ธ ์˜คํƒ€(`{platform}}` -> `{platform}`)๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestivalNotificationRemoteDataSourceTest`์—์„œ `saveFestivalNotification` ํ˜ธ์ถœ ์‹œ ํ”Œ๋žซํผ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: DeviceRepositoryImpl ๊ธฐ๊ธฐ ๋“ฑ๋ก ๋กœ์ง ๋‹จ์ˆœํ™”**
    * `registerDevice` ๋ฉ”์„œ๋“œ์—์„œ FCM ํ† ํฐ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋˜ ์กฐ๊ฑด๋ถ€ ๋กœ์ง์„ ์ œ๊ฑฐํ•˜๊ณ , ์ „๋‹ฌ๋ฐ›์€ ํ† ํฐ์„ ํ•ญ์ƒ ์ €์žฅ ๋ฐ ์‚ฌ์šฉํ•˜๋„๋ก ๋กœ์ง์„ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: applicationScope ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝ

* `FestabookApp`์—์„œ ์ง์ ‘ ์ƒ์„ฑํ•˜๋˜ `applicationScope`๋ฅผ `@Inject`๋ฅผ ํ†ตํ•ด ์ฃผ์ž…๋ฐ›๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ์˜์กด์„ฑ ๊ด€๋ฆฌ๋ฅผ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: Android RememberDeepLinkHandler ๋‚ด ComponentActivity ํ˜•๋ณ€ํ™˜ ์˜ˆ์™ธ ๋ฐฉ์ง€

* LocalContext๋ฅผ ComponentActivity๋กœ ์บ์ŠคํŒ…ํ•  ๋•Œ ์•ˆ์ „ํ•œ ์บ์ŠคํŒ…(`as?`)๊ณผ ์กฐ๊ธฐ ๋ฐ˜ํ™˜์„ ์ ์šฉํ•˜์—ฌ ๋ฐœ์ƒ ๊ฐ€๋Šฅํ•œ `ClassCastException`์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์•ˆ์ •์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* refactor: Android ๋ฐ iOS ํ”Œ๋žซํผ ์ฝ”๋“œ ๋‚ด ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ์ œ๊ฑฐ

* `DefaultFestabookAppDelegate.kt` (iOS): ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `platform.Foundation.NSLog` ์ž„ํฌํŠธ ์ œ๊ฑฐ
* `FestabookApp.kt` (Android): ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `kotlinx.coroutines.Dispatchers` ๋ฐ `kotlinx.coroutines.SupervisorJob` ์ž„ํฌํŠธ ์ œ๊ฑฐ
* chore: Napier ๋กœ๊น… ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€ ๋ฐ Android ์ดˆ๊ธฐํ™” ์„ค์ •

* **chore: Napier ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์˜์กด์„ฑ ์ถ”๊ฐ€**
    * `libs.versions.toml`์— Napier(v2.7.1)๋ฅผ ์ •์˜ํ•˜๊ณ , `commonMain` ์†Œ์Šค์…‹์— ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: Android ํ”Œ๋žซํผ ๋‚ด Napier ์ดˆ๊ธฐํ™” ๋กœ์ง ๊ตฌํ˜„**
    * `FestabookApp` ํด๋ž˜์Šค์— ๋””๋ฒ„๊ทธ ๋นŒ๋“œ(`BuildConfig.DEBUG`) ์‹œ์—๋งŒ ๋กœ๊ทธ๊ฐ€ ์ถœ๋ ฅ๋˜๋„๋ก `DebugAntilog`๋ฅผ ์„ค์ •ํ•˜๋Š” `setupNapier()` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

# Conflicts:
#	composeApp/src/androidMain/kotlin/com/daedan/festabook/FestabookApp.kt

* fix: iOS `AppDelegate` ์›๊ฒฉ ์•Œ๋ฆผ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ํƒ€์ž… ๋ณ€ํ™˜ ๋กœ์ง ์ˆ˜์ •

* `didReceiveRemoteNotification` ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ `UIBackgroundFetchResult` ์ƒ์„ฑ ์‹œ `result.value.uint64Value`๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ํƒ€์ž… ๋ณ€ํ™˜ ๋กœ์ง์„ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* style: FestabookApp.kt ๋‚ด import ๊ตฌ๋ฌธ ์ •๋ ฌ

* `FestabookApp.kt` ํŒŒ์ผ์˜ import ๋ฌธ ์ˆœ์„œ๋ฅผ ์ •๋ ฌํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
* feat: ํ”Œ๋žซํผ ๊ณตํ†ต ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„ ๊ตฌํ˜„ ๋ฐ ๋ชจ๋“ˆํ™”

* **feat: ์‹ ๊ทœ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„ ์ •์˜**
    * `HomeNavigation.kt`: `homeNavGraph`๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํ™ˆ ํ™”๋ฉด ์ง„์ž… ์‹œ ์ฒซ ๋ฐฉ๋ฌธ ํŒ์—…(`FirstVisitDialog`) ๋…ธ์ถœ ๋กœ์ง ๋ฐ `HomeScreen` ์—ฐ๊ฒฐ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainNavigation.kt`: `mainNavGraph`๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ฉ”์ธ ํ™”๋ฉด์˜ ํ•˜์œ„ ํƒญ๋“ค์—์„œ ๊ณต์œ ํ•  ViewModel๋“ค์„ `mainBackEntry` ๋ฒ”์œ„๋กœ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  `MainScreen`์œผ๋กœ ์ „๋‹ฌํ•˜๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceMapNavigation.kt`: `placeMapNavGraph`๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , `PlaceDetail` ์ด๋™ ์‹œ `PlaceUiModel` ๋“ฑ์„ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•œ ์ปค์Šคํ…€ `NavType`๊ณผ ํ™”๋ฉด ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜(์Šฌ๋ผ์ด๋“œ ๋ฐ ํŽ˜์ด๋“œ)์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ๊ธฐ์กด ๋„ค๋น„๊ฒŒ์ด์…˜ ์ฝ”๋“œ ๋ณต๊ตฌ ๋ฐ ์ •๋ฆฌ**
    * `NewsNavigation.kt`, `SettingNavigation.kt`, `ScheduleNavigation.kt`: ์ฃผ์„ ์ฒ˜๋ฆฌ๋˜์–ด ์žˆ๋˜ ๊ฐ ํƒญ๋ณ„ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„ ๊ตฌํ˜„๋ถ€๋ฅผ ๋ณต๊ตฌํ•˜๊ณ  ๋ฏธ์‚ฌ์šฉ `@file:Suppress` ์„ค์ •์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ํ™ˆ ํ™”๋ฉด UI ๊ตฌํ˜„ ๋ฐ ๋ฉ”์ธ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๊ตฌ์กฐ ๊ฐœ์„ 

* **feat: ํ™ˆ ํ™”๋ฉด(Home) ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€**
    * `HomeScreen`: ์ถ•์ œ ๊ฐœ์š”, ํฌ์Šคํ„ฐ ๋ฆฌ์ŠคํŠธ, ๋ผ์ธ์—… ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ๋ฉ”์ธ ํ™ˆ ํ™”๋ฉด์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomePosterList`: `HorizontalPager`๋ฅผ ์ด์šฉํ•ด ์ถ•์ œ ํฌ์Šคํ„ฐ๋ฅผ ๋ฌดํ•œ ์Šคํฌ๋กค ๋ฐ ์Šค์ผ€์ผ ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ ํ‘œ์‹œํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeHeader` & `HomeFestivalInfo`: ๋Œ€ํ•™๊ต ์ด๋ฆ„ ๋ฐ ์ถ•์ œ ์ƒ์„ธ ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•˜๋Š” ํ—ค๋”๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeLineupItem` & `HomeArtistItem`: ์ผ์ž๋ณ„ ์•„ํ‹ฐ์ŠคํŠธ ๋ผ์ธ์—…์„ ๊ฐ€๋กœ ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ ํ‘œ์‹œํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ๋ฉ”์ธ ํ™”๋ฉด(Main) ๋ฐ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„**
    * `MainScreen`: ํ•˜๋‹จ ํƒญ ๋‚ด๋น„๊ฒŒ์ด์…˜๊ณผ ๊ฐ ํ™”๋ฉด๋ณ„ `NavHost`๋ฅผ ํ†ตํ•ฉ ๊ด€๋ฆฌํ•˜๋Š” ๋ฉ”์ธ ์ง„์ž…์ ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookBottomNavigationBar`: ์ปค์Šคํ…€ ๋””์ž์ธ์ด ์ ์šฉ๋œ ํ•˜๋‹จ ํƒญ ๋ฐ”๋ฅผ ์ถ”๊ฐ€ํ–ˆ์œผ๋ฉฐ, ์ค‘์•™ ์ง€๋„(PLACE_MAP) ํƒญ์— ํŠน์ˆ˜ ์•„์ด์ฝ˜ ๋ฐ ๋ ˆ์ด์•„์›ƒ์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FirstVisitDialog`: ์•ฑ ์ตœ์ดˆ ๋ฐฉ๋ฌธ ์‹œ ์•Œ๋ฆผ ๊ถŒํ•œ ๋“ฑ์„ ์•ˆ๋‚ดํ•˜๋Š” ํŒ์—… ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ViewModel ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐ ๋กœ์ง ๊ฐœ์„ **
    * `MainViewModel`: ๊ธฐ๊ธฐ UUID ๋ฐ FCM ํ† ํฐ ๋“ฑ๋ก, ๋’ค๋กœ๊ฐ€๊ธฐ ์ด์ค‘ ํด๋ฆญ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ, ์ตœ์ดˆ ๋ฐฉ๋ฌธ ์—ฌ๋ถ€ ํ™•์ธ ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceDetailViewModel`: Metro ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•œ `@AssistedInject` ๋ฐ `ViewModelAssistedFactory` ๊ตฌ์กฐ๋ฅผ ์ ์šฉํ•˜์—ฌ ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐฉ์‹์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MetroViewModelFactoryImpl`: `assistedFactoryProviders`๋ฅผ ์ฃผ์ž…๋ฐ›๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **fix: ๊ธฐํƒ€ ์ฝ”๋“œ ์ •๋ฆฌ**
    * `PlaceMapRoute` ๋‚ด ๋ถˆํ•„์š”ํ•œ `@Suppress` ์–ด๋…ธํ…Œ์ด์…˜์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ํ™ˆ ํ™”๋ฉด ํŽ˜์Šคํ‹ฐ๋ฒŒ ์ •๋ณด ๋ฐ ๋ผ์ธ์—… ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„

* **feat: ํ™ˆ ํ™”๋ฉด ์ฃผ์š” UI ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€**
    * `HomeHeader`: ๋Œ€ํ•™๊ต ์ด๋ฆ„ ํ‘œ์‹œ ๋ฐ ํ™•์žฅ ๋ฒ„ํŠผ์„ ํฌํ•จํ•œ ํ—ค๋” ๊ตฌํ˜„.
    * `HomeFestivalInfo`: ํŽ˜์Šคํ‹ฐ๋ฒŒ ์ด๋ฆ„๊ณผ ๊ธฐ๊ฐ„ ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€.
    * `HomePosterList`: `HorizontalPager`๋ฅผ ์ด์šฉํ•œ ๋ฌดํ•œ ์Šคํฌ๋กค ํฌ์Šคํ„ฐ ๋ฆฌ์ŠคํŠธ ๊ตฌํ˜„ (์ค‘์•™ ๊ฐ•์กฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํฌํ•จ).
    * `HomeLineupItem` & `HomeArtistItem`: ๋‚ ์งœ๋ณ„ ์•„ํ‹ฐ์ŠคํŠธ ๋ผ์ธ์—… ์ •๋ณด ๋ฐ ๊ฐœ๋ณ„ ์•„ํ‹ฐ์ŠคํŠธ ์•„์ดํ…œ ๋ ˆ์ด์•„์›ƒ ๊ตฌํ˜„.
    * `HomeLineupHeader`: ๋ผ์ธ์—… ์„น์…˜ ํƒ€์ดํ‹€ ๋ฐ ์ผ์ • ํ™”๋ฉด ์ด๋™ ๋ฒ„ํŠผ ์ถ”๊ฐ€.

* **feat: ํ™ˆ ํ™”๋ฉด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ฐ ์ƒํƒœ ๊ด€๋ฆฌ ์ถ”๊ฐ€**
    * `HomeViewModel`: ํŽ˜์Šคํ‹ฐ๋ฒŒ ์ •๋ณด ๋ฐ ๋ผ์ธ์—… ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๊ณ  `UiState`๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋กœ์ง ๊ตฌํ˜„.
    * `FestivalUiState` & `LineupUiState`: ๋กœ๋”ฉ, ์„ฑ๊ณต, ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ์ •์˜ํ•˜๋Š” Sealed Interface ์ถ”๊ฐ€.
    * `LineupItemUiModel` & `LineUpItemOfDayUiModel`: ๋„๋ฉ”์ธ ๋ชจ๋ธ์„ UI ํ‘œํ˜„์‹์— ๋งž๊ฒŒ ๋ณ€ํ™˜ํ•˜๋Š” Mapper ๋ฐ ๋ชจ๋ธ ์ •์˜.

* **refactor: ํ”Œ๋žซํผ๋ณ„ ํ…์ŠคํŠธ ์Šคํƒ€์ผ ๋Œ€์‘**
    * `HomeTextStyle`: Android์—์„œ ํฐํŠธ ํŒจ๋”ฉ์„ ์ œ๊ฑฐ(`includeFontPadding = false`)ํ•˜๊ธฐ ์œ„ํ•ด `expect`/`actual` ํ”„๋กœํผํ‹ฐ ์ถ”๊ฐ€.

* style: ๊ณตํ†ต UI ์•„์ด์ฝ˜ ๋ฆฌ์†Œ์Šค ์†์„ฑ ์ •๋ฆฌ

* **style: ๋ฒกํ„ฐ ์•„์ด์ฝ˜์˜ ๋ถˆํ•„์š”ํ•œ ์†์„ฑ ์ œ๊ฑฐ**
    * `ic_home.xml`, `ic_news.xml`, `ic_schedule.xml`, `ic_setting.xml` ํŒŒ์ผ์—์„œ `android:fillColor="?attr/colorControlNormal"` ์†์„ฑ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•„์ด์ฝ˜ ํ…Œ๋งˆ ๋ฐ ์ƒ‰์ƒ์„ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ๋™์ ์œผ๋กœ ์ œ์–ดํ•˜๊ธฐ ์œ„ํ•ด ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์†์„ฑ์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ic_schedule.xml`์˜ ๋ฏธ์‚ฌ์šฉ ๊ฒฝ๋กœ ๋ฐ์ดํ„ฐ ๋ฐ ์†์„ฑ์„ ์ •๋ฆฌํ•˜์—ฌ ๋ฆฌ์†Œ์Šค ํŒŒ์ผ์„ ๊ฒฝ๋Ÿ‰ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: Compottie ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฒ„์ „ ์—…๋ฐ์ดํŠธ

* **chore: Compottie ์˜์กด์„ฑ ๋ฒ„์ „ ์ƒํ–ฅ**
    * `libs.versions.toml` ํŒŒ์ผ์—์„œ `compottie` ๋ฒ„์ „์„ `2.0.3`์—์„œ `2.1.0`์œผ๋กœ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: Metro ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•œ ์˜์กด์„ฑ ์ฃผ์ž… ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ ํ”Œ๋žซํผ ์˜์กด์„ฑ ์ œ๊ฑฐ

* **refactor: `PlaceDetailViewModel` ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐฉ์‹ ๋ณ€๊ฒฝ**
    * Metro์˜ `assistedMetroViewModel`์„ ๋„์ž…ํ•˜์—ฌ `@AssistedInject` ๊ธฐ๋ฐ˜์˜ ViewModel ์ƒ์„ฑ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `CreationExtras.Key`์ธ `PlaceKey`์™€ `PlaceDetailKey`๋ฅผ ์ •์˜ํ•˜์—ฌ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ธ์ž๋ฅผ ViewModel์— ์•ˆ์ „ํ•˜๊ฒŒ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด์˜ ์ˆ˜๋™ `Factory` ์ƒ์„ฑ ๋กœ์ง์„ ์ œ๊ฑฐํ•˜๊ณ  `MutableCreationExtras`๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ๋„ค๋น„๊ฒŒ์ด์…˜ ์ปค์Šคํ…€ `NavType` ๋ฐ Android ์˜์กด์„ฑ ์ œ๊ฑฐ**
    * `PlaceMapNavigation.kt`์—์„œ `android.net.Uri` ๋ฐ `android.os.Bundle` ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * Compose Multiplatform ํ™˜๊ฒฝ์— ๋งž๊ฒŒ `androidx.savedstate.SavedState`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ์ฝ์–ด์˜ค๋„๋ก `defaultNavType` ๊ตฌํ˜„์„ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `Uri.decode`/`encode` ๋Œ€์‹  `kotlinx.serialization`์„ ์ด์šฉํ•œ ์ง์ ‘์ ์ธ JSON ์ง๋ ฌํ™” ๋ฐฉ์‹์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: Navigation ์ด๋ฒคํŠธ ๋ฐ ๋ฐฑํ•ธ๋“ค๋Ÿฌ ๊ฐœ์„ **
    * `androidx.navigationevent` ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋„์ž…ํ•˜์—ฌ `NavigationBackHandler` ๋ฐ `rememberNavigationEventState`๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด `BackHandler`๋ฅผ ๋Œ€์ฒดํ•˜์—ฌ ๋ณด๋‹ค ๊ณตํ†ตํ™”๋œ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐฑํ”„๋ ˆ์Šค ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์ฝ”๋“œ ์ •๋ฆฌ ๋ฐ ๊ตฌ์กฐ ์ตœ์ ํ™”**
    * `MainScreen`์—์„œ ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `placeDetailViewModelFactory` ์ฃผ์ž… ๋กœ์ง์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainViewModel`์˜ `registerDeviceAndFcmToken` ํ˜ธ์ถœ ๋“ฑ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๋‚จ์•„์žˆ๋˜ `LaunchedEffect` ๋ฐ ์ž„ํฌํŠธ ๊ตฌ๋ฌธ์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: iOS ๋”ฅ๋งํฌ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ตฌํ˜„ ๋ฐ Kotlin Multiplatform ๋Œ€์‘ ๋ฆฌํŒฉํ„ฐ๋ง

* **feat: iOS ์•Œ๋ฆผ ๊ธฐ๋ฐ˜ ๋”ฅ๋งํฌ ์ฒ˜๋ฆฌ ๊ตฌํ˜„**
    * `DefaultUserNotificationDelegate`: iOS ํฌ๊ทธ๋ผ์šด๋“œ ๋ฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์•Œ๋ฆผ ์ˆ˜์‹  ์‹œ `announcementId`๋ฅผ ์ถ”์ถœํ•˜์—ฌ `NSNotificationCenter`๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `RememberDeepLinkHandler.ios.kt`: `fcmNewsNotification` ์ด๋ฒคํŠธ๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ์•Œ๋ฆผ ์ƒ์„ธ ID ๋ฐ ์ด๋™ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ํฌํ•จํ•œ `Intent` ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `MainViewModel` ๋กœ์ง ๊ฐœ์„  ๋ฐ ์˜์กด์„ฑ ์ •๋ฆฌ**
    * `DeviceRepository` ์˜์กด์„ฑ ๋ฐ FCM ํ† ํฐ ์ˆ˜๋™ ๋“ฑ๋ก ๋กœ์ง์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `festivalRepository.getIsFirstVisit()`์„ Flow ํ˜•ํƒœ๋กœ ๊ตฌ๋…ํ•˜์—ฌ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ตœ์ดˆ ๋ฐฉ๋ฌธ ์ƒํƒœ๋ฅผ ๋ฐ˜์˜ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `System.currentTimeMillis()` ๋Œ€์‹  `kotlinx-datetime`์˜ `Clock`์„ ์‚ฌ์šฉํ•˜๋„๋ก ๊ต์ฒดํ•˜์—ฌ ํ”Œ๋žซํผ ๋…๋ฆฝ์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **refactor: ๊ณตํ†ต `Intent` ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ตฌ์กฐ ๊ฐœ์„ **
    * `RememberDeepLinkHandler.kt`: `expect class Intent`์— `getLongExtra`, `getBooleanExtra` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํ”Œ๋žซํผ ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ™•์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `mainNavGraph`: ํ”Œ๋žซํผ๋ณ„ ์œ„์น˜ ์†Œ์Šค ๊ตฌํ˜„์„ ์œ„ํ•ด `FusedLocationSource` ํƒ€์ž…์„ ์ถ”์ƒํ™”๋œ `LocationSource`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainActivity.kt`: ๊ฐœ๋ณ„ ๋ทฐ๋ชจ๋ธ ๊ธฐ๋ฐ˜์˜ ์ดˆ๊ธฐ ์ง„์ž… ์ฝ”๋“œ๋ฅผ ํ†ตํ•ฉ `FestabookScreen`์œผ๋กœ ๋Œ€์ฒดํ•˜์—ฌ ์ง„์ž…์  ๊ตฌ์กฐ๋ฅผ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ํ™ˆ ํ™”๋ฉด UI ๋ฐ ๋ฆฌ์†Œ์Šค ์ฐธ์กฐ ๋ฐฉ์‹ ์ˆ˜์ •**
    * `HomeScreen`: `LocalContext` ๋ฐ Android ์ „์šฉ ๋ฆฌ์†Œ์Šค(`R.string`) ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜๊ณ , Compose Multiplatform์˜ `Res` ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋‚ ์งœ ์ฒ˜๋ฆฌ ๋กœ์ง์„ `java.time`์—์„œ `kotlinx-datetime`์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `Organization` ๋ชจ๋ธ์˜ ์†์„ฑ๋ช…์„ `universityName`์—์„œ `organizationName`์œผ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๊ธฐํƒ€ ๋ณ€๊ฒฝ ์‚ฌํ•ญ**
    * `FestaBookAppGraph`์˜ ์˜คํƒ€๋ฅผ `FestabookAppGraph`๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ๋ฉ”์ธ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„ ๋ณต๊ตฌ ๋ฐ API ์ธ์ฆ ๋กœ์ง ๊ฐœ์„ 

* **feat: ๋ฉ”์ธ ํ™”๋ฉด ๋‚ด๋น„๊ฒŒ์ด์…˜ ์—ฐ๊ฒฐ**
    * `FestabookScreen.kt`: ์ฃผ์„ ์ฒ˜๋ฆฌ๋˜์–ด ์žˆ๋˜ `mainNavGraph` ํ˜ธ์ถœ๋ถ€๋ฅผ ๋ณต๊ตฌํ•˜์—ฌ ์•ฑ ์ง„์ž… ์‹œ ๋ฉ”์ธ ํ™”๋ฉด์œผ๋กœ์˜ ๋‚ด๋น„๊ฒŒ์ด์…˜ ํ๋ฆ„์„ ํ™œ์„ฑํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: API ์ธ์ฆ ํ—ค๋”์˜ ์ถ•์ œ ID ๋™์  ํ• ๋‹น ๋กœ์ง ๊ตฌํ˜„**
    * `FestaBookAuthPlugin.kt`: HTTP ์š”์ฒญ ํ—ค๋”(`festival`)์— ์‚ฝ์ž…๋˜๋Š” ์ถ•์ œ ID๋ฅผ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๊ฐ’์—์„œ `festivalLocalDataSource`๋ฅผ ํ†ตํ•ด ๋กœ์ปฌ ์ €์žฅ์†Œ์˜ ๊ฐ’์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ฐ€์ ธ์˜ค๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. `firstOrNull()`์„ ์‚ฌ์šฉํ•˜์—ฌ ์ตœ์‹  ์ƒํƒœ์˜ ID๋ฅผ ๋™์ ์œผ๋กœ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค.

* fix: ์•Œ๋ฆผ ์Šค์œ„์น˜ ์„ค์ • ๋ฒ„๊ทธ, ํ”Œ๋ ˆ์ด์Šค ์ƒ์„ธ ํ™”๋ฉด ๋ฒ„๊ทธ ํ•ด๊ฒฐ

* **refactor: ViewModel ์ฃผ์ž… ๋ฐฉ์‹ ๊ฐ„์†Œํ™”**
    * `MainNavigation.kt`: `viewModel(mainBackEntry, factory = ...)` ํ˜•ํƒœ์˜ ๋ณต์žกํ•œ ์ฃผ์ž… ๋ฐฉ์‹์„ `metroViewModel(mainBackEntry)`๋กœ ๊ฐœ์„ ํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.
    * `SettingScreen.kt`: ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ์ง์ ‘ ์ƒ์„ฑํ•˜๋˜ ViewModel์„ ์™ธ๋ถ€์—์„œ ์ฃผ์ž…๋ฐ›๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ์˜์กด์„ฑ ๊ตฌ์กฐ๋ฅผ ๋ช…ํ™•ํžˆ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ์„ค์ • ํ™”๋ฉด ์•Œ๋ฆผ ๊ถŒํ•œ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„ **
    * `SettingScreen`: ์•Œ๋ฆผ ๊ถŒํ•œ์ด ํ—ˆ์šฉ(`GRANTED`)๋˜์—ˆ์„ ๋•Œ, `saveNotificationId()`๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์„œ๋ฒ„์— ๊ธฐ๊ธฐ ์‹๋ณ„์ž๋ฅผ ์ €์žฅํ•˜๊ณ  ์„ฑ๊ณต ๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•˜๋„๋ก ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด `success` Flow๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ์Šค๋‚ต๋ฐ”๋ฅผ ๋„์šฐ๋˜ `ObserveAsEvents` ๋กœ์ง์„ ์ œ๊ฑฐํ•˜๊ณ , ๊ถŒํ•œ ์ƒํƒœ ๋ณ€ํ™”์— ๋”ฐ๋ฅธ ์ง์ ‘ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **fix: ๋„ค๋น„๊ฒŒ์ด์…˜ ์ปค์Šคํ…€ ํƒ€์ž… ์ง๋ ฌํ™” ์ถ”๊ฐ€**
    * `PlaceMapNavigation.kt`: ์ปค์Šคํ…€ `NavType` ๊ตฌํ˜„์ฒด์— `serializeAsValue` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฐ์ฒด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: Android `MainActivity` ๋ฐ ์„ค์ • ์ฝ”๋“œ ์ •๋ฆฌ**
    * `MainActivity.kt`: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” Import ๋ฌธ๊ณผ ๋ฉค๋ฒ„ ๋ณ€์ˆ˜(`androidAppGraph`, `notificationPermissionManagerFactory`)๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: iOS ์ง„์ž…์  ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์•„์›ƒ ์ˆ˜์ •

* **refactor: iOS `MainViewController` ๊ตฌ์กฐ ๋‹จ์ˆœํ™”**
    * ๊ธฐ์กด์— `MainViewController`์—์„œ ์ง์ ‘ ๊ด€๋ฆฌํ•˜๋˜ `SettingRoute`, `Scaffold`, `SnackbarHost` ๋กœ์ง์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ณตํ†ต ์—”ํŠธ๋ฆฌํฌ์ธํŠธ์ธ `FestabookScreen`์„ ํ˜ธ์ถœํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜๊ณ , ์•ฑ ์ข…๋ฃŒ ์ฝœ๋ฐฑ(`onAppFinish`)์— `exit(0)`๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* **fix: `NaverMapLogo` ๋ฆฌ๋ Œ๋”๋ง ๋กœ์ง ์ˆ˜์ • (iOS)**
    * `NaverMapLogo`์˜ `LaunchedEffect` ํ‚ค์— `naverMap` ๊ฐ์ฒด๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ, ์ง€๋„ ์ธ์Šคํ„ด์Šค ๋ณ€๊ฒฝ ์‹œ ๋กœ๊ณ  ์œ„์น˜ ์„ค์ • ๋กœ์ง์ด ์ •ํ™•ํžˆ ์žฌ์‹คํ–‰๋˜๋„๋ก ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: `FestabookTopAppBar` ๋ ˆ์ด์•„์›ƒ ์ˆ˜์ •**
    * `FestabookTopAppBar`์—์„œ `windowInsetsPadding(WindowInsets.statusBars)` ์„ค์ •์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ƒ์œ„ ์ปจํ…Œ์ด๋„ˆ์—์„œ Insets ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋„๋ก ๋ณ€๊ฒฝ๋œ ๊ฒƒ์— ๋”ฐ๋ฅธ ์ˆ˜์ •์ž…๋‹ˆ๋‹ค.

* refactor: ๋ถˆํ•„์š”ํ•œ ์ž„ํฌํŠธ ์ œ๊ฑฐ ๋ฐ ์ •์  ๋ถ„์„ ์„ค์ • ์—…๋ฐ์ดํŠธ

* **refactor: ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ๋ฐ ์˜์กด์„ฑ ์ •๋ฆฌ**
    * `SettingScreen.kt`: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `metroViewModel` ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ๋ฅผ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainNavigation.kt`: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `androidx.lifecycle.viewmodel.compose.viewModel` ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `RememberDeepLinkHandler.ios.kt`: iOS ์†Œ์Šค์…‹ ๋‚ด ๋ฏธ์‚ฌ์šฉ `NSURL` ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์ •์  ๋ถ„์„(ktlint) ์˜ˆ์™ธ ์„ค์ • ์ถ”๊ฐ€**
    * `PlaceMapScreen.kt`: `PlaceMapRoute` ํ•จ์ˆ˜์—์„œ ViewModel์„ ์ง์ ‘ ์ „๋‹ฌํ•˜๋Š” ํŒจํ„ด์— ๋Œ€ํ•ด `ktlint:compose:vm-forwarding-check` ์–ต์ œ(Suppress) ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: ์•„์ด์ฝ˜ XML fillColor ํ•˜๋“œ์ฝ”๋”ฉ ์ œ๊ฑฐ ๋ฐ tint ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ƒ‰์ƒ ์ œ์–ด

drawable XML์—์„œ ํ•˜๋“œ์ฝ”๋”ฉ๋œ #ffffff๋ฅผ #000000(์ค‘๋ฆฝ ๊ธฐ๋ณธ์ƒ‰)์œผ๋กœ ๋ณ€๊ฒฝ.
Icon ์ปดํฌ์ €๋ธ”์˜ tint ํŒŒ๋ผ๋ฏธํ„ฐ(selectedColor/unselectedColor)๊ฐ€ ์‹ค์ œ ์ƒ‰์ƒ์„ ์ œ์–ดํ•˜๋„๋ก ์ˆ˜์ •.

Co-authored-by: YongJun Jung <oungsi2000@users.noreply.github.com>

* fix: `FestaBookAuthPlugin` ๋‚ด Festival ID ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ถ”๊ฐ€

* **fix: API ์š”์ฒญ ์‹œ Festival ID๊ฐ€ ์—†์„ ๊ฒฝ์šฐ์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€**
    * `FestaBookAuthPlugin`์—์„œ `festivalLocalDataSource`๋กœ๋ถ€ํ„ฐ ๊ฐ€์ ธ์˜จ `festivalId`๊ฐ€ `null`์ธ ๊ฒฝ์šฐ, ํ—ค๋” ์„ค์ • ๋กœ์ง์„ ์ง„ํ–‰ํ•˜์ง€ ์•Š๊ณ  ์ฆ‰์‹œ ๋ฐ˜ํ™˜(`return@onRequest`)ํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ๋น„์ •์ƒ์ ์ธ ์š”์ฒญ ๋ฐœ์ƒ์„ ๋ฐฉ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: iOS ์•ฑ ์ข…๋ฃŒ ๋กœ์ง ์ˆ˜์ •

* **fix: iOS `MainViewController` ๋‚ด ์•ฑ ์ข…๋ฃŒ ์ฝœ๋ฐฑ ์ˆ˜์ •**
    * `FestabookScreen`์˜ `onAppFinish` ์ฝœ๋ฐฑ์—์„œ ํ˜ธ์ถœํ•˜๋˜ `exit(0)` ๋กœ์ง์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” iOS ์•ฑ ๊ฐ€์ด๋“œ๋ผ์ธ์— ๋”ฐ๋ฅธ ๋น„์ •์ƒ ์ข…๋ฃŒ ๋ฐฉ์ง€ ๋ฐ ์ž์—ฐ์Šค๋Ÿฌ์šด ์•ฑ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ์ˆ˜์ •์ž…๋‹ˆ๋‹ค.

* refactor: iOS ๋”ฅ๋งํฌ ์ฒ˜๋ฆฌ ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ Android ์ดˆ๊ธฐํ™” ๋กœ์ง ์ˆ˜์ •

* **refactor: iOS `Intent` ํด๋ž˜์Šค ๋ฐ ๋”ฅ๋งํฌ ์ฒ˜๋ฆฌ ๋กœ์ง ์œ ์—ฐํ™”**
    * iOS ํ”Œ๋žซํผ์˜ `Intent` ํด๋ž˜์Šค ๊ตฌํ˜„์„ ๊ณ ์ • ํ”„๋กœํผํ‹ฐ ๋ฐฉ์‹์—์„œ `Map<String, Any>` ๊ธฐ๋ฐ˜์˜ `extras` ์‹œ์Šคํ…œ์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `getLongExtra`, `getBooleanExtra` ๋ฉ”์„œ๋“œ๊ฐ€ `extras` ๋งต์—์„œ ๊ฐ’์„ ๋™์ ์œผ๋กœ ์กฐํšŒํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋”ฅ๋งํฌ ์•Œ๋ฆผ ์ˆ˜์‹  ์‹œ `DeepLinkKeys`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ `Intent`์— ์ „๋‹ฌํ•˜๋„๋ก ๋กœ์ง์„ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **fix: Android `MainActivity` ํ˜ธ์ถœ ์ˆœ์„œ ์ˆ˜์ •**
    * `enableEdgeToEdge()` ํ˜ธ์ถœ ์‹œ์ ์„ `super.onCreate()` ์ดํ›„๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ดˆ๊ธฐํ™” ๋กœ์ง์˜ ์•ˆ์ •์„ฑ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: `HomeScreen` ํ…Œ๋งˆ ์ปฌ๋Ÿฌ ์ ์šฉ**
    * `HomeScreen`์˜ `Scaffold` ๋ฐฐ๊ฒฝ์ƒ‰์„ ํ•˜๋“œ์ฝ”๋”ฉ๋œ `Color.White` ๋Œ€์‹  ๊ณตํ†ต ํ…Œ๋งˆ ์ •์˜์ธ `FestabookColor.white`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: PlaceDetailViewModel ๋‚ด ๋ฏธ์‚ฌ์šฉ ์ฃผ์„ ์ฝ”๋“œ ์ œ๊ฑฐ

* **refactor: ๋ฏธ์‚ฌ์šฉ ViewModel Factory ์ฃผ์„ ์‚ญ์ œ**
    * `PlaceDetailViewModel.kt` ํŒŒ์ผ์—์„œ ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ฃผ์„ ์ฒ˜๋ฆฌ๋˜์–ด ์žˆ๋˜ `companion object`์™€ `factory` ๋ฉ”์„œ๋“œ ๊ด€๋ จ ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: iOS `MainViewController` ๋‚ด ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ์ œ๊ฑฐ

* **refactor: ๋ถˆํ•„์š”ํ•œ POSIX exit ์ž„ํฌํŠธ ์‚ญ์ œ**
    * `MainViewController.kt`์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `platform.posix.exit` ์ž„ํฌํŠธ ๊ตฌ๋ฌธ์„ ์ œ๊ฑฐํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* style: HomeScreen.kt ๋‚ด ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ์ œ๊ฑฐ

* **style: ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ์ •๋ฆฌ**
    * `HomeScreen.kt`: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `androidx.compose.ui.graphics.Color` ์ž„ํฌํŠธ ๊ตฌ๋ฌธ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

---------

Co-authored-by: ๋ฐ•์ง€๋ฏผ <pjm5489@naver.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: YongJun Jung <oungsi2000@users.noreply.github.com>
* feat: ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ(Global Exception Handling) ๊ตฌํ˜„ ๋ฐ ์•ฑ ์„ค์ • ์ตœ์ ํ™”

* **feat: ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ ๋ฐ ์—๋Ÿฌ ํ™”๋ฉด ๊ตฌํ˜„**
    * `ErrorScreen`: ์˜ˆ๊ธฐ์น˜ ๋ชปํ•œ ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋…ธ์ถœ๋˜๋Š” ๊ณตํ†ต UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookGlobalExceptionHandler` & `ErrorActivity` (Android): Uncaught Exception ๋ฐœ์ƒ ์‹œ ์—๋Ÿฌ ํ™”๋ฉด์œผ๋กœ ์ „ํ™˜ํ•˜๊ณ , Firebase Crashlytics์— ์—๋Ÿฌ๋ฅผ ๊ธฐ๋กํ•˜๋ฉฐ ์•ฑ์„ ์žฌ์‹œ์ž‘ํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DefaultFestabookAppDelegate` & `ErrorViewController` (iOS): iOS ํ”Œ๋žซํผ์˜ ์ „์—ญ ์˜ˆ์™ธ ํฌ์ฐฉ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๊ณ , ์—๋Ÿฌ ์ƒํ™ฉ์—์„œ Crashlytics ๊ธฐ๋ก ๋ฐ ์—๋Ÿฌ ํ™”๋ฉด์ด ํ‘œ์‹œ๋˜๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์—๋Ÿฌ ๊ด€๋ จ ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค ๋ฐ `ic_uncaught_error` ์•„์ด์ฝ˜ ์Šคํƒ€์ผ์„ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ์•ฑ ํ…Œ๋งˆ ๋ฐ ํ™”๋ฉด ๋ฐฉํ–ฅ ๊ณ ์ • ์„ค์ •**
    * ๋ผ์ดํŠธ ๋ชจ๋“œ ๊ฐ•์ œ: Android(`AppCompatDelegate`) ๋ฐ iOS(`UIUserInterfaceStyleLight`) ํ™˜๊ฒฝ์—์„œ ์•ฑ ํ…Œ๋งˆ๋ฅผ ๋ผ์ดํŠธ ๋ชจ๋“œ๋กœ ๊ณ ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์„ธ๋กœ ํ™”๋ฉด ๊ณ ์ •: `AndroidManifest.xml` ์„ค์ •๊ณผ iOS `AppDelegate` ๋กœ์ง์„ ํ†ตํ•ด ์•ฑ์˜ ํ™”๋ฉด ๋ฐฉํ–ฅ์„ ์„ธ๋กœ(Portrait)๋กœ ๊ณ ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ๊ธฐํƒ€ ์ฝ”๋“œ ์ˆ˜์ •**
    * `SettingScreen`: ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ ๋™์ž‘ ๊ฒ€์ฆ์„ ์œ„ํ•ด ์ด์šฉ์•ฝ๊ด€ ํด๋ฆญ ์‹œ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์ž„์‹œ ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookApp` (Android): ์•ฑ ์ดˆ๊ธฐํ™” ๋‹จ๊ณ„์—์„œ ๋ผ์ดํŠธ ํ…Œ๋งˆ ๋ฐ ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ ์„ค์ • ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: iOS ํ…Œ๋งˆ ์„ค์ • ๋ฐฉ์‹ ๊ฐœ์„  ๋ฐ ๋ฏธ์‚ฌ์šฉ ํŒŒ์ผ ์ œ๊ฑฐ

* **refactor: iOS ๋ผ์ดํŠธ ๋ชจ๋“œ ๊ฐ•์ œ ์„ค์ • ๋กœ์ง ์ตœ์ ํ™”**
    * `DefaultFestabookAppDelegate`: `application.windows`๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ ๊ฐœ๋ณ„ ์œˆ๋„์šฐ์˜ ์Šคํƒ€์ผ์„ ๋ณ€๊ฒฝํ•˜๋˜ ๋ฐฉ์‹์—์„œ `UIWindow.appearance()`๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ํ…Œ๋งˆ ์„ค์ • ๋กœ์ง์„ ๊ฐ„์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AppDelegate.swift`: Swift ์ง„์ž…์ ์—์„œ ์ˆ˜ํ–‰ํ•˜๋˜ `UIWindow.appearance()` ์„ค์ •์„ ์ œ๊ฑฐํ•˜๊ณ  Kotlin ๊ณตํ†ต ๋กœ์ง์œผ๋กœ ํ†ตํ•ฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๋ฏธ์‚ฌ์šฉ iOS ์—๋Ÿฌ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ์‚ญ์ œ**
    * `ErrorViewController.kt`: ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” iOS ์ „์šฉ ์—๋Ÿฌ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ํŒŒ์ผ์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ด€๋ จ ๋ฏธ์‚ฌ์šฉ ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค ์ œ๊ฑฐ

* **refactor: ์•ˆ๋“œ๋กœ์ด๋“œ ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ**
    * `strings.xml` ํŒŒ์ผ์—์„œ ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ(Uncaught Exception Handler) UI์— ์‚ฌ์šฉ๋˜์—ˆ๋˜ `uncaught_exception_title`, `uncaught_exception_message`, `uncaught_exception_confirm` ๋ฆฌ์†Œ์Šค๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
* feat: ์ถ•์ œ ์•Œ๋ฆผ ๋™๊ธฐํ™” ๋กœ์ง ๊ตฌํ˜„ ๋ฐ PlaceMap ํ•ธ๋“ค๋Ÿฌ ๋ฆฌํŒฉํ„ฐ๋ง

* **feat: ์ถ•์ œ ์•Œ๋ฆผ ์ƒํƒœ ๋™๊ธฐํ™” ๊ธฐ๋Šฅ ์ถ”๊ฐ€**
    * `FestivalNotificationRepository`์— ์„œ๋ฒ„์™€ ์•Œ๋ฆผ ์„ค์ •์„ ๋™๊ธฐํ™”ํ•˜๋Š” `syncFestivalNotificationIsAllow` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `SettingViewModel` ์ดˆ๊ธฐํ™” ์‹œ `syncFestivalNotificationIsAllow`๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ๋กœ์ปฌ์— ๋ฐ˜์˜ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•Œ๋ฆผ ์„ค์ • ๋ณ€๊ฒฝ ์‹œ `updateNotificationIsAllowed`๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ UI ์ƒํƒœ๋ฅผ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestivalNotificationRemoteDataSource` ๋ฐ `FestivalNotificationService`์— ํŠน์ • ๋””๋ฐ”์ด์Šค์˜ ์•Œ๋ฆผ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” API๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์„œ๋ฒ„ ์‘๋‹ต ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ `RegisteredFestivalNotificationResponse` ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: PlaceMap ๊ด€๋ จ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ถ”์ƒํ™” ๋ฐ ๊ตฌํ˜„ ๋ถ„๋ฆฌ**
    * `MapControlEventHandler`, `SelectEventHandler`, `FilterEventHandler`๋ฅผ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ •์˜ํ•˜๊ณ  ๊ธฐ์กด ๊ตฌํ˜„์ฒด๋ฅผ `Impl` ํด๋ž˜์Šค๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ธํ„ฐํŽ˜์ด์Šค ๊ธฐ๋ฐ˜์˜ ์˜์กด์„ฑ ์ฃผ์ž…์„ ์œ„ํ•ด `@ContributesBinding` ์„ค์ •์„ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๊ธฐํƒ€ ๋ณ€๊ฒฝ ์‚ฌํ•ญ**
    * `FestivalNotificationRepositoryImpl` ๋‚ด ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ `companion object` ์ƒ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookApp.kt` ๋‚ด ๋ถˆํ•„์š”ํ•œ ์ž„ํฌํŠธ ์ •๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

* test: Compose Multiplatform ํ”„๋กœ์ ํŠธ ์ „ ๊ณ„์ธต์— ๋Œ€ํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€

* **test: `ViewModel` ๋ฐ `ActionHandler` ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„**
    * `HomeViewModel`, `ScheduleViewModel`, `NewsViewModel`, `SplashViewModel`, `ExploreViewModel`, `SettingViewModel`, `PlaceDetailViewModel` ๋ฐ `PlaceMapViewModel`์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฐ ํ™”๋ฉด์˜ ์ƒํƒœ ๊ด€๋ฆฌ ๋ฐ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
    * `SelectEventHandler`, `FilterEventHandler`, `MapControlEventHandler` ๋“ฑ `PlaceMap` ๋„๋ฉ”์ธ์˜ ์„ธ๋ถ€ ์•ก์…˜ ํ•ธ๋“ค๋Ÿฌ ๋กœ์ง์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: ํ…Œ์ŠคํŠธ ์ „์šฉ ํ”ฝ์Šค์ฒ˜(Fixtures) ๋ฐ ํ™•์žฅ ํ•จ์ˆ˜ ์ถ”๊ฐ€**
    * ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด `FAKE_` ์ ‘๋‘์‚ฌ๋ฅผ ๊ฐ€์ง„ ๋„๋ฉ”์ธ ๋ชจ๋ธ ํ”ฝ์Šค์ฒ˜ ํŒŒ์ผ๋“ค์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค (`PlaceMapTestFixture`, `ScheduleTestFixtures` ๋“ฑ).
    * `Flow` ์ด๋ฒคํŠธ ๊ฒ€์ฆ์„ ์œ„ํ•œ `observeEvent`, `observeMultipleEvent` ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ๋น„๋™๊ธฐ ์ŠคํŠธ๋ฆผ ํ…Œ์ŠคํŠธ ํŽธ์˜์„ฑ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: `Repository` ๊ณ„์ธต ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€**
    * `FestivalNotificationRepositoryImpl` ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ์•Œ๋ฆผ ๊ถŒํ•œ ๋™๊ธฐํ™”, ์•Œ๋ฆผ ID ์ €์žฅ ๋ฐ ์‚ญ์ œ ๋กœ์ง๊ณผ ์„œ๋ฒ„ ์‹คํŒจ ์‹œ ๋กœ์ปฌ ์ƒํƒœ ์›๋ณต ๋กœ์ง์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

* **chore: ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ •**
    * `dev.mokkery`๋ฅผ ์‚ฌ์šฉํ•œ ๋ชจํ‚น(Mocking) ๋ฐ `kotlinx-coroutines-test`๋ฅผ ํ™œ์šฉํ•œ ์ฝ”๋ฃจํ‹ด ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค.

* test: MainViewModel ์œ ๋‹› ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ ๋ฐ ๊ธฐ์กด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ •๋ฆฌ

* **test: `MainViewModelTest` ํด๋ž˜์Šค ์ถ”๊ฐ€**
    * ๋’ค๋กœ ๊ฐ€๊ธฐ ๋ฒ„ํŠผ ์—ฐ์† ํด๋ฆญ ์‹œ ์ข…๋ฃŒ ์ด๋ฒคํŠธ ๋ฐœ์ƒ ๋กœ์ง์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestivalRepository`๋ฅผ ํ†ตํ•œ ์ฒซ ๋ฐฉ๋ฌธ ์—ฌ๋ถ€ ํ™•์ธ ๋กœ์ง์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `ScheduleViewModelTest` ๋‚ด ๋ถˆํ•„์š”ํ•œ ํƒ€์ž… ์บ์ŠคํŒ… ์ œ๊ฑฐ**
    * `ScheduleUiState.Content.Success` ์ƒํƒœ ํ™•์ธ ์‹œ ์ค‘๋ณต๋˜๋Š” ๋ช…์‹œ์  ํƒ€์ž… ๋ณ€ํ™˜ ๋กœ์ง์„ ์ œ๊ฑฐํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ๊ฐ„์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ํ•œ๊ธ€ ์„ค๋ช… ๋ฌธ๊ตฌ ์ˆ˜์ •**
    * `FilterActionHandlerTest`, `PlaceDetailViewModelTest` ๋“ฑ ์ฃผ์š” ํ…Œ์ŠคํŠธ ํŒŒ์ผ ๋‚ด ๋ฉ”์„œ๋“œ ๋ช…์นญ์˜ ์กฐ์‚ฌ ๋ฐ ์–ด๋ฒ•์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
* refactor: MainViewModel ๋‚ด ์ดˆ๊ธฐ ๋ฐฉ๋ฌธ ์—ฌ๋ถ€ ํ™•์ธ ๋กœ์ง ๊ฐœ์„ 

* `MainViewModel` ์ดˆ๊ธฐํ™” ์‹œ `festivalRepository.getIsFirstVisit()`๋ฅผ ์ง€์†์ ์œผ๋กœ `collect` ํ•˜๋˜ ๋ฐฉ์‹์—์„œ `firstOrNull()`์„ ์‚ฌ์šฉํ•˜์—ฌ ์ตœ์ดˆ 1ํšŒ๋งŒ ํ™•์ธํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
* ๊ฐ’์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ์— ๋Œ€๋น„ํ•œ null ์ฒ˜๋ฆฌ ๋กœ์ง ๋ฐ ๊ด€๋ จ TODO ์ฃผ์„์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ ์ˆ˜์ •
refactor: LineupResponse์˜ ๋‚ ์งœ ํŒŒ์‹ฑ ๋กœ์ง ์ˆ˜์ •

* **refactor: LineupResponse ๋‚ด ๋‚ ์งœ ๋ณ€ํ™˜ ๋กœ์ง ๊ฐ„์†Œํ™”**
    * `LineupResponse.kt`: `performanceAt` ๋ฌธ์ž์—ด์„ `Instant`๋ฅผ ๊ฑฐ์ณ UTC ๊ธฐ์ค€์˜ `LocalDateTime`์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋˜ ๋ฐฉ์‹์„ `LocalDateTime.parse()`๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
* refactor: PlaceDetail ํŒจํ‚ค์ง€ PlaceMap ํ•˜์œ„ ํŒจํ‚ค์ง€๋กœ ์ด๋™

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: testํŒŒ์ผ ktlint format

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: ํƒ์ƒ‰ ํ™”๋ฉด ๋‚ด ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด UI ๋ฐ ๊ฒ€์ƒ‰ ๋žœ๋”ฉ ์ฝ˜ํ…์ธ  ๋ฆฌํŒฉํ„ฐ๋ง

* **feat: ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๊ธฐ๋Šฅ ๋ฐ UI ์ถ”๊ฐ€**
    * `ExploreUiState`์— `recentSearches` ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ธฐ๋ก์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `RecentSearchTitle` ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๊ฒ€์ƒ‰์–ด๊ฐ€ ์—†๋Š” `Idle` ์ƒํƒœ์ผ ๋•Œ `ExploreSearchResultList`์—์„œ ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๋ชฉ๋ก์„ ํ‘œ์‹œํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ด€๋ จ ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค(`recent_searches`, `clear_all`)๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ํƒ์ƒ‰ ํ™”๋ฉด ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ๊ฐœ์„ **
    * `ExploreLandingContent.kt`๋ฅผ ์‹ ์„คํ•˜์—ฌ ๋žœ๋”ฉ ํ™”๋ฉด์˜ ๊ฒ€์ƒ‰๋ฐ”์™€ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋กœ์ง์„ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreSearchResultList` ๋“ฑ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ `SearchUiState` ๋Œ€์‹  `ExploreUiState`๋ฅผ ์ง์ ‘ ์ „๋‹ฌ๋ฐ›์•„ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreSearchResultList` ๋‚ด `LazyColumn` ์•„์ดํ…œ์— `key`๋ฅผ ์ง€์ •ํ•˜์—ฌ ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์ฝ”๋“œ ์ •๋ฆฌ**
    * `SearchUiState` ๋‚ด ๋ถˆํ•„์š”ํ•œ ์ฃผ์„์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreScreen` ๋‚ด ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ์„ ์œ„ํ•ด ์ƒํƒœ ๋ณ€์ˆ˜๋ช…์„ `uiState`์—์„œ `exploreUiState`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์ตœ๊ทผ ์ถ•์ œ ๊ฒ€์ƒ‰์–ด ์ €์žฅ ๋ฐ ์กฐํšŒ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* **feat: ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ๋ฐ ๋ณ€ํ™˜ ๋กœ์ง ์ถ”๊ฐ€**
    * `FestivalSearchItemEntity` ํด๋ž˜์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๋„๋ฉ”์ธ ๋ชจ๋ธ(`FestivalSearchItem`)๊ณผ์˜ ์ƒํ˜ธ ๋ณ€ํ™˜์„ ์œ„ํ•œ ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๋กœ์ปฌ ์ €์žฅ์†Œ ๋กœ์ง ๊ตฌํ˜„**
    * `FestivalLocalDataSource` ๋ฐ `FestivalLocalDataSourceImpl`์— ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด๋ฅผ `DataStore`์— JSON ํ˜•ํƒœ๋กœ ์ €์žฅํ•˜๊ณ  ์กฐํšŒํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ค‘๋ณต๋œ ID์˜ ๊ฒ€์ƒ‰์–ด๋Š” ์ œ๊ฑฐํ•˜๋ฉฐ, ์ตœ๋Œ€ 10๊ฐœ๊นŒ์ง€ ์ตœ์‹  ์ˆœ์œผ๋กœ ์œ ์ง€ํ•˜๋„๋ก ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

* **feat: ํƒ์ƒ‰ ๊ด€๋ จ Repository ๋ฐ ViewModel ๊ธฐ๋Šฅ ํ™•์žฅ**
    * `ExploreRepository`์— ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์ €์žฅ(`saveRecentFestivalSearch`) ๋ฐ ์กฐํšŒ(`getRecentFestivalSearches`) ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreViewModel` ์ดˆ๊ธฐํ™” ์‹œ ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๋ชฉ๋ก์„ ๊ด€์ฐฐ(Observe)ํ•˜์—ฌ UI ์ƒํƒœ(`ExploreUiState`)์— ๋ฐ˜์˜ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ์ฝ”๋“œ ๊ฐ€๋…์„ฑ ๊ฐœ์„ **
    * `ExploreViewModel` ๋‚ด `_sideEffect` ์„ ์–ธ๋ถ€์˜ ์ค„๋ฐ”๊ฟˆ ๋“ฑ ์ฝ”๋“œ ์Šคํƒ€์ผ์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์ €์žฅ ๋ฐ ์ „์ฒด ์‚ญ์ œ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* **feat: ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ์ถ”๊ฐ€**
    * ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—์„œ ๋Œ€ํ•™ ์„ ํƒ ์‹œ ํ•ด๋‹น ์ •๋ณด๋ฅผ ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๋ฆฌ์ŠคํŠธ์— ์ €์žฅํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๋‚ด์—ญ์„ ๋ชจ๋‘ ์ง€์šธ ์ˆ˜ ์žˆ๋Š” `clearRecentFestivalSearches` ๊ธฐ๋Šฅ์„ Repository ๋ฐ Data Source ๊ณ„์ธต์— ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์ตœ๋Œ€ ์ €์žฅ ๊ฐœ์ˆ˜๋ฅผ 10๊ฐœ์—์„œ 5๊ฐœ๋กœ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: Explore ๋„๋ฉ”์ธ ๋ฐ UI ์ปดํฌ๋„ŒํŠธ ๊ณ ๋„ํ™”**
    * `ExploreViewModel`์— ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์‚ญ์ œ(`onClearRecentSearches`) ๋ฐ ํ…์ŠคํŠธ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ด€๋ จ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์žฌ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreScreen`, `ExploreSearchResultList` ๋“ฑ UI ์ปดํฌ๋„ŒํŠธ์— ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์‚ญ์ œ ์ด๋ฒคํŠธ ์ฝœ๋ฐฑ์„ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `SearchResultUiModel`์„ ๋„๋ฉ”์ธ ๋ชจ๋ธ์ธ `FestivalSearchItem`์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ ํ™•์ธ์„ ์œ„ํ•œ ๋””๋ฒ„๊ทธ ๋กœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์‚ญ์ œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ๋ฐ ๊ฒ€์ƒ‰ UI ๊ฐœ์„ 

* **feat: ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๊ฐœ๋ณ„ ์‚ญ์ œ ๊ธฐ๋Šฅ ๊ตฌํ˜„**
    * `ExploreResultItem`์— ์‚ญ์ œ ๋ฒ„ํŠผ(X ์•„์ด์ฝ˜)์„ ์ถ”๊ฐ€ํ•˜๊ณ  `onDeleteClick` ์ฝœ๋ฐฑ์„ ์ง€์›ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreViewModel`์— ๊ฐœ๋ณ„ ๊ฒ€์ƒ‰ ๊ธฐ๋ก ์‚ญ์ œ๋ฅผ ์œ„ํ•œ `onRecentSearchDelete` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreScreen`, `ExploreSearchContent` ๋“ฑ ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ์— ์‚ญ์ œ ์ด๋ฒคํŠธ ์ „๋‹ฌ ๋กœ์ง์„ ๋ฐ˜์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์˜์—ญ UI ๋กœ์ง ๊ฐœ์„ **
    * `RecentSearchTitle.kt`๋ฅผ `RecentSearchMessageView.kt`๋กœ ์ด๋ฆ„์„ ๋ณ€๊ฒฝํ•˜๊ณ , ๊ฒ€์ƒ‰ ๊ธฐ๋ก ์œ ๋ฌด์— ๋”ฐ๋ผ ๋‹ค๋ฅธ UI๋ฅผ ๋ณด์—ฌ์ฃผ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ํ‘œ์‹œํ•  "์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๋‚ด์—ญ์ด ์—†์Šต๋‹ˆ๋‹ค." ๋ฉ”์‹œ์ง€ ๋ทฐ(`NoRecentSearchTitle`)๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๋ฆฌ์†Œ์Šค ์ถ”๊ฐ€**
    * `strings.xml`์— ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ ๋ฉ”์‹œ์ง€ ๋ฐ ๋‹ซ๊ธฐ ๋ฒ„ํŠผ์„ ์œ„ํ•œ `content_description` ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ธฐ๋ก ๊ฐœ๋ณ„ ์‚ญ์ œ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* **feat: ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์‚ญ์ œ ๋กœ์ง ๊ตฌํ˜„**
    * `ExploreViewModel` ๋‚ด `onRecentSearchDelete` ํ˜ธ์ถœ ์‹œ `exploreRepository.deleteRecentFestivalSearch`๋ฅผ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreRepository` ๋ฐ `FestivalLocalDataSource` ์ธํ„ฐํŽ˜์ด์Šค์— ๊ฐœ๋ณ„ ๊ฒ€์ƒ‰ ๊ธฐ๋ก ์‚ญ์ œ๋ฅผ ์œ„ํ•œ `deleteRecentFestivalSearch` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ฐ ์ €์žฅ์†Œ ๊ตฌํ˜„**
    * `FestivalLocalDataSourceImpl`์—์„œ DataStore์— ์ €์žฅ๋œ ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด JSON ๋ฆฌ์ŠคํŠธ๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ํŠน์ • ํ•ญ๋ชฉ์„ ์ œ๊ฑฐํ•˜๊ณ  ๋‹ค์‹œ ์ €์žฅํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreRepositoryImpl` ๋‚ด ์˜คํƒ€ ์ˆ˜์ •(`entites` -> `entities`) ๋ฐ ๋ฉ”์„œ๋“œ ํ‘œํ˜„์‹์„ ๊ฐ„์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ExploreViewModel ๋กœ๊ทธ ์ œ๊ฑฐ ๋ฐ LocalDataSource ๋กœ์ง ๊ฐœ์„ 

* **refactor: `ExploreViewModel` ๋‚ด ๋””๋ฒ„๊ทธ ๋กœ๊ทธ ๋ฐ ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ์ œ๊ฑฐ**
    * ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๋ชฉ๋ก์„ `collect` ํ•˜๋Š” ๊ณผ์ •์—์„œ ์‚ฌ์šฉํ•˜๋˜ `Napier` ๋กœ๊ทธ ์ถœ๋ ฅ ์ฝ”๋“œ์™€ ๊ด€๋ จ ์ž„ํฌํŠธ ๋ฌธ์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `FestivalLocalDataSourceImpl` ๋‚ด ์กฐ๊ฑด๋ฌธ ๋กœ์ง ๊ตฌ์กฐ ๋ณ€๊ฒฝ**
    * `deleteFestivalSearchItem` ๋ฉ”์„œ๋“œ์—์„œ ์ €์žฅ๋œ JSON ๋ฌธ์ž์—ด์˜ ๋นˆ ๊ฐ’ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” `if` ๋ฌธ ๊ตฌ์กฐ๋ฅผ `isEmpty()` ์šฐ์„  ํ™•์ธ ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋กœ์ง์˜ ๊ฐ€๋…์„ฑ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* style: ์ฝ”๋“œ ํฌ๋งทํŒ…

* refactor: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ •

* chore: iOS ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ์„ค์ • ๋ฐ Info.plist ์ •๋ฆฌ

* **chore: `Info.plist` ํŒŒ์ผ ๊ตฌ์กฐ ๋ฐ ๋‚ด์šฉ ์ •๋ฆฌ**
    * ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์ค‘๋ณต ์ •์˜๋˜์—ˆ๋˜ `CADisableMinimumFrameDurationOnPhone` ํ‚ค๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ํฌ๋งท์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `CFBundleName`, `CFBundleShortVersionString`, `CFBundleVersion` ๋“ฑ Xcode ํ”„๋กœ์ ํŠธ ์„ค์ •์—์„œ ๊ด€๋ฆฌ ๊ฐ€๋Šฅํ•œ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํ‚ค๋“ค์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: Xcode ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ์„ค์ • ์—…๋ฐ์ดํŠธ**
    * **์ฝ”๋“œ ์„œ๋ช… ์„ค์ • ์ถ”๊ฐ€**: Debug ๋นŒ๋“œ ๊ตฌ์„ฑ์— `CODE_SIGN_IDENTITY`, `DEVELOPMENT_TEAM`, `PROVISIONING_PROFILE_SPECIFIER` ์„ค์ •์„ SDK(`iphoneos*`)๋ณ„๋กœ ๊ตฌ์ฒดํ™”ํ•˜์—ฌ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **Bundle Identifier ๋ณ€๊ฒฝ**: `PRODUCT_BUNDLE_IDENTIFIER`๋ฅผ `eoehdeksruf.festabook.debug`์—์„œ `debug.eoehdeksruf.festabook`์œผ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **์•ฑ ํ‘œ์‹œ ์ด๋ฆ„ ์ˆ˜์ •**: Debug ๋นŒ๋“œ ์‹œ ์•ฑ์˜ ์‹๋ณ„์„ ๋•๊ธฐ ์œ„ํ•ด `INFOPLIST_KEY_CFBundleDisplayName`์„ `festabook(Debug)`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `project.pbxproj` ๋‚ด ๋ถˆํ•„์š”ํ•œ `exceptions` ํ•ญ๋ชฉ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ExploreLandingContent ๋‚ด Modifier ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€

* **refactor: ExploreLandingContent ์ปดํฌ๋„ŒํŠธ ํ™•์žฅ์„ฑ ๊ฐœ์„ **
    * `ExploreLandingContent` ํ•จ์ˆ˜์— `modifier` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์™ธ๋ถ€์—์„œ ๋ ˆ์ด์•„์›ƒ ์†์„ฑ์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋‚ด๋ถ€์˜ `ExploreSearchBar` ๋ฐ `AnimatedContent`์— ์ „๋‹ฌ๋ฐ›์€ `modifier`๋ฅผ ์ ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ธฐ๋ก ์ €์žฅ ๋กœ์ง ์ˆ˜์ •

* **refactor: `FestivalLocalDataSourceImpl` ๋‚ด ๊ฒ€์ƒ‰ ๊ธฐ๋ก ์ œํ•œ ๋ฐฉ์‹ ๊ฐœ์„ **
    * ์ƒˆ๋กœ์šด ๊ฒ€์ƒ‰ ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•  ๋•Œ ์ „์ฒด ๋ชฉ๋ก์„ ๋จผ์ € ์ƒ์„ฑํ•œ ๋’ค `take(MAX_RECENT_SEARCH_COUNT)`๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ, ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ์˜ ์ตœ๋Œ€ ๊ฐœ์ˆ˜๊ฐ€ ์ •ํ™•ํžˆ ์œ ์ง€๋˜๋„๋ก ๋กœ์ง์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* [Fix] ExploreViewModel ๋‚ด ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๊ด€์ฐฐ ๋ฐ ์ €์žฅ ๋กœ์ง ๊ฐœ์„ 

* **fix: ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด Job ๊ด€๋ฆฌ ๋ฐ ์‹คํ–‰ ์ˆœ์„œ ๋ณด์ •**
    * `recentSearchJob` ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ `observeRecentSearches`์—์„œ ์‹œ์ž‘๋œ ์ฝ”๋ฃจํ‹ด์„ ๊ด€๋ฆฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `onUniversitySelected` ํ˜ธ์ถœ ์‹œ, ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๊ด€์ฐฐ Job์„ ์ทจ์†Œํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค ์†Œ๋ชจ๋ฅผ ๋ฐฉ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ฒ€์ƒ‰ ๊ธฐ๋ก ์ €์žฅ(`saveRecentFestivalSearch`) ๋กœ์ง์ด ์™„๋ฃŒ๋œ ํ›„ ๋„ค๋น„๊ฒŒ์ด์…˜ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋„๋ก ์‹คํ–‰ ์ˆœ์„œ๋ฅผ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: FestivalLocalDataSourceImpl ๋‚ด JSON ์—ญ์ง๋ ฌํ™” ๋กœ์ง ๊ณตํ†ตํ™” ๋ฐ ์•ˆ์ •์„ฑ ๊ฐœ์„ 

* **refactor: `decodeFromStringOrDefault` ํ—ฌํผ ํ•จ์ˆ˜ ๋„์ž…**
    * JSON ๋ฌธ์ž์—ด ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ๊ธฐ๋ณธ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก `runCatching` ๊ธฐ๋ฐ˜์˜ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `saveRecentSearch` ๋ฐ `deleteRecentSearch` ๋‚ด ์ค‘๋ณต๋˜๋˜ ์ˆ˜๋™ ํŒŒ์‹ฑ ๋ฐ null ์ฒดํฌ ๋กœ์ง์„ ํ•ด๋‹น ํ—ฌํผ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ฆฌํŒฉํ„ฐ๋งํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `RecentSearchMessageView`์˜ ์ฝœ๋ฐฑ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ช…์นญ ๋ณ€๊ฒฝ

* **refactor: `RecentSearchMessageView` ๋ฐ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ ๋ฆฌํŒฉํ„ฐ๋ง**
    * ์ „์ฒด ์‚ญ์ œ ๊ธฐ๋Šฅ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋‚˜ํƒ€๋‚ด๊ธฐ ์œ„ํ•ด `onClick` ํŒŒ๋ผ๋ฏธํ„ฐ ์ด๋ฆ„์„ `onClearAllClick`์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreSearchResultList`์—์„œ `RecentSearchMessageView` ํ˜ธ์ถœ ์‹œ ๋ณ€๊ฒฝ๋œ ํŒŒ๋ผ๋ฏธํ„ฐ๋ช…์„ ๋ฐ˜์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: iOS Info.plist ๋ฒ„์ „ ์—…๋ฐ์ดํŠธ ๋กœ์ง ๊ฐœ์„ 

* **chore: `updateIosVersion` ํƒœ์Šคํฌ ๋‚ด ๋ฒ„์ „ ์ฃผ์ž… ๋กœ์ง ๊ณ ๋„ํ™”**
    * `CFBundleShortVersionString` ๋ฐ `CFBundleVersion` ํ‚ค๊ฐ€ `.plist` ํŒŒ์ผ ๋‚ด์— ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, `</dict>` ํƒœ๊ทธ ์ง์ „์— ํ•ด๋‹น ํ‚ค์™€ ๊ฐ’์„ ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•˜๋„๋ก ๋กœ์ง์„ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํ‚ค๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๊ธฐ์กด๊ณผ ๋™์ผํ•˜๊ฒŒ ์ •๊ทœํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ’์„ ๊ต์ฒดํ•˜๋„๋ก ์กฐ๊ฑด๋ถ€ ์ฒ˜๋ฆฌ๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํƒœ์Šคํฌ ๊ตฌ์„ฑ ๋‹จ๊ณ„์—์„œ ๋ถˆํ•„์š”ํ•œ `inputs` ์„ ์–ธ๋ถ€๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ExploreLandingContent ๋ ˆ์ด์•„์›ƒ ์ˆ˜์ • ๋ฐ iOS ๋ฒ„์ „ ์ •๋ณด ์—…๋ฐ์ดํŠธ

* **refactor: ExploreLandingContent ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ๊ฐœ์„ **
    * `ExploreSearchBar`์™€ `AnimatedContent`๋ฅผ `Column`์œผ๋กœ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์กฐ๋ฅผ ์•ˆ์ •ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋ฐ›์€ `modifier`๋ฅผ ์ตœ์ƒ์œ„ `Column`์— ์ ์šฉํ•˜๊ณ , ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์˜ `modifier` ์„ค์ •์„ ์ด์— ๋งž์ถฐ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: iOS ์•ฑ ๋ฒ„์ „ ์ •๋ณด ๋ช…์‹œ**
    * `Info.plist` ๋‚ด์— `CFBundleShortVersionString`(2.1.0) ๋ฐ `CFBundleVersion`(20100) ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
* feat: FCM ์•Œ๋ฆผ ์ˆ˜์‹  ์‹œ ์ถ•์ œ ID ์ €์žฅ ๋กœ์ง ๊ตฌํ˜„

* **feat: FCM ์•Œ๋ฆผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋ฐ ๋กœ์ปฌ ์ €์žฅ ๊ธฐ๋Šฅ ์ถ”๊ฐ€**
    * `MyFirebaseMessagingService`์— `FestivalLocalDataSource`๋ฅผ ์ฃผ์ž…ํ•˜์—ฌ ์•Œ๋ฆผ ์ˆ˜์‹  ์‹œ ์ „๋‹ฌ๋ฐ›์€ `festivalId`๋ฅผ ๋กœ์ปฌ์— ์ €์žฅํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `onMessageReceived`์—์„œ `festivalId` ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•˜๊ณ , `CoroutineScope(Dispatchers.IO)`๋ฅผ ํ†ตํ•ด ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ €์žฅํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `MyFirebaseMessagingService` ์˜์กด์„ฑ ์ฃผ์ž… ์„ค์ •**
    * `onCreate`์—์„œ `androidAppGraph.inject(this)`๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์˜์กด์„ฑ ์ฃผ์ž…์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AndroidAppGraph` ์ธํ„ฐํŽ˜์ด์Šค์— `FirebaseMessagingService` ์ธ์ ์…˜์„ ์œ„ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: .gitignore ๋‚ด iOS Google Service ์„ค์ • ํŒŒ์ผ ๊ฒฝ๋กœ ์ˆ˜์ •

* **chore: GoogleService-Info.plist ๋ฌด์‹œ ๊ฒฝ๋กœ ๊ตฌ์ฒดํ™”**
    * `.gitignore`์— ๋“ฑ๋ก๋œ `GoogleService-Info.plist`, `GoogleService-Info-DEBUG.plist`, `GoogleService-Info-RELEASE.plist` ํŒŒ์ผ์˜ ๊ฒฝ๋กœ๋ฅผ ์‹ค์ œ ์œ„์น˜์ธ `iosApp/iosApp/` ํ•˜์œ„๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* [Config] iOS ๋””๋ฒ„๊ทธ ๋นŒ๋“œ ์„ค์ • ๋ฐ ์‚ฌ์ด๋‹ ๊ตฌ์„ฑ ์—…๋ฐ์ดํŠธ

* **build: iOS ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ๋ฐ ์‚ฌ์ด๋‹ ์„ค์ • ์ˆ˜์ •**
    * `CODE_SIGN_IDENTITY`, `DEVELOPMENT_TEAM`, `PROVISIONING_PROFILE_SPECIFIER` ์„ค์ •์— `iphoneos` SDK ์ „์šฉ ๊ฐ’์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ˆ˜๋™ ์‚ฌ์ด๋‹ ๊ตฌ์„ฑ์„ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PRODUCT_BUNDLE_IDENTIFIER`๋ฅผ `eoehdeksruf.festabook.debug`์—์„œ `debug.eoehdeksruf.festabook`์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์•ฑ ํ‘œ์‹œ ๋ช…์นญ ๋ฐ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์ •๋ฆฌ**
    * ๋””๋ฒ„๊ทธ ๋นŒ๋“œ์˜ `CFBundleDisplayName`์„ `festabook`์—์„œ `festabook(Debug)`๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์„ค์น˜๋œ ์•ฑ์˜ ์‹๋ณ„๋ ฅ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.
    * `project.pbxproj` ๋‚ด ๋ถˆํ•„์š”ํ•œ `exceptions` ํ•ญ๋ชฉ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* [Feat] iOS ์›๊ฒฉ ์•Œ๋ฆผ ๋“ฑ๋ก ๋กœ์ง ์ถ”๊ฐ€

* **feat: ์•ฑ ์‹คํ–‰ ์‹œ ์›๊ฒฉ ์•Œ๋ฆผ ๋“ฑ๋ก ๊ธฐ๋Šฅ ์ถ”๊ฐ€**
    * `AppDelegate`์˜ `didFinishLaunchingWithOptions` ๋ฉ”์„œ๋“œ ๋‚ด์— `application.registerForRemoteNotifications()` ํ˜ธ์ถœ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์•ฑ ์‹œ์ž‘ ์‹œ ์›๊ฒฉ ์•Œ๋ฆผ์„ ๋“ฑ๋กํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `festabookAppDelegate.application`์˜ ๋ฐ˜ํ™˜ ๊ฐ’์„ ๋ณ€์ˆ˜๋กœ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋กœ์ง์„ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: AppDelegate ๋‚ด ๋ฉ”์„œ๋“œ ํฌ๋งทํŒ… ์ˆ˜์ •**
    * `didRegisterForRemoteNotificationsWithDeviceToken` ๋ฐ `didReceiveRemoteNotification` ๋“ฑ ์ฃผ์š” ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐœํ–‰ ๋ฐ ์ธ๋ดํŠธ๋ฅผ ์ •๋ฆฌํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: iosApp Info.plist ์„ค์ • ์ •๋ฆฌ ๋ฐ ์ตœ์ ํ™”

* **chore: Info.plist ๋‚ด ๋ถˆํ•„์š”ํ•œ ํ‚ค ์ œ๊ฑฐ ๋ฐ ์ค‘๋ณต ์ •๋ฆฌ**
    * `CFBundleName`, `CFBundleShortVersionString`, `CFBundleVersion` ๋“ฑ ํ”„๋กœ์ ํŠธ ์„ค์ •์—์„œ ๊ด€๋ฆฌ๋˜๋Š” ์ค‘๋ณต ํ‚ค๋ฅผ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ค‘๋ณต ์„ ์–ธ๋˜์–ด ์žˆ๋˜ `CADisableMinimumFrameDurationOnPhone` ํ•ญ๋ชฉ์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FirebaseAppDelegateProxyEnabled` ์„ค์ •์˜ ์œ„์น˜๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ํ˜•์‹์„ ํ†ต์ผํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: MyFirebaseMessagingService ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐฉ์‹ ๋ณ€๊ฒฝ

* **refactor: MyFirebaseMessagingService ๋‚ด ์˜์กด์„ฑ ํš๋“ ๋ฐฉ์‹ ์ˆ˜์ •**
    * `@Inject` ํ•„๋“œ ์ฃผ์ž… ๋ฐ `onCreate`์—์„œ์˜ ๋ช…์‹œ์  `inject` ํ˜ธ์ถœ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `festivalLocalDataSource`๋ฅผ `application.androidAppGraph`์—์„œ ์ง์ ‘ ๊ฐ€์ ธ์˜ค๋Š” lazy ํ”„๋กœํผํ‹ฐ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์˜์กด์„ฑ ํš๋“ ๋ฐฉ์‹์„ ๊ฐ„์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: AndroidAppGraph ์ธํ„ฐํŽ˜์ด์Šค ์ •๋ฆฌ**
    * `FirebaseMessagingService`๋ฅผ ์œ„ํ•œ `inject` ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์„œ๋น„์Šค ๋“ฑ์—์„œ ์ง์ ‘ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก `festivalLocalDataSource` ํ”„๋กœํผํ‹ฐ๋ฅผ ์ธํ„ฐํŽ˜์ด์Šค์— ๋…ธ์ถœํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: FestabookGlobalExceptionHandler ๋‚ด ์˜ˆ์™ธ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ๋กœ๊น… ์ถ”๊ฐ€

* **feat: ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์‹œ ๋กœ๊ทธ ๊ธฐ๋ก ๋กœ์ง ์ถ”๊ฐ€**
    * `FestabookGlobalExceptionHandler`์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ `ErrorActivity`๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์ „, `Napier`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜ˆ์™ธ์˜ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๋ฅผ ๋กœ๊ทธ๋กœ ๋‚จ๊ธฐ๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: iOS FCM ํ† ํฐ ๋“ฑ๋ก ๋กœ์ง ์ˆ˜์ •

* **refactor: `DefaultFirebaseMessagingDelegate` ๋‚ด ํ† ํฐ ํš๋“ ๋ฐฉ์‹ ๋ณ€๊ฒฝ**
    * ๊ธฐ์กด์˜ `didReceiveRegistrationToken` ์ธ์ž๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€์‹ , `FIRMessaging.messaging().tokenWithCompletion` ์ฝœ๋ฐฑ์„ ํ†ตํ•ด ์ตœ์‹  FCM ํ† ํฐ์„ ๋ช…์‹œ์ ์œผ๋กœ ๊ฐ€์ ธ์˜ค๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋””๋ฒ„๊น…์„ ์œ„ํ•ด ํ† ํฐ ๋ฐ ์—๋Ÿฌ ์ •๋ณด๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ๋กœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๊ธฐ์กด `NSLog` ํ˜ธ์ถœ์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: iOS Delegate ์˜์กด์„ฑ ์ฃผ์ž… ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ CoroutineScope ๊ด€๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ

* **refactor: iOS App Delegate ๋ฐ Delegate ํด๋ž˜์Šค ๋ฆฌํŒฉํ„ฐ๋ง**
    * `DefaultFestabookAppDelegate`์—์„œ ์ง์ ‘ ์ƒ์„ฑํ•˜๋˜ `DefaultFirebaseMessagingDelegate`์™€ `DefaultUserNotificationDelegate`๋ฅผ `IosAppGraph`๋ฅผ ํ†ตํ•ด ์ฃผ์ž…๋ฐ›๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DefaultFirebaseMessagingDelegate`์™€ `DefaultUserNotificationDelegate`์— `@Inject` ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ , `DefaultFirebaseMessagingDelegate`๊ฐ€ `DeviceRepository`๋ฅผ ์ƒ์„ฑ์ž ์ฃผ์ž…์œผ๋กœ ๋ฐ›๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์˜์กด์„ฑ ์ฃผ์ž…(Metro) ์„ค์ • ๋ฐ CoroutineScope ๊ตฌ๋ถ„**
    * `FestabookAppGraph` ๋ฐ `IosAppGraph`์—์„œ `CoroutineScope`๋ฅผ `Dispatchers.IO`์™€ `Dispatchers.Main`์œผ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ œ๊ณตํ•˜๋„๋ก `@Named` ํ•œ์ •์ž("IO", "Main")๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DeviceRepositoryImpl`์—์„œ `IO` ์Šค์ฝ”ํ”„๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๊ณ , `DefaultFirebaseMessagingDelegate`์—์„œ `Main` ์Šค์ฝ”ํ”„๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๊ธฐํƒ€ ์ฝ”๋“œ ์ •๋ฆฌ**
    * `DefaultFirebaseMessagingDelegate` ๋‚ด ๋ถˆํ•„์š”ํ•œ ์ฃผ์„์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * iOS ํ”Œ๋žซํผ ๋นŒ๋“œ๋ฅผ ์œ„ํ•œ `IosAppGraph` ์ธํ„ฐํŽ˜์ด์Šค์— ํ•„์š”ํ•œ ์†์„ฑ ๋ฐ ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: iOS ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„  ๋ฐ ๋กœ์ปฌ ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* **feat: iOS `DefaultUserNotificationDelegate` ๋‚ด ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ๋ฐ ๋กœ์ปฌ ์•Œ๋ฆผ ์—ฐ๋™**
    * ํฌ๊ทธ๋ผ์šด๋“œ ์ƒํƒœ์—์„œ ์ˆ˜์‹ ๋œ ์•Œ๋ฆผ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ปค์Šคํ…€ ๋กœ์ปฌ ์•Œ๋ฆผ์„ ์ƒ์„ฑํ•˜์—ฌ ํ‘œ์‹œํ•˜๋Š” `showLocalNotificationWithImage` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•Œ๋ฆผ ์‹๋ณ„์ž๊ฐ€ `festabook_local`๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋„๋ก ๋ถ„๊ธฐ ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•Œ๋ฆผ ํด๋ฆญ ์‘๋‹ต ์ฒ˜๋ฆฌ ์‹œ `festivalId`๋ฅผ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ์†Œ์Šค์— ์ €์žฅํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestivalLocalDataSource` ๋ฐ `CoroutineScope`๋ฅผ ์ƒ์„ฑ์ž ์ฃผ์ž… ๋ฐฉ์‹์œผ๋กœ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: Android `FestabookApp` ๋‚ด ์˜์กด์„ฑ ์ฃผ์ž… ์ˆ˜์ •**
    * `applicationScope` ํ•„๋“œ์— `@Named("IO")` ํ•œ์ •์ž๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์˜์กด์„ฑ ์ฃผ์ž… ๋Œ€์ƒ์„ ๋ช…ํ™•ํžˆ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๊ธฐํƒ€ ๋ณ€๊ฒฝ ์‚ฌํ•ญ**
    * iOS ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ๊ณผ์ •์—์„œ ๋กœ๊น…์„ ์œ„ํ•œ `Napier` ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DefaultUserNotificationDelegate` ๋‚ด ์ƒ์ˆ˜ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ `companion object`๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์˜์กด์„ฑ ์ฃผ์ž… ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ iOS ์•Œ๋ฆผ/FCM ํ† ํฐ ๊ฐฑ์‹  ๋กœ์ง ์ตœ์ ํ™”

* **refactor: Metro DI `@Named` ์–ด๋…ธํ…Œ์ด์…˜ ์ œ๊ฑฐ ๋ฐ CoroutineScope ์ฃผ์ž… ๋ฐฉ์‹ ๋‹จ์ˆœํ™”**
    * `FestabookAppGraph`, `IosAppGraph` ๋“ฑ DI ์„ค์ •์—์„œ ๋ถˆํ•„์š”ํ•œ `@Named("IO")`, `@Named("Main")` ๊ตฌ๋ถ„์„ ์ œ๊ฑฐํ•˜๊ณ  ๋‹จ์ผ `CoroutineScope`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DefaultUserNotificationDelegate`, `DeviceRepositoryImpl`, `FestabookApp` ๋“ฑ ๊ด€๋ จ ํด๋ž˜์Šค์˜ ์ƒ์„ฑ์ž ๋ฐ ํ•„๋“œ ์ฃผ์ž… ๋กœ์ง์„ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: iOS ์•Œ๋ฆผ ์•„์ด์ฝ˜ ์„ค์ • ๋ฐ ๋กœ์ปฌ ์•Œ๋ฆผ ๋กœ์ง ๊ฐœ์„ **
    * `DefaultUserNotificationDelegate`: ์•Œ๋ฆผ ๋ฐœ์ƒ ์‹œ ์•ฑ ๋‚ด ๋กœ๊ณ  ์•„์ด์ฝ˜(`logo_festabook_icon.png`)์ด ํ‘œ์‹œ๋˜๋„๋ก `imagePath` ์„ค์ • ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `showLocalNotificationWithImage` ๋ฉ”์„œ๋“œ ์ด๋ฆ„์„ `showNotification`์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , `festivalId` ๋ฐ `announcementId` ํ‚ค ๊ฐ’์„ ์ƒ์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **fix: DeviceRepository ๊ธฐ๊ธฐ ๋“ฑ๋ก ๋ฐ ํ† ํฐ ์ €์žฅ ์ˆœ์„œ ์กฐ์ •**
    * `DeviceRepositoryImpl.registerDevice`: ์„œ๋ฒ„ ๊ธฐ๊ธฐ ๋“ฑ๋ก API ํ˜ธ์ถœ ์„ฑ๊ณต ํ›„์—๋งŒ ๋กœ์ปฌ FCM ํ† ํฐ์„ ์ €์žฅํ•˜๋„๋ก ๋กœ์ง ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **perf: iOS FCM ํ† ํฐ ๋“ฑ๋ก ๋กœ์ง ์ตœ์ ํ™”**
    * `DefaultFirebaseMessagingDelegate`: `MutableSharedFlow`์™€ `debounce(3000L)`, `distinctUntilChanged`๋ฅผ ์ ์šฉํ•˜์—ฌ ๋‹จ๊ธฐ๊ฐ„์— ๋ฐœ์ƒํ•˜๋Š” ์ค‘๋ณต ํ† ํฐ ๋“ฑ๋ก ์š”์ฒญ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ๋ถ€ํ•˜๋ฅผ ์ค„์˜€์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด `FIRMessaging.messaging().tokenWithCompletion` ์ง์ ‘ ํ˜ธ์ถœ ๋ฐฉ์‹์—์„œ `didReceiveRegistrationToken` ์ด๋ฒคํŠธ๋ฅผ ๊ด€์ฐฐํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* [Chore] iOS ์•ฑ ์•„์ด์ฝ˜ ์ž์‚ฐ ์—…๋ฐ์ดํŠธ ๋ฐ ํ”„๋กœ์ ํŠธ ์„ค์ • ๋ณ€๊ฒฝ

* **chore: ์•ฑ ์•„์ด์ฝ˜ ์ด๋ฏธ์ง€ ์„ธํŠธ ๊ต์ฒด ๋ฐ ๊ตฌ์„ฑ ์ตœ์ ํ™”**
    * ๊ธฐ์กด `app-icon-1024.png` ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๊ณ , ๋‹ค์–‘ํ•œ ํ•ด์ƒ๋„์— ๋Œ€์‘ํ•˜๋Š” ๊ฐœ๋ณ„ ์•„์ด์ฝ˜ ํŒŒ์ผ๋“ค(16.png ~ 1024.png)์„ ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AppIcon.appiconset/Contents.json`์„ ์ˆ˜์ •ํ•˜์—ฌ ๋‹คํฌ ๋ชจ๋“œ ๋ฐ ํ‹ดํ‹ฐ๋“œ(tinted) ์™ธ๊ด€ ์„ค์ •์„ ์ œ๊ฑฐํ•˜๊ณ  ๋‹จ์ผ ๋ฒ”์šฉ(universal) ์ด๋ฏธ์ง€ ๊ตฌ์กฐ๋กœ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: Xcode ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ์„ค์ • ์ˆ˜์ •**
    * `ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS` ์˜ต์…˜์„ `YES`๋กœ ์„ค์ •ํ•˜์—ฌ ๋ชจ๋“  ์•„์ด์ฝ˜ ์ž์‚ฐ์ด ๋นŒ๋“œ์— ํฌํ•จ๋˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํŠน์ • ๋นŒ๋“œ ๊ตฌ์„ฑ์—์„œ `ASSETCATALOG_COMPILER_APPICON_NAME` ๊ฐ’์„ ์กฐ์ •ํ•˜์—ฌ ์•„์ด์ฝ˜ ์ฐธ์กฐ ๋ฐฉ์‹์„ ์ตœ์ ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: iOS ์•Œ๋ฆผ ๋‚ด ์ด๋ฏธ์ง€ ์ฒจ๋ถ€ ๋กœ์ง ์ œ๊ฑฐ

* **refactor: `DefaultUserNotificationDelegate` ๋‚ด ์•Œ๋ฆผ ์ด๋ฏธ์ง€ ์„ค์ • ์ฝ”๋“œ ์‚ญ์ œ**
    * `showNotification` ๋ฉ”์„œ๋“œ์—์„œ `imagePath` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , `UNNotificationAttachment`๋ฅผ ํ†ตํ•ด ์•Œ๋ฆผ์— ์ด๋ฏธ์ง€๋ฅผ ์ฒจ๋ถ€ํ•˜๋˜ ๋กœ์ง์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `userNotificationCenter`์—์„œ ๋ฉ”์ธ ๋ฒˆ๋“ค(Main Bundle)๋กœ๋ถ€ํ„ฐ ๋กœ๊ณ  ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋ฅผ ๊ฐ€์ ธ์˜ค๋˜ ๋ถˆํ•„์š”ํ•œ ๋กœ์ง์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `NSURL`, `UNNotificationAttachment` ๋“ฑ์˜ ์ž„ํฌํŠธ๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: FCM ๊ด€๋ จ ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค๋ฅผ ๊ณตํ†ต ๋ชจ๋“ˆ๋กœ ์ด๋™

* **refactor: Android ์ „์šฉ ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค๋ฅผ `commonMain`์œผ๋กœ ์ด์ „**
    * `androidMain`์˜ `strings.xml`์— ์ •์˜๋˜์–ด ์žˆ๋˜ FCM ๊ธฐ๋ณธ ์•Œ๋ฆผ ์ œ๋ชฉ(`default_notification_title`)๊ณผ ๋‚ด์šฉ(`default_notification_body`)์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `commonMain`์˜ `composeResources` ๋‚ด `strings.xml`๋กœ ํ•ด๋‹น ๋ฆฌ์†Œ์Šค๋ฅผ ์ด๋™ํ•˜๋ฉฐ ํ‚ค ์ด๋ฆ„์„ ๊ฐ๊ฐ `notification_default_title` ๋ฐ `notification_default_body`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„  ๋ฐ ๋‹ค๊ตญ์–ด ๋ฆฌ์†Œ์Šค ์ ์šฉ

* **feat: Android ๋ฐ iOS ์•Œ๋ฆผ ๋กœ์ง ๊ฐœ์„ **
    * `DefaultUserNotificationDelegate` (iOS) ๋ฐ `MyFirebaseMessagingService` (Android)์—์„œ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด ๋Œ€์‹  Compose Multiplatform ๋ฆฌ์†Œ์Šค(`Res.string`)๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * Android ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ์‹œ `ioCoroutineScope`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋น„๋™๊ธฐ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ณ , ์œ ํšจํ•˜์ง€ ์•Š์€ `festivalId`(-1)์— ๋Œ€ํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AndroidAppGraph`์— ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•  `ioCoroutineScope` ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: iOS FCM ํ† ํฐ ๋“ฑ๋ก ๋กœ์ง ์ˆ˜์ •**
    * `DefaultFirebaseMessagingDelegate`์—์„œ ๋ถˆํ•„์š”ํ•œ `distinctUntilChanged()` ํ˜ธ์ถœ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ์ฝ”๋“œ ์ •๋ฆฌ**
    * iOS ์•Œ๋ฆผ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๋‚ด ์ฝ”๋ฃจํ‹ด ์Šค์ฝ”ํ”„ ์ ์šฉ ๋ฒ”์œ„๋ฅผ ์ˆ˜์ •ํ•˜์—ฌ ๋น„๋™๊ธฐ ์ž‘์—… ์•ˆ์ •์„ฑ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์•Œ๋ฆผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ณ ๋„ํ™” ๋ฐ ID ํƒ€์ž… ํ†ต์ผ

* **refactor: Android ์•Œ๋ฆผ ์„œ๋น„์Šค ๋‚ด ID ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๊ฐœ์„ **
    * `MyFirebaseMessagingService` ๋ฐ `NotificationHelper`์—์„œ `festivalId`์™€ `announcementId`๋ฅผ `String` ๋Œ€์‹  `Long` ํƒ€์ž…์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ํƒ€์ž… ์•ˆ์ •์„ฑ์„ ๊ฐ•ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•Œ๋ฆผ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์‹œ ๋งค์ง ๋„˜๋ฒ„(`"-1"`) ๋Œ€์‹  `DeepLinkKeys.INITIALIZED_ID` ์ƒ์ˆ˜๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์‚ฌ์šฉํ•˜๋„๋ก ํ†ต์ผํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `handleMessageData` ๋‚ด ๋ถˆํ•„์š”ํ•œ ํƒ€์ž… ๋ณ€ํ™˜ ๋กœ์ง์„ ์ œ๊ฑฐํ•˜๊ณ  ์ƒ์ˆ˜ ๊ธฐ๋ฐ˜์˜ ์กฐ๊ฑด๋ฌธ์œผ๋กœ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: iOS ์•Œ๋ฆผ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๊ธฐ๋ณธ๊ฐ’ ์ƒ์ˆ˜ํ™”**
    * `DefaultUserNotificationDelegate`์—์„œ ์•Œ๋ฆผ ์ˆ˜์‹  ์‹œ `userInfo` ๋‚ด ID ๊ฐ’์ด ์—†์„ ๊ฒฝ์šฐ `DeepLinkKeys.INITIALIZED_ID`๋ฅผ ํ• ๋‹นํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: iOS ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„  ๋ฐ ๋”ฅ๋งํฌ ํ‚ค ์ƒ์ˆ˜ํ™”

* **refactor: iOS `UNUserNotificationCenterDelegate` ๊ตฌํ˜„ ์ •๋ฆฌ**
    * `DefaultUserNotificationDelegate`์—์„œ ๋ถˆํ•„์š”ํ•œ ๋กœ์ปฌ ์•Œ๋ฆผ ์žฌ์ƒ์„ฑ ๋กœ์ง(`showNotification`)์„ ์ œ๊ฑฐํ•˜๊ณ  ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ์•Œ๋ฆผ ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•Œ๋ฆผ ์‘๋‹ต(`didReceiveNotificationResponse`) ์ฒ˜๋ฆฌ ์‹œ, ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ `NSNotificationCenter` ์ด๋ฒคํŠธ๋ฅผ ํฌ์ŠคํŠธํ•˜๋„๋ก `withContext(Dispatchers.Main)`๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `festivalId` ๋ฐ `announcementId` ํŒŒ์‹ฑ ๋กœ์ง์„ ๊ฐœ์„ ํ•˜๊ณ  `DeepLinkKeys` ์ƒ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ๋”ฅ๋งํฌ ๊ด€๋ จ ์ƒ์ˆ˜ ์ •์˜ ๋ฐ ์ ์šฉ**
    * `DeepLinkKeys` ๋‚ด์— `KEY_FESTIVAL_ID`, `KEY_ANNOUNCEMENT_ID`, `KEY_FCM_NOTIFICATION` ์ƒ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * iOS ํ”Œ๋žซํผ์˜ `RememberDeepLinkHandler`์—์„œ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด์„ ์ •์˜๋œ ์ƒ์ˆ˜๋กœ ๊ต์ฒดํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: MainScreen ๋‚ด ๋ถˆํ•„์š”ํ•œ ์–ด๋…ธํ…Œ์ด์…˜ ์ œ๊ฑฐ**
    * `MainScreen` ์ปดํฌ์ €๋ธ”์—์„œ ๋” ์ด์ƒ ํ•„์š”ํ•˜์ง€ ์•Š์€ `@Suppress("ktlint:compose:vm-forwarding-check")` ์„ค์ •์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: FCM ํ† ํฐ ๋“ฑ๋ก ๋กœ์ง ๋ฐ ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€ ๋ฐฉ์‹ ๊ฐœ์„ 

* **refactor: `DeviceRepositoryImpl` ๋‚ด ํ† ํฐ ์ค‘๋ณต ๊ฒ€์‚ฌ ๋กœ์ง ์ถ”๊ฐ€**
    * `registerDevice` ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ์‹œ, ์ „๋‹ฌ๋ฐ›์€ `fcmToken`์ด ๊ธฐ์กด์— ์บ์‹ฑ๋œ ํ† ํฐ(`cachedFcmToken`)๊ณผ ๋™์ผํ•  ๊ฒฝ์šฐ ์„œ๋ฒ„ ๋“ฑ๋ก ๊ณผ์ •์„ ์ƒ๋žตํ•˜๊ณ  ์ฆ‰์‹œ ์„ฑ๊ณต์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `DefaultFirebaseMessagingDelegate` (iOS) ๋กœ์ง ๊ฐ„์†Œํ™”**
    * `DeviceRepository`์—์„œ ์ค‘๋ณต ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•จ์— ๋”ฐ๋ผ, `init` ๋ธ”๋ก ๋‚ด์—์„œ ๋ณ„๋„๋กœ ์ €์žฅ๋œ ํ† ํฐ์„ ์กฐํšŒํ•˜์—ฌ ๋น„๊ตํ•˜๋˜ ๋ถˆํ•„์š”ํ•œ ๋กœ์ง์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: iOS FCM ํ† ํฐ ๋“ฑ๋ก ๋กœ์ง ์ˆ˜์ • ๋ฐ ์ตœ์ ํ™”

* **feat: FCM ํ† ํฐ ๋“ฑ๋ก ๋ฐฉ์‹ ๋ณ€๊ฒฝ**
    * `DefaultFestabookAppDelegate`์˜ `didRegisterForRemoteNotificationsWithDeviceToken` ๋‹จ๊ณ„์—์„œ `firebaseMessagingDelegate.registerFcmToken()`์„ ํ˜ธ์ถœํ•˜์—ฌ ํ† ํฐ์„ ๋“ฑ๋กํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DefaultFirebaseMessagingDelegate`์—์„œ `MutableSharedFlow`์™€ `debounce`๋ฅผ ์ด์šฉํ•ด ํ† ํฐ์„ ์ˆ˜์ง‘ํ•˜๋˜ ๊ธฐ์กด ๋กœ์ง์„ ์ œ๊ฑฐํ•˜๊ณ , `FIRMessaging.tokenWithCompletion`์„ ํ†ตํ•ด ์ง์ ‘ ํ† ํฐ์„ ๊ฐ€์ ธ์™€ ๋“ฑ๋กํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๋กœ๊ทธ ๋ฐ ์ฃผ์„ ์ถ”๊ฐ€**
    * `DefaultFestabookAppDelegate` ๋‚ด ์•ฑ ์‹คํ–‰ ๋‹จ๊ณ„์— ๋Œ€ํ•œ ์ฃผ์„์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํ† ํฐ ํš๋“ ์‹คํŒจ ์‹œ ์—๋Ÿฌ ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•˜๋„๋ก `Napier`๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* chore: bump iOS app version to 2.1.0 (20100)

* **chore: iOS ๋ฒ„์ „ ๋ฐ ๋นŒ๋“œ ๋ฒˆํ˜ธ ์—…๋ฐ์ดํŠธ**
    * `Info.plist` ํŒŒ์ผ์— `CFBundleShortVersionString` (2.1.0) ๋ฐ `CFBundleVersion` (20100) ์„ค์ •์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: MainScreen ๋‚ด ktlint rule (vm-forwarding-check) ์–ต์ œ ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€

* **feat: `MainScreen` ํ•จ์ˆ˜์— `@Suppress("ktlint:compose:vm-forwarding-check")` ์ถ”๊ฐ€**
    * ViewModel ์ „๋‹ฌ ๊ด€๋ จ ktlint ๊ทœ์น™ ์ฒดํฌ๋ฅผ ๋ฌด์‹œํ•˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

---------

Co-authored-by: oungsi2000 <oungsi1000@gmail.com>
* chore: Android ์•ฑ ์•„์ด์ฝ˜ ์ž์‚ฐ ๊ต์ฒด ๋ฐ ์„ค์ • ์—…๋ฐ์ดํŠธ

* **chore: Android ์•ฑ ์•„์ด์ฝ˜ ์ด๋ฏธ์ง€ ์ž์‚ฐ ๊ต์ฒด ๋ฐ WebP ํฌ๋งท ์ ์šฉ**
    * ๊ธฐ์กด PNG ํ˜•์‹์˜ `ic_launcher` ๋ฐ `ic_launcher_round` ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๊ณ , ์ตœ์ ํ™”๋œ WebP ํ˜•์‹์˜ ํŒŒ์ผ๋“ค๋กœ ๊ต์ฒดํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์‹ ๊ทœ ๋กœ๊ณ  ์ž์‚ฐ(`ic_festabook_logo`, `ic_festabook_logo_foreground`, `ic_festabook_logo_round`)์„ ํ•ด์ƒ๋„๋ณ„(hdpi ~ xxxhdpi)๋กœ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์ ์‘ํ˜• ์•„์ด์ฝ˜(Adaptive Icon) ์„ค์ • ๋ฐ ๋ฐฐ๊ฒฝ์ƒ‰ ์ถ”๊ฐ€**
    * `ic_festabook_logo.xml` ๋ฐ `ic_festabook_logo_round.xml`: ๋ฐฐ๊ฒฝ(`ic_festabook_logo_background`)๊ณผ ์ „๊ฒฝ ์ž์‚ฐ์„ ๊ฒฐํ•ฉํ•œ ์ ์‘ํ˜• ์•„์ด์ฝ˜ ์„ค์ •์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ic_festabook_logo_background.xml`: ๋กœ๊ณ  ๋ฐฐ๊ฒฝ์ƒ‰์„ ํฐ์ƒ‰(#FFFFFF)์œผ๋กœ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด `ic_launcher.xml` ๋ฐ `ic_launcher_round.xml` ์„ค์ • ํŒŒ์ผ์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* **build: AndroidManifest.xml ๋‚ด ์•ฑ ์•„์ด์ฝ˜ ์ฐธ์กฐ ์ˆ˜์ •**
    * ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ธฐ๋ณธ ์•„์ด์ฝ˜(`android:icon`)๊ณผ ๋ผ์šด๋“œ ์•„์ด์ฝ˜(`android:roundIcon`) ์ฐธ์กฐ ๊ฒฝ๋กœ๋ฅผ ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€๋œ `@mipmap/ic_festabook_logo` ๋ฐ `@mipmap/ic_festabook_logo_round`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: ์„ค์ •ํ™”๋ฉด ์•„์ด์ฝ˜ ์ƒ‰์ƒ, ์ฒซ ์ง„์ž… ์‹œ, ๋นˆ ๊ฒ€์ƒ‰์ผ ๋•Œ ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ํ…์ŠคํŠธ ์ž ๊น ๋œจ๋Š” ๋ฒ„๊ทธํ•ด๊ฒฐ

* **refactor: `ExploreSearchResultList` ๋‚ด ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ํ—ค๋” ํ‘œ์‹œ ๋กœ์ง ์ˆ˜์ •**
    * `isLanding` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋žœ๋”ฉ ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ์—๋Š” "์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด" ๋ฉ”์‹œ์ง€ ๋ทฐ(`RecentSearchMessageView`)๊ฐ€ ๋…ธ์ถœ๋˜์ง€ ์•Š๋„๋ก ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreLandingContent`์—์„œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ ํ˜ธ์ถœ ์‹œ `isLanding = true` ์˜ต์…˜์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: `SettingScreen` ๋‚ด ์•„์ด์ฝ˜ ์ƒ‰์ƒ ์ˆ˜์ •**
    * ์„ค์ • ํ•ญ๋ชฉ์˜ ์ด๋™ ํ™”์‚ดํ‘œ ์•„์ด์ฝ˜(`ic_arrow_forward_right`)์— ์ ์šฉ๋˜์–ด ์žˆ๋˜ `Color.Unspecified` ํ‹ดํŠธ๋ฅผ `FestabookColor.gray300`์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์‹œ๊ฐ์  ์ผ๊ด€์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.
* feat: ์›จ์ดํŒ… data layer ์•„ํ‚คํ…์ฒ˜ ๊ตฌํ˜„ (#105)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: Swagger ์ŠคํŽ™ ๊ธฐ๋ฐ˜ ์›จ์ดํŒ… ๋ฐ์ดํ„ฐ ๊ณ„์ธต ์ˆ˜์ •

* test: ์›จ์ดํŒ… data layer ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€

* refactor: WaitingLocalDataSource ์ œ๊ฑฐ ๋ฐ PlaceDetailResponse ์ •๋ฆฌ

* feat: Device ํ—ค๋” ๋ฐ ์ „ํ™”๋ฒˆํ˜ธ API ์—ฐ๋™

* refactor: WaitingStatus @serializable ๋ณ€ํ™˜ ๋ฐ ์ฝ”๋“œ ํฌ๋งทํŒ…

* refactor: ์›จ์ดํŒ… ๊ด€๋ จ API ์—”๋“œํฌ์ธํŠธ ์ด๋™ ๋ฐ Service ์ธํ„ฐํŽ˜์ด์Šค ์ •๋ฆฌ

* **refactor: ์žฅ์†Œ ๊ด€๋ จ ์›จ์ดํŒ… API๋ฅผ `WaitingService`์—์„œ `PlaceService`๋กœ ์ด๋™**
    * `places/{placeId}/waiting-status` (์กฐํšŒ) ๋ฐ `places/{placeId}/waitings` (๋“ฑ๋ก) API๋ฅผ ๋„๋ฉ”์ธ ์„ฑ๊ฒฉ์— ๋งž์ถฐ `PlaceService`๋กœ ์žฌ๋ฐฐ์น˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingService`์—๋Š” ๋ณธ์ธ์˜ ์›จ์ดํŒ… ์ •๋ณด ์กฐํšŒ ๋ฐ ์ทจ์†Œ API๋งŒ ๋‚จ๋„๋ก ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `WaitingRemoteDataSourceImpl` ๋‚ด ์„œ๋น„์Šค ์˜์กด์„ฑ ์ˆ˜์ •**
    * `PlaceService`๋ฅผ ์ฃผ์ž…๋ฐ›์•„ ์ด๋™๋œ ์›จ์ดํŒ… ๊ด€๋ จ API(`fetchPlaceWaiting`, `registerWaiting`)๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ๋กœ์ง์„ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ถˆํ•„์š”ํ•ด์ง„ ์ž„ํฌํŠธ ๋ฐ ๊ด€๋ จ ์š”์ฒญ/์‘๋‹ต ๋ชจ๋ธ ์ฐธ์กฐ๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์›จ์ดํŒ… ์กด์žฌ ์—ฌ๋ถ€ API ์—ฐ๋™ ๋ฐ 404 ๋Œ€์‹  exist ์ฒดํฌ ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝ

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: WaitingLocalDataSource ์ถ”๊ฐ€ ๋ฐ WaitingInfoRepository ์ „ํ™”๋ฒˆํ˜ธ ์ €์žฅ/์ˆ˜์ • ๋กœ์ง ๊ตฌํ˜„

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: WaitingStatus @serializable ์ œ๊ฐ€

* **refactor: `WaitingStatus` enum ํด๋ž˜์Šค์—์„œ `@Serializable` ์–ด๋…ธํ…Œ์ด์…˜ ์ œ๊ฑฐ**
    * ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `kotlinx.serialization.Serializable` ์ž„ํฌํŠธ ๋ฌธ๊ณผ ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: PlaceDetail ๋ชจ๋ธ ๋ฐ ๊ด€๋ จ ์ฝ”๋“œ ๋ณ€์ˆ˜๋ช… ์ˆ˜์ •

* **refactor: `PlaceDetail` ๋„๋ฉ”์ธ ๋ชจ๋ธ์˜ ์†์„ฑ๋ช… ๋ณ€๊ฒฝ**
    * ๋Œ€๊ธฐ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” `isWaitingAvailable` ๋ณ€์ˆ˜๋ช…์„ `isWaitingActive`๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ๋ฐ์ดํ„ฐ ๋งคํ•‘ ๋ฐ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋ฐ˜์˜**
    * `PlaceDetailResponse`์—์„œ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋กœ์˜ ๋งคํ•‘ ๋กœ์ง์„ ๋ณ€๊ฒฝ๋œ ๋ณ€์ˆ˜๋ช…์— ๋งž๊ฒŒ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceDetailTestFixture` ๋‚ด ํ…Œ์ŠคํŠธ์šฉ ๊ฐ€์งœ ๋ฐ์ดํ„ฐ(Fake Data)์˜ ํ•„๋“œ๋ช…์„ `isWaitingActive`๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
build: iOS Staging ๋นŒ๋“œ ๊ตฌ์„ฑ ์ถ”๊ฐ€ ๋ฐ ๋ฒ„์ „ ๋™๊ธฐํ™” ๋กœ์ง ๊ฐœ์„ 

* **feat: iOS 'Staging' ๋นŒ๋“œ ๊ตฌ์„ฑ ์ถ”๊ฐ€**
    * `project.pbxproj`: `Staging` ๋นŒ๋“œ ๊ตฌ์„ฑ์„ ์ƒˆ๋กญ๊ฒŒ ์ •์˜ํ•˜๊ณ , `Debug`์™€ ๋™์ผํ•œ ๋ฒˆ๋“ค ID(`debug.eoehdeksruf.festabook`) ๋ฐ ํ”„๋กœ๋น„์ €๋‹ ํ”„๋กœํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `project.pbxproj`: ๋นŒ๋“œ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ˆ˜์ •ํ•˜์—ฌ `Staging` ๊ตฌ์„ฑ์—์„œ๋„ `Debug`์šฉ Firebase ์„ค์ •(`GoogleService-Info-DEBUG.plist`)์„ ์ฐธ์กฐํ•˜๋„๋ก ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `Podfile`: `Staging` ๊ตฌ์„ฑ์„ `:release` ๋ชจ๋“œ๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ”„๋กœ์ ํŠธ ์„ค์ •์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: Android `Staging` ๋นŒ๋“œ ๋ฒ„์ „(Flavor) ์ถ”๊ฐ€**
    * `build.gradle.kts`: `Staging` ๋นŒ๋“œ ๋ฒ„์ „์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ฐœ๋ฐœ์šฉ API URL ๋ฐ ๋ฒˆ๋“ค ID๋ฅผ ์ฃผ์ž…ํ•˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: iOS ๋ฒ„์ „ ๊ด€๋ฆฌ ์ž๋™ํ™” ๋ฐ ์ดˆ๊ธฐ๊ฐ’ ๋™๊ธฐํ™”**
    * `build.gradle.kts`: `updateIosVersion` ํƒœ์Šคํฌ์—์„œ `Info.plist`๋ฟ๋งŒ ์•„๋‹ˆ๋ผ `Config.xcconfig` ํŒŒ์ผ์˜ `MARKETING_VERSION`๊ณผ `CURRENT_PROJECT_VERSION`๋„ ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ๋กœ์ง์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `Config.xcconfig`: ์•ฑ ๋ฒ„์ „์„ `2.1.0 (20100)`์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ๋‹ค๋ฅธ ์„ค์ • ํŒŒ์ผ๋“ค๊ณผ ๋™๊ธฐํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `Info.plist`: ๋“ค์—ฌ์“ฐ๊ธฐ ํฌ๋งท์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* **build: iOS ๋ฐฐํฌ ๋ฐ ์‚ฌ์ด๋‹ ์„ค์ • ์ˆ˜์ •**
    * `project.pbxproj`: `Debug` ๋ฐ `Staging` ๊ตฌ์„ฑ์˜ `IPHONEOS_DEPLOYMENT_TARGET`์„ `17.0`์œผ๋กœ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `CODE_SIGN_IDENTITY`๋ฅผ `iPhone Distribution`์œผ๋กœ, ํ”„๋กœ๋น„์ €๋‹ ํ”„๋กœํŒŒ์ผ์„ `festabook_distribution_debug_provisioning_profile`๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ฐฐํฌ ์„ค์ •์„ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.
* ci: GitHub Actions runner๋ฅผ self-hosted๋กœ ์ „ํ™˜

GitHub-hosted runner ์‚ฌ์šฉ ๋ถˆ๊ฐ€๋กœ ์ธํ•ด
๋ชจ๋“  ์›Œํฌํ”Œ๋กœ์šฐ์˜ runs-on์„ self-hosted๋กœ ๋ณ€๊ฒฝ

closes #129

* fix: setup android ์ถ”๊ฐ€

* chore: CI ์›Œํฌํ”Œ๋กœ์šฐ ๋‚ด ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๊ฒŒ์‹œ ์•ก์…˜ ๊ฒฝ๋กœ ์ˆ˜์ •

* **chore: `publish-unit-test-result-action` ์‚ฌ์šฉ ๊ฒฝ๋กœ ์—…๋ฐ์ดํŠธ**
    * `.github/workflows/cmp-ci.yml`: `EnricoMi/publish-unit-test-result-action@v2`๋ฅผ `EnricoMi/publish-unit-test-result-action/linux@v2`๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ํŠน์ • ํ”Œ๋žซํผ ์‹คํ–‰ ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

---------

Co-authored-by: oungsi2000 <oungsi1000@gmail.com>
fix: iOS ๋„ค์ด๋ฒ„ ์ง€๋„ ์ปจํ…์ธ  ํŒจ๋”ฉ ์„ค์ • ์˜ค๋ฅ˜ ์ˆ˜์ • ๋ฐ ์ดˆ๊ธฐ ํŒจ๋”ฉ ๊ฐ’ ์กฐ์ •

* **fix: iOS `NaverMap`์˜ `setContentInset` ํ˜ธ์ถœ ๋ฐฉ์‹ ๋ฐ ์ธ์ž ์ˆœ์„œ ์ˆ˜์ •**
    * `NaverMap.ios.kt`: `platformMap.mapView.contentInset` ์†์„ฑ์— ์ง์ ‘ ๊ฐ’์„ ํ• ๋‹นํ•˜๋˜ ๋ฐฉ์‹์—์„œ `setContentInset(_:animate:)` ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง€์›์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `UIEdgeInsetsMake` ํ˜ธ์ถœ ์‹œ ์ž˜๋ชป ๋งคํ•‘๋˜์–ด ์žˆ๋˜ ์ธ์ž ์ˆœ์„œ(left, top, right, bottom)๋ฅผ iOS UIKit ํ‘œ์ค€ ์ˆœ์„œ(top, left, bottom, right)์— ๋งž๊ฒŒ ๊ต์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์ง€๋„ ์ดˆ๊ธฐ ๋ฐ”ํ…€ ํŒจ๋”ฉ ๊ฐ’ ์กฐ์ •**
    * `MapManager.kt`: UI ๋ ˆ์ด์•„์›ƒ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด `INITIAL_PADDING` ๊ฐ’์„ `254.dp`์—์„œ `180.dp`๋กœ ํ•˜ํ–ฅ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
* fix: Android ํ”Œ๋žซํผ์—์„œ๋งŒ ๋ฐฑ ๋ฒ„ํŠผ ๋™์ž‘์ด ์ˆ˜ํ–‰๋˜๋„๋ก ์ˆ˜์ •

* **fix: `MainScreen` ๋‚ด ๋ฐฑ ๋ฒ„ํŠผ ์ฒ˜๋ฆฌ ๋กœ์ง ํ”Œ๋žซํผ ๋ถ„๊ธฐ ์ถ”๊ฐ€**
    * `NavigationBackHandler`์—์„œ `getPlatform()`์„ ํ™•์ธํ•˜์—ฌ `Platform.ANDROID`์ผ ๋•Œ๋งŒ `mainViewModel.onBackPressed()`๊ฐ€ ํ˜ธ์ถœ๋˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” iOS ๋“ฑ ํƒ€ ํ”Œ๋žซํผ์—์„œ ์˜๋„์น˜ ์•Š๊ฒŒ ๋ฐฑ ๋ฒ„ํŠผ ๋กœ์ง์ด ํŠธ๋ฆฌ๊ฑฐ๋˜๋Š” ํ˜„์ƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

* **feat: ๋ฉ€ํ‹ฐํ”Œ๋žซํผ ๋Œ€์‘์„ ์œ„ํ•œ `Platform` ํด๋ž˜์Šค ๋ฐ ์ธํ„ฐํŽ˜์ด์Šค ์ถ”๊ฐ€**
    * `commonMain`: `Platform` enum ํด๋ž˜์Šค(ANDROID, IOS)์™€ ํ”Œ๋žซํผ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” `getPlatform()` expect ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `androidMain`: `getPlatform()`์˜ actual ๊ตฌํ˜„์œผ๋กœ `Platform.ANDROID`๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `iosMain`: `getPlatform()`์˜ actual ๊ตฌํ˜„์œผ๋กœ `Platform.IOS`๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ํ”Œ๋žซํผ ๊ตฌ๋ถ„ ๋กœ์ง ๊ฐœ์„  ๋ฐ `expect/actual` ์ œ๊ฑฐ

* **refactor: `expect/actual` ๊ธฐ๋ฐ˜์˜ ํ”Œ๋žซํผ ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜ ๋กœ์ง ์ œ๊ฑฐ**
    * `FestivalNotificationPlatform.kt` (`expect`), `FestivalNotificationEndpoint.android.kt`, `FestivalNotificationEndpoint.ios.kt` (`actual`) ํŒŒ์ผ์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ณตํ†ต ๋กœ์ง์ธ `getPlatform()`์„ ์‚ฌ์šฉํ•˜์—ฌ ํ”Œ๋žซํผ๋ณ„ ๋ฌธ์ž์—ด("android", "ios")์„ ๊ฒฐ์ •ํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `FestivalNotificationRemoteDataSourceImpl` ๋‚ด ํ”Œ๋žซํผ ํŒŒ๋ผ๋ฏธํ„ฐ ์ฃผ์ž… ๋ฐฉ์‹ ์ˆ˜์ •**
    * `festivalNotificationPlatform()` ํ˜ธ์ถœ ๋Œ€์‹ , companion object ๋‚ด์—์„œ `getPlatform()` ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ์ •์˜๋œ `platform` ์ƒ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: `FestivalNotificationRemoteDataSourceTest` ๋‚ด ํ”Œ๋žซํผ ์—”๋“œํฌ์ธํŠธ ๋กœ์ง ๋ฐ˜์˜**
    * ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ๋„ `getPlatform()` ๊ธฐ๋ฐ˜์œผ๋กœ ์—”๋“œํฌ์ธํŠธ ๋ฌธ์ž์—ด์„ ๊ฒฐ์ •ํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๋ฐ˜์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
style: MainScreen ๋‚ด ๋„ค๋น„๊ฒŒ์ด์…˜ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ๊ฑฐ

* **style: NavHost ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋น„ํ™œ์„ฑํ™”**
    * `MainScreen`์˜ `NavHost` ์„ค์ •์— `enterTransition` ๋ฐ `exitTransition`์„ `EnterTransition.None`๊ณผ `ExitTransition.None`์œผ๋กœ ๊ฐ๊ฐ ์„ค์ •ํ•˜์—ฌ ํ™”๋ฉด ์ „ํ™˜ ์‹œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
* feat: WaitingUiState sealed interface ์ถ”๊ฐ€

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: PlaceDetailViewModel ์›จ์ดํŒ… ์ƒํƒœ ๋กœ๋“œ ๋กœ์ง ์ถ”๊ฐ€

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: ํ”Œ๋ ˆ์ด์Šค ์ƒ์„ธ ํ™”๋ฉด ์‹ค์‹œ๊ฐ„ ์›จ์ดํŒ… ์„น์…˜ ๋ฐ ํ•˜๋‹จ ๋ฒ„ํŠผ UI ์ถ”๊ฐ€

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: PlaceDetailViewModelTest ์›จ์ดํŒ… ์ƒํƒœ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: PlaceDetailUiModel์— isWaitingActive ํ•„๋“œ ์ถ”๊ฐ€

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: receivedPlaceDetail ์ง„์ž… ์‹œ ์›จ์ดํŒ… ์ƒํƒœ ๋กœ๋“œ ์ถ”๊ฐ€

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: ํ”Œ๋ ˆ์ด์Šค ์ƒ์„ธ ํ™”๋ฉด ์›จ์ดํŒ… ์„น์…˜ ์ˆœ์„œ ์ˆ˜์ • (์„ค๋ช… ํ…์ŠคํŠธ ์ด์ „์œผ๋กœ ์ด๋™)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: ์‹ค์‹œ๊ฐ„ ์›จ์ดํŒ… ์„น์…˜ UI ํ”ผ๊ทธ๋งˆ ๋””์ž์ธ์— ๋งž๊ฒŒ ์ˆ˜์ •

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: ์‹ค์‹œ๊ฐ„ ์›จ์ดํŒ… ์„น์…˜ ํ•˜๋‹จ 4dp ๊ตฌ๋ถ„์„  ์ถ”๊ฐ€

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: 4dp ๊ตฌ๋ถ„์„  ์œ„์น˜ ์ˆ˜์ • (PlaceWaitingSection ์™ธ๋ถ€ ์„น์…˜ ๊ตฌ๋ถ„์ž๋กœ ์ด๋™)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: ํ•˜๋‹จ ๋ฐ” ๋ฒ„ํŠผ ๋ชจ์–‘ pill ํ˜•ํƒœ๋กœ ์ˆ˜์ • (RoundedCornerShape 8dp โ†’ 50dp)

* fix: ์›จ์ดํŒ… ๋กœ๋”ฉ ์ƒํƒœ ํ”Œ๋ ˆ์ด์Šคํ™€๋” ํšŒ์ƒ‰ ์Šค์ผˆ๋ ˆํ†ค ๋ฐ•์Šค๋กœ ๋ณ€๊ฒฝ

* feat: ์žฅ์†Œ ์ƒ์„ธ ํŽ˜์ด์ง€ ๋‚ด ์›จ์ดํŒ… ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* **feat: ์žฅ์†Œ ์ƒ์„ธ ์›จ์ดํŒ… ์ •๋ณด ์ƒˆ๋กœ๊ณ ์นจ ๊ธฐ๋Šฅ ์ถ”๊ฐ€**
    * `PlaceDetailScreen`: ์›จ์ดํŒ… ์ƒํƒœ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๊ฐฑ์‹ ํ•  ์ˆ˜ ์žˆ๋Š” `onWaitingRefresh` ์ฝœ๋ฐฑ์„ ์ถ”๊ฐ€ํ•˜๊ณ , `PlaceWaitingContent`์— ์ „๋‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceDetailViewModel`: `loadWaitingStatus` ๋ฉ”์„œ๋“œ๋ฅผ `public`์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ํ˜ธ์ถœ ์‹œ ํ˜„์žฌ `PlaceDetail` ์ƒํƒœ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ `placeId`์™€ ํ™œ์„ฑํ™” ์—ฌ๋ถ€๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ๋กœ์ง์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceDetailViewModel`: ์ƒˆ๋กœ๊ณ ์นจ ์‹œ์ž‘ ์‹œ `WaitingUiState.Loading` ์ƒํƒœ๋ฅผ ๋…ธ์ถœํ•˜์—ฌ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์›จ์ดํŒ… ์ฝ˜ํ…์ธ  ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ ์Šคํƒ€์ผ ์ˆ˜์ •**
    * `PlaceWaitingContent.kt`: `PlaceWaitingContent` ๋‚ด์—์„œ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๋‚˜๋‰˜์–ด ์žˆ๋˜ `when` ๋ถ„๊ธฐ ๋กœ์ง์„ `CurrentWaitingTeams` ๋‚ด๋ถ€๋กœ ์œ„์ž„ํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ๊ฐ„์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `CurrentWaitingTeams`: ๋กœ๋”ฉ ์ƒํƒœ(Loading)์ผ ๋•Œ ๋ฐฐ๊ฒฝ ํ˜•์ƒ์„ `festabookShapes.radius1`์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ๋””์ž์ธ ์‹œ์Šคํ…œ ์ผ๊ด€์„ฑ์„ ๋งž์ท„์Šต๋‹ˆ๋‹ค.
    * `CurrentWaitingTeams`: ํ…์ŠคํŠธ ๋ ˆ์ด์•„์›ƒ ๋ฐ ๊ฐ€๋…์„ฑ ๊ฐœ์„ ์„ ์œ„ํ•ด ์ฝ”๋“œ ํฌ๋งทํŒ…์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๋ถˆํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค ๋ฐ ์ž„ํฌํŠธ ์ œ๊ฑฐ**
    * ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค `place_detail_waiting_inactive`๋ฅผ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceDetailViewModel`์—์„œ ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ(`CancellationException`)๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์žฅ์†Œ ์ƒ์„ธ ํ™”๋ฉด ์›จ์ดํŒ… ์ƒํƒœ ๋ถ„๋ฆฌ ๋ฐ UI ๋กœ์ง ๊ฐœ์„ 

* **refactor: `WaitingUiState`๋ฅผ `WaitingTeamUiState`์™€ `WaitingStatusUiState`๋กœ ๋ถ„๋ฆฌ**
    * ๊ธฐ์กด `WaitingUiState`๋ฅผ ๋Œ€๊ธฐ ํŒ€ ์ •๋ณด(`WaitingTeamUiState`)์™€ ์›จ์ดํŒ… ํ™œ์„ฑํ™” ์ƒํƒœ(`WaitingStatusUiState`)๋ฅผ ๋‹ค๋ฃจ๋Š” ๋‘ ๊ฐœ์˜ ์ƒํƒœ ๊ฐ์ฒด๋กœ ์„ธ๋ถ„ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingStatusUiState`: `Active`(์˜ˆ์ƒ ์‹œ๊ฐ„ ํฌํ•จ), `Closed`, `Loading` ์ƒํƒœ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
    * `WaitingTeamUiState`: `Success`(๋Œ€๊ธฐ ํŒ€ ์ˆ˜ ํฌํ•จ), `Loading`, `Error` ์ƒํƒœ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
    * `PlaceDetailUiState.Success`: ์œ„ ๋‘ ๊ฐ€์ง€ ์ƒํƒœ๋ฅผ ๊ฐ๊ฐ ๊ด€๋ฆฌํ•˜๋„๋ก ํ•„๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `PlaceDetailViewModel` ๋‚ด ์›จ์ดํŒ… ๋กœ๋“œ ๋ฐ ๊ฐฑ์‹  ๋กœ์ง ์ˆ˜์ •**
    * `refreshWaitingStatus`: ๋Œ€๊ธฐ ํŒ€ ์ •๋ณด(`waitingTeam`)๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋กœ๋”ฉ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•œ ๋’ค ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `loadWaitingStatus`: ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ ๋Œ€๊ธฐ ํŒ€ ์ˆ˜์™€ ์›จ์ดํŒ… ํ™œ์„ฑํ™” ์ƒํƒœ(Active/Closed)๋ฅผ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•˜์—ฌ UI ์ƒํƒœ์— ๋ฐ˜์˜ํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * API ์‹คํŒจ ์‹œ ์ „์ฒด ํ™”๋ฉด ์—๋Ÿฌ๊ฐ€ ์•„๋‹Œ ๊ฐœ๋ณ„ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋„๋ก ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: UI ์ปดํฌ๋„ŒํŠธ ๋‚ด ๋ถ„๋ฆฌ๋œ ์›จ์ดํŒ… ์ƒํƒœ ์ ์šฉ**
    * `PlaceWaitingContent`: ๋Œ€๊ธฐ ํŒ€ ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด `WaitingTeamUiState`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๊ณ , ์˜ˆ์™ธ ์ผ€์ด์Šค์— ๋Œ€ํ•œ ๋Œ€์‘ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceDetailBottomBar`: ํ•˜๋‹จ ๋ฒ„ํŠผ์˜ ํ™œ์„ฑํ™” ์ƒํƒœ ๋ฐ ์˜ˆ์ƒ ์‹œ๊ฐ„ ํ‘œ์‹œ๋ฅผ ์œ„ํ•ด `WaitingStatusUiState`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceDetailScreen`: ์ƒํƒœ ๋ถ„๋ฆฌ์— ๋”ฐ๋ฅธ ํŒŒ๋ผ๋ฏธํ„ฐ ์ „๋‹ฌ ๋กœ์ง์„ ์ˆ˜์ •ํ•˜๊ณ , ํ•˜๋‹จ ๋ฐ”์— ์›จ์ดํŒ… ๋“ฑ๋ก ํด๋ฆญ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ์ฝ”๋“œ ์ •๋ฆฌ ๋ฐ ๋„ค์ด๋ฐ ๋ณ€๊ฒฝ**
    * `loadWaitingStatus`๋ฅผ `refreshWaitingStatus`๋กœ ๋ช…์นญ์„ ๋ณ€๊ฒฝํ•˜์—ฌ ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ถˆํ•„์š”ํ•œ ์ŠคํŽ˜์ด์„œ ์กฐ๊ฑด๋ถ€ ๋…ธ์ถœ ๋กœ์ง์„ ์ •๋ฆฌํ•˜๊ณ  ํ•˜๋‹จ ์—ฌ๋ฐฑ์„ ๊ณ ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: ์›จ์ดํŒ… ์ƒ์„ธ ์ •๋ณด ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฐ ์Šค๋‚ต๋ฐ” ์•Œ๋ฆผ ์ถ”๊ฐ€

* **fix: `PlaceWaitingContent` ๋‚ด ์—๋Ÿฌ ์ƒํƒœ ์ฒ˜๋ฆฌ ๋ฐ ์Šค๋‚ต๋ฐ” ์—ฐ๋™**
    * `LaunchedEffect`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ `WaitingTeamUiState.Error` ์ƒํƒœ ๋ฐœ์ƒ ์‹œ `onShowErrorSnackbar` ์ฝœ๋ฐฑ์„ ํ†ตํ•ด ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•˜๋„๋ก ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceWaitingContent` ์ปดํฌ์ €๋ธ”์˜ ํŒŒ๋ผ๋ฏธํ„ฐ์— ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ `onShowErrorSnackbar` ์ฝœ๋ฐฑ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `PlaceDetailScreen` ๋‚ด ์›จ์ดํŒ… ์ฝ˜ํ…์ธ  ์ธ์ž ์ „๋‹ฌ ์ˆ˜์ •**
    * `PlaceDetailScreen`์—์„œ `PlaceWaitingContent`๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ํ˜„์žฌ ์ปจํ…์ŠคํŠธ์˜ ์—๋Ÿฌ ํ‘œ์‹œ ์ฝœ๋ฐฑ(`currentOnShowErrorSnackbar`)์„ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* test: PlaceDetailViewModelTest๋ฅผ WaitingTeamUiState / WaitingStatusUiState ๋ถ„๋ฆฌ ๊ตฌ์กฐ์— ๋งž๊ฒŒ ์ˆ˜์ •

* refactor: ์žฅ์†Œ ์ƒ์„ธ ํ™”๋ฉด ๋‚ด ์›จ์ดํŒ… ๋งˆ๊ฐ ์ƒํƒœ์˜ ์˜ˆ์ƒ ์‹œ๊ฐ„ ํ‘œ์‹œ ๋กœ์ง ์ถ”๊ฐ€

* **feat: ์›จ์ดํŒ… ๋งˆ๊ฐ(`Closed`) ์ƒํƒœ์—์„œ๋„ ์˜ˆ์ƒ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ํ‘œ์‹œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€**
    * `WaitingStatusUiState.Closed`: `data object`์—์„œ `data class`๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  `estimatedMinutes` ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceDetailBottomBar.kt`: `WaitingStatusUiState.Closed` ์ƒํƒœ์ผ ๋•Œ๋„ `EstimatedTime` ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋…ธ์ถœ๋˜๋„๋ก UI ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceDetailViewModel.kt`: ์›จ์ดํŒ… ์ƒํƒœ๊ฐ€ ๋น„ํ™œ์„ฑ(Closed)์ผ ๋•Œ๋„ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ `estimatedWaitTime`์„ UI ์ƒํƒœ์— ๋ฐ˜์˜ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `EstimatedTime` ์ปดํฌ๋„ŒํŠธ ๋ฒ”์šฉ์„ฑ ๊ฐœ์„ **
    * `EstimatedTime`์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํŠน์ • UI ์ƒํƒœ(`WaitingStatusUiState.Active`) ๋Œ€์‹  ์ผ๋ฐ˜ `Int` ํƒ€์ž…(`estimatedMinutes`)์„ ๋ฐ›๋„๋ก ๋ณ€๊ฒฝํ•˜์—ฌ ์—ฌ๋Ÿฌ ์ƒํƒœ์—์„œ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค ํ˜ธ์ถœ ์‹œ ์ œ๋ชฉ(`place_detail_waiting_estimated_time_title`) ๋ถ€๋ถ„์˜ ๋ถˆํ•„์š”ํ•œ ์ธ์ž ์ „๋‹ฌ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์›จ์ดํŒ… InActive ์ƒํƒœ ์ถ”๊ฐ€

* [Refactor] PlaceDetailViewModel ์˜์กด์„ฑ ๋‹จ์ˆœํ™” ๋ฐ ๊ด€๋ จ UI ๋ชจ๋ธ ์ •๋ฆฌ

* **refactor: `PlaceDetailViewModel` ์ƒ์„ฑ์ž ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ ๋กœ๋”ฉ ๋กœ์ง ์ˆ˜์ •**
    * ๊ธฐ์กด์— `PlaceUiModel`์ด๋‚˜ `PlaceDetailUiModel` ์ „์ฒด๋ฅผ ์ „๋‹ฌ๋ฐ›๋˜ ๋ฐฉ์‹์—์„œ `placeId` (Long)๋งŒ ๋ฐ›๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ด์— ๋”ฐ๋ผ ๋ทฐ๋ชจ๋ธ ์ƒ์„ฑ ์‹œ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ์กด์žฌ ์—ฌ๋ถ€์™€ ์ƒ๊ด€์—†์ด ํ•ญ์ƒ ์„œ๋ฒ„์—์„œ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋กœ๋“œํ•˜๋„๋ก ๋กœ์ง์„ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: `PlaceDetailViewModelTest` ๋‚ด ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ์ˆ˜์ •**
    * ๋ทฐ๋ชจ๋ธ์˜ ์ƒ์„ฑ์ž ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๋ฐ˜์˜ํ•˜๊ณ , ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ ์„œ๋ฒ„ ์š”์ฒญ์„ ๊ฑด๋„ˆ๋›ฐ๋˜ ํ…Œ์ŠคํŠธ๋ฅผ "์ƒ์„ฑ ์‹œ๋งˆ๋‹ค ํ”Œ๋ ˆ์ด์Šค๋ฅผ ๋กœ๋“œํ•œ๋‹ค"๋Š” ๊ฒ€์ฆ ๋กœ์ง์œผ๋กœ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `placeDetailRepository` mock ์ƒ์„ฑ ์‹œ `MockMode.autofill`์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: UI ๋ชจ๋ธ ํด๋ž˜์Šค ๋‚ด ๋ถˆํ•„์š”ํ•œ ์„ค์ • ์ œ๊ฑฐ**
    * `ImageUiModel`, `PlaceUiModel`, `PlaceDetailUiModel`, `NoticeUiModel` ๋“ฑ ์—ฌ๋Ÿฌ UI ๋ชจ๋ธ ํด๋ž˜์Šค์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `kotlinx.serialization.Serializable` ์–ด๋…ธํ…Œ์ด์…˜ ๋ฐ ์ž„ํฌํŠธ ๊ตฌ๋ฌธ์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๋ถˆํ•„์š”ํ•œ ์ž„ํฌํŠธ ๋ฐ ์˜์กด์„ฑ ์ •๋ฆฌ**
    * `PlaceWaitingContent.kt`: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `CoilImage` ๋ฐ Shimmer ๊ด€๋ จ ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookRoute.kt`: ๋” ์ด์ƒ ๋ผ์šฐํŒ… ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ธฐ์ง€ ์•Š๋Š” `PlaceUiModel`, `PlaceDetailUiModel` ์ฐธ์กฐ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์›จ์ดํŒ… ์ƒˆ๋กœ๊ณ ์นจ ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ถ”๊ฐ€

* **feat: ์›จ์ดํŒ… ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ถ”๊ฐ€**
    * `PlaceWaitingContent.kt`: `RefreshButton`์— `Animatable`๊ณผ `LaunchedEffect`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ `isRefreshing` ์ƒํƒœ์ผ ๋•Œ ๋ฌดํ•œ ํšŒ์ „ํ•˜๊ณ , ์ •์ง€ ์‹œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ •์œ„์น˜๋กœ ๋Œ์•„์˜ค๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์›จ์ดํŒ… ์ƒˆ๋กœ๊ณ ์นจ ์ค‘ ๊ธฐ์กด ๋ฐ์ดํ„ฐ ํ‘œ์‹œ**
    * `WaitingTeamUiState.kt`: `Refresh` ์ƒํƒœ๊ฐ€ `totalTeams` ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋„๋ก `data class`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceDetailViewModel.kt`: ์ƒˆ๋กœ๊ณ ์นจ ์‹œ์ž‘ ์‹œ ๊ธฐ์กด์— ๋กœ๋“œ๋œ `totalTeams` ๊ฐ’์„ ์œ ์ง€ํ•˜์—ฌ `Refresh` ์ƒํƒœ๋กœ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceWaitingContent.kt`: ์ƒˆ๋กœ๊ณ ์นจ(`Refresh`) ์ƒํƒœ์ผ ๋•Œ ์Šค์ผˆ๋ ˆํ†ค UI ๋Œ€์‹  ์ด์ „ ํŒ€ ์ˆซ์ž๋ฅผ ํ™”๋ฉด์— ๊ณ„์† ํ‘œ์‹œํ•˜๋„๋ก UI ๋กœ์ง์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* [Feat] ์„ค์ • ์ „ํ™”๋ฒˆํ˜ธ(WaitingInfo) ๋“ฑ๋ก/์ˆ˜์ • ๊ธฐ๋Šฅ ๊ตฌํ˜„ (#120)

- WaitingInfoViewModel: ์ „ํ™”๋ฒˆํ˜ธ ๋กœ๋“œ/์ €์žฅ/์ˆ˜์ • ์ƒํƒœ ๊ด€๋ฆฌ (Metro DI)
- AddWaitingInfoScreen: ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋กยท์ˆ˜์ • ๊ณต์šฉ ํ™”๋ฉด (๋ฏธ๋“ฑ๋ก ์‹œ ๋นˆ ์ž…๋ ฅ, ๋“ฑ๋ก ์‹œ pre-fill)
- SettingScreen: ๋“ฑ๋ก ์ •๋ณด ์„น์…˜ ์ถ”๊ฐ€ (์ „ํ™”๋ฒˆํ˜ธ ํ‘œ์‹œ ๋ฐ ํด๋ฆญ ๋„ค๋น„๊ฒŒ์ด์…˜)
- FestabookRoute: AddWaitingInfo ๋ผ์šฐํŠธ ์ถ”๊ฐ€
- SettingNavigation: AddWaitingInfo ์ปดํฌ์ €๋ธ” ๋“ฑ๋ก ๋ฐ WaitingInfoViewModel ์—ฐ๋™

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: ์„ค์ • ํ™”๋ฉด(SettingScreen) UI ์Šคํƒ€์ผ ๋ฐ ๋ ˆ์ด์•„์›ƒ ๊ฐœ์„ 

* **style: `SettingScreen` ๋‚ด ์„น์…˜ ๊ตฌ๋ถ„์„  ๋ฐ ๊ฐ„๊ฒฉ ์กฐ์ •**
    * ์„น์…˜ ์‚ฌ์ด์˜ `HorizontalDivider` ์ƒ‰์ƒ์„ `gray100`์—์„œ `gray200`์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ๋‘๊ป˜๋ฅผ `paddingBody2`์—์„œ `paddingBody1`์œผ๋กœ ๋‘๊ป๊ฒŒ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ตฌ๋ถ„์„ ์˜ ์ˆ˜์ง ํŒจ๋”ฉ(Padding)์„ ๋Š˜๋ ค ์„น์…˜ ๊ฐ„ ์‹œ๊ฐ์  ์—ฌ๋ฐฑ์„ ์ถฉ๋ถ„ํžˆ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ์„ค์ • ํ•ญ๋ชฉ ์„น์…˜ ํƒ€์ดํ‹€ ๋ฐ ํ…์ŠคํŠธ ์Šคํƒ€์ผ ํ†ต์ผ**
    * ์›จ์ดํŒ… ์ •๋ณด, ๋“ฑ๋ก ์ •๋ณด, ์•ฑ ์ •๋ณด ๋“ฑ ๊ฐ ์„น์…˜ ํƒ€์ดํ‹€์˜ ํ…์ŠคํŠธ ์Šคํƒ€์ผ์„ `bodyMedium`์—์„œ `bodyLarge`๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ์ƒ‰์ƒ์„ `gray500`์œผ๋กœ ๋ช…์‹œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•ฑ ๋ฒ„์ „ ํ‘œ์‹œ ๋ฐ ์›จ์ดํŒ… ์ „ํ™”๋ฒˆํ˜ธ ํ…์ŠคํŠธ์˜ ์Šคํƒ€์ผ์„ `bodyLarge` ๋ฐ `gray500`์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **refactor: ์›จ์ดํŒ… ์ •๋ณด ํ•ญ๋ชฉ ๋ ˆ์ด์•„์›ƒ ๋‹จ์ˆœํ™”**
    * `RegistrationInfoContent` ๋‚ด ์ „ํ™”๋ฒˆํ˜ธ ํ‘œ์‹œ ๋ถ€๋ถ„์—์„œ ๋ถˆํ•„์š”ํ•œ `Row`์™€ ํ™”์‚ดํ‘œ ์•„์ด์ฝ˜(`ic_arrow_forward_right`)์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํด๋ฆญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์˜ ํŒจ๋”ฉ์„ `14.dp`๋กœ ๊ณ ์ •ํ•˜๊ณ  ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ์„ ๊ฐ„์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ๋“ฑ๋ก ์ •๋ณด ๋ฐ ์•ฑ ์ •๋ณด ๋ ˆ์ด์•„์›ƒ ์„ธ๋ถ€ ์ˆ˜์ •**
    * ๋Œ€ํ•™๊ต ์ด๋ฆ„ ํ‘œ์‹œ ์˜์—ญ์˜ ํ•˜๋‹จ ํŒจ๋”ฉ์„ ์ œ๊ฑฐํ•˜๊ณ  ์ƒ๋‹จ ํŒจ๋”ฉ๋งŒ ์œ ์ง€ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•ฑ ๋ฒ„์ „ ์ •๋ณด ํ•ญ๋ชฉ(`AppVersionInfo`)์˜ ์ˆ˜์ง ํŒจ๋”ฉ์„ `14.dp`๋กœ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์›จ์ดํŒ… ์ •๋ณด ๋“ฑ๋ก ํ™”๋ฉด UI ๊ฐœ์„  ๋ฐ ๋กœ์ง ๊ณ ๋„ํ™”

* **feat: ์›จ์ดํŒ… ์ •๋ณด(์ „ํ™”๋ฒˆํ˜ธ) ๋“ฑ๋ก ํ™”๋ฉด UI ์ „๋ฉด ๊ฐœํŽธ**
    * `AddWaitingInfoScreen`: ํ—ค๋“œ๋ผ์ธ, ์ƒ์„ธ ์„ค๋ช…, ์•ฝ๊ด€ ๋™์˜ ์ฒดํฌ๋ฐ•์Šค ๋“ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `CenterAlignedTopAppBar`๋ฅผ ์ ์šฉํ•˜๊ณ  ๋””์ž์ธ ์‹œ์Šคํ…œ(`festabookShapes`, `festabookSpacing`)์— ๋งž์ถฐ ์—ฌ๋ฐฑ ๋ฐ ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ์„ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HorizontalDivider` ๋ฐ ์ „ํ™”๋ฒˆํ˜ธ ์•„์ด์ฝ˜ ๋“ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์‹œ๊ฐ์  ์™„์„ฑ๋„๋ฅผ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **feat: ์›จ์ดํŒ… ์ •๋ณด ๋“ฑ๋ก ์‹œ ์•ฝ๊ด€ ๋™์˜ ๋กœ์ง ์ถ”๊ฐ€**
    * `WaitingInfoViewModel`: ํ•„์ˆ˜ ์•ฝ๊ด€ ๋™์˜ ์—ฌ๋ถ€(`isTermsAgreed`) ์ƒํƒœ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `isSaveEnabled`: ์ „ํ™”๋ฒˆํ˜ธ ์œ ํšจ์„ฑ(์ˆซ์ž 9์ž๋ฆฌ ์ด์ƒ), ์•ฝ๊ด€ ๋™์˜ ์—ฌ๋ถ€, ์ €์žฅ ์ค‘ ์ƒํƒœ๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ์ €์žฅ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” ๋กœ์ง์„ ๊ฐ•ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์›จ์ดํŒ… ์ •๋ณด ๊ด€๋ จ ํŒจํ‚ค์ง€ ๊ตฌ์กฐ ๋ณ€๊ฒฝ ๋ฐ ์ฝ”๋“œ ์ •๋ฆฌ**
    * ์›จ์ดํŒ… ์ •๋ณด ๊ด€๋ จ ํŒŒ์ผ๋“ค์„ `presentation.setting.waitinginfo`์—์„œ `presentation.waitinginfo` ํŒจํ‚ค์ง€๋กœ ์ด๋™ํ•˜์—ฌ ๊ตฌ์กฐ๋ฅผ ์žฌ๋ฐฐ์น˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingInfoUiState`๋ฅผ ๋ณ„๋„์˜ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌํ•˜๊ณ , ๊ด€๋ จ ์ž„ํฌํŠธ ๊ฒฝ๋กœ๋ฅผ ๋ชจ๋‘ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingInfoViewModel` ๋‚ด `loadWaitingInfo` ๋ฉ”์„œ๋“œ๋ฅผ `private`์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์ดˆ๊ธฐํ™” ์‹œ์ ์— ์ž๋™์œผ๋กœ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์›จ์ดํŒ… ์ •๋ณด ๊ด€๋ จ ๋‹ค๊ตญ์–ด ๋ฆฌ์†Œ์Šค ์ถ”๊ฐ€ ๋ฐ ์ˆ˜์ •**
    * `strings.xml`: "์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก", "์•ฝ๊ด€ ๋™์˜" ๋“ฑ ์‹ ๊ทœ ํ™”๋ฉด ๊ตฌ์„ฑ์— ํ•„์š”ํ•œ ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ธฐ์กด ๋ฌธ๊ตฌ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: Scaffold innerPadding ์ ์šฉ์„ ํ†ตํ•œ ๋„ค๋น„๊ฒŒ์ด์…˜ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ๊ฐœ์„ 

* **refactor: ๋ฉ”์ธ ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„์— `innerPadding` ์ „๋‹ฌ ๋ฐ ์ ์šฉ**
    * `MainScreen.kt`: `Scaffold`์—์„œ ์ œ๊ณตํ•˜๋Š” `innerPadding`์„ `FestabookNavHost`๋กœ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeNavigation.kt`, `ScheduleNavigation.kt`, `NewsNavigation.kt`, `SettingNavigation.kt`, `PlaceMapNavigation.kt`: ๊ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„ ํ•จ์ˆ˜(`navGraph`)์— `innerPadding: PaddingValues` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ฐ ํƒญ์˜ ๋ฉ”์ธ ์Šคํฌ๋ฆฐ(`HomeScreen`, `ScheduleScreen`, `NewsScreen`, `SettingRoute`, `PlaceDetailRoute`) ํ˜ธ์ถœ ์‹œ `Modifier.padding(innerPadding)`์„ ์ ์šฉํ•˜์—ฌ ์ƒํ•˜๋‹จ ๋ฐ” ์˜์—ญ๊ณผ ๊ฒน์น˜์ง€ ์•Š๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ์›จ์ดํŒ… ์ •๋ณด ์ถ”๊ฐ€ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์„ค์ • ์ตœ์ ํ™”**
    * `AddWaitingInfoScreen.kt`: `Scaffold` ๋ฐ `TopAppBar`์—์„œ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ๋˜ `windowInsets = WindowInsets(0)` ์„ค์ •์„ ์ œ๊ฑฐํ•˜์—ฌ ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ์ธ์…‹์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ ์šฉ๋˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: ์›จ์ดํŒ… ์ •๋ณด(์ „ํ™”๋ฒˆํ˜ธ) ๋“ฑ๋ก ์„ฑ๊ณต ์‹œ ์Šค๋‚ต๋ฐ” ์•Œ๋ฆผ ์ถ”๊ฐ€ ๋ฐ ์•ฝ๊ด€ ๋™์˜ ์ƒํƒœ ๋กœ์ง ์ˆ˜์ •

* **feat: ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก ์„ฑ๊ณต ์‹œ ์Šค๋‚ต๋ฐ” ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ**
    * `strings.xml`: ๋“ฑ๋ก ์™„๋ฃŒ ๋ฉ”์‹œ์ง€ ๋ฆฌ์†Œ์Šค(`setting_waiting_info_save_success`)๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AddWaitingInfoScreen.kt`: `saveSuccessEvent` ๋ฐœ์ƒ ์‹œ `onShowSnackBar`๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์„ฑ๊ณต ๋ฉ”์‹œ์ง€๋ฅผ ๋„์šฐ๋„๋ก ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `SettingNavigation.kt`: `AddWaitingInfoRoute` ํ˜ธ์ถœ ์‹œ `onShowSnackBar` ์ฝœ๋ฐฑ์„ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์›จ์ดํŒ… ์ •๋ณด ์กฐํšŒ ์‹œ ์•ฝ๊ด€ ๋™์˜ ์ƒํƒœ ๋™๊ธฐํ™”**
    * `WaitingInfoViewModel.kt`: ์ €์žฅ๋œ ์›จ์ดํŒ… ์ •๋ณด๊ฐ€ ์กด์žฌํ•  ๊ฒฝ์šฐ, ํ™”๋ฉด ์ง„์ž… ์‹œ `isTermsAgreed` ์ƒํƒœ๋ฅผ `true`๋กœ ์„ค์ •ํ•˜์—ฌ ์‚ฌ์šฉ์ž UI ์ƒํƒœ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: WaitingInfo ๋กœ๋“œ ์‹œ์  ๋ณ€๊ฒฝ ๋ฐ ViewModel ์ดˆ๊ธฐํ™” ๋กœ์ง ์ˆ˜์ •

* **refactor: `WaitingInfoViewModel` ๋‚ด ์ž๋™ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ๋กœ์ง ์ œ๊ฑฐ ๋ฐ ์ ‘๊ทผ ์ œ์–ด์ž ์ˆ˜์ •**
    * `init` ๋ธ”๋ก์—์„œ `loadWaitingInfo()`๋ฅผ ํ˜ธ์ถœํ•˜๋˜ ๋กœ์ง์„ ์‚ญ์ œํ•˜์—ฌ ViewModel ์ƒ์„ฑ ์‹œ ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜์ง€ ์•Š๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * UI ๋‹จ๊ณ„์—์„œ ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก `loadWaitingInfo()` ๋ฉ”์„œ๋“œ์˜ ์ ‘๊ทผ ์ œ์–ด์ž๋ฅผ `private`์—์„œ `public`์œผ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ํ™”๋ฉด ์ง„์ž… ์‹œ WaitingInfo ๋กœ๋“œ ๋กœ์ง ์ถ”๊ฐ€**
    * `SettingScreen.kt`: `LaunchedEffect(Unit)`์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์„ค์ • ํ™”๋ฉด์— ์ง„์ž…ํ•  ๋•Œ ์›จ์ดํŒ… ์ •๋ณด๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AddWaitingInfoScreen.kt`: `LaunchedEffect(Unit)`์„ ํ†ตํ•ด ํ™”๋ฉด ์ง„์ž… ์‹œ ์›จ์ดํŒ… ์ •๋ณด๋ฅผ ๋กœ๋“œํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: ์Šค๋‚ต๋ฐ” ํ‘œ์‹œ ์‹œ ํ‚ค๋ณด๋“œ ๊ฐ€๋ฆผ ํ˜„์ƒ ์ˆ˜์ •

* **style: `MainScreen` ๋‚ด `SnackbarHost`์— `imePadding` ์ ์šฉ**
    * `Scaffold`์˜ `snackbarHost`์— `Modifier.imePadding()`์„ ์ถ”๊ฐ€ํ•˜์—ฌ, ์†Œํ”„ํŠธ ํ‚ค๋ณด๋“œ๊ฐ€ ํ™œ์„ฑํ™”๋˜์—ˆ์„ ๋•Œ ์Šค๋‚ต๋ฐ”๊ฐ€ ํ‚ค๋ณด๋“œ ์œ„๋กœ ์ž๋™์œผ๋กœ ๋ฐ€๋ ค ์˜ฌ๋ผ๊ฐ€๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `WaitingInfoViewModel` ๋‚ด ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋กœ์ง ๋ฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ ์ตœ์ ํ™”

* **refactor: `updatePhoneNumber` ๋ฉ”์„œ๋“œ ๋‚ด ์ž…๋ ฅ ์ œํ•œ ๋กœ์ง ์ˆ˜์ •**
    * ์ž…๋ ฅ ๋ฌธ์ž์—ด์˜ ๊ธธ์ด๊ฐ€ 11์ž๋ฅผ ์ดˆ๊ณผํ•  ๊ฒฝ์šฐ ์—…๋ฐ์ดํŠธ๋ฅผ ์ค‘๋‹จํ•˜๋Š” ๊ฐ€๋“œ ํด๋กœ์ฆˆ(Guard Clause)๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด `take(11)` ๋Œ€์‹  ์ „์ฒด ์ž…๋ ฅ๊ฐ’์—์„œ ์ˆซ์ž๋งŒ ํ•„ํ„ฐ๋งํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์—ฌ ์ž…๋ ฅ ์ œ์–ด์˜ ์ •ํ™•์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **refactor: `SharedFlow` ์„ค์ • ๊ฐ„์†Œํ™”**
    * `_saveSuccessEvent` ๋ฐ `_errorEvent`์—์„œ ๋ถˆํ•„์š”ํ•œ `replay = 0` ๋ฐ `extraBufferCapacity = 1` ์„ค์ •์„ ์ œ๊ฑฐํ•˜์—ฌ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `WaitingInfoViewModel` ๋‚ด SharedFlow ๋ฒ„ํผ ์„ค์ • ์ถ”๊ฐ€

* **refactor: `saveSuccessEvent` ๋ฐ `errorEvent`์— ๋ฒ„ํผ ์šฉ๋Ÿ‰ ์„ค์ •**
    * `MutableSharedFlow` ์ƒ์„ฑ ์‹œ `extraBufferCapacity = 1`์„ ์ถ”๊ฐ€ํ•˜์—ฌ, ๊ตฌ๋…์ž๊ฐ€ ์—†๋Š” ์ƒํƒœ์—์„œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„ ์ตœ์†Œ 1๊ฐœ์˜ ์ตœ์‹  ์ด๋ฒคํŠธ๋ฅผ ๋ณด๊ด€ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์„œ๋น„์Šค ์ด์šฉ์•ฝ๊ด€ ๋ฐ ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ ๋ถ„๋ฆฌ ๋ฐ ๋งํฌ ์—ฐ๋™ (#120)

* **feat: ์„ค์ • ํ™”๋ฉด ๋‚ด ์ •์ฑ… ๊ด€๋ จ ๋ฉ”๋‰ด ์„ธ๋ถ„ํ™” ๋ฐ URL ์—…๋ฐ์ดํŠธ**
    * `strings.xml`: ๊ธฐ์กด `setting_service_policy` ๋ช…์นญ์„ "์„œ๋น„์Šค ์ด์šฉ์•ฝ๊ด€"์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , `setting_personal_information_policy`("๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ") ํ•ญ๋ชฉ์„ ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `SettingScreen.kt`: ์ •์ฑ… URL์„ `SERVICE_POLICY_URL`๊ณผ `PERSONAL_INFORMATION_POLICY_URL`๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `SettingScreen.kt`: `AppInfoContent` ๋ฐ ๊ด€๋ จ ์ปดํฌ์ €๋ธ”์— ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ ๋ฒ„ํŠผ๊ณผ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฐ ์ •์ฑ… ํŽ˜์ด์ง€๋กœ ์—ฐ๊ฒฐ๋˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ์›จ์ดํŒ… ์ •๋ณด ๋“ฑ๋ก ํ™”๋ฉด ๋‚ด ์•ฝ๊ด€ ๋™์˜ ๋งํฌ ์ถ”๊ฐ€**
    * `AddWaitingInfoScreen.kt`: ์›จ์ดํŒ… ์ •๋ณด ๋“ฑ๋ก ์‹œ ๋™์˜๊ฐ€ ํ•„์š”ํ•œ ์•ฝ๊ด€ URL(`TERMS_URL`) ์ƒ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AddWaitingInfoScreen.kt`: ์•ฝ๊ด€ ๋™์˜ ํ…์ŠคํŠธ์— ๋ฐ‘์ค„(`Underline`) ์Šคํƒ€์ผ์„ ์ ์šฉํ•˜๊ณ , ํด๋ฆญ ์‹œ `LocalUriHandler`๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ์•ฝ๊ด€ ํŽ˜์ด์ง€๊ฐ€ ์—ด๋ฆฌ๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์„ค์ • ํ™”๋ฉด ๋‚ด ๋ฌธ์˜ํ•˜๊ธฐ URL ์—…๋ฐ์ดํŠธ**
    * `SettingScreen.kt`: ๊ฐœ๋ฐœ์ž ๋ฌธ์˜ํ•˜๊ธฐ ๋งํฌ(`CONTACT_US_URL`)๋ฅผ ์ตœ์‹  Google Forms ์ฃผ์†Œ๋กœ ๊ฐฑ์‹ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์›จ์ดํŒ… ์ •๋ณด ํŒจํ‚ค์ง€ ๊ตฌ์กฐ ๋ณ€๊ฒฝ, ํ™”๋ฉด ์ „ํ™˜ ์•ต๋‹ˆ๋ฉ”์ด์…˜, ๊ฐ navigation์œผ๋กœ ์ด๋™

* **refactor: ์›จ์ดํŒ… ์ •๋ณด(WaitingInfo) ๊ด€๋ จ ํŒŒ์ผ์„ `setting` ํŒจํ‚ค์ง€ ํ•˜์œ„๋กœ ์ด๋™**
    * ์›จ์ดํŒ… ์ •๋ณด ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์ด ์„ค์ • ๋„๋ฉ”์ธ์— ์ข…์†๋จ์— ๋”ฐ๋ผ ๊ด€๋ จ ํŒŒ์ผ๋“ค์˜ ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `presentation.waitinginfo` -> `presentation.setting.waitinginfo`
    * `WaitingInfoViewModel`, `WaitingInfoUiState`, `AddWaitingInfoScreen` ๋“ฑ์˜ ์œ„์น˜๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ๊ด€๋ จ ์ž„ํฌํŠธ ๋ฌธ์„ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ๋ฉ”์ธ ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ๊ฑฐ**
    * `MainScreen`์˜ ์ „์—ญ ์„ค์ •์œผ๋กœ ๋˜์–ด ์žˆ๋˜ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ œ๊ฑฐํ•˜๊ณ , ๊ฐ ๊ฐœ๋ณ„ ํƒญ(`Home`, `News`, `Schedule`, `PlaceMap`, `Setting`)์˜ `composable` ์„ค์ •์— `EnterTransition.None` ๋ฐ `ExitTransition.None`์„ ๋ช…์‹œ์ ์œผ๋กœ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํƒญ ์ „ํ™˜ ์‹œ ์‹œ๊ฐ์  ์ง€์—ฐ ์—†์ด ์ฆ‰๊ฐ์ ์œผ๋กœ ํ™”๋ฉด์ด ์ „ํ™˜๋˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์ฝ”๋“œ ์ •๋ฆฌ ๋ฐ ๊ฒฝ๋กœ ์ตœ์ ํ™”**
    * `MainScreen.kt`, `SettingNavigation.kt` ๋“ฑ์—์„œ ๋ณ€๊ฒฝ๋œ ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ์— ๋งž์ถฐ ์˜์กด์„ฑ ์ฐธ์กฐ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `WaitingInfoViewModel` ๋‚ด ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ ์ž๋ฆฟ์ˆ˜ ์ œํ•œ ์ƒ์ˆ˜ํ™”

* **refactor: ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง์— ์ƒ์ˆ˜ ๋„์ž…**
    * ๋งค์ง ๋„˜๋ฒ„๋กœ ๊ด€๋ฆฌ๋˜๋˜ ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ์˜ ์ตœ์†Œ ์ž๋ฆฟ์ˆ˜(`9`)์™€ ์ตœ๋Œ€ ์ž๋ฆฟ์ˆ˜(`11`)๋ฅผ `MIN_DIGIT_COUNT` ๋ฐ `MAX_DIGIT_COUNT` ์ƒ์ˆ˜๋กœ ์ •์˜ํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.
    * `updatePhoneNumber`: ์ž…๋ ฅ ์ œํ•œ ๊ธธ์ด๋ฅผ `MAX_DIGIT_COUNT` ์ƒ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `isSubmitEnabled`: ๋“ฑ๋ก ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํŒ๋‹จ ์‹œ `MIN_DIGIT_COUNT` ์ƒ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: MainViewModel ํƒ์ƒ‰ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ ๋ฐ ๋”ฅ๋งํฌ ์ค‘๋ณต ๋ฐฉ์ง€

* **refactor: `MainViewModel` ๋‚ด ์ผํšŒ์„ฑ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„ **
    * `navigateNewsEvent`๋ฅผ ๊ธฐ์กด `MutableSharedFlow` ๋ฐฉ์‹์—์„œ `Channel` ๊ธฐ๋ฐ˜์˜ `Flow`๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋‰ด์Šค ์ด๋™ ์ด๋ฒคํŠธ๊ฐ€ ํ•œ ๋ฒˆ๋งŒ ์†Œ๋น„๋˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `navigateToNews` ๋ฉ”์„œ๋“œ ๋‚ด ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋กœ์ง์„ `tryEmit`์—์„œ `trySend`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **fix: ๋”ฅ๋งํฌ ์ฒ˜๋ฆฌ ํ›„ Intent Extra ์ œ๊ฑฐ ๋กœ์ง ์ถ”๊ฐ€**
    * `RememberDeepLinkHandler.android.kt`์—์„œ ๋”ฅ๋งํฌ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•œ ์งํ›„, ์‚ฌ์šฉ๋œ Intent Extra(`KEY_CAN_NAVIGATE_TO_NEWS`, `KEY_NOTICE_ID_TO_EXPAND`)๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ด๋Š” ์•กํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์žฌ์ƒ์„ฑ๋˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ์šด ์ธํ…ํŠธ๋ฅผ ๋ฐ›์„ ๋•Œ ๋™์ผํ•œ ๋”ฅ๋งํฌ๊ฐ€ ์ค‘๋ณต์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋Š” ํ˜„์ƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

* style: ์ฝ”๋“œ ํฌ๋งทํŒ…

* [Fix] iOS FCM ์•Œ๋ฆผ ํด๋ฆญ ์‹œ ๋ฐ์ดํ„ฐ ์œ ์‹ค ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐ ๋”ฅ๋งํฌ ์—ฐ๋™ ๊ฐœ์„ 

* **feat: iOS FCM ์•Œ๋ฆผ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ `PendingFcmNotification` ์ถ”๊ฐ€**
    * `NSNotification` ๊ธฐ๋ฐ˜์˜ ํœ˜๋ฐœ์„ฑ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋ฐฉ์‹์„ `Channel` ๊ธฐ๋ฐ˜์˜ `PendingFcmNotification` ์˜ค๋ธŒ์ ํŠธ๋กœ ๊ต์ฒดํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ด๋ฅผ ํ†ตํ•ด ์•ฑ์ด ์‹คํ–‰๋˜์ง€ ์•Š์€ ์ƒํƒœ(Cold Start)์—์„œ ์•Œ๋ฆผ์„ ํด๋ฆญํ•˜๋”๋ผ๋„ Compose UI ์ค€๋น„ ํ›„ ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `RememberDeepLinkHandler` ๊ตฌ์กฐ ๋ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋กœ์ง ์ˆ˜์ •**
    * `RememberDeepLinkHandler`์— `onFestivalChanged` ์ฝœ๋ฐฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ถ•์ œ ID ๋ณ€๊ฒฝ ์ƒํ™ฉ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ™•์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookRoute.Main`์— `pendingAnnouncementId` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋„ค๋น„๊ฒŒ์ด์…˜ ์‹œ ํŠน์ • ๊ณต์ง€์‚ฌํ•ญ์œผ๋กœ ์ฆ‰์‹œ ์ด๋™ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋ฐ˜์„ ๋งˆ๋ จํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainScreen`์—์„œ `pendingAnnouncementId` ๋ณ€๊ฒฝ ๊ฐ์ง€ ์‹œ `NewsViewModel`์„ ํ†ตํ•ด ํ•ด๋‹น ๊ณต์ง€๋ฅผ ํ™•์žฅํ•˜๊ณ  ๋‰ด์Šค ํƒญ์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: iOS ์•Œ๋ฆผ ๋ฐ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฐ ํ•ธ๋“ค๋Ÿฌ ๋กœ์ง ๊ฐœ์„ **
    * `DefaultUserNotificationDelegate`์—์„œ ์•Œ๋ฆผ ์ˆ˜์‹  ์‹œ ํ˜„์žฌ ์ถ•์ œ ID์™€ ๋น„๊ตํ•˜์—ฌ ๋ณ€๊ฒฝ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•˜๊ณ , `PendingFcmNotification`์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `RememberDeepLinkHandler.ios.kt`์—์„œ `NSNotificationCenter` ๋Œ€์‹  `PendingFcmNotification`์˜ Flow๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ๋”ฅ๋งํฌ ๋ฐ ์ถ•์ œ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๊ธฐํƒ€ ๋ณ€๊ฒฝ ์‚ฌํ•ญ**
    * `FestabookScreen.kt` ๋‚ด `Main` ๊ฒฝ๋กœ ์ด๋™ ์‹œ ๋ถˆํ•„์š”ํ•œ ์ธ์ž ์ œ๊ฑฐ ๋ฐ ๊ธฐ๋ณธ ์ƒ์„ฑ์ž ํ˜ธ์ถœ ๋ฐฉ์‹์œผ๋กœ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DeepLinkKeys`์—์„œ ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `KEY_FCM_NOTIFICATION` ์ƒ์ˆ˜๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: DefaultUserNotificationDelegate ๋‚ด ํŽ˜์Šคํ‹ฐ๋ฒŒ ID ์ €์žฅ ๋กœ์ง ๊ฐœ์„ 

* **refactor: ์•Œ๋ฆผ ์ˆ˜์‹  ์‹œ ํŽ˜์Šคํ‹ฐ๋ฒŒ ID ์ฒ˜๋ฆฌ ๋กœ์ง ์ˆ˜์ •**
    * `festivalLocalDataSource.getFestivalId()` ํ˜ธ์ถœ ์‹œ `first()` ๋Œ€์‹  `firstOrNull()`์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ณด๋‹ค ์•ˆ์ „ํ•˜๊ฒŒ ํ˜„์žฌ ์ €์žฅ๋œ ID๋ฅผ ์กฐํšŒํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด์—๋Š” ์œ ํšจํ•œ ID์ธ ๊ฒฝ์šฐ ๋ฌด์กฐ๊ฑด ์ €์žฅํ•˜๋˜ ๋ฐฉ์‹์—์„œ, ํ˜„์žฌ ์ €์žฅ๋œ ID์™€ ๋น„๊ตํ•˜์—ฌ ๊ฐ’์ด ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ(`festivalIdChanged`)์—๋งŒ ์ €์žฅํ•˜๋„๋ก ๋กœ์ง์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ˆ˜์‹ ๋œ ID๋ฅผ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด ๋ณ€์ˆ˜๋ช…์„ `festivalId`์—์„œ `newFestivalId`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ๋”ฅ๋งํฌ ๋ฐ FCM ์•Œ๋ฆผ ํ•ธ๋“ค๋ง ๋กœ์ง ํ†ตํ•ฉ ๋ฐ ๊ฐ„์†Œํ™”

* **refactor: `RememberDeepLinkHandler` ์ธํ„ฐํŽ˜์ด์Šค ํ†ตํ•ฉ**
    * ๊ธฐ์กด `onFestivalChanged`์™€ `onDeepLink`๋กœ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ๋˜ ์ฝœ๋ฐฑ์„ `onNotificationClicked` ํ•˜๋‚˜๋กœ ํ†ตํ•ฉํ•˜์—ฌ ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋”ฅ๋งํฌ ์ฒ˜๋ฆฌ์— ํ•„์š”ํ•œ `announcementId`์™€ `festivalIdChanged` ์—ฌ๋ถ€๋ฅผ ํ•œ ๋ฒˆ์— ์ „๋‹ฌํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: Android ํ”Œ๋žซํผ ํ•ธ๋“ค๋Ÿฌ ๊ตฌํ˜„ ์ˆ˜์ •**
    * `Intent`์—์„œ ๋”ฅ๋งํฌ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๋Š” ๋กœ์ง์„ `handleDeepLink` ํ™•์žฅ ํ•จ์ˆ˜๋กœ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `LaunchedEffect`์™€ `onNewIntentListener`์—์„œ ์ค‘๋ณต๋˜๋˜ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ํ†ตํ•ฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: iOS ํ”Œ๋žซํผ ํ•ธ๋“ค๋Ÿฌ ๊ตฌํ˜„ ์ˆ˜์ •**
    * `PendingFcmNotification` ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์‹ ํ•  ๋•Œ ๋ถˆํ•„์š”ํ•œ `Intent` ๊ฐ์ฒด ์ƒ์„ฑ ๊ณผ์ •์„ ์ œ๊ฑฐํ•˜๊ณ  ํ†ตํ•ฉ๋œ ์ฝœ๋ฐฑ์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `MainScreen` ๋‚ด ๋„ค๋น„๊ฒŒ์ด์…˜ ๋กœ์ง ์ •๋ฆฌ**
    * `NewsViewModel`๊ณผ `MainViewModel`์„ ์กฐ์ž‘ํ•˜์—ฌ ์†Œ์‹ ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋กœ์ง์„ `navigateToNewsScreen` ํ•จ์ˆ˜๋กœ ์ถ”์ถœํ•˜์—ฌ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.
    * `RememberDeepLinkHandler`์˜ ๋ณ€๊ฒฝ๋œ ์ธํ„ฐํŽ˜์ด์Šค์— ๋งž์ถฐ ๋”ฅ๋งํฌ ์ˆ˜์‹  ์‹œ์˜ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ถ„๊ธฐ ๋กœ์ง์„ ๊ฐ„์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ฝ”๋“œ ์ œ๊ฑฐ

* refactor: ํŒŒ๋ผ๋ฏธํ„ฐ๋ช… ๋ณ€๊ฒฝ
* [Feat] ์„ค์ • ์ „ํ™”๋ฒˆํ˜ธ(WaitingInfo) ๋“ฑ๋ก/์ˆ˜์ • ๊ธฐ๋Šฅ ๊ตฌํ˜„ (#120)

- WaitingInfoViewModel: ์ „ํ™”๋ฒˆํ˜ธ ๋กœ๋“œ/์ €์žฅ/์ˆ˜์ • ์ƒํƒœ ๊ด€๋ฆฌ (Metro DI)
- AddWaitingInfoScreen: ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋กยท์ˆ˜์ • ๊ณต์šฉ ํ™”๋ฉด (๋ฏธ๋“ฑ๋ก ์‹œ ๋นˆ ์ž…๋ ฅ, ๋“ฑ๋ก ์‹œ pre-fill)
- SettingScreen: ๋“ฑ๋ก ์ •๋ณด ์„น์…˜ ์ถ”๊ฐ€ (์ „ํ™”๋ฒˆํ˜ธ ํ‘œ์‹œ ๋ฐ ํด๋ฆญ ๋„ค๋น„๊ฒŒ์ด์…˜)
- FestabookRoute: AddWaitingInfo ๋ผ์šฐํŠธ ์ถ”๊ฐ€
- SettingNavigation: AddWaitingInfo ์ปดํฌ์ €๋ธ” ๋“ฑ๋ก ๋ฐ WaitingInfoViewModel ์—ฐ๋™

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: ์„ค์ • ํ™”๋ฉด(SettingScreen) UI ์Šคํƒ€์ผ ๋ฐ ๋ ˆ์ด์•„์›ƒ ๊ฐœ์„ 

* **style: `SettingScreen` ๋‚ด ์„น์…˜ ๊ตฌ๋ถ„์„  ๋ฐ ๊ฐ„๊ฒฉ ์กฐ์ •**
    * ์„น์…˜ ์‚ฌ์ด์˜ `HorizontalDivider` ์ƒ‰์ƒ์„ `gray100`์—์„œ `gray200`์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ๋‘๊ป˜๋ฅผ `paddingBody2`์—์„œ `paddingBody1`์œผ๋กœ ๋‘๊ป๊ฒŒ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ตฌ๋ถ„์„ ์˜ ์ˆ˜์ง ํŒจ๋”ฉ(Padding)์„ ๋Š˜๋ ค ์„น์…˜ ๊ฐ„ ์‹œ๊ฐ์  ์—ฌ๋ฐฑ์„ ์ถฉ๋ถ„ํžˆ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ์„ค์ • ํ•ญ๋ชฉ ์„น์…˜ ํƒ€์ดํ‹€ ๋ฐ ํ…์ŠคํŠธ ์Šคํƒ€์ผ ํ†ต์ผ**
    * ์›จ์ดํŒ… ์ •๋ณด, ๋“ฑ๋ก ์ •๋ณด, ์•ฑ ์ •๋ณด ๋“ฑ ๊ฐ ์„น์…˜ ํƒ€์ดํ‹€์˜ ํ…์ŠคํŠธ ์Šคํƒ€์ผ์„ `bodyMedium`์—์„œ `bodyLarge`๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ์ƒ‰์ƒ์„ `gray500`์œผ๋กœ ๋ช…์‹œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•ฑ ๋ฒ„์ „ ํ‘œ์‹œ ๋ฐ ์›จ์ดํŒ… ์ „ํ™”๋ฒˆํ˜ธ ํ…์ŠคํŠธ์˜ ์Šคํƒ€์ผ์„ `bodyLarge` ๋ฐ `gray500`์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **refactor: ์›จ์ดํŒ… ์ •๋ณด ํ•ญ๋ชฉ ๋ ˆ์ด์•„์›ƒ ๋‹จ์ˆœํ™”**
    * `RegistrationInfoContent` ๋‚ด ์ „ํ™”๋ฒˆํ˜ธ ํ‘œ์‹œ ๋ถ€๋ถ„์—์„œ ๋ถˆํ•„์š”ํ•œ `Row`์™€ ํ™”์‚ดํ‘œ ์•„์ด์ฝ˜(`ic_arrow_forward_right`)์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํด๋ฆญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์˜ ํŒจ๋”ฉ์„ `14.dp`๋กœ ๊ณ ์ •ํ•˜๊ณ  ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ์„ ๊ฐ„์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ๋“ฑ๋ก ์ •๋ณด ๋ฐ ์•ฑ ์ •๋ณด ๋ ˆ์ด์•„์›ƒ ์„ธ๋ถ€ ์ˆ˜์ •**
    * ๋Œ€ํ•™๊ต ์ด๋ฆ„ ํ‘œ์‹œ ์˜์—ญ์˜ ํ•˜๋‹จ ํŒจ๋”ฉ์„ ์ œ๊ฑฐํ•˜๊ณ  ์ƒ๋‹จ ํŒจ๋”ฉ๋งŒ ์œ ์ง€ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•ฑ ๋ฒ„์ „ ์ •๋ณด ํ•ญ๋ชฉ(`AppVersionInfo`)์˜ ์ˆ˜์ง ํŒจ๋”ฉ์„ `14.dp`๋กœ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์›จ์ดํŒ… ์ •๋ณด ๋“ฑ๋ก ํ™”๋ฉด UI ๊ฐœ์„  ๋ฐ ๋กœ์ง ๊ณ ๋„ํ™”

* **feat: ์›จ์ดํŒ… ์ •๋ณด(์ „ํ™”๋ฒˆํ˜ธ) ๋“ฑ๋ก ํ™”๋ฉด UI ์ „๋ฉด ๊ฐœํŽธ**
    * `AddWaitingInfoScreen`: ํ—ค๋“œ๋ผ์ธ, ์ƒ์„ธ ์„ค๋ช…, ์•ฝ๊ด€ ๋™์˜ ์ฒดํฌ๋ฐ•์Šค ๋“ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `CenterAlignedTopAppBar`๋ฅผ ์ ์šฉํ•˜๊ณ  ๋””์ž์ธ ์‹œ์Šคํ…œ(`festabookShapes`, `festabookSpacing`)์— ๋งž์ถฐ ์—ฌ๋ฐฑ ๋ฐ ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ์„ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HorizontalDivider` ๋ฐ ์ „ํ™”๋ฒˆํ˜ธ ์•„์ด์ฝ˜ ๋“ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์‹œ๊ฐ์  ์™„์„ฑ๋„๋ฅผ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **feat: ์›จ์ดํŒ… ์ •๋ณด ๋“ฑ๋ก ์‹œ ์•ฝ๊ด€ ๋™์˜ ๋กœ์ง ์ถ”๊ฐ€**
    * `WaitingInfoViewModel`: ํ•„์ˆ˜ ์•ฝ๊ด€ ๋™์˜ ์—ฌ๋ถ€(`isTermsAgreed`) ์ƒํƒœ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `isSaveEnabled`: ์ „ํ™”๋ฒˆํ˜ธ ์œ ํšจ์„ฑ(์ˆซ์ž 9์ž๋ฆฌ ์ด์ƒ), ์•ฝ๊ด€ ๋™์˜ ์—ฌ๋ถ€, ์ €์žฅ ์ค‘ ์ƒํƒœ๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ์ €์žฅ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” ๋กœ์ง์„ ๊ฐ•ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์›จ์ดํŒ… ์ •๋ณด ๊ด€๋ จ ํŒจํ‚ค์ง€ ๊ตฌ์กฐ ๋ณ€๊ฒฝ ๋ฐ ์ฝ”๋“œ ์ •๋ฆฌ**
    * ์›จ์ดํŒ… ์ •๋ณด ๊ด€๋ จ ํŒŒ์ผ๋“ค์„ `presentation.setting.waitinginfo`์—์„œ `presentation.waitinginfo` ํŒจํ‚ค์ง€๋กœ ์ด๋™ํ•˜์—ฌ ๊ตฌ์กฐ๋ฅผ ์žฌ๋ฐฐ์น˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingInfoUiState`๋ฅผ ๋ณ„๋„์˜ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌํ•˜๊ณ , ๊ด€๋ จ ์ž„ํฌํŠธ ๊ฒฝ๋กœ๋ฅผ ๋ชจ๋‘ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingInfoViewModel` ๋‚ด `loadWaitingInfo` ๋ฉ”์„œ๋“œ๋ฅผ `private`์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์ดˆ๊ธฐํ™” ์‹œ์ ์— ์ž๋™์œผ๋กœ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์›จ์ดํŒ… ์ •๋ณด ๊ด€๋ จ ๋‹ค๊ตญ์–ด ๋ฆฌ์†Œ์Šค ์ถ”๊ฐ€ ๋ฐ ์ˆ˜์ •**
    * `strings.xml`: "์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก", "์•ฝ๊ด€ ๋™์˜" ๋“ฑ ์‹ ๊ทœ ํ™”๋ฉด ๊ตฌ์„ฑ์— ํ•„์š”ํ•œ ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ธฐ์กด ๋ฌธ๊ตฌ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: Scaffold innerPadding ์ ์šฉ์„ ํ†ตํ•œ ๋„ค๋น„๊ฒŒ์ด์…˜ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ๊ฐœ์„ 

* **refactor: ๋ฉ”์ธ ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„์— `innerPadding` ์ „๋‹ฌ ๋ฐ ์ ์šฉ**
    * `MainScreen.kt`: `Scaffold`์—์„œ ์ œ๊ณตํ•˜๋Š” `innerPadding`์„ `FestabookNavHost`๋กœ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeNavigation.kt`, `ScheduleNavigation.kt`, `NewsNavigation.kt`, `SettingNavigation.kt`, `PlaceMapNavigation.kt`: ๊ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„ ํ•จ์ˆ˜(`navGraph`)์— `innerPadding: PaddingValues` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ฐ ํƒญ์˜ ๋ฉ”์ธ ์Šคํฌ๋ฆฐ(`HomeScreen`, `ScheduleScreen`, `NewsScreen`, `SettingRoute`, `PlaceDetailRoute`) ํ˜ธ์ถœ ์‹œ `Modifier.padding(innerPadding)`์„ ์ ์šฉํ•˜์—ฌ ์ƒํ•˜๋‹จ ๋ฐ” ์˜์—ญ๊ณผ ๊ฒน์น˜์ง€ ์•Š๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ์›จ์ดํŒ… ์ •๋ณด ์ถ”๊ฐ€ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์„ค์ • ์ตœ์ ํ™”**
    * `AddWaitingInfoScreen.kt`: `Scaffold` ๋ฐ `TopAppBar`์—์„œ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ๋˜ `windowInsets = WindowInsets(0)` ์„ค์ •์„ ์ œ๊ฑฐํ•˜์—ฌ ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ์ธ์…‹์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ ์šฉ๋˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: ์›จ์ดํŒ… ์ •๋ณด(์ „ํ™”๋ฒˆํ˜ธ) ๋“ฑ๋ก ์„ฑ๊ณต ์‹œ ์Šค๋‚ต๋ฐ” ์•Œ๋ฆผ ์ถ”๊ฐ€ ๋ฐ ์•ฝ๊ด€ ๋™์˜ ์ƒํƒœ ๋กœ์ง ์ˆ˜์ •

* **feat: ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก ์„ฑ๊ณต ์‹œ ์Šค๋‚ต๋ฐ” ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ**
    * `strings.xml`: ๋“ฑ๋ก ์™„๋ฃŒ ๋ฉ”์‹œ์ง€ ๋ฆฌ์†Œ์Šค(`setting_waiting_info_save_success`)๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AddWaitingInfoScreen.kt`: `saveSuccessEvent` ๋ฐœ์ƒ ์‹œ `onShowSnackBar`๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์„ฑ๊ณต ๋ฉ”์‹œ์ง€๋ฅผ ๋„์šฐ๋„๋ก ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `SettingNavigation.kt`: `AddWaitingInfoRoute` ํ˜ธ์ถœ ์‹œ `onShowSnackBar` ์ฝœ๋ฐฑ์„ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์›จ์ดํŒ… ์ •๋ณด ์กฐํšŒ ์‹œ ์•ฝ๊ด€ ๋™์˜ ์ƒํƒœ ๋™๊ธฐํ™”**
    * `WaitingInfoViewModel.kt`: ์ €์žฅ๋œ ์›จ์ดํŒ… ์ •๋ณด๊ฐ€ ์กด์žฌํ•  ๊ฒฝ์šฐ, ํ™”๋ฉด ์ง„์ž… ์‹œ `isTermsAgreed` ์ƒํƒœ๋ฅผ `true`๋กœ ์„ค์ •ํ•˜์—ฌ ์‚ฌ์šฉ์ž UI ์ƒํƒœ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: WaitingInfo ๋กœ๋“œ ์‹œ์  ๋ณ€๊ฒฝ ๋ฐ ViewModel ์ดˆ๊ธฐํ™” ๋กœ์ง ์ˆ˜์ •

* **refactor: `WaitingInfoViewModel` ๋‚ด ์ž๋™ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ๋กœ์ง ์ œ๊ฑฐ ๋ฐ ์ ‘๊ทผ ์ œ์–ด์ž ์ˆ˜์ •**
    * `init` ๋ธ”๋ก์—์„œ `loadWaitingInfo()`๋ฅผ ํ˜ธ์ถœํ•˜๋˜ ๋กœ์ง์„ ์‚ญ์ œํ•˜์—ฌ ViewModel ์ƒ์„ฑ ์‹œ ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜์ง€ ์•Š๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * UI ๋‹จ๊ณ„์—์„œ ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก `loadWaitingInfo()` ๋ฉ”์„œ๋“œ์˜ ์ ‘๊ทผ ์ œ์–ด์ž๋ฅผ `private`์—์„œ `public`์œผ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ํ™”๋ฉด ์ง„์ž… ์‹œ WaitingInfo ๋กœ๋“œ ๋กœ์ง ์ถ”๊ฐ€**
    * `SettingScreen.kt`: `LaunchedEffect(Unit)`์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์„ค์ • ํ™”๋ฉด์— ์ง„์ž…ํ•  ๋•Œ ์›จ์ดํŒ… ์ •๋ณด๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AddWaitingInfoScreen.kt`: `LaunchedEffect(Unit)`์„ ํ†ตํ•ด ํ™”๋ฉด ์ง„์ž… ์‹œ ์›จ์ดํŒ… ์ •๋ณด๋ฅผ ๋กœ๋“œํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: ์Šค๋‚ต๋ฐ” ํ‘œ์‹œ ์‹œ ํ‚ค๋ณด๋“œ ๊ฐ€๋ฆผ ํ˜„์ƒ ์ˆ˜์ •

* **style: `MainScreen` ๋‚ด `SnackbarHost`์— `imePadding` ์ ์šฉ**
    * `Scaffold`์˜ `snackbarHost`์— `Modifier.imePadding()`์„ ์ถ”๊ฐ€ํ•˜์—ฌ, ์†Œํ”„ํŠธ ํ‚ค๋ณด๋“œ๊ฐ€ ํ™œ์„ฑํ™”๋˜์—ˆ์„ ๋•Œ ์Šค๋‚ต๋ฐ”๊ฐ€ ํ‚ค๋ณด๋“œ ์œ„๋กœ ์ž๋™์œผ๋กœ ๋ฐ€๋ ค ์˜ฌ๋ผ๊ฐ€๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `WaitingInfoViewModel` ๋‚ด ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋กœ์ง ๋ฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ ์ตœ์ ํ™”

* **refactor: `updatePhoneNumber` ๋ฉ”์„œ๋“œ ๋‚ด ์ž…๋ ฅ ์ œํ•œ ๋กœ์ง ์ˆ˜์ •**
    * ์ž…๋ ฅ ๋ฌธ์ž์—ด์˜ ๊ธธ์ด๊ฐ€ 11์ž๋ฅผ ์ดˆ๊ณผํ•  ๊ฒฝ์šฐ ์—…๋ฐ์ดํŠธ๋ฅผ ์ค‘๋‹จํ•˜๋Š” ๊ฐ€๋“œ ํด๋กœ์ฆˆ(Guard Clause)๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด `take(11)` ๋Œ€์‹  ์ „์ฒด ์ž…๋ ฅ๊ฐ’์—์„œ ์ˆซ์ž๋งŒ ํ•„ํ„ฐ๋งํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์—ฌ ์ž…๋ ฅ ์ œ์–ด์˜ ์ •ํ™•์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **refactor: `SharedFlow` ์„ค์ • ๊ฐ„์†Œํ™”**
    * `_saveSuccessEvent` ๋ฐ `_errorEvent`์—์„œ ๋ถˆํ•„์š”ํ•œ `replay = 0` ๋ฐ `extraBufferCapacity = 1` ์„ค์ •์„ ์ œ๊ฑฐํ•˜์—ฌ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `WaitingInfoViewModel` ๋‚ด SharedFlow ๋ฒ„ํผ ์„ค์ • ์ถ”๊ฐ€

* **refactor: `saveSuccessEvent` ๋ฐ `errorEvent`์— ๋ฒ„ํผ ์šฉ๋Ÿ‰ ์„ค์ •**
    * `MutableSharedFlow` ์ƒ์„ฑ ์‹œ `extraBufferCapacity = 1`์„ ์ถ”๊ฐ€ํ•˜์—ฌ, ๊ตฌ๋…์ž๊ฐ€ ์—†๋Š” ์ƒํƒœ์—์„œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„ ์ตœ์†Œ 1๊ฐœ์˜ ์ตœ์‹  ์ด๋ฒคํŠธ๋ฅผ ๋ณด๊ด€ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์„œ๋น„์Šค ์ด์šฉ์•ฝ๊ด€ ๋ฐ ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ ๋ถ„๋ฆฌ ๋ฐ ๋งํฌ ์—ฐ๋™ (#120)

* **feat: ์„ค์ • ํ™”๋ฉด ๋‚ด ์ •์ฑ… ๊ด€๋ จ ๋ฉ”๋‰ด ์„ธ๋ถ„ํ™” ๋ฐ URL ์—…๋ฐ์ดํŠธ**
    * `strings.xml`: ๊ธฐ์กด `setting_service_policy` ๋ช…์นญ์„ "์„œ๋น„์Šค ์ด์šฉ์•ฝ๊ด€"์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , `setting_personal_information_policy`("๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ") ํ•ญ๋ชฉ์„ ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `SettingScreen.kt`: ์ •์ฑ… URL์„ `SERVICE_POLICY_URL`๊ณผ `PERSONAL_INFORMATION_POLICY_URL`๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `SettingScreen.kt`: `AppInfoContent` ๋ฐ ๊ด€๋ จ ์ปดํฌ์ €๋ธ”์— ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ ๋ฒ„ํŠผ๊ณผ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฐ ์ •์ฑ… ํŽ˜์ด์ง€๋กœ ์—ฐ๊ฒฐ๋˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ์›จ์ดํŒ… ์ •๋ณด ๋“ฑ๋ก ํ™”๋ฉด ๋‚ด ์•ฝ๊ด€ ๋™์˜ ๋งํฌ ์ถ”๊ฐ€**
    * `AddWaitingInfoScreen.kt`: ์›จ์ดํŒ… ์ •๋ณด ๋“ฑ๋ก ์‹œ ๋™์˜๊ฐ€ ํ•„์š”ํ•œ ์•ฝ๊ด€ URL(`TERMS_URL`) ์ƒ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AddWaitingInfoScreen.kt`: ์•ฝ๊ด€ ๋™์˜ ํ…์ŠคํŠธ์— ๋ฐ‘์ค„(`Underline`) ์Šคํƒ€์ผ์„ ์ ์šฉํ•˜๊ณ , ํด๋ฆญ ์‹œ `LocalUriHandler`๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ์•ฝ๊ด€ ํŽ˜์ด์ง€๊ฐ€ ์—ด๋ฆฌ๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์„ค์ • ํ™”๋ฉด ๋‚ด ๋ฌธ์˜ํ•˜๊ธฐ URL ์—…๋ฐ์ดํŠธ**
    * `SettingScreen.kt`: ๊ฐœ๋ฐœ์ž ๋ฌธ์˜ํ•˜๊ธฐ ๋งํฌ(`CONTACT_US_URL`)๋ฅผ ์ตœ์‹  Google Forms ์ฃผ์†Œ๋กœ ๊ฐฑ์‹ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์›จ์ดํŒ… ์ •๋ณด ํŒจํ‚ค์ง€ ๊ตฌ์กฐ ๋ณ€๊ฒฝ, ํ™”๋ฉด ์ „ํ™˜ ์•ต๋‹ˆ๋ฉ”์ด์…˜, ๊ฐ navigation์œผ๋กœ ์ด๋™

* **refactor: ์›จ์ดํŒ… ์ •๋ณด(WaitingInfo) ๊ด€๋ จ ํŒŒ์ผ์„ `setting` ํŒจํ‚ค์ง€ ํ•˜์œ„๋กœ ์ด๋™**
    * ์›จ์ดํŒ… ์ •๋ณด ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์ด ์„ค์ • ๋„๋ฉ”์ธ์— ์ข…์†๋จ์— ๋”ฐ๋ผ ๊ด€๋ จ ํŒŒ์ผ๋“ค์˜ ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `presentation.waitinginfo` -> `presentation.setting.waitinginfo`
    * `WaitingInfoViewModel`, `WaitingInfoUiState`, `AddWaitingInfoScreen` ๋“ฑ์˜ ์œ„์น˜๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ๊ด€๋ จ ์ž„ํฌํŠธ ๋ฌธ์„ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ๋ฉ”์ธ ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ๊ฑฐ**
    * `MainScreen`์˜ ์ „์—ญ ์„ค์ •์œผ๋กœ ๋˜์–ด ์žˆ๋˜ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ œ๊ฑฐํ•˜๊ณ , ๊ฐ ๊ฐœ๋ณ„ ํƒญ(`Home`, `News`, `Schedule`, `PlaceMap`, `Setting`)์˜ `composable` ์„ค์ •์— `EnterTransition.None` ๋ฐ `ExitTransition.None`์„ ๋ช…์‹œ์ ์œผ๋กœ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํƒญ ์ „ํ™˜ ์‹œ ์‹œ๊ฐ์  ์ง€์—ฐ ์—†์ด ์ฆ‰๊ฐ์ ์œผ๋กœ ํ™”๋ฉด์ด ์ „ํ™˜๋˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์ฝ”๋“œ ์ •๋ฆฌ ๋ฐ ๊ฒฝ๋กœ ์ตœ์ ํ™”**
    * `MainScreen.kt`, `SettingNavigation.kt` ๋“ฑ์—์„œ ๋ณ€๊ฒฝ๋œ ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ์— ๋งž์ถฐ ์˜์กด์„ฑ ์ฐธ์กฐ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `WaitingInfoViewModel` ๋‚ด ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ ์ž๋ฆฟ์ˆ˜ ์ œํ•œ ์ƒ์ˆ˜ํ™”

* **refactor: ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง์— ์ƒ์ˆ˜ ๋„์ž…**
    * ๋งค์ง ๋„˜๋ฒ„๋กœ ๊ด€๋ฆฌ๋˜๋˜ ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ์˜ ์ตœ์†Œ ์ž๋ฆฟ์ˆ˜(`9`)์™€ ์ตœ๋Œ€ ์ž๋ฆฟ์ˆ˜(`11`)๋ฅผ `MIN_DIGIT_COUNT` ๋ฐ `MAX_DIGIT_COUNT` ์ƒ์ˆ˜๋กœ ์ •์˜ํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.
    * `updatePhoneNumber`: ์ž…๋ ฅ ์ œํ•œ ๊ธธ์ด๋ฅผ `MAX_DIGIT_COUNT` ์ƒ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `isSubmitEnabled`: ๋“ฑ๋ก ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํŒ๋‹จ ์‹œ `MIN_DIGIT_COUNT` ์ƒ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์›จ์ดํŒ… ๋“ฑ๋ก UiState, ViewModel, UiModel ๊ตฌํ˜„

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: ์›จ์ดํŒ… ๋“ฑ๋ก ํ™”๋ฉด UI ๋ฐ ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค ์ถ”๊ฐ€

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: ์›จ์ดํŒ… ๋“ฑ๋ก ํ™”๋ฉด ๋„ค๋น„๊ฒŒ์ด์…˜ ์—ฐ๊ฒฐ

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: WaitingRegisterViewModelTest ์ถ”๊ฐ€

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: WaitingPlaceSummaryCard์— PlaceDetailPreviewContent ์žฌ์‚ฌ์šฉ ๋ฐ ์ƒ๋‹จ๋ฐ” ๊ธ€์”จ ํฌ๊ธฐ titleMedium์œผ๋กœ ๋ณ€๊ฒฝ

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: ์›จ์ดํŒ… ๋“ฑ๋ก ํ™”๋ฉด ์•ฝ๊ด€ ๋™๊ธฐํ™” ๋ฐ UI/UX ๊ฐœ์„  (#115)

* **feat: ๊ฐœ์ธ์ •๋ณด ๋™์˜ ํ•ญ๋ชฉ ๊ฐ„์†Œํ™” ๋ฐ ์•ฝ๊ด€ ๋งํฌ ์—ฐ๊ฒฐ**
    * `strings.xml`: "(์„ ํƒ) ๋งˆ์ผ€ํŒ… ์ •๋ณด ์ˆ˜์‹  ๋™์˜" ํ•ญ๋ชฉ์„ ์ œ๊ฑฐํ•˜๊ณ , "(ํ•„์ˆ˜) ์„œ๋น„์Šค ์ด์šฉ ์•ฝ๊ด€ ๋™์˜" ๋ฌธ๊ตฌ๋ฅผ "(ํ•„์ˆ˜) ๊ฐœ์ธ์ •๋ณด ์ œ3์ž ์ œ๊ณต ๋™์˜์„œ"๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingRegisterScreen`: ์•ฝ๊ด€ ํ…์ŠคํŠธ ํด๋ฆญ ์‹œ ๋…ธ์…˜(Notion) ๊ธฐ๋ฐ˜์˜ ์™ธ๋ถ€ ์•ฝ๊ด€ ํŽ˜์ด์ง€๋กœ ์—ฐ๊ฒฐ๋˜๋„๋ก `LocalUriHandler`๋ฅผ ์ ์šฉํ•˜๊ณ  ๋ฐ‘์ค„(`Underline`) ์Šคํƒ€์ผ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingRegisterUiState` & `WaitingRegisterViewModel`: ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `isMarketingAgreed` ์†์„ฑ ๋ฐ ๊ด€๋ จ ํ† ๊ธ€ ๋กœ์ง์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `WaitingRegisterViewModel` ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง ๊ฐœ์„ **
    * `withDerivedState` ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ๋„์ž…ํ•˜์—ฌ `partySize` ๋ณ€๊ฒฝ, ์ œ์ถœ ์ค‘ ์ƒํƒœ(`isSubmitting`), ์•ฝ๊ด€ ๋™์˜ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ ํŒŒ์ƒ ์ƒํƒœ(`canSubmit`, `canIncreasePartySize` ๋“ฑ) ๊ณ„์‚ฐ ๋กœ์ง์„ ์บก์Аํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `_uiState.update` ํ˜ธ์ถœ ์‹œ๋งˆ๋‹ค `withDerivedState()`๋ฅผ ํ†ตํ•ด UI ์ƒํƒœ์˜ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ์›จ์ดํŒ… ๋“ฑ๋ก ํ™”๋ฉด UI ์ปดํฌ๋„ŒํŠธ ๋ฐ ํ…Œ๋งˆ ์ˆ˜์ •**
    * `WaitingRegisterScreen`: ๊ธฐ์กด `SectionDivider` ๋Œ€์‹  `HorizontalDivider`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ๊ฐ์  ๊ตฌ๋ถ„์„ ๋ช…ํ™•ํžˆ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ฒดํฌ๋ฐ•์Šค ๋ฐ ๋“ฑ๋ก ๋ฒ„ํŠผ์˜ ๊ฐ•์กฐ ์ƒ‰์ƒ์„ `accentBlue`์—์„œ `black`์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋””์ž์ธ ํ†ค์•ค๋งค๋„ˆ๋ฅผ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ ˆ์ด์•„์›ƒ ์—ฌ๋ฐฑ(Padding) ๋ฐ ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๊ฐ„๊ฒฉ์„ ์ „๋ฐ˜์ ์œผ๋กœ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: ์•ฝ๊ด€ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋ฐ˜์˜ ๋ฐ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ •๋น„**
    * `WaitingRegisterViewModelTest`: ๋งˆ์ผ€ํŒ… ๋™์˜ ๊ด€๋ จ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์‚ญ์ œํ•˜๊ณ , ๋ณ€๊ฒฝ๋œ ํ•„์ˆ˜ ์•ฝ๊ด€ ๋™์˜ ๋กœ์ง์— ๋งž์ถฐ ๊ธฐ์กด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์ฝ”๋“œ ์ •๋ฆฌ ๋ฐ ์ฃผ์„ ์ถ”๊ฐ€**
    * `WaitingRegisterViewModel` ๋‚ด ์ „ํ™”๋ฒˆํ˜ธ ์ €์žฅ ์ƒํƒœ์— ๋Œ€ํ•œ ์ž‘์—… ๋ฉ”๋ชจ(NOTE/TODO)๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ถˆํ•„์š”ํ•œ ์ž„ํฌํŠธ ๋ฐ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์Šคํƒ€์ผ ์ •์˜๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์›จ์ดํŒ… ๋“ฑ๋ก ํ™”๋ฉด ์•ฝ๊ด€ ๋™๊ธฐํ™” ๋ฐ UI/UX ๊ฐœ์„  (#115)

* **feat: ๊ฐœ์ธ์ •๋ณด ๋™์˜ ํ•ญ๋ชฉ ๊ฐ„์†Œํ™” ๋ฐ ์•ฝ๊ด€ ๋งํฌ ์—ฐ๊ฒฐ**
    * `strings.xml`: "(์„ ํƒ) ๋งˆ์ผ€ํŒ… ์ •๋ณด ์ˆ˜์‹  ๋™์˜" ํ•ญ๋ชฉ์„ ์ œ๊ฑฐํ•˜๊ณ , "(ํ•„์ˆ˜) ์„œ๋น„์Šค ์ด์šฉ ์•ฝ๊ด€ ๋™์˜" ๋ฌธ๊ตฌ๋ฅผ "(ํ•„์ˆ˜) ๊ฐœ์ธ์ •๋ณด ์ œ3์ž ์ œ๊ณต ๋™์˜์„œ"๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingRegisterScreen`: ์•ฝ๊ด€ ํ…์ŠคํŠธ ํด๋ฆญ ์‹œ ๋…ธ์…˜(Notion) ๊ธฐ๋ฐ˜์˜ ์™ธ๋ถ€ ์•ฝ๊ด€ ํŽ˜์ด์ง€๋กœ ์—ฐ๊ฒฐ๋˜๋„๋ก `LocalUriHandler`๋ฅผ ์ ์šฉํ•˜๊ณ  ๋ฐ‘์ค„(`Underline`) ์Šคํƒ€์ผ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingRegisterUiState` & `WaitingRegisterViewModel`: ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” `isMarketingAgreed` ์†์„ฑ ๋ฐ ๊ด€๋ จ ํ† ๊ธ€ ๋กœ์ง์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `WaitingRegisterViewModel` ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง ๊ฐœ์„ **
    * `withDerivedState` ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ๋„์ž…ํ•˜์—ฌ `partySize` ๋ณ€๊ฒฝ, ์ œ์ถœ ์ค‘ ์ƒํƒœ(`isSubmitting`), ์•ฝ๊ด€ ๋™์˜ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ ํŒŒ์ƒ ์ƒํƒœ(`canSubmit`, `canIncreasePartySize` ๋“ฑ) ๊ณ„์‚ฐ ๋กœ์ง์„ ์บก์Аํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `_uiState.update` ํ˜ธ์ถœ ์‹œ๋งˆ๋‹ค `withDerivedState()`๋ฅผ ํ†ตํ•ด UI ์ƒํƒœ์˜ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ์›จ์ดํŒ… ๋“ฑ๋ก ํ™”๋ฉด UI ์ปดํฌ๋„ŒํŠธ ๋ฐ ํ…Œ๋งˆ ์ˆ˜์ •**
    * `WaitingRegisterScreen`: ๊ธฐ์กด `SectionDivider` ๋Œ€์‹  `HorizontalDivider`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ๊ฐ์  ๊ตฌ๋ถ„์„ ๋ช…ํ™•ํžˆ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ฒดํฌ๋ฐ•์Šค ๋ฐ ๋“ฑ๋ก ๋ฒ„ํŠผ์˜ ๊ฐ•์กฐ ์ƒ‰์ƒ์„ `accentBlue`์—์„œ `black`์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋””์ž์ธ ํ†ค์•ค๋งค๋„ˆ๋ฅผ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ ˆ์ด์•„์›ƒ ์—ฌ๋ฐฑ(Padding) ๋ฐ ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๊ฐ„๊ฒฉ์„ ์ „๋ฐ˜์ ์œผ๋กœ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: ์•ฝ๊ด€ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋ฐ˜์˜ ๋ฐ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ •๋น„**
    * `WaitingRegisterViewModelTest`: ๋งˆ์ผ€ํŒ… ๋™์˜ ๊ด€๋ จ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์‚ญ์ œํ•˜๊ณ , ๋ณ€๊ฒฝ๋œ ํ•„์ˆ˜ ์•ฝ๊ด€ ๋™์˜ ๋กœ์ง์— ๋งž์ถฐ ๊ธฐ์กด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์ฝ”๋“œ ์ •๋ฆฌ ๋ฐ ์ฃผ์„ ์ถ”๊ฐ€**
    * `WaitingRegisterViewModel` ๋‚ด ์ „ํ™”๋ฒˆํ˜ธ ์ €์žฅ ์ƒํƒœ์— ๋Œ€ํ•œ ์ž‘์—… ๋ฉ”๋ชจ(NOTE/TODO)๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ถˆํ•„์š”ํ•œ ์ž„ํฌํŠธ ๋ฐ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์Šคํƒ€์ผ ์ •์˜๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์›จ์ดํŒ… ๋“ฑ๋ก ์ „ ์ „ํ™”๋ฒˆํ˜ธ ํ™•์ธ ๋กœ์ง ์ถ”๊ฐ€ ๋ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ตฌ์กฐ ๊ฐœ์„ 

* **feat: ์ „ํ™”๋ฒˆํ˜ธ ๋ฏธ๋“ฑ๋ก ์‹œ ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋กœ์ง ์ถ”๊ฐ€**
    * `WaitingRegisterViewModel`: ์ดˆ๊ธฐํ™” ์‹œ `waitingInfo`๊ฐ€ ์—†์„ ๊ฒฝ์šฐ `navigateToPhoneRegistrationEvent` ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingRegisterScreen`: ViewModel์—์„œ ๋ฐœํ–‰๋œ ์ด๋ฒคํŠธ๋ฅผ ๊ด€์ฐฐํ•˜์—ฌ `onNavigateToPhoneRegistration` ์ฝœ๋ฐฑ์„ ํ˜ธ์ถœํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingRegisterViewModelTest`: ์ „ํ™”๋ฒˆํ˜ธ ๋ฏธ๋“ฑ๋ก ์ƒํƒœ์—์„œ ์ดˆ๊ธฐํ™” ์‹œ ์ด๋ฒคํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋ฐœํ–‰๋˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์›จ์ดํŒ… ๋“ฑ๋ก ๊ด€๋ จ ๋ผ์šฐํŠธ ๋ช…์นญ ๋ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ์„ค์ • ๋ณ€๊ฒฝ**
    * `FestabookRoute`: `AddWaitingInfo(placeId: Long)`๋ฅผ `WaitingRegister(placeId: Long)`๋กœ ์ด๋ฆ„์„ ๋ณ€๊ฒฝํ•˜๊ณ , ๊ธฐ์กด `AddWaitingInfo`๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์—†๋Š” ํ˜•ํƒœ๋กœ ์ „ํ™˜ํ•˜์—ฌ ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก ํ™”๋ฉด์œผ๋กœ ์‚ฌ์šฉํ•˜๋„๋ก ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceMapNavigation`: `AddWaitingInfo`๋กœ ์ •์˜๋˜์–ด ์žˆ๋˜ ์›จ์ดํŒ… ๋“ฑ๋ก ํ™”๋ฉด์˜ ๋ผ์šฐํŠธ๋ฅผ `WaitingRegister`๋กœ ์ˆ˜์ •ํ•˜๊ณ , ์ƒˆ๋กœ์šด `onNavigateToPhoneRegistration` ์ฝœ๋ฐฑ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainScreen`: ๋ฐ”๋€ ๋ผ์šฐํŠธ ๋ช…์นญ์— ๋งž์ถฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ํ˜ธ์ถœ ๋กœ์ง์„ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๋ถˆํ•„์š”ํ•œ TODO ์ œ๊ฑฐ ๋ฐ ์ฝ”๋“œ ์ •๋ฆฌ**
    * `WaitingRegisterViewModel`: ์ „ํ™”๋ฒˆํ˜ธ ๋ฏธ๋“ฑ๋ก ์‹œ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ `TODO` ์ฃผ์„์„ ๊ตฌํ˜„๋œ ๋กœ์ง์œผ๋กœ ๋Œ€์ฒดํ•˜๋ฉฐ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์›จ์ดํŒ… ๋“ฑ๋ก ํ™•์ธ ๋ฐ”ํ…€์‹œํŠธ ์ถ”๊ฐ€

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: ์›จ์ดํŒ… ๋“ฑ๋ก ํ™•์ธ ๋ฐ”ํ…€์‹œํŠธ UI ๊ฐœ์„  ๋ฐ ์ด๋ฏธ์ง€ ์ž์‚ฐ ๊ต์ฒด

* **style: `WaitingRegisterConfirmBottomSheet` UI ๋ฐ ์Šคํƒ€์ผ ์—…๋ฐ์ดํŠธ**
    * **์ด๋ฏธ์ง€ ๊ต์ฒด**: ๊ธฐ์กด์˜ `ic_alarm`, `ic_clock` ์กฐํ•ฉ ์•„์ด์ฝ˜์„ ์‚ญ์ œํ•˜๊ณ  ์‹ ๊ทœ ์ด๋ฏธ์ง€ ์ž์‚ฐ์ธ `img_waiting_push_notification_alert`๋กœ ๊ต์ฒดํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ ๋ฐ ์ƒ‰์ƒ ๋ณ€๊ฒฝ**: ์ œ๋ชฉ ์Šคํƒ€์ผ์„ `titleMedium`์—์„œ `displaySmall`๋กœ, ์„ค๋ช… ์Šคํƒ€์ผ์„ `bodyMedium`์—์„œ `bodyLarge`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ์ฒดํฌ๋ฐ•์Šค ํ…์ŠคํŠธ ์ƒ‰์ƒ์„ `gray600`์—์„œ `black`์œผ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **๋ ˆ์ด์•„์›ƒ ์กฐ์ •**: `ModalBottomSheet`์˜ `dragHandle`์„ ์ œ๊ฑฐํ•˜๊ณ , ๋ฒ„ํŠผ ๊ฐ„๊ฒฉ ๋ฐ ํŒจ๋”ฉ์— `festabookSpacing` ์‹œ์Šคํ…œ์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **๋ฒ„ํŠผ ์Šคํƒ€์ผ ์ˆ˜์ •**: `ConfirmBottomSheetButton`์˜ ๊ณ ์ • ๋†’์ด(52.dp)๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ์ˆ˜์ง ํŒจ๋”ฉ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์œ ์—ฐํ•œ ๋†’์ด๋ฅผ ๊ฐ–๋„๋ก ๊ฐœ์„ ํ–ˆ์œผ๋ฉฐ, ํ…์ŠคํŠธ ์Šคํƒ€์ผ์„ `displayMedium`์—์„œ `displaySmall`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์›จ์ดํŒ… ๋“ฑ๋ก ๊ฒฝ๋กœ ๋‚ด ๋ ˆ์ด์•„์›ƒ ์ˆ˜์ •**
    * `PlaceMapNavigation.kt`: `WaitingRegisterRoute` ํ˜ธ์ถœ ์‹œ `innerPadding`์„ ๋ฐ˜์˜ํ•˜๋„๋ก `modifier`๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **resource: ์‹ ๊ทœ ์ด๋ฏธ์ง€ ์ž์‚ฐ ์ถ”๊ฐ€**
    * `img_waiting_push_notification_alert.png` ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: MyWaitingWithPlace ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฐ MyWaitingRepository ๋ฐ˜ํ™˜ํƒ€์ž… ๋ณ€๊ฒฝ

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: placeId ๋กœ์ปฌ ์บ์‹œ ์ถ”๊ฐ€ ๋ฐ ์›จ์ดํŒ… ๋“ฑ๋ก/์กฐํšŒ/์ทจ์†Œ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: MyWaitingWithPlace๋ฅผ MyWaiting์œผ๋กœ ํ†ตํ•ฉ

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: ์›จ์ดํŒ… ์ •๋ณด ์ €์žฅ ์ค‘ ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ ๋ฐ UI ๊ฐœ์„ 

* **feat: `WaitingInfoViewModel` ๋‚ด ์ €์žฅ ์ƒํƒœ ๊ด€์ฐฐ ๋กœ์ง ์ถ”๊ฐ€**
    * ๊ธฐ์กด ํ”„๋ผ์ด๋น— ํ•„๋“œ์˜€๋˜ `isSaving`์„ `StateFlow` ํ˜•ํƒœ์ธ `_isSaving`๊ณผ `isSaving`์œผ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ UI์—์„œ ๊ด€์ฐฐํ•  ์ˆ˜ ์žˆ๋„๋ก ๋…ธ์ถœํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `saveWaitingInfo` ๋ฉ”์„œ๋“œ ์‹คํ–‰ ์‹œ `_isSaving` ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€ ๋ฐ ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์ œ์–ดํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: `AddWaitingInfoScreen` ๋‚ด ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ถ”๊ฐ€**
    * `isSaving` ์ƒํƒœ๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ, ์ •๋ณด ์ €์žฅ ์ค‘์ผ ๋•Œ ํ™•์ธ ๋ฒ„ํŠผ ๋‚ด ํ…์ŠคํŠธ ๋Œ€์‹  `CircularProgressIndicator`๊ฐ€ ํ‘œ์‹œ๋˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ConfirmButton` ๋ฐ ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ๋“ค์— `isSaving` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ํ”„๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: `WaitingRegisterScreen` ๋‚ด ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์Šคํƒ€์ผ ์ˆ˜์ •**
    * `CircularProgressIndicator`์˜ ํฌ๊ธฐ๋ฅผ `24.dp`์—์„œ `28.dp`๋กœ, ๋‘๊ป˜(`strokeWidth`)๋ฅผ `2.dp`์—์„œ `4.dp`๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์‹œ๊ฐ์  ์ธ์ง€์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **chore: `WaitingRegisterScreen` ๋‚ด TODO ์ฃผ์„ ์ถ”๊ฐ€**
    * ๋“ฑ๋ก ์„ฑ๊ณต ์‹œ "๋‚˜์˜ ์›จ์ดํŒ… ํ™”๋ฉด"์œผ๋กœ ์ด๋™ํ•ด์•ผ ํ•จ์„ ์•Œ๋ฆฌ๋Š” ์ž‘์—… ์ฃผ์„์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์›จ์ดํŒ… ๋“ฑ๋ก UI ์ƒํƒœ ๋ชจ๋ธ ๋ถ„๋ฆฌ ๋ฐ ๊ตฌ์กฐ ๊ฐœ์„ 

* **feat: `WaitingRegisterUiModel` ๋„์ž… ๋ฐ UI ์ƒํƒœ ๊ตฌ์กฐ ๋ณ€๊ฒฝ**
    * `WaitingRegisterUiState.Success` ํด๋ž˜์Šค์— ์ง์ ‘ ์ •์˜๋˜์–ด ์žˆ๋˜ ํ•„๋“œ๋“ค์„ ๋ณ„๋„์˜ `WaitingRegisterUiModel` ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค๋กœ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ธ์›์ˆ˜ ์ฆ๊ฐ ๊ฐ€๋Šฅ ์—ฌ๋ถ€(`canDecreasePartySize`, `canIncreasePartySize`) ๋ฐ ๋“ฑ๋ก ๊ฐ€๋Šฅ ์—ฌ๋ถ€(`canSubmit`) ๋“ฑ ์œ ๋„ ์ƒํƒœ(Derived State) ๊ณ„์‚ฐ ๋กœ์ง์„ ์ƒˆ ๋ชจ๋ธ๋กœ ์ด๋™ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ธ์›์ˆ˜ ์ œํ•œ ์ƒ์ˆ˜(`MIN_PARTY_SIZE`, `MAX_PARTY_SIZE`)๋ฅผ `WaitingRegisterUiState`์—์„œ `WaitingRegisterUiModel`๋กœ ์žฌ๋ฐฐ์น˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `WaitingRegisterViewModel` ๋‚ด ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง ์ตœ์ ํ™”**
    * `updateRegister` inline ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ `WaitingRegisterUiState.Success` ์ƒํƒœ์ผ ๋•Œ์˜ `WaitingRegisterUiModel` ์—…๋ฐ์ดํŠธ ๋กœ์ง์„ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `withDerivedState` ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ `WaitingRegisterUiModel` ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ UI ๋กœ์ง ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ท„์Šต๋‹ˆ๋‹ค.

* **style: `WaitingRegisterScreen` UI ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์ฐธ์กฐ ์ˆ˜์ •**
    * ์ƒํƒœ ๊ตฌ์กฐ ๋ณ€๊ฒฝ์— ๋”ฐ๋ผ `uiState.waitingRegister`๋ฅผ ํ†ตํ•ด ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ(`isSubmitting`, `partySize`, `isServiceAgreed` ๋“ฑ)๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: `WaitingRegisterViewModelTest` ๋ฐ˜์˜**
    * ๋ชจ๋ธ ๋ถ„๋ฆฌ ๋ฐ ๊ฒฝ๋กœ ๋ณ€๊ฒฝ์— ๋งž์ถฐ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋‚ด Assert ๋ฌธ๊ณผ ์ƒ์ˆ˜ ์ฐธ์กฐ ๊ฒฝ๋กœ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์›จ์ดํŒ… ์ค‘๋ณต ๋“ฑ๋ก ๋ฐฉ์ง€ ๋กœ์ง ๋ฐ ์•ˆ๋‚ด ๋‹ค์ด์–ผ๋กœ๊ทธ ๊ตฌํ˜„

* **feat: ์›จ์ดํŒ… ์ค‘๋ณต ๋“ฑ๋ก ๋ฐฉ์ง€ ์•ˆ๋‚ด ๋‹ค์ด์–ผ๋กœ๊ทธ(`WaitingDuplicateDialog`) ์ถ”๊ฐ€**
    * ๊ธฐ๊ธฐ๋‹น ํ•˜๋‚˜์˜ ์žฅ์†Œ๋งŒ ์›จ์ดํŒ… ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์•ˆ๋‚ด ๋ฌธ๊ตฌ์™€ ํ•จ๊ป˜ "๋‚˜์˜ ์›จ์ดํŒ…" ํ™•์ธ ๋ฐ "๋‹ค์Œ์— ํ•˜๊ธฐ" ๋ฒ„ํŠผ์„ ํฌํ•จํ•˜๋Š” ์ปค์Šคํ…€ ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `strings.xml`: ๋‹ค์ด์–ผ๋กœ๊ทธ์— ์‚ฌ์šฉ๋˜๋Š” ๋‹ค๊ตญ์–ด ๋ฆฌ์†Œ์Šค ๋ฌธ์ž์—ด์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: `PlaceDetailViewModel` ๋‚ด ์›จ์ดํŒ… ์ค‘๋ณต ์ฒดํฌ ๋กœ์ง ๊ตฌํ˜„**
    * `onRegisterWaitingClick` ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€: ์›จ์ดํŒ… ๋“ฑ๋ก ์‹œ `MyWaitingRepository`๋ฅผ ํ†ตํ•ด ๊ธฐ์กด ์›จ์ดํŒ… ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
    * ๊ธฐ์กด ์›จ์ดํŒ…์ด ์—†๋Š” ๊ฒฝ์šฐ ๋“ฑ๋ก ํ™”๋ฉด์œผ๋กœ ์ด๋™(`navigateToWaitingRegisterEvent`)ํ•˜๊ณ , ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ ์•ˆ๋‚ด ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ๋…ธ์ถœ(`showDuplicateWaitingDialogEvent`)ํ•˜๋„๋ก ์ด๋ฒคํŠธ๋ฅผ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MyWaitingRepository` ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์ดˆ๊ธฐํ™” ๋กœ์ง์„ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `PlaceDetailScreen` UI-ViewModel ์ด๋ฒคํŠธ ์—ฐ๋™**
    * `ObserveAsEvents`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ทฐ๋ชจ๋ธ์˜ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ ๋‹ค์ด์–ผ๋กœ๊ทธ ๋…ธ์ถœ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํ•˜๋‹จ ๋ฐ”์˜ ์›จ์ดํŒ… ๋“ฑ๋ก ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ทฐ๋ชจ๋ธ์˜ `onRegisterWaitingClick`์„ ํ˜ธ์ถœํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: `PlaceDetailViewModelTest` ๋‚ด ์˜์กด์„ฑ ์ถ”๊ฐ€์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ •**
    * `PlaceDetailViewModel` ์ƒ์„ฑ์ž ๋ณ€๊ฒฝ์— ๋งž์ถฐ `MyWaitingRepository` ๋ชจํ‚น(Mocking) ๋ฐ ์ดˆ๊ธฐํ™” ๋กœ์ง์„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์— ๋ฐ˜์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์›จ์ดํŒ… ์ค‘๋ณต ๋“ฑ๋ก ๋ฐฉ์ง€ ๋กœ์ง ๊ณ ๋„ํ™” ๋ฐ ๊ถŒํ•œ ์š”์ฒญ Flow ์ถ”๊ฐ€ (#120)

* **feat: ์›จ์ดํŒ… ์ค‘๋ณต ๋“ฑ๋ก ์ฒ˜๋ฆฌ UX ๊ฐœ์„ **
    * `WaitingDuplicateDialog`๋ฅผ ์‚ญ์ œํ•˜๊ณ , ๋” ์ƒ์„ธํ•œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๋Š” `WaitingDuplicateBottomSheet` ๋ฐ `WaitingCancelConfirmDialog`๋ฅผ ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์˜ ํ”Œ๋ ˆ์ด์Šค๋งŒ ์›จ์ดํŒ… ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์•ˆ๋‚ด ๋ฌธ๊ตฌ์™€ ํ•จ๊ป˜ ๊ธฐ์กด ์›จ์ดํŒ… ์ทจ์†Œ ํ›„ ์žฌ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `strings.xml`: ์ค‘๋ณต ์›จ์ดํŒ… ์•ˆ๋‚ด ๋ฐ ์ทจ์†Œ ํ™•์ธ ๊ด€๋ จ ๋‹ค๊ตญ์–ด ๋ฆฌ์†Œ์Šค๋ฅผ ์ถ”๊ฐ€ ๋ฐ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ์›จ์ดํŒ… ๋“ฑ๋ก ์‹œ ์•Œ๋ฆผ ๊ถŒํ•œ ์ฒดํฌ ๋กœ์ง ์ถ”๊ฐ€**
    * `WaitingRegisterRoute`: ์›จ์ดํŒ… ๋“ฑ๋ก ์ œ์ถœ ์‹œ `NotificationPermissionManager`๋ฅผ ํ†ตํ•ด ์•Œ๋ฆผ ๊ถŒํ•œ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๊ณ , ์ƒํƒœ์— ๋”ฐ๋ผ ๊ถŒํ•œ ์š”์ฒญ ๋˜๋Š” ์•ˆ๋‚ด ๋‹ค์ด์–ผ๋กœ๊ทธ(`NotificationPermissionDialog`)๋ฅผ ํ‘œ์‹œํ•˜๋„๋ก ๋กœ์ง์„ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceMapNavigation`: ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„์— `NotificationPermissionManager` ์˜์กด์„ฑ์„ ์ฃผ์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `PlaceDetailViewModel` ๋ฐ `PlaceDetailScreen` ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๊ฐœ์„ **
    * `showDuplicateWaitingDialogEvent`๋ฅผ `showDuplicateWaitingBottomSheetEvent`๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๊ธฐ์กด ์›จ์ดํŒ… ID๋ฅผ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด ์›จ์ดํŒ… ์ทจ์†Œ ๋ฐ ์‹ ๊ทœ ๋“ฑ๋ก์„ ์œ„ํ•œ `cancelAndRegister` ๋ฉ”์„œ๋“œ์™€ ์‹คํŒจ ์ฒ˜๋ฆฌ์šฉ `cancelWaitingFailureEvent`๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: `PlaceDetailViewModel` ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€**
    * ์›จ์ดํŒ… ๋“ฑ๋ก ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋“ฑ๋ก ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋กœ์ง(`navigateToWaitingRegisterEvent`, `showDuplicateWaitingBottomSheetEvent`)์— ๋Œ€ํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์›จ์ดํŒ… ์ทจ์†Œ ํ›„ ์žฌ๋“ฑ๋ก(`cancelAndRegister`) ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋Œ€ํ•œ ์„ฑ๊ณต ๋ฐ ์‹คํŒจ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์ฝ”๋“œ ์ •๋ฆฌ ๋ฐ ์ฃผ์„ ์ถ”๊ฐ€**
    * `WaitingLocalDataSource.kt`: ์„œ๋ฒ„ ์‘๋‹ต ์ŠคํŽ™ ๋ณ€๊ฒฝ์— ๋”ฐ๋ผ ํ–ฅํ›„ ์‚ญ์ œ๊ฐ€ ํ•„์š”ํ•œ `placeId` ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋“ค์— TODO ์ฃผ์„์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์›จ์ดํŒ… ๋“ฑ๋ก ์•Œ๋ฆผ ๊ถŒํ•œ ๋กœ์ง ๊ฐœ์„  ๋ฐ ์žฅ์†Œ ์ƒ์„ธ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์—ฐ๋™ (#120)

* **refactor: ์›จ์ดํŒ… ๋“ฑ๋ก ์‹œ ์•Œ๋ฆผ ๊ถŒํ•œ ์ฒดํฌ ๋กœ์ง์„ `SettingViewModel` ๊ธฐ๋ฐ˜์œผ๋กœ ํ†ตํ•ฉ**
    * `MainScreen` ๋ฐ `placeMapNavGraph`: `WaitingRegisterRoute`์— `SettingViewModel`์„ ์ฃผ์ž…ํ•˜๋„๋ก ๊ตฌ์กฐ๋ฅผ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingRegisterRoute`: ์•Œ๋ฆผ ๊ถŒํ•œ ์ƒํƒœ(`isAllowed`)๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ๊ถŒํ•œ์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋งŒ ์•ˆ๋‚ด ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ํ‘œ์‹œํ•˜๋„๋ก ๋กœ์ง์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingRegisterRoute`: ๊ถŒํ•œ ํ—ˆ์šฉ ์ด๋ฒคํŠธ(`permissionCheckEvent`) ๋ฐœ์ƒ ์‹œ `NotificationPermissionManager`๋ฅผ ํ†ตํ•ด ์‹ค์ œ ๊ถŒํ•œ์„ ํ™•์ธํ•˜๊ณ  ์ƒํƒœ๋ฅผ ๋™๊ธฐํ™”ํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์žฅ์†Œ ์ƒ์„ธ(`PlaceDetailViewModel`) ๋‚ด ๋Œ€๊ธฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง ๋ณด์™„**
    * `PlaceDetailViewModel`: ์›จ์ดํŒ… ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต ์‹œ, ๊ธฐ์กด์˜ ๋Œ€๊ธฐ ํŒ€ ์ˆ˜๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์˜ˆ์ƒ ๋Œ€๊ธฐ ์‹œ๊ฐ„(`estimatedWaitTime`)๋„ `waitingStatus` UI ์ƒํƒœ์— ๋ฐ˜์˜ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์›จ์ดํŒ… ๋“ฑ๋ก ํ™”๋ฉด UI ์ƒํƒœ ์ฒ˜๋ฆฌ ๊ฐœ์„ **
    * `WaitingRegisterRoute`: ์•Œ๋ฆผ ํ™œ์„ฑํ™” ์‹œ ํ‘œ์‹œํ•  ์Šค๋‚ต๋ฐ” ๋ฉ”์‹œ์ง€(`setting_notice_enabled`) ์—ฐ๋™์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ถˆํ•„์š”ํ•ด์ง„ `rememberCoroutineScope` ์ž„ํฌํŠธ ๋ฐ ๊ด€๋ จ ๋กœ์ง์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ๋ฆฌ์†Œ์Šค ์˜คํƒ€ ์ˆ˜์ •

* **refactor: `strings.xml` ๋‚ด ์˜คํƒ€ ์ˆ˜์ •**
    * `waiting_duplicate_discription` ๋ฆฌ์†Œ์Šค ํ‚ค๋ฅผ `waiting_duplicate_description`์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์˜คํƒ€๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์›จ์ดํŒ… ๋“ฑ๋ก ViewModel ์บก์Аํ™” ๋ฐ ๋ฆฌ์†Œ์Šค ์˜คํƒ€ ์ˆ˜์ •

* **refactor: `WaitingRegisterViewModel` ๋‚ด ์žฅ์†Œ ์ •๋ณด ๋กœ๋“œ ๋กœ์ง ์บก์Аํ™”**
    * `loadPlaceSummary()` ๋ฉ”์„œ๋“œ์˜ ์ ‘๊ทผ ์ œ์–ด์ž๋ฅผ `private`์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์™ธ๋ถ€ ๋…ธ์ถœ์„ ์ œํ•œํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ๋กœ์ง์€ ํด๋ž˜์Šค ์ดˆ๊ธฐํ™” ์‹œ์ ์— ์‹คํ–‰๋˜๋„๋ก ๊ด€๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

* **test: `WaitingRegisterViewModelTest` ๋‚ด ๊ฐ€์‹œ์„ฑ ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ •**
    * `loadPlaceSummary()`๊ฐ€ `private`์œผ๋กœ ๋ณ€๊ฒฝ๋จ์— ๋”ฐ๋ผ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ๋ฉ”์„œ๋“œ๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๋Œ€์‹  ViewModel์„ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜์—ฌ ์ดˆ๊ธฐํ™” ๋กœ์ง์ด ์‹คํ–‰๋˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ๋ฆฌ์†Œ์Šค ์‹๋ณ„์ž ์˜คํƒ€ ์ˆ˜์ •**
    * `WaitingDuplicateBottomSheet`: ์ž˜๋ชป ์ž‘์„ฑ๋œ ๋ฆฌ์†Œ์Šค ๋ช… `waiting_duplicate_discription`์„ `waiting_duplicate_description`์œผ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `LocalTime` ํฌ๋งทํŒ… ๋ฐฉ์‹ ๋ณ€๊ฒฝ

* **refactor: `LocalTime.toFormattedString` ๊ตฌํ˜„ ๊ฐœ์„ **
    * ๊ธฐ์กด์˜ ๊ฐœ๋ณ„ ๊ตฌ์„ฑ ์š”์†Œ(`hour`, `minute`) ์ง€์ • ๋ฐฉ์‹ ๋Œ€์‹  `byUnicodePattern("HH:mm")`์„ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ด์— ๋”ฐ๋ผ ๋ถˆํ•„์š”ํ•ด์ง„ `placeTimeFormat` ์ƒ์ˆ˜๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๋กœ์ง์„ ํ•จ์ˆ˜ ๋‚ด๋ถ€๋กœ ํ†ตํ•ฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `AddWaitingInfo` ๋‚ด `placeId` ํƒ€์ž…์„ nullable๋กœ ๋ณ€๊ฒฝ ๋ฐ ๊ด€๋ จ ๋กœ์ง ๊ฐ„์†Œํ™”

* **refactor: `FestabookRoute.AddWaitingInfo` ๋‚ด `placeId` ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๊ฐœ์„ **
    * ๊ธฐ์กด์— ์œ ํšจํ•˜์ง€ ์•Š์€ ID๋ฅผ ๋‚˜ํƒ€๋‚ด๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋˜ ์„ผํ‹ฐ๋„ฌ ๊ฐ’(`NO_PLACE_ID = -1L`)๊ณผ ์ปดํŒจ๋‹ˆ์–ธ ๊ฐ์ฒด๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `placeId` ํƒ€์ž…์„ `Long?`์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ๊ธฐ๋ณธ๊ฐ’์„ `null`๋กœ ์„ค์ •ํ•˜์—ฌ ๋„๋ฉ”์ธ ๋ชจ๋ธ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `SettingNavigation` ๋‚ด ์กฐ๊ฑด๋ถ€ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋กœ์ง ์ˆ˜์ •**
    * `onSaveSuccess` ์ฝœ๋ฐฑ ๋‚ด์—์„œ `placeId` ์œ ํšจ์„ฑ์„ ํ™•์ธํ•˜๋Š” ๋กœ์ง์„ ์„ผํ‹ฐ๋„ฌ ๊ฐ’ ๋น„๊ต์—์„œ `null` ์ฒดํฌ ๋ฐฉ์‹์œผ๋กœ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: PlaceDetailPreviewContent ์ ‘๊ทผ ์ œ์–ด์ž ๋ณ€๊ฒฝ

* **refactor: `PlaceDetailPreviewContent` ๊ฐ€์‹œ์„ฑ ๋ฒ”์œ„ ํ™•์žฅ**
    * `PlaceDetailPreviewContent` ์ปดํฌ์ €๋ธ” ํ•จ์ˆ˜์˜ ์ ‘๊ทผ ์ œ์–ด์ž๋ฅผ `internal`์—์„œ `public`์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์™ธ๋ถ€์—์„œ์˜ ์ ‘๊ทผ์„ฑ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `PlaceMapNavigation` ๋‚ด ๋„ค๋น„๊ฒŒ์ด์…˜ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์ˆ˜ํ™”

* **refactor: ํ™”๋ฉด ์ „ํ™˜ ์˜คํ”„์…‹ ๊ฐ’์„ ์ƒ์ˆ˜๋กœ ๋ถ„๋ฆฌ**
    * `PlaceDetail` ๊ฒฝ๋กœ์˜ `enterTransition` ๋ฐ `exitTransition`์— ์‚ฌ์šฉ๋˜๋˜ ๋งค์ง ๋„˜๋ฒ„(10)๋ฅผ `INITIAL_OFFSET_CONSTANT` ์ƒ์ˆ˜๋กœ ์ •์˜ํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `WaitingRegisterConfirmBottomSheet` UI ๊ฐ„๊ฒฉ ์„ค์ • ๋ฐฉ์‹ ๊ฐœ์„ 

* **refactor: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ ˆ์ด์•„์›ƒ ์ˆ˜์น˜๋ฅผ ๊ณตํ†ต ๋””์ž์ธ ์‹œ์Šคํ…œ(`festabookSpacing`)์œผ๋กœ ๋Œ€์ฒด**
    * ์ฒดํฌ ์•„์ด์ฝ˜์˜ ์ƒํ•˜ ํŒจ๋”ฉ์„ `40.dp`์—์„œ `festabookSpacing.paddingTitleHorizontal`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ œ๋ชฉ๊ณผ ์„ค๋ช… ๋ฌธ๊ตฌ ์‚ฌ์ด์˜ ๊ฐ„๊ฒฉ์„ `8.dp`์—์„œ `festabookSpacing.paddingBody2`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋‚ด ์ค‘๋ณต ํ…Œ์ŠคํŠธ ํ”ฝ์Šค์ฒ˜ ํ†ตํ•ฉ ๋ฐ ๊ฐ€๋…์„ฑ ๊ฐœ์„ 

* **refactor: `FAKE_MY_WAITING`์„ ๊ณตํ†ต ํ…Œ์ŠคํŠธ ํ”ฝ์Šค์ฒ˜๋กœ ๋ถ„๋ฆฌ**
    * `PlaceDetailViewModelTest`์— ์ค‘๋ณต ์ •์˜๋˜์–ด ์žˆ๋˜ `FAKE_MY_WAITING` ๊ฐ์ฒด๋ฅผ `PlaceDetailTestFixture.kt`๋กœ ์ด๋™ํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ์˜ ์ผ๊ด€์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **test: `WaitingRegisterViewModelTest` ๋‚ด ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๋ฐ ํ™˜๊ฒฝ ๊ฐœ์„ **
    * ๊ธฐ์กด์˜ ๋กœ์ปฌ `fakeMyWaiting` ๋Œ€์‹  ๊ณตํ†ต ํ”ฝ์Šค์ฒ˜์ธ `FAKE_MY_WAITING`์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ „ํ™”๋ฒˆํ˜ธ ๋ฏธ๋“ฑ๋ก ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ ์‹œ, ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์‹œ์ ๊ณผ์˜ ๋™๊ธฐํ™”๋ฅผ ์œ„ํ•ด `UnconfinedTestDispatcher`๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ํฌ๋งทํŒ… ์ •๋ฆฌ**
    * `Result.success` ๋ฐ `Result.failure` ๋ฐ˜ํ™˜ ๋ฌธ ๋“ฑ ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด ๊ฐœํ–‰ ๋ฐ ๋“ค์—ฌ์“ฐ๊ธฐ ์Šคํƒ€์ผ์„ ์ผ๊ด€๋˜๊ฒŒ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingRegisterViewModelTest`์—์„œ ๋ถˆํ•„์š”ํ•ด์ง„ ์ž„ํฌํŠธ ๊ตฌ๋ฌธ์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: getWaitingInfo๊ฐ€ ์‹คํŒจํ–ˆ์„ ๋•Œ๋„, ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋ฒ„๊ทธ ๊ฐœ์„ 

* **refactor: `WaitingRegisterViewModel` ๋‚ด ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ๋กœ์ง ๋ณด์™„**
    * `loadPlaceSummary` ์‹œ `waitingInfoRepository.getWaitingInfo()` ํ˜ธ์ถœ ๊ฒฐ๊ณผ์— ๋Œ€ํ•ด `onSuccess`์™€ `onFailure` ๋ธ”๋ก์„ ์‚ฌ์šฉํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์›จ์ดํŒ… ์ •๋ณด ์กฐํšŒ ์‹คํŒจ ์‹œ UI ์ƒํƒœ๋ฅผ `WaitingRegisterUiState.Error`๋กœ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด์˜ `getOrNull` ๊ธฐ๋ฐ˜ ์กฐ๊ฑด๋ฌธ์„ `onSuccess` ๋‚ด ํ๋ฆ„์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **style: ์ฝ”๋“œ ํฌ๋งทํŒ… ์ˆ˜์ •**
    * `MutableSharedFlow` ์„ ์–ธ๋ถ€ ๋ฐ `navigateToPhoneRegistrationEvent` ๋“ฑ ์ด๋ฒคํŠธ ๋ณ€์ˆ˜์˜ ์ค„๋ฐ”๊ฟˆ์„ ์กฐ์ •ํ•˜์—ฌ ์ฝ”๋“œ ์ปจ๋ฒค์…˜์„ ๋งž์ท„์Šต๋‹ˆ๋‹ค.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: '๋‚˜์˜ ์›จ์ดํŒ…' ํ™”๋ฉด ๋ฐ ํ™ˆ ์›จ์ดํŒ…๋ฐ” ๊ธฐ๋ณธ ๊ตฌ์กฐ ๊ตฌํ˜„

- MyWaitingViewModel/UiState/Screen ๋ฐ WaitingStatusCard ์ถ”๊ฐ€
- MyWaitingNavigation ๊ฒฝ๋กœ ๋“ฑ๋ก ๋ฐ FestabookRoute ์—ฐ๋™
- ํ™ˆ ์›จ์ดํŒ…๋ฐ”(HomeWaitingBar) UI ๋ฐ WaitingBarUiState ์ถ”๊ฐ€
- HomeViewModelยทHomeScreenยทMainScreen ์—ฐ๋™

* [Test] ์›จ์ดํŒ… ๋กœ์ปฌ/๋ฆฌ๋ชจํŠธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋ฐ HomeViewModel ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: ํ™ˆ ์›จ์ดํŒ…๋ฐ” ๋ฐ '๋‚˜์˜ ์›จ์ดํŒ…' ํ™”๋ฉด UI ๊ณ ๋„ํ™”

- ํ™ˆ ์›จ์ดํŒ…๋ฐ” ๋””์ž์ธ ๊ฐœ์„  (ic_waiting_bar ์ ์šฉ, ๋ ˆ์ด์•„์›ƒ ๊ตฌ์กฐ ๊ฐœ์„ )
- '๋‚˜์˜ ์›จ์ดํŒ…' ํ™”๋ฉด UI ๊ณ ๋„ํ™” (MyWaitingScreen, WaitingStatusCard) ๋ฐ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ๊ฐœ์„ 

* feat: ์›จ์ดํŒ… ๋“ฑ๋ก/์ทจ์†Œ ํ๋ฆ„ ํ†ตํ•ฉ ๋ฐ ๋‹ค์ด์–ผ๋กœ๊ทธ ๋ฒ”์šฉ์„ฑ ๊ฐœ์„ 

- ์›จ์ดํŒ… ๋“ฑ๋ก ์„ฑ๊ณต ๋ฐ ์ค‘๋ณต ์‹œ '๋‚˜์˜ ์›จ์ดํŒ…' ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋„ค๋น„๊ฒŒ์ด์…˜ ์—ฐ๊ฒฐ
- WaitingCancelConfirmDialog๋ฅผ ๋ฒ”์šฉ ์ปดํฌ๋„ŒํŠธ๋กœ ์ถ”์ถœํ•˜์—ฌ '๋‚˜์˜ ์›จ์ดํŒ…' ํ™”๋ฉด์— ์ ์šฉ

* refactor: ๋กœ์ปฌ ์ €์žฅ์†Œ ๊ธฐ๋ฐ˜ `placeId` ๊ด€๋ฆฌ ๋กœ์ง ์ œ๊ฑฐ ๋ฐ ์„œ๋ฒ„ ์‘๋‹ต ์—ฐ๋™

* **refactor: `placeId` ๊ด€๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ ๋ฐ ๊ด€๋ จ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ œ๊ฑฐ**
    * ์„œ๋ฒ„ ์‘๋‹ต(`MyWaitingResponse`)์— `placeId`๊ฐ€ ์ถ”๊ฐ€๋จ์— ๋”ฐ๋ผ, ๋กœ์ปฌ(`WaitingLocalDataSource`)์—์„œ `placeId`๋ฅผ ๋ณ„๋„๋กœ ์ €์žฅ, ์กฐํšŒ, ์‚ญ์ œํ•˜๋˜ ๋กœ์ง๊ณผ ๊ด€๋ จ ์ƒ์ˆ˜๋ฅผ ๋ชจ๋‘ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MyWaiting` ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฐ `MyWaitingUiState.Success`์—์„œ `placeId` ํƒ€์ž…์„ `Long?`์—์„œ ํ•„์ˆ˜ ๊ฐ’์ธ `Long`์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋ ˆ์ด์–ด ๋กœ์ง ์ตœ์ ํ™”**
    * `MyWaitingRepositoryImpl`: ์›จ์ดํŒ… ์ƒํƒœ ์ฒดํฌ ์‹œ ๋กœ์ปฌ `placeId`๋ฅผ ํด๋ฆฌ์–ดํ•˜๊ฑฐ๋‚˜ ๋ณ„๋„๋กœ ์กฐํšŒํ•˜์—ฌ ๋ณ‘ํ•ฉํ•˜๋˜ ๋กœ์ง์„ ์‚ญ์ œํ•˜๊ณ  ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋งŒ ์‚ฌ์šฉํ•˜๋„๋ก ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingRegisterInfoRepositoryImpl`: ์›จ์ดํŒ… ๋“ฑ๋ก ์„ฑ๊ณต ํ›„ ๋กœ์ปฌ์— `placeId`๋ฅผ ์ €์žฅํ•˜๋˜ ๊ณผ์ •์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: UI ๋ฐ ๋ทฐ๋ชจ๋ธ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๊ฐœ์„ **
    * `MyWaitingViewModel` & `MyWaitingScreen`: `placeId`๊ฐ€ ํ•ญ์ƒ ์กด์žฌํ•จ์— ๋”ฐ๋ผ ๊ด€๋ จ ๋„ ์ฒ˜๋ฆฌ(Null-safe) ๋กœ์ง์„ ์ œ๊ฑฐํ•˜๊ณ , ์žฅ์†Œ ์ƒ์„ธ ์ด๋™ ์‹œ UI ๋ชจ๋ธ์˜ `placeId`๋ฅผ ์ง์ ‘ ์ฐธ์กฐํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ฐ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋ฐ˜์˜**
    * `WaitingLocalDataSourceTest`: ์ œ๊ฑฐ๋œ `placeId` ์ €์žฅ/์กฐํšŒ/์‚ญ์ œ ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MyWaitingRepositoryImplTest` & `WaitingRegisterInfoRepositoryImplTest`: ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜๊ณ , ์„œ๋ฒ„ ์‘๋‹ต์—์„œ `placeId`๋ฅผ ์ง์ ‘ ๊ฒ€์ฆํ•˜๋„๋ก ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeViewModelTest`, `PlaceDetailViewModelTest` ๋“ฑ: ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ(Fixture)์— `placeId` ํ•„์ˆ˜ ๊ฐ’์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: '๋‚˜์˜ ์›จ์ดํŒ…' UiState ๋ชจ๋ธ ๋ถ„๋ฆฌ ๋ฐ ๋„๋ฉ”์ธ ๋ชจ๋ธ ์ •๋ฆฌ

- MyWaitingUiState.Success๋ฅผ MyWaitingUiModel๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์ƒํƒœ ๊ตฌ์กฐ ๋‹จ์ˆœํ™”
- ์›จ์ดํŒ… ๋“ฑ๋ก ์‹œ ์•Œ๋ฆผ ๊ถŒํ•œ ์š”์ฒญ ๋กœ์ง ์„ธ๋ถ„ํ™” (DENIED ์ƒํƒœ ์‹œ ์‹œ์Šคํ…œ ๊ถŒํ•œ ์š”์ฒญ)
- ์›จ์ดํŒ… ์ˆœ๋ฒˆ(Order) ๊ณ„์‚ฐ ๋กœ์ง ๊ฐœ์„  ๋ฐ MyWaiting ๋„๋ฉ”์ธ ๋ชจ๋ธ ์ˆ˜์ •

* feat: ํ™ˆ ํ™”๋ฉด ์›จ์ดํŒ… ๋ฐ” UI ์ƒํƒœ์— ์›จ์ดํŒ… ์ƒํƒœ(Status) ๋ฐ˜์˜

* **feat: `WaitingBarUiState` ๋ฐ `HomeWaitingBar` ๋‚ด ์›จ์ดํŒ… ์ƒํƒœ ์ •๋ณด ์ถ”๊ฐ€**
    * `WaitingBarUiState.Visible`: ํ˜„์žฌ ์›จ์ดํŒ…์˜ ์ง„ํ–‰ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” `status` ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeWaitingBar`: ์ „๋‹ฌ๋ฐ›์€ `status` ๊ฐ’์— ๋”ฐ๋ผ ํ…์ŠคํŠธ๋ฅผ ๋‹ค๋ฅด๊ฒŒ ํ‘œ์‹œํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ˜ธ์ถœ ์ƒํƒœ(`CALLED`)์ธ ๊ฒฝ์šฐ "์ง€๊ธˆ ์ž…์žฅํ•ด ์ฃผ์„ธ์š”" ๋ฉ”์‹œ์ง€๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

* **refactor: `HomeViewModel` ๋ฐ `HomeScreen` ์—ฐ๋™ ๋กœ์ง ์ˆ˜์ •**
    * `HomeViewModel`: ๋‚˜์˜ ์›จ์ดํŒ… ์ •๋ณด ์กฐํšŒ ์‹œ, ์‘๋‹ต ๋ชจ๋ธ์˜ `waitingStatus`๋ฅผ `WaitingBarUiState`์— ํฌํ•จํ•˜๋„๋ก ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeScreen`: `HomeWaitingBar` ํ˜ธ์ถœ ์‹œ ๋ทฐ๋ชจ๋ธ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์ƒํƒœ ๊ฐ’์„ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: FCM ์•Œ๋ฆผ ๊ธฐ๋ฐ˜ ๋”ฅ๋งํฌ ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ ๊ณ ๋„ํ™”

* **feat: FCM ๋ฉ”์‹œ์ง€ ํƒ€์ž… ๋ฐ ๋”ฅ๋งํฌ ์•ก์…˜ ๋ชจ๋ธ ๋„์ž…**
    * `FcmMessageType` ์—ด๊ฑฐํ˜•์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ณต์ง€์‚ฌํ•ญ, ์›จ์ดํŒ… ํ˜ธ์ถœ, ์›จ์ดํŒ… ๊ฑฐ์˜ ์™„๋ฃŒ, ์žฅ์†Œ ์ ‘๊ทผ ์ทจ์†Œ ๋“ฑ ์•Œ๋ฆผ ์œ ํ˜•์„ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FcmDeepLinkAction` ์‹ค๋“œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ๊ณต์ง€์‚ฌํ•ญ ์ƒ์„ธ(`OpenAnnouncement`), ๋‚˜์˜ ์›จ์ดํŒ…(`OpenMyWaiting`), ์žฅ์†Œ ์ƒ์„ธ(`OpenPlaceDetail`)๋กœ์˜ ์ด๋™ ์•ก์…˜์„ ๊ตฌ์กฐํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DeepLinkKeys`์— `KEY_TYPE`, `KEY_PLACE_ID` ๋“ฑ ์‹ ๊ทœ ํ‚ค ์ƒ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ์•Œ๋ฆผ ์ฑ„๋„ ๋ถ„๋ฆฌ ๋ฐ ํ™•์žฅ (Android)**
    * `NotificationHelper`: ๊ธฐ์กด ๊ณต์ง€์‚ฌํ•ญ ์ฑ„๋„ ์™ธ์— ์›จ์ดํŒ… ๊ด€๋ จ ์•Œ๋ฆผ์„ ์œ„ํ•œ `waiting_channel`์„ ์ถ”๊ฐ€๋กœ ์ƒ์„ฑํ•˜๊ณ  ์ค‘์š”๋„๋ฅผ ๋†’๊ฒŒ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์•Œ๋ฆผ ์ƒ์„ฑ ์‹œ `FcmDeepLinkAction`์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์•Œ๋ฆผ ์ฑ„๋„์„ ํ• ๋‹นํ•˜๊ณ , `Intent`์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ `Extra`๋กœ ํฌํ•จํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ํ”Œ๋žซํผ๋ณ„ ๋”ฅ๋งํฌ ํ•ธ๋“ค๋ง ๋กœ์ง ํ†ตํ•ฉ ๋ฐ ๊ฐœ์„ **
    * `RememberDeepLinkHandler`: ์ฝœ๋ฐฑ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋‹จ์ˆœ ID์—์„œ `FcmDeepLinkAction` ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **Android**: `MyFirebaseMessagingService`์—์„œ ์ˆ˜์‹ ๋œ ๋ฐ์ดํ„ฐ๋ฅผ `FcmDeepLinkAction`์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๊ณ , `Intent`์—์„œ ์•ก์…˜ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜์—ฌ ์†Œ๋น„ํ•˜๋Š” ๋กœ์ง์„ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **iOS**: `DefaultUserNotificationDelegate`์—์„œ `userInfo`๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ `FcmDeepLinkAction`์„ ํŒ๋ณ„ํ•˜์—ฌ `PendingFcmNotification`์— ์ €์žฅํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: `MainScreen` ๋ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋‚ด ๋”ฅ๋งํฌ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ**
    * `FestabookRoute.Main`: ํŽœ๋”ฉ ์ƒํƒœ์˜ ์žฅ์†Œ ์ƒ์„ธ ID(`pendingPlaceDetailId`)์™€ ๋‚˜์˜ ์›จ์ดํŒ… ์ด๋™ ์—ฌ๋ถ€(`pendingMyWaiting`) ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainScreen`: `RememberDeepLinkHandler`๋กœ๋ถ€ํ„ฐ ์ˆ˜์‹ ํ•œ ์•ก์…˜ ํƒ€์ž…์— ๋”ฐ๋ผ ๊ณต์ง€์‚ฌํ•ญ, ๋‚˜์˜ ์›จ์ดํŒ…, ์žฅ์†Œ ์ƒ์„ธ ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ถ„๊ธฐ ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ถ•์ œ(Festival) ์ •๋ณด๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ์—๋„ ํŽœ๋”ฉ๋œ ์•ก์…˜์„ ์œ ์ง€ํ•˜๋ฉฐ ๋ฉ”์ธ ํ™”๋ฉด์„ ์žฌ์ง„์ž…ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์›จ์ดํŒ… ๋“ฑ๋ก ์‹œ ์•Œ๋ฆผ ๊ถŒํ•œ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„  ๋ฐ Factory ํŒจํ„ด ์ ์šฉ

* **refactor: `NotificationPermissionManager` ์ƒ์„ฑ ๋ฐฉ์‹ ๋ณ€๊ฒฝ ๋ฐ ์˜์กด์„ฑ ์ฃผ์ž… ๊ตฌ์กฐ ๊ฐœ์„ **
    * `MainScreen` ๋ฐ `placeMapNavGraph`: `NotificationPermissionManager` ์ธ์Šคํ„ด์Šค ๋Œ€์‹  `NotificationPermissionManager.Factory`๋ฅผ ์ฃผ์ž…๋ฐ›๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingRegisterRoute`: `rememberNotificationPermissionManager`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ปดํฌ์ง€์…˜ ์ƒ๋ช…์ฃผ๊ธฐ์— ๋งž๊ฒŒ ๊ถŒํ•œ ๋งค๋‹ˆ์ €๋ฅผ ์ƒ์„ฑํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์›จ์ดํŒ… ๋“ฑ๋ก ํ™”๋ฉด(`WaitingRegisterRoute`) ๋‚ด ๊ถŒํ•œ ํš๋“ ์ฝœ๋ฐฑ ๋กœ์ง ๋ณด์™„**
    * ์•Œ๋ฆผ ๊ถŒํ•œ ํ—ˆ์šฉ ์‹œ(`onPermissionGrant`), ๋ฐ”ํ…€์‹œํŠธ๋ฅผ ๋‹ซ๊ณ  ์•Œ๋ฆผ ID๋ฅผ ์ €์žฅํ•œ ๋’ค ์ฆ‰์‹œ ์›จ์ดํŒ… ๋“ฑ๋ก(`submitWaitingRegister`)์ด ์ง„ํ–‰๋˜๋„๋ก ํ๋ฆ„์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `SettingViewModel`๊ณผ์˜ ์—ฐ๋™์„ ํ†ตํ•ด ๊ถŒํ•œ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ๋™๊ธฐํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* style: ์ฝ”๋“œ ํฌ๋งทํŒ… ์ •๋ ฌ ๋ฐ ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ์ œ๊ฑฐ

* **style: ์ „๋ฐ˜์ ์ธ ์ฝ”๋“œ ํฌ๋งทํŒ… ๋ฐ ๊ฐ€๋…์„ฑ ๊ฐœ์„ **
    * `WaitingRegisterInfoRepositoryImplTest`, `MyWaitingRepositoryImplTest`: ์ €์žฅ์†Œ ์ดˆ๊ธฐํ™” ๋ฐ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์‹œ์˜ ๋“ค์—ฌ์“ฐ๊ธฐ ์Šคํƒ€์ผ์„ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DefaultUserNotificationDelegate.kt` (iOS): ๋ณ€์ˆ˜ ํ• ๋‹น ๋ฐ ์ œ์–ด๋ฌธ ๋‚ด ์ค„๋ฐ”๊ฟˆ์„ ์ ์šฉํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.
    * `HomeWaitingBar.kt`, `MyWaitingUiState.kt`: `when` ์กฐ๊ฑด๋ฌธ ๋ฐ ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค ์„ ์–ธ๋ถ€์˜ ํฌ๋งท์„ ์ผ๊ด€๋˜๊ฒŒ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ž„ํฌํŠธ ๋ฐ ๋ฆฌ์†Œ์Šค ์ฐธ์กฐ ์ •๋ฆฌ**
    * `MyWaitingScreen.kt`: ๋ถˆํ•„์š”ํ•œ `border`, `RoundedCornerShape`, `Button` ๊ด€๋ จ ์ž„ํฌํŠธ์™€ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค(`waiting_register_submit`)๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MyWaitingNavigation.kt`: ๋„ค๋น„๊ฒŒ์ด์…˜ ์„ค์ • ๋‚ด์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `Modifier`, `PaddingValues` ๋“ฑ์˜ ์ž„ํฌํŠธ๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingLocalDataSourceTest.kt`: ํŒŒ์ผ ๋์˜ ๋ถˆํ•„์š”ํ•œ ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ๋‚˜์˜ ์›จ์ดํŒ… ์ทจ์†Œ ๋ฒ„ํŠผ ์ƒํƒœ ์—ฐ๋™ ๋ฐ UX ๊ฐœ์„ 

* **refactor: `MyWaitingScreen` ๋‚ด ์ทจ์†Œ ๋ฒ„ํŠผ์˜ ํ™œ์„ฑํ™” ์ƒํƒœ ๋กœ์ง ์ˆ˜์ •**
    * ์›จ์ดํŒ… ์ทจ์†Œ ์ค‘์ธ ์ƒํƒœ(`isCanceling`)๋ฅผ UI ์ƒํƒœ์™€ ์—ฐ๋™ํ•˜์—ฌ, ์ทจ์†Œ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ง„ํ–‰ ์ค‘์ผ ๋•Œ ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™”๋˜๊ณ  ๋กœ๋”ฉ ์ƒํƒœ๊ฐ€ ํ‘œ์‹œ๋˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด์— `true`/`false`๋กœ ๊ณ ์ •๋˜์–ด ์žˆ๋˜ `isEnabled`์™€ `isCancelling` ์†์„ฑ์„ `uiState.myWaiting.isCanceling` ๊ฐ’์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ๋ณ€ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `HomeViewModel` ์ดˆ๊ธฐํ™” ์‹œ `loadWaitingBar` ํ˜ธ์ถœ ์ œ๊ฑฐ

* **refactor: `HomeViewModel` ์ดˆ๊ธฐํ™” ๋กœ์ง ์ˆ˜์ •**
    * ๋ทฐ๋ชจ๋ธ ์ƒ์„ฑ ์‹œ์ ์— `loadWaitingBar()`๋ฅผ ์ž๋™์œผ๋กœ ํ˜ธ์ถœํ•˜๋˜ ๋กœ์ง์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์›จ์ดํŒ… ์ƒํƒœ ์นด๋“œ ์ž„๊ณ„๊ฐ’ ์ƒ์ˆ˜ํ™” ๋ฐ ํ™ˆ ์›จ์ดํŒ… ๋ฐ” ๊ทธ๋ฆผ์ž UI ์ˆ˜์ •

* **refactor: `WaitingStatusCard` ๋‚ด ๋Œ€๊ธฐ ์ˆœ๋ฒˆ ์ž„๊ณ„๊ฐ’ ์ƒ์ˆ˜ ๋„์ž…**
    * ๋งค์ง ๋„˜๋ฒ„ `3`์„ `WAITING_NEAR_THRESHOLD` ์ƒ์ˆ˜๋กœ ์ •์˜ํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋Œ€๊ธฐ ์ˆœ๋ฒˆ์— ๋”ฐ๋ฅธ ํ—ค๋“œ๋ผ์ธ ํ…์ŠคํŠธ(`waiting_far`, `waiting_near`) ๊ฒฐ์ • ๋กœ์ง์— ํ•ด๋‹น ์ƒ์ˆ˜๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: `HomeWaitingBar` ์ปดํฌ๋„ŒํŠธ Modifier ์ˆœ์„œ ์กฐ์ •**
    * `shadow` ์†์„ฑ์ด ๋ฐฐ๊ฒฝ ๋ฐ ํด๋ฆญ ์ด๋ฒคํŠธ๋ณด๋‹ค ๋จผ์ € ์ ์šฉ๋˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ UI ๊ทธ๋ฆผ์ž ๋ Œ๋”๋ง ์ˆœ์„œ๋ฅผ ์ตœ์ ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: Fastlane ๊ธฐ๋ฐ˜ Android ๋ฐ iOS CI/CD ๋ฐฐํฌ ์ž๋™ํ™” ํ™˜๊ฒฝ ๊ตฌ์ถ•

* **feat: Fastlane ์„ค์ • ํŒŒ์ผ(`Appfile`, `Fastfile`) ๋ฐ ์˜์กด์„ฑ ์ถ”๊ฐ€**
    * `Fastfile`: Android(Firebase App Distribution) ๋ฐ iOS(TestFlight) ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ ๋นŒ๋“œ/์—…๋กœ๋“œ lane์„ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `local.properties` ๊ธฐ๋ฐ˜์œผ๋กœ API ํ‚ค, ์ธ์ฆ์„œ ๊ฒฝ๋กœ, Slack ์›นํ›… ๋“ฑ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ๊ด€๋ฆฌํ•˜๋„๋ก ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `Appfile`: Android ํŒจํ‚ค์ง€๋ช…(Release/Debug) ์ •๋ณด๋ฅผ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `Gemfile`, `Gemfile.lock`: `fastlane`, `cocoapods`, `firebase_app_distribution` ํ”Œ๋Ÿฌ๊ทธ์ธ ์˜์กด์„ฑ์„ ๋ช…์‹œํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ๋ฐฐํฌ ์ƒํƒœ ์•Œ๋ฆผ ๋ฐ ๋ฒ„์ „ ๋™๊ธฐํ™” ๋กœ์ง ์ถ”๊ฐ€**
    * `notify_slack`: ๋นŒ๋“œ ๊ฒฐ๊ณผ(์„ฑ๊ณต/์‹คํŒจ), ํ”Œ๋žซํผ, ๋ฒ„์ „ ์ •๋ณด๋ฅผ Slack์œผ๋กœ ์ „์†กํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * iOS ๋นŒ๋“œ ์ „ `updateIosVersion` Gradle ํƒœ์Šคํฌ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ํ”„๋กœ์ ํŠธ ๋ฒ„์ „ ์ •๋ณด๋ฅผ ๋™๊ธฐํ™”ํ•˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: iOS ํ”„๋กœ์ ํŠธ ์„ค์ • ๋ณด์™„**
    * `iosApp.xcscheme`: CI ํ™˜๊ฒฝ์—์„œ ๋นŒ๋“œ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ณต์œ  ์Šคํ‚ค๋งˆ ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜๊ณ , ๋””๋ฒ„๊น…์„ ์œ„ํ•œ Firebase Analytics ์ธ์ž๊ฐ’์„ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: `.gitignore` ๋ฐ ๋ณด์•ˆ ์„ค์ • ์—…๋ฐ์ดํŠธ**
    * Fastlane ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ(`report.xml`, `build/` ๋“ฑ) ๋ฐ ๋ฏผ๊ฐํ•œ ํŒŒ์ผ(`*.p8`, `*.p12`, `*.mobileprovision`, `firebase-services.json`)์„ ๋ฌด์‹œ ๋ชฉ๋ก์— ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ณต์œ ๊ฐ€ ํ•„์š”ํ•œ iOS ์Šคํ‚ค๋งˆ ํŒŒ์ผ์€ git ์ถ”์  ๋Œ€์ƒ์— ํฌํ•จ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.

* chore: ์•ฑ ๋ฒ„์ „ ์ •๋ณด ์—…๋ฐ์ดํŠธ (2.1.0 โ†’ 2.1.1)

* **chore: iOS ํ”„๋กœ์ ํŠธ ๋ฒ„์ „ ๋ฐ ๋นŒ๋“œ ๋ฒˆํ˜ธ ์ƒํ–ฅ**
    * `Info.plist`: `CFBundleShortVersionString`์„ `2.1.1`๋กœ, `CFBundleVersion`์„ `20101`๋กœ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: Gradle ํ”„๋กœ์ ํŠธ ๋ฒ„์ „ ์ •๋ณด ์ƒํ–ฅ**
    * `gradle.properties`: `APP_VERSION_NAME`์„ `2.1.1`๋กœ, `APP_VERSION_CODE`๋ฅผ `20101`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
* feat: ํŽ˜์Šคํƒ€ํŒ… View ๊ตฌํ˜„

* refactor: ํ™ˆ ํ™”๋ฉด ๊ด€๋ จ UI ๋ชจ๋ธ ํŒจํ‚ค์ง€ ๊ตฌ์กฐ ๊ฐœ์„ 

* **refactor: UI ๋ชจ๋ธ ํด๋ž˜์Šค ํŒจํ‚ค์ง€ ์ด๋™**
    * `LineupItemUiModel`, `LineUpItemOfDayUiModel`, `LineUpItemGroupUiModel` ๋“ฑ ํ™ˆ ํ™”๋ฉด์—์„œ ์‚ฌ์šฉํ•˜๋Š” UI ๋ชจ๋ธ ํด๋ž˜์Šค๋“ค์„ `presentation.home` ํŒจํ‚ค์ง€์—์„œ ํ•˜์œ„ ํŒจํ‚ค์ง€์ธ `presentation.home.model`๋กœ ์ด๋™ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํŒจํ‚ค์ง€ ๋ณ€๊ฒฝ์— ๋”ฐ๋ผ `HomeViewModel`, `HomeScreen`, `HomeLineupItem` ๋ฐ ๊ด€๋ จ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ์ž„ํฌํŠธ ๊ฒฝ๋กœ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: LineupUiState ์˜์กด์„ฑ ์ˆ˜์ •**
    * `LineupUiState`์—์„œ ์ด๋™๋œ ํŒจํ‚ค์ง€์˜ `LineUpItemOfDayUiModel`์„ ์ฐธ์กฐํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ํ™ˆ ํ™”๋ฉด UI ๋ชจ๋ธ ๋„์ž… ๋ฐ ๋ฆฌํŒฉํ„ฐ๋ง

* **feat: Presentation ๊ณ„์ธต ์ „์šฉ UI ๋ชจ๋ธ ์ถ”๊ฐ€**
    * `FestivalPosterUiModel`, `FestivalUiModel`, `OrganizationUiModel`์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋„๋ฉ”์ธ ๋ชจ๋ธ๊ณผ UI ๊ณ„์ธต์˜ ์˜์กด์„ฑ์„ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋„๋ฉ”์ธ ๋ชจ๋ธ(`Poster`, `Festival`, `Organization`)์„ UI ๋ชจ๋ธ๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์œ„ํ•œ `toUiModel()` ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `HomeViewModel` ๋ฐ UI ์ƒํƒœ ๊ด€๋ฆฌ ๋กœ์ง ์ˆ˜์ •**
    * `FestivalUiState`์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ `Organization`์—์„œ `OrganizationUiModel`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeViewModel`์—์„œ ํŽ˜์Šคํ‹ฐ๋ฒŒ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ `toUiModel()`์„ ์‚ฌ์šฉํ•˜์—ฌ UI ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeViewModel` ๋‚ด์— `navigateToFestatingClick` ๋ฉ”์„œ๋“œ placeholder๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: UI ์ปดํฌ๋„ŒํŠธ ๋‚ด ๋„๋ฉ”์ธ ๋ชจ๋ธ ์˜์กด์„ฑ ์ œ๊ฑฐ**
    * `HomeScreen.kt` ๋ฐ `SettingScreen.kt`์—์„œ ๊ธฐ์กด ๋„๋ฉ”์ธ ๋ชจ๋ธ ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ์ƒˆ๋กญ๊ฒŒ ์ •์˜๋œ UI ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * Preview ์ฝ”๋“œ ๋‚ด ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ๋กœ์ง์„ `OrganizationUiModel` ๋“ฑ UI ๋ชจ๋ธ ๊ธฐ๋ฐ˜์œผ๋กœ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

* test: HomeViewModelTest ๋‚ด ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ ๋ฐ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ์ˆ˜์ •

* **refactor: `HomeViewModelTest` ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ ๋ณ€๊ฒฝ**
    * `com.daedan.festabook.home`์—์„œ `com.daedan.festabook.viewModel.home`์œผ๋กœ ํŒจํ‚ค์ง€ ์œ„์น˜๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: `HomeViewModelTest` ๋‚ด UI ๋ชจ๋ธ ๋ณ€ํ™˜ ๋กœ์ง ๋ฐ˜์˜**
    * ์ถ•์ œ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์—์„œ ์˜ˆ์ƒ ๊ฒฐ๊ณผ๊ฐ’์„ ๋„๋ฉ”์ธ ๋ชจ๋ธ(`FAKE_ORGANIZATION`) ๋Œ€์‹  UI ๋ชจ๋ธ(`FAKE_ORGANIZATION.toUiModel()`)์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํ•„์š”ํ•œ ํ”ฝ์Šค์ฒ˜(`FAKE_LINEUP`, `FAKE_ORGANIZATION`)์— ๋Œ€ํ•œ ์ž„ํฌํŠธ ๋ฌธ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์ถ•์ œ ๊ธฐ๊ฐ„ ํฌ๋งทํŒ… ๋กœ์ง ๊ฐœ์„ 

* **refactor: ์ถ•์ œ ๊ธฐ๊ฐ„ ํฌ๋งทํŒ… ๋กœ์ง ๋ฆฌํŒฉํ„ฐ๋ง**
    * `TextUtil.kt`์— ์ •์˜๋˜์–ด ์žˆ๋˜ `formatFestivalPeriod` ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋ฅผ ์‚ญ์ œํ•˜๊ณ , `HomeScreen.kt` ๋‚ด๋ถ€์— `@Composable` ํ•จ์ˆ˜๋กœ ์žฌ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ์กด์˜ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋‚ ์งœ ๋‹จ์œ„("๋…„", "์›”", "์ผ")๋ฅผ `strings.xml` ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์—ฌ ๋‹ค๊ตญ์–ด ๋Œ€์‘์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ ๋ฐ ํŒŒ์ผ ์ œ๊ฑฐ**
    * ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ธ `OnMenuItemReClickListener.kt` ํŒŒ์ผ์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `formatFestivalPeriod` ์ด๋™์— ๋”ฐ๋ผ ๋” ์ด์ƒ ํ•„์š” ์—†๋Š” `TextUtil.kt` ํŒŒ์ผ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค ์ถ”๊ฐ€**
    * ๋‚ ์งœ ํฌ๋งทํŒ…์„ ์œ„ํ•œ `format_festival_period_year` ๋ฐ `format_festival_period_date` ๋ฌธ์ž์—ด ๋ฆฌ์†Œ์Šค๋ฅผ `strings.xml`์— ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ํŽ˜์Šคํƒ€ํŒ…(Festating) ์›น๋ทฐ ํ™”๋ฉด ์ถ”๊ฐ€ ๋ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ์—ฐ๊ฒฐ

* **feat: ํŽ˜์Šคํƒ€ํŒ…(Festating) ํ™”๋ฉด ๊ตฌํ˜„ ๋ฐ ๋ผ์šฐํŒ… ์ถ”๊ฐ€**
    * `FestatingScreen`: `BuildKonfig.FESTA_TING_URL`์„ ๋กœ๋“œํ•˜๋Š” ์›น๋ทฐ ๊ธฐ๋ฐ˜์˜ ํ™”๋ฉด์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookRoute`: ์ƒˆ๋กœ์šด ๊ฒฝ๋กœ์ธ `Festating`์„ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestatingNavigation`: ํŽ˜์Šคํƒ€ํŒ… ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜๊ธฐ ์œ„ํ•œ `festatingNavGraph`๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookScreen`: ์ „์ฒด ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„์— `festatingNavGraph`๋ฅผ ํฌํ•จํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ํ™ˆ ํ™”๋ฉด ๋‚ด ํŽ˜์Šคํƒ€ํŒ… ํฌ์Šคํ„ฐ ์ปดํฌ๋„ŒํŠธ ๊ฐœ์„  ๋ฐ ์ด๋™ ๋กœ์ง ์—ฐ๊ฒฐ**
    * `FestatingPoster`๋ฅผ `presentation.home.component`์—์„œ `presentation.festating.component` ํŒจํ‚ค์ง€๋กœ ์ด๋™ํ•˜๊ณ , ํด๋ฆญ ์ด๋ฒคํŠธ๋ฅผ ์ƒ์œ„์—์„œ ์ „๋‹ฌ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeScreen` ๋ฐ `homeNavGraph`์— ํŽ˜์Šคํƒ€ํŒ… ํ™”๋ฉด ์ด๋™์„ ์œ„ํ•œ `onNavigateToFestating` ์ฝœ๋ฐฑ์„ ์ถ”๊ฐ€ํ•˜๊ณ , ํฌ์Šคํ„ฐ ํด๋ฆญ ์‹œ ํ•ด๋‹น ๋กœ์ง์ด ์‹คํ–‰๋˜๋„๋ก ์—ฐ๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeViewModel`์—์„œ ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `navigateToFestatingClick` ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ์›น๋ทฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€ ๋ฐ ๋นŒ๋“œ ํ™˜๊ฒฝ ์„ค์ •**
    * `build.gradle.kts`: `compose-webview-multiplatform` ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `BuildKonfig` ์„ค์ •์— `FESTA_TING_URL` ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , `local.properties`๋กœ๋ถ€ํ„ฐ ํ™˜๊ฒฝ๋ณ„(dev/release) URL์„ ๋กœ๋“œํ•˜๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainScreen`์˜ `homeNavGraph` ํ˜ธ์ถœ ์‹œ `viewModel` ํŒŒ๋ผ๋ฏธํ„ฐ ๋ช…์นญ์„ `homeViewModel`๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: Festating ํ™”๋ฉด์— ์ถ•์ œ ID ์ „๋‹ฌ ๋กœ์ง ๊ตฌํ˜„ ๋ฐ ViewModel ์ฃผ์ž… ๋ฐฉ์‹ ๊ฐœ์„ 

* **feat: Festating ๊ฒฝ๋กœ์— festivalId ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€**
    * `FestabookRoute.Festating`์„ `data object`์—์„œ `data class`๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ `festivalId: Long`์„ ์ „๋‹ฌ๋ฐ›๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeNavigation` ๋ฐ `HomeScreen` ๋‚ด ๋„ค๋น„๊ฒŒ์ด์…˜ ์ฝœ๋ฐฑ ํ•จ์ˆ˜๊ฐ€ `festivalId`๋ฅผ ์ธ์ž๋กœ ๋ฐ›๋„๋ก ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestatingPoster` ํด๋ฆญ ์‹œ ํ˜„์žฌ ์ถ•์ œ ์กฐ์ง์˜ ID๋ฅผ ์ „๋‹ฌํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: FestatingScreen ์›น๋ทฐ ์—ฐ๋™ ๊ฐ•ํ™”**
    * `FestatingScreen` ํ˜ธ์ถœ ์‹œ `festivalId`๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ์›น๋ทฐ ๋กœ๋“œ ์‹œ HTTP ํ—ค๋”(`festivalId`)์— ํฌํ•จํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `festatingNavGraph`์—์„œ `toRoute()`๋ฅผ ํ†ตํ•ด ๋„ค๋น„๊ฒŒ์ด์…˜ ์ธ์ž๋ฅผ ์ถ”์ถœํ•˜์—ฌ ํ™”๋ฉด์— ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: Explore ๋ฐ Home ๊ด€๋ จ ๋กœ์ง ๊ฐœ์„ **
    * `ExploreNavigation`์—์„œ `LocalMetroViewModelFactory`๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€์‹  `metroViewModel()` ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๊ฐ„์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeViewModel` ๋‚ด ์ถ•์ œ ์ •๋ณด ๋กœ๋“œ ์‹œ ๋ณ€์ˆ˜ ๋ช…์นญ์„ `festival`์—์„œ `organization`์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

* **style: ์ฝ”๋“œ ์ •๋ฆฌ**
    * `MainScreen` ๋‚ด ๋ถˆํ•„์š”ํ•œ `Unit` ๋ฐ˜ํ™˜ ๋กœ์ง์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ์ƒ์ˆ˜ํ™”

* chore: `compose-webview-multiplatform` ์˜์กด์„ฑ ๊ด€๋ฆฌ ๋ฐฉ์‹ ๊ฐœ์„ 

* **chore: `libs.versions.toml` ๋‚ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€ ๋ฐ ์ •๋ฆฌ**
    * `composeWebviewMultiplatform` ๋ฒ„์ „์„ ์ •์˜ํ•˜๊ณ  `compose-webview-multiplatform` ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `firebaseCrashlytcis` ํ”Œ๋Ÿฌ๊ทธ์ธ ์„ ์–ธ๋ถ€์˜ ๋ถˆํ•„์š”ํ•œ ๊ณต๋ฐฑ์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `build.gradle.kts` ๋‚ด ์˜์กด์„ฑ ์ฐธ์กฐ ๋ฐฉ์‹ ๋ณ€๊ฒฝ**
    * `composeApp/build.gradle.kts`์—์„œ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด๋กœ ๊ด€๋ฆฌํ•˜๋˜ `compose-webview-multiplatform` ์˜์กด์„ฑ์„ `libs.versions.toml` ๋‚ด์˜ Version Catalog ์ฐธ์กฐ ๋ฐฉ์‹์œผ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: FestabookTopAppBar ๋‚ด๋น„๊ฒŒ์ด์…˜ ์•„์ด์ฝ˜ ์ถ”๊ฐ€ ๋ฐ ๊ตฌ์กฐ ๊ฐœ์„ 

* **refactor: `FestabookTopAppBar` ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ๋ณ€๊ฒฝ**
    * ๊ธฐ์กด `Text` ๋‹จ์ผ ์ปดํฌ๋„ŒํŠธ์—์„œ `Row` ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋‚ด๋น„๊ฒŒ์ด์…˜ ์•„์ด์ฝ˜๊ณผ ์ œ๋ชฉ์„ ๊ฐ€๋กœ๋กœ ๋ฐฐ์น˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `navigationIcon` ์Šฌ๋กฏ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋’ค๋กœ ๊ฐ€๊ธฐ ๋ฒ„ํŠผ ๋“ฑ ์ปค์Šคํ…€ ์•„์ด์ฝ˜์„ ์‚ฝ์ž…ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ธฐ๋ณธ ๋ฐฐ๊ฒฝ์ƒ‰์„ `FestabookColor.white`๋กœ ์„ค์ •ํ•˜๊ณ  ์•„์ดํ…œ ์ˆ˜์ง ์ •๋ ฌ ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ํ”„๋ฆฌ๋ทฐ ์ฝ”๋“œ ์—…๋ฐ์ดํŠธ**
    * `FestabookTopAppBarPreview`์— `ic_arrow_back` ์•„์ด์ฝ˜์„ ํฌํ•จํ•œ ์ƒ˜ํ”Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ณ€๊ฒฝ๋œ ๋ ˆ์ด์•„์›ƒ์„ ํ™•์ธํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* [Feat] FestatingScreen ๋‚ด ์›น๋ทฐ ๋’ค๋กœ๊ฐ€๊ธฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* **feat: ์›น๋ทฐ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฐฑ ํ•ธ๋“ค๋ง ์ถ”๊ฐ€**
    * `NavigationBackHandler`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์›น๋ทฐ ๋‚ด์—์„œ ๋’ค๋กœ ์ด๋™์ด ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ(`navigator.canGoBack`), ์‹œ์Šคํ…œ ๋’ค๋กœ๊ฐ€๊ธฐ ๋™์ž‘ ์‹œ ์›น๋ทฐ์˜ ์ด์ „ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋„๋ก ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋’ค๋กœ๊ฐ€๊ธฐ ์ด๋ฒคํŠธ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด `rememberNavigationEventState`๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์›น๋ทฐ ์ƒํƒœ ๋ณ€์ˆ˜๋ช… ๋ณ€๊ฒฝ**
    * ๊ธฐ์กด `state`๋กœ ์ •์˜๋˜์—ˆ๋˜ ์›น๋ทฐ ์ƒํƒœ ๋ณ€์ˆ˜๋ฅผ `webViewState`๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์šฉ๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: WebView ์ƒํƒœ ์ƒ์„ฑ ๋กœ์ง ๋ถ„๋ฆฌ ๋ฐ DOM Storage ํ™œ์„ฑํ™”

* **refactor: WebView ์„ค์ • ๋กœ์ง ์บก์Аํ™” ๋ฐ Android ํ™˜๊ฒฝ ์ตœ์ ํ™”**
    * `rememberConfiguredWebViewState` ์ปดํฌ์ €๋ธ”์„ ์ถ”๊ฐ€ํ•˜์—ฌ `WebViewState` ์ƒ์„ฑ ๋ฐ ์ดˆ๊ธฐ ์„ค์ • ๋กœ์ง์„ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `LaunchedEffect`๋ฅผ ํ†ตํ•ด Android WebView์˜ `domStorageEnabled` ์„ค์ •์„ `true`๋กœ ํ™œ์„ฑํ™”ํ•˜์—ฌ ์›น ์ฝ˜ํ…์ธ  ๋ Œ๋”๋ง ํ˜ธํ™˜์„ฑ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestatingScreen` ๋‚ด `WebView` ์ปดํฌ๋„ŒํŠธ์˜ `Modifier` ์ฝ”๋“œ ์Šคํƒ€์ผ์„ ๊ฐ€๋…์„ฑ ์žˆ๊ฒŒ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์ง๋ ฌํ™” ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€

* refactor: `rememberConfiguredWebViewState` ๋‚ด WebView ์„ค์ • ๋กœ์ง ๊ฐ„์†Œํ™”

* **refactor: WebViewState ์„ค์ • ๋ฐฉ์‹ ๋ณ€๊ฒฝ**
    * `rememberConfiguredWebViewState` ํ•จ์ˆ˜ ๋‚ด์—์„œ `LaunchedEffect`๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , `apply` ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ `WebViewState` ์ƒ์„ฑ ์‹œ์ ์— `domStorageEnabled` ์„ค์ •์„ ์ง์ ‘ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: WebView ๋‚ด DOM storage ์„ค์ • ๋กœ์ง ๊ฐœ์„ 

* **refactor: `rememberConfiguredWebViewState` ๋‚ด side-effect ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์ˆ˜์ •**
    * `WebViewState` ์ดˆ๊ธฐํ™” ์‹œ `apply`๋ฅผ ํ†ตํ•ด ์ง์ ‘ ์„ค์ •ํ•˜๋˜ ๋ฐฉ์‹์„ `remember(state)` ๋ธ”๋ก์œผ๋กœ ๊ฐ์‹ธ๋„๋ก ๋ณ€๊ฒฝํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ์žฌ์„ค์ •์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์ตœ์ ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: FestabookTopAppBar ๋ฐฐ๊ฒฝ์ƒ‰ ์ ์šฉ ๋กœ์ง ์ˆ˜์ •

* **refactor: `FestabookTopAppBar` ๋‚ด๋ถ€ ๋ฐฐ๊ฒฝ์ƒ‰ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ**
    * `modifier` ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ๊ธฐ๋ณธ๊ฐ’์—์„œ `background(FestabookColor.white)`๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , ๋‚ด๋ถ€ `Row` ์ปดํฌ๋„ŒํŠธ์˜ `modifier`์— ์ง์ ‘ ์ ์šฉํ•˜์—ฌ UI ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookTopAppBarPreview`์˜ ๋“ค์—ฌ์“ฐ๊ธฐ ๋ฐ ์ฝ”๋“œ ํฌ๋งทํŒ…์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* style: ์ฝ”๋“œ ํฌ๋งทํŒ…

* refactor: `FestabookNavigator` ๋‚ด ๋„ค๋น„๊ฒŒ์ด์…˜ ์ƒํƒœ ๊ด€์ฐฐ ๋ฐฉ์‹ ๊ฐœ์„ 

* **refactor: `collectAsStateWithLifecycle` ์ ์šฉ**
    * `FestabookNavigator`์—์„œ `navController.visibleEntries`๋ฅผ ๊ตฌ๋…ํ•  ๋•Œ, ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ณ ๋ คํ•œ ์•ˆ์ „ํ•œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘์„ ์œ„ํ•ด `collectAsState()` ๋Œ€์‹  `collectAsStateWithLifecycle()`์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
fix: ์›จ์ดํŒ… ๋“ฑ๋ก ์‹œ ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ ๋ฏธ๋“ฑ๋ก ์‹œ ํ™”๋ฉด ์ด๋™ ๋ฒ„๊ทธ ์ˆ˜์ •

* **refactor: `WaitingRegisterViewModel` ๋‚ด ํœด๋Œ€ํฐ ๋“ฑ๋ก ๋„ค๋น„๊ฒŒ์ด์…˜ ๋กœ์ง ์ˆ˜์ •**
    * ๊ธฐ์กด์˜ `navigateToPhoneRegistrationEvent` (SharedFlow)๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  `WaitingRegisterUiState.NeedsPhoneRegistration` ์ƒํƒœ๋ฅผ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `loadPlaceSummary` ์‹œ ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด `NeedsPhoneRegistration` ์ƒํƒœ๋กœ ์ „์ดํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `waitingInfoRepository.getWaitingInfo()` ํ˜ธ์ถœ ์‹คํŒจ ์‹œ ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ์„ค์ •ํ•˜๊ณ  ์ดํ›„ ๋กœ์ง์„ ์ค‘๋‹จํ•˜๋„๋ก ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๋ณด์™„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: `WaitingRegisterUiState` ๋‚ด ์‹ ๊ทœ ์ƒํƒœ ์ถ”๊ฐ€**
    * ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ ๋“ฑ๋ก์ด ํ•„์š”ํ•œ ์ƒํƒœ๋ฅผ ๋ช…์‹œํ•˜๊ธฐ ์œ„ํ•ด `NeedsPhoneRegistration` ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `WaitingRegisterScreen` ๋‚ด UI ์ƒํƒœ ๋Œ€์‘ ๋ฐ ๋กœ์ง ์ˆ˜์ •**
    * `LaunchedEffect`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ `uiState`๊ฐ€ `NeedsPhoneRegistration`์ผ ๋•Œ ํœด๋Œ€ํฐ ๋“ฑ๋ก ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋กœ๋”ฉ ํ™”๋ฉด ํ‘œ์‹œ ์กฐ๊ฑด์— `NeedsPhoneRegistration` ์ƒํƒœ๋ฅผ ํฌํ•จํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: `PlaceMapNavigation` ๋‚ด ๋ถˆํ•„์š”ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฝ”๋“œ ์ œ๊ฑฐ**
    * `FestabookRoute.PlaceDetail` ๊ฒฝ๋กœ์˜ ์ค‘๋ณต๋˜๊ฑฐ๋‚˜ ๋ถˆํ•„์š”ํ•œ `enterTransition` ๋ฐ `exitTransition` ์„ค์ •์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: `WaitingRegisterViewModelTest` ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ์ถ”๊ฐ€**
    * `createViewModel` ํ—ฌํผ ํ•จ์ˆ˜๋ฅผ ๋„์ž…ํ•˜์—ฌ ๋ทฐ๋ชจ๋ธ ์ƒ์„ฑ ๋กœ์ง์„ ์บก์Аํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * "์ •๋ณด ์กฐํšŒ ์‹คํŒจ ์‹œ ์—๋Ÿฌ ์ƒํƒœ ํ™•์ธ ๋ฐ ์ถ”๊ฐ€ API ํ˜ธ์ถœ ์ฐจ๋‹จ"์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฐฉ์‹์—์„œ UI ์ƒํƒœ ๊ธฐ๋ฐ˜ ํ™•์ธ ๋ฐฉ์‹์œผ๋กœ ํ…Œ์ŠคํŠธ ๋กœ์ง์„ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
parkjiminnnn and others added 8 commits April 30, 2026 22:50
* chore: release ๋นŒ๋“œ ์‹œ Proguard ๋ฐ ๋ฆฌ์†Œ์Šค ์ถ•์†Œ ํ™œ์„ฑํ™”

* **chore: release ๋นŒ๋“œ ์ตœ์ ํ™” ์„ค์ • ์ ์šฉ**
    * `composeApp/build.gradle.kts` ๋‚ด release ๋นŒ๋“œ ๋ธ”๋ก์—์„œ ์ฃผ์„ ์ฒ˜๋ฆฌ๋˜์–ด ์žˆ๋˜ `isMinifyEnabled`, `isShrinkResources`, `proguardFiles` ์„ค์ •์„ ํ™œ์„ฑํ™”ํ•˜์—ฌ ์ฝ”๋“œ ๋‚œ๋…ํ™” ๋ฐ ๋ฆฌ์†Œ์Šค ์ตœ์ ํ™”๊ฐ€ ์ด๋ฃจ์–ด์ง€๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: CoroutineScope DI ๋ฐ”์ธ๋”ฉ ๋ฐ ํ•œ์ •์ž ์ถ”๊ฐ€

* **feat: Coroutine ์ „์šฉ DI ๋ฐ”์ธ๋”ฉ ๋ฐ Qualifier ์ถ”๊ฐ€**
    * `CoroutineBindings`๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ `IO`, `Main`, `Default` ๋””์ŠคํŒจ์ฒ˜ ๊ธฐ๋ฐ˜์˜ `CoroutineScope`๋ฅผ `AppScope` ์ˆ˜์ค€์—์„œ ์ œ๊ณตํ•˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `@IO`, `@Main`, `@Default` ํ•œ์ •์ž(Qualifier) ์–ด๋…ธํ…Œ์ด์…˜์„ ์ •์˜ํ•˜์—ฌ ๋ชฉ์ ์— ๋งž๋Š” ์Šค์ฝ”ํ”„ ์ฃผ์ž…์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: CoroutineScope ์ฃผ์ž… ๋ฐฉ์‹ ๊ณ ๋„ํ™”**
    * `DeviceRepositoryImpl`, `DefaultUserNotificationDelegate`, `DefaultFirebaseMessagingDelegate` ๋“ฑ ์ฃผ์š” ํด๋ž˜์Šค์—์„œ `@IO` ํ•œ์ •์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŠน์ • `CoroutineScope`๋ฅผ ์ฃผ์ž…๋ฐ›๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookApp.kt` ๋‚ด `applicationScope`์— `@IO` ์–ด๋…ธํ…Œ์ด์…˜์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AndroidAppGraph` ๋ฐ `MyFirebaseMessagingService`์—์„œ ์‚ฌ์šฉ๋˜๋˜ ๊ธฐ์กด `ioCoroutineScope` ๋ช…์นญ์„ ํ•œ์ •์ž๊ฐ€ ์ ์šฉ๋œ `coroutineScope`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๊ธฐํƒ€ ์ฝ”๋“œ ์ •๋ฆฌ**
    * `FestabookAppGraph`์— ์ง์ ‘ ์ •์˜๋˜์–ด ์žˆ๋˜ `CoroutineScope` ์ œ๊ณต ๋กœ์ง์„ ์ œ๊ฑฐํ•˜๊ณ  `CoroutineBindings`๋กœ ์ด๊ด€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DefaultUserNotificationDelegate` ๋‚ด ๋ถˆํ•„์š”ํ•œ ๊ณต๋ฐฑ ์ˆ˜์ • ๋ฐ ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* [Build] iOS ๋นŒ๋“œ ์„ค์ • ๋‚ด ์‹ฌ๋ณผ ์ŠคํŠธ๋ฆฝ(Strip) ์˜ต์…˜ ์ˆ˜์ •

* **build: Release ๋ฐ Debug ํ™˜๊ฒฝ๋ณ„ ๋ฐ”์ด๋„ˆ๋ฆฌ ์ตœ์ ํ™” ์„ค์ • ๋ณ€๊ฒฝ**
    * `project.pbxproj`: Release ๋นŒ๋“œ ์‹œ ๋ฐ”์ด๋„ˆ๋ฆฌ ํฌ๊ธฐ ์ตœ์ ํ™” ๋ฐ ๋ณด์•ˆ์„ ์œ„ํ•ด `COPY_PHASE_STRIP`, `DEPLOYMENT_POSTPROCESSING`, `STRIP_INSTALLED_PRODUCT` ์˜ต์…˜์„ `YES`๋กœ ํ™œ์„ฑํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `project.pbxproj`: Debug ๋นŒ๋“œ ์‹œ ๋””๋ฒ„๊น… ํŽธ์˜๋ฅผ ์œ„ํ•ด `STRIP_INSTALLED_PRODUCT`๋ฅผ `NO`๋กœ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
* refactor: ์—๋Ÿฌ ์ƒํƒœ ๋ฐ ๋นˆ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ์ ์šฉ ๋ฐ ๊ฐœ์„ 

* **refactor: `ErrorStateScreen` ๋„์ž…์„ ํ†ตํ•œ ์—๋Ÿฌ ํ™”๋ฉด ์ฒ˜๋ฆฌ ํ†ต์ผ**
    * `HomeScreen`, `PlaceListScreen`, `PlaceDetailScreen`์—์„œ ๊ธฐ์กด์— ๊ฐœ๋ณ„์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜ `EmptyStateScreen`์„ ์˜ค์šฉํ•˜๋˜ ์—๋Ÿฌ ์ƒํƒœ UI๋ฅผ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ์ธ `ErrorStateScreen`์œผ๋กœ ๊ต์ฒดํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeScreen`์˜ ๊ฒฝ์šฐ, ์ธ๋ผ์ธ์œผ๋กœ ์ž‘์„ฑ๋œ ํ…์ŠคํŠธ ์ถœ๋ ฅ ๋กœ์ง์„ ์ œ๊ฑฐํ•˜๊ณ  ๊ณตํ†ต ์—๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๊ตฌ์กฐ๋ฅผ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `PlaceListScreen` ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง ์ˆ˜์ •**
    * ๋ฆฌ์ŠคํŠธ ๋กœ๋“œ ์‹คํŒจ ์‹œ(`ListLoadState.Error`) ๊ธฐ์กด์— ๋นˆ ํ™”๋ฉด(`EmptyStateScreen`)์„ ๋ณด์—ฌ์ฃผ๋˜ ๋กœ์ง์„ ์—๋Ÿฌ ํ™”๋ฉด(`ErrorStateScreen`)์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ƒํƒœ ์˜๋ฏธ์— ๋งž๊ฒŒ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: `LostItemScreen` ๋‚ด ๋นˆ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ๊ฐœ์„ **
    * ๋ถ„์‹ค๋ฌผ ๋ชฉ๋ก์ด ์—†์„ ๋•Œ ํ‘œ์‹œ๋˜๋Š” `EmptyStateScreen`์— `verticalScroll` ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ, ์ž‘์€ ํ™”๋ฉด ๊ธฐ๊ธฐ์—์„œ๋„ ๋ ˆ์ด์•„์›ƒ ์œ ์—ฐ์„ฑ์„ ํ™•๋ณดํ•˜๊ณ  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: ๋ถ„์‹ค๋ฌผ์˜ ๊ฒฝ์šฐ์—๋„ Result๋ฅผ ๋ฐ›์•„์˜ค๋„๋ก ๋ณ€๊ฒฝ

* **refactor: `LostItemRepository`์˜ ๋ถ„์‹ค๋ฌผ ์กฐํšŒ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ `Result`๋กœ ๋ณ€๊ฒฝ**
    * ๋น„์ •์ƒ์ ์ธ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ด๋‚˜ ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ ์ƒํ™ฉ์— ๋Œ€์‘ํ•˜๊ธฐ ์œ„ํ•ด `getLost()`์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ `List<Lost?>`์—์„œ `Result<List<Lost?>>`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `LostItemRepositoryImpl`์—์„œ `async`๋ฅผ ์‚ฌ์šฉํ•ด ๋ณ‘๋ ฌ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ, `getOrElse`๋ฅผ ํ™œ์šฉํ•œ ์กฐ๊ธฐ ๋ฐ˜ํ™˜(Early Return) ๋กœ์ง์„ ์ ์šฉํ•˜์—ฌ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ•ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `mutableListOf`๋ฅผ `buildList`๋กœ ๊ต์ฒดํ•˜์—ฌ ๋ถˆ๋ณ€์„ฑ์„ ํ™•๋ณดํ•˜๊ณ  ๊ฐ€์ด๋“œ ํ•ญ๋ชฉ๊ณผ ๋ถ„์‹ค๋ฌผ ํ•ญ๋ชฉ์„ ๊ฒฐํ•ฉํ•˜๋Š” ๋กœ์ง์„ ๊ฐ€๋…์„ฑ ์žˆ๊ฒŒ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `NewsViewModel` ๋‚ด ๋ถ„์‹ค๋ฌผ ๋กœ๋“œ ๋กœ์ง ๊ฐœ์„ **
    * ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์˜ ๋ฐ˜ํ™˜ ํƒ€์ž… ๋ณ€๊ฒฝ์— ๋งž์ถฐ `onSuccess`์™€ `onFailure` ๋ธ”๋ก์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ ์‹œ `LostUiState.Content.Error`๋ฅผ ์ƒํƒœ์— ๋ฐ˜์˜ํ•˜์—ฌ UI์—์„œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ •**
    * `FestivalRemoteDataSourceTest`: `FestivalResponse` ํ•„๋“œ ํ™•์žฅ์— ๋”ฐ๋ผ `organizationId`, `festivalSponsors`, `instagramLink` ๋“ฑ์˜ ๋ˆ„๋ฝ๋œ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ…Œ์ŠคํŠธ ํ”ฝ์Šค์ฒ˜์— ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `NewsViewModelTest`: `lostItemRepository.getLost()`๊ฐ€ `Result.success`๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก Mocking ๋กœ์ง์„ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `HomeViewModelTest`: ๋ถˆํ•„์š”ํ•œ import ๋ฌธ์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
feat: `NaverMapContent` ๊ฐ€์‹œ์„ฑ ์ œ์–ด ๋กœ์ง ๊ตฌํ˜„ ๋ฐ iOS ๋งต ๋ทฐ ์ตœ์ ํ™”

* **feat: `NaverMapContent` ๊ฐ€์‹œ์„ฑ ์ œ์–ด๋ฅผ ์œ„ํ•œ `isVisible` ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€**
    * `NaverMapContent`, `PlaceMapScreen`, `PlaceMapContent`์— `isVisible` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ง€๋„ ์ปดํฌ๋„ŒํŠธ์˜ ๋…ธ์ถœ ์—ฌ๋ถ€๋ฅผ ์™ธ๋ถ€์—์„œ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainScreen`์—์„œ ํ•ด๋‹น ์ƒํƒœ๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ์ง€๋„์˜ ๋ Œ๋”๋ง ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

* **refactor: iOS ํ™˜๊ฒฝ์—์„œ ์ง€๋„ ๋ทฐ์˜ ๊ฐ€์‹œ์„ฑ ์ฒ˜๋ฆฌ ์ตœ์ ํ™”**
    * iOS์˜ `UIKitView` ๋‚ด `update` ๋ธ”๋ก์„ ํ™œ์šฉํ•˜์—ฌ `isVisible` ์ƒํƒœ์— ๋”ฐ๋ผ `mapView.hidden` ์†์„ฑ์ด ๋™์ ์œผ๋กœ ๋ณ€๊ฒฝ๋˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ด๋ฅผ ํ†ตํ•ด ํ™”๋ฉด ์ „ํ™˜์ด๋‚˜ ํƒญ ๋ณ€๊ฒฝ ์‹œ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค ์†Œ๋ชจ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ์‹œ๊ฐ์  ์ƒํƒœ๋ฅผ ์ •ํ™•ํžˆ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค.

* **refactor: ํ”Œ๋žซํผ๋ณ„ ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •**
    * `commonMain`์˜ `NaverMapContent` ์ธํ„ฐํŽ˜์ด์Šค์— `isVisible` ๊ธฐ๋ณธ๊ฐ’(`true`)์„ ์„ค์ •ํ•˜์—ฌ ๊ธฐ์กด ํ˜ธ์ถœ๋ถ€์™€์˜ ํ˜ธํ™˜์„ฑ์„ ์œ ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * Android ๋ฐ iOS์˜ ์‹ค์ œ ๊ตฌํ˜„์ฒด์—์„œ ํ•ด๋‹น ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ˆ˜์šฉํ•˜๋„๋ก ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
refactor: `WaitingRegister` ํ™”๋ฉด ์ง„์ž… ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ๊ฑฐ ๋ฐ ๋กœ๋”ฉ UI ๊ตฌ์กฐ ์ตœ์ ํ™”

* **refactor: `WaitingRegister` ๊ฒฝ๋กœ์˜ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ๊ฑฐ**
    * `PlaceMapNavigation` ๋‚ด `WaitingRegister` ํ™”๋ฉด ์ด๋™ ์‹œ ์„ค์ •๋˜์—ˆ๋˜ ์ปค์Šคํ…€ `enterTransition` ๋ฐ `exitTransition`(์Šฌ๋ผ์ด๋“œ ๋ฐ ํŽ˜์ด๋“œ ํšจ๊ณผ)์„ ์ œ๊ฑฐํ•˜์—ฌ ๊ธฐ๋ณธ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ๋กœ๋”ฉ ์ƒํƒœ์˜ ๋ถˆํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ ๋ฐ ๋ ˆ์ด์•„์›ƒ ์ œ๊ฑฐ**
    * `WaitingRegisterScreen`์—์„œ `Loading` ๋ฐ `NeedsPhoneRegistration` ์ƒํƒœ์ผ ๋•Œ ํ‘œ์‹œํ•˜๋˜ `LoadingStateScreen`๊ณผ ์ด๋ฅผ ๊ฐ์‹ธ๋˜ `Box` ๋ ˆ์ด์•„์›ƒ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํ•ด๋‹น ์ƒํƒœ์—์„œ ๋ณ„๋„์˜ UI ๋…ธ์ถœ ์—†์ด ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์—ฌ ํ™”๋ฉด ๊นœ๋นก์ž„์ด๋‚˜ ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง์„ ๋ฐฉ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ์ฝ”๋“œ ํฌ๋งทํŒ… ์ˆ˜์ •**
    * `isSubmitting` ๋ณ€์ˆ˜ ์„ ์–ธ๋ถ€์˜ ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด ์ค„๋ฐ”๊ฟˆ ๋ฐ ๋“ค์—ฌ์“ฐ๊ธฐ๋ฅผ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
refactor: ์žฅ์†Œ ์ง€๋„ ๋งˆ์ปค ๋กœ๋“œ ํƒ€์ž„์•„์›ƒ ์ฒ˜๋ฆฌ ๊ฐ•ํ™” ๋ฐ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์กฐ์ •

* **refactor: `PlaceMapUiState.await` ๊ธฐ๋ณธ ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„ ์—ฐ์žฅ**
    * ๋„คํŠธ์›Œํฌ ์ง€์—ฐ ๋“ฑ ๋‹ค์–‘ํ•œ ํ†ต์‹  ํ™˜๊ฒฝ์„ ๊ณ ๋ คํ•˜์—ฌ `await` ํ•จ์ˆ˜์˜ ๊ธฐ๋ณธ ํƒ€์ž„์•„์›ƒ ๊ฐ’์„ `3.seconds`์—์„œ `5.seconds`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ๋งˆ์ปค ๋ฐ์ดํ„ฐ ๋กœ๋“œ ํƒ€์ž„์•„์›ƒ ์‹œ ์—๋Ÿฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง ์ถ”๊ฐ€**
    * `PlaceMapViewModel`์—์„œ ๋งˆ์ปค ์ •๋ณด(`placeGeographies`)๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ ํƒ€์ž„์•„์›ƒ์ด ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ, `_uiState`๋ฅผ `LoadState.Error`๋กœ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํƒ€์ž„์•„์›ƒ ๋ฐœ์ƒ ์‹œ "์„œ๋ฒ„์—์„œ ๋งˆ์ปค ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."๋ผ๋Š” ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ƒํƒœ์— ๋ฐ˜์˜ํ•˜์—ฌ ์˜ˆ์™ธ ์ƒํ™ฉ์— ๋Œ€ํ•œ UI ๋Œ€์‘์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
refactor: `PlaceMapSideEffectHandler` ๋‚ด ๋ฉ”๋‰ด ์•„์ดํ…œ ์žฌํด๋ฆญ ์‹œ ๋™์ž‘ ๋กœ์ง ๊ฐœ์„ 

* **refactor: `MenuItemReClicked` ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ณ ๋„ํ™”**
    * ๋ฉ”๋‰ด ์•„์ดํ…œ ์žฌํด๋ฆญ ์‹œ, ํ•˜๋‹จ ์‹œํŠธ(`bottomSheetState`)๊ฐ€ ์™„์ „ํžˆ ํŽผ์ณ์ง„ ์ƒํƒœ(`EXPANDED`)๋ผ๋ฉด ์ ˆ๋ฐ˜ ํŽผ์นจ ์ƒํƒœ(`HALF_EXPANDED`)๋กœ ๋ณ€๊ฒฝํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์žฅ์†Œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ(`isPreviewVisible`)์—๋งŒ ์žฅ์†Œ ์„ ํƒ ํ•ด์ œ(`UnSelectPlace`) ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋„๋ก ๋กœ์ง์„ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ถˆํ•„์š”ํ•œ ์ฃผ์„ ์ฒ˜๋ฆฌ๋œ ๋กœ๊ฑฐ ๊ด€๋ จ ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
* build: iOS Firebase Crashlytics ์„ค์ • ๊ฐœ์„  ๋ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ตฌ์กฐ ์ตœ์ ํ™”

* **build: iOS Firebase Crashlytics dSYM ์ž๋™ ์—…๋กœ๋“œ ๋ฐ ๋นŒ๋“œ ๋‹จ๊ณ„ ์กฐ์ •**
    * Firebase Crashlytics์˜ ์ •ํ™•ํ•œ ๋””๋ฒ„๊น…์„ ์œ„ํ•ด dSYM ํŒŒ์ผ์„ ์ž๋™์œผ๋กœ ์ฐพ์•„ ์—…๋กœ๋“œํ•˜๋Š” `Upload Firebase Crashlytics dSYMs` ๋นŒ๋“œ ๋‹จ๊ณ„๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋นŒ๋“œ ๋‹จ๊ณ„ ์ˆœ์„œ๋ฅผ ์กฐ์ •ํ•˜์—ฌ Crashlytics ์ดˆ๊ธฐํ™” ๋ฐ ์‹ฌ๋ณผ ์—…๋กœ๋“œ๊ฐ€ ์ ์ ˆํ•œ ์‹œ์ ์— ์ˆ˜ํ–‰๋˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * Debug ๋นŒ๋“œ์˜ ์•ฑ ํ‘œ์‹œ ๋ช…์นญ(`CFBundleDisplayName`)์„ "festabook(Debug)"์—์„œ "festabook"์œผ๋กœ ํ†ต์ผํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `festatingNavGraph` ์œ„์น˜ ๋ณ€๊ฒฝ ๋ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋กœ์ง ์ˆ˜์ •**
    * ์ตœ์ƒ์œ„ ์ˆ˜์ค€์˜ `FestabookScreen`์—์„œ ๊ด€๋ฆฌํ•˜๋˜ `festatingNavGraph`๋ฅผ `MainScreen` ๋‚ด๋ถ€์˜ ๋„ค๋น„๊ฒŒ์ด์…˜ ์˜์—ญ์œผ๋กœ ์ด๋™ํ•˜์—ฌ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ์ตœ์ ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainScreen` ๋‚ด `onNavigateToFestating` ์ฝœ๋ฐฑ์ด ์ƒ์œ„ `festabookNavigator` ๋Œ€์‹  ํ˜„์žฌ ์ปจํ…์ŠคํŠธ์˜ `navigator`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* refactor: `PlaceListScreen` ๋‚ด ํ˜„์žฌ ์œ„์น˜ ๋ฒ„ํŠผ ๋…ธ์ถœ ์กฐ๊ฑด ์ˆ˜์ • ๋ฐ ๋งค๊ฐœ๋ณ€์ˆ˜ ์ถ”๊ฐ€

* **refactor: `CurrentLocationButton` ํ‘œ์‹œ ๋กœ์ง ๊ฐœ์„ **
    * `PlaceListScreen`์— ์ง€๋„ ํ‘œ์‹œ ์—ฌ๋ถ€๋ฅผ ์ œ์–ดํ•˜๋Š” `isPlaceMapVisible` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `CurrentLocationButton`์˜ ๋…ธ์ถœ ์กฐ๊ฑด์„ ๊ธฐ์กด ์žฅ์†Œ ๋ชฉ๋ก ๊ฐ€์‹œ์„ฑ(`isPlaceListVisible`) ๋‹จ๋… ์ฒดํฌ์—์„œ ์ง€๋„ ๊ฐ€์‹œ์„ฑ(`isPlaceMapVisible`)๊นŒ์ง€ ๋ชจ๋‘ ๋งŒ์กฑํ•˜๋Š” ๊ฒฝ์šฐ๋กœ ๊ฐ•ํ™”ํ•˜์—ฌ, ์ง€๋„๊ฐ€ ๋ณด์ด์ง€ ์•Š๋Š” ์ƒํ™ฉ์—์„œ ๋ฒ„ํŠผ์ด ๋…ธ์ถœ๋˜๋Š” ํ˜„์ƒ์„ ๋ฐฉ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ ๋ฐ ํ”„๋ฆฌ๋ทฐ ๋ฐ˜์˜**
    * `PlaceMapScreen`์—์„œ `PlaceListScreen`์„ ํ˜ธ์ถœํ•  ๋•Œ ์ง€๋„์˜ ๊ฐ€์‹œ์„ฑ ์ƒํƒœ(`isVisible`)๋ฅผ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceListScreen` ํ”„๋ฆฌ๋ทฐ ์ฝ”๋“œ์— ์‹ ๊ทœ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
@oungsi2000
oungsi2000 requested a review from etama123 May 1, 2026 11:11
@oungsi2000 oungsi2000 self-assigned this May 1, 2026
@oungsi2000 oungsi2000 added the โš™๏ธ Build ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ์Šคํฌ๋ฆฝํŠธ ๋ณ€๊ฒฝ, ํ™˜๊ฒฝ ์„ค์ • ์ˆ˜์ • label May 1, 2026
@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 287 files, which is 137 over the limit of 150. Please upgrade to Pro or Pro Plus to get higher limits.

โš™๏ธ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: b05a076e-3d67-42f1-98e6-00f655827e6f

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 629cbf4 and 54d77d3.

โ›” Files ignored due to path filters (13)
  • Gemfile.lock is excluded by !**/*.lock
  • composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png
  • composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png is excluded by !**/*.png
  • composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png
  • composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png is excluded by !**/*.png
  • composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png
  • composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png is excluded by !**/*.png
  • composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png
  • composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • composeApp/src/commonMain/composeResources/drawable/festa-ting_poster.png is excluded by !**/*.png
  • composeApp/src/commonMain/composeResources/drawable/img_waiting_push_notification_alert.png is excluded by !**/*.png
๐Ÿ“’ Files selected for processing (287)
  • .github/workflows/android-release-drafter.yml
  • .github/workflows/claude-code-review.yml
  • .github/workflows/claude.yml
  • .github/workflows/cmp-ci.yml
  • .github/workflows/common-slack-notify-opened.yml
  • .github/workflows/common-slack-notify-rerequested.yml
  • .github/workflows/common-slack-notify-submitted.yml
  • .github/workflows/common-slack-review-reminder.yml
  • .gitignore
  • Gemfile
  • build.gradle.kts
  • composeApp/build.gradle.kts
  • composeApp/proguard-rules.pro
  • composeApp/src/androidMain/AndroidManifest.xml
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/FestabookApp.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/MainActivity.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/Platform.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/Platform.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/data/util/UUIDExtension.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/di/AndroidAppGraph.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/logging/AndroidFirebaseAnalytics.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/logging/FirebaseCrashlytics.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/logging/LoggingUtil.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/NotificationPermissionManager.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/error/ErrorActivity.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/error/FestabookGlobalExceptionHandler.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/festating/component/platfom/AndroidWebViewClient.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/festating/component/platfom/FestatingWebView.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/home/component/HomeTextStyle.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/platform/RememberAppGraph.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/platform/RememberDeepLinkHandler.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/setting/component/platform/RememberNotificationPermissionManager.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/setting/component/platform/RememberOpenAppSettings.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/presentation/setting/component/platform/RememberPermissionLauncher.android.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/service/MyFirebaseMessagingService.kt
  • composeApp/src/androidMain/kotlin/com/daedan/festabook/service/NotificationHelper.kt
  • composeApp/src/androidMain/res/drawable/ic_festabook_logo_notification_large.xml
  • composeApp/src/androidMain/res/drawable/ic_festabook_logo_notification_small.xml
  • composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_festabook_logo.xml
  • composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_festabook_logo_round.xml
  • composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
  • composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
  • composeApp/src/androidMain/res/mipmap-hdpi/ic_festabook_logo.webp
  • composeApp/src/androidMain/res/mipmap-hdpi/ic_festabook_logo_foreground.webp
  • composeApp/src/androidMain/res/mipmap-hdpi/ic_festabook_logo_round.webp
  • composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp
  • composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp
  • composeApp/src/androidMain/res/mipmap-mdpi/ic_festabook_logo.webp
  • composeApp/src/androidMain/res/mipmap-mdpi/ic_festabook_logo_foreground.webp
  • composeApp/src/androidMain/res/mipmap-mdpi/ic_festabook_logo_round.webp
  • composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp
  • composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp
  • composeApp/src/androidMain/res/mipmap-xhdpi/ic_festabook_logo.webp
  • composeApp/src/androidMain/res/mipmap-xhdpi/ic_festabook_logo_foreground.webp
  • composeApp/src/androidMain/res/mipmap-xhdpi/ic_festabook_logo_round.webp
  • composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp
  • composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp
  • composeApp/src/androidMain/res/mipmap-xxhdpi/ic_festabook_logo.webp
  • composeApp/src/androidMain/res/mipmap-xxhdpi/ic_festabook_logo_foreground.webp
  • composeApp/src/androidMain/res/mipmap-xxhdpi/ic_festabook_logo_round.webp
  • composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp
  • composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp
  • composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_festabook_logo.webp
  • composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_festabook_logo_foreground.webp
  • composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_festabook_logo_round.webp
  • composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp
  • composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp
  • composeApp/src/androidMain/res/values/ic_festabook_logo_background.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_arrow_back.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_arrow_forward_right.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_close.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_home.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_homepage.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_instagram.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_news.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_schedule.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_setting.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_uncaught_error.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_waiting_bar.xml
  • composeApp/src/commonMain/composeResources/values/strings.xml
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/Greeting.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/Platform.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/datasource/local/FestivalLocalDataSource.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/datasource/local/FestivalLocalDataSourceImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/datasource/local/WaitingLocalDataSource.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/datasource/local/WaitingLocalDataSourceImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/datasource/remote/device/DeviceRemoteDataSource.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/datasource/remote/device/DeviceRemoteDataSourceImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/datasource/remote/festival/FestivalNotificationRemoteDataSource.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/datasource/remote/festival/FestivalNotificationRemoteDataSourceImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/datasource/remote/waiting/WaitingRemoteDataSource.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/datasource/remote/waiting/WaitingRemoteDataSourceImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/model/entity/FestivalSearchItemEntity.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/model/request/PhoneRegisterRequest.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/model/request/WaitingRegisterRequest.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/model/response/festival/FestivalResponse.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/model/response/festival/FestivalSponsorsResponse.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/model/response/festival/RegisteredFestivalNotificationResponse.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/model/response/lineup/LineupResponse.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/model/response/place/PlaceDetailResponse.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/model/response/waiting/MyWaitingResponse.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/model/response/waiting/PlaceWaitingResponse.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/model/response/waiting/WaitingExistResponse.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/DeviceRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/ExploreRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/FAQRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/FestivalNotificationRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/FestivalRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/LostItemRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/MyWaitingRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/NoticeRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/PlaceDetailRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/PlaceListRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/ScheduleRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/WaitingInfoRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/repository/WaitingRegisterInfoRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/service/DeviceService.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/service/FestivalNotificationService.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/service/PlaceService.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/service/WaitingService.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/service/api/FestaBookAuthPlugin.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/data/util/UUIDExtension.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/di/FestabookAppGraph.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/di/NetworkBindings.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/di/coroutine/CoroutineBindings.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/di/coroutine/Default.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/di/coroutine/IO.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/di/coroutine/Main.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/di/viewmodel/MetroViewModelFactoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/model/Festating.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/model/Festival.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/model/FestivalSponsor.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/model/MyWaiting.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/model/PlaceDetail.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/model/PlaceWaiting.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/model/WaitingInfo.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/model/WaitingStatus.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/repository/DeviceRepository.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/repository/ExploreRepository.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/repository/FestivalNotificationRepository.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/repository/FestivalRepository.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/repository/LostItemRepository.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/repository/MyWaitingRepository.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/repository/WaitingInfoRepository.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/domain/repository/WaitingRegisterInfoRepository.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/logging/ClickableLogger.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/logging/FirebaseAnalytics.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/logging/FirebaseCrashlytics.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/logging/LoggingUtil.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/logging/ScreenViewLogger.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/FestabookScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/NotificationPermissionManager.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/PermissionState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/common/OnMenuItemReClickListener.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/common/RememberAnalytics.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/common/RunIfAtLeastState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/common/TextUtil.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/common/component/CoilImage.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/common/component/FestabookTopAppBar.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/common/component/SkeletonBox.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/common/component/SnackBar.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/common/format/PlaceTimeFormat.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/error/ErrorScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/ExploreSideEffect.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/ExploreUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/ExploreViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/SearchUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/component/ExploreBackHeader.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/component/ExploreLandingContent.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/component/ExploreResultItem.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/component/ExploreScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/component/ExploreSearchBar.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/component/ExploreSearchContent.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/component/ExploreSearchResultList.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/component/RecentSearchMessageView.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/model/SearchResultUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/explore/navigation/ExploreNavigation.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/festating/FestatingUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/festating/FestatingViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/festating/component/FestatingPoster.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/festating/component/FestatingScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/festating/component/platfom/FestatingWebView.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/festating/navigation/FestatingNavigation.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/FestivalUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/HomeViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/LineupUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/WaitingBarUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/component/HomeArtistItem.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/component/HomeFestivalInfo.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/component/HomeHeader.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/component/HomeLineupHeader.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/component/HomeLineupItem.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/component/HomePosterList.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/component/HomeScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/component/HomeSponsorBanner.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/component/HomeTextStyle.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/component/HomeWaitingBar.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/model/FestatingUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/model/FestivalPosterUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/model/FestivalSponsorUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/model/FestivalUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/model/LineUpItemOfDayUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/model/LineupItemUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/model/OrganizationUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/home/navigation/HomeNavigation.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/main/FestabookMainTab.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/main/FestabookNavigator.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/main/FestabookRoute.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/main/MainViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/main/component/FirstVisitDialog.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/main/component/MainScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/main/navigation/MainNavigation.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/news/NewsViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/news/component/NewsScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/news/navigation/NewsNavigation.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/news/notice/model/NoticeUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeDetail/PlaceDetailViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/component/PlaceCategoryScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/component/PlaceDetailPreviewScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/component/PlaceDetailPreviewSecondaryScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/component/PlaceListScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/intent/event/SelectEvent.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/intent/handler/FilterEventHandler.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/intent/handler/MapControlEventHandlerImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/intent/handler/PlaceMapSideEffectHandler.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/intent/handler/SelectEventHandlerImpl.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/intent/sideEffect/MapControlSideEffect.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/intent/sideEffect/PlaceMapSideEffect.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/intent/state/LoadState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/intent/state/PlaceMapUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/intent/state/StateExt.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/mapManager/MapManager.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/model/PlaceUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/navigation/PlaceMapNavigation.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/placeDetail/PlaceDetailViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/placeDetail/component/PlaceDetailBottomBar.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/placeDetail/component/PlaceDetailScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/placeDetail/component/PlaceWaitingContent.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/placeDetail/component/WaitingCancelConfirmDialog.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/placeDetail/component/WaitingDuplicateBottomSheet.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/placeDetail/model/ImageUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/placeDetail/model/PlaceDetailUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/placeDetail/model/PlaceDetailUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/placeDetail/model/WaitingStatusUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/placeDetail/model/WaitingTeamUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/waitingRegister/WaitingRegisterViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/waitingRegister/component/WaitingRegisterConfirmBottomSheet.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/waitingRegister/component/WaitingRegisterScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/waitingRegister/model/WaitingPlaceSummaryUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/waitingRegister/model/WaitingRegisterUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/placeMap/waitingRegister/model/WaitingRegisterUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/platform/RememberAppGraph.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/platform/RememberDeepLinkHandler.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/schedule/component/ScheduleScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/schedule/navigation/ScheduleNavigation.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/setting/SettingViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/setting/component/NotificationPermissionDialog.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/setting/component/SettingScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/setting/component/platform/RememberNotificationPermissionManager.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/setting/component/platform/RememberOpenAppSettings.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/setting/component/platform/RememberPermissionLauncher.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/setting/navigation/SettingNavigation.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/setting/waitinginfo/WaitingInfoViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/setting/waitinginfo/component/AddWaitingInfoScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/setting/waitinginfo/model/WaitingInfoUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/splash/navigation/SplashNavigation.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/theme/FestabookTheme.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/waiting/MyWaitingUiState.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/waiting/MyWaitingViewModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/waiting/component/MyWaitingScreen.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/waiting/component/WaitingStatusCard.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/waiting/model/MyWaitingUiModel.kt
  • composeApp/src/commonMain/kotlin/com/daedan/festabook/presentation/waiting/navigation/MyWaitingNavigation.kt
  • composeApp/src/commonTest/kotlin/com/daedan/festabook/data/datasource/local/WaitingLocalDataSourceTest.kt
  • composeApp/src/commonTest/kotlin/com/daedan/festabook/data/datasource/remote/festival/FestivalNotificationRemoteDataSourceTest.kt
  • composeApp/src/commonTest/kotlin/com/daedan/festabook/data/datasource/remote/festival/FestivalRemoteDataSourceTest.kt
  • composeApp/src/commonTest/kotlin/com/daedan/festabook/data/datasource/remote/place/TestFixture.kt
  • composeApp/src/commonTest/kotlin/com/daedan/festabook/data/datasource/remote/waiting/WaitingRemoteDataSourceTest.kt
  • composeApp/src/commonTest/kotlin/com/daedan/festabook/data/datasource/remote/waiting/WaitingTestFixture.kt

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ๐Ÿ” Trigger review

Tip

๐Ÿ’ฌ Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire orgโ€”no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-inโ€”scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

๐Ÿ‘‰ Get started


Note

๐ŸŽ Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

oungsi2000 and others added 7 commits May 2, 2026 13:59
* chore: cmp-ci.yml runner๋ฅผ ubuntu-latest๋กœ ์ „ํ™˜

self-hosted runner์—์„œ ํ‰๊ท  21.3๋ถ„ ๊ฑธ๋ฆฌ๋˜ PR ๊ฒ€์ฆ์„
hosted runner๋กœ ์ „ํ™˜ํ•˜์—ฌ ์•ฝ 7๋ถ„๋Œ€๋กœ ๋‹จ์ถ•. PR ํ”ผ๋“œ๋ฐฑ ์ง€์—ฐ ํ•ด์†Œ.

closes #184

Co-Authored-By: Claude Code <noreply@anthropic.com>

* style: ์ฝ”๋“œ ํฌ๋งทํŒ… ๊ฐœ์„  ๋ฐ ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ์ œ๊ฑฐ

* **style: `FestivalRemoteDataSourceTest` ๋‚ด ํŠธ๋ ˆ์ผ๋ง ์ฝค๋งˆ ์ถ”๊ฐ€**
    * ์ฝ”๋“œ ์ผ๊ด€์„ฑ ๋ฐ ํ–ฅํ›„ ๋ณ€๊ฒฝ ์‹œ์˜ ํŽธ์˜์„ฑ์„ ์œ„ํ•ด ๋งค๊ฐœ๋ณ€์ˆ˜ ๋ชฉ๋ก ๋์— ์ฝค๋งˆ(`,`)๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `SettingScreen` ๋‚ด ๋ถˆํ•„์š”ํ•œ ์ž„ํฌํŠธ ์ œ๊ฑฐ**
    * ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `FestatingUiModel` ์ž„ํฌํŠธ ๊ตฌ๋ฌธ์„ ์ œ๊ฑฐํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* test: `PlaceMapViewModelTest` ๋‚ด ์žฅ์†Œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ์ˆ˜์ •

* **test: `placeGeographies` ๋กœ๋“œ ์‹คํŒจ ๊ฒ€์ฆ ๋กœ์ง ์ˆ˜์ •**
    * ๊ธฐ์กด์— `placeListRepository.getPlaceGeographies()`๊ฐ€ ์‹คํŒจ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •๋˜์—ˆ๋˜ ๋ถ€๋ถ„์„ ์„ฑ๊ณต(`FAKE_PLACE_GEOGRAPHIES`)์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `uiState.placeGeographies`์˜ ์ƒํƒœ๋ฅผ ๊ฒ€์ฆํ•˜๋˜ ๋กœ์ง์„ `uiState.places`๊ฐ€ `ListLoadState.Error(exception)` ์ƒํƒœ์ธ์ง€ ํ™•์ธํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ๋ณ€๊ฒฝ๋œ UI ์ƒํƒœ ๊ตฌ์กฐ๋ฅผ ๋ฐ˜์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **test: `getPlaceGeographies` ์‹คํŒจ ์‹œ๋‚˜๋ฆฌ์˜ค ๋ชจํ‚น ์ฝ”๋“œ ์ •๋ฆฌ**
    * `placeListRepository.getPlaceGeographies()`๊ฐ€ ์‹คํŒจ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์˜ ์ฝ”๋“œ ํฌ๋งทํŒ…์„ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: oungsi2000 <oungsi1000@gmail.com>
feat: ์›น๋ทฐ ๋‚ด ํ† ์Šค(Toss) ๊ฒฐ์ œ ์—ฐ๋™์„ ์œ„ํ•œ ์™ธ๋ถ€ ์Šคํ‚ด ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€

* **feat: Android ์™ธ๋ถ€ ์•ฑ ์‹คํ–‰ ๋ฐ ํŒจํ‚ค์ง€ ์ฟผ๋ฆฌ ์„ค์ •**
    * `AndroidManifest.xml`์— ํ† ์Šค ํŒจํ‚ค์ง€(`viva.republica.toss`)์— ๋Œ€ํ•œ `<queries>`๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AndroidWebViewClient`๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ `supertoss` ๋ฐ `intent` ์Šคํ‚ด ๊ฐ์ง€ ์‹œ ์™ธ๋ถ€ ์•ฑ์„ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜ ๋งˆ์ผ“์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: iOS ์™ธ๋ถ€ ์•ฑ ์‹คํ–‰ ๋ฐ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๊ตฌํ˜„**
    * `Info.plist`์— `supertoss` ์Šคํ‚ด์„ `LSApplicationQueriesSchemes`์— ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `DefaultWKNavigationDelegate`๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ `supertoss` ์Šคํ‚ด ๋ฐœ์ƒ ์‹œ ํ† ์Šค ์•ฑ ๋˜๋Š” ์•ฑ์Šคํ† ์–ด๋กœ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ํ”Œ๋žซํผ๋ณ„ `FestatingWebView` ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค ์ถ”์ถœ**
    * `commonMain`์— `FestatingWebView` expect ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•˜๊ณ , ๊ฐ ํ”Œ๋žซํผ(`androidMain`, `iosMain`)์—์„œ ์‹ค์ œ ์›น๋ทฐ ๊ตฌํ˜„ ๋ฐ ์ปค์Šคํ…€ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ/ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ ์šฉํ•˜๋„๋ก ๊ตฌ์กฐ๋ฅผ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestatingScreen`์—์„œ ์ง์ ‘ `WebView`๋ฅผ ํ˜ธ์ถœํ•˜๋˜ ๋ฐฉ์‹์„ ์‹ ๊ทœ ๊ตฌํ˜„ํ•œ `FestatingWebView`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๋นŒ๋“œ ๊ตฌ์„ฑ ๋ณ€๊ฒฝ**
    * `gradle.properties` ๋‚ด `buildkonfig.flavor`๋ฅผ `release`์—์„œ `dev`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
feat: `WaitingRegisterScreen` ๋‚ด ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ ์•ฑ ์„ค์ • ์ด๋™ ๋กœ์ง ์ถ”๊ฐ€

* **feat: ๊ถŒํ•œ ๊ฑฐ๋ถ€(onPermissionDeny) ํ•ธ๋“ค๋Ÿฌ ๊ตฌํ˜„**
    * `WaitingRegisterScreen`์—์„œ ๊ถŒํ•œ ์š”์ฒญ์ด ๊ฑฐ๋ถ€๋˜์—ˆ์„ ๋•Œ, ์•„๋ฌด๋Ÿฐ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๋˜ ๊ธฐ์กด ์ฝ”๋“œ์— `onOpenAppSettings()` ํ˜ธ์ถœ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ด๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ๊ถŒํ•œ์„ ๊ฑฐ๋ถ€ํ•œ ๊ฒฝ์šฐ ์•ฑ ์„ค์ • ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜์—ฌ ์ง์ ‘ ๊ถŒํ•œ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ์œ ๋„ํ•ฉ๋‹ˆ๋‹ค.
refactor: `MainScreen` ๋„ค๋น„๊ฒŒ์ด์…˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ๊ฑฐ ๋ฐ ๋นŒ๋“œ ํ”Œ๋ ˆ์ด๋ฒ„ ๋ณ€๊ฒฝ

* **refactor: `MainScreen` ๋‚ด `NavHost` ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋น„ํ™œ์„ฑํ™”**
    * `NavHost`์˜ `enterTransition`, `exitTransition`, `popEnterTransition`, `popExitTransition` ์†์„ฑ์„ ๋ชจ๋‘ `None`์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ํ™”๋ฉด ์ „ํ™˜ ์‹œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **chore: ๋นŒ๋“œ ์ปจํ”ผ๊ทธ ํ”Œ๋ ˆ์ด๋ฒ„๋ฅผ `dev`๋กœ ๋ณ€๊ฒฝ**
    * `gradle.properties`์—์„œ `buildkonfig.flavor` ์„ค์ •์„ ๊ธฐ์กด `release`์—์„œ `dev`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
* **fix: ์ƒˆ๋กœ๊ณ ์นจ ์•„์ด์ฝ˜ ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋กœ์ง ๊ฐœ์„ **
    * `PlaceWaitingContent` ๋ฐ `WaitingStatusCard`์—์„œ `infiniteRepeatable`์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌดํ•œ ๋ฃจํ”„ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ตฌ์กฐ๋ฅผ ์•ˆ์ •ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ƒˆ๋กœ๊ณ ์นจ ์ข…๋ฃŒ ์‹œ ์ž”์—ฌ ํšŒ์ „ ๊ฐ๋„๋ฅผ ๊ณ„์‚ฐํ•˜์—ฌ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋ฉˆ์ถ”๋„๋ก ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ•ํ™”ํ•˜๊ณ , ์™„๋ฃŒ ํ›„ ์ƒํƒœ๋ฅผ ์ดˆ๊ธฐํ™”(`snapTo(0f)`)ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **fix: ๋Œ€๊ธฐ ์ƒํƒœ ๋ฌธ๊ตฌ ๋…ธ์ถœ ์ž„๊ณ„๊ฐ’ ์กฐ๊ฑด ์ˆ˜์ •**
    * `WaitingStatusCard`์—์„œ ๋Œ€๊ธฐ ์ˆœ์„œ์— ๋”ฐ๋ฅธ ๋ฌธ๊ตฌ ๋…ธ์ถœ ์กฐ๊ฑด์„ `order > WAITING_NEAR_THRESHOLD`์™€ `order <= WAITING_NEAR_THRESHOLD`๋กœ ์กฐ์ •ํ•˜์—ฌ ๊ฒฝ๊ณ„๊ฐ’ ๋™์ž‘์„ ๋ช…ํ™•ํžˆ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `AddWaitingInfoScreen` ํ‚ค๋ณด๋“œ ์ฒ˜๋ฆฌ ๋ฐ ์ž…๋ ฅ ๋กœ์ง ๊ฐœ์„ **
    * `Scaffold`์˜ `contentWindowInsets`์—์„œ `IME`๋ฅผ ์ œ์™ธํ•˜์—ฌ ํ‚ค๋ณด๋“œ ํ™œ์„ฑํ™” ์‹œ ๋ ˆ์ด์•„์›ƒ ์ค‘๋ณต ๊ณ„์‚ฐ์„ ๋ฐฉ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `WaitingInfoViewModel`์—์„œ ์ „ํ™”๋ฒˆํ˜ธ ์—…๋ฐ์ดํŠธ ์‹œ ์ˆซ์ž ํ•„ํ„ฐ๋ง ํ›„ ๊ธธ์ด๋ฅผ ์ฒดํฌํ•˜๋„๋ก ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•˜์—ฌ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **build: ์•ฑ ๋ฒ„์ „ ์ฝ”๋“œ ์—…๋ฐ์ดํŠธ**
    * `APP_VERSION_CODE`๋ฅผ `2010103`์—์„œ `2010106`์œผ๋กœ ์˜ฌ๋ ธ์Šต๋‹ˆ๋‹ค.
* feat: `FirebaseAnalytics` ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ ์ž๋™ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆ˜์ง‘ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* **feat: `FirebaseAnalytics` ํด๋ž˜์Šคํ™” ๋ฐ ์˜์กด์„ฑ ์ฃผ์ž… ์ ์šฉ**
    * ๊ธฐ์กด `object` ์„ ์–ธ์„ `class`๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  Metro๋ฅผ ํ†ตํ•œ ์˜์กด์„ฑ ์ฃผ์ž…(`@Inject`)์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestivalLocalDataSource` ๋ฐ `DeviceLocalDataSource`๋ฅผ ์ฃผ์ž…๋ฐ›์•„ ์ถ•์ œ ID์™€ ์‚ฌ์šฉ์ž UUID๋ฅผ ์ž๋™์œผ๋กœ ์ˆ˜์ง‘ํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ๊ธฐ๋ณธ ์ด๋ฒคํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ ์ž๋™ ์„ค์ • ๋ฐ ์ง€์—ฐ ์ „์†ก ๋กœ์ง ๊ตฌํ˜„**
    * ์•ฑ ๋ฒ„์ „, OS ๋ฒ„์ „, ๋””๋ฐ”์ด์Šค ๋ชจ๋ธ, ์–ธ์–ด, ์‚ฌ์šฉ์ž ID ๋“ฑ์„ ํฌํ•จํ•˜๋Š” ๊ธฐ๋ณธ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ `setDefaultEventParameters`๋ฅผ ํ†ตํ•ด ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
    * ์ดˆ๊ธฐํ™” ์ž‘์—…์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „ ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ๋ฅผ `Channel`์— ๋ณด๊ด€ํ–ˆ๋‹ค๊ฐ€ ์ดˆ๊ธฐํ™” ํ›„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ „์†กํ•˜๋Š” `PendingEvent` ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋ชจ๋“  ์ด๋ฒคํŠธ ๋กœ๊น… ์‹œ ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ `festival_id`๊ฐ€ ์ž๋™์œผ๋กœ ํฌํ•จ๋˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ํ”Œ๋žซํผ๋ณ„ ๋ถ„์„ ๋กœ์ง ๊ณ ๋„ํ™” ๋ฐ ๋ถˆํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ ์ œ๊ฑฐ**
    * **Android**: `Firebase.analytics`๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๊ธฐ๋ณธ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ `festival_id` ๋งคํ•‘ ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **iOS**: `utsname`์„ ์‚ฌ์šฉํ•œ ์ƒ์„ธ ๋””๋ฐ”์ด์Šค ๋ชจ๋ธ ์ถ”์ถœ ๋ฐ `NSDate` ๊ธฐ๋ฐ˜ ํƒ€์ž„์Šคํƒฌํ”„ ๊ณ„์‚ฐ ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `getAppInstanceId`, `getSessionId` ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **infra: ์•ฑ ๊ทธ๋ž˜ํ”„ ๋‚ด ๋ถ„์„ ์„œ๋น„์Šค ๋“ฑ๋ก**
    * `FestabookAppGraph`์— `FirebaseAnalytics`๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๋น„๋™๊ธฐ ์ž‘์—…์„ ์œ„ํ•œ `IOCoroutineScope` ์ œ๊ณต ๋กœ์ง์„ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

# Conflicts:
#	composeApp/src/commonMain/kotlin/com/daedan/festabook/di/FestabookAppGraph.kt

* feat: ์•Œ๋ฆผ ํด๋ฆญ ๋ถ„์„ ๋กœ์ง ๊ตฌํ˜„ ๋ฐ ๊ณตํ†ต ๋กœ๊น… ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ถ”๊ฐ€

* **feat: ๊ณตํ†ต ์‹œ๊ฐ„ ์ธก์ • ์œ ํ‹ธ๋ฆฌํ‹ฐ `currentTimeMillis` ๊ตฌํ˜„**
    * Kotlin Multiplatform์˜ `expect/actual`์„ ์‚ฌ์šฉํ•˜์—ฌ ํ”Œ๋žซํผ๋ณ„ ํ˜„์žฌ ์‹œ๊ฐ„(ms) ์ธก์ • ๋กœ์ง์„ ํ†ตํ•ฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    * **Android**: `System.currentTimeMillis()`๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    * **iOS**: `NSDate`์™€ `timeIntervalSince1970`์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•˜๊ณ , ๊ธฐ์กด `IosFirebaseAnalytics` ๋‚ด ์ค‘๋ณต ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ์•Œ๋ฆผ ํด๋ฆญ ์‹œ ๋ถ„์„ ์ด๋ฒคํŠธ(`notification_tapped`) ๋กœ๊น… ์ถ”๊ฐ€**
    * `FcmDeepLinkAction.OpenAnnouncement` ๋ชจ๋ธ์— `notificationSentAt` ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์•Œ๋ฆผ ๋ฐœ์†ก ์‹œ์ ์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.
    * ์•Œ๋ฆผ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์•Œ๋ฆผ ID, ๋ฐœ์†ก ์‹œ์ , ๊ทธ๋ฆฌ๊ณ  ํด๋ฆญ๊นŒ์ง€ ๊ฑธ๋ฆฐ ์‹œ๊ฐ„(`elapsed_sec`)์„ ํฌํ•จํ•œ ๋ถ„์„ ์ด๋ฒคํŠธ๋ฅผ ์ „์†กํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `MainScreen` ์ง„์ž… ์‹œ `app_foreground` ์ด๋ฒคํŠธ๋ฅผ ๋กœ๊น…ํ•˜๋„๋ก ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: ๋ถ„์„ ๋„๊ตฌ ๋ฐ ๋”ฅ๋งํฌ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„ **
    * `rememberAnalytics()` Composable์„ ์ถ”๊ฐ€ํ•˜์—ฌ `AppGraph`์—์„œ `FirebaseAnalytics` ์ธ์Šคํ„ด์Šค๋ฅผ ์‰ฝ๊ฒŒ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * Android ๋ฐ iOS์˜ ๋ฉ”์‹œ์ง• ์„œ๋น„์Šค/๋ธ๋ฆฌ๊ฒŒ์ดํŠธ์—์„œ ๊ณต์ง€์‚ฌํ•ญ ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ์‹œ ํ˜„์žฌ ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ํ•จ๊ป˜ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AndroidFirebaseAnalytics`์—์„œ ๊ธฐ๋ณธ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ • ์‹œ ์ƒˆ๋กœ ์ •์˜ํ•œ `currentTimeMillis()`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ํ†ต์ผํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ๋ถ„์„์šฉ ๋กœ๊น… ์œ ํ‹ธ๋ฆฌํ‹ฐ `logClick` ๋ฐ `ScreenViewLogger` ๊ตฌํ˜„

* **feat: ํด๋ฆญ ์ด๋ฒคํŠธ ๋กœ๊น…์„ ์œ„ํ•œ `Modifier.logClick` ํ™•์žฅ ํ•จ์ˆ˜ ์ถ”๊ฐ€**
    * `Modifier.clickable`์„ ๋ž˜ํ•‘ํ•˜์—ฌ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ `button_id`์™€ `screen_name` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํฌํ•จํ•œ `button_click` ์ด๋ฒคํŠธ๋ฅผ ์ž๋™์œผ๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

* **feat: ํ™”๋ฉด ์ง„์ž… ๋ฐ ์ดํƒˆ ๋กœ๊น…์„ ์œ„ํ•œ `ScreenViewLogger` ์ปดํฌ์ €๋ธ” ๊ตฌํ˜„**
    * `DisposableEffect`๋ฅผ ํ™œ์šฉํ•ด ํ™”๋ฉด ์ง„์ž…(`screen_view`)๊ณผ ์ดํƒˆ(`screen_exit`) ์‹œ์ ์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.
    * ์ดํƒˆ ์‹œ ํ™”๋ฉด ์ฒด๋ฅ˜ ์‹œ๊ฐ„(`duration_sec`)์„ ๊ณ„์‚ฐํ•˜์—ฌ ํ•จ๊ป˜ ์ „์†กํ•˜๋„๋ก ๋กœ์ง์„ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ๊ฐ€์‹œ์„ฑ ๊ธฐ๋ฐ˜ ๋กœ๊น…์„ ์œ„ํ•œ `ScreenViewVisibilityLogger` ๊ตฌํ˜„**
    * `isVisible` ์ƒํƒœ ๋ณ€ํ™”์— ๋”ฐ๋ผ ํ™”๋ฉด ๋…ธ์ถœ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•˜์—ฌ ์ง„์ž…/์ดํƒˆ ์ด๋ฒคํŠธ๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.
    * ์ฒด๋ฅ˜ ์‹œ๊ฐ„์„ ์†Œ์ˆ˜์  ๋‹จ์œ„์˜ ์ดˆ(`Double`)๋กœ ์ •๋ฐ€ํ•˜๊ฒŒ ๊ณ„์‚ฐํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ๊ณต์ง€์‚ฌํ•ญ ์•Œ๋ฆผ ๋ฐœ์ƒ ์‹œ๊ฐ(`notificationSentAt`) ์ „๋‹ฌ ๋กœ์ง ์ถ”๊ฐ€

* **feat: `DeepLinkKeys` ๋‚ด ์•Œ๋ฆผ ์ „์†ก ์‹œ๊ฐ ํ‚ค ์ •์˜**
    * ๋”ฅ๋งํฌ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ `KEY_NOTIFICATION_SENT_AT` ์ƒ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: `NotificationHelper` ์•Œ๋ฆผ ์ƒ์„ฑ ์‹œ ํ˜„์žฌ ์‹œ๊ฐ ํฌํ•จ**
    * ๊ณต์ง€์‚ฌํ•ญ ์•Œ๋ฆผ(`ANNOUNCEMENT`) ์ƒ์„ฑ ์‹œ, ์ธํ…ํŠธ์— ํ˜„์žฌ ์‹œ๊ฐ(`currentTimeMillis()`)์„ `KEY_NOTIFICATION_SENT_AT`์— ๋‹ด์•„ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `RememberDeepLinkHandler` ๋‚ด ์•Œ๋ฆผ ์‹œ๊ฐ ์ถ”์ถœ ๋ฐ ์ฒ˜๋ฆฌ ๋กœ์ง ๋ฐ˜์˜**
    * ์•ˆ๋“œ๋กœ์ด๋“œ ์ธํ…ํŠธ์—์„œ `notificationSentAt` ๊ฐ’์„ ์ถ”์ถœํ•˜์—ฌ `FcmDeepLinkAction.OpenAnnouncement`๋กœ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋œ `KEY_NOTIFICATION_SENT_AT` ๋ฐ์ดํ„ฐ๋ฅผ ์ธํ…ํŠธ์—์„œ ์ œ๊ฑฐํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* feat: ์ „์—ญ ํ™”๋ฉด ๋ฐฉ๋ฌธ ๋กœ๊น… ๋ฐ ํด๋ฆญ ์ด๋ฒคํŠธ ๋กœ๊น… ๊ธฐ๋Šฅ ๊ฐ•ํ™”

* **feat: ์ฃผ์š” ํ™”๋ฉด์— `ScreenViewLogger` ๋ฐ `ScreenViewVisibilityLogger` ์ถ”๊ฐ€**
    * `HomeScreen`, `PlaceMapScreen`, `NewsScreen`, `ScheduleScreen`, `SettingScreen` ๋“ฑ ๊ฑฐ์˜ ๋ชจ๋“  ์ฃผ์š” ํ™”๋ฉด(Screen) ์ปดํฌ๋„ŒํŠธ์— ์ง„์ž… ์‹œ ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๋Š” `ScreenViewLogger`๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceMapScreen`์—๋Š” ๊ฐ€์‹œ์„ฑ ์ƒํƒœ์— ๋”ฐ๋ผ ๋กœ๊น… ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” `ScreenViewVisibilityLogger`๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: `logClick` ํ•จ์ˆ˜ ๋‚ด ์ถ”๊ฐ€ ํŒŒ๋ผ๋ฏธํ„ฐ(`extraParam`) ์ง€์›**
    * ๋ฒ„ํŠผ ํด๋ฆญ ๋กœ๊ทธ๋ฅผ ๊ธฐ๋กํ•  ๋•Œ ๊ธฐ์กด `button_id`์™€ `screen` ์™ธ์—๋„ ์‚ฌ์šฉ์ž ์ •์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋„๋ก `extraParam` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `buildMap`์„ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ๊ทธ ์ด๋ฒคํŠธ์— ํฌํ•จ๋  ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ํ™•์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: `FestaingScreen` ๋‚ด ์˜คํƒ€ ์ˆ˜์ •**
    * `ScreenViewLogger`์— ์ „๋‹ฌ๋˜๋Š” ๋ฌธ์ž์—ด ๋‚ด ์˜คํƒ€๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. (`FestaingScreen`)

* refactor: ๋กœ๊น… ๋กœ์ง ์ตœ์ ํ™” ๋ฐ ์ฝ”๋“œ ์ •๋ฆฌ

* **refactor: `ScreenViewLogger` ๋‚ด ์ƒํƒœ ๊ด€๋ฆฌ ๋ฐ ์ฝ”๋“œ ์ •๋ฆฌ**
    * `enteredTime`์„ ์ €์žฅํ•  ๋•Œ ์ผ๋ฐ˜ `mutableStateOf<Long>` ๋Œ€์‹  `mutableLongStateOf`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์—ฌ ์˜คํ† ๋ฐ•์‹ฑ์„ ๋ฐฉ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ํŒŒ์ผ ๋์˜ ๋ถˆํ•„์š”ํ•œ ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

* **refactor: `IosFirebaseAnalytics` ์˜์กด์„ฑ ์ฃผ์ž… ์ˆ˜์ • ๋ฐ ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ์ œ๊ฑฐ**
    * `CoroutineScope` ์ฃผ์ž… ์‹œ `@IO` ํ•œ์ •์ž(qualifier)๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ ์ ˆํ•œ ์Šค์ฝ”ํ”„๊ฐ€ ์ฃผ์ž…๋˜๋„๋ก ๋ช…์‹œํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `NSDate`, `date`, `timeIntervalSince1970` ๋“ฑ iOS ํ”Œ๋žซํผ์˜ ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: ์ฝ”๋“œ ํฌ๋งทํŒ… ๋ฐ ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ์ œ๊ฑฐ**
    * `ClickableLogger.kt`: `logEvent` ํ˜ธ์ถœ ์‹œ ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด ํŒŒ๋ผ๋ฏธํ„ฐ ์ค„๋ฐ”๊ฟˆ ๋ฐ ํŠธ๋ ˆ์ผ๋ง ์ฝค๋งˆ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `AndroidAppGraph.kt`, `RememberDeepLinkHandler.android.kt`: ๋ถˆํ•„์š”ํ•œ ์ž„ํฌํŠธ ๊ตฌ๋ฌธ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestabookAppGraph.kt`: ๋ถˆํ•„์š”ํ•œ ๋นˆ ์ค„์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `FestatingScreen.kt`: ์ž„ํฌํŠธ ์ˆœ์„œ๋ฅผ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

* fix: ios์—์„œ elapsed time์ด 0์œผ๋กœ ํ‘œ์‹œ๋˜๋Š” ๋ฒ„๊ทธ ํ•ด๊ฒฐ

* feat: ์•ฑ ์ „๋ฐ˜์˜ ์ฃผ์š” ์‚ฌ์šฉ์ž ํด๋ฆญ ์•ก์…˜์— ๋Œ€ํ•œ ๋กœ๊น… ๋กœ์ง ์ถ”๊ฐ€

* **feat: `FAQScreen`, `NoticeScreen` ๋‚ด ์•„์ดํ…œ ํด๋ฆญ ๋กœ๊น…**
    * FAQ ๋ฐ ๊ณต์ง€์‚ฌํ•ญ ํ•ญ๋ชฉ ํด๋ฆญ ์‹œ ๊ฐ๊ฐ `faq_id`, `notice_id`๋ฅผ ํฌํ•จํ•˜์—ฌ ๋กœ๊น…ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๊ณต์ง€์‚ฌํ•ญ ๋ฐ ์œ ์‹ค๋ฌผ ํ™”๋ฉด์˜ ์ƒˆ๋กœ๊ณ ์นจ ์•ก์…˜์— ๋กœ๊น…์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: `HomeScreen` ๋ฐ ๋ฉ”์ธ ๋‚ด๋น„๊ฒŒ์ด์…˜ ํด๋ฆญ ๋กœ๊น…**
    * ํ™ˆ ํ™”๋ฉด์—์„œ ํƒ์ƒ‰, ํŽ˜์Šคํƒ€ํŒ…, ์ผ์ •, ์›จ์ดํŒ… ๋ฐ” ํด๋ฆญ ์‹œ ๋กœ๊น…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
    * ํ•˜๋‹จ ํƒญ ๋ฐ”์—์„œ ๊ฐ ํƒญ ์„ ํƒ ์‹œ ํƒญ ์ด๋ฆ„์„ ํฌํ•จํ•˜์—ฌ ๋กœ๊น…ํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ์žฅ์†Œ ๊ด€๋ จ ํ™”๋ฉด ํด๋ฆญ ๋กœ๊น…**
    * ์žฅ์†Œ ์ง€๋„(`PlaceMapScreen`)์˜ ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ ๋ฐ ๋ฆฌ์ŠคํŠธ ์•„์ดํ…œ ํด๋ฆญ ์‹œ ๋กœ๊น…์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์žฅ์†Œ ์ƒ์„ธ ๋ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ™”๋ฉด์—์„œ ์ƒ์„ธ ์ด๋™, ์›จ์ดํŒ… ๋“ฑ๋ก ๋“ฑ์˜ ์•ก์…˜ ๋กœ๊น…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

* **feat: ์›จ์ดํŒ… ๋ฐ ์„ค์ • ํ™”๋ฉด ์•ก์…˜ ๋กœ๊น…**
    * `MyWaitingScreen`์˜ ์ƒˆ๋กœ๊ณ ์นจ, ์ทจ์†Œ, ์ƒ์„ธ ์ด๋™ ์•ก์…˜์— ๋กœ๊น…์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ์›จ์ดํŒ… ๋“ฑ๋ก ํ™”๋ฉด์˜ ์ธ์› ์กฐ์ ˆ, ์•ฝ๊ด€ ๋™์˜, ๋“ฑ๋ก ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋กœ๊น…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
    * ์„ค์ • ํ™”๋ฉด์˜ ๊ณต์ง€์‚ฌํ•ญ/์ •์ฑ… ๋งํฌ ํด๋ฆญ, ์•Œ๋ฆผ ์„ค์ • ํ† ๊ธ€, ๋‚ด ์ •๋ณด ์ €์žฅ ์•ก์…˜ ๋“ฑ์— ๋กœ๊น…์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

* **feat: ๊ฒ€์ƒ‰ ๋ฐ ๋’ค๋กœ๊ฐ€๊ธฐ ์•ก์…˜ ๋กœ๊น…**
    * ํƒ์ƒ‰ ํ™”๋ฉด(`ExploreScreen`) ๋ฐ ๊ฐ ์ฃผ์š” ํ™”๋ฉด์˜ ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋กœ๊น…์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ฝ”๋“œ ๋ฆฌ๋ทฐ โ€” PR #186 [Release] Release 2.1.1

์ด 13๊ฐœ ์ด์Šˆ๋ฅผ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค. (p1: 4๊ฐœ, p2: 6๊ฐœ, p3: 3๊ฐœ)

์ธ๋ผ์ธ ์ฝ”๋ฉ˜ํŠธ๋กœ ๊ฐ ์ด์Šˆ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.

import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metrox.viewmodel.ViewModelKey

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p1] ์ž˜๋ชป๋œ ViewModelKey import โ€” ๋Ÿฐํƒ€์ž„ DI ์˜ค๋ฅ˜ ๋ฐœ์ƒ ๊ฐ€๋Šฅ

FestatingViewModel๋งŒ dev.zacsweers.metrox.viewmodel.ViewModelKey๋ฅผ importํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๋‚˜๋จธ์ง€ 11๊ฐœ ViewModel์€ ๋ชจ๋‘ ํ”„๋กœ์ ํŠธ ๋‚ด๋ถ€ com.daedan.festabook.di.viewmodel.ViewModelKey๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Metro DI์—์„œ @mapkey ์–ด๋…ธํ…Œ์ด์…˜์ด ๋‹ค๋ฅด๋ฉด ๋ณ„๋„์˜ ๋ฉ€ํ‹ฐ๋ฐ”์ธ๋”ฉ ๋งต์ด ์ƒ์„ฑ๋˜์–ด, MetroViewModelFactoryImpl์ด FestatingViewModel์„ ์ฐพ์ง€ ๋ชปํ•˜๊ณ  ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ปดํŒŒ์ผ์€ ์„ฑ๊ณตํ•˜์ง€๋งŒ ํ•ด๋‹น ํ™”๋ฉด ์ง„์ž… ์‹œ ํฌ๋ž˜์‹œ๋ฉ๋‹ˆ๋‹ค.

์ˆ˜์ •:
import com.daedan.festabook.di.viewmodel.ViewModelKey

remember {
{
val url =
NSURL.URLWithString(UIApplicationOpenSettingsURLString) ?: return@remember

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p1] return@remember ์ปดํŒŒ์ผ ์˜ค๋ฅ˜ โ€” ๋น„์ธ๋ผ์ธ ๋žŒ๋‹ค์—์„œ ๋น„์ง€์—ญ ๋ฐ˜ํ™˜ ๋ถˆ๊ฐ€

return@remember๊ฐ€ ์ค‘์ฒฉ๋œ ๋น„์ธ๋ผ์ธ () -> Unit ๋žŒ๋‹ค ๋‚ด๋ถ€์— ์œ„์น˜ํ•ด ์žˆ์Šต๋‹ˆ๋‹ค. Kotlin์€ ์ธ๋ผ์ธ์ด ์•„๋‹Œ ํ•จ์ˆ˜ ๋ฆฌํ„ฐ๋Ÿด์„ ํ†ตํ•œ ๋น„์ง€์—ญ ๋ฐ˜ํ™˜(non-local return)์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ปดํŒŒ์ผ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜์ • ๋ฐฉ๋ฒ•:
val url = NSURL.URLWithString(UIApplicationOpenSettingsURLString)
if (url != null) {
UIApplication.sharedApplication.openURL(url, mapOf<Any?, Any?>(), null)
}

์ฐธ๊ณ : https://kotlinlang.org/docs/returns.html#return-to-labels

private lateinit var applicationScope: CoroutineScope

override fun onCreate() {
androidAppGraph.inject(this)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p1] super.onCreate() ํ˜ธ์ถœ ์ด์ „์— DI inject ์‹คํ–‰ โ€” Android ๋ผ์ดํ”„์‚ฌ์ดํด ์œ„๋ฐ˜

androidAppGraph.inject(this)๊ฐ€ super.onCreate()๋ณด๋‹ค ๋จผ์ € ํ˜ธ์ถœ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Android์˜ Application.onCreate() ๊ณ„์•ฝ์ƒ super.onCreate()๊ฐ€ ๊ฐ€์žฅ ๋จผ์ € ํ˜ธ์ถœ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. super.onCreate() ์ด์ „์— inject๋ฅผ ์‹คํ–‰ํ•˜๋ฉด Context ์ดˆ๊ธฐํ™”๊ฐ€ ์™„๋ฃŒ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ์˜์กด์„ฑ์ด ์ฃผ์ž…๋˜์–ด ๋Ÿฐํƒ€์ž„ ํฌ๋ž˜์‹œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜์ •:
override fun onCreate() {
super.onCreate() // ๋ฐ˜๋“œ์‹œ ์ฒซ ๋ฒˆ์งธ
androidAppGraph.inject(this)
...
}

์ฐธ๊ณ : https://developer.android.com/reference/android/app/Application#onCreate()

val url = request.url

url.scheme?.takeIf { it == TOSS_SCHEME || it == "intent" }?.let {
val intent = Intent.parseUri(url.toString(), Intent.URI_INTENT_SCHEME)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p1] Intent Scheme ์ธ์ ์…˜ ์ทจ์•ฝ์  โ€” intent:// URL ์ฒ˜๋ฆฌ ์‹œ ์ปดํฌ๋„ŒํŠธ ๋ฏธ๊ฒ€์ฆ

Intent.parseUri()๋กœ ํŒŒ์‹ฑ๋œ intent๊ฐ€ setComponent(null), setSelector(null), addCategory(CATEGORY_BROWSABLE) ์—†์ด startActivity()์— ์ง์ ‘ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ์•…์˜์ ์ธ ์›น ํŽ˜์ด์ง€๊ฐ€ component= ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ง€์ •ํ•œ intent:// URL์„ ํ†ตํ•ด ์•ฑ ๋‚ด ๋น„๊ณต๊ฐœ Activity ๋˜๋Š” ๋‹ค๋ฅธ ์•ฑ์˜ Activity๋ฅผ ์ž„์˜๋กœ ์‹คํ–‰์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค (CWE-926, Android Intent Scheme ์ธ์ ์…˜).

์ˆ˜์ •:
val intent = Intent.parseUri(url.toString(), Intent.URI_INTENT_SCHEME).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
component = null
selector = null
}

์ฐธ๊ณ : https://developer.android.com/privacy-and-security/risks/intent-redirection

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p2 ์ด์Šˆ ์ธ๋ผ์ธ ์ฝ”๋ฉ˜ํŠธ

)

remember(state) {
state.webSettings.androidWebSettings.domStorageEnabled = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] commonMain์—์„œ Android ์ „์šฉ API ์‚ฌ์šฉ โ€” KMP ์œ„๋ฐ˜

androidWebSettings.domStorageEnabled = true๋Š” Android ์ „์šฉ WebSettings ์†์„ฑ์ž…๋‹ˆ๋‹ค. ์ด ์ฝ”๋“œ๊ฐ€ commonMain์˜ Composable ํ•จ์ˆ˜ ์•ˆ์— ์žˆ์–ด iOS ๋“ฑ ๋‹ค๋ฅธ ํ”Œ๋žซํผ์—์„œ๋„ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. Compose Multiplatform์—์„œ ํ”Œ๋žซํผ ์ „์šฉ ์„ค์ •์€ ๊ฐ ํ”Œ๋žซํผ์˜ actual ๊ตฌํ˜„์œผ๋กœ ๋ถ„๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ถŒ์žฅ ์ˆ˜์ •: androidWebSettings ์„ค์ •์„ expect/actual ํŒจํ„ด์œผ๋กœ ๋ถ„๋ฆฌํ•˜๊ฑฐ๋‚˜, FestatingWebView์˜ androidMain actual ๊ตฌํ˜„ ๋‚ด๋ถ€๋กœ ์ด๋™ํ•˜์„ธ์š”.

์ฐธ๊ณ : https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-expect-actual.html

if (current !is WaitingRegisterUiState.Success) return
if (!current.waitingRegister.canSubmit) return

viewModelScope.launch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] isSubmitting ๊ฐ€๋“œ๊ฐ€ ์ฝ”๋ฃจํ‹ด ๋‚ด๋ถ€์—์„œ ์„ค์ •๋จ โ€” ์ค‘๋ณต ์ œ์ถœ ๋ ˆ์ด์Šค ์ปจ๋””์…˜

canSubmit (isSubmitting ํฌํ•จ) ์ฒดํฌ๊ฐ€ launch { } ์ด์ „์— ์ด๋ฃจ์–ด์ง€์ง€๋งŒ, isSubmitting = true๋กœ์˜ ์ƒํƒœ ๋ณ€๊ฒฝ์€ launch { } ๋‚ด๋ถ€์—์„œ ๋น„๋™๊ธฐ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋‘ ๋ฒˆ์˜ ์—ฐ์† ํ˜ธ์ถœ์ด ๋ชจ๋‘ canSubmit = true๋ฅผ ์ฝ์€ ๋’ค ๊ฐ์ž launch๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด, ๋Œ€๊ธฐ ๋“ฑ๋ก์ด ์ค‘๋ณต์œผ๋กœ ์„œ๋ฒ„์— ์ „์†ก๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

WaitingRegisterScreen์—์„œ submitWaitingRegister()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ๋กœ๊ฐ€ ์„ธ ๊ณณ(onPermissionGrant, permissionCheckEvent, onConfirm)์ด์–ด์„œ ๊ถŒํ•œ ํ—ˆ์šฉ ์‹œ ๋‘ ๊ฒฝ๋กœ๊ฐ€ ๋™์‹œ์— ์‹คํ–‰๋˜์–ด ์ค‘๋ณต ์ œ์ถœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ถŒ์žฅ ์ˆ˜์ •: launch ํ˜ธ์ถœ ์ „์— AtomicBoolean ๋˜๋Š” StateFlow.compareAndSet์„ ์‚ฌ์šฉํ•ด isSubmitting์„ ๋™๊ธฐ์ ์œผ๋กœ ์„ค์ •ํ•˜์„ธ์š”.

์ฐธ๊ณ : https://kotlinlang.org/docs/shared-mutable-state-and-concurrency.html

val pendingIntent =
PendingIntent.getActivity(
context,
System.currentTimeMillis().toInt(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] System.currentTimeMillis().toInt() โ€” ์•Œ๋ฆผ ID ์ถฉ๋Œ ๋ฐ ์˜ค๋ฒ„ํ”Œ๋กœ ์œ„ํ—˜

System.currentTimeMillis()๋Š” Long ๊ฐ’์ด๋ฉฐ, .toInt() ๋ณ€ํ™˜ ์‹œ ์ƒ์œ„ 32๋น„ํŠธ๊ฐ€ ์ž˜๋ ค๋‚˜๊ฐ€ ์˜ˆ์ธก ๋ถˆ๊ฐ€ํ•œ ์Œ์ˆ˜ ID๊ฐ€ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ๋™์ผ ๋ฐ€๋ฆฌ์ดˆ ๋‚ด ๋‘ ์•Œ๋ฆผ์ด ๋„์ฐฉํ•˜๋ฉด ๊ฐ™์€ ID๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‘ ๋ฒˆ์งธ ์•Œ๋ฆผ์ด ์ฒซ ๋ฒˆ์งธ ์•Œ๋ฆผ์„ ๋ฎ์–ด์”๋‹ˆ๋‹ค.

๊ฐ™์€ ํŒจํ„ด์ด line 85 (notificationManager.notify)์—๋„ ๋ฐ˜๋ณต ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

๊ถŒ์žฅ ์ˆ˜์ •: AtomicInteger๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋‹จ์กฐ ์ฆ๊ฐ€ ์นด์šดํ„ฐ๋กœ ๊ต์ฒดํ•˜์„ธ์š”.

์ฐธ๊ณ : https://developer.android.com/develop/ui/views/notifications/build-notification#notify

class ErrorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Thread.setDefaultUncaughtExceptionHandler(null)
enableEdgeToEdge()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] enableEdgeToEdge()๊ฐ€ super.onCreate() ์ด์ „์— ํ˜ธ์ถœ๋จ

enableEdgeToEdge()๋Š” Window ๊ฐ์ฒด์— ํ”Œ๋ž˜๊ทธ๋ฅผ ์ ์šฉํ•˜๋Š”๋ฐ, Window ๊ฐ์ฒด๋Š” super.onCreate() ์‹คํ–‰ ํ›„ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค. super.onCreate() ์ด์ „์— ํ˜ธ์ถœํ•˜๋ฉด ์ผ๋ถ€ API ๋ ˆ๋ฒจ์—์„œ NullPointerException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜์ •:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) // ๋ฐ˜๋“œ์‹œ ์ฒซ ๋ฒˆ์งธ
enableEdgeToEdge()
setContent { ... }
}

์ฐธ๊ณ : https://developer.android.com/develop/ui/views/layout/edge-to-edge

buildMap {
params?.let { putAll(it) }
put("festival_id", festivalId.value ?: -1)
} as? Map<Any?, Any?>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] as? ์บ์ŠคํŠธ ์‹คํŒจ ์‹œ analytics ํŒŒ๋ผ๋ฏธํ„ฐ ์ „์ฒด ์†Œ์‹ค

buildMap { ... } as? Map<Any?, Any?> ์บ์ŠคํŠธ๊ฐ€ null์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด, festival_id๋ฅผ ํฌํ•จํ•œ ๋ชจ๋“  ์ด๋ฒคํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ logEventWithName(name, null)๋กœ ์ „๋‹ฌ๋˜์–ด Firebase์— ํŒŒ๋ผ๋ฏธํ„ฐ ์—†์ด ์ด๋ฒคํŠธ๊ฐ€ ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค. ์‹คํŒจ ๋กœ๊ทธ๋‚˜ ํด๋ฐฑ์ด ์—†์Šต๋‹ˆ๋‹ค.

๊ถŒ์žฅ ์ˆ˜์ •: as? ๋Œ€์‹  as (๊ฐ•์ œ ์บ์ŠคํŠธ + @Suppress) ๋˜๋Š” ์ฒ˜์Œ๋ถ€ํ„ฐ mutableMapOf<Any?, Any?>()๋กœ ๋งต์„ ๊ตฌ์„ฑํ•˜์—ฌ ์บ์ŠคํŠธ ์ž์ฒด๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.

val mergedParams: Map<Any?, Any?> = buildMap {
params?.let { putAll(it) }
put("festival_id", festivalId.value ?: -1)
}


expect fun shouldShowRationale(
permission: String,
activity: Any?,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] expect fun์˜ activity: Any? โ€” Android ์ถ”์ƒํ™” ๋ˆ„์ˆ˜

activity: Any? ํƒ€์ž… ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” Android Activity๋ฅผ commonMain์—์„œ ์ง์ ‘ ์ฐธ์กฐํ•  ์ˆ˜ ์—†์–ด ํƒ€์ž…์„ ์ง€์šด ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Š” commonMain API์— Android ์ „์šฉ ์˜๋ฏธ๋ก ์ด ๋…ธ์ถœ๋˜๋Š” expect/actual ์„ค๊ณ„ ์œ„๋ฐ˜์ž…๋‹ˆ๋‹ค. iOS actual์—์„œ๋Š” ์ด ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์˜๋ฏธ๊ฐ€ ์—†์œผ๋ฉฐ, Android actual์—์„œ๋Š” Any?๋ฅผ Activity๋กœ unsafeCastํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ถŒ์žฅ ์ˆ˜์ •: activity ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‹œ๊ทธ๋‹ˆ์ฒ˜์—์„œ ์ œ๊ฑฐํ•˜๊ณ , Android actual ๊ตฌํ˜„ ๋‚ด๋ถ€์—์„œ LocalContext.current ๋˜๋Š” LocalActivity.current๋ฅผ ํ†ตํ•ด Activity๋ฅผ ํš๋“ํ•˜์„ธ์š”.

์ฐธ๊ณ : https://kotlinlang.org/docs/multiplatform-expect-actual.html

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ฝ”๋“œ ๋ฆฌ๋ทฐ (PR #186 โ€” Release 2.1.1)

๋ณ€๊ฒฝ๋œ ํŒŒ์ผ์„ Compose Multiplatform / Android / iOS ๊ด€์ ์—์„œ ๊ฒ€ํ† ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด 14๊ฐœ ์ด์Šˆ๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์œผ๋ฉฐ ์ค‘์š”๋„์— ๋”ฐ๋ผ p1/p2/p3์œผ๋กœ ๋ถ„๋ฅ˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

val intent = Intent.parseUri(url.toString(), Intent.URI_INTENT_SCHEME)
val packageManager = view.context.packageManager
if (intent.resolveActivity(packageManager) != null) {
view.context.startActivity(intent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] ๐Ÿ”ด ๋ณด์•ˆ: intent:// URL ์Šคํ‚ด ์ธ์ ์…˜ ์ทจ์•ฝ์ 

Intent.parseUri()๋กœ ํŒŒ์‹ฑ๋œ ์ธํ…ํŠธ๋ฅผ ํŒจํ‚ค์ง€๋ช…/์ปดํฌ๋„ŒํŠธ/์•ก์…˜์— ๋Œ€ํ•œ ํ—ˆ์šฉ ๋ชฉ๋ก ๊ฒ€์ฆ ์—†์ด startActivity()๋กœ ์ง์ ‘ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

WebView ๋‚ด์—์„œ ๋กœ๋“œ๋˜๋Š” ์•…์˜์ ์ธ ํŽ˜์ด์ง€๊ฐ€ ์ž„์˜์˜ ์•ฑ์ด๋‚˜ ๋‚ด๋ถ€ ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์‹คํ–‰ํ•˜๋„๋ก ์กฐ์ž‘๋œ intent:// URL์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ: intent://#Intent;component=com.victim.app/.SecretActivity;end

์ˆ˜์ • ๋ฐฉ๋ฒ•:

val intent = Intent.parseUri(url.toString(), Intent.URI_INTENT_SCHEME)
intent.addCategory(Intent.CATEGORY_BROWSABLE)
intent.component = null
intent.selector = null
if (intent.`package` != TOSS_PACKAGE_NAME) return false

์ฐธ๊ณ : Android WebView ๋ณด์•ˆ ๊ฐ€์ด๋“œ


val packageName = intent.`package` ?: TOSS_PACKAGE_NAME
val marketIntent = Intent(Intent.ACTION_VIEW, "market://details?id=$packageName".toUri())
view.context.startActivity(marketIntent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] โš ๏ธ ๊ณต๊ฒฉ์ž๊ฐ€ ์ œ์–ดํ•˜๋Š” ํŒจํ‚ค์ง€๋ช…์œผ๋กœ Play Store๊ฐ€ ์—ด๋ฆด ์ˆ˜ ์žˆ์Œ

intent.package๋Š” intent:// URL์—์„œ ํŒŒ์‹ฑ๋œ ๊ฐ’์ด๋ฉฐ ๊ณต๊ฒฉ์ž๊ฐ€ ์ž„์˜๋กœ ์ง€์ • ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ?: TOSS_PACKAGE_NAME์€ null์ธ ๊ฒฝ์šฐ์—๋งŒ ๊ธฐ๋ณธ๊ฐ’์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ์•…์˜์ ์ธ ํŽ˜์ด์ง€๊ฐ€ intent://#Intent;package=com.evil.phishing;end URL์„ ์ œ๊ณตํ•˜๋ฉด market://details?id=com.evil.phishing์œผ๋กœ Play Store๊ฐ€ ์—ด๋ฆฝ๋‹ˆ๋‹ค.

์ˆ˜์ • ๋ฐฉ๋ฒ•:

val packageName = intent.`package`
if (packageName != TOSS_PACKAGE_NAME) return true
val marketIntent = Intent(Intent.ACTION_VIEW, "market://details?id=$packageName".toUri())
view.context.startActivity(marketIntent)

private lateinit var applicationScope: CoroutineScope

override fun onCreate() {
androidAppGraph.inject(this)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] โš ๏ธ super.onCreate() ํ˜ธ์ถœ ์ „์— Metro DI inject() ์‹คํ–‰

Android Application.onCreate()์—์„œ๋Š” super.onCreate()๊ฐ€ ๊ฐ€์žฅ ๋จผ์ € ํ˜ธ์ถœ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. androidAppGraph.inject(this)๋ฅผ ๋จผ์ € ํ˜ธ์ถœํ•˜๋ฉด Metro DI ๊ทธ๋ž˜ํ”„๊ฐ€ Application ๊ธฐ๋ฐ˜ ํด๋ž˜์Šค ์ดˆ๊ธฐํ™” ์ „์— ์ƒ์„ฑ๋˜์–ด, ์ฃผ์ž…๋œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ Context๋‚˜ ์‹œ์Šคํ…œ ์„œ๋น„์Šค์— ์ ‘๊ทผํ•  ๋•Œ ์ผ๋ถ€ OEM ๊ธฐ๊ธฐ์—์„œ ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ ํฌ๋ž˜์‹œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜์ •:

override fun onCreate() {
    super.onCreate()
    androidAppGraph.inject(this)
    ...
}

์ฐธ๊ณ : Android Application.onCreate() ๋ฌธ์„œ

e: Throwable,
) {
Napier.e(e.stackTraceToString())
application.startActivity(ErrorActivity.newIntent(application, e))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] โš ๏ธ ๊ธ€๋กœ๋ฒŒ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ์—์„œ Throwable์„ Intent extra๋กœ ์ง๋ ฌํ™” โ€” ํ•ธ๋“ค๋Ÿฌ ์ž์ฒด๊ฐ€ ํฌ๋ž˜์‹œํ•  ์œ„ํ—˜

Throwable์€ Serializable์„ ๊ตฌํ˜„ํ•˜์ง€๋งŒ, ์‹ค์ œ ์˜ˆ์™ธ ๊ฐ์ฒด์— ์ง๋ ฌํ™” ๋ถˆ๊ฐ€๋Šฅํ•œ ํ•„๋“œ(์˜ˆ: Context, View, OkHttp/Coroutines ๋‚ด๋ถ€ ๊ฐ์ฒด ๋“ฑ)๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ Android๊ฐ€ Intent๋ฅผ parcelizeํ•  ๋•Œ RuntimeException์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ๊ธ€๋กœ๋ฒŒ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ ์ž์ฒด๊ฐ€ ์‹คํŒจํ•˜์—ฌ ErrorActivity๊ฐ€ ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ˆ˜์ • ๋ฐฉ๋ฒ•:

fun newIntent(context: Context, error: Throwable): Intent =
    Intent(context, ErrorActivity::class.java).apply {
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
        putExtra(KEY_ERROR_CLASS, error.javaClass.name)
        putExtra(KEY_ERROR_MESSAGE, error.message)
        putExtra(KEY_ERROR_STACK, error.stackTraceToString())
    }

์ฐธ๊ณ : Android Parcel ๋ฌธ์„œ

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3 ์ด์Šˆ ์ธ๋ผ์ธ ์ฝ”๋ฉ˜ํŠธ

}
}

@Suppress("UNCHECKED_CAST")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p3] ๋ถˆํ•„์š”ํ•œ @Suppress("UNCHECKED_CAST") โ€” ํ•จ์ˆ˜ ๋‚ด ์บ์ŠคํŠธ ์—†์Œ

setLightTheme() ํ•จ์ˆ˜์— @Suppress("UNCHECKED_CAST") ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์–ด ์žˆ์ง€๋งŒ, ํ•จ์ˆ˜ ๋ณธ๋ฌธ์—๋Š” ์บ์ŠคํŠธ๊ฐ€ ์ „ํ˜€ ์—†์Šต๋‹ˆ๋‹ค. ์ด ์–ด๋…ธํ…Œ์ด์…˜์€ ์ฝ”๋“œ ๋…์ž์—๊ฒŒ ํ˜ผ๋ž€์„ ์ฃผ๋ฉฐ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜์ •: @Suppress("UNCHECKED_CAST") ์–ด๋…ธํ…Œ์ด์…˜์„ ์ œ๊ฑฐํ•˜์„ธ์š”.

val url = decidePolicyForNavigationAction.request.URL
val scheme = url?.scheme

Napier.d("scheme:$scheme")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p3] ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์— Napier.d() ๋””๋ฒ„๊ทธ ๋กœ๊ทธ โ€” URL ์ •๋ณด ๋…ธ์ถœ

Napier.d("scheme:$scheme")์™€ Napier.d("nsUrl:$nsUrl")๊ฐ€ ๋นŒ๋“œ ํ”Œ๋ž˜๋ฒ„ ๊ตฌ๋ถ„ ์—†์ด ํ”„๋กœ๋•์…˜์—์„œ๋„ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. WebView ํƒ์ƒ‰๋งˆ๋‹ค ๋‚ด๋ถ€ URL ์ „์ฒด๊ฐ€ ๋กœ๊ทธ์— ๊ธฐ๋ก๋˜์–ด ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜์ •: ๋””๋ฒ„๊ทธ ๋กœ๊ทธ๋ฅผ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜, FestabookApp์—์„œ Android์ฒ˜๋Ÿผ DEBUG ๋นŒ๋“œ์—์„œ๋งŒ Napier ๋ฒ ์ด์Šค๋ฅผ ๋“ฑ๋กํ•˜๋„๋ก iOS Napier ์„ค์ •์„ ๋ถ„๋ฆฌํ•˜์„ธ์š”.

private val center = UNUserNotificationCenter.currentNotificationCenter()

actual suspend fun checkPermission(): PermissionState =
suspendCancellableCoroutine { const ->

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p3] const๋ฅผ ๋žŒ๋‹ค ํŒŒ๋ผ๋ฏธํ„ฐ ์ด๋ฆ„์œผ๋กœ ์‚ฌ์šฉ โ€” Kotlin ์†Œํ”„ํŠธ ํ‚ค์›Œ๋“œ์™€ ์ถฉ๋Œ

suspendCancellableCoroutine { const -> ... }์—์„œ const๋Š” Kotlin ์†Œํ”„ํŠธ ํ‚ค์›Œ๋“œ(const val ์„ ์–ธ์— ์‚ฌ์šฉ)์ž…๋‹ˆ๋‹ค. ์ปดํŒŒ์ผ์€ ๋˜์ง€๋งŒ ์ฝ”๋“œ ๋…์ž์—๊ฒŒ ํ˜ผ๋ž€์„ ์ค๋‹ˆ๋‹ค. continuation ๋˜๋Š” cont๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜์ •:
suspendCancellableCoroutine { cont ->
center.getNotificationSettingsWithCompletionHandler { settings ->
...
cont.resume(result)
}
}

์ฐธ๊ณ : https://kotlinlang.org/docs/keyword-reference.html

content: String,
action: FcmDeepLinkAction?,
) {
if (festivalId != DeepLinkKeys.INITIALIZED_ID) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] โš ๏ธ FCM ๋ฐ์ดํ„ฐ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ์‹œ festivalId๊ฐ€ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ์—†์ด ๋ฎ์–ด์”Œ์›Œ์ง

onMessageReceived()์—์„œ FCM ๋ฐ์ดํ„ฐ ํŽ˜์ด๋กœ๋“œ์˜ festivalId๋ฅผ ๋ฌด์กฐ๊ฑด ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์•Œ๋ฆผ์„ ํƒญํ•˜์ง€ ์•Š์•„๋„ ๋ฐœ์ƒํ•˜๋ฉฐ, FCM ํ† ํ”ฝ/๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ๋กœ ์ž„์˜์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ํ˜„์žฌ ๋ณด๊ณ  ์žˆ๋Š” ํŽ˜์Šคํ‹ฐ๋ฒŒ์ด ์กฐ์šฉํžˆ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

iOS ๊ตฌํ˜„(DefaultUserNotificationDelegate)์€ ์‚ฌ์šฉ์ž๊ฐ€ ์•Œ๋ฆผ์„ ํƒญ(didReceiveNotificationResponse)ํ•  ๋•Œ๋งŒ festivalId๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ๊ณผ ๋Œ€์กฐ์ ์ž…๋‹ˆ๋‹ค.

์ˆ˜์ • ๋ฐฉ๋ฒ•: deep link action ์ฝœ๋ฐฑ(์•Œ๋ฆผ ํƒญ ์ด๋ฒคํŠธ)์—์„œ๋งŒ festivalId๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ฑฐ๋‚˜, ํ˜„์žฌ ์ €์žฅ๋œ ๊ฐ’๊ณผ ๋น„๊ต ํ›„ ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ์—๋งŒ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์„ธ์š”.

application.androidAppGraph.coroutineScope
}

override fun onNewToken(token: String) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] โš ๏ธ onNewToken()์ด ์•„๋ฌด ๋™์ž‘๋„ ํ•˜์ง€ ์•Š์•„ FCM ํ† ํฐ ๊ฐฑ์‹  ์‹œ ์•Œ๋ฆผ์ด ์ˆ˜์‹ ๋˜์ง€ ์•Š์Œ

FCM์€ ์•ฑ ์žฌ์„ค์น˜, ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”, ์ฃผ๊ธฐ์ ์ธ ํ† ํฐ ์ˆœํ™˜ ์‹œ onNewToken()์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. FestabookApp.setupDeviceIdentifiers()๋Š” ์•ฑ ์ตœ์ดˆ ์‹คํ–‰ ์‹œ์—๋งŒ ํ† ํฐ์„ ๋ฐฑ์—”๋“œ์— ๋“ฑ๋กํ•˜๋ฏ€๋กœ, ์ดํ›„ ํ† ํฐ์ด ๊ฐฑ์‹ ๋˜๋ฉด ๋ฐฑ์—”๋“œ๋Š” ๋งŒ๋ฃŒ๋œ ํ† ํฐ์„ ๋ณด์œ ํ•˜๊ฒŒ ๋˜๊ณ  FCM ์•Œ๋ฆผ์ด ๋” ์ด์ƒ ์ „๋‹ฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ˆ˜์ • ๋ฐฉ๋ฒ•:

override fun onNewToken(token: String) {
    ioCoroutineScope.launch {
        deviceRepository.registerDevice(token)
    }
}

์ฐธ๊ณ : FCM ํ† ํฐ ๊ฐฑ์‹  ์ฒ˜๋ฆฌ


val pendingIntent =
PendingIntent.getActivity(
context,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] โš ๏ธ currentTimeMillis().toInt() ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ๋กœ ์•Œ๋ฆผ ID ์ถฉ๋Œ

System.currentTimeMillis()๋Š” ํ˜„์žฌ ์•ฝ 1.7์กฐ(ms)๋กœ Int.MAX_VALUE(์•ฝ 21์–ต)๋ฅผ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค. toLong().toInt()๋Š” ์ƒ์œ„ ๋น„ํŠธ๋ฅผ ์ž˜๋ผ๋‚ด์–ด ์•ฝ 49.7์ผ๋งˆ๋‹ค ์‚ฌ์ดํด์ด ๋ฐ˜๋ณต๋ฉ๋‹ˆ๋‹ค. ๋™์ผํ•œ ์‚ฌ์ดํด ๋‚ด์—์„œ ๊ฐ™์€ ID๊ฐ€ ์ƒ์„ฑ๋˜๋ฉด ์ด์ „ ์•Œ๋ฆผ์ด ์ƒˆ ์•Œ๋ฆผ์œผ๋กœ ์กฐ์šฉํžˆ ๊ต์ฒด๋˜๊ณ , PendingIntent requestCode๋„ ์ถฉ๋Œํ•˜์—ฌ ์ž˜๋ชป๋œ extras๋กœ ์ธํ…ํŠธ๊ฐ€ ์‹คํ–‰๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

L84์—์„œ๋„ notificationManager.notify()์— ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜์ • ๋ฐฉ๋ฒ•:

private val notificationIdCounter = java.util.concurrent.atomic.AtomicInteger(0)

// ์‚ฌ์šฉ ์‹œ:
val id = notificationIdCounter.incrementAndGet()
PendingIntent.getActivity(context, id, intent, ...)
notificationManager.notify(id, notificationBuilder.build())

์ฐธ๊ณ : Android ์•Œ๋ฆผ ๊ฐ€์ด๋“œ

val notificationBuilder =
NotificationCompat
.Builder(context, action.toChannelId())
.setSmallIcon(R.drawable.ic_festabook_logo_notification_small)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] ์•Œ๋ฆผ setColor()์— ํฐ์ƒ‰ ์„ค์ • โ€” ์•Œ๋ฆผ ํŒจ๋„์—์„œ ์•„์ด์ฝ˜์ด ๋ณด์ด์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ

setColor()๋Š” ์•Œ๋ฆผ์˜ ์ž‘์€ ์•„์ด์ฝ˜ ๋ฐ ํ—ค์ฆˆ์—… ์•Œ๋ฆผ์˜ ๊ฐ•์กฐ ์ƒ‰์ƒ์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ํฐ์ƒ‰(FestabookColorPalette().white)์œผ๋กœ ์„ค์ •ํ•˜๋ฉด ๊ธฐ๋ณธ ํฐ์ƒ‰ ์•Œ๋ฆผ ๋ฐฐ๊ฒฝ์—์„œ ์•„์ด์ฝ˜์ด ๊ฑฐ์˜ ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Android 12+์—์„œ๋Š” ํ™•์žฅ๋œ ์•Œ๋ฆผ ๋ฐฐ๊ฒฝ์—๋„ ์˜ํ–ฅ์„ ์ค๋‹ˆ๋‹ค.

๊ถŒ์žฅ: ์•ฑ ๋ธŒ๋žœ๋“œ ์ƒ‰์ƒ(ํฐ์ƒ‰/ํˆฌ๋ช…์ด ์•„๋‹Œ ์ƒ‰์ƒ)์„ ์‚ฌ์šฉํ•˜์„ธ์š”.

์ฐธ๊ณ : Android ์•Œ๋ฆผ ๋””์ž์ธ ๊ฐ€์ด๋“œ

companion object {
fun newIntent(context: Context) =
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] โš ๏ธ FLAG_ACTIVITY_CLEAR_TASK๋กœ ์•Œ๋ฆผ ํƒญ ์‹œ ์ „์ฒด ๋ฐฑ์Šคํƒ์ด ์ดˆ๊ธฐํ™”๋จ

FLAG_ACTIVITY_CLEAR_TASK๋Š” ์ธํ…ํŠธ ์‹คํ–‰ ์ „ ๊ธฐ์กด ํƒœ์Šคํฌ์˜ ๋ชจ๋“  ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์„ ์‚ฌ์šฉ ์ค‘์— ์•Œ๋ฆผ์„ ํƒญํ•˜๋ฉด ํ˜„์žฌ ํ™”๋ฉด ์ƒํƒœ(์ž‘์„ฑ ์ค‘์ธ ํผ, ์„ ํƒ๋œ ํŽ˜์Šคํ‹ฐ๋ฒŒ ๋“ฑ)๊ฐ€ ๋ชจ๋‘ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ RememberDeepLinkHandler๊ฐ€ addOnNewIntentListener๋กœ ์ƒˆ ์ธํ…ํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ตฌ์กฐ์™€๋„ ์ถฉ๋Œํ•ฉ๋‹ˆ๋‹ค โ€” CLEAR_TASK๊ฐ€ ์„ค์ •๋˜๋ฉด ๊ธฐ์กด ์ธ์Šคํ„ด์Šค๊ฐ€ ํŒŒ๊ดด๋˜๋ฏ€๋กœ onNewIntent() ๊ฒฝ๋กœ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ˆ˜์ • ๋ฐฉ๋ฒ•:

fun newIntent(context: Context) =
    Intent(context, MainActivity::class.java).apply {
        flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
    }

(AndroidManifest.xml์—์„œ launchMode="singleTop" ์„ค์ • ๊ถŒ์žฅ)

์ฐธ๊ณ : Android ์•Œ๋ฆผ ๋”ฅ๋งํฌ ๊ฐ€์ด๋“œ

import festabookkmp.composeapp.generated.resources.waiting_register_title
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] ํ•˜๋“œ์ฝ”๋”ฉ๋œ Notion URL โ€” ์ƒ์ˆ˜ ๋˜๋Š” ์„ค์ • ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌ ํ•„์š”

๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ ๋“ฑ ๋ฒ•์  ๋ฌธ์„œ ๋งํฌ๊ฐ€ Composable ํŒŒ์ผ์— private const๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. URL์ด ๋ณ€๊ฒฝ๋  ๊ฒฝ์šฐ(Notion ๋งํฌ๋Š” ์ž์ฃผ ๋ณ€๊ฒฝ๋จ) ์ฝ”๋“œ ์ˆ˜์ • ๋ฐ ์•ฑ ์žฌ๋ฐฐํฌ๊ฐ€ ํ•„์š”ํ•˜๋ฉฐ, ๋ณ€๊ฒฝ์ด ํ•„์š”ํ•œ URL์ด ์–ด๋””์— ์žˆ๋Š”์ง€ ์ฐพ๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

๊ถŒ์žฅ: BuildKonfig์— PRIVACY_AGREEMENT_URL ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ์•ฑ ์„ค์ • ํŒŒ์ผ(AppConfig.kt ๋“ฑ)์˜ ์ค‘์•™ ์ƒ์ˆ˜๋กœ ๋ถ„๋ฆฌํ•˜์„ธ์š”.

์ฐธ๊ณ : Kotlin Multiplatform BuildKonfig ํ”Œ๋Ÿฌ๊ทธ์ธ


companion object {
private val platform =
when (getPlatform()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] commonMain ๋ฐ์ดํ„ฐ ๊ณ„์ธต์—์„œ getPlatform() ๋ถ„๊ธฐ ๋ฐ ํ”Œ๋žซํผ ๋ฌธ์ž์—ด ํ•˜๋“œ์ฝ”๋”ฉ

๋‘ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์„œ๋ฒ„ ๊ณ„์•ฝ: API์— ์ „๋‹ฌ๋˜๋Š” "android"/"ios" ๋ฌธ์ž์—ด์ด ์ƒ์ˆ˜ ์—†์ด ์ฝ”๋“œ์— ์ง์ ‘ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  2. getPlatform() ๋ถ„๊ธฐ ์•ˆํ‹ฐํŒจํ„ด: ํ”Œ๋žซํผ๋ณ„ ๊ฐ’์€ expect/actual ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด๋‚˜ DI ์ฃผ์ž…์„ ํ†ตํ•ด ์ œ๊ณต๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. getPlatform()์œผ๋กœ ๋ถ„๊ธฐํ•˜๋Š” ๊ฒƒ์€ ์ƒˆ ํ”Œ๋žซํผ ์ถ”๊ฐ€ ์‹œ ๋†“์น˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค.

๊ถŒ์žฅ ์ˆ˜์ •:

// Platform.kt์— ํ™•์žฅ ํ”„๋กœํผํ‹ฐ ์ถ”๊ฐ€
val Platform.apiValue: String get() = when (this) {
    Platform.ANDROID -> "android"
    Platform.IOS -> "ios"
}

// ์‚ฌ์šฉ
companion object {
    private val platform = getPlatform().apiValue
}

์ฐธ๊ณ : KMP expect/actual ๋ฌธ์„œ

mainNavigator.navigateToMainTab(FestabookMainTab.SCHEDULE)
}

NavigationBackHandler(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] commonMain UI์—์„œ getPlatform() ๋ถ„๊ธฐ ์‚ฌ์šฉ โ€” expect/actual ํŒจํ„ด ๊ถŒ์žฅ

@Composable ํ•จ์ˆ˜ ๋‚ด์—์„œ getPlatform() == Platform.ANDROID ์กฐ๊ฑด์œผ๋กœ ํ”Œ๋žซํผ๋ณ„ ๋™์ž‘์„ ๋ถ„๊ธฐํ•˜๋Š” ๊ฒƒ์€ ๊ณตํ†ต UI ์ฝ”๋“œ์— ํ”Œ๋žซํผ ์ง€์‹์„ ๋‚ด์žฌํ™”ํ•˜๋Š” ์•ˆํ‹ฐํŒจํ„ด์ž…๋‹ˆ๋‹ค. if (Build.VERSION.SDK_INT > ...) ๊ตฌ๋ฌธ์„ commonMain์— ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด ํ”Œ๋žซํผ(๋ฐ์Šคํฌํƒ‘ ๋“ฑ)์ด ์ถ”๊ฐ€๋  ๊ฒฝ์šฐ ์ด ์กฐ๊ฑด์ด ์กฐ์šฉํžˆ ๋ฌด์‹œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ถŒ์žฅ ์ˆ˜์ •:

// commonMain
expect fun handleNavigationBackPress(onBackPressed: () -> Unit)

// androidMain
actual fun handleNavigationBackPress(onBackPressed: () -> Unit) { onBackPressed() }

// iosMain
actual fun handleNavigationBackPress(onBackPressed: () -> Unit) { /* no-op */ }

์ฐธ๊ณ : KMP expect/actual ๋ฌธ์„œ

expect fun shouldShowRationale(
permission: String,
activity: Any?,
): Boolean

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] commonMain์˜ expect fun์— Android ์ „์šฉ ๊ฐœ๋…(activity: Any?)์ด ๋…ธ์ถœ๋จ

activity: Any? ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” Android Activity ๊ฐœ๋…์œผ๋กœ, iOS์—์„œ๋Š” ์˜๋ฏธ ์—†๋Š” null์„ ๋ฐ›๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. Any? ํƒ€์ž…์ด๊ธฐ ๋•Œ๋ฌธ์— ์ปดํŒŒ์ผ ์‹œ ํƒ€์ž… ์•ˆ์ „์„ฑ์ด ๋ณด์žฅ๋˜์ง€ ์•Š์œผ๋ฉฐ, Android actual ๊ตฌํ˜„์—์„œ Activity๋กœ ๊ฐ•์ œ ํ˜•๋ณ€ํ™˜ํ•  ๋•Œ ๋Ÿฐํƒ€์ž„ ํฌ๋ž˜์‹œ ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.

ํ”Œ๋žซํผ ์ „์šฉ ํƒ€์ž…์ด commonMain expect ์‹œ๊ทธ๋‹ˆ์ฒ˜์— ๋…ธ์ถœ๋˜๋Š” ๊ฒƒ์€ KMP ์„ค๊ณ„ ์›์น™์— ์œ„๋ฐฐ๋ฉ๋‹ˆ๋‹ค.

๊ถŒ์žฅ ์ˆ˜์ •: ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  Android actual ๊ตฌํ˜„์—์„œ Compose ์ปจํ…์ŠคํŠธ๋ฅผ ํ†ตํ•ด ์ง์ ‘ ํš๋“ํ•˜์„ธ์š”.

// commonMain
expect fun shouldShowRationale(permission: String): Boolean

// androidMain
actual fun shouldShowRationale(permission: String): Boolean {
    val activity = LocalContext.current as? Activity ?: return false
    return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}

์ฐธ๊ณ : KMP expect/actual ๋ชจ๋ฒ” ์‚ฌ๋ก€

modifier: Modifier = Modifier,
)

const val TOSS_SCHEME = "supertoss"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] TOSS_SCHEME ์ƒ์ˆ˜๊ฐ€ expect fun ํŒŒ์ผ์— ์œ„์น˜ โ€” ๋ถ„๋ฆฌ ๊ถŒ์žฅ

๊ฒฐ์ œ ๊ณต๊ธ‰์ž์˜ URL ์Šคํ‚ด์ด ํ”Œ๋žซํผ ๋””์ŠคํŒจ์น˜ ์ฝ”๋“œ(expect fun)์™€ ๋™์ผํ•œ ํŒŒ์ผ์— ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์Šคํ‚ด ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๊ฒฝ์šฐ ์ฐพ๊ธฐ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ƒ์ˆ˜์˜ ์˜๋ฏธ๊ฐ€ ๋ชจํ˜ธํ•ฉ๋‹ˆ๋‹ค.

๊ถŒ์žฅ: ์•ฑ์˜ ๋”ฅ๋งํฌ/URL ์Šคํ‚ด ์ƒ์ˆ˜๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ณ„๋„ ํŒŒ์ผ(์˜ˆ: AppUrlSchemes.kt ๋˜๋Š” DeepLinkKeys.kt)๋กœ ์ด๋™ํ•˜์„ธ์š”.

* **build: iOS ๋นŒ๋“œ ๋ฒˆํ˜ธ ์—…๋ฐ์ดํŠธ ๋ฐ ๋””๋ฒ„๊ทธ ํ‘œ์‹œ ๋ช…์นญ ์ˆ˜์ •**
    * `CFBundleVersion`์„ `2010103`์—์„œ `2010106`์œผ๋กœ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
    * ๋””๋ฒ„๊ทธ ๋นŒ๋“œ์˜ ์•ฑ ํ‘œ์‹œ ๋ช…์นญ(`CFBundleDisplayName`) ํ˜•์‹์„ `festabook(Debug)`์—์„œ `(Debug)festabook`์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

* **style: Compose UI ์ฝ”๋“œ ํฌ๋งทํŒ… ๋ฐ ๊ฐ€๋…์„ฑ ๊ฐœ์„ **
    * `MyWaitingScreen`, `WaitingRegisterScreen`, `PlaceDetailScreen`, `ExploreSearchResultList` ๋‚ด `logClick` ํ˜ธ์ถœ๋ถ€์˜ ๋“ค์—ฌ์“ฐ๊ธฐ ๋ฐ ์ฝ”๋“œ ๊ตฌ์กฐ๋ฅผ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `PlaceWaitingContent` ๋ฐ `WaitingStatusCard` ๋‚ด ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‚ฌ์–‘(`animationSpec`) ์„ค์ • ์ฝ”๋“œ์˜ ํฌ๋งท์„ ์ผ๊ด€๋˜๊ฒŒ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    * `ExploreSearchResultList` ๋‚ด ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ์ˆœ์„œ๋ฅผ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ฝ”๋“œ ๋ฆฌ๋ทฐ ๊ฒฐ๊ณผ (Release 2.1.1)

๋ณ€๊ฒฝ๋œ ํŒŒ์ผ ์ค‘์‹ฌ์œผ๋กœ ์ด์Šˆ๋ฅผ ๊ฒ€ํ† ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ์ธ๋ผ์ธ ์ฝ”๋ฉ˜ํŠธ์— ์šฐ์„ ์ˆœ์œ„(p1 Critical / p2 Important)๋ฅผ ๋ช…์‹œํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ์ด์Šˆ ์š”์•ฝ

์šฐ์„ ์ˆœ์œ„ ์ด์Šˆ ํŒŒ์ผ
p1 super.onCreate() ์ „ DI ์ฃผ์ž… FestabookApp.kt
p1 Long.toInt() ์˜ค๋ฒ„ํ”Œ๋กœ NotificationHelper.kt
p1 onNewToken no-op MyFirebaseMessagingService.kt
p1 Intent ์Šคํ‚ด ์ธ์ ์…˜ (๋ณด์•ˆ) AndroidWebViewClient.kt
p1 iOS completion handler ๋ฏธ๋ณด์žฅ DefaultUserNotificationDelegate.kt
p2 market:// ActivityNotFoundException AndroidWebViewClient.kt
p2 ์ž˜๋ชป๋œ ViewModelKey import FestatingViewModel.kt
p2 Analytics ์Šคํฌ๋ฆฐ๋ช… ์˜คํƒ€ FestatingScreen.kt
p2 NSException ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ๋ˆ„๋ฝ DefaultFestabookAppDelegate.kt
p2 ํ•˜๋“œ์ฝ”๋”ฉ๋œ URL (3๊ฐœ ํŒŒ์ผ) SettingScreen.kt, AddWaitingInfoScreen.kt, WaitingRegisterScreen.kt

Comment on lines +37 to 38
androidAppGraph.inject(this)
super.onCreate()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p1] super.onCreate() ํ˜ธ์ถœ ์ˆœ์„œ ์˜ค๋ฅ˜

androidAppGraph.inject(this)๊ฐ€ super.onCreate() ์ด์ „์— ํ˜ธ์ถœ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Android Application.onCreate()์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ super.onCreate()๋ฅผ ๋จผ์ € ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Firebase๋Š” FirebaseInitProvider(ContentProvider)๋ฅผ ํ†ตํ•ด ์ดˆ๊ธฐํ™”๋˜๋ฉฐ, ์ด๋Š” super.onCreate() ์ดํ›„์— ์™„๋ฃŒ๊ฐ€ ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค. ์ฃผ์ž…๋œ FirebaseMessaging, DeviceRepository ๋“ฑ์ด ์•„์ง ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ์ ‘๊ทผ๋˜๋ฉด ์ฝœ๋“œ ์Šคํƒ€ํŠธ ์‹œ ํฌ๋ž˜์‹œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ : Application.onCreate() ๊ณต์‹ ๋ฌธ์„œ

Suggested change
androidAppGraph.inject(this)
super.onCreate()
super.onCreate()
androidAppGraph.inject(this)

val pendingIntent =
PendingIntent.getActivity(
context,
System.currentTimeMillis().toInt(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p1] System.currentTimeMillis().toInt() ์ •์ˆ˜ ์˜ค๋ฒ„ํ”Œ๋กœ

2026๋…„ ๊ธฐ์ค€ System.currentTimeMillis()๋Š” ์•ฝ 1,746,000,000,000L๋กœ, Int.MAX_VALUE(์•ฝ 2,147,483,647)๋ฅผ ํ›จ์”ฌ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค. .toInt() ๋ณ€ํ™˜ ์‹œ ์ƒ์œ„ ๋น„ํŠธ๊ฐ€ ์ž˜๋ ค ์Œ์ˆ˜ ๊ฐ’์ด ๋ฉ๋‹ˆ๋‹ค.

  • 63๋ฒˆ ์ค„ - PendingIntent ์š”์ฒญ ์ฝ”๋“œ: ๋น ๋ฅด๊ฒŒ ์—ฐ์† ์ˆ˜์‹ ๋œ ์•Œ๋ฆผ์ด ๋™์ผํ•œ ์š”์ฒญ ์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ FLAG_UPDATE_CURRENT๋กœ ์ธํ•ด ์ด์ „ ์•Œ๋ฆผ์˜ extras๊ฐ€ ๋ฎ์–ด์”Œ์›Œ์ง€๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ์ด์ „ ์•Œ๋ฆผ์„ ํƒญํ•˜๋ฉด ์ž˜๋ชป๋œ ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • 85๋ฒˆ ์ค„ - notificationManager.notify() ID: ์•Œ๋ฆผ์ด ์Œ“์ด์ง€ ์•Š๊ฑฐ๋‚˜ ๊ธฐ์กด ์•Œ๋ฆผ์ด ์˜๋„์น˜ ์•Š๊ฒŒ ์ทจ์†Œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

AtomicInteger ์นด์šดํ„ฐ ๋˜๋Š” (System.currentTimeMillis() % Int.MAX_VALUE).toInt()๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

์ฐธ๊ณ : PendingIntent ๊ณต์‹ ๋ฌธ์„œ, NotificationManager.notify()

application.androidAppGraph.coroutineScope
}

override fun onNewToken(token: String) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p1] onNewToken์ด no-op โ€” FCM ํ† ํฐ ๊ฐฑ์‹ ์ด ๋ฐฑ์—”๋“œ์— ์ „๋‹ฌ๋˜์ง€ ์•Š์Œ

FCM์€ ์•ฑ ์žฌ์„ค์น˜, ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”, ํ† ํฐ ๋งŒ๋ฃŒ ๋“ฑ ๋‹ค์–‘ํ•œ ์ƒํ™ฉ์—์„œ ํ† ํฐ์„ ๊ฐฑ์‹ ํ•˜๋ฉฐ onNewToken(token)์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ตฌํ˜„์€ ์ฃผ์„๋งŒ ๋‚จ๊ธฐ๊ณ  ์•„๋ฌด ๋™์ž‘๋„ ํ•˜์ง€ ์•Š์•„, ํ† ํฐ์ด ๊ฐฑ์‹ ๋˜๋ฉด ๋ฐฑ์—”๋“œ๊ฐ€ ๊ตฌ ํ† ํฐ์œผ๋กœ ํ‘ธ์‹œ๋ฅผ ๊ณ„์† ์ „์†กํ•˜๊ฒŒ ๋˜์–ด ์•Œ๋ฆผ์ด ์˜๊ตฌ์ ์œผ๋กœ ์ˆ˜์‹ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

FestabookApp.setupDeviceIdentifiers()์—์„œ์™€ ๋™์ผํ•˜๊ฒŒ deviceRepository.registerDevice(token)์„ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ : FCM ํ† ํฐ ๊ฐฑ์‹  ๊ณต์‹ ๋ฌธ์„œ

์ˆ˜์ • ๋ฐฉ๋ฒ•:

override fun onNewToken(token: String) {
    ioCoroutineScope.launch {
        application.androidAppGraph.deviceRepository.registerDevice(token)
    }
}

Comment on lines +17 to +21
url.scheme?.takeIf { it == TOSS_SCHEME || it == "intent" }?.let {
val intent = Intent.parseUri(url.toString(), Intent.URI_INTENT_SCHEME)
val packageManager = view.context.packageManager
if (intent.resolveActivity(packageManager) != null) {
view.context.startActivity(intent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p1] Intent ์Šคํ‚ด ์ธ์ ์…˜ ์ทจ์•ฝ์  (๋ณด์•ˆ)

"intent" ์Šคํ‚ด์„ ๋ช…์‹œ์ ์œผ๋กœ ํ—ˆ์šฉํ•˜๊ณ , Intent.parseUri(..., URI_INTENT_SCHEME)๋กœ ํŒŒ์‹ฑํ•œ ๋’ค ๊ฒ€์ฆ ์—†์ด ๋ฐ”๋กœ startActivity(intent)๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

WebView์—์„œ ๋ Œ๋”๋ง๋˜๋Š” ์„œ๋“œํŒŒํ‹ฐ ์ฝ˜ํ…์ธ , ๋ฆฌ๋‹ค์ด๋ ‰์…˜, ๋˜๋Š” XSS๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๊ณต๊ฒฉ์ž๊ฐ€ intent:// URL์„ ํ†ตํ•ด ์ž„์˜ ์ปดํฌ๋„ŒํŠธ ํ˜ธ์ถœ, ๋น„๊ณต๊ฐœ Activity ์ ‘๊ทผ, ์œ„ํ—˜ํ•œ ํ”Œ๋ž˜๊ทธ ์„ค์ • ๋“ฑ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค (CWE-939 / OWASP MASVS-PLATFORM-1).

Google ๊ณต์‹ ๊ฐ€์ด๋“œ์— ๋”ฐ๋ฅด๋ฉด URI_INTENT_SCHEME์œผ๋กœ ํŒŒ์‹ฑ๋œ Intent๋Š” ๋ฐ˜๋“œ์‹œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์ œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

val intent = Intent.parseUri(url.toString(), Intent.URI_INTENT_SCHEME)
// ํ•„์ˆ˜ ์ •์ œ
intent.addCategory(Intent.CATEGORY_BROWSABLE)
intent.component = null
intent.selector = null
intent.flags = intent.flags and
    Intent.FLAG_GRANT_READ_URI_PERMISSION.inv() and
    Intent.FLAG_GRANT_WRITE_URI_PERMISSION.inv()
// ์ดํ›„ resolveActivity ํ™•์ธ ํ›„ startActivity

๋˜๋Š” ํ—ˆ์šฉ ํŒจํ‚ค์ง€๋ฅผ Toss๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์ œํ•œํ•˜์„ธ์š”.

์ฐธ๊ณ : Android WebView Intent Scheme ์ฒ˜๋ฆฌ ๊ฐ€์ด๋“œ, Chrome Intent ์Šคํ‚ด ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€


val packageName = intent.`package` ?: TOSS_PACKAGE_NAME
val marketIntent = Intent(Intent.ACTION_VIEW, "market://details?id=$packageName".toUri())
view.context.startActivity(marketIntent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] Google Play ์—†๋Š” ๊ธฐ๊ธฐ์—์„œ ActivityNotFoundException ํฌ๋ž˜์‹œ

market:// Intent๋ฅผ resolveActivity() ํ™•์ธ ์—†์ด ์ง์ ‘ startActivity()๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. Google Play๊ฐ€ ์—†๋Š” ๊ธฐ๊ธฐ(AOSP ๊ธฐ๋ฐ˜, ์•„๋งˆ์กด ๊ธฐ๊ธฐ, ์ผ๋ถ€ ์ค‘๊ตญ OEM)์—์„œ๋Š” ActivityNotFoundException์ด ๋ฐœ์ƒํ•ด ์•ฑ์ด ํฌ๋ž˜์‹œํ•ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ : startActivity ๊ณต์‹ ๋ฌธ์„œ

Suggested change
view.context.startActivity(marketIntent)
runCatching { view.context.startActivity(marketIntent) }

modifier: Modifier = Modifier,
festatingViewModel: FestatingViewModel = metroViewModel(),
) {
ScreenViewLogger("FestaingScreen")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] Firebase Analytics ์Šคํฌ๋ฆฐ๋ช… ์˜คํƒ€ โ€” ๋ฐ์ดํ„ฐ ์œ ์‹ค

"FestaingScreen"์€ t๊ฐ€ ๋น ์ง„ ์˜คํƒ€์ž…๋‹ˆ๋‹ค. "FestatingScreen"์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Firebase Analytics๋Š” ์ด๋ฒคํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์„ ๊ฒ€์ฆํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ž˜๋ชป๋œ ์ด๋ฆ„์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ˆ„์ ๋˜์–ด ์ด ํ™”๋ฉด์˜ ์‚ฌ์šฉ ๋ถ„์„์ด ๋ถˆ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค.

์ฐธ๊ณ : Firebase Analytics ์ด๋ฒคํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ

Suggested change
ScreenViewLogger("FestaingScreen")
ScreenViewLogger("FestatingScreen")

Comment on lines +83 to +87
NSSetUncaughtExceptionHandler(
staticCFunction { exception: NSException? ->
val message =
"${exception?.name ?: "Unknown"}: ${exception?.reason ?: "Unknown error"}"
FirebaseCrashlytics.recordException(Exception(message))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] NSException ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๋ฅผ ๋ฒ„๋ฆผ (iOS)

NSException์˜ name๊ณผ reason๋งŒ ์ถ”์ถœํ•ด Exception(message)๋กœ ์ƒˆ ์˜ˆ์™ธ๋ฅผ ์ƒ์„ฑํ•˜๋ฏ€๋กœ, Crashlytics์— ์ฝœ ์Šคํƒ ์—†์ด ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋งŒ ์ „์†ก๋ฉ๋‹ˆ๋‹ค. exception?.callStackSymbols์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ๋ฐ๋„ ์ด๋ฅผ ๋ฌด์‹œํ•˜๋ฉด ํ”„๋กœ๋•์…˜ ํฌ๋ž˜์‹œ ์›์ธ ํŒŒ์•…์ด ๊ฑฐ์˜ ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

callStackSymbols๋ฅผ ํ•จ๊ป˜ ๊ธฐ๋กํ•˜์„ธ์š”:

NSSetUncaughtExceptionHandler(
    staticCFunction { exception: NSException? ->
        val name = exception?.name ?: "Unknown"
        val reason = exception?.reason ?: "Unknown error"
        val stack = exception?.callStackSymbols?.joinToString("\n") ?: "No stack"
        FirebaseCrashlytics.recordException(Exception("$name: $reason\n$stack"))
    },
)

์ฐธ๊ณ : Kotlin/Native iOS ์‹ฌ๋ณผ๋ฆฌ์ผ€์ด์…˜

Comment on lines +72 to +79
private const val SERVICE_POLICY_URL: String =
"https://www.notion.so/festabook-2026-04-01-335a540dc0b78055a450d7c82a1bdd40?source=copy_link"

private const val PERSONAL_INFORMATION_POLICY_URL: String =
"https://www.notion.so/festabook-2026-04-01-335a540dc0b780fdb79cc83372595721?source=copy_link"

private const val CONTACT_US_URL =
"https://forms.gle/XjqJFfQrTPgkZzGZ9"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํ”„๋กœ๋•์…˜ URL (Notion, Google Forms)

Notion ํŽ˜์ด์ง€ 3๊ฐœ์™€ Google Forms URL์ด ์†Œ์Šค ์ฝ”๋“œ์— ์ง์ ‘ ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. URL์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ฝ”๋“œ ์ˆ˜์ • โ†’ ๋ฆฌ๋ทฐ โ†’ ์•ฑ ๋ฆด๋ฆฌ์ฆˆ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ํ”„๋กœ์ ํŠธ์—์„œ FESTABOOK_URL, FESTA_TING_URL์€ BuildKonfig์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ๋ถ„๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ TERMS_URL์ด AddWaitingInfoScreen.kt์™€ WaitingRegisterScreen.kt์— ๋™์ผํ•œ ๊ฐ’์œผ๋กœ ์ค‘๋ณต ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ถŒ์žฅ ๋ฐฉ๋ฒ•:

// buildkonfig.gradle.kts์— ์ถ”๊ฐ€
BuildKonfigField("String", "SERVICE_POLICY_URL", "\"https://...\"")
BuildKonfigField("String", "PERSONAL_INFORMATION_POLICY_URL", "\"https://...\"")
BuildKonfigField("String", "CONTACT_US_URL", "\"https://...\"")

// ๋˜๋Š” ๊ณตํ†ต AppUrls ์˜ค๋ธŒ์ ํŠธ๋กœ ์ถ”์ถœ
object AppUrls {
    const val SERVICE_POLICY = "..."
    const val PERSONAL_INFORMATION_POLICY = "..."
    const val CONTACT_US = "..."
    const val TERMS = "..."
}

์ฐธ๊ณ : BuildKonfig ๊ณต์‹ ๋ฌธ์„œ

Comment on lines +83 to +84
private const val TERMS_URL =
"https://www.notion.so/2026-04-01-335a540dc0b780fa9897e0a3bdf45bae"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] ํ•˜๋“œ์ฝ”๋”ฉ๋œ Notion URL โ€” WaitingRegisterScreen.kt์™€ ๋™์ผ ๊ฐ’ ์ค‘๋ณต

TERMS_URL(https://www.notion.so/2026-04-01-335a540dc0b780fa9897e0a3bdf45bae)์ด WaitingRegisterScreen.kt์˜ PRIVACY_AGREEMENT_URL๊ณผ ๋™์ผํ•œ ๊ฐ’์œผ๋กœ ์ค‘๋ณต ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. URL ๋ณ€๊ฒฝ ์‹œ ๋‘ ํŒŒ์ผ์„ ๋ชจ๋‘ ์ˆ˜์ •ํ•ด์•ผ ํ•˜๋Š” ์œ ์ง€๋ณด์ˆ˜ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

SettingScreen.kt์˜ URL๋“ค๊ณผ ํ•จ๊ป˜ ๊ณตํ†ต ์ƒ์ˆ˜(AppUrls ์˜ค๋ธŒ์ ํŠธ ๋˜๋Š” BuildKonfig)๋กœ ์ถ”์ถœํ•˜์—ฌ ๋‹จ์ผ ๋ณ€๊ฒฝ์ (Single Source of Truth)์„ ์œ ์ง€ํ•˜์„ธ์š”.

์ฐธ๊ณ : Kotlin Multiplatform ๊ณตํ†ต ์ฝ”๋“œ ๊ด€๋ฆฌ

Comment on lines +82 to +83
private const val PRIVACY_AGREEMENT_URL =
"https://www.notion.so/2026-04-01-335a540dc0b780fa9897e0a3bdf45bae"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] ํ•˜๋“œ์ฝ”๋”ฉ๋œ Notion URL โ€” AddWaitingInfoScreen.kt์™€ ๋™์ผ ๊ฐ’ ์ค‘๋ณต

PRIVACY_AGREEMENT_URL์ด AddWaitingInfoScreen.kt์˜ TERMS_URL๊ณผ ๋™์ผํ•œ URL๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. URL ๋ณ€๊ฒฝ ์‹œ ๋‘ ํŒŒ์ผ์„ ๋ชจ๋‘ ์ฐพ์•„ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ณตํ†ต ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•˜์—ฌ ๋‹จ์ผ ๋ณ€๊ฒฝ์ (Single Source of Truth)์„ ์œ ์ง€ํ•˜์„ธ์š”. SettingScreen.kt์—์„œ ๊ด€๋ฆฌ๋˜๋Š” URL๋“ค๊ณผ ํ•จ๊ป˜ AppUrls ์˜ค๋ธŒ์ ํŠธ ๋˜๋Š” BuildKonfig์œผ๋กœ ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

@oungsi2000
oungsi2000 merged commit 075a80c into main May 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

โš™๏ธ Build ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ์Šคํฌ๋ฆฝํŠธ ๋ณ€๊ฒฝ, ํ™˜๊ฒฝ ์„ค์ • ์ˆ˜์ •

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants