[Release] Release 2.1.1 - #186
Conversation
[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) ์ ๋ณด๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
fix: Slack Reminder ์ญ์
* 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 ์ํ ๊ธฐ๋ฐ ํ์ธ ๋ฐฉ์์ผ๋ก ํ
์คํธ ๋ก์ง์ ์
๋ฐ์ดํธํ์ต๋๋ค.
* 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` ํ๋ฆฌ๋ทฐ ์ฝ๋์ ์ ๊ท ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
|
Important Review skippedToo 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: โ Files ignored due to path filters (13)
๐ Files selected for processing (287)
You can disable this status message by setting the Use the checkbox below for a quick retry:
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. ๐ Get started Note ๐ Summarized by CodeRabbit FreeYour 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 |
* 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`) ๋ฐ ๊ฐ ์ฃผ์ ํ๋ฉด์ ๋ค๋ก๊ฐ๊ธฐ ๋ฒํผ ํด๋ฆญ ์ ๋ก๊น
์ ์ํํ๋๋ก ์์ ํ์ต๋๋ค.
There was a problem hiding this comment.
์ฝ๋ ๋ฆฌ๋ทฐ โ 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 |
There was a problem hiding this comment.
[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 |
There was a problem hiding this comment.
[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) |
There was a problem hiding this comment.
[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) |
There was a problem hiding this comment.
[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
| ) | ||
|
|
||
| remember(state) { | ||
| state.webSettings.androidWebSettings.domStorageEnabled = true |
There was a problem hiding this comment.
[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 { |
There was a problem hiding this comment.
[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(), |
There was a problem hiding this comment.
[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() |
There was a problem hiding this comment.
[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?> |
There was a problem hiding this comment.
[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?, |
There was a problem hiding this comment.
[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
There was a problem hiding this comment.
์ฝ๋ ๋ฆฌ๋ทฐ (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) |
There was a problem hiding this comment.
[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) |
There was a problem hiding this comment.
[P2]
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) |
There was a problem hiding this comment.
[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)
...
}| e: Throwable, | ||
| ) { | ||
| Napier.e(e.stackTraceToString()) | ||
| application.startActivity(ErrorActivity.newIntent(application, e)) |
There was a problem hiding this comment.
[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 ๋ฌธ์
| } | ||
| } | ||
|
|
||
| @Suppress("UNCHECKED_CAST") |
There was a problem hiding this comment.
[p3] ๋ถํ์ํ @Suppress("UNCHECKED_CAST") โ ํจ์ ๋ด ์บ์คํธ ์์
setLightTheme() ํจ์์ @Suppress("UNCHECKED_CAST") ์ด๋ ธํ ์ด์ ์ด ๋ถ์ด ์์ง๋ง, ํจ์ ๋ณธ๋ฌธ์๋ ์บ์คํธ๊ฐ ์ ํ ์์ต๋๋ค. ์ด ์ด๋ ธํ ์ด์ ์ ์ฝ๋ ๋ ์์๊ฒ ํผ๋์ ์ฃผ๋ฉฐ ๋ถํ์ํฉ๋๋ค.
์์ : @Suppress("UNCHECKED_CAST") ์ด๋ ธํ ์ด์ ์ ์ ๊ฑฐํ์ธ์.
| val url = decidePolicyForNavigationAction.request.URL | ||
| val scheme = url?.scheme | ||
|
|
||
| Napier.d("scheme:$scheme") |
There was a problem hiding this comment.
[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 -> |
There was a problem hiding this comment.
[p3] const๋ฅผ ๋๋ค ํ๋ผ๋ฏธํฐ ์ด๋ฆ์ผ๋ก ์ฌ์ฉ โ Kotlin ์ํํธ ํค์๋์ ์ถฉ๋
suspendCancellableCoroutine { const -> ... }์์ const๋ Kotlin ์ํํธ ํค์๋(const val ์ ์ธ์ ์ฌ์ฉ)์ ๋๋ค. ์ปดํ์ผ์ ๋์ง๋ง ์ฝ๋ ๋ ์์๊ฒ ํผ๋์ ์ค๋๋ค. continuation ๋๋ cont๋ก ๋ณ๊ฒฝํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
์์ :
suspendCancellableCoroutine { cont ->
center.getNotificationSettingsWithCompletionHandler { settings ->
...
cont.resume(result)
}
}
| content: String, | ||
| action: FcmDeepLinkAction?, | ||
| ) { | ||
| if (festivalId != DeepLinkKeys.INITIALIZED_ID) { |
There was a problem hiding this comment.
[P2]
onMessageReceived()์์ FCM ๋ฐ์ดํฐ ํ์ด๋ก๋์ festivalId๋ฅผ ๋ฌด์กฐ๊ฑด ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์ ์ฅํฉ๋๋ค. ์ฌ์ฉ์๊ฐ ์๋ฆผ์ ํญํ์ง ์์๋ ๋ฐ์ํ๋ฉฐ, FCM ํ ํฝ/๋ธ๋ก๋์บ์คํธ๋ก ์์์ ๋ฉ์์ง๋ฅผ ์์ ํ๋ฉด ์ฌ์ฉ์๊ฐ ํ์ฌ ๋ณด๊ณ ์๋ ํ์คํฐ๋ฒ์ด ์กฐ์ฉํ ๋ณ๊ฒฝ๋ ์ ์์ต๋๋ค.
iOS ๊ตฌํ(DefaultUserNotificationDelegate)์ ์ฌ์ฉ์๊ฐ ์๋ฆผ์ ํญ(didReceiveNotificationResponse)ํ ๋๋ง festivalId๋ฅผ ์
๋ฐ์ดํธํ๋ ๊ฒ๊ณผ ๋์กฐ์ ์
๋๋ค.
์์ ๋ฐฉ๋ฒ: deep link action ์ฝ๋ฐฑ(์๋ฆผ ํญ ์ด๋ฒคํธ)์์๋ง festivalId๋ฅผ ์ ๋ฐ์ดํธํ๊ฑฐ๋, ํ์ฌ ์ ์ฅ๋ ๊ฐ๊ณผ ๋น๊ต ํ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ์๋ง ์ ๋ฐ์ดํธํ๋๋ก ์์ ํ์ธ์.
| application.androidAppGraph.coroutineScope | ||
| } | ||
|
|
||
| override fun onNewToken(token: String) { |
There was a problem hiding this comment.
[P2] onNewToken()์ด ์๋ฌด ๋์๋ ํ์ง ์์ FCM ํ ํฐ ๊ฐฑ์ ์ ์๋ฆผ์ด ์์ ๋์ง ์์
FCM์ ์ฑ ์ฌ์ค์น, ๋ฐ์ดํฐ ์ด๊ธฐํ, ์ฃผ๊ธฐ์ ์ธ ํ ํฐ ์ํ ์ onNewToken()์ ํธ์ถํฉ๋๋ค. FestabookApp.setupDeviceIdentifiers()๋ ์ฑ ์ต์ด ์คํ ์์๋ง ํ ํฐ์ ๋ฐฑ์๋์ ๋ฑ๋กํ๋ฏ๋ก, ์ดํ ํ ํฐ์ด ๊ฐฑ์ ๋๋ฉด ๋ฐฑ์๋๋ ๋ง๋ฃ๋ ํ ํฐ์ ๋ณด์ ํ๊ฒ ๋๊ณ FCM ์๋ฆผ์ด ๋ ์ด์ ์ ๋ฌ๋์ง ์์ต๋๋ค.
์์ ๋ฐฉ๋ฒ:
override fun onNewToken(token: String) {
ioCoroutineScope.launch {
deviceRepository.registerDevice(token)
}
}์ฐธ๊ณ : FCM ํ ํฐ ๊ฐฑ์ ์ฒ๋ฆฌ
|
|
||
| val pendingIntent = | ||
| PendingIntent.getActivity( | ||
| context, |
There was a problem hiding this comment.
[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) |
There was a problem hiding this comment.
[P3] ์๋ฆผ setColor()์ ํฐ์ ์ค์ โ ์๋ฆผ ํจ๋์์ ์์ด์ฝ์ด ๋ณด์ด์ง ์์ ์ ์์
setColor()๋ ์๋ฆผ์ ์์ ์์ด์ฝ ๋ฐ ํค์ฆ์
์๋ฆผ์ ๊ฐ์กฐ ์์์ผ๋ก ์ฌ์ฉ๋ฉ๋๋ค. ํฐ์(FestabookColorPalette().white)์ผ๋ก ์ค์ ํ๋ฉด ๊ธฐ๋ณธ ํฐ์ ์๋ฆผ ๋ฐฐ๊ฒฝ์์ ์์ด์ฝ์ด ๊ฑฐ์ ๋ณด์ด์ง ์์ต๋๋ค. Android 12+์์๋ ํ์ฅ๋ ์๋ฆผ ๋ฐฐ๊ฒฝ์๋ ์ํฅ์ ์ค๋๋ค.
๊ถ์ฅ: ์ฑ ๋ธ๋๋ ์์(ํฐ์/ํฌ๋ช ์ด ์๋ ์์)์ ์ฌ์ฉํ์ธ์.
| companion object { | ||
| fun newIntent(context: Context) = | ||
| Intent(context, MainActivity::class.java).apply { | ||
| flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK |
There was a problem hiding this comment.
[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" ์ค์ ๊ถ์ฅ)
| import festabookkmp.composeapp.generated.resources.waiting_register_title | ||
| import org.jetbrains.compose.resources.painterResource | ||
| import org.jetbrains.compose.resources.stringResource | ||
|
|
There was a problem hiding this comment.
[P2] ํ๋์ฝ๋ฉ๋ Notion URL โ ์์ ๋๋ ์ค์ ํ์ผ๋ก ๋ถ๋ฆฌ ํ์
๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ ๋ฑ ๋ฒ์ ๋ฌธ์ ๋งํฌ๊ฐ Composable ํ์ผ์ private const๋ก ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. URL์ด ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ(Notion ๋งํฌ๋ ์์ฃผ ๋ณ๊ฒฝ๋จ) ์ฝ๋ ์์ ๋ฐ ์ฑ ์ฌ๋ฐฐํฌ๊ฐ ํ์ํ๋ฉฐ, ๋ณ๊ฒฝ์ด ํ์ํ URL์ด ์ด๋์ ์๋์ง ์ฐพ๊ธฐ ์ด๋ ต์ต๋๋ค.
๊ถ์ฅ: BuildKonfig์ PRIVACY_AGREEMENT_URL ํ๋๋ฅผ ์ถ๊ฐํ๊ฑฐ๋, ์ฑ ์ค์ ํ์ผ(AppConfig.kt ๋ฑ)์ ์ค์ ์์๋ก ๋ถ๋ฆฌํ์ธ์.
|
|
||
| companion object { | ||
| private val platform = | ||
| when (getPlatform()) { |
There was a problem hiding this comment.
[P2] commonMain ๋ฐ์ดํฐ ๊ณ์ธต์์ getPlatform() ๋ถ๊ธฐ ๋ฐ ํ๋ซํผ ๋ฌธ์์ด ํ๋์ฝ๋ฉ
๋ ๊ฐ์ง ๋ฌธ์ ๊ฐ ์์ต๋๋ค:
- ํ๋์ฝ๋ฉ๋ ์๋ฒ ๊ณ์ฝ: API์ ์ ๋ฌ๋๋
"android"/"ios"๋ฌธ์์ด์ด ์์ ์์ด ์ฝ๋์ ์ง์ ํฌํจ๋์ด ์์ต๋๋ค. - 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( |
There was a problem hiding this comment.
[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 |
There was a problem hiding this comment.
[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" |
There was a problem hiding this comment.
[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` ๋ด ๋ฏธ์ฌ์ฉ ์ํฌํธ ์์๋ฅผ ์กฐ์ ํ์ต๋๋ค.
There was a problem hiding this comment.
์ฝ๋ ๋ฆฌ๋ทฐ ๊ฒฐ๊ณผ (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 |
| androidAppGraph.inject(this) | ||
| super.onCreate() |
There was a problem hiding this comment.
[p1] super.onCreate() ํธ์ถ ์์ ์ค๋ฅ
androidAppGraph.inject(this)๊ฐ super.onCreate() ์ด์ ์ ํธ์ถ๋๊ณ ์์ต๋๋ค. Android Application.onCreate()์์๋ ๋ฐ๋์ super.onCreate()๋ฅผ ๋จผ์ ํธ์ถํด์ผ ํฉ๋๋ค.
Firebase๋ FirebaseInitProvider(ContentProvider)๋ฅผ ํตํด ์ด๊ธฐํ๋๋ฉฐ, ์ด๋ super.onCreate() ์ดํ์ ์๋ฃ๊ฐ ๋ณด์ฅ๋ฉ๋๋ค. ์ฃผ์
๋ FirebaseMessaging, DeviceRepository ๋ฑ์ด ์์ง ์ด๊ธฐํ๋์ง ์์ ์ํ์์ ์ ๊ทผ๋๋ฉด ์ฝ๋ ์คํํธ ์ ํฌ๋์๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
์ฐธ๊ณ : Application.onCreate() ๊ณต์ ๋ฌธ์
| androidAppGraph.inject(this) | |
| super.onCreate() | |
| super.onCreate() | |
| androidAppGraph.inject(this) |
| val pendingIntent = | ||
| PendingIntent.getActivity( | ||
| context, | ||
| System.currentTimeMillis().toInt(), |
There was a problem hiding this comment.
[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) { |
There was a problem hiding this comment.
[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)
}
}| 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) |
There was a problem hiding this comment.
[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) |
There was a problem hiding this comment.
[p2] Google Play ์๋ ๊ธฐ๊ธฐ์์ ActivityNotFoundException ํฌ๋์
market:// Intent๋ฅผ resolveActivity() ํ์ธ ์์ด ์ง์ startActivity()๋ก ์คํํฉ๋๋ค. Google Play๊ฐ ์๋ ๊ธฐ๊ธฐ(AOSP ๊ธฐ๋ฐ, ์๋ง์กด ๊ธฐ๊ธฐ, ์ผ๋ถ ์ค๊ตญ OEM)์์๋ ActivityNotFoundException์ด ๋ฐ์ํด ์ฑ์ด ํฌ๋์ํฉ๋๋ค.
์ฐธ๊ณ : startActivity ๊ณต์ ๋ฌธ์
| view.context.startActivity(marketIntent) | |
| runCatching { view.context.startActivity(marketIntent) } |
| modifier: Modifier = Modifier, | ||
| festatingViewModel: FestatingViewModel = metroViewModel(), | ||
| ) { | ||
| ScreenViewLogger("FestaingScreen") |
There was a problem hiding this comment.
[p2] Firebase Analytics ์คํฌ๋ฆฐ๋ช ์คํ โ ๋ฐ์ดํฐ ์ ์ค
"FestaingScreen"์ t๊ฐ ๋น ์ง ์คํ์
๋๋ค. "FestatingScreen"์ด์ด์ผ ํฉ๋๋ค. Firebase Analytics๋ ์ด๋ฒคํธ ํ๋ผ๋ฏธํฐ ๊ฐ์ ๊ฒ์ฆํ์ง ์์ผ๋ฏ๋ก, ๋์๋ณด๋์์ ์๋ชป๋ ์ด๋ฆ์ผ๋ก ๋ฐ์ดํฐ๊ฐ ๋์ ๋์ด ์ด ํ๋ฉด์ ์ฌ์ฉ ๋ถ์์ด ๋ถ๊ฐ๋ฅํด์ง๋๋ค.
์ฐธ๊ณ : Firebase Analytics ์ด๋ฒคํธ ํ๋ผ๋ฏธํฐ
| ScreenViewLogger("FestaingScreen") | |
| ScreenViewLogger("FestatingScreen") |
| NSSetUncaughtExceptionHandler( | ||
| staticCFunction { exception: NSException? -> | ||
| val message = | ||
| "${exception?.name ?: "Unknown"}: ${exception?.reason ?: "Unknown error"}" | ||
| FirebaseCrashlytics.recordException(Exception(message)) |
There was a problem hiding this comment.
[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"))
},
)| 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" |
There was a problem hiding this comment.
[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 ๊ณต์ ๋ฌธ์
| private const val TERMS_URL = | ||
| "https://www.notion.so/2026-04-01-335a540dc0b780fa9897e0a3bdf45bae" |
There was a problem hiding this comment.
[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)์ ์ ์งํ์ธ์.
| private const val PRIVACY_AGREEMENT_URL = | ||
| "https://www.notion.so/2026-04-01-335a540dc0b780fa9897e0a3bdf45bae" |
There was a problem hiding this comment.
[p2] ํ๋์ฝ๋ฉ๋ Notion URL โ AddWaitingInfoScreen.kt์ ๋์ผ ๊ฐ ์ค๋ณต
PRIVACY_AGREEMENT_URL์ด AddWaitingInfoScreen.kt์ TERMS_URL๊ณผ ๋์ผํ URL๋ก ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. URL ๋ณ๊ฒฝ ์ ๋ ํ์ผ์ ๋ชจ๋ ์ฐพ์ ์์ ํด์ผ ํฉ๋๋ค.
๊ณตํต ์์๋ก ์ถ์ถํ์ฌ ๋จ์ผ ๋ณ๊ฒฝ์ (Single Source of Truth)์ ์ ์งํ์ธ์. SettingScreen.kt์์ ๊ด๋ฆฌ๋๋ URL๋ค๊ณผ ํจ๊ป AppUrls ์ค๋ธ์ ํธ ๋๋ BuildKonfig์ผ๋ก ํตํฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
#๏ธโฃ ์ด์ ๋ฒํธ
๐ ๏ธ ์์ ๋ด์ฉ
๐๐ป ์ค์ ๋ฆฌ๋ทฐ ์์ฒญ
๐ธ ์ด๋ฏธ์ง ์ฒจ๋ถ (Optional)