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
@@ -1,14 +1,17 @@
package busanVibe.busan.domain.festival.controller

import busanVibe.busan.domain.festival.dto.FestivalDetailsDTO
import busanVibe.busan.domain.festival.dto.FestivalLikeResponseDTO
import busanVibe.busan.domain.festival.dto.FestivalListResponseDTO
import busanVibe.busan.domain.festival.enums.FestivalSortType
import busanVibe.busan.domain.festival.enums.FestivalStatus
import busanVibe.busan.domain.festival.service.FestivalCommandService
import busanVibe.busan.domain.festival.service.FestivalQueryService
import busanVibe.busan.global.apiPayload.exception.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
Expand All @@ -18,7 +21,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/festivals")
class FestivalController(
private val festivalQueryService: FestivalQueryService
private val festivalQueryService: FestivalQueryService,
private val festivalCommandService: FestivalCommandService
) {

@GetMapping
Expand All @@ -42,4 +46,12 @@ class FestivalController(
return ApiResponse.onSuccess(festivalDetails);
}

@PatchMapping("/like/{festivalId}")
@Operation(summary = "지역축제 좋아요 API",
description = "지역축제 좋아요 API 입니다. 이미지 좋아요를 누른 경우 좋아요가 취소됩니다.")
fun like(@PathVariable("festivalId") festivalId: Long?): ApiResponse<FestivalLikeResponseDTO>{
val likeDTO = festivalCommandService.like(festivalId)
return ApiResponse.onSuccess(likeDTO);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,30 @@ package busanVibe.busan.domain.festival.domain

import busanVibe.busan.domain.common.BaseEntity
import busanVibe.busan.domain.user.data.User
import jakarta.persistence.EmbeddedId
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.MapsId

@Entity
class FestivalLike(
open class FestivalLike(

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@EmbeddedId
val id: FestivalLikeId,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@MapsId("userId")
val user: User,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "festival_id")
@MapsId("festivalId")
val festival: Festival

): BaseEntity() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package busanVibe.busan.domain.festival.domain

import jakarta.persistence.Embeddable
import lombok.Getter
import java.io.Serializable

@Embeddable
@Getter
open class FestivalLikeId(

private val userId: Long,
private val festivalId: Long
): Serializable {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package busanVibe.busan.domain.festival.dto

import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
data class FestivalLikeResponseDTO(
val success: Boolean,
val message: String,
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ class FestivalListResponseDTO {
val likeAmount: Int,
)



}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ package busanVibe.busan.domain.festival.repository

import busanVibe.busan.domain.festival.domain.Festival
import busanVibe.busan.domain.festival.domain.FestivalLike
import busanVibe.busan.domain.user.data.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param

interface FestivalLikesRepository: JpaRepository<FestivalLike, String> {

fun findAllByFestivalIn(festivalList: List<Festival>): List<FestivalLike>
fun findAllByFestivalIn(festivalList: Set<Festival>): List<FestivalLike>

@Query(
"""
SELECT fl FROM FestivalLike fl
Expand All @@ -20,4 +18,8 @@ interface FestivalLikesRepository: JpaRepository<FestivalLike, String> {
)
fun findLikeByFestival(@Param("festivals") festivalList: List<Festival>): List<FestivalLike>

fun findByFestivalAndUser(@Param("festival") festival: Festival, @Param("user") user: User): FestivalLike?

fun deleteByFestivalAndUser(@Param("festival") festival: Festival, @Param("user") user: User)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package busanVibe.busan.domain.festival.service

import busanVibe.busan.domain.festival.domain.Festival
import busanVibe.busan.domain.festival.domain.FestivalLike
import busanVibe.busan.domain.festival.domain.FestivalLikeId
import busanVibe.busan.domain.festival.dto.FestivalLikeResponseDTO
import busanVibe.busan.domain.festival.repository.FestivalLikesRepository
import busanVibe.busan.domain.festival.repository.FestivalRepository
import busanVibe.busan.domain.user.data.User
import busanVibe.busan.domain.user.service.login.AuthService
import busanVibe.busan.global.apiPayload.code.status.ErrorStatus
import busanVibe.busan.global.apiPayload.exception.handler.ExceptionHandler
import jakarta.transaction.Transactional
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.stereotype.Service

@Service
class FestivalCommandService(
val festivalRepository: FestivalRepository,
val festivalLikeRepository: FestivalLikesRepository,
private val festivalLikesRepository: FestivalLikesRepository
) {

@Transactional
fun like(festivalId: Long?): FestivalLikeResponseDTO{

// null 처리
if( festivalId == null ){
throw ExceptionHandler(ErrorStatus.FESTIVAL_ID_REQUIRED)
}

// 필요한 객체 생성
val currentUser = AuthService().getCurrentUser()
val festival: Festival = festivalRepository.findById(festivalId).orElseThrow{ ExceptionHandler(ErrorStatus.FESTIVAL_NOT_FOUND) }
var message: String

// 좋아요 정보 조회
val festivalLike = festivalLikeRepository.findByFestivalAndUser(festival, currentUser)

// 기존 좋아요 여부에 따른 처리
if(festivalLike != null){ // 이미 좋아요 누른 경우
cancelLike(festival, currentUser) // 좋아요 취소
message = "좋아요 취소 성공"
}else{ // 이전에 좋아요 누르지 않은 경우
val festivalLikeId = FestivalLikeId(userId = currentUser.id!!, festivalId = festivalId) // ID 객체 생성

var festivalLike: FestivalLike // 객체 미리 생성

try {
festivalLike =
FestivalLike(id = festivalLikeId, user = currentUser, festival = festival)
festivalLikesRepository.save(festivalLike) // 좋아요 정보 저장
message = "좋아요 성공"
} catch (e: DataIntegrityViolationException) {
// 동시성 문제 대비 2차로 중복 처리 - 복합키 조회하여 중복 조회
cancelLike(festival, currentUser) // 좋아요 취소
message = "좋아요 취소 성공"
}
}

// DTO 생성 및 반환
return FestivalLikeResponseDTO(
success = true,
message = message
)

}

// 좋아요 취소 처리 메서드
private fun cancelLike(festival: Festival, user: User) {
try{
festivalLikeRepository.deleteByFestivalAndUser(festival, user)
}catch (e: Exception){
throw RuntimeException(e.message, e)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package busanVibe.busan.domain.place.controller
import busanVibe.busan.domain.place.dto.PlaceResponseDTO
import busanVibe.busan.domain.place.enums.PlaceType
import busanVibe.busan.domain.place.enums.PlaceSortType
import busanVibe.busan.domain.place.service.PlaceCommandService
import busanVibe.busan.domain.place.service.PlaceQueryService
import busanVibe.busan.global.apiPayload.exception.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
Expand All @@ -17,7 +19,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/places")
class PlaceController(
val placeQueryService: PlaceQueryService
val placeQueryService: PlaceQueryService,
val placeCommandService: PlaceCommandService
) {

@GetMapping
Expand All @@ -27,13 +30,14 @@ class PlaceController(
@RequestParam("sort", required = false) sort: PlaceSortType?
): ApiResponse<PlaceResponseDTO.PlaceListDto>?{


val placeList = placeQueryService.getPlaceList(type, sort)
return ApiResponse.onSuccess(placeList)

}

@GetMapping("/{placeId}")
@Operation(summary = "명소 상세 조회",
@Operation(summary = "명소 상세 조회 API",
description =
"""
명소 상세 조회 API 입니다. 타입 구분 없이 API 응답 형태가 동일합니다.
Expand All @@ -49,4 +53,14 @@ class PlaceController(
return ApiResponse.onSuccess(placeDetail)
}

@PatchMapping("/like/{placeId}")
@Operation(summary = "명소 좋아요 API",
description = "명소 좋아요 API 입니다. 만약 이미 좋아요를 누른 상태라면 좋아요가 취소됩니다.")
fun like(@PathVariable("placeId") placeId: Long?): ApiResponse<PlaceResponseDTO.LikeDto>{
val likeDTO = placeCommandService.like(placeId)
return ApiResponse.onSuccess(likeDTO)
}



}
4 changes: 2 additions & 2 deletions src/main/kotlin/busanVibe/busan/domain/place/domain/Place.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ class Place(
@OneToMany(mappedBy = "place", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true)
val placeImages: MutableSet<PlaceImage> = mutableSetOf(),

@OneToOne(fetch = FetchType.LAZY)
@OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST, CascadeType.MERGE])
@JoinColumn(name = "visitor_distribution_id")
val visitorDistribution: VisitorDistribution? = null,
val visitorDistribution: VisitorDistribution,

) : BaseEntity(){

Expand Down
21 changes: 13 additions & 8 deletions src/main/kotlin/busanVibe/busan/domain/place/domain/PlaceLike.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
package busanVibe.busan.domain.place.domain

import busanVibe.busan.domain.common.BaseEntity
import busanVibe.busan.domain.user.data.User
import jakarta.persistence.EmbeddedId
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.MapsId

@Entity
class PlaceLike(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long?,
open class PlaceLike(
// @Id
// @GeneratedValue(strategy = GenerationType.IDENTITY)
// val id: Long?,

@EmbeddedId
val id: PlaceLikeId,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@MapsId("userId")
val user: User,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "place_id")
@MapsId("placeId")
val place: Place,

) {
): BaseEntity() {

}
14 changes: 14 additions & 0 deletions src/main/kotlin/busanVibe/busan/domain/place/domain/PlaceLikeId.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package busanVibe.busan.domain.place.domain

import jakarta.persistence.Embeddable
import lombok.Getter
import java.io.Serializable

@Embeddable
@Getter
open class PlaceLikeId(
private val userId: Long,
private val placeId: Long
): Serializable {

}
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,10 @@ class PlaceResponseDTO {
// val content: String
// )

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
data class LikeDto(
val success: Boolean,
val message: String,
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ package busanVibe.busan.domain.place.repository

import busanVibe.busan.domain.place.domain.Place
import busanVibe.busan.domain.place.domain.PlaceLike
import busanVibe.busan.domain.user.data.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param

interface PlaceLikeRepository: JpaRepository<PlaceLike, Long> {
fun findAllByPlaceIn(placeList: List<Place>): List<PlaceLike>
fun findByPlace(place: Place): List<PlaceLike>

@Query("SELECT pl FROM PlaceLike pl WHERE pl.place IN :places")
fun findLikeByPlace(@Param("places") placeList: List<Place>): List<PlaceLike>

fun findByPlaceAndUser(@Param("place") place: Place, @Param("user") user: User): PlaceLike?

fun deleteByPlaceAndUser(@Param("place") place: Place, @Param("user") user: User)


}
Loading