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