build(room): KSP room.schemaLocation 설정 및 exportSchema=true로 스키마 export 활성화

프로젝트가 이미 KSP를 사용하고 있어 KSP 인수 기반으로 Room 스키마 export를 활성화했습니다.
- app/build.gradle: ksp { room.schemaLocation 등 } 추가
- Room DB 클래스 3종: exportSchema=true
- app/schemas 디렉터리 버전 관리
This commit is contained in:
2025-10-22 19:19:31 +09:00
parent 7ff3d7f1e5
commit 23c05b91d5
10 changed files with 233 additions and 8 deletions

View File

@@ -214,3 +214,11 @@ dependencies {
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.3.1'
testImplementation 'io.mockk:mockk:1.13.10'
}
// KSP args for Room schema export
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
arg("room.expandProjection", "true")
}

1
app/schemas/.gitkeep Normal file
View File

@@ -0,0 +1 @@
# Keep schemas directory under version control

View File

@@ -0,0 +1,76 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "b9a331035b36b70f8ca7a14962b13fdf",
"entities": [
{
"tableName": "playback_tracking",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contentId` INTEGER NOT NULL, `totalDuration` INTEGER NOT NULL, `startPosition` INTEGER NOT NULL, `isFree` INTEGER NOT NULL, `isPreview` INTEGER NOT NULL, `endPosition` INTEGER, `playDateTime` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentId",
"columnName": "contentId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "totalDuration",
"columnName": "totalDuration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "startPosition",
"columnName": "startPosition",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isFree",
"columnName": "isFree",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPreview",
"columnName": "isPreview",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endPosition",
"columnName": "endPosition",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "playDateTime",
"columnName": "playDateTime",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"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, 'b9a331035b36b70f8ca7a14962b13fdf')"
]
}
}

View File

@@ -0,0 +1,82 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "7429c2998f64cb70e5e8b1d2525a4708",
"entities": [
{
"tableName": "alarms",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `time` INTEGER NOT NULL, `days` TEXT NOT NULL, `contentId` INTEGER NOT NULL, `contentTitle` TEXT NOT NULL, `contentCreatorNickname` TEXT NOT NULL, `volume` INTEGER NOT NULL, `isEnabled` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "days",
"columnName": "days",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contentId",
"columnName": "contentId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentTitle",
"columnName": "contentTitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contentCreatorNickname",
"columnName": "contentCreatorNickname",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "volume",
"columnName": "volume",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isEnabled",
"columnName": "isEnabled",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"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, '7429c2998f64cb70e5e8b1d2525a4708')"
]
}
}

View File

@@ -0,0 +1,58 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "e46a8b457c3ea6ceefd0db76bb763056",
"entities": [
{
"tableName": "recent_contents",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contentId` INTEGER NOT NULL, `coverImageUrl` TEXT NOT NULL, `title` TEXT NOT NULL, `creatorNickname` TEXT NOT NULL, `listenedAt` INTEGER NOT NULL, PRIMARY KEY(`contentId`))",
"fields": [
{
"fieldPath": "contentId",
"columnName": "contentId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "coverImageUrl",
"columnName": "coverImageUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "creatorNickname",
"columnName": "creatorNickname",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "listenedAt",
"columnName": "listenedAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"contentId"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"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, 'e46a8b457c3ea6ceefd0db76bb763056')"
]
}
}

View File

@@ -8,7 +8,7 @@ import androidx.room.TypeConverters
import kr.co.vividnext.sodalive.audio_content.PlaybackTracking
import kr.co.vividnext.sodalive.common.Converter
@Database(entities = [PlaybackTracking::class], version = 1)
@Database(entities = [PlaybackTracking::class], version = 1, exportSchema = true)
@TypeConverters(Converter::class)
abstract class PlaybackTrackingDatabase : RoomDatabase() {
abstract fun playbackTrackingDao(): PlaybackTrackingDao

View File

@@ -9,7 +9,7 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import kr.co.vividnext.sodalive.common.Converter
@Database(entities = [Alarm::class], version = 2)
@Database(entities = [Alarm::class], version = 2, exportSchema = true)
@TypeConverters(Converter::class)
abstract class AlarmDatabase : RoomDatabase() {
abstract fun alarmDao(): AlarmDao
@@ -33,8 +33,8 @@ abstract class AlarmDatabase : RoomDatabase() {
}
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE 'alarms' ADD COLUMN 'volume' integer not null default 15"
)
}

View File

@@ -7,7 +7,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import kr.co.vividnext.sodalive.common.Converter
@Database(entities = [RecentContent::class], version = 1)
@Database(entities = [RecentContent::class], version = 1, exportSchema = true)
@TypeConverters(Converter::class)
abstract class RecentContentDatabase : RoomDatabase() {
abstract fun recentContentDao(): RecentContentDao

View File

@@ -16,8 +16,8 @@ buildscript {
}
plugins {
id 'com.android.application' version '8.4.2' apply false
id 'com.android.library' version '8.4.2' apply false
id 'com.android.application' version '8.7.2' apply false
id 'com.android.library' version '8.7.2' apply false
id 'org.jetbrains.kotlin.android' version '2.0.21' apply false
id 'com.google.devtools.ksp' version '2.0.21-1.0.25' apply false

View File

@@ -1,6 +1,6 @@
#Sun Jul 23 18:26:44 KST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists