Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import com.yapp2app.photo.api.dto.CreateFolderRequest
import com.yapp2app.photo.api.dto.CreateFolderResponse
import com.yapp2app.photo.api.dto.DeleteFoldersRequest
import com.yapp2app.photo.api.dto.GetAllFolderResponse
import com.yapp2app.photo.api.dto.RemovePhotosFromFolderRequest
import com.yapp2app.photo.api.dto.UpdateFolderRequest
import com.yapp2app.photo.application.usecase.CreateFolderUseCase
import com.yapp2app.photo.application.usecase.DeleteFoldersUseCase
import com.yapp2app.photo.application.usecase.GetFoldersUseCase
import com.yapp2app.photo.application.usecase.RemovePhotosFromFolderUseCase
import com.yapp2app.photo.application.usecase.UpdateFolderUseCase
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
Expand All @@ -24,6 +26,7 @@ import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

/**
Expand All @@ -41,6 +44,7 @@ class FolderController(
private val getFoldersUseCase: GetFoldersUseCase,
private val deleteFoldersUseCase: DeleteFoldersUseCase,
private val updateFolderUseCase: UpdateFolderUseCase,
private val removePhotosFromFolderUseCase: RemovePhotosFromFolderUseCase,
private val commandConverter: FolderCommandConverter,
private val resultConverter: FolderResultConverter,
) {
Expand Down Expand Up @@ -80,14 +84,15 @@ class FolderController(

@Operation(
summary = "폴더 삭제 API",
description = "폴더를 삭제합니다.",
description = "폴더를 삭제합니다. deletePhotos=true이면 폴더 내 사진까지 완전 삭제합니다.",
)
@DeleteMapping
fun deleteFolders(
@AuthenticationPrincipal(expression = "id") userId: Long,
@RequestParam(defaultValue = "false") deletePhotos: Boolean,
@Valid @RequestBody request: DeleteFoldersRequest,
): BaseResponse<Any> {
val command = commandConverter.toDeleteFoldersCommand(request, userId)
val command = commandConverter.toDeleteFoldersCommand(request, userId, deletePhotos)

deleteFoldersUseCase.execute(command)

Expand All @@ -110,4 +115,21 @@ class FolderController(

return BaseResponse()
}

@Operation(
summary = "폴더에서 사진 제외 API",
description = "폴더에서 사진을 제외합니다. 사진 자체는 삭제되지 않고 폴더와의 연관관계만 해제됩니다.",
)
@DeleteMapping("/{folderId}/photos")
fun removePhotosFromFolder(
@AuthenticationPrincipal(expression = "id") userId: Long,
@PathVariable folderId: Long,
@Valid @RequestBody request: RemovePhotosFromFolderRequest,
): BaseResponse<Any> {
val command = commandConverter.toRemovePhotosFromFolderCommand(request, folderId, userId)

removePhotosFromFolderUseCase.execute(command)

return BaseResponse()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package com.yapp2app.photo.api.converter

import com.yapp2app.photo.api.dto.CreateFolderRequest
import com.yapp2app.photo.api.dto.DeleteFoldersRequest
import com.yapp2app.photo.api.dto.RemovePhotosFromFolderRequest
import com.yapp2app.photo.api.dto.UpdateFolderRequest
import com.yapp2app.photo.application.command.CreateFolderCommand
import com.yapp2app.photo.application.command.DeleteFoldersCommand
import com.yapp2app.photo.application.command.GetFoldersCommand
import com.yapp2app.photo.application.command.RemovePhotosFromFolderCommand
import com.yapp2app.photo.application.command.UpdateFolderCommand
import org.springframework.stereotype.Component

Expand All @@ -23,9 +25,12 @@ class FolderCommandConverter {

fun toGetFoldersCommand(userId: Long): GetFoldersCommand = GetFoldersCommand(userId)

fun toDeleteFoldersCommand(request: DeleteFoldersRequest, userId: Long) =
DeleteFoldersCommand(userId, request.folderIds)
fun toDeleteFoldersCommand(request: DeleteFoldersRequest, userId: Long, deletePhotos: Boolean) =
DeleteFoldersCommand(userId, request.folderIds, deletePhotos)

fun toUpdateFolderCommand(request: UpdateFolderRequest, folderId: Long, userId: Long) =
UpdateFolderCommand(userId, folderId, request.name!!)

fun toRemovePhotosFromFolderCommand(request: RemovePhotosFromFolderRequest, folderId: Long, userId: Long) =
RemovePhotosFromFolderCommand(userId, folderId, request.photoIds)
}
9 changes: 9 additions & 0 deletions src/main/kotlin/com/yapp2app/photo/api/dto/FolderRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,12 @@ data class UpdateFolderRequest(
@field:Size(min = 1, max = 16, message = "폴더명은 1 ~ 16자 사이여야 합니다.")
val name: String?,
)

data class RemovePhotosFromFolderRequest(
@field:Schema(
description = "폴더에서 제외할 사진 ID 목록",
example = "[1, 2, 3]",
)
@field:NotEmpty(message = "제외할 사진 ID 목록은 비어있을 수 없습니다.")
val photoIds: List<Long>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ package com.yapp2app.photo.application.command
*/
data class CreateFolderCommand(val userId: Long, val name: String)

data class DeleteFoldersCommand(val userId: Long, val folderIds: List<Long>)
data class DeleteFoldersCommand(val userId: Long, val folderIds: List<Long>, val deletePhotos: Boolean = false)

data class GetFoldersCommand(val userId: Long)

data class UpdateFolderCommand(val userId: Long, val folderId: Long, val newName: String)

data class RemovePhotosFromFolderCommand(val userId: Long, val folderId: Long, val photoIds: List<Long>)
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,26 @@ import com.yapp2app.photo.domain.entity.PhotoImage
*/
interface PhotoImageRepositoryPort {

fun getOwnedPhotoWithFavorite(userId: Long, photoId: Long): PhotoWithFavorite?

/**
* 저장
*/
fun save(photoImage: PhotoImage): PhotoImage

fun saveAll(photoImages: List<PhotoImage>): List<PhotoImage>

/**
* 조회
*/
fun existsOwnedPhoto(userId: Long, photoId: Long): Boolean

fun getOwnedPhoto(userId: Long, photoId: Long): PhotoImage?

fun getLatestFavoritePhoto(userId: Long): PhotoImage?

fun getOwnedPhotoWithFavorite(userId: Long, photoId: Long): PhotoWithFavorite?

fun getPhotoIdsByFolderIds(userId: Long, folderIds: List<Long>): List<Long>

fun listOwnedPhotos(userId: Long, offset: Int, limit: Int, sortOrder: SortOrder): List<PhotoImage>

fun listOwnedPhotosWithFavorite(
Expand All @@ -29,13 +44,15 @@ interface PhotoImageRepositoryPort {

fun listOwnedFavoritePhotos(userId: Long, offset: Int, limit: Int, sortOrder: SortOrder): List<PhotoImage>

/**
* 삭제
*/
fun deleteOwnedPhotos(userId: Long, photoIds: List<Long>): List<PhotoImage>

fun getOwnedPhoto(userId: Long, photoId: Long): PhotoImage?

fun existsOwnedPhoto(userId: Long, photoId: Long): Boolean

fun getLatestFavoritePhoto(userId: Long): PhotoImage?
fun removePhotosFromFolder(userId: Long, folderId: Long, photoIds: List<Long>): Int

/**
* 갱신
*/
fun updatePhotosFolderIdToNull(userId: Long, folderIds: List<Long>): Int
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package com.yapp2app.photo.application.usecase
import com.yapp2app.common.annotation.UseCase
import com.yapp2app.common.api.dto.ResultCode
import com.yapp2app.common.exception.BusinessException
import com.yapp2app.common.transaction.TransactionRunner
import com.yapp2app.photo.application.command.DeleteFoldersCommand
import com.yapp2app.photo.application.port.FavoriteImageRepositoryPort
import com.yapp2app.photo.application.port.FolderRepositoryPort
import com.yapp2app.photo.application.port.MediaClientPort
import com.yapp2app.photo.application.port.PhotoImageRepositoryPort
import org.springframework.transaction.annotation.Transactional

/**
* fileName : DeleteFolderUseCase
Expand All @@ -18,19 +20,53 @@ import org.springframework.transaction.annotation.Transactional
class DeleteFoldersUseCase(
private val folderRepository: FolderRepositoryPort,
private val photoImageRepository: PhotoImageRepositoryPort,
private val favoriteImageRepository: FavoriteImageRepositoryPort,
private val mediaClient: MediaClientPort,
private val transactionRunner: TransactionRunner,
) {

@Transactional
fun execute(command: DeleteFoldersCommand) {
photoImageRepository.updatePhotosFolderIdToNull(command.userId, command.folderIds)
// 삭제할 사진 ID 조회
val photoIdsToDelete = if (command.deletePhotos) {
transactionRunner.readOnly {
photoImageRepository.getPhotoIdsByFolderIds(command.userId, command.folderIds)
}
} else {
emptyList()
}

val deletedPhotos = transactionRunner.run {
if (command.deletePhotos) {
// 사진까지 삭제하는 경우 즐겨찾기 먼저 삭제
if (photoIdsToDelete.isNotEmpty()) {
favoriteImageRepository.deleteAll(command.userId, photoIdsToDelete)
}
} else {
// 사진 삭제를 하지 않는 경우 사진에서 folderId만 없앰
photoImageRepository.updatePhotosFolderIdToNull(command.userId, command.folderIds)
}

// 폴더 삭제
val deletedCount = folderRepository.deleteOwnedFolders(
command.userId,
command.folderIds,
)

val deletedCount = folderRepository.deleteOwnedFolders(
command.userId,
command.folderIds,
)
if (deletedCount != command.folderIds.size) {
throw BusinessException(ResultCode.NOT_FOUND)
}

// 사진까지 삭제하는 경우 사진 삭제
if (photoIdsToDelete.isNotEmpty()) {
photoImageRepository.deleteOwnedPhotos(command.userId, photoIdsToDelete)
} else {
emptyList()
}
}

if (deletedCount != command.folderIds.size) {
throw BusinessException(ResultCode.NOT_FOUND)
// 미디어 삭제
if (deletedPhotos.isNotEmpty()) {
mediaClient.deleteMedias(command.userId, deletedPhotos.map { it.mediaId })
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.yapp2app.photo.application.usecase

import com.yapp2app.common.annotation.UseCase
import com.yapp2app.common.api.dto.ResultCode
import com.yapp2app.common.exception.BusinessException
import com.yapp2app.photo.application.command.RemovePhotosFromFolderCommand
import com.yapp2app.photo.application.port.FolderRepositoryPort
import com.yapp2app.photo.application.port.PhotoImageRepositoryPort
import org.springframework.transaction.annotation.Transactional

/**
* fileName : RemovePhotosFromFolderUseCase
* author : claude
* date : 2026. 1. 28.
* description : 폴더에서 사진 제외 usecase (연관관계만 해제)
*/
@UseCase
class RemovePhotosFromFolderUseCase(
private val folderRepository: FolderRepositoryPort,
private val photoImageRepository: PhotoImageRepositoryPort,
) {

@Transactional
fun execute(command: RemovePhotosFromFolderCommand) {
// 폴더 소유권 확인
folderRepository.getOwnedFolder(command.userId, command.folderId)
?: throw BusinessException(ResultCode.NOT_FOUND)

// 사진들의 folderId를 NULL로 설정
photoImageRepository.removePhotosFromFolder(
command.userId,
command.folderId,
command.photoIds,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class PhotoImageRepositoryAdapter(
}

jpaRepository.deleteAll(photos)
jpaRepository.flush()

return photos
}
Expand All @@ -66,4 +67,10 @@ class PhotoImageRepositoryAdapter(

override fun updatePhotosFolderIdToNull(userId: Long, folderIds: List<Long>): Int =
queryRepository.updatePhotosFolderIdToNull(userId, folderIds)

override fun getPhotoIdsByFolderIds(userId: Long, folderIds: List<Long>): List<Long> =
queryRepository.getPhotoIdsByFolderIds(userId, folderIds)

override fun removePhotosFromFolder(userId: Long, folderId: Long, photoIds: List<Long>): Int =
queryRepository.removePhotosFromFolder(userId, folderId, photoIds)
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,31 @@ class PhotoImageQueryRepository(private val queryFactory: JPAQueryFactory) {
.where(photoImage.userId.eq(userId), photoImage.folderId.`in`(folderIds))
.execute().toInt()
}

fun getPhotoIdsByFolderIds(userId: Long, folderIds: List<Long>): List<Long> {
if (folderIds.isEmpty()) return emptyList()

return queryFactory
.select(photoImage.id)
.from(photoImage)
.where(
photoImage.userId.eq(userId),
photoImage.folderId.`in`(folderIds),
)
.fetch()
}

fun removePhotosFromFolder(userId: Long, folderId: Long, photoIds: List<Long>): Int {
if (photoIds.isEmpty()) return 0

return queryFactory
.update(photoImage)
.setNull(photoImage.folderId)
.where(
photoImage.userId.eq(userId),
photoImage.folderId.eq(folderId),
photoImage.id.`in`(photoIds),
)
.execute().toInt()
}
}
Loading