diff --git a/Diary/Diary.xcodeproj/project.pbxproj b/Diary/Diary.xcodeproj/project.pbxproj index bc899dc0..f2e8ff92 100644 --- a/Diary/Diary.xcodeproj/project.pbxproj +++ b/Diary/Diary.xcodeproj/project.pbxproj @@ -315,7 +315,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.8.0; + MARKETING_VERSION = 1.8.1; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary.dev; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -413,7 +413,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.8.0; + MARKETING_VERSION = 1.8.1; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -518,7 +518,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.8.0; + MARKETING_VERSION = 1.8.1; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Diary/Diary/DiaryApp.swift b/Diary/Diary/DiaryApp.swift index 804f26d0..12081157 100644 --- a/Diary/Diary/DiaryApp.swift +++ b/Diary/Diary/DiaryApp.swift @@ -1,10 +1,12 @@ import SwiftUI import FirebaseCore +import KMP class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { FirebaseApp.configure() + LoggerInitializerKt.setupLogger() return true } diff --git a/app/android/build.gradle.kts b/app/android/build.gradle.kts index 189c151d..a986ffbb 100644 --- a/app/android/build.gradle.kts +++ b/app/android/build.gradle.kts @@ -84,7 +84,12 @@ dependencyGuard { dependencies { implementation(projects.app.shared) + implementation(projects.logger.core) + implementation(projects.logger.analytics.impl) + implementation(projects.logger.console.impl) + implementation(projects.logger.crashlytics.impl) implementation(libs.androidx.activity.compose) + implementation(libs.androidx.startup.runtime) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) diff --git a/app/android/dependencies/realReleaseRuntimeClasspath.txt b/app/android/dependencies/realReleaseRuntimeClasspath.txt index 28019611..1bc9218d 100644 --- a/app/android/dependencies/realReleaseRuntimeClasspath.txt +++ b/app/android/dependencies/realReleaseRuntimeClasspath.txt @@ -10,7 +10,7 @@ androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 androidx.autofill:autofill:1.0.0 androidx.biometric:biometric:1.1.0 -androidx.browser:browser:1.9.0 +androidx.browser:browser:1.10.0 androidx.collection:collection-jvm:1.5.0 androidx.collection:collection-ktx:1.5.0 androidx.collection:collection:1.5.0 @@ -244,25 +244,25 @@ com.squareup.okhttp3:okhttp-android:5.3.2 com.squareup.okhttp3:okhttp:5.3.2 com.squareup.okio:okio-jvm:3.17.0 com.squareup.okio:okio:3.17.0 -dev.whyoleg.cryptography:cryptography-bigint-jvm:0.5.0 -dev.whyoleg.cryptography:cryptography-bigint:0.5.0 -dev.whyoleg.cryptography:cryptography-bom:0.5.0 -dev.whyoleg.cryptography:cryptography-core-jvm:0.5.0 -dev.whyoleg.cryptography:cryptography-core:0.5.0 -dev.whyoleg.cryptography:cryptography-provider-base-jvm:0.5.0 -dev.whyoleg.cryptography:cryptography-provider-base:0.5.0 -dev.whyoleg.cryptography:cryptography-provider-jdk-jvm:0.5.0 -dev.whyoleg.cryptography:cryptography-provider-jdk:0.5.0 -dev.whyoleg.cryptography:cryptography-provider-optimal-jvm:0.5.0 -dev.whyoleg.cryptography:cryptography-provider-optimal:0.5.0 -dev.whyoleg.cryptography:cryptography-random-jvm:0.5.0 -dev.whyoleg.cryptography:cryptography-random:0.5.0 -dev.whyoleg.cryptography:cryptography-serialization-asn1-jvm:0.5.0 -dev.whyoleg.cryptography:cryptography-serialization-asn1-modules-jvm:0.5.0 -dev.whyoleg.cryptography:cryptography-serialization-asn1-modules:0.5.0 -dev.whyoleg.cryptography:cryptography-serialization-asn1:0.5.0 -dev.whyoleg.cryptography:cryptography-serialization-pem-jvm:0.5.0 -dev.whyoleg.cryptography:cryptography-serialization-pem:0.5.0 +dev.whyoleg.cryptography:cryptography-bigint-jvm:0.6.0 +dev.whyoleg.cryptography:cryptography-bigint:0.6.0 +dev.whyoleg.cryptography:cryptography-bom:0.6.0 +dev.whyoleg.cryptography:cryptography-core-jvm:0.6.0 +dev.whyoleg.cryptography:cryptography-core:0.6.0 +dev.whyoleg.cryptography:cryptography-provider-base-jvm:0.6.0 +dev.whyoleg.cryptography:cryptography-provider-base:0.6.0 +dev.whyoleg.cryptography:cryptography-provider-jdk-jvm:0.6.0 +dev.whyoleg.cryptography:cryptography-provider-jdk:0.6.0 +dev.whyoleg.cryptography:cryptography-provider-optimal-jvm:0.6.0 +dev.whyoleg.cryptography:cryptography-provider-optimal:0.6.0 +dev.whyoleg.cryptography:cryptography-random-jvm:0.6.0 +dev.whyoleg.cryptography:cryptography-random:0.6.0 +dev.whyoleg.cryptography:cryptography-serialization-asn1-jvm:0.6.0 +dev.whyoleg.cryptography:cryptography-serialization-asn1-modules-jvm:0.6.0 +dev.whyoleg.cryptography:cryptography-serialization-asn1-modules:0.6.0 +dev.whyoleg.cryptography:cryptography-serialization-asn1:0.6.0 +dev.whyoleg.cryptography:cryptography-serialization-pem-jvm:0.6.0 +dev.whyoleg.cryptography:cryptography-serialization-pem:0.6.0 io.coil-kt.coil3:coil-android:3.4.0 io.coil-kt.coil3:coil-compose-android:3.4.0 io.coil-kt.coil3:coil-compose-core-android:3.4.0 @@ -275,12 +275,14 @@ io.coil-kt.coil3:coil-network-core:3.4.0 io.coil-kt.coil3:coil-network-ktor3-android:3.4.0 io.coil-kt.coil3:coil-network-ktor3:3.4.0 io.coil-kt.coil3:coil:3.4.0 -io.github.jan-tennert.supabase:auth-kt-android:3.5.0 -io.github.jan-tennert.supabase:auth-kt:3.5.0 -io.github.jan-tennert.supabase:functions-kt-android:3.5.0 -io.github.jan-tennert.supabase:functions-kt:3.5.0 -io.github.jan-tennert.supabase:supabase-kt-android:3.5.0 -io.github.jan-tennert.supabase:supabase-kt:3.5.0 +io.github.aakira:napier-android:2.7.1 +io.github.aakira:napier:2.7.1 +io.github.jan-tennert.supabase:auth-kt-android:3.6.0 +io.github.jan-tennert.supabase:auth-kt:3.6.0 +io.github.jan-tennert.supabase:functions-kt-android:3.6.0 +io.github.jan-tennert.supabase:functions-kt:3.6.0 +io.github.jan-tennert.supabase:supabase-kt-android:3.6.0 +io.github.jan-tennert.supabase:supabase-kt:3.6.0 io.insert-koin:koin-android:4.2.1 io.insert-koin:koin-androidx-workmanager:4.2.1 io.insert-koin:koin-annotations-jvm:4.2.1 @@ -368,18 +370,18 @@ org.jetbrains.kotlinx:atomicfu-jvm:0.28.0 org.jetbrains.kotlinx:atomicfu:0.28.0 org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.4.0 org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0-rc01 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0-rc01 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0-rc01 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-rc01 -org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.11.0-rc01 -org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.11.0-rc01 -org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.8.0-rc01 -org.jetbrains.kotlinx:kotlinx-datetime:0.8.0-rc01 -org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.2 -org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.2 -org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.2 -org.jetbrains.kotlinx:kotlinx-io-core:0.8.2 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0-rc02 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0-rc02 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0-rc02 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-rc02 +org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.11.0-rc02 +org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.11.0-rc02 +org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.8.0-rc02 +org.jetbrains.kotlinx:kotlinx-datetime:0.8.0-rc02 +org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.9.0 +org.jetbrains.kotlinx:kotlinx-io-bytestring:0.9.0 +org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.9.0 +org.jetbrains.kotlinx:kotlinx-io-core:0.9.0 org.jetbrains.kotlinx:kotlinx-serialization-bom:1.11.0 org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.11.0 org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0 diff --git a/app/android/src/main/AndroidManifest.xml b/app/android/src/main/AndroidManifest.xml index 4a16d6c7..1345f5b0 100644 --- a/app/android/src/main/AndroidManifest.xml +++ b/app/android/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ android:name="androidx.work.WorkManagerInitializer" android:value="androidx.startup" tools:node="remove" /> + diff --git a/app/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/LoggerInitializer.kt b/app/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/LoggerInitializer.kt new file mode 100644 index 00000000..0a0d4638 --- /dev/null +++ b/app/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/LoggerInitializer.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.initializer + +import android.content.Context +import androidx.startup.Initializer +import io.github.taetae98coding.diary.logger.analytics.impl.AndroidAnalyticsLogger +import io.github.taetae98coding.diary.logger.console.impl.ConsoleLogger +import io.github.taetae98coding.diary.logger.core.DiaryLogger +import io.github.taetae98coding.diary.logger.crashlytics.impl.AndroidCrashlyticsLogger + +internal class LoggerInitializer : Initializer { + override fun create(context: Context) { + DiaryLogger.addLogger(ConsoleLogger) + DiaryLogger.addLogger(AndroidAnalyticsLogger) + DiaryLogger.addLogger(AndroidCrashlyticsLogger) + } + + override fun dependencies(): List>> = emptyList() +} diff --git a/app/ios/build.gradle.kts b/app/ios/build.gradle.kts index 18140bef..fff8a3e3 100644 --- a/app/ios/build.gradle.kts +++ b/app/ios/build.gradle.kts @@ -22,6 +22,10 @@ kotlin { commonMain { dependencies { implementation(projects.app.shared) + implementation(projects.logger.core) + implementation(projects.logger.analytics.impl) + implementation(projects.logger.console.impl) + implementation(projects.logger.crashlytics.impl) implementation(libs.jetbrains.compose.ui) } } diff --git a/app/ios/src/commonMain/kotlin/io/github/taetae98coding/diary/initializer/LoggerInitializer.kt b/app/ios/src/commonMain/kotlin/io/github/taetae98coding/diary/initializer/LoggerInitializer.kt new file mode 100644 index 00000000..e1867ad1 --- /dev/null +++ b/app/ios/src/commonMain/kotlin/io/github/taetae98coding/diary/initializer/LoggerInitializer.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.initializer + +import io.github.taetae98coding.diary.logger.analytics.impl.AppleAnalyticsLogger +import io.github.taetae98coding.diary.logger.console.impl.ConsoleLogger +import io.github.taetae98coding.diary.logger.core.DiaryLogger +import io.github.taetae98coding.diary.logger.crashlytics.impl.AppleCrashlyticsLogger + +public fun setupLogger() { + DiaryLogger.addLogger(ConsoleLogger) + DiaryLogger.addLogger(AppleAnalyticsLogger) + DiaryLogger.addLogger(AppleCrashlyticsLogger) +} diff --git a/app/jvm/build.gradle.kts b/app/jvm/build.gradle.kts index 0be62818..a8af71ec 100644 --- a/app/jvm/build.gradle.kts +++ b/app/jvm/build.gradle.kts @@ -13,6 +13,8 @@ kotlin { commonMain { dependencies { implementation(projects.app.shared) + implementation(projects.logger.core) + implementation(projects.logger.console.impl) implementation(libs.jetbrains.compose.ui) runtimeOnly(compose.desktop.currentOs) } diff --git a/app/jvm/src/commonMain/kotlin/io/github/taetae98coding/diary/JvmApp.kt b/app/jvm/src/commonMain/kotlin/io/github/taetae98coding/diary/JvmApp.kt index a15cd64c..2443a68d 100644 --- a/app/jvm/src/commonMain/kotlin/io/github/taetae98coding/diary/JvmApp.kt +++ b/app/jvm/src/commonMain/kotlin/io/github/taetae98coding/diary/JvmApp.kt @@ -5,6 +5,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.singleWindowApplication import io.github.taetae98coding.diary.app.shared.App +import io.github.taetae98coding.diary.logger.console.impl.ConsoleLogger +import io.github.taetae98coding.diary.logger.core.DiaryLogger import java.awt.Dimension // iPhone 17 Pro Max 비율 @@ -15,6 +17,8 @@ private const val MIN_WIDTH = 360 private const val MIN_HEIGHT = 784 public fun main() { + DiaryLogger.addLogger(ConsoleLogger) + singleWindowApplication( state = WindowState(size = DpSize(WIDTH.dp, HEIGHT.dp)), title = BuildKonfig.APP_NAME, diff --git a/app/shared/build.gradle.kts b/app/shared/build.gradle.kts index ef7c8de1..98fee2fb 100644 --- a/app/shared/build.gradle.kts +++ b/app/shared/build.gradle.kts @@ -116,6 +116,9 @@ buildkonfig { buildConfigField(type = STRING, name = "APP_NAME", value = "DiaryDev", nullable = false, const = true) buildConfigField(type = STRING, name = "GOOGLE_CLIENT_ID", value = requireNotNull(localProperties.getProperty("dev.desktop.google.client.id")), nullable = false, const = true) } + create("wasmJs") { + buildConfigField(type = STRING, name = "GOOGLE_CLIENT_ID", value = requireNotNull(localProperties.getProperty("dev.web.google.client.id")), nullable = false, const = true) + } } targetConfigs("real") { @@ -126,5 +129,8 @@ buildkonfig { buildConfigField(type = STRING, name = "APP_NAME", value = "Diary", nullable = false, const = true) buildConfigField(type = STRING, name = "GOOGLE_CLIENT_ID", value = requireNotNull(localProperties.getProperty("real.desktop.google.client.id")), nullable = false, const = true) } + create("wasmJs") { + buildConfigField(type = STRING, name = "GOOGLE_CLIENT_ID", value = requireNotNull(localProperties.getProperty("real.web.google.client.id")), nullable = false, const = true) + } } } diff --git a/app/shared/src/commonMain/kotlin/io/github/taetae98coding/diary/app/shared/App.kt b/app/shared/src/commonMain/kotlin/io/github/taetae98coding/diary/app/shared/App.kt index c3ad1f6a..10046a4c 100644 --- a/app/shared/src/commonMain/kotlin/io/github/taetae98coding/diary/app/shared/App.kt +++ b/app/shared/src/commonMain/kotlin/io/github/taetae98coding/diary/app/shared/App.kt @@ -22,7 +22,7 @@ public fun App(configuration: KoinAppDeclaration = {}) { }, ) { DiaryTheme { - AppScaffold() + AppScaffold(state = rememberAppState()) } } } diff --git a/app/shared/src/commonMain/kotlin/io/github/taetae98coding/diary/app/shared/AppScaffold.kt b/app/shared/src/commonMain/kotlin/io/github/taetae98coding/diary/app/shared/AppScaffold.kt index a7378777..e499f59e 100644 --- a/app/shared/src/commonMain/kotlin/io/github/taetae98coding/diary/app/shared/AppScaffold.kt +++ b/app/shared/src/commonMain/kotlin/io/github/taetae98coding/diary/app/shared/AppScaffold.kt @@ -25,8 +25,8 @@ import org.koin.compose.viewmodel.koinViewModel @Composable internal fun AppScaffold( + state: AppState, modifier: Modifier = Modifier, - state: AppState = rememberAppState(), ) { NavigationSuiteScaffold( modifier = modifier diff --git a/app/shared/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/app/shared/WasmJsAppModule.kt b/app/shared/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/app/shared/WasmJsAppModule.kt new file mode 100644 index 00000000..6b336a0a --- /dev/null +++ b/app/shared/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/app/shared/WasmJsAppModule.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.app.shared + +import io.github.taetae98coding.diary.feature.login.di.GoogleClientId +import org.koin.core.annotation.Configuration +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Module + +@Module +@Configuration +public class WasmJsAppModule { + @Factory + @GoogleClientId + internal fun providesGoogleClientId(): String { + return BuildKonfig.GOOGLE_CLIENT_ID + } +} diff --git a/app/wasm/build.gradle.kts b/app/wasm/build.gradle.kts index 28468885..df325eea 100644 --- a/app/wasm/build.gradle.kts +++ b/app/wasm/build.gradle.kts @@ -17,8 +17,16 @@ kotlin { commonMain { dependencies { implementation(projects.app.shared) + implementation(libs.jetbrains.compose.components.resources) implementation(libs.jetbrains.compose.ui) + implementation(libs.jetbrains.compose.material3) } } } } + +compose.resources { + publicResClass = false + packageOfResClass = "io.github.taetae98coding.diary" + generateResClass = always +} diff --git a/app/wasm/src/wasmJsMain/composeResources/font/NotoSans.ttf b/app/wasm/src/wasmJsMain/composeResources/font/NotoSans.ttf new file mode 100644 index 00000000..36e634d2 Binary files /dev/null and b/app/wasm/src/wasmJsMain/composeResources/font/NotoSans.ttf differ diff --git a/app/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/WasmApp.kt b/app/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/WasmApp.kt index 9a7f971d..32e24a6e 100644 --- a/app/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/WasmApp.kt +++ b/app/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/WasmApp.kt @@ -1,13 +1,33 @@ package io.github.taetae98coding.diary +import androidx.compose.material3.MaterialExpressiveTheme +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.window.ComposeViewport import io.github.taetae98coding.diary.app.shared.App import kotlinx.browser.document +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.preloadFont public fun main() { ComposeViewport( viewportContainer = requireNotNull(document.body), ) { - App() + val fontFamily = rememberNotoSansFontFamily() + + fontFamily?.let { family -> + MaterialExpressiveTheme(typography = Typography(fontFamily = family)) { + App() + } + } } } + +@OptIn(ExperimentalResourceApi::class) +@Composable +private fun rememberNotoSansFontFamily(): FontFamily? { + val font by preloadFont(Res.font.NotoSans) + return font?.let { FontFamily(it) } +} diff --git a/app/wasm/src/wasmJsMain/resources/index.html b/app/wasm/src/wasmJsMain/resources/index.html index 3e728e76..af8d0361 100644 --- a/app/wasm/src/wasmJsMain/resources/index.html +++ b/app/wasm/src/wasmJsMain/resources/index.html @@ -15,6 +15,7 @@ + diff --git a/build-logic/src/main/kotlin/BuildConfig.kt b/build-logic/src/main/kotlin/BuildConfig.kt index b13b8b36..72778bd7 100644 --- a/build-logic/src/main/kotlin/BuildConfig.kt +++ b/build-logic/src/main/kotlin/BuildConfig.kt @@ -4,6 +4,6 @@ public data object BuildConfig { internal const val ANDROID_TARGET_SDK = 36 public const val NAMESPACE: String = "io.github.taetae98coding.diary" - public const val VERSION_NAME: String = "1.8.0" - public const val VERSION_CODE: Int = 9 + public const val VERSION_NAME: String = "1.8.1" + public const val VERSION_CODE: Int = 10 } diff --git a/build.gradle.kts b/build.gradle.kts index bd81395e..afececa8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -57,6 +57,9 @@ projectGuard { deny(":core:network") deny(":core:supabase") deny(":core:weather-network") + deny(":logger:analytics:impl") + deny(":logger:console:impl") + deny(":logger:crashlytics:impl") } guard(":feature") { @@ -70,6 +73,9 @@ projectGuard { deny(":core:network") deny(":core:supabase") deny(":core:weather-network") + deny(":logger:analytics:impl") + deny(":logger:console:impl") + deny(":logger:crashlytics:impl") } guard(":presenter") { @@ -83,5 +89,8 @@ projectGuard { deny(":core:network") deny(":core:supabase") deny(":core:weather-network") + deny(":logger:analytics:impl") + deny(":logger:console:impl") + deny(":logger:crashlytics:impl") } } diff --git a/compose/core/src/commonMain/kotlin/io/github/taetae98coding/diary/compose/core/padding/PaddingValuesPlus.kt b/compose/core/src/commonMain/kotlin/io/github/taetae98coding/diary/compose/core/padding/PaddingValuesPlus.kt new file mode 100644 index 00000000..2b9bbd19 --- /dev/null +++ b/compose/core/src/commonMain/kotlin/io/github/taetae98coding/diary/compose/core/padding/PaddingValuesPlus.kt @@ -0,0 +1,20 @@ +package io.github.taetae98coding.diary.compose.core.padding + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.platform.LocalLayoutDirection + +@Composable +@ReadOnlyComposable +public operator fun PaddingValues.plus(other: PaddingValues): PaddingValues { + val layoutDirection = LocalLayoutDirection.current + return PaddingValues( + start = calculateStartPadding(layoutDirection) + other.calculateStartPadding(layoutDirection), + top = calculateTopPadding() + other.calculateTopPadding(), + end = calculateEndPadding(layoutDirection) + other.calculateEndPadding(layoutDirection), + bottom = calculateBottomPadding() + other.calculateBottomPadding(), + ) +} diff --git a/compose/core/src/commonMain/kotlin/io/github/taetae98coding/diary/compose/core/scaffold/DiaryScaffold.kt b/compose/core/src/commonMain/kotlin/io/github/taetae98coding/diary/compose/core/scaffold/DiaryScaffold.kt new file mode 100644 index 00000000..2a6d6bd0 --- /dev/null +++ b/compose/core/src/commonMain/kotlin/io/github/taetae98coding/diary/compose/core/scaffold/DiaryScaffold.kt @@ -0,0 +1,41 @@ +package io.github.taetae98coding.diary.compose.core.scaffold + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.exclude +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun DiaryScaffold( + modifier: Modifier = Modifier, + topBar: @Composable () -> Unit = {}, + snackbarHost: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, + floatingActionButtonPosition: FabPosition = FabPosition.End, + content: @Composable (PaddingValues) -> Unit, +) { + Scaffold( + modifier = modifier, + topBar = topBar, + snackbarHost = { + Box(modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars)) { + snackbarHost() + } + }, + floatingActionButton = { + Box(modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars)) { + floatingActionButton() + } + }, + floatingActionButtonPosition = floatingActionButtonPosition, + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), + content = content, + ) +} diff --git a/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountMemoLocalEntity.kt b/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountMemoLocalEntity.kt index 3e0e5dad..3f650202 100644 --- a/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountMemoLocalEntity.kt +++ b/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountMemoLocalEntity.kt @@ -3,6 +3,7 @@ package io.github.taetae98coding.diary.core.database.api.entity import androidx.room3.ColumnInfo import androidx.room3.Entity import androidx.room3.ForeignKey +import androidx.room3.Index import kotlin.uuid.Uuid @Entity( @@ -17,6 +18,7 @@ import kotlin.uuid.Uuid onUpdate = ForeignKey.CASCADE, ), ], + indices = [Index(value = ["memoId"])], ) public data class AccountMemoLocalEntity( @ColumnInfo(name = "accountId", defaultValue = "00000000-0000-0000-0000-000000000000") diff --git a/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountRoutineLocalEntity.kt b/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountRoutineLocalEntity.kt index f672c435..5a3404d4 100644 --- a/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountRoutineLocalEntity.kt +++ b/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountRoutineLocalEntity.kt @@ -3,6 +3,7 @@ package io.github.taetae98coding.diary.core.database.api.entity import androidx.room3.ColumnInfo import androidx.room3.Entity import androidx.room3.ForeignKey +import androidx.room3.Index import kotlin.uuid.Uuid @Entity( @@ -17,6 +18,7 @@ import kotlin.uuid.Uuid onUpdate = ForeignKey.CASCADE, ), ], + indices = [Index(value = ["routineId"])], ) public data class AccountRoutineLocalEntity( @ColumnInfo(name = "accountId", defaultValue = "00000000-0000-0000-0000-000000000000") diff --git a/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountTagLocalEntity.kt b/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountTagLocalEntity.kt index 89fc482b..d5286c10 100644 --- a/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountTagLocalEntity.kt +++ b/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/AccountTagLocalEntity.kt @@ -3,6 +3,7 @@ package io.github.taetae98coding.diary.core.database.api.entity import androidx.room3.ColumnInfo import androidx.room3.Entity import androidx.room3.ForeignKey +import androidx.room3.Index import kotlin.uuid.Uuid @Entity( @@ -17,6 +18,7 @@ import kotlin.uuid.Uuid onUpdate = ForeignKey.CASCADE, ), ], + indices = [Index(value = ["tagId"])], ) public data class AccountTagLocalEntity( @ColumnInfo(name = "accountId", defaultValue = "00000000-0000-0000-0000-000000000000") diff --git a/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/MemoTagLocalEntity.kt b/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/MemoTagLocalEntity.kt index eb65a435..f7f2c5c8 100644 --- a/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/MemoTagLocalEntity.kt +++ b/core/database/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/api/entity/MemoTagLocalEntity.kt @@ -3,6 +3,7 @@ package io.github.taetae98coding.diary.core.database.api.entity import androidx.room3.ColumnInfo import androidx.room3.Entity import androidx.room3.ForeignKey +import androidx.room3.Index import kotlin.uuid.Uuid @Entity( @@ -24,6 +25,7 @@ import kotlin.uuid.Uuid onUpdate = ForeignKey.CASCADE, ), ], + indices = [Index(value = ["tagId"])], ) public data class MemoTagLocalEntity( @ColumnInfo(name = "memoId", defaultValue = "00000000-0000-0000-0000-000000000000") diff --git a/core/database/impl/build.gradle.kts b/core/database/impl/build.gradle.kts index aee34f03..ee0b04a8 100644 --- a/core/database/impl/build.gradle.kts +++ b/core/database/impl/build.gradle.kts @@ -29,6 +29,13 @@ kotlin { } } + wasmJsMain { + dependencies { + implementation(libs.androidx.sqlite.web) + implementation(projects.library.sqliteWasmWorker) + } + } + getByName("androidHostTest") { dependencies { implementation(libs.androidx.room3.testing) diff --git a/core/database/impl/schemas/io.github.taetae98coding.diary.core.database.impl.DiaryDatabase/5.json b/core/database/impl/schemas/io.github.taetae98coding.diary.core.database.impl.DiaryDatabase/5.json new file mode 100644 index 00000000..97b71396 --- /dev/null +++ b/core/database/impl/schemas/io.github.taetae98coding.diary.core.database.impl.DiaryDatabase/5.json @@ -0,0 +1,746 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "23554c11c091a37047ea7bd82a57332d", + "entities": [ + { + "tableName": "Memo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `primaryTag` TEXT DEFAULT NULL, `isFinished` INTEGER NOT NULL DEFAULT 0, `isDeleted` INTEGER NOT NULL DEFAULT 0, `updatedAt` INTEGER NOT NULL DEFAULT 0, `createdAt` INTEGER NOT NULL DEFAULT 0, `title` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', `isAllDay` INTEGER NOT NULL DEFAULT 0, `start` TEXT DEFAULT NULL, `endInclusive` TEXT DEFAULT NULL, `color` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "primaryTag", + "columnName": "primaryTag", + "affinity": "TEXT", + "defaultValue": "NULL" + }, + { + "fieldPath": "isFinished", + "columnName": "isFinished", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "detail.title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "detail.description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "detail.isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "detail.start", + "columnName": "start", + "affinity": "TEXT", + "defaultValue": "NULL" + }, + { + "fieldPath": "detail.endInclusive", + "columnName": "endInclusive", + "affinity": "TEXT", + "defaultValue": "NULL" + }, + { + "fieldPath": "detail.color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Tag", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `isFinished` INTEGER NOT NULL DEFAULT 0, `isDeleted` INTEGER NOT NULL DEFAULT 0, `updatedAt` INTEGER NOT NULL DEFAULT 0, `createdAt` INTEGER NOT NULL DEFAULT 0, `title` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', `color` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "isFinished", + "columnName": "isFinished", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "detail.title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "detail.description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "detail.color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "MemoTag", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memoId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `tagId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `isMemoTag` INTEGER NOT NULL DEFAULT 0, `updatedAt` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`memoId`, `tagId`), FOREIGN KEY(`memoId`) REFERENCES `Memo`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`tagId`) REFERENCES `Tag`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "memoId", + "columnName": "memoId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "isMemoTag", + "columnName": "isMemoTag", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "memoId", + "tagId" + ] + }, + "indices": [ + { + "name": "index_MemoTag_tagId", + "unique": false, + "columnNames": [ + "tagId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemoTag_tagId` ON `${TABLE_NAME}` (`tagId`)" + } + ], + "foreignKeys": [ + { + "table": "Memo", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "memoId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Tag", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tagId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CalendarMemoFilterTag", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tagId` TEXT NOT NULL, PRIMARY KEY(`tagId`), FOREIGN KEY(`tagId`) REFERENCES `Tag`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tagId" + ] + }, + "foreignKeys": [ + { + "table": "Tag", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tagId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ListMemoFilterTag", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tagId` TEXT NOT NULL, PRIMARY KEY(`tagId`), FOREIGN KEY(`tagId`) REFERENCES `Tag`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tagId" + ] + }, + "foreignKeys": [ + { + "table": "Tag", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tagId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AccountMemo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `memoId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', PRIMARY KEY(`accountId`, `memoId`), FOREIGN KEY(`memoId`) REFERENCES `Memo`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "memoId", + "columnName": "memoId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "memoId" + ] + }, + "indices": [ + { + "name": "index_AccountMemo_memoId", + "unique": false, + "columnNames": [ + "memoId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMemo_memoId` ON `${TABLE_NAME}` (`memoId`)" + } + ], + "foreignKeys": [ + { + "table": "Memo", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "memoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AccountTag", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `tagId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', PRIMARY KEY(`accountId`, `tagId`), FOREIGN KEY(`tagId`) REFERENCES `Tag`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "tagId" + ] + }, + "indices": [ + { + "name": "index_AccountTag_tagId", + "unique": false, + "columnNames": [ + "tagId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountTag_tagId` ON `${TABLE_NAME}` (`tagId`)" + } + ], + "foreignKeys": [ + { + "table": "Tag", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tagId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncMemo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memoId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `state` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`memoId`), FOREIGN KEY(`memoId`) REFERENCES `Memo`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "memoId", + "columnName": "memoId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "memoId" + ] + }, + "foreignKeys": [ + { + "table": "Memo", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "memoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncTag", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tagId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `state` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`tagId`), FOREIGN KEY(`tagId`) REFERENCES `Tag`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tagId" + ] + }, + "foreignKeys": [ + { + "table": "Tag", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tagId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncMemoTag", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memoId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `tagId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `state` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`memoId`, `tagId`), FOREIGN KEY(`memoId`, `tagId`) REFERENCES `MemoTag`(`memoId`, `tagId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "memoId", + "columnName": "memoId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "memoId", + "tagId" + ] + }, + "foreignKeys": [ + { + "table": "MemoTag", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "memoId", + "tagId" + ], + "referencedColumns": [ + "memoId", + "tagId" + ] + } + ] + }, + { + "tableName": "Routine", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `rRules` TEXT NOT NULL DEFAULT '[]', `rDates` TEXT NOT NULL DEFAULT '[]', `exDates` TEXT NOT NULL DEFAULT '[]', `isCalendarVisible` INTEGER NOT NULL DEFAULT 1, `isFinished` INTEGER NOT NULL DEFAULT 0, `isDeleted` INTEGER NOT NULL DEFAULT 0, `updatedAt` INTEGER NOT NULL DEFAULT 0, `createdAt` INTEGER NOT NULL DEFAULT 0, `title` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', `start` TEXT DEFAULT NULL, `endInclusive` TEXT DEFAULT NULL, `color` INTEGER NOT NULL DEFAULT 0, `routineCount` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "rRules", + "columnName": "rRules", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "rDates", + "columnName": "rDates", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "exDates", + "columnName": "exDates", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "isCalendarVisible", + "columnName": "isCalendarVisible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isFinished", + "columnName": "isFinished", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "detail.title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "detail.description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "detail.start", + "columnName": "start", + "affinity": "TEXT", + "defaultValue": "NULL" + }, + { + "fieldPath": "detail.endInclusive", + "columnName": "endInclusive", + "affinity": "TEXT", + "defaultValue": "NULL" + }, + { + "fieldPath": "detail.color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "detail.routineCount", + "columnName": "routineCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "AccountRoutine", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `routineId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', PRIMARY KEY(`accountId`, `routineId`), FOREIGN KEY(`routineId`) REFERENCES `Routine`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "routineId", + "columnName": "routineId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "routineId" + ] + }, + "indices": [ + { + "name": "index_AccountRoutine_routineId", + "unique": false, + "columnNames": [ + "routineId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountRoutine_routineId` ON `${TABLE_NAME}` (`routineId`)" + } + ], + "foreignKeys": [ + { + "table": "Routine", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "routineId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncRoutine", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`routineId` TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', `state` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`routineId`), FOREIGN KEY(`routineId`) REFERENCES `Routine`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "routineId", + "columnName": "routineId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'00000000-0000-0000-0000-000000000000'" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "routineId" + ] + }, + "foreignKeys": [ + { + "table": "Routine", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "routineId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '23554c11c091a37047ea7bd82a57332d')" + ] + } +} \ No newline at end of file diff --git a/core/database/impl/src/androidHostTest/kotlin/io/github/taetae98coding/diary/core/database/impl/DiaryDatabaseMigrationTest.kt b/core/database/impl/src/androidHostTest/kotlin/io/github/taetae98coding/diary/core/database/impl/DiaryDatabaseMigrationTest.kt index 9ff3e7fb..24cbf580 100644 --- a/core/database/impl/src/androidHostTest/kotlin/io/github/taetae98coding/diary/core/database/impl/DiaryDatabaseMigrationTest.kt +++ b/core/database/impl/src/androidHostTest/kotlin/io/github/taetae98coding/diary/core/database/impl/DiaryDatabaseMigrationTest.kt @@ -43,4 +43,46 @@ class DiaryDatabaseMigrationTest { migrationTestHelper.createDatabase(3).close() migrationTestHelper.runMigrationsAndValidate(4).close() } + + @Test + fun `version 4에서 5로 AutoMigration이 정상 동작한다`() = runTest { + migrationTestHelper.createDatabase(4).close() + migrationTestHelper.runMigrationsAndValidate(5).close() + } + + @Test + fun `version 5 마이그레이션 후 FK 보조 인덱스가 생성되어 있다`() = runTest { + migrationTestHelper.createDatabase(4).close() + val connection = migrationTestHelper.runMigrationsAndValidate(5) + + val expectedIndices = setOf( + "AccountMemo" to "memoId", + "AccountTag" to "tagId", + "AccountRoutine" to "routineId", + "MemoTag" to "tagId", + ) + + val actualIndices = mutableSetOf>() + connection.prepare( + """ + SELECT m.tbl_name, ii.name + FROM sqlite_master m + JOIN pragma_index_list(m.tbl_name) il ON il.name = m.name + JOIN pragma_index_info(m.name) ii + WHERE m.type = 'index' + AND m.tbl_name IN ('AccountMemo', 'AccountTag', 'AccountRoutine', 'MemoTag') + AND il.origin = 'c' + """.trimIndent(), + ).use { stmt -> + while (stmt.step()) { + actualIndices += stmt.getText(0) to stmt.getText(1) + } + } + + connection.close() + + assert(actualIndices.containsAll(expectedIndices)) { + "FK 보조 인덱스 누락. expected=$expectedIndices, actual=$actualIndices" + } + } } diff --git a/core/database/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/impl/DiaryDatabase.kt b/core/database/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/impl/DiaryDatabase.kt index 324ca202..fe7cfc02 100644 --- a/core/database/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/impl/DiaryDatabase.kt +++ b/core/database/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/database/impl/DiaryDatabase.kt @@ -60,11 +60,12 @@ import io.github.taetae98coding.diary.library.room.common.converter.UuidTypeConv AccountRoutineLocalEntity::class, SyncRoutineLocalEntity::class, ], - version = 4, + version = 5, autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(from = 3, to = 4), + AutoMigration(from = 4, to = 5), ], ) @TypeConverters( diff --git a/core/database/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/database/impl/WasmJsDatabaseModule.kt b/core/database/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/database/impl/WasmJsDatabaseModule.kt new file mode 100644 index 00000000..cdfbc726 --- /dev/null +++ b/core/database/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/database/impl/WasmJsDatabaseModule.kt @@ -0,0 +1,20 @@ +package io.github.taetae98coding.diary.core.database.impl + +import androidx.room3.Room +import androidx.room3.RoomDatabase +import io.github.taetae98coding.diary.core.database.impl.di.DiaryDatabaseBuilder +import io.github.taetae98coding.diary.library.sqlite.wasm.worker.createSqliteWasmWorkerDriver +import org.koin.core.annotation.Configuration +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Module + +@Module +@Configuration +public class WasmJsDatabaseModule { + @DiaryDatabaseBuilder + @Factory + internal fun providesDiaryDatabaseBuilder(): RoomDatabase.Builder { + return Room.databaseBuilder(name = "diary.db") + .setDriver(createSqliteWasmWorkerDriver()) + } +} diff --git a/core/datastore/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/datastore/impl/WasmJsDataStoreModule.kt b/core/datastore/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/datastore/impl/WasmJsDataStoreModule.kt new file mode 100644 index 00000000..18ea545c --- /dev/null +++ b/core/datastore/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/datastore/impl/WasmJsDataStoreModule.kt @@ -0,0 +1,43 @@ +package io.github.taetae98coding.diary.core.datastore.impl + +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.okio.WebOpfsStorage +import androidx.datastore.core.okio.WebSerializer +import io.github.taetae98coding.diary.core.datastore.api.entity.AccountMetaDataDataStoreEntity +import io.github.taetae98coding.diary.core.datastore.api.entity.ListMemoFilterOptionDataStoreEntity +import org.koin.core.annotation.Configuration +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +@Configuration +public class WasmJsDataStoreModule { + @Single + @AccountMetaDataDataStore + internal fun providesAccountMetaDataDataStore(): DataStore { + return DataStoreFactory.create( + storage = WebOpfsStorage( + serializer = WebSerializer( + kSerializer = AccountMetaDataDataStoreEntity.serializer(), + defaultValue = AccountMetaDataDataStoreEntity(), + ), + name = "account_metadata", + ), + ) + } + + @Single + @ListMemoFilterOptionDataStore + internal fun providesListMemoFilterOptionDataStore(): DataStore { + return DataStoreFactory.create( + storage = WebOpfsStorage( + serializer = WebSerializer( + kSerializer = ListMemoFilterOptionDataStoreEntity.serializer(), + defaultValue = ListMemoFilterOptionDataStoreEntity(), + ), + name = "list_memo_filter_option", + ), + ) + } +} diff --git a/core/google-credentials/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/google/credentials/api/GoogleCredentials.kt b/core/google-credentials/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/google/credentials/api/GoogleCredentials.kt index 223cea1e..9bb6694f 100644 --- a/core/google-credentials/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/google/credentials/api/GoogleCredentials.kt +++ b/core/google-credentials/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/google/credentials/api/GoogleCredentials.kt @@ -1,10 +1,10 @@ package io.github.taetae98coding.diary.core.google.credentials.api -public sealed class GoogleCredentials { - public data class IdToken(val idToken: String) : GoogleCredentials() +public sealed interface GoogleCredentials { + public data class IdToken(val idToken: String) : GoogleCredentials public data class AuthorizationCode( val clientId: String, val code: String, val redirectUri: String, - ) : GoogleCredentials() + ) : GoogleCredentials } diff --git a/core/google-credentials/compose/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/google/credentials/compose/RememberGoogleCredentialsManager.kt b/core/google-credentials/compose/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/google/credentials/compose/RememberGoogleCredentialsManager.kt new file mode 100644 index 00000000..5ca63dd1 --- /dev/null +++ b/core/google-credentials/compose/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/google/credentials/compose/RememberGoogleCredentialsManager.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.core.google.credentials.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import io.github.taetae98coding.diary.core.google.credentials.api.GoogleCredentialsManager +import io.github.taetae98coding.diary.core.google.credentials.impl.GoogleCredentialsManagerImpl + +@Composable +public fun rememberGoogleCredentialsManager(clientId: String): GoogleCredentialsManager { + return remember(clientId) { GoogleCredentialsManagerImpl(clientId) } +} diff --git a/core/google-credentials/impl/build.gradle.kts b/core/google-credentials/impl/build.gradle.kts index 1cb8bc47..3fb6d4d0 100644 --- a/core/google-credentials/impl/build.gradle.kts +++ b/core/google-credentials/impl/build.gradle.kts @@ -50,5 +50,11 @@ kotlin { implementation(libs.google.oauth.client.jetty) } } + + wasmJsMain { + dependencies { + implementation(libs.kotlinx.coroutines.core) + } + } } } diff --git a/core/google-credentials/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/google/credentials/impl/GoogleCredentialsManagerImpl.kt b/core/google-credentials/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/google/credentials/impl/GoogleCredentialsManagerImpl.kt new file mode 100644 index 00000000..56d10abc --- /dev/null +++ b/core/google-credentials/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/google/credentials/impl/GoogleCredentialsManagerImpl.kt @@ -0,0 +1,83 @@ +@file:OptIn(ExperimentalWasmJsInterop::class) + +package io.github.taetae98coding.diary.core.google.credentials.impl + +import io.github.taetae98coding.diary.core.google.credentials.api.GoogleCredentials +import io.github.taetae98coding.diary.core.google.credentials.api.GoogleCredentialsException +import io.github.taetae98coding.diary.core.google.credentials.api.GoogleCredentialsManager +import io.github.taetae98coding.diary.core.google.credentials.api.GoogleCredentialsNotSupportException +import io.github.taetae98coding.diary.core.google.credentials.api.GoogleCredentialsUserCancelException +import kotlin.js.Promise +import kotlinx.coroutines.await + +public class GoogleCredentialsManagerImpl(private val clientId: String) : GoogleCredentialsManager { + override suspend fun get(): GoogleCredentials { + if (!isGoogleAccountsOAuth2Available()) { + throw GoogleCredentialsNotSupportException() + } + + val code = try { + requestAuthorizationCode(clientId, SCOPE).await().toString() + } catch (cause: Throwable) { + throw cause.toGoogleCredentialsException() + } + + return GoogleCredentials.AuthorizationCode( + clientId = clientId, + code = code, + redirectUri = REDIRECT_URI, + ) + } + + private fun Throwable.toGoogleCredentialsException(): GoogleCredentialsException { + val message = message.orEmpty() + return when { + USER_CANCEL_KEYWORDS.any { it in message } -> GoogleCredentialsUserCancelException(this) + else -> GoogleCredentialsException(this) + } + } + + private companion object { + const val SCOPE = "openid email profile" + + // GIS popup 모드에서 OAuth2 token 교환 시 사용하는 표준 redirect_uri 값. + const val REDIRECT_URI = "postmessage" + + val USER_CANCEL_KEYWORDS = listOf( + "popup_closed", + "popup_failed_to_open", + "user_cancel", + "access_denied", + ) + } +} + +private fun isGoogleAccountsOAuth2Available(): Boolean = js( + "typeof google !== 'undefined' && typeof google.accounts !== 'undefined' && typeof google.accounts.oauth2 !== 'undefined'", +) + +private fun requestAuthorizationCode( + clientId: String, + scope: String, +): Promise = js( + """ + new Promise(function(resolve, reject) { + var client = google.accounts.oauth2.initCodeClient({ + client_id: clientId, + scope: scope, + ux_mode: 'popup', + callback: function(response) { + if (response && response.code) { + resolve(response.code); + } else { + reject(new Error(response && response.error ? response.error : 'unknown')); + } + }, + error_callback: function(err) { + reject(new Error(err && err.type ? err.type : 'unknown')); + } + }); + client.requestCode(); + }) + """, +) diff --git a/core/holiday-database/impl/build.gradle.kts b/core/holiday-database/impl/build.gradle.kts index ccd0db69..3740fde7 100644 --- a/core/holiday-database/impl/build.gradle.kts +++ b/core/holiday-database/impl/build.gradle.kts @@ -24,6 +24,13 @@ kotlin { implementation(libs.androidx.sqlite.bundled) } } + + wasmJsMain { + dependencies { + implementation(projects.library.sqliteWasmWorker) + implementation(libs.androidx.sqlite.web) + } + } } } diff --git a/core/holiday-database/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/impl/WasmJsHolidayDatabaseModule.kt b/core/holiday-database/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/impl/WasmJsHolidayDatabaseModule.kt new file mode 100644 index 00000000..ec83896b --- /dev/null +++ b/core/holiday-database/impl/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/impl/WasmJsHolidayDatabaseModule.kt @@ -0,0 +1,20 @@ +package io.github.taetae98coding.diary.core.holiday.database.impl + +import androidx.room3.Room +import androidx.room3.RoomDatabase +import io.github.taetae98coding.diary.core.holiday.database.impl.di.HolidayDatabaseBuilder +import io.github.taetae98coding.diary.library.sqlite.wasm.worker.createSqliteWasmWorkerDriver +import org.koin.core.annotation.Configuration +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Module + +@Module +@Configuration +public class WasmJsHolidayDatabaseModule { + @HolidayDatabaseBuilder + @Factory + internal fun providesHolidayDatabaseBuilder(): RoomDatabase.Builder { + return Room.databaseBuilder(name = "holiday.db") + .setDriver(createSqliteWasmWorkerDriver()) + } +} diff --git a/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt b/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt index fb1aea7f..c39f8706 100644 --- a/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt +++ b/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt @@ -2,10 +2,10 @@ package io.github.taetae98coding.diary.core.model.account import kotlin.uuid.Uuid -public sealed class Account { - public abstract val accountId: Uuid +public sealed interface Account { + public val accountId: Uuid - public data object Guest : Account() { + public data object Guest : Account { override val accountId: Uuid = Uuid.NIL } @@ -13,5 +13,5 @@ public sealed class Account { override val accountId: Uuid, val email: String, val profileImage: String?, - ) : Account() + ) : Account } diff --git a/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/sync/SyncStatus.kt b/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/sync/SyncStatus.kt index e0868542..5fa78b57 100644 --- a/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/sync/SyncStatus.kt +++ b/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/sync/SyncStatus.kt @@ -1,7 +1,7 @@ package io.github.taetae98coding.diary.core.model.sync -public sealed class SyncStatus { - public data object Idle : SyncStatus() - public data class Syncing(val type: SyncType) : SyncStatus() - public data object Failed : SyncStatus() +public sealed interface SyncStatus { + public data object Idle : SyncStatus + public data class Syncing(val type: SyncType) : SyncStatus + public data object Failed : SyncStatus } diff --git a/core/supabase/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/impl/SupabaseAuthImpl.kt b/core/supabase/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/impl/SupabaseAuthImpl.kt index 5f6e9e9b..407566b4 100644 --- a/core/supabase/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/impl/SupabaseAuthImpl.kt +++ b/core/supabase/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/impl/SupabaseAuthImpl.kt @@ -28,7 +28,7 @@ internal class SupabaseAuthImpl(private val supabase: SupabaseClient) : Supabase is SessionStatus.NotAuthenticated -> SupabaseSessionStatus.NotAuthenticated - is SessionStatus.Initializing -> SupabaseSessionStatus.Loading + is SessionStatus.Initializing -> getSessionStatusFromStorage() ?: SupabaseSessionStatus.Loading is SessionStatus.RefreshFailure -> { val cause = (supabase.auth.events.first() as? AuthEvent.RefreshFailure)?.cause @@ -40,18 +40,15 @@ internal class SupabaseAuthImpl(private val supabase: SupabaseClient) : Supabase private suspend fun resolveRefreshFailure(cause: RefreshFailureCause?): SupabaseSessionStatus { return when (cause) { is RefreshFailureCause.InternalServerError -> SupabaseSessionStatus.NotAuthenticated - - is RefreshFailureCause.NetworkError, null -> { - val user = supabase.auth.sessionManager.loadSession()?.user - if (user != null) { - SupabaseSessionStatus.Authenticated(userId = user.id, email = user.email) - } else { - SupabaseSessionStatus.NotAuthenticated - } - } + else -> getSessionStatusFromStorage() ?: SupabaseSessionStatus.NotAuthenticated } } + private suspend fun getSessionStatusFromStorage(): SupabaseSessionStatus? { + return supabase.auth.sessionManager.loadSession().user + ?.let { SupabaseSessionStatus.Authenticated(userId = it.id, email = it.email) } + } + override suspend fun signOut() { supabase.auth.signOut() } diff --git a/feature/login/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/login/di/GoogleClientId.kt b/feature/login/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/login/di/GoogleClientId.kt similarity index 100% rename from feature/login/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/login/di/GoogleClientId.kt rename to feature/login/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/login/di/GoogleClientId.kt diff --git a/feature/login/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/login/home/LoginEffect.kt b/feature/login/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/login/home/LoginEffect.kt index 9469110d..e0ea10f6 100644 --- a/feature/login/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/login/home/LoginEffect.kt +++ b/feature/login/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/login/home/LoginEffect.kt @@ -1,7 +1,7 @@ package io.github.taetae98coding.diary.feature.login.home -internal sealed class LoginEffect { - data object None : LoginEffect() - data object Finish : LoginEffect() - data object UnknownError : LoginEffect() +internal sealed interface LoginEffect { + data object None : LoginEffect + data object Finish : LoginEffect + data object UnknownError : LoginEffect } diff --git a/feature/login/src/jvmMain/kotlin/io/github/taetae98coding/diary/feature/login/di/GoogleClientId.kt b/feature/login/src/jvmMain/kotlin/io/github/taetae98coding/diary/feature/login/di/GoogleClientId.kt deleted file mode 100644 index 81fafefb..00000000 --- a/feature/login/src/jvmMain/kotlin/io/github/taetae98coding/diary/feature/login/di/GoogleClientId.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.taetae98coding.diary.feature.login.di - -import org.koin.core.annotation.Qualifier - -@Qualifier -public annotation class GoogleClientId diff --git a/feature/login/src/webMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.web.kt b/feature/login/src/webMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.web.kt new file mode 100644 index 00000000..d7608367 --- /dev/null +++ b/feature/login/src/webMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.web.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.feature.login.home + +import androidx.compose.runtime.Composable +import io.github.taetae98coding.diary.core.google.credentials.api.GoogleCredentialsManager +import io.github.taetae98coding.diary.core.google.credentials.compose.rememberGoogleCredentialsManager +import io.github.taetae98coding.diary.feature.login.di.GoogleClientId +import org.koin.compose.koinInject +import org.koin.core.qualifier.StringQualifier + +@Composable +internal actual fun rememberGoogleCredentialsManagerCompat(): GoogleCredentialsManager { + return rememberGoogleCredentialsManager(clientId = koinInject(qualifier = StringQualifier(requireNotNull(GoogleClientId::class.simpleName)))) +} diff --git a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddEffect.kt b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddEffect.kt index aa5c87f1..288129ed 100644 --- a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddEffect.kt +++ b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddEffect.kt @@ -1,8 +1,8 @@ package io.github.taetae98coding.diary.feature.memo.add -internal sealed class MemoAddEffect { - data object None : MemoAddEffect() - data object AddFinish : MemoAddEffect() - data object TitleBlank : MemoAddEffect() - data object UnknownError : MemoAddEffect() +internal sealed interface MemoAddEffect { + data object None : MemoAddEffect + data object AddFinish : MemoAddEffect + data object TitleBlank : MemoAddEffect + data object UnknownError : MemoAddEffect } diff --git a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddScaffold.kt b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddScaffold.kt index 8d37646e..e01f3bef 100644 --- a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddScaffold.kt +++ b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddScaffold.kt @@ -1,11 +1,13 @@ package io.github.taetae98coding.diary.feature.memo.add import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -24,7 +26,9 @@ import io.github.taetae98coding.diary.compose.core.card.DateRangeCard import io.github.taetae98coding.diary.compose.core.card.DescriptionCard import io.github.taetae98coding.diary.compose.core.card.TitleCard import io.github.taetae98coding.diary.compose.core.modifier.focusableKeyEvent +import io.github.taetae98coding.diary.compose.core.padding.plus import io.github.taetae98coding.diary.compose.core.preview.ScreenPreview +import io.github.taetae98coding.diary.compose.core.scaffold.DiaryScaffold import io.github.taetae98coding.diary.compose.core.theme.DiaryTheme import io.github.taetae98coding.diary.feature.memo.common.TagCard import io.github.taetae98coding.diary.feature.memo.common.TagCardUiState @@ -41,7 +45,7 @@ internal fun MemoAddScaffold( onTag: (Uuid) -> Unit = {}, onAdd: () -> Unit = {}, ) { - Scaffold( + DiaryScaffold( modifier = modifier .focusableKeyEvent(autoFocus = false) { event -> if (event.type == KeyEventType.KeyDown && event.isMetaPressed && event.key == Key.Enter) { @@ -63,7 +67,7 @@ internal fun MemoAddScaffold( LazyColumn( modifier = Modifier.padding(paddingValues) .fillMaxSize(), - contentPadding = DiaryTheme.dimen.screenPaddingValues, + contentPadding = DiaryTheme.dimen.screenPaddingValues + WindowInsets.navigationBars.asPaddingValues(), verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.screenCardSpace), ) { item { diff --git a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/common/TagCardUiState.kt b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/common/TagCardUiState.kt index d065f588..080dbf0f 100644 --- a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/common/TagCardUiState.kt +++ b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/common/TagCardUiState.kt @@ -1,7 +1,7 @@ package io.github.taetae98coding.diary.feature.memo.common -internal sealed class TagCardUiState { - data object Loading : TagCardUiState() +internal sealed interface TagCardUiState { + data object Loading : TagCardUiState - data class State(val tagList: List) : TagCardUiState() + data class State(val tagList: List) : TagCardUiState } diff --git a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailEffect.kt b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailEffect.kt index 2edeb8b4..a388e654 100644 --- a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailEffect.kt +++ b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailEffect.kt @@ -1,9 +1,9 @@ package io.github.taetae98coding.diary.feature.memo.detail -internal sealed class MemoDetailEffect { - data object None : MemoDetailEffect() - data object UpdateFinish : MemoDetailEffect() - data object CopyFinish : MemoDetailEffect() - data object DeleteFinish : MemoDetailEffect() - data object UnknownError : MemoDetailEffect() +internal sealed interface MemoDetailEffect { + data object None : MemoDetailEffect + data object UpdateFinish : MemoDetailEffect + data object CopyFinish : MemoDetailEffect + data object DeleteFinish : MemoDetailEffect + data object UnknownError : MemoDetailEffect } diff --git a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScaffold.kt b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScaffold.kt index d9e4710c..824ab23b 100644 --- a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScaffold.kt +++ b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScaffold.kt @@ -7,15 +7,17 @@ import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.IconButton import androidx.compose.material3.IconToggleButton -import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -43,7 +45,9 @@ import io.github.taetae98coding.diary.compose.core.icon.CopyIcon import io.github.taetae98coding.diary.compose.core.icon.DeleteIcon import io.github.taetae98coding.diary.compose.core.icon.FinishIcon import io.github.taetae98coding.diary.compose.core.modifier.focusableKeyEvent +import io.github.taetae98coding.diary.compose.core.padding.plus import io.github.taetae98coding.diary.compose.core.preview.ScreenPreview +import io.github.taetae98coding.diary.compose.core.scaffold.DiaryScaffold import io.github.taetae98coding.diary.compose.core.theme.DiaryTheme import io.github.taetae98coding.diary.core.model.memo.MemoDetail import io.github.taetae98coding.diary.feature.memo.common.TagCard @@ -71,7 +75,7 @@ internal fun MemoDetailScaffold( ) { val isLoading by remember { derivedStateOf { detailProvider() == null } } - Scaffold( + DiaryScaffold( modifier = modifier .focusableKeyEvent { event -> if (event.type == KeyEventType.KeyDown && event.isMetaPressed && event.key == Key.Enter) { @@ -123,7 +127,7 @@ internal fun MemoDetailScaffold( if (!isLoading) { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = DiaryTheme.dimen.screenPaddingValues, + contentPadding = DiaryTheme.dimen.screenPaddingValues + WindowInsets.navigationBars.asPaddingValues(), verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.screenCardSpace), ) { item { diff --git a/feature/more/build.gradle.kts b/feature/more/build.gradle.kts index 42245aa0..5c0d59fd 100644 --- a/feature/more/build.gradle.kts +++ b/feature/more/build.gradle.kts @@ -11,6 +11,8 @@ kotlin { implementation(projects.domain.credentials) implementation(projects.domain.holiday) implementation(projects.library.kotlinxDatetime) + implementation(projects.logger.analytics.api) + implementation(projects.logger.crashlytics.api) } } } diff --git a/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/goldenholiday/GoldenHolidayScaffoldUiState.kt b/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/goldenholiday/GoldenHolidayScaffoldUiState.kt index 150ad7cb..3e9d3319 100644 --- a/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/goldenholiday/GoldenHolidayScaffoldUiState.kt +++ b/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/goldenholiday/GoldenHolidayScaffoldUiState.kt @@ -2,16 +2,16 @@ package io.github.taetae98coding.diary.feature.more.goldenholiday import io.github.taetae98coding.diary.core.model.holiday.GoldenHoliday -internal sealed class GoldenHolidayScaffoldUiState { - data object Idle : GoldenHolidayScaffoldUiState() - data class Loading(val annualLeave: Int) : GoldenHolidayScaffoldUiState() +internal sealed interface GoldenHolidayScaffoldUiState { + data object Idle : GoldenHolidayScaffoldUiState + data class Loading(val annualLeave: Int) : GoldenHolidayScaffoldUiState data class State( val annualLeave: Int, val goldenHolidayList: List, - ) : GoldenHolidayScaffoldUiState() + ) : GoldenHolidayScaffoldUiState - data object HolidayNotExist : GoldenHolidayScaffoldUiState() + data object HolidayNotExist : GoldenHolidayScaffoldUiState - data object UnknownError : GoldenHolidayScaffoldUiState() + data object UnknownError : GoldenHolidayScaffoldUiState } diff --git a/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/goldenholiday/GoldenHolidayScreen.kt b/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/goldenholiday/GoldenHolidayScreen.kt index 7de04aaa..1c213f49 100644 --- a/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/goldenholiday/GoldenHolidayScreen.kt +++ b/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/goldenholiday/GoldenHolidayScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.IconButton import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -29,6 +28,7 @@ import androidx.lifecycle.viewmodel.compose.rememberViewModelStoreProvider import io.github.taetae98coding.diary.compose.core.button.NavigateUpButton import io.github.taetae98coding.diary.compose.core.chip.DiaryFilterChip import io.github.taetae98coding.diary.compose.core.icon.SettingsIcon +import io.github.taetae98coding.diary.compose.core.scaffold.DiaryScaffold import io.github.taetae98coding.diary.compose.core.theme.DiaryTheme import kotlinx.coroutines.flow.collectLatest import kotlinx.datetime.LocalDateRange @@ -62,7 +62,7 @@ private fun GoldenHolidayScreen( ) { val provider = rememberViewModelStoreProvider() - Scaffold( + DiaryScaffold( modifier = modifier, topBar = { TopBar( diff --git a/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountUiState.kt b/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountUiState.kt index 29c08aba..7b15e9a3 100644 --- a/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountUiState.kt +++ b/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountUiState.kt @@ -1,10 +1,10 @@ package io.github.taetae98coding.diary.feature.more.home -internal sealed class MoreHomeAccountUiState { - data object Loading : MoreHomeAccountUiState() - data object NotLogin : MoreHomeAccountUiState() +internal sealed interface MoreHomeAccountUiState { + data object Loading : MoreHomeAccountUiState + data object NotLogin : MoreHomeAccountUiState data class Login( val email: String, val profileImage: String?, - ) : MoreHomeAccountUiState() + ) : MoreHomeAccountUiState } diff --git a/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeScaffold.kt b/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeScaffold.kt index 1a3d5c57..52500171 100644 --- a/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeScaffold.kt +++ b/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeScaffold.kt @@ -20,6 +20,9 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.dropUnlessResumed import io.github.taetae98coding.diary.compose.core.preview.ScreenPreview import io.github.taetae98coding.diary.compose.core.theme.DiaryTheme +import io.github.taetae98coding.diary.logger.analytics.api.AnalyticsLogEntry +import io.github.taetae98coding.diary.logger.core.DiaryLogger +import io.github.taetae98coding.diary.logger.crashlytics.api.CrashlyticsLogEntry @Composable internal fun MoreHomeScaffold( @@ -71,6 +74,35 @@ internal fun MoreHomeScaffold( } } } + + item { + Card( + onClick = { + val throwable = IllegalStateException("test crash") + + DiaryLogger.log(AnalyticsLogEntry(name = "test_crash")) + DiaryLogger.log(CrashlyticsLogEntry(message = "test crash", throwable = throwable)) + + throw throwable + }, + modifier = Modifier.aspectRatio(1F), + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "💥", + fontSize = 24.sp, + ) + Text( + text = "Crash", + style = DiaryTheme.typography.bodySmall, + ) + } + } + } } } } diff --git a/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/add/RoutineAddEffect.kt b/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/add/RoutineAddEffect.kt index 907674cc..ea6c0ffc 100644 --- a/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/add/RoutineAddEffect.kt +++ b/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/add/RoutineAddEffect.kt @@ -1,13 +1,13 @@ package io.github.taetae98coding.diary.feature.routine.add -internal sealed class RoutineAddEffect { - data object None : RoutineAddEffect() +internal sealed interface RoutineAddEffect { + data object None : RoutineAddEffect - data object AddFinish : RoutineAddEffect() + data object AddFinish : RoutineAddEffect - data object TitleBlank : RoutineAddEffect() + data object TitleBlank : RoutineAddEffect - data object RRuleEmpty : RoutineAddEffect() + data object RRuleEmpty : RoutineAddEffect - data object UnknownError : RoutineAddEffect() + data object UnknownError : RoutineAddEffect } diff --git a/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/add/RoutineAddScaffold.kt b/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/add/RoutineAddScaffold.kt index 6950d5fb..af94f7b2 100644 --- a/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/add/RoutineAddScaffold.kt +++ b/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/add/RoutineAddScaffold.kt @@ -1,13 +1,15 @@ package io.github.taetae98coding.diary.feature.routine.add import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -27,7 +29,9 @@ import io.github.taetae98coding.diary.compose.core.card.LocalDateRangeCard import io.github.taetae98coding.diary.compose.core.card.TitleCard import io.github.taetae98coding.diary.compose.core.icon.AddIcon import io.github.taetae98coding.diary.compose.core.modifier.focusableKeyEvent +import io.github.taetae98coding.diary.compose.core.padding.plus import io.github.taetae98coding.diary.compose.core.preview.ScreenPreview +import io.github.taetae98coding.diary.compose.core.scaffold.DiaryScaffold import io.github.taetae98coding.diary.compose.core.theme.DiaryTheme import io.github.taetae98coding.diary.feature.routine.add.component.CalendarVisibilityCard import io.github.taetae98coding.diary.feature.routine.add.component.RRuleEditorDialog @@ -42,7 +46,7 @@ internal fun RoutineAddScaffold( onNavigateUp: () -> Unit = {}, onAdd: () -> Unit = {}, ) { - Scaffold( + DiaryScaffold( modifier = modifier .focusableKeyEvent(autoFocus = false) { event -> if (event.type == KeyEventType.KeyDown && event.isMetaPressed && event.key == Key.Enter) { @@ -66,7 +70,7 @@ internal fun RoutineAddScaffold( modifier = Modifier .padding(paddingValues) .fillMaxSize(), - contentPadding = DiaryTheme.dimen.screenPaddingValues, + contentPadding = DiaryTheme.dimen.screenPaddingValues + WindowInsets.navigationBars.asPaddingValues(), verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.screenCardSpace), ) { item { diff --git a/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/detail/RoutineDetailEffect.kt b/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/detail/RoutineDetailEffect.kt index b8f9fdbc..5a95796d 100644 --- a/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/detail/RoutineDetailEffect.kt +++ b/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/detail/RoutineDetailEffect.kt @@ -1,9 +1,9 @@ package io.github.taetae98coding.diary.feature.routine.detail -internal sealed class RoutineDetailEffect { - data object None : RoutineDetailEffect() +internal sealed interface RoutineDetailEffect { + data object None : RoutineDetailEffect - data object UpdateFinish : RoutineDetailEffect() + data object UpdateFinish : RoutineDetailEffect - data object UnknownError : RoutineDetailEffect() + data object UnknownError : RoutineDetailEffect } diff --git a/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/detail/RoutineDetailScaffold.kt b/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/detail/RoutineDetailScaffold.kt index 4335edbe..c62c7adf 100644 --- a/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/detail/RoutineDetailScaffold.kt +++ b/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/detail/RoutineDetailScaffold.kt @@ -5,13 +5,15 @@ import androidx.compose.animation.Crossfade import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -34,6 +36,8 @@ import io.github.taetae98coding.diary.compose.core.card.LocalDateRangeCard import io.github.taetae98coding.diary.compose.core.card.TitleCard import io.github.taetae98coding.diary.compose.core.icon.AddIcon import io.github.taetae98coding.diary.compose.core.modifier.focusableKeyEvent +import io.github.taetae98coding.diary.compose.core.padding.plus +import io.github.taetae98coding.diary.compose.core.scaffold.DiaryScaffold import io.github.taetae98coding.diary.compose.core.theme.DiaryTheme import io.github.taetae98coding.diary.core.model.routine.Routine import io.github.taetae98coding.diary.feature.routine.add.component.CalendarVisibilityCard @@ -53,7 +57,7 @@ internal fun RoutineDetailScaffold( ) { val isLoading by remember { derivedStateOf { routineProvider() == null } } - Scaffold( + DiaryScaffold( modifier = modifier .focusableKeyEvent(autoFocus = false) { event -> if (event.type == KeyEventType.KeyDown && event.isMetaPressed && event.key == Key.Enter) { @@ -102,7 +106,7 @@ internal fun RoutineDetailScaffold( if (!isLoading) { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = DiaryTheme.dimen.screenPaddingValues, + contentPadding = DiaryTheme.dimen.screenPaddingValues + WindowInsets.navigationBars.asPaddingValues(), verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.screenCardSpace), ) { item { diff --git a/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddEffect.kt b/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddEffect.kt index d4824560..002f1583 100644 --- a/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddEffect.kt +++ b/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddEffect.kt @@ -1,11 +1,11 @@ package io.github.taetae98coding.diary.feature.tag.add -internal sealed class TagAddEffect { - data object None : TagAddEffect() +internal sealed interface TagAddEffect { + data object None : TagAddEffect - data object AddFinish : TagAddEffect() + data object AddFinish : TagAddEffect - data object TitleBlank : TagAddEffect() + data object TitleBlank : TagAddEffect - data object UnknownError : TagAddEffect() + data object UnknownError : TagAddEffect } diff --git a/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddScaffold.kt b/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddScaffold.kt index 6c267045..46cd06c8 100644 --- a/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddScaffold.kt +++ b/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddScaffold.kt @@ -1,11 +1,13 @@ package io.github.taetae98coding.diary.feature.tag.add import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -23,7 +25,9 @@ import io.github.taetae98coding.diary.compose.core.card.ColorCard import io.github.taetae98coding.diary.compose.core.card.DescriptionCard import io.github.taetae98coding.diary.compose.core.card.TitleCard import io.github.taetae98coding.diary.compose.core.modifier.focusableKeyEvent +import io.github.taetae98coding.diary.compose.core.padding.plus import io.github.taetae98coding.diary.compose.core.preview.ScreenPreview +import io.github.taetae98coding.diary.compose.core.scaffold.DiaryScaffold import io.github.taetae98coding.diary.compose.core.theme.DiaryTheme @Composable @@ -34,7 +38,7 @@ internal fun TagAddScaffold( onNavigateUp: () -> Unit = {}, onAdd: () -> Unit = {}, ) { - Scaffold( + DiaryScaffold( modifier = modifier .focusableKeyEvent(autoFocus = false) { event -> if (event.type == KeyEventType.KeyDown && event.isMetaPressed && event.key == Key.Enter) { @@ -57,7 +61,7 @@ internal fun TagAddScaffold( modifier = Modifier .padding(paddingValues) .fillMaxSize(), - contentPadding = DiaryTheme.dimen.screenPaddingValues, + contentPadding = DiaryTheme.dimen.screenPaddingValues + WindowInsets.navigationBars.asPaddingValues(), verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.screenCardSpace), ) { item { diff --git a/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailEffect.kt b/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailEffect.kt index 958a2408..5e1ec3d7 100644 --- a/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailEffect.kt +++ b/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailEffect.kt @@ -1,11 +1,11 @@ package io.github.taetae98coding.diary.feature.tag.detail -internal sealed class TagDetailEffect { - data object None : TagDetailEffect() +internal sealed interface TagDetailEffect { + data object None : TagDetailEffect - data object UpdateFinish : TagDetailEffect() + data object UpdateFinish : TagDetailEffect - data object DeleteFinish : TagDetailEffect() + data object DeleteFinish : TagDetailEffect - data object UnknownError : TagDetailEffect() + data object UnknownError : TagDetailEffect } diff --git a/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScaffold.kt b/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScaffold.kt index dd611cf7..6b35861b 100644 --- a/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScaffold.kt +++ b/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScaffold.kt @@ -7,8 +7,11 @@ import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -16,7 +19,6 @@ import androidx.compose.material3.Card import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.IconButton import androidx.compose.material3.IconToggleButton -import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -43,7 +45,9 @@ import io.github.taetae98coding.diary.compose.core.icon.DeleteIcon import io.github.taetae98coding.diary.compose.core.icon.FinishIcon import io.github.taetae98coding.diary.compose.core.icon.MemoIcon import io.github.taetae98coding.diary.compose.core.modifier.focusableKeyEvent +import io.github.taetae98coding.diary.compose.core.padding.plus import io.github.taetae98coding.diary.compose.core.preview.ScreenPreview +import io.github.taetae98coding.diary.compose.core.scaffold.DiaryScaffold import io.github.taetae98coding.diary.compose.core.theme.DiaryTheme import io.github.taetae98coding.diary.core.model.tag.TagDetail @@ -64,7 +68,7 @@ internal fun TagDetailScaffold( ) { val isLoading by remember { derivedStateOf { detailProvider() == null } } - Scaffold( + DiaryScaffold( modifier = modifier .focusableKeyEvent { event -> if (event.type == KeyEventType.KeyDown && event.isMetaPressed && event.key == Key.Enter) { @@ -114,7 +118,7 @@ internal fun TagDetailScaffold( if (!isLoading) { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = DiaryTheme.dimen.screenPaddingValues, + contentPadding = DiaryTheme.dimen.screenPaddingValues + WindowInsets.navigationBars.asPaddingValues(), verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.screenCardSpace), ) { item { diff --git a/gradle.properties b/gradle.properties index 3671547a..0db48791 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,6 @@ org.gradle.configuration-cache=true android.useAndroidX=true android.nonTransitiveRClass=true -android.enableR8.fullMode=true kotlin.code.style=official kotlin.mpp.enableCInteropCommonization=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1309da27..4cb2c13e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,8 +11,9 @@ jetbrains-compose-material3 = "1.11.0-alpha07" jetbrains-lifecycle = "2.11.0-alpha03" jetbrains-navigation3 = "1.1.0" # kotlinx -kotlinx-coroutines = "1.11.0-rc01" # https://github.com/Kotlin/kotlinx.coroutines/releases -kotlinx-datetime = "0.8.0-rc01" # https://github.com/Kotlin/kotlinx-datetime/releases +kotlinx-browser = "0.5.0" # https://github.com/Kotlin/kotlinx-browser/releases +kotlinx-coroutines = "1.11.0-rc02" # https://github.com/Kotlin/kotlinx.coroutines/releases +kotlinx-datetime = "0.8.0-rc02" # https://github.com/Kotlin/kotlinx-datetime/releases kotlinx-serialization = "1.11.0" # https://github.com/Kotlin/kotlinx.serialization/releases # androidx androidx-activity = "1.13.0" # https://developer.android.com/jetpack/androidx/releases/activity?hl=en @@ -24,6 +25,7 @@ androidx-datastore = "1.3.0-alpha08" # https://developer.android.com/ androidx-paging = "3.5.0-rc01" # https://developer.android.com/jetpack/androidx/releases/paging?hl=en androidx-room3 = "3.0.0-alpha03" # https://developer.android.com/jetpack/androidx/releases/room3?hl=en androidx-sqlite = "2.7.0-alpha03" # https://developer.android.com/jetpack/androidx/releases/sqlite?hl=en +androidx-startup = "1.2.0" # https://developer.android.com/jetpack/androidx/releases/startup?hl=en androidx-test-core = "1.7.0" # https://developer.android.com/jetpack/androidx/releases/test?hl=en androidx-work = "2.11.2" # https://developer.android.com/jetpack/androidx/releases/work?hl=en # abc @@ -45,11 +47,12 @@ kotest = "6.1.11" # https://github.com/kotest/kote ktor = "3.4.3" # https://github.com/ktorio/ktor/releases leakcanary = "2.14" # https://github.com/square/leakcanary/releases mockk = "1.14.9" # https://github.com/mockk/mockk/releases +napier = "2.7.1" # https://github.com/AAkira/Napier/releases robolectric = "4.16.1" # https://github.com/robolectric/robolectric/releases multiplatform-markdown = "0.40.2" # https://github.com/mikepenz/multiplatform-markdown-renderer/releases spotless = "8.4.0" # https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md -spm = "1.9.0" # https://github.com/frankois944/spm4Kmp/releases -supabase = "3.5.0" # https://github.com/supabase-community/supabase-kt/releases +spm = "1.9.1" # https://github.com/frankois944/spm4Kmp/releases +supabase = "3.6.0" # https://github.com/supabase-community/supabase-kt/releases turbine = "1.2.1" # https://github.com/cashapp/turbine/releases [libraries] @@ -60,10 +63,11 @@ buildlogic-android = { group = "com.android.tools.build", name = "gradle", versi buildlogic-jetbrains-compose = { group = "org.jetbrains.compose", name = "compose-gradle-plugin", version.ref = "jetbrains-compose" } buildlogic-koin-compiler = { group = "io.insert-koin", name = "koin-compiler-gradle-plugin", version.ref = "koin-compiler-plugin" } # jetbrains +jetbrains-compose-runtime = { group = "org.jetbrains.compose.runtime", name = "runtime", version.ref = "jetbrains-compose" } +jetbrains-compose-components-resources = { group = "org.jetbrains.compose.components", name = "components-resources", version.ref = "jetbrains-compose" } jetbrains-compose-ui = { group = "org.jetbrains.compose.ui", name = "ui", version.ref = "jetbrains-compose" } jetbrains-compose-ui-tooling = { group = "org.jetbrains.compose.ui", name = "ui-tooling", version.ref = "jetbrains-compose" } jetbrains-compose-ui-tooling-preview = { group = "org.jetbrains.compose.ui", name = "ui-tooling-preview", version.ref = "jetbrains-compose" } -jetbrains-compose-runtime = { group = "org.jetbrains.compose.runtime", name = "runtime", version.ref = "jetbrains-compose" } jetbrains-compose-material-icons-extended = { group = "org.jetbrains.compose.material", name = "material-icons-extended", version.ref = "jetbrains-compose-material-icons" } jetbrains-compose-material3 = { group = "org.jetbrains.compose.material3", name = "material3", version.ref = "jetbrains-compose-material3" } jetbrains-compose-material3-navigation-suite = { group = "org.jetbrains.compose.material3", name = "material3-adaptive-navigation-suite", version.ref = "jetbrains-compose-material3" } @@ -71,6 +75,7 @@ jetbrains-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycl jetbrains-lifecycle-viewmodel-navigation3 = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "jetbrains-lifecycle" } jetbrains-navigation3-ui = { group = "org.jetbrains.androidx.navigation3", name = "navigation3-ui", version.ref = "jetbrains-navigation3" } # kotlinx +kotlinx-browser = { group = "org.jetbrains.kotlinx", name = "kotlinx-browser", version.ref = "kotlinx-browser" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } @@ -94,6 +99,8 @@ androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-p androidx-paging-common = { group = "androidx.paging", name = "paging-common", version.ref = "androidx-paging" } androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "androidx-paging" } androidx-sqlite-bundled = { group = "androidx.sqlite", name = "sqlite-bundled", version.ref = "androidx-sqlite" } +androidx-sqlite-web = { group = "androidx.sqlite", name = "sqlite-web", version.ref = "androidx-sqlite" } +androidx-startup-runtime = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidx-startup" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4-android", version.ref = "androidx-compose" } androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "androidx-compose" } androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidx-test-core" } @@ -127,6 +134,7 @@ ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serializatio leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanary" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } multiplatform-markdown-material3 = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-m3", version.ref = "multiplatform-markdown" } +napier = { group = "io.github.aakira", name = "napier", version.ref = "napier" } robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } supabase-auth = { group = "io.github.jan-tennert.supabase", name = "auth-kt", version.ref = "supabase" } supabase-functions = { group = "io.github.jan-tennert.supabase", name = "functions-kt", version.ref = "supabase" } diff --git a/kotlin-js-store/wasm/yarn.lock b/kotlin-js-store/wasm/yarn.lock index 5f4567d6..a5aa7e19 100644 --- a/kotlin-js-store/wasm/yarn.lock +++ b/kotlin-js-store/wasm/yarn.lock @@ -6,3 +6,23 @@ version "3.2.0" resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + +"@sqlite.org/sqlite-wasm@^3.50.1-build1": + version "3.50.1-build1" + resolved "https://registry.yarnpkg.com/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.50.1-build1.tgz#67dd9944b0e37ddb0ef2c8b195baa74ece838e44" + integrity sha512-yH4M/SHN98NibniIwTVk6rwTJjy7n39l7zwWY3u+qsfZBGTi4lC1uEl2NDvIlkzsFtfCBvHBJJFJ1iuU3UzzEQ== + +"sqlite-wasm-worker@file:../../library/sqlite-wasm-worker/worker": + version "0.0.3" + dependencies: + "@sqlite.org/sqlite-wasm" "^3.50.1-build1" + +ws@8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +ws@8.18.3: + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== diff --git a/library/sqlite-wasm-worker/build.gradle.kts b/library/sqlite-wasm-worker/build.gradle.kts new file mode 100644 index 00000000..585a44ff --- /dev/null +++ b/library/sqlite-wasm-worker/build.gradle.kts @@ -0,0 +1,22 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + alias(libs.plugins.diary.primitive.wasm) +} + +@OptIn(ExperimentalWasmDsl::class) +kotlin { + wasmJs { + useEsModules() + } + + sourceSets { + wasmJsMain { + dependencies { + implementation(libs.kotlinx.browser) + implementation(libs.androidx.sqlite.web) + implementation(npm("sqlite-wasm-worker", project.layout.projectDirectory.dir("worker").asFile)) + } + } + } +} diff --git a/library/sqlite-wasm-worker/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/library/sqlite/wasm/worker/SqliteWasmWorkerDriver.kt b/library/sqlite-wasm-worker/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/library/sqlite/wasm/worker/SqliteWasmWorkerDriver.kt new file mode 100644 index 00000000..c9f39702 --- /dev/null +++ b/library/sqlite-wasm-worker/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/library/sqlite/wasm/worker/SqliteWasmWorkerDriver.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.library.sqlite.wasm.worker + +import androidx.sqlite.driver.web.WebWorkerSQLiteDriver +import org.w3c.dom.Worker + +public fun createSqliteWasmWorkerDriver(): WebWorkerSQLiteDriver { + return WebWorkerSQLiteDriver(createSqliteWasmWorker()) +} + +@OptIn(ExperimentalWasmJsInterop::class) +private fun createSqliteWasmWorker(): Worker = js("""new Worker(new URL("sqlite-wasm-worker/worker.js", import.meta.url), { type: "module" })""") diff --git a/library/sqlite-wasm-worker/worker/package.json b/library/sqlite-wasm-worker/worker/package.json new file mode 100644 index 00000000..82efc10d --- /dev/null +++ b/library/sqlite-wasm-worker/worker/package.json @@ -0,0 +1,8 @@ +{ + "name": "sqlite-wasm-worker", + "version": "0.0.3", + "license": "APACHE-2.0", + "dependencies": { + "@sqlite.org/sqlite-wasm": "^3.50.1-build1" + } +} diff --git a/library/sqlite-wasm-worker/worker/worker.js b/library/sqlite-wasm-worker/worker/worker.js new file mode 100644 index 00000000..961c612a --- /dev/null +++ b/library/sqlite-wasm-worker/worker/worker.js @@ -0,0 +1,176 @@ +import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; + +let sqlite3 = null; + +// WASM beta uses an in-memory DB (sqlite's built-in `memdb` VFS) instead of OPFS. +// Reasons: +// 1. The default OPFS VFS (OpfsDb) requires SharedArrayBuffer + Atomics, +// which require COOP/COEP cross-origin isolation. We disabled COEP so +// that the Google OAuth popup can send postMessage back to the page. +// 2. OpfsSAHPoolDb works without isolation but introduces a separate +// storage namespace and per-tab locking issues. +// Trade-off: data does not survive a page reload. The app re-syncs from +// Supabase on next load. Switch to OpfsSAHPoolDb (or another persistent VFS) +// before promoting WASM out of beta. + +// Maps to track of active database connections and prepared statements by their unique IDs. +const databases = new Map(); // stores databaseId -> SQLiteDbObject +const statements = new Map(); // stores statementId -> SQLiteStatementObject + +// Counters to generate unique IDs for new database connections and statements. +let nextDatabaseId = 0; +let nextStatementId = 0; + +function openRequest(id, requestData) { + try { + console.log(`[sqlite-worker] openRequest fileName=${requestData.fileName} (already open=${databases.size})`); + const newDatabaseId = nextDatabaseId++; + // Same fileName ↔ same in-memory DB (memdb VFS shares named DBs across + // connections within this worker), so Room's writer+reader pool sees a + // single coherent database. + const uri = `file:${requestData.fileName}?vfs=memdb`; + const newDatabase = new sqlite3.oo1.DB(uri, 'c'); + databases.set(newDatabaseId, newDatabase); + console.log(`[sqlite-worker] memdb opened id=${newDatabaseId}`); + postMessage({'id': id, data: {'databaseId': newDatabaseId}}); + } catch (error) { + console.error(`[sqlite-worker] memdb open FAILED:`, error); + postMessage({'id': id, error: error.message}); + } +} + +function prepareRequest(id, requestData) { + try { + const newStatementId = nextStatementId++; + const resultData = { + 'statementId': newStatementId, + 'parameterCount': 0, + 'columnNames': [] + }; + const database = databases.get(requestData.databaseId); + if (!database) { + postMessage({'id': id, error: "Invalid database ID: " + requestData.databaseId}); + return; + } + const statement = database.prepare(requestData.sql); + statements.set(newStatementId, statement); + resultData.parameterCount = sqlite3.capi.sqlite3_bind_parameter_count(statement); + for (let i = 0; i < statement.columnCount; i++) { + resultData.columnNames.push(sqlite3.capi.sqlite3_column_name(statement, i)); + } + postMessage({'id': id, data: resultData}); + } catch (error) { + postMessage({'id': id, error: error.message}); + } +} + +function stepRequest(id, requestData) { + const statement = statements.get(requestData.statementId); + if (!statement) { + postMessage({'id': id, error: "Invalid statement ID: " + requestData.statementId}); + return; + } + try { + const resultData = { + 'rows': [], + 'columnTypes': [] + }; + statement.reset() + statement.clearBindings() + for (let i = 0; i < requestData.bindings.length; i++) { + statement.bind(i + 1, requestData.bindings[i]); + } + while (statement.step()) { + if (!resultData.columnTypes.length) { + for (let i = 0; i < statement.columnCount; i++) { + resultData.columnTypes.push(sqlite3.capi.sqlite3_column_type(statement, i)); + } + } + resultData.rows.push(statement.get([])); + } + postMessage({'id': id, data: resultData}); + } catch (error) { + postMessage({'id': id, error: error.message}); + } +} + +function closeRequest(id, requestData) { + if (requestData.statementId) { + const statement = statements.get(requestData.statementId); + if (!statement) { + postMessage({'id': id, error: "Invalid statement ID: " + requestData.statementId}); + return; + } + try { + statement.finalize(); + statements.delete(requestData.statementId); + } catch (error) { + postMessage({'id': id, error: error.message}); + } + } + + if (requestData.databaseId) { + const database = databases.get(requestData.databaseId); + if (!database) { + postMessage({'id': id, error: "Invalid database ID: " + requestData.databaseId}); + return; + } + try { + database.close(); + databases.delete(requestData.databaseId); + } catch (error) { + postMessage({'id': id, error: error.message}); + } + } +} + +const commandMap = { + 'open': openRequest, + 'prepare': prepareRequest, + 'step': stepRequest, + 'close': closeRequest, +}; + +function handleMessage(e) { + const requestMsg = e.data; + if (!Object.hasOwn(requestMsg, 'data') && requestMsg.data == null) { + postMessage( + {'id': requestMsg.id, 'error': "Invalid request, missing 'data'."} + ); + return; + } + if (!Object.hasOwn(requestMsg.data, 'cmd') && requestMsg.data.cmd == null) { + postMessage( + {'id': requestMsg.id, 'error': "Invalid request, missing 'cmd'."} + ); + return; + } + const command = requestMsg.data.cmd; + const requestHandler = commandMap[command]; + if (requestHandler) { + requestHandler(requestMsg.id, requestMsg.data); + } else { + postMessage( + {'id': requestMsg.id, 'error': "Invalid request, unknown command: '" + command + "'."} + ); + } +} + +const messageQueue = []; +onmessage = (e) => { + if (!sqlite3) { + messageQueue.push(e); + } else { + handleMessage(e); + } +}; + +sqlite3InitModule().then((instance) => { + sqlite3 = instance; + console.log(`[sqlite-worker] sqlite3 initialized (in-memory mode).`); + while (messageQueue.length > 0) { + handleMessage(messageQueue.shift()); + } +}).catch(err => { + console.error(`[sqlite-worker] sqlite3InitModule FAILED:`, err); +}); diff --git a/logger/analytics/api/build.gradle.kts b/logger/analytics/api/build.gradle.kts new file mode 100644 index 00000000..e6d78820 --- /dev/null +++ b/logger/analytics/api/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(libs.plugins.diary.primitive.multiplatform) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(projects.logger.core) + } + } + } +} diff --git a/logger/analytics/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/analytics/api/AnalyticsLogEntry.kt b/logger/analytics/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/analytics/api/AnalyticsLogEntry.kt new file mode 100644 index 00000000..4086ffd7 --- /dev/null +++ b/logger/analytics/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/analytics/api/AnalyticsLogEntry.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.logger.analytics.api + +import io.github.taetae98coding.diary.logger.core.LogEntry + +public data class AnalyticsLogEntry( + val name: String, + val params: Map = emptyMap(), +) : LogEntry() diff --git a/logger/analytics/impl/build.gradle.kts b/logger/analytics/impl/build.gradle.kts new file mode 100644 index 00000000..1f37d16c --- /dev/null +++ b/logger/analytics/impl/build.gradle.kts @@ -0,0 +1,40 @@ +import io.github.frankois944.spmForKmp.swiftPackageConfig +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.konan.target.Family + +plugins { + alias(libs.plugins.diary.primitive.multiplatform.android) + alias(libs.plugins.spm) +} + +kotlin { + targets.withType() + .filter { it.konanTarget.family == Family.IOS } + .forEach { target -> + target.swiftPackageConfig("KMPFirebaseAnalytics") { + minIos = "26.4" + dependency { + remotePackageVersion( + url = uri("https://github.com/firebase/firebase-ios-sdk"), + products = { add("FirebaseAnalytics", exportToKotlin = true) }, + version = "12.11.0", + ) + } + } + } + + sourceSets { + commonMain { + dependencies { + api(projects.logger.analytics.api) + } + } + + androidMain { + dependencies { + implementation(project.dependencies.platform(libs.firebase.bom)) + implementation(libs.firebase.analytics) + } + } + } +} diff --git a/logger/analytics/impl/src/androidMain/kotlin/io/github/taetae98coding/diary/logger/analytics/impl/AndroidAnalyticsLogger.kt b/logger/analytics/impl/src/androidMain/kotlin/io/github/taetae98coding/diary/logger/analytics/impl/AndroidAnalyticsLogger.kt new file mode 100644 index 00000000..387601ce --- /dev/null +++ b/logger/analytics/impl/src/androidMain/kotlin/io/github/taetae98coding/diary/logger/analytics/impl/AndroidAnalyticsLogger.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.logger.analytics.impl + +import android.os.Bundle +import com.google.firebase.Firebase +import com.google.firebase.analytics.analytics +import io.github.taetae98coding.diary.logger.analytics.api.AnalyticsLogEntry +import io.github.taetae98coding.diary.logger.core.LogEntry +import io.github.taetae98coding.diary.logger.core.Logger + +public data object AndroidAnalyticsLogger : Logger { + override fun log(entry: LogEntry) { + if (entry !is AnalyticsLogEntry) return + + val bundle = Bundle() + .apply { entry.params.forEach { (key, value) -> putString(key, value) } } + + Firebase.analytics.logEvent(entry.name, bundle) + } +} diff --git a/logger/analytics/impl/src/appleMain/kotlin/io/github/taetae98coding/diary/logger/analytics/impl/AppleAnalyticsLogger.kt b/logger/analytics/impl/src/appleMain/kotlin/io/github/taetae98coding/diary/logger/analytics/impl/AppleAnalyticsLogger.kt new file mode 100644 index 00000000..0d25ee9a --- /dev/null +++ b/logger/analytics/impl/src/appleMain/kotlin/io/github/taetae98coding/diary/logger/analytics/impl/AppleAnalyticsLogger.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary.logger.analytics.impl + +import FirebaseAnalytics.FIRAnalytics +import io.github.taetae98coding.diary.logger.analytics.api.AnalyticsLogEntry +import io.github.taetae98coding.diary.logger.core.LogEntry +import io.github.taetae98coding.diary.logger.core.Logger +import kotlinx.cinterop.ExperimentalForeignApi + +@Suppress("UNCHECKED_CAST") +@OptIn(ExperimentalForeignApi::class) +public data object AppleAnalyticsLogger : Logger { + override fun log(entry: LogEntry) { + if (entry !is AnalyticsLogEntry) return + + FIRAnalytics.logEventWithName(name = entry.name, parameters = entry.params as Map) + } +} diff --git a/logger/analytics/impl/src/swift/KMPFirebaseAnalytics/KMPFirebaseAnalyticsBridge.swift b/logger/analytics/impl/src/swift/KMPFirebaseAnalytics/KMPFirebaseAnalyticsBridge.swift new file mode 100644 index 00000000..67f25fbd --- /dev/null +++ b/logger/analytics/impl/src/swift/KMPFirebaseAnalytics/KMPFirebaseAnalyticsBridge.swift @@ -0,0 +1,2 @@ +import Foundation +import FirebaseAnalytics diff --git a/logger/console/api/build.gradle.kts b/logger/console/api/build.gradle.kts new file mode 100644 index 00000000..e6d78820 --- /dev/null +++ b/logger/console/api/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(libs.plugins.diary.primitive.multiplatform) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(projects.logger.core) + } + } + } +} diff --git a/logger/console/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/console/api/ConsoleLogEntry.kt b/logger/console/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/console/api/ConsoleLogEntry.kt new file mode 100644 index 00000000..bf34702b --- /dev/null +++ b/logger/console/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/console/api/ConsoleLogEntry.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.logger.console.api + +import io.github.taetae98coding.diary.logger.core.LogEntry + +public data class ConsoleLogEntry( + val level: LogLevel, + val message: String, + val throwable: Throwable? = null, +) : LogEntry() diff --git a/logger/console/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/console/api/LogLevel.kt b/logger/console/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/console/api/LogLevel.kt new file mode 100644 index 00000000..1a00dd96 --- /dev/null +++ b/logger/console/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/console/api/LogLevel.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.logger.console.api + +public enum class LogLevel { + VERBOSE, + DEBUG, + INFO, + WARNING, + ERROR, + ASSERT, +} diff --git a/logger/console/impl/build.gradle.kts b/logger/console/impl/build.gradle.kts new file mode 100644 index 00000000..e6838742 --- /dev/null +++ b/logger/console/impl/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.diary.primitive.multiplatform) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(libs.napier) + api(projects.logger.console.api) + } + } + } +} diff --git a/logger/console/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/console/impl/ConsoleLogger.kt b/logger/console/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/console/impl/ConsoleLogger.kt new file mode 100644 index 00000000..21cc997c --- /dev/null +++ b/logger/console/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/console/impl/ConsoleLogger.kt @@ -0,0 +1,24 @@ +package io.github.taetae98coding.diary.logger.console.impl + +import io.github.aakira.napier.Napier +import io.github.taetae98coding.diary.logger.console.api.ConsoleLogEntry +import io.github.taetae98coding.diary.logger.console.api.LogLevel +import io.github.taetae98coding.diary.logger.core.LogEntry +import io.github.taetae98coding.diary.logger.core.Logger + +public data object ConsoleLogger : Logger { + override fun log(entry: LogEntry) { + if (entry is ConsoleLogEntry) { + when (entry.level) { + LogLevel.VERBOSE -> Napier.v(message = entry.message, throwable = entry.throwable) + LogLevel.DEBUG -> Napier.d(message = entry.message, throwable = entry.throwable) + LogLevel.INFO -> Napier.i(message = entry.message, throwable = entry.throwable) + LogLevel.WARNING -> Napier.w(message = entry.message, throwable = entry.throwable) + LogLevel.ERROR -> Napier.e(message = entry.message, throwable = entry.throwable) + LogLevel.ASSERT -> Napier.wtf(message = entry.message, throwable = entry.throwable) + } + } else { + Napier.d(message = entry.toString()) + } + } +} diff --git a/logger/core/build.gradle.kts b/logger/core/build.gradle.kts new file mode 100644 index 00000000..a5e2410d --- /dev/null +++ b/logger/core/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + alias(libs.plugins.diary.primitive.multiplatform.android) +} diff --git a/logger/core/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/core/DiaryLogger.kt b/logger/core/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/core/DiaryLogger.kt new file mode 100644 index 00000000..ea7c4cb9 --- /dev/null +++ b/logger/core/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/core/DiaryLogger.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.logger.core + +public data object DiaryLogger : Logger { + private val loggers: MutableList = mutableListOf() + + public fun addLogger(logger: Logger) { + loggers += logger + } + + override fun log(entry: LogEntry) { + loggers.forEach { it.log(entry) } + } +} diff --git a/logger/core/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/core/LogEntry.kt b/logger/core/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/core/LogEntry.kt new file mode 100644 index 00000000..0cd67586 --- /dev/null +++ b/logger/core/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/core/LogEntry.kt @@ -0,0 +1,3 @@ +package io.github.taetae98coding.diary.logger.core + +public open class LogEntry diff --git a/logger/core/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/core/Logger.kt b/logger/core/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/core/Logger.kt new file mode 100644 index 00000000..9458c43e --- /dev/null +++ b/logger/core/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/core/Logger.kt @@ -0,0 +1,5 @@ +package io.github.taetae98coding.diary.logger.core + +public interface Logger { + public fun log(entry: LogEntry) +} diff --git a/logger/crashlytics/api/build.gradle.kts b/logger/crashlytics/api/build.gradle.kts new file mode 100644 index 00000000..e6d78820 --- /dev/null +++ b/logger/crashlytics/api/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(libs.plugins.diary.primitive.multiplatform) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(projects.logger.core) + } + } + } +} diff --git a/logger/crashlytics/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/crashlytics/api/CrashlyticsLogEntry.kt b/logger/crashlytics/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/crashlytics/api/CrashlyticsLogEntry.kt new file mode 100644 index 00000000..eab0b6e1 --- /dev/null +++ b/logger/crashlytics/api/src/commonMain/kotlin/io/github/taetae98coding/diary/logger/crashlytics/api/CrashlyticsLogEntry.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.logger.crashlytics.api + +import io.github.taetae98coding.diary.logger.core.LogEntry + +public data class CrashlyticsLogEntry( + val message: String, + val throwable: Throwable, +) : LogEntry() diff --git a/logger/crashlytics/impl/build.gradle.kts b/logger/crashlytics/impl/build.gradle.kts new file mode 100644 index 00000000..91d6dc14 --- /dev/null +++ b/logger/crashlytics/impl/build.gradle.kts @@ -0,0 +1,40 @@ +import io.github.frankois944.spmForKmp.swiftPackageConfig +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.konan.target.Family + +plugins { + alias(libs.plugins.diary.primitive.multiplatform.android) + alias(libs.plugins.spm) +} + +kotlin { + targets.withType() + .filter { it.konanTarget.family == Family.IOS } + .forEach { target -> + target.swiftPackageConfig("KMPFirebaseCrashlytics") { + minIos = "26.4" + dependency { + remotePackageVersion( + url = uri("https://github.com/firebase/firebase-ios-sdk"), + products = { add("FirebaseCrashlytics", exportToKotlin = true) }, + version = "12.11.0", + ) + } + } + } + + sourceSets { + commonMain { + dependencies { + api(projects.logger.crashlytics.api) + } + } + + androidMain { + dependencies { + implementation(project.dependencies.platform(libs.firebase.bom)) + implementation(libs.firebase.crashlytics) + } + } + } +} diff --git a/logger/crashlytics/impl/src/androidMain/kotlin/io/github/taetae98coding/diary/logger/crashlytics/impl/AndroidCrashlyticsLogger.kt b/logger/crashlytics/impl/src/androidMain/kotlin/io/github/taetae98coding/diary/logger/crashlytics/impl/AndroidCrashlyticsLogger.kt new file mode 100644 index 00000000..3df466b3 --- /dev/null +++ b/logger/crashlytics/impl/src/androidMain/kotlin/io/github/taetae98coding/diary/logger/crashlytics/impl/AndroidCrashlyticsLogger.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.logger.crashlytics.impl + +import com.google.firebase.Firebase +import com.google.firebase.crashlytics.crashlytics +import io.github.taetae98coding.diary.logger.core.LogEntry +import io.github.taetae98coding.diary.logger.core.Logger +import io.github.taetae98coding.diary.logger.crashlytics.api.CrashlyticsLogEntry + +public data object AndroidCrashlyticsLogger : Logger { + override fun log(entry: LogEntry) { + if (entry !is CrashlyticsLogEntry) return + + Firebase.crashlytics.log(entry.message) + Firebase.crashlytics.recordException(entry.throwable) + } +} diff --git a/logger/crashlytics/impl/src/appleMain/kotlin/io/github/taetae98coding/diary/logger/crashlytics/impl/AppleCrashlyticsLogger.kt b/logger/crashlytics/impl/src/appleMain/kotlin/io/github/taetae98coding/diary/logger/crashlytics/impl/AppleCrashlyticsLogger.kt new file mode 100644 index 00000000..35b1dcb8 --- /dev/null +++ b/logger/crashlytics/impl/src/appleMain/kotlin/io/github/taetae98coding/diary/logger/crashlytics/impl/AppleCrashlyticsLogger.kt @@ -0,0 +1,34 @@ +package io.github.taetae98coding.diary.logger.crashlytics.impl + +import FirebaseCrashlytics.FIRCrashlytics +import io.github.taetae98coding.diary.logger.core.LogEntry +import io.github.taetae98coding.diary.logger.core.Logger +import io.github.taetae98coding.diary.logger.crashlytics.api.CrashlyticsLogEntry +import kotlinx.cinterop.ExperimentalForeignApi +import platform.Foundation.NSError +import platform.Foundation.NSLocalizedDescriptionKey + +@OptIn(ExperimentalForeignApi::class) +public data object AppleCrashlyticsLogger : Logger { + override fun log(entry: LogEntry) { + if (entry !is CrashlyticsLogEntry) return + + FIRCrashlytics.crashlytics().log(entry.message) + FIRCrashlytics.crashlytics().recordError(entry.throwable.toNSError()) + } + + private fun Throwable.toNSError(): NSError { + val userInfo = mutableMapOf( + "KotlinExceptionClass" to this::class.qualifiedName, + NSLocalizedDescriptionKey to (message ?: this::class.simpleName ?: "Unknown"), + "KotlinStackTrace" to stackTraceToString(), + ) + cause?.let { userInfo["KotlinCause"] = it.toNSError() } + + return NSError.errorWithDomain( + domain = this::class.qualifiedName ?: "KotlinException", + code = 0, + userInfo = userInfo as Map, + ) + } +} diff --git a/logger/crashlytics/impl/src/swift/KMPFirebaseCrashlytics/KMPFirebaseCrashlyticsBridge.swift b/logger/crashlytics/impl/src/swift/KMPFirebaseCrashlytics/KMPFirebaseCrashlyticsBridge.swift new file mode 100644 index 00000000..076fad85 --- /dev/null +++ b/logger/crashlytics/impl/src/swift/KMPFirebaseCrashlytics/KMPFirebaseCrashlyticsBridge.swift @@ -0,0 +1,2 @@ +import Foundation +import FirebaseCrashlytics diff --git a/presenter/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/presenter/memo/api/MemoListEffect.kt b/presenter/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/presenter/memo/api/MemoListEffect.kt index 878b81cc..aa13054a 100644 --- a/presenter/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/presenter/memo/api/MemoListEffect.kt +++ b/presenter/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/presenter/memo/api/MemoListEffect.kt @@ -2,8 +2,8 @@ package io.github.taetae98coding.diary.presenter.memo.api import kotlin.uuid.Uuid -public sealed class MemoListEffect { - public data object None : MemoListEffect() - public data class FinishComplete(val memoId: Uuid) : MemoListEffect() - public data class DeleteComplete(val memoId: Uuid) : MemoListEffect() +public sealed interface MemoListEffect { + public data object None : MemoListEffect + public data class FinishComplete(val memoId: Uuid) : MemoListEffect + public data class DeleteComplete(val memoId: Uuid) : MemoListEffect } diff --git a/presenter/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/presenter/memo/compose/list/MemoListNavigation.kt b/presenter/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/presenter/memo/compose/list/MemoListNavigation.kt index 0ead7612..6271770c 100644 --- a/presenter/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/presenter/memo/compose/list/MemoListNavigation.kt +++ b/presenter/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/presenter/memo/compose/list/MemoListNavigation.kt @@ -1,6 +1,6 @@ package io.github.taetae98coding.diary.presenter.memo.compose.list -public sealed class MemoListNavigation { - public data object None : MemoListNavigation() - public data class NavigateUp(val onNavigateUp: () -> Unit) : MemoListNavigation() +public sealed interface MemoListNavigation { + public data object None : MemoListNavigation + public data class NavigateUp(val onNavigateUp: () -> Unit) : MemoListNavigation } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5bdf0756..0c90a6c1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -92,8 +92,16 @@ include(":feature:tag") include(":library:kotlinx-file") include(":library:paging-common") include(":library:room-common") +include(":library:sqlite-wasm-worker") include(":library:compose-ui") include(":library:koin-compose") include(":library:kotlinx-coroutines-core") include(":library:kotlinx-datetime") include(":library:navigation3-runtime") +include(":logger:core") +include(":logger:console:api") +include(":logger:console:impl") +include(":logger:analytics:api") +include(":logger:analytics:impl") +include(":logger:crashlytics:api") +include(":logger:crashlytics:impl") diff --git a/supabase/CLAUDE.md b/supabase/CLAUDE.md new file mode 100644 index 00000000..4089dfde --- /dev/null +++ b/supabase/CLAUDE.md @@ -0,0 +1,46 @@ +# Supabase 운영 지침 + +## DB 접근 정책 + +- 모든 DB 접근은 Edge Function(`service_role`)을 경유한다. 클라이언트(supabase-js 등)에서 테이블에 직접 접근하지 않는다. +- 모든 public 테이블에 `service_role only` RLS 정책을 둔다. `service_role`은 `BYPASSRLS`로 정책을 거치지 않고 통과하며, anon/authenticated는 정책에 의해 차단된다. +- 이벤트 트리거 `ensure_rls`(`public.rls_auto_enable()`)가 신규 테이블 생성 시 **RLS enable + 정책 추가**를 자동 수행한다. 따라서 새 테이블 마이그레이션에서 별도 RLS/정책 작업은 불필요하다. +- `rls_auto_enable()`은 `SECURITY DEFINER` 함수이며 `PUBLIC`/`anon`/`authenticated`의 `EXECUTE` 권한이 `REVOKE`되어 있어 RPC로 호출할 수 없다. 신규 `SECURITY DEFINER` 함수 추가 시 동일 패턴(EXECUTE REVOKE)을 따른다. + +정책 정의: + +```sql +CREATE POLICY "service_role only" ON public. FOR ALL TO public + USING ((select auth.role()) = 'service_role') + WITH CHECK ((select auth.role()) = 'service_role'); +``` + +`auth.role()`을 `(select ...)`로 감싸면 row마다 재평가되지 않고 쿼리당 1회만 평가된다 (initplan 캐싱). Supabase Advisor의 `auth_rls_initplan` 경고를 피하기 위함. + +## Dev / Real 환경 비교 + +Dev와 Real 두 Supabase 프로젝트의 정합성을 점검할 때 따르는 절차. + +### 원칙 + +- **마이그레이션 히스토리는 비교하지 않는다.** Dev는 개발 중 시행착오·롤백·재정렬이 잦아 1:1 대조가 무의미하다. +- **현재 스키마 객체 자체(결과물)를 비교한다.** Real에 누락된 객체 / 타입 불일치만 추려내는 것이 목표. + +### 비교 대상 + +| 항목 | 비교 도구 | +|---|---| +| 테이블 / 컬럼 / PK / FK | `mcp__supabase__list_tables` (verbose) | +| RLS Policy | `pg_policies` | +| Index | `pg_indexes` | +| Trigger | `information_schema.triggers` | +| Function | `pg_proc` + `pg_namespace` | +| Unique / Check 제약 | `information_schema.table_constraints` | +| Sequence / default | `pg_attribute` + `pg_attrdef` | +| Edge Function 본문 | `mcp__supabase__get_edge_function` (sha256 비교 후 본문 diff) | + +### 차이 발견 시 처리 방향 + +- **Real에만 누락**: Dev 정의를 정본으로 보고 Real에 마이그레이션 적용. +- **Dev에만 누락**: 실험성 객체일 가능성. Dev에서 정리 후 적용 여부 결정. +- **양쪽에 다른 정의**: 코드(클라이언트/Edge Function) 사용 형태를 기준으로 어느 쪽이 맞는지 판단. diff --git a/supabase/functions/_shared/cors.ts b/supabase/functions/_shared/cors.ts new file mode 100644 index 00000000..0563fc47 --- /dev/null +++ b/supabase/functions/_shared/cors.ts @@ -0,0 +1,15 @@ +export const corsHeaders: Record = { + "Access-Control-Allow-Origin": "*", + // `*` allows all headers; `authorization` is listed explicitly because the + // CORS spec excludes Authorization from the wildcard. + "Access-Control-Allow-Headers": "authorization, *", + "Access-Control-Allow-Methods": "*", + "Access-Control-Max-Age": "86400", +}; + +export function handleCorsPreflight(req: Request): Response | null { + if (req.method === "OPTIONS") { + return new Response(null, { status: 204, headers: corsHeaders }); + } + return null; +} diff --git a/supabase/functions/v1-memo-pull/entity/memo-pull-request-v1.ts b/supabase/functions/v1-memo-pull/entity/memo-pull-request-v1.ts deleted file mode 100644 index ba4830a1..00000000 --- a/supabase/functions/v1-memo-pull/entity/memo-pull-request-v1.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface MemoPullRequestV1 { - updatedAt: number; -} diff --git a/supabase/functions/v1-memo-pull/index.ts b/supabase/functions/v1-memo-pull/index.ts deleted file mode 100644 index 00d2c067..00000000 --- a/supabase/functions/v1-memo-pull/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import "jsr:@supabase/functions-js/edge-runtime.d.ts"; -import { supabaseAdmin } from "../_shared/supabase-admin.ts"; -import { MemoRepository } from "../_shared/memo-repository.ts"; -import { Memo } from "../_shared/entity/memo.ts"; -import { MemoPullRequestV1 } from "./entity/memo-pull-request-v1.ts"; -import { MemoV1 } from "../v1-memo-push/entity/memo-v1.ts"; - -const memoRepository = new MemoRepository(supabaseAdmin); - -function toMemoV1(memo: Memo): MemoV1 { - return { - id: memo.id, - detail: { - title: memo.detail.title, - description: memo.detail.description, - isAllDay: memo.detail.isAllDay, - start: memo.detail.start, - endInclusive: memo.detail.endInclusive, - color: memo.detail.color, - }, - isFinished: memo.isFinished, - isDeleted: memo.isDeleted, - updatedAt: memo.updatedAt, - createdAt: memo.createdAt, - }; -} - -Deno.serve(async (req: Request) => { - if (req.method !== "POST") { - return new Response(JSON.stringify({ error: "Method not allowed" }), { - status: 405, - headers: { "Content-Type": "application/json" }, - }); - } - - try { - const authorization = req.headers.get("Authorization"); - if (!authorization) { - return new Response(JSON.stringify({ error: "Unauthorized" }), { - status: 401, - headers: { "Content-Type": "application/json" }, - }); - } - - const { data: { user }, error: authError } = await supabaseAdmin.auth.getUser( - authorization.replace("Bearer ", ""), - ); - - if (authError || !user) { - return new Response(JSON.stringify({ error: "Unauthorized" }), { - status: 401, - headers: { "Content-Type": "application/json" }, - }); - } - - const { updatedAt }: MemoPullRequestV1 = await req.json(); - const memoList = await memoRepository.getUpdatedAfter(user.id, updatedAt); - - return new Response(JSON.stringify(memoList.map(toMemoV1)), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } catch (error) { - return new Response(JSON.stringify({ error: error.message }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); - } -}); diff --git a/supabase/functions/v1-memo-push/entity/memo-detail-v1.ts b/supabase/functions/v1-memo-push/entity/memo-detail-v1.ts deleted file mode 100644 index 2a7fab30..00000000 --- a/supabase/functions/v1-memo-push/entity/memo-detail-v1.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface MemoDetailV1 { - title: string; - description: string; - isAllDay: boolean; - start: string | null; - endInclusive: string | null; - color: number; -} diff --git a/supabase/functions/v1-memo-push/entity/memo-v1.ts b/supabase/functions/v1-memo-push/entity/memo-v1.ts deleted file mode 100644 index 87fde30e..00000000 --- a/supabase/functions/v1-memo-push/entity/memo-v1.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { MemoDetailV1 } from "./memo-detail-v1.ts"; - -export interface MemoV1 { - id: string; - detail: MemoDetailV1; - isFinished: boolean; - isDeleted: boolean; - updatedAt: number; - createdAt: number; -} diff --git a/supabase/functions/v1-memo-push/index.ts b/supabase/functions/v1-memo-push/index.ts deleted file mode 100644 index 3e633624..00000000 --- a/supabase/functions/v1-memo-push/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import "jsr:@supabase/functions-js/edge-runtime.d.ts"; -import { supabaseAdmin } from "../_shared/supabase-admin.ts"; -import { MemoRepository } from "../_shared/memo-repository.ts"; -import { Memo } from "../_shared/entity/memo.ts"; -import { MemoV1 } from "./entity/memo-v1.ts"; - -const memoRepository = new MemoRepository(supabaseAdmin); - -async function toMemo(v1: MemoV1): Promise { - const existing = await memoRepository.findById(v1.id); - - return { - id: v1.id, - detail: { - title: v1.detail.title, - description: v1.detail.description, - isAllDay: v1.detail.isAllDay, - start: v1.detail.start, - endInclusive: v1.detail.endInclusive, - color: v1.detail.color, - }, - primaryTag: existing?.primaryTag ?? null, - isFinished: v1.isFinished, - isDeleted: v1.isDeleted, - updatedAt: v1.updatedAt, - createdAt: v1.createdAt, - }; -} - -Deno.serve(async (req: Request) => { - if (req.method !== "POST") { - return new Response(JSON.stringify({ error: "Method not allowed" }), { - status: 405, - headers: { "Content-Type": "application/json" }, - }); - } - - try { - const authorization = req.headers.get("Authorization"); - if (!authorization) { - return new Response(JSON.stringify({ error: "Unauthorized" }), { - status: 401, - headers: { "Content-Type": "application/json" }, - }); - } - - const { data: { user }, error: authError } = await supabaseAdmin.auth.getUser( - authorization.replace("Bearer ", ""), - ); - - if (authError || !user) { - return new Response(JSON.stringify({ error: "Unauthorized" }), { - status: 401, - headers: { "Content-Type": "application/json" }, - }); - } - - const { memoList }: { memoList: MemoV1[] } = await req.json(); - const memos = await Promise.all(memoList.map(toMemo)); - await memoRepository.upsertIfNewer(user.id, memos); - - return new Response(null, { status: 204 }); - } catch (error) { - return new Response(JSON.stringify({ error: error.message }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); - } -}); diff --git a/supabase/functions/v1-memo-tag-pull/index.ts b/supabase/functions/v1-memo-tag-pull/index.ts index 936b1652..7da17a28 100644 --- a/supabase/functions/v1-memo-tag-pull/index.ts +++ b/supabase/functions/v1-memo-tag-pull/index.ts @@ -2,6 +2,7 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { supabaseAdmin } from "../_shared/supabase-admin.ts"; import { MemoTagRepository } from "../_shared/memo-tag-repository.ts"; import { MemoTag } from "../_shared/entity/memo-tag.ts"; +import { corsHeaders, handleCorsPreflight } from "../_shared/cors.ts"; import { MemoTagPullRequestV1 } from "./entity/memo-tag-pull-request-v1.ts"; import { MemoTagV1 } from "../v1-memo-tag-push/entity/memo-tag-v1.ts"; @@ -17,10 +18,13 @@ function toMemoTagV1(memoTag: MemoTag): MemoTagV1 { } Deno.serve(async (req: Request) => { + const preflight = handleCorsPreflight(req); + if (preflight) return preflight; + if (req.method !== "POST") { return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -29,7 +33,7 @@ Deno.serve(async (req: Request) => { if (!authorization) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -40,7 +44,7 @@ Deno.serve(async (req: Request) => { if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -49,12 +53,12 @@ Deno.serve(async (req: Request) => { return new Response(JSON.stringify(memoTagList.map(toMemoTagV1)), { status: 200, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } }); diff --git a/supabase/functions/v1-memo-tag-push/index.ts b/supabase/functions/v1-memo-tag-push/index.ts index d4a915c8..4b08c6db 100644 --- a/supabase/functions/v1-memo-tag-push/index.ts +++ b/supabase/functions/v1-memo-tag-push/index.ts @@ -2,6 +2,7 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { supabaseAdmin } from "../_shared/supabase-admin.ts"; import { MemoTagRepository } from "../_shared/memo-tag-repository.ts"; import { MemoTag } from "../_shared/entity/memo-tag.ts"; +import { corsHeaders, handleCorsPreflight } from "../_shared/cors.ts"; import { MemoTagV1 } from "./entity/memo-tag-v1.ts"; const memoTagRepository = new MemoTagRepository(supabaseAdmin); @@ -16,10 +17,13 @@ function toMemoTag(v1: MemoTagV1): MemoTag { } Deno.serve(async (req: Request) => { + const preflight = handleCorsPreflight(req); + if (preflight) return preflight; + if (req.method !== "POST") { return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -28,7 +32,7 @@ Deno.serve(async (req: Request) => { if (!authorization) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -39,18 +43,18 @@ Deno.serve(async (req: Request) => { if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } const { memoTagList }: { memoTagList: MemoTagV1[] } = await req.json(); await memoTagRepository.upsertIfNewer(user.id, memoTagList.map(toMemoTag)); - return new Response(null, { status: 204 }); + return new Response(null, { status: 204, headers: corsHeaders }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } }); diff --git a/supabase/functions/v1-routine-pull/index.ts b/supabase/functions/v1-routine-pull/index.ts index 8795c203..81220024 100644 --- a/supabase/functions/v1-routine-pull/index.ts +++ b/supabase/functions/v1-routine-pull/index.ts @@ -2,6 +2,7 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { supabaseAdmin } from "../_shared/supabase-admin.ts"; import { RoutineRepository } from "../_shared/routine-repository.ts"; import { Routine } from "../_shared/entity/routine.ts"; +import { corsHeaders, handleCorsPreflight } from "../_shared/cors.ts"; import { RoutinePullRequestV1 } from "./entity/routine-pull-request-v1.ts"; import { RoutineV1 } from "../v1-routine-push/entity/routine-v1.ts"; @@ -36,10 +37,13 @@ function toRoutineV1(routine: Routine): RoutineV1 { } Deno.serve(async (req: Request) => { + const preflight = handleCorsPreflight(req); + if (preflight) return preflight; + if (req.method !== "POST") { return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -48,7 +52,7 @@ Deno.serve(async (req: Request) => { if (!authorization) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -59,7 +63,7 @@ Deno.serve(async (req: Request) => { if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -68,12 +72,12 @@ Deno.serve(async (req: Request) => { return new Response(JSON.stringify(routineList.map(toRoutineV1)), { status: 200, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } }); diff --git a/supabase/functions/v1-routine-push/index.ts b/supabase/functions/v1-routine-push/index.ts index 4f15d77a..535cb6c3 100644 --- a/supabase/functions/v1-routine-push/index.ts +++ b/supabase/functions/v1-routine-push/index.ts @@ -2,6 +2,7 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { supabaseAdmin } from "../_shared/supabase-admin.ts"; import { RoutineRepository } from "../_shared/routine-repository.ts"; import { Routine } from "../_shared/entity/routine.ts"; +import { corsHeaders, handleCorsPreflight } from "../_shared/cors.ts"; import { RoutineV1 } from "./entity/routine-v1.ts"; const routineRepository = new RoutineRepository(supabaseAdmin); @@ -35,10 +36,13 @@ function toRoutine(v1: RoutineV1): Routine { } Deno.serve(async (req: Request) => { + const preflight = handleCorsPreflight(req); + if (preflight) return preflight; + if (req.method !== "POST") { return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -47,7 +51,7 @@ Deno.serve(async (req: Request) => { if (!authorization) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -58,18 +62,18 @@ Deno.serve(async (req: Request) => { if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } const { routineList }: { routineList: RoutineV1[] } = await req.json(); await routineRepository.upsertIfNewer(user.id, routineList.map(toRoutine)); - return new Response(null, { status: 204 }); + return new Response(null, { status: 204, headers: corsHeaders }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } }); diff --git a/supabase/functions/v1-session-google-code/index.ts b/supabase/functions/v1-session-google-code/index.ts index 58731bd0..2a6a97ac 100644 --- a/supabase/functions/v1-session-google-code/index.ts +++ b/supabase/functions/v1-session-google-code/index.ts @@ -3,6 +3,7 @@ import { supabaseAdmin } from "../_shared/supabase-admin.ts"; import { GoogleRepository } from "../_shared/google-repository.ts"; import { AccountRepository } from "../_shared/account-repository.ts"; import { AuthRepository } from "../_shared/auth-repository.ts"; +import { corsHeaders, handleCorsPreflight } from "../_shared/cors.ts"; import { SessionGoogleCodeRequestV1 } from "./entity/session-google-code-request-v1.ts"; import { SessionGoogleCodeResponseV1 } from "./entity/session-google-code-response-v1.ts"; @@ -11,10 +12,13 @@ const accountRepository = new AccountRepository(supabaseAdmin); const authRepository = new AuthRepository(supabaseAdmin); Deno.serve(async (req: Request) => { + const preflight = handleCorsPreflight(req); + if (preflight) return preflight; + if (req.method !== "POST") { return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -40,12 +44,12 @@ Deno.serve(async (req: Request) => { return new Response(JSON.stringify(response), { status: 200, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } }); diff --git a/supabase/functions/v1-session-google-idToken/index.ts b/supabase/functions/v1-session-google-idToken/index.ts index 7d7b6555..bf20cfac 100644 --- a/supabase/functions/v1-session-google-idToken/index.ts +++ b/supabase/functions/v1-session-google-idToken/index.ts @@ -2,6 +2,7 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { supabaseAdmin } from "../_shared/supabase-admin.ts"; import { AccountRepository } from "../_shared/account-repository.ts"; import { AuthRepository } from "../_shared/auth-repository.ts"; +import { corsHeaders, handleCorsPreflight } from "../_shared/cors.ts"; import { SessionGoogleIdTokenRequestV1 } from "./entity/session-google-id-token-request-v1.ts"; import { SessionGoogleIdTokenResponseV1 } from "./entity/session-google-id-token-response-v1.ts"; @@ -9,10 +10,13 @@ const accountRepository = new AccountRepository(supabaseAdmin); const authRepository = new AuthRepository(supabaseAdmin); Deno.serve(async (req: Request) => { + const preflight = handleCorsPreflight(req); + if (preflight) return preflight; + if (req.method !== "POST") { return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -39,12 +43,12 @@ Deno.serve(async (req: Request) => { return new Response(JSON.stringify(response), { status: 200, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } }); diff --git a/supabase/functions/v1-tag-pull/index.ts b/supabase/functions/v1-tag-pull/index.ts index f885d624..13323561 100644 --- a/supabase/functions/v1-tag-pull/index.ts +++ b/supabase/functions/v1-tag-pull/index.ts @@ -2,6 +2,7 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { supabaseAdmin } from "../_shared/supabase-admin.ts"; import { TagRepository } from "../_shared/tag-repository.ts"; import { Tag } from "../_shared/entity/tag.ts"; +import { corsHeaders, handleCorsPreflight } from "../_shared/cors.ts"; import { TagPullRequestV1 } from "./entity/tag-pull-request-v1.ts"; import { TagV1 } from "../v1-tag-push/entity/tag-v1.ts"; @@ -23,10 +24,13 @@ function toTagV1(tag: Tag): TagV1 { } Deno.serve(async (req: Request) => { + const preflight = handleCorsPreflight(req); + if (preflight) return preflight; + if (req.method !== "POST") { return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -35,7 +39,7 @@ Deno.serve(async (req: Request) => { if (!authorization) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -46,7 +50,7 @@ Deno.serve(async (req: Request) => { if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -55,12 +59,12 @@ Deno.serve(async (req: Request) => { return new Response(JSON.stringify(tagList.map(toTagV1)), { status: 200, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } }); diff --git a/supabase/functions/v1-tag-push/index.ts b/supabase/functions/v1-tag-push/index.ts index a9a951d9..d4569291 100644 --- a/supabase/functions/v1-tag-push/index.ts +++ b/supabase/functions/v1-tag-push/index.ts @@ -2,6 +2,7 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { supabaseAdmin } from "../_shared/supabase-admin.ts"; import { TagRepository } from "../_shared/tag-repository.ts"; import { Tag } from "../_shared/entity/tag.ts"; +import { corsHeaders, handleCorsPreflight } from "../_shared/cors.ts"; import { TagV1 } from "./entity/tag-v1.ts"; const tagRepository = new TagRepository(supabaseAdmin); @@ -22,10 +23,13 @@ function toTag(v1: TagV1): Tag { } Deno.serve(async (req: Request) => { + const preflight = handleCorsPreflight(req); + if (preflight) return preflight; + if (req.method !== "POST") { return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -34,7 +38,7 @@ Deno.serve(async (req: Request) => { if (!authorization) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -45,18 +49,18 @@ Deno.serve(async (req: Request) => { if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } const { tagList }: { tagList: TagV1[] } = await req.json(); await tagRepository.upsertIfNewer(user.id, tagList.map(toTag)); - return new Response(null, { status: 204 }); + return new Response(null, { status: 204, headers: corsHeaders }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } }); diff --git a/supabase/functions/v2-memo-pull/index.ts b/supabase/functions/v2-memo-pull/index.ts index 0d38057f..acfc289a 100644 --- a/supabase/functions/v2-memo-pull/index.ts +++ b/supabase/functions/v2-memo-pull/index.ts @@ -2,6 +2,7 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { supabaseAdmin } from "../_shared/supabase-admin.ts"; import { MemoRepository } from "../_shared/memo-repository.ts"; import { Memo } from "../_shared/entity/memo.ts"; +import { corsHeaders, handleCorsPreflight } from "../_shared/cors.ts"; import { MemoPullRequestV2 } from "./entity/memo-pull-request-v2.ts"; import { MemoV2 } from "../v2-memo-push/entity/memo-v2.ts"; @@ -27,10 +28,13 @@ function toMemoV2(memo: Memo): MemoV2 { } Deno.serve(async (req: Request) => { + const preflight = handleCorsPreflight(req); + if (preflight) return preflight; + if (req.method !== "POST") { return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -39,7 +43,7 @@ Deno.serve(async (req: Request) => { if (!authorization) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -50,7 +54,7 @@ Deno.serve(async (req: Request) => { if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -59,12 +63,12 @@ Deno.serve(async (req: Request) => { return new Response(JSON.stringify(memoList.map(toMemoV2)), { status: 200, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } }); diff --git a/supabase/functions/v2-memo-push/index.ts b/supabase/functions/v2-memo-push/index.ts index fa20191e..31b98636 100644 --- a/supabase/functions/v2-memo-push/index.ts +++ b/supabase/functions/v2-memo-push/index.ts @@ -2,6 +2,7 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { supabaseAdmin } from "../_shared/supabase-admin.ts"; import { MemoRepository } from "../_shared/memo-repository.ts"; import { Memo } from "../_shared/entity/memo.ts"; +import { corsHeaders, handleCorsPreflight } from "../_shared/cors.ts"; import { MemoV2 } from "./entity/memo-v2.ts"; const memoRepository = new MemoRepository(supabaseAdmin); @@ -26,10 +27,13 @@ function toMemo(v2: MemoV2): Memo { } Deno.serve(async (req: Request) => { + const preflight = handleCorsPreflight(req); + if (preflight) return preflight; + if (req.method !== "POST") { return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -38,7 +42,7 @@ Deno.serve(async (req: Request) => { if (!authorization) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } @@ -49,18 +53,18 @@ Deno.serve(async (req: Request) => { if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } const { memoList }: { memoList: MemoV2[] } = await req.json(); await memoRepository.upsertIfNewer(user.id, memoList.map(toMemo)); - return new Response(null, { status: 204 }); + return new Response(null, { status: 204, headers: corsHeaders }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 400, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...corsHeaders }, }); } });