diff --git a/.gitignore b/.gitignore index 186687b..3fc7688 100644 --- a/.gitignore +++ b/.gitignore @@ -189,3 +189,4 @@ gradle-app.setting docker/data src/main/resources/application.yml +dev.application.yml diff --git a/build.gradle.kts b/build.gradle.kts index bbb67de..eb066bb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,8 +36,8 @@ dependencies { // r2dbc implementation("org.springframework.boot:spring-boot-starter-data-r2dbc:3.0.4") - implementation("org.postgresql:r2dbc-postgresql:1.0.1.RELEASE") - runtimeOnly("org.postgresql:postgresql") + implementation("org.postgresql:r2dbc-postgresql:1.0.7.RELEASE") + implementation("org.postgresql:postgresql:42.7.4") // feign client implementation("org.springframework.cloud:spring-cloud-starter-openfeign") @@ -50,7 +50,7 @@ dependencies { implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") // validation - implementation("org.hibernate.validator:hibernate-validator:8.0.0.Final") + implementation("org.springframework.boot:spring-boot-starter-validation") // jwt implementation("io.jsonwebtoken:jjwt-api:$jwtVersion") @@ -93,6 +93,10 @@ dependencies { jooqCodegen("org.jooq:jooq-meta-kotlin:${jooqVersion}") // workaround of array type codegen, see: https://github.com/jOOQ/jOOQ/issues/13322 jooqCodegen("com.h2database:h2:2.3.232") + + // flyway + implementation("org.flywaydb:flyway-core:10.22.0") + implementation("org.flywaydb:flyway-database-postgresql:10.22.0") } jooq { @@ -125,7 +129,7 @@ jooq { // - ? matches a single character in a directory / file name property { key = "scripts" - value = "src/main/resources/schema.sql" + value = "src/main/resources/db/jooq/schema.sql" } // The sort order of the scripts within a directory, where: diff --git a/src/main/kotlin/com/devooks/backend/auth/v1/controller/AuthController.kt b/src/main/kotlin/com/devooks/backend/auth/v1/controller/AuthController.kt index 8ca5d4a..d5fcdb2 100644 --- a/src/main/kotlin/com/devooks/backend/auth/v1/controller/AuthController.kt +++ b/src/main/kotlin/com/devooks/backend/auth/v1/controller/AuthController.kt @@ -20,6 +20,7 @@ import com.devooks.backend.auth.v1.service.OauthService import com.devooks.backend.auth.v1.service.TokenService import com.devooks.backend.member.v1.domain.Member import com.devooks.backend.member.v1.service.MemberService +import jakarta.validation.Valid import java.util.* import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.annotation.PostMapping @@ -38,6 +39,7 @@ class AuthController( @PostMapping("/login") override suspend fun login( + @Valid @RequestBody request: LoginRequest, ): LoginResponse { @@ -51,6 +53,7 @@ class AuthController( @Transactional @PostMapping("/logout") override suspend fun logout( + @Valid @RequestBody request: LogoutRequest, ): LogoutResponse { @@ -62,6 +65,7 @@ class AuthController( @PostMapping("/reissue") override suspend fun reissue( + @Valid @RequestBody request: ReissueRequest, ): ReissueResponse { @@ -72,6 +76,7 @@ class AuthController( @PostMapping("/check/email") override suspend fun checkEmail( + @Valid @RequestBody request: CheckEmailRequest, ): CheckEmailResponse { diff --git a/src/main/kotlin/com/devooks/backend/auth/v1/controller/AuthControllerDocs.kt b/src/main/kotlin/com/devooks/backend/auth/v1/controller/AuthControllerDocs.kt index 6a1555f..f9af6f7 100644 --- a/src/main/kotlin/com/devooks/backend/auth/v1/controller/AuthControllerDocs.kt +++ b/src/main/kotlin/com/devooks/backend/auth/v1/controller/AuthControllerDocs.kt @@ -35,9 +35,7 @@ interface AuthControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- AUTH-400-1 : 인증 코드(authorizationCode)가 NULL이거나 빈 문자일 경우\n" + - "- AUTH-400-2 : 인증 유형(oauthType)이 NAVER, KAKAO, GOOGLE 이 아닐 경우 ", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -102,8 +100,7 @@ interface AuthControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- AUTH-400-3 : 리프래시 토큰(refreshToken)이 NULL이거나 빈 문자일 경우", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -152,8 +149,7 @@ interface AuthControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- AUTH-400-3 : 리프래시 토큰(refreshToken)이 NULL이거나 빈 문자일 경우", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -214,9 +210,7 @@ interface AuthControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- AUTH-400-16 : 이메일 값이 존재하지 않을 경우" + - "- AUTH-400-17 : 이메일 형식이 아닐 경우", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, diff --git a/src/main/kotlin/com/devooks/backend/auth/v1/dto/CheckEmailRequest.kt b/src/main/kotlin/com/devooks/backend/auth/v1/dto/CheckEmailRequest.kt index ba4aace..77ba372 100644 --- a/src/main/kotlin/com/devooks/backend/auth/v1/dto/CheckEmailRequest.kt +++ b/src/main/kotlin/com/devooks/backend/auth/v1/dto/CheckEmailRequest.kt @@ -1,14 +1,12 @@ package com.devooks.backend.auth.v1.dto -import com.devooks.backend.member.v1.error.validateEmail import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Pattern data class CheckEmailRequest( - @Schema(description = "이메일", required = true, nullable = false) - val email: String?, + @field:Pattern(regexp = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9]+\\.[A-Za-z]+$") + @Schema(description = "이메일", required = true) + val email: String, ) { - fun toCommand() = - CheckEmailCommand( - email = email.validateEmail() - ) + fun toCommand() = CheckEmailCommand(email = email) } diff --git a/src/main/kotlin/com/devooks/backend/auth/v1/dto/LoginRequest.kt b/src/main/kotlin/com/devooks/backend/auth/v1/dto/LoginRequest.kt index fa2bf42..e4b1ab0 100644 --- a/src/main/kotlin/com/devooks/backend/auth/v1/dto/LoginRequest.kt +++ b/src/main/kotlin/com/devooks/backend/auth/v1/dto/LoginRequest.kt @@ -1,23 +1,19 @@ package com.devooks.backend.auth.v1.dto -import com.devooks.backend.auth.v1.error.validateAuthorizationCode -import com.devooks.backend.auth.v1.error.validateOauthType +import com.devooks.backend.auth.v1.domain.OauthType import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank data class LoginRequest( - @Schema(description = "OAuth2 인증 코드", required = true, nullable = false) - val authorizationCode: String?, - @Schema( - description = "OAuth2 인증 유형 (ex. NAVER, KAKAO, GOOGLE)", - required = true, - nullable = false, - example = "NAVER" - ) - val oauthType: String?, + @field:NotBlank + @Schema(description = "OAuth2 인증 코드", required = true) + val authorizationCode: String, + @Schema(description = "OAuth2 인증 유형 (ex. NAVER, KAKAO, GOOGLE)", required = true) + val oauthType: OauthType, ) { fun toCommand(): LoginCommand = LoginCommand( - authorizationCode = authorizationCode.validateAuthorizationCode(), - oauthType = oauthType.validateOauthType() + authorizationCode = authorizationCode, + oauthType = oauthType ) } diff --git a/src/main/kotlin/com/devooks/backend/auth/v1/dto/LogoutRequest.kt b/src/main/kotlin/com/devooks/backend/auth/v1/dto/LogoutRequest.kt index 20bb9f3..514fd01 100644 --- a/src/main/kotlin/com/devooks/backend/auth/v1/dto/LogoutRequest.kt +++ b/src/main/kotlin/com/devooks/backend/auth/v1/dto/LogoutRequest.kt @@ -1,14 +1,13 @@ package com.devooks.backend.auth.v1.dto -import com.devooks.backend.auth.v1.error.validateRefreshToken import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank data class LogoutRequest( - @Schema(description = "Refresh 토큰", required = true, nullable = false) - val refreshToken: String?, + @field:NotBlank + @Schema(description = "Refresh 토큰", required = true) + val refreshToken: String, ) { fun toCommand(): LogoutCommand = - LogoutCommand( - refreshToken = refreshToken.validateRefreshToken() - ) + LogoutCommand(refreshToken = refreshToken) } diff --git a/src/main/kotlin/com/devooks/backend/auth/v1/dto/ReissueRequest.kt b/src/main/kotlin/com/devooks/backend/auth/v1/dto/ReissueRequest.kt index 2750f30..8fdcbb9 100644 --- a/src/main/kotlin/com/devooks/backend/auth/v1/dto/ReissueRequest.kt +++ b/src/main/kotlin/com/devooks/backend/auth/v1/dto/ReissueRequest.kt @@ -1,14 +1,12 @@ package com.devooks.backend.auth.v1.dto -import com.devooks.backend.auth.v1.error.validateRefreshToken import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank data class ReissueRequest( - @Schema(description = "리프래시 토큰", required = true, nullable = false) - val refreshToken: String?, + @field:NotBlank + @Schema(description = "리프래시 토큰", required = true) + val refreshToken: String, ) { - fun toCommand(): ReissueCommand = - ReissueCommand( - refreshToken = refreshToken.validateRefreshToken() - ) + fun toCommand(): ReissueCommand = ReissueCommand(refreshToken = refreshToken) } diff --git a/src/main/kotlin/com/devooks/backend/auth/v1/error/AuthError.kt b/src/main/kotlin/com/devooks/backend/auth/v1/error/AuthError.kt index 9c8a984..30d2260 100644 --- a/src/main/kotlin/com/devooks/backend/auth/v1/error/AuthError.kt +++ b/src/main/kotlin/com/devooks/backend/auth/v1/error/AuthError.kt @@ -2,19 +2,12 @@ package com.devooks.backend.auth.v1.error import com.devooks.backend.common.exception.GeneralException import org.springframework.http.HttpStatus -import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.CONFLICT import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.http.HttpStatus.UNAUTHORIZED enum class AuthError(val exception: GeneralException) { - // 400 - REQUIRED_AUTHORIZATION_CODE(GeneralException("AUTH-400-1", BAD_REQUEST, "인증 코드가 반드시 필요합니다.")), - INVALID_OAUTH_TYPE(GeneralException("AUTH-400-2", BAD_REQUEST, "인증 유형은 NAVER, KAKAO, GOOGLE 만 가능합니다.")), - REQUIRED_TOKEN(GeneralException("AUTH-400-3", BAD_REQUEST, "토큰이 반드시 필요합니다.")), - REQUIRED_OAUTH_ID(GeneralException("AUTH-400-4", BAD_REQUEST, "인증 식별자는 반드시 필요합니다.")), - // 401 EXPIRED_TOKEN(GeneralException("AUTH-401-1", UNAUTHORIZED, "만료된 토큰입니다.")), INVALID_REFRESH_TOKEN(GeneralException("AUTH-401-2", UNAUTHORIZED, "유효하지 않는 리프래시 토큰입니다.")), diff --git a/src/main/kotlin/com/devooks/backend/auth/v1/error/AuthValidation.kt b/src/main/kotlin/com/devooks/backend/auth/v1/error/AuthValidation.kt deleted file mode 100644 index 5948232..0000000 --- a/src/main/kotlin/com/devooks/backend/auth/v1/error/AuthValidation.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.devooks.backend.auth.v1.error - -import com.devooks.backend.auth.v1.domain.OauthType -import com.devooks.backend.common.error.validateNotBlank - - -fun String?.validateOauthId(): String = - validateNotBlank(AuthError.REQUIRED_OAUTH_ID.exception) - -fun String?.validateOauthType(): OauthType = - validateNotBlank(AuthError.INVALID_OAUTH_TYPE.exception) - .let { runCatching { OauthType.valueOf(it) }.getOrElse { null } } - ?: throw AuthError.INVALID_OAUTH_TYPE.exception - -fun String?.validateAuthorizationCode(): String = - validateNotBlank(AuthError.REQUIRED_AUTHORIZATION_CODE.exception) - -fun String?.validateRefreshToken(): String = - validateNotBlank(AuthError.REQUIRED_TOKEN.exception) \ No newline at end of file diff --git a/src/main/kotlin/com/devooks/backend/common/config/cors/CorsGlobalConfig.kt b/src/main/kotlin/com/devooks/backend/common/config/cors/CorsGlobalConfig.kt index 8e84161..be1625b 100644 --- a/src/main/kotlin/com/devooks/backend/common/config/cors/CorsGlobalConfig.kt +++ b/src/main/kotlin/com/devooks/backend/common/config/cors/CorsGlobalConfig.kt @@ -11,8 +11,8 @@ class CorsGlobalConfig : WebFluxConfigurer { registry .addMapping("/**") .allowedOrigins("http://localhost:3000", "http://localhost:3001") - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("Authorization", "Content-Type", "X-Requested-With") + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + .allowedHeaders("Authorization", "Content-Type", "X-Requested-With", "Accepts") .allowCredentials(true) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/common/config/database/R2dbcConfiguration.kt b/src/main/kotlin/com/devooks/backend/common/config/database/R2dbcConfiguration.kt index cfdc5eb..5e87e60 100644 --- a/src/main/kotlin/com/devooks/backend/common/config/database/R2dbcConfiguration.kt +++ b/src/main/kotlin/com/devooks/backend/common/config/database/R2dbcConfiguration.kt @@ -7,12 +7,10 @@ import io.r2dbc.spi.ConnectionFactoryOptions import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.convert.converter.Converter -import org.springframework.core.io.ClassPathResource import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration import org.springframework.data.r2dbc.convert.R2dbcCustomConversions import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer -import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator @Configuration @EnableR2dbcRepositories @@ -43,11 +41,5 @@ class R2dbcConfiguration( ConnectionFactoryInitializer() .apply { setConnectionFactory(connectionFactory()) - setDatabasePopulator( - ResourceDatabasePopulator( - ClassPathResource("schema.sql"), - ClassPathResource("data.sql") - ) - ) } } diff --git a/src/main/kotlin/com/devooks/backend/common/domain/Image.kt b/src/main/kotlin/com/devooks/backend/common/domain/Image.kt index c417482..4b54342 100644 --- a/src/main/kotlin/com/devooks/backend/common/domain/Image.kt +++ b/src/main/kotlin/com/devooks/backend/common/domain/Image.kt @@ -6,22 +6,14 @@ import java.util.* class Image( val base64Raw: String, val extension: ImageExtension, - val byteSize: Long, + val byteSize: Int, val order: Int ) { fun convertDecodedImage(): ByteArray = runCatching { - Base64.getMimeDecoder().decode(base64Raw) + Base64.getDecoder().decode(base64Raw) }.getOrElse { throw CommonError.FAIL_SAVE_IMAGE.exception } - - companion object { - private const val MAX_BYTE_SIZE = 50_000_000 - - fun Long?.validateByteSize(): Long = - this?.takeIf { it <= MAX_BYTE_SIZE } - ?: throw CommonError.INVALID_BYTE_SIZE.exception - } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/common/domain/ImageExtension.kt b/src/main/kotlin/com/devooks/backend/common/domain/ImageExtension.kt index 4229e38..855bc8f 100644 --- a/src/main/kotlin/com/devooks/backend/common/domain/ImageExtension.kt +++ b/src/main/kotlin/com/devooks/backend/common/domain/ImageExtension.kt @@ -1,6 +1,5 @@ package com.devooks.backend.common.domain -import com.devooks.backend.common.error.CommonError import java.util.* enum class ImageExtension { @@ -8,15 +7,4 @@ enum class ImageExtension { override fun toString(): String = this.name.lowercase(Locale.getDefault()) - - companion object { - fun String?.validateImageExtension(): ImageExtension = - runCatching { - this?.takeIf { it.isNotBlank() } - ?.let { ImageExtension.valueOf(it.uppercase()) } - ?: throw CommonError.INVALID_IMAGE_EXTENSION.exception - }.getOrElse { - throw CommonError.INVALID_IMAGE_EXTENSION.exception - } - } } diff --git a/src/main/kotlin/com/devooks/backend/common/dto/ImageDto.kt b/src/main/kotlin/com/devooks/backend/common/dto/ImageDto.kt index a5e5de4..9b0faf1 100644 --- a/src/main/kotlin/com/devooks/backend/common/dto/ImageDto.kt +++ b/src/main/kotlin/com/devooks/backend/common/dto/ImageDto.kt @@ -1,28 +1,35 @@ package com.devooks.backend.common.dto import com.devooks.backend.common.domain.Image -import com.devooks.backend.common.domain.Image.Companion.validateByteSize -import com.devooks.backend.common.domain.ImageExtension.Companion.validateImageExtension -import com.devooks.backend.common.error.CommonError -import com.devooks.backend.common.error.validateImageOrder -import com.devooks.backend.common.error.validateNotBlank +import com.devooks.backend.common.domain.ImageExtension import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank data class ImageDto( - @Schema(description = "base64 프로필 사진", required = true, nullable = false) - val base64Raw: String?, - @Schema(description = "확장자 (ex. JPG, PNG, JPEG)", required = true, nullable = false) - val extension: String?, - @Schema(description = "파일 크기 (byte, 최대 50MB)", required = true, nullable = false) - val byteSize: Long?, - @Schema(description = "파일 순서", required = true, nullable = false) - val order: Int? + @field:NotBlank + @Schema(description = "base64 프로필 사진", required = true) + val base64Raw: String, + @Schema(description = "확장자", required = true) + val extension: ImageExtension, + @field:Min(1) + @field:Max(50_000_000) + @Schema(description = "파일 크기 (byte, 최대 50MB)", required = true) + val byteSize: Int, ) { - fun toDomain(): Image = + fun toDomain(index: Int? = null): Image = Image( - base64Raw = base64Raw.validateNotBlank(CommonError.REQUIRED_BASE64RAW.exception), - extension = extension.validateImageExtension(), - byteSize = byteSize.validateByteSize(), - order = order.validateImageOrder() + base64Raw = base64Raw, + extension = extension, + byteSize = byteSize, + order = index ?: DEFAULT_IMAGE_ORDER ) + + companion object { + private const val DEFAULT_IMAGE_ORDER = 0 + + fun List.toDomain() = + mapIndexed { index, imageDto -> imageDto.toDomain(index) } + } } diff --git a/src/main/kotlin/com/devooks/backend/common/dto/PageResponse.kt b/src/main/kotlin/com/devooks/backend/common/dto/PageResponse.kt index 20590ea..8d7ed1d 100644 --- a/src/main/kotlin/com/devooks/backend/common/dto/PageResponse.kt +++ b/src/main/kotlin/com/devooks/backend/common/dto/PageResponse.kt @@ -1,10 +1,13 @@ package com.devooks.backend.common.dto import com.devooks.backend.common.dto.PageResponse.PageableResponse.Companion.toPageable +import io.swagger.v3.oas.annotations.media.Schema import org.springframework.data.domain.Page data class PageResponse( + @Schema(description = "조회 데이터") val data: List, + @Schema(description = "페이징 정보") val pageable: PageableResponse, ) { companion object { @@ -16,7 +19,9 @@ data class PageResponse( } data class PageableResponse( + @Schema(description = "조회 가능한 페이지 수") val totalPages: Int, + @Schema(description = "조회 가능한 전체 수") val totalElements: Long, ) { companion object { diff --git a/src/main/kotlin/com/devooks/backend/common/dto/Paging.kt b/src/main/kotlin/com/devooks/backend/common/dto/Paging.kt index 6f3b328..24d5d52 100644 --- a/src/main/kotlin/com/devooks/backend/common/dto/Paging.kt +++ b/src/main/kotlin/com/devooks/backend/common/dto/Paging.kt @@ -1,10 +1,9 @@ package com.devooks.backend.common.dto -import com.devooks.backend.common.error.CommonError import org.springframework.data.domain.Pageable -private typealias Count = String -private typealias Page = String +private typealias Count = Int +private typealias Page = Int data class Paging( val value: Pageable, @@ -14,22 +13,10 @@ data class Paging( count: Count, ) : this( value = Pageable - .ofSize(count.toIntCount()) - .withPage(page.toIntPage() - 1) + .ofSize(count) + .withPage(page - 1) ) val offset: Int = value.offset.toInt() val limit: Int = value.pageSize * (value.pageNumber + 1) } - -private fun Page.toIntPage() = - runCatching { - this.toInt().takeIf { it > 0 } - ?: throw CommonError.INVALID_PAGE.exception - }.getOrElse { throw CommonError.INVALID_PAGE.exception } - -private fun Count.toIntCount() = - runCatching { - this.toInt().takeIf { it in 1..1000 } - ?: throw CommonError.INVALID_COUNT.exception - }.getOrElse { throw CommonError.INVALID_COUNT.exception } diff --git a/src/main/kotlin/com/devooks/backend/common/error/CommonError.kt b/src/main/kotlin/com/devooks/backend/common/error/CommonError.kt index 3091b21..ec77c13 100644 --- a/src/main/kotlin/com/devooks/backend/common/error/CommonError.kt +++ b/src/main/kotlin/com/devooks/backend/common/error/CommonError.kt @@ -3,25 +3,20 @@ package com.devooks.backend.common.error import com.devooks.backend.common.exception.GeneralException import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR +import org.springframework.http.HttpStatus.METHOD_NOT_ALLOWED enum class CommonError(val exception: GeneralException) { // 400 + INVALID_REQUEST(GeneralException("COMMON-400-0", BAD_REQUEST, "유효하지 않은 요청입니다.")), INVALID_PAGE(GeneralException("COMMON-400-1", BAD_REQUEST, "페이지는 1부터 조회할 수 있습니다.")), INVALID_COUNT(GeneralException("COMMON-400-2", BAD_REQUEST, "개수는 1~1000 까지 조회할 수 있습니다.")), - REQUIRED_BASE64RAW(GeneralException("COMMON-400-3", BAD_REQUEST, "이미지 내용이 반드시 필요합니다.")), - INVALID_IMAGE_EXTENSION( - GeneralException( - "COMMON-400-4", - BAD_REQUEST, - "유효하지 않은 이미지 확장자입니다. JPG, PNG, JPEG만 가능합니다." - ) - ), - INVALID_BYTE_SIZE(GeneralException("COMMON-400-5", BAD_REQUEST, "50MB 이하의 영상만 저장이 가능합니다.")), - REQUIRED_IMAGE(GeneralException("COMMON-400-6", BAD_REQUEST, "이미지가 반드시 필요합니다.")), - INVALID_IMAGE_ORDER(GeneralException("COMMON-400-7", BAD_REQUEST, "유효하지 않은 이미지 순서입니다.")), + REQUIRED_IMAGE(GeneralException("COMMON-400-6", BAD_REQUEST, "사진이 반드시 필요합니다.")), + + // 405 + INVALID_METHOD(GeneralException("COMMON-405-1", METHOD_NOT_ALLOWED, "유효하지 않은 HTTP 메서드입니다.")), // 500 - FAIL_SAVE_IMAGE(GeneralException("COMMON-500-1", INTERNAL_SERVER_ERROR, "이미지 저장을 실패했습니다.")), + FAIL_SAVE_IMAGE(GeneralException("COMMON-500-1", INTERNAL_SERVER_ERROR, "사진 저장을 실패했습니다.")), FAIL_SAVE_FILE(GeneralException("COMMON-500-2", INTERNAL_SERVER_ERROR, "파일 저장을 실패했습니다.")), FAIL_CREATE_DIRECTORY(GeneralException("COMMON-500-3", INTERNAL_SERVER_ERROR, "디렉터리 저장을 실패했습니다.")) ; diff --git a/src/main/kotlin/com/devooks/backend/common/error/CommonValidation.kt b/src/main/kotlin/com/devooks/backend/common/error/CommonValidation.kt index c052eb7..7d7491d 100644 --- a/src/main/kotlin/com/devooks/backend/common/error/CommonValidation.kt +++ b/src/main/kotlin/com/devooks/backend/common/error/CommonValidation.kt @@ -5,23 +5,13 @@ import com.devooks.backend.common.dto.ImageDto import com.devooks.backend.common.exception.GeneralException import java.util.* -inline fun T?.validateNotNull(exception: GeneralException): T = - takeIf { it != null } ?: throw exception - fun String?.validateNotBlank(exception: GeneralException): String = takeIf { it.isNullOrBlank().not() } ?: throw exception -fun List?.validateNotEmpty(exception: GeneralException): List = - takeIf { !isNullOrEmpty() } ?: throw exception - -fun ImageDto?.validateImage(): Image = - this?.toDomain() ?: throw CommonError.REQUIRED_IMAGE.exception - fun List?.validateImages(): List = - takeIf { it.isNullOrEmpty().not() }?.map { it.toDomain() } ?: throw CommonError.REQUIRED_IMAGE.exception - -fun Int?.validateImageOrder(): Int = - takeIf { it != null && it >= 0 } ?: throw CommonError.INVALID_IMAGE_ORDER.exception + takeIf { it.isNullOrEmpty().not() } + ?.mapIndexed { index, dto -> dto.toDomain(index) } + ?: throw CommonError.REQUIRED_IMAGE.exception fun String?.validateUUID(exception: GeneralException): UUID = - runCatching { UUID.fromString(this) }.getOrElse { throw exception } \ No newline at end of file + runCatching { UUID.fromString(this) }.getOrElse { throw exception } diff --git a/src/main/kotlin/com/devooks/backend/common/exception/ErrorResponse.kt b/src/main/kotlin/com/devooks/backend/common/exception/ErrorResponse.kt index 2b8c1f7..2f5ced6 100644 --- a/src/main/kotlin/com/devooks/backend/common/exception/ErrorResponse.kt +++ b/src/main/kotlin/com/devooks/backend/common/exception/ErrorResponse.kt @@ -1,6 +1,7 @@ package com.devooks.backend.common.exception import io.swagger.v3.oas.annotations.media.Schema +import java.time.Instant import java.util.* class ErrorResponse( @@ -17,5 +18,29 @@ class ErrorResponse( @Schema(description = "에러 코드", example = "EBOOK-400-11") val code: String, @Schema(description = "에러 메시지", example = "문의 식별자가 반드시 필요합니다.") - val message: String, -) + val message: Any, +) { + companion object { + private val DEFAULT_TIMESTAMP = Date.from(Instant.now()) + private const val DEFAULT_MESSAGE = "UNKNOWN" + private const val DEFAULT_STATUS = 500 + + fun Map.toErrorResponse( + timestamp: Date? = null, + path: String? = null, + status: Int? = null, + error: String? = null, + requestId: String? = null, + code: String? = null, + message: Any? = null, + ) = ErrorResponse( + timestamp = timestamp ?: this["timestamp"] as? Date ?: DEFAULT_TIMESTAMP, + path = path ?: this["path"] as? String ?: DEFAULT_MESSAGE, + status = status ?: this["status"] as? Int ?: DEFAULT_STATUS, + error = error ?: this["error"] as? String ?: DEFAULT_MESSAGE, + requestId = requestId ?: this["requestId"] as? String ?: DEFAULT_MESSAGE, + code = code ?: this["code"] as? String ?: DEFAULT_MESSAGE, + message = message ?: this["message"] as? String ?: DEFAULT_MESSAGE, + ) + } +} diff --git a/src/main/kotlin/com/devooks/backend/common/exception/GlobalExceptionHandler.kt b/src/main/kotlin/com/devooks/backend/common/exception/GlobalExceptionHandler.kt index 38fc198..760558c 100644 --- a/src/main/kotlin/com/devooks/backend/common/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/com/devooks/backend/common/exception/GlobalExceptionHandler.kt @@ -1,5 +1,7 @@ package com.devooks.backend.common.exception +import com.devooks.backend.common.error.CommonError +import com.devooks.backend.common.exception.ErrorResponse.Companion.toErrorResponse import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import org.springframework.boot.autoconfigure.web.WebProperties @@ -10,19 +12,24 @@ import org.springframework.boot.web.reactive.error.ErrorAttributes import org.springframework.context.ApplicationContext import org.springframework.core.annotation.Order import org.springframework.http.HttpStatus -import org.springframework.http.MediaType +import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.METHOD_NOT_ALLOWED +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.http.codec.ServerCodecConfigurer import org.springframework.stereotype.Component import org.springframework.web.bind.support.WebExchangeBindException -import org.springframework.web.reactive.function.BodyInserters import org.springframework.web.reactive.function.server.RequestPredicates import org.springframework.web.reactive.function.server.RouterFunction import org.springframework.web.reactive.function.server.RouterFunctions import org.springframework.web.reactive.function.server.ServerRequest import org.springframework.web.reactive.function.server.ServerResponse import org.springframework.web.reactive.resource.NoResourceFoundException +import org.springframework.web.server.MethodNotAllowedException import org.springframework.web.server.MissingRequestValueException +import org.springframework.web.server.ServerErrorException import org.springframework.web.server.ServerWebInputException +import org.springframework.web.server.UnsupportedMediaTypeStatusException import reactor.core.publisher.Mono @Component @@ -31,7 +38,7 @@ class GlobalErrorWebExceptionHandler( globalErrorAttributes: DefaultErrorAttributes, applicationContext: ApplicationContext, serverCodecConfigurer: ServerCodecConfigurer, - val objectMapper: ObjectMapper + val objectMapper: ObjectMapper, ) : AbstractErrorWebExceptionHandler(globalErrorAttributes, WebProperties.Resources(), applicationContext) { init { @@ -45,60 +52,135 @@ class GlobalErrorWebExceptionHandler( private fun renderErrorResponse(request: ServerRequest): Mono { val errorAttributes = getErrorAttributes(request, ErrorAttributeOptions.defaults()) + val commonException = CommonError.INVALID_REQUEST.exception return when (val error = getError(request)) { is WebExchangeBindException -> { - errorAttributes["errors"] = error.bindingResult.allErrors.map { "${it.code} : ${it.defaultMessage}" } - ServerResponse.status(HttpStatus.BAD_REQUEST) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(errorAttributes)) + ServerResponse.status(BAD_REQUEST) + .contentType(APPLICATION_JSON) + .bodyValue( + errorAttributes + .toErrorResponse( + code = commonException.code, + error = BAD_REQUEST.name, + message = error + .bindingResult + .allErrors + .map { "${it.codes?.first() ?: "request"} : ${it.defaultMessage}" } + .toString() + ) + ) } is GeneralException -> { - errorAttributes["code"] = error.code - errorAttributes["message"] = - if (error.code == "MEMBER-404-1") { - objectMapper.readValue>(error.message) - } else { - error.message - } - errorAttributes["status"] = error.status.value() - errorAttributes["error"] = error.status.name ServerResponse .status(error.status) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(errorAttributes)) + .contentType(APPLICATION_JSON) + .bodyValue( + errorAttributes + .toErrorResponse( + code = error.code, + status = error.status.value(), + error = error.status.name, + message = + if (error.code == "MEMBER-404-1") { + objectMapper.readValue>(error.message) + } else { + error.message + } + ) + ) } is MissingRequestValueException -> { - errorAttributes["reason"] = error.reason + val exception = CommonError.INVALID_REQUEST.exception ServerResponse - .status(HttpStatus.BAD_REQUEST) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(errorAttributes)) + .status(BAD_REQUEST) + .contentType(APPLICATION_JSON) + .bodyValue( + errorAttributes + .toErrorResponse( + code = exception.code, + error = BAD_REQUEST.name, + message = "${exception.message} ${error.reason ?: ""}" + ) + ) } is ServerWebInputException -> { - errorAttributes["reason"] = error.cause.toString() + val exception = CommonError.INVALID_REQUEST.exception ServerResponse - .status(error.statusCode) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(errorAttributes)) + .status(BAD_REQUEST) + .contentType(APPLICATION_JSON) + .bodyValue( + errorAttributes + .toErrorResponse( + code = exception.code, + error = BAD_REQUEST.name, + message = "${exception.message} ${error.cause?.message ?: ""}" + ) + ) } is NoResourceFoundException -> { - errorAttributes["reason"] = "존재하지 않는 API 입니다." ServerResponse - .status(HttpStatus.NOT_FOUND) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(errorAttributes)) + .status(NOT_FOUND) + .contentType(APPLICATION_JSON) + .bodyValue( + errorAttributes + .toErrorResponse( + error = NOT_FOUND.name, + message = "존재하지 않는 API 입니다." + ) + ) + } + + is MethodNotAllowedException -> { + val exception = CommonError.INVALID_METHOD.exception + ServerResponse + .status(error.statusCode) + .contentType(APPLICATION_JSON) + .bodyValue( + errorAttributes + .toErrorResponse( + error = METHOD_NOT_ALLOWED.name, + code = exception.code, + message = "${exception.message} ${error.cause?.message ?: ""}" + ) + ) + } + + is UnsupportedMediaTypeStatusException -> { + ServerResponse + .status(error.statusCode) + .contentType(APPLICATION_JSON) + .bodyValue( + errorAttributes + .toErrorResponse( + error = HttpStatus.valueOf(error.statusCode.value()).name, + message = error.reason + ) + ) + } + + is ServerErrorException -> { + ServerResponse + .status(error.statusCode) + .contentType(APPLICATION_JSON) + .bodyValue( + errorAttributes + .toErrorResponse( + error = HttpStatus.valueOf(error.statusCode.value()).name, + message = error.stackTraceToString() + ) + ) } else -> { ServerResponse .status(HttpStatus.INTERNAL_SERVER_ERROR) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(errorAttributes)) + .contentType(APPLICATION_JSON) + .bodyValue(errorAttributes.toErrorResponse(message = error.cause.toString())) } } } diff --git a/src/main/kotlin/com/devooks/backend/common/utils/FileUtils.kt b/src/main/kotlin/com/devooks/backend/common/utils/FileUtils.kt index 0155d95..b710bce 100644 --- a/src/main/kotlin/com/devooks/backend/common/utils/FileUtils.kt +++ b/src/main/kotlin/com/devooks/backend/common/utils/FileUtils.kt @@ -19,6 +19,7 @@ suspend inline fun saveImage(image: Image, rootPath: String): String = content = image.convertDecodedImage() ).await() ?.path + ?.let { "/$it" } ?: throw CommonError.FAIL_SAVE_FILE.exception fun saveFileOrNull( @@ -28,7 +29,7 @@ fun saveFileOrNull( ): Deferred = CoroutineScope(Dispatchers.IO) .async { - val fileName = UUID.randomUUID().toString() + val fileName = UUID.randomUUID() val targetLocation = Path.of(rootPath, "$fileName.$extension") runCatching { Files.write(targetLocation, content).toFile() @@ -55,4 +56,4 @@ inline fun T.createDirectory(path: String) { logger.error(it.stackTraceToString()) throw CommonError.FAIL_CREATE_DIRECTORY.exception } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookController.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookController.kt index d4c0519..d8c8515 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookController.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookController.kt @@ -4,9 +4,14 @@ import com.devooks.backend.auth.v1.domain.Authorization import com.devooks.backend.auth.v1.service.TokenService import com.devooks.backend.category.v1.domain.Category import com.devooks.backend.category.v1.service.CategoryService +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.dto.PageResponse.Companion.toResponse import com.devooks.backend.ebook.v1.controller.docs.EbookControllerDocs import com.devooks.backend.ebook.v1.domain.Ebook import com.devooks.backend.ebook.v1.domain.EbookImage +import com.devooks.backend.ebook.v1.domain.EbookOrder +import com.devooks.backend.ebook.v1.dto.EbookView +import com.devooks.backend.ebook.v1.dto.EbookView.Companion.toEbookView import com.devooks.backend.ebook.v1.dto.command.CreateEbookCommand import com.devooks.backend.ebook.v1.dto.command.DeleteEbookCommand import com.devooks.backend.ebook.v1.dto.command.GetDetailOfEbookCommand @@ -19,13 +24,12 @@ import com.devooks.backend.ebook.v1.dto.response.DeleteEbookResponse import com.devooks.backend.ebook.v1.dto.response.EbookResponse import com.devooks.backend.ebook.v1.dto.response.GetDetailOfEbookResponse import com.devooks.backend.ebook.v1.dto.response.GetDetailOfEbookResponse.Companion.toGetDetailOfEbookResponse -import com.devooks.backend.ebook.v1.dto.response.GetEbooksResponse -import com.devooks.backend.ebook.v1.dto.response.GetEbooksResponse.Companion.toGetEbooksResponse import com.devooks.backend.ebook.v1.dto.response.ModifyEbookResponse import com.devooks.backend.ebook.v1.service.EbookImageService import com.devooks.backend.ebook.v1.service.EbookService import com.devooks.backend.ebook.v1.service.RelatedCategoryService import com.devooks.backend.pdf.v1.service.PdfService +import jakarta.validation.Valid import java.util.* import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.transaction.annotation.Transactional @@ -54,6 +58,7 @@ class EbookController( @Transactional @PostMapping override suspend fun createEbook( + @Valid @RequestBody request: CreateEbookRequest, @RequestHeader(AUTHORIZATION) @@ -75,29 +80,27 @@ class EbookController( @GetMapping override suspend fun getEbooks( - @RequestParam(required = false, defaultValue = "") - page: String, - @RequestParam(required = false, defaultValue = "") - count: String, - @RequestParam(required = false, defaultValue = "") - title: String, - @RequestParam(required = false, defaultValue = "") - sellingMemberId: String, - @RequestParam(required = false, defaultValue = "") - ebookIdList: List, - @RequestParam(required = false, defaultValue = "") - categoryIdList: List, - @RequestParam(required = false, defaultValue = "") - orderBy: String, - @RequestHeader(AUTHORIZATION, required = false, defaultValue = "") - authorization: String, - ): GetEbooksResponse { - val requesterId = authorization - .takeIf { it.isNotBlank() } - ?.let { tokenService.getMemberId(Authorization(it)) } + @RequestParam + page: Int, + @RequestParam + count: Int, + @RequestParam(required = false) + title: String?, + @RequestParam(required = false) + sellerMemberId: UUID?, + @RequestParam(required = false) + ebookIdList: List?, + @RequestParam(required = false) + categoryIdList: List?, + @RequestParam(required = false) + orderBy: EbookOrder?, + @RequestHeader(AUTHORIZATION) + authorization: String?, + ): PageResponse { + val requesterId = authorization?.let { tokenService.getMemberId(Authorization(it)) } val command = GetEbookCommand( title = title, - sellingMemberId = sellingMemberId, + sellerMemberId = sellerMemberId, ebookIdList = ebookIdList, categoryIdList = categoryIdList, orderBy = orderBy, @@ -105,19 +108,18 @@ class EbookController( page = page, count = count ) - return ebookService.get(command).toGetEbooksResponse() + val ebooks = ebookService.get(command) + return ebooks.map { it.toEbookView() }.toResponse() } @GetMapping("/{ebookId}") override suspend fun getDetailOfEbook( - @PathVariable("ebookId", required = true) - ebookId: String, - @RequestHeader(AUTHORIZATION, required = false, defaultValue = "") - authorization: String, + @PathVariable("ebookId") + ebookId: UUID, + @RequestHeader(AUTHORIZATION, required = false) + authorization: String?, ): GetDetailOfEbookResponse { - val requesterId = authorization - .takeIf { it.isNotBlank() } - ?.let { tokenService.getMemberId(Authorization(it)) } + val requesterId = authorization?.let { tokenService.getMemberId(Authorization(it)) } val command = GetDetailOfEbookCommand(ebookId, requesterId) return ebookService.get(command).toGetDetailOfEbookResponse() } @@ -125,8 +127,9 @@ class EbookController( @Transactional @PatchMapping("/{ebookId}") override suspend fun modifyEbook( - @PathVariable("ebookId", required = true) - ebookId: String, + @PathVariable("ebookId") + ebookId: UUID, + @Valid @RequestBody request: ModifyEbookRequest, @RequestHeader(AUTHORIZATION) @@ -144,8 +147,8 @@ class EbookController( @Transactional @DeleteMapping("/{ebookId}") override suspend fun deleteEbook( - @PathVariable("ebookId", required = true) - ebookId: String, + @PathVariable("ebookId") + ebookId: UUID, @RequestHeader(AUTHORIZATION) authorization: String, ): DeleteEbookResponse { diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookImageController.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookImageController.kt index 6ba8797..37aa507 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookImageController.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookImageController.kt @@ -2,6 +2,7 @@ package com.devooks.backend.ebook.v1.controller import com.devooks.backend.auth.v1.domain.Authorization import com.devooks.backend.auth.v1.service.TokenService +import com.devooks.backend.ebook.v1.controller.docs.EbookImageControllerDocs import com.devooks.backend.ebook.v1.domain.EbookImage import com.devooks.backend.ebook.v1.dto.command.SaveImagesCommand import com.devooks.backend.ebook.v1.dto.request.SaveDescriptionImagesRequest @@ -11,6 +12,7 @@ import com.devooks.backend.ebook.v1.dto.response.SaveDescriptionImagesResponse.C import com.devooks.backend.ebook.v1.dto.response.SaveMainImageResponse import com.devooks.backend.ebook.v1.dto.response.SaveMainImageResponse.Companion.toSaveMainImageResponse import com.devooks.backend.ebook.v1.service.EbookImageService +import jakarta.validation.Valid import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.annotation.PostMapping @@ -24,13 +26,15 @@ import org.springframework.web.bind.annotation.RestController class EbookImageController( private val ebookImageService: EbookImageService, private val tokenService: TokenService, -) { +) : EbookImageControllerDocs { + @Transactional @PostMapping("/description-images") - suspend fun saveDescriptionImages( + override suspend fun saveDescriptionImages( + @Valid @RequestBody request: SaveDescriptionImagesRequest, - @RequestHeader(AUTHORIZATION, required = true) + @RequestHeader(AUTHORIZATION) authorization: String, ): SaveDescriptionImagesResponse { val requesterId = tokenService.getMemberId(Authorization(authorization)) @@ -41,10 +45,11 @@ class EbookImageController( @Transactional @PostMapping("/main-image") - suspend fun saveMainImage( + override suspend fun saveMainImage( + @Valid @RequestBody request: SaveMainImageRequest, - @RequestHeader(AUTHORIZATION, required = true) + @RequestHeader(AUTHORIZATION) authorization: String, ): SaveMainImageResponse { val requesterId = tokenService.getMemberId(Authorization(authorization)) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryCommentController.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryCommentController.kt index 4fda1f9..6767fa3 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryCommentController.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryCommentController.kt @@ -2,7 +2,12 @@ package com.devooks.backend.ebook.v1.controller import com.devooks.backend.auth.v1.domain.Authorization import com.devooks.backend.auth.v1.service.TokenService +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.dto.PageResponse.Companion.toResponse +import com.devooks.backend.ebook.v1.controller.docs.EbookInquiryCommentControllerDocs import com.devooks.backend.ebook.v1.domain.EbookInquiryComment +import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentView +import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentView.Companion.toEbookInquiryCommentView import com.devooks.backend.ebook.v1.dto.command.CreateEbookInquiryCommentCommand import com.devooks.backend.ebook.v1.dto.command.DeleteEbookInquiryCommentCommand import com.devooks.backend.ebook.v1.dto.command.GetEbookInquireCommentsCommand @@ -12,13 +17,14 @@ import com.devooks.backend.ebook.v1.dto.request.ModifyEbookInquiryCommentRequest import com.devooks.backend.ebook.v1.dto.response.CreateEbookInquiryCommentResponse import com.devooks.backend.ebook.v1.dto.response.CreateEbookInquiryCommentResponse.Companion.toCreateEbookInquiryCommentResponse import com.devooks.backend.ebook.v1.dto.response.DeleteEbookInquiryCommentResponse -import com.devooks.backend.ebook.v1.dto.response.GetEbookInquiryCommentsResponse -import com.devooks.backend.ebook.v1.dto.response.GetEbookInquiryCommentsResponse.Companion.toGetEbookInquiryCommentsResponse import com.devooks.backend.ebook.v1.dto.response.ModifyEbookInquiryCommentResponse import com.devooks.backend.ebook.v1.dto.response.ModifyEbookInquiryCommentResponse.Companion.toModifyEbookInquiryCommentResponse import com.devooks.backend.ebook.v1.service.EbookInquiryCommentEventService import com.devooks.backend.ebook.v1.service.EbookInquiryCommentService import com.devooks.backend.ebook.v1.service.EbookInquiryService +import jakarta.validation.Valid +import java.util.* +import org.springframework.data.domain.Page import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.annotation.DeleteMapping @@ -39,11 +45,12 @@ class EbookInquiryCommentController( private val tokenService: TokenService, private val ebookInquiryCommentService: EbookInquiryCommentService, private val ebookInquiryCommentEventService: EbookInquiryCommentEventService, -) { +) : EbookInquiryCommentControllerDocs { @Transactional @PostMapping - suspend fun createEbookInquiryComment( + override suspend fun createEbookInquiryComment( + @Valid @RequestBody request: CreateEbookInquiryCommentRequest, @RequestHeader(AUTHORIZATION) @@ -58,24 +65,25 @@ class EbookInquiryCommentController( } @GetMapping - suspend fun getEbookInquiryComments( - @RequestParam(required = false, defaultValue = "") - inquiryId: String, - @RequestParam(required = false, defaultValue = "") - page: String, - @RequestParam(required = false, defaultValue = "") - count: String, - ): GetEbookInquiryCommentsResponse { + override suspend fun getEbookInquiryComments( + @RequestParam + inquiryId: UUID, + @RequestParam + page: Int, + @RequestParam + count: Int, + ): PageResponse { val command = GetEbookInquireCommentsCommand(inquiryId, page, count) - val inquiryCommentList: List = ebookInquiryCommentService.get(command) - return inquiryCommentList.toGetEbookInquiryCommentsResponse() + val inquiryCommentList: Page = ebookInquiryCommentService.get(command) + return inquiryCommentList.map { it.toEbookInquiryCommentView() }.toResponse() } @Transactional @PatchMapping("/{commentId}") - suspend fun modifyEbookInquiryComment( - @PathVariable(name = "commentId", required = false) - commentId: String, + override suspend fun modifyEbookInquiryComment( + @PathVariable(name = "commentId") + commentId: UUID, + @Valid @RequestBody request: ModifyEbookInquiryCommentRequest, @RequestHeader(AUTHORIZATION) @@ -89,9 +97,9 @@ class EbookInquiryCommentController( @Transactional @DeleteMapping("/{commentId}") - suspend fun deleteEbookInquiryComment( - @PathVariable(name = "commentId", required = false) - commentId: String, + override suspend fun deleteEbookInquiryComment( + @PathVariable(name = "commentId") + commentId: UUID, @RequestHeader(AUTHORIZATION) authorization: String, ): DeleteEbookInquiryCommentResponse { diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryController.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryController.kt index 14e9219..dea61a8 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryController.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryController.kt @@ -2,7 +2,12 @@ package com.devooks.backend.ebook.v1.controller import com.devooks.backend.auth.v1.domain.Authorization import com.devooks.backend.auth.v1.service.TokenService +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.dto.PageResponse.Companion.toResponse +import com.devooks.backend.ebook.v1.controller.docs.EbookInquiryControllerDocs import com.devooks.backend.ebook.v1.domain.EbookInquiry +import com.devooks.backend.ebook.v1.dto.EbookInquiryView +import com.devooks.backend.ebook.v1.dto.EbookInquiryView.Companion.toEbookInquiryView import com.devooks.backend.ebook.v1.dto.command.CreateEbookInquiryCommand import com.devooks.backend.ebook.v1.dto.command.DeleteEbookInquiryCommand import com.devooks.backend.ebook.v1.dto.command.GetEbookInquiresCommand @@ -12,13 +17,14 @@ import com.devooks.backend.ebook.v1.dto.request.ModifyEbookInquiryRequest import com.devooks.backend.ebook.v1.dto.response.CreateEbookInquiryResponse import com.devooks.backend.ebook.v1.dto.response.CreateEbookInquiryResponse.Companion.toCreateEbookInquiryResponse import com.devooks.backend.ebook.v1.dto.response.DeleteEbookInquiryResponse -import com.devooks.backend.ebook.v1.dto.response.GetEbookInquiriesResponse -import com.devooks.backend.ebook.v1.dto.response.GetEbookInquiriesResponse.Companion.toGetEbookInquiriesResponse import com.devooks.backend.ebook.v1.dto.response.ModifyEbookInquiryResponse import com.devooks.backend.ebook.v1.dto.response.ModifyEbookInquiryResponse.Companion.toModifyEbookInquiryResponse import com.devooks.backend.ebook.v1.service.EbookInquiryEventService import com.devooks.backend.ebook.v1.service.EbookInquiryService import com.devooks.backend.ebook.v1.service.EbookService +import jakarta.validation.Valid +import java.util.* +import org.springframework.data.domain.Page import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.annotation.DeleteMapping @@ -39,11 +45,12 @@ class EbookInquiryController( private val tokenService: TokenService, private val ebookInquiryService: EbookInquiryService, private val ebookInquiryEventService: EbookInquiryEventService, -) { +) : EbookInquiryControllerDocs { @Transactional @PostMapping - suspend fun createEbookInquiry( + override suspend fun createEbookInquiry( + @Valid @RequestBody request: CreateEbookInquiryRequest, @RequestHeader(AUTHORIZATION) @@ -58,24 +65,25 @@ class EbookInquiryController( } @GetMapping - suspend fun getEbookInquiries( - @RequestParam(required = false, defaultValue = "") - ebookId: String, - @RequestParam(required = false, defaultValue = "") - page: String, - @RequestParam(required = false, defaultValue = "") - count: String, - ): GetEbookInquiriesResponse { + override suspend fun getEbookInquiries( + @RequestParam + ebookId: UUID, + @RequestParam + page: Int, + @RequestParam + count: Int, + ): PageResponse { val command = GetEbookInquiresCommand(ebookId, page, count) - val ebookInquiryList: List = ebookInquiryService.get(command) - return ebookInquiryList.toGetEbookInquiriesResponse() + val ebookInquiryList: Page = ebookInquiryService.get(command) + return ebookInquiryList.map { it.toEbookInquiryView() }.toResponse() } @Transactional @PatchMapping("/{inquiryId}") - suspend fun modifyEbookInquiry( - @PathVariable(name = "inquiryId", required = false) - inquiryId: String, + override suspend fun modifyEbookInquiry( + @PathVariable + inquiryId: UUID, + @Valid @RequestBody request: ModifyEbookInquiryRequest, @RequestHeader(AUTHORIZATION) @@ -89,9 +97,9 @@ class EbookInquiryController( @Transactional @DeleteMapping("/{inquiryId}") - suspend fun deleteEbookInquiry( - @PathVariable(name = "inquiryId", required = false) - inquiryId: String, + override suspend fun deleteEbookInquiry( + @PathVariable + inquiryId: UUID, @RequestHeader(AUTHORIZATION) authorization: String, ): DeleteEbookInquiryResponse { diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookControllerDocs.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookControllerDocs.kt index 924fb57..8fff70d 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookControllerDocs.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookControllerDocs.kt @@ -1,12 +1,14 @@ package com.devooks.backend.ebook.v1.controller.docs +import com.devooks.backend.common.dto.PageResponse import com.devooks.backend.common.exception.ErrorResponse +import com.devooks.backend.ebook.v1.domain.EbookOrder +import com.devooks.backend.ebook.v1.dto.EbookView import com.devooks.backend.ebook.v1.dto.request.CreateEbookRequest import com.devooks.backend.ebook.v1.dto.request.ModifyEbookRequest import com.devooks.backend.ebook.v1.dto.response.CreateEbookResponse import com.devooks.backend.ebook.v1.dto.response.DeleteEbookResponse import com.devooks.backend.ebook.v1.dto.response.GetDetailOfEbookResponse -import com.devooks.backend.ebook.v1.dto.response.GetEbooksResponse import com.devooks.backend.ebook.v1.dto.response.ModifyEbookResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content @@ -14,6 +16,7 @@ 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 java.util.* import org.springframework.http.MediaType.APPLICATION_JSON_VALUE @Tag(name = "전자책") @@ -24,20 +27,11 @@ interface EbookControllerDocs { value = [ ApiResponse( responseCode = "200", - description = "OK", - content = [ - Content( - mediaType = APPLICATION_JSON_VALUE, - schema = Schema(implementation = GetEbooksResponse::class) - ) - ] + description = "OK" ), ApiResponse( responseCode = "400", - description = - "- COMMON-400-1 : 페이지는 1부터 조회할 수 있습니다.\n" + - "- COMMON-400-2 : 개수는 1~1000 까지 조회할 수 있습니다.\n" + - "- EBOOK-400-9 : 잘못된 형식의 EbookOrder(ex. LATEST, REVIEW) 입니다.", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -48,23 +42,23 @@ interface EbookControllerDocs { ] ) suspend fun getEbooks( - @Schema(description = "페이지", required = true, nullable = false) - page: String, - @Schema(description = "개수", required = true, nullable = false) - count: String, - @Schema(description = "검색할 전자책 제목", required = false, nullable = true) - title: String, - @Schema(description = "검색할 판매자 식별자", required = false, nullable = true) - sellingMemberId: String, - @Schema(description = "검색할 전자책 식별자", required = false, nullable = true) - ebookIdList: List, - @Schema(description = "검색할 카테고리 식별자 목록", required = false, nullable = true) - categoryIdList: List, - @Schema(description = "정렬할 속성 (ex. LATEST, REVIEW)", required = false, nullable = true) - orderBy: String, - @Schema(description = "액세스 토큰", required = false, nullable = true) - authorization: String, - ): GetEbooksResponse + @Schema(description = "페이지", implementation = Int::class, required = true) + page: Int, + @Schema(description = "개수", implementation = Int::class, required = true) + count: Int, + @Schema(description = "검색할 전자책 제목", nullable = true) + title: String?, + @Schema(description = "검색할 판매자 식별자", implementation = UUID::class, nullable = true) + sellerMemberId: UUID?, + @Schema(description = "검색할 전자책 식별자 목록", type = "array", format = "uuid", nullable = true) + ebookIdList: List?, + @Schema(description = "검색할 카테고리 식별자 목록", type = "array", format = "uuid", nullable = true) + categoryIdList: List?, + @Schema(description = "정렬할 속성", implementation = EbookOrder::class, nullable = true) + orderBy: EbookOrder?, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", nullable = true) + authorization: String?, + ): PageResponse @Operation(summary = "전자책 상세 조회") @ApiResponses( @@ -81,9 +75,7 @@ interface EbookControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- EBOOK-400-24 : 전자책 식별자가 반드시 필요합니다.\n" + - "- EBOOK-400-16 : 잘못된 형식의 전자책 식별자입니다.", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -105,10 +97,10 @@ interface EbookControllerDocs { ] ) suspend fun getDetailOfEbook( - @Schema(description = "전자책 식별자", required = true, nullable = false) - ebookId: String, - @Schema(description = "액세스 토큰", required = false, nullable = true) - authorization: String, + @Schema(description = "전자책 식별자", implementation = UUID::class, required = true) + ebookId: UUID, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", nullable = true) + authorization: String?, ): GetDetailOfEbookResponse @Operation(summary = "전자책 등록") @@ -126,19 +118,7 @@ interface EbookControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- EBOOK-400-1 : PDF 식별자가 존재하지 않을 경우\n" + - "- EBOOK-400-2 : PDF 식별자가 UUID가 아닐 경우\n" + - "- EBOOK-400-3 : 전자책 제목이 비어있을 경우\n" + - "- EBOOK-400-4 : 관련 카테고리가 비어있을 경우\n" + - "- CATEGORY-400-1 : 카테고리 식별자가 UUID가 아닐 경우\n" + - "- EBOOK-400-20 : 메인 사진 식별자가 존재하지 않을 경우\n" + - "- EBOOK-400-21 : 메인 사진 식별자가 UUID가 아닐 경우\n" + - "- EBOOK-400-22 : 설명 사진 식별자가 존재하지 않을 경우\n" + - "- EBOOK-400-23 : 설명 사진 식별자가 UUID가 아닐 경우\n" + - "- EBOOK-400-5 : 가격이 0 ~ 9,999,999원이 아닐 경우\n" + - "- EBOOK-400-6 : 전자책 소개가 비어있을 경우\n" + - "- EBOOK-400-7 : 목차가 비어있을 경우", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -174,6 +154,7 @@ interface EbookControllerDocs { ) suspend fun createEbook( request: CreateEbookRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) authorization: String, ): CreateEbookResponse @@ -192,19 +173,7 @@ interface EbookControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- EBOOK-400-23 : 전자책 식별자가 존재하지 않을 경우\n" + - "- EBOOK-400-16 : 전자책 식별자가 UUID가 아닐 경우\n" + - "- EBOOK-400-3 : 전자책 제목이 null이 아니며 비어있을 경우\n" + - "- EBOOK-400-4 : 관련 카테고리가 null이 아니며 비어있을 경우\n" + - "- CATEGORY-400-1 : 관련 카테고리가 null이 아니며 카테고리 식별자가 UUID가 아닐 경우\n" + - "- EBOOK-400-20 : 메인 사진 식별자가 null이 아니며 존재하지 않을 경우\n" + - "- EBOOK-400-21 : 메인 사진 식별자가 null이 아니며 UUID가 아닐 경우\n" + - "- EBOOK-400-22 : 설명 사진 식별자가 null이 아니며 존재하지 않을 경우\n" + - "- EBOOK-400-23 : 설명 사진 식별자가 null이 아니며 UUID가 아닐 경우\n" + - "- EBOOK-400-5 : 가격이 null이 아니며 0 ~ 9,999,999원이 아닐 경우\n" + - "- EBOOK-400-6 : 전자책 소개가 null이 아니며 비어있을 경우\n" + - "- EBOOK-400-7 : 목차가 null이 아니며 비어있을 경우", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -238,10 +207,10 @@ interface EbookControllerDocs { ] ) suspend fun modifyEbook( - @Schema(description = "전자책 식별자", required = true, nullable = false) - ebookId: String, + @Schema(description = "전자책 식별자", implementation = UUID::class, required = true) + ebookId: UUID, request: ModifyEbookRequest, - @Schema(description = "액세스 토큰", required = true, nullable = false) + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) authorization: String, ): ModifyEbookResponse @@ -260,9 +229,7 @@ interface EbookControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- EBOOK-400-23 : 전자책 식별자가 존재하지 않을 경우\n" + - "- EBOOK-400-16 : 전자책 식별자가 UUID가 아닐 경우", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -295,9 +262,9 @@ interface EbookControllerDocs { ] ) suspend fun deleteEbook( - @Schema(description = "전자책 식별자", required = true, nullable = false) - ebookId: String, - @Schema(description = "액세스 토큰", required = true, nullable = false) + @Schema(description = "전자책 식별자", implementation = UUID::class, required = true) + ebookId: UUID, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) authorization: String, ): DeleteEbookResponse } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookImageControllerDocs.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookImageControllerDocs.kt new file mode 100644 index 0000000..ace77ad --- /dev/null +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookImageControllerDocs.kt @@ -0,0 +1,102 @@ +package com.devooks.backend.ebook.v1.controller.docs + +import com.devooks.backend.common.exception.ErrorResponse +import com.devooks.backend.ebook.v1.dto.request.SaveDescriptionImagesRequest +import com.devooks.backend.ebook.v1.dto.request.SaveMainImageRequest +import com.devooks.backend.ebook.v1.dto.response.SaveDescriptionImagesResponse +import com.devooks.backend.ebook.v1.dto.response.SaveMainImageResponse +import io.swagger.v3.oas.annotations.Operation +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 org.springframework.http.MediaType.APPLICATION_JSON_VALUE + +@Tag(name = "전자책 사진") +interface EbookImageControllerDocs { + + @Operation(summary = "전자책 설명 사진 저장") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = SaveDescriptionImagesResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "500", + description = "- COMMON-500-1: 사진 저장을 실패했습니다.\n" + + "- COMMON-500-2: 파일 저장을 실패했습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun saveDescriptionImages( + request: SaveDescriptionImagesRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): SaveDescriptionImagesResponse + + @Operation(summary = "전자책 메인 사진 저장") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = SaveMainImageResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "500", + description = "- COMMON-500-1: 사진 저장을 실패했습니다.\n" + + "- COMMON-500-2: 파일 저장을 실패했습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun saveMainImage( + request: SaveMainImageRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): SaveMainImageResponse +} diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookInquiryCommentControllerDocs.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookInquiryCommentControllerDocs.kt new file mode 100644 index 0000000..6856ea2 --- /dev/null +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookInquiryCommentControllerDocs.kt @@ -0,0 +1,197 @@ +package com.devooks.backend.ebook.v1.controller.docs + +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.exception.ErrorResponse +import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentView +import com.devooks.backend.ebook.v1.dto.request.CreateEbookInquiryCommentRequest +import com.devooks.backend.ebook.v1.dto.request.ModifyEbookInquiryCommentRequest +import com.devooks.backend.ebook.v1.dto.response.CreateEbookInquiryCommentResponse +import com.devooks.backend.ebook.v1.dto.response.DeleteEbookInquiryCommentResponse +import com.devooks.backend.ebook.v1.dto.response.ModifyEbookInquiryCommentResponse +import io.swagger.v3.oas.annotations.Operation +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 java.util.* +import org.springframework.http.MediaType.APPLICATION_JSON_VALUE + +@Tag(name = "전자책 문의 댓글") +interface EbookInquiryCommentControllerDocs { + + @Operation(summary = "전자책 문의 댓글 작성") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = CreateEbookInquiryCommentResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- EBOOK-404-2: 문의을 찾을 수 없습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun createEbookInquiryComment( + request: CreateEbookInquiryCommentRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): CreateEbookInquiryCommentResponse + + @Operation(summary = "전자책 문의 댓글 목록 조회") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun getEbookInquiryComments( + @Schema(description = "전자책 문의 식별자", required = true, implementation = UUID::class) + inquiryId: UUID, + @Schema(description = "페이지", implementation = Int::class, required = true) + page: Int, + @Schema(description = "개수", implementation = Int::class, required = true) + count: Int, + ): PageResponse + + @Operation(summary = "전자책 문의 댓글 수정") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ModifyEbookInquiryCommentResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- EBOOK-403-3: 자신이 작성한 댓글만 수정할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- EBOOK-404-3: 댓글을 찾을 수 없습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ) + ] + ) + suspend fun modifyEbookInquiryComment( + @Schema(description = "전자책 문의 댓글 식별자", required = true, implementation = UUID::class) + commentId: UUID, + request: ModifyEbookInquiryCommentRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): ModifyEbookInquiryCommentResponse + + @Operation(summary = "전자책 문의 댓글 삭제") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = DeleteEbookInquiryCommentResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- EBOOK-403-3: 자신이 작성한 댓글만 수정할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- EBOOK-404-3: 댓글을 찾을 수 없습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ) + ] + ) + suspend fun deleteEbookInquiryComment( + @Schema(description = "전자책 문의 댓글 식별자", required = true, implementation = UUID::class) + commentId: UUID, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): DeleteEbookInquiryCommentResponse + +} diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookInquiryControllerDocs.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookInquiryControllerDocs.kt new file mode 100644 index 0000000..bb930d8 --- /dev/null +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/controller/docs/EbookInquiryControllerDocs.kt @@ -0,0 +1,197 @@ +package com.devooks.backend.ebook.v1.controller.docs + +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.exception.ErrorResponse +import com.devooks.backend.ebook.v1.dto.EbookInquiryView +import com.devooks.backend.ebook.v1.dto.request.CreateEbookInquiryRequest +import com.devooks.backend.ebook.v1.dto.request.ModifyEbookInquiryRequest +import com.devooks.backend.ebook.v1.dto.response.CreateEbookInquiryResponse +import com.devooks.backend.ebook.v1.dto.response.DeleteEbookInquiryResponse +import com.devooks.backend.ebook.v1.dto.response.ModifyEbookInquiryResponse +import io.swagger.v3.oas.annotations.Operation +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 java.util.* +import org.springframework.http.MediaType.APPLICATION_JSON_VALUE + +@Tag(name = "전자책 문의") +interface EbookInquiryControllerDocs { + + @Operation(summary = "전자책 문의 작성") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = CreateEbookInquiryResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- EBOOK-404-1: 전자책을 찾을 수 없습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun createEbookInquiry( + request: CreateEbookInquiryRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): CreateEbookInquiryResponse + + @Operation(summary = "전자책 문의 목록 조회") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun getEbookInquiries( + @Schema(description = "전자책 식별자", implementation = UUID::class, required = true) + ebookId: UUID, + @Schema(description = "페이지", implementation = Int::class, required = true) + page: Int, + @Schema(description = "개수", implementation = Int::class, required = true) + count: Int, + ): PageResponse + + @Operation(summary = "전자책 문의 수정") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ModifyEbookInquiryResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- EBOOK-403-2: 자신이 작성한 문의만 수정할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- EBOOK-404-2: 문의을 찾을 수 없습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ) + ] + ) + suspend fun modifyEbookInquiry( + @Schema(description = "전자책 문의 식별자", required = true, implementation = UUID::class) + inquiryId: UUID, + request: ModifyEbookInquiryRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): ModifyEbookInquiryResponse + + @Operation(summary = "전자책 문의 삭제") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = DeleteEbookInquiryResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- EBOOK-403-2: 자신이 작성한 문의만 수정할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- EBOOK-404-2: 문의을 찾을 수 없습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ) + ] + ) + suspend fun deleteEbookInquiry( + @Schema(description = "전자책 문의 식별자", required = true, implementation = UUID::class) + inquiryId: UUID, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): DeleteEbookInquiryResponse + +} diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryCommentDto.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryCommentView.kt similarity index 56% rename from src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryCommentDto.kt rename to src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryCommentView.kt index 8babdc7..631e6cc 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryCommentDto.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryCommentView.kt @@ -1,20 +1,27 @@ package com.devooks.backend.ebook.v1.dto import com.devooks.backend.ebook.v1.domain.EbookInquiryComment +import io.swagger.v3.oas.annotations.media.Schema import java.time.Instant import java.util.* -data class EbookInquiryCommentDto( +data class EbookInquiryCommentView( + @Schema(description = "전자책 문의 댓글 식별자") val id: UUID, + @Schema(description = "내용") val content: String, + @Schema(description = "전자책 문의 식별자") val inquiryId: UUID, + @Schema(description = "작성자 식별자") val writerMemberId: UUID, + @Schema(description = "작성 날짜") val writtenDate: Instant, + @Schema(description = "수정 날짜") val modifiedDate: Instant, ) { companion object { - fun EbookInquiryComment.toDto() = - EbookInquiryCommentDto( + fun EbookInquiryComment.toEbookInquiryCommentView() = + EbookInquiryCommentView( id = this.id, content = this.content, inquiryId = this.inquiryId, diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryDto.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryView.kt similarity index 58% rename from src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryDto.kt rename to src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryView.kt index 6c832d1..43ad6f8 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryDto.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/EbookInquiryView.kt @@ -1,20 +1,27 @@ package com.devooks.backend.ebook.v1.dto import com.devooks.backend.ebook.v1.domain.EbookInquiry +import io.swagger.v3.oas.annotations.media.Schema import java.time.Instant import java.util.* -data class EbookInquiryDto( +data class EbookInquiryView( + @Schema(description = "전자책 문의 식별자") val id: UUID, + @Schema(description = "내용") val content: String, + @Schema(description = "전자책 식별자") val ebookId: UUID, + @Schema(description = "작성자 식별자") val writerMemberId: UUID, + @Schema(description = "작성 날짜") val writtenDate: Instant, + @Schema(description = "수정 날짜") val modifiedDate: Instant, ) { companion object { - fun EbookInquiry.toDto() = - EbookInquiryDto( + fun EbookInquiry.toEbookInquiryView() = + EbookInquiryView( id = this.id, content = this.content, ebookId = this.ebookId, diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/ReviewView.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/ReviewView.kt index dcd2bfd..9762062 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/ReviewView.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/ReviewView.kt @@ -5,6 +5,6 @@ import io.swagger.v3.oas.annotations.media.Schema data class ReviewView( @Schema(description = "평점") val rating: Double, - @Schema(description = "개수") + @Schema(description = "개수", implementation = Int::class) val count: Int ) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookCommand.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookCommand.kt index 6a18d70..c177f9b 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookCommand.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookCommand.kt @@ -1,17 +1,8 @@ package com.devooks.backend.ebook.v1.dto.command -import com.devooks.backend.wishlist.v1.error.validateEbookId import java.util.* class DeleteEbookCommand( val ebookId: UUID, val requesterId: UUID, -) { - constructor( - ebookId: String, - requesterId: UUID, - ) : this( - ebookId = ebookId.validateEbookId(), - requesterId = requesterId - ) -} +) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookInquiryCommand.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookInquiryCommand.kt index f3ae696..563498e 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookInquiryCommand.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookInquiryCommand.kt @@ -1,17 +1,8 @@ package com.devooks.backend.ebook.v1.dto.command -import com.devooks.backend.ebook.v1.error.validateEbookInquiryId import java.util.* class DeleteEbookInquiryCommand( val inquiryId: UUID, val requesterId: UUID, -) { - constructor( - inquiryId: String, - requesterId: UUID, - ) : this( - inquiryId = inquiryId.validateEbookInquiryId(), - requesterId = requesterId, - ) -} +) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookInquiryCommentCommand.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookInquiryCommentCommand.kt index bd61e16..304d764 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookInquiryCommentCommand.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/DeleteEbookInquiryCommentCommand.kt @@ -1,17 +1,8 @@ package com.devooks.backend.ebook.v1.dto.command -import com.devooks.backend.ebook.v1.error.validateEbookInquiryCommentId import java.util.* class DeleteEbookInquiryCommentCommand( val commentId: UUID, val requesterId: UUID, -) { - constructor( - commentId: String, - requesterId: UUID, - ) : this( - commentId = commentId.validateEbookInquiryCommentId(), - requesterId = requesterId - ) -} +) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetDetailOfEbookCommand.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetDetailOfEbookCommand.kt index eee0da2..9095abe 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetDetailOfEbookCommand.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetDetailOfEbookCommand.kt @@ -1,17 +1,8 @@ package com.devooks.backend.ebook.v1.dto.command -import com.devooks.backend.wishlist.v1.error.validateEbookId import java.util.* class GetDetailOfEbookCommand( val ebookId: UUID, val requesterId: UUID?, -) { - constructor( - ebookId: String, - requesterId: UUID?, - ) : this( - ebookId = ebookId.validateEbookId(), - requesterId = requesterId - ) -} \ No newline at end of file +) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookCommand.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookCommand.kt index 48f584f..bdae430 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookCommand.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookCommand.kt @@ -2,15 +2,11 @@ package com.devooks.backend.ebook.v1.dto.command import com.devooks.backend.common.dto.Paging import com.devooks.backend.ebook.v1.domain.EbookOrder -import com.devooks.backend.ebook.v1.domain.EbookOrder.Companion.toEbookOrder -import com.devooks.backend.ebook.v1.error.validateEbookIds -import com.devooks.backend.member.v1.error.validateMemberId -import com.devooks.backend.wishlist.v1.error.validateCategoryIds import java.util.* class GetEbookCommand( val title: String?, - val sellingMemberId: UUID?, + val sellerMemberId: UUID?, val ebookIdList: List?, val categoryIdList: List?, val orderBy: EbookOrder, @@ -19,20 +15,20 @@ class GetEbookCommand( ) { constructor( - title: String, - sellingMemberId: String, - ebookIdList: List, - categoryIdList: List, - orderBy: String, + title: String?, + sellerMemberId: UUID?, + ebookIdList: List?, + categoryIdList: List?, + orderBy: EbookOrder?, requesterId: UUID?, - page: String, - count: String, + page: Int, + count: Int, ) : this( - title = title.takeIf { it.isNotBlank() }?.let { "%$title%" }, - sellingMemberId = sellingMemberId.takeIf { it.isNotBlank() }?.let { it.validateMemberId() }, - ebookIdList = ebookIdList.takeIf { it.isNotEmpty() }?.validateEbookIds(), - categoryIdList = categoryIdList.takeIf { it.isNotEmpty() }?.validateCategoryIds(), - orderBy = orderBy.takeIf { it.isNotBlank() }?.toEbookOrder() ?: EbookOrder.LATEST, + title = title?.let { "%$title%" }, + sellerMemberId = sellerMemberId, + ebookIdList = ebookIdList, + categoryIdList = categoryIdList, + orderBy = orderBy ?: EbookOrder.LATEST, requesterId = requesterId, paging = Paging(page, count) ) @@ -43,4 +39,5 @@ class GetEbookCommand( val limit: Int get() = paging.limit + val pageable = paging.value } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookInquireCommentsCommand.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookInquireCommentsCommand.kt index 42030dc..c0e61e1 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookInquireCommentsCommand.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookInquireCommentsCommand.kt @@ -1,7 +1,6 @@ package com.devooks.backend.ebook.v1.dto.command import com.devooks.backend.common.dto.Paging -import com.devooks.backend.ebook.v1.error.validateEbookInquiryId import java.util.* import org.springframework.data.domain.Pageable @@ -10,11 +9,11 @@ class GetEbookInquireCommentsCommand( private val paging: Paging, ) { constructor( - inquiryId: String, - page: String, - count: String, + inquiryId: UUID, + page: Int, + count: Int, ) : this( - inquiryId = inquiryId.validateEbookInquiryId(), + inquiryId = inquiryId, paging = Paging(page, count), ) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookInquiresCommand.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookInquiresCommand.kt index 53dcd62..94902e4 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookInquiresCommand.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/GetEbookInquiresCommand.kt @@ -1,7 +1,6 @@ package com.devooks.backend.ebook.v1.dto.command import com.devooks.backend.common.dto.Paging -import com.devooks.backend.wishlist.v1.error.validateEbookId import java.util.* import org.springframework.data.domain.Pageable @@ -10,11 +9,11 @@ class GetEbookInquiresCommand( private val paging: Paging, ) { constructor( - ebookId: String, - page: String, - count: String, + ebookId: UUID, + page: Int, + count: Int, ) : this( - ebookId = ebookId.validateEbookId(), + ebookId = ebookId, paging = Paging(page, count) ) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/ModifyEbookCommand.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/ModifyEbookCommand.kt index a5b7f26..d4c4c65 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/ModifyEbookCommand.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/command/ModifyEbookCommand.kt @@ -15,6 +15,5 @@ class ModifyEbookCommand( ) { val isChangedEbook: Boolean = title != null || mainImageId != null || introduction != null || tableOfContents != null || price != null - val isChangedDescriptionImageList: Boolean = descriptionImageIdList != null val isChangedRelatedCategoryIdList: Boolean = relatedCategoryIdList != null } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/CreateEbookInquiryRequest.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/CreateEbookInquiryRequest.kt index 5714287..8b4119f 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/CreateEbookInquiryRequest.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/CreateEbookInquiryRequest.kt @@ -1,18 +1,21 @@ package com.devooks.backend.ebook.v1.dto.request import com.devooks.backend.ebook.v1.dto.command.CreateEbookInquiryCommand -import com.devooks.backend.ebook.v1.error.validateEbookInquiryContent -import com.devooks.backend.wishlist.v1.error.validateEbookId +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank import java.util.* data class CreateEbookInquiryRequest( - val ebookId: String?, - val content: String?, + @Schema(description = "전자책 식별자", implementation = UUID::class, required = true) + val ebookId: UUID, + @field:NotBlank + @Schema(description = "내용", required = true) + val content: String, ) { fun toCommand(requesterId: UUID) = CreateEbookInquiryCommand( - ebookId = ebookId.validateEbookId(), - content = content.validateEbookInquiryContent(), + ebookId = ebookId, + content = content, requesterId = requesterId ) } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/CreateEbookRequest.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/CreateEbookRequest.kt index 97916d9..d27dc03 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/CreateEbookRequest.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/CreateEbookRequest.kt @@ -1,45 +1,48 @@ package com.devooks.backend.ebook.v1.dto.request import com.devooks.backend.ebook.v1.dto.command.CreateEbookCommand -import com.devooks.backend.ebook.v1.error.validateDescriptionImageIdList -import com.devooks.backend.ebook.v1.error.validateEbookIntroduction -import com.devooks.backend.ebook.v1.error.validateEbookPrice -import com.devooks.backend.ebook.v1.error.validateEbookTitle -import com.devooks.backend.ebook.v1.error.validateMainImageId -import com.devooks.backend.ebook.v1.error.validatePdfId -import com.devooks.backend.ebook.v1.error.validateRelatedCategoryList -import com.devooks.backend.ebook.v1.error.validateTableOfContents import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.Size import java.util.* data class CreateEbookRequest( - @Schema(description = "PDF 식별자", required = true, nullable = false) - val pdfId: String?, - @Schema(description = "제목", required = true, nullable = false) - val title: String?, - @Schema(description = "카테고리 식별자 목록", required = true, nullable = false) - val relatedCategoryIdList: List?, - @Schema(description = "메인 사진 식별자", required = true, nullable = false) - val mainImageId: String?, - @Schema(description = "설명 사진 식별자 목록", required = true, nullable = false) - val descriptionImageIdList: List?, - @Schema(description = "가격", required = true, nullable = false) - val price: Int?, - @Schema(description = "소개", required = true, nullable = false) - val introduction: String?, - @Schema(description = "목차", required = true, nullable = false) - val tableOfContents: String?, + @Schema(description = "PDF 식별자", required = true, implementation = UUID::class) + val pdfId: UUID, + @field:Size(min = 1, max = 30) + @Schema(description = "전자책 제목", required = true) + val title: String, + @field:NotEmpty + @Schema(description = "카테고리 식별자 목록", required = true) + val relatedCategoryIdList: List, + @Schema(description = "메인 사진 식별자", required = true, implementation = UUID::class) + val mainImageId: UUID, + @Schema(description = "설명 사진 식별자 목록", required = true) + val descriptionImageIdList: List, + @field:Min(0) + @field:Max(10_000_000) + @Schema(description = "가격", required = true) + val price: Int, + @field:NotBlank + @Schema(description = "소개", required = true) + val introduction: String, + @field:NotBlank + @Schema(description = "목차", required = true) + val tableOfContents: String, ) { fun toCommand(requesterId: UUID): CreateEbookCommand = CreateEbookCommand( - pdfId = pdfId.validatePdfId(), - title = title.validateEbookTitle(), - relatedCategoryIdList = relatedCategoryIdList.validateRelatedCategoryList(), - mainImageId = mainImageId.validateMainImageId(), - descriptionImageIdList = descriptionImageIdList.validateDescriptionImageIdList(), - price = price.validateEbookPrice(), - introduction = introduction.validateEbookIntroduction(), - tableOfContents = tableOfContents.validateTableOfContents(), + pdfId = pdfId, + title = title, + relatedCategoryIdList = relatedCategoryIdList, + mainImageId = mainImageId, + descriptionImageIdList = descriptionImageIdList, + price = price, + introduction = introduction, + tableOfContents = tableOfContents, sellingMemberId = requesterId ) } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/EbookInquiryCommentRequest.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/EbookInquiryCommentRequest.kt index 29b588b..296c317 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/EbookInquiryCommentRequest.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/EbookInquiryCommentRequest.kt @@ -1,19 +1,22 @@ package com.devooks.backend.ebook.v1.dto.request import com.devooks.backend.ebook.v1.dto.command.CreateEbookInquiryCommentCommand -import com.devooks.backend.ebook.v1.error.validateEbookInquiryCommentContent -import com.devooks.backend.ebook.v1.error.validateEbookInquiryId +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank import java.util.* data class CreateEbookInquiryCommentRequest( - val inquiryId: String?, - val content: String?, + @Schema(description = "전자책 문의 식별자", required = true, implementation = UUID::class) + val inquiryId: UUID, + @field:NotBlank + @Schema(description = "내용", required = true) + val content: String, ) { fun toCommand(requesterId: UUID) = CreateEbookInquiryCommentCommand( - inquiryId = inquiryId.validateEbookInquiryId(), - content = content.validateEbookInquiryCommentContent(), + inquiryId = inquiryId, + content = content, requesterId = requesterId ) -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookInquiryCommentRequest.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookInquiryCommentRequest.kt index bbc320a..6bd6c9d 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookInquiryCommentRequest.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookInquiryCommentRequest.kt @@ -1,17 +1,17 @@ package com.devooks.backend.ebook.v1.dto.request import com.devooks.backend.ebook.v1.dto.command.ModifyEbookInquiryCommentCommand -import com.devooks.backend.ebook.v1.error.validateEbookInquiryCommentContent -import com.devooks.backend.ebook.v1.error.validateEbookInquiryCommentId +import jakarta.validation.constraints.NotBlank import java.util.* data class ModifyEbookInquiryCommentRequest( - val content: String?, + @field:NotBlank + val content: String, ) { - fun toCommand(commentId: String, requesterId: UUID) = + fun toCommand(commentId: UUID, requesterId: UUID) = ModifyEbookInquiryCommentCommand( - content = content.validateEbookInquiryCommentContent(), - commentId = commentId.validateEbookInquiryCommentId(), + content = content, + commentId = commentId, requesterId = requesterId ) } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookInquiryRequest.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookInquiryRequest.kt index 82335d5..511f154 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookInquiryRequest.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookInquiryRequest.kt @@ -1,17 +1,17 @@ package com.devooks.backend.ebook.v1.dto.request import com.devooks.backend.ebook.v1.dto.command.ModifyEbookInquiryCommand -import com.devooks.backend.ebook.v1.error.validateEbookInquiryContent -import com.devooks.backend.ebook.v1.error.validateEbookInquiryId +import io.swagger.v3.oas.annotations.media.Schema import java.util.* data class ModifyEbookInquiryRequest( - val content: String?, + @Schema(description = "내용", required = true) + val content: String, ) { - fun toCommand(inquiryId: String, requesterId: UUID) = + fun toCommand(inquiryId: UUID, requesterId: UUID) = ModifyEbookInquiryCommand( - content = content.validateEbookInquiryContent(), - inquiryId = inquiryId.validateEbookInquiryId(), + content = content, + inquiryId = inquiryId, requesterId = requesterId, ) } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookRequest.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookRequest.kt index fe99740..7895962 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookRequest.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/ModifyEbookRequest.kt @@ -1,54 +1,45 @@ package com.devooks.backend.ebook.v1.dto.request import com.devooks.backend.ebook.v1.dto.command.ModifyEbookCommand -import com.devooks.backend.ebook.v1.error.EbookError -import com.devooks.backend.ebook.v1.error.validateDescriptionImageIdList -import com.devooks.backend.ebook.v1.error.validateEbookIntroduction -import com.devooks.backend.ebook.v1.error.validateEbookPrice -import com.devooks.backend.ebook.v1.error.validateEbookTitle -import com.devooks.backend.ebook.v1.error.validateMainImageId -import com.devooks.backend.ebook.v1.error.validateRelatedCategoryList -import com.devooks.backend.ebook.v1.error.validateTableOfContents -import com.devooks.backend.wishlist.v1.error.validateEbookId import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.Size import java.util.* data class ModifyEbookRequest( - @Schema(description = "수정할 내용 (값이 존재하지 않을 경우 수정하지 않음)") - val ebook: Ebook?, + @field:Size(min = 1, max = 30) + @Schema(description = "전자책 제목", nullable = true) + val title: String?, + @field:Size(min = 1) + @Schema(description = "카테고리 식별자 목록", nullable = true) + val relatedCategoryIdList: List?, + @Schema(description = "메인 사진 식별자", nullable = true) + val mainImageId: UUID?, + @Schema(description = "설명 사진 식별자 목록", nullable = true) + val descriptionImageIdList: List?, + @field:Size(min = 1) + @Schema(description = "소개", nullable = true) + val introduction: String?, + @field:Size(min = 1) + @Schema(description = "목차", nullable = true) + val tableOfContents: String?, + @field:Min(0) + @field:Max(10_000_000) + @Schema(description = "가격", nullable = true) + val price: Int?, ) { - data class Ebook( - @Schema(description = "전자책 이름", required = false, nullable = true) - val title: String? = null, - @Schema(description = "카테고리 식별자 목록", required = false, nullable = true) - val relatedCategoryIdList: List? = null, - @Schema(description = "메인 사진 식별자", required = false, nullable = true) - val mainImageId: String? = null, - @Schema(description = "설명 사진 식별자 목록", required = false, nullable = true) - val descriptionImageIdList: List? = null, - @Schema(description = "소개", required = false, nullable = true) - val introduction: String? = null, - @Schema(description = "목차", required = false, nullable = true) - val tableOfContents: String? = null, - @Schema(description = "가격", required = false, nullable = true) - val price: Int? = null, - ) - - fun toCommand(ebookId: String, requesterId: UUID): ModifyEbookCommand = - if (ebook != null) { - ModifyEbookCommand( - ebookId = ebookId.validateEbookId(), - title = ebook.title?.validateEbookTitle(), - relatedCategoryIdList = ebook.relatedCategoryIdList?.validateRelatedCategoryList(), - mainImageId = ebook.mainImageId?.validateMainImageId(), - descriptionImageIdList = ebook.descriptionImageIdList?.validateDescriptionImageIdList(), - introduction = ebook.introduction?.validateEbookIntroduction(), - tableOfContents = ebook.tableOfContents?.validateTableOfContents(), - price = ebook.price?.validateEbookPrice(), - requesterId = requesterId - ) - } else { - throw EbookError.REQUIRED_EBOOK_FOR_MODIFY.exception - } + fun toCommand(ebookId: UUID, requesterId: UUID): ModifyEbookCommand = + ModifyEbookCommand( + ebookId = ebookId, + title = title, + relatedCategoryIdList = relatedCategoryIdList, + mainImageId = mainImageId, + descriptionImageIdList = descriptionImageIdList, + introduction = introduction, + tableOfContents = tableOfContents, + price = price, + requesterId = requesterId + ) } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/SaveDescriptionImagesRequest.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/SaveDescriptionImagesRequest.kt index 7738991..66ff2ae 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/SaveDescriptionImagesRequest.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/SaveDescriptionImagesRequest.kt @@ -1,17 +1,21 @@ package com.devooks.backend.ebook.v1.dto.request import com.devooks.backend.common.dto.ImageDto -import com.devooks.backend.common.error.validateImages +import com.devooks.backend.common.dto.ImageDto.Companion.toDomain import com.devooks.backend.ebook.v1.domain.EbookImageType.DESCRIPTION import com.devooks.backend.ebook.v1.dto.command.SaveImagesCommand +import jakarta.validation.Valid +import jakarta.validation.constraints.NotEmpty import java.util.* data class SaveDescriptionImagesRequest( - val imageList: List? + @field:NotEmpty + @field:Valid + val imageList: List, ) { fun toCommand(requesterId: UUID) = SaveImagesCommand( - imageList = imageList.validateImages(), + imageList = imageList.toDomain(), requesterId = requesterId, imageType = DESCRIPTION ) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/SaveMainImageRequest.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/SaveMainImageRequest.kt index deefb37..ec67756 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/SaveMainImageRequest.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/request/SaveMainImageRequest.kt @@ -1,37 +1,21 @@ package com.devooks.backend.ebook.v1.dto.request -import com.devooks.backend.common.domain.Image -import com.devooks.backend.common.domain.Image.Companion.validateByteSize -import com.devooks.backend.common.domain.ImageExtension.Companion.validateImageExtension -import com.devooks.backend.common.error.CommonError -import com.devooks.backend.common.error.validateNotBlank +import com.devooks.backend.common.dto.ImageDto import com.devooks.backend.ebook.v1.domain.EbookImageType.MAIN import com.devooks.backend.ebook.v1.dto.command.SaveImagesCommand -import com.devooks.backend.ebook.v1.error.EbookError +import jakarta.validation.Valid import java.util.* data class SaveMainImageRequest( - val image: MainImageDto?, + @field:Valid + val image: ImageDto, ) { - data class MainImageDto( - val base64Raw: String?, - val extension: String?, - val byteSize: Long?, - ) { - fun toCommand() = - Image( - base64Raw = base64Raw.validateNotBlank(CommonError.REQUIRED_BASE64RAW.exception), - extension = extension.validateImageExtension(), - byteSize = byteSize.validateByteSize(), - order = 1 - ) - } - fun toCommand(requesterId: UUID) = SaveImagesCommand( - imageList = listOf(image?.toCommand() ?: throw EbookError.REQUIRED_MAIN_IMAGE.exception), + imageList = listOf(image.toDomain()), requesterId = requesterId, imageType = MAIN ) + } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/CreateEbookInquiryCommentResponse.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/CreateEbookInquiryCommentResponse.kt index 47f9abc..9b76f9d 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/CreateEbookInquiryCommentResponse.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/CreateEbookInquiryCommentResponse.kt @@ -1,14 +1,14 @@ package com.devooks.backend.ebook.v1.dto.response import com.devooks.backend.ebook.v1.domain.EbookInquiryComment -import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentDto -import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentDto.Companion.toDto +import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentView +import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentView.Companion.toEbookInquiryCommentView data class CreateEbookInquiryCommentResponse( - val comment: EbookInquiryCommentDto, + val comment: EbookInquiryCommentView, ) { companion object { fun EbookInquiryComment.toCreateEbookInquiryCommentResponse() = - CreateEbookInquiryCommentResponse(toDto()) + CreateEbookInquiryCommentResponse(toEbookInquiryCommentView()) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/CreateEbookInquiryResponse.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/CreateEbookInquiryResponse.kt index 9cc81ae..0681fb1 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/CreateEbookInquiryResponse.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/CreateEbookInquiryResponse.kt @@ -1,14 +1,14 @@ package com.devooks.backend.ebook.v1.dto.response import com.devooks.backend.ebook.v1.domain.EbookInquiry -import com.devooks.backend.ebook.v1.dto.EbookInquiryDto -import com.devooks.backend.ebook.v1.dto.EbookInquiryDto.Companion.toDto +import com.devooks.backend.ebook.v1.dto.EbookInquiryView +import com.devooks.backend.ebook.v1.dto.EbookInquiryView.Companion.toEbookInquiryView data class CreateEbookInquiryResponse( - val ebookInquiry: EbookInquiryDto, + val ebookInquiry: EbookInquiryView, ) { companion object { fun EbookInquiry.toCreateEbookInquiryResponse() = - CreateEbookInquiryResponse(toDto()) + CreateEbookInquiryResponse(toEbookInquiryView()) } } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/DeleteEbookInquiryCommentResponse.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/DeleteEbookInquiryCommentResponse.kt index d0092a4..73c0ddf 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/DeleteEbookInquiryCommentResponse.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/DeleteEbookInquiryCommentResponse.kt @@ -1,5 +1,8 @@ package com.devooks.backend.ebook.v1.dto.response +import io.swagger.v3.oas.annotations.media.Schema + data class DeleteEbookInquiryCommentResponse( + @Schema(description = "결과 메시지", example = "댓글 삭제를 완료했습니다.") val message: String = "댓글 삭제를 완료했습니다." ) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/DeleteEbookInquiryResponse.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/DeleteEbookInquiryResponse.kt index 35933b1..011d4a6 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/DeleteEbookInquiryResponse.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/DeleteEbookInquiryResponse.kt @@ -1,5 +1,8 @@ package com.devooks.backend.ebook.v1.dto.response +import io.swagger.v3.oas.annotations.media.Schema + data class DeleteEbookInquiryResponse( + @Schema(description = "결과 메시지", example = "문의 삭제를 완료했습니다.") val message: String = "문의 삭제를 완료했습니다." ) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/GetEbookInquiriesResponse.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/GetEbookInquiriesResponse.kt deleted file mode 100644 index 5216602..0000000 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/GetEbookInquiriesResponse.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.devooks.backend.ebook.v1.dto.response - -import com.devooks.backend.ebook.v1.domain.EbookInquiry -import com.devooks.backend.ebook.v1.dto.EbookInquiryDto -import com.devooks.backend.ebook.v1.dto.EbookInquiryDto.Companion.toDto - -data class GetEbookInquiriesResponse( - val ebookInquiryList: List, -) { - companion object { - fun List.toGetEbookInquiriesResponse() = - GetEbookInquiriesResponse(map { it.toDto() }) - } -} diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/GetEbookInquiryCommentsResponse.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/GetEbookInquiryCommentsResponse.kt deleted file mode 100644 index b38c824..0000000 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/GetEbookInquiryCommentsResponse.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.devooks.backend.ebook.v1.dto.response - -import com.devooks.backend.ebook.v1.domain.EbookInquiryComment -import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentDto -import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentDto.Companion.toDto - -data class GetEbookInquiryCommentsResponse( - val comments: List, -) { - companion object { - fun List.toGetEbookInquiryCommentsResponse() = - GetEbookInquiryCommentsResponse(map { it.toDto() }) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/GetEbooksResponse.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/GetEbooksResponse.kt deleted file mode 100644 index fdfd2fb..0000000 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/GetEbooksResponse.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.devooks.backend.ebook.v1.dto.response - -import com.devooks.backend.ebook.v1.dto.EbookView -import com.devooks.backend.ebook.v1.dto.EbookView.Companion.toEbookView -import com.devooks.backend.ebook.v1.repository.row.EbookRow - -data class GetEbooksResponse( - val ebookList: List, -) { - companion object { - fun List.toGetEbooksResponse() = - GetEbooksResponse(this.map { it.toEbookView() }) - } -} diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/ModifyEbookInquiryCommentResponse.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/ModifyEbookInquiryCommentResponse.kt index 167a9f9..a16c5ee 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/ModifyEbookInquiryCommentResponse.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/ModifyEbookInquiryCommentResponse.kt @@ -1,15 +1,15 @@ package com.devooks.backend.ebook.v1.dto.response import com.devooks.backend.ebook.v1.domain.EbookInquiryComment -import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentDto -import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentDto.Companion.toDto +import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentView +import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentView.Companion.toEbookInquiryCommentView data class ModifyEbookInquiryCommentResponse( - val comment: EbookInquiryCommentDto, + val comment: EbookInquiryCommentView, ) { companion object { fun EbookInquiryComment.toModifyEbookInquiryCommentResponse() = - ModifyEbookInquiryCommentResponse(toDto()) + ModifyEbookInquiryCommentResponse(toEbookInquiryCommentView()) } } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/ModifyEbookInquiryResponse.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/ModifyEbookInquiryResponse.kt index 522efc9..15c5886 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/ModifyEbookInquiryResponse.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/ModifyEbookInquiryResponse.kt @@ -1,14 +1,14 @@ package com.devooks.backend.ebook.v1.dto.response import com.devooks.backend.ebook.v1.domain.EbookInquiry -import com.devooks.backend.ebook.v1.dto.EbookInquiryDto -import com.devooks.backend.ebook.v1.dto.EbookInquiryDto.Companion.toDto +import com.devooks.backend.ebook.v1.dto.EbookInquiryView +import com.devooks.backend.ebook.v1.dto.EbookInquiryView.Companion.toEbookInquiryView data class ModifyEbookInquiryResponse( - val ebookInquiry: EbookInquiryDto + val ebookInquiry: EbookInquiryView ) { companion object { fun EbookInquiry.toModifyEbookInquiryResponse() = - ModifyEbookInquiryResponse(toDto()) + ModifyEbookInquiryResponse(toEbookInquiryView()) } } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/SaveMainImageResponse.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/SaveMainImageResponse.kt index a2a7d1e..94ee6f6 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/SaveMainImageResponse.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/dto/response/SaveMainImageResponse.kt @@ -1,24 +1,14 @@ package com.devooks.backend.ebook.v1.dto.response import com.devooks.backend.ebook.v1.domain.EbookImage -import java.util.* -import kotlin.io.path.pathString +import com.devooks.backend.ebook.v1.dto.EbookImageDto +import com.devooks.backend.ebook.v1.dto.EbookImageDto.Companion.toDto data class SaveMainImageResponse( - val mainImage: MainImageDto, + val mainImage: EbookImageDto, ) { - data class MainImageDto( - val id: UUID, - val imagePath: String, - ) - companion object { fun EbookImage.toSaveMainImageResponse() = - SaveMainImageResponse( - MainImageDto( - id = id, - imagePath = imagePath.pathString - ) - ) + SaveMainImageResponse(toDto()) } } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/error/EbookError.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/error/EbookError.kt index f6eb985..bc3bc3c 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/error/EbookError.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/error/EbookError.kt @@ -7,28 +7,10 @@ import org.springframework.http.HttpStatus.NOT_FOUND enum class EbookError(val exception: GeneralException) { // 400 - REQUIRED_PDF_ID(GeneralException("EBOOK-400-1", BAD_REQUEST, "PDF 식별자가 반드시 필요합니다.")), - INVALID_PDF_ID(GeneralException("EBOOK-400-2", BAD_REQUEST, "잘못된 형식의 PDF 식별자 입니다.")), - REQUIRED_TITLE(GeneralException("EBOOK-400-3", BAD_REQUEST, "전자책 제목이 반드시 필요합니다.")), - REQUIRED_RELATED_CATEGORY_LIST(GeneralException("EBOOK-400-4", BAD_REQUEST, "관련 카테고리가 반드시 필요합니다.")), INVALID_EBOOK_PRICE(GeneralException("EBOOK-400-5", BAD_REQUEST, "유효하지 않은 가격입니다.")), - REQUIRED_EBOOK_INTRODUCTION(GeneralException("EBOOK-400-6", BAD_REQUEST, "전자책 소개가 반드시 필요합니다.")), - REQUIRED_TABLE_OF_CONTENTS(GeneralException("EBOOK-400-7", BAD_REQUEST, "목차가 반드시 필요합니다.")), INVALID_TOP_100(GeneralException("EBOOK-400-8", BAD_REQUEST, "잘못된 형식의 TOP100(ex. DAILY, WEEKLY, MONTHLY) 입니다.")), INVALID_EBOOK_ORDER(GeneralException("EBOOK-400-9", BAD_REQUEST, "잘못된 형식의 EbookOrder(ex. LATEST, REVIEW) 입니다.")), - REQUIRED_EBOOK_INQUIRY_CONTENT(GeneralException("EBOOK-400-10", BAD_REQUEST, "문의 내용이 반드시 필요합니다.")), - REQUIRED_EBOOK_INQUIRY_ID(GeneralException("EBOOK-400-11", BAD_REQUEST, "문의 식별자가 반드시 필요합니다.")), - INVALID_EBOOK_INQUIRY_ID(GeneralException("EBOOK-400-12", BAD_REQUEST, "잘못된 형식의 문의 식별자입니다.")), - REQUIRED_EBOOK_INQUIRY_COMMENT_CONTENT(GeneralException("EBOOK-400-13", BAD_REQUEST, "댓글 내용이 반드시 필요합니다.")), - REQUIRED_EBOOK_INQUIRY_COMMENT_ID(GeneralException("EBOOK-400-14", BAD_REQUEST, "댓글 식별자가 반드시 필요합니다.")), - INVALID_EBOOK_INQUIRY_COMMENT_ID(GeneralException("EBOOK-400-15", BAD_REQUEST, "잘못된 형식의 댓글 식별자입니다.")), INVALID_EBOOK_ID(GeneralException("EBOOK-400-16", BAD_REQUEST, "잘못된 형식의 전자책 식별자입니다.")), - REQUIRED_EBOOK_FOR_MODIFY(GeneralException("EBOOK-400-17", BAD_REQUEST, "전자책이 반드시 필요합니다.")), - REQUIRED_MAIN_IMAGE(GeneralException("EBOOK-400-18", BAD_REQUEST, "메인 사진이 반드시 필요합니다.")), - REQUIRED_MAIN_IMAGE_ID(GeneralException("EBOOK-400-19", BAD_REQUEST, "메인 사진 식별자가 반드시 필요합니다.")), - INVALID_MAIN_IMAGE_ID(GeneralException("EBOOK-400-20", BAD_REQUEST, "잘못된 형식의 메인 사진 식별자입니다.")), - REQUIRED_DESCRIPTION_IMAGE_ID(GeneralException("EBOOK-400-21", BAD_REQUEST, "설명 사진 식별자가 반드시 필요합니다.")), - INVALID_DESCRIPTION_IMAGE_ID(GeneralException("EBOOK-400-22", BAD_REQUEST, "잘못된 형식의 설명 사진 식별자입니다.")), REQUIRED_EBOOK_ID(GeneralException("EBOOK-400-23", BAD_REQUEST, "전자책 식별자가 반드시 필요합니다.")), // 403 @@ -40,9 +22,9 @@ enum class EbookError(val exception: GeneralException) { FORBIDDEN_DELETE_EBOOK(GeneralException("EBOOK-403-6", FORBIDDEN, "자신이 등록한 전자책만 삭제할 수 있습니다.")), // 404 - NOT_FOUND_EBOOK(GeneralException("EBOOK-404-1", NOT_FOUND, "전자책을 찾을 수 없습니다")), - NOT_FOUND_EBOOK_INQUIRY(GeneralException("EBOOK-404-2", NOT_FOUND, "문의을 찾을 수 없습니다")), - NOT_FOUND_EBOOK_INQUIRY_COMMENT(GeneralException("EBOOK-404-3", NOT_FOUND, "댓글을 찾을 수 없습니다")), - NOT_FOUND_MAIN_IMAGE(GeneralException("EBOOK-404-4", NOT_FOUND, "메인 사진을 찾을 수 없습니다")), - NOT_FOUND_DESCRIPTION_IMAGE(GeneralException("EBOOK-404-5", NOT_FOUND, "설명 사진을 찾을 수 없습니다")), + NOT_FOUND_EBOOK(GeneralException("EBOOK-404-1", NOT_FOUND, "전자책을 찾을 수 없습니다.")), + NOT_FOUND_EBOOK_INQUIRY(GeneralException("EBOOK-404-2", NOT_FOUND, "문의을 찾을 수 없습니다.")), + NOT_FOUND_EBOOK_INQUIRY_COMMENT(GeneralException("EBOOK-404-3", NOT_FOUND, "댓글을 찾을 수 없습니다.")), + NOT_FOUND_MAIN_IMAGE(GeneralException("EBOOK-404-4", NOT_FOUND, "메인 사진을 찾을 수 없습니다.")), + NOT_FOUND_DESCRIPTION_IMAGE(GeneralException("EBOOK-404-5", NOT_FOUND, "설명 사진을 찾을 수 없습니다.")), } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/error/EbookValidation.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/error/EbookValidation.kt index 6b7d617..16f01b4 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/error/EbookValidation.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/error/EbookValidation.kt @@ -1,54 +1,6 @@ package com.devooks.backend.ebook.v1.error -import com.devooks.backend.category.v1.error.CategoryError -import com.devooks.backend.common.error.validateNotBlank -import com.devooks.backend.common.error.validateNotEmpty -import com.devooks.backend.common.error.validateNotNull -import com.devooks.backend.common.error.validateUUID -import java.util.* - -fun String?.validatePdfId(): UUID = - validateNotNull(EbookError.REQUIRED_PDF_ID.exception) - .validateUUID(EbookError.INVALID_PDF_ID.exception) - -fun String?.validateEbookTitle(): String = - validateNotBlank(EbookError.REQUIRED_TITLE.exception) - -fun List?.validateRelatedCategoryList(): List = - validateNotEmpty(EbookError.REQUIRED_RELATED_CATEGORY_LIST.exception) - .map { it.validateUUID(CategoryError.INVALID_CATEGORY_ID.exception) } - fun Int?.validateEbookPrice(): Int = takeIf { it != null && it in 0..9_999_999 } ?: throw EbookError.INVALID_EBOOK_PRICE.exception -fun String?.validateEbookIntroduction(): String = - validateNotBlank(EbookError.REQUIRED_EBOOK_INTRODUCTION.exception) - -fun String?.validateTableOfContents(): String = - validateNotBlank(EbookError.REQUIRED_TABLE_OF_CONTENTS.exception) - -fun String?.validateEbookInquiryContent(): String = - validateNotBlank(EbookError.REQUIRED_EBOOK_INQUIRY_CONTENT.exception) - -fun String?.validateEbookInquiryId(): UUID = - validateNotBlank(EbookError.REQUIRED_EBOOK_INQUIRY_ID.exception) - .validateUUID(EbookError.INVALID_EBOOK_INQUIRY_ID.exception) - -fun String?.validateEbookInquiryCommentContent(): String = - validateNotBlank(EbookError.REQUIRED_EBOOK_INQUIRY_COMMENT_CONTENT.exception) - -fun String?.validateEbookInquiryCommentId(): UUID = - validateNotBlank(EbookError.REQUIRED_EBOOK_INQUIRY_COMMENT_ID.exception) - .validateUUID(EbookError.INVALID_EBOOK_INQUIRY_COMMENT_ID.exception) - -fun List.validateEbookIds(): List = - map { it.validateUUID(EbookError.INVALID_EBOOK_ID.exception) } - -fun String?.validateMainImageId(): UUID = - validateNotBlank(EbookError.REQUIRED_MAIN_IMAGE_ID.exception) - .validateUUID(EbookError.INVALID_MAIN_IMAGE_ID.exception) - -fun List?.validateDescriptionImageIdList(): List = - validateNotNull(EbookError.REQUIRED_DESCRIPTION_IMAGE_ID.exception) - .map { it.validateUUID(EbookError.INVALID_DESCRIPTION_IMAGE_ID.exception) } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookImageRepository.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookImageRepository.kt index 78bfbbf..1bfc335 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookImageRepository.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookImageRepository.kt @@ -9,4 +9,5 @@ import org.springframework.stereotype.Repository @Repository interface EbookImageRepository : CoroutineCrudRepository { suspend fun findAllByEbookIdAndImageType(ebookId: UUID, imageType: EbookImageType): List + suspend fun findByEbookIdAndImageType(ebookId: UUID, imageType: EbookImageType): EbookImageEntity } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookInquiryCommentRepository.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookInquiryCommentRepository.kt index b77a7a3..ee4c8ec 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookInquiryCommentRepository.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookInquiryCommentRepository.kt @@ -10,4 +10,5 @@ import org.springframework.stereotype.Repository @Repository interface EbookInquiryCommentRepository : CoroutineCrudRepository { suspend fun findAllByInquiryId(inquiryId: UUID, pageable: Pageable): Flow + suspend fun countByInquiryId(inquiryId: UUID): Flow } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookInquiryRepository.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookInquiryRepository.kt index d5ce885..2f0c8a1 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookInquiryRepository.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookInquiryRepository.kt @@ -8,4 +8,5 @@ import org.springframework.data.repository.kotlin.CoroutineCrudRepository interface EbookInquiryRepository : CoroutineCrudRepository { suspend fun findAllByEbookId(ebookId: UUID, pageable: Pageable): Flow + suspend fun countByEbookId(ebookId: UUID): Long } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookQueryRepository.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookQueryRepository.kt index 7c8e6f4..0878ab8 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookQueryRepository.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/repository/EbookQueryRepository.kt @@ -14,12 +14,18 @@ import com.devooks.backend.jooq.tables.references.PDF import com.devooks.backend.jooq.tables.references.RELATED_CATEGORY import com.devooks.backend.jooq.tables.references.REVIEW import com.devooks.backend.jooq.tables.references.WISHLIST +import com.devooks.backend.wishlist.v1.dto.GetWishlistCommand +import java.math.BigDecimal import java.util.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import org.jooq.Condition import org.jooq.DSLContext +import org.jooq.JSON +import org.jooq.Record2 +import org.jooq.Record3 +import org.jooq.Table import org.jooq.impl.DSL import org.jooq.impl.DSL.field import org.jooq.impl.DSL.key @@ -29,31 +35,63 @@ import org.springframework.stereotype.Repository @Repository class EbookQueryRepository : JooqR2dbcRepository() { - suspend fun findBy(command: GetEbookCommand): Flow = + suspend fun count(command: GetEbookCommand): Flow = query { - val reviewSubQuery = getReviewSubQuery() - val mainImageSubQuery = getMainImageSubQuery() val relatedCategorySubQuery = getRelatedCategorySubQuery() select( - EBOOK.EBOOK_ID, - EBOOK.TITLE, - EBOOK.PRICE, - MEMBER.MEMBER_ID.`as`("seller_member_id"), - MEMBER.NICKNAME.`as`("seller_nickname"), - MEMBER.PROFILE_IMAGE_PATH.`as`("seller_profile_image_path"), - DSL.coalesce(reviewSubQuery.field("review_rating"), 0.0) - .`as`("review_rating"), - DSL.coalesce(reviewSubQuery.field("review_count"), 0) - .`as`("review_count"), - EBOOK.CREATED_DATE, - EBOOK.MODIFIED_DATE, - DSL.coalesce( - relatedCategorySubQuery.field("related_category_id_list"), - DSL.inline("{}") - ).`as`("related_category_id_list"), - WISHLIST.WISHLIST_ID, - mainImageSubQuery.field("main_image_json_data"), + DSL.count() + ).from( + EBOOK + .leftJoin(relatedCategorySubQuery).on( + EBOOK.EBOOK_ID.eq(relatedCategorySubQuery.field("ebook_id", UUID::class.java)) + ) + ).where( + buildConditionsToGetEbooks( + title = command.title, + sellerMemberId = command.sellerMemberId, + ebookIdList = command.ebookIdList, + categoryIdList = command.categoryIdList + ) + ) + }.map { it.into(Long::class.java) } + + suspend fun findWishlistBy(command: GetWishlistCommand): Flow = + query { + val (reviewSubQuery, mainImageSubQuery, relatedCategorySubQuery) = getSubQueries() + + getEbookRowSelectQuery( + reviewSubQuery, relatedCategorySubQuery, mainImageSubQuery + ).from( + EBOOK + .join(MEMBER).on(EBOOK.SELLING_MEMBER_ID.eq(MEMBER.MEMBER_ID)) + .join(WISHLIST).on( + EBOOK.EBOOK_ID.eq(WISHLIST.EBOOK_ID).and(WISHLIST.MEMBER_ID.eq(command.memberId)) + ) + .leftJoin(reviewSubQuery).on( + EBOOK.EBOOK_ID.eq(reviewSubQuery.field("ebook_id", UUID::class.java)) + ) + .leftJoin(mainImageSubQuery).on( + EBOOK.EBOOK_ID.eq(mainImageSubQuery.field("ebook_id", UUID::class.java)) + ) + .leftJoin(relatedCategorySubQuery).on( + EBOOK.EBOOK_ID.eq(relatedCategorySubQuery.field("ebook_id", UUID::class.java)) + ) + ).where( + buildConditionsToGetEbooks(categoryIdList = command.categoryIdList) + ).orderBy( + WISHLIST.CREATED_DATE.desc() + ).offset(command.offset).limit(command.limit) + }.map { + it.into(EbookRow::class.java) + } + + suspend fun findEbooksBy(command: GetEbookCommand): Flow = + query { + val (reviewSubQuery, mainImageSubQuery, relatedCategorySubQuery) = getSubQueries() + + getEbookRowSelectQuery( + reviewSubQuery, relatedCategorySubQuery, mainImageSubQuery ).from( EBOOK .join(MEMBER).on(EBOOK.SELLING_MEMBER_ID.eq(MEMBER.MEMBER_ID)) @@ -70,7 +108,12 @@ class EbookQueryRepository : JooqR2dbcRepository() { EBOOK.EBOOK_ID.eq(relatedCategorySubQuery.field("ebook_id", UUID::class.java)) ) ).where( - buildConditionsToGetEbooks(command) + buildConditionsToGetEbooks( + title = command.title, + sellerMemberId = command.sellerMemberId, + ebookIdList = command.ebookIdList, + categoryIdList = command.categoryIdList + ) ).run { when (command.orderBy) { EbookOrder.LATEST -> orderBy(EBOOK.CREATED_DATE.desc()) @@ -81,7 +124,7 @@ class EbookQueryRepository : JooqR2dbcRepository() { it.into(EbookRow::class.java) } - suspend fun findBy(command: GetDetailOfEbookCommand): EbookDetailRow? = + suspend fun findEbooksBy(command: GetDetailOfEbookCommand): EbookDetailRow? = query { val reviewSubQuery = getReviewSubQuery() val mainImageSubQuery = getMainImageSubQuery() @@ -141,6 +184,38 @@ class EbookQueryRepository : JooqR2dbcRepository() { it.into(EbookDetailRow::class.java) }.firstOrNull() + private fun DSLContext.getEbookRowSelectQuery( + reviewSubQuery: Table>, + relatedCategorySubQuery: Table>>, + mainImageSubQuery: Table>, + ) = select( + EBOOK.EBOOK_ID, + EBOOK.TITLE, + EBOOK.PRICE, + MEMBER.MEMBER_ID.`as`("seller_member_id"), + MEMBER.NICKNAME.`as`("seller_nickname"), + MEMBER.PROFILE_IMAGE_PATH.`as`("seller_profile_image_path"), + DSL.coalesce(reviewSubQuery.field("review_rating"), 0.0) + .`as`("review_rating"), + DSL.coalesce(reviewSubQuery.field("review_count"), 0) + .`as`("review_count"), + EBOOK.CREATED_DATE, + EBOOK.MODIFIED_DATE, + DSL.coalesce( + relatedCategorySubQuery.field("related_category_id_list"), + DSL.inline("{}") + ).`as`("related_category_id_list"), + WISHLIST.WISHLIST_ID, + mainImageSubQuery.field("main_image_json_data"), + ) + + private fun DSLContext.getSubQueries(): Triple>, Table>, Table>>> { + val reviewSubQuery = getReviewSubQuery() + val mainImageSubQuery = getMainImageSubQuery() + val relatedCategorySubQuery = getRelatedCategorySubQuery() + return Triple(reviewSubQuery, mainImageSubQuery, relatedCategorySubQuery) + } + private fun DSLContext.getDescriptionImageSubQuery() = select( EBOOK_IMAGE.EBOOK_ID, @@ -194,24 +269,29 @@ class EbookQueryRepository : JooqR2dbcRepository() { EBOOK_IMAGE.IMAGE_TYPE.eq(EbookImageType.MAIN.name) ).asTable("ebook_main_image") - private fun buildConditionsToGetEbooks(command: GetEbookCommand): List { + private fun buildConditionsToGetEbooks( + title: String? = null, + sellerMemberId: UUID? = null, + ebookIdList: List? = null, + categoryIdList: List? = null, + ): List { val conditions = mutableListOf() conditions.add(EBOOK.DELETED_DATE.isNull) - command.title?.also { + title?.also { conditions.add(EBOOK.TITLE.likeIgnoreCase(it)) } - command.sellingMemberId?.also { + sellerMemberId?.also { conditions.add(EBOOK.SELLING_MEMBER_ID.eq(it)) } - command.ebookIdList?.also { + ebookIdList?.also { conditions.add(EBOOK.EBOOK_ID.`in`(it)) } - command.categoryIdList?.also { + categoryIdList?.also { conditions.add( field("related_category_id_list", SQLDataType.UUID.arrayDataType) .contains(it.toTypedArray()) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/repository/row/EbookDetailRow.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/repository/row/EbookDetailRow.kt index fb8dd9c..3c90011 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/repository/row/EbookDetailRow.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/repository/row/EbookDetailRow.kt @@ -31,5 +31,6 @@ data class EbookDetailRow( descriptionImageJsonData .filter { it["id"] != null } .map { it.toEbookImageDto() } + .sortedBy { it.order } } diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookImageService.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookImageService.kt index 79eca03..87dd0ec 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookImageService.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookImageService.kt @@ -59,42 +59,28 @@ class EbookImageService( val mainImageId = command.mainImageId return if (mainImageId != null) { ebookImageRepository - .findAllByEbookIdAndImageType(command.ebookId, MAIN) - .firstOrNull() - ?.let { ebookImageRepository.save(it.copy(ebookId = null)) } + .findByEbookIdAndImageType(command.ebookId, MAIN) + .also { ebookImage -> ebookImageRepository.delete(ebookImage) } save(listOf(command.mainImageId), ebook).first() } else { - ebookImageRepository.findAllByEbookIdAndImageType(command.ebookId, MAIN).first().toDomain() + ebookImageRepository.findByEbookIdAndImageType(command.ebookId, MAIN).toDomain() } } suspend fun modifyDescriptionImageList(command: ModifyEbookCommand, ebook: Ebook): List { - val descriptionImageList = ebookImageRepository + val existingImageList = ebookImageRepository .findAllByEbookIdAndImageType(command.ebookId, DESCRIPTION) - val ebookImageList = - if (command.isChangedDescriptionImageList && command.descriptionImageIdList != null) { - val changeDescriptionImageIdList = command.descriptionImageIdList - - val (deletedImages, existImages) = - descriptionImageList - .partition { image -> - changeDescriptionImageIdList.all { descriptionImageId -> - image.id != descriptionImageId - } - } - - val newImageList = - changeDescriptionImageIdList.filter { change -> existImages.none { it.id == change } } - val newEbookImageList = save(newImageList, ebook) - - ebookImageRepository.deleteAll(deletedImages) - - newEbookImageList.plus(existImages.map { it.toDomain() }) - } else { - descriptionImageList.map { it.toDomain() } - } - return ebookImageList.sortedBy { it.order } + return command.descriptionImageIdList?.let { descriptionImageIdList -> + val (imagesToDelete, imageToKeep) = + partitionImages(existingImageList, descriptionImageIdList) + val newImageIdList = descriptionImageIdList.filterNot { id -> imageToKeep.any { it.id == id } } + save(newImageIdList, ebook) + ebookImageRepository.deleteAll(imagesToDelete) + updateImageOrder(descriptionImageIdList) + } ?: existingImageList + .map { it.toDomain() } + .sortedBy { it.order } } suspend fun validate(command: CreateEbookCommand) { @@ -118,6 +104,23 @@ class EbookImageService( return savedImagePath } + private suspend fun updateImageOrder(descriptionImageIdList: List): List = + ebookImageRepository + .findAllById(descriptionImageIdList) + .map { it.copy(imageOrder = descriptionImageIdList.indexOf(it.id)) } + .let { ebookImageRepository.saveAll(it) } + .map { it.toDomain() } + .toList() + .sortedBy { it.order } + + private fun partitionImages( + existingImageList: List, + imageIdList: List, + ): Pair, List> = + existingImageList.partition { image -> + image.id !in imageIdList + } + private suspend fun validateEbookImageList( ebookImageList: Flow, ebook: Ebook, diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookInquiryCommentService.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookInquiryCommentService.kt index 5fce041..72c3633 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookInquiryCommentService.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookInquiryCommentService.kt @@ -10,8 +10,11 @@ import com.devooks.backend.ebook.v1.error.EbookError import com.devooks.backend.ebook.v1.repository.EbookInquiryCommentRepository import java.time.Instant import java.util.* +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl import org.springframework.stereotype.Service @Service @@ -27,11 +30,13 @@ class EbookInquiryCommentService( return ebookInquiryCommentRepository.save(entity).toDomain() } - suspend fun get(command: GetEbookInquireCommentsCommand): List = - ebookInquiryCommentRepository + suspend fun get(command: GetEbookInquireCommentsCommand): Page { + val ebookInquiryComments = ebookInquiryCommentRepository .findAllByInquiryId(command.inquiryId, command.pageable) .map { it.toDomain() } - .toList() + val count = ebookInquiryCommentRepository.countByInquiryId(command.inquiryId) + return PageImpl(ebookInquiryComments.toList(), command.pageable, count.first()) + } suspend fun modify(command: ModifyEbookInquiryCommentCommand): EbookInquiryComment = findBy(command.commentId) @@ -56,4 +61,4 @@ class EbookInquiryCommentService( ebookInquiryCommentRepository .findById(commentId) ?: throw EbookError.NOT_FOUND_EBOOK_INQUIRY_COMMENT.exception -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookInquiryService.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookInquiryService.kt index 55c8897..9e0ee17 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookInquiryService.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookInquiryService.kt @@ -13,6 +13,8 @@ import java.time.Instant import java.util.* import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl import org.springframework.stereotype.Service @Service @@ -28,11 +30,13 @@ class EbookInquiryService( return ebookInquiryRepository.save(entity).toDomain() } - suspend fun get(command: GetEbookInquiresCommand): List = - ebookInquiryRepository + suspend fun get(command: GetEbookInquiresCommand): Page { + val ebookInquiries = ebookInquiryRepository .findAllByEbookId(command.ebookId, command.pageable) .map { it.toDomain() } - .toList() + val count = ebookInquiryRepository.countByEbookId(command.ebookId) + return PageImpl(ebookInquiries.toList(), command.pageable, count) + } suspend fun modify(command: ModifyEbookInquiryCommand): EbookInquiry = findById(command.inquiryId) diff --git a/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookService.kt b/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookService.kt index 8a99d74..3ea6aeb 100644 --- a/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookService.kt +++ b/src/main/kotlin/com/devooks/backend/ebook/v1/service/EbookService.kt @@ -1,8 +1,6 @@ package com.devooks.backend.ebook.v1.service import com.devooks.backend.ebook.v1.domain.Ebook -import com.devooks.backend.ebook.v1.repository.row.EbookDetailRow -import com.devooks.backend.ebook.v1.repository.row.EbookRow import com.devooks.backend.ebook.v1.dto.command.CreateEbookCommand import com.devooks.backend.ebook.v1.dto.command.CreateEbookInquiryCommand import com.devooks.backend.ebook.v1.dto.command.DeleteEbookCommand @@ -14,11 +12,16 @@ import com.devooks.backend.ebook.v1.entity.EbookEntity.Companion.toEntity import com.devooks.backend.ebook.v1.error.EbookError import com.devooks.backend.ebook.v1.repository.EbookQueryRepository import com.devooks.backend.ebook.v1.repository.EbookRepository +import com.devooks.backend.ebook.v1.repository.row.EbookDetailRow +import com.devooks.backend.ebook.v1.repository.row.EbookRow import com.devooks.backend.review.v1.dto.CreateReviewCommand import com.devooks.backend.transaciton.v1.dto.CreateTransactionCommand import java.time.Instant.now import java.util.* +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl import org.springframework.stereotype.Service @Service @@ -71,11 +74,14 @@ class EbookService( findById(command.ebookId) } - suspend fun get(command: GetEbookCommand): List = - ebookQueryRepository.findBy(command).toList() + suspend fun get(command: GetEbookCommand): Page { + val ebooks = ebookQueryRepository.findEbooksBy(command) + val count = ebookQueryRepository.count(command) + return PageImpl(ebooks.toList(), command.pageable, count.first()) + } suspend fun get(command: GetDetailOfEbookCommand): EbookDetailRow = - ebookQueryRepository.findBy(command) + ebookQueryRepository.findEbooksBy(command) ?: throw EbookError.NOT_FOUND_EBOOK.exception suspend fun modify(command: ModifyEbookCommand): Ebook { diff --git a/src/main/kotlin/com/devooks/backend/member/v1/controller/MemberController.kt b/src/main/kotlin/com/devooks/backend/member/v1/controller/MemberController.kt index 1ccb2e1..0f43359 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/controller/MemberController.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/controller/MemberController.kt @@ -24,6 +24,7 @@ import com.devooks.backend.member.v1.dto.WithdrawMemberResponse import com.devooks.backend.member.v1.service.FavoriteCategoryService import com.devooks.backend.member.v1.service.MemberInfoService import com.devooks.backend.member.v1.service.MemberService +import jakarta.validation.Valid import java.util.* import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.transaction.annotation.Transactional @@ -49,6 +50,7 @@ class MemberController( @Transactional @PostMapping("/signup") override suspend fun signUp( + @Valid @RequestBody request: SignUpRequest, ): SignUpResponse { @@ -67,6 +69,7 @@ class MemberController( @Transactional @PatchMapping("/account") override suspend fun modifyAccountInfo( + @Valid @RequestBody request: ModifyAccountInfoRequest, @RequestHeader(AUTHORIZATION) @@ -81,6 +84,7 @@ class MemberController( @Transactional @PatchMapping("/image") override suspend fun modifyProfileImage( + @Valid @RequestBody request: ModifyProfileImageRequest, @RequestHeader(AUTHORIZATION) @@ -95,9 +99,10 @@ class MemberController( @Transactional @PatchMapping("/profile") override suspend fun modifyProfile( + @Valid @RequestBody request: ModifyProfileRequest, - @RequestHeader(AUTHORIZATION, required = true) + @RequestHeader(AUTHORIZATION) authorization: String, ): ModifyProfileResponse { val requesterId: UUID = tokenService.getMemberId(Authorization(authorization)) @@ -117,7 +122,7 @@ class MemberController( @GetMapping("/{memberId}/profile") override suspend fun getProfile( - @PathVariable(required = true) + @PathVariable memberId: UUID, @RequestHeader(AUTHORIZATION, required = false, defaultValue = "") authorization: String, @@ -135,6 +140,7 @@ class MemberController( @Transactional @PatchMapping("/withdrawal") override suspend fun withdrawMember( + @Valid @RequestBody request: WithdrawMemberRequest, @RequestHeader(AUTHORIZATION) diff --git a/src/main/kotlin/com/devooks/backend/member/v1/controller/MemberControllerDocs.kt b/src/main/kotlin/com/devooks/backend/member/v1/controller/MemberControllerDocs.kt index 410bd38..708bc9a 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/controller/MemberControllerDocs.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/controller/MemberControllerDocs.kt @@ -39,11 +39,7 @@ interface MemberControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- AUTH-400-1 : 인증 코드(authorizationCode)가 NULL이거나 빈 문자일 경우\n" + - "- AUTH-400-2 : 인증 유형(oauthType)이 NAVER, KAKAO, GOOGLE 이 아닐 경우\n" + - "- MEMBER-400-1 : 닉네임(nickname)이 2~12 글자가 아닐 경우\n" + - "- MEMBER-400-2 : 관심 카테고리(favoriteCategories)가 NULL일 경우", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -94,10 +90,7 @@ interface MemberControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- MEMBER-400-4 : 이름이 반드시 필요합니다.\n" + - "- MEMBER-400-5 : 은행이 반드시 필요합니다.\n" + - "- MEMBER-400-6 : 계좌번호가 반드시 필요합니다.", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -120,7 +113,7 @@ interface MemberControllerDocs { ) suspend fun modifyAccountInfo( request: ModifyAccountInfoRequest, - @Schema(description = "액세스 토큰", required = true, nullable = false) + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) authorization: String, ): ModifyAccountInfoResponse @@ -139,12 +132,7 @@ interface MemberControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- COMMON-400-3 : 이미지 내용이 반드시 필요합니다.\n" + - "- COMMON-400-4 : 유효하지 않은 이미지 확장자입니다. JPG, PNG, JPEG만 가능합니다.\n" + - "- COMMON-400-5 : 50MB 이하의 영상만 저장이 가능합니다.\n" + - "- COMMON-400-6 : 이미지가 반드시 필요합니다.\n" + - "- COMMON-400-7 : 유효하지 않은 이미지 순서입니다.", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -178,7 +166,7 @@ interface MemberControllerDocs { ) suspend fun modifyProfileImage( request: ModifyProfileImageRequest, - @Schema(description = "액세스 토큰", required = true, nullable = false) + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) authorization: String, ): ModifyProfileImageResponse @@ -197,12 +185,7 @@ interface MemberControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- MEMBER-400-8 : 전화번호 형식(ex. 010-1234-1234)이 아닐 경우\n" + - "- MEMBER-400-9 : 블로그 링크가 null이 아니여 비어있을 경우\n" + - "- MEMBER-400-10 : 인스타그램 링크가 null이 아니여 비어있을 경우\n" + - "- MEMBER-400-2 : 관심 카테고리 목록이 null이 아니여 비어있을 경우\n" + - "- MEMBER-400-16 : 이메일이 null이 아니며 이메일 형식이 아닐 경우", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -225,7 +208,7 @@ interface MemberControllerDocs { ) suspend fun modifyProfile( request: ModifyProfileRequest, - @Schema(description = "액세스 토큰", required = true, nullable = false) + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) authorization: String, ): ModifyProfileResponse @@ -245,9 +228,7 @@ interface MemberControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- 회원 식별자가 UUID가 아닐 경우\n" + - "- 회원 식별자가 존재하지 않을 경우", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -269,9 +250,9 @@ interface MemberControllerDocs { ] ) suspend fun getProfile( - @Schema(description = "회원 식별자", required = true, nullable = false) + @Schema(description = "회원 식별자", required = true, implementation = UUID::class) memberId: UUID, - @Schema(description = "액세스 토큰", required = false, nullable = true) + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", nullable = true) authorization: String, ): GetProfileResponse @@ -290,8 +271,7 @@ interface MemberControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- MEMBER-400-14 : 탈퇴 이유가 반드시 필요합니다.", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -314,7 +294,7 @@ interface MemberControllerDocs { ) suspend fun withdrawMember( request: WithdrawMemberRequest, - @Schema(description = "액세스 토큰", required = true, nullable = false) + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) authorization: String, ): WithdrawMemberResponse } diff --git a/src/main/kotlin/com/devooks/backend/member/v1/dto/MemberProfile.kt b/src/main/kotlin/com/devooks/backend/member/v1/dto/MemberProfile.kt index fa6119c..a3c6d33 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/dto/MemberProfile.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/dto/MemberProfile.kt @@ -13,7 +13,8 @@ data class MemberProfile( val nickname: String, @Schema(description = "프로필 사진 경로") val profileImagePath: String, - val favoriteCategoryList: List, + @Schema(description = "관심 카테고리 식별자 목록") + val favoriteCategoryIdList: List, @Schema(description = "블로그 링크") val blogLink: String, @Schema(description = "인스타그램 링크") @@ -43,7 +44,7 @@ data class MemberProfile( id = member.id, nickname = member.nickname, profileImagePath = member.profileImagePath, - favoriteCategoryList = categoryList, + favoriteCategoryIdList = categoryList.map { it.id }, blogLink = memberInfo.blogLink, instagramLink = memberInfo.instagramLink, youtubeLink = memberInfo.youtubeLink, diff --git a/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyAccountInfoCommand.kt b/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyAccountInfoCommand.kt index 92fa471..a1c9aeb 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyAccountInfoCommand.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyAccountInfoCommand.kt @@ -1,7 +1,7 @@ package com.devooks.backend.member.v1.dto class ModifyAccountInfoCommand( - val realName: String, - val bank: String, - val accountNumber: String, + val realName: String?, + val bank: String?, + val accountNumber: String?, ) diff --git a/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyAccountInfoRequest.kt b/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyAccountInfoRequest.kt index d34b69a..a0ecaec 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyAccountInfoRequest.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyAccountInfoRequest.kt @@ -1,24 +1,27 @@ package com.devooks.backend.member.v1.dto -import com.devooks.backend.member.v1.error.validateAccountNumber -import com.devooks.backend.member.v1.error.validateBank -import com.devooks.backend.member.v1.error.validateRealName import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size data class ModifyAccountInfoRequest( - @Schema(description = "수취인 이름", required = true, nullable = false) + @field:Size(min = 1, max = 10) + @Schema(description = "수취인 이름", nullable = true) val realName: String?, - @Schema(description = "은행 이름", required = true, nullable = false) + @field:Size(min = 1, max = 10) + @Schema(description = "은행 이름", nullable = true) val bank: String?, - @Schema(description = "계좌 번호", required = true, nullable = false) + @field:Size(min = 1, max = 20) + @field:Pattern(regexp = "[0-9]+") + @Schema(description = "계좌 번호", nullable = true) val accountNumber: String?, ) { fun toCommand(): ModifyAccountInfoCommand = ModifyAccountInfoCommand( - realName = realName.validateRealName(), - bank = bank.validateBank(), - accountNumber = accountNumber.validateAccountNumber() + realName = realName, + bank = bank, + accountNumber = accountNumber ) } diff --git a/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyNicknameRequest.kt b/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyNicknameRequest.kt deleted file mode 100644 index ee7d129..0000000 --- a/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyNicknameRequest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.devooks.backend.member.v1.dto - -import com.devooks.backend.member.v1.error.validateNickname -import io.swagger.v3.oas.annotations.media.Schema - -data class ModifyNicknameRequest( - @Schema(description = "닉네임", required = true, nullable = false) - val nickname: String? -) { - fun toCommand(): ModifyNicknameCommand = - ModifyNicknameCommand( - nickname = nickname.validateNickname() - ) -} diff --git a/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyProfileImageRequest.kt b/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyProfileImageRequest.kt index db0a3f1..0111047 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyProfileImageRequest.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyProfileImageRequest.kt @@ -1,13 +1,14 @@ package com.devooks.backend.member.v1.dto import com.devooks.backend.common.dto.ImageDto -import com.devooks.backend.common.error.validateImage +import jakarta.validation.Valid data class ModifyProfileImageRequest( - val image: ImageDto? + @field:Valid + val image: ImageDto ) { fun toCommand(): ModifyProfileImageCommand = ModifyProfileImageCommand( - image = image.validateImage() + image = image.toDomain() ) } diff --git a/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyProfileRequest.kt b/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyProfileRequest.kt index 0370530..6156898 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyProfileRequest.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/dto/ModifyProfileRequest.kt @@ -1,42 +1,44 @@ package com.devooks.backend.member.v1.dto -import com.devooks.backend.member.v1.error.validateBlogLink -import com.devooks.backend.member.v1.error.validateEmail -import com.devooks.backend.member.v1.error.validateFavoriteCategoryIdList -import com.devooks.backend.member.v1.error.validateInstagramLink -import com.devooks.backend.member.v1.error.validateIntroduction -import com.devooks.backend.member.v1.error.validateNickname -import com.devooks.backend.member.v1.error.validatePhoneNumber -import com.devooks.backend.member.v1.error.validateYoutubeLink import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import java.util.* data class ModifyProfileRequest( - @Schema(description = "닉네임", required = false, nullable = true) + @field:Size(min = 2, max = 12) + @Schema(description = "닉네임", nullable = true) val nickname: String?, - @Schema(description = "전화번호 (ex. 010-1234-1234)", required = false, nullable = true) + @field:Pattern(regexp = "^[0-9]{2,3}-[0-9]{3,4}-[0-9]{3,4}\$") + @Schema(description = "전화번호 (ex. 010-1234-1234)", nullable = true) val phoneNumber: String?, - @Schema(description = "블로그 링크", required = false, nullable = true) + @field:Size(min = 1, max = 255) + @Schema(description = "블로그 링크", nullable = true) val blogLink: String?, - @Schema(description = "인스타그램 링크", required = false, nullable = true) + @field:Size(min = 1, max = 255) + @Schema(description = "인스타그램 링크", nullable = true) val instagramLink: String?, - @Schema(description = "유튜브 링크", required = false, nullable = true) + @field:Size(min = 1, max = 255) + @Schema(description = "유튜브 링크", nullable = true) val youtubeLink: String?, - @Schema(description = "소개글", required = false, nullable = true) + @field:Size(min = 1, max = 5_000) + @Schema(description = "소개글", nullable = true) val introduction: String?, - @Schema(description = "관심 카테고리 식별자 목록", required = false, nullable = true) - val favoriteCategoryIdList: List?, - @Schema(description = "이메일", required = false, nullable = true) + @Schema(description = "관심 카테고리 식별자 목록", nullable = true) + val favoriteCategoryIdList: List?, + @field:Pattern(regexp = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9]+\\.[A-Za-z]+$") + @Schema(description = "이메일", nullable = true) val email: String?, ) { fun toCommand(): ModifyProfileCommand = ModifyProfileCommand( - nickname = nickname?.validateNickname(), - phoneNumber = phoneNumber?.validatePhoneNumber(), - blogLink = blogLink?.validateBlogLink(), - instagramLink = instagramLink?.validateInstagramLink(), - youtubeLink = youtubeLink?.validateYoutubeLink(), - introduction = introduction?.validateIntroduction(), - favoriteCategoryIdList = favoriteCategoryIdList?.validateFavoriteCategoryIdList(), - email = email?.validateEmail(), + nickname = nickname, + phoneNumber = phoneNumber, + blogLink = blogLink, + instagramLink = instagramLink, + youtubeLink = youtubeLink, + introduction = introduction, + favoriteCategoryIdList = favoriteCategoryIdList, + email = email, ) } diff --git a/src/main/kotlin/com/devooks/backend/member/v1/dto/SignUpRequest.kt b/src/main/kotlin/com/devooks/backend/member/v1/dto/SignUpRequest.kt index a4df3b4..70cd451 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/dto/SignUpRequest.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/dto/SignUpRequest.kt @@ -1,33 +1,30 @@ package com.devooks.backend.member.v1.dto -import com.devooks.backend.auth.v1.error.validateOauthId -import com.devooks.backend.auth.v1.error.validateOauthType -import com.devooks.backend.member.v1.error.validateFavoriteCategoryIdList -import com.devooks.backend.member.v1.error.validateNickname +import com.devooks.backend.auth.v1.domain.OauthType import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Size +import java.util.* data class SignUpRequest( - @Schema(description = "OAuth2 식별자", required = true, nullable = false) - val oauthId: String?, - @Schema( - description = "OAuth2 인증 유형 (ex. NAVER, KAKAO, GOOGLE)", - required = true, - nullable = false, - example = "NAVER" - ) - val oauthType: String?, - @Schema(description = "닉네임", required = true, nullable = false) - val nickname: String?, - @Schema(description = "관심 카테고리 식별자 목록", required = true, nullable = false) - val favoriteCategoryIdList: List?, + @field:NotBlank + @Schema(description = "OAuth2 식별자", required = true, implementation = UUID::class) + val oauthId: String, + @Schema(description = "OAuth2 인증 유형", required = true) + val oauthType: OauthType, + @field:Size(min = 2, max = 12) + @Schema(description = "닉네임", required = true) + val nickname: String, + @Schema(description = "관심 카테고리 식별자 목록", required = true) + val favoriteCategoryIdList: List, ) { fun toCommand(): SignUpCommand = SignUpCommand( - oauthId = oauthId.validateOauthId(), - oauthType = oauthType.validateOauthType(), - nickname = nickname.validateNickname(), - favoriteCategoryIdList = favoriteCategoryIdList.validateFavoriteCategoryIdList() + oauthId = oauthId, + oauthType = oauthType, + nickname = nickname, + favoriteCategoryIdList = favoriteCategoryIdList ) } diff --git a/src/main/kotlin/com/devooks/backend/member/v1/dto/WithdrawMemberRequest.kt b/src/main/kotlin/com/devooks/backend/member/v1/dto/WithdrawMemberRequest.kt index 8c295f6..f3037f9 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/dto/WithdrawMemberRequest.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/dto/WithdrawMemberRequest.kt @@ -1,13 +1,12 @@ package com.devooks.backend.member.v1.dto -import com.devooks.backend.member.v1.error.validateWithdrawalReason +import jakarta.validation.constraints.Size data class WithdrawMemberRequest( - val withdrawalReason: String? + @field:Size(min = 1, max = 255) + val withdrawalReason: String ) { fun toCommand(): WithdrawMemberCommand = - WithdrawMemberCommand( - withdrawalReason = withdrawalReason.validateWithdrawalReason() - ) + WithdrawMemberCommand(withdrawalReason = withdrawalReason) } diff --git a/src/main/kotlin/com/devooks/backend/member/v1/entity/MemberInfoEntity.kt b/src/main/kotlin/com/devooks/backend/member/v1/entity/MemberInfoEntity.kt index 0221af9..f2d8c71 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/entity/MemberInfoEntity.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/entity/MemberInfoEntity.kt @@ -1,5 +1,6 @@ package com.devooks.backend.member.v1.entity +import com.devooks.backend.member.v1.dto.ModifyAccountInfoCommand import com.devooks.backend.member.v1.dto.ModifyProfileCommand import java.util.* import org.springframework.data.annotation.Id @@ -28,7 +29,7 @@ data class MemberInfoEntity( override fun isNew(): Boolean = id == null - fun update(command: ModifyProfileCommand) = + fun updateProfile(command: ModifyProfileCommand) = copy( phoneNumber = command.phoneNumber ?: this.phoneNumber, blogLink = command.blogLink ?: this.blogLink, @@ -37,4 +38,11 @@ data class MemberInfoEntity( introduction = command.introduction ?: this.introduction, email = command.email ?: this.email, ) + + fun updateAccount(command: ModifyAccountInfoCommand) = + copy( + realName = command.realName ?: this.realName, + bank = command.bank ?: this.bank, + accountNumber = command.accountNumber ?: this.accountNumber, + ) } diff --git a/src/main/kotlin/com/devooks/backend/member/v1/error/MemberError.kt b/src/main/kotlin/com/devooks/backend/member/v1/error/MemberError.kt index 36c6c1d..adfb5fd 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/error/MemberError.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/error/MemberError.kt @@ -1,31 +1,11 @@ package com.devooks.backend.member.v1.error import com.devooks.backend.common.exception.GeneralException -import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.CONFLICT import org.springframework.http.HttpStatus.FORBIDDEN import org.springframework.http.HttpStatus.NOT_FOUND enum class MemberError(val exception: GeneralException) { - // 400 - REQUIRED_NICKNAME(GeneralException("MEMBER-400-1", BAD_REQUEST, "닉네임이 반드시 필요합니다.")), - REQUIRED_FAVORITE_CATEGORIES(GeneralException("MEMBER-400-2", BAD_REQUEST, "관심 카테고리 목록이 반드시 필요합니다.")), - INVALID_NICKNAME(GeneralException("MEMBER-400-3", BAD_REQUEST, "닉네임은 2자 이상 12자 이하만 가능합니다.")), - REQUIRED_REAL_NAME(GeneralException("MEMBER-400-4", BAD_REQUEST, "이름이 반드시 필요합니다.")), - REQUIRED_BANK(GeneralException("MEMBER-400-5", BAD_REQUEST, "은행이 반드시 필요합니다.")), - REQUIRED_ACCOUNT_NUMBER(GeneralException("MEMBER-400-6", BAD_REQUEST, "계좌번호가 반드시 필요합니다.")), - REQUIRED_PHONE_NUMBER(GeneralException("MEMBER-400-7", BAD_REQUEST, "전화번호가 반드시 필요합니다.")), - INVALID_PHONE_NUMBER(GeneralException("MEMBER-400-8", BAD_REQUEST, "잘못된 형식의 전화번호 입니다.")), - REQUIRED_BLOG_LINK(GeneralException("MEMBER-400-9", BAD_REQUEST, "블로그 링크가 반드시 필요합니다.")), - REQUIRED_INSTAGRAM_LINK(GeneralException("MEMBER-400-10", BAD_REQUEST, "인스타그램 링크가 반드시 필요합니다.")), - REQUIRED_YOUTUBE_LINK(GeneralException("MEMBER-400-11", BAD_REQUEST, "유튜브 링크가 반드시 필요합니다.")), - REQUIRED_INTRODUCTION_LINK(GeneralException("MEMBER-400-12", BAD_REQUEST, "소개글이 반드시 필요합니다.")), - INVALID_FAVORITE_CATEGORIES(GeneralException("MEMBER-400-13", BAD_REQUEST, "잘못된 형식의 카테고리 식별자 입니다.")), - REQUIRED_WITHDRAWAL_REASON(GeneralException("MEMBER-400-14", BAD_REQUEST, "탈퇴 이유가 반드시 필요합니다.")), - INVALID_MEMBER_ID(GeneralException("MEMBER-400-15", BAD_REQUEST, "잘못된 형식의 회원 식별자 입니다.")), - REQUIRED_EMAIL(GeneralException("MEMBER-400-16", BAD_REQUEST, "이메일은 반드시 필요합니다.")), - INVALID_EMAIL(GeneralException("MEMBER-400-17", BAD_REQUEST, "잘못된 형식의 이메일 입니다.")), - // 403 SUSPENDED_MEMBER(GeneralException("MEMBER-403-1", FORBIDDEN, "정지된 회원으로 서비스 이용이 불가합니다.")), WITHDREW_MEMBER(GeneralException("MEMBER-403-2", FORBIDDEN, "탈퇴한 회원으로 계정 복구가 필요합니다.")), diff --git a/src/main/kotlin/com/devooks/backend/member/v1/error/MemberValidation.kt b/src/main/kotlin/com/devooks/backend/member/v1/error/MemberValidation.kt deleted file mode 100644 index 25caee6..0000000 --- a/src/main/kotlin/com/devooks/backend/member/v1/error/MemberValidation.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.devooks.backend.member.v1.error - -import com.devooks.backend.common.error.validateNotBlank -import com.devooks.backend.common.error.validateNotNull -import com.devooks.backend.common.error.validateUUID -import java.util.* - -private val phoneRegex = Regex("^[0-9]{2,3}-[0-9]{3,4}-[0-9]{3,4}$") -val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$") - -fun String?.validateNickname(): String = - validateNotBlank(MemberError.REQUIRED_NICKNAME.exception) - .also { - it.takeIf { it.length in 2..12 } - ?: throw MemberError.INVALID_NICKNAME.exception - } - -fun List?.validateFavoriteCategoryIdList(): List = - validateNotNull(MemberError.REQUIRED_FAVORITE_CATEGORIES.exception) - .map { it.validateUUID(MemberError.INVALID_FAVORITE_CATEGORIES.exception) } - -fun String?.validateRealName(): String = - validateNotBlank(MemberError.REQUIRED_REAL_NAME.exception) - -fun String?.validateBank(): String = - validateNotBlank(MemberError.REQUIRED_BANK.exception) - -fun String?.validateAccountNumber(): String = - validateNotBlank(MemberError.REQUIRED_ACCOUNT_NUMBER.exception) - -fun String?.validatePhoneNumber(): String = - validateNotBlank(MemberError.REQUIRED_PHONE_NUMBER.exception) - .also { it.takeIf { phoneRegex.matches(it) } ?: throw MemberError.INVALID_PHONE_NUMBER.exception } - -fun String?.validateBlogLink(): String = - validateNotBlank(MemberError.REQUIRED_BLOG_LINK.exception) - -fun String?.validateInstagramLink(): String = - validateNotBlank(MemberError.REQUIRED_INSTAGRAM_LINK.exception) - -fun String?.validateYoutubeLink(): String = - validateNotBlank(MemberError.REQUIRED_YOUTUBE_LINK.exception) - -fun String?.validateIntroduction(): String = - validateNotBlank(MemberError.REQUIRED_INTRODUCTION_LINK.exception) - -fun String?.validateWithdrawalReason(): String = - validateNotBlank(MemberError.REQUIRED_WITHDRAWAL_REASON.exception) - -fun String?.validateMemberId(): UUID = - validateUUID(MemberError.INVALID_MEMBER_ID.exception) - -fun String?.validateEmail(): String = - validateNotBlank(MemberError.REQUIRED_EMAIL.exception) - .also { it.takeIf { emailRegex.matches(it) } ?: throw MemberError.INVALID_EMAIL.exception } diff --git a/src/main/kotlin/com/devooks/backend/member/v1/service/MemberInfoService.kt b/src/main/kotlin/com/devooks/backend/member/v1/service/MemberInfoService.kt index 860c6ab..9047b6e 100644 --- a/src/main/kotlin/com/devooks/backend/member/v1/service/MemberInfoService.kt +++ b/src/main/kotlin/com/devooks/backend/member/v1/service/MemberInfoService.kt @@ -25,16 +25,13 @@ class MemberInfoService( suspend fun updateAccountInfo( command: ModifyAccountInfoCommand, requesterId: UUID, - ): MemberInfo { - val memberInfo = - memberInfoRepository - .findByMemberId(requesterId) - ?: throw MemberError.NOT_FOUND_MEMBER_INFO_BY_ID.exception - val updatedMemberInfo = - memberInfo.copy(accountNumber = command.accountNumber, bank = command.bank, realName = command.realName) - val savedMemberInfo = memberInfoRepository.save(updatedMemberInfo) - return savedMemberInfo.toDomain() - } + ): MemberInfo = + memberInfoRepository + .findByMemberId(requesterId) + ?.updateAccount(command) + ?.let { updatedAccount -> memberInfoRepository.save(updatedAccount) } + ?.toDomain() + ?: throw MemberError.NOT_FOUND_MEMBER_INFO_BY_ID.exception suspend fun findById(memberId: UUID): MemberInfo = findMemberInfoById(memberId).toDomain() @@ -44,7 +41,7 @@ class MemberInfoService( requesterId: UUID, ): MemberInfo { val memberInfo = findMemberInfoById(requesterId) - val updateMemberInfo = memberInfo.update(command) + val updateMemberInfo = memberInfo.updateProfile(command) return memberInfoRepository.save(updateMemberInfo).toDomain() } diff --git a/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/CheckNotificationResponse.kt b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/CheckNotificationResponse.kt index a56b581..2afceec 100644 --- a/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/CheckNotificationResponse.kt +++ b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/CheckNotificationResponse.kt @@ -1,5 +1,8 @@ package com.devooks.backend.notification.v1.adapter.`in`.dto +import io.swagger.v3.oas.annotations.media.Schema + data class CheckNotificationResponse( - val count: Int, + @Schema(description = "확인하지 않은 알림 개수") + val countOfUncheckedNotification: Int, ) diff --git a/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/GetNotificationsRequest.kt b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/GetNotificationsRequest.kt index f26cfb8..c5d5e4e 100644 --- a/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/GetNotificationsRequest.kt +++ b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/GetNotificationsRequest.kt @@ -10,8 +10,8 @@ data class GetNotificationsRequest( ) { constructor( memberId: UUID, - page: String, - count: String, + page: Int, + count: Int, ) : this(memberId, Paging(page, count)) val pageable: Pageable = paging.value diff --git a/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/NotificationResponse.kt b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/NotificationResponse.kt index 647e878..6c1ba4f 100644 --- a/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/NotificationResponse.kt +++ b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/NotificationResponse.kt @@ -3,15 +3,39 @@ package com.devooks.backend.notification.v1.adapter.`in`.dto import com.devooks.backend.notification.v1.domain.NotificationType import com.devooks.backend.notification.v1.domain.event.NotificationContent import com.devooks.backend.notification.v1.domain.event.NotificationNote +import io.swagger.v3.oas.annotations.media.Schema import java.time.Instant import java.util.* data class NotificationResponse( + @Schema(description = "알림 식별자") val id: UUID, + @Schema( + description = "알림 유형 " + + "(ex. REVIEW, REVIEW_COMMENT, INQUIRY, INQUIRY_COMMENT, ANNOUNCE, PURCHASE, SALES, WITHDRAWAL)", + example = "REVIEW" + ) val type: NotificationType, + @Schema(description = "내용", example = "[회원1] 님이 [전자책1]에 리뷰를 남겼습니다.") val content: NotificationContent, + @Schema( + description = "상세 정보 (알림 유형에 따라 달라질 수 있음)
" + + "- REVIEW: {\"type\":\"com.devooks.backend.notification.v1.domain.event.CreateReviewEvent\"," + + "\"reviewId\":\"a79c4b6d-4261-4a5d-8fbb-dc8364d517cd\",\"reviewerName\":\"reviewer\"," + + "\"ebookId\":\"66dba0ad-8f8a-413a-8249-de3e5ce5a796\",\"ebookTitle\":\"title\"," + + "\"writtenDate\":\"2024-11-22T02:08:03.869505Z\"," + + "\"receiverId\":\"29e4c18a-f2af-44fd-ba6f-34770dab1d55\"}", + example = "{\"type\":\"com.devooks.backend.notification.v1.domain.event.CreateReviewEvent\"," + + "\"reviewId\":\"a79c4b6d-4261-4a5d-8fbb-dc8364d517cd\",\"reviewerName\":\"reviewer\"," + + "\"ebookId\":\"66dba0ad-8f8a-413a-8249-de3e5ce5a796\",\"ebookTitle\":\"title\"," + + "\"writtenDate\":\"2024-11-22T02:08:03.869505Z\"," + + "\"receiverId\":\"29e4c18a-f2af-44fd-ba6f-34770dab1d55\"}" + ) val note: NotificationNote, + @Schema(description = "수신자 회원 식별자") val receiverId: UUID, + @Schema(description = "알림 날짜") val notifiedDate: Instant, + @Schema(description = "확인 여부") val checked: Boolean, ) diff --git a/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/StreamCountResponse.kt b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/StreamCountResponse.kt index e937e40..eff48bd 100644 --- a/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/StreamCountResponse.kt +++ b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/dto/StreamCountResponse.kt @@ -1,5 +1,8 @@ package com.devooks.backend.notification.v1.adapter.`in`.dto +import io.swagger.v3.oas.annotations.media.Schema + data class StreamCountResponse( - val countOfUncheckedNotification: Long, + @Schema(description = "확인하지 않은 알림 개수") + val countOfUncheckedNotification: Int, ) diff --git a/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/http/NotificationRouter.kt b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/http/NotificationRouter.kt index e7ac9c9..26f9ea3 100644 --- a/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/http/NotificationRouter.kt +++ b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/http/NotificationRouter.kt @@ -34,10 +34,10 @@ class NotificationRouter( private val tokenService: TokenService, private val getNotificationUseCase: GetNotificationUseCase, private val modifyNotificationUseCase: ModifyNotificationUseCase, -) { +): NotificationRouterDocs { @GetMapping("/count") - suspend fun streamCountOfUncheckedNotifications( + override suspend fun streamCountOfUncheckedNotifications( @RequestHeader(AUTHORIZATION) authorization: String, ): Flow> = @@ -50,13 +50,13 @@ class NotificationRouter( } @GetMapping - suspend fun getNotifications( + override suspend fun getNotifications( @RequestHeader(AUTHORIZATION) authorization: String, - @RequestParam(name = "page", defaultValue = "1") - page: String, - @RequestParam(name = "count", defaultValue = "10") - count: String, + @RequestParam + page: Int, + @RequestParam + count: Int, ): PageResponse { val memberId = tokenService.getMemberId(Authorization(authorization)) val request = GetNotificationsRequest(memberId, page, count) @@ -65,20 +65,21 @@ class NotificationRouter( } @PatchMapping(path = ["/{notificationId}/checked", "/checked"]) - suspend fun checkNotifications( + override suspend fun checkNotifications( @RequestHeader(AUTHORIZATION) authorization: String, @PathVariable("notificationId", required = false) - notificationId: String?, + notificationId: UUID?, ): CheckNotificationResponse { val memberId = tokenService.getMemberId(Authorization(authorization)) val request = CheckNotificationsRequest(memberId, notificationId) - val size: Int = modifyNotificationUseCase.check(request) - return CheckNotificationResponse(size) + modifyNotificationUseCase.check(request) + val count = getNotificationUseCase.getCountOfUnchecked(memberId) + return CheckNotificationResponse(count) } private suspend fun getCountOfUncheckedNotifications(memberId: UUID): ServerSentEvent { - val size: Long = getNotificationUseCase.getCountOfUnchecked(memberId) + val size = getNotificationUseCase.getCountOfUnchecked(memberId) val response = StreamCountResponse(size) return ServerSentEvent .builder() @@ -93,4 +94,4 @@ class NotificationRouter( private val streamIntervalDuration = Duration.ofSeconds(1) private val retryDuration = Duration.ofSeconds(10) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/http/NotificationRouterDocs.kt b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/http/NotificationRouterDocs.kt new file mode 100644 index 0000000..2e6f8c8 --- /dev/null +++ b/src/main/kotlin/com/devooks/backend/notification/v1/adapter/in/http/NotificationRouterDocs.kt @@ -0,0 +1,83 @@ +package com.devooks.backend.notification.v1.adapter.`in`.http + +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.exception.ErrorResponse +import com.devooks.backend.notification.v1.adapter.`in`.dto.CheckNotificationResponse +import com.devooks.backend.notification.v1.adapter.`in`.dto.NotificationResponse +import com.devooks.backend.notification.v1.adapter.`in`.dto.StreamCountResponse +import io.swagger.v3.oas.annotations.Operation +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 java.util.* +import kotlinx.coroutines.flow.Flow +import org.springframework.http.MediaType.APPLICATION_JSON_VALUE +import org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE +import org.springframework.http.codec.ServerSentEvent + +@Tag(name = "알림") +interface NotificationRouterDocs { + + @Operation(summary = "확인하지 않은 알림 개수 실시간 조회") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = TEXT_EVENT_STREAM_VALUE, + schema = Schema(implementation = StreamCountResponse::class) + ) + ] + ), + ] + ) + suspend fun streamCountOfUncheckedNotifications( + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): Flow> + + @Operation(summary = "알림 조회") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK" + ), + ApiResponse( + responseCode = "400", + description = + "- COMMON-400-1 : 페이지는 1부터 조회할 수 있습니다.\n" + + "- COMMON-400-2 : 개수는 1~1000 까지 조회할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun getNotifications( + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + @Schema(description = "페이지", implementation = Int::class, required = true) + page: Int, + @Schema(description = "개수", implementation = Int::class, required = true) + count: Int, + ): PageResponse + + @Operation( + summary = "전체 혹은 선택된 알림 확인 여부 변경", + description = "알림 식별자가 존재하지 않을 경우 확인되지 않을 알림을 모두 확인 상태로 변경함" + ) + suspend fun checkNotifications( + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + @Schema(description = "알림 식별자", required = false, nullable = true, implementation = UUID::class) + notificationId: UUID?, + ): CheckNotificationResponse +} diff --git a/src/main/kotlin/com/devooks/backend/notification/v1/application/port/in/GetNotificationUseCase.kt b/src/main/kotlin/com/devooks/backend/notification/v1/application/port/in/GetNotificationUseCase.kt index 39cb1de..8486292 100644 --- a/src/main/kotlin/com/devooks/backend/notification/v1/application/port/in/GetNotificationUseCase.kt +++ b/src/main/kotlin/com/devooks/backend/notification/v1/application/port/in/GetNotificationUseCase.kt @@ -6,6 +6,6 @@ import java.util.* import org.springframework.data.domain.Page interface GetNotificationUseCase { - suspend fun getCountOfUnchecked(memberId: UUID): Long + suspend fun getCountOfUnchecked(memberId: UUID): Int suspend fun getNotifications(request: GetNotificationsRequest): Page } diff --git a/src/main/kotlin/com/devooks/backend/notification/v1/application/service/NotificationQueryService.kt b/src/main/kotlin/com/devooks/backend/notification/v1/application/service/NotificationQueryService.kt index 554887d..f035443 100644 --- a/src/main/kotlin/com/devooks/backend/notification/v1/application/service/NotificationQueryService.kt +++ b/src/main/kotlin/com/devooks/backend/notification/v1/application/service/NotificationQueryService.kt @@ -16,12 +16,12 @@ class NotificationQueryService( private val loadNotificationPort: LoadNotificationPort, ) : GetNotificationUseCase { - override suspend fun getCountOfUnchecked(memberId: UUID): Long = - loadNotificationPort.loadCountOfUnchecked(memberId) + override suspend fun getCountOfUnchecked(memberId: UUID): Int = + loadNotificationPort.loadCountOfUnchecked(memberId).toInt() override suspend fun getNotifications(request: GetNotificationsRequest): Page { val notifications = loadNotificationPort.loadNotifications(request).map { it.toResponse() } val count = loadNotificationPort.loadCount(request.memberId) return PageImpl(notifications.toList(), request.pageable, count) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/pdf/v1/controller/PdfControllerDocs.kt b/src/main/kotlin/com/devooks/backend/pdf/v1/controller/PdfControllerDocs.kt index 1168418..04b3928 100644 --- a/src/main/kotlin/com/devooks/backend/pdf/v1/controller/PdfControllerDocs.kt +++ b/src/main/kotlin/com/devooks/backend/pdf/v1/controller/PdfControllerDocs.kt @@ -57,9 +57,9 @@ interface PdfControllerDocs { ] ) suspend fun uploadPdf( - @Schema(description = "PDF 파일", required = true, nullable = false) + @Schema(description = "PDF 파일", required = true) filePart: FilePart, - @Schema(description = "액세스 토큰", required = true, nullable = false) + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) authorization: String, ): UploadPdfResponse @@ -114,7 +114,7 @@ interface PdfControllerDocs { ] ) suspend fun getPreviewImageList( - @Schema(description = "PDF 식별자", required = true, nullable = false) + @Schema(description = "PDF 식별자", required = true, implementation = UUID::class) pdfId: UUID ): GetPreviewImageListResponse } diff --git a/src/main/kotlin/com/devooks/backend/pdf/v1/domain/PdfInfo.kt b/src/main/kotlin/com/devooks/backend/pdf/v1/domain/PdfInfo.kt index 3862d1d..376f315 100644 --- a/src/main/kotlin/com/devooks/backend/pdf/v1/domain/PdfInfo.kt +++ b/src/main/kotlin/com/devooks/backend/pdf/v1/domain/PdfInfo.kt @@ -1,8 +1,12 @@ package com.devooks.backend.pdf.v1.domain +import java.io.File import java.nio.file.Path +import kotlin.io.path.pathString class PdfInfo( val filePath: Path, val pageCount: Int, -) \ No newline at end of file +) { + val systemFile = File(filePath.pathString.substring(1)) +} diff --git a/src/main/kotlin/com/devooks/backend/pdf/v1/entity/PdfEntity.kt b/src/main/kotlin/com/devooks/backend/pdf/v1/entity/PdfEntity.kt index 6a7a3d9..83bc90b 100644 --- a/src/main/kotlin/com/devooks/backend/pdf/v1/entity/PdfEntity.kt +++ b/src/main/kotlin/com/devooks/backend/pdf/v1/entity/PdfEntity.kt @@ -45,4 +45,4 @@ data class PdfEntity( uploadMemberId = uploadMemberId ) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/pdf/v1/entity/PreviewImageEntity.kt b/src/main/kotlin/com/devooks/backend/pdf/v1/entity/PreviewImageEntity.kt index 26d14fe..6e9325b 100644 --- a/src/main/kotlin/com/devooks/backend/pdf/v1/entity/PreviewImageEntity.kt +++ b/src/main/kotlin/com/devooks/backend/pdf/v1/entity/PreviewImageEntity.kt @@ -4,7 +4,6 @@ import com.devooks.backend.pdf.v1.domain.PreviewImage import com.devooks.backend.pdf.v1.domain.PreviewImageInfo import java.util.* import kotlin.io.path.Path -import kotlin.io.path.pathString import org.springframework.data.annotation.Id import org.springframework.data.domain.Persistable import org.springframework.data.relational.core.mapping.Column @@ -35,9 +34,9 @@ data class PreviewImageEntity( companion object { fun PreviewImageInfo.toEntity(pdfId: UUID) = PreviewImageEntity( - imagePath = this.imagePath.pathString, + imagePath = "/${this.imagePath}", previewOrder = this.order, pdfId = pdfId ) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/pdf/v1/service/PdfResolver.kt b/src/main/kotlin/com/devooks/backend/pdf/v1/service/PdfResolver.kt index bf46489..1186518 100644 --- a/src/main/kotlin/com/devooks/backend/pdf/v1/service/PdfResolver.kt +++ b/src/main/kotlin/com/devooks/backend/pdf/v1/service/PdfResolver.kt @@ -12,6 +12,7 @@ import java.nio.file.Paths import java.util.* import javax.imageio.ImageIO import kotlin.io.path.fileSize +import kotlin.io.path.pathString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow @@ -37,7 +38,7 @@ class PdfResolver { val pdfFilePath = savePdfFile(filePart) validatePdfFile(pdfFilePath) val numberOfPages = getNumberOfPages(pdfFilePath) - PdfInfo(filePath = pdfFilePath, pageCount = numberOfPages) + PdfInfo(filePath = Path.of("/", pdfFilePath.pathString), pageCount = numberOfPages) }.getOrElse { exception -> throw when (exception) { is GeneralException -> exception @@ -51,7 +52,7 @@ class PdfResolver { suspend fun savePreviewImages(pdf: PdfInfo): List = runCatching { - val pdfFile = pdf.filePath.toFile() + val pdfFile = pdf.systemFile PDDocument .load(pdfFile) diff --git a/src/main/kotlin/com/devooks/backend/review/v1/controller/ReviewCommentController.kt b/src/main/kotlin/com/devooks/backend/review/v1/controller/ReviewCommentController.kt index 500c6f5..d472790 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/controller/ReviewCommentController.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/controller/ReviewCommentController.kt @@ -2,6 +2,9 @@ package com.devooks.backend.review.v1.controller import com.devooks.backend.auth.v1.domain.Authorization import com.devooks.backend.auth.v1.service.TokenService +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.dto.PageResponse.Companion.toResponse +import com.devooks.backend.review.v1.controller.docs.ReviewCommentControllerDocs import com.devooks.backend.review.v1.domain.ReviewComment import com.devooks.backend.review.v1.dto.CreateReviewCommentCommand import com.devooks.backend.review.v1.dto.CreateReviewCommentRequest @@ -10,15 +13,18 @@ import com.devooks.backend.review.v1.dto.CreateReviewCommentResponse.Companion.t import com.devooks.backend.review.v1.dto.DeleteReviewCommentCommand import com.devooks.backend.review.v1.dto.DeleteReviewCommentResponse import com.devooks.backend.review.v1.dto.GetReviewCommentsCommand -import com.devooks.backend.review.v1.dto.GetReviewCommentsResponse -import com.devooks.backend.review.v1.dto.GetReviewCommentsResponse.Companion.toGetReviewCommentsResponse import com.devooks.backend.review.v1.dto.ModifyReviewCommentCommand import com.devooks.backend.review.v1.dto.ModifyReviewCommentRequest import com.devooks.backend.review.v1.dto.ModifyReviewCommentResponse import com.devooks.backend.review.v1.dto.ModifyReviewCommentResponse.Companion.toModifyReviewCommentResponse +import com.devooks.backend.review.v1.dto.ReviewCommentView +import com.devooks.backend.review.v1.dto.ReviewCommentView.Companion.toReviewCommentView import com.devooks.backend.review.v1.service.ReviewCommentEventService import com.devooks.backend.review.v1.service.ReviewCommentService import com.devooks.backend.review.v1.service.ReviewService +import jakarta.validation.Valid +import java.util.UUID +import org.springframework.data.domain.Page import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.annotation.DeleteMapping @@ -39,11 +45,12 @@ class ReviewCommentController( private val reviewCommentEventService: ReviewCommentEventService, private val tokenService: TokenService, private val reviewService: ReviewService, -) { +): ReviewCommentControllerDocs { @Transactional @PostMapping - suspend fun createReviewComment( + override suspend fun createReviewComment( + @Valid @RequestBody request: CreateReviewCommentRequest, @RequestHeader(AUTHORIZATION) @@ -58,24 +65,25 @@ class ReviewCommentController( } @GetMapping - suspend fun getReviewComments( - @RequestParam(required = false, defaultValue = "") - reviewId: String, - @RequestParam(required = false, defaultValue = "") - page: String, - @RequestParam(required = false, defaultValue = "") - count: String, - ): GetReviewCommentsResponse { + override suspend fun getReviewComments( + @RequestParam + reviewId: UUID, + @RequestParam + page: Int, + @RequestParam + count: Int, + ): PageResponse { val command = GetReviewCommentsCommand(reviewId, page, count) - val reviewCommentList: List = reviewCommentService.get(command) - return reviewCommentList.toGetReviewCommentsResponse() + val reviewCommentList: Page = reviewCommentService.get(command) + return reviewCommentList.map { it.toReviewCommentView() }.toResponse() } @Transactional @PatchMapping("/{commentId}") - suspend fun modifyReviewComment( - @PathVariable(name = "commentId", required = false) - commentId: String, + override suspend fun modifyReviewComment( + @PathVariable(name = "commentId") + commentId: UUID, + @Valid @RequestBody request: ModifyReviewCommentRequest, @RequestHeader(AUTHORIZATION) @@ -89,9 +97,9 @@ class ReviewCommentController( @Transactional @DeleteMapping("/{commentId}") - suspend fun deleteReviewComment( - @PathVariable(name = "commentId", required = false) - commentId: String, + override suspend fun deleteReviewComment( + @PathVariable(name = "commentId") + commentId: UUID, @RequestHeader(AUTHORIZATION) authorization: String, ): DeleteReviewCommentResponse { diff --git a/src/main/kotlin/com/devooks/backend/review/v1/controller/ReviewController.kt b/src/main/kotlin/com/devooks/backend/review/v1/controller/ReviewController.kt index 9f3d69f..2f9919b 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/controller/ReviewController.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/controller/ReviewController.kt @@ -2,7 +2,10 @@ package com.devooks.backend.review.v1.controller import com.devooks.backend.auth.v1.domain.Authorization import com.devooks.backend.auth.v1.service.TokenService +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.dto.PageResponse.Companion.toResponse import com.devooks.backend.ebook.v1.service.EbookService +import com.devooks.backend.review.v1.controller.docs.ReviewControllerDocs import com.devooks.backend.review.v1.domain.Review import com.devooks.backend.review.v1.dto.CreateReviewCommand import com.devooks.backend.review.v1.dto.CreateReviewRequest @@ -11,16 +14,18 @@ import com.devooks.backend.review.v1.dto.CreateReviewResponse.Companion.toCreate import com.devooks.backend.review.v1.dto.DeleteReviewCommand import com.devooks.backend.review.v1.dto.DeleteReviewResponse import com.devooks.backend.review.v1.dto.GetReviewsCommand -import com.devooks.backend.review.v1.dto.GetReviewsResponse -import com.devooks.backend.review.v1.dto.GetReviewsResponse.Companion.toGetReviewsResponse import com.devooks.backend.review.v1.dto.ModifyReviewCommand import com.devooks.backend.review.v1.dto.ModifyReviewRequest import com.devooks.backend.review.v1.dto.ModifyReviewResponse import com.devooks.backend.review.v1.dto.ModifyReviewResponse.Companion.toModifyReviewResponse +import com.devooks.backend.review.v1.dto.ReviewView +import com.devooks.backend.review.v1.dto.ReviewView.Companion.toReviewView import com.devooks.backend.review.v1.service.ReviewEventService import com.devooks.backend.review.v1.service.ReviewService import com.devooks.backend.transaciton.v1.service.TransactionService +import jakarta.validation.Valid import java.util.* +import org.springframework.data.domain.Page import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.annotation.DeleteMapping @@ -42,11 +47,12 @@ class ReviewController( private val transactionService: TransactionService, private val ebookService: EbookService, private val reviewEventService: ReviewEventService, -) { +) : ReviewControllerDocs { @Transactional @PostMapping - suspend fun createReview( + override suspend fun createReview( + @Valid @RequestBody request: CreateReviewRequest, @RequestHeader(AUTHORIZATION) @@ -62,26 +68,25 @@ class ReviewController( } @GetMapping - suspend fun getReviews( - @RequestParam(required = false, defaultValue = "") - ebookId: String, - @RequestParam(required = false, defaultValue = "") - memberId: String, - @RequestParam(required = false, defaultValue = "") - page: String, - @RequestParam(required = false, defaultValue = "") - count: String, - ): GetReviewsResponse { - val command = GetReviewsCommand(ebookId, memberId, page, count) - val reviewList: List = reviewService.get(command) - return reviewList.toGetReviewsResponse() + override suspend fun getReviews( + @RequestParam + ebookId: UUID, + @RequestParam + page: Int, + @RequestParam + count: Int, + ): PageResponse { + val command = GetReviewsCommand(ebookId, page, count) + val reviewList: Page = reviewService.get(command) + return reviewList.map { it.toReviewView() }.toResponse() } @Transactional @PatchMapping("/{reviewId}") - suspend fun modifyReview( - @PathVariable(name = "reviewId", required = false) - reviewId: String, + override suspend fun modifyReview( + @PathVariable(name = "reviewId") + reviewId: UUID, + @Valid @RequestBody request: ModifyReviewRequest, @RequestHeader(AUTHORIZATION) @@ -95,9 +100,9 @@ class ReviewController( @Transactional @DeleteMapping("/{reviewId}") - suspend fun deleteReview( - @PathVariable(name = "reviewId", required = false) - reviewId: String, + override suspend fun deleteReview( + @PathVariable(name = "reviewId") + reviewId: UUID, @RequestHeader(AUTHORIZATION) authorization: String, ): DeleteReviewResponse { diff --git a/src/main/kotlin/com/devooks/backend/review/v1/controller/docs/ReviewCommentControllerDocs.kt b/src/main/kotlin/com/devooks/backend/review/v1/controller/docs/ReviewCommentControllerDocs.kt new file mode 100644 index 0000000..6b81e68 --- /dev/null +++ b/src/main/kotlin/com/devooks/backend/review/v1/controller/docs/ReviewCommentControllerDocs.kt @@ -0,0 +1,196 @@ +package com.devooks.backend.review.v1.controller.docs + +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.exception.ErrorResponse +import com.devooks.backend.review.v1.dto.CreateReviewCommentRequest +import com.devooks.backend.review.v1.dto.CreateReviewCommentResponse +import com.devooks.backend.review.v1.dto.DeleteReviewCommentResponse +import com.devooks.backend.review.v1.dto.ModifyReviewCommentRequest +import com.devooks.backend.review.v1.dto.ModifyReviewCommentResponse +import com.devooks.backend.review.v1.dto.ReviewCommentView +import io.swagger.v3.oas.annotations.Operation +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 java.util.* +import org.springframework.http.MediaType.APPLICATION_JSON_VALUE + +@Tag(name = "리뷰 댓글") +interface ReviewCommentControllerDocs { + + @Operation(summary = "리뷰 댓글 작성") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = CreateReviewCommentResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- REVIEW-404-1: 존재하지 않는 리뷰입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun createReviewComment( + request: CreateReviewCommentRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): CreateReviewCommentResponse + + @Operation(summary = "리뷰 댓글 목록 조회") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun getReviewComments( + @Schema(description = "리뷰 식별자", required = true, implementation = UUID::class) + reviewId: UUID, + @Schema(description = "페이지", implementation = Int::class, required = true) + page: Int, + @Schema(description = "개수", implementation = Int::class, required = true) + count: Int, + ): PageResponse + + @Operation(summary = "리뷰 댓글 수정") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ModifyReviewCommentResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- REVIEW-403-2: 자신이 작성한 리뷰 댓글만 수정할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- REVIEW-404-1: 존재하지 않는 리뷰 댓글입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ) + ] + ) + suspend fun modifyReviewComment( + @Schema(description = "리뷰 댓글 식별자", required = true, implementation = UUID::class) + commentId: UUID, + request: ModifyReviewCommentRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): ModifyReviewCommentResponse + + @Operation(summary = "리뷰 댓글 삭제") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = DeleteReviewCommentResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- REVIEW-403-2: 자신이 작성한 리뷰 댓글만 수정할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- REVIEW-404-1: 존재하지 않는 리뷰 댓글입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ) + ] + ) + suspend fun deleteReviewComment( + @Schema(description = "리뷰 댓글 식별자", required = true, implementation = UUID::class) + commentId: UUID, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): DeleteReviewCommentResponse +} diff --git a/src/main/kotlin/com/devooks/backend/review/v1/controller/docs/ReviewControllerDocs.kt b/src/main/kotlin/com/devooks/backend/review/v1/controller/docs/ReviewControllerDocs.kt new file mode 100644 index 0000000..695cc57 --- /dev/null +++ b/src/main/kotlin/com/devooks/backend/review/v1/controller/docs/ReviewControllerDocs.kt @@ -0,0 +1,217 @@ +package com.devooks.backend.review.v1.controller.docs + +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.exception.ErrorResponse +import com.devooks.backend.review.v1.dto.CreateReviewRequest +import com.devooks.backend.review.v1.dto.CreateReviewResponse +import com.devooks.backend.review.v1.dto.DeleteReviewResponse +import com.devooks.backend.review.v1.dto.ModifyReviewRequest +import com.devooks.backend.review.v1.dto.ModifyReviewResponse +import com.devooks.backend.review.v1.dto.ReviewView +import io.swagger.v3.oas.annotations.Operation +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 java.util.* +import org.springframework.http.MediaType.APPLICATION_JSON_VALUE + +@Tag(name = "리뷰") +interface ReviewControllerDocs { + + @Operation(summary = "리뷰 작성") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = CreateReviewResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- TRANSACTION-403-1: 구매한 전자책만 리뷰가 가능합니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- EBOOK-404-1: 전자책을 찾을 수 없습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "409", + description = "- REVIEW-409-1: 이미 작성한 리뷰입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ) + ] + ) + suspend fun createReview( + request: CreateReviewRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): CreateReviewResponse + + @Operation(summary = "리뷰 목록 조회") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun getReviews( + @Schema(description = "전자책 식별자", implementation = UUID::class, required = true) + ebookId: UUID, + @Schema(description = "페이지", implementation = Int::class, required = true) + page: Int, + @Schema(description = "개수", implementation = Int::class, required = true) + count: Int, + ): PageResponse + + @Operation(summary = "리뷰 수정") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ModifyReviewResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- REVIEW-403-1: 자신이 작성한 리뷰만 수정할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- REVIEW-404-1: 존재하지 않는 리뷰입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ) + ] + ) + suspend fun modifyReview( + @Schema(description = "리뷰 식별자", required = true, implementation = UUID::class) + reviewId: UUID, + request: ModifyReviewRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): ModifyReviewResponse + + @Operation(summary = "리뷰 삭제") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = DeleteReviewResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- REVIEW-403-1: 자신이 작성한 리뷰만 수정할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- REVIEW-404-1: 존재하지 않는 리뷰입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ) + ] + ) + suspend fun deleteReview( + @Schema(description = "리뷰 식별자", required = true, implementation = UUID::class) + reviewId: UUID, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): DeleteReviewResponse + +} diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewCommentRequest.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewCommentRequest.kt index c7c2817..d353b0d 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewCommentRequest.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewCommentRequest.kt @@ -1,17 +1,20 @@ package com.devooks.backend.review.v1.dto -import com.devooks.backend.review.v1.error.validateReviewContent -import com.devooks.backend.review.v1.error.validateReviewId +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank import java.util.* data class CreateReviewCommentRequest( - val reviewId: String?, - val content: String?, + @Schema(description = "리뷰 식별자", required = true, implementation = UUID::class) + val reviewId: UUID, + @field:NotBlank + @Schema(description = "내용", required = true) + val content: String, ) { fun toCommand(requesterId: UUID): CreateReviewCommentCommand = CreateReviewCommentCommand( - reviewId = reviewId.validateReviewId(), - content = content.validateReviewContent(), + reviewId = reviewId, + content = content, requesterId = requesterId, ) } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewCommentResponse.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewCommentResponse.kt index 4f18e2d..5cdf2d8 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewCommentResponse.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewCommentResponse.kt @@ -1,13 +1,13 @@ package com.devooks.backend.review.v1.dto import com.devooks.backend.review.v1.domain.ReviewComment -import com.devooks.backend.review.v1.dto.ReviewCommentDto.Companion.toDto +import com.devooks.backend.review.v1.dto.ReviewCommentView.Companion.toReviewCommentView data class CreateReviewCommentResponse( - val reviewComment: ReviewCommentDto, + val reviewComment: ReviewCommentView, ) { companion object { fun ReviewComment.toCreateReviewCommentResponse() = - CreateReviewCommentResponse(this.toDto()) + CreateReviewCommentResponse(this.toReviewCommentView()) } } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewRequest.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewRequest.kt index 6f927a4..f7caa4f 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewRequest.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewRequest.kt @@ -1,20 +1,27 @@ package com.devooks.backend.review.v1.dto -import com.devooks.backend.review.v1.error.validateRating -import com.devooks.backend.review.v1.error.validateReviewContent -import com.devooks.backend.wishlist.v1.error.validateEbookId +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank import java.util.* data class CreateReviewRequest( - val ebookId: String?, - val rating: String?, - val content: String? + @Schema(description = "전자책 식별자", implementation = UUID::class, required = true) + val ebookId: UUID, + @field:Min(0) + @field:Max(5) + @Schema(description = "평점 (0~5점)", required = true) + val rating: Int, + @field:NotBlank + @Schema(description = "내용", required = true) + val content: String ) { fun toCommand(requesterId: UUID): CreateReviewCommand = CreateReviewCommand( - ebookId = ebookId.validateEbookId(), - rating = rating.validateRating(), - content = content.validateReviewContent(), + ebookId = ebookId, + rating = rating, + content = content, requesterId = requesterId ) } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewResponse.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewResponse.kt index 506ff04..3a8615f 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewResponse.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/CreateReviewResponse.kt @@ -1,12 +1,12 @@ package com.devooks.backend.review.v1.dto import com.devooks.backend.review.v1.domain.Review -import com.devooks.backend.review.v1.dto.ReviewDto.Companion.toDto +import com.devooks.backend.review.v1.dto.ReviewView.Companion.toReviewView data class CreateReviewResponse( - val review: ReviewDto, + val review: ReviewView, ) { companion object { - fun Review.toCreateReviewResponse() = CreateReviewResponse(this.toDto()) + fun Review.toCreateReviewResponse() = CreateReviewResponse(this.toReviewView()) } } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommand.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommand.kt index 553906f..35ef819 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommand.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommand.kt @@ -1,17 +1,8 @@ package com.devooks.backend.review.v1.dto -import com.devooks.backend.review.v1.error.validateReviewId import java.util.* data class DeleteReviewCommand( val reviewId: UUID, val requesterId: UUID, -) { - constructor( - reviewId: String, - requesterId: UUID, - ) : this( - reviewId = reviewId.validateReviewId(), - requesterId = requesterId - ) -} +) diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommentCommand.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommentCommand.kt index 76bf816..8216f59 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommentCommand.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommentCommand.kt @@ -1,17 +1,8 @@ package com.devooks.backend.review.v1.dto -import com.devooks.backend.review.v1.error.validateReviewCommentId import java.util.* class DeleteReviewCommentCommand( val commentId: UUID, val requesterId: UUID, -) { - constructor( - commentId: String, - requesterId: UUID, - ) : this( - commentId = commentId.validateReviewCommentId(), - requesterId = requesterId, - ) -} +) diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommentResponse.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommentResponse.kt index 3b6889f..678314f 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommentResponse.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewCommentResponse.kt @@ -1,5 +1,8 @@ package com.devooks.backend.review.v1.dto +import io.swagger.v3.oas.annotations.media.Schema + data class DeleteReviewCommentResponse( + @Schema(description = "결과 메시지", example = "리뷰 댓글 삭제를 완료했습니다.") val message: String = "리뷰 댓글 삭제를 완료했습니다." ) diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewResponse.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewResponse.kt index d7060b3..0332431 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewResponse.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/DeleteReviewResponse.kt @@ -1,5 +1,8 @@ package com.devooks.backend.review.v1.dto +import io.swagger.v3.oas.annotations.media.Schema + data class DeleteReviewResponse( + @Schema(description = "결과 메시지", example = "리뷰 삭제를 완료했습니다.") val message: String = "리뷰 삭제를 완료했습니다." ) diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewCommentsCommand.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewCommentsCommand.kt index 5f10074..cf869b4 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewCommentsCommand.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewCommentsCommand.kt @@ -1,20 +1,19 @@ package com.devooks.backend.review.v1.dto import com.devooks.backend.common.dto.Paging -import com.devooks.backend.review.v1.error.validateReviewId import java.util.* import org.springframework.data.domain.Pageable data class GetReviewCommentsCommand( val reviewId: UUID, - private val paging: Paging + private val paging: Paging, ) { constructor( - reviewId: String, - page: String, - count: String + reviewId: UUID, + page: Int, + count: Int, ) : this( - reviewId = reviewId.validateReviewId(), + reviewId = reviewId, paging = Paging(page, count) ) diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewCommentsResponse.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewCommentsResponse.kt deleted file mode 100644 index 8d91d2f..0000000 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewCommentsResponse.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.devooks.backend.review.v1.dto - -import com.devooks.backend.review.v1.domain.ReviewComment -import com.devooks.backend.review.v1.dto.ReviewCommentDto.Companion.toDto - -data class GetReviewCommentsResponse( - val reviewComments: List, -) { - companion object { - fun List.toGetReviewCommentsResponse() = - GetReviewCommentsResponse(map { it.toDto() }) - } -} diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewsCommand.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewsCommand.kt index c45fd74..b783ee4 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewsCommand.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewsCommand.kt @@ -1,23 +1,18 @@ package com.devooks.backend.review.v1.dto import com.devooks.backend.common.dto.Paging -import com.devooks.backend.member.v1.error.validateMemberId -import com.devooks.backend.wishlist.v1.error.validateEbookId import java.util.* class GetReviewsCommand( - val ebookId: UUID?, - val memberId: UUID?, + val ebookId: UUID, private val paging: Paging, ) { constructor( - ebookId: String, - memberId: String, - page: String, - count: String, + ebookId: UUID, + page: Int, + count: Int, ) : this( - ebookId = ebookId.takeIf { it.isNotBlank() }?.validateEbookId(), - memberId = memberId.takeIf { it.isNotBlank() }?.validateMemberId(), + ebookId = ebookId, paging = Paging(page, count) ) @@ -26,4 +21,6 @@ class GetReviewsCommand( val limit: Int get() = paging.limit + + val pageable = paging.value } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewsResponse.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewsResponse.kt deleted file mode 100644 index 03faa48..0000000 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/GetReviewsResponse.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.devooks.backend.review.v1.dto - -import com.devooks.backend.review.v1.domain.Review -import com.devooks.backend.review.v1.dto.ReviewDto.Companion.toDto - -data class GetReviewsResponse( - val reviews: List, -) { - companion object { - fun List.toGetReviewsResponse() = - GetReviewsResponse(reviews = this.map { it.toDto() }) - } -} diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommand.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommand.kt index 2d6d624..6d4f297 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommand.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommand.kt @@ -4,7 +4,7 @@ import java.util.* class ModifyReviewCommand( val reviewId: UUID, - val rating: Int, - val content: String, + val rating: Int?, + val content: String?, val requesterId: UUID, ) diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommentRequest.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommentRequest.kt index 865b099..d0fbb04 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommentRequest.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommentRequest.kt @@ -1,16 +1,18 @@ package com.devooks.backend.review.v1.dto -import com.devooks.backend.review.v1.error.validateReviewCommentId -import com.devooks.backend.review.v1.error.validateReviewContent +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank import java.util.* data class ModifyReviewCommentRequest( - val content: String?, + @field:NotBlank + @Schema(description = "내용", required = true) + val content: String, ) { - fun toCommand(commentId: String, requesterId: UUID): ModifyReviewCommentCommand = + fun toCommand(commentId: UUID, requesterId: UUID): ModifyReviewCommentCommand = ModifyReviewCommentCommand( - content = content.validateReviewContent(), - commentId = commentId.validateReviewCommentId(), + content = content, + commentId = commentId, requesterId = requesterId, ) } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommentResponse.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommentResponse.kt index a32d443..8b8dc77 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommentResponse.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewCommentResponse.kt @@ -1,13 +1,13 @@ package com.devooks.backend.review.v1.dto import com.devooks.backend.review.v1.domain.ReviewComment -import com.devooks.backend.review.v1.dto.ReviewCommentDto.Companion.toDto +import com.devooks.backend.review.v1.dto.ReviewCommentView.Companion.toReviewCommentView data class ModifyReviewCommentResponse( - val reviewComment: ReviewCommentDto + val reviewComment: ReviewCommentView ) { companion object { fun ReviewComment.toModifyReviewCommentResponse() = - ModifyReviewCommentResponse(toDto()) + ModifyReviewCommentResponse(toReviewCommentView()) } } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewRequest.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewRequest.kt index e7f8218..de000d3 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewRequest.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewRequest.kt @@ -1,19 +1,25 @@ package com.devooks.backend.review.v1.dto -import com.devooks.backend.review.v1.error.validateRating -import com.devooks.backend.review.v1.error.validateReviewContent -import com.devooks.backend.review.v1.error.validateReviewId +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.Size import java.util.* data class ModifyReviewRequest( - val rating: String?, + @field:Min(0) + @field:Max(5) + @Schema(description = "평점 (0~5점)", required = true) + val rating: Int?, + @field:Size(min = 1) + @Schema(description = "내용", required = true) val content: String?, ) { - fun toCommand(reviewId: String, requesterId: UUID): ModifyReviewCommand = + fun toCommand(reviewId: UUID, requesterId: UUID): ModifyReviewCommand = ModifyReviewCommand( - reviewId = reviewId.validateReviewId(), - rating = rating.validateRating(), - content = content.validateReviewContent(), + reviewId = reviewId, + rating = rating, + content = content, requesterId = requesterId ) } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewResponse.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewResponse.kt index 47319f0..9d4af9b 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewResponse.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/ModifyReviewResponse.kt @@ -1,12 +1,12 @@ package com.devooks.backend.review.v1.dto import com.devooks.backend.review.v1.domain.Review -import com.devooks.backend.review.v1.dto.ReviewDto.Companion.toDto +import com.devooks.backend.review.v1.dto.ReviewView.Companion.toReviewView class ModifyReviewResponse( - val review: ReviewDto + val review: ReviewView ) { companion object { - fun Review.toModifyReviewResponse() = ModifyReviewResponse(this.toDto()) + fun Review.toModifyReviewResponse() = ModifyReviewResponse(this.toReviewView()) } } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewCommentDto.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewCommentView.kt similarity index 57% rename from src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewCommentDto.kt rename to src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewCommentView.kt index c62330f..abbbd94 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewCommentDto.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewCommentView.kt @@ -1,20 +1,27 @@ package com.devooks.backend.review.v1.dto import com.devooks.backend.review.v1.domain.ReviewComment +import io.swagger.v3.oas.annotations.media.Schema import java.time.Instant import java.util.* -data class ReviewCommentDto( +data class ReviewCommentView( + @Schema(description = "리뷰 댓글 식별자") val id: UUID, + @Schema(description = "내용") val content: String, + @Schema(description = "리뷰 식별자") val reviewId: UUID, + @Schema(description = "작성자 식별자") val writerMemberId: UUID, + @Schema(description = "작성 날짜") val writtenDate: Instant, + @Schema(description = "수정 날짜") val modifiedDate: Instant, ) { companion object { - fun ReviewComment.toDto(): ReviewCommentDto = - ReviewCommentDto( + fun ReviewComment.toReviewCommentView(): ReviewCommentView = + ReviewCommentView( id = this.id, content = this.content, reviewId = this.reviewId, diff --git a/src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewDto.kt b/src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewView.kt similarity index 58% rename from src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewDto.kt rename to src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewView.kt index a3d796b..b0719e8 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewDto.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/dto/ReviewView.kt @@ -1,21 +1,29 @@ package com.devooks.backend.review.v1.dto import com.devooks.backend.review.v1.domain.Review +import io.swagger.v3.oas.annotations.media.Schema import java.time.Instant import java.util.* -data class ReviewDto( +data class ReviewView( + @Schema(description = "리뷰 식별자") val id: UUID, + @Schema(description = "평점 (0~5점)") val rating: Int, + @Schema(description = "내용") val content: String, + @Schema(description = "전자책 식별자") val ebookId: UUID, + @Schema(description = "작성자 회원 식별자") val writerMemberId: UUID, + @Schema(description = "작성 날짜") val writtenDate: Instant, + @Schema(description = "수정 날짜") val modifiedDate: Instant, ) { companion object { - fun Review.toDto(): ReviewDto = - ReviewDto( + fun Review.toReviewView(): ReviewView = + ReviewView( id = this.id, rating = this.rating, content = this.content, diff --git a/src/main/kotlin/com/devooks/backend/review/v1/entity/ReviewEntity.kt b/src/main/kotlin/com/devooks/backend/review/v1/entity/ReviewEntity.kt index e5501fb..26b59a3 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/entity/ReviewEntity.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/entity/ReviewEntity.kt @@ -1,6 +1,7 @@ package com.devooks.backend.review.v1.entity import com.devooks.backend.review.v1.domain.Review +import com.devooks.backend.review.v1.dto.ModifyReviewCommand import java.time.Instant import java.util.* import org.springframework.data.annotation.Id @@ -35,4 +36,11 @@ data class ReviewEntity( writtenDate = this.writtenDate, modifiedDate = this.modifiedDate, ) + + fun update(command: ModifyReviewCommand) = + copy( + rating = command.rating ?: this.rating, + content = command.content ?: this.content, + modifiedDate = Instant.now(), + ) } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/error/ReviewError.kt b/src/main/kotlin/com/devooks/backend/review/v1/error/ReviewError.kt index 8ed23f5..771a5b9 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/error/ReviewError.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/error/ReviewError.kt @@ -1,21 +1,11 @@ package com.devooks.backend.review.v1.error import com.devooks.backend.common.exception.GeneralException -import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.CONFLICT import org.springframework.http.HttpStatus.FORBIDDEN import org.springframework.http.HttpStatus.NOT_FOUND enum class ReviewError(val exception: GeneralException) { - // 400 - REQUIRED_RATING(GeneralException("REVIEW-400-1", BAD_REQUEST, "평점이 반드시 필요합니다.")), - INVALID_RATING(GeneralException("REVIEW-400-2", BAD_REQUEST, "잘못된 형식의 평점입니다.")), - REQUIRED_REVIEW_CONTENT(GeneralException("REVIEW-400-3", BAD_REQUEST, "내용이 반드시 필요합니다.")), - REQUIRED_REVIEW_ID(GeneralException("REVIEW-400-4", BAD_REQUEST, "리뷰 식별자가 반드시 필요합니다.")), - INVALID_REVIEW_ID(GeneralException("REVIEW-400-5", BAD_REQUEST, "잘못된 형식의 리뷰 식별자입니다.")), - REQUIRED_REVIEW_COMMENT_ID(GeneralException("REVIEW-400-6", BAD_REQUEST, "리뷰 댓글 식별자가 반드시 필요합니다.")), - INVALID_REVIEW_COMMENT_ID(GeneralException("REVIEW-400-7", BAD_REQUEST, "잘못된 형식의 리뷰 댓글 식별자입니다.")), - // 403 FORBIDDEN_MODIFY_REVIEW(GeneralException("REVIEW-403-1", FORBIDDEN, "자신이 작성한 리뷰만 수정할 수 있습니다.")), FORBIDDEN_MODIFY_REVIEW_COMMENT(GeneralException("REVIEW-403-2", FORBIDDEN, "자신이 작성한 리뷰 댓글만 수정할 수 있습니다.")), @@ -28,4 +18,4 @@ enum class ReviewError(val exception: GeneralException) { DUPLICATE_REVIEW(GeneralException("REVIEW-409-1", CONFLICT, "이미 작성한 리뷰입니다.")) ; -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/review/v1/error/ReviewValidation.kt b/src/main/kotlin/com/devooks/backend/review/v1/error/ReviewValidation.kt deleted file mode 100644 index 3d7cdf1..0000000 --- a/src/main/kotlin/com/devooks/backend/review/v1/error/ReviewValidation.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.devooks.backend.review.v1.error - -import com.devooks.backend.common.error.validateUUID -import com.devooks.backend.common.error.validateNotBlank -import java.util.* - -fun String?.validateRating(): Int = - validateNotBlank(ReviewError.REQUIRED_RATING.exception) - .runCatching { toInt() }.getOrElse { throw ReviewError.INVALID_RATING.exception } - .also { - if (it !in 0..5) { - throw ReviewError.INVALID_RATING.exception - } - } - -fun String?.validateReviewContent(): String = - validateNotBlank(ReviewError.REQUIRED_REVIEW_CONTENT.exception) - -fun String?.validateReviewId(): UUID = - validateNotBlank(ReviewError.REQUIRED_REVIEW_ID.exception) - .validateUUID(ReviewError.INVALID_REVIEW_ID.exception) - -fun String?.validateReviewCommentId(): UUID = - validateNotBlank(ReviewError.REQUIRED_REVIEW_COMMENT_ID.exception) - .validateUUID(ReviewError.INVALID_REVIEW_COMMENT_ID.exception) \ No newline at end of file diff --git a/src/main/kotlin/com/devooks/backend/review/v1/repository/ReviewCommentRepository.kt b/src/main/kotlin/com/devooks/backend/review/v1/repository/ReviewCommentRepository.kt index 0aa2831..e15b541 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/repository/ReviewCommentRepository.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/repository/ReviewCommentRepository.kt @@ -11,4 +11,5 @@ import org.springframework.stereotype.Repository interface ReviewCommentRepository : CoroutineCrudRepository { suspend fun findAllByReviewId(reviewId: UUID, pageable: Pageable): Flow + suspend fun countByReviewId(reviewId: UUID): Long } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/repository/ReviewQueryRepository.kt b/src/main/kotlin/com/devooks/backend/review/v1/repository/ReviewQueryRepository.kt index d7724de..668c4fe 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/repository/ReviewQueryRepository.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/repository/ReviewQueryRepository.kt @@ -7,7 +7,6 @@ import com.devooks.backend.review.v1.domain.Review import com.devooks.backend.review.v1.dto.GetReviewsCommand import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import org.jooq.Condition import org.jooq.impl.DSL import org.springframework.stereotype.Repository @@ -28,24 +27,21 @@ class ReviewQueryRepository : JooqR2dbcRepository() { REVIEW .join(EBOOK).on(EBOOK.EBOOK_ID.eq(REVIEW.EBOOK_ID)) ).where( - buildConditions(command) + REVIEW.EBOOK_ID.eq(command.ebookId) ).orderBy( REVIEW.WRITTEN_DATE.desc(), ).offset(command.offset).limit(command.limit) }.map { it.into(Review::class.java) } - private fun buildConditions(command: GetReviewsCommand): Condition { - val conditions = mutableListOf() - - command.ebookId?.also { - conditions.add(REVIEW.EBOOK_ID.eq(it)) - } - - command.memberId?.also { - conditions.add(EBOOK.SELLING_MEMBER_ID.eq(it)) - } - - return conditions.reduceOrNull { acc, condition -> acc.and(condition) } ?: DSL.noCondition() - } + suspend fun countBy(command: GetReviewsCommand): Flow = + query { + select( + DSL.count() + ).from( + REVIEW + ).where( + REVIEW.EBOOK_ID.eq(command.ebookId) + ) + }.map { it.into(Long::class.java) } } diff --git a/src/main/kotlin/com/devooks/backend/review/v1/service/ReviewCommentService.kt b/src/main/kotlin/com/devooks/backend/review/v1/service/ReviewCommentService.kt index 03d8051..b4c9e01 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/service/ReviewCommentService.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/service/ReviewCommentService.kt @@ -12,6 +12,8 @@ import java.time.Instant import java.util.* import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl import org.springframework.stereotype.Service @Service @@ -27,11 +29,13 @@ class ReviewCommentService( return reviewCommentRepository.save(entity).toDomain() } - suspend fun get(command: GetReviewCommentsCommand): List = - reviewCommentRepository + suspend fun get(command: GetReviewCommentsCommand): Page { + val reviewComments = reviewCommentRepository .findAllByReviewId(command.reviewId, command.pageable) .map { it.toDomain() } - .toList() + val count = reviewCommentRepository.countByReviewId(command.reviewId) + return PageImpl(reviewComments.toList(), command.pageable, count) + } suspend fun modify(command: ModifyReviewCommentCommand): ReviewComment = findBy(command.commentId) diff --git a/src/main/kotlin/com/devooks/backend/review/v1/service/ReviewService.kt b/src/main/kotlin/com/devooks/backend/review/v1/service/ReviewService.kt index 6f571e9..c877479 100644 --- a/src/main/kotlin/com/devooks/backend/review/v1/service/ReviewService.kt +++ b/src/main/kotlin/com/devooks/backend/review/v1/service/ReviewService.kt @@ -10,9 +10,11 @@ import com.devooks.backend.review.v1.entity.ReviewEntity import com.devooks.backend.review.v1.error.ReviewError import com.devooks.backend.review.v1.repository.ReviewQueryRepository import com.devooks.backend.review.v1.repository.ReviewRepository -import java.time.Instant import java.util.* +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl import org.springframework.stereotype.Service @Service @@ -31,13 +33,16 @@ class ReviewService( return reviewRepository.save(entity).toDomain() } - suspend fun get(command: GetReviewsCommand): List = - reviewQueryRepository.findBy(command).toList() + suspend fun get(command: GetReviewsCommand): Page { + val reviews = reviewQueryRepository.findBy(command) + val count = reviewQueryRepository.countBy(command) + return PageImpl(reviews.toList(), command.pageable, count.first()) + } suspend fun modify(command: ModifyReviewCommand): Review = findById(command.reviewId) .also { review -> validateRequesterId(review, command.requesterId) } - .copy(rating = command.rating, content = command.content, modifiedDate = Instant.now()) + .update(command) .let { reviewRepository.save(it) } .toDomain() diff --git a/src/main/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryController.kt b/src/main/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryController.kt index e9fec79..8522268 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryController.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryController.kt @@ -2,20 +2,24 @@ package com.devooks.backend.service.v1.controller import com.devooks.backend.auth.v1.domain.Authorization import com.devooks.backend.auth.v1.service.TokenService +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.dto.PageResponse.Companion.toResponse +import com.devooks.backend.service.v1.controller.docs.ServiceInquiryControllerDocs import com.devooks.backend.service.v1.domain.ServiceInquiry import com.devooks.backend.service.v1.domain.ServiceInquiryImage +import com.devooks.backend.service.v1.dto.ServiceInquiryView +import com.devooks.backend.service.v1.dto.ServiceInquiryView.Companion.toServiceInquiryView import com.devooks.backend.service.v1.dto.command.CreateServiceInquiryCommand import com.devooks.backend.service.v1.dto.command.GetServiceInquiriesCommand import com.devooks.backend.service.v1.dto.command.ModifyServiceInquiryCommand import com.devooks.backend.service.v1.dto.request.CreateServiceInquiryRequest import com.devooks.backend.service.v1.dto.request.ModifyServiceInquiryRequest import com.devooks.backend.service.v1.dto.response.CreateServiceInquiryResponse -import com.devooks.backend.service.v1.dto.response.GetServiceInquiriesResponse -import com.devooks.backend.service.v1.dto.response.GetServiceInquiriesResponse.Companion.toGetServiceInquiriesResponse import com.devooks.backend.service.v1.dto.response.ModifyServiceInquiryResponse import com.devooks.backend.service.v1.dto.response.ServiceInquiryResponse import com.devooks.backend.service.v1.service.ServiceInquiryImageService import com.devooks.backend.service.v1.service.ServiceInquiryService +import jakarta.validation.Valid import java.util.* import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.transaction.annotation.Transactional @@ -35,14 +39,15 @@ class ServiceInquiryController( private val tokenService: TokenService, private val serviceInquiryService: ServiceInquiryService, private val serviceInquiryImageService: ServiceInquiryImageService, -) { +) : ServiceInquiryControllerDocs { @Transactional @PostMapping - suspend fun createServiceInquiry( + override suspend fun createServiceInquiry( + @Valid @RequestBody request: CreateServiceInquiryRequest, - @RequestHeader(AUTHORIZATION, required = false, defaultValue = "") + @RequestHeader(AUTHORIZATION) authorization: String, ): CreateServiceInquiryResponse { val requesterId: UUID = tokenService.getMemberId(Authorization(authorization)) @@ -54,34 +59,37 @@ class ServiceInquiryController( } @GetMapping - suspend fun getServiceInquiries( - @RequestParam(required = false, defaultValue = "") - page: String, - @RequestParam(required = false, defaultValue = "") - count: String, - @RequestHeader(AUTHORIZATION, required = false, defaultValue = "") + override suspend fun getServiceInquiries( + @RequestParam + page: Int, + @RequestParam + count: Int, + @RequestHeader(AUTHORIZATION) authorization: String, - ): GetServiceInquiriesResponse { + ): PageResponse { val requesterId = tokenService.getMemberId(Authorization(authorization)) val command = GetServiceInquiriesCommand(page = page, count = count, requesterId = requesterId) - return serviceInquiryService.get(command).toGetServiceInquiriesResponse() + val pageServiceInquiry = serviceInquiryService.get(command) + return pageServiceInquiry.map { it.toServiceInquiryView() }.toResponse() } @Transactional @PatchMapping("/{serviceInquiryId}") - suspend fun modifyServiceInquiry( - @PathVariable("serviceInquiryId", required = false) - serviceInquiryId: String, + override suspend fun modifyServiceInquiry( + @PathVariable("serviceInquiryId") + serviceInquiryId: UUID, + @Valid @RequestBody request: ModifyServiceInquiryRequest, - @RequestHeader(AUTHORIZATION, required = false, defaultValue = "") + @RequestHeader(AUTHORIZATION) authorization: String, ): ModifyServiceInquiryResponse { val requesterId = tokenService.getMemberId(Authorization(authorization)) val command: ModifyServiceInquiryCommand = request.toCommand(serviceInquiryId, requesterId) val serviceInquiry: ServiceInquiry = serviceInquiryService.modify(command) - val serviceInquiryImageList: List = serviceInquiryImageService.modify(command, serviceInquiry) + val serviceInquiryImageList: List = + serviceInquiryImageService.modify(command, serviceInquiry) return ModifyServiceInquiryResponse(ServiceInquiryResponse(serviceInquiry, serviceInquiryImageList)) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryImagesController.kt b/src/main/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryImagesController.kt index 04d97c2..7e45961 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryImagesController.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryImagesController.kt @@ -2,12 +2,14 @@ package com.devooks.backend.service.v1.controller import com.devooks.backend.auth.v1.domain.Authorization import com.devooks.backend.auth.v1.service.TokenService +import com.devooks.backend.service.v1.controller.docs.ServiceInquiryImagesControllerDocs import com.devooks.backend.service.v1.domain.ServiceInquiryImage import com.devooks.backend.service.v1.dto.command.SaveServiceInquiryImagesCommand import com.devooks.backend.service.v1.dto.request.SaveServiceInquiryImagesRequest import com.devooks.backend.service.v1.dto.response.SaveServiceInquiryImagesResponse import com.devooks.backend.service.v1.dto.response.SaveServiceInquiryImagesResponse.Companion.toSaveServiceInquiryImagesResponse import com.devooks.backend.service.v1.service.ServiceInquiryImageService +import jakarta.validation.Valid import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.annotation.PostMapping @@ -21,14 +23,15 @@ import org.springframework.web.bind.annotation.RestController class ServiceInquiryImagesController( private val serviceInquiryImageService: ServiceInquiryImageService, private val tokenService: TokenService, -) { +): ServiceInquiryImagesControllerDocs { @Transactional @PostMapping - suspend fun saveServiceInquiryImages( + override suspend fun saveServiceInquiryImages( + @Valid @RequestBody request: SaveServiceInquiryImagesRequest, - @RequestHeader(AUTHORIZATION, required = false, defaultValue = "") + @RequestHeader(AUTHORIZATION) authorization: String, ): SaveServiceInquiryImagesResponse { val requesterId = tokenService.getMemberId(Authorization(authorization)) @@ -37,4 +40,4 @@ class ServiceInquiryImagesController( return imageList.toSaveServiceInquiryImagesResponse() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/service/v1/controller/docs/ServiceInquiryControllerDocs.kt b/src/main/kotlin/com/devooks/backend/service/v1/controller/docs/ServiceInquiryControllerDocs.kt new file mode 100644 index 0000000..8b82fbe --- /dev/null +++ b/src/main/kotlin/com/devooks/backend/service/v1/controller/docs/ServiceInquiryControllerDocs.kt @@ -0,0 +1,145 @@ +package com.devooks.backend.service.v1.controller.docs + +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.exception.ErrorResponse +import com.devooks.backend.review.v1.dto.ModifyReviewCommentResponse +import com.devooks.backend.service.v1.dto.ServiceInquiryView +import com.devooks.backend.service.v1.dto.request.CreateServiceInquiryRequest +import com.devooks.backend.service.v1.dto.request.ModifyServiceInquiryRequest +import com.devooks.backend.service.v1.dto.response.CreateServiceInquiryResponse +import com.devooks.backend.service.v1.dto.response.ModifyServiceInquiryResponse +import io.swagger.v3.oas.annotations.Operation +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 java.util.* +import org.springframework.http.MediaType.APPLICATION_JSON_VALUE + +@Tag(name = "서비스 문의") +interface ServiceInquiryControllerDocs { + + @Operation(summary = "서비스 문의 작성") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = CreateServiceInquiryResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- SERVICE-403-1: 자신이 등록한 사진만 등록할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun createServiceInquiry( + request: CreateServiceInquiryRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): CreateServiceInquiryResponse + + @Operation(summary = "서비스 문의 목록 조회") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun getServiceInquiries( + @Schema(description = "페이지", implementation = Int::class, required = true) + page: Int, + @Schema(description = "개수", implementation = Int::class, required = true) + count: Int, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): PageResponse + + @Operation(summary = "서비스 문의 수정") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ModifyReviewCommentResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- SERVICE-403-2: 자신이 등록한 서비스 문의만 수정할 수 있습니다.\n" + + "- SERVICE-403-1: 자신이 등록한 사진만 등록할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "404", + description = "- SERVICE-404-1: 서비스 문의를 찾을 수 없습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ) + ] + ) + suspend fun modifyServiceInquiry( + @Schema(description = "서비스 문의 식별자", required = true, implementation = UUID::class) + serviceInquiryId: UUID, + request: ModifyServiceInquiryRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String, + ): ModifyServiceInquiryResponse +} diff --git a/src/main/kotlin/com/devooks/backend/service/v1/controller/docs/ServiceInquiryImagesControllerDocs.kt b/src/main/kotlin/com/devooks/backend/service/v1/controller/docs/ServiceInquiryImagesControllerDocs.kt new file mode 100644 index 0000000..330565d --- /dev/null +++ b/src/main/kotlin/com/devooks/backend/service/v1/controller/docs/ServiceInquiryImagesControllerDocs.kt @@ -0,0 +1,58 @@ +package com.devooks.backend.service.v1.controller.docs + +import com.devooks.backend.common.exception.ErrorResponse +import com.devooks.backend.service.v1.dto.request.SaveServiceInquiryImagesRequest +import com.devooks.backend.service.v1.dto.response.SaveServiceInquiryImagesResponse +import io.swagger.v3.oas.annotations.Operation +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 org.springframework.http.MediaType.APPLICATION_JSON_VALUE + +@Tag(name = "서비스 문의 사진") +interface ServiceInquiryImagesControllerDocs { + + @Operation(summary = "서비스 문의 사진 저장") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "OK", + content = [ + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = SaveServiceInquiryImagesResponse::class) + ) + ] + ), + ApiResponse( + responseCode = "400", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ApiResponse( + responseCode = "403", + description = "- SERVICE-403-1: 자신이 등록한 사진만 등록할 수 있습니다.", + content = arrayOf( + Content( + mediaType = APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class) + ) + ) + ), + ] + ) + suspend fun saveServiceInquiryImages( + request: SaveServiceInquiryImagesRequest, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) + authorization: String + ): SaveServiceInquiryImagesResponse + +} diff --git a/src/main/kotlin/com/devooks/backend/service/v1/dto/ServiceInquiryImageDto.kt b/src/main/kotlin/com/devooks/backend/service/v1/dto/ServiceInquiryImageDto.kt index 72b1234..b2b142d 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/dto/ServiceInquiryImageDto.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/dto/ServiceInquiryImageDto.kt @@ -1,12 +1,16 @@ package com.devooks.backend.service.v1.dto import com.devooks.backend.service.v1.domain.ServiceInquiryImage +import io.swagger.v3.oas.annotations.media.Schema import java.util.* import kotlin.io.path.pathString data class ServiceInquiryImageDto( + @Schema(description = "서비스 문의 사진 식별자") val id: UUID, + @Schema(description = "사진 경로") val imagePath: String, + @Schema(description = "사진 순서") val order: Int, ) { companion object { diff --git a/src/main/kotlin/com/devooks/backend/service/v1/dto/ServiceInquiryDto.kt b/src/main/kotlin/com/devooks/backend/service/v1/dto/ServiceInquiryView.kt similarity index 58% rename from src/main/kotlin/com/devooks/backend/service/v1/dto/ServiceInquiryDto.kt rename to src/main/kotlin/com/devooks/backend/service/v1/dto/ServiceInquiryView.kt index 59cd2b8..a07d72c 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/dto/ServiceInquiryDto.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/dto/ServiceInquiryView.kt @@ -2,22 +2,31 @@ package com.devooks.backend.service.v1.dto import com.devooks.backend.service.v1.domain.InquiryProcessingStatus import com.devooks.backend.service.v1.repository.row.ServiceInquiryRow +import io.swagger.v3.oas.annotations.media.Schema import java.time.Instant import java.util.* -data class ServiceInquiryDto( +data class ServiceInquiryView( + @Schema(description = "서비스 문의 식별자") val id: UUID, + @Schema(description = "제목") val title: String, + @Schema(description = "내용") val content: String, + @Schema(description = "생성 날짜") val createdDate: Instant, + @Schema(description = "수정 날짜") val modifiedDate: Instant, + @Schema(description = "처리 상태", example = "WAITING") val inquiryProcessingStatus: InquiryProcessingStatus, + @Schema(description = "작성자 식별자") val writerMemberId: UUID, - val imageList: List + @Schema(description = "서비스 문의 사진 목록") + val imageList: List, ) { companion object { - fun ServiceInquiryRow.toDto() = - ServiceInquiryDto( + fun ServiceInquiryRow.toServiceInquiryView() = + ServiceInquiryView( id = this.id, title = this.title, content = this.content, diff --git a/src/main/kotlin/com/devooks/backend/service/v1/dto/command/GetServiceInquiriesCommand.kt b/src/main/kotlin/com/devooks/backend/service/v1/dto/command/GetServiceInquiriesCommand.kt index 395bfbc..f4c159d 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/dto/command/GetServiceInquiriesCommand.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/dto/command/GetServiceInquiriesCommand.kt @@ -8,8 +8,8 @@ class GetServiceInquiriesCommand( private val paging: Paging, ) { constructor( - page: String, - count: String, + page: Int, + count: Int, requesterId: UUID, ) : this( requesterId = requesterId, @@ -21,4 +21,6 @@ class GetServiceInquiriesCommand( val limit: Int get() = paging.limit + + val pageable = paging.value } diff --git a/src/main/kotlin/com/devooks/backend/service/v1/dto/request/CreateServiceInquiryRequest.kt b/src/main/kotlin/com/devooks/backend/service/v1/dto/request/CreateServiceInquiryRequest.kt index 589038b..9b9d04b 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/dto/request/CreateServiceInquiryRequest.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/dto/request/CreateServiceInquiryRequest.kt @@ -1,21 +1,25 @@ package com.devooks.backend.service.v1.dto.request import com.devooks.backend.service.v1.dto.command.CreateServiceInquiryCommand -import com.devooks.backend.service.v1.error.validateServiceInquiryContent -import com.devooks.backend.service.v1.error.validateServiceInquiryImageIdList -import com.devooks.backend.service.v1.error.validateServiceInquiryTitle +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank import java.util.* data class CreateServiceInquiryRequest( - val title: String?, - val content: String?, - val imageIdList: List?, + @field:NotBlank + @Schema(description = "제목", required = true) + val title: String, + @field:NotBlank + @Schema(description = "내용", required = true) + val content: String, + @Schema(description = "사진 식별자 목록") + val imageIdList: List?, ) { fun toCommand(requesterId: UUID): CreateServiceInquiryCommand = CreateServiceInquiryCommand( - title = title.validateServiceInquiryTitle(), - content = content.validateServiceInquiryContent(), - imageIdList = imageIdList?.validateServiceInquiryImageIdList(), + title = title, + content = content, + imageIdList = imageIdList, requesterId = requesterId ) } diff --git a/src/main/kotlin/com/devooks/backend/service/v1/dto/request/ModifyServiceInquiryRequest.kt b/src/main/kotlin/com/devooks/backend/service/v1/dto/request/ModifyServiceInquiryRequest.kt index 6eab224..1cd7dd2 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/dto/request/ModifyServiceInquiryRequest.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/dto/request/ModifyServiceInquiryRequest.kt @@ -1,46 +1,28 @@ package com.devooks.backend.service.v1.dto.request import com.devooks.backend.service.v1.dto.command.ModifyServiceInquiryCommand -import com.devooks.backend.service.v1.error.ServiceInquiryError -import com.devooks.backend.service.v1.error.validateServiceInquiryContent -import com.devooks.backend.service.v1.error.validateServiceInquiryId -import com.devooks.backend.service.v1.error.validateServiceInquiryImageIdList -import com.devooks.backend.service.v1.error.validateServiceInquiryTitle +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Size import java.util.* data class ModifyServiceInquiryRequest( - val serviceInquiry: ServiceInquiry?, - val isChanged: IsChanged?, + @field:Size(min = 1) + @Schema(description = "제목", nullable = true) + val title: String?, + @field:Size(min = 1) + @Schema(description = "내용", nullable = true) + val content: String?, + @Schema(description = "사진 식별자 목록", nullable = true) + val imageIdList: List?, ) { - data class ServiceInquiry( - val title: String? = null, - val content: String? = null, - val imageIdList: List? = null, - ) - - data class IsChanged( - val title: Boolean? = null, - val content: Boolean? = null, - val imageIdList: Boolean? = null, - ) - - fun toCommand(serviceInquiryId: String, requesterId: UUID) = - if (isChanged != null) { - if (serviceInquiry != null) { - ModifyServiceInquiryCommand( - serviceInquiryId.validateServiceInquiryId(), - if (isChanged.title == true) serviceInquiry.title.validateServiceInquiryTitle() else null, - if (isChanged.content == true) serviceInquiry.content.validateServiceInquiryContent() else null, - if (isChanged.imageIdList != null) serviceInquiry.imageIdList?.validateServiceInquiryImageIdList() else null, - requesterId - - ) - } else { - throw ServiceInquiryError.REQUIRED_IS_CHANGED_FOR_MODIFY.exception - } - } else { - throw ServiceInquiryError.REQUIRED_SERVICE_INQUIRY_FOR_MODIFY.exception - } + fun toCommand(serviceInquiryId: UUID, requesterId: UUID) = + ModifyServiceInquiryCommand( + serviceInquiryId = serviceInquiryId, + title = this.title, + content = this.content, + imageIdList = this.imageIdList, + requesterId = requesterId + ) } diff --git a/src/main/kotlin/com/devooks/backend/service/v1/dto/request/SaveServiceInquiryImagesRequest.kt b/src/main/kotlin/com/devooks/backend/service/v1/dto/request/SaveServiceInquiryImagesRequest.kt index f77a5ba..f0bea90 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/dto/request/SaveServiceInquiryImagesRequest.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/dto/request/SaveServiceInquiryImagesRequest.kt @@ -1,16 +1,20 @@ package com.devooks.backend.service.v1.dto.request import com.devooks.backend.common.dto.ImageDto -import com.devooks.backend.common.error.validateImages +import com.devooks.backend.common.dto.ImageDto.Companion.toDomain import com.devooks.backend.service.v1.dto.command.SaveServiceInquiryImagesCommand +import jakarta.validation.Valid +import jakarta.validation.constraints.NotEmpty import java.util.* data class SaveServiceInquiryImagesRequest( - val imageList: List?, + @field:NotEmpty + @field:Valid + val imageList: List, ) { fun toCommand(requesterId: UUID) = SaveServiceInquiryImagesCommand( - imageList = imageList.validateImages(), + imageList = imageList.toDomain(), requesterId = requesterId ) } diff --git a/src/main/kotlin/com/devooks/backend/service/v1/dto/response/GetServiceInquiriesResponse.kt b/src/main/kotlin/com/devooks/backend/service/v1/dto/response/GetServiceInquiriesResponse.kt index c39d4f2..3a30358 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/dto/response/GetServiceInquiriesResponse.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/dto/response/GetServiceInquiriesResponse.kt @@ -1,14 +1,14 @@ package com.devooks.backend.service.v1.dto.response -import com.devooks.backend.service.v1.dto.ServiceInquiryDto -import com.devooks.backend.service.v1.dto.ServiceInquiryDto.Companion.toDto +import com.devooks.backend.service.v1.dto.ServiceInquiryView +import com.devooks.backend.service.v1.dto.ServiceInquiryView.Companion.toServiceInquiryView import com.devooks.backend.service.v1.repository.row.ServiceInquiryRow data class GetServiceInquiriesResponse( - val serviceInquiryList: List + val serviceInquiryList: List ) { companion object { fun List.toGetServiceInquiriesResponse() = - GetServiceInquiriesResponse(map { it.toDto() }) + GetServiceInquiriesResponse(map { it.toServiceInquiryView() }) } } diff --git a/src/main/kotlin/com/devooks/backend/service/v1/dto/response/ServiceInquiryResponse.kt b/src/main/kotlin/com/devooks/backend/service/v1/dto/response/ServiceInquiryResponse.kt index 918072e..7beb6a4 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/dto/response/ServiceInquiryResponse.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/dto/response/ServiceInquiryResponse.kt @@ -5,17 +5,26 @@ import com.devooks.backend.service.v1.domain.ServiceInquiry import com.devooks.backend.service.v1.domain.ServiceInquiryImage import com.devooks.backend.service.v1.dto.ServiceInquiryImageDto import com.devooks.backend.service.v1.dto.ServiceInquiryImageDto.Companion.toDto +import io.swagger.v3.oas.annotations.media.Schema import java.time.Instant import java.util.* data class ServiceInquiryResponse( + @Schema(description = "서비스 문의 식별자") val id: UUID, + @Schema(description = "제목") val title: String, + @Schema(description = "내용") val content: String, + @Schema(description = "생성 날짜") val createdDate: Instant, + @Schema(description = "수정 날짜") val modifiedDate: Instant, + @Schema(description = "처리 상태") val inquiryProcessingStatus: InquiryProcessingStatus, + @Schema(description = "작성자 식별자") val writerMemberId: UUID, + @Schema(description = "서비스 문의 사진 목록") val imageList: List, ) { constructor( diff --git a/src/main/kotlin/com/devooks/backend/service/v1/error/ServiceInquiryError.kt b/src/main/kotlin/com/devooks/backend/service/v1/error/ServiceInquiryError.kt index cd33a02..bb2aaf1 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/error/ServiceInquiryError.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/error/ServiceInquiryError.kt @@ -4,17 +4,8 @@ import com.devooks.backend.common.exception.GeneralException import org.springframework.http.HttpStatus enum class ServiceInquiryError(val exception: GeneralException) { - // 400 - REQUIRED_SERVICE_INQUIRY_TITLE(GeneralException("SERVICE-400-1", HttpStatus.BAD_REQUEST, "서비스 문의 제목이 반드시 필요합니다.")), - REQUIRED_SERVICE_INQUIRY_CONTENT(GeneralException("SERVICE-400-2", HttpStatus.BAD_REQUEST, "서비스 문의 내용이 반드시 필요합니다.")), - INVALID_SERVICE_INQUIRY_IMAGE_ID(GeneralException("SERVICE-400-3", HttpStatus.BAD_REQUEST, "잘못된 형식의 사진 식별자입니다.")), - REQUIRED_SERVICE_INQUIRY_FOR_MODIFY(GeneralException("SERVICE-400-4", HttpStatus.BAD_REQUEST, "서비스 문의가 반드시 필요합니다.")), - REQUIRED_IS_CHANGED_FOR_MODIFY(GeneralException("SERVICE-400-5", HttpStatus.BAD_REQUEST, "수정 여부가 반드시 필요합니다.")), - REQUIRED_SERVICE_INQUIRY_ID(GeneralException("SERVICE-400-6", HttpStatus.BAD_REQUEST, "서비스 문의 식별자가 반드시 필요합니다.")), - INVALID_SERVICE_INQUIRY_ID(GeneralException("SERVICE-400-7", HttpStatus.BAD_REQUEST, "잘못된 형식의 서비스 문의 식별자입니다.")), - // 403 - FORBIDDEN_REGISTER_SERVICE_INQUIRY_TO_IMAGE(GeneralException("SERVICE-403-1", HttpStatus.FORBIDDEN, "자신이 등록한 사진만 서비스 문의에 등록할 수 있습니다.")), + FORBIDDEN_REGISTER_SERVICE_INQUIRY_TO_IMAGE(GeneralException("SERVICE-403-1", HttpStatus.FORBIDDEN, "자신이 등록한 사진만 등록할 수 있습니다.")), FORBIDDEN_MODIFY_SERVICE_INQUIRY(GeneralException("SERVICE-403-2", HttpStatus.FORBIDDEN, "자신이 등록한 서비스 문의만 수정할 수 있습니다.")), // 404 @@ -22,4 +13,4 @@ enum class ServiceInquiryError(val exception: GeneralException) { // 500 FAIL_MAP_TO_INQUIRY_PROCESSING_STATUS(GeneralException("SERVICE-500-1", HttpStatus.INTERNAL_SERVER_ERROR, "서비스 문의 조회를 실패했습니다.")), -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/service/v1/error/ServiceInquiryValidation.kt b/src/main/kotlin/com/devooks/backend/service/v1/error/ServiceInquiryValidation.kt deleted file mode 100644 index 4ac3fdf..0000000 --- a/src/main/kotlin/com/devooks/backend/service/v1/error/ServiceInquiryValidation.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.devooks.backend.service.v1.error - -import com.devooks.backend.common.error.validateNotBlank -import com.devooks.backend.common.error.validateUUID -import java.util.* - -fun String?.validateServiceInquiryTitle(): String = - validateNotBlank(ServiceInquiryError.REQUIRED_SERVICE_INQUIRY_TITLE.exception) - -fun String?.validateServiceInquiryContent(): String = - validateNotBlank(ServiceInquiryError.REQUIRED_SERVICE_INQUIRY_CONTENT.exception) - -fun List.validateServiceInquiryImageIdList(): List = - map { it.validateUUID(ServiceInquiryError.INVALID_SERVICE_INQUIRY_IMAGE_ID.exception) } - -fun String?.validateServiceInquiryId(): UUID = - validateNotBlank(ServiceInquiryError.REQUIRED_SERVICE_INQUIRY_ID.exception) - .validateUUID(ServiceInquiryError.INVALID_SERVICE_INQUIRY_ID.exception) \ No newline at end of file diff --git a/src/main/kotlin/com/devooks/backend/service/v1/repository/ServiceInquiryQueryRepository.kt b/src/main/kotlin/com/devooks/backend/service/v1/repository/ServiceInquiryQueryRepository.kt index f0b7660..0bd4138 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/repository/ServiceInquiryQueryRepository.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/repository/ServiceInquiryQueryRepository.kt @@ -3,8 +3,8 @@ package com.devooks.backend.service.v1.repository import com.devooks.backend.common.config.database.JooqR2dbcRepository import com.devooks.backend.jooq.tables.references.SERVICE_INQUIRY import com.devooks.backend.jooq.tables.references.SERVICE_INQUIRY_IMAGE -import com.devooks.backend.service.v1.repository.row.ServiceInquiryRow import com.devooks.backend.service.v1.dto.command.GetServiceInquiriesCommand +import com.devooks.backend.service.v1.repository.row.ServiceInquiryRow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.jooq.impl.DSL @@ -54,4 +54,17 @@ class ServiceInquiryQueryRepository : JooqR2dbcRepository() { it.into(ServiceInquiryRow::class.java) } + suspend fun countBy(command: GetServiceInquiriesCommand): Flow = + query { + val inquiry = SERVICE_INQUIRY + + select( + DSL.count() + ).from( + inquiry + ).where( + inquiry.WRITER_MEMBER_ID.eq(command.requesterId) + ) + }.map { it.into(Long::class.java) } + } diff --git a/src/main/kotlin/com/devooks/backend/service/v1/repository/row/ServiceInquiryRow.kt b/src/main/kotlin/com/devooks/backend/service/v1/repository/row/ServiceInquiryRow.kt index 716f6fb..355451d 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/repository/row/ServiceInquiryRow.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/repository/row/ServiceInquiryRow.kt @@ -23,5 +23,5 @@ data class ServiceInquiryRow( imagePath = it["image_path"] as String, order = (it["order"] as Long).toInt() ) - } + }.sortedBy { it.order } } diff --git a/src/main/kotlin/com/devooks/backend/service/v1/service/ServiceInquiryImageService.kt b/src/main/kotlin/com/devooks/backend/service/v1/service/ServiceInquiryImageService.kt index 29ebc5d..34bab57 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/service/ServiceInquiryImageService.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/service/ServiceInquiryImageService.kt @@ -60,33 +60,37 @@ class ServiceInquiryImageService( command: ModifyServiceInquiryCommand, serviceInquiry: ServiceInquiry, ): List { - val serviceInquiryImageList = - serviceInquiryImageCrudRepository - .findAllByServiceInquiryId(command.serviceInquiryId) + val existingImageList = serviceInquiryImageCrudRepository + .findAllByServiceInquiryId(command.serviceInquiryId) - val changedServiceInquiryImageList = - if (command.isChangedImageList) { - val changeImageIdList = command.imageIdList!! - - val (deletedImageList, existImageList) = - serviceInquiryImageList - .partition { image -> - changeImageIdList.all { imageId -> - image.id != imageId - } - } - - val newImageList = changeImageIdList.filter { change -> existImageList.none { it.id == change } } - val newServiceInquiryImageList = save(newImageList, serviceInquiry) + return command.imageIdList?.let { imageIdList -> + val (imagesToDelete, imageToKeep) = + partitionImages(existingImageList, imageIdList) + val newImageIdList = imageIdList.filterNot { id -> imageToKeep.any { it.id == id } } + save(newImageIdList, serviceInquiry) + serviceInquiryImageCrudRepository.deleteAll(imagesToDelete) + updateImageOrder(imageIdList) + } ?: existingImageList + .map { it.toDomain() } + .sortedBy { it.order } + } - serviceInquiryImageCrudRepository.deleteAll(deletedImageList) + private suspend fun updateImageOrder(imageIdList: List): List = + serviceInquiryImageCrudRepository + .findAllById(imageIdList) + .map { it.copy(imageOrder = imageIdList.indexOf(it.id)) } + .let { serviceInquiryImageCrudRepository.saveAll(it) } + .map { it.toDomain() } + .toList() + .sortedBy { it.order } - newServiceInquiryImageList.plus(existImageList.map { it.toDomain() }) - } else { - serviceInquiryImageList.map { it.toDomain() } - } - return changedServiceInquiryImageList.sortedBy { it.order } - } + private fun partitionImages( + existingImageList: List, + imageIdList: List, + ): Pair, List> = + existingImageList.partition { image -> + image.id !in imageIdList + } } diff --git a/src/main/kotlin/com/devooks/backend/service/v1/service/ServiceInquiryService.kt b/src/main/kotlin/com/devooks/backend/service/v1/service/ServiceInquiryService.kt index 205dae8..48aa8ab 100644 --- a/src/main/kotlin/com/devooks/backend/service/v1/service/ServiceInquiryService.kt +++ b/src/main/kotlin/com/devooks/backend/service/v1/service/ServiceInquiryService.kt @@ -11,7 +11,10 @@ import com.devooks.backend.service.v1.repository.ServiceInquiryCrudRepository import com.devooks.backend.service.v1.repository.ServiceInquiryQueryRepository import com.devooks.backend.service.v1.repository.row.ServiceInquiryRow import java.util.* +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl import org.springframework.stereotype.Service @Service @@ -30,8 +33,11 @@ class ServiceInquiryService( return serviceInquiryEntity.toDomain() } - suspend fun get(command: GetServiceInquiriesCommand): List = - serviceInquiryQueryRepository.findBy(command).toList() + suspend fun get(command: GetServiceInquiriesCommand): Page { + val serviceInquiries = serviceInquiryQueryRepository.findBy(command) + val count = serviceInquiryQueryRepository.countBy(command) + return PageImpl(serviceInquiries.toList(), command.pageable, count.first()) + } suspend fun modify(command: ModifyServiceInquiryCommand): ServiceInquiry { val serviceInquiry = findBy(command.serviceInquiryId) diff --git a/src/main/kotlin/com/devooks/backend/transaciton/v1/controller/TransactionController.kt b/src/main/kotlin/com/devooks/backend/transaciton/v1/controller/TransactionController.kt index d3d2b15..7fd230d 100644 --- a/src/main/kotlin/com/devooks/backend/transaciton/v1/controller/TransactionController.kt +++ b/src/main/kotlin/com/devooks/backend/transaciton/v1/controller/TransactionController.kt @@ -14,6 +14,7 @@ import com.devooks.backend.transaciton.v1.dto.GetSellHistoriesCommand import com.devooks.backend.transaciton.v1.dto.GetSellHistoriesResponse import com.devooks.backend.transaciton.v1.dto.GetSellHistoriesResponse.Companion.toGetSellHistoriesResponse import com.devooks.backend.transaciton.v1.service.TransactionService +import jakarta.validation.Valid import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.annotation.GetMapping @@ -35,6 +36,7 @@ class TransactionController( @Transactional @PostMapping suspend fun createTransaction( + @Valid @RequestBody request: CreateTransactionRequest, @RequestHeader(AUTHORIZATION) @@ -51,10 +53,10 @@ class TransactionController( suspend fun getBuyHistories( @RequestParam(required = false, defaultValue = "") ebookTitle: String, - @RequestParam(required = false, defaultValue = "") - page: String, - @RequestParam(required = false, defaultValue = "") - count: String, + @RequestParam + page: Int, + @RequestParam + count: Int, @RequestHeader(AUTHORIZATION) authorization: String, ): GetBuyHistoriesResponse { @@ -65,10 +67,10 @@ class TransactionController( @GetMapping("/sell-histories") suspend fun getSellHistories( - @RequestParam(required = false, defaultValue = "") - page: String, - @RequestParam(required = false, defaultValue = "") - count: String, + @RequestParam + page: Int, + @RequestParam + count: Int, @RequestHeader(AUTHORIZATION) authorization: String, ): GetSellHistoriesResponse { @@ -76,4 +78,4 @@ class TransactionController( val command = GetSellHistoriesCommand(page, count, requesterId) return transactionService.get(command).toGetSellHistoriesResponse() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/transaciton/v1/domain/PaymentMethod.kt b/src/main/kotlin/com/devooks/backend/transaciton/v1/domain/PaymentMethod.kt index 582a16d..ceb6bd2 100644 --- a/src/main/kotlin/com/devooks/backend/transaciton/v1/domain/PaymentMethod.kt +++ b/src/main/kotlin/com/devooks/backend/transaciton/v1/domain/PaymentMethod.kt @@ -4,7 +4,6 @@ import com.devooks.backend.transaciton.v1.domain.PaymentMethod.BANK_DEPOSIT import com.devooks.backend.transaciton.v1.domain.PaymentMethod.CREDIT_CARD import com.devooks.backend.transaciton.v1.domain.PaymentMethod.MOBILE_PHONE import com.devooks.backend.transaciton.v1.domain.PaymentMethod.REAL_TIME_BANK_TRANSFER -import com.devooks.backend.transaciton.v1.error.TransactionError /** * 결제 방법 @@ -17,13 +16,4 @@ import com.devooks.backend.transaciton.v1.error.TransactionError */ enum class PaymentMethod { CREDIT_CARD, REAL_TIME_BANK_TRANSFER, BANK_DEPOSIT, MOBILE_PHONE; - - companion object { - fun String.toPaymentMethod(): PaymentMethod = - runCatching { - PaymentMethod.valueOf(this) - }.getOrElse { - throw TransactionError.INVALID_PAYMENT_METHOD.exception - } - } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/CreateTransactionRequest.kt b/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/CreateTransactionRequest.kt index 0a25078..fc2a3a3 100644 --- a/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/CreateTransactionRequest.kt +++ b/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/CreateTransactionRequest.kt @@ -1,20 +1,22 @@ package com.devooks.backend.transaciton.v1.dto -import com.devooks.backend.ebook.v1.error.validateEbookPrice -import com.devooks.backend.transaciton.v1.error.validatePaymentMethod -import com.devooks.backend.wishlist.v1.error.validateEbookId +import com.devooks.backend.transaciton.v1.domain.PaymentMethod +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min import java.util.* data class CreateTransactionRequest( - val ebookId: String?, - val paymentMethod: String?, - val price: Int? + val ebookId: UUID, + val paymentMethod: PaymentMethod, + @field:Min(0) + @field:Max(10_000_000) + val price: Int ) { fun toCommand(requesterId: UUID): CreateTransactionCommand = CreateTransactionCommand( - ebookId = ebookId.validateEbookId(), - paymentMethod = paymentMethod.validatePaymentMethod(), - price = price.validateEbookPrice(), + ebookId = ebookId, + paymentMethod = paymentMethod, + price = price, requesterId = requesterId ) } diff --git a/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/GetBuyHistoriesCommand.kt b/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/GetBuyHistoriesCommand.kt index 58f5890..4425c04 100644 --- a/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/GetBuyHistoriesCommand.kt +++ b/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/GetBuyHistoriesCommand.kt @@ -10,8 +10,8 @@ class GetBuyHistoriesCommand( ) { constructor( ebookTitle: String, - page: String, - count: String, + page: Int, + count: Int, requesterId: UUID, ) : this( ebookTitle = ebookTitle.takeIf { it.isNotBlank() }?.let { "%$ebookTitle%" }, diff --git a/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/GetSellHistoriesCommand.kt b/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/GetSellHistoriesCommand.kt index f35b60a..db9bc63 100644 --- a/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/GetSellHistoriesCommand.kt +++ b/src/main/kotlin/com/devooks/backend/transaciton/v1/dto/GetSellHistoriesCommand.kt @@ -8,8 +8,8 @@ class GetSellHistoriesCommand( private val paging: Paging, ) { constructor( - page: String, - count: String, + page: Int, + count: Int, requesterId: UUID, ) : this( requesterId = requesterId, @@ -21,4 +21,4 @@ class GetSellHistoriesCommand( val limit: Int get() = paging.limit -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/devooks/backend/transaciton/v1/error/TransactionError.kt b/src/main/kotlin/com/devooks/backend/transaciton/v1/error/TransactionError.kt index f56bce7..9dd8ab0 100644 --- a/src/main/kotlin/com/devooks/backend/transaciton/v1/error/TransactionError.kt +++ b/src/main/kotlin/com/devooks/backend/transaciton/v1/error/TransactionError.kt @@ -1,23 +1,14 @@ package com.devooks.backend.transaciton.v1.error import com.devooks.backend.common.exception.GeneralException -import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.CONFLICT import org.springframework.http.HttpStatus.FORBIDDEN enum class TransactionError(val exception: GeneralException) { - // 400 - REQUIRED_PAYMENT_METHOD(GeneralException("TRANSACTION-400-1", BAD_REQUEST, "결제 수단이 반드시 필요합니다.")), - INVALID_PAYMENT_METHOD(GeneralException("TRANSACTION-400-2", BAD_REQUEST, "잘못된 형식의 결제 방법 입니다.")), - // 403 FORBIDDEN_REVIEW(GeneralException("TRANSACTION-403-1", FORBIDDEN, "구매한 전자책만 리뷰가 가능합니다.")), // 409 DUPLICATE_TRANSACTION(GeneralException("TRANSACTION-409-1", CONFLICT, "이미 구매한 책 입니다.")) ; - - override fun toString(): String { - return super.toString() - } } diff --git a/src/main/kotlin/com/devooks/backend/transaciton/v1/error/TransactionValidation.kt b/src/main/kotlin/com/devooks/backend/transaciton/v1/error/TransactionValidation.kt deleted file mode 100644 index efb3d5a..0000000 --- a/src/main/kotlin/com/devooks/backend/transaciton/v1/error/TransactionValidation.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.devooks.backend.transaciton.v1.error - -import com.devooks.backend.common.error.validateNotBlank -import com.devooks.backend.transaciton.v1.domain.PaymentMethod -import com.devooks.backend.transaciton.v1.domain.PaymentMethod.Companion.toPaymentMethod - -fun String?.validatePaymentMethod(): PaymentMethod = - validateNotBlank(TransactionError.REQUIRED_PAYMENT_METHOD.exception) - .toPaymentMethod() \ No newline at end of file diff --git a/src/main/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistController.kt b/src/main/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistController.kt index acccba1..307c768 100644 --- a/src/main/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistController.kt +++ b/src/main/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistController.kt @@ -2,7 +2,11 @@ package com.devooks.backend.wishlist.v1.controller import com.devooks.backend.auth.v1.domain.Authorization import com.devooks.backend.auth.v1.service.TokenService +import com.devooks.backend.common.dto.PageResponse +import com.devooks.backend.common.dto.PageResponse.Companion.toResponse import com.devooks.backend.ebook.v1.domain.Ebook +import com.devooks.backend.ebook.v1.dto.EbookView +import com.devooks.backend.ebook.v1.dto.EbookView.Companion.toEbookView import com.devooks.backend.ebook.v1.service.EbookService import com.devooks.backend.wishlist.v1.domain.Wishlist import com.devooks.backend.wishlist.v1.dto.CreateWishlistCommand @@ -11,9 +15,8 @@ import com.devooks.backend.wishlist.v1.dto.CreateWishlistResponse import com.devooks.backend.wishlist.v1.dto.DeleteWishlistCommand import com.devooks.backend.wishlist.v1.dto.DeleteWishlistResponse import com.devooks.backend.wishlist.v1.dto.GetWishlistCommand -import com.devooks.backend.wishlist.v1.dto.GetWishlistResponse -import com.devooks.backend.wishlist.v1.dto.GetWishlistResponse.Companion.toResponse import com.devooks.backend.wishlist.v1.service.WishlistService +import jakarta.validation.Valid import java.util.* import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.transaction.annotation.Transactional @@ -33,11 +36,12 @@ class WishlistController( private val wishlistService: WishlistService, private val ebookService: EbookService, private val tokenService: TokenService, -): WishlistControllerDocs { +) : WishlistControllerDocs { @Transactional @PostMapping override suspend fun createWishlist( + @Valid @RequestBody request: CreateWishlistRequest, @RequestHeader(AUTHORIZATION) @@ -52,25 +56,26 @@ class WishlistController( @GetMapping override suspend fun getWishlist( - @RequestParam(required = false, defaultValue = "") - categoryIds: List, - @RequestParam(required = false, defaultValue = "") - page: String, - @RequestParam(required = false, defaultValue = "") - count: String, + @RequestParam(required = false) + categoryIdList: List?, + @RequestParam + page: Int, + @RequestParam + count: Int, @RequestHeader(AUTHORIZATION) authorization: String, - ): GetWishlistResponse { + ): PageResponse { val memberId = tokenService.getMemberId(Authorization(authorization)) - val command = GetWishlistCommand(memberId, categoryIds, page, count) - return wishlistService.get(command).toResponse() + val command = GetWishlistCommand(memberId, categoryIdList, page, count) + val ebooks = wishlistService.get(command) + return ebooks.map { it.toEbookView() }.toResponse() } @Transactional @DeleteMapping("/{wishlistId}") override suspend fun deleteWishlist( - @PathVariable - wishlistId: String, + @PathVariable("wishlistId") + wishlistId: UUID, @RequestHeader(AUTHORIZATION) authorization: String, ): DeleteWishlistResponse { diff --git a/src/main/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistControllerDocs.kt b/src/main/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistControllerDocs.kt index 2366b99..72a900a 100644 --- a/src/main/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistControllerDocs.kt +++ b/src/main/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistControllerDocs.kt @@ -1,16 +1,18 @@ package com.devooks.backend.wishlist.v1.controller +import com.devooks.backend.common.dto.PageResponse import com.devooks.backend.common.exception.ErrorResponse +import com.devooks.backend.ebook.v1.dto.EbookView import com.devooks.backend.wishlist.v1.dto.CreateWishlistRequest import com.devooks.backend.wishlist.v1.dto.CreateWishlistResponse import com.devooks.backend.wishlist.v1.dto.DeleteWishlistResponse -import com.devooks.backend.wishlist.v1.dto.GetWishlistResponse import io.swagger.v3.oas.annotations.Operation 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 java.util.* import org.springframework.http.MediaType.APPLICATION_JSON_VALUE @Tag(name = "찜") @@ -31,9 +33,7 @@ interface WishlistControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- EBOOK-400-23 : 전자책 식별자가 반드시 필요합니다.\n" + - "- EBOOK-400-16 : 잘못된 형식의 전자책 식별자입니다.", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -66,7 +66,7 @@ interface WishlistControllerDocs { ) suspend fun createWishlist( request: CreateWishlistRequest, - @Schema(description = "액세스 토큰", required = true, nullable = false) + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) authorization: String, ): CreateWishlistResponse @@ -76,18 +76,10 @@ interface WishlistControllerDocs { ApiResponse( responseCode = "200", description = "OK", - content = [ - Content( - mediaType = APPLICATION_JSON_VALUE, - schema = Schema(implementation = GetWishlistResponse::class) - ) - ] ), ApiResponse( responseCode = "400", - description = - "- COMMON-400-1 : 페이지는 1부터 조회할 수 있습니다.\n" + - "- COMMON-400-2 : 개수는 1~1000 까지 조회할 수 있습니다.", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -98,15 +90,15 @@ interface WishlistControllerDocs { ] ) suspend fun getWishlist( - @Schema(description = "카테고리 식별자 목록", required = false) - categoryIds: List, - @Schema(description = "페이지", required = true, nullable = false) - page: String, - @Schema(description = "개수", required = true, nullable = false) - count: String, - @Schema(description = "액세스 토큰", required = true, nullable = false) + @Schema(description = "카테고리 식별자 목록", type = "array", format = "uuid", required = false) + categoryIdList: List?, + @Schema(description = "페이지", implementation = Int::class, required = true) + page: Int, + @Schema(description = "개수", implementation = Int::class, required = true) + count: Int, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) authorization: String, - ): GetWishlistResponse + ): PageResponse @Operation(summary = "찜 취소") @ApiResponses( @@ -123,8 +115,7 @@ interface WishlistControllerDocs { ), ApiResponse( responseCode = "400", - description = - "- WISHLIST-400-2 : 잘못된 형식의 찜 식별자 입니다.", + description = "- COMMON-400-0 : 유효하지 않은 요청입니다.", content = arrayOf( Content( mediaType = APPLICATION_JSON_VALUE, @@ -157,9 +148,9 @@ interface WishlistControllerDocs { ] ) suspend fun deleteWishlist( - @Schema(description = "찜 식별자", required = true, nullable = false) - wishlistId: String, - @Schema(description = "액세스 토큰", required = true, nullable = false) + @Schema(description = "찜 식별자", required = true, implementation = UUID::class) + wishlistId: UUID, + @Schema(description = "액세스 토큰", example = "Bearer \${accessToken}", required = true) authorization: String, ): DeleteWishlistResponse } diff --git a/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/CreateWishlistRequest.kt b/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/CreateWishlistRequest.kt index 05279cd..e13f146 100644 --- a/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/CreateWishlistRequest.kt +++ b/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/CreateWishlistRequest.kt @@ -1,17 +1,16 @@ package com.devooks.backend.wishlist.v1.dto -import com.devooks.backend.wishlist.v1.error.validateEbookId import io.swagger.v3.oas.annotations.media.Schema import java.util.* data class CreateWishlistRequest( - @Schema(description = "전자책 식별자", required = true, nullable = false) - val ebookId: String?, + @Schema(description = "전자책 식별자", implementation = UUID::class, required = true) + val ebookId: UUID, ) { fun toCommand(requesterId: UUID): CreateWishlistCommand = CreateWishlistCommand( - ebookId = ebookId.validateEbookId(), + ebookId = ebookId, requesterId = requesterId ) diff --git a/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/DeleteWishlistCommand.kt b/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/DeleteWishlistCommand.kt index 94c99fe..a9d3615 100644 --- a/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/DeleteWishlistCommand.kt +++ b/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/DeleteWishlistCommand.kt @@ -1,17 +1,8 @@ package com.devooks.backend.wishlist.v1.dto -import com.devooks.backend.wishlist.v1.error.validateWishlistId import java.util.* class DeleteWishlistCommand( val memberId: UUID, val wishlistId: UUID, -) { - constructor( - memberId: UUID, - wishlistId: String, - ): this( - memberId = memberId, - wishlistId = wishlistId.validateWishlistId() - ) -} +) diff --git a/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/GetWishlistCommand.kt b/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/GetWishlistCommand.kt index 019c4cc..96b3e8d 100644 --- a/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/GetWishlistCommand.kt +++ b/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/GetWishlistCommand.kt @@ -1,29 +1,29 @@ package com.devooks.backend.wishlist.v1.dto import com.devooks.backend.common.dto.Paging -import com.devooks.backend.wishlist.v1.error.validateCategoryIds import java.util.* -import org.springframework.data.domain.Pageable class GetWishlistCommand( val memberId: UUID, - val categoryIds: List?, - private val pageable: Pageable, + val categoryIdList: List?, + private val paging: Paging, ) { constructor( memberId: UUID, - categoryIds: List, - page: String, - count: String, + categoryIdList: List?, + page: Int, + count: Int, ) : this( memberId = memberId, - categoryIds = categoryIds.takeIf { it.isNotEmpty() }?.validateCategoryIds(), - pageable = Paging(page, count).value + categoryIdList = categoryIdList, + paging = Paging(page, count) ) val offset: Int - get() = pageable.offset.toInt() + get() = paging.offset val limit: Int - get() = pageable.pageSize + get() = paging.limit + + val pageable = paging.value } diff --git a/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/GetWishlistResponse.kt b/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/GetWishlistResponse.kt deleted file mode 100644 index fb4011a..0000000 --- a/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/GetWishlistResponse.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.devooks.backend.wishlist.v1.dto - -import com.devooks.backend.wishlist.v1.domain.Wishlist - -data class GetWishlistResponse( - val wishlist: List, -) { - companion object { - fun List.toResponse() = - GetWishlistResponse(map { WishlistDto(it.id, it.memberId, it.ebookId) }) - } -} diff --git a/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/WishlistDto.kt b/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/WishlistView.kt similarity index 50% rename from src/main/kotlin/com/devooks/backend/wishlist/v1/dto/WishlistDto.kt rename to src/main/kotlin/com/devooks/backend/wishlist/v1/dto/WishlistView.kt index efbf8c7..ee83e9b 100644 --- a/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/WishlistDto.kt +++ b/src/main/kotlin/com/devooks/backend/wishlist/v1/dto/WishlistView.kt @@ -1,13 +1,23 @@ package com.devooks.backend.wishlist.v1.dto +import com.devooks.backend.wishlist.v1.domain.Wishlist import io.swagger.v3.oas.annotations.media.Schema import java.util.* -data class WishlistDto( +data class WishlistView( @Schema(description = "찜 식별자") val id: UUID, @Schema(description = "회원 식별자") val memberId: UUID, @Schema(description = "전자책 식별자") val ebookId: UUID, -) +) { + companion object { + fun Wishlist.toWishlistView() = + WishlistView( + id = this.id, + memberId = this.memberId, + ebookId = this.ebookId, + ) + } +} diff --git a/src/main/kotlin/com/devooks/backend/wishlist/v1/error/WishlistError.kt b/src/main/kotlin/com/devooks/backend/wishlist/v1/error/WishlistError.kt index 317ab13..a0cc57f 100644 --- a/src/main/kotlin/com/devooks/backend/wishlist/v1/error/WishlistError.kt +++ b/src/main/kotlin/com/devooks/backend/wishlist/v1/error/WishlistError.kt @@ -1,16 +1,11 @@ package com.devooks.backend.wishlist.v1.error import com.devooks.backend.common.exception.GeneralException -import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.CONFLICT import org.springframework.http.HttpStatus.FORBIDDEN import org.springframework.http.HttpStatus.NOT_FOUND enum class WishlistError(val exception: GeneralException) { - // 400 - INVALID_CATEGORY_ID(GeneralException("WISHLIST-400-1", BAD_REQUEST, "잘못된 형식의 카테고리 식별자 입니다.")), - INVALID_WISHLIST_ID(GeneralException("WISHLIST-400-2", BAD_REQUEST, "잘못된 형식의 찜 식별자 입니다.")), - // 403 FORBIDDEN_DELETE_WISHLIST(GeneralException("WISHLIST-403-1", FORBIDDEN, "자신의 찜만 삭제할 수 있습니다.")), diff --git a/src/main/kotlin/com/devooks/backend/wishlist/v1/error/WishlistValidation.kt b/src/main/kotlin/com/devooks/backend/wishlist/v1/error/WishlistValidation.kt deleted file mode 100644 index dbd2cbb..0000000 --- a/src/main/kotlin/com/devooks/backend/wishlist/v1/error/WishlistValidation.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.devooks.backend.wishlist.v1.error - -import com.devooks.backend.common.error.validateNotBlank -import com.devooks.backend.common.error.validateUUID -import com.devooks.backend.ebook.v1.error.EbookError -import java.util.* - -fun String?.validateEbookId(): UUID = - validateNotBlank(EbookError.REQUIRED_EBOOK_ID.exception) - .validateUUID(EbookError.INVALID_EBOOK_ID.exception) - -fun List.validateCategoryIds(): List = - map { it.validateUUID(WishlistError.INVALID_CATEGORY_ID.exception) } - -fun String.validateWishlistId(): UUID = - validateUUID(WishlistError.INVALID_WISHLIST_ID.exception) diff --git a/src/main/kotlin/com/devooks/backend/wishlist/v1/repository/WishlistQueryRepository.kt b/src/main/kotlin/com/devooks/backend/wishlist/v1/repository/WishlistQueryRepository.kt index 4f5c73d..fe6c7a6 100644 --- a/src/main/kotlin/com/devooks/backend/wishlist/v1/repository/WishlistQueryRepository.kt +++ b/src/main/kotlin/com/devooks/backend/wishlist/v1/repository/WishlistQueryRepository.kt @@ -4,31 +4,31 @@ import com.devooks.backend.common.config.database.JooqR2dbcRepository import com.devooks.backend.jooq.tables.references.EBOOK import com.devooks.backend.jooq.tables.references.RELATED_CATEGORY import com.devooks.backend.jooq.tables.references.WISHLIST -import com.devooks.backend.wishlist.v1.domain.Wishlist import com.devooks.backend.wishlist.v1.dto.GetWishlistCommand import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import org.jooq.impl.DSL import org.springframework.stereotype.Repository @Repository class WishlistQueryRepository : JooqR2dbcRepository() { - suspend fun findBy(command: GetWishlistCommand): Flow = + suspend fun countBy(command: GetWishlistCommand): Flow = query { select( - WISHLIST.WISHLIST_ID.`as`("id"), - WISHLIST.MEMBER_ID, - WISHLIST.EBOOK_ID, - WISHLIST.CREATED_DATE, + DSL.count() ).from( WISHLIST - .join(EBOOK) - .on(EBOOK.EBOOK_ID.eq(WISHLIST.EBOOK_ID)) - .join(RELATED_CATEGORY) - .on(RELATED_CATEGORY.RELATED_CATEGORY_ID.eq(RELATED_CATEGORY.RELATED_CATEGORY_ID)) - ).where(WISHLIST.MEMBER_ID.eq(command.memberId)) - .offset(command.offset) - .limit(command.limit) - }.map { it.into(Wishlist::class.java) } + .join(EBOOK).on(WISHLIST.EBOOK_ID.eq(EBOOK.EBOOK_ID)) + .join(RELATED_CATEGORY).on(EBOOK.EBOOK_ID.eq(RELATED_CATEGORY.EBOOK_ID)) + ).where( + WISHLIST.MEMBER_ID.eq(command.memberId).and(EBOOK.DELETED_DATE.isNull) + .let { where -> + command.categoryIdList?.let { categoryIdList -> + where.and(RELATED_CATEGORY.CATEGORY_ID.`in`(categoryIdList)) + } ?: where + } + ) + }.map { it.into(Long::class.java) } } diff --git a/src/main/kotlin/com/devooks/backend/wishlist/v1/service/WishlistService.kt b/src/main/kotlin/com/devooks/backend/wishlist/v1/service/WishlistService.kt index 67b726d..c50e2b6 100644 --- a/src/main/kotlin/com/devooks/backend/wishlist/v1/service/WishlistService.kt +++ b/src/main/kotlin/com/devooks/backend/wishlist/v1/service/WishlistService.kt @@ -1,6 +1,8 @@ package com.devooks.backend.wishlist.v1.service import com.devooks.backend.ebook.v1.domain.Ebook +import com.devooks.backend.ebook.v1.repository.EbookQueryRepository +import com.devooks.backend.ebook.v1.repository.row.EbookRow import com.devooks.backend.wishlist.v1.domain.Wishlist import com.devooks.backend.wishlist.v1.dto.CreateWishlistCommand import com.devooks.backend.wishlist.v1.dto.DeleteWishlistCommand @@ -9,13 +11,17 @@ import com.devooks.backend.wishlist.v1.entity.WishlistEntity import com.devooks.backend.wishlist.v1.error.WishlistError import com.devooks.backend.wishlist.v1.repository.WishlistCrudRepository import com.devooks.backend.wishlist.v1.repository.WishlistQueryRepository +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.toList +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl import org.springframework.stereotype.Service @Service class WishlistService( private val wishlistCrudRepository: WishlistCrudRepository, private val wishlistQueryRepository: WishlistQueryRepository, + private val ebookQueryRepository: EbookQueryRepository, ) { suspend fun create(command: CreateWishlistCommand, ebook: Ebook): Wishlist { wishlistCrudRepository @@ -25,13 +31,20 @@ class WishlistService( return wishlistCrudRepository.save(wishlistEntity).toDomain() } - suspend fun get(command: GetWishlistCommand): List = - wishlistQueryRepository.findBy(command).toList() + suspend fun get(command: GetWishlistCommand): Page { + val wishlists = ebookQueryRepository.findWishlistBy(command) + val count = wishlistQueryRepository.countBy(command) + return PageImpl(wishlists.toList(), command.pageable, count.first()) + } suspend fun delete(command: DeleteWishlistCommand) { wishlistCrudRepository .findById(command.wishlistId) - ?.also { if (it.memberId != command.memberId) { throw WishlistError.FORBIDDEN_DELETE_WISHLIST.exception } } + ?.also { + if (it.memberId != command.memberId) { + throw WishlistError.FORBIDDEN_DELETE_WISHLIST.exception + } + } ?.also { wishlistCrudRepository.delete(it) } ?: throw WishlistError.NOT_FOUND_WISHLIST.exception } diff --git a/src/main/resources/schema.sql b/src/main/resources/db/jooq/schema.sql similarity index 100% rename from src/main/resources/schema.sql rename to src/main/resources/db/jooq/schema.sql diff --git a/src/main/resources/db/migration/V1__init_schema.sql b/src/main/resources/db/migration/V1__init_schema.sql new file mode 100644 index 0000000..da86d47 --- /dev/null +++ b/src/main/resources/db/migration/V1__init_schema.sql @@ -0,0 +1,218 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE IF NOT EXISTS member +( + member_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + nickname VARCHAR UNIQUE NOT NULL, + profile_image_path VARCHAR UNIQUE, + authority VARCHAR NOT NULL, + withdrawal_date TIMESTAMP, + until_suspension_date TIMESTAMP, + registered_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS member_info +( + member_info_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + blog_link VARCHAR NOT NULL, + instagram_link VARCHAR NOT NULL, + youtube_link VARCHAR NOT NULL, + real_name VARCHAR NOT NULL, + bank VARCHAR NOT NULL, + account_number VARCHAR NOT NULL, + introduction TEXT NOT NULL, + phone_number VARCHAR NOT NULL, + member_id uuid NOT NULL UNIQUE, + email VARCHAR NOT NULL, + CONSTRAINT member_info_fk_member_id FOREIGN KEY (member_id) REFERENCES member_info (member_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS oauth_info +( + oauth_id VARCHAR PRIMARY KEY, + oauth_type VARCHAR NOT NULL, + member_id uuid UNIQUE NOT NULL, + registered_date TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT oauth_info_fk_member_id FOREIGN KEY (member_id) REFERENCES member (member_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS refresh_token +( + refresh_token_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + member_id uuid UNIQUE NOT NULL, + token VARCHAR UNIQUE NOT NULL, + registered_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL, + CONSTRAINT refresh_token_fk_member_id FOREIGN KEY (member_id) REFERENCES member (member_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS category +( + category_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR UNIQUE NOT NULL, + registered_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL, + deleted_date TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS favorite_category +( + favorite_category_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + favorite_member_id uuid NOT NULL, + category_id uuid NOT NULL, + CONSTRAINT favorite_category_fk_member_id FOREIGN KEY (favorite_member_id) + REFERENCES member (member_id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT favorite_category_fk_category_id FOREIGN KEY (category_id) + REFERENCES category (category_id) +); + +CREATE TABLE IF NOT EXISTS pdf +( + pdf_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + file_path VARCHAR UNIQUE NOT NULL, + page_count INT NOT NULL, + created_date TIMESTAMP NOT NULL, + upload_member_id uuid NOT NULL, + CONSTRAINT pdf_fk_member_id FOREIGN KEY (upload_member_id) REFERENCES member (member_id) +); + +CREATE TABLE IF NOT EXISTS preview_image +( + preview_image_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + image_path VARCHAR UNIQUE NOT NULL, + preview_order INT NOT NULL, + pdf_id uuid NOT NULL, + CONSTRAINT preview_image_fk_pdf_id FOREIGN KEY (pdf_id) REFERENCES pdf (pdf_id) +); + +CREATE TABLE IF NOT EXISTS ebook +( + ebook_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + selling_member_id uuid NOT NULL, + pdf_id uuid NOT NULL UNIQUE, + main_image_id uuid NOT NULL UNIQUE, + title VARCHAR NOT NULL, + price INT NOT NULL, + table_of_contents TEXT NOT NULL, + introduction TEXT NOT NULL, + created_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL, + deleted_date TIMESTAMP, + CONSTRAINT ebook_fk_member_id FOREIGN KEY (selling_member_id) REFERENCES member (member_id), + CONSTRAINT ebook_fk_pdf_id FOREIGN KEY (pdf_id) REFERENCES pdf (pdf_id) +); + +CREATE TABLE IF NOT EXISTS ebook_image +( + ebook_image_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + image_path VARCHAR UNIQUE NOT NULL, + image_order INT NOT NULL, + upload_member_id uuid NOT NULL, + image_type VARCHAR NOT NULL, + ebook_id uuid, + CONSTRAINT ebook_image_fk_ebook_id FOREIGN KEY (ebook_id) REFERENCES ebook (ebook_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS related_category +( + related_category_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + ebook_id uuid NOT NULL, + category_id uuid NOT NULL, + CONSTRAINT related_category_fk_ebook_id FOREIGN KEY (ebook_id) REFERENCES ebook (ebook_id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT related_category_fk_category_id FOREIGN KEY (category_id) REFERENCES category (category_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS wishlist +( + wishlist_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + member_id uuid NOT NULL, + ebook_id uuid NOT NULL, + created_date TIMESTAMP NOT NULL, + CONSTRAINT wishlist_fk_member_id FOREIGN KEY (member_id) REFERENCES member (member_id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT wishlist_fk_ebook_id FOREIGN KEY (ebook_id) REFERENCES ebook (ebook_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS transaction +( + transaction_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + ebook_id uuid NOT NULL, + price INT NOT NULL, + payment_method VARCHAR NOT NULL, + buyer_member_id uuid NOT NULL, + transaction_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS review +( + review_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + rating INT NOT NULL, + content VARCHAR NOT NULL, + ebook_id uuid NOT NULL, + writer_member_id uuid NOT NULL, + written_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS review_comment +( + review_comment_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + content VARCHAR NOT NULL, + review_id uuid NOT NULL, + writer_member_id uuid NOT NULL, + written_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS ebook_inquiry +( + ebook_inquiry_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + content VARCHAR NOT NULL, + ebook_id uuid NOT NULL, + writer_member_id uuid NOT NULL, + written_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS ebook_inquiry_comment +( + ebook_inquiry_comment_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + content VARCHAR NOT NULL, + inquiry_id uuid NOT NULL, + writer_member_id uuid NOT NULL, + written_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS service_inquiry +( + service_inquiry_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + title VARCHAR NOT NULL, + content TEXT NOT NULL, + created_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL, + inquiry_processing_status VARCHAR NOT NULL, + writer_member_id uuid NOT NULL, + CONSTRAINT service_inquiry_fk_member_id FOREIGN KEY (writer_member_id) REFERENCES member (member_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS service_inquiry_image +( + service_inquiry_image_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + image_path VARCHAR UNIQUE NOT NULL, + image_order INT NOT NULL, + upload_member_id uuid NOT NULL, + service_inquiry_id uuid, + CONSTRAINT service_inquiry_image_fk_service_inquiry_id FOREIGN KEY (service_inquiry_id) REFERENCES service_inquiry (service_inquiry_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS notification +( + notification_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + type VARCHAR NOT NULL, + content VARCHAR NOT NULL, + note jsonb NOT NULL, + receiver_id uuid NOT NULL, + notified_date TIMESTAMP NOT NULL, + checked BOOLEAN NOT NULL +); diff --git a/src/main/resources/data.sql b/src/main/resources/db/migration/V2__init_data.sql similarity index 100% rename from src/main/resources/data.sql rename to src/main/resources/db/migration/V2__init_data.sql diff --git a/src/main/resources/sample.application.yml b/src/main/resources/sample.application.yml index 61c1f4e..09615cf 100644 --- a/src/main/resources/sample.application.yml +++ b/src/main/resources/sample.application.yml @@ -1,19 +1,41 @@ server: - port: 8081 + port: 80 spring: r2dbc: - url: ${DATABASE_URL:r2dbc:postgresql://localhost:35432/devooksdb} + url: r2dbc:postgresql://localhost:5432/devooksdb driver: postgresql protocol: r2dbc host: localhost - port: 35432 + port: 5432 database: devooksdb - username: ${DATABASE_USERNAME:devooks} - password: ${DATABASE_PASSWORD:devooks} + username: devooks + password: devooks + mail: + host: smtp.gmail.com + port: 587 + username: email@gmail.com + password: password + protocol: smtp + timeout: 10000 + auth: true + tls: true + debug: true + + webflux: + static-path-pattern: /static/** + web: + resources: + static-locations: "file:static/" codec: - max-in-memory-size: 500MB + max-in-memory-size: 1GB + + flyway: + url: jdbc:postgresql://localhost:5432/devooksdb + locations: classpath:/db/migration + user: devooks + password: devooks jwt: secretKey: @@ -27,7 +49,7 @@ naver: profileUrl: /v1/nid/me clientId: clientSecret: - state: + state: test kakao: oauthHost: https://kauth.kakao.com diff --git a/src/test/kotlin/com/devooks/backend/auth/v1/controller/AuthControllerTest.kt b/src/test/kotlin/com/devooks/backend/auth/v1/controller/AuthControllerTest.kt index f591e4f..bb003c0 100644 --- a/src/test/kotlin/com/devooks/backend/auth/v1/controller/AuthControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/auth/v1/controller/AuthControllerTest.kt @@ -18,7 +18,7 @@ import com.devooks.backend.auth.v1.service.TokenService import com.devooks.backend.category.v1.repository.CategoryRepository import com.devooks.backend.config.IntegrationTest import com.devooks.backend.fixture.ErrorResponse -import com.devooks.backend.fixture.ErrorResponse.Companion.postForBadRequest +import com.devooks.backend.fixture.ErrorResponse.Companion.isBadRequest import com.devooks.backend.member.v1.domain.Member.Companion.toDomain import com.devooks.backend.member.v1.dto.SignUpRequest import com.devooks.backend.member.v1.dto.SignUpResponse @@ -66,9 +66,9 @@ internal class AuthControllerTest @Autowired constructor( val categoryEntity = categoryRepository.findAll().toList()[0] signUpRequest = SignUpRequest( oauthId = "oauthId", - oauthType = OauthType.NAVER.name, + oauthType = OauthType.NAVER, nickname = "nickname", - favoriteCategoryIdList = listOf(categoryEntity.id!!.toString()) + favoriteCategoryIdList = listOf(categoryEntity.id!!) ) val responseMember = webTestClient .post() @@ -102,9 +102,26 @@ internal class AuthControllerTest @Autowired constructor( assertThat(response.member.id).isEqualTo(member.id) } + @Test + fun `로그인시 인증 코드가 존재하지 않을 경우 예외가 발생한다`(): Unit = runBlocking { + val request = mapOf( + "oauthType" to "NAVER" + ) + webTestClient.post().isBadRequest("/api/v1/auth/login", request) + } + + @Test + fun `로그인시 인증 코드가 비어 있을 경우 예외가 발생한다`(): Unit = runBlocking { + val request = mapOf( + "authorizationCode" to "", + "oauthType" to "NAVER" + ) + webTestClient.post().isBadRequest("/api/v1/auth/login", request) + } + @Test fun `잘못된 인증 코드로 로그인을 실패할 경우 예외가 발생한다`(): Unit = runBlocking { - val request = LoginRequest("code", OauthType.NAVER.name) + val request = LoginRequest("code", OauthType.NAVER) val getNaverTokenResponse = GetNaverTokenResponse(null, null, null, null, "error", "erorDescription") given( @@ -112,7 +129,7 @@ internal class AuthControllerTest @Autowired constructor( OauthGrantType.AUTHORIZATION_CODE.value, naverOauthProperties.clientId, naverOauthProperties.clientSecret, - request.authorizationCode!!, + request.authorizationCode, state = naverOauthProperties.state ) ).willReturn(getNaverTokenResponse) @@ -133,7 +150,7 @@ internal class AuthControllerTest @Autowired constructor( @Test fun `회원이 존재하지 않을 경우 예외가 발생한다`(): Unit = runBlocking { - val request = LoginRequest("code", OauthType.NAVER.name) + val request = LoginRequest("code", OauthType.NAVER) val getNaverTokenResponse = GetNaverTokenResponse("accessToken", "refreshToken", "tokenType", "expiresIn", null, null) given( @@ -141,7 +158,7 @@ internal class AuthControllerTest @Autowired constructor( OauthGrantType.AUTHORIZATION_CODE.value, naverOauthProperties.clientId, naverOauthProperties.clientSecret, - request.authorizationCode!!, + request.authorizationCode, state = naverOauthProperties.state ) ).willReturn(getNaverTokenResponse) @@ -173,7 +190,7 @@ internal class AuthControllerTest @Autowired constructor( @Test fun `정지 당한 회원일 경우 예외가 발생한다`(): Unit = runBlocking { - val request = LoginRequest("code", OauthType.NAVER.name) + val request = LoginRequest("code", OauthType.NAVER) val getNaverTokenResponse = GetNaverTokenResponse("accessToken", "refreshToken", "tokenType", "expiresIn", null, null) memberRepository.save(member.copy(untilSuspensionDate = Instant.now().plusSeconds(60L))) @@ -182,7 +199,7 @@ internal class AuthControllerTest @Autowired constructor( OauthGrantType.AUTHORIZATION_CODE.value, naverOauthProperties.clientId, naverOauthProperties.clientSecret, - request.authorizationCode!!, + request.authorizationCode, state = naverOauthProperties.state ) ).willReturn(getNaverTokenResponse) @@ -213,7 +230,7 @@ internal class AuthControllerTest @Autowired constructor( @Test fun `탈퇴한 회원일 경우 예외가 발생한다`(): Unit = runBlocking { - val request = LoginRequest("code", OauthType.NAVER.name) + val request = LoginRequest("code", OauthType.NAVER) val getNaverTokenResponse = GetNaverTokenResponse("accessToken", "refreshToken", "tokenType", "expiresIn", null, null) given( @@ -221,7 +238,7 @@ internal class AuthControllerTest @Autowired constructor( OauthGrantType.AUTHORIZATION_CODE.value, naverOauthProperties.clientId, naverOauthProperties.clientSecret, - request.authorizationCode!!, + request.authorizationCode, state = naverOauthProperties.state ) ).willReturn(getNaverTokenResponse) @@ -297,85 +314,70 @@ internal class AuthControllerTest @Autowired constructor( @Test fun `authorizationCode가 존재하지 않을 경우 로그인시 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "oauthType" : "NAVER" - } - """.trimIndent() - - val response = webTestClient.postForBadRequest("/api/v1/auth/login", request) + val request = mapOf("oauthType" to "NAVER") - response.isEqualTo(AuthError.REQUIRED_AUTHORIZATION_CODE.exception) + webTestClient.post().isBadRequest("/api/v1/auth/login", request) } @Test fun `authorizationCode가 비어있을 경우 로그인시 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "authorizationCode" : "", - "oauthType" : "NAVER" - } - """.trimIndent() - - val response = webTestClient.postForBadRequest("/api/v1/auth/login", request) + val request = mapOf( + "authorizationCode" to "", + "oauthType" to "NAVER" + ) - response.isEqualTo(AuthError.REQUIRED_AUTHORIZATION_CODE.exception) + webTestClient.post().isBadRequest("/api/v1/auth/login", request) } @Test fun `oauthType이 존재하지 않을 경우 로그인시 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "authorizationCode": "code" - } - """.trimIndent() - - val response = webTestClient.postForBadRequest("/api/v1/auth/login", request) - - response.isEqualTo(AuthError.INVALID_OAUTH_TYPE.exception) + val request = mapOf( + "authorizationCode" to "code", + ) + webTestClient.post().isBadRequest("/api/v1/auth/login", request) } @Test fun `oauthType이 값이 NAVER, GOOGLE, KAKAO가 아닐 경우 로그인시 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "authorizationCode": "code", - "oauthType": "NAVERR" - } - """.trimIndent() - - val response = webTestClient.postForBadRequest("/api/v1/auth/login", request) - - response.isEqualTo(AuthError.INVALID_OAUTH_TYPE.exception) + val request = mapOf( + "authorizationCode" to "code", + "oauthType" to "NAVERR" + ) + webTestClient.post().isBadRequest("/api/v1/auth/login", request) } @Test fun `refreshToken이 존재하지 않을 경우 로그아웃시 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - } - """.trimIndent() - - val response = webTestClient.postForBadRequest("/api/v1/auth/logout", request) - - response.isEqualTo(AuthError.REQUIRED_TOKEN.exception) + val request = mapOf() + webTestClient.post().isBadRequest("/api/v1/auth/logout", request) } @Test fun `refreshToken이 비어있을 경우 로그아웃시 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "refreshToken" : "" - } - """.trimIndent() + val request = mapOf( + "refreshToken" to "", + ) + webTestClient.post().isBadRequest("/api/v1/auth/logout", request) + } - val response = webTestClient.postForBadRequest("/api/v1/auth/logout", request) + @Test + fun `토큰 재발급시 refreshToken이 비어있을 경우 예외가 발생한다`(): Unit = runBlocking { + val request = mapOf( + "refreshToken" to "", + ) + webTestClient.post().isBadRequest("/api/v1/auth/reissue", request) + } - response.isEqualTo(AuthError.REQUIRED_TOKEN.exception) + @Test + fun `이메일 확인시 email 형식이 잘못되어 있을 경우 예외가 발생한다`(): Unit = runBlocking { + val request = mapOf( + "email" to "asd@asd", + ) + webTestClient.post().isBadRequest("/api/v1/auth/check/email", request) } private fun postLogin(): LoginResponse { - val request = LoginRequest("code", OauthType.NAVER.name) + val request = LoginRequest("code", OauthType.NAVER) val getNaverTokenResponse = GetNaverTokenResponse("accessToken", "refreshToken", "tokenType", "expiresIn", null, null) given( @@ -383,7 +385,7 @@ internal class AuthControllerTest @Autowired constructor( OauthGrantType.AUTHORIZATION_CODE.value, naverOauthProperties.clientId, naverOauthProperties.clientSecret, - request.authorizationCode!!, + request.authorizationCode, state = naverOauthProperties.state ) ).willReturn(getNaverTokenResponse) diff --git a/src/test/kotlin/com/devooks/backend/common/dto/PagingTest.kt b/src/test/kotlin/com/devooks/backend/common/dto/PagingTest.kt index 69bdaea..64f6061 100644 --- a/src/test/kotlin/com/devooks/backend/common/dto/PagingTest.kt +++ b/src/test/kotlin/com/devooks/backend/common/dto/PagingTest.kt @@ -1,7 +1,5 @@ package com.devooks.backend.common.dto -import com.devooks.backend.common.assertThrows -import com.devooks.backend.common.error.CommonError import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -10,20 +8,10 @@ internal class PagingTest { @Test fun `Paging으로부터 offset과 limit을 가져올 수 있다`() { val list = listOf(1, 2, 3, 4) - val paging1 = Paging(page = "1", count = "2") - val paging2 = Paging(page = "2", count = "2") + val paging1 = Paging(page = 1, count = 2) + val paging2 = Paging(page = 2, count = 2) assertThat(list.subList(paging1.offset, paging1.limit)).containsAll(listOf(1, 2)) assertThat(list.subList(paging2.offset, paging2.limit)).containsAll(listOf(3, 4)) } - - @Test - fun `페이지가 0 이하일 경우 예외가 발생한다`() { - assertThrows(CommonError.INVALID_PAGE.exception) { Paging(page = "0", count = "2") } - } - - @Test - fun `개수가 0 이하이며 1000초과일 경우 예외가 발생한다`() { - assertThrows(CommonError.INVALID_COUNT.exception) { Paging(page = "1", count = "1001") } - } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/devooks/backend/config/PostgresqlInitializer.kt b/src/test/kotlin/com/devooks/backend/config/PostgresqlInitializer.kt index c1162a8..0be57e3 100644 --- a/src/test/kotlin/com/devooks/backend/config/PostgresqlInitializer.kt +++ b/src/test/kotlin/com/devooks/backend/config/PostgresqlInitializer.kt @@ -11,7 +11,7 @@ class PostgresqlInitializer : ApplicationContextInitializer() + .expectBody>() .returnResult() .responseBody!! - .ebookList + val pageable = ebookPage.pageable + val ebookViewList = ebookPage.data val ebookView = ebookViewList[0] + assertThat(pageable.totalPages).isEqualTo(1) + assertThat(pageable.totalElements).isEqualTo(1) assertThat(ebookViewList.size).isEqualTo(1) assertThat(ebookView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookView.title).isEqualTo(response.ebook.title) assertThat(ebookView.price).isEqualTo(response.ebook.price) assertThat(ebookView.seller.id).isEqualTo(expectedMember1.id) @@ -190,10 +196,10 @@ internal class EbookControllerTest @Autowired constructor( .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .ebookList + .data assertThat(ebookList.isEmpty()).isTrue() } @@ -233,10 +239,10 @@ internal class EbookControllerTest @Autowired constructor( .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .ebookList + .data assertThat(ebookList.isEmpty()).isTrue() } @@ -265,15 +271,15 @@ internal class EbookControllerTest @Autowired constructor( .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .ebookList + .data val ebookView = ebookViewList[0] assertThat(ebookViewList.size).isEqualTo(1) assertThat(ebookView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookView.title).isEqualTo(response.ebook.title) assertThat(ebookView.price).isEqualTo(response.ebook.price) assertThat(ebookView.seller.id).isEqualTo(expectedMember1.id) @@ -291,19 +297,19 @@ internal class EbookControllerTest @Autowired constructor( val ebookViewList = webTestClient .get() - .uri("/api/v1/ebooks?page=1&count=10&sellingMemberId=${response.ebook.sellingMemberId}") + .uri("/api/v1/ebooks?page=1&count=10&sellerMemberId=${response.ebook.sellingMemberId}") .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .ebookList + .data val ebookView = ebookViewList[0] assertThat(ebookViewList.size).isEqualTo(1) assertThat(ebookView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookView.title).isEqualTo(response.ebook.title) assertThat(ebookView.price).isEqualTo(response.ebook.price) assertThat(ebookView.seller.id).isEqualTo(expectedMember1.id) @@ -325,15 +331,15 @@ internal class EbookControllerTest @Autowired constructor( .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .ebookList + .data val ebookView = ebookViewList[0] assertThat(ebookViewList.size).isEqualTo(1) assertThat(ebookView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookView.title).isEqualTo(response.ebook.title) assertThat(ebookView.price).isEqualTo(response.ebook.price) assertThat(ebookView.seller.id).isEqualTo(expectedMember1.id) @@ -357,15 +363,15 @@ internal class EbookControllerTest @Autowired constructor( .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .ebookList + .data val ebookView = ebookViewList[0] assertThat(ebookViewList.size).isEqualTo(1) assertThat(ebookView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookView.title).isEqualTo(response.ebook.title) assertThat(ebookView.price).isEqualTo(response.ebook.price) assertThat(ebookView.seller.id).isEqualTo(expectedMember1.id) @@ -387,15 +393,15 @@ internal class EbookControllerTest @Autowired constructor( .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .ebookList + .data val ebookView = ebookViewList[0] assertThat(ebookViewList.size).isEqualTo(1) assertThat(ebookView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookView.title).isEqualTo(response.ebook.title) assertThat(ebookView.price).isEqualTo(response.ebook.price) assertThat(ebookView.seller.id).isEqualTo(expectedMember1.id) @@ -426,15 +432,15 @@ internal class EbookControllerTest @Autowired constructor( .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .ebookList + .data val ebookView = ebookViewList[0] assertThat(ebookViewList.size).isEqualTo(1) assertThat(ebookView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookView.title).isEqualTo(response.ebook.title) assertThat(ebookView.price).isEqualTo(response.ebook.price) assertThat(ebookView.seller.id).isEqualTo(expectedMember1.id) @@ -452,8 +458,8 @@ internal class EbookControllerTest @Autowired constructor( val createReviewRequest = CreateReviewRequest( - ebookId = response.ebook.id.toString(), - rating = "5", + ebookId = response.ebook.id, + rating = 5, content = "content" ) val createReviewResponse = webTestClient @@ -475,13 +481,13 @@ internal class EbookControllerTest @Autowired constructor( .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .ebookList[0] + .data[0] assertThat(ebookView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookView.title).isEqualTo(response.ebook.title) assertThat(ebookView.wishlistId).isNull() assertThat(ebookView.review.rating).isEqualTo(createReviewResponse.review.rating.toDouble()) @@ -500,7 +506,7 @@ internal class EbookControllerTest @Autowired constructor( .header(AUTHORIZATION, "Bearer $accessToken") .contentType(APPLICATION_JSON) .accept(APPLICATION_JSON) - .bodyValue(CreateWishlistRequest(response.ebook.id.toString())) + .bodyValue(CreateWishlistRequest(response.ebook.id)) .exchange() .expectStatus().isOk .expectBody() @@ -515,13 +521,13 @@ internal class EbookControllerTest @Autowired constructor( .header(AUTHORIZATION, "Bearer $accessToken") .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .ebookList[0] + .data[0] assertThat(ebookView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookView.title).isEqualTo(response.ebook.title) assertThat(ebookView.wishlistId).isEqualTo(wishlistId) assertThat(ebookView.review.rating).isZero() @@ -540,7 +546,7 @@ internal class EbookControllerTest @Autowired constructor( .header(AUTHORIZATION, "Bearer $accessToken") .contentType(APPLICATION_JSON) .accept(APPLICATION_JSON) - .bodyValue(CreateWishlistRequest(response.ebook.id.toString())) + .bodyValue(CreateWishlistRequest(response.ebook.id)) .exchange() .expectStatus().isOk .expectBody() @@ -561,7 +567,7 @@ internal class EbookControllerTest @Autowired constructor( .ebook assertThat(ebookDetailView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookDetailView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookDetailView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookDetailView.title).isEqualTo(response.ebook.title) assertThat(ebookDetailView.wishlistId).isEqualTo(wishlistId) assertThat(ebookDetailView.review.rating).isZero() @@ -603,7 +609,7 @@ internal class EbookControllerTest @Autowired constructor( } @Test - fun `설명 이미지가 존재하지 않는 전자책을 상세 조회할 수 있다`(): Unit = runBlocking { + fun `설명 사진이 존재하지 않는 전자책을 상세 조회할 수 있다`(): Unit = runBlocking { val (_, response) = postCreateEbookWithNoneDescriptionImageList() val accessToken = tokenService.createTokenGroup(expectedMember1).accessToken @@ -613,7 +619,7 @@ internal class EbookControllerTest @Autowired constructor( .header(AUTHORIZATION, "Bearer $accessToken") .contentType(APPLICATION_JSON) .accept(APPLICATION_JSON) - .bodyValue(CreateWishlistRequest(response.ebook.id.toString())) + .bodyValue(CreateWishlistRequest(response.ebook.id)) .exchange() .expectStatus().isOk .expectBody() @@ -634,7 +640,7 @@ internal class EbookControllerTest @Autowired constructor( .ebook assertThat(ebookDetailView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookDetailView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookDetailView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookDetailView.title).isEqualTo(response.ebook.title) assertThat(ebookDetailView.wishlistId).isEqualTo(wishlistId) assertThat(ebookDetailView.review.rating).isZero() @@ -659,8 +665,8 @@ internal class EbookControllerTest @Autowired constructor( val createReviewRequest = CreateReviewRequest( - ebookId = response.ebook.id.toString(), - rating = "5", + ebookId = response.ebook.id, + rating = 5, content = "content" ) val createReviewResponse = webTestClient @@ -689,7 +695,7 @@ internal class EbookControllerTest @Autowired constructor( .ebook assertThat(ebookDetailView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookDetailView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookDetailView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookDetailView.title).isEqualTo(response.ebook.title) assertThat(ebookDetailView.wishlistId).isNull() assertThat(ebookDetailView.review.rating).isEqualTo(createReviewResponse.review.rating.toDouble()) @@ -724,7 +730,7 @@ internal class EbookControllerTest @Autowired constructor( .ebook assertThat(ebookDetailView.id).isEqualTo(response.ebook.id) - assertThat(File(ebookDetailView.mainImage.imagePath).exists()).isTrue() + assertThat(File(ebookDetailView.mainImage.imagePath.substring(1)).exists()).isTrue() assertThat(ebookDetailView.title).isEqualTo(response.ebook.title) assertThat(ebookDetailView.wishlistId).isNull() assertThat(ebookDetailView.review.rating).isZero() @@ -753,21 +759,19 @@ internal class EbookControllerTest @Autowired constructor( val newMainImage = postSaveMainImage(imageBase64Raw, imagePath, accessToken) val newDescriptionImages = postSaveDescriptionImages(imageBase64Raw, imagePath, accessToken) - val categoryId = categoryRepository.findAll().toList()[1].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[1].id!! val modifyEbookRequest = ModifyEbookRequest( - ebook = ModifyEbookRequest.Ebook( - title = "title2", - relatedCategoryIdList = listOf(categoryId), - mainImageId = newMainImage.id.toString(), - descriptionImageIdList = - newDescriptionImages - .map { it.id.toString() } - .plus(response.ebook.descriptionImageList.map { it.id.toString() }.first()), - price = 20000, - tableOfContents = "tableOfContents2", - introduction = "introduction2" - ), + title = "title2", + relatedCategoryIdList = listOf(categoryId), + mainImageId = newMainImage.id, + descriptionImageIdList = + newDescriptionImages + .map { it.id } + .plus(response.ebook.descriptionImageList.map { it.id }.first()), + price = 20000, + tableOfContents = "tableOfContents2", + introduction = "introduction2" ) val updatedEbook = webTestClient @@ -787,14 +791,13 @@ internal class EbookControllerTest @Autowired constructor( assertThat(updatedEbook.id).isEqualTo(response.ebook.id) assertThat(updatedEbook.mainImage.id).isEqualTo(newMainImage.id) assertThat(updatedEbook.mainImage.imagePath).isEqualTo(newMainImage.imagePath) - assertThat(updatedEbook.title).isEqualTo(modifyEbookRequest.ebook!!.title) - assertThat(updatedEbook.relatedCategoryIdList.map { it.toString() }) - .containsAll(modifyEbookRequest.ebook!!.relatedCategoryIdList!!) - assertThat(updatedEbook.price).isEqualTo(modifyEbookRequest.ebook!!.price) - val expected = modifyEbookRequest.ebook!!.descriptionImageIdList!!.map { UUID.fromString(it) } - assertThat(updatedEbook.descriptionImageList.map { it.id }).containsAll(expected) - assertThat(updatedEbook.introduction).isEqualTo(modifyEbookRequest.ebook!!.introduction) - assertThat(updatedEbook.tableOfContents).isEqualTo(modifyEbookRequest.ebook!!.tableOfContents) + assertThat(updatedEbook.title).isEqualTo(modifyEbookRequest.title) + assertThat(updatedEbook.relatedCategoryIdList).containsAll(modifyEbookRequest.relatedCategoryIdList) + assertThat(updatedEbook.price).isEqualTo(modifyEbookRequest.price) + val expected = modifyEbookRequest.descriptionImageIdList + assertThat(updatedEbook.descriptionImageList.map { it.id }).isEqualTo(expected) + assertThat(updatedEbook.introduction).isEqualTo(modifyEbookRequest.introduction) + assertThat(updatedEbook.tableOfContents).isEqualTo(modifyEbookRequest.tableOfContents) } @Test @@ -808,7 +811,7 @@ internal class EbookControllerTest @Autowired constructor( val newMainImage = postSaveMainImage(imageBase64Raw, imagePath, accessToken) val newDescriptionImages = postSaveDescriptionImages(imageBase64Raw, imagePath, accessToken) - val categoryId = categoryRepository.findAll().toList()[1].id.toString() + val categoryId = categoryRepository.findAll().toList()[1].id!! webTestClient .delete() @@ -819,18 +822,16 @@ internal class EbookControllerTest @Autowired constructor( .expectStatus().isOk val modifyEbookRequest = ModifyEbookRequest( - ebook = ModifyEbookRequest.Ebook( - title = "title2", - relatedCategoryIdList = listOf(categoryId), - mainImageId = newMainImage.id.toString(), - descriptionImageIdList = - newDescriptionImages - .map { it.id.toString() } - .plus(response.ebook.descriptionImageList.map { it.id.toString() }.first()), - price = 20000, - tableOfContents = "tableOfContents2", - introduction = "introduction2" - ) + title = "title2", + relatedCategoryIdList = listOf(categoryId), + mainImageId = newMainImage.id, + descriptionImageIdList = + newDescriptionImages + .map { it.id } + .plus(response.ebook.descriptionImageList.map { it.id }.first()), + price = 20000, + tableOfContents = "tableOfContents2", + introduction = "introduction2" ) webTestClient @@ -848,13 +849,16 @@ internal class EbookControllerTest @Autowired constructor( @Test fun `전자책의 제목만 수정할 수 있다`(): Unit = runBlocking { val (_, response) = postCreateEbook() - val originEbookEntity = ebookRepository.findById(response.ebook.id)!! val accessToken = tokenService.createTokenGroup(expectedMember1).accessToken val modifyEbookRequest = ModifyEbookRequest( - ebook = ModifyEbookRequest.Ebook( - title = "title2", - ) + title = "title2", + relatedCategoryIdList = null, + mainImageId = null, + descriptionImageIdList = null, + introduction = null, + tableOfContents = null, + price = null, ) val updatedEbook = webTestClient @@ -883,7 +887,7 @@ internal class EbookControllerTest @Autowired constructor( assertThat(updatedEbook.id).isEqualTo(response.ebook.id) assertThat(updatedEbook.mainImage).isEqualTo(mainImage) - assertThat(updatedEbook.title).isEqualTo(modifyEbookRequest.ebook!!.title) + assertThat(updatedEbook.title).isEqualTo(modifyEbookRequest.title) assertThat(updatedEbook.descriptionImageList).isEqualTo(descriptionImageList) } @@ -893,7 +897,13 @@ internal class EbookControllerTest @Autowired constructor( val accessToken = tokenService.createTokenGroup(expectedMember1).accessToken val modifyEbookRequest = ModifyEbookRequest( - ebook = ModifyEbookRequest.Ebook(title = "") + title = "", + relatedCategoryIdList = null, + mainImageId = null, + descriptionImageIdList = null, + introduction = null, + tableOfContents = null, + price = null, ) webTestClient @@ -912,9 +922,13 @@ internal class EbookControllerTest @Autowired constructor( val accessToken = tokenService.createTokenGroup(expectedMember1).accessToken val modifyEbookRequest = ModifyEbookRequest( - ebook = ModifyEbookRequest.Ebook( - title = "title2", - ) + title = "title2", + relatedCategoryIdList = null, + mainImageId = null, + descriptionImageIdList = null, + introduction = null, + tableOfContents = null, + price = null, ) webTestClient @@ -934,9 +948,13 @@ internal class EbookControllerTest @Autowired constructor( val accessToken = tokenService.createTokenGroup(expectedMember2).accessToken val modifyEbookRequest = ModifyEbookRequest( - ebook = ModifyEbookRequest.Ebook( - title = "title2", - ) + title = "title2", + relatedCategoryIdList = null, + mainImageId = null, + descriptionImageIdList = null, + introduction = null, + tableOfContents = null, + price = null, ) webTestClient @@ -950,11 +968,91 @@ internal class EbookControllerTest @Autowired constructor( .expectStatus().isForbidden } + @Test + fun `전자책 수정시 관련 카테고리가 비어 있을 경우 예외가 발생한다`(): Unit = runBlocking { + val tokenGroup = tokenService.createTokenGroup(expectedMember1) + val request = mapOf("relatedCategoryIdList" to listOf()) + webTestClient.patch().isBadRequest("/api/v1/ebooks/${UUID.randomUUID()}", request, tokenGroup.accessToken) + } + + @Test + fun `전자책 삭제시 전자책 식별자가 유효하지 않을 경우 예외가 발생한다`(): Unit = runBlocking { + webTestClient + .delete() + .uri("/api/v1/ebooks/test") + .exchange() + .expectStatus().isBadRequest + } + + @Test + fun `전자책 등록시 관련 카테고리가 비어 있을 경우 예외가 발생한다`(): Unit = runBlocking { + val tokenGroup = tokenService.createTokenGroup(expectedMember1) + val request = mapOf( + "pdfId" to UUID.randomUUID(), + "title" to "title", + "relatedCategoryIdList" to listOf(), + "mainImageId" to UUID.randomUUID(), + "descriptionImageIdList" to listOf(UUID.randomUUID()), + "price" to 1000, + "introduction" to "introduction", + "tableOfContents" to "tableOfContents" + ) + webTestClient.post().isBadRequest("/api/v1/ebooks", request, tokenGroup.accessToken) + } + + @Test + fun `전자책 등록시 소개가 비어 있을 경우 예외가 발생한다`(): Unit = runBlocking { + val tokenGroup = tokenService.createTokenGroup(expectedMember1) + val request = mapOf( + "pdfId" to UUID.randomUUID(), + "title" to "title", + "relatedCategoryIdList" to listOf(UUID.randomUUID()), + "mainImageId" to UUID.randomUUID(), + "descriptionImageIdList" to listOf(UUID.randomUUID()), + "price" to 1000, + "introduction" to "", + "tableOfContents" to "tableOfContents" + ) + webTestClient.post().isBadRequest("/api/v1/ebooks", request, tokenGroup.accessToken) + } + + @Test + fun `전자책 등록시 가격이 음수일 경우 예외가 발생한다`(): Unit = runBlocking { + val tokenGroup = tokenService.createTokenGroup(expectedMember1) + val request = mapOf( + "pdfId" to UUID.randomUUID(), + "title" to "title", + "relatedCategoryIdList" to listOf(UUID.randomUUID()), + "mainImageId" to UUID.randomUUID(), + "descriptionImageIdList" to listOf(UUID.randomUUID()), + "price" to -1, + "introduction" to "introduction", + "tableOfContents" to "tableOfContents" + ) + webTestClient.post().isBadRequest("/api/v1/ebooks", request, tokenGroup.accessToken) + } + + @Test + fun `전자책 등록시 가격이 1000만원을 초과할 경우 예외가 발생한다`(): Unit = runBlocking { + val tokenGroup = tokenService.createTokenGroup(expectedMember1) + val request = mapOf( + "pdfId" to UUID.randomUUID(), + "title" to "title", + "relatedCategoryIdList" to listOf(UUID.randomUUID()), + "mainImageId" to UUID.randomUUID(), + "descriptionImageIdList" to listOf(UUID.randomUUID()), + "price" to 10_000_001, + "introduction" to "introduction", + "tableOfContents" to "tableOfContents" + ) + webTestClient.post().isBadRequest("/api/v1/ebooks", request, tokenGroup.accessToken) + } + suspend fun postCreateEbookAndCreateTransaction(): Pair { val (_, response) = postCreateEbook() val createTransactionRequest = CreateTransactionRequest( - ebookId = response.ebook.id.toString(), - paymentMethod = PaymentMethod.CREDIT_CARD.name, + ebookId = response.ebook.id, + paymentMethod = PaymentMethod.CREDIT_CARD, price = response.ebook.price ) @@ -986,14 +1084,14 @@ internal class EbookControllerTest @Autowired constructor( val mainImage = postSaveMainImage(imageBase64Raw, imagePath, accessToken) val descriptionImageList = postSaveDescriptionImages(imageBase64Raw, imagePath, accessToken) - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = CreateEbookRequest( - pdfId = pdf.id.toString(), + pdfId = pdf.id, title = "title", relatedCategoryIdList = listOf(categoryId), - mainImageId = mainImage.id.toString(), - descriptionImageIdList = descriptionImageList.map { it.id.toString() }, + mainImageId = mainImage.id, + descriptionImageIdList = descriptionImageList.map { it.id }, 10000, "introduction", "tableOfContent" @@ -1014,7 +1112,7 @@ internal class EbookControllerTest @Autowired constructor( } fun postSaveDescriptionImages( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, ): List { @@ -1022,15 +1120,13 @@ internal class EbookControllerTest @Autowired constructor( imageList = listOf( ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 1 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt() ), ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 2 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt() ), ) ) @@ -1052,15 +1148,15 @@ internal class EbookControllerTest @Autowired constructor( } private fun postSaveMainImage( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, - ): SaveMainImageResponse.MainImageDto { + ): EbookImageDto { val saveMainImageRequest = SaveMainImageRequest( - SaveMainImageRequest.MainImageDto( - imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), + ImageDto( + base64Raw = imageBase64Raw, + extension = ImageExtension.valueOf(imagePath.extension.uppercase()), + byteSize = imagePath.fileSize().toInt() ) ) @@ -1088,13 +1184,13 @@ internal class EbookControllerTest @Autowired constructor( val imageBytes = Files.readAllBytes(imagePath) val imageBase64Raw = Base64.getEncoder().encodeToString(imageBytes) val mainImage = postSaveMainImage(imageBase64Raw, imagePath, accessToken) - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val createEbookRequest = CreateEbookRequest( - pdfId = pdf.id.toString(), + pdfId = pdf.id, title = "title", relatedCategoryIdList = listOf(categoryId), - mainImageId = mainImage.id.toString(), + mainImageId = mainImage.id, descriptionImageIdList = listOf(), 10000, "introduction", diff --git a/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookImageControllerTest.kt b/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookImageControllerTest.kt index 8654506..8bc9314 100644 --- a/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookImageControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookImageControllerTest.kt @@ -3,6 +3,7 @@ package com.devooks.backend.ebook.v1.controller import com.devooks.backend.BackendApplication.Companion.STATIC_ROOT_PATH import com.devooks.backend.BackendApplication.Companion.createDirectories import com.devooks.backend.auth.v1.service.TokenService +import com.devooks.backend.common.domain.ImageExtension import com.devooks.backend.common.dto.ImageDto import com.devooks.backend.config.IntegrationTest import com.devooks.backend.ebook.v1.dto.request.SaveDescriptionImagesRequest @@ -79,15 +80,13 @@ internal class EbookImageControllerTest @Autowired constructor( imageList = listOf( ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 1 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 2 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ) ) @@ -107,9 +106,8 @@ internal class EbookImageControllerTest @Autowired constructor( .descriptionImageList descriptionImageList.forEachIndexed { index, image -> - val expected = request.imageList!![index] - assertThat(image.order).isEqualTo(expected.order) - assertThat(File(image.imagePath).exists()).isTrue() + assertThat(image.order).isEqualTo(index) + assertThat(File(image.imagePath.substring(1)).exists()).isTrue() } } @@ -122,10 +120,10 @@ internal class EbookImageControllerTest @Autowired constructor( val imageBase64Raw = Base64.getEncoder().encodeToString(imageBytes) val request = SaveMainImageRequest( - SaveMainImageRequest.MainImageDto( + ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ) ) @@ -143,6 +141,6 @@ internal class EbookImageControllerTest @Autowired constructor( .responseBody!! .mainImage - assertThat(File(mainImage.imagePath).exists()).isTrue() + assertThat(File(mainImage.imagePath.substring(1)).exists()).isTrue() } } diff --git a/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryCommentControllerTest.kt b/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryCommentControllerTest.kt index 4ba55e2..f2c74e1 100644 --- a/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryCommentControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryCommentControllerTest.kt @@ -5,11 +5,13 @@ import com.devooks.backend.BackendApplication.Companion.createDirectories import com.devooks.backend.auth.v1.domain.AccessToken import com.devooks.backend.auth.v1.service.TokenService import com.devooks.backend.category.v1.repository.CategoryRepository +import com.devooks.backend.common.domain.ImageExtension import com.devooks.backend.common.dto.ImageDto +import com.devooks.backend.common.dto.PageResponse import com.devooks.backend.config.IntegrationTest import com.devooks.backend.ebook.v1.dto.EbookImageDto -import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentDto -import com.devooks.backend.ebook.v1.dto.EbookInquiryDto +import com.devooks.backend.ebook.v1.dto.EbookInquiryCommentView +import com.devooks.backend.ebook.v1.dto.EbookInquiryView import com.devooks.backend.ebook.v1.dto.request.CreateEbookInquiryCommentRequest import com.devooks.backend.ebook.v1.dto.request.CreateEbookInquiryRequest import com.devooks.backend.ebook.v1.dto.request.CreateEbookRequest @@ -19,7 +21,6 @@ import com.devooks.backend.ebook.v1.dto.request.SaveMainImageRequest import com.devooks.backend.ebook.v1.dto.response.CreateEbookInquiryCommentResponse import com.devooks.backend.ebook.v1.dto.response.CreateEbookInquiryResponse import com.devooks.backend.ebook.v1.dto.response.CreateEbookResponse -import com.devooks.backend.ebook.v1.dto.response.GetEbookInquiryCommentsResponse import com.devooks.backend.ebook.v1.dto.response.ModifyEbookInquiryCommentResponse import com.devooks.backend.ebook.v1.dto.response.SaveDescriptionImagesResponse import com.devooks.backend.ebook.v1.dto.response.SaveMainImageResponse @@ -117,7 +118,7 @@ internal class EbookInquiryCommentControllerTest @Autowired constructor( val (accessToken, ebookInquiry) = postCreateEbookInquiry() val createEbookInquiryCommentRequest = CreateEbookInquiryCommentRequest( - inquiryId = ebookInquiry.id.toString(), + inquiryId = ebookInquiry.id, content = "content" ) @@ -135,7 +136,7 @@ internal class EbookInquiryCommentControllerTest @Autowired constructor( .responseBody!! .comment - assertThat(ebookInquiryComment.inquiryId.toString()).isEqualTo(createEbookInquiryCommentRequest.inquiryId) + assertThat(ebookInquiryComment.inquiryId).isEqualTo(createEbookInquiryCommentRequest.inquiryId) assertThat(ebookInquiryComment.content).isEqualTo(createEbookInquiryCommentRequest.content) assertThat(ebookInquiryComment.writerMemberId).isEqualTo(expectedMember1.id) @@ -156,17 +157,20 @@ internal class EbookInquiryCommentControllerTest @Autowired constructor( fun `전자책 문의 댓글을 조회할 수 있다`(): Unit = runBlocking { val ebookInquiryComment = postCreateEbookInquiryComment() - val foundEbookInquiryComment = webTestClient + val pageEbookInquiryComment = webTestClient .get() .uri("/api/v1/ebook-inquiry-comments?inquiryId=${ebookInquiryComment.inquiryId}&page=1&count=10") .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .comments[0] + val foundEbookInquiryComment = pageEbookInquiryComment.data[0] + + assertThat(pageEbookInquiryComment.pageable.totalPages).isEqualTo(1) + assertThat(pageEbookInquiryComment.pageable.totalPages).isEqualTo(1) assertThat(foundEbookInquiryComment.id).isEqualTo(ebookInquiryComment.id) assertThat(foundEbookInquiryComment.content).isEqualTo(ebookInquiryComment.content) assertThat(foundEbookInquiryComment.inquiryId).isEqualTo(ebookInquiryComment.inquiryId) @@ -272,7 +276,7 @@ internal class EbookInquiryCommentControllerTest @Autowired constructor( val (accessToken, _) = postCreateEbookInquiry() val createEbookInquiryCommentRequest = CreateEbookInquiryCommentRequest( - inquiryId = UUID.randomUUID().toString(), + inquiryId = UUID.randomUUID(), content = "content" ) @@ -287,11 +291,11 @@ internal class EbookInquiryCommentControllerTest @Autowired constructor( .expectStatus().isNotFound } - private suspend fun EbookInquiryCommentControllerTest.postCreateEbookInquiryComment(): EbookInquiryCommentDto { + private suspend fun EbookInquiryCommentControllerTest.postCreateEbookInquiryComment(): EbookInquiryCommentView { val (accessToken, ebookInquiry) = postCreateEbookInquiry() val createEbookInquiryCommentRequest = CreateEbookInquiryCommentRequest( - inquiryId = ebookInquiry.id.toString(), + inquiryId = ebookInquiry.id, content = "content" ) @@ -311,11 +315,11 @@ internal class EbookInquiryCommentControllerTest @Autowired constructor( return ebookInquiryComment } - private suspend fun EbookInquiryCommentControllerTest.postCreateEbookInquiry(): Pair { + private suspend fun EbookInquiryCommentControllerTest.postCreateEbookInquiry(): Pair { val (_, createEbookResponse) = postCreateEbook() val accessToken = tokenService.createTokenGroup(expectedMember1).accessToken val createEbookInquiryRequest = CreateEbookInquiryRequest( - ebookId = createEbookResponse.ebook.id.toString(), + ebookId = createEbookResponse.ebook.id, content = "content" ) @@ -345,14 +349,14 @@ internal class EbookInquiryCommentControllerTest @Autowired constructor( val mainImage = postSaveMainImage(imageBase64Raw, imagePath, accessToken) val descriptionImageList = postSaveDescriptionImages(imageBase64Raw, imagePath, accessToken) - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = CreateEbookRequest( - pdfId = pdf.id.toString(), + pdfId = pdf.id, title = "title", relatedCategoryIdList = listOf(categoryId), - mainImageId = mainImage.id.toString(), - descriptionImageIdList = descriptionImageList.map { it.id.toString() }, + mainImageId = mainImage.id, + descriptionImageIdList = descriptionImageList.map { it.id }, 10000, "introduction", "tableOfContent" @@ -373,7 +377,7 @@ internal class EbookInquiryCommentControllerTest @Autowired constructor( } fun postSaveDescriptionImages( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, ): List { @@ -381,15 +385,13 @@ internal class EbookInquiryCommentControllerTest @Autowired constructor( imageList = listOf( ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 1 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 2 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ) ) @@ -411,15 +413,15 @@ internal class EbookInquiryCommentControllerTest @Autowired constructor( } private fun postSaveMainImage( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, - ): SaveMainImageResponse.MainImageDto { + ): EbookImageDto { val saveMainImageRequest = SaveMainImageRequest( - SaveMainImageRequest.MainImageDto( + ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ) ) diff --git a/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryControllerTest.kt b/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryControllerTest.kt index faa64d2..c1b70ac 100644 --- a/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/ebook/v1/controller/EbookInquiryControllerTest.kt @@ -5,10 +5,12 @@ import com.devooks.backend.BackendApplication.Companion.createDirectories import com.devooks.backend.auth.v1.domain.AccessToken import com.devooks.backend.auth.v1.service.TokenService import com.devooks.backend.category.v1.repository.CategoryRepository +import com.devooks.backend.common.domain.ImageExtension import com.devooks.backend.common.dto.ImageDto +import com.devooks.backend.common.dto.PageResponse import com.devooks.backend.config.IntegrationTest import com.devooks.backend.ebook.v1.dto.EbookImageDto -import com.devooks.backend.ebook.v1.dto.EbookInquiryDto +import com.devooks.backend.ebook.v1.dto.EbookInquiryView import com.devooks.backend.ebook.v1.dto.request.CreateEbookInquiryRequest import com.devooks.backend.ebook.v1.dto.request.CreateEbookRequest import com.devooks.backend.ebook.v1.dto.request.ModifyEbookInquiryRequest @@ -16,7 +18,6 @@ import com.devooks.backend.ebook.v1.dto.request.SaveDescriptionImagesRequest import com.devooks.backend.ebook.v1.dto.request.SaveMainImageRequest import com.devooks.backend.ebook.v1.dto.response.CreateEbookInquiryResponse import com.devooks.backend.ebook.v1.dto.response.CreateEbookResponse -import com.devooks.backend.ebook.v1.dto.response.GetEbookInquiriesResponse import com.devooks.backend.ebook.v1.dto.response.ModifyEbookInquiryResponse import com.devooks.backend.ebook.v1.dto.response.SaveDescriptionImagesResponse import com.devooks.backend.ebook.v1.dto.response.SaveMainImageResponse @@ -112,7 +113,7 @@ internal class EbookInquiryControllerTest @Autowired constructor( val (_, createEbookResponse) = postCreateEbook() val accessToken = tokenService.createTokenGroup(expectedMember1).accessToken val createEbookInquiryRequest = CreateEbookInquiryRequest( - ebookId = createEbookResponse.ebook.id.toString(), + ebookId = createEbookResponse.ebook.id, content = "content" ) @@ -130,7 +131,7 @@ internal class EbookInquiryControllerTest @Autowired constructor( .responseBody!! .ebookInquiry - assertThat(ebookInquiry.ebookId.toString()).isEqualTo(createEbookInquiryRequest.ebookId) + assertThat(ebookInquiry.ebookId).isEqualTo(createEbookInquiryRequest.ebookId) assertThat(ebookInquiry.content).isEqualTo(createEbookInquiryRequest.content) assertThat(ebookInquiry.writerMemberId).isEqualTo(expectedMember1.id) @@ -149,18 +150,21 @@ internal class EbookInquiryControllerTest @Autowired constructor( fun `전자책 문의를 조회할 수 있다`(): Unit = runBlocking { val createdEbookInquiry = postCreateEbookInquiry() - val foundEbookInquiry = + val pageEbookInquiry = webTestClient .get() .uri("/api/v1/ebook-inquiries?ebookId=${createdEbookInquiry.ebookId}&page=1&count=10") .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .ebookInquiryList[0] + val foundEbookInquiry = pageEbookInquiry.data[0] + + assertThat(pageEbookInquiry.pageable.totalPages).isEqualTo(1) + assertThat(pageEbookInquiry.pageable.totalElements).isEqualTo(1) assertThat(foundEbookInquiry.id).isEqualTo(createdEbookInquiry.id) assertThat(foundEbookInquiry.content).isEqualTo(createdEbookInquiry.content) assertThat(foundEbookInquiry.ebookId).isEqualTo(createdEbookInquiry.ebookId) @@ -273,7 +277,7 @@ internal class EbookInquiryControllerTest @Autowired constructor( fun `전자책 문의 작성시 전자책이 존재하지 않을 경우 예외가 발생한다`(): Unit = runBlocking { val accessToken = tokenService.createTokenGroup(expectedMember1).accessToken val createEbookInquiryRequest = CreateEbookInquiryRequest( - ebookId = UUID.randomUUID().toString(), + ebookId = UUID.randomUUID(), content = "content" ) @@ -288,11 +292,11 @@ internal class EbookInquiryControllerTest @Autowired constructor( .expectStatus().isNotFound } - private suspend fun EbookInquiryControllerTest.postCreateEbookInquiry(): EbookInquiryDto { + private suspend fun EbookInquiryControllerTest.postCreateEbookInquiry(): EbookInquiryView { val (_, createEbookResponse) = postCreateEbook() val accessToken = tokenService.createTokenGroup(expectedMember1).accessToken val createEbookInquiryRequest = CreateEbookInquiryRequest( - ebookId = createEbookResponse.ebook.id.toString(), + ebookId = createEbookResponse.ebook.id, content = "content" ) @@ -323,14 +327,14 @@ internal class EbookInquiryControllerTest @Autowired constructor( val mainImage = postSaveMainImage(imageBase64Raw, imagePath, accessToken) val descriptionImageList = postSaveDescriptionImages(imageBase64Raw, imagePath, accessToken) - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = CreateEbookRequest( - pdfId = pdf.id.toString(), + pdfId = pdf.id, title = "title", relatedCategoryIdList = listOf(categoryId), - mainImageId = mainImage.id.toString(), - descriptionImageIdList = descriptionImageList.map { it.id.toString() }, + mainImageId = mainImage.id, + descriptionImageIdList = descriptionImageList.map { it.id }, 10000, "introduction", "tableOfContent" @@ -351,7 +355,7 @@ internal class EbookInquiryControllerTest @Autowired constructor( } fun postSaveDescriptionImages( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, ): List { @@ -359,15 +363,13 @@ internal class EbookInquiryControllerTest @Autowired constructor( imageList = listOf( ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 1 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 2 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ) ) @@ -389,15 +391,15 @@ internal class EbookInquiryControllerTest @Autowired constructor( } private fun postSaveMainImage( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, - ): SaveMainImageResponse.MainImageDto { + ): EbookImageDto { val saveMainImageRequest = SaveMainImageRequest( - SaveMainImageRequest.MainImageDto( + ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ) ) diff --git a/src/test/kotlin/com/devooks/backend/fixture/ErrorResponse.kt b/src/test/kotlin/com/devooks/backend/fixture/ErrorResponse.kt index 34c8ab7..9f47828 100644 --- a/src/test/kotlin/com/devooks/backend/fixture/ErrorResponse.kt +++ b/src/test/kotlin/com/devooks/backend/fixture/ErrorResponse.kt @@ -1,12 +1,11 @@ package com.devooks.backend.fixture +import com.devooks.backend.common.error.CommonError import com.devooks.backend.common.exception.GeneralException import java.time.Instant import org.assertj.core.api.Assertions.assertThat import org.springframework.http.HttpHeaders.AUTHORIZATION -import org.springframework.http.HttpStatus import org.springframework.http.MediaType -import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.WebTestClient.RequestBodyUriSpec import org.springframework.test.web.reactive.server.expectBody @@ -23,31 +22,21 @@ internal data class ErrorResponse( assertThat(status).isEqualTo(exception.status.value()) assertThat(error).isEqualTo(exception.status.name) assertThat(code).isEqualTo(exception.code) - assertThat(message).isEqualTo(exception.message) } companion object { - fun WebTestClient.postForBadRequest( + fun RequestBodyUriSpec.isBadRequest( uri: String, - request: String, + request: Map, token: String? = null, - ): ErrorResponse = post().getResponseBodyAboutBadRequest(uri, request, token) - - fun WebTestClient.patchForBadRequest( - uri: String, - request: String, - token: String? = null, - ): ErrorResponse = patch().getResponseBodyAboutBadRequest(uri, request, token) - - fun WebTestClient.patchForConflict( - uri: String, - request: String, - token: String? = null, - ): ErrorResponse = patch().getResponseBodyAboutConflict(uri, request, token) + ) { + getResponseBodyAboutBadRequest(uri, request, token) + .isEqualTo(CommonError.INVALID_REQUEST.exception) + } private fun RequestBodyUriSpec.getResponseBodyAboutBadRequest( uri: String, - request: String, + request: Map, token: String? = null, ): ErrorResponse = this.uri(uri) @@ -61,20 +50,5 @@ internal data class ErrorResponse( .returnResult() .responseBody!! - private fun RequestBodyUriSpec.getResponseBodyAboutConflict( - uri: String, - request: String, - token: String? = null, - ): ErrorResponse = - this.uri(uri) - .apply { token?.also { header(AUTHORIZATION, it) } } - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(request) - .exchange() - .expectStatus().isEqualTo(HttpStatus.CONFLICT) - .expectBody() - .returnResult() - .responseBody!! } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/devooks/backend/member/v1/controller/MemberControllerTest.kt b/src/test/kotlin/com/devooks/backend/member/v1/controller/MemberControllerTest.kt index e4ec608..2ef0c4b 100644 --- a/src/test/kotlin/com/devooks/backend/member/v1/controller/MemberControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/member/v1/controller/MemberControllerTest.kt @@ -12,12 +12,11 @@ import com.devooks.backend.auth.v1.service.TokenService import com.devooks.backend.category.v1.domain.Category.Companion.toDomain import com.devooks.backend.category.v1.dto.CategoryDto.Companion.toDto import com.devooks.backend.category.v1.repository.CategoryRepository +import com.devooks.backend.common.domain.ImageExtension import com.devooks.backend.common.dto.ImageDto -import com.devooks.backend.common.error.CommonError import com.devooks.backend.config.IntegrationTest import com.devooks.backend.fixture.ErrorResponse -import com.devooks.backend.fixture.ErrorResponse.Companion.patchForBadRequest -import com.devooks.backend.fixture.ErrorResponse.Companion.postForBadRequest +import com.devooks.backend.fixture.ErrorResponse.Companion.isBadRequest import com.devooks.backend.member.v1.domain.Member.Companion.toDomain import com.devooks.backend.member.v1.dto.GetProfileResponse import com.devooks.backend.member.v1.dto.ModifyAccountInfoRequest @@ -34,10 +33,8 @@ import com.devooks.backend.member.v1.repository.FavoriteCategoryRepository import com.devooks.backend.member.v1.repository.MemberInfoRepository import com.devooks.backend.member.v1.repository.MemberRepository import java.io.File -import java.nio.file.Files import java.time.Instant import java.util.* -import kotlin.io.path.Path import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking @@ -92,10 +89,10 @@ internal class MemberControllerTest @Autowired constructor( @Test fun `회원가입 할 수 있다`(): Unit = runBlocking { // given - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = SignUpRequest( oauthId = "oauthId", - oauthType = OauthType.NAVER.name, + oauthType = OauthType.NAVER, nickname = "nickname", favoriteCategoryIdList = listOf(categoryId) ) @@ -109,7 +106,7 @@ internal class MemberControllerTest @Autowired constructor( assertThat(response.member.profileImagePath).isEqualTo("") val category = categoryRepository.findAll().firstOrNull()!! - assertThat(category.id.toString()).isEqualTo(request.favoriteCategoryIdList!!.first()) + assertThat(category.id).isEqualTo(request.favoriteCategoryIdList.first()) val favoriteCategory = favoriteCategoryRepository.findAll().firstOrNull()!! assertThat(favoriteCategory.categoryId).isEqualTo(category.id) @@ -122,126 +119,105 @@ internal class MemberControllerTest @Autowired constructor( @Test fun `oauthId가 존재하지 않을 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "oauthType" : "NAVER", - "nickname" : "nickname", - "favoriteCategories" : [ "category" ] - } - """.trimIndent() - val response = webTestClient.postForBadRequest("/api/v1/members/signup", request) - - response.isEqualTo(AuthError.REQUIRED_OAUTH_ID.exception) + val request = mapOf( + "oauthType" to "NAVER", + "nickname" to "nickname", + "favoriteCategories" to UUID.randomUUID() + ) + webTestClient.post().isBadRequest("/api/v1/members/signup", request) } @Test fun `oauthId가 빈문자열인 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "oauthId": "", - "oauthType" : "NAVER", - "nickname" : "nickname", - "favoriteCategories" : [ "category" ] - } - """.trimIndent() - val response = webTestClient.postForBadRequest("/api/v1/members/signup", request) - - response.isEqualTo(AuthError.REQUIRED_OAUTH_ID.exception) + val request = mapOf( + "oauthId" to "", + "oauthType" to "NAVER", + "nickname" to "nickname", + "favoriteCategories" to UUID.randomUUID() + ) + webTestClient.post().isBadRequest("/api/v1/members/signup", request) } @Test fun `oauthType이 존재하지 않을 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "oauthId" : "oauthId", - "nickname" : "nickname", - "favoriteCategories" : [ "category" ] - } - """.trimIndent() - val response = webTestClient.postForBadRequest("/api/v1/members/signup", request) - - response.isEqualTo(AuthError.INVALID_OAUTH_TYPE.exception) + val request = mapOf( + "oauthId" to "123", + "nickname" to "nickname", + "favoriteCategories" to UUID.randomUUID() + ) + webTestClient.post().isBadRequest("/api/v1/members/signup", request) } @Test fun `oauthType이 잘못된 형식일 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "oauthId" : "oauthId", - "oauthType" : " ", - "nickname" : "nickname", - "favoriteCategories" : [ "category" ] - } - """.trimIndent() - val response = webTestClient.postForBadRequest("/api/v1/members/signup", request) - - response.isEqualTo(AuthError.INVALID_OAUTH_TYPE.exception) + val request = mapOf( + "oauthId" to "123", + "oauthType" to "test", + "nickname" to "nickname", + "favoriteCategories" to UUID.randomUUID() + ) + webTestClient.post().isBadRequest("/api/v1/members/signup", request) } @Test fun `nickname 이 null 일 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "oauthId" : "oauthId", - "oauthType" : "NAVER", - "favoriteCategories" : [ "category" ] - } - """.trimIndent() - val response = webTestClient.postForBadRequest("/api/v1/members/signup", request) - - response.isEqualTo(MemberError.REQUIRED_NICKNAME.exception) + val request = mapOf( + "oauthId" to "123", + "oauthType" to "NAVER", + "favoriteCategories" to UUID.randomUUID() + ) + webTestClient.post().isBadRequest("/api/v1/members/signup", request) } @Test fun `nickname 이 2자 미만일 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "oauthId" : "oauthId", - "oauthType" : "NAVER", - "nickname" : "n", - "favoriteCategories" : [ "category" ] - } - """.trimIndent() - val response = webTestClient.postForBadRequest("/api/v1/members/signup", request) - - response.isEqualTo(MemberError.INVALID_NICKNAME.exception) + val request = mapOf( + "oauthId" to "123", + "oauthType" to "NAVER", + "nickname" to "1", + "favoriteCategories" to UUID.randomUUID() + ) + webTestClient.post().isBadRequest("/api/v1/members/signup", request) } @Test fun `nickname 이 12자 초과일 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "oauthId" : "oauthId", - "oauthType" : "NAVER", - "nickname" : "1111111111111", - "favoriteCategories" : [ "category" ] - } - """.trimIndent() - val response = webTestClient.postForBadRequest("/api/v1/members/signup", request) - - response.isEqualTo(MemberError.INVALID_NICKNAME.exception) + val request = mapOf( + "oauthId" to "123", + "oauthType" to "NAVER", + "nickname" to "1111111111111", + "favoriteCategories" to UUID.randomUUID() + ) + webTestClient.post().isBadRequest("/api/v1/members/signup", request) } @Test fun `favoriteCategories 가 null 일 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "oauthId" : "oauthId", - "oauthType" : "NAVER", - "nickname" : "nickname" - } - """.trimIndent() - val response = webTestClient.postForBadRequest("/api/v1/members/signup", request) + val request = mapOf( + "oauthId" to "123", + "oauthType" to "NAVER", + "nickname" to "nickname", + ) + webTestClient.post().isBadRequest("/api/v1/members/signup", request) + } - response.isEqualTo(MemberError.REQUIRED_FAVORITE_CATEGORIES.exception) + @Test + fun `favoriteCategories 가 UUID가 아닐 경우 예외가 발생한다`(): Unit = runBlocking { + val request = mapOf( + "oauthId" to "123", + "oauthType" to "NAVER", + "nickname" to "nickname", + "favoriteCategories" to "test" + ) + webTestClient.post().isBadRequest("/api/v1/members/signup", request) } @Test fun `닉네임이 이미 존재할 경우 회원가입 실패`(): Unit = runBlocking { - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = SignUpRequest( oauthId = "oauthId", - oauthType = "NAVER", + oauthType = OauthType.NAVER, nickname = "nickname", favoriteCategoryIdList = listOf(categoryId) ) @@ -264,10 +240,10 @@ internal class MemberControllerTest @Autowired constructor( @Test fun `정지당한 회원일 경우 회원가입 실패`(): Unit = runBlocking { - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = SignUpRequest( oauthId = "oauthId", - oauthType = "NAVER", + oauthType = OauthType.NAVER, nickname = "nickname", favoriteCategoryIdList = listOf(categoryId) ) @@ -292,10 +268,10 @@ internal class MemberControllerTest @Autowired constructor( @Test fun `탈퇴한 회원일 경우 회원가입 실패`(): Unit = runBlocking { - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = SignUpRequest( oauthId = "oauthId", - oauthType = "NAVER", + oauthType = OauthType.NAVER, nickname = "nickname", favoriteCategoryIdList = listOf(categoryId) ) @@ -320,10 +296,10 @@ internal class MemberControllerTest @Autowired constructor( @Test fun `이미 존재하는 회원일 경우 회원가입 실패`(): Unit = runBlocking { - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = SignUpRequest( oauthId = "oauthId", - oauthType = "NAVER", + oauthType = OauthType.NAVER, nickname = "nickname", favoriteCategoryIdList = listOf(categoryId) ) @@ -348,7 +324,7 @@ internal class MemberControllerTest @Autowired constructor( fun `계좌정보를 수정할 수 있다`(): Unit = runBlocking { val (signUpResponse, tokenGroup) = signUp() val modifyAccountInfoRequest = ModifyAccountInfoRequest( - realName = "이상민", + realName = "1", bank = "농협", accountNumber = "12312341234", ) @@ -373,15 +349,54 @@ internal class MemberControllerTest @Autowired constructor( assertThat(response.realName).isEqualTo(memberInfo.realName) } + @Test + fun `계좌정보에서 이름만 수정할 수 있다`(): Unit = runBlocking { + val (signUpResponse, tokenGroup) = signUp() + val modifyAccountInfoRequest = ModifyAccountInfoRequest( + realName = "1111111111", + bank = null, + accountNumber = null, + ) + + val response = webTestClient + .patch() + .uri("/api/v1/members/account") + .header(AUTHORIZATION, "Bearer ${tokenGroup.accessToken}") + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .bodyValue(modifyAccountInfoRequest) + .exchange() + .expectStatus().isOk + .expectBody() + .returnResult() + .responseBody!! + + val memberInfo = memberInfoRepository.findByMemberId(signUpResponse.member.id)!! + + assertThat(response.realName).isEqualTo(memberInfo.realName) + assertThat(response.bank).isNotNull() + assertThat(response.accountNumber).isNotNull() + } + + @Test + fun `계좌번호에 특수문자가 입력될 경우 예외가 발생한다`(): Unit = runBlocking { + val request = mapOf( + "realName" to "123", + "bank" to "bank", + "accountNumber" to "1234-1234-1234" + ) + + webTestClient.patch().isBadRequest("/api/v1/members/account", request) + } + @Test fun `프로필 사진을 수정할 수 있다`(): Unit = runBlocking { val (signUpResponse, tokenGroup) = signUp() val modifyProfileImageRequest = ModifyProfileImageRequest( image = ImageDto( "test", - "png", + ImageExtension.PNG, 4, - 1 ) ) @@ -401,15 +416,13 @@ internal class MemberControllerTest @Autowired constructor( val member = response.member assertThat(member.id).isEqualTo(signUpResponse.member.id) assertThat(member.nickname).isEqualTo(signUpResponse.member.nickname) - assertThat(File(member.profileImagePath).canRead()).isEqualTo(true) - - Files.delete(Path(member.profileImagePath)) + assertThat(File(member.profileImagePath.substring(1)).exists()).isEqualTo(true) } @Test fun `프로필을 수정할 수 있다`(): Unit = runBlocking { val (_, tokenGroup) = signUp() - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val modifyProfileRequest = ModifyProfileRequest( nickname = "newNickname", @@ -425,6 +438,78 @@ internal class MemberControllerTest @Autowired constructor( postModifyProfile(tokenGroup, modifyProfileRequest) } + @Test + fun `이메일만 수정할 수 있다`(): Unit = runBlocking { + val (_, tokenGroup) = signUp() + val modifyProfileRequest = + ModifyProfileRequest( + nickname = null, + phoneNumber = null, + blogLink = null, + instagramLink = null, + youtubeLink = null, + introduction = null, + favoriteCategoryIdList = null, + email = "asd@naver.com" + ) + + val response = webTestClient + .patch() + .uri("/api/v1/members/profile") + .header(AUTHORIZATION, "Bearer ${tokenGroup.accessToken}") + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .bodyValue(modifyProfileRequest) + .exchange() + .expectStatus().isOk + .expectBody() + .returnResult() + .responseBody!! + + val memberInfo = response.profile + + assertThat(memberInfo.email).isEqualTo(modifyProfileRequest.email) + } + + @Test + fun `전화번호 수정시 전화번호가 잘못되어 있을 경우 예외가 발생한다`(): Unit = runBlocking { + val (_, tokenGroup) = signUp() + val request = mapOf( + "phoneNumber" to "q-1234-1234" + ) + webTestClient.patch().isBadRequest("/api/v1/members/profile", request, tokenGroup.accessToken) + } + + @Test + fun `닉네임 수정시 12 글자를 넘을 경우 예외가 발생한다`(): Unit = runBlocking { + val (_, tokenGroup) = signUp() + val request = mapOf("nickname" to "".plus("a").repeat(13)) + webTestClient.patch().isBadRequest("/api/v1/members/profile", request, tokenGroup.accessToken) + } + + @Test + fun `링크 수정시 255 글자를 넘을 경우 예외가 발생한다`(): Unit = runBlocking { + val (_, tokenGroup) = signUp() + val request = mapOf("blogLink" to "".plus("a").repeat(256)) + webTestClient.patch().isBadRequest("/api/v1/members/profile", request, tokenGroup.accessToken) + } + + @Test + fun `이메일 수정시 이메일이 잘못되어 있을 경우 예외가 발생한다`(): Unit = runBlocking { + val (_, tokenGroup) = signUp() + val request = mapOf( + "email" to "test@asd" + ) + webTestClient.patch().isBadRequest("/api/v1/members/profile", request, tokenGroup.accessToken) + } + + @Test + fun `소개글 수정시 5000 글자가 넘을 경우 예외가 발생한다`(): Unit = runBlocking { + val (_, tokenGroup) = signUp() + val request = mapOf("introduction" to "".plus("a").repeat(5001)) + webTestClient.patch().isBadRequest("/api/v1/members/profile", request, tokenGroup.accessToken) + } + @Test fun `프로필을 조회할 수 있다`(): Unit = runBlocking { val (signUpResponse, _) = signUp() @@ -439,13 +524,13 @@ internal class MemberControllerTest @Autowired constructor( .returnResult() .responseBody!! - val categoryDto = categoryRepository.findAll().toList()[0].toDomain().toDto().id + val categoryId = categoryRepository.findAll().toList()[0].toDomain().toDto().id val foundMemberInfo = memberInfoRepository.findByMemberId(signUpResponse.member.id)!! assertThat(response.profile.id).isEqualTo(signUpResponse.member.id) assertThat(response.profile.nickname).isEqualTo(signUpResponse.member.nickname) assertThat(response.profile.profileImagePath).isEqualTo(signUpResponse.member.profileImagePath) - assertThat(response.profile.favoriteCategoryList.first().id).isEqualTo(categoryDto) + assertThat(response.profile.favoriteCategoryIdList.first()).isEqualTo(categoryId) assertThat(response.profile.blogLink).isEqualTo(foundMemberInfo.blogLink) assertThat(response.profile.youtubeLink).isEqualTo(foundMemberInfo.youtubeLink) assertThat(response.profile.introduction).isEqualTo(foundMemberInfo.introduction) @@ -457,6 +542,16 @@ internal class MemberControllerTest @Autowired constructor( assertThat(response.profile.email).isNull() } + @Test + fun `프로필 조회시 회원 식별자가 유효하지 않을 경우 예외가 발생한다`(): Unit = runBlocking { + webTestClient + .get() + .uri("/api/v1/members/test/profile") + .accept(APPLICATION_JSON) + .exchange() + .expectStatus().isBadRequest + } + @Test fun `자신의 프로필을 조회할 경우 개인 정보를 확인할 수 있다`(): Unit = runBlocking { val (signUpResponse, _) = signUp() @@ -474,13 +569,13 @@ internal class MemberControllerTest @Autowired constructor( .returnResult() .responseBody!! - val categoryDto = categoryRepository.findAll().toList()[0].toDomain().toDto().id + val categoryId = categoryRepository.findAll().toList()[0].toDomain().toDto().id val foundMemberInfo = memberInfoRepository.findByMemberId(signUpResponse.member.id)!! assertThat(response.profile.id).isEqualTo(signUpResponse.member.id) assertThat(response.profile.nickname).isEqualTo(signUpResponse.member.nickname) assertThat(response.profile.profileImagePath).isEqualTo(signUpResponse.member.profileImagePath) - assertThat(response.profile.favoriteCategoryList.first().id).isEqualTo(categoryDto) + assertThat(response.profile.favoriteCategoryIdList.first()).isEqualTo(categoryId) assertThat(response.profile.blogLink).isEqualTo(foundMemberInfo.blogLink) assertThat(response.profile.youtubeLink).isEqualTo(foundMemberInfo.youtubeLink) assertThat(response.profile.introduction).isEqualTo(foundMemberInfo.introduction) @@ -521,180 +616,127 @@ internal class MemberControllerTest @Autowired constructor( } @Test - fun `image 가 null 일 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - } - """.trimIndent() + fun `회원 탈퇴시 탈퇴 사유가 비어 있을 경우 예외가 발생한다`(): Unit = runBlocking { val (_, tokenGroup) = signUp() - val response = - webTestClient - .patchForBadRequest("/api/v1/members/image", request, tokenGroup.accessToken) + val request = mapOf( + "withdrawalReason" to "" + ) + webTestClient.patch().isBadRequest("/api/v1/members/withdrawal", request, tokenGroup.accessToken) + } - response.isEqualTo(CommonError.REQUIRED_IMAGE.exception) + @Test + fun `회원 탈퇴시 탈퇴 사유가 255 글자를 넘을 경우 예외가 발생한다`(): Unit = runBlocking { + val (_, tokenGroup) = signUp() + val request = mapOf( + "withdrawalReason" to "".plus("a").repeat(256) + ) + webTestClient.patch().isBadRequest("/api/v1/members/withdrawal", request, tokenGroup.accessToken) } @Test - fun `base64Raw 가 비어있을 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "image" : { - "base64Raw" : "", - "extension" : "PNG", - "byteSize" : 4 - } - } - """.trimIndent() + fun `image 가 null 일 경우 예외가 발생한다`(): Unit = runBlocking { + val request = mapOf() val (_, tokenGroup) = signUp() - val response = - webTestClient - .patchForBadRequest("/api/v1/members/image", request, tokenGroup.accessToken) + webTestClient.patch().isBadRequest("/api/v1/members/image", request, tokenGroup.accessToken) + } - response.isEqualTo(CommonError.REQUIRED_BASE64RAW.exception) + @Test + fun `base64Raw 가 비어있을 경우 예외가 발생한다`(): Unit = runBlocking { + val request = mapOf( + "image" to mapOf( + "base64Raw" to "", + "extension" to "PNG", + "byteSize" to 4 + ) + ) + val (_, tokenGroup) = signUp() + webTestClient.patch().isBadRequest("/api/v1/members/image", request, tokenGroup.accessToken) } @Test fun `extension 이 JPG, PNG, JPEG 이 아닐 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "image" : { - "base64Raw" : "test", - "extension" : "JJP", - "byteSize" : 4 - } - } - """.trimIndent() + val request = mapOf( + "image" to mapOf( + "base64Raw" to "asdf", + "extension" to "asd", + "byteSize" to 4 + ) + ) val (_, tokenGroup) = signUp() - val response = - webTestClient - .patchForBadRequest("/api/v1/members/image", request, tokenGroup.accessToken) - - response.isEqualTo(CommonError.INVALID_IMAGE_EXTENSION.exception) + webTestClient.patch().isBadRequest("/api/v1/members/image", request, tokenGroup.accessToken) } @Test fun `byteSize 가 50MB가 넘을 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "image" : { - "base64Raw" : "test", - "extension" : "PNG", - "byteSize" : 51000000 - } - } - """.trimIndent() + val request = mapOf( + "image" to mapOf( + "base64Raw" to "1234", + "extension" to "PNG", + "byteSize" to 51_000_000 + ) + ) val (_, tokenGroup) = signUp() - val response = - webTestClient - .patchForBadRequest("/api/v1/members/image", request, tokenGroup.accessToken) - - response.isEqualTo(CommonError.INVALID_BYTE_SIZE.exception) + webTestClient.patch().isBadRequest("/api/v1/members/image", request, tokenGroup.accessToken) } @Test fun `realName 이 null 일 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "bank" : "bank", - "accountNumber" : "accountNumber" - } - """.trimIndent() + val request = mapOf( + "bank" to "bank", + "accountNumber" to "accountNumber" + ) val (_, tokenGroup) = signUp() - val response = - webTestClient - .patchForBadRequest("/api/v1/members/account", request, tokenGroup.accessToken) - - response.isEqualTo(MemberError.REQUIRED_REAL_NAME.exception) + webTestClient.patch().isBadRequest("/api/v1/members/account", request, tokenGroup.accessToken) } @Test fun `realName 이 비어있을 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "realName" : "", - "bank" : "bank", - "accountNumber" : "accountNumber" - } - """.trimIndent() + val request = mapOf( + "realName" to "", + "bank" to "bank", + "accountNumber" to "accountNumber" + ) val (_, tokenGroup) = signUp() - val response = - webTestClient - .patchForBadRequest("/api/v1/members/account", request, tokenGroup.accessToken) - - response.isEqualTo(MemberError.REQUIRED_REAL_NAME.exception) + webTestClient.patch().isBadRequest("/api/v1/members/account", request, tokenGroup.accessToken) } @Test fun `bank 가 null 일 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "realName" : "realName", - "accountNumber" : "accountNumber" - } - """.trimIndent() + val request = mapOf( + "realName" to "realName", + "accountNumber" to "accountNumber" + ) val (_, tokenGroup) = signUp() - val response = - webTestClient - .patchForBadRequest("/api/v1/members/account", request, tokenGroup.accessToken) - - response.isEqualTo(MemberError.REQUIRED_BANK.exception) + webTestClient.patch().isBadRequest("/api/v1/members/account", request, tokenGroup.accessToken) } @Test fun `bank 가 비어있을 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "bank" : "", - "realName" : "realName", - "accountNumber" : "accountNumber" - } - """.trimIndent() - val (_, tokenGroup) = signUp() - val response = - webTestClient - .patchForBadRequest("/api/v1/members/account", request, tokenGroup.accessToken) - - response.isEqualTo(MemberError.REQUIRED_BANK.exception) - } - - @Test - fun `accountNumber 가 null 일 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "bank" : "bank", - "realName" : "realName" - } - """.trimIndent() + val request = mapOf( + "realName" to "realName", + "bank" to "", + "accountNumber" to "accountNumber" + ) val (_, tokenGroup) = signUp() - val response = - webTestClient - .patchForBadRequest("/api/v1/members/account", request, tokenGroup.accessToken) - - response.isEqualTo(MemberError.REQUIRED_ACCOUNT_NUMBER.exception) + webTestClient.patch().isBadRequest("/api/v1/members/account", request, tokenGroup.accessToken) } @Test fun `accountNumber 가 비어있을 경우 예외가 발생한다`(): Unit = runBlocking { - val request = """ - { - "bank" : "bank", - "realName" : "realName", - "accountNumber" : "" - } - """.trimIndent() + val request = mapOf( + "realName" to "realName", + "bank" to "bank", + "accountNumber" to "" + ) val (_, tokenGroup) = signUp() - val response = - webTestClient - .patchForBadRequest("/api/v1/members/account", request, tokenGroup.accessToken) - - response.isEqualTo(MemberError.REQUIRED_ACCOUNT_NUMBER.exception) + webTestClient.patch().isBadRequest("/api/v1/members/account", request, tokenGroup.accessToken) } private suspend fun signUp(): Pair { - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val signUpRequest = SignUpRequest( oauthId = "oauthId", - oauthType = "NAVER", + oauthType = OauthType.NAVER, nickname = "nickname", favoriteCategoryIdList = listOf(categoryId) ) @@ -734,7 +776,7 @@ internal class MemberControllerTest @Autowired constructor( .responseBody!! val memberInfo = response.profile - val favoriteCategories = response.profile.favoriteCategoryList.map { it.id.toString() } + val favoriteCategories = response.profile.favoriteCategoryIdList assertThat(memberInfo.nickname).isEqualTo(modifyProfileRequest.nickname) assertThat(memberInfo.phoneNumber).isEqualTo(modifyProfileRequest.phoneNumber) diff --git a/src/test/kotlin/com/devooks/backend/notification/v1/adapter/in/http/NotificationRouterTest.kt b/src/test/kotlin/com/devooks/backend/notification/v1/adapter/in/http/NotificationRouterTest.kt index 0567402..beb3c27 100644 --- a/src/test/kotlin/com/devooks/backend/notification/v1/adapter/in/http/NotificationRouterTest.kt +++ b/src/test/kotlin/com/devooks/backend/notification/v1/adapter/in/http/NotificationRouterTest.kt @@ -88,7 +88,7 @@ internal class NotificationRouterTest @Autowired constructor( // when val notificationsResponse = webTestClient .get() - .uri("/api/v1/notifications?page=1&size=10") + .uri("/api/v1/notifications?page=1&count=10") .accept(APPLICATION_JSON) .header(AUTHORIZATION, accessToken) .exchange() @@ -125,10 +125,10 @@ internal class NotificationRouterTest @Autowired constructor( .expectBody() .returnResult() .responseBody!! - .count + .countOfUncheckedNotification // then - assertThat(count).isOne() + assertThat(count).isZero() } @Test @@ -147,9 +147,9 @@ internal class NotificationRouterTest @Autowired constructor( .expectBody() .returnResult() .responseBody!! - .count + .countOfUncheckedNotification // then - assertThat(count).isOne() + assertThat(count).isZero() } } diff --git a/src/test/kotlin/com/devooks/backend/pdf/v1/controller/PdfControllerTest.kt b/src/test/kotlin/com/devooks/backend/pdf/v1/controller/PdfControllerTest.kt index 5b5f3ca..1ff449e 100644 --- a/src/test/kotlin/com/devooks/backend/pdf/v1/controller/PdfControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/pdf/v1/controller/PdfControllerTest.kt @@ -74,13 +74,13 @@ internal class PdfControllerTest @Autowired constructor( fun `PDF 파일을 저장할 수 있다`(): Unit = runBlocking { val pdf = postUploadPdfFile() - val pdfPath = Path(pdf.pdfInfo.filePath) + val pdfPath = Path(pdf.pdfInfo.filePath.substring(1)) assertThat(pdf.uploadMemberId).isEqualTo(expectedMember.id) assertThat(pdfPath.exists()).isTrue() assertThat(pdf.pdfInfo.pageCount).isEqualTo(9) pdf.previewImageList.forEach { previewImage -> - val imagePath = Path(previewImage.imagePath) + val imagePath = Path(previewImage.imagePath.substring(1)) assertThat(imagePath.exists()).isTrue() assertThat(previewImage.pdfId).isEqualTo(pdf.id) assertThat(previewImage.previewOrder).isNotZero() @@ -102,7 +102,7 @@ internal class PdfControllerTest @Autowired constructor( .previewImageList previewImageList.forEach { - assertThat(Path(it.imagePath).exists()).isTrue() + assertThat(Path(it.imagePath.substring(1)).exists()).isTrue() assertThat(it.pdfId).isEqualTo(pdf.id) assertThat(it.previewOrder).isNotZero() } @@ -138,4 +138,4 @@ internal class PdfControllerTest @Autowired constructor( .pdf return pdf } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/devooks/backend/pdf/v1/service/PdfResolverTest.kt b/src/test/kotlin/com/devooks/backend/pdf/v1/service/PdfResolverTest.kt index fed522f..2551bff 100644 --- a/src/test/kotlin/com/devooks/backend/pdf/v1/service/PdfResolverTest.kt +++ b/src/test/kotlin/com/devooks/backend/pdf/v1/service/PdfResolverTest.kt @@ -8,6 +8,7 @@ import com.devooks.backend.pdf.v1.domain.PdfInfo import com.devooks.backend.pdf.v1.error.PdfError import java.io.File import kotlin.io.path.exists +import kotlin.io.path.pathString import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterAll @@ -43,7 +44,7 @@ internal class PdfResolverTest { val savedPdf = pdfResolver.savePdf(filePart) - assertThat(savedPdf.filePath.exists()).isEqualTo(true) + assertThat(File(savedPdf.filePath.pathString.substring(1)).exists()).isEqualTo(true) assertThat(savedPdf.pageCount).isEqualTo(9) } @@ -74,7 +75,7 @@ internal class PdfResolverTest { val savedPdf: PdfInfo = pdfResolver.savePdf(filePart) - assertThat(savedPdf.filePath.exists()).isTrue() + assertThat(File(savedPdf.filePath.pathString.substring(1)).exists()).isTrue() assertThat(savedPdf.pageCount).isEqualTo(9) val images = pdfResolver.savePreviewImages(savedPdf) @@ -88,4 +89,4 @@ internal class PdfResolverTest { private fun getFilePart(file: File): FilePart { return CustomFilePart(file) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/devooks/backend/review/v1/controller/ReviewCommentControllerTest.kt b/src/test/kotlin/com/devooks/backend/review/v1/controller/ReviewCommentControllerTest.kt index 8c40eca..5c0aa16 100644 --- a/src/test/kotlin/com/devooks/backend/review/v1/controller/ReviewCommentControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/review/v1/controller/ReviewCommentControllerTest.kt @@ -5,7 +5,9 @@ import com.devooks.backend.BackendApplication.Companion.createDirectories import com.devooks.backend.auth.v1.domain.AccessToken import com.devooks.backend.auth.v1.service.TokenService import com.devooks.backend.category.v1.repository.CategoryRepository +import com.devooks.backend.common.domain.ImageExtension import com.devooks.backend.common.dto.ImageDto +import com.devooks.backend.common.dto.PageResponse import com.devooks.backend.config.IntegrationTest import com.devooks.backend.ebook.v1.dto.EbookImageDto import com.devooks.backend.ebook.v1.dto.request.CreateEbookRequest @@ -30,11 +32,10 @@ import com.devooks.backend.review.v1.dto.CreateReviewCommentRequest import com.devooks.backend.review.v1.dto.CreateReviewCommentResponse import com.devooks.backend.review.v1.dto.CreateReviewRequest import com.devooks.backend.review.v1.dto.CreateReviewResponse -import com.devooks.backend.review.v1.dto.GetReviewCommentsResponse import com.devooks.backend.review.v1.dto.ModifyReviewCommentRequest import com.devooks.backend.review.v1.dto.ModifyReviewCommentResponse -import com.devooks.backend.review.v1.dto.ReviewCommentDto -import com.devooks.backend.review.v1.dto.ReviewDto +import com.devooks.backend.review.v1.dto.ReviewCommentView +import com.devooks.backend.review.v1.dto.ReviewView import com.devooks.backend.review.v1.repository.ReviewCommentRepository import com.devooks.backend.review.v1.repository.ReviewRepository import com.devooks.backend.transaciton.v1.domain.PaymentMethod @@ -123,7 +124,7 @@ internal class ReviewCommentControllerTest @Autowired constructor( fun `리뷰 댓글을 작성할 수 있다`(): Unit = runBlocking { val review = postCreateReview() val accessToken = tokenService.createTokenGroup(expectedMember2).accessToken - val request = CreateReviewCommentRequest(review.id.toString(), review.content) + val request = CreateReviewCommentRequest(review.id, review.content) val reviewComment = webTestClient .post() @@ -139,7 +140,7 @@ internal class ReviewCommentControllerTest @Autowired constructor( .responseBody!! .reviewComment - assertThat(reviewComment.reviewId.toString()).isEqualTo(request.reviewId) + assertThat(reviewComment.reviewId).isEqualTo(request.reviewId) assertThat(reviewComment.content).isEqualTo(request.content) delay(100) @@ -156,7 +157,7 @@ internal class ReviewCommentControllerTest @Autowired constructor( fun `리뷰 댓글 작성시 리뷰가 존재하지 않을 경우 예외가 발생한다`(): Unit = runBlocking { val review = postCreateReview() val accessToken = tokenService.createTokenGroup(expectedMember2).accessToken - val request = CreateReviewCommentRequest(UUID.randomUUID().toString(), review.content) + val request = CreateReviewCommentRequest(UUID.randomUUID(), review.content) webTestClient .post() @@ -269,10 +270,10 @@ internal class ReviewCommentControllerTest @Autowired constructor( .expectStatus().isNotFound } - private suspend fun postCreateReviewComment(): Pair { + private suspend fun postCreateReviewComment(): Pair { val review = postCreateReview() val accessToken = tokenService.createTokenGroup(expectedMember2).accessToken - val createReviewCommentRequest = CreateReviewCommentRequest(review.id.toString(), review.content) + val createReviewCommentRequest = CreateReviewCommentRequest(review.id, review.content) val reviewComment = webTestClient .post() @@ -294,20 +295,22 @@ internal class ReviewCommentControllerTest @Autowired constructor( .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .reviewComments[0] - return Pair(reviewComment, response) + + assertThat(response.pageable.totalElements).isEqualTo(1) + assertThat(response.pageable.totalPages).isEqualTo(1) + return Pair(reviewComment, response.data[0]) } - private suspend fun postCreateReview(): ReviewDto { + private suspend fun postCreateReview(): ReviewView { val (createEbookResponse, accessToken) = postCreateEbookAndCreateTransaction() val createReviewRequest = CreateReviewRequest( - ebookId = createEbookResponse.ebook.id.toString(), - rating = "5", + ebookId = createEbookResponse.ebook.id, + rating = 5, content = "content" ) val createReviewResponse = webTestClient @@ -328,8 +331,8 @@ internal class ReviewCommentControllerTest @Autowired constructor( private suspend fun postCreateEbookAndCreateTransaction(): Pair { val (_, createEbookResponse) = postCreateEbook() val createTransactionRequest = CreateTransactionRequest( - ebookId = createEbookResponse.ebook.id.toString(), - paymentMethod = PaymentMethod.CREDIT_CARD.name, + ebookId = createEbookResponse.ebook.id, + paymentMethod = PaymentMethod.CREDIT_CARD, price = createEbookResponse.ebook.price ) @@ -362,13 +365,13 @@ internal class ReviewCommentControllerTest @Autowired constructor( val mainImage = postSaveMainImage(imageBase64Raw, imagePath, accessToken) val descriptionImageList = postSaveDescriptionImages(imageBase64Raw, imagePath, accessToken) - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = CreateEbookRequest( - pdfId = pdf.id.toString(), + pdfId = pdf.id, title = "title", relatedCategoryIdList = listOf(categoryId), - mainImageId = mainImage.id.toString(), - descriptionImageIdList = descriptionImageList.map { it.id.toString() }, + mainImageId = mainImage.id, + descriptionImageIdList = descriptionImageList.map { it.id }, 10000, "introduction", "tableOfContent" @@ -389,7 +392,7 @@ internal class ReviewCommentControllerTest @Autowired constructor( } fun postSaveDescriptionImages( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, ): List { @@ -397,15 +400,13 @@ internal class ReviewCommentControllerTest @Autowired constructor( imageList = listOf( ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 1 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 2 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ) ) @@ -427,15 +428,15 @@ internal class ReviewCommentControllerTest @Autowired constructor( } private fun postSaveMainImage( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, - ): SaveMainImageResponse.MainImageDto { + ): EbookImageDto { val saveMainImageRequest = SaveMainImageRequest( - SaveMainImageRequest.MainImageDto( + ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ) ) diff --git a/src/test/kotlin/com/devooks/backend/review/v1/controller/ReviewControllerTest.kt b/src/test/kotlin/com/devooks/backend/review/v1/controller/ReviewControllerTest.kt index 767901e..fc7f382 100644 --- a/src/test/kotlin/com/devooks/backend/review/v1/controller/ReviewControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/review/v1/controller/ReviewControllerTest.kt @@ -5,7 +5,9 @@ import com.devooks.backend.BackendApplication.Companion.createDirectories import com.devooks.backend.auth.v1.domain.AccessToken import com.devooks.backend.auth.v1.service.TokenService import com.devooks.backend.category.v1.repository.CategoryRepository +import com.devooks.backend.common.domain.ImageExtension import com.devooks.backend.common.dto.ImageDto +import com.devooks.backend.common.dto.PageResponse import com.devooks.backend.config.IntegrationTest import com.devooks.backend.ebook.v1.dto.EbookImageDto import com.devooks.backend.ebook.v1.dto.request.CreateEbookRequest @@ -28,10 +30,9 @@ import com.devooks.backend.pdf.v1.repository.PdfRepository import com.devooks.backend.pdf.v1.repository.PreviewImageRepository import com.devooks.backend.review.v1.dto.CreateReviewRequest import com.devooks.backend.review.v1.dto.CreateReviewResponse -import com.devooks.backend.review.v1.dto.GetReviewsResponse import com.devooks.backend.review.v1.dto.ModifyReviewRequest import com.devooks.backend.review.v1.dto.ModifyReviewResponse -import com.devooks.backend.review.v1.dto.ReviewDto +import com.devooks.backend.review.v1.dto.ReviewView import com.devooks.backend.review.v1.repository.ReviewRepository import com.devooks.backend.transaciton.v1.domain.PaymentMethod import com.devooks.backend.transaciton.v1.dto.CreateTransactionRequest @@ -119,8 +120,8 @@ internal class ReviewControllerTest @Autowired constructor( val createReviewRequest = CreateReviewRequest( - ebookId = createEbookResponse.ebook.id.toString(), - rating = "5", + ebookId = createEbookResponse.ebook.id, + rating = 5, content = "content" ) val createReviewResponse = webTestClient @@ -137,9 +138,9 @@ internal class ReviewControllerTest @Autowired constructor( .responseBody!! val review = createReviewResponse.review - assertThat(review.rating.toString()).isEqualTo(createReviewRequest.rating) + assertThat(review.rating).isEqualTo(createReviewRequest.rating) assertThat(review.content).isEqualTo(createReviewRequest.content) - assertThat(review.ebookId.toString()).isEqualTo(createReviewRequest.ebookId) + assertThat(review.ebookId).isEqualTo(createReviewRequest.ebookId) assertThat(review.writerMemberId).isEqualTo(expectedMember2.id) delay(100) @@ -162,35 +163,13 @@ internal class ReviewControllerTest @Autowired constructor( .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - val review = getReviewsResponse.reviews[0] - assertThat(review.id).isEqualTo(createReviewResponse.id) - assertThat(review.ebookId).isEqualTo(createReviewResponse.ebookId) - assertThat(review.content).isEqualTo(createReviewResponse.content) - assertThat(review.rating).isEqualTo(createReviewResponse.rating) - assertThat(review.writerMemberId).isEqualTo(createReviewResponse.writerMemberId) - assertThat(review.writtenDate.toEpochMilli()).isEqualTo(createReviewResponse.writtenDate.toEpochMilli()) - assertThat(review.modifiedDate.toEpochMilli()).isEqualTo(createReviewResponse.modifiedDate.toEpochMilli()) - } - - @Test - fun `회원에 대한 리뷰를 조회할 수 있다`(): Unit = runBlocking { - val createReviewResponse = postCreateReview() - - val getReviewsResponse = webTestClient - .get() - .uri("/api/v1/reviews?page=1&count=10&memberId=${expectedMember1.id}") - .accept(APPLICATION_JSON) - .exchange() - .expectStatus().isOk - .expectBody() - .returnResult() - .responseBody!! - - val review = getReviewsResponse.reviews[0] + val review = getReviewsResponse.data[0] + assertThat(getReviewsResponse.pageable.totalPages).isEqualTo(1) + assertThat(getReviewsResponse.pageable.totalElements).isEqualTo(1) assertThat(review.id).isEqualTo(createReviewResponse.id) assertThat(review.ebookId).isEqualTo(createReviewResponse.ebookId) assertThat(review.content).isEqualTo(createReviewResponse.content) @@ -205,7 +184,7 @@ internal class ReviewControllerTest @Autowired constructor( val createReviewResponse = postCreateReview() val accessToken = tokenService.createTokenGroup(expectedMember2).accessToken val request = ModifyReviewRequest( - rating = "5", + rating = 5, content = "content" ) @@ -224,7 +203,7 @@ internal class ReviewControllerTest @Autowired constructor( val review = modifyReviewsResponse.review assertThat(review.content).isEqualTo(request.content) - assertThat(review.rating.toString()).isEqualTo(request.rating) + assertThat(review.rating).isEqualTo(request.rating) } @Test @@ -260,7 +239,7 @@ internal class ReviewControllerTest @Autowired constructor( postCreateReview() val accessToken = tokenService.createTokenGroup(expectedMember2).accessToken val request = ModifyReviewRequest( - rating = "5", + rating = 5, content = "content" ) @@ -280,7 +259,7 @@ internal class ReviewControllerTest @Autowired constructor( val createReviewResponse = postCreateReview() val accessToken = tokenService.createTokenGroup(expectedMember1).accessToken val request = ModifyReviewRequest( - rating = "5", + rating = 5, content = "content" ) @@ -301,29 +280,8 @@ internal class ReviewControllerTest @Autowired constructor( val createReviewRequest = CreateReviewRequest( - ebookId = createEbookResponse.ebook.id.toString(), - rating = "6", - content = "content" - ) - webTestClient - .post() - .uri("/api/v1/reviews") - .header(AUTHORIZATION, "Bearer $accessToken") - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .bodyValue(createReviewRequest) - .exchange() - .expectStatus().isBadRequest - } - - @Test - fun `리뷰를 작성시 평점이 숫자가 아닐경우 예외가 발생한다`(): Unit = runBlocking { - val (createEbookResponse, accessToken) = postCreateEbookAndCreateTransaction() - - val createReviewRequest = - CreateReviewRequest( - ebookId = createEbookResponse.ebook.id.toString(), - rating = "a", + ebookId = createEbookResponse.ebook.id, + rating = 6, content = "content" ) webTestClient @@ -344,8 +302,8 @@ internal class ReviewControllerTest @Autowired constructor( val createReviewRequest = CreateReviewRequest( - ebookId = createEbookResponse.ebook.id.toString(), - rating = "5", + ebookId = createEbookResponse.ebook.id, + rating = 5, content = "content" ) webTestClient @@ -365,8 +323,8 @@ internal class ReviewControllerTest @Autowired constructor( val createReviewRequest = CreateReviewRequest( - ebookId = UUID.randomUUID().toString(), - rating = "5", + ebookId = UUID.randomUUID(), + rating = 5, content = "content" ) webTestClient @@ -386,8 +344,8 @@ internal class ReviewControllerTest @Autowired constructor( val createReviewRequest = CreateReviewRequest( - ebookId = createEbookResponse.ebook.id.toString(), - rating = "5", + ebookId = createEbookResponse.ebook.id, + rating = 5, content = "content" ) webTestClient @@ -411,13 +369,13 @@ internal class ReviewControllerTest @Autowired constructor( .expectStatus().isEqualTo(CONFLICT.code()) } - private suspend fun postCreateReview(): ReviewDto { + private suspend fun postCreateReview(): ReviewView { val (createEbookResponse, accessToken) = postCreateEbookAndCreateTransaction() val createReviewRequest = CreateReviewRequest( - ebookId = createEbookResponse.ebook.id.toString(), - rating = "5", + ebookId = createEbookResponse.ebook.id, + rating = 5, content = "content" ) val createReviewResponse = webTestClient @@ -438,8 +396,8 @@ internal class ReviewControllerTest @Autowired constructor( private suspend fun postCreateEbookAndCreateTransaction(): Pair { val (_, createEbookResponse) = postCreateEbook() val createTransactionRequest = CreateTransactionRequest( - ebookId = createEbookResponse.ebook.id.toString(), - paymentMethod = PaymentMethod.CREDIT_CARD.name, + ebookId = createEbookResponse.ebook.id, + paymentMethod = PaymentMethod.CREDIT_CARD, price = createEbookResponse.ebook.price ) @@ -471,14 +429,14 @@ internal class ReviewControllerTest @Autowired constructor( val mainImage = postSaveMainImage(imageBase64Raw, imagePath, accessToken) val descriptionImageList = postSaveDescriptionImages(imageBase64Raw, imagePath, accessToken) - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = CreateEbookRequest( - pdfId = pdf.id.toString(), + pdfId = pdf.id, title = "title", relatedCategoryIdList = listOf(categoryId), - mainImageId = mainImage.id.toString(), - descriptionImageIdList = descriptionImageList.map { it.id.toString() }, + mainImageId = mainImage.id, + descriptionImageIdList = descriptionImageList.map { it.id }, 10000, "introduction", "tableOfContent" @@ -499,7 +457,7 @@ internal class ReviewControllerTest @Autowired constructor( } fun postSaveDescriptionImages( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, ): List { @@ -507,15 +465,13 @@ internal class ReviewControllerTest @Autowired constructor( imageList = listOf( ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 1 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 2 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ) ) @@ -537,15 +493,15 @@ internal class ReviewControllerTest @Autowired constructor( } private fun postSaveMainImage( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, - ): SaveMainImageResponse.MainImageDto { + ): EbookImageDto { val saveMainImageRequest = SaveMainImageRequest( - SaveMainImageRequest.MainImageDto( + ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ) ) diff --git a/src/test/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryControllerTest.kt b/src/test/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryControllerTest.kt index 5b2be02..c8d6c02 100644 --- a/src/test/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryControllerTest.kt @@ -4,7 +4,9 @@ import com.devooks.backend.BackendApplication.Companion.STATIC_ROOT_PATH import com.devooks.backend.BackendApplication.Companion.createDirectories import com.devooks.backend.auth.v1.domain.AccessToken import com.devooks.backend.auth.v1.service.TokenService +import com.devooks.backend.common.domain.ImageExtension import com.devooks.backend.common.dto.ImageDto +import com.devooks.backend.common.dto.PageResponse import com.devooks.backend.config.IntegrationTest import com.devooks.backend.member.v1.domain.Member import com.devooks.backend.member.v1.domain.Member.Companion.toDomain @@ -12,11 +14,11 @@ import com.devooks.backend.member.v1.entity.MemberEntity import com.devooks.backend.member.v1.repository.MemberRepository import com.devooks.backend.service.v1.domain.InquiryProcessingStatus import com.devooks.backend.service.v1.dto.ServiceInquiryImageDto +import com.devooks.backend.service.v1.dto.ServiceInquiryView import com.devooks.backend.service.v1.dto.request.CreateServiceInquiryRequest import com.devooks.backend.service.v1.dto.request.ModifyServiceInquiryRequest import com.devooks.backend.service.v1.dto.request.SaveServiceInquiryImagesRequest import com.devooks.backend.service.v1.dto.response.CreateServiceInquiryResponse -import com.devooks.backend.service.v1.dto.response.GetServiceInquiriesResponse import com.devooks.backend.service.v1.dto.response.ModifyServiceInquiryResponse import com.devooks.backend.service.v1.dto.response.SaveServiceInquiryImagesResponse import com.devooks.backend.service.v1.repository.ServiceInquiryCrudRepository @@ -85,7 +87,7 @@ internal class ServiceInquiryControllerTest @Autowired constructor( val createServiceInquiryRequest = CreateServiceInquiryRequest( title = "title", content = "content", - imageIdList = imageList.map { it.id.toString() } + imageIdList = imageList.map { it.id } ) val serviceInquiry = webTestClient @@ -110,14 +112,14 @@ internal class ServiceInquiryControllerTest @Autowired constructor( } @Test - fun `서비스 문의 생성시 이미지가 자신이 업로드한 경우가 아닐 경우 예외가 발생한다`(): Unit = runBlocking { + fun `서비스 문의 생성시 사진이 자신이 업로드한 경우가 아닐 경우 예외가 발생한다`(): Unit = runBlocking { val (_, imageList) = postSaveServiceInquiryImages() val accessToken = tokenService.createTokenGroup(expectedMember2).accessToken val createServiceInquiryRequest = CreateServiceInquiryRequest( title = "title", content = "content", - imageIdList = imageList.map { it.id.toString() } + imageIdList = imageList.map { it.id } ) webTestClient @@ -138,7 +140,7 @@ internal class ServiceInquiryControllerTest @Autowired constructor( val createServiceInquiryRequest = CreateServiceInquiryRequest( title = "title", content = "content", - imageIdList = imageList.map { it.id.toString() } + imageIdList = imageList.map { it.id } ) val serviceInquiry = webTestClient @@ -155,26 +157,29 @@ internal class ServiceInquiryControllerTest @Autowired constructor( .responseBody!! .serviceInquiry - val serviceInquiryDto = webTestClient + val pageServiceInquiry = webTestClient .get() .uri("/api/v1/service-inquiries?page=1&count=10") .accept(APPLICATION_JSON) .header(AUTHORIZATION, "Bearer $accessToken") .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .serviceInquiryList[0] - - assertThat(serviceInquiryDto.id).isEqualTo(serviceInquiry.id) - assertThat(serviceInquiryDto.title).isEqualTo(serviceInquiry.title) - assertThat(serviceInquiryDto.imageList).containsAll(serviceInquiry.imageList) - assertThat(serviceInquiryDto.content).isEqualTo(serviceInquiry.content) - assertThat(serviceInquiryDto.inquiryProcessingStatus).isEqualTo(serviceInquiry.inquiryProcessingStatus) - assertThat(serviceInquiryDto.createdDate.toEpochMilli()).isEqualTo(serviceInquiry.createdDate.toEpochMilli()) - assertThat(serviceInquiryDto.modifiedDate.toEpochMilli()).isEqualTo(serviceInquiry.modifiedDate.toEpochMilli()) - assertThat(serviceInquiryDto.writerMemberId).isEqualTo(serviceInquiry.writerMemberId) + + val serviceInquiryView = pageServiceInquiry.data[0] + + assertThat(pageServiceInquiry.pageable.totalPages).isEqualTo(1) + assertThat(pageServiceInquiry.pageable.totalElements).isEqualTo(1) + assertThat(serviceInquiryView.id).isEqualTo(serviceInquiry.id) + assertThat(serviceInquiryView.title).isEqualTo(serviceInquiry.title) + assertThat(serviceInquiryView.imageList).containsAll(serviceInquiry.imageList) + assertThat(serviceInquiryView.content).isEqualTo(serviceInquiry.content) + assertThat(serviceInquiryView.inquiryProcessingStatus).isEqualTo(serviceInquiry.inquiryProcessingStatus) + assertThat(serviceInquiryView.createdDate.toEpochMilli()).isEqualTo(serviceInquiry.createdDate.toEpochMilli()) + assertThat(serviceInquiryView.modifiedDate.toEpochMilli()).isEqualTo(serviceInquiry.modifiedDate.toEpochMilli()) + assertThat(serviceInquiryView.writerMemberId).isEqualTo(serviceInquiry.writerMemberId) } @Test @@ -184,7 +189,7 @@ internal class ServiceInquiryControllerTest @Autowired constructor( val createServiceInquiryRequest = CreateServiceInquiryRequest( title = "title", content = "content", - imageIdList = imageList1.map { it.id.toString() } + imageIdList = imageList1.map { it.id } ) val createdServiceInquiry = webTestClient @@ -203,16 +208,9 @@ internal class ServiceInquiryControllerTest @Autowired constructor( val (_, imageList2) = postSaveServiceInquiryImages() val modifyServiceInquiryRequest = ModifyServiceInquiryRequest( - serviceInquiry = ModifyServiceInquiryRequest.ServiceInquiry( - title = "title2", - content = "content2", - imageIdList = listOf(imageList1.map { it.id.toString() }.first(), imageList2.first().id.toString()) - ), - isChanged = ModifyServiceInquiryRequest.IsChanged( - title = true, - content = true, - imageIdList = true - ) + title = "title2", + content = "content2", + imageIdList = listOf(imageList1.map { it.id }.first(), imageList2.first().id) ) val updatedServiceInquiry = webTestClient @@ -230,9 +228,9 @@ internal class ServiceInquiryControllerTest @Autowired constructor( .serviceInquiry assertThat(updatedServiceInquiry.id).isEqualTo(createdServiceInquiry.id) - assertThat(updatedServiceInquiry.title).isEqualTo(modifyServiceInquiryRequest.serviceInquiry!!.title) - assertThat(updatedServiceInquiry.content).isEqualTo(modifyServiceInquiryRequest.serviceInquiry!!.content) - assertThat(updatedServiceInquiry.imageList.map { it.id.toString() }).containsAll(modifyServiceInquiryRequest.serviceInquiry!!.imageIdList) + assertThat(updatedServiceInquiry.title).isEqualTo(modifyServiceInquiryRequest.title) + assertThat(updatedServiceInquiry.content).isEqualTo(modifyServiceInquiryRequest.content) + assertThat(updatedServiceInquiry.imageList.map { it.id }).containsAll(modifyServiceInquiryRequest.imageIdList) assertThat(updatedServiceInquiry.writerMemberId).isEqualTo(createdServiceInquiry.writerMemberId) assertThat(updatedServiceInquiry.inquiryProcessingStatus).isEqualTo(createdServiceInquiry.inquiryProcessingStatus) } @@ -245,7 +243,7 @@ internal class ServiceInquiryControllerTest @Autowired constructor( val createServiceInquiryRequest = CreateServiceInquiryRequest( title = "title", content = "content", - imageIdList = imageList1.map { it.id.toString() } + imageIdList = imageList1.map { it.id } ) val createdServiceInquiry = webTestClient @@ -264,16 +262,9 @@ internal class ServiceInquiryControllerTest @Autowired constructor( val (_, imageList2) = postSaveServiceInquiryImages() val modifyServiceInquiryRequest = ModifyServiceInquiryRequest( - serviceInquiry = ModifyServiceInquiryRequest.ServiceInquiry( - title = "title2", - content = "content2", - imageIdList = listOf(imageList1.map { it.id.toString() }.first(), imageList2.first().id.toString()) - ), - isChanged = ModifyServiceInquiryRequest.IsChanged( - title = true, - content = true, - imageIdList = true - ) + title = "title2", + content = "content2", + imageIdList = listOf(imageList1.map { it.id }.first(), imageList2.first().id) ) webTestClient @@ -292,14 +283,9 @@ internal class ServiceInquiryControllerTest @Autowired constructor( val (accessToken, _) = postSaveServiceInquiryImages() val modifyServiceInquiryRequest = ModifyServiceInquiryRequest( - serviceInquiry = ModifyServiceInquiryRequest.ServiceInquiry( - title = "title2", - content = "content2", - ), - isChanged = ModifyServiceInquiryRequest.IsChanged( - title = true, - content = true, - ) + title = "title2", + content = "content2", + imageIdList = null ) webTestClient @@ -324,15 +310,13 @@ internal class ServiceInquiryControllerTest @Autowired constructor( imageList = listOf( ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 1 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 2 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ) ) diff --git a/src/test/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryImagesControllerTest.kt b/src/test/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryImagesControllerTest.kt index 62201fb..3ee02c5 100644 --- a/src/test/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryImagesControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/service/v1/controller/ServiceInquiryImagesControllerTest.kt @@ -3,6 +3,7 @@ package com.devooks.backend.service.v1.controller import com.devooks.backend.BackendApplication.Companion.STATIC_ROOT_PATH import com.devooks.backend.BackendApplication.Companion.createDirectories import com.devooks.backend.auth.v1.service.TokenService +import com.devooks.backend.common.domain.ImageExtension import com.devooks.backend.common.dto.ImageDto import com.devooks.backend.config.IntegrationTest import com.devooks.backend.member.v1.domain.Member @@ -77,15 +78,13 @@ internal class ServiceInquiryImagesControllerTest @Autowired constructor( imageList = listOf( ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 1 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 2 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ) ) @@ -105,9 +104,8 @@ internal class ServiceInquiryImagesControllerTest @Autowired constructor( .imageList imageList.forEachIndexed { index, image -> - val expected = request.imageList!![index] - assertThat(image.order).isEqualTo(expected.order) - assertThat(File(image.imagePath).exists()).isTrue() + assertThat(image.order).isEqualTo(index) + assertThat(File(image.imagePath.substring(1)).exists()).isTrue() } } } diff --git a/src/test/kotlin/com/devooks/backend/service/v1/repository/ServiceInquiryQueryRepositoryTest.kt b/src/test/kotlin/com/devooks/backend/service/v1/repository/ServiceInquiryQueryRepositoryTest.kt index e4655cd..43befbc 100644 --- a/src/test/kotlin/com/devooks/backend/service/v1/repository/ServiceInquiryQueryRepositoryTest.kt +++ b/src/test/kotlin/com/devooks/backend/service/v1/repository/ServiceInquiryQueryRepositoryTest.kt @@ -59,7 +59,7 @@ internal class ServiceInquiryQueryRepositoryTest @Autowired constructor( @Test fun `서비스 문의를 조회할 수 있다`() = runTest { // given - val command = GetServiceInquiriesCommand("1", "10", expectedMember.id!!) + val command = GetServiceInquiriesCommand(1, 10, expectedMember.id!!) // when val foundInquiry = serviceInquiryQueryRepository.findBy(command).toList() diff --git a/src/test/kotlin/com/devooks/backend/transaciton/v1/controller/TransactionControllerTest.kt b/src/test/kotlin/com/devooks/backend/transaciton/v1/controller/TransactionControllerTest.kt index c6fd6bf..0e45653 100644 --- a/src/test/kotlin/com/devooks/backend/transaciton/v1/controller/TransactionControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/transaciton/v1/controller/TransactionControllerTest.kt @@ -5,6 +5,7 @@ import com.devooks.backend.BackendApplication.Companion.createDirectories import com.devooks.backend.auth.v1.domain.AccessToken import com.devooks.backend.auth.v1.service.TokenService import com.devooks.backend.category.v1.repository.CategoryRepository +import com.devooks.backend.common.domain.ImageExtension import com.devooks.backend.common.dto.ImageDto import com.devooks.backend.config.IntegrationTest import com.devooks.backend.ebook.v1.dto.EbookImageDto @@ -105,8 +106,8 @@ internal class TransactionControllerTest @Autowired constructor( fun `전자책을 구매할 수 있다`(): Unit = runBlocking { val (_, createEbookResponse) = postCreateEbook() val createTransactionRequest = CreateTransactionRequest( - ebookId = createEbookResponse.ebook.id.toString(), - paymentMethod = PaymentMethod.CREDIT_CARD.name, + ebookId = createEbookResponse.ebook.id, + paymentMethod = PaymentMethod.CREDIT_CARD, price = createEbookResponse.ebook.price ) @@ -134,8 +135,8 @@ internal class TransactionControllerTest @Autowired constructor( @Test fun `전자책을 구매할 때 전자책이 존재하지 않을 경우 예외가 발생한다`(): Unit = runBlocking { val createTransactionRequest = CreateTransactionRequest( - ebookId = UUID.randomUUID().toString(), - paymentMethod = PaymentMethod.CREDIT_CARD.name, + ebookId = UUID.randomUUID(), + paymentMethod = PaymentMethod.CREDIT_CARD, price = 1000 ) @@ -157,8 +158,8 @@ internal class TransactionControllerTest @Autowired constructor( fun `전자책을 구매할 때 가격이 다를 경우 예외가 발생한다`(): Unit = runBlocking { val (_, createEbookResponse) = postCreateEbook() val createTransactionRequest = CreateTransactionRequest( - ebookId = createEbookResponse.ebook.id.toString(), - paymentMethod = PaymentMethod.CREDIT_CARD.name, + ebookId = createEbookResponse.ebook.id, + paymentMethod = PaymentMethod.CREDIT_CARD, price = 1000 ) @@ -180,8 +181,8 @@ internal class TransactionControllerTest @Autowired constructor( fun `전자책을 구매할 때 자신의 책을 구매할 경우 예외가 발생한다`(): Unit = runBlocking { val (_, createEbookResponse) = postCreateEbook() val createTransactionRequest = CreateTransactionRequest( - ebookId = createEbookResponse.ebook.id.toString(), - paymentMethod = PaymentMethod.CREDIT_CARD.name, + ebookId = createEbookResponse.ebook.id, + paymentMethod = PaymentMethod.CREDIT_CARD, price = createEbookResponse.ebook.price ) @@ -203,8 +204,8 @@ internal class TransactionControllerTest @Autowired constructor( fun `전자책을 구매할 때 이미 구매했을 경우 예외가 발생한다`(): Unit = runBlocking { val (_, createEbookResponse) = postCreateEbook() val createTransactionRequest = CreateTransactionRequest( - ebookId = createEbookResponse.ebook.id.toString(), - paymentMethod = PaymentMethod.CREDIT_CARD.name, + ebookId = createEbookResponse.ebook.id, + paymentMethod = PaymentMethod.CREDIT_CARD, price = createEbookResponse.ebook.price ) @@ -306,8 +307,8 @@ internal class TransactionControllerTest @Autowired constructor( private suspend fun TransactionControllerTest.postCreateTransaction(): Triple { val (_, createEbookResponse) = postCreateEbook() val createTransactionRequest = CreateTransactionRequest( - ebookId = createEbookResponse.ebook.id.toString(), - paymentMethod = PaymentMethod.CREDIT_CARD.name, + ebookId = createEbookResponse.ebook.id, + paymentMethod = PaymentMethod.CREDIT_CARD, price = createEbookResponse.ebook.price ) @@ -339,14 +340,14 @@ internal class TransactionControllerTest @Autowired constructor( val mainImage = postSaveMainImage(imageBase64Raw, imagePath, accessToken) val descriptionImageList = postSaveDescriptionImages(imageBase64Raw, imagePath, accessToken) - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = CreateEbookRequest( - pdfId = pdf.id.toString(), + pdfId = pdf.id, title = "title", relatedCategoryIdList = listOf(categoryId), - mainImageId = mainImage.id.toString(), - descriptionImageIdList = descriptionImageList.map { it.id.toString() }, + mainImageId = mainImage.id, + descriptionImageIdList = descriptionImageList.map { it.id }, 10000, "introduction", "tableOfContent" @@ -367,7 +368,7 @@ internal class TransactionControllerTest @Autowired constructor( } fun postSaveDescriptionImages( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, ): List { @@ -375,15 +376,13 @@ internal class TransactionControllerTest @Autowired constructor( imageList = listOf( ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 1 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 2 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ) ) @@ -405,15 +404,15 @@ internal class TransactionControllerTest @Autowired constructor( } private fun postSaveMainImage( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, - ): SaveMainImageResponse.MainImageDto { + ): EbookImageDto { val saveMainImageRequest = SaveMainImageRequest( - SaveMainImageRequest.MainImageDto( + ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ) ) diff --git a/src/test/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistControllerTest.kt b/src/test/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistControllerTest.kt index d37cb70..69d76ca 100644 --- a/src/test/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistControllerTest.kt +++ b/src/test/kotlin/com/devooks/backend/wishlist/v1/controller/WishlistControllerTest.kt @@ -5,9 +5,12 @@ import com.devooks.backend.BackendApplication.Companion.createDirectories import com.devooks.backend.auth.v1.domain.AccessToken import com.devooks.backend.auth.v1.service.TokenService import com.devooks.backend.category.v1.repository.CategoryRepository +import com.devooks.backend.common.domain.ImageExtension import com.devooks.backend.common.dto.ImageDto +import com.devooks.backend.common.dto.PageResponse import com.devooks.backend.config.IntegrationTest import com.devooks.backend.ebook.v1.dto.EbookImageDto +import com.devooks.backend.ebook.v1.dto.EbookView import com.devooks.backend.ebook.v1.dto.request.CreateEbookRequest import com.devooks.backend.ebook.v1.dto.request.SaveDescriptionImagesRequest import com.devooks.backend.ebook.v1.dto.request.SaveMainImageRequest @@ -26,7 +29,6 @@ import com.devooks.backend.pdf.v1.repository.PdfRepository import com.devooks.backend.pdf.v1.repository.PreviewImageRepository import com.devooks.backend.wishlist.v1.dto.CreateWishlistRequest import com.devooks.backend.wishlist.v1.dto.CreateWishlistResponse -import com.devooks.backend.wishlist.v1.dto.GetWishlistResponse import com.devooks.backend.wishlist.v1.repository.WishlistCrudRepository import io.netty.handler.codec.http.HttpResponseStatus.CONFLICT import java.io.File @@ -111,7 +113,7 @@ internal class WishlistControllerTest @Autowired constructor( fun `존재하지 않는 전자책을 찜할 경우 예외가 발생한다`(): Unit = runBlocking { val accessToken = tokenService.createTokenGroup(expectedMember).accessToken val request = CreateWishlistRequest( - ebookId = UUID.randomUUID().toString() + ebookId = UUID.randomUUID() ) webTestClient @@ -130,7 +132,7 @@ internal class WishlistControllerTest @Autowired constructor( val (accessToken, createEbookResponse) = postCreateEbook() val request = CreateWishlistRequest( - ebookId = createEbookResponse.ebook.id.toString() + ebookId = createEbookResponse.ebook.id ) webTestClient @@ -160,21 +162,24 @@ internal class WishlistControllerTest @Autowired constructor( val response = postCreateWishlist(createEbookResponse, accessToken) - val wishlist = webTestClient + val pageEbookList = webTestClient .get() .uri("/api/v1/wishlist?page=1&count=10") .header(AUTHORIZATION, "Bearer $accessToken") .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .wishlist - assertThat(wishlist[0].id).isEqualTo(response.wishlistId) - assertThat(wishlist[0].ebookId).isEqualTo(response.ebookId) - assertThat(wishlist[0].memberId).isEqualTo(response.memberId) + val ebookList = pageEbookList.data + + assertThat(pageEbookList.pageable.totalPages).isEqualTo(1) + assertThat(pageEbookList.pageable.totalElements).isEqualTo(1) + assertThat(ebookList.size).isEqualTo(1) + assertThat(ebookList[0].id).isEqualTo(response.ebookId) + assertThat(ebookList[0].wishlistId).isEqualTo(response.wishlistId) } @Test @@ -183,7 +188,7 @@ internal class WishlistControllerTest @Autowired constructor( val response = postCreateWishlist(createEbookResponse, accessToken) - val wishlist = webTestClient + val pageEbookList = webTestClient .get() .uri( "/api/v1/wishlist?page=1&count=10&" + @@ -194,14 +199,17 @@ internal class WishlistControllerTest @Autowired constructor( .accept(APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody() + .expectBody>() .returnResult() .responseBody!! - .wishlist - assertThat(wishlist[0].id).isEqualTo(response.wishlistId) - assertThat(wishlist[0].ebookId).isEqualTo(response.ebookId) - assertThat(wishlist[0].memberId).isEqualTo(response.memberId) + val ebookList = pageEbookList.data + + assertThat(pageEbookList.pageable.totalPages).isEqualTo(1) + assertThat(pageEbookList.pageable.totalElements).isEqualTo(1) + assertThat(ebookList.size).isEqualTo(1) + assertThat(ebookList[0].id).isEqualTo(response.ebookId) + assertThat(ebookList[0].wishlistId).isEqualTo(response.wishlistId) } @Test @@ -255,7 +263,7 @@ internal class WishlistControllerTest @Autowired constructor( webTestClient .delete() - .uri("/api/v1/wishlist/ ") + .uri("/api/v1/wishlist/asd") .accept(APPLICATION_JSON) .header(AUTHORIZATION, "Bearer $accessToken") .exchange() @@ -267,7 +275,7 @@ internal class WishlistControllerTest @Autowired constructor( accessToken: AccessToken, ): CreateWishlistResponse { val request = CreateWishlistRequest( - ebookId = createEbookResponse.ebook.id.toString() + ebookId = createEbookResponse.ebook.id ) val response = webTestClient @@ -295,14 +303,14 @@ internal class WishlistControllerTest @Autowired constructor( val mainImage = postSaveMainImage(imageBase64Raw, imagePath, accessToken) val descriptionImageList = postSaveDescriptionImages(imageBase64Raw, imagePath, accessToken) - val categoryId = categoryRepository.findAll().toList()[0].id!!.toString() + val categoryId = categoryRepository.findAll().toList()[0].id!! val request = CreateEbookRequest( - pdfId = pdf.id.toString(), + pdfId = pdf.id, title = "title", relatedCategoryIdList = listOf(categoryId), - mainImageId = mainImage.id.toString(), - descriptionImageIdList = descriptionImageList.map { it.id.toString() }, + mainImageId = mainImage.id, + descriptionImageIdList = descriptionImageList.map { it.id }, 10000, "introduction", "tableOfContent" @@ -323,7 +331,7 @@ internal class WishlistControllerTest @Autowired constructor( } fun postSaveDescriptionImages( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, ): List { @@ -331,15 +339,13 @@ internal class WishlistControllerTest @Autowired constructor( imageList = listOf( ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 1 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), - 2 + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ), ) ) @@ -361,15 +367,15 @@ internal class WishlistControllerTest @Autowired constructor( } private fun postSaveMainImage( - imageBase64Raw: String?, + imageBase64Raw: String, imagePath: Path, accessToken: AccessToken, - ): SaveMainImageResponse.MainImageDto { + ): EbookImageDto { val saveMainImageRequest = SaveMainImageRequest( - SaveMainImageRequest.MainImageDto( + ImageDto( imageBase64Raw, - imagePath.extension, - imagePath.fileSize(), + ImageExtension.valueOf(imagePath.extension.uppercase()), + imagePath.fileSize().toInt(), ) ) diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 634163c..f59d381 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -26,6 +26,9 @@ spring: tls: true debug: true + flyway: + enabled: false + jwt: secretKey: testtesttesttesttesttesttesttesttesttesttesttesttesttesttest accessTokenExpirationHour: 1 diff --git a/src/test/resources/initialize.sql b/src/test/resources/initialize.sql new file mode 100644 index 0000000..15a81be --- /dev/null +++ b/src/test/resources/initialize.sql @@ -0,0 +1,229 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE IF NOT EXISTS member +( + member_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + nickname VARCHAR UNIQUE NOT NULL, + profile_image_path VARCHAR UNIQUE, + authority VARCHAR NOT NULL, + withdrawal_date TIMESTAMP, + until_suspension_date TIMESTAMP, + registered_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS member_info +( + member_info_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + blog_link VARCHAR NOT NULL, + instagram_link VARCHAR NOT NULL, + youtube_link VARCHAR NOT NULL, + real_name VARCHAR NOT NULL, + bank VARCHAR NOT NULL, + account_number VARCHAR NOT NULL, + introduction TEXT NOT NULL, + phone_number VARCHAR NOT NULL, + member_id uuid NOT NULL UNIQUE, + email VARCHAR NOT NULL, + CONSTRAINT member_info_fk_member_id FOREIGN KEY (member_id) REFERENCES member_info (member_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS oauth_info +( + oauth_id VARCHAR PRIMARY KEY, + oauth_type VARCHAR NOT NULL, + member_id uuid UNIQUE NOT NULL, + registered_date TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT oauth_info_fk_member_id FOREIGN KEY (member_id) REFERENCES member (member_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS refresh_token +( + refresh_token_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + member_id uuid UNIQUE NOT NULL, + token VARCHAR UNIQUE NOT NULL, + registered_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL, + CONSTRAINT refresh_token_fk_member_id FOREIGN KEY (member_id) REFERENCES member (member_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS category +( + category_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR UNIQUE NOT NULL, + registered_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL, + deleted_date TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS favorite_category +( + favorite_category_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + favorite_member_id uuid NOT NULL, + category_id uuid NOT NULL, + CONSTRAINT favorite_category_fk_member_id FOREIGN KEY (favorite_member_id) + REFERENCES member (member_id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT favorite_category_fk_category_id FOREIGN KEY (category_id) + REFERENCES category (category_id) +); + +CREATE TABLE IF NOT EXISTS pdf +( + pdf_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + file_path VARCHAR UNIQUE NOT NULL, + page_count INT NOT NULL, + created_date TIMESTAMP NOT NULL, + upload_member_id uuid NOT NULL, + CONSTRAINT pdf_fk_member_id FOREIGN KEY (upload_member_id) REFERENCES member (member_id) +); + +CREATE TABLE IF NOT EXISTS preview_image +( + preview_image_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + image_path VARCHAR UNIQUE NOT NULL, + preview_order INT NOT NULL, + pdf_id uuid NOT NULL, + CONSTRAINT preview_image_fk_pdf_id FOREIGN KEY (pdf_id) REFERENCES pdf (pdf_id) +); + +CREATE TABLE IF NOT EXISTS ebook +( + ebook_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + selling_member_id uuid NOT NULL, + pdf_id uuid NOT NULL UNIQUE, + main_image_id uuid NOT NULL UNIQUE, + title VARCHAR NOT NULL, + price INT NOT NULL, + table_of_contents TEXT NOT NULL, + introduction TEXT NOT NULL, + created_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL, + deleted_date TIMESTAMP, + CONSTRAINT ebook_fk_member_id FOREIGN KEY (selling_member_id) REFERENCES member (member_id), + CONSTRAINT ebook_fk_pdf_id FOREIGN KEY (pdf_id) REFERENCES pdf (pdf_id) +); + +CREATE TABLE IF NOT EXISTS ebook_image +( + ebook_image_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + image_path VARCHAR UNIQUE NOT NULL, + image_order INT NOT NULL, + upload_member_id uuid NOT NULL, + image_type VARCHAR NOT NULL, + ebook_id uuid, + CONSTRAINT ebook_image_fk_ebook_id FOREIGN KEY (ebook_id) REFERENCES ebook (ebook_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS related_category +( + related_category_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + ebook_id uuid NOT NULL, + category_id uuid NOT NULL, + CONSTRAINT related_category_fk_ebook_id FOREIGN KEY (ebook_id) REFERENCES ebook (ebook_id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT related_category_fk_category_id FOREIGN KEY (category_id) REFERENCES category (category_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS wishlist +( + wishlist_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + member_id uuid NOT NULL, + ebook_id uuid NOT NULL, + created_date TIMESTAMP NOT NULL, + CONSTRAINT wishlist_fk_member_id FOREIGN KEY (member_id) REFERENCES member (member_id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT wishlist_fk_ebook_id FOREIGN KEY (ebook_id) REFERENCES ebook (ebook_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS transaction +( + transaction_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + ebook_id uuid NOT NULL, + price INT NOT NULL, + payment_method VARCHAR NOT NULL, + buyer_member_id uuid NOT NULL, + transaction_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS review +( + review_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + rating INT NOT NULL, + content VARCHAR NOT NULL, + ebook_id uuid NOT NULL, + writer_member_id uuid NOT NULL, + written_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS review_comment +( + review_comment_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + content VARCHAR NOT NULL, + review_id uuid NOT NULL, + writer_member_id uuid NOT NULL, + written_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS ebook_inquiry +( + ebook_inquiry_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + content VARCHAR NOT NULL, + ebook_id uuid NOT NULL, + writer_member_id uuid NOT NULL, + written_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS ebook_inquiry_comment +( + ebook_inquiry_comment_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + content VARCHAR NOT NULL, + inquiry_id uuid NOT NULL, + writer_member_id uuid NOT NULL, + written_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS service_inquiry +( + service_inquiry_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + title VARCHAR NOT NULL, + content TEXT NOT NULL, + created_date TIMESTAMP NOT NULL, + modified_date TIMESTAMP NOT NULL, + inquiry_processing_status VARCHAR NOT NULL, + writer_member_id uuid NOT NULL, + CONSTRAINT service_inquiry_fk_member_id FOREIGN KEY (writer_member_id) REFERENCES member (member_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS service_inquiry_image +( + service_inquiry_image_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + image_path VARCHAR UNIQUE NOT NULL, + image_order INT NOT NULL, + upload_member_id uuid NOT NULL, + service_inquiry_id uuid, + CONSTRAINT service_inquiry_image_fk_service_inquiry_id FOREIGN KEY (service_inquiry_id) REFERENCES service_inquiry (service_inquiry_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS notification +( + notification_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + type VARCHAR NOT NULL, + content VARCHAR NOT NULL, + note jsonb NOT NULL, + receiver_id uuid NOT NULL, + notified_date TIMESTAMP NOT NULL, + checked BOOLEAN NOT NULL +); + +INSERT INTO category (name, registered_date, modified_date) +VALUES ('IT/프로그래밍', NOW(), NOW()), + ('게임', NOW(), NOW()), + ('비즈니스', NOW(), NOW()), + ('하드웨어', NOW(), NOW()), + ('인공지능', NOW(), NOW()), + ('디자인', NOW(), NOW()), + ('금융/재테크', NOW(), NOW()), + ('교양', NOW(), NOW()) +ON CONFLICT (name) DO NOTHING;