diff --git a/server/src/main/kotlin/com/fone/profile/application/ValidateProfileFacade.kt b/server/src/main/kotlin/com/fone/profile/application/ValidateProfileFacade.kt new file mode 100644 index 00000000..14f7f963 --- /dev/null +++ b/server/src/main/kotlin/com/fone/profile/application/ValidateProfileFacade.kt @@ -0,0 +1,26 @@ +package com.fone.profile.application + +import com.fone.profile.domain.service.ValidateProfileService +import com.fone.profile.presentation.dto.ValidateProfileDto +import org.springframework.stereotype.Service + +@Service +class ValidateProfileFacade( + private val validateProfileService: ValidateProfileService, +) { + + suspend fun validateBasicPage(basicPageValidation: ValidateProfileDto.BasicPageValidation) = + validateProfileService.validateBasicPage(basicPageValidation) + + suspend fun validateDetailPage(email: String, detailPageValidation: ValidateProfileDto.DetailPageValidation) = + validateProfileService.validateDetailPage(email, detailPageValidation) + + suspend fun validateDescriptionPage(descriptionPageValidation: ValidateProfileDto.DescriptionPageValidation) = + validateProfileService.validateDescriptionPage(descriptionPageValidation) + + suspend fun validateCareerPage(careerPageValidation: ValidateProfileDto.CareerPageValidation) = + validateProfileService.validateCareerPage(careerPageValidation) + + suspend fun validateInterestPage(interestPageValidation: ValidateProfileDto.InterestPageValidation) = + validateProfileService.validateInterestPage(interestPageValidation) +} diff --git a/server/src/main/kotlin/com/fone/profile/domain/service/ValidateProfileService.kt b/server/src/main/kotlin/com/fone/profile/domain/service/ValidateProfileService.kt new file mode 100644 index 00000000..7c2cbe97 --- /dev/null +++ b/server/src/main/kotlin/com/fone/profile/domain/service/ValidateProfileService.kt @@ -0,0 +1,86 @@ +package com.fone.profile.domain.service + +import com.fone.common.exception.NotFoundUserException +import com.fone.common.exception.RequestValidationException +import com.fone.profile.presentation.dto.ValidateProfileDto +import com.fone.user.domain.enum.Job +import com.fone.user.domain.repository.UserRepository +import org.hibernate.validator.internal.constraintvalidators.hv.URLValidator +import org.springframework.stereotype.Service + +@Service +class ValidateProfileService( + private val userRepository: UserRepository, +) { + private val urlValidator = URLValidator() + + fun validateBasicPage(basicPageValidation: ValidateProfileDto.BasicPageValidation) { + if (basicPageValidation.name.isNullOrBlank()) { + throw RequestValidationException("항목 '이름'이 비었습니다.") + } + if (basicPageValidation.hookingComment.isNullOrEmpty()) { + throw RequestValidationException("항목 '후킹멘트'가 비었습니다.") + } + if (basicPageValidation.profileImages.isNullOrEmpty()) { + throw RequestValidationException("항목 '이미지 첨부'가 비었습니다.") + } + if (basicPageValidation.profileImages.any { urlValidator.isValid(it, null) }) { + throw RequestValidationException("항목 '이미지 첨부'에 유효하지 않은 값이 있습니다.") + } + } + + suspend fun validateDetailPage( + email: String, + detailPageValidation: ValidateProfileDto.DetailPageValidation, + ) { + val user = userRepository.findByEmail(email) ?: throw NotFoundUserException() + if (detailPageValidation.birthday == null) { + throw RequestValidationException("항목 '출생연도'가 비었습니다.") + } + if (detailPageValidation.gender == null) { + throw RequestValidationException("항목 '성별'이 비었습니다.") + } + if (user.job == Job.STAFF) { + if (detailPageValidation.domains.isNullOrEmpty()) { + throw RequestValidationException("항목 '도메인'이 비었습니다.") + } + } else { + if (detailPageValidation.height == null) { + throw RequestValidationException("항목 '신장'이 비었습니다.") + } + if (detailPageValidation.weight == null) { + throw RequestValidationException("항목 '체중'이 비었습니다.") + } + } + if (detailPageValidation.email == null) { + throw RequestValidationException("항목 '성별'이 비었습니다.") + } + } + + fun validateDescriptionPage(descriptionPageValidation: ValidateProfileDto.DescriptionPageValidation) { + if (descriptionPageValidation.details.isNullOrBlank()) { + throw RequestValidationException("항목 '상세요강'이 비었습니다.") + } + if (descriptionPageValidation.details.length > 200) { + throw RequestValidationException("항목 '상세요강'이 200자 넘습니다.") + } + } + + fun validateCareerPage(careerPageValidation: ValidateProfileDto.CareerPageValidation) { + if (careerPageValidation.career == null) { + throw RequestValidationException("항목 '경력'이 비었습니다.") + } + if (careerPageValidation.careerDetail.isNullOrBlank()) { + throw RequestValidationException("항목 '경력 상세사항'이 비었습니다.") + } + if (careerPageValidation.careerDetail.length > 500) { + throw RequestValidationException("항목 '경력 상세사항'이 500자 넘습니다.") + } + } + + fun validateInterestPage(interestPageValidation: ValidateProfileDto.InterestPageValidation) { + if (interestPageValidation.categories.isNullOrEmpty()) { + throw RequestValidationException("항목 '관심사'가 비었습니다.") + } + } +} diff --git a/server/src/main/kotlin/com/fone/profile/presentation/controller/ValidateProfileController.kt b/server/src/main/kotlin/com/fone/profile/presentation/controller/ValidateProfileController.kt new file mode 100644 index 00000000..6551ff97 --- /dev/null +++ b/server/src/main/kotlin/com/fone/profile/presentation/controller/ValidateProfileController.kt @@ -0,0 +1,97 @@ +package com.fone.profile.presentation.controller + +import com.fone.common.response.CommonResponse +import com.fone.profile.application.ValidateProfileFacade +import com.fone.profile.presentation.dto.ValidateProfileDto +import io.swagger.annotations.Api +import io.swagger.annotations.ApiOperation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.security.Principal + +@Api(tags = ["04. Profile Info"], description = "프로필 서비스") +@RestController +@RequestMapping("/api/v1/profiles/validate") +class ValidateProfileController( + private val validateProfileFacade: ValidateProfileFacade, +) { + @PostMapping("/basic") + @PreAuthorize("hasRole('USER')") + @ApiOperation(value = "프로필 검증 API") + @ApiResponse( + responseCode = "200", + description = "성공" + ) + suspend fun basicPageValidate( + @RequestBody + basicPageValidation: ValidateProfileDto.BasicPageValidation, + ): CommonResponse { + validateProfileFacade.validateBasicPage(basicPageValidation) + return CommonResponse.success() + } + + @PostMapping("/details") + @PreAuthorize("hasRole('USER')") + @ApiOperation(value = "프로필 검증 API") + @ApiResponse( + responseCode = "200", + description = "성공" + ) + suspend fun detailPageValidate( + principal: Principal, + @RequestBody + detailPageValidation: ValidateProfileDto.DetailPageValidation, + ): CommonResponse { + validateProfileFacade.validateDetailPage(principal.name, detailPageValidation) + return CommonResponse.success() + } + + @PostMapping("/description") + @PreAuthorize("hasRole('USER')") + @ApiOperation(value = "프로필 검증 API") + @ApiResponse( + responseCode = "200", + description = "성공" + ) + suspend fun descriptionValidate( + @RequestBody + descriptionPageValidation: ValidateProfileDto.DescriptionPageValidation, + ): CommonResponse { + validateProfileFacade.validateDescriptionPage(descriptionPageValidation) + return CommonResponse.success() + } + + @PostMapping("/career") + @PreAuthorize("hasRole('USER')") + @ApiOperation(value = "프로필 검증 API") + @ApiResponse( + responseCode = "200", + description = "성공" + ) + suspend fun careerValidate( + @RequestBody + careerPageValidation: ValidateProfileDto.CareerPageValidation, + ): CommonResponse { + validateProfileFacade.validateCareerPage(careerPageValidation) + return CommonResponse.success() + } + + @PostMapping("/interest") + @PreAuthorize("hasRole('USER')") + @ApiOperation(value = "프로필 검증 API") + @ApiResponse( + responseCode = "200", + description = "성공" + ) + suspend fun interestValidate( + @RequestBody + interestPageValidation: ValidateProfileDto.InterestPageValidation, + ): CommonResponse { + validateProfileFacade.validateInterestPage(interestPageValidation) + return CommonResponse.success() + } +} diff --git a/server/src/main/kotlin/com/fone/profile/presentation/dto/ValidateProfileDto.kt b/server/src/main/kotlin/com/fone/profile/presentation/dto/ValidateProfileDto.kt new file mode 100644 index 00000000..dd2c8105 --- /dev/null +++ b/server/src/main/kotlin/com/fone/profile/presentation/dto/ValidateProfileDto.kt @@ -0,0 +1,59 @@ +package com.fone.profile.presentation.dto + +import com.fone.common.entity.Career +import com.fone.common.entity.CategoryType +import com.fone.common.entity.DomainType +import com.fone.common.entity.Gender +import com.fone.profile.presentation.dto.common.ProfileSnsUrl +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +class ValidateProfileDto { + data class BasicPageValidation( + @Schema(description = "프로필 이름", required = true, example = "차이나는 클라스") + val name: String?, + @Schema(description = "후킹멘트", required = true, example = "제가 좋아하는 색은 노랑색이에요") + val hookingComment: String?, + @Schema( + description = "이미지 URL", + example = "['https://s3-ap-northeast-2.amazonaws.com/f-one-image/prod/user-profile/image.jpg']" + ) + val profileImages: List?, + ) + + data class DetailPageValidation( + @Schema(description = "생년월일", example = "2000-10-01") + val birthday: LocalDate?, + @Schema(description = "성별", example = "WOMAN") + val gender: Gender?, + @Schema(description = "키", example = "188") + val height: Int?, + @Schema(description = "몸무게", example = "70") + val weight: Int?, + @Schema(description = "이메일", example = "example@something.com") + val email: String?, + @Schema(description = "SNS v2") + val snsUrls: List?, + @Schema(description = "특기", example = "매운 음식 먹기") + val specialty: String?, + @Schema(description = "분야", example = "PLANNING") + val domains: List?, + ) + + data class DescriptionPageValidation( + @Schema(description = "상세요강") + val details: String?, + ) + + data class CareerPageValidation( + @Schema(description = "경력", example = "LESS_THAN_3YEARS") + val career: Career?, + @Schema(description = "경력 상세 설명", example = "복숭아 요거트 제작 3년") + val careerDetail: String?, + ) + + data class InterestPageValidation( + @Schema(description = "관심사", example = "WEB_DRAMA") + val categories: List?, + ) +} diff --git a/server/src/test/kotlin/com/fone/jobOpening/presentation/controller/ValidateControllerTest.kt b/server/src/test/kotlin/com/fone/jobOpening/presentation/controller/ValidateJobOpeningControllerTest.kt similarity index 98% rename from server/src/test/kotlin/com/fone/jobOpening/presentation/controller/ValidateControllerTest.kt rename to server/src/test/kotlin/com/fone/jobOpening/presentation/controller/ValidateJobOpeningControllerTest.kt index 0e7733c2..8346b1bd 100644 --- a/server/src/test/kotlin/com/fone/jobOpening/presentation/controller/ValidateControllerTest.kt +++ b/server/src/test/kotlin/com/fone/jobOpening/presentation/controller/ValidateJobOpeningControllerTest.kt @@ -14,7 +14,7 @@ import org.springframework.test.web.reactive.server.WebTestClient import java.time.LocalDate @IntegrationTest -class ValidateControllerTest(client: WebTestClient) : CustomDescribeSpec() { +class ValidateJobOpeningControllerTest(client: WebTestClient) : CustomDescribeSpec() { init { val url = "/api/v1/job-openings/validate" val (accessToken, _) = CommonUserCallApi.getAccessToken(client) diff --git a/server/src/test/kotlin/com/fone/profile/presentation/controller/ValidateProfileControllerTest.kt b/server/src/test/kotlin/com/fone/profile/presentation/controller/ValidateProfileControllerTest.kt new file mode 100644 index 00000000..ee8ddcb1 --- /dev/null +++ b/server/src/test/kotlin/com/fone/profile/presentation/controller/ValidateProfileControllerTest.kt @@ -0,0 +1,78 @@ +package com.fone.profile.presentation.controller + +import com.fone.common.CommonUserCallApi +import com.fone.common.CustomDescribeSpec +import com.fone.common.IntegrationTest +import com.fone.common.doPost +import com.fone.common.entity.Gender +import com.fone.profile.domain.enum.SNS +import com.fone.profile.presentation.dto.ValidateProfileDto +import com.fone.profile.presentation.dto.common.ProfileSnsUrl +import org.springframework.test.web.reactive.server.WebTestClient +import java.time.LocalDate + +@IntegrationTest +class ValidateProfileControllerTest(client: WebTestClient) : CustomDescribeSpec() { + init { + val url = "/api/v1/profiles/validate" + val (accessToken, _) = CommonUserCallApi.getAccessToken(client) + describe("#Profile 검증 API") { + context("basic 페이지") { + it("성공한다") { + val request = + ValidateProfileDto.BasicPageValidation( + "제목", + "후킹멘트", + listOf("https://s3-ap-northeast-2.amazonaws.com/f-one-image/prod/user-profile/image.jpg") + ) + client.doPost("$url/basic", request, accessToken) + .expectStatus().isOk.expectBody() + .consumeWith { println(it) }.jsonPath("$.result").isEqualTo("SUCCESS") + } + it("실패한다") { + val request = + ValidateProfileDto.BasicPageValidation( + "제목", + "후킹멘트", + listOf("") + ) + client.doPost("$url/basic", request, accessToken).expectStatus().isBadRequest.expectBody() + .consumeWith { println(it) }.jsonPath("$.result").isEqualTo("FAIL") + } + } + context("details 페이지") { + it("성공한다") { + val request = + ValidateProfileDto.DetailPageValidation( + LocalDate.now(), + Gender.MAN, + 170, + 300, + "mail@mail.com", + listOf(ProfileSnsUrl("123", SNS.YOUTUBE)), + "고추장 잘 먹음", + null + ) + client.doPost("$url/details", request, accessToken) + .expectStatus().isOk.expectBody() + .consumeWith { println(it) }.jsonPath("$.result").isEqualTo("SUCCESS") + } + it("실패한다") { + val request = + ValidateProfileDto.DetailPageValidation( + LocalDate.now(), + Gender.MAN, + null, + 300, + "mail@mail.com", + listOf(ProfileSnsUrl("123", SNS.YOUTUBE)), + "고추장 잘 먹음", + null + ) + client.doPost("$url/details", request, accessToken).expectStatus().isBadRequest.expectBody() + .consumeWith { println(it) }.jsonPath("$.result").isEqualTo("FAIL") + } + } + } + } +}