Skip to content

MA_02 구현 #22 #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 23, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ class CustomSuccessHandler(

response!!.addCookie(createCookie("access_token", accessToken))
// response.sendRedirect("https://frontend-sarang.vercel.app")
response.sendRedirect("http://localhost:8080")
response.sendRedirect("http://localhost:5173")
}

private fun createCookie(key: String, value: String): Cookie {
val cookie = Cookie(key, value)
cookie.path = "/"
cookie.isHttpOnly = true
cookie.secure = true
cookie.secure = false //Todo : 밋업 할 때는 true로 변경하기
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

보안 취약점: 쿠키 secure 플래그가 비활성화되었습니다.

개발 환경에서는 적절할 수 있지만, 쿠키의 secure 플래그를 false로 설정하면 HTTP를 통해 민감한 쿠키가 전송될 수 있으므로 보안 위험이 있습니다. 주석에 작성된 대로 배포 시에는 반드시 true로 변경해야 합니다.

다음과 같이 환경에 따라 자동으로 설정되도록 변경할 것을 권장합니다:

- cookie.secure = false //Todo : 밋업 할 때는 true로 변경하기
+ cookie.secure = environment.activeProfiles.contains("prod") || environment.activeProfiles.contains("stage")

🏁 Script executed:

#!/bin/bash
# 환경 설정 파일을 확인하여 개발/프로덕션 환경 분리가 되어있는지 확인
fd --type f "application-prod.*properties|application-prod.*yml" --exec grep -l "cookie|secure" {} \;

Length of output: 106


쿠키 secure 플래그 설정 자동화 필요

현재 CustomSuccessHandler.kt의 cookie.secure = false가 하드코딩되어 있어 배포 시에도 HTTP로 민감한 쿠키가 전송될 수 있는 보안 취약점이 존재합니다. 운영 환경에서는 반드시 secure = true로 설정되어야 하므로, 환경에 따라 자동으로 설정하도록 아래와 같이 수정하세요.

  • 환경 설정(application.yml 또는 application.properties)에 쿠키 보안 플래그용 프로퍼티 추가
    예시 (application.yml):
    security:
      cookie:
        secure: ${COOKIE_SECURE:false}
  • 코드에서 하드코딩된 값을 프로퍼티로 주입받도록 변경
    // 핸들러 클래스 상단에 import 및 프로퍼티 주입 추가
    @Value("\${security.cookie.secure}")
    private lateinit var cookieSecure: Boolean
    
    // 기존 코드 수정
    // cookie.secure = false // Todo: 밋업 할 때는 true로 변경하기
    cookie.secure = cookieSecure
  • 운영(staging/production) 환경에서는 CI/CD 혹은 환경변수로 COOKIE_SECURE=true를 설정하여 자동 반영되도록 구성

위 작업을 통해 개발 환경에서는 기본값(false)을, 운영 환경에서는 true를 자동으로 적용하여 보안 취약점을 제거할 수 있습니다.

cookie.maxAge = 1800
return cookie
}
Expand Down
10 changes: 9 additions & 1 deletion src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Member(
var isCouple: Boolean = false,

@Column(name = "emotion")
var emotion: String = "",
var emotion : Int? = null,

@Column(name = "fcm_token", nullable = false)
var fcmToken: String = "",
Expand Down Expand Up @@ -74,4 +74,12 @@ class Member(
fun updateCoupleStatus() {
this.isCouple = !this.isCouple
}

fun updateEmotion(emotion: Int) {
this.emotion = emotion
}
Comment on lines +78 to +80
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

emotion 값 유효성 검증 추가 필요

updateEmotion 메소드에서 입력값의 유효성 검증이 누락되었습니다. DTO에서는 1에서 7 사이의 값으로 제한하고 있지만, 엔티티 수준에서도 이 검증이 필요합니다.

 fun updateEmotion(emotion: Int) {
+    require(emotion in 1..7) { "감정 값은 1에서 7 사이여야 합니다." }
     this.emotion = emotion
 }


fun updateStatusMessage(statusMessage: String) {
this.statusMessage = statusMessage
}
Comment on lines +82 to +84
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

상태 메시지 길이 제한 검증 누락

updateStatusMessage 메소드에서 상태 메시지 길이에 대한 유효성 검증이 누락되었습니다. DTO에서는 최대 25자로 제한하고 있지만, 엔티티 수준에서도 이 검증이 필요합니다.

 fun updateStatusMessage(statusMessage: String) {
+    require(statusMessage.length <= 25) { "상태 메시지는 25자 이하여야 합니다." }
     this.statusMessage = statusMessage
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun updateStatusMessage(statusMessage: String) {
this.statusMessage = statusMessage
}
fun updateStatusMessage(statusMessage: String) {
require(statusMessage.length <= 25) { "상태 메시지는 25자 이하여야 합니다." }
this.statusMessage = statusMessage
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gomushin.backend.member.domain.service
import gomushin.backend.core.infrastructure.exception.BadRequestException
import gomushin.backend.member.domain.entity.Member
import gomushin.backend.member.domain.repository.MemberRepository
import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -21,4 +22,11 @@ class MemberService(
fun findById(id: Long): Member? {
return memberRepository.findByIdOrNull(id)
}

@Transactional
fun updateMyEmotionAndStatusMessage(id: Long, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest) {
val member = getById(id)
member.updateEmotion(updateMyEmotionAndStatusMessageRequest.emotion)
member.updateStatusMessage(updateMyEmotionAndStatusMessageRequest.statusMessage)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package gomushin.backend.member.dto.request

import io.swagger.v3.oas.annotations.media.Schema


data class UpdateMyEmotionAndStatusMessageRequest(
@Schema(description = "이모지(1 : 보고싶어요, 2: 기분 좋아요, 3 : 아무느낌 없어요, " +
"4 : 피곤해요, 5: 서운해요, 6 : 걱정돼요, 7 : 짜증나요)", example = "1")
val emotion : Int,

@Schema(description = "상태 메시지", example = "보고 싶어요")
val statusMessage : String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package gomushin.backend.member.dto.response

import gomushin.backend.member.domain.entity.Member

data class MyStatusMessageResponse (
val statusMessage : String?
) {
companion object {
fun of(member: Member) = MyStatusMessageResponse(
member.statusMessage
)
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package gomushin.backend.member.facade

import gomushin.backend.core.CustomUserDetails
import gomushin.backend.member.domain.service.MemberService
import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest
import gomushin.backend.member.dto.response.MyInfoResponse
import gomushin.backend.member.dto.response.MyStatusMessageResponse
import org.springframework.stereotype.Component

@Component
Expand All @@ -13,4 +15,12 @@ class MemberInfoFacade(
val member = memberService.getById(customUserDetails.getId())
return MyInfoResponse.of(member)
}

fun getMyStatusMessage(customUserDetails: CustomUserDetails): MyStatusMessageResponse {
val member = memberService.getById(customUserDetails.getId())
return MyStatusMessageResponse.of(member)
}

fun updateMyEmotionAndStatusMessage(customUserDetails: CustomUserDetails, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest)
= memberService.updateMyEmotionAndStatusMessage(customUserDetails.getId(), updateMyEmotionAndStatusMessageRequest)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ package gomushin.backend.member.presentation
object ApiPath {
const val ONBOARDING = "/v1/member/onboarding"
const val MY_INFO = "/v1/member/my-info"
const val MY_STATUS_MESSAGE = "/v1/member/my-status-message"
const val UPDATE_MY_EMOTION_AND_STATUS_MESSAGE = "/v1/member/my-emotion-and-status-message"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ package gomushin.backend.member.presentation

import gomushin.backend.core.CustomUserDetails
import gomushin.backend.core.common.web.response.ApiResponse
import gomushin.backend.core.infrastructure.exception.BadRequestException
import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest
import gomushin.backend.member.facade.MemberInfoFacade
import gomushin.backend.member.dto.response.MyInfoResponse
import gomushin.backend.member.dto.response.MyStatusMessageResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.validation.BindingResult
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController

Expand All @@ -27,4 +35,25 @@ class MemberInfoController(
val member = memberInfoFacade.getMemberInfo(customUserDetails)
return ApiResponse.success(member)
}

@ResponseStatus(HttpStatus.OK)
@GetMapping(ApiPath.MY_STATUS_MESSAGE)
@Operation(summary = "내 상태 메시지 조회", description = "getMyStatusMessage")
fun getMyStatusMessage(
@AuthenticationPrincipal customUserDetails: CustomUserDetails
): ApiResponse<MyStatusMessageResponse> {
val statusMessage = memberInfoFacade.getMyStatusMessage(customUserDetails)
return ApiResponse.success(statusMessage)
}

@ResponseStatus(HttpStatus.OK)
@PostMapping(ApiPath.UPDATE_MY_EMOTION_AND_STATUS_MESSAGE)
@Operation(summary = "내 상태 이모지 및 상태 메시지 저장", description = "updateMyEmotionAndStatusMessage")
fun updateMyEmotionAndStatusMessage(
@AuthenticationPrincipal customUserDetails: CustomUserDetails,
@RequestBody updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest
): ApiResponse<Boolean> {
memberInfoFacade.updateMyEmotionAndStatusMessage(customUserDetails, updateMyEmotionAndStatusMessageRequest)
return ApiResponse.success(true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import gomushin.backend.member.domain.entity.Member
import gomushin.backend.member.domain.repository.MemberRepository
import gomushin.backend.member.domain.value.Provider
import gomushin.backend.member.domain.value.Role
import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
Expand Down Expand Up @@ -46,4 +47,30 @@ class MemberServiceTest {
// then
assertEquals(expectedMember, result)
}

@DisplayName("이모지 및 상태 메시지 업데이트 - 성공")
@Test
fun updateMyEmotionAndStatusMessage() {
// given
val memberId = 1L
val expectedMember = Member(
id = 1L,
name = "테스트",
nickname = "테스트 닉네임",
email = "[email protected]",
birthDate = null,
profileImageUrl = null,
provider = Provider.KAKAO,
role = Role.GUEST,
emotion = 1,
statusMessage = "상태 변경전"
)
val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(2, "상태 변경후")
//when
`when`(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember))
val result = memberService.updateMyEmotionAndStatusMessage(memberId, updateMyEmotionAndStatusMessageRequest)
//then
assertEquals(expectedMember.emotion, updateMyEmotionAndStatusMessageRequest.emotion)
assertEquals(expectedMember.statusMessage, updateMyEmotionAndStatusMessageRequest.statusMessage)
}
Comment on lines +51 to +75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

테스트 메서드 개선 필요

테스트 구현에 몇 가지 개선이 필요합니다:

  1. 서비스 구현에서는 findByIdOrNull을 사용하지만 테스트에서는 findById를 모킹하고 있어 불일치가 있습니다.
  2. 서비스 메서드가 현재 아무것도 반환하지 않기 때문에 71번 줄의 result 변수는 불필요합니다.
@DisplayName("이모지 및 상태 메시지 업데이트 - 성공")
@Test
fun updateMyEmotionAndStatusMessage() {
    // given
    val memberId = 1L
    val expectedMember = Member(
        id = 1L,
        name = "테스트",
        nickname = "테스트 닉네임",
        email = "[email protected]",
        birthDate = null,
        profileImageUrl = null,
        provider = Provider.KAKAO,
        role = Role.GUEST,
        emotion = 1,
        statusMessage = "상태 변경전"
    )
    val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(2, "상태 변경후")
    //when
-   `when`(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember))
-   val result = memberService.updateMyEmotionAndStatusMessage(memberId, updateMyEmotionAndStatusMessageRequest)
+   `when`(memberRepository.findByIdOrNull(memberId)).thenReturn(expectedMember)
+   memberService.updateMyEmotionAndStatusMessage(memberId, updateMyEmotionAndStatusMessageRequest)
    //then
    assertEquals(expectedMember.emotion, updateMyEmotionAndStatusMessageRequest.emotion)
    assertEquals(expectedMember.statusMessage, updateMyEmotionAndStatusMessageRequest.statusMessage)
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@DisplayName("이모지 및 상태 메시지 업데이트 - 성공")
@Test
fun updateMyEmotionAndStatusMessage() {
// given
val memberId = 1L
val expectedMember = Member(
id = 1L,
name = "테스트",
nickname = "테스트 닉네임",
email = "[email protected]",
birthDate = null,
profileImageUrl = null,
provider = Provider.KAKAO,
role = Role.GUEST,
emotion = 1,
statusMessage = "상태 변경전"
)
val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(2, "상태 변경후")
//when
`when`(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember))
val result = memberService.updateMyEmotionAndStatusMessage(memberId, updateMyEmotionAndStatusMessageRequest)
//then
assertEquals(expectedMember.emotion, updateMyEmotionAndStatusMessageRequest.emotion)
assertEquals(expectedMember.statusMessage, updateMyEmotionAndStatusMessageRequest.statusMessage)
}
@DisplayName("이모지 및 상태 메시지 업데이트 - 성공")
@Test
fun updateMyEmotionAndStatusMessage() {
// given
val memberId = 1L
val expectedMember = Member(
id = 1L,
name = "테스트",
nickname = "테스트 닉네임",
email = "[email protected]",
birthDate = null,
profileImageUrl = null,
provider = Provider.KAKAO,
role = Role.GUEST,
emotion = 1,
statusMessage = "상태 변경전"
)
val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(2, "상태 변경후")
//when
`when`(memberRepository.findByIdOrNull(memberId)).thenReturn(expectedMember)
memberService.updateMyEmotionAndStatusMessage(memberId, updateMyEmotionAndStatusMessageRequest)
//then
assertEquals(expectedMember.emotion, updateMyEmotionAndStatusMessageRequest.emotion)
assertEquals(expectedMember.statusMessage, updateMyEmotionAndStatusMessageRequest.statusMessage)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import gomushin.backend.member.domain.entity.Member
import gomushin.backend.member.domain.service.MemberService
import gomushin.backend.member.domain.value.Provider
import gomushin.backend.member.domain.value.Role
import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -37,6 +38,7 @@ class MemberInfoFacadeTest {
profileImageUrl = null,
provider = Provider.KAKAO,
role = Role.MEMBER,
statusMessage = "상태 메시지"
)

customUserDetails = mock(CustomUserDetails::class.java)
Expand All @@ -56,4 +58,27 @@ class MemberInfoFacadeTest {
verify(memberService).getById(1L)
assertEquals(member.nickname, result.nickname)
}

@DisplayName("내 상태 메시지 조회")
@Test
fun getMyStatusMessage() {
//given
`when`(memberService.getById(customUserDetails.getId())).thenReturn(member)
//when
val result = memberInfoFacade.getMyStatusMessage(customUserDetails)
//then
verify(memberService).getById(1L)
assertEquals(member.statusMessage, result.statusMessage)
}

@DisplayName("이모지 및 상태 메시지 업데이트")
@Test
fun updateMyEmotionAndStatusMessage() {
//given
val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(1, "좋은 날씨야")
//when
val result = memberInfoFacade.updateMyEmotionAndStatusMessage(customUserDetails, updateMyEmotionAndStatusMessageRequest)
//then
verify(memberService).updateMyEmotionAndStatusMessage(1L, updateMyEmotionAndStatusMessageRequest)
}
Comment on lines +74 to +83
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

이모지 및 상태 메시지 업데이트 테스트 개선 필요

이모지 및 상태 메시지 업데이트 기능에 대한 테스트가 구현되었지만, 결과값에 대한 구체적인 검증이 누락되었습니다. 서비스 메소드 호출 여부만 검증하고 있어, 실제 반환값이나 상태 변경에 대한 검증이 필요합니다.

 @DisplayName("이모지 및 상태 메시지 업데이트")
 @Test
 fun updateMyEmotionAndStatusMessage() {
     //given
     val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(1, "좋은 날씨야")
+    val updatedMember = Member(
+        id = 1L,
+        name = "테스트",
+        nickname = "테스트 닉네임",
+        email = "[email protected]",
+        profileImageUrl = null,
+        provider = Provider.KAKAO,
+        role = Role.MEMBER,
+        emotion = 1,
+        statusMessage = "좋은 날씨야"
+    )
+    `when`(memberService.updateMyEmotionAndStatusMessage(1L, updateMyEmotionAndStatusMessageRequest)).thenReturn(updatedMember)
     //when
     val result = memberInfoFacade.updateMyEmotionAndStatusMessage(customUserDetails, updateMyEmotionAndStatusMessageRequest)
     //then
     verify(memberService).updateMyEmotionAndStatusMessage(1L, updateMyEmotionAndStatusMessageRequest)
+    assertEquals(updatedMember, result)
 }

}