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
366 changes: 366 additions & 0 deletions src/main/kotlin/com/tripsync/application/photo/PhotoService.kt

Large diffs are not rendered by default.

79 changes: 58 additions & 21 deletions src/main/kotlin/com/tripsync/application/room/RoomService.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.tripsync.application.room

import com.tripsync.application.schedule.ScheduleResponseMapper
import com.tripsync.common.dto.ApiResponse
import com.tripsync.common.exception.DomainException
import com.tripsync.domain.entity.RoomMember
Expand All @@ -22,10 +23,12 @@ class RoomService(
private val roomMemberRepository: RoomMemberRepository,
private val roomMemberProfileRepository: RoomMemberProfileRepository,
private val tptiResultRepository: TptiResultRepository,
private val scheduleRepository: ScheduleRepository,
private val scheduleResponseMapper: ScheduleResponseMapper,
) {

@Transactional
fun createRoom(host: User, destination: String, tripDate: LocalDate): ApiResponse<Map<String, Any>> {
fun createRoom(host: User, destination: String, tripDate: LocalDate): ApiResponse<Map<String, Any?>> {
if (host.isGuest) {
throw DomainException(HttpStatus.FORBIDDEN, "FORBIDDEN", "방장 권한이 필요합니다.")
}
Expand Down Expand Up @@ -64,16 +67,16 @@ class RoomService(
}

@Transactional(readOnly = true)
fun getRoom(roomId: Long, user: User): ApiResponse<Map<String, Any>> {
fun getRoom(roomId: Long, user: User): ApiResponse<Map<String, Any?>> {
validateRoomMember(roomId, user.id)
val room = getActiveRoom(roomId)
val memberCount = roomMemberRepository.findAllByRoomIdAndDelYn(room.id, YnFlag.N).size

return ApiResponse.ok(roomSummary(room, memberCount))
return ApiResponse.ok(roomSummary(room, memberCount, includeScheduleState = true))
}

@Transactional(readOnly = true)
fun getMyRooms(user: User): ApiResponse<Map<String, Any>> {
fun getMyRooms(user: User): ApiResponse<Map<String, Any?>> {
if (user.isGuest) {
throw DomainException(HttpStatus.FORBIDDEN, "FORBIDDEN", "방장 계정으로 로그인해주세요.")
}
Expand All @@ -85,24 +88,24 @@ class RoomService(
.sortedWith(compareByDescending<TripRoom> { it.createdAt }.thenByDescending { it.id })
.map { room ->
val memberCount = roomMemberRepository.findAllByRoomIdAndDelYn(room.id, YnFlag.N).size
roomSummary(room, memberCount)
roomSummary(room, memberCount, includeScheduleState = false)
}

return ApiResponse.ok(mapOf("rooms" to rooms))
}

@Transactional(readOnly = true)
fun getShareRoom(shareCode: String): ApiResponse<Map<String, Any>> {
fun getShareRoom(shareCode: String): ApiResponse<Map<String, Any?>> {
val room = tripRoomRepository.findByShareCodeAndDelYn(shareCode, YnFlag.N)
?: throw DomainException(HttpStatus.NOT_FOUND, "INVALID_SHARE_CODE", "유효하지 않은 공유 코드입니다.")
val memberCount = roomMemberRepository.findAllByRoomIdAndDelYn(room.id, YnFlag.N).size
return ApiResponse.ok(
roomSummary(room, memberCount) + mapOf("hostNickname" to room.hostUser.nickname)
roomSummary(room, memberCount, includeScheduleState = false) + mapOf("hostNickname" to room.hostUser.nickname)
)
}

@Transactional
fun joinRoom(shareCode: String, tptiResultId: Long?, user: User): ApiResponse<Map<String, Any>> {
fun joinRoom(shareCode: String, tptiResultId: Long?, user: User): ApiResponse<Map<String, Any?>> {
val room = tripRoomRepository.findByShareCodeAndDelYn(shareCode, YnFlag.N)
?: throw DomainException(HttpStatus.NOT_FOUND, "INVALID_SHARE_CODE", "유효하지 않은 공유 코드입니다.")

Expand Down Expand Up @@ -137,7 +140,7 @@ class RoomService(
}

@Transactional(readOnly = true)
fun getMembers(roomId: Long, user: User): ApiResponse<Map<String, Any>> {
fun getMembers(roomId: Long, user: User): ApiResponse<Map<String, Any?>> {
validateRoomMember(roomId, user.id)
val members = roomMemberRepository.findAllByRoomIdAndDelYn(roomId, YnFlag.N)
val profilesByUserId = roomMemberProfileRepository.findAllByRoomIdAndDelYn(roomId, YnFlag.N)
Expand Down Expand Up @@ -219,18 +222,52 @@ class RoomService(
}
}

private fun roomSummary(room: TripRoom, memberCount: Int): Map<String, Any> = mapOf(
"roomId" to room.id,
"destination" to room.destination,
"tripDate" to room.tripDate.toString(),
"tripStartDate" to room.tripDate.toString(),
"tripEndDate" to room.tripDate.toString(),
"shareCode" to room.shareCode,
"status" to room.status.name.lowercase(),
"hostUserId" to room.hostUser.id,
"memberCount" to memberCount,
"createdAt" to room.createdAt.toString(),
)
private fun roomSummary(room: TripRoom, memberCount: Int, includeScheduleState: Boolean): Map<String, Any?> {
val schedules = scheduleRepository.findByRoomIdAndDelYn(room.id, YnFlag.N)
val confirmed = schedules
.filter { it.isConfirmed }
.maxWithOrNull(compareBy<com.tripsync.domain.entity.Schedule> { it.version }.thenBy { it.id })
val latestVersion = schedules.maxOfOrNull { it.version }
val base = mapOf(
"roomId" to room.id,
"destination" to room.destination,
"tripDate" to room.tripDate.toString(),
"tripStartDate" to room.tripDate.toString(),
"tripEndDate" to room.tripDate.toString(),
"shareCode" to room.shareCode,
"status" to room.status.name.lowercase(),
"hostUserId" to room.hostUser.id,
"memberCount" to memberCount,
"createdAt" to room.createdAt.toString(),
"hasGeneratedSchedule" to schedules.isNotEmpty(),
"confirmedScheduleId" to confirmed?.id,
"latestScheduleVersion" to latestVersion,
)
if (!includeScheduleState) return base

val latestOptions = latestVersion
?.let { version -> schedules.filter { it.version == version }.sortedBy { it.optionType.ordinal } }
.orEmpty()
val scheduleState = when {
confirmed != null -> mapOf(
"status" to "confirmed",
"confirmedSchedule" to scheduleResponseMapper.formatStoredSchedule(confirmed),
"options" to latestOptions.map { scheduleResponseMapper.formatStoredSchedule(it) },
)
latestOptions.isNotEmpty() -> mapOf(
"status" to "generated",
"confirmedSchedule" to null,
"options" to latestOptions.map { scheduleResponseMapper.formatStoredSchedule(it) },
)
else -> mapOf(
"status" to "empty",
"confirmedSchedule" to null,
"options" to emptyList<Map<String, Any?>>(),
)
}

return base + mapOf("scheduleState" to scheduleState)
}

private fun generateShareCode(): String {
val suffix = UUID.randomUUID().toString().replace("-", "").take(5).uppercase()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ class ScheduleGenerationPersistenceService(
personaValidation = personaValidation,
)
}
room.status = TripRoomStatus.COMPLETED
return SavedScheduleGeneration(version = version, options = saved)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ class ScheduleService(
return ApiResponse.ok(responseMapper.formatStoredSchedule(schedule))
}

@Transactional(readOnly = true)
fun getConfirmedSchedule(roomId: Long, userId: Long): ApiResponse<Map<String, Any?>> {
accessPolicy.validateRoomMember(roomId, userId)
val schedule = scheduleRepository.findConfirmedByRoomId(roomId, YnFlag.N).firstOrNull()
?: throw DomainException(HttpStatus.NOT_FOUND, "SCHEDULE_NOT_FOUND", "확정된 일정이 없습니다.")
return ApiResponse.ok(responseMapper.formatStoredSchedule(schedule))
}


@Transactional(readOnly = true)
fun searchPlacesForSchedule(scheduleId: Long, userId: Long, query: String): ApiResponse<Map<String, Any?>> {
Expand Down Expand Up @@ -183,6 +191,7 @@ class ScheduleService(

scheduleRepository.findByRoomIdAndDelYn(roomId, YnFlag.N).forEach { it.isConfirmed = false }
target.isConfirmed = true
target.room.status = com.tripsync.domain.enums.TripRoomStatus.COMPLETED

return ApiResponse.ok(
mapOf(
Expand Down
80 changes: 80 additions & 0 deletions src/main/kotlin/com/tripsync/domain/entity/TripPhoto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.tripsync.domain.entity

import com.tripsync.domain.enums.PhotoStatus
import jakarta.persistence.*
import org.hibernate.annotations.JdbcTypeCode
import org.hibernate.annotations.UpdateTimestamp
import org.hibernate.type.SqlTypes
import java.time.Instant

@Entity
@Table(
name = "trip_photos",
indexes = [
Index(name = "idx_trip_photos_schedule_slot", columnList = "schedule_id, schedule_slot_id"),
Index(name = "idx_trip_photos_room", columnList = "room_id"),
Index(name = "idx_trip_photos_uploader", columnList = "uploader_user_id"),
]
)
class TripPhoto(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "room_id", nullable = false)
var room: TripRoom,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "schedule_id", nullable = false)
var schedule: Schedule,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "schedule_slot_id", nullable = false)
var scheduleSlot: ScheduleSlot,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "place_id", nullable = false)
var place: Place,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "uploader_user_id", nullable = false)
var uploader: User,

@Column(name = "original_filename", nullable = false, length = 255)
var originalFilename: String,

@Column(name = "content_type", nullable = false, length = 100)
var contentType: String,

@Column(name = "file_size", nullable = false)
var fileSize: Long,

@JdbcTypeCode(SqlTypes.VARBINARY)
@Column(name = "content", nullable = false, columnDefinition = "bytea")
var content: ByteArray,

@Column(name = "width")
var width: Int? = null,

@Column(name = "height")
var height: Int? = null,

@Column(name = "caption", length = 500)
var caption: String? = null,

@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
var status: PhotoStatus = PhotoStatus.ACTIVE,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "deleted_by_user_id")
var deletedBy: User? = null,

@Column(name = "deleted_at")
var deletedAt: Instant? = null,
) : BaseEntity() {
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
var updatedAt: Instant = Instant.now()
}
7 changes: 7 additions & 0 deletions src/main/kotlin/com/tripsync/domain/enums/PhotoStatus.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.tripsync.domain.enums

enum class PhotoStatus {
ACTIVE,
HIDDEN,
DELETED,
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,27 @@ package com.tripsync.domain.repository
import com.tripsync.domain.entity.Schedule
import com.tripsync.domain.enums.YnFlag
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository

@Repository
interface ScheduleRepository : JpaRepository<Schedule, Long> {
fun findByRoomIdAndDelYn(roomId: Long, delYn: YnFlag): List<Schedule>
fun findTopByRoomIdAndDelYnOrderByVersionDesc(roomId: Long, delYn: YnFlag): Schedule?

@Query(
"""
select schedule
from Schedule schedule
where schedule.room.id = :roomId
and schedule.delYn = :delYn
and schedule.isConfirmed = true
order by schedule.version desc, schedule.id desc
"""
)
fun findConfirmedByRoomId(
@Param("roomId") roomId: Long,
@Param("delYn") delYn: YnFlag,
): List<Schedule>
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.springframework.stereotype.Repository
@Repository
interface ScheduleSlotRepository : JpaRepository<ScheduleSlot, Long> {
fun findAllByScheduleIdAndDelYn(scheduleId: Long, delYn: YnFlag): List<ScheduleSlot>
fun findByIdAndDelYn(id: Long, delYn: YnFlag): ScheduleSlot?

@Query("select slot.place.id from ScheduleSlot slot where slot.schedule.id = :scheduleId and slot.delYn = :delYn")
fun findActivePlaceIdsByScheduleId(scheduleId: Long, delYn: YnFlag = YnFlag.N): List<Long>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.tripsync.domain.repository

import com.tripsync.domain.entity.TripPhoto
import com.tripsync.domain.enums.PhotoStatus
import com.tripsync.domain.enums.YnFlag
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository
import java.time.Instant

@Repository
interface TripPhotoRepository : JpaRepository<TripPhoto, Long> {
@Query(
"""
select new com.tripsync.domain.repository.TripPhotoAlbumRow(
photo.id,
photo.schedule.id,
photo.scheduleSlot.id,
photo.place.id,
photo.uploader.id,
photo.uploader.nickname,
photo.originalFilename,
photo.contentType,
photo.fileSize,
photo.caption,
photo.status,
photo.createdAt,
photo.updatedAt
)
from TripPhoto photo
where photo.schedule.id = :scheduleId
and photo.delYn = :delYn
and photo.status = :status
order by photo.scheduleSlot.orderIndex asc, photo.createdAt asc
"""
)
fun findAlbumRowsByScheduleIdAndDelYnAndStatus(
@Param("scheduleId") scheduleId: Long,
@Param("delYn") delYn: YnFlag,
@Param("status") status: PhotoStatus,
): List<TripPhotoAlbumRow>

fun findAllByScheduleIdAndDelYnAndStatusOrderByScheduleSlotOrderIndexAscCreatedAtAsc(
scheduleId: Long,
delYn: YnFlag,
status: PhotoStatus,
): List<TripPhoto>

fun countByScheduleIdAndDelYnAndStatus(scheduleId: Long, delYn: YnFlag, status: PhotoStatus): Long
fun countByScheduleSlotIdAndDelYnAndStatus(scheduleSlotId: Long, delYn: YnFlag, status: PhotoStatus): Long
fun countByScheduleIdAndUploaderIdAndDelYnAndStatus(scheduleId: Long, uploaderId: Long, delYn: YnFlag, status: PhotoStatus): Long

fun findByIdAndDelYn(id: Long, delYn: YnFlag): TripPhoto?
fun findByIdAndScheduleIdAndDelYn(id: Long, scheduleId: Long, delYn: YnFlag): TripPhoto?
fun findByIdAndScheduleIdAndDelYnAndStatus(
id: Long,
scheduleId: Long,
delYn: YnFlag,
status: PhotoStatus,
): TripPhoto?
}


data class TripPhotoAlbumRow(
val id: Long,
val scheduleId: Long,
val scheduleSlotId: Long,
val placeId: Long,
val uploaderUserId: Long,
val uploaderNickname: String,
val originalFilename: String,
val contentType: String,
val fileSize: Long,
val caption: String?,
val status: PhotoStatus,
val createdAt: Instant,
val updatedAt: Instant,
)
5 changes: 5 additions & 0 deletions src/main/kotlin/com/tripsync/web/dto/AuthDto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,8 @@ data class ReorderScheduleSlotsDto(
data class AddScheduleSlotDto(
val placeId: Long,
)

data class UpdatePhotoCaptionDto(
@field:Size(max = 500)
val caption: String? = null,
)
Loading
Loading