From 805bceb85ea64c93ac3661a909e06627534c130a Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Sun, 4 Feb 2024 04:03:44 -0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20nickname=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20(escape)=20(#267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 공백, 특수문자 escape 처리 * fix: nickname null 예외 처리 * fix: spotlessApply * fix: 닉네임 수정 시 공백 허용 * fix: 삼항 연산자 수정 * feat: 닉네임 검사에 따른 테스트 케이스 추가 * feat: 닉네임 Pattern validate message 추가 * fix: nickname Pattern message 는 -> 은으로 변경 * chore: gitkeep 삭제 (잠수함 패치) * fix: nickname 유효성 공백 예외 처리 추가 * fix: spotlessApply --- .../domain/member/api/MemberController.java | 9 +-- .../member/application/MemberService.java | 23 ++++++-- .../domain/member/dao/MemberRepository.java | 6 +- .../domain/member/domain/Profile.java | 4 ++ .../domain/notification/application/.gitkeep | 0 .../global/error/exception/ErrorCode.java | 1 + .../member/application/MemberServiceTest.java | 58 +++++++++++++++++++ 7 files changed, 91 insertions(+), 10 deletions(-) delete mode 100644 src/main/java/com/depromeet/domain/notification/application/.gitkeep diff --git a/src/main/java/com/depromeet/domain/member/api/MemberController.java b/src/main/java/com/depromeet/domain/member/api/MemberController.java index d86ade861..20e7078a6 100644 --- a/src/main/java/com/depromeet/domain/member/api/MemberController.java +++ b/src/main/java/com/depromeet/domain/member/api/MemberController.java @@ -44,7 +44,7 @@ public ResponseEntity memberUsernameCheck( return ResponseEntity.ok().build(); } - @Operation(summary = "닉네임 중복 체크", description = "닉네임 중복 체크를 진행합니다.") + @Operation(summary = "닉네임 유효성 체크", description = "닉네임 유효성 체크를 진행합니다.") @PostMapping("/check-nickname") public ResponseEntity memberNicknameCheck( @Valid @RequestBody NicknameCheckRequest request) { @@ -54,7 +54,8 @@ public ResponseEntity memberNicknameCheck( @Operation(summary = "닉네임으로 회원 검색", description = "닉네임으로 회원을 검색합니다.") @GetMapping("/search") - public List memberNicknameSearch(@RequestParam String nickname) { + public List memberNicknameSearch( + @RequestParam(required = false) String nickname) { return memberService.searchMemberNickname(nickname); } @@ -76,8 +77,8 @@ public ResponseEntity memberSocialInfoFind() { @Operation(summary = "회원 닉네임 변경", description = "회원 닉네임을 변경합니다.") @PutMapping("/me/nickname") public ResponseEntity memberNicknameUpdate( - @Valid @RequestBody NicknameUpdateRequest reqest) { - memberService.updateMemberNickname(reqest); + @Valid @RequestBody NicknameUpdateRequest request) { + memberService.updateMemberNickname(request); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/depromeet/domain/member/application/MemberService.java b/src/main/java/com/depromeet/domain/member/application/MemberService.java index 2b3fb6640..f98eaedce 100644 --- a/src/main/java/com/depromeet/domain/member/application/MemberService.java +++ b/src/main/java/com/depromeet/domain/member/application/MemberService.java @@ -62,6 +62,13 @@ public void checkUsername(UsernameCheckRequest request) { @Transactional(readOnly = true) public void checkNickname(NicknameCheckRequest request) { validateNicknameNotDuplicate(request.nickname()); + if (validateNicknameText(request.nickname())) { + throw new CustomException(ErrorCode.MEMBER_INVALID_NICKNAME); + } + } + + private boolean validateNicknameText(String nickname) { + return nickname == null || nickname.trim().isEmpty(); } private void validateNicknameNotDuplicate(String nickname) { @@ -73,8 +80,11 @@ private void validateNicknameNotDuplicate(String nickname) { @Transactional(readOnly = true) public List searchMemberNickname(String nickname) { final Member currentMember = memberUtil.getCurrentMember(); + final String escapingNickname = escapeSpecialCharacters(nickname); + List members = - memberRepository.nicknameSearch(nickname, currentMember.getProfile().getNickname()); + memberRepository.nicknameSearch( + escapingNickname, currentMember.getProfile().getNickname()); List memberRelationBySourceId = memberRelationRepository.findAllBySourceIdAndTargetIn( currentMember.getId(), members); @@ -147,10 +157,10 @@ private void validateSocialInfoNotNull(Member member) { } } - public void updateMemberNickname(NicknameUpdateRequest reqest) { + public void updateMemberNickname(NicknameUpdateRequest request) { final Member currentMember = memberUtil.getCurrentMember(); - validateNicknameNotDuplicate(reqest.nickname()); - currentMember.updateNickname(reqest.nickname()); + validateNicknameNotDuplicate(request.nickname()); + currentMember.updateNickname(escapeSpecialCharacters(request.nickname())); } private ImageFileExtension getImageFileExtension(Profile profile) { @@ -174,4 +184,9 @@ public void updateFcmToken(UpdateFcmTokenRequest updateFcmTokenRequest) { final Member currentMember = memberUtil.getCurrentMember(); currentMember.updateFcmToken(currentMember.getFcmInfo(), updateFcmTokenRequest.fcmToken()); } + + private String escapeSpecialCharacters(String nickname) { + // 여기서 특수문자를 '_'로 대체할 수 있도록 정규표현식을 활용하여 구현 + return nickname == null ? "" : nickname.replaceAll("[^0-9a-zA-Z가-힣 ]", "_"); + } } diff --git a/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java b/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java index 0932908c4..df936de37 100644 --- a/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java +++ b/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java @@ -2,6 +2,7 @@ import com.depromeet.domain.member.domain.Member; import com.depromeet.domain.member.domain.OauthInfo; +import io.lettuce.core.dynamic.annotation.Param; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -20,6 +21,7 @@ public interface MemberRepository extends JpaRepository { Optional findByProfileNickname(String nickname); @Query( - "SELECT m FROM Member m WHERE m.profile.nickname like %:searchNickname% AND m.profile.nickname != :myNickname") - List nicknameSearch(String searchNickname, String myNickname); + "SELECT m FROM Member m WHERE m.profile.nickname LIKE CONCAT('%', :searchNickname, '%') escape '_' AND m.profile.nickname != :myNickname") + List nicknameSearch( + @Param("searchNickname") String searchNickname, @Param("myNickname") String myNickname); } diff --git a/src/main/java/com/depromeet/domain/member/domain/Profile.java b/src/main/java/com/depromeet/domain/member/domain/Profile.java index bc16641f9..a66ed152c 100644 --- a/src/main/java/com/depromeet/domain/member/domain/Profile.java +++ b/src/main/java/com/depromeet/domain/member/domain/Profile.java @@ -1,13 +1,17 @@ package com.depromeet.domain.member.domain; import jakarta.persistence.Embeddable; +import jakarta.validation.constraints.Pattern; import lombok.*; @Embeddable @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Profile { + + @Pattern(regexp = "[^0-9a-zA-Z가-힣 ]", message = "올바르지 않은 닉네임 표현입니다.") private String nickname; + @Getter private String profileImageUrl; @Builder(access = AccessLevel.PRIVATE) diff --git a/src/main/java/com/depromeet/domain/notification/application/.gitkeep b/src/main/java/com/depromeet/domain/notification/application/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/main/java/com/depromeet/global/error/exception/ErrorCode.java b/src/main/java/com/depromeet/global/error/exception/ErrorCode.java index 7f23ce60a..f61e1ddbd 100644 --- a/src/main/java/com/depromeet/global/error/exception/ErrorCode.java +++ b/src/main/java/com/depromeet/global/error/exception/ErrorCode.java @@ -25,6 +25,7 @@ public enum ErrorCode { EXPIRED_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 JWT 토큰입니다."), MEMBER_ALREADY_REGISTERED(HttpStatus.CONFLICT, "이미 가입된 회원입니다."), MEMBER_ALREADY_NICKNAME(HttpStatus.CONFLICT, "이미 존재하는 닉네임입니다."), + MEMBER_INVALID_NICKNAME(HttpStatus.BAD_REQUEST, "올바르지 않는 닉네임입니다."), MEMBER_ALREADY_DELETED(HttpStatus.NOT_FOUND, "이미 탈퇴한 회원입니다."), PASSWORD_NOT_MATCHES(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."), ID_TOKEN_VERIFICATION_FAILED(HttpStatus.UNAUTHORIZED, "ID 토큰 검증에 실패했습니다."), diff --git a/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java b/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java index 8d5143b80..319a4fa05 100644 --- a/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java +++ b/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java @@ -181,6 +181,64 @@ void setUp() { assertFalse(responses.contains(currentMember.getProfile().getNickname())); } + @Test + void 검색_키워드에_퍼센트_키워드가_들어왔을_때_NPE_무시_처리한다() { + // given + memberRepository.save( + Member.createNormalMember(Profile.createProfile("도모", "도모 이미지 URL"))); + memberRepository.save( + Member.createNormalMember(Profile.createProfile("도모 바보", "testImageUrl"))); + + String searchNickname = "%"; + + // when + List responses = + memberService.searchMemberNickname(searchNickname); + + // then + assertEquals(0, responses.size()); + } + + @Test + void 검색_키워드에_공백에_따른_처리만_허용한다() { + // given + memberRepository.save(Member.createNormalMember(Profile.createProfile("바보", "도모 얼굴"))); + memberRepository.save( + Member.createNormalMember(Profile.createProfile("도 모", "도모 이미지 URL"))); + memberRepository.save( + Member.createNormalMember(Profile.createProfile("도모 바보", "testImageUrl"))); + memberRepository.save( + Member.createNormalMember(Profile.createProfile(" 도모", "도모 잘생김"))); + String searchNickname = " "; + + // when + List responses = + memberService.searchMemberNickname(searchNickname); + + // then + assertEquals(3, responses.size()); + } + + @Test + void 검색_키워드에_특수문자_입력_시_정규식_예외처리를_한다() { + // given + memberRepository.save( + Member.createNormalMember(Profile.createProfile("#도모", "도모 이미지 URL"))); + memberRepository.save( + Member.createNormalMember(Profile.createProfile("도모 바보#", "testImageUrl"))); + + memberRepository.save( + Member.createNormalMember(Profile.createProfile("#도모 바보#", "testImageUrl"))); + String searchNickname = "#"; + + // when + List responses = + memberService.searchMemberNickname(searchNickname); + + // then + assertEquals(0, responses.size()); + } + @Test void 정렬조건은_일치하는경우_먼저보여주고_나머지는_사전순에_따른다() { // given From 232871fdfafb7b2e574701a5f7a008e5b5bcbb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=AA=A8?= Date: Tue, 6 Feb 2024 00:10:04 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=ED=8C=94?= =?UTF-8?q?=EB=A1=9C=EC=9E=89/=ED=8C=94=EB=A1=9C=EC=9B=8C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20(#274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 유저 팔로잉/팔로워 목록 조회 API * style: spotless * refactor: 메소드 추출 * style: spotless --- .../domain/follow/api/FollowController.java | 8 ++ .../follow/application/FollowService.java | 80 ++++++++++++++++++- .../dto/response/FollowListResponse.java | 16 ++++ 3 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/depromeet/domain/follow/dto/response/FollowListResponse.java diff --git a/src/main/java/com/depromeet/domain/follow/api/FollowController.java b/src/main/java/com/depromeet/domain/follow/api/FollowController.java index b974e58e6..3fcd80209 100644 --- a/src/main/java/com/depromeet/domain/follow/api/FollowController.java +++ b/src/main/java/com/depromeet/domain/follow/api/FollowController.java @@ -5,6 +5,7 @@ import com.depromeet.domain.follow.dto.request.FollowDeleteRequest; import com.depromeet.domain.follow.dto.response.FollowFindMeInfoResponse; import com.depromeet.domain.follow.dto.response.FollowFindTargetInfoResponse; +import com.depromeet.domain.follow.dto.response.FollowListResponse; import com.depromeet.domain.follow.dto.response.MemberFollowedResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -56,4 +57,11 @@ public FollowFindMeInfoResponse followFindMe() { public List followedUserFindAll() { return followService.findAllFollowedMember(); } + + // 팔로잉, 팔로워 리스트 응답 + @GetMapping("/{targetId}/list") + @Operation(summary = "팔로잉, 팔로워 유저 리스트를 반환합니다", description = "팔로잉, 팔로워 유저들을 반환합니다.") + public FollowListResponse followList(@PathVariable Long targetId) { + return followService.findFollowList(targetId); + } } diff --git a/src/main/java/com/depromeet/domain/follow/application/FollowService.java b/src/main/java/com/depromeet/domain/follow/application/FollowService.java index d7d6306c4..3cb37811b 100644 --- a/src/main/java/com/depromeet/domain/follow/application/FollowService.java +++ b/src/main/java/com/depromeet/domain/follow/application/FollowService.java @@ -4,12 +4,10 @@ import com.depromeet.domain.follow.domain.MemberRelation; import com.depromeet.domain.follow.dto.request.FollowCreateRequest; import com.depromeet.domain.follow.dto.request.FollowDeleteRequest; -import com.depromeet.domain.follow.dto.response.FollowFindMeInfoResponse; -import com.depromeet.domain.follow.dto.response.FollowFindTargetInfoResponse; -import com.depromeet.domain.follow.dto.response.FollowStatus; -import com.depromeet.domain.follow.dto.response.MemberFollowedResponse; +import com.depromeet.domain.follow.dto.response.*; import com.depromeet.domain.member.dao.MemberRepository; import com.depromeet.domain.member.domain.Member; +import com.depromeet.domain.member.dto.response.MemberSearchResponse; import com.depromeet.domain.mission.domain.Mission; import com.depromeet.domain.missionRecord.domain.ImageUploadStatus; import com.depromeet.domain.missionRecord.domain.MissionRecord; @@ -188,4 +186,78 @@ private Member getTargetMember(Long targetId) { ErrorCode.FOLLOW_TARGET_MEMBER_NOT_FOUND)); return targetMember; } + + public FollowListResponse findFollowList(Long targetId) { + final Member currentMember = memberUtil.getCurrentMember(); + Member targetMember = getTargetMember(targetId); + + List followingList = new ArrayList<>(); + List followerList = new ArrayList<>(); + + List targetMemberSources = + memberRelationRepository.findAllBySourceId(targetMember.getId()); + List targetMemberTargets = + memberRelationRepository.findAllByTargetId(targetMember.getId()); + + List currentMemberSources = + memberRelationRepository.findAllBySourceId(currentMember.getId()); + List currentMemberTargets = + memberRelationRepository.findAllByTargetId(currentMember.getId()); + + // target 유저의 팔로잉 + List followingMembers = + targetMemberSources.stream().map(MemberRelation::getTarget).toList(); + + // target 유저의 팔로워 + List followerMembers = + targetMemberTargets.stream().map(MemberRelation::getSource).toList(); + + // 팔로잉 리스트 구하기 + getFollowStatusIncludeList( + followingMembers, currentMemberSources, followingList, currentMemberTargets); + + // 팔로워 리스트 구하기 + getFollowStatusIncludeList( + followerMembers, currentMemberSources, followerList, currentMemberTargets); + + return FollowListResponse.of( + targetMember.getProfile().getNickname(), followingList, followerList); + } + + private static void getFollowStatusIncludeList( + List targetMembers, + List currentMemberSources, + List resultList, + List currentMemberTargets) { + for (Member member : targetMembers) { + boolean existRelation = false; + for (MemberRelation memberRelation : currentMemberSources) { + if (member.getId().equals(memberRelation.getTarget().getId())) { + existRelation = true; + break; + } + } + + if (existRelation) { // 조회 된 애들 중 내가 팔로우한 애라면 + resultList.add(MemberSearchResponse.toFollowingResponse(member)); + continue; + } + + // 내가 팔로우를 하지 않았을 때 + Optional optionalMemberRelation = + currentMemberTargets.stream() + .filter( + memberRelation -> + member.getId() + .equals(memberRelation.getSource().getId())) + .findFirst(); + if (optionalMemberRelation.isPresent()) { // 상대방만 나를 팔로우 하고 있을 때 + resultList.add(MemberSearchResponse.toFollowedByMeResponse(member)); + continue; + } + + // 아니라면 서로 팔로우가 아닌 상태 + resultList.add(MemberSearchResponse.toNotFollowingResponse(member)); + } + } } diff --git a/src/main/java/com/depromeet/domain/follow/dto/response/FollowListResponse.java b/src/main/java/com/depromeet/domain/follow/dto/response/FollowListResponse.java new file mode 100644 index 000000000..cd15b49a1 --- /dev/null +++ b/src/main/java/com/depromeet/domain/follow/dto/response/FollowListResponse.java @@ -0,0 +1,16 @@ +package com.depromeet.domain.follow.dto.response; + +import com.depromeet.domain.member.dto.response.MemberSearchResponse; +import java.util.List; + +public record FollowListResponse( + String targetNickname, + List followingList, + List followerList) { + public static FollowListResponse of( + String targetNickname, + List followingList, + List followerList) { + return new FollowListResponse(targetNickname, followingList, followerList); + } +} From 434f2a4d5839b33c01ada06d6db3c2628307e9cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=AA=A8?= Date: Tue, 6 Feb 2024 13:26:18 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20fcm=20=ED=86=A0=ED=81=B0=EC=9D=B4=20?= =?UTF-8?q?=EB=B9=88=20=EA=B0=92=EC=9D=B4=EB=9D=BC=EB=A9=B4=20=ED=91=B8?= =?UTF-8?q?=EC=8B=9C=20=EC=95=8C=EB=A6=BC=EC=9D=84=20=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?(#279)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/depromeet/domain/follow/api/FollowController.java | 1 - src/main/java/com/depromeet/domain/member/domain/FcmInfo.java | 2 +- src/main/java/com/depromeet/domain/member/domain/Member.java | 2 +- src/main/java/com/depromeet/global/config/fcm/FcmService.java | 3 +++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/depromeet/domain/follow/api/FollowController.java b/src/main/java/com/depromeet/domain/follow/api/FollowController.java index 3fcd80209..afac7d44c 100644 --- a/src/main/java/com/depromeet/domain/follow/api/FollowController.java +++ b/src/main/java/com/depromeet/domain/follow/api/FollowController.java @@ -58,7 +58,6 @@ public List followedUserFindAll() { return followService.findAllFollowedMember(); } - // 팔로잉, 팔로워 리스트 응답 @GetMapping("/{targetId}/list") @Operation(summary = "팔로잉, 팔로워 유저 리스트를 반환합니다", description = "팔로잉, 팔로워 유저들을 반환합니다.") public FollowListResponse followList(@PathVariable Long targetId) { diff --git a/src/main/java/com/depromeet/domain/member/domain/FcmInfo.java b/src/main/java/com/depromeet/domain/member/domain/FcmInfo.java index e34ab1bbf..1893245a8 100644 --- a/src/main/java/com/depromeet/domain/member/domain/FcmInfo.java +++ b/src/main/java/com/depromeet/domain/member/domain/FcmInfo.java @@ -21,7 +21,7 @@ private FcmInfo(String fcmToken, Boolean appAlarm) { } public static FcmInfo createFcmInfo() { - return FcmInfo.builder().fcmToken("").appAlarm(true).build(); + return FcmInfo.builder().appAlarm(true).build(); } public static FcmInfo toggleAlarm(FcmInfo fcmState) { diff --git a/src/main/java/com/depromeet/domain/member/domain/Member.java b/src/main/java/com/depromeet/domain/member/domain/Member.java index d64db0eb5..3f4e4dee9 100644 --- a/src/main/java/com/depromeet/domain/member/domain/Member.java +++ b/src/main/java/com/depromeet/domain/member/domain/Member.java @@ -36,7 +36,7 @@ public class Member extends BaseTimeEntity { @Embedded private OauthInfo oauthInfo; - @Embedded private FcmInfo fcmInfo = FcmInfo.createFcmInfo(); + @Embedded private FcmInfo fcmInfo; @Enumerated(EnumType.STRING) private MemberStatus status; diff --git a/src/main/java/com/depromeet/global/config/fcm/FcmService.java b/src/main/java/com/depromeet/global/config/fcm/FcmService.java index c1e395576..27d5e81ad 100644 --- a/src/main/java/com/depromeet/global/config/fcm/FcmService.java +++ b/src/main/java/com/depromeet/global/config/fcm/FcmService.java @@ -29,6 +29,9 @@ public ApiFuture sendGroupMessageAsync( } public ApiFuture sendMessageSync(String token, String title, String content) { + if (token == null || token.isEmpty()) { + return null; + } Message message = Message.builder() .setToken(token) From eb653619361ada515de62e48d45bc31b3d8bab7e Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:52:39 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20Dev=20=ED=99=98=EA=B2=BD=20=EC=BF=A0?= =?UTF-8?q?=ED=82=A4=20=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=20(#276)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 테스트 트리거 활성화 * fix: 개발환경에서는 쿠키의 secure 옵션 false이도록 수정 * refactor: 변수명 변경 * fix: CORS dev 설정 롤백 * refactor: enum을 대체하는 환경변수 상수 클래스 추가 * refactor: 운영환경 체크 유틸리티에서 새로운 상수 클래스를 사용하도록 개선 * refactor: 원시 boolean 사용하도록 변경 * refactor: 새로운 상수 클래스 사용하도록 개선 * refactor: 기존 상수 클래스 대체하도록 변경 * refactor: sameSite 상수 사용하도록 변경 * refactor: 쿠키 헤더 상수로 변경 * refactor: cors origin 로직에서 switch문 제거하도록 개선 * refactor: secure 정책 로직 개선 * chore: 테스트 트리거 비활성화 * fix: SameSite가 None일 때 작동할 수 있도록 secure을 항상 참으로 설정 * refactor: 롬복 기본 생성자를 사용하도록 변경 --- .../constants/EnvironmentConstants.java | 19 +++++++------- .../config/security/WebSecurityConfig.java | 25 ++++++++----------- .../com/depromeet/global/util/CookieUtil.java | 9 ++++--- .../global/util/SpringEnvironmentUtil.java | 25 ++++++++----------- .../util/SpringEnvironmentUtilTest.java | 17 ++++++------- 5 files changed, 41 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/depromeet/global/common/constants/EnvironmentConstants.java b/src/main/java/com/depromeet/global/common/constants/EnvironmentConstants.java index 8ee41f6d6..7bc7c17c2 100644 --- a/src/main/java/com/depromeet/global/common/constants/EnvironmentConstants.java +++ b/src/main/java/com/depromeet/global/common/constants/EnvironmentConstants.java @@ -1,15 +1,14 @@ package com.depromeet.global.common.constants; -import lombok.AllArgsConstructor; -import lombok.Getter; +import java.util.List; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; -@Getter -@AllArgsConstructor -public enum EnvironmentConstants { - PROD("prod"), - DEV("dev"), - LOCAL("local"), - ; +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EnvironmentConstants { - private String value; + public static final String PROD = "prod"; + public static final String DEV = "dev"; + public static final String LOCAL = "local"; + public static final List PROD_AND_DEV = List.of(PROD, DEV); } diff --git a/src/main/java/com/depromeet/global/config/security/WebSecurityConfig.java b/src/main/java/com/depromeet/global/config/security/WebSecurityConfig.java index 6517453d5..b149a7118 100644 --- a/src/main/java/com/depromeet/global/config/security/WebSecurityConfig.java +++ b/src/main/java/com/depromeet/global/config/security/WebSecurityConfig.java @@ -1,5 +1,7 @@ package com.depromeet.global.config.security; +import static com.depromeet.global.common.constants.EnvironmentConstants.*; +import static org.springframework.http.HttpHeaders.*; import static org.springframework.security.config.Customizer.*; import com.depromeet.domain.auth.application.JwtTokenService; @@ -117,26 +119,19 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - switch (springEnvironmentUtil.getCurrentProfile()) { - case "prod": - configuration.addAllowedOriginPattern(UrlConstants.PROD_DOMAIN_URL.getValue()); - break; - // TODO: 프론트 모바일에서 웹뷰 테스트를 위해 임시 주석 처리 - // case "dev": - // - // configuration.addAllowedOriginPattern(UrlConstants.DEV_DOMAIN_URL.getValue()); - // - // configuration.addAllowedOriginPattern(UrlConstants.LOCAL_DOMAIN_URL.getValue()); - // break; - default: - configuration.addAllowedOriginPattern("*"); - break; + if (springEnvironmentUtil.isProdProfile()) { + configuration.addAllowedOriginPattern(UrlConstants.PROD_DOMAIN_URL.getValue()); + } + + if (springEnvironmentUtil.isDevProfile()) { + configuration.addAllowedOriginPattern(UrlConstants.DEV_DOMAIN_URL.getValue()); + configuration.addAllowedOriginPattern(UrlConstants.LOCAL_DOMAIN_URL.getValue()); } configuration.addAllowedHeader("*"); configuration.addAllowedMethod("*"); configuration.setAllowCredentials(true); - configuration.addExposedHeader("Set-Cookie"); + configuration.addExposedHeader(SET_COOKIE); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); diff --git a/src/main/java/com/depromeet/global/util/CookieUtil.java b/src/main/java/com/depromeet/global/util/CookieUtil.java index 7d464f8df..e859f4630 100644 --- a/src/main/java/com/depromeet/global/util/CookieUtil.java +++ b/src/main/java/com/depromeet/global/util/CookieUtil.java @@ -4,6 +4,7 @@ import static com.depromeet.global.common.constants.SecurityConstants.REFRESH_TOKEN_COOKIE_NAME; import lombok.RequiredArgsConstructor; +import org.springframework.boot.web.server.Cookie; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @@ -23,7 +24,7 @@ public HttpHeaders generateTokenCookies(String accessToken, String refreshToken) .path("/") .secure(true) .sameSite(sameSite) - .httpOnly(false) + .httpOnly(true) .build(); ResponseCookie refreshTokenCookie = @@ -31,7 +32,7 @@ public HttpHeaders generateTokenCookies(String accessToken, String refreshToken) .path("/") .secure(true) .sameSite(sameSite) - .httpOnly(false) + .httpOnly(true) .build(); HttpHeaders headers = new HttpHeaders(); @@ -43,8 +44,8 @@ public HttpHeaders generateTokenCookies(String accessToken, String refreshToken) private String determineSameSitePolicy() { if (springEnvironmentUtil.isProdProfile()) { - return "Strict"; + return Cookie.SameSite.STRICT.attributeValue(); } - return "None"; + return Cookie.SameSite.NONE.attributeValue(); } } diff --git a/src/main/java/com/depromeet/global/util/SpringEnvironmentUtil.java b/src/main/java/com/depromeet/global/util/SpringEnvironmentUtil.java index 9b7278e8c..2fafd4a32 100644 --- a/src/main/java/com/depromeet/global/util/SpringEnvironmentUtil.java +++ b/src/main/java/com/depromeet/global/util/SpringEnvironmentUtil.java @@ -1,8 +1,8 @@ package com.depromeet.global.util; -import com.depromeet.global.common.constants.EnvironmentConstants; +import static com.depromeet.global.common.constants.EnvironmentConstants.*; + import java.util.Arrays; -import java.util.List; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.springframework.core.env.Environment; @@ -11,30 +11,25 @@ @Component @RequiredArgsConstructor public class SpringEnvironmentUtil { - private final Environment environment; - private final List PROD_AND_DEV = - List.of(EnvironmentConstants.PROD.getValue(), EnvironmentConstants.DEV.getValue()); + private final Environment environment; public String getCurrentProfile() { return getActiveProfiles() - .filter( - profile -> - profile.equals(EnvironmentConstants.PROD.getValue()) - || profile.equals(EnvironmentConstants.DEV.getValue())) + .filter(profile -> profile.equals(PROD) || profile.equals(DEV)) .findFirst() - .orElse(EnvironmentConstants.LOCAL.getValue()); + .orElse(LOCAL); } - public Boolean isProdProfile() { - return getActiveProfiles().anyMatch(EnvironmentConstants.PROD.getValue()::equals); + public boolean isProdProfile() { + return getActiveProfiles().anyMatch(PROD::equals); } - public Boolean isDevProfile() { - return getActiveProfiles().anyMatch(EnvironmentConstants.DEV.getValue()::equals); + public boolean isDevProfile() { + return getActiveProfiles().anyMatch(DEV::equals); } - public Boolean isProdAndDevProfile() { + public boolean isProdAndDevProfile() { return getActiveProfiles().anyMatch(PROD_AND_DEV::contains); } diff --git a/src/test/java/com/depromeet/global/util/SpringEnvironmentUtilTest.java b/src/test/java/com/depromeet/global/util/SpringEnvironmentUtilTest.java index fa7ed177a..a571bdfd2 100644 --- a/src/test/java/com/depromeet/global/util/SpringEnvironmentUtilTest.java +++ b/src/test/java/com/depromeet/global/util/SpringEnvironmentUtilTest.java @@ -1,9 +1,9 @@ package com.depromeet.global.util; +import static com.depromeet.global.common.constants.EnvironmentConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; -import com.depromeet.global.common.constants.EnvironmentConstants; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -17,9 +17,9 @@ class SpringEnvironmentUtilTest { @InjectMocks private SpringEnvironmentUtil springEnvironmentUtil; - private final String[] PROD_ARRAY = new String[] {EnvironmentConstants.PROD.getValue()}; - private final String[] DEV_ARRAY = new String[] {EnvironmentConstants.DEV.getValue()}; - private final String[] LOCAL_ARRAY = new String[] {EnvironmentConstants.LOCAL.getValue()}; + private static final String[] PROD_ARRAY = new String[] {PROD}; + private static final String[] DEV_ARRAY = new String[] {DEV}; + private static final String[] LOCAL_ARRAY = new String[] {LOCAL}; @Test void 상용_환경이라면_isProdProfile은_true를_반환한다() { @@ -88,8 +88,7 @@ class SpringEnvironmentUtilTest { // when // then - assertEquals( - springEnvironmentUtil.getCurrentProfile(), EnvironmentConstants.PROD.getValue()); + assertEquals(springEnvironmentUtil.getCurrentProfile(), PROD); } @Test @@ -99,8 +98,7 @@ class SpringEnvironmentUtilTest { // when // then - assertEquals( - springEnvironmentUtil.getCurrentProfile(), EnvironmentConstants.DEV.getValue()); + assertEquals(springEnvironmentUtil.getCurrentProfile(), DEV); } @Test @@ -110,7 +108,6 @@ class SpringEnvironmentUtilTest { // when // then - assertEquals( - springEnvironmentUtil.getCurrentProfile(), EnvironmentConstants.LOCAL.getValue()); + assertEquals(springEnvironmentUtil.getCurrentProfile(), LOCAL); } } From f1d578cfd0c0ec8e74ae4732b4950a3dfe9d9f35 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:19:40 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#282)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 탈퇴한 회원이면 재생성하도록 수정 * fix: 닉네임으로 멤버 제거하는 로직 수정 * feat: 쿠키 삭제 로직 구현 --- .../domain/auth/application/AuthService.java | 2 +- .../domain/member/api/MemberController.java | 8 +++-- .../member/application/MemberService.java | 8 ++--- .../domain/member/dao/MemberRepository.java | 3 +- .../com/depromeet/global/util/CookieUtil.java | 29 +++++++++++++++++++ 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/depromeet/domain/auth/application/AuthService.java b/src/main/java/com/depromeet/domain/auth/application/AuthService.java index 0e54083f3..6af856d49 100644 --- a/src/main/java/com/depromeet/domain/auth/application/AuthService.java +++ b/src/main/java/com/depromeet/domain/auth/application/AuthService.java @@ -103,7 +103,7 @@ public SocialLoginResponse socialLoginMember(IdTokenRequest request, OauthProvid private Member fetchOrCreate(OidcUser oidcUser) { return memberRepository - .findByOauthInfo(extractOauthInfo(oidcUser)) + .findByOauthInfoAndStatus(extractOauthInfo(oidcUser), MemberStatus.NORMAL) .orElseGet(() -> saveMember(oidcUser)); } diff --git a/src/main/java/com/depromeet/domain/member/api/MemberController.java b/src/main/java/com/depromeet/domain/member/api/MemberController.java index 20e7078a6..15dace813 100644 --- a/src/main/java/com/depromeet/domain/member/api/MemberController.java +++ b/src/main/java/com/depromeet/domain/member/api/MemberController.java @@ -8,6 +8,7 @@ import com.depromeet.domain.member.dto.response.MemberFindOneResponse; import com.depromeet.domain.member.dto.response.MemberSearchResponse; import com.depromeet.domain.member.dto.response.MemberSocialInfoResponse; +import com.depromeet.global.util.CookieUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -23,6 +24,7 @@ public class MemberController { private final MemberService memberService; + private final CookieUtil cookieUtil; @Operation(summary = "회원 정보 확인", description = "로그인 된 회원의 정보를 확인합니다.") @GetMapping("/me") @@ -62,9 +64,9 @@ public List memberNicknameSearch( // TODO: 테스트 코드 작성 필요 @Operation(summary = "회원 탈퇴", description = "회원탈퇴를 진행합니다.") @DeleteMapping("/withdrawal") - public ResponseEntity memberWithdrawal(@Valid @RequestBody UsernameCheckRequest request) { - memberService.withdrawal(request); - return ResponseEntity.ok().build(); + public ResponseEntity memberWithdrawal() { + memberService.withdrawal(); + return ResponseEntity.ok().headers(cookieUtil.deleteTokenCookies()).build(); } @Operation(summary = "소셜 로그인 정보 조회하기", description = "소셜 로그인 정보를 조회합니다.") diff --git a/src/main/java/com/depromeet/domain/member/application/MemberService.java b/src/main/java/com/depromeet/domain/member/application/MemberService.java index f98eaedce..1baca346a 100644 --- a/src/main/java/com/depromeet/domain/member/application/MemberService.java +++ b/src/main/java/com/depromeet/domain/member/application/MemberService.java @@ -135,12 +135,8 @@ public List searchMemberNickname(String nickname) { return response; } - public void withdrawal(UsernameCheckRequest request) { - final Member member = - memberRepository - .findByUsername(request.username()) - .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); - + public void withdrawal() { + final Member member = memberUtil.getCurrentMember(); refreshTokenRepository.deleteById(member.getId()); member.withdrawal(); } diff --git a/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java b/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java index df936de37..186f9304e 100644 --- a/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java +++ b/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java @@ -1,6 +1,7 @@ package com.depromeet.domain.member.dao; import com.depromeet.domain.member.domain.Member; +import com.depromeet.domain.member.domain.MemberStatus; import com.depromeet.domain.member.domain.OauthInfo; import io.lettuce.core.dynamic.annotation.Param; import java.util.List; @@ -10,7 +11,7 @@ public interface MemberRepository extends JpaRepository { - Optional findByOauthInfo(OauthInfo oauthInfo); + Optional findByOauthInfoAndStatus(OauthInfo oauthInfo, MemberStatus status); boolean existsByUsername(String username); diff --git a/src/main/java/com/depromeet/global/util/CookieUtil.java b/src/main/java/com/depromeet/global/util/CookieUtil.java index e859f4630..399adc157 100644 --- a/src/main/java/com/depromeet/global/util/CookieUtil.java +++ b/src/main/java/com/depromeet/global/util/CookieUtil.java @@ -48,4 +48,33 @@ private String determineSameSitePolicy() { } return Cookie.SameSite.NONE.attributeValue(); } + + public HttpHeaders deleteTokenCookies() { + + String sameSite = determineSameSitePolicy(); + + ResponseCookie accessTokenCookie = + ResponseCookie.from(ACCESS_TOKEN_COOKIE_NAME, "") + .path("/") + .maxAge(0) + .secure(true) + .sameSite(sameSite) + .httpOnly(false) + .build(); + + ResponseCookie refreshTokenCookie = + ResponseCookie.from(REFRESH_TOKEN_COOKIE_NAME, "") + .path("/") + .maxAge(0) + .secure(true) + .sameSite(sameSite) + .httpOnly(false) + .build(); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); + headers.add(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); + + return headers; + } }