feat: 즐겨찾기 기능 데이터 레이어 추가

- HotDeal, HotDealEntity에 isFavorite 필드 추가
- HotDealDao에 즐겨찾기 관련 쿼리 메서드 추가
- 데이터베이스 마이그레이션 (v3 -> v4) 추가

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
sanjeok77
2026-03-05 04:13:32 +09:00
parent b4d2460937
commit 9b6fc1dd01
5 changed files with 58 additions and 23 deletions

View File

@@ -21,7 +21,7 @@ import com.hotdeal.alarm.data.local.db.entity.SiteConfigEntity
SiteConfigEntity::class, SiteConfigEntity::class,
KeywordEntity::class KeywordEntity::class
], ],
version = 3, version = 4,
exportSchema = false exportSchema = false
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
@@ -46,11 +46,19 @@ abstract class AppDatabase : RoomDatabase() {
/** /**
* Migration 2 -> 3: 인덱스 추가 * Migration 2 -> 3: 인덱스 추가
*/ */
val MIGRATION_2_3 = object : Migration(2, 3) { val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE INDEX IF NOT EXISTS index_hot_deals_siteName_boardName ON hot_deals(siteName, boardName)") db.execSQL("CREATE INDEX IF NOT EXISTS index_hot_deals_siteName_boardName ON hot_deals(siteName, boardName)")
db.execSQL("CREATE INDEX IF NOT EXISTS index_hot_deals_createdAt ON hot_deals(createdAt)") db.execSQL("CREATE INDEX IF NOT EXISTS index_hot_deals_createdAt ON hot_deals(createdAt)")
} }
} }
/**
* Migration 3 -> 4: 즐겨찾기 필드 추가
*/
val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE hot_deals ADD COLUMN isFavorite INTEGER NOT NULL DEFAULT 0")
}
} }
} }

View File

@@ -97,6 +97,28 @@ interface HotDealDao {
/** /**
* 제목으로 검색 * 제목으로 검색
*/ */
@Query("SELECT * FROM hot_deals WHERE title LIKE '%' || :query || '%' ORDER BY createdAt DESC") @Query("SELECT * FROM hot_deals WHERE title LIKE '%' || :query || '%' ORDER BY createdAt DESC")
fun searchDeals(query: String): Flow<List<HotDealEntity>> fun searchDeals(query: String): Flow<List<HotDealEntity>>
// =========================
// 즐겨찾기 관련 메서드
// =========================
/**
* 즐겨찾기 핫딜 조회
*/
@Query("SELECT * FROM hot_deals WHERE isFavorite = 1 ORDER BY createdAt DESC")
fun observeFavoriteDeals(): Flow<List<HotDealEntity>>
/**
* 즐겨찾기 상태 토글
*/
@Query("UPDATE hot_deals SET isFavorite = NOT isFavorite WHERE id = :id")
suspend fun toggleFavorite(id: String)
/**
* 즐겨찾기 상태 설정
*/
@Query("UPDATE hot_deals SET isFavorite = :isFavorite WHERE id = :id")
suspend fun setFavorite(id: String, isFavorite: Boolean)
} }

View File

@@ -23,9 +23,10 @@ data class HotDealEntity(
val title: String, val title: String,
val url: String, val url: String,
val mallUrl: String?, val mallUrl: String?,
val createdAt: Long, val createdAt: Long,
val isNotified: Boolean = false, val isNotified: Boolean = false,
val isKeywordMatch: Boolean = false val isKeywordMatch: Boolean = false,
val isFavorite: Boolean = false // 즐겨찾기 여부
) { ) {
/** /**
* Domain 모델로 변환 * Domain 모델로 변환
@@ -39,8 +40,9 @@ data class HotDealEntity(
url = url, url = url,
mallUrl = mallUrl, mallUrl = mallUrl,
createdAt = createdAt, createdAt = createdAt,
isNotified = isNotified, isNotified = isNotified,
isKeywordMatch = isKeywordMatch isKeywordMatch = isKeywordMatch,
isFavorite = isFavorite
) )
} }
@@ -57,8 +59,9 @@ data class HotDealEntity(
url = domain.url, url = domain.url,
mallUrl = domain.mallUrl, mallUrl = domain.mallUrl,
createdAt = domain.createdAt, createdAt = domain.createdAt,
isNotified = domain.isNotified, isNotified = domain.isNotified,
isKeywordMatch = domain.isKeywordMatch isKeywordMatch = domain.isKeywordMatch,
isFavorite = domain.isFavorite
) )
} }
} }

View File

@@ -31,10 +31,11 @@ object DatabaseModule {
AppDatabase::class.java, AppDatabase::class.java,
AppDatabase.DATABASE_NAME AppDatabase.DATABASE_NAME
) )
.addMigrations( .addMigrations(
AppDatabase.MIGRATION_1_2, AppDatabase.MIGRATION_1_2,
AppDatabase.MIGRATION_2_3 AppDatabase.MIGRATION_2_3,
) AppDatabase.MIGRATION_3_4
)
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()
} }

View File

@@ -10,9 +10,10 @@ data class HotDeal(
val title: String, // 게시글 제목 val title: String, // 게시글 제목
val url: String, // 게시글 URL val url: String, // 게시글 URL
val mallUrl: String? = null, // 쇼핑몰 URL (추출된 경우) val mallUrl: String? = null, // 쇼핑몰 URL (추출된 경우)
val createdAt: Long, // 수집 시간 (timestamp) val createdAt: Long, // 수집 시간 (timestamp)
val isNotified: Boolean = false, val isNotified: Boolean = false,
val isKeywordMatch: Boolean = false val isKeywordMatch: Boolean = false,
val isFavorite: Boolean = false // 즐겨찾기 여부
) { ) {
/** /**
* 사이트 타입 반환 * 사이트 타입 반환