generated from pricelees/issue-pr-template
[#13] Theme 도메인 코드 코틀린 마이그레이션 #15
@ -69,9 +69,9 @@ class AdminInterceptor(
|
||||
val token: String? = request.accessTokenCookie().value
|
||||
val memberId: Long = jwtHandler.getMemberIdFromToken(token)
|
||||
member = memberService.findById(memberId)
|
||||
} catch (e: RoomescapeException) {
|
||||
} catch (_: RoomescapeException) {
|
||||
response.sendRedirect("/login")
|
||||
throw e
|
||||
throw RoomescapeException(ErrorType.LOGIN_REQUIRED, HttpStatus.FORBIDDEN)
|
||||
}
|
||||
|
||||
with(member) {
|
||||
|
||||
@ -8,11 +8,4 @@ import roomescape.common.exception.ErrorType
|
||||
data class RoomescapeErrorResponse(
|
||||
val errorType: ErrorType,
|
||||
val message: String
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun of(errorType: ErrorType, message: String? = null): RoomescapeErrorResponse =
|
||||
RoomescapeErrorResponse(errorType, message ?: errorType.description)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -18,7 +18,7 @@ import jakarta.persistence.ManyToOne;
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
|
||||
@Entity
|
||||
public class Reservation {
|
||||
@ -35,7 +35,7 @@ public class Reservation {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "theme_id", nullable = false)
|
||||
private Theme theme;
|
||||
private ThemeEntity theme;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id", nullable = false)
|
||||
@ -50,7 +50,7 @@ public class Reservation {
|
||||
public Reservation(
|
||||
LocalDate date,
|
||||
ReservationTime reservationTime,
|
||||
Theme theme,
|
||||
ThemeEntity theme,
|
||||
Member member,
|
||||
ReservationStatus status
|
||||
) {
|
||||
@ -61,7 +61,7 @@ public class Reservation {
|
||||
Long id,
|
||||
LocalDate date,
|
||||
ReservationTime reservationTime,
|
||||
Theme theme,
|
||||
ThemeEntity theme,
|
||||
Member member,
|
||||
ReservationStatus status
|
||||
) {
|
||||
@ -74,7 +74,7 @@ public class Reservation {
|
||||
this.reservationStatus = status;
|
||||
}
|
||||
|
||||
private void validateIsNull(LocalDate date, ReservationTime reservationTime, Theme theme, Member member,
|
||||
private void validateIsNull(LocalDate date, ReservationTime reservationTime, ThemeEntity theme, Member member,
|
||||
ReservationStatus reservationStatus) {
|
||||
if (date == null || reservationTime == null || theme == null || member == null || reservationStatus == null) {
|
||||
throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[values: %s]", this),
|
||||
@ -98,7 +98,7 @@ public class Reservation {
|
||||
return reservationTime;
|
||||
}
|
||||
|
||||
public Theme getTheme() {
|
||||
public ThemeEntity getTheme() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import roomescape.member.web.MemberResponse;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.theme.dto.ThemeResponse;
|
||||
import roomescape.theme.web.ThemeResponse;
|
||||
|
||||
@Schema(name = "예약 정보", description = "예약 저장 및 조회 응답에 사용됩니다.")
|
||||
public record ReservationResponse(
|
||||
|
||||
@ -24,8 +24,8 @@ import roomescape.reservation.dto.request.WaitingRequest;
|
||||
import roomescape.reservation.dto.response.MyReservationsResponse;
|
||||
import roomescape.reservation.dto.response.ReservationResponse;
|
||||
import roomescape.reservation.dto.response.ReservationsResponse;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.service.ThemeService;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.business.ThemeService;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@ -146,7 +146,7 @@ public class ReservationService {
|
||||
private Reservation getReservationForSave(Long timeId, Long themeId, LocalDate date, Long memberId,
|
||||
ReservationStatus status) {
|
||||
ReservationTime time = reservationTimeService.findTimeById(timeId);
|
||||
Theme theme = themeService.findThemeById(themeId);
|
||||
ThemeEntity theme = themeService.findThemeById(themeId);
|
||||
Member member = memberService.findById(memberId);
|
||||
|
||||
validateDateAndTime(date, time);
|
||||
|
||||
74
src/main/java/roomescape/theme/business/ThemeService.kt
Normal file
74
src/main/java/roomescape/theme/business/ThemeService.kt
Normal file
@ -0,0 +1,74 @@
|
||||
package roomescape.theme.business
|
||||
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||
import roomescape.theme.web.ThemeRequest
|
||||
import roomescape.theme.web.ThemeResponse
|
||||
import roomescape.theme.web.ThemesResponse
|
||||
import roomescape.theme.web.toResponse
|
||||
import java.time.LocalDate
|
||||
|
||||
@Service
|
||||
class ThemeService(
|
||||
private val themeRepository: ThemeRepository
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun findThemeById(id: Long): ThemeEntity = themeRepository.findByIdOrNull(id)
|
||||
?: throw RoomescapeException(
|
||||
ErrorType.THEME_NOT_FOUND,
|
||||
"[themeId: $id]",
|
||||
HttpStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findAllThemes(): ThemesResponse = themeRepository.findAll()
|
||||
.toResponse()
|
||||
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getMostReservedThemesByCount(count: Int): ThemesResponse {
|
||||
val today = LocalDate.now()
|
||||
val startDate = today.minusDays(7)
|
||||
val endDate = today.minusDays(1)
|
||||
|
||||
return themeRepository.findTopNThemeBetweenStartDateAndEndDate(startDate, endDate, count)
|
||||
.toResponse()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun save(request: ThemeRequest): ThemeResponse {
|
||||
if (themeRepository.existsByName(request.name)) {
|
||||
throw RoomescapeException(
|
||||
ErrorType.THEME_DUPLICATED,
|
||||
"[name: ${request.name}]",
|
||||
HttpStatus.CONFLICT
|
||||
)
|
||||
}
|
||||
|
||||
return ThemeEntity(
|
||||
name = request.name,
|
||||
description = request.description,
|
||||
thumbnail = request.thumbnail
|
||||
).also {
|
||||
themeRepository.save(it)
|
||||
}.toResponse()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteById(id: Long) {
|
||||
if (themeRepository.isReservedTheme(id)) {
|
||||
throw RoomescapeException(
|
||||
ErrorType.THEME_IS_USED_CONFLICT,
|
||||
"[themeId: %d]",
|
||||
HttpStatus.CONFLICT
|
||||
)
|
||||
}
|
||||
themeRepository.deleteById(id)
|
||||
}
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
package roomescape.theme.controller;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import roomescape.auth.web.support.Admin;
|
||||
import roomescape.auth.web.support.LoginRequired;
|
||||
import roomescape.common.dto.response.RoomescapeApiResponse;
|
||||
import roomescape.common.dto.response.RoomescapeErrorResponse;
|
||||
import roomescape.theme.dto.ThemeRequest;
|
||||
import roomescape.theme.dto.ThemeResponse;
|
||||
import roomescape.theme.dto.ThemesResponse;
|
||||
import roomescape.theme.service.ThemeService;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "5. 테마 API", description = "테마를 조회 / 추가 / 삭제할 때 사용합니다.")
|
||||
public class ThemeController {
|
||||
|
||||
private final ThemeService themeService;
|
||||
|
||||
public ThemeController(ThemeService themeService) {
|
||||
this.themeService = themeService;
|
||||
}
|
||||
|
||||
@LoginRequired
|
||||
@GetMapping("/themes")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "모든 테마 조회", description = "모든 테마를 조회합니다.", tags = "로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
|
||||
})
|
||||
public RoomescapeApiResponse<ThemesResponse> getAllThemes() {
|
||||
return RoomescapeApiResponse.success(themeService.findAllThemes());
|
||||
}
|
||||
|
||||
@GetMapping("/themes/most-reserved-last-week")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "가장 많이 예약된 테마 조회")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
|
||||
})
|
||||
public RoomescapeApiResponse<ThemesResponse> getMostReservedThemes(
|
||||
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") int count
|
||||
) {
|
||||
return RoomescapeApiResponse.success(themeService.getMostReservedThemesByCount(count));
|
||||
}
|
||||
|
||||
@Admin
|
||||
@PostMapping("/themes")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "테마 추가", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true),
|
||||
@ApiResponse(responseCode = "409", description = "같은 이름의 테마를 추가할 수 없습니다.",
|
||||
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
|
||||
})
|
||||
public RoomescapeApiResponse<ThemeResponse> saveTheme(
|
||||
@Valid @RequestBody ThemeRequest request,
|
||||
HttpServletResponse response
|
||||
) {
|
||||
ThemeResponse themeResponse = themeService.addTheme(request);
|
||||
response.setHeader(HttpHeaders.LOCATION, "/themes/" + themeResponse.id());
|
||||
|
||||
return RoomescapeApiResponse.success(themeResponse);
|
||||
}
|
||||
|
||||
@Admin
|
||||
@DeleteMapping("/themes/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Operation(summary = "테마 삭제", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true),
|
||||
@ApiResponse(responseCode = "409", description = "예약된 테마는 삭제할 수 없습니다.",
|
||||
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
|
||||
})
|
||||
public RoomescapeApiResponse<Void> removeTheme(
|
||||
@NotNull(message = "themeId는 null일 수 없습니다.") @PathVariable Long id
|
||||
) {
|
||||
themeService.removeThemeById(id);
|
||||
|
||||
return RoomescapeApiResponse.success();
|
||||
}
|
||||
}
|
||||
51
src/main/java/roomescape/theme/docs/ThemeAPI.kt
Normal file
51
src/main/java/roomescape/theme/docs/ThemeAPI.kt
Normal file
@ -0,0 +1,51 @@
|
||||
package roomescape.theme.docs
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import roomescape.auth.web.support.Admin
|
||||
import roomescape.auth.web.support.LoginRequired
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.theme.web.ThemeRequest
|
||||
import roomescape.theme.web.ThemeResponse
|
||||
import roomescape.theme.web.ThemesResponse
|
||||
|
||||
@Tag(name = "5. 테마 API", description = "테마를 조회 / 추가 / 삭제할 때 사용합니다.")
|
||||
interface ThemeAPI {
|
||||
|
||||
@LoginRequired
|
||||
@Operation(summary = "모든 테마 조회", description = "모든 테마를 조회합니다.", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun getAllThemes(): ResponseEntity<CommonApiResponse<ThemesResponse>>
|
||||
|
||||
@Operation(summary = "가장 많이 예약된 테마 조회")
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun getMostReservedThemes(
|
||||
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
|
||||
): ResponseEntity<CommonApiResponse<ThemesResponse>>
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true),
|
||||
)
|
||||
fun saveTheme(
|
||||
@Valid @RequestBody request: ThemeRequest,
|
||||
): ResponseEntity<CommonApiResponse<ThemeResponse>>
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true),
|
||||
)
|
||||
fun removeTheme(
|
||||
@PathVariable id: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>>
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
package roomescape.theme.domain;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
@Entity
|
||||
public class Theme {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
private String thumbnail;
|
||||
|
||||
protected Theme() {
|
||||
}
|
||||
|
||||
public Theme(String name, String description, String thumbnail) {
|
||||
this(null, name, description, thumbnail);
|
||||
}
|
||||
|
||||
public Theme(
|
||||
Long id,
|
||||
String name,
|
||||
String description,
|
||||
String thumbnail
|
||||
) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.thumbnail = thumbnail;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getThumbnail() {
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Theme{" +
|
||||
"id=" + id +
|
||||
", name=" + name +
|
||||
", description=" + description +
|
||||
", thumbnail=" + thumbnail +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
package roomescape.theme.domain.repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import roomescape.theme.domain.Theme;
|
||||
|
||||
public interface ThemeRepository extends JpaRepository<Theme, Long> {
|
||||
|
||||
@Query(value = """
|
||||
SELECT t
|
||||
FROM Theme t
|
||||
RIGHT JOIN Reservation r ON t.id = r.theme.id
|
||||
WHERE r.date BETWEEN :startDate AND :endDate
|
||||
GROUP BY r.theme.id
|
||||
ORDER BY COUNT(r.theme.id) DESC, t.id ASC
|
||||
LIMIT :limit
|
||||
""")
|
||||
List<Theme> findTopNThemeBetweenStartDateAndEndDate(LocalDate startDate, LocalDate endDate, int limit);
|
||||
|
||||
boolean existsByName(String name);
|
||||
|
||||
@Query(value = """
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM Reservation r
|
||||
WHERE r.theme.id = :id
|
||||
)
|
||||
""")
|
||||
boolean isReservedTheme(Long id);
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
package roomescape.theme.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
@Schema(name = "테마 저장 요청", description = "테마 정보를 저장할 때 사용합니다.")
|
||||
public record ThemeRequest(
|
||||
@NotBlank(message = "테마의 이름은 null 또는 공백일 수 없습니다.")
|
||||
@Size(min = 1, max = 20, message = "테마의 이름은 1~20글자 사이여야 합니다.")
|
||||
@Schema(description = "필수 값이며, 최대 20글자까지 입력 가능합니다.")
|
||||
String name,
|
||||
@NotBlank(message = "테마의 설명은 null 또는 공백일 수 없습니다.")
|
||||
@Size(min = 1, max = 100, message = "테마의 설명은 1~100글자 사이여야 합니다.")
|
||||
@Schema(description = "필수 값이며, 최대 100글자까지 입력 가능합니다.")
|
||||
String description,
|
||||
@NotBlank(message = "테마의 쌈네일은 null 또는 공백일 수 없습니다.")
|
||||
@Schema(description = "필수 값이며, 썸네일 이미지 URL 을 입력해주세요.")
|
||||
String thumbnail
|
||||
) {
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
package roomescape.theme.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import roomescape.theme.domain.Theme;
|
||||
|
||||
@Schema(name = "테마 정보", description = "테마 추가 및 조회 응답에 사용됩니다.")
|
||||
public record ThemeResponse(
|
||||
@Schema(description = "테마 번호. 테마를 식별할 때 사용합니다.")
|
||||
Long id,
|
||||
@Schema(description = "테마 이름. 중복을 허용하지 않습니다.")
|
||||
String name,
|
||||
@Schema(description = "테마 설명")
|
||||
String description,
|
||||
@Schema(description = "테마 썸네일 이미지 URL")
|
||||
String thumbnail
|
||||
) {
|
||||
|
||||
public static ThemeResponse from(Theme theme) {
|
||||
return new ThemeResponse(theme.getId(), theme.getName(), theme.getDescription(), theme.getThumbnail());
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package roomescape.theme.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(name = "테마 목록 조회 응답", description = "모든 테마 목록 조회 응답시 사용됩니다.")
|
||||
public record ThemesResponse(
|
||||
@Schema(description = "모든 테마 목록") List<ThemeResponse> themes
|
||||
) {
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package roomescape.theme.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "theme")
|
||||
class ThemeEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
|
||||
var name: String,
|
||||
var description: String,
|
||||
var thumbnail: String
|
||||
)
|
||||
@ -0,0 +1,32 @@
|
||||
package roomescape.theme.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import java.time.LocalDate
|
||||
|
||||
interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
|
||||
|
||||
@Query(value = """
|
||||
SELECT t
|
||||
FROM ThemeEntity t
|
||||
RIGHT JOIN Reservation r ON t.id = r.theme.id
|
||||
WHERE r.date BETWEEN :startDate AND :endDate
|
||||
GROUP BY r.theme.id
|
||||
ORDER BY COUNT(r.theme.id) DESC, t.id ASC
|
||||
LIMIT :limit
|
||||
|
||||
"""
|
||||
)
|
||||
fun findTopNThemeBetweenStartDateAndEndDate(startDate: LocalDate, endDate: LocalDate, limit: Int): List<ThemeEntity>
|
||||
|
||||
fun existsByName(name: String): Boolean
|
||||
|
||||
@Query(value = """
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM Reservation r
|
||||
WHERE r.theme.id = :id
|
||||
)
|
||||
""")
|
||||
fun isReservedTheme(id: Long): Boolean
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
package roomescape.theme.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.domain.repository.ThemeRepository;
|
||||
import roomescape.theme.dto.ThemeRequest;
|
||||
import roomescape.theme.dto.ThemeResponse;
|
||||
import roomescape.theme.dto.ThemesResponse;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class ThemeService {
|
||||
|
||||
private final ThemeRepository themeRepository;
|
||||
|
||||
public ThemeService(ThemeRepository themeRepository) {
|
||||
this.themeRepository = themeRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Theme findThemeById(Long id) {
|
||||
return themeRepository.findById(id)
|
||||
.orElseThrow(() -> new RoomescapeException(ErrorType.THEME_NOT_FOUND,
|
||||
String.format("[themeId: %d]", id), HttpStatus.BAD_REQUEST));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ThemesResponse findAllThemes() {
|
||||
List<ThemeResponse> response = themeRepository.findAll()
|
||||
.stream()
|
||||
.map(ThemeResponse::from)
|
||||
.toList();
|
||||
|
||||
return new ThemesResponse(response);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ThemesResponse getMostReservedThemesByCount(int count) {
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate startDate = today.minusDays(7);
|
||||
LocalDate endDate = today.minusDays(1);
|
||||
|
||||
List<ThemeResponse> response = themeRepository.findTopNThemeBetweenStartDateAndEndDate(startDate, endDate,
|
||||
count)
|
||||
.stream()
|
||||
.map(ThemeResponse::from)
|
||||
.toList();
|
||||
|
||||
return new ThemesResponse(response);
|
||||
}
|
||||
|
||||
public ThemeResponse addTheme(ThemeRequest request) {
|
||||
validateIsSameThemeNameExist(request.name());
|
||||
Theme theme = themeRepository.save(new Theme(request.name(), request.description(), request.thumbnail()));
|
||||
|
||||
return ThemeResponse.from(theme);
|
||||
}
|
||||
|
||||
private void validateIsSameThemeNameExist(String name) {
|
||||
if (themeRepository.existsByName(name)) {
|
||||
throw new RoomescapeException(ErrorType.THEME_DUPLICATED,
|
||||
String.format("[name: %s]", name), HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeThemeById(Long id) {
|
||||
if (themeRepository.isReservedTheme(id)) {
|
||||
throw new RoomescapeException(ErrorType.THEME_IS_USED_CONFLICT,
|
||||
String.format("[themeId: %d]", id), HttpStatus.CONFLICT);
|
||||
}
|
||||
themeRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
51
src/main/java/roomescape/theme/web/ThemeController.kt
Normal file
51
src/main/java/roomescape/theme/web/ThemeController.kt
Normal file
@ -0,0 +1,51 @@
|
||||
package roomescape.theme.web
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.theme.business.ThemeService
|
||||
import roomescape.theme.docs.ThemeAPI
|
||||
import java.net.URI
|
||||
|
||||
@RestController
|
||||
class ThemeController(
|
||||
private val themeService: ThemeService
|
||||
) : ThemeAPI {
|
||||
|
||||
@GetMapping("/themes")
|
||||
override fun getAllThemes(): ResponseEntity<CommonApiResponse<ThemesResponse>> {
|
||||
val response: ThemesResponse = themeService.findAllThemes()
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@GetMapping("/themes/most-reserved-last-week")
|
||||
override fun getMostReservedThemes(
|
||||
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
|
||||
): ResponseEntity<CommonApiResponse<ThemesResponse>> {
|
||||
val response: ThemesResponse = themeService.getMostReservedThemesByCount(count)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@PostMapping("/themes")
|
||||
override fun saveTheme(
|
||||
@RequestBody @Valid request: ThemeRequest
|
||||
): ResponseEntity<CommonApiResponse<ThemeResponse>> {
|
||||
val themeResponse: ThemeResponse = themeService.save(request)
|
||||
|
||||
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
|
||||
.body(CommonApiResponse(themeResponse))
|
||||
}
|
||||
|
||||
@DeleteMapping("/themes/{id}")
|
||||
override fun removeTheme(
|
||||
@PathVariable id: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||
themeService.deleteById(id)
|
||||
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
}
|
||||
68
src/main/java/roomescape/theme/web/ThemeDTO.kt
Normal file
68
src/main/java/roomescape/theme/web/ThemeDTO.kt
Normal file
@ -0,0 +1,68 @@
|
||||
package roomescape.theme.web
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import jakarta.validation.constraints.Size
|
||||
import org.hibernate.validator.constraints.URL
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
|
||||
@Schema(name = "테마 저장 요청", description = "테마 정보를 저장할 때 사용합니다.")
|
||||
@JvmRecord
|
||||
data class ThemeRequest(
|
||||
@field:Schema(description = "필수 값이며, 최대 20글자까지 입력 가능합니다.")
|
||||
@NotBlank
|
||||
@Size(max = 20, message = "테마의 이름은 1~20글자 사이여야 합니다.")
|
||||
val name: String,
|
||||
|
||||
@field:Schema(description = "필수 값이며, 최대 100글자까지 입력 가능합니다.")
|
||||
@NotBlank
|
||||
@Size(max = 100, message = "테마의 설명은 1~100글자 사이여야 합니다.")
|
||||
val description: String,
|
||||
|
||||
@field:Schema(description = "필수 값이며, 썸네일 이미지 URL 을 입력해주세요.")
|
||||
@NotBlank
|
||||
@URL
|
||||
val thumbnail: String
|
||||
)
|
||||
|
||||
@Schema(name = "테마 정보", description = "테마 추가 및 조회 응답에 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class ThemeResponse(
|
||||
@field:Schema(description = "테마 번호. 테마를 식별할 때 사용합니다.")
|
||||
val id: Long,
|
||||
|
||||
@field:Schema(description = "테마 이름. 중복을 허용하지 않습니다.")
|
||||
val name: String,
|
||||
|
||||
@field:Schema(description = "테마 설명")
|
||||
val description: String,
|
||||
|
||||
@field:Schema(description = "테마 썸네일 이미지 URL")
|
||||
val thumbnail: String
|
||||
) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(themeEntity: ThemeEntity): ThemeResponse {
|
||||
return ThemeResponse(themeEntity.id!!, themeEntity.name, themeEntity.description, themeEntity.thumbnail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ThemeEntity.toResponse(): ThemeResponse = ThemeResponse(
|
||||
id = this.id!!,
|
||||
name = this.name,
|
||||
description = this.description,
|
||||
thumbnail = this.thumbnail
|
||||
)
|
||||
|
||||
|
||||
@Schema(name = "테마 목록 조회 응답", description = "모든 테마 목록 조회 응답시 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class ThemesResponse(
|
||||
@field:Schema(description = "모든 테마 목록")
|
||||
val themes: List<ThemeResponse>
|
||||
)
|
||||
|
||||
fun List<ThemeEntity>.toResponse(): ThemesResponse = ThemesResponse(
|
||||
themes = this.map { it.toResponse()}
|
||||
)
|
||||
@ -27,8 +27,8 @@ import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.domain.repository.ThemeRepository;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@SpringBootTest
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
@ -57,7 +57,7 @@ class PaymentServiceTest {
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER));
|
||||
Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member,
|
||||
ReservationStatus.CONFIRMED));
|
||||
|
||||
@ -79,7 +79,7 @@ class PaymentServiceTest {
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER));
|
||||
Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member,
|
||||
ReservationStatus.CONFIRMED));
|
||||
|
||||
@ -116,7 +116,7 @@ class PaymentServiceTest {
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER));
|
||||
Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member,
|
||||
ReservationStatus.CONFIRMED));
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ package roomescape.reservation.controller;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ -12,7 +11,6 @@ import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@ -51,8 +49,8 @@ import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.reservation.dto.request.AdminReservationRequest;
|
||||
import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.request.WaitingRequest;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.domain.repository.ThemeRepository;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
@ -91,7 +89,7 @@ public class ReservationControllerTest {
|
||||
LocalDate date = LocalDate.now().plusDays(1L);
|
||||
|
||||
reservationTimeRepository.save(new ReservationTime(time));
|
||||
themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
|
||||
Map<String, String> reservationParams = Map.of(
|
||||
"date", date.toString(),
|
||||
@ -126,7 +124,7 @@ public class ReservationControllerTest {
|
||||
String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password");
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member1 = memberRepository.save(new Member(null, "name1", "email1r@email.com", "password", Role.MEMBER));
|
||||
|
||||
// when
|
||||
@ -154,7 +152,7 @@ public class ReservationControllerTest {
|
||||
String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password");
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member waitingMember = memberRepository.save(
|
||||
new Member(null, "name1", "email1r@email.com", "password", Role.MEMBER));
|
||||
|
||||
@ -181,7 +179,7 @@ public class ReservationControllerTest {
|
||||
String accessTokenCookie = getAdminAccessTokenCookieByLogin("admin@admin.com", "12341234");
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
// when
|
||||
@ -210,7 +208,7 @@ public class ReservationControllerTest {
|
||||
String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword());
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Reservation reservation = reservationRepository.save(
|
||||
new Reservation(LocalDate.now(), reservationTime, theme, member, ReservationStatus.CONFIRMED));
|
||||
|
||||
@ -230,7 +228,7 @@ public class ReservationControllerTest {
|
||||
String adminTokenCookie = getAdminAccessTokenCookieByLogin("admin@email.com", "password");
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member confirmedMember = memberRepository.save(
|
||||
new Member(null, "name1", "email@email.com", "password", Role.MEMBER));
|
||||
Member waitingMember = memberRepository.save(
|
||||
@ -258,7 +256,7 @@ public class ReservationControllerTest {
|
||||
String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword());
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member anotherMember = memberRepository.save(
|
||||
new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
@ -339,7 +337,7 @@ public class ReservationControllerTest {
|
||||
void getAllReservations(String requestURI, String responseFieldName, int expectedSize) {
|
||||
// given
|
||||
LocalDate date = LocalDate.now().plusDays(1);
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
ReservationTime time1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(18, 30)));
|
||||
ReservationTime time2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(19, 30)));
|
||||
@ -368,7 +366,7 @@ public class ReservationControllerTest {
|
||||
void removeNotPaidReservation() {
|
||||
// given
|
||||
LocalDate date = LocalDate.now().plusDays(1);
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password");
|
||||
|
||||
@ -392,7 +390,7 @@ public class ReservationControllerTest {
|
||||
// given
|
||||
String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password");
|
||||
LocalDate date = LocalDate.now().plusDays(1);
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
@ -423,7 +421,7 @@ public class ReservationControllerTest {
|
||||
LocalDateTime localDateTime = LocalDateTime.now().minusHours(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
String accessToken = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword());
|
||||
|
||||
@ -516,7 +514,7 @@ public class ReservationControllerTest {
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER));
|
||||
|
||||
@ -542,7 +540,7 @@ public class ReservationControllerTest {
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password");
|
||||
@ -587,7 +585,7 @@ public class ReservationControllerTest {
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
String adminAccessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password");
|
||||
|
||||
@ -29,8 +29,8 @@ import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.domain.repository.ThemeRepository;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
@ -224,7 +224,7 @@ public class ReservationTimeControllerTest {
|
||||
ReservationTime reservationTime1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 0)));
|
||||
ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
ReservationTime reservationTime3 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(18, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명1", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명1", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
reservationRepository.save(
|
||||
|
||||
@ -13,14 +13,14 @@ import org.junit.jupiter.params.provider.MethodSource;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
|
||||
public class ReservationTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("validateConstructorParameterBlankSource")
|
||||
@DisplayName("객체 생성 시, null 또는 공백이 존재하면 예외를 발생한다.")
|
||||
void validateConstructorParameterBlank(LocalDate date, ReservationTime reservationTime, Theme theme,
|
||||
void validateConstructorParameterBlank(LocalDate date, ReservationTime reservationTime, ThemeEntity theme,
|
||||
Member member) {
|
||||
|
||||
// when & then
|
||||
@ -33,12 +33,12 @@ public class ReservationTest {
|
||||
return Stream.of(
|
||||
Arguments.of(null,
|
||||
new ReservationTime(LocalTime.now().plusHours(1)),
|
||||
new Theme("테마명", "설명", "썸네일URI"),
|
||||
new ThemeEntity(null, "테마명", "설명", "썸네일URI"),
|
||||
new Member(null, "name", "email@email.com", "password", Role.MEMBER)),
|
||||
Arguments.of(
|
||||
LocalDate.now(),
|
||||
null,
|
||||
new Theme("테마명", "설명", "썸네일URI"),
|
||||
new ThemeEntity(null, "테마명", "설명", "썸네일URI"),
|
||||
new Member(null, "name", "email@email.com", "password", Role.MEMBER)),
|
||||
Arguments.of(
|
||||
LocalDate.now(),
|
||||
@ -48,7 +48,7 @@ public class ReservationTest {
|
||||
Arguments.of(
|
||||
LocalDate.now(),
|
||||
new ReservationTime(LocalTime.now().plusHours(1)),
|
||||
new Theme("테마명", "설명", "썸네일URI"),
|
||||
new ThemeEntity(null, "테마명", "설명", "썸네일URI"),
|
||||
null)
|
||||
);
|
||||
}
|
||||
|
||||
@ -19,8 +19,8 @@ import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.domain.repository.ThemeRepository;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@DataJpaTest
|
||||
class ReservationSearchSpecificationTest {
|
||||
@ -52,7 +52,7 @@ class ReservationSearchSpecificationTest {
|
||||
LocalDateTime dateTime = LocalDateTime.now();
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
ReservationTime time = timeRepository.save(new ReservationTime(dateTime.toLocalTime()));
|
||||
Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "description", "thumbnail"));
|
||||
|
||||
reservation1 = reservationRepository.save(
|
||||
new Reservation(dateTime.toLocalDate(), time, theme, member, ReservationStatus.CONFIRMED));
|
||||
|
||||
@ -27,9 +27,9 @@ import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.request.WaitingRequest;
|
||||
import roomescape.reservation.dto.response.ReservationResponse;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.domain.repository.ThemeRepository;
|
||||
import roomescape.theme.service.ThemeService;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
import roomescape.theme.business.ThemeService;
|
||||
|
||||
@SpringBootTest
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
@ -52,7 +52,7 @@ class ReservationServiceTest {
|
||||
void reservationAlreadyExistFail() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member1 = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
Member member2 = memberRepository.save(new Member(null, "name2", "email2@email.com", "password", Role.MEMBER));
|
||||
LocalDate date = LocalDate.now().plusDays(1L);
|
||||
@ -74,7 +74,7 @@ class ReservationServiceTest {
|
||||
void requestWaitWhenAlreadyReserveFail() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
LocalDate date = LocalDate.now().plusDays(1L);
|
||||
|
||||
@ -94,7 +94,7 @@ class ReservationServiceTest {
|
||||
void requestWaitTwiceFail() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER));
|
||||
LocalDate date = LocalDate.now().plusDays(1L);
|
||||
@ -118,7 +118,7 @@ class ReservationServiceTest {
|
||||
void beforeDateReservationFail() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
LocalDate beforeDate = LocalDate.now().minusDays(1L);
|
||||
|
||||
@ -135,7 +135,7 @@ class ReservationServiceTest {
|
||||
// given
|
||||
LocalDateTime beforeTime = LocalDateTime.now().minusHours(1L).withNano(0);
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(beforeTime.toLocalTime()));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
// when & then
|
||||
@ -151,7 +151,7 @@ class ReservationServiceTest {
|
||||
// given
|
||||
LocalDateTime beforeTime = LocalDateTime.now().minusDays(1L).withNano(0);
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(beforeTime.toLocalTime()));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Long NotExistMemberId = 1L;
|
||||
|
||||
// when & then
|
||||
@ -179,7 +179,7 @@ class ReservationServiceTest {
|
||||
void confirmWaitingWhenReservationExist() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member admin = memberRepository.save(new Member(null, "admin", "admin@email.com", "password", Role.ADMIN));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER));
|
||||
@ -202,7 +202,7 @@ class ReservationServiceTest {
|
||||
void approveWaiting() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member admin = memberRepository.save(new Member(null, "admin", "admin@email.com", "password", Role.ADMIN));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
|
||||
@ -23,8 +23,8 @@ import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.reservation.dto.request.ReservationTimeRequest;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.domain.repository.ThemeRepository;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@SpringBootTest
|
||||
@Import(ReservationTimeService.class)
|
||||
@ -74,7 +74,7 @@ class ReservationTimeServiceTest {
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(
|
||||
new ReservationTime(localDateTime.toLocalTime()));
|
||||
Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
// when
|
||||
|
||||
@ -28,8 +28,8 @@ import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.response.ReservationResponse;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.domain.repository.ThemeRepository;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@SpringBootTest
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
@ -60,7 +60,7 @@ class ReservationWithPaymentServiceTest {
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER));
|
||||
Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key",
|
||||
"order-id", 10000L, "NORMAL");
|
||||
|
||||
@ -96,7 +96,7 @@ class ReservationWithPaymentServiceTest {
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN));
|
||||
Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key",
|
||||
"order-id", 10000L, "NORMAL");
|
||||
|
||||
@ -124,7 +124,7 @@ class ReservationWithPaymentServiceTest {
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN));
|
||||
Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
|
||||
Reservation saved = reservationRepository.save(
|
||||
new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED));
|
||||
@ -146,7 +146,7 @@ class ReservationWithPaymentServiceTest {
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN));
|
||||
Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail"));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key",
|
||||
"order-id", 10000L, "NORMAL");
|
||||
|
||||
|
||||
103
src/test/java/roomescape/theme/business/ThemeServiceTest.kt
Normal file
103
src/test/java/roomescape/theme/business/ThemeServiceTest.kt
Normal file
@ -0,0 +1,103 @@
|
||||
package roomescape.theme.business
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.http.HttpStatus
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||
import roomescape.theme.web.ThemeRequest
|
||||
import roomescape.util.ThemeFixture
|
||||
|
||||
class ThemeServiceTest : FunSpec({
|
||||
|
||||
val themeRepository: ThemeRepository = mockk()
|
||||
val themeService = ThemeService(themeRepository)
|
||||
|
||||
context("findThemeById") {
|
||||
val themeId = 1L
|
||||
test("조회 성공") {
|
||||
val theme: ThemeEntity = ThemeFixture.create(id = themeId)
|
||||
every {
|
||||
themeRepository.findByIdOrNull(themeId)
|
||||
} returns theme
|
||||
|
||||
theme.id shouldBe themeId
|
||||
}
|
||||
|
||||
test("ID로 테마를 찾을 수 없으면 400 예외를 던진다.") {
|
||||
every {
|
||||
themeRepository.findByIdOrNull(themeId)
|
||||
} returns null
|
||||
|
||||
val exception = shouldThrow<RoomescapeException> {
|
||||
themeService.findThemeById(themeId)
|
||||
}
|
||||
|
||||
exception.errorType shouldBe ErrorType.THEME_NOT_FOUND
|
||||
}
|
||||
}
|
||||
|
||||
context("findAllThemes") {
|
||||
test("모든 테마를 조회한다.") {
|
||||
val themes = listOf(ThemeFixture.create(id = 1, name = "t1"), ThemeFixture.create(id = 2, name = "t2"))
|
||||
every {
|
||||
themeRepository.findAll()
|
||||
} returns themes
|
||||
|
||||
assertSoftly(themeService.findAllThemes()) {
|
||||
this.themes.size shouldBe themes.size
|
||||
this.themes[0].name shouldBe "t1"
|
||||
this.themes[1].name shouldBe "t2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("save") {
|
||||
test("테마 이름이 중복되면 409 예외를 던진다.") {
|
||||
val name = "Duplicate Theme"
|
||||
|
||||
every {
|
||||
themeRepository.existsByName(name)
|
||||
} returns true
|
||||
|
||||
val exception = shouldThrow<RoomescapeException> {
|
||||
themeService.save(ThemeRequest(
|
||||
name = name,
|
||||
description = "Description",
|
||||
thumbnail = "http://example.com/thumbnail.jpg"
|
||||
))
|
||||
}
|
||||
|
||||
assertSoftly(exception) {
|
||||
this.errorType shouldBe ErrorType.THEME_DUPLICATED
|
||||
this.httpStatus shouldBe HttpStatus.CONFLICT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("deleteById") {
|
||||
test("이미 예약 중인 테마라면 409 예외를 던진다.") {
|
||||
val themeId = 1L
|
||||
|
||||
every {
|
||||
themeRepository.isReservedTheme(themeId)
|
||||
} returns true
|
||||
|
||||
val exception = shouldThrow<RoomescapeException> {
|
||||
themeService.deleteById(themeId)
|
||||
}
|
||||
|
||||
assertSoftly(exception) {
|
||||
this.errorType shouldBe ErrorType.THEME_IS_USED_CONFLICT
|
||||
this.httpStatus shouldBe HttpStatus.CONFLICT
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,184 +0,0 @@
|
||||
package roomescape.theme.controller;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.http.Header;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
class ThemeControllerTest {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
private MemberRepository memberRepository;
|
||||
|
||||
@Test
|
||||
@DisplayName("모든 테마 정보를 조회한다.")
|
||||
void readThemes() {
|
||||
String email = "admin@test.com";
|
||||
String password = "12341234";
|
||||
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password);
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.port(port)
|
||||
.when().get("/themes")
|
||||
.then().log().all()
|
||||
.statusCode(200)
|
||||
.body("data.themes.size()", is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("테마를 추가한다.")
|
||||
void createThemes() {
|
||||
String email = "admin@test.com";
|
||||
String password = "12341234";
|
||||
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password);
|
||||
|
||||
Map<String, String> params = Map.of(
|
||||
"name", "테마명",
|
||||
"description", "설명",
|
||||
"thumbnail", "http://testsfasdgasd.com"
|
||||
);
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.port(port)
|
||||
.body(params)
|
||||
.when().post("/themes")
|
||||
.then().log().all()
|
||||
.statusCode(201)
|
||||
.body("data.id", is(1))
|
||||
.header("Location", "/themes/1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("테마를 삭제한다.")
|
||||
void deleteThemes() {
|
||||
String email = "admin@test.com";
|
||||
String password = "12341234";
|
||||
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password);
|
||||
|
||||
Map<String, String> params = Map.of(
|
||||
"name", "테마명",
|
||||
"description", "설명",
|
||||
"thumbnail", "http://testsfasdgasd.com"
|
||||
);
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.port(port)
|
||||
.body(params)
|
||||
.when().post("/themes")
|
||||
.then().log().all()
|
||||
.statusCode(201)
|
||||
.body("data.id", is(1))
|
||||
.header("Location", "/themes/1");
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.port(port)
|
||||
.when().delete("/themes/1")
|
||||
.then().log().all()
|
||||
.statusCode(204);
|
||||
}
|
||||
|
||||
/*
|
||||
* reservationData DataSet ThemeID 별 reservation 개수
|
||||
* 5,4,2,5,2,3,1,1,1,1,1
|
||||
* 예약 수 내림차순 + ThemeId 오름차순 정렬 순서
|
||||
* 1, 4, 2, 6, 3, 5, 7, 8, 9, 10
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("예약 수 상위 10개 테마를 조회했을 때 내림차순으로 정렬된다. 만약 예약 수가 같다면, id 순으로 오름차순 정렬된다.")
|
||||
@Sql(scripts = {"/truncate.sql", "/reservationData.sql"}, executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
void readTop10ThemesDescOrder() {
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.when().get("/themes/most-reserved-last-week?count=10")
|
||||
.then().log().all()
|
||||
.statusCode(200)
|
||||
.body("data.themes.size()", is(10))
|
||||
.body("data.themes.id", contains(1, 4, 2, 6, 3, 5, 7, 8, 9, 10));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("requestValidateSource")
|
||||
@DisplayName("테마 생성 시, 요청 값에 공백 또는 null이 포함되어 있으면 400 에러를 발생한다.")
|
||||
void validateBlankRequest(Map<String, String> invalidRequestBody) {
|
||||
String email = "admin@test.com";
|
||||
String password = "12341234";
|
||||
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password);
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.port(port)
|
||||
.body(invalidRequestBody)
|
||||
.when().post("/themes")
|
||||
.then().log().all()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
static Stream<Map<String, String>> requestValidateSource() {
|
||||
return Stream.of(
|
||||
Map.of(
|
||||
"name", "테마명",
|
||||
"thumbnail", "http://testsfasdgasd.com"
|
||||
),
|
||||
Map.of(
|
||||
"name", "",
|
||||
"description", "설명",
|
||||
"thumbnail", "http://testsfasdgasd.com"
|
||||
),
|
||||
Map.of(
|
||||
"name", " ",
|
||||
"description", "설명",
|
||||
"thumbnail", "http://testsfasdgasd.com"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private String getAdminAccessTokenCookieByLogin(final String email, final String password) {
|
||||
memberRepository.save(new Member(null, "이름", email, password, Role.ADMIN));
|
||||
|
||||
Map<String, String> loginParams = Map.of(
|
||||
"email", email,
|
||||
"password", password
|
||||
);
|
||||
|
||||
String accessToken = RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.body(loginParams)
|
||||
.when().post("/login")
|
||||
.then().log().all().extract().cookie("accessToken");
|
||||
|
||||
return "accessToken=" + accessToken;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
package roomescape.theme.infrastructure.persistence
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.collections.shouldContainInOrder
|
||||
import io.kotest.matchers.shouldBe
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import roomescape.theme.util.TestThemeCreateUtil
|
||||
import java.time.LocalDate
|
||||
|
||||
@DataJpaTest
|
||||
class ThemeRepositoryTest(
|
||||
val themeRepository: ThemeRepository,
|
||||
val entityManager: EntityManager
|
||||
) : FunSpec() {
|
||||
|
||||
init {
|
||||
context("findTopNThemeBetweenStartDateAndEndDate") {
|
||||
beforeTest {
|
||||
for (i in 1..10) {
|
||||
TestThemeCreateUtil.createThemeWithReservations(
|
||||
entityManager = entityManager,
|
||||
name = "테마$i",
|
||||
reservedCount = i,
|
||||
date = LocalDate.now().minusDays(i.toLong()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회한다.") {
|
||||
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
|
||||
LocalDate.now().minusDays(10),
|
||||
LocalDate.now().minusDays(1),
|
||||
5
|
||||
).also { themes ->
|
||||
themes.size shouldBe 5
|
||||
themes.map { it.name } shouldContainInOrder listOf(
|
||||
"테마10", "테마9", "테마8", "테마7", "테마6"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회한다.") {
|
||||
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
|
||||
LocalDate.now().minusDays(8),
|
||||
LocalDate.now().minusDays(5),
|
||||
3
|
||||
).also { themes ->
|
||||
themes.size shouldBe 3
|
||||
themes.map { it.name } shouldContainInOrder listOf(
|
||||
"테마8", "테마7", "테마6"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test("예약 수가 동일하면 먼저 생성된 테마를 우선 조회한다.") {
|
||||
TestThemeCreateUtil.createThemeWithReservations(
|
||||
entityManager = entityManager,
|
||||
name = "테마11",
|
||||
reservedCount = 5,
|
||||
date = LocalDate.now().minusDays(5),
|
||||
)
|
||||
|
||||
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
|
||||
LocalDate.now().minusDays(6),
|
||||
LocalDate.now().minusDays(4),
|
||||
5
|
||||
).also { themes ->
|
||||
themes.size shouldBe 4
|
||||
themes.map { it.name } shouldContainInOrder listOf(
|
||||
"테마6", "테마5", "테마11", "테마4"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환한다.") {
|
||||
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
|
||||
LocalDate.now().minusDays(10),
|
||||
LocalDate.now().minusDays(6),
|
||||
10
|
||||
).also { themes ->
|
||||
themes.size shouldBe 5
|
||||
}
|
||||
}
|
||||
|
||||
test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환한다.") {
|
||||
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
|
||||
LocalDate.now().minusDays(10),
|
||||
LocalDate.now().minusDays(1),
|
||||
15
|
||||
).also { themes ->
|
||||
themes.size shouldBe 10
|
||||
}
|
||||
}
|
||||
|
||||
test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트를 반환한다.") {
|
||||
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
|
||||
LocalDate.now().plusDays(1),
|
||||
LocalDate.now().plusDays(10),
|
||||
5
|
||||
).also { themes ->
|
||||
themes.size shouldBe 0
|
||||
}
|
||||
}
|
||||
}
|
||||
context("existsByName ") {
|
||||
val themeName = "test-theme"
|
||||
beforeTest {
|
||||
TestThemeCreateUtil.createThemeWithReservations(
|
||||
entityManager = entityManager,
|
||||
name = themeName,
|
||||
reservedCount = 0,
|
||||
date = LocalDate.now()
|
||||
)
|
||||
}
|
||||
test("테마 이름이 존재하면 true를 반환한다.") {
|
||||
themeRepository.existsByName(themeName) shouldBe true
|
||||
}
|
||||
|
||||
test("테마 이름이 존재하지 않으면 false를 반환한다.") {
|
||||
themeRepository.existsByName(themeName.repeat(2)) shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
context("isReservedTheme") {
|
||||
test("테마가 예약 중이면 true를 반환한다.") {
|
||||
val theme = TestThemeCreateUtil.createThemeWithReservations(
|
||||
entityManager = entityManager,
|
||||
name = "예약된 테마",
|
||||
reservedCount = 1,
|
||||
date = LocalDate.now()
|
||||
)
|
||||
themeRepository.isReservedTheme(theme.id!!) shouldBe true
|
||||
}
|
||||
|
||||
test("테마가 예약 중이 아니면 false를 반환한다.") {
|
||||
val theme = TestThemeCreateUtil.createThemeWithReservations(
|
||||
entityManager = entityManager,
|
||||
name = "예약되지 않은 테마",
|
||||
reservedCount = 0,
|
||||
date = LocalDate.now()
|
||||
)
|
||||
themeRepository.isReservedTheme(theme.id!!) shouldBe false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,164 +0,0 @@
|
||||
package roomescape.theme.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
|
||||
import roomescape.member.business.MemberService;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.request.ReservationTimeRequest;
|
||||
import roomescape.reservation.dto.response.ReservationTimeResponse;
|
||||
import roomescape.reservation.service.ReservationService;
|
||||
import roomescape.reservation.service.ReservationTimeService;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.domain.repository.ThemeRepository;
|
||||
import roomescape.theme.dto.ThemeRequest;
|
||||
import roomescape.theme.dto.ThemeResponse;
|
||||
import roomescape.theme.dto.ThemesResponse;
|
||||
|
||||
@DataJpaTest
|
||||
@Import({ReservationTimeService.class, ReservationService.class, MemberService.class, ThemeService.class})
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
class ThemeServiceTest {
|
||||
|
||||
@Autowired
|
||||
private ThemeRepository themeRepository;
|
||||
|
||||
@Autowired
|
||||
private ThemeService themeService;
|
||||
|
||||
@Autowired
|
||||
private ReservationTimeService reservationTimeService;
|
||||
|
||||
@Autowired
|
||||
private MemberRepository memberRepository;
|
||||
|
||||
@Autowired
|
||||
private ReservationService reservationService;
|
||||
|
||||
@Test
|
||||
@DisplayName("테마를 조회한다.")
|
||||
void findThemeById() {
|
||||
// given
|
||||
Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail"));
|
||||
|
||||
// when
|
||||
Theme foundTheme = themeService.findThemeById(theme.getId());
|
||||
|
||||
// then
|
||||
assertThat(foundTheme).isEqualTo(theme);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("존재하지 않는 ID로 테마를 조회하면 예외가 발생한다.")
|
||||
void findThemeByNotExistId() {
|
||||
// given
|
||||
Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail"));
|
||||
|
||||
// when
|
||||
Long notExistId = theme.getId() + 1;
|
||||
|
||||
// then
|
||||
assertThatThrownBy(() -> themeService.findThemeById(notExistId))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("모든 테마를 조회한다.")
|
||||
void findAllThemes() {
|
||||
// given
|
||||
Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail"));
|
||||
Theme theme1 = themeRepository.save(new Theme("name1", "description1", "thumbnail1"));
|
||||
|
||||
// when
|
||||
ThemesResponse found = themeService.findAllThemes();
|
||||
|
||||
// then
|
||||
assertThat(found.themes()).extracting("id").containsExactly(theme.getId(), theme1.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약 수 상위 10개 테마를 조회했을 때 내림차순으로 정렬된다. 만약 예약 수가 같다면, id 순으로 오름차순 정렬된다.")
|
||||
@Sql({"/truncate.sql", "/reservationData.sql"})
|
||||
void getMostReservedThemesByCount() {
|
||||
// given
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
// when
|
||||
List<ThemeResponse> found = themeService.getMostReservedThemesByCount(10).themes();
|
||||
|
||||
// then : 11번 테마는 조회되지 않아야 한다.
|
||||
assertThat(found).extracting("id").containsExactly(1L, 4L, 2L, 6L, 3L, 5L, 7L, 8L, 9L, 10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("테마를 추가한다.")
|
||||
void addTheme() {
|
||||
// given
|
||||
ThemeResponse themeResponse = themeService.addTheme(new ThemeRequest("name", "description", "thumbnail"));
|
||||
|
||||
// when
|
||||
Theme found = themeRepository.findById(themeResponse.id()).orElse(null);
|
||||
|
||||
// then
|
||||
assertThat(found).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("테마를 추가할 때 같은 이름의 테마가 존재하면 예외가 발생한다. ")
|
||||
void addDuplicateTheme() {
|
||||
// given
|
||||
ThemeResponse themeResponse = themeService.addTheme(new ThemeRequest("name", "description", "thumbnail"));
|
||||
|
||||
// when
|
||||
ThemeRequest invalidRequest = new ThemeRequest(themeResponse.name(), "description", "thumbnail");
|
||||
|
||||
// then
|
||||
assertThatThrownBy(() -> themeService.addTheme(invalidRequest))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("테마를 삭제한다.")
|
||||
void removeThemeById() {
|
||||
// given
|
||||
Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail"));
|
||||
|
||||
// when
|
||||
themeService.removeThemeById(theme.getId());
|
||||
|
||||
// then
|
||||
assertThat(themeRepository.findById(theme.getId())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약이 존재하는 테마를 삭제하면 예외가 발생한다.")
|
||||
void removeReservedTheme() {
|
||||
// given
|
||||
LocalDateTime dateTime = LocalDateTime.now().plusDays(1);
|
||||
ReservationTimeResponse time = reservationTimeService.addTime(
|
||||
new ReservationTimeRequest(dateTime.toLocalTime()));
|
||||
Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail"));
|
||||
Member member = memberRepository.save(new Member(null, "member", "password", "name", Role.MEMBER));
|
||||
reservationService.addReservation(
|
||||
new ReservationRequest(dateTime.toLocalDate(), time.id(), theme.getId(), "paymentKey", "orderId", 1000L,
|
||||
"NORMAL"), member.getId());
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> themeService.removeThemeById(theme.getId()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
}
|
||||
44
src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt
Normal file
44
src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt
Normal file
@ -0,0 +1,44 @@
|
||||
package roomescape.theme.util
|
||||
|
||||
import jakarta.persistence.EntityManager
|
||||
import roomescape.member.infrastructure.persistence.Member
|
||||
import roomescape.reservation.domain.ReservationStatus
|
||||
import roomescape.reservation.domain.ReservationTime
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.util.MemberFixture
|
||||
import roomescape.util.ReservationFixture
|
||||
import roomescape.util.ReservationTimeFixture
|
||||
import roomescape.util.ThemeFixture
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
object TestThemeCreateUtil {
|
||||
fun createThemeWithReservations(
|
||||
entityManager: EntityManager,
|
||||
name: String,
|
||||
reservedCount: Int,
|
||||
date: LocalDate,
|
||||
): ThemeEntity {
|
||||
val themeEntity: ThemeEntity = ThemeFixture.create(name = name).also { entityManager.persist(it) }
|
||||
val member: Member = MemberFixture.create().also { entityManager.persist(it) }
|
||||
|
||||
for (i in 1..reservedCount) {
|
||||
val time: ReservationTime = ReservationTimeFixture.create(
|
||||
startAt = LocalTime.now().plusMinutes(i.toLong())
|
||||
).also { entityManager.persist(it) }
|
||||
|
||||
ReservationFixture.create(
|
||||
date = date,
|
||||
themeEntity = themeEntity,
|
||||
member = member,
|
||||
reservationTime = time,
|
||||
status = ReservationStatus.CONFIRMED
|
||||
).also { entityManager.persist(it) }
|
||||
}
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
return themeEntity
|
||||
}
|
||||
}
|
||||
113
src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt
Normal file
113
src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt
Normal file
@ -0,0 +1,113 @@
|
||||
package roomescape.theme.web
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.restassured.module.kotlin.extensions.Given
|
||||
import io.restassured.module.kotlin.extensions.Then
|
||||
import io.restassured.module.kotlin.extensions.When
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.web.server.LocalServerPort
|
||||
import org.springframework.test.context.jdbc.Sql
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import roomescape.theme.business.ThemeService
|
||||
import roomescape.theme.util.TestThemeCreateUtil
|
||||
import java.time.LocalDate
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* GET /themes/most-reserved-last-week API 테스트
|
||||
* 상세 테스트는 Repository 테스트에서 진행
|
||||
* 날짜 범위, 예약 수만 검증
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@Sql(value = ["/truncate.sql"], executionPhase = Sql.ExecutionPhase.AFTER_TEST_CLASS)
|
||||
class MostReservedThemeAPITest(
|
||||
@LocalServerPort val port: Int,
|
||||
val themeService: ThemeService,
|
||||
val transactionTemplate: TransactionTemplate,
|
||||
val entityManager: EntityManager,
|
||||
) : FunSpec() {
|
||||
init {
|
||||
beforeSpec {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
// 지난 7일간 예약된 테마 10개 생성
|
||||
for (i in 1..10) {
|
||||
TestThemeCreateUtil.createThemeWithReservations(
|
||||
entityManager = entityManager,
|
||||
name = "테마$i",
|
||||
reservedCount = 1,
|
||||
date = LocalDate.now().minusDays(Random.nextLong(1, 7))
|
||||
)
|
||||
}
|
||||
|
||||
// 8일 전 예약된 테마 1개 생성
|
||||
TestThemeCreateUtil.createThemeWithReservations(
|
||||
entityManager = entityManager,
|
||||
name = "테마11",
|
||||
reservedCount = 1,
|
||||
date = LocalDate.now().minusDays(8)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
context("가장 많이 예약된 테마를 조회할 때, ") {
|
||||
val endpoint = "/themes/most-reserved-last-week"
|
||||
test("갯수를 입력하지 않으면 10개를 반환한다.") {
|
||||
Given {
|
||||
port(port)
|
||||
} When {
|
||||
get(endpoint)
|
||||
} Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
body("data.themes.size()", equalTo(10))
|
||||
}
|
||||
}
|
||||
|
||||
test("입력된 갯수가 조회된 갯수보다 크면 조회된 갯수만큼 반환한다.") {
|
||||
val count = 15
|
||||
Given {
|
||||
port(port)
|
||||
} When {
|
||||
param("count", count)
|
||||
get("/themes/most-reserved-last-week")
|
||||
} Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
body("data.themes.size()", equalTo(10))
|
||||
}
|
||||
}
|
||||
|
||||
test("입력된 갯수가 조회된 갯수보다 작으면 입력된 갯수만큼 반환한다.") {
|
||||
val count = 5
|
||||
Given {
|
||||
port(port)
|
||||
} When {
|
||||
param("count", count)
|
||||
get("/themes/most-reserved-last-week")
|
||||
} Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
body("data.themes.size()", equalTo(count))
|
||||
}
|
||||
}
|
||||
|
||||
test("7일 전 부터 1일 전 까지 예약된 테마를 대상으로 한다.") {
|
||||
// 현재 저장된 데이터는 지난 7일간 예약된 테마 10개와 8일 전 예약된 테마 1개
|
||||
// 8일 전 예약된 테마는 제외되어야 하므로, 10개가 조회되어야 한다.
|
||||
val count = 11
|
||||
Given {
|
||||
port(port)
|
||||
} When {
|
||||
param("count", count)
|
||||
get("/themes/most-reserved-last-week")
|
||||
} Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
body("data.themes.size()", equalTo(10))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
307
src/test/java/roomescape/theme/web/ThemeControllerTest.kt
Normal file
307
src/test/java/roomescape/theme/web/ThemeControllerTest.kt
Normal file
@ -0,0 +1,307 @@
|
||||
package roomescape.theme.web
|
||||
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import com.ninjasquad.springmockk.SpykBean
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.matchers.collections.shouldContainAll
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.runs
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import roomescape.theme.business.ThemeService
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||
import roomescape.util.RoomescapeApiTest
|
||||
import roomescape.util.ThemeFixture
|
||||
|
||||
@WebMvcTest(ThemeController::class)
|
||||
class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
||||
|
||||
@SpykBean
|
||||
private lateinit var themeService: ThemeService
|
||||
|
||||
@MockkBean
|
||||
private lateinit var themeRepository: ThemeRepository
|
||||
|
||||
init {
|
||||
Given("모든 테마를 조회할 때") {
|
||||
val endpoint = "/themes"
|
||||
|
||||
When("로그인 상태가 아니라면") {
|
||||
doNotLogin()
|
||||
|
||||
Then("로그인 페이지로 이동한다.") {
|
||||
runGetTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
log = true
|
||||
) {
|
||||
status { is3xxRedirection() }
|
||||
header {
|
||||
string("Location", "/login")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
When("로그인 상태라면") {
|
||||
loginAsUser()
|
||||
|
||||
Then("조회에 성공한다.") {
|
||||
every {
|
||||
themeRepository.findAll()
|
||||
} returns listOf(
|
||||
ThemeFixture.create(id = 1, name = "theme1"),
|
||||
ThemeFixture.create(id = 2, name = "theme2"),
|
||||
ThemeFixture.create(id = 3, name = "theme3")
|
||||
)
|
||||
|
||||
val response: ThemesResponse = runGetTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
log = true
|
||||
) {
|
||||
status { isOk() }
|
||||
content {
|
||||
contentType(MediaType.APPLICATION_JSON)
|
||||
}
|
||||
}.andReturn().readValue(ThemesResponse::class.java)
|
||||
|
||||
assertSoftly(response.themes) {
|
||||
it.size shouldBe 3
|
||||
it.map { m -> m.name } shouldContainAll listOf("theme1", "theme2", "theme3")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Given("테마를 추가할 때") {
|
||||
val endpoint = "/themes"
|
||||
val request = ThemeRequest(
|
||||
name = "theme1",
|
||||
description = "description1",
|
||||
thumbnail = "http://example.com/thumbnail1.jpg"
|
||||
)
|
||||
|
||||
When("로그인 상태가 아니라면") {
|
||||
doNotLogin()
|
||||
Then("로그인 페이지로 이동한다.") {
|
||||
runPostTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
body = request,
|
||||
log = true
|
||||
) {
|
||||
status { is3xxRedirection() }
|
||||
header {
|
||||
string("Location", "/login")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
When("관리자가 아닌 회원은") {
|
||||
loginAsUser()
|
||||
Then("로그인 페이지로 이동한다.") {
|
||||
runPostTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
body = request,
|
||||
log = true
|
||||
) {
|
||||
status { is3xxRedirection() }
|
||||
jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
When("동일한 이름의 테마가 있으면") {
|
||||
loginAsAdmin()
|
||||
|
||||
Then("409 에러를 응답한다.") {
|
||||
every {
|
||||
themeRepository.existsByName(request.name)
|
||||
} returns true
|
||||
|
||||
runPostTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
body = request,
|
||||
log = true
|
||||
) {
|
||||
status { isConflict() }
|
||||
jsonPath("$.errorType") { value("THEME_DUPLICATED") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
When("값이 잘못 입력되면 400 에러를 응답한다") {
|
||||
beforeTest {
|
||||
loginAsAdmin()
|
||||
}
|
||||
|
||||
val request = ThemeRequest(
|
||||
name = "theme1",
|
||||
description = "description1",
|
||||
thumbnail = "http://example.com/thumbnail1.jpg"
|
||||
)
|
||||
|
||||
fun runTest(request: ThemeRequest) {
|
||||
runPostTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
body = request,
|
||||
log = true
|
||||
) {
|
||||
status { isBadRequest() }
|
||||
}
|
||||
}
|
||||
|
||||
Then("이름이 공백인 경우") {
|
||||
val invalidRequest = request.copy(name = " ")
|
||||
runTest(invalidRequest)
|
||||
}
|
||||
|
||||
Then("이름이 20글자를 초과하는 경우") {
|
||||
val invalidRequest = request.copy(name = "a".repeat(21))
|
||||
runTest(invalidRequest)
|
||||
}
|
||||
|
||||
Then("설명이 공백인 경우") {
|
||||
val invalidRequest = request.copy(description = " ")
|
||||
runTest(invalidRequest)
|
||||
}
|
||||
|
||||
Then("설명이 100글자를 초과하는 경우") {
|
||||
val invalidRequest = request.copy(description = "a".repeat(101))
|
||||
runTest(invalidRequest)
|
||||
}
|
||||
|
||||
Then("썸네일이 공백인 경우") {
|
||||
val invalidRequest = request.copy(thumbnail = " ")
|
||||
runTest(invalidRequest)
|
||||
}
|
||||
|
||||
Then("썸네일이 URL 형식이 아닌 경우") {
|
||||
val invalidRequest = request.copy(thumbnail = "invalid-url")
|
||||
runTest(invalidRequest)
|
||||
}
|
||||
}
|
||||
|
||||
When("저장에 성공하면") {
|
||||
loginAsAdmin()
|
||||
|
||||
val theme = ThemeFixture.create(
|
||||
id = 1,
|
||||
name = request.name,
|
||||
description = request.description,
|
||||
thumbnail = request.thumbnail
|
||||
)
|
||||
|
||||
every {
|
||||
themeRepository.existsByName(request.name)
|
||||
} returns false
|
||||
|
||||
every {
|
||||
themeRepository.save(any())
|
||||
} returns theme
|
||||
|
||||
Then("201 응답을 받는다.") {
|
||||
runPostTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
body = request,
|
||||
log = true
|
||||
) {
|
||||
status { isCreated() }
|
||||
header {
|
||||
string("Location", "/themes/${theme.id}")
|
||||
}
|
||||
jsonPath("$.data.id") { value(theme.id) }
|
||||
jsonPath("$.data.name") { value(theme.name) }
|
||||
jsonPath("$.data.description") { value(theme.description) }
|
||||
jsonPath("$.data.thumbnail") { value(theme.thumbnail) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Given("테마를 제거할 때") {
|
||||
val themeId = 1L
|
||||
val endpoint = "/themes/$themeId"
|
||||
|
||||
When("로그인 상태가 아니라면") {
|
||||
doNotLogin()
|
||||
Then("로그인 페이지로 이동한다.") {
|
||||
runDeleteTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
log = true
|
||||
) {
|
||||
status { is3xxRedirection() }
|
||||
header {
|
||||
string("Location", "/login")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
When("관리자가 아닌 회원은") {
|
||||
loginAsUser()
|
||||
Then("로그인 페이지로 이동한다.") {
|
||||
runDeleteTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
log = true
|
||||
) {
|
||||
status { is3xxRedirection() }
|
||||
jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
When("입력된 ID에 해당하는 테마가 없으면") {
|
||||
loginAsAdmin()
|
||||
|
||||
Then("409 에러를 응답한다.") {
|
||||
every {
|
||||
themeRepository.isReservedTheme(themeId)
|
||||
} returns true
|
||||
|
||||
runDeleteTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
log = true
|
||||
) {
|
||||
status { isConflict() }
|
||||
jsonPath("$.errorType") { value("THEME_IS_USED_CONFLICT") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
When("정상적으로 제거되면") {
|
||||
loginAsAdmin()
|
||||
|
||||
every {
|
||||
themeRepository.isReservedTheme(themeId)
|
||||
} returns false
|
||||
|
||||
every {
|
||||
themeRepository.deleteById(themeId)
|
||||
} just runs
|
||||
|
||||
Then("204 응답을 받는다.") {
|
||||
runDeleteTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
log = true
|
||||
) {
|
||||
status { isNoContent() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@ import roomescape.payment.web.PaymentCancel
|
||||
import roomescape.reservation.domain.Reservation
|
||||
import roomescape.reservation.domain.ReservationStatus
|
||||
import roomescape.reservation.domain.ReservationTime
|
||||
import roomescape.theme.domain.Theme
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.OffsetDateTime
|
||||
@ -62,18 +62,18 @@ object ThemeFixture {
|
||||
name: String = "Default Theme",
|
||||
description: String = "Default Description",
|
||||
thumbnail: String = "https://example.com/default-thumbnail.jpg"
|
||||
): Theme = Theme(id, name, description, thumbnail)
|
||||
): ThemeEntity = ThemeEntity(id, name, description, thumbnail)
|
||||
}
|
||||
|
||||
object ReservationFixture {
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
date: LocalDate = LocalDate.now().plusWeeks(1),
|
||||
theme: Theme = ThemeFixture.create(),
|
||||
themeEntity: ThemeEntity = ThemeFixture.create(),
|
||||
reservationTime: ReservationTime = ReservationTimeFixture.create(),
|
||||
member: Member = MemberFixture.create(),
|
||||
status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
): Reservation = Reservation(id, date, reservationTime, theme, member, status)
|
||||
): Reservation = Reservation(id, date, reservationTime, themeEntity, member, status)
|
||||
}
|
||||
|
||||
object JwtFixture {
|
||||
|
||||
@ -55,9 +55,7 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
|
||||
header(HttpHeaders.COOKIE, "accessToken=token")
|
||||
}.apply {
|
||||
log.takeIf { it }?.let { this.andDo { print() } }
|
||||
}.andExpect {
|
||||
assert
|
||||
}
|
||||
}.andExpect(assert)
|
||||
|
||||
fun runPostTest(
|
||||
mockMvc: MockMvc,
|
||||
@ -77,6 +75,19 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
|
||||
assert
|
||||
}
|
||||
|
||||
fun runDeleteTest(
|
||||
mockMvc: MockMvc,
|
||||
endpoint: String,
|
||||
log: Boolean = false,
|
||||
assert: MockMvcResultMatchersDsl.() -> Unit
|
||||
): ResultActionsDsl = mockMvc.delete(endpoint) {
|
||||
header(HttpHeaders.COOKIE, "accessToken=token")
|
||||
}.apply {
|
||||
log.takeIf { it }?.let { this.andDo { print() } }
|
||||
}.andExpect {
|
||||
assert
|
||||
}
|
||||
|
||||
fun loginAsAdmin() {
|
||||
every {
|
||||
jwtHandler.getMemberIdFromToken(any())
|
||||
@ -104,6 +115,13 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
|
||||
every { memberRepository.findByIdOrNull(NOT_LOGGED_IN_USERID) } returns null
|
||||
}
|
||||
|
||||
fun <T> MvcResult.readValue(valueType: Class<T>): T = this.response.contentAsString
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let { readValue(it, valueType) }
|
||||
?: throw RuntimeException("""
|
||||
[Test] Exception occurred while reading response json: ${this.response.contentAsString} with value type: $valueType
|
||||
""".trimIndent())
|
||||
|
||||
fun <T> readValue(responseJson: String, valueType: Class<T>): T = objectMapper
|
||||
.readTree(responseJson)["data"]
|
||||
?.let { objectMapper.convertValue(it, valueType) }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user