From 75528794c5b6f80a2ec5ce69b1c58849fc0f82bb Mon Sep 17 00:00:00 2001 From: hwk9407 Date: Sat, 13 Jun 2026 15:03:28 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=82=B4=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=8B=A8=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=97=90=20=ED=99=9C=EB=8F=99=20ID=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/review/application/mapper/ReviewViewMapperDetail.kt | 1 + .../review/application/query/model/MyReviewDetailView.kt | 1 + .../backend/review/entrypoint/mapper/ReviewDetailMapper.kt | 1 + .../backend/review/entrypoint/response/MyReviewResponse.kt | 2 ++ 4 files changed, 5 insertions(+) diff --git a/src/main/kotlin/picklab/backend/review/application/mapper/ReviewViewMapperDetail.kt b/src/main/kotlin/picklab/backend/review/application/mapper/ReviewViewMapperDetail.kt index f6f39bb8..533eb574 100644 --- a/src/main/kotlin/picklab/backend/review/application/mapper/ReviewViewMapperDetail.kt +++ b/src/main/kotlin/picklab/backend/review/application/mapper/ReviewViewMapperDetail.kt @@ -12,6 +12,7 @@ import picklab.backend.review.domain.entity.Review */ fun Review.toDetailView(): MyReviewDetailView = MyReviewDetailView( + activityId = this.activity.id, jobGroup = this.jobCategory.jobGroup, jobDetail = this.jobCategory.jobDetail, overallScore = this.overallScore, diff --git a/src/main/kotlin/picklab/backend/review/application/query/model/MyReviewDetailView.kt b/src/main/kotlin/picklab/backend/review/application/query/model/MyReviewDetailView.kt index 524b6642..479ecb8d 100644 --- a/src/main/kotlin/picklab/backend/review/application/query/model/MyReviewDetailView.kt +++ b/src/main/kotlin/picklab/backend/review/application/query/model/MyReviewDetailView.kt @@ -4,6 +4,7 @@ import picklab.backend.job.domain.enums.JobDetail import picklab.backend.job.domain.enums.JobGroup data class MyReviewDetailView( + val activityId: Long, val jobGroup: JobGroup, val jobDetail: JobDetail?, val overallScore: Int, diff --git a/src/main/kotlin/picklab/backend/review/entrypoint/mapper/ReviewDetailMapper.kt b/src/main/kotlin/picklab/backend/review/entrypoint/mapper/ReviewDetailMapper.kt index b7b639f3..537e635d 100644 --- a/src/main/kotlin/picklab/backend/review/entrypoint/mapper/ReviewDetailMapper.kt +++ b/src/main/kotlin/picklab/backend/review/entrypoint/mapper/ReviewDetailMapper.kt @@ -5,6 +5,7 @@ import picklab.backend.review.entrypoint.response.MyReviewResponse fun MyReviewDetailView.toResponse(): MyReviewResponse = MyReviewResponse( + activityId = this.activityId, jobGroup = this.jobGroup, jobDetail = this.jobDetail, overallScore = this.overallScore, diff --git a/src/main/kotlin/picklab/backend/review/entrypoint/response/MyReviewResponse.kt b/src/main/kotlin/picklab/backend/review/entrypoint/response/MyReviewResponse.kt index caac1eb8..c4e06152 100644 --- a/src/main/kotlin/picklab/backend/review/entrypoint/response/MyReviewResponse.kt +++ b/src/main/kotlin/picklab/backend/review/entrypoint/response/MyReviewResponse.kt @@ -6,6 +6,8 @@ import picklab.backend.job.domain.enums.JobGroup @Schema(description = "내 리뷰 단건 조회 응답") data class MyReviewResponse( + @field:Schema(description = "활동 ID") + val activityId: Long, @field:Schema(description = "직무") val jobGroup: JobGroup, @field:Schema(description = "상세 직무 (null = 전체)") From 3738ecffbe01b66386825e76a33d4e7e39c6aee7 Mon Sep 17 00:00:00 2001 From: hwk9407 Date: Sat, 13 Jun 2026 15:37:35 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=82=B4=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=EC=97=90=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=88=98=EC=8B=A0=20=EB=8F=99=EC=9D=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/application/MemberUseCase.kt | 8 ++++- .../application/mapper/MemberMeMapper.kt | 10 +++++- .../application/model/MemberMeResult.kt | 3 ++ .../backend/member/domain/MemberService.kt | 6 ++++ .../entrypoint/mapper/MemberMeMapper.kt | 7 ++++ .../response/GetMemberMeResponse.kt | 11 +++++++ .../entrypoint/MemberMeControllerTest.kt | 6 ++++ .../member/service/MemberMeServiceTest.kt | 32 +++++++++++++++++++ 8 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/picklab/backend/member/application/MemberUseCase.kt b/src/main/kotlin/picklab/backend/member/application/MemberUseCase.kt index 4bbd8f8e..eb9e8939 100644 --- a/src/main/kotlin/picklab/backend/member/application/MemberUseCase.kt +++ b/src/main/kotlin/picklab/backend/member/application/MemberUseCase.kt @@ -134,7 +134,13 @@ class MemberUseCase( fun getMemberMe(memberId: Long): MemberMeResult { val member = memberService.findActiveMember(memberId) val interestedJobCategories = memberService.findInterestedJobCategories(memberId) - return member.toMemberMeResult(interestedJobCategories) + val emailAgreement = memberService.findEmailAgreement(memberId) + val notificationPreference = memberService.findNotificationPreference(memberId) + return member.toMemberMeResult( + interestedJobCategories = interestedJobCategories, + emailAgreement = emailAgreement, + notificationPreference = notificationPreference, + ) } fun withdrawMember(memberId: Long) { diff --git a/src/main/kotlin/picklab/backend/member/application/mapper/MemberMeMapper.kt b/src/main/kotlin/picklab/backend/member/application/mapper/MemberMeMapper.kt index 8c1a7c66..f4ca2f8f 100644 --- a/src/main/kotlin/picklab/backend/member/application/mapper/MemberMeMapper.kt +++ b/src/main/kotlin/picklab/backend/member/application/mapper/MemberMeMapper.kt @@ -3,8 +3,13 @@ package picklab.backend.member.application.mapper import picklab.backend.member.application.model.MemberMeResult import picklab.backend.member.domain.entity.InterestedJobCategory import picklab.backend.member.domain.entity.Member +import picklab.backend.member.domain.entity.NotificationPreference -fun Member.toMemberMeResult(interestedJobCategories: List): MemberMeResult = +fun Member.toMemberMeResult( + interestedJobCategories: List, + emailAgreement: Boolean, + notificationPreference: NotificationPreference, +): MemberMeResult = MemberMeResult( name = this.name, nickname = this.nickname, @@ -14,4 +19,7 @@ fun Member.toMemberMeResult(interestedJobCategories: List jobFields = interestedJobCategories.map { it.jobCategory.jobGroup }.distinct(), employmentStatus = this.employmentStatus, company = this.company, + emailAgreement = emailAgreement, + notifyPopularActivity = notificationPreference.notifyPopularActivity, + notifyBookmarkedActivity = notificationPreference.notifyBookmarkedActivity, ) diff --git a/src/main/kotlin/picklab/backend/member/application/model/MemberMeResult.kt b/src/main/kotlin/picklab/backend/member/application/model/MemberMeResult.kt index 95bb876d..1d553a83 100644 --- a/src/main/kotlin/picklab/backend/member/application/model/MemberMeResult.kt +++ b/src/main/kotlin/picklab/backend/member/application/model/MemberMeResult.kt @@ -13,4 +13,7 @@ data class MemberMeResult( val jobFields: List, val employmentStatus: String, val company: String, + val emailAgreement: Boolean, + val notifyPopularActivity: Boolean, + val notifyBookmarkedActivity: Boolean, ) diff --git a/src/main/kotlin/picklab/backend/member/domain/MemberService.kt b/src/main/kotlin/picklab/backend/member/domain/MemberService.kt index d6f70e8c..b5b3227a 100644 --- a/src/main/kotlin/picklab/backend/member/domain/MemberService.kt +++ b/src/main/kotlin/picklab/backend/member/domain/MemberService.kt @@ -239,6 +239,12 @@ class MemberService( fun findInterestedJobCategories(memberId: Long): List = interestedJobCategoryRepository.findAllByMemberIdWithJobCategory(memberId) + @Transactional(readOnly = true) + fun findEmailAgreement(memberId: Long): Boolean = memberAgreementRepository.findByMemberId(memberId)?.emailAgreement ?: false + + fun findNotificationPreference(memberId: Long): NotificationPreference = + notificationPreferenceService.getNotificationPreference(memberId) + @Transactional fun withdrawMember(memberId: Long) { val member = findActiveMember(memberId) diff --git a/src/main/kotlin/picklab/backend/member/entrypoint/mapper/MemberMeMapper.kt b/src/main/kotlin/picklab/backend/member/entrypoint/mapper/MemberMeMapper.kt index 371d2eb9..1a9329f3 100644 --- a/src/main/kotlin/picklab/backend/member/entrypoint/mapper/MemberMeMapper.kt +++ b/src/main/kotlin/picklab/backend/member/entrypoint/mapper/MemberMeMapper.kt @@ -3,6 +3,7 @@ package picklab.backend.member.entrypoint.mapper import picklab.backend.member.application.model.MemberMeResult import picklab.backend.member.entrypoint.response.EmploymentInfoResponse import picklab.backend.member.entrypoint.response.GetMemberMeResponse +import picklab.backend.member.entrypoint.response.NotificationPreferencesResponse fun MemberMeResult.toResponse(): GetMemberMeResponse = GetMemberMeResponse( @@ -17,4 +18,10 @@ fun MemberMeResult.toResponse(): GetMemberMeResponse = employmentStatus = this.employmentStatus, company = this.company, ), + emailAgreement = this.emailAgreement, + notificationPreferences = + NotificationPreferencesResponse( + popular = this.notifyPopularActivity, + bookmarked = this.notifyBookmarkedActivity, + ), ) diff --git a/src/main/kotlin/picklab/backend/member/entrypoint/response/GetMemberMeResponse.kt b/src/main/kotlin/picklab/backend/member/entrypoint/response/GetMemberMeResponse.kt index 1053bf6d..8cf6d237 100644 --- a/src/main/kotlin/picklab/backend/member/entrypoint/response/GetMemberMeResponse.kt +++ b/src/main/kotlin/picklab/backend/member/entrypoint/response/GetMemberMeResponse.kt @@ -20,6 +20,10 @@ data class GetMemberMeResponse( val jobFields: List, @field:Schema(description = "재직 상태") val employment: EmploymentInfoResponse, + @field:Schema(description = "이메일 마케팅 수신 동의 여부") + val emailAgreement: Boolean, + @field:Schema(description = "알림 수신 설정") + val notificationPreferences: NotificationPreferencesResponse, ) data class EmploymentInfoResponse( @@ -28,3 +32,10 @@ data class EmploymentInfoResponse( @field:Schema(description = "회사") val company: String, ) + +data class NotificationPreferencesResponse( + @field:Schema(description = "인기 공고 알림 수신 여부") + val popular: Boolean, + @field:Schema(description = "저장한 공고 알림 수신 여부") + val bookmarked: Boolean, +) diff --git a/src/test/kotlin/picklab/backend/member/entrypoint/MemberMeControllerTest.kt b/src/test/kotlin/picklab/backend/member/entrypoint/MemberMeControllerTest.kt index fcf0b934..81813f1d 100644 --- a/src/test/kotlin/picklab/backend/member/entrypoint/MemberMeControllerTest.kt +++ b/src/test/kotlin/picklab/backend/member/entrypoint/MemberMeControllerTest.kt @@ -39,6 +39,9 @@ class MemberMeControllerTest { jobFields = listOf(JobGroup.DEVELOPMENT), employmentStatus = "재직 중", company = "Picklab", + emailAgreement = true, + notifyPopularActivity = true, + notifyBookmarkedActivity = false, ), ) @@ -57,5 +60,8 @@ class MemberMeControllerTest { .andExpect { jsonPath("$.data.job_fields[0]") { value("DEVELOPMENT") } } .andExpect { jsonPath("$.data.employment.employment_status") { value("재직 중") } } .andExpect { jsonPath("$.data.employment.company") { value("Picklab") } } + .andExpect { jsonPath("$.data.email_agreement") { value(true) } } + .andExpect { jsonPath("$.data.notification_preferences.popular") { value(true) } } + .andExpect { jsonPath("$.data.notification_preferences.bookmarked") { value(false) } } } } diff --git a/src/test/kotlin/picklab/backend/member/service/MemberMeServiceTest.kt b/src/test/kotlin/picklab/backend/member/service/MemberMeServiceTest.kt index 18e440f3..50ea065e 100644 --- a/src/test/kotlin/picklab/backend/member/service/MemberMeServiceTest.kt +++ b/src/test/kotlin/picklab/backend/member/service/MemberMeServiceTest.kt @@ -12,8 +12,12 @@ import picklab.backend.job.domain.enums.JobGroup import picklab.backend.member.application.MemberUseCase import picklab.backend.member.domain.entity.InterestedJobCategory import picklab.backend.member.domain.entity.Member +import picklab.backend.member.domain.entity.MemberAgreement +import picklab.backend.member.domain.entity.NotificationPreference import picklab.backend.member.domain.repository.InterestedJobCategoryRepository +import picklab.backend.member.domain.repository.MemberAgreementRepository import picklab.backend.member.domain.repository.MemberRepository +import picklab.backend.member.domain.repository.NotificationPreferenceRepository import picklab.backend.template.IntegrationTest import java.time.LocalDate @@ -27,6 +31,12 @@ class MemberMeServiceTest : IntegrationTest() { @Autowired lateinit var interestedJobCategoryRepository: InterestedJobCategoryRepository + @Autowired + lateinit var memberAgreementRepository: MemberAgreementRepository + + @Autowired + lateinit var notificationPreferenceRepository: NotificationPreferenceRepository + @Autowired lateinit var jobCategoryRepository: JobCategoryRepository @@ -51,6 +61,21 @@ class MemberMeServiceTest : IntegrationTest() { ), ) + memberAgreementRepository.save( + MemberAgreement( + member = member, + emailAgreement = true, + privacyAgreement = true, + ), + ) + notificationPreferenceRepository.save( + NotificationPreference( + member = member, + notifyPopularActivity = true, + notifyBookmarkedActivity = false, + ), + ) + val backend = jobCategoryRepository.save( JobCategory( @@ -83,5 +108,12 @@ class MemberMeServiceTest : IntegrationTest() { assertThat(result.jobFields).containsExactly(JobGroup.DEVELOPMENT) assertThat(result.employmentStatus).isEqualTo("재직 중") assertThat(result.company).isEqualTo("Picklab") + assertThat(result.emailAgreement).isTrue() + assertThat(result.notifyPopularActivity).isTrue() + assertThat(result.notifyBookmarkedActivity).isFalse() + + memberAgreementRepository.deleteAll() + + assertThat(memberUseCase.getMemberMe(member.id).emailAgreement).isFalse() } } From dc8f66ec5b2f488937dc8005958dc859772331f0 Mon Sep 17 00:00:00 2001 From: hwk9407 Date: Sat, 13 Jun 2026 15:51:15 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EC=82=AC=EC=9A=A9=20=EA=B0=80=EB=8A=A5=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/common/model/SuccessCode.kt | 1 + .../member/application/MemberUseCase.kt | 2 + .../backend/member/entrypoint/MemberApi.kt | 18 ++++++++ .../member/entrypoint/MemberController.kt | 12 ++++++ .../request/NicknameAvailabilityRequest.kt | 15 +++++++ .../response/NicknameAvailabilityResponse.kt | 8 ++++ .../member/entrypoint/MemberControllerTest.kt | 41 +++++++++++++++++++ 7 files changed, 97 insertions(+) create mode 100644 src/main/kotlin/picklab/backend/member/entrypoint/request/NicknameAvailabilityRequest.kt create mode 100644 src/main/kotlin/picklab/backend/member/entrypoint/response/NicknameAvailabilityResponse.kt diff --git a/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt b/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt index 3ef636c9..83adf1d4 100644 --- a/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt +++ b/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt @@ -21,6 +21,7 @@ enum class SuccessCode( UPDATE_EMAIL_AGREEMENT(HttpStatus.OK, "이메일 마케팅 수신 동의 정보를 수정했습니다."), GET_MEMBER_SOCIAL_LOGINS(HttpStatus.OK, "소셜 로그인 연동 정보 조회에 성공했습니다."), GET_MEMBER_ME(HttpStatus.OK, "내 정보 조회에 성공했습니다."), + CHECK_NICKNAME_AVAILABILITY(HttpStatus.OK, "닉네임 사용 가능 여부 조회에 성공했습니다."), MEMBER_WITHDRAW(HttpStatus.OK, "회원 탈퇴에 성공했습니다."), SUBMIT_SURVEY(HttpStatus.OK, "탈퇴 설문 제출에 성공했습니다."), MEMBER_NOTIFICATION_UPDATED(HttpStatus.OK, "알림 변경에 성공했습니다."), diff --git a/src/main/kotlin/picklab/backend/member/application/MemberUseCase.kt b/src/main/kotlin/picklab/backend/member/application/MemberUseCase.kt index eb9e8939..b47272ae 100644 --- a/src/main/kotlin/picklab/backend/member/application/MemberUseCase.kt +++ b/src/main/kotlin/picklab/backend/member/application/MemberUseCase.kt @@ -130,6 +130,8 @@ class MemberUseCase( fun getSocialLogins(memberId: Long) = memberService.getSocialLogins(memberId) + fun isNicknameAvailable(nickname: String): Boolean = !memberService.existByNickname(nickname) + @Transactional(readOnly = true) fun getMemberMe(memberId: Long): MemberMeResult { val member = memberService.findActiveMember(memberId) diff --git a/src/main/kotlin/picklab/backend/member/entrypoint/MemberApi.kt b/src/main/kotlin/picklab/backend/member/entrypoint/MemberApi.kt index 1195b673..40bd4140 100644 --- a/src/main/kotlin/picklab/backend/member/entrypoint/MemberApi.kt +++ b/src/main/kotlin/picklab/backend/member/entrypoint/MemberApi.kt @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid +import org.springdoc.core.annotations.ParameterObject import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.RequestBody @@ -12,6 +13,7 @@ import picklab.backend.common.model.MemberPrincipal import picklab.backend.common.model.ResponseWrapper import picklab.backend.member.entrypoint.request.AdditionalInfoRequest import picklab.backend.member.entrypoint.request.MemberWithdrawalRequest +import picklab.backend.member.entrypoint.request.NicknameAvailabilityRequest import picklab.backend.member.entrypoint.request.SendEmailRequest import picklab.backend.member.entrypoint.request.ToggleMemberNotificationRequest import picklab.backend.member.entrypoint.request.UpdateEmailAgreementRequest @@ -22,6 +24,7 @@ import picklab.backend.member.entrypoint.request.UpdateProfileImageRequest import picklab.backend.member.entrypoint.request.VerifyEmailCodeRequest import picklab.backend.member.entrypoint.response.GetMemberMeResponse import picklab.backend.member.entrypoint.response.GetSocialLoginsResponse +import picklab.backend.member.entrypoint.response.NicknameAvailabilityResponse @Tag(name = "회원 API", description = "회원 관련 작업을 하는 API") interface MemberApi { @@ -42,6 +45,21 @@ interface MemberApi { @Valid @RequestBody request: AdditionalInfoRequest, ): ResponseEntity> + @Operation( + summary = "닉네임 사용 가능 여부 조회", + description = "회원가입에 사용할 닉네임의 중복 여부를 확인합니다.", + ) + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "닉네임 사용 가능 여부 조회에 성공했습니다."), + ApiResponse(responseCode = "400", description = "닉네임 형식이 올바르지 않습니다."), + ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다."), + ], + ) + fun checkNicknameAvailability( + @Valid @ParameterObject request: NicknameAvailabilityRequest, + ): ResponseEntity> + @Operation( summary = "사용자 정보 수정", description = "사용자의 정보를 수정한다", diff --git a/src/main/kotlin/picklab/backend/member/entrypoint/MemberController.kt b/src/main/kotlin/picklab/backend/member/entrypoint/MemberController.kt index 00e9b208..7f412e12 100644 --- a/src/main/kotlin/picklab/backend/member/entrypoint/MemberController.kt +++ b/src/main/kotlin/picklab/backend/member/entrypoint/MemberController.kt @@ -6,6 +6,7 @@ import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PutMapping @@ -19,6 +20,7 @@ import picklab.backend.member.application.MemberUseCase import picklab.backend.member.entrypoint.mapper.toResponse import picklab.backend.member.entrypoint.request.AdditionalInfoRequest import picklab.backend.member.entrypoint.request.MemberWithdrawalRequest +import picklab.backend.member.entrypoint.request.NicknameAvailabilityRequest import picklab.backend.member.entrypoint.request.SendEmailRequest import picklab.backend.member.entrypoint.request.ToggleMemberNotificationRequest import picklab.backend.member.entrypoint.request.UpdateEmailAgreementRequest @@ -29,6 +31,7 @@ import picklab.backend.member.entrypoint.request.UpdateProfileImageRequest import picklab.backend.member.entrypoint.request.VerifyEmailCodeRequest import picklab.backend.member.entrypoint.response.GetMemberMeResponse import picklab.backend.member.entrypoint.response.GetSocialLoginsResponse +import picklab.backend.member.entrypoint.response.NicknameAvailabilityResponse @RestController @RequestMapping("/v1/members") @@ -50,6 +53,15 @@ class MemberController( .body(ResponseWrapper.success(SuccessCode.SIGNUP_SUCCESS, Unit)) } + @GetMapping("/nickname-availability") + override fun checkNicknameAvailability( + @Valid @ModelAttribute request: NicknameAvailabilityRequest, + ): ResponseEntity> = + NicknameAvailabilityResponse( + available = memberUseCase.isNicknameAvailable(request.nickname), + ).let { ResponseWrapper.success(SuccessCode.CHECK_NICKNAME_AVAILABILITY, it) } + .let { ResponseEntity.ok(it) } + @PutMapping("/info") override fun updateMemberInfo( @AuthenticationPrincipal member: MemberPrincipal, diff --git a/src/main/kotlin/picklab/backend/member/entrypoint/request/NicknameAvailabilityRequest.kt b/src/main/kotlin/picklab/backend/member/entrypoint/request/NicknameAvailabilityRequest.kt new file mode 100644 index 00000000..e838c644 --- /dev/null +++ b/src/main/kotlin/picklab/backend/member/entrypoint/request/NicknameAvailabilityRequest.kt @@ -0,0 +1,15 @@ +package picklab.backend.member.entrypoint.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Pattern + +data class NicknameAvailabilityRequest( + @field:NotBlank(message = "닉네임은 필수 입력값입니다.") + @field:Pattern( + regexp = "^[a-zA-Z0-9가-힣_.-]{1,20}$", + message = "닉네임은 영문, 숫자, 한글, _, -, .만 사용 가능하며 최대 20자까지 입력 가능합니다.", + ) + @field:Schema(description = "중복 확인할 닉네임", example = "picklab멤버", maxLength = 20) + val nickname: String, +) diff --git a/src/main/kotlin/picklab/backend/member/entrypoint/response/NicknameAvailabilityResponse.kt b/src/main/kotlin/picklab/backend/member/entrypoint/response/NicknameAvailabilityResponse.kt new file mode 100644 index 00000000..728e4f9b --- /dev/null +++ b/src/main/kotlin/picklab/backend/member/entrypoint/response/NicknameAvailabilityResponse.kt @@ -0,0 +1,8 @@ +package picklab.backend.member.entrypoint.response + +import io.swagger.v3.oas.annotations.media.Schema + +data class NicknameAvailabilityResponse( + @field:Schema(description = "닉네임 사용 가능 여부") + val available: Boolean, +) diff --git a/src/test/kotlin/picklab/backend/member/entrypoint/MemberControllerTest.kt b/src/test/kotlin/picklab/backend/member/entrypoint/MemberControllerTest.kt index 36576a1f..e1d384a9 100644 --- a/src/test/kotlin/picklab/backend/member/entrypoint/MemberControllerTest.kt +++ b/src/test/kotlin/picklab/backend/member/entrypoint/MemberControllerTest.kt @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.http.MediaType @@ -169,6 +170,46 @@ class MemberControllerTest { } } + @Nested + @WithMockUser + @DisplayName("닉네임 사용 가능 여부 조회") + inner class CheckNicknameAvailability { + @Test + @DisplayName("[성공] 사용 가능한 닉네임이면 true를 반환한다.") + fun available() { + given(memberUseCase.isNicknameAvailable("newNickname")).willReturn(true) + + mockMvc + .get("/v1/members/nickname-availability") { + param("nickname", "newNickname") + }.andExpect { status { isOk() } } + .andExpect { jsonPath("$.code") { value(SuccessCode.CHECK_NICKNAME_AVAILABILITY.status.value()) } } + .andExpect { jsonPath("$.message") { value(SuccessCode.CHECK_NICKNAME_AVAILABILITY.message) } } + .andExpect { jsonPath("$.data.available") { value(true) } } + } + + @Test + @DisplayName("[성공] 중복된 닉네임이면 false를 반환한다.") + fun unavailable() { + given(memberUseCase.isNicknameAvailable("usedNickname")).willReturn(false) + + mockMvc + .get("/v1/members/nickname-availability") { + param("nickname", "usedNickname") + }.andExpect { status { isOk() } } + .andExpect { jsonPath("$.data.available") { value(false) } } + } + + @Test + @DisplayName("[실패] 닉네임 형식이 올바르지 않으면 400을 반환한다.") + fun invalidNickname() { + mockMvc + .get("/v1/members/nickname-availability") { + param("nickname", "invalid nickname") + }.andExpect { status { isBadRequest() } } + } + } + @Nested @WithMockUser @DisplayName("회원 정보 수정") From 3d3246a2ac1fa3256a02143aaa91a0cb9653d287 Mon Sep 17 00:00:00 2001 From: hwk9407 Date: Sat, 13 Jun 2026 16:01:38 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?500=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entrypoint/ActivityParticipationApi.kt | 11 +++-- .../ActivityParticipationController.kt | 11 ++--- .../ActivityParticipationIntegrationTest.kt | 41 +++++++++++++++++++ 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/picklab/backend/participation/entrypoint/ActivityParticipationApi.kt b/src/main/kotlin/picklab/backend/participation/entrypoint/ActivityParticipationApi.kt index 0b9e9180..5a8311c8 100644 --- a/src/main/kotlin/picklab/backend/participation/entrypoint/ActivityParticipationApi.kt +++ b/src/main/kotlin/picklab/backend/participation/entrypoint/ActivityParticipationApi.kt @@ -3,6 +3,9 @@ package picklab.backend.participation.entrypoint import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min import org.springframework.http.ResponseEntity import picklab.backend.common.model.MemberPrincipal import picklab.backend.common.model.PageResponse @@ -42,7 +45,7 @@ interface ActivityParticipationApi { fun updateApplicationStatus( member: MemberPrincipal, participationId: Long, - request: UpdateApplicationStatusRequest, + @Valid request: UpdateApplicationStatusRequest, ): ResponseEntity> @Operation( @@ -52,7 +55,7 @@ interface ActivityParticipationApi { fun updateProgressStatus( member: MemberPrincipal, participationId: Long, - request: UpdateProgressStatusRequest, + @Valid request: UpdateProgressStatusRequest, ): ResponseEntity> @Operation( @@ -62,8 +65,8 @@ interface ActivityParticipationApi { fun getResults( member: MemberPrincipal, applicationStatus: List?, - page: Int, - size: Int, + @Min(1) page: Int, + @Min(1) @Max(100) size: Int, ): ResponseEntity>> @Operation( diff --git a/src/main/kotlin/picklab/backend/participation/entrypoint/ActivityParticipationController.kt b/src/main/kotlin/picklab/backend/participation/entrypoint/ActivityParticipationController.kt index dab30c96..fe3c3a8d 100644 --- a/src/main/kotlin/picklab/backend/participation/entrypoint/ActivityParticipationController.kt +++ b/src/main/kotlin/picklab/backend/participation/entrypoint/ActivityParticipationController.kt @@ -1,8 +1,5 @@ package picklab.backend.participation.entrypoint -import jakarta.validation.Valid -import jakarta.validation.constraints.Max -import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -53,7 +50,7 @@ class ActivityParticipationController( override fun updateApplicationStatus( @AuthenticationPrincipal member: MemberPrincipal, @PathVariable participationId: Long, - @Valid @RequestBody request: UpdateApplicationStatusRequest, + @RequestBody request: UpdateApplicationStatusRequest, ): ResponseEntity> { activityParticipationUseCase.updateApplicationStatus( memberId = member.memberId, @@ -67,7 +64,7 @@ class ActivityParticipationController( override fun updateProgressStatus( @AuthenticationPrincipal member: MemberPrincipal, @PathVariable participationId: Long, - @Valid @RequestBody request: UpdateProgressStatusRequest, + @RequestBody request: UpdateProgressStatusRequest, ): ResponseEntity> { activityParticipationUseCase.updateProgressStatus( memberId = member.memberId, @@ -81,8 +78,8 @@ class ActivityParticipationController( override fun getResults( @AuthenticationPrincipal member: MemberPrincipal, @RequestParam(required = false) applicationStatus: List?, - @RequestParam(defaultValue = "1") @Min(1) page: Int, - @RequestParam(defaultValue = "10") @Min(1) @Max(100) size: Int, + @RequestParam(defaultValue = "1") page: Int, + @RequestParam(defaultValue = "10") size: Int, ): ResponseEntity>> { val response = activityParticipationUseCase.getResults( diff --git a/src/test/kotlin/picklab/backend/participation/ActivityParticipationIntegrationTest.kt b/src/test/kotlin/picklab/backend/participation/ActivityParticipationIntegrationTest.kt index 86d03f0f..5839b2b6 100644 --- a/src/test/kotlin/picklab/backend/participation/ActivityParticipationIntegrationTest.kt +++ b/src/test/kotlin/picklab/backend/participation/ActivityParticipationIntegrationTest.kt @@ -179,6 +179,47 @@ class ActivityParticipationIntegrationTest : IntegrationTest() { .andExpect { jsonPath("$.data.completed_count") { value(1) } } } + @Test + @DisplayName("[성공] 활동 결과 목록을 조회한다") + fun getResults() { + val participation = + participationRepository.save( + ActivityParticipation( + applicationStatus = ApplicationStatus.APPLIED, + progressStatus = ProgressStatus.NOT_SELECTED, + member = member, + activity = activity, + ), + ) + + mockMvc + .get("/v1/activity-participations/results") { + param("page", "1") + param("size", "10") + }.andExpect { status { isOk() } } + .andExpect { jsonPath("$.data.items[0].participation_id") { value(participation.id) } } + .andExpect { jsonPath("$.data.items[0].activity_id") { value(activity.id) } } + .andExpect { jsonPath("$.data.items[0].title") { value("테스트 대외활동") } } + .andExpect { jsonPath("$.data.page") { value(1) } } + .andExpect { jsonPath("$.data.size") { value(10) } } + .andExpect { jsonPath("$.data.total_elements") { value(1) } } + + mockMvc + .get("/v1/activity-participations/results") + .andExpect { status { isOk() } } + + mockMvc + .get("/v1/activity-participations/results") { + param("size", "1") + }.andExpect { status { isOk() } } + .andExpect { jsonPath("$.data.size") { value(1) } } + + mockMvc + .get("/v1/activity-participations/results") { + param("page", "0") + }.andExpect { status { isBadRequest() } } + } + @Test @DisplayName("[실패] 최종 합격 상태가 아니면 수료 여부를 수정할 수 없다") fun cannotUpdateProgressStatusWithoutAccepted() {