Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
29884f9
[feat] 즐겨찾기 업데이트 API 구현 #55
ikseong00 Jan 26, 2026
02cc305
[feat] 즐겨찾기 사진 조회 API 구현 #55
ikseong00 Jan 26, 2026
4916300
[feat] 즐겨찾기 요약 조회 API 구현 #55
ikseong00 Jan 26, 2026
93589f2
[feat] 즐겨찾기 업데이트 실패 시 상태 복원 기능 추가 #56
ikseong00 Jan 26, 2026
32278dd
[refactor] #55: 앨범 상세화면 State에서 Album 모델 의존성 제거
ikseong00 Jan 26, 2026
7834ad2
[feat] #55: 즐겨찾기 앨범 사진 목록을 가져오는 기능 추가
ikseong00 Jan 26, 2026
0591179
[refactor] #55: FavoriteSummary를 AlbumPreview로 대체
ikseong00 Jan 26, 2026
3072d69
[refactor] #55: 아카이브 화면에서 Album 모델을 AlbumPreview로 변경 및 즐겨찾기 폴더 데이터 가져…
ikseong00 Jan 26, 2026
cbe0140
[fix] #55: 사진 상세화면 즐겨찾기 API 중복 호출 수정
ikseong00 Jan 26, 2026
8f3bfef
[refactor] #55: 아카이브 메인 화면 데이터 로딩 병렬 처리
ikseong00 Jan 26, 2026
94a947d
[fix] #55: 즐겨찾기 목록 할당하는 변수 수정
ikseong00 Jan 26, 2026
99bad56
[fix] #56: 보관함 사진 아이템 좌우 패딩 추가
ikseong00 Jan 26, 2026
6001722
[chore] #55: 더미 앨범 데이터 및 미사용 import 제거
ikseong00 Jan 26, 2026
a7ef58f
[feat] 사진 등록 API 수정 #56
ikseong00 Jan 26, 2026
e0ac8d0
[feat] 사진 삭제 API 수정 #56
ikseong00 Jan 26, 2026
238ff58
[feat] 사진 조회 API 수정 #56
ikseong00 Jan 26, 2026
3433f76
[refactor] #56: 단일 사진만 삭제하는 함수 추가
ikseong00 Jan 26, 2026
e00e3f2
[chore] #56: 사용하지 않는 import 문 제거
ikseong00 Jan 26, 2026
9c5c3e8
[feat] #56: 사진 데이터 없을 때 엠티 뷰 추가
ikseong00 Jan 26, 2026
38d3ab8
[refactor] #56: 즐겨찾기 사진 조회 시 favorite 속성 부여하지 않는 오류 수정
ikseong00 Jan 26, 2026
ba1fb87
[fix] #56: 사진 상세 화면에서 즐겨찾기 상태 복구 로직 수정
ikseong00 Jan 26, 2026
b2023f4
[feat] #56: 사진 상세 화면에서 변경 시 아카이브 메인 화면 갱신
ikseong00 Jan 26, 2026
5536904
[feat] #56: 아카이빙 메인 즐겨찾는 앨범 기본 제목 설정
ikseong00 Jan 26, 2026
a78a834
[refactor] #56: 로딩 상태가 항상 false로 설정되도록 finally 블록 추가
ikseong00 Jan 26, 2026
52bd128
[feat] #56: ApplicationScope를 갖는 CoroutineScope 추가
ikseong00 Jan 27, 2026
5f28e1a
[refactor] #56: ViewModel 스코프를 applicationScope로 변경하여 생명주기 확장
ikseong00 Jan 27, 2026
628b639
[refactor] #56: ViewModel에서 SideEffect를 직접 호출하지 못하도록 수정
ikseong00 Jan 27, 2026
ff9856d
[refactor] #56: 미디어 업로드 API 응답 모델 변경
ikseong00 Jan 27, 2026
090cfec
[feat] #55: 중복 발행을 막는 `allowDuplicate` 옵션 ResultEventBus에 추가
ikseong00 Jan 27, 2026
6aef0c6
[refactor] #56: 아카이브 메인 화면 레이아웃 구조 개선
ikseong00 Jan 27, 2026
a4fd408
[fix] #56: 보관함 이미지 추가 시 로딩 상태가 계속 유지되는 버그 수정
ikseong00 Jan 27, 2026
87133c3
[feat] #55: 즐겨찾는사진 앨범 사진 삭제 로직 구현
ikseong00 Jan 27, 2026
0403697
[refactor] #55: 포토카드 즐겨찾기 상태 관리 로직 개선
ikseong00 Jan 27, 2026
f64157a
[refactor] #55: PhotoDetail의 favoriteRequests를 ViewModel로 이동
ikseong00 Jan 27, 2026
990e2b9
[refactor] #55: PhotoDetailViewModel의 favoriteRequests 가시성 변경 및 리팩토링
ikseong00 Jan 27, 2026
6e291f0
[refactor] #55: 포토카드 상세화면의 초기 좋아요 상태를 photo.isFavorite로 설정
ikseong00 Jan 27, 2026
eba3da4
Merge branch 'develop' into feat/#55,#56-photo-api
ikseong00 Jan 28, 2026
44941f7
[fix] #55: 앨범 상세화면 로딩 중 빈 화면이 표시되는 버그 수정
ikseong00 Jan 28, 2026
ec0ea59
[refactor] #55: ApplicationScope 대신 ViewModelScope 에서 좋아요 상태 구독
ikseong00 Jan 28, 2026
bf290fa
[refactor] #55: ViewModel이 onCleared 시점에 좋아요 상태를 업데이트하도록 변경
ikseong00 Jan 28, 2026
62b596e
[chore] 사용하지 않는 import 제거
ikseong00 Jan 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.neki.android.library)
alias(libs.plugins.neki.android.library.compose)
alias(libs.plugins.neki.hilt)
}

android {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.neki.android.core.common.coroutine.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import javax.inject.Qualifier
import javax.inject.Singleton

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ApplicationScope

@Module
@InstallIn(SingletonComponent::class)
object CoroutineScopeModule {

@Provides
@Singleton
@ApplicationScope
fun provideApplicationScope(): CoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.Default)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import com.neki.android.core.model.MediaUploadTicket

interface MediaUploadRepository {
suspend fun getUploadTicket(
uploadCount: Int = 1,
fileName: String,
contentType: String,
mediaType: String,
): Result<MediaUploadTicket>
): Result<List<MediaUploadTicket>>

suspend fun uploadImage(
uploadUrl: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.neki.android.core.dataapi.repository

import com.neki.android.core.model.AlbumPreview
import com.neki.android.core.model.Photo
import com.neki.android.core.model.SortOrder

interface PhotoRepository {
suspend fun getPhotos(
Expand All @@ -10,9 +12,20 @@ interface PhotoRepository {
): Result<List<Photo>>

suspend fun registerPhoto(
mediaId: Long,
mediaIds: List<Long>,
folderId: Long? = null,
): Result<Long>
): Result<Unit>

suspend fun deletePhoto(photoId: Long): Result<Unit>
suspend fun deletePhoto(photoIds: List<Long>): Result<Unit>

suspend fun updateFavorite(photoId: Long, favorite: Boolean): Result<Unit>

suspend fun getFavoritePhotos(
page: Int = 0,
size: Int = 20,
sortOrder: SortOrder = SortOrder.DESC,
): Result<List<Photo>>

suspend fun getFavoriteSummary(): Result<AlbumPreview>
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package com.neki.android.core.data.remote.api

import com.neki.android.core.data.remote.model.request.DeletePhotoRequest
import com.neki.android.core.data.remote.model.request.RegisterPhotoRequest
import com.neki.android.core.data.remote.model.response.BasicResponse
import com.neki.android.core.data.remote.model.request.UpdateFavoriteRequest
import com.neki.android.core.data.remote.model.response.BasicNullableResponse
import com.neki.android.core.data.remote.model.response.BasicResponse
import com.neki.android.core.data.remote.model.response.FavoritePhotoResponse
import com.neki.android.core.data.remote.model.response.FavoriteSummaryResponse
import com.neki.android.core.data.remote.model.response.PhotoResponse
import com.neki.android.core.data.remote.model.response.RegisterPhotoResponse
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.patch
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import javax.inject.Inject
Expand All @@ -31,12 +36,37 @@ class PhotoService @Inject constructor(
}

// 사진 등록
suspend fun registerPhoto(requestBody: RegisterPhotoRequest): BasicResponse<RegisterPhotoResponse> {
suspend fun registerPhoto(requestBody: RegisterPhotoRequest): BasicNullableResponse<RegisterPhotoResponse> {
return client.post("/api/photos") { setBody(requestBody) }.body()
}

// 사진 삭제
suspend fun deletePhoto(photoId: Long): BasicNullableResponse<Unit> {
return client.delete("/api/photos/$photoId").body()
suspend fun deletePhoto(requestBody: DeletePhotoRequest): BasicNullableResponse<Unit> {
return client.delete("/api/photos") { setBody(requestBody) }.body()
}

// 즐겨찾기 업데이트
suspend fun updateFavorite(photoId: Long, favorite: Boolean): BasicNullableResponse<Unit> {
return client.patch("/api/photos/$photoId/favorite") {
setBody(UpdateFavoriteRequest(favorite))
}.body()
}

// 즐겨찾기 사진 조회
suspend fun getFavoritePhotos(
page: Int = 0,
size: Int = 20,
sortOrder: String,
): BasicResponse<FavoritePhotoResponse> {
return client.get("/api/photos/favorite") {
parameter("page", page)
parameter("size", size)
parameter("sortOrder", sortOrder)
}.body()
}

// 즐겨찾기 요약 조회
suspend fun getFavoriteSummary(): BasicResponse<FavoriteSummaryResponse> {
return client.get("/api/photos/favorite/summary").body()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.neki.android.core.data.remote.api

import com.neki.android.core.data.remote.model.request.MediaUploadTicketRequest
import com.neki.android.core.data.remote.model.response.BasicResponse
import com.neki.android.core.data.remote.model.response.MediaUploadTicketResponse
import com.neki.android.core.data.remote.model.response.MediaUploadTicketDataResponse
import com.neki.android.core.data.remote.qualifier.UploadHttpClient
import io.ktor.client.HttpClient
import io.ktor.client.call.body
Expand All @@ -18,7 +18,7 @@ class UploadService @Inject constructor(
@UploadHttpClient private val uploadClient: HttpClient,
) {
// Media Upload Ticket 받기
suspend fun getUploadTicket(requestBody: MediaUploadTicketRequest): BasicResponse<MediaUploadTicketResponse> {
suspend fun getUploadTicket(requestBody: MediaUploadTicketRequest): BasicResponse<MediaUploadTicketDataResponse> {
return client.post("/api/media/upload") { setBody(requestBody) }.body()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.neki.android.core.data.remote.model.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class DeletePhotoRequest(
@SerialName("photoIds") val photoIds: List<Long>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import kotlinx.serialization.Serializable

@Serializable
data class MediaUploadTicketRequest(
@SerialName("contentType") val contentType: String,
@SerialName("filename") val filename: String = "",
@SerialName("mediaType") val mediaType: String,
)
@SerialName("items") val items: List<Item>,
) {
@Serializable
data class Item(
@SerialName("contentType") val contentType: String,
@SerialName("filename") val filename: String = "",
@SerialName("mediaType") val mediaType: String,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import kotlinx.serialization.Serializable

@Serializable
data class RegisterPhotoRequest(
@SerialName("folderId") val folderId: Long?,
@SerialName("mediaId") val mediaId: Long,
@SerialName("memo") val memo: String = "",
)
@SerialName("folderId") val folderId: Long? = null,
@SerialName("uploads") val uploads: List<Upload>,
) {
@Serializable
data class Upload(
@SerialName("mediaId") val mediaId: Long,
@SerialName("memo") val memo: String? = null,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.neki.android.core.data.remote.model.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class UpdateFavoriteRequest(
@SerialName("favorite") val favorite: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.neki.android.core.data.remote.model.response

import com.neki.android.core.common.util.toFormattedDate
import com.neki.android.core.model.Photo
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class FavoritePhotoResponse(
@SerialName("hasNext") val hasNext: Boolean,
@SerialName("items") val items: List<Item>,
) {
@Serializable
data class Item(
@SerialName("contentType") val contentType: String,
@SerialName("createdAt") val createdAt: String,
@SerialName("favorite") val favorite: Boolean,
@SerialName("folderId") val folderId: Long?,
@SerialName("imageUrl") val imageUrl: String,
@SerialName("photoId") val photoId: Long,
) {
internal fun toModel() = Photo(
id = photoId,
imageUrl = imageUrl,
isFavorite = favorite,
date = createdAt.toFormattedDate(),
)
}

fun toModels() = items.map { it.toModel() }
Comment thread
ikseong00 marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.neki.android.core.data.remote.model.response

import com.neki.android.core.model.AlbumPreview
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class FavoriteSummaryResponse(
@SerialName("latestImageUrl") val latestImageUrl: String?,
@SerialName("totalCount") val totalCount: Int,
) {
fun toModel() = AlbumPreview(
id = -1L,
title = "즐겨찾는사진",
thumbnailUrl = latestImageUrl,
photoCount = totalCount,
)
Comment on lines +12 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

문자열 하드코딩으로 i18n 이슈 가능

"즐겨찾는사진"이 데이터 레이어에 하드코딩되어 있어 다국어/브랜딩 변경 시 확장성이 떨어집니다. UI 레이어의 리소스 또는 공통 상수로 이동을 권장합니다.

🤖 Prompt for AI Agents
In
`@core/data/src/main/java/com/neki/android/core/data/remote/model/response/FavoriteSummaryResponse.kt`
around lines 12 - 17, FavoriteSummaryResponse.toModel currently hardcodes the
title "즐겨찾는사진" in the data layer; remove the hardcoded Korean string and instead
supply a title via a constant or resource key passed from upper layers
(UI/domain). Update the toModel implementation in FavoriteSummaryResponse to use
a provided value (e.g., a constant like AlbumPreview.DEFAULT_TITLE or a title
parameter) or return a title placeholder/enum that the UI will map to a
localized string resource, ensuring no UI strings are embedded in the data
layer.

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class MediaUploadTicketResponse(
@SerialName("contentType") val contentType: String,
@SerialName("expiresIn") val expiresIn: String,
@SerialName("mediaId") val mediaId: Long,
data class MediaUploadTicketDataResponse(
@SerialName("method") val method: String,
@SerialName("uploadUrl") val uploadUrl: String,
@SerialName("expiresIn") val expiresIn: String,
@SerialName("items") val items: List<MediaUploadTicketItemResponse>,
) {
fun toModel() = MediaUploadTicket(
mediaId = this.mediaId,
uploadUrl = this.uploadUrl,
)
fun toModels() = items.map { it.toModel() }

@Serializable
data class MediaUploadTicketItemResponse(
@SerialName("mediaId") val mediaId: Long,
@SerialName("uploadTicket") val uploadTicket: String,
@SerialName("contentType") val contentType: String,
) {
fun toModel() = MediaUploadTicket(
mediaId = mediaId,
uploadUrl = uploadTicket,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ data class PhotoResponse(
data class Item(
@SerialName("contentType") val contentType: String,
@SerialName("createdAt") val createdAt: String,
@SerialName("favorite") val isFavorite: Boolean,
@SerialName("folderId") val folderId: Long?,
@SerialName("imageUrl") val imageUrl: String,
@SerialName("photoId") val photoId: Long,
) {
internal fun toModel() = Photo(
id = photoId,
imageUrl = imageUrl,
isFavorite = isFavorite,
date = createdAt.toFormattedDate(),
Comment thread
ikseong00 marked this conversation as resolved.
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@ class MediaUploadRepositoryImpl @Inject constructor(
private val uploadService: UploadService,
) : MediaUploadRepository {
override suspend fun getUploadTicket(
uploadCount: Int,
fileName: String,
contentType: String,
mediaType: String,
) = runSuspendCatching {
uploadService.getUploadTicket(
requestBody = MediaUploadTicketRequest(
filename = fileName,
contentType = contentType,
mediaType = mediaType,
items = List(uploadCount) {
MediaUploadTicketRequest.Item(
filename = fileName,
contentType = contentType,
mediaType = mediaType,
)
},
),
Comment thread
ikseong00 marked this conversation as resolved.
).data.toModel()
).data.toModels()
}

override suspend fun uploadImage(
Expand Down
Loading