Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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, "알림 변경에 성공했습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,19 @@ 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)
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<InterestedJobCategory>): MemberMeResult =
fun Member.toMemberMeResult(
interestedJobCategories: List<InterestedJobCategory>,
emailAgreement: Boolean,
notificationPreference: NotificationPreference,
): MemberMeResult =
MemberMeResult(
name = this.name,
nickname = this.nickname,
Expand All @@ -14,4 +19,7 @@ fun Member.toMemberMeResult(interestedJobCategories: List<InterestedJobCategory>
jobFields = interestedJobCategories.map { it.jobCategory.jobGroup }.distinct(),
employmentStatus = this.employmentStatus,
company = this.company,
emailAgreement = emailAgreement,
notifyPopularActivity = notificationPreference.notifyPopularActivity,
notifyBookmarkedActivity = notificationPreference.notifyBookmarkedActivity,
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ data class MemberMeResult(
val jobFields: List<JobGroup>,
val employmentStatus: String,
val company: String,
val emailAgreement: Boolean,
val notifyPopularActivity: Boolean,
val notifyBookmarkedActivity: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ class MemberService(
fun findInterestedJobCategories(memberId: Long): List<InterestedJobCategory> =
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)
Expand Down
18 changes: 18 additions & 0 deletions src/main/kotlin/picklab/backend/member/entrypoint/MemberApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ 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
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
Expand All @@ -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 {
Expand All @@ -42,6 +45,21 @@ interface MemberApi {
@Valid @RequestBody request: AdditionalInfoRequest,
): ResponseEntity<ResponseWrapper<Unit>>

@Operation(
summary = "닉네임 사용 가능 여부 조회",
description = "회원가입에 사용할 닉네임의 중복 여부를 확인합니다.",
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "닉네임 사용 가능 여부 조회에 성공했습니다."),
ApiResponse(responseCode = "400", description = "닉네임 형식이 올바르지 않습니다."),
ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다."),
],
)
fun checkNicknameAvailability(
@Valid @ParameterObject request: NicknameAvailabilityRequest,
): ResponseEntity<ResponseWrapper<NicknameAvailabilityResponse>>

@Operation(
summary = "사용자 정보 수정",
description = "사용자의 정보를 수정한다",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -50,6 +53,15 @@ class MemberController(
.body(ResponseWrapper.success(SuccessCode.SIGNUP_SUCCESS, Unit))
}

@GetMapping("/nickname-availability")
override fun checkNicknameAvailability(
@Valid @ModelAttribute request: NicknameAvailabilityRequest,
): ResponseEntity<ResponseWrapper<NicknameAvailabilityResponse>> =
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
),
)
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ data class GetMemberMeResponse(
val jobFields: List<JobGroup>,
@field:Schema(description = "재직 상태")
val employment: EmploymentInfoResponse,
@field:Schema(description = "이메일 마케팅 수신 동의 여부")
val emailAgreement: Boolean,
@field:Schema(description = "알림 수신 설정")
val notificationPreferences: NotificationPreferencesResponse,
)

data class EmploymentInfoResponse(
Expand All @@ -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,
)
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -42,7 +45,7 @@ interface ActivityParticipationApi {
fun updateApplicationStatus(
member: MemberPrincipal,
participationId: Long,
request: UpdateApplicationStatusRequest,
@Valid request: UpdateApplicationStatusRequest,
): ResponseEntity<ResponseWrapper<Unit>>

@Operation(
Expand All @@ -52,7 +55,7 @@ interface ActivityParticipationApi {
fun updateProgressStatus(
member: MemberPrincipal,
participationId: Long,
request: UpdateProgressStatusRequest,
@Valid request: UpdateProgressStatusRequest,
): ResponseEntity<ResponseWrapper<Unit>>

@Operation(
Expand All @@ -62,8 +65,8 @@ interface ActivityParticipationApi {
fun getResults(
member: MemberPrincipal,
applicationStatus: List<ApplicationStatus>?,
page: Int,
size: Int,
@Min(1) page: Int,
@Min(1) @Max(100) size: Int,
): ResponseEntity<ResponseWrapper<PageResponse<ActivityParticipationResultResponse>>>

@Operation(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -53,7 +50,7 @@ class ActivityParticipationController(
override fun updateApplicationStatus(
@AuthenticationPrincipal member: MemberPrincipal,
@PathVariable participationId: Long,
@Valid @RequestBody request: UpdateApplicationStatusRequest,
@RequestBody request: UpdateApplicationStatusRequest,
): ResponseEntity<ResponseWrapper<Unit>> {
activityParticipationUseCase.updateApplicationStatus(
memberId = member.memberId,
Expand All @@ -67,7 +64,7 @@ class ActivityParticipationController(
override fun updateProgressStatus(
@AuthenticationPrincipal member: MemberPrincipal,
@PathVariable participationId: Long,
@Valid @RequestBody request: UpdateProgressStatusRequest,
@RequestBody request: UpdateProgressStatusRequest,
): ResponseEntity<ResponseWrapper<Unit>> {
activityParticipationUseCase.updateProgressStatus(
memberId = member.memberId,
Expand All @@ -81,8 +78,8 @@ class ActivityParticipationController(
override fun getResults(
@AuthenticationPrincipal member: MemberPrincipal,
@RequestParam(required = false) applicationStatus: List<ApplicationStatus>?,
@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<ResponseWrapper<PageResponse<ActivityParticipationResultResponse>>> {
val response =
activityParticipationUseCase.getResults(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 전체)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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("회원 정보 수정")
Expand Down
Loading
Loading