diff --git a/data/src/main/kotlin/com/kw/data/domain/member/Member.kt b/data/src/main/kotlin/com/kw/data/domain/member/Member.kt index 2c409fc..7756ef1 100644 --- a/data/src/main/kotlin/com/kw/data/domain/member/Member.kt +++ b/data/src/main/kotlin/com/kw/data/domain/member/Member.kt @@ -38,6 +38,14 @@ class Member(email: String) : Base() { var memberRoles : MutableList = mutableListOf(MemberRoleType.ROLE_USER) + @OneToMany(mappedBy = "member", cascade = [CascadeType.ALL], orphanRemoval = true) + var snsList: MutableList = mutableListOf() + protected set + + @OneToMany(mappedBy = "member", cascade = [CascadeType.ALL], orphanRemoval = true) + var memberTags: MutableList = mutableListOf() + protected set + fun updateMemberNickname(nickname: String) { this.nickname = nickname } @@ -50,6 +58,22 @@ class Member(email: String) : Base() { this.deletedAt = LocalDateTime.now() } + fun updateMemberSns(snsList: List) { + if (snsList.size > 3) { + throw IllegalArgumentException("Sns는 최대 3개까지 지정 가능합니다.") + } + this.snsList.clear() + this.snsList.addAll(snsList) + } + + fun updateMemberTags(memberTags: List) { + if (memberTags.size > 3) { + throw IllegalArgumentException("관심 태그는 최대 3개까지 지정 가능합니다.") + } + this.memberTags.clear() + this.memberTags.addAll(memberTags) + } + enum class MemberRoleType { ROLE_USER, ROLE_ADMIN diff --git a/data/src/main/kotlin/com/kw/data/domain/member/MemberTag.kt b/data/src/main/kotlin/com/kw/data/domain/member/MemberTag.kt new file mode 100644 index 0000000..45127c2 --- /dev/null +++ b/data/src/main/kotlin/com/kw/data/domain/member/MemberTag.kt @@ -0,0 +1,21 @@ +package com.kw.data.domain.member + +import com.kw.data.domain.Base +import com.kw.data.domain.tag.Tag +import jakarta.persistence.* + +@Entity +class MemberTag(member: Member, tag: Tag): Base() { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false, updatable = false) + val id: Long? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + val member: Member = member + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id", nullable = false) + val tag: Tag = tag +} diff --git a/data/src/main/kotlin/com/kw/data/domain/member/Sns.kt b/data/src/main/kotlin/com/kw/data/domain/member/Sns.kt index 9fc14ab..05e138a 100644 --- a/data/src/main/kotlin/com/kw/data/domain/member/Sns.kt +++ b/data/src/main/kotlin/com/kw/data/domain/member/Sns.kt @@ -4,7 +4,7 @@ import com.kw.data.domain.Base import jakarta.persistence.* @Entity -class Sns(name: String, url: String) : Base() { +class Sns(name: String, url: String, member: Member) : Base() { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false, updatable = false) @@ -17,4 +17,8 @@ class Sns(name: String, url: String) : Base() { @Column(name = "url", nullable = false) var url: String = url protected set + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + val member: Member = member } diff --git a/infra/infra-security/build.gradle.kts b/infra/infra-security/build.gradle.kts index d0339f1..b30ea7f 100644 --- a/infra/infra-security/build.gradle.kts +++ b/infra/infra-security/build.gradle.kts @@ -25,4 +25,7 @@ dependencies { // redis implementation("org.springframework.boot:spring-boot-starter-data-redis") + + // swagger + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0") } diff --git a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/oauth/OAuth2SuccessHandler.kt b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/oauth/OAuth2SuccessHandler.kt index b37cb1c..75efe8e 100644 --- a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/oauth/OAuth2SuccessHandler.kt +++ b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/oauth/OAuth2SuccessHandler.kt @@ -18,7 +18,8 @@ import org.springframework.transaction.annotation.Transactional @Transactional class OAuth2SuccessHandler(private val jwtTokenProvider: JwtTokenProvider, private val memberRepository: MemberRepository, - private val redisRefreshTokenRepository: RedisRefreshTokenRepository) : AuthenticationSuccessHandler { + private val redisRefreshTokenRepository: RedisRefreshTokenRepository, + private val httpResponseUtil: HttpResponseUtil) : AuthenticationSuccessHandler { override fun onAuthenticationSuccess( request: HttpServletRequest?, @@ -29,11 +30,13 @@ class OAuth2SuccessHandler(private val jwtTokenProvider: JwtTokenProvider, val email = principal.getAttribute("email") var member = Member(email = email!!) + var isSignUp = false if(isMember(email)){ member = getMember(email) } else { member = createMember(member) + isSignUp = true } val authorities = member.memberRoles.map { memberRole -> SimpleGrantedAuthority(memberRole.toString()) @@ -50,7 +53,7 @@ class OAuth2SuccessHandler(private val jwtTokenProvider: JwtTokenProvider, redisRefreshTokenRepository.save(refreshToken = refreshToken, memberId = member.id!!) - HttpResponseUtil.writeResponse(response!!, accessToken, refreshToken) + httpResponseUtil.writeResponse(response!!, accessToken, refreshToken, isSignUp) } private fun getMember(email : String) : Member { diff --git a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMember.kt b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMember.kt index 661bfee..ade8552 100644 --- a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMember.kt +++ b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMember.kt @@ -1,5 +1,8 @@ package com.kw.infrasecurity.resolver +import io.swagger.v3.oas.annotations.Parameter + +@Parameter(hidden = true) @Retention(value = AnnotationRetention.RUNTIME) @Target(AnnotationTarget.VALUE_PARAMETER) annotation class AuthToMember { diff --git a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMemberArgumentResolver.kt b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMemberArgumentResolver.kt index 95ea264..af2ca37 100644 --- a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMemberArgumentResolver.kt +++ b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMemberArgumentResolver.kt @@ -2,9 +2,9 @@ package com.kw.infrasecurity.resolver import com.kw.data.domain.member.Member import com.kw.data.domain.member.repository.MemberRepository -import com.kw.infrasecurity.oauth.OAuth2UserDetails import org.springframework.core.MethodParameter import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.core.user.DefaultOAuth2User import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.support.WebDataBinderFactory @@ -27,11 +27,9 @@ class AuthToMemberArgumentResolver(val memberRepository: MemberRepository) : Han webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory? ): Any? { - val authentication = SecurityContextHolder.getContext().authentication - ?: throw IllegalArgumentException("접근이 거부되었습니다.") - val userDetails = authentication.principal as OAuth2UserDetails - val member = memberRepository.findMemberByEmail(userDetails.email) - ?: throw IllegalArgumentException("존재하지 않는 회원입니다.") + val authentication = SecurityContextHolder.getContext().authentication ?: throw IllegalArgumentException("접근이 거부되었습니다.") + val userDetails = authentication.principal as DefaultOAuth2User + val member = memberRepository.findMemberByEmail(userDetails.getAttribute("email").toString()) ?: throw IllegalArgumentException("존재하지 않는 회원입니다.") isMemberWithdraw(member) diff --git a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/util/HttpResponseUtil.kt b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/util/HttpResponseUtil.kt index 55d278e..09cd72f 100644 --- a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/util/HttpResponseUtil.kt +++ b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/util/HttpResponseUtil.kt @@ -1,21 +1,21 @@ package com.kw.infrasecurity.util -import com.fasterxml.jackson.databind.ObjectMapper import jakarta.servlet.http.HttpServletResponse -import org.springframework.http.HttpStatus -import org.springframework.http.MediaType +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component -class HttpResponseUtil { - data class tokenRespone(val accessToken: String, val refreshToken: String) +@Component +class HttpResponseUtil(@Value("\${client-redirect-url}") val REDIRECT_URL: String) { - companion object { - fun writeResponse(response : HttpServletResponse, accessToken : String, refreshToken : String) { - val objectMapper = ObjectMapper() - val responseBody: String = objectMapper.writeValueAsString(tokenRespone(accessToken, refreshToken)) - response.contentType = MediaType.APPLICATION_JSON_VALUE - response.status = HttpStatus.OK.value() - response.characterEncoding = "UTF-8" - response.writer.write(responseBody) + fun writeResponse(response : HttpServletResponse, accessToken : String, refreshToken : String, isSignUp : Boolean) { + var redirectUrl = REDIRECT_URL + if(isSignUp) { + redirectUrl += "/welcome" } + val sb = StringBuffer(redirectUrl) + sb.append("?").append("access-token=").append(accessToken) + sb.append("&").append("refresh-token=").append(refreshToken) + + response.sendRedirect(sb.toString()) } } diff --git a/server/api/src/main/kotlin/com/kw/api/domain/member/controller/MemberController.kt b/server/api/src/main/kotlin/com/kw/api/domain/member/controller/MemberController.kt index 708d25e..0ba390d 100644 --- a/server/api/src/main/kotlin/com/kw/api/domain/member/controller/MemberController.kt +++ b/server/api/src/main/kotlin/com/kw/api/domain/member/controller/MemberController.kt @@ -1,5 +1,6 @@ package com.kw.api.domain.member.controller +import com.kw.api.domain.member.dto.request.MemberSnsUpdateRequest import com.kw.api.domain.member.dto.response.MemberInfoResponse import com.kw.api.domain.member.service.MemberService import com.kw.data.domain.member.Member @@ -23,13 +24,29 @@ class MemberController(private val memberService: MemberService) { @Operation(summary = "사용자의 닉네임을 변경합니다.") @PatchMapping("/me/nickname") - fun updateMemberNickname( - @AuthToMember member: Member, - @RequestParam nickname: String - ): MemberInfoResponse { - return memberService.updateMemberNickname(member, nickname) + fun updateMemberNickname(@AuthToMember member: Member, + @RequestParam nickname: String): MemberInfoResponse { + val response = memberService.updateMemberNickname(member, nickname) + return response } + @Operation(summary = "사용자의 소셜 링크를 변경합니다.") + @PatchMapping("/me/sns") + fun updateMemberSns(@AuthToMember member: Member, + @RequestBody memberSnsUpdateRequest: MemberSnsUpdateRequest): MemberInfoResponse { + val response = memberService.updateMemberSns(member, memberSnsUpdateRequest) + return response + } + + @Operation(summary = "사용자의 관심 태그를 변경합니다.") + @PatchMapping("/me/tags") + fun updateMemberTags(@AuthToMember member: Member, + @RequestParam tagIds: List): MemberInfoResponse { + val response = memberService.updateMemberTags(member, tagIds) + return response + } + + @Operation(summary = "회원 프로필 사진을 저장합니다.") @PatchMapping(value = ["/me/profile-image"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) fun updateMemberProfileImage( @@ -38,4 +55,11 @@ class MemberController(private val memberService: MemberService) { ): MemberInfoResponse { return memberService.updateMemberProfileImage(member, file) } + + @Operation(summary = "회원 아이디로 회원 정보를 가져옵니다.") + @GetMapping("/{id}") + fun updateMemberProfileImage(@PathVariable id: Long): MemberInfoResponse { + val response = memberService.getMemberInfoById(id) + return response + } } diff --git a/server/api/src/main/kotlin/com/kw/api/domain/member/dto/request/MemberSnsUpdateRequest.kt b/server/api/src/main/kotlin/com/kw/api/domain/member/dto/request/MemberSnsUpdateRequest.kt new file mode 100644 index 0000000..be89eca --- /dev/null +++ b/server/api/src/main/kotlin/com/kw/api/domain/member/dto/request/MemberSnsUpdateRequest.kt @@ -0,0 +1,6 @@ +package com.kw.api.domain.member.dto.request + +data class MemberSnsUpdateRequest(val snsRequests: List) { + data class snsRequest(val name: String, + val url: String) +} diff --git a/server/api/src/main/kotlin/com/kw/api/domain/member/dto/response/MemberInfoResponse.kt b/server/api/src/main/kotlin/com/kw/api/domain/member/dto/response/MemberInfoResponse.kt index 4da62fb..f9beece 100644 --- a/server/api/src/main/kotlin/com/kw/api/domain/member/dto/response/MemberInfoResponse.kt +++ b/server/api/src/main/kotlin/com/kw/api/domain/member/dto/response/MemberInfoResponse.kt @@ -1,22 +1,44 @@ package com.kw.api.domain.member.dto.response +import com.kw.api.domain.tag.dto.response.TagResponse import com.kw.data.domain.member.Member data class MemberInfoResponse(val id: Long?, val nickname: String?, val email: String, val provider: Member.Provider, - val profileImage: String? + val profileImage: String?, + val snsList: List, + val memberTags: List ) { companion object { fun from(member: Member) : MemberInfoResponse { + val snsResponses = member.snsList.map { sns -> + SnsResponse( + name = sns.name, + url = sns.url + ) + }.toList() + + val tagResponses = member.memberTags.map { memberTag -> + TagResponse( + id = memberTag.tag.id, + name = memberTag.tag.name + ) + }.toList() + return MemberInfoResponse( id = member.id, nickname = member.nickname, email = member.email, provider = member.provider, - profileImage = member.profileImage + profileImage = member.profileImage, + snsList = snsResponses, + memberTags = tagResponses ) } } + + data class SnsResponse(val name: String, + val url: String) } diff --git a/server/api/src/main/kotlin/com/kw/api/domain/member/service/MemberService.kt b/server/api/src/main/kotlin/com/kw/api/domain/member/service/MemberService.kt index cbca6da..170b7c0 100644 --- a/server/api/src/main/kotlin/com/kw/api/domain/member/service/MemberService.kt +++ b/server/api/src/main/kotlin/com/kw/api/domain/member/service/MemberService.kt @@ -2,10 +2,15 @@ package com.kw.api.domain.member.service import com.kw.api.common.exception.ApiErrorCode import com.kw.api.common.exception.ApiException +import com.kw.api.domain.member.dto.request.MemberSnsUpdateRequest import com.kw.api.domain.member.dto.response.MemberInfoResponse import com.kw.data.domain.member.Member +import com.kw.data.domain.member.MemberTag +import com.kw.data.domain.member.Sns import com.kw.data.domain.member.repository.MemberRepository +import com.kw.data.domain.tag.repository.TagRepository import com.kw.infras3.service.S3Service +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile @@ -13,7 +18,8 @@ import org.springframework.web.multipart.MultipartFile @Service @Transactional class MemberService(private val memberRepository: MemberRepository, - private val s3Service: S3Service) { + private val s3Service: S3Service, + private val tagRepository: TagRepository) { @Transactional(readOnly = true) fun getMemberInfo(member: Member): MemberInfoResponse{ @@ -32,6 +38,34 @@ class MemberService(private val memberRepository: MemberRepository, return MemberInfoResponse.from(member) } + fun updateMemberSns(member: Member, memberSnsUpdateRequest: MemberSnsUpdateRequest): MemberInfoResponse { + val snsList = memberSnsUpdateRequest.snsRequests.map { snsRequest -> + Sns( + name = snsRequest.name, + url = snsRequest.url, + member = member + ) + }.toList() + + member.updateMemberSns(snsList) + return MemberInfoResponse.from(member) + } + + fun updateMemberTags(member: Member, tagIds: List): MemberInfoResponse { + val memberTags = tagIds.map { tagId -> + val tag = tagRepository.findByIdOrNull(tagId) ?: throw ApiException(ApiErrorCode.INCLUDE_NOT_FOUND_TAG) + MemberTag(member, tag) + }.toList() + + member.updateMemberTags(memberTags) + return MemberInfoResponse.from(member) + } + + fun getMemberInfoById(id: Long): MemberInfoResponse { + val member = memberRepository.findByIdOrNull(id) ?: throw ApiException(ApiErrorCode.NOT_FOUND_MEMBER) + return MemberInfoResponse.from(member) + } + private fun isNicknameUnique(nickname: String) { if(memberRepository.existsByNickname(nickname)){ throw ApiException(ApiErrorCode.NICKNAME_ALREADY_EXISTS) diff --git a/server/api/src/main/kotlin/com/kw/api/domain/question/controller/QuestionController.kt b/server/api/src/main/kotlin/com/kw/api/domain/question/controller/QuestionController.kt index 52ac70a..fade289 100644 --- a/server/api/src/main/kotlin/com/kw/api/domain/question/controller/QuestionController.kt +++ b/server/api/src/main/kotlin/com/kw/api/domain/question/controller/QuestionController.kt @@ -1,6 +1,7 @@ package com.kw.api.domain.question.controller import com.kw.api.domain.question.dto.request.QuestionCreateRequest +import com.kw.api.domain.question.dto.request.QuestionReportRequest import com.kw.api.domain.question.dto.request.QuestionSearchRequest import com.kw.api.domain.question.dto.request.QuestionUpdateRequest import com.kw.api.domain.question.dto.response.QuestionListResponse @@ -54,7 +55,7 @@ class QuestionController(val questionService: QuestionService) { @ResponseStatus(HttpStatus.CREATED) @PostMapping("/questions/{id}/report") fun reportQuestion( - @RequestParam reason: String, + @RequestBody reason: QuestionReportRequest, @PathVariable id: Long ): QuestionReportResponse { return questionService.reportQuestion(reason, id) diff --git a/server/api/src/main/kotlin/com/kw/api/domain/question/dto/request/QuestionReportRequest.kt b/server/api/src/main/kotlin/com/kw/api/domain/question/dto/request/QuestionReportRequest.kt new file mode 100644 index 0000000..3111c62 --- /dev/null +++ b/server/api/src/main/kotlin/com/kw/api/domain/question/dto/request/QuestionReportRequest.kt @@ -0,0 +1,3 @@ +package com.kw.api.domain.question.dto.request + +data class QuestionReportRequest(val reason: String) diff --git a/server/api/src/main/kotlin/com/kw/api/domain/question/service/QuestionService.kt b/server/api/src/main/kotlin/com/kw/api/domain/question/service/QuestionService.kt index 7a45e51..8bb93ee 100644 --- a/server/api/src/main/kotlin/com/kw/api/domain/question/service/QuestionService.kt +++ b/server/api/src/main/kotlin/com/kw/api/domain/question/service/QuestionService.kt @@ -4,6 +4,7 @@ import com.kw.api.common.dto.PageResponse import com.kw.api.common.exception.ApiErrorCode import com.kw.api.common.exception.ApiException import com.kw.api.domain.question.dto.request.QuestionCreateRequest +import com.kw.api.domain.question.dto.request.QuestionReportRequest import com.kw.api.domain.question.dto.request.QuestionSearchRequest import com.kw.api.domain.question.dto.request.QuestionUpdateRequest import com.kw.api.domain.question.dto.response.QuestionListResponse @@ -67,11 +68,11 @@ class QuestionService( questionRepository.delete(question) } - fun reportQuestion(reason: String, id: Long): QuestionReportResponse { + fun reportQuestion(request: QuestionReportRequest, id: Long): QuestionReportResponse { val question = getExistQuestion(id) val report = QuestionReport( - reason = reason, + reason = request.reason, question = question ) return QuestionReportResponse.from(questionReportRepository.save(report)) diff --git a/server/api/src/main/kotlin/com/kw/api/domain/tag/dto/response/TagResponse.kt b/server/api/src/main/kotlin/com/kw/api/domain/tag/dto/response/TagResponse.kt index 4eedd70..9f896f6 100644 --- a/server/api/src/main/kotlin/com/kw/api/domain/tag/dto/response/TagResponse.kt +++ b/server/api/src/main/kotlin/com/kw/api/domain/tag/dto/response/TagResponse.kt @@ -3,13 +3,13 @@ package com.kw.api.domain.tag.dto.response import com.kw.data.domain.tag.Tag data class TagResponse( - val id: Long, + val id: Long?, val name: String ) { companion object { fun from(tag: Tag): TagResponse { return TagResponse( - id = tag.id!!, + id = tag.id, name = tag.name ) }