Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
197d1e8
[chore] #64: `core:data-api` 모듈을 Android 라이브러리로 변경
ikseong00 Jan 29, 2026
367fd97
[feat] #64: 미디어 업로드 티켓 발급 API 분리 및 URI/URL 업로드 기능 추가
ikseong00 Jan 29, 2026
1bc5510
[feat] #64: 여러 장의 사진을 업로드하는 UseCase 추가
ikseong00 Jan 29, 2026
cb51a9a
[feat] #64: MediaUploadRepository 이미지 업로드 기능 구현
ikseong00 Jan 29, 2026
94b2c4d
[refactor] #64: 이미지 업로드 방식을 ByteArray에서 URL로 변경
ikseong00 Jan 29, 2026
ba9933a
[feat] #64: 이미지 여러 장 업로드 기능 구현
ikseong00 Jan 29, 2026
e82e376
[feat] #69: 폴더 생성 및 목록 조회 API 구현
ikseong00 Jan 29, 2026
956d3c9
[feat] #69: 앨범 목록 조회 기능 추가
ikseong00 Jan 29, 2026
1b9f5b6
[feat] #69: 새 앨범 추가 기능 구현
ikseong00 Jan 29, 2026
e566956
[feat] #69: 전체 앨범 화면 API 연동
ikseong00 Jan 29, 2026
41c8a06
[feat] #69: 업로드 시 앨범 목록 조회 기능 추가
ikseong00 Jan 29, 2026
9a3ea5e
[feat] #69: 앨범 상세화면 사진 목록 API 연동
ikseong00 Jan 29, 2026
08cc5de
[feat] #59: 사진 목록 페이징 기능 구현
ikseong00 Jan 29, 2026
7a7695e
[feat] #59: 앨범 상세 화면 Paging 적용
ikseong00 Jan 29, 2026
491f052
[feat] #70: 전체 사진 화면 Paging 적용
ikseong00 Jan 29, 2026
f2ca191
[refactor] #59: Paging 초기 로드 사이즈 조정
ikseong00 Jan 29, 2026
3e2d451
[refactor] #69: 앨범 상세 화면 이동 시 albumId 대신 AlbumPreview 객체 전달
ikseong00 Jan 29, 2026
1fd2acd
[feat] #69: 모든 앨범 앨범 추가 API 연동
ikseong00 Jan 29, 2026
5b2b0f1
[feat] #69: 앨범 추가 성공 시 fetch api 호출
ikseong00 Jan 29, 2026
82c8897
[refactor] #69: 폴더 생성 API 응답 타입 변경 및 불필요한 반환값 제거
ikseong00 Jan 29, 2026
7d9cea1
[feat] #69: 앨범 삭제 기능 구현
ikseong00 Jan 29, 2026
b3613cb
[refactor] #59: 사진 그리드 레이아웃 관련 상수명 변경 및 적용
ikseong00 Jan 29, 2026
84db147
[refactor] #69 일반 앨범에서 사진 삭제 로직 구현
ikseong00 Jan 29, 2026
447dbb8
[feat] #69: 전체 사진 화면 필터링 기능 적용
ikseong00 Jan 29, 2026
597c7f9
[refactor] #69: ScrollToTop Effect 수신 후, LoadState가 완료된 후에, scrollToI…
ikseong00 Jan 29, 2026
a3671f2
[fix] #64: 여러 개 업로드 타입 오류 수정
ikseong00 Jan 29, 2026
221d8b4
[refactor] #64: 사진 여러개 업로드 병렬 및 Dispatcher 적용
ikseong00 Jan 29, 2026
4bd7919
[refactor] #59: 프리페치 거리 조정
ikseong00 Jan 29, 2026
4a0e1e1
[fix] #69: 앨범 상세화면에서 사진 삭제 실패 시 다이얼로그/바텀시트가 닫히지 않는 버그 수정
ikseong00 Jan 29, 2026
e5fb897
[chore] #69: 린트 수정
ikseong00 Jan 29, 2026
44dc5ea
[refactor] #69: 포토카드 상세화면 결과 타입을 ArchiveResult로 변경
ikseong00 Jan 29, 2026
f1b12f3
[refactor] #69: 앨범 이름 중복 검사 로직 개선
ikseong00 Jan 29, 2026
d5f26e4
[refactor] #64: 업로드 타입 로직 변경
ikseong00 Jan 29, 2026
36095a6
[feat] #69: 사진 상세에서 삭제/즐겨찾기 변경 시 목록 화면 동기화
ikseong00 Jan 29, 2026
8c7521f
[fix] #69: entry 중첩 구조 오류 수정
ikseong00 Jan 29, 2026
ac6e255
[refactor] #64: 업로드 타입 로직 변경
ikseong00 Jan 29, 2026
fb2196a
Merge branch 'develop' into feat/#59,64,69-archive-api
ikseong00 Jan 29, 2026
1f52203
[build] #59,64,69: detekt 린트 수정
ikseong00 Jan 29, 2026
7bdf2b6
[refactor] 아카이브 EntryProvider 통합
ikseong00 Jan 29, 2026
75ed9d5
[feat] #70: 폴더 삭제 API deletePhotos 쿼리 추가 및 앨범 사진 삭제 옵션별 분기 구현
ikseong00 Jan 29, 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
7 changes: 6 additions & 1 deletion core/data-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
plugins {
alias(libs.plugins.neki.kotlin.library)
alias(libs.plugins.neki.android.library)
}

android {
namespace = "com.neki.android.core.dataapi"
}

dependencies {
implementation(projects.core.model)
implementation(libs.kotlinx.coroutines.core)
api(libs.androidx.datastore.preferences)
api(libs.androidx.paging.runtime)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.neki.android.core.dataapi.repository

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

interface FolderRepository {
suspend fun getFolders(): Result<List<AlbumPreview>>
suspend fun createFolder(name: String): Result<Unit>
suspend fun deleteFolder(id: List<Long>, deletePhotos: Boolean): Result<Unit>
suspend fun removePhotosFromFolder(folderId: Long, photoIds: List<Long>): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package com.neki.android.core.dataapi.repository

import android.net.Uri
import com.neki.android.core.model.ContentType
import com.neki.android.core.model.MediaUploadTicket

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

suspend fun getMultipleUploadTicket(
uploadCount: Int,
fileName: String,
contentType: String,
mediaType: String,
Expand All @@ -16,4 +23,16 @@ interface MediaUploadRepository {
imageBytes: ByteArray,
contentType: ContentType,
): Result<Unit>

suspend fun uploadImageFromUri(
uploadUrl: String,
uri: Uri,
contentType: ContentType,
): Result<Unit>

suspend fun uploadImageFromUrl(
uploadUrl: String,
imageUrl: String,
contentType: ContentType,
): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.neki.android.core.dataapi.repository

import androidx.paging.PagingData
import com.neki.android.core.model.AlbumPreview
import com.neki.android.core.model.Photo
import com.neki.android.core.model.SortOrder
import kotlinx.coroutines.flow.Flow

interface PhotoRepository {
suspend fun getPhotos(
Expand All @@ -28,4 +30,13 @@ interface PhotoRepository {
): Result<List<Photo>>

suspend fun getFavoriteSummary(): Result<AlbumPreview>

fun getPhotosFlow(
folderId: Long? = null,
sortOrder: SortOrder = SortOrder.DESC,
): Flow<PagingData<Photo>>

fun getFavoritePhotosFlow(
sortOrder: SortOrder = SortOrder.DESC,
): Flow<PagingData<Photo>>
}
1 change: 1 addition & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ dependencies {

implementation(libs.androidx.datastore.core)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.paging.runtime)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.neki.android.core.data.paging

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.neki.android.core.data.remote.api.PhotoService
import com.neki.android.core.model.Photo
import com.neki.android.core.model.SortOrder

class FavoritePhotoPagingSource(
private val photoService: PhotoService,
private val sortOrder: SortOrder,
) : PagingSource<Int, Photo>() {

override fun getRefreshKey(state: PagingState<Int, Photo>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Photo> {
return try {
val page = params.key ?: 0
val response = photoService.getFavoritePhotos(
page = page,
size = params.loadSize,
sortOrder = sortOrder.name,
)
val photos = response.data.toModels()
val hasNext = response.data.hasNext

LoadResult.Page(
data = photos,
prevKey = if (page == 0) null else page - 1,
nextKey = if (hasNext) page + 1 else null,
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.neki.android.core.data.paging

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.neki.android.core.data.remote.api.PhotoService
import com.neki.android.core.model.Photo

class PhotoPagingSource(
private val photoService: PhotoService,
private val folderId: Long?,
private val sortOrder: String,
) : PagingSource<Int, Photo>() {

override fun getRefreshKey(state: PagingState<Int, Photo>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Photo> {
return try {
val page = params.key ?: 0
val response = photoService.getPhotos(
folderId = folderId,
page = page,
size = params.loadSize,
sortOrder = sortOrder,
)
val photos = response.data.toModels()
val hasNext = response.data.hasNext

LoadResult.Page(
data = photos,
prevKey = if (page == 0) null else page - 1,
nextKey = if (hasNext) page + 1 else null,
)
} catch (e: Exception) {
LoadResult.Error(e)
}
Comment thread
ikseong00 marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.neki.android.core.data.remote.api

import com.neki.android.core.data.remote.model.request.CreateFolderRequest
import com.neki.android.core.data.remote.model.request.DeleteFolderRequest
import com.neki.android.core.data.remote.model.request.DeletePhotoRequest
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.CreateFolderResponse
import com.neki.android.core.data.remote.model.response.FolderResponse
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.post
import io.ktor.client.request.setBody
import javax.inject.Inject

class FolderService @Inject constructor(
private val client: HttpClient,
) {
// 폴더 목록 조회
suspend fun getFolders(): BasicResponse<FolderResponse> {
return client.get("/api/folders").body()
}

// 폴더 생성
suspend fun createFolder(requestBody: CreateFolderRequest): BasicNullableResponse<CreateFolderResponse> {
return client.post("/api/folders") { setBody(requestBody) }.body()
}

// 폴더 삭제
suspend fun deleteFolder(requestBody: DeleteFolderRequest, deletePhotos: Boolean): BasicNullableResponse<Unit> {
return client.delete("/api/folders") {
setBody(requestBody)
parameter("deletePhotos", deletePhotos)
}.body()
}

// 폴더에서 사진 제거 (사진 자체는 삭제되지 않음)
suspend fun removePhotosFromFolder(folderId: Long, requestBody: DeletePhotoRequest): BasicNullableResponse<Unit> {
return client.delete("/api/folders/$folderId/photos") {
setBody(requestBody)
}.body()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ class PhotoService @Inject constructor(
folderId: Long? = null,
page: Int = 0,
size: Int = 20,
sortOrder: String = "DESC",
): BasicResponse<PhotoResponse> {
return client.get("/api/photos") {
parameter("folderId", folderId)
parameter("page", page)
parameter("size", size)
parameter("sortOrder", sortOrder)
}.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 CreateFolderRequest(
@SerialName("name") val name: String,
)
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 DeleteFolderRequest(
@SerialName("folderIds") val folderIds: List<Long>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.neki.android.core.data.remote.model.response

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

// TODO: 추후 API 스펙 업데이트 시 제거
@Serializable
data class CreateFolderResponse(
@SerialName("folderId") val folderId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 FolderResponse(
@SerialName("items") val items: List<Item>,
) {
@Serializable
data class Item(
@SerialName("folderId") val folderId: Long,
@SerialName("name") val name: String,
@SerialName("latestImageUrl") val latestImageUrl: String?,
@SerialName("totalCount") val totalCount: Int,
) {
internal fun toModel() = AlbumPreview(
id = folderId,
title = name,
thumbnailUrl = latestImageUrl,
photoCount = totalCount,
)
}

fun toModels() = items.map { it.toModel() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import com.neki.android.core.data.auth.AuthEventManagerImpl
import com.neki.android.core.data.repository.impl.AuthRepositoryImpl
import com.neki.android.core.data.repository.impl.DataStoreRepositoryImpl
import com.neki.android.core.data.repository.impl.MediaUploadRepositoryImpl
import com.neki.android.core.data.repository.impl.FolderRepositoryImpl
import com.neki.android.core.data.repository.impl.MapRepositoryImpl
import com.neki.android.core.data.repository.impl.PhotoRepositoryImpl
import com.neki.android.core.data.repository.impl.TokenRepositoryImpl
import com.neki.android.core.dataapi.auth.AuthEventManager
import com.neki.android.core.dataapi.repository.FolderRepository
import com.neki.android.core.dataapi.repository.AuthRepository
import com.neki.android.core.dataapi.repository.DataStoreRepository
import com.neki.android.core.dataapi.repository.MediaUploadRepository
Expand Down Expand Up @@ -60,6 +62,12 @@ internal interface RepositoryModule {
photoRepositoryImpl: PhotoRepositoryImpl,
): PhotoRepository

@Binds
@Singleton
fun bindFolderRepositoryImpl(
folderRepositoryImpl: FolderRepositoryImpl,
): FolderRepository

@Binds
@Singleton
fun bindMapRepositoryImpl(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.neki.android.core.data.repository.impl

import com.neki.android.core.data.remote.api.FolderService
import com.neki.android.core.data.remote.model.request.CreateFolderRequest
import com.neki.android.core.data.remote.model.request.DeleteFolderRequest
import com.neki.android.core.data.remote.model.request.DeletePhotoRequest
import com.neki.android.core.data.util.runSuspendCatching
import com.neki.android.core.dataapi.repository.FolderRepository
import com.neki.android.core.model.AlbumPreview
import javax.inject.Inject

class FolderRepositoryImpl @Inject constructor(
private val folderService: FolderService,
) : FolderRepository {
override suspend fun getFolders(): Result<List<AlbumPreview>> = runSuspendCatching {
folderService.getFolders().data.toModels()
}

override suspend fun createFolder(name: String): Result<Unit> = runSuspendCatching {
folderService.createFolder(
requestBody = CreateFolderRequest(name = name),
).data
}

override suspend fun deleteFolder(id: List<Long>, deletePhotos: Boolean): Result<Unit> = runSuspendCatching {
folderService.deleteFolder(
requestBody = DeleteFolderRequest(folderIds = id),
deletePhotos = deletePhotos,
).data
}

override suspend fun removePhotosFromFolder(folderId: Long, photoIds: List<Long>): Result<Unit> = runSuspendCatching {
folderService.removePhotosFromFolder(
folderId = folderId,
requestBody = DeletePhotoRequest(photoIds = photoIds),
).data
}
}
Loading
Loading