From 4e39fe4e0aa0ff769ce79b9d9b25f6bfc4f3c668 Mon Sep 17 00:00:00 2001 From: Jaewon Lee <58386334+jaewonLeeKOR@users.noreply.github.com> Date: Sun, 18 Aug 2024 22:28:17 +0900 Subject: [PATCH 01/21] =?UTF-8?q?feat:=20#126=20=EB=94=94=EB=B0=94?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=ED=86=A0=ED=81=B0=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 16 ++- .../domain/member/dto/MemberRequest.java | 1 + .../DeviceTokenRedisRepository.java | 36 +++++ .../domain/member/service/MemberService.java | 7 +- .../member/service/MemberServiceImpl.java | 21 ++- .../DeviceTokenRedisRepositoryTest.java | 134 ++++++++++++++++++ 6 files changed, 204 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java create mode 100644 src/test/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepositoryTest.java diff --git a/src/main/java/com/server/capple/domain/member/controller/MemberController.java b/src/main/java/com/server/capple/domain/member/controller/MemberController.java index da8757f9..3d01510c 100644 --- a/src/main/java/com/server/capple/domain/member/controller/MemberController.java +++ b/src/main/java/com/server/capple/domain/member/controller/MemberController.java @@ -84,8 +84,8 @@ public BaseResponse deleteOrphanageImages() """, description = "서버측의 애플 서버 접근 클라언트 관련 문제로 서버 관리자에게 문의해주세요."), })), }) - public BaseResponse login(@RequestParam String code) { - return BaseResponse.onSuccess(memberService.signIn(code)); + public BaseResponse login(@RequestParam String code, @RequestParam String deviceToken) { + return BaseResponse.onSuccess(memberService.signIn(code, deviceToken)); } @Operation(summary = "회원가입 API", description = "회원가입 API 입니다." + @@ -94,15 +94,15 @@ public BaseResponse login(@RequestParam String co "회원가입 성공시 accessToken과 refreshToken이 반환됩니다.") @PostMapping("/sign-up") public BaseResponse signUp(@RequestBody MemberRequest.signUp request) { - return BaseResponse.onSuccess(memberService.signUp(request.getSignUpToken(), request.getEmail(), request.getNickname(), request.getProfileImage())); + return BaseResponse.onSuccess(memberService.signUp(request.getSignUpToken(), request.getEmail(), request.getNickname(), request.getProfileImage(), request.getDeviceToken())); } @Operation(summary = "테스트용 로컬 로그인 API", description = "테스트용 로컬 로그인 API 입니다." + "쿼리 파라미터를 이용해 테스트용 아이디를 입력해주세요." + "refreshToken의 위치에 signUpToken이 반환됩니다.") @GetMapping("/local-sign-in") - public BaseResponse localLogin(@RequestParam String testId) { - return BaseResponse.onSuccess(memberService.localSignIn(testId)); + public BaseResponse localLogin(@RequestParam String testId, @RequestParam String deviceToken) { + return BaseResponse.onSuccess(memberService.localSignIn(testId, deviceToken)); } @Operation(summary = "회원탈퇴 API", description = "회원탈퇴 API 입니다.") @@ -187,4 +187,10 @@ public BaseResponse registerEmailWhitelist( @RequestParam Long whitelistDurationMinutes) { return BaseResponse.onSuccess(memberService.registerEmailWhitelist(mail, whitelistDurationMinutes)); } + + @Operation(summary = "로그아웃 API", description = "로그아웃 API 입니다.
저장된 디바이스 토큰을 지웁니다.") + @GetMapping("/logout") + public BaseResponse logout(@AuthMember Member member) { + return BaseResponse.onSuccess(memberService.logout(member)); + } } diff --git a/src/main/java/com/server/capple/domain/member/dto/MemberRequest.java b/src/main/java/com/server/capple/domain/member/dto/MemberRequest.java index fde93ee5..68b4d553 100644 --- a/src/main/java/com/server/capple/domain/member/dto/MemberRequest.java +++ b/src/main/java/com/server/capple/domain/member/dto/MemberRequest.java @@ -30,6 +30,7 @@ public static class signUp { private String email; private String nickname; private String profileImage; + private String deviceToken; } } diff --git a/src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java b/src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java new file mode 100644 index 00000000..0993ff2c --- /dev/null +++ b/src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java @@ -0,0 +1,36 @@ +package com.server.capple.domain.member.repository; + + +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class DeviceTokenRedisRepository { + public static final String DEVICE_TOKEN_KEY = "device-token-"; + + @Resource(name = "redisTemplate") + private ValueOperations valueOperations; + private final RedisTemplate redisTemplate; + + public void saveDeviceToken(String key, String deviceToken) { + valueOperations.set(DEVICE_TOKEN_KEY + key, deviceToken); + } + + public String getDeviceToken(String key) { + return valueOperations.get(DEVICE_TOKEN_KEY + key); + } + + public List getDeviceTokens(List keys) { + return valueOperations.multiGet(keys.stream().map(key -> DEVICE_TOKEN_KEY + key).toList()); + } + + public void deleteDeviceToken(String key) { + redisTemplate.delete(DEVICE_TOKEN_KEY + key); + } +} diff --git a/src/main/java/com/server/capple/domain/member/service/MemberService.java b/src/main/java/com/server/capple/domain/member/service/MemberService.java index b0203810..c72ebb8d 100644 --- a/src/main/java/com/server/capple/domain/member/service/MemberService.java +++ b/src/main/java/com/server/capple/domain/member/service/MemberService.java @@ -13,9 +13,9 @@ public interface MemberService { MemberResponse.EditMemberInfo editMemberInfo(Member member, MemberRequest.EditMemberInfo request); MemberResponse.ProfileImage uploadImage(MultipartFile image); MemberResponse.DeleteProfileImages deleteOrphanageImages(); - MemberResponse.SignInResponse signIn(String authorizationCode); - MemberResponse.Tokens signUp(String signUpToken, String email, String nickname, String profileImage); - MemberResponse.SignInResponse localSignIn(String testId); + MemberResponse.SignInResponse signIn(String authorizationCode, String deviceToken); + MemberResponse.Tokens signUp(String signUpToken, String email, String nickname, String profileImage, String deviceToken); + MemberResponse.SignInResponse localSignIn(String testId, String deviceToken); MemberResponse.Tokens changeRole(Long memberId, Role role); MemberResponse.MemberId resignMember (Member member); Boolean checkNickname(String nickname); @@ -23,4 +23,5 @@ public interface MemberService { Boolean registerEmailWhitelist(String email, Long whitelistDurationMinutes); Boolean sendCertMail(String signUpToken, String email); Boolean checkCertCode(String signUpToken, String email, String certCode); + Boolean logout(Member member); } diff --git a/src/main/java/com/server/capple/domain/member/service/MemberServiceImpl.java b/src/main/java/com/server/capple/domain/member/service/MemberServiceImpl.java index 234b3ca5..114b27fe 100644 --- a/src/main/java/com/server/capple/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/com/server/capple/domain/member/service/MemberServiceImpl.java @@ -11,6 +11,7 @@ import com.server.capple.domain.member.entity.Role; import com.server.capple.domain.member.mapper.MemberMapper; import com.server.capple.domain.member.mapper.TokensMapper; +import com.server.capple.domain.member.repository.DeviceTokenRedisRepository; import com.server.capple.domain.member.repository.MemberRepository; import com.server.capple.global.exception.RestApiException; import com.server.capple.global.exception.errorCode.AuthErrorCode; @@ -39,6 +40,7 @@ public class MemberServiceImpl implements MemberService { private final AppleAuthService appleAuthService; private final JwtService jwtService; private final MailService mailService; + private final DeviceTokenRedisRepository deviceTokenRedisRepository; @Override public MemberResponse.MyPageMemberInfo getMemberInfo(Member member) { @@ -93,7 +95,7 @@ public MemberResponse.DeleteProfileImages deleteOrphanageImages() { return new MemberResponse.DeleteProfileImages(deleteImages); } - public MemberResponse.SignInResponse signIn(String authorizationCode) { + public MemberResponse.SignInResponse signIn(String authorizationCode, String deviceToken) { AppleIdTokenPayload appleIdTokenPayload = appleAuthService.get(authorizationCode); Optional optionalMember = memberRepository.findBySub(appleIdTokenPayload.getSub()); if (optionalMember.isEmpty()) { @@ -104,12 +106,14 @@ public MemberResponse.SignInResponse signIn(String authorizationCode) { String role = optionalMember.get().getRole().getName(); String accessToken = jwtService.createJwt(memberId, role, "access"); String refreshToken = jwtService.createJwt(memberId, role, "refresh"); + if(deviceToken != null) + deviceTokenRedisRepository.saveDeviceToken(memberId.toString(), deviceToken); return memberMapper.toSignInResponse(accessToken, refreshToken, true); } @Override @Transactional - public MemberResponse.Tokens signUp(String signUpToken, String email, String nickname, String profileImage) { + public MemberResponse.Tokens signUp(String signUpToken, String email, String nickname, String profileImage, String deviceToken) { String sub = jwtService.getSub(signUpToken); String encryptedEmail = convertEmailToJwt(email); @@ -120,11 +124,13 @@ public MemberResponse.Tokens signUp(String signUpToken, String email, String nic String role = member.getRole().getName(); String accessToken = jwtService.createJwt(memberId, role, "access"); String refreshToken = jwtService.createJwt(memberId, role, "refresh"); + if(deviceToken != null) + deviceTokenRedisRepository.saveDeviceToken(memberId.toString(), deviceToken); return tokensMapper.toTokens(accessToken, refreshToken); } @Override - public MemberResponse.SignInResponse localSignIn(String testId) { + public MemberResponse.SignInResponse localSignIn(String testId, String deviceToken) { Optional optionalMember = memberRepository.findBySub(testId); if (optionalMember.isEmpty()) { String signUpToken = jwtService.createSignUpAccessJwt(testId); @@ -134,6 +140,8 @@ public MemberResponse.SignInResponse localSignIn(String testId) { String role = optionalMember.get().getRole().getName(); String accessToken = jwtService.createJwt(memberId, role, "access"); String refreshToken = jwtService.createJwt(memberId, role, "refresh"); + if(deviceToken != null) + deviceTokenRedisRepository.saveDeviceToken(memberId.toString(), deviceToken); return memberMapper.toSignInResponse(accessToken, refreshToken, true); } @@ -153,6 +161,7 @@ public MemberResponse.MemberId resignMember(Member member) { Member resignedMember = memberRepository.findById(member.getId()).orElseThrow( () -> new RestApiException(MemberErrorCode.MEMBER_NOT_FOUND)); resignedMember.resignMember(); + deviceTokenRedisRepository.deleteDeviceToken(member.getId().toString()); return memberMapper.toMemberId(member); } @@ -212,4 +221,10 @@ public Boolean checkCertCode(String signUpToken, String email, String certCode) // 이메일 인증코드 체크 return mailService.checkEmailCertificationCode(emailJwt, certCode); } + + @Override + public Boolean logout(Member member) { + deviceTokenRedisRepository.deleteDeviceToken(member.getId().toString()); + return true; + } } \ No newline at end of file diff --git a/src/test/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepositoryTest.java b/src/test/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepositoryTest.java new file mode 100644 index 00000000..fe9ed909 --- /dev/null +++ b/src/test/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepositoryTest.java @@ -0,0 +1,134 @@ +package com.server.capple.domain.member.repository; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("디바이스 토큰 레포지토리의 ") +@SpringBootTest +@ActiveProfiles("test") +class DeviceTokenRedisRepositoryTest { + @Autowired + private DeviceTokenRedisRepository deviceTokenRedisRepository; + + private String testKeyPrefix = "testKey-"; + private List testKeys = new ArrayList<>(); + + @AfterEach + void afterEach() { + testKeys.forEach(deviceTokenRedisRepository::deleteDeviceToken); + } + + @Test + @DisplayName("디바이스 토큰 저장, 조회 테스트") + void saveDeviceTokenTest() { + //given + String key = testKeyPrefix + UUID.randomUUID().toString(); + testKeys.add(key); + String deviceToken = UUID.randomUUID().toString(); + + //when + deviceTokenRedisRepository.saveDeviceToken(key, deviceToken); + + //then + assertEquals(deviceToken, deviceTokenRedisRepository.getDeviceToken(key)); + } + + @Test + @DisplayName("디바이스 토큰 삭제 테스트") + void deleteDeviceTokenTest() { + //given + String key = testKeyPrefix + UUID.randomUUID().toString(); + String deviceToken = UUID.randomUUID().toString(); + + //when + deviceTokenRedisRepository.saveDeviceToken(key, deviceToken); + deviceTokenRedisRepository.deleteDeviceToken(key); + + //then + assertNull(deviceTokenRedisRepository.getDeviceToken(key)); + } + + @Test + @DisplayName("디바이스 토큰 여러개 조회 테스트") + void getDeviceTokensTest() { + //given + String key1 = testKeyPrefix + UUID.randomUUID().toString(); + String key2 = testKeyPrefix + UUID.randomUUID().toString(); + String key3 = testKeyPrefix + UUID.randomUUID().toString(); + testKeys.add(key1); + testKeys.add(key2); + testKeys.add(key3); + String deviceToken1 = UUID.randomUUID().toString(); + String deviceToken2 = UUID.randomUUID().toString(); + String deviceToken3 = UUID.randomUUID().toString(); + + //when + deviceTokenRedisRepository.saveDeviceToken(key1, deviceToken1); + deviceTokenRedisRepository.saveDeviceToken(key2, deviceToken2); + deviceTokenRedisRepository.saveDeviceToken(key3, deviceToken3); + + //then + List deviceTokens = deviceTokenRedisRepository.getDeviceTokens(List.of(key1, key2, key3)); + assertEquals(3, deviceTokens.size()); + assertTrue(deviceTokens.contains(deviceToken1)); + assertTrue(deviceTokens.contains(deviceToken2)); + assertTrue(deviceTokens.contains(deviceToken3)); + } + + @Test + @DisplayName("디바이스 토큰 삭제 후 여러개 조회 테스트") + void getDeviceTokensAfterDeleteTest() { + //given + String key1 = testKeyPrefix + UUID.randomUUID().toString(); + String key2 = testKeyPrefix + UUID.randomUUID().toString(); + String key3 = testKeyPrefix + UUID.randomUUID().toString(); + testKeys.add(key1); + testKeys.add(key2); + testKeys.add(key3); + String deviceToken1 = UUID.randomUUID().toString(); + String deviceToken2 = UUID.randomUUID().toString(); + String deviceToken3 = UUID.randomUUID().toString(); + + //when + deviceTokenRedisRepository.saveDeviceToken(key1, deviceToken1); + deviceTokenRedisRepository.saveDeviceToken(key2, deviceToken2); + deviceTokenRedisRepository.saveDeviceToken(key3, deviceToken3); + deviceTokenRedisRepository.deleteDeviceToken(key2); + + //then + List deviceTokens = deviceTokenRedisRepository.getDeviceTokens(List.of(key1, key2, key3)); + assertEquals(3, deviceTokens.size()); + assertTrue(deviceTokens.contains(deviceToken1)); + assertFalse(deviceTokens.contains(deviceToken2)); + assertNull(deviceTokens.get(1)); + assertTrue(deviceTokens.contains(deviceToken3)); + } + + @Test + @DisplayName("디바이스 토큰 중복 저장, 조회 테스트") + void saveDuplicateDeviceTokenTest() { + //given + String key = testKeyPrefix + UUID.randomUUID().toString(); + testKeys.add(key); + String deviceToken1 = UUID.randomUUID().toString(); + String deviceToken2 = UUID.randomUUID().toString(); + + //when + deviceTokenRedisRepository.saveDeviceToken(key, deviceToken1); + deviceTokenRedisRepository.saveDeviceToken(key, deviceToken2); + + //then + assertNotEquals(deviceToken1, deviceTokenRedisRepository.getDeviceToken(key)); + assertEquals(deviceToken2, deviceTokenRedisRepository.getDeviceToken(key)); + } +} \ No newline at end of file From 1c8a06e263f1e8faf69d6d952d9962005723d055 Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Mon, 19 Aug 2024 21:48:34 +0900 Subject: [PATCH 02/21] =?UTF-8?q?feat:=20#128=20board=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EB=A7=A4=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../capple/domain/board/entity/Board.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/com/server/capple/domain/board/entity/Board.java diff --git a/src/main/java/com/server/capple/domain/board/entity/Board.java b/src/main/java/com/server/capple/domain/board/entity/Board.java new file mode 100644 index 00000000..f168df31 --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/entity/Board.java @@ -0,0 +1,34 @@ +package com.server.capple.domain.board.entity; + +import com.server.capple.domain.member.entity.Member; +import com.server.capple.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.SQLRestriction; + +@Getter +@Builder +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@SQLRestriction("deleted_at is null") +@DynamicInsert +public class Board extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(nullable = false) + private String content; + + @Column(nullable = false) + private Integer heartCount; + + @Column(nullable = false) + private Integer commentCount; +} From 5539d5f00c3f7f910b433f46f07f3e7926f2985c Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Mon, 19 Aug 2024 21:54:13 +0900 Subject: [PATCH 03/21] =?UTF-8?q?feat:=20#128=20board=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/BoardController.java | 17 +++++++++++++++++ .../capple/domain/board/dto/BoardRequest.java | 4 ++++ .../capple/domain/board/dto/BoardResponse.java | 4 ++++ .../capple/domain/board/mapper/BoardMapper.java | 8 ++++++++ .../board/repository/BoardRepository.java | 7 +++++++ .../domain/board/service/BoardService.java | 4 ++++ .../domain/board/service/BoardServiceImpl.java | 16 ++++++++++++++++ 7 files changed, 60 insertions(+) create mode 100644 src/main/java/com/server/capple/domain/board/controller/BoardController.java create mode 100644 src/main/java/com/server/capple/domain/board/dto/BoardRequest.java create mode 100644 src/main/java/com/server/capple/domain/board/dto/BoardResponse.java create mode 100644 src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java create mode 100644 src/main/java/com/server/capple/domain/board/repository/BoardRepository.java create mode 100644 src/main/java/com/server/capple/domain/board/service/BoardService.java create mode 100644 src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java diff --git a/src/main/java/com/server/capple/domain/board/controller/BoardController.java b/src/main/java/com/server/capple/domain/board/controller/BoardController.java new file mode 100644 index 00000000..209230f8 --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/controller/BoardController.java @@ -0,0 +1,17 @@ +package com.server.capple.domain.board.controller; + +import com.server.capple.domain.board.service.BoardService; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "게시판 API", description = "게시판 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/boards") +public class BoardController { + + private final BoardService boardService; + +} diff --git a/src/main/java/com/server/capple/domain/board/dto/BoardRequest.java b/src/main/java/com/server/capple/domain/board/dto/BoardRequest.java new file mode 100644 index 00000000..68925988 --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/dto/BoardRequest.java @@ -0,0 +1,4 @@ +package com.server.capple.domain.board.dto; + +public class BoardRequest { +} diff --git a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java new file mode 100644 index 00000000..49a2e0ed --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java @@ -0,0 +1,4 @@ +package com.server.capple.domain.board.dto; + +public class BoardResponse { +} diff --git a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java new file mode 100644 index 00000000..0bb7d8df --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java @@ -0,0 +1,8 @@ +package com.server.capple.domain.board.mapper; + +import org.springframework.stereotype.Component; + +@Component +public class BoardMapper { + +} diff --git a/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java b/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java new file mode 100644 index 00000000..4cd89bb1 --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java @@ -0,0 +1,7 @@ +package com.server.capple.domain.board.repository; + +import com.server.capple.domain.board.entity.Board; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BoardRepository extends JpaRepository { +} diff --git a/src/main/java/com/server/capple/domain/board/service/BoardService.java b/src/main/java/com/server/capple/domain/board/service/BoardService.java new file mode 100644 index 00000000..9049a876 --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/service/BoardService.java @@ -0,0 +1,4 @@ +package com.server.capple.domain.board.service; + +public interface BoardService { +} diff --git a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java new file mode 100644 index 00000000..28f780a4 --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java @@ -0,0 +1,16 @@ +package com.server.capple.domain.board.service; + +import com.server.capple.domain.board.mapper.BoardMapper; +import com.server.capple.domain.board.repository.BoardRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BoardServiceImpl implements BoardService { + + private final BoardRepository boardRepository; + private final BoardMapper boardMapper; +} From 396275bdf710a30084cd7ab7964c2f65797a542d Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Mon, 19 Aug 2024 23:30:29 +0900 Subject: [PATCH 04/21] =?UTF-8?q?feat:=20#128=20BoardType=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/capple/domain/board/entity/Board.java | 4 ++++ .../capple/domain/board/entity/BoardType.java | 13 +++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/main/java/com/server/capple/domain/board/entity/BoardType.java diff --git a/src/main/java/com/server/capple/domain/board/entity/Board.java b/src/main/java/com/server/capple/domain/board/entity/Board.java index f168df31..951b303c 100644 --- a/src/main/java/com/server/capple/domain/board/entity/Board.java +++ b/src/main/java/com/server/capple/domain/board/entity/Board.java @@ -23,6 +23,10 @@ public class Board extends BaseEntity { @JoinColumn(name = "member_id", nullable = false) private Member member; + @Column(nullable = false) + private BoardType boardType; + + @Column(nullable = false) private String content; diff --git a/src/main/java/com/server/capple/domain/board/entity/BoardType.java b/src/main/java/com/server/capple/domain/board/entity/BoardType.java new file mode 100644 index 00000000..a0e8a753 --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/entity/BoardType.java @@ -0,0 +1,13 @@ +package com.server.capple.domain.board.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BoardType { + FREEBOARD("자유게시판"), + HOTBOARD("인기게시판"); + + private final String toKorean; +} From fb80dc89afec7e05105a96ba5cd33290918a5685 Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Tue, 20 Aug 2024 00:12:27 +0900 Subject: [PATCH 05/21] =?UTF-8?q?feat:=20#128=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=9E=91=EC=84=B1=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/security/SecurityConfig.java | 1 + .../board/controller/BoardController.java | 24 ++++++++++++++-- .../capple/domain/board/dto/BoardRequest.java | 15 ++++++++++ .../domain/board/dto/BoardResponse.java | 13 +++++++++ .../domain/board/mapper/BoardMapper.java | 28 +++++++++++++++++++ .../domain/board/service/BoardService.java | 5 ++++ .../board/service/BoardServiceImpl.java | 19 ++++++++++++- .../exception/errorCode/BoardErrorCode.java | 28 +++++++++++++++++++ 8 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/server/capple/global/exception/errorCode/BoardErrorCode.java diff --git a/src/main/java/com/server/capple/config/security/SecurityConfig.java b/src/main/java/com/server/capple/config/security/SecurityConfig.java index aff7da82..a4b96fef 100644 --- a/src/main/java/com/server/capple/config/security/SecurityConfig.java +++ b/src/main/java/com/server/capple/config/security/SecurityConfig.java @@ -55,6 +55,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/tags","/tags/**").authenticated() .requestMatchers("/questions","/questions/**").authenticated() .requestMatchers("/reports", "/reports/**").authenticated() + .requestMatchers("/boards", "/boards/**").authenticated() .anyRequest().denyAll()); http .addFilterBefore(new JwtFilter(jwtService), UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/com/server/capple/domain/board/controller/BoardController.java b/src/main/java/com/server/capple/domain/board/controller/BoardController.java index 209230f8..766ad4a3 100644 --- a/src/main/java/com/server/capple/domain/board/controller/BoardController.java +++ b/src/main/java/com/server/capple/domain/board/controller/BoardController.java @@ -1,10 +1,18 @@ package com.server.capple.domain.board.controller; +import com.server.capple.config.security.AuthMember; +import com.server.capple.domain.board.dto.BoardRequest; +import com.server.capple.domain.board.dto.BoardResponse; import com.server.capple.domain.board.service.BoardService; +import com.server.capple.domain.member.entity.Member; +import com.server.capple.domain.question.dto.response.QuestionResponse; +import com.server.capple.global.common.BaseResponse; +import io.swagger.v3.oas.annotations.Operation; +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 lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @Tag(name = "게시판 API", description = "게시판 관련 API") @RestController @@ -14,4 +22,16 @@ public class BoardController { private final BoardService boardService; + @Operation(summary = "게시판 생성 API", description = "게시판을 생성합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + }) + @PostMapping() + private BaseResponse createBoard( + @AuthMember Member member, + @RequestBody BoardRequest.BoardCreate request + ) { + return BaseResponse.onSuccess(boardService.createBoard(member, request.getBoardType(), request.getContent())); + } + } diff --git a/src/main/java/com/server/capple/domain/board/dto/BoardRequest.java b/src/main/java/com/server/capple/domain/board/dto/BoardRequest.java index 68925988..65410197 100644 --- a/src/main/java/com/server/capple/domain/board/dto/BoardRequest.java +++ b/src/main/java/com/server/capple/domain/board/dto/BoardRequest.java @@ -1,4 +1,19 @@ package com.server.capple.domain.board.dto; +import com.server.capple.domain.board.entity.BoardType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + public class BoardRequest { + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BoardCreate { + private String content; + private BoardType boardType; + } } diff --git a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java index 49a2e0ed..03865941 100644 --- a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java +++ b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java @@ -1,4 +1,17 @@ package com.server.capple.domain.board.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + public class BoardResponse { + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BoardCreate { + private Long boardId; + } } diff --git a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java index 0bb7d8df..7f963643 100644 --- a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java +++ b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java @@ -1,8 +1,36 @@ package com.server.capple.domain.board.mapper; +import com.server.capple.domain.board.dto.BoardRequest; +import com.server.capple.domain.board.dto.BoardResponse; +import com.server.capple.domain.board.entity.Board; +import com.server.capple.domain.board.entity.BoardType; +import com.server.capple.domain.member.entity.Member; import org.springframework.stereotype.Component; @Component public class BoardMapper { + public Board toBoard( + Member member, + BoardType boardType, + String content, + Integer heartCount, + Integer commentCount + ) { + return Board.builder() + .member(member) + .boardType(boardType) + .content(content) + .heartCount(heartCount) + .commentCount(commentCount) + .build(); + } + + public BoardResponse.BoardCreate toBoardCreate( + Board board + ) { + return BoardResponse.BoardCreate.builder() + .boardId(board.getId()) + .build(); + } } diff --git a/src/main/java/com/server/capple/domain/board/service/BoardService.java b/src/main/java/com/server/capple/domain/board/service/BoardService.java index 9049a876..3d7c0bb8 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardService.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardService.java @@ -1,4 +1,9 @@ package com.server.capple.domain.board.service; +import com.server.capple.domain.board.dto.BoardResponse; +import com.server.capple.domain.board.entity.BoardType; +import com.server.capple.domain.member.entity.Member; + public interface BoardService { + BoardResponse.BoardCreate createBoard(Member member, BoardType boardType, String content); } diff --git a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java index 28f780a4..d6f9903d 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java @@ -1,16 +1,33 @@ package com.server.capple.domain.board.service; +import com.server.capple.domain.board.dto.BoardResponse; +import com.server.capple.domain.board.entity.Board; +import com.server.capple.domain.board.entity.BoardType; import com.server.capple.domain.board.mapper.BoardMapper; import com.server.capple.domain.board.repository.BoardRepository; +import com.server.capple.domain.member.entity.Member; +import com.server.capple.global.exception.RestApiException; +import com.server.capple.global.exception.errorCode.BoardErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor -@Transactional(readOnly = true) +@Transactional public class BoardServiceImpl implements BoardService { private final BoardRepository boardRepository; private final BoardMapper boardMapper; + + @Override + public BoardResponse.BoardCreate createBoard(Member member, BoardType boardType, String content) { + Board board; + if (content != null) { + board = boardRepository.save(boardMapper.toBoard(member, boardType, content, 0, 0)); + } else { + throw new RestApiException(BoardErrorCode.BOARD_BAD_REQUEST); + } + return boardMapper.toBoardCreate(board); + } } diff --git a/src/main/java/com/server/capple/global/exception/errorCode/BoardErrorCode.java b/src/main/java/com/server/capple/global/exception/errorCode/BoardErrorCode.java new file mode 100644 index 00000000..18c6660d --- /dev/null +++ b/src/main/java/com/server/capple/global/exception/errorCode/BoardErrorCode.java @@ -0,0 +1,28 @@ +package com.server.capple.global.exception.errorCode; + +import com.server.capple.global.exception.ErrorCode; +import com.server.capple.global.exception.ErrorCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum BoardErrorCode implements ErrorCodeInterface { + BOARD_NOT_FOUND("BOARD001", "게시글이 존재하지 않습니다.", HttpStatus.NOT_FOUND), + BOARD_BAD_REQUEST("BOARD002", "게시글에 내용이 없습니다.",HttpStatus.BAD_REQUEST), + BOARD_NO_AUTHORIZATION("BOARD003", "해당 게시글에 삭제 권한이 없습니다.", HttpStatus.FORBIDDEN) + ; + private final String code; + private final String message; + private final HttpStatus httpStatus; + + @Override + public ErrorCode getErrorCode() { + return ErrorCode.builder() + .code(code) + .message(message) + .httpStatus(httpStatus) + .build(); + } +} From 7ee47d1f4b30d30278836cf6d1d7c3a9e79fea4f Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Tue, 20 Aug 2024 01:11:16 +0900 Subject: [PATCH 06/21] =?UTF-8?q?feat:=20#128=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=EB=B3=84=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/BoardController.java | 18 +++++++++++++ .../domain/board/dto/BoardResponse.java | 25 +++++++++++++++++++ .../domain/board/mapper/BoardMapper.java | 22 ++++++++++++++++ .../board/repository/BoardRepository.java | 5 ++++ .../domain/board/service/BoardService.java | 2 ++ .../board/service/BoardServiceImpl.java | 21 ++++++++++++++++ 6 files changed, 93 insertions(+) diff --git a/src/main/java/com/server/capple/domain/board/controller/BoardController.java b/src/main/java/com/server/capple/domain/board/controller/BoardController.java index 766ad4a3..0fc3b6a1 100644 --- a/src/main/java/com/server/capple/domain/board/controller/BoardController.java +++ b/src/main/java/com/server/capple/domain/board/controller/BoardController.java @@ -3,15 +3,20 @@ import com.server.capple.config.security.AuthMember; import com.server.capple.domain.board.dto.BoardRequest; import com.server.capple.domain.board.dto.BoardResponse; +import com.server.capple.domain.board.entity.BoardType; import com.server.capple.domain.board.service.BoardService; import com.server.capple.domain.member.entity.Member; import com.server.capple.domain.question.dto.response.QuestionResponse; import com.server.capple.global.common.BaseResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; 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 lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; @Tag(name = "게시판 API", description = "게시판 관련 API") @@ -34,4 +39,17 @@ private BaseResponse createBoard( return BaseResponse.onSuccess(boardService.createBoard(member, request.getBoardType(), request.getContent())); } + @Operation(summary = "카테고리별 게시판 조회 API", description = "카테고리별 게시판을 생성합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + }) + @GetMapping() + private BaseResponse getBoardsByBoardType( + @RequestParam(name = "boardType", required = false) BoardType boardType + // TODO: 페이징 프론트 이슈로 추후 구현 +// @PageableDefault(sort = "created_at", direction = Sort.Direction.DESC) @Parameter(hidden = true) Pageable pageable + ) { + return BaseResponse.onSuccess(boardService.getBoardsByBoardType(boardType)); + } + } diff --git a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java index 03865941..5258a008 100644 --- a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java +++ b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java @@ -1,10 +1,15 @@ package com.server.capple.domain.board.dto; +import com.server.capple.domain.member.entity.Member; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + public class BoardResponse { @Getter @@ -14,4 +19,24 @@ public class BoardResponse { public static class BoardCreate { private Long boardId; } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BoardsGetByBoardType { + private List boards = new ArrayList<>(); + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BoardsGetByBoardTypeBoardInfo { + private Long writerId; + private String content; + private Integer heartCount; + private Integer commentCount; + private LocalDateTime createAt; + } } diff --git a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java index 7f963643..8805b462 100644 --- a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java +++ b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java @@ -7,6 +7,8 @@ import com.server.capple.domain.member.entity.Member; import org.springframework.stereotype.Component; +import java.util.List; + @Component public class BoardMapper { @@ -33,4 +35,24 @@ public BoardResponse.BoardCreate toBoardCreate( .boardId(board.getId()) .build(); } + + public BoardResponse.BoardsGetByBoardType toBoardsGetByBoardType( + List boards + ) { + return BoardResponse.BoardsGetByBoardType.builder() + .boards(boards) + .build(); + } + + public BoardResponse.BoardsGetByBoardTypeBoardInfo toBoardsGetByBoardTypeBoardInfo( + Board board + ) { + return BoardResponse.BoardsGetByBoardTypeBoardInfo.builder() + .writerId(board.getMember().getId()) + .content(board.getContent()) + .heartCount(board.getHeartCount()) + .commentCount(board.getCommentCount()) + .createAt(board.getCreatedAt()) + .build(); + } } diff --git a/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java b/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java index 4cd89bb1..5a247477 100644 --- a/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java +++ b/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java @@ -1,7 +1,12 @@ package com.server.capple.domain.board.repository; import com.server.capple.domain.board.entity.Board; +import com.server.capple.domain.board.entity.BoardType; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface BoardRepository extends JpaRepository { + + List findBoardsByBoardType(BoardType boardType); } diff --git a/src/main/java/com/server/capple/domain/board/service/BoardService.java b/src/main/java/com/server/capple/domain/board/service/BoardService.java index 3d7c0bb8..08bfbfa2 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardService.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardService.java @@ -6,4 +6,6 @@ public interface BoardService { BoardResponse.BoardCreate createBoard(Member member, BoardType boardType, String content); + + BoardResponse.BoardsGetByBoardType getBoardsByBoardType(BoardType boardType); } diff --git a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java index d6f9903d..e06da3ef 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java @@ -12,6 +12,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; + @Service @RequiredArgsConstructor @Transactional @@ -30,4 +33,22 @@ public BoardResponse.BoardCreate createBoard(Member member, BoardType boardType, } return boardMapper.toBoardCreate(board); } + + @Override + public BoardResponse.BoardsGetByBoardType getBoardsByBoardType(BoardType boardType) { + List boards = new ArrayList<>(); + if (boardType == null) { + boards = boardRepository.findAll(); + } else if (boardType == BoardType.FREEBOARD) { + boards = boardRepository.findBoardsByBoardType(BoardType.FREEBOARD); + } else if (boardType == BoardType.HOTBOARD) { + boards = boardRepository.findBoardsByBoardType(BoardType.HOTBOARD); + } else { + throw new RestApiException(BoardErrorCode.BOARD_BAD_REQUEST); + } + return boardMapper.toBoardsGetByBoardType(boards.stream() + .map(boardMapper::toBoardsGetByBoardTypeBoardInfo) + .toList() + ); + } } From 1ee9934fa8817b332d3c97ea6217aba7ea4ead5a Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Tue, 20 Aug 2024 01:30:13 +0900 Subject: [PATCH 07/21] =?UTF-8?q?feat:=20#128=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=82=AD=EC=A0=9C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/controller/BoardController.java | 12 ++++++++++++ .../capple/domain/board/dto/BoardResponse.java | 8 ++++++++ .../server/capple/domain/board/entity/Board.java | 2 +- .../capple/domain/board/mapper/BoardMapper.java | 10 ++++++++-- .../capple/domain/board/service/BoardService.java | 2 ++ .../domain/board/service/BoardServiceImpl.java | 14 ++++++++++++++ 6 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/server/capple/domain/board/controller/BoardController.java b/src/main/java/com/server/capple/domain/board/controller/BoardController.java index 0fc3b6a1..293d89ba 100644 --- a/src/main/java/com/server/capple/domain/board/controller/BoardController.java +++ b/src/main/java/com/server/capple/domain/board/controller/BoardController.java @@ -52,4 +52,16 @@ private BaseResponse getBoardsByBoardType( return BaseResponse.onSuccess(boardService.getBoardsByBoardType(boardType)); } + @Operation(summary = "게시글 삭제 API", description = "게시글을 삭제합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + }) + @DeleteMapping("/{boardId}") + private BaseResponse deleteBoard( + @AuthMember Member member, + @PathVariable(name = "boardId") Long boardId + ) { + return BaseResponse.onSuccess(boardService.deleteBoard(member, boardId)); + } + } diff --git a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java index 5258a008..5e1565c0 100644 --- a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java +++ b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java @@ -39,4 +39,12 @@ public static class BoardsGetByBoardTypeBoardInfo { private Integer commentCount; private LocalDateTime createAt; } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BoardDelete { + private Long boardId; + } } diff --git a/src/main/java/com/server/capple/domain/board/entity/Board.java b/src/main/java/com/server/capple/domain/board/entity/Board.java index 951b303c..0ee20372 100644 --- a/src/main/java/com/server/capple/domain/board/entity/Board.java +++ b/src/main/java/com/server/capple/domain/board/entity/Board.java @@ -21,7 +21,7 @@ public class Board extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) - private Member member; + private Member writer; @Column(nullable = false) private BoardType boardType; diff --git a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java index 8805b462..85599174 100644 --- a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java +++ b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java @@ -20,7 +20,7 @@ public Board toBoard( Integer commentCount ) { return Board.builder() - .member(member) + .writer(member) .boardType(boardType) .content(content) .heartCount(heartCount) @@ -48,11 +48,17 @@ public BoardResponse.BoardsGetByBoardTypeBoardInfo toBoardsGetByBoardTypeBoardIn Board board ) { return BoardResponse.BoardsGetByBoardTypeBoardInfo.builder() - .writerId(board.getMember().getId()) + .writerId(board.getWriter().getId()) .content(board.getContent()) .heartCount(board.getHeartCount()) .commentCount(board.getCommentCount()) .createAt(board.getCreatedAt()) .build(); } + + public BoardResponse.BoardDelete toBoardDelete(Board board) { + return BoardResponse.BoardDelete.builder() + .boardId(board.getId()) + .build(); + } } diff --git a/src/main/java/com/server/capple/domain/board/service/BoardService.java b/src/main/java/com/server/capple/domain/board/service/BoardService.java index 08bfbfa2..125dd8c9 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardService.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardService.java @@ -8,4 +8,6 @@ public interface BoardService { BoardResponse.BoardCreate createBoard(Member member, BoardType boardType, String content); BoardResponse.BoardsGetByBoardType getBoardsByBoardType(BoardType boardType); + + BoardResponse.BoardDelete deleteBoard(Member member, Long boardId); } diff --git a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java index e06da3ef..6c3e1f48 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; @Service @RequiredArgsConstructor @@ -51,4 +52,17 @@ public BoardResponse.BoardsGetByBoardType getBoardsByBoardType(BoardType boardTy .toList() ); } + + @Override + public BoardResponse.BoardDelete deleteBoard(Member member, Long boardId) { + Board board = boardRepository.findById(boardId) + .orElseThrow(() -> new RestApiException(BoardErrorCode.BOARD_NOT_FOUND)); + + if (board.getWriter().getId() != member.getId()) { + throw new RestApiException(BoardErrorCode.BOARD_NO_AUTHORIZATION); + } + + board.delete(); + return boardMapper.toBoardDelete(board); + } } From ded08b9bdfc693492167a26acfb351badcb435ea Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Tue, 20 Aug 2024 01:57:13 +0900 Subject: [PATCH 08/21] =?UTF-8?q?feat:=20#128=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EA=B2=80=EC=83=89=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/BoardController.java | 15 ++++++++++++-- .../domain/board/dto/BoardResponse.java | 20 +++++++++++++++++++ .../domain/board/mapper/BoardMapper.java | 20 +++++++++++++++++++ .../board/repository/BoardRepository.java | 8 ++++++++ .../domain/board/service/BoardService.java | 3 +++ .../board/service/BoardServiceImpl.java | 11 ++++++++++ 6 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/server/capple/domain/board/controller/BoardController.java b/src/main/java/com/server/capple/domain/board/controller/BoardController.java index 293d89ba..70901b48 100644 --- a/src/main/java/com/server/capple/domain/board/controller/BoardController.java +++ b/src/main/java/com/server/capple/domain/board/controller/BoardController.java @@ -27,7 +27,7 @@ public class BoardController { private final BoardService boardService; - @Operation(summary = "게시판 생성 API", description = "게시판을 생성합니다.") + @Operation(summary = "게시글 생성 API", description = "게시글을 생성합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "COMMON200", description = "성공"), }) @@ -39,7 +39,7 @@ private BaseResponse createBoard( return BaseResponse.onSuccess(boardService.createBoard(member, request.getBoardType(), request.getContent())); } - @Operation(summary = "카테고리별 게시판 조회 API", description = "카테고리별 게시판을 생성합니다.") + @Operation(summary = "카테고리별 게시글 조회 API", description = "카테고리별 게시글을 조회합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "COMMON200", description = "성공"), }) @@ -64,4 +64,15 @@ private BaseResponse deleteBoard( return BaseResponse.onSuccess(boardService.deleteBoard(member, boardId)); } + @Operation(summary = "게시글 검색 API", description = "게시글을 검색합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공"), + }) + @GetMapping("/search") + private BaseResponse searchBoardsByKeyword( + @RequestParam(name = "keyword") String keyword + ) { + return BaseResponse.onSuccess(boardService.searchBoardsByKeyword(keyword)); + } + } diff --git a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java index 5e1565c0..1e659b37 100644 --- a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java +++ b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java @@ -47,4 +47,24 @@ public static class BoardsGetByBoardTypeBoardInfo { public static class BoardDelete { private Long boardId; } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BoardsSearchByKeyword { + private List boards = new ArrayList<>(); + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BoardsSearchByKeywordBoardInfo { + private Long writerId; + private String content; + private Integer heartCount; + private Integer commentCount; + private LocalDateTime createAt; + } } diff --git a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java index 85599174..1e44629d 100644 --- a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java +++ b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java @@ -61,4 +61,24 @@ public BoardResponse.BoardDelete toBoardDelete(Board board) { .boardId(board.getId()) .build(); } + + public BoardResponse.BoardsSearchByKeywordBoardInfo toBoardsSearchByKeywordBoardInfo( + Board board + ) { + return BoardResponse.BoardsSearchByKeywordBoardInfo.builder() + .writerId(board.getWriter().getId()) + .content(board.getContent()) + .heartCount(board.getHeartCount()) + .commentCount(board.getCommentCount()) + .createAt(board.getCreatedAt()) + .build(); + } + + public BoardResponse.BoardsSearchByKeyword toBoardsSearchByKeyword( + List boards + ) { + return BoardResponse.BoardsSearchByKeyword.builder() + .boards(boards) + .build(); + } } diff --git a/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java b/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java index 5a247477..789beffb 100644 --- a/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java +++ b/src/main/java/com/server/capple/domain/board/repository/BoardRepository.java @@ -1,12 +1,20 @@ package com.server.capple.domain.board.repository; +import com.server.capple.domain.answer.entity.Answer; import com.server.capple.domain.board.entity.Board; import com.server.capple.domain.board.entity.BoardType; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.List; +import java.util.Optional; public interface BoardRepository extends JpaRepository { List findBoardsByBoardType(BoardType boardType); + + @Query("SELECT b FROM Board b WHERE b.content LIKE %:keyword%") + List findBoardsByKeyword(String keyword); } diff --git a/src/main/java/com/server/capple/domain/board/service/BoardService.java b/src/main/java/com/server/capple/domain/board/service/BoardService.java index 125dd8c9..1a213b0a 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardService.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardService.java @@ -3,6 +3,7 @@ import com.server.capple.domain.board.dto.BoardResponse; import com.server.capple.domain.board.entity.BoardType; import com.server.capple.domain.member.entity.Member; +import org.springframework.data.domain.Pageable; public interface BoardService { BoardResponse.BoardCreate createBoard(Member member, BoardType boardType, String content); @@ -10,4 +11,6 @@ public interface BoardService { BoardResponse.BoardsGetByBoardType getBoardsByBoardType(BoardType boardType); BoardResponse.BoardDelete deleteBoard(Member member, Long boardId); + + BoardResponse.BoardsSearchByKeyword searchBoardsByKeyword(String keyword); } diff --git a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java index 6c3e1f48..6caf7c13 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java @@ -9,6 +9,7 @@ import com.server.capple.global.exception.RestApiException; import com.server.capple.global.exception.errorCode.BoardErrorCode; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -65,4 +66,14 @@ public BoardResponse.BoardDelete deleteBoard(Member member, Long boardId) { board.delete(); return boardMapper.toBoardDelete(board); } + + @Override + public BoardResponse.BoardsSearchByKeyword searchBoardsByKeyword(String keyword) { + List boards = boardRepository.findBoardsByKeyword(keyword); + return boardMapper.toBoardsSearchByKeyword(boards.stream() + .map(boardMapper::toBoardsSearchByKeywordBoardInfo) + .toList()); + } + + } From 183552936f2204a88969ff61d13b816a62626dfb Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Tue, 20 Aug 2024 02:59:28 +0900 Subject: [PATCH 09/21] =?UTF-8?q?feat:=20#128=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=A2=8B=EC=95=84=EC=9A=94=20=ED=86=A0=EA=B8=80=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/BoardController.java | 8 +++ .../domain/board/dto/BoardResponse.java | 11 ++++ .../capple/domain/board/entity/Board.java | 4 -- .../domain/board/mapper/BoardMapper.java | 18 +++--- .../repository/BoardHeartRedisRepository.java | 60 +++++++++++++++++++ .../domain/board/service/BoardService.java | 2 + .../board/service/BoardServiceImpl.java | 17 ++++-- 7 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java diff --git a/src/main/java/com/server/capple/domain/board/controller/BoardController.java b/src/main/java/com/server/capple/domain/board/controller/BoardController.java index 70901b48..82035305 100644 --- a/src/main/java/com/server/capple/domain/board/controller/BoardController.java +++ b/src/main/java/com/server/capple/domain/board/controller/BoardController.java @@ -1,6 +1,7 @@ package com.server.capple.domain.board.controller; import com.server.capple.config.security.AuthMember; +import com.server.capple.domain.answer.dto.AnswerResponse; import com.server.capple.domain.board.dto.BoardRequest; import com.server.capple.domain.board.dto.BoardResponse; import com.server.capple.domain.board.entity.BoardType; @@ -75,4 +76,11 @@ private BaseResponse searchBoardsByKeyword( return BaseResponse.onSuccess(boardService.searchBoardsByKeyword(keyword)); } + @Operation(summary = "게시글 좋아요/취소 API", description = " 게시글 좋아요/취소 API 입니다." + + "pathvariable 으로 boardId를 주세요.") + @PostMapping("/{boardId}/heart") + public BaseResponse toggleBoardHeart(@AuthMember Member member, @PathVariable(value = "boardId") Long boardId) { + return BaseResponse.onSuccess(boardService.toggleBoardHeart(member, boardId)); + } + } diff --git a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java index 1e659b37..5627f505 100644 --- a/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java +++ b/src/main/java/com/server/capple/domain/board/dto/BoardResponse.java @@ -33,6 +33,7 @@ public static class BoardsGetByBoardType { @AllArgsConstructor @NoArgsConstructor public static class BoardsGetByBoardTypeBoardInfo { + private Long boardId; private Long writerId; private String content; private Integer heartCount; @@ -61,10 +62,20 @@ public static class BoardsSearchByKeyword { @AllArgsConstructor @NoArgsConstructor public static class BoardsSearchByKeywordBoardInfo { + private Long boardId; private Long writerId; private String content; private Integer heartCount; private Integer commentCount; private LocalDateTime createAt; } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BoardToggleHeart { + private Long boardId; + private Boolean isLiked; + } } diff --git a/src/main/java/com/server/capple/domain/board/entity/Board.java b/src/main/java/com/server/capple/domain/board/entity/Board.java index 0ee20372..af811828 100644 --- a/src/main/java/com/server/capple/domain/board/entity/Board.java +++ b/src/main/java/com/server/capple/domain/board/entity/Board.java @@ -26,13 +26,9 @@ public class Board extends BaseEntity { @Column(nullable = false) private BoardType boardType; - @Column(nullable = false) private String content; - @Column(nullable = false) - private Integer heartCount; - @Column(nullable = false) private Integer commentCount; } diff --git a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java index 1e44629d..337ffe5a 100644 --- a/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java +++ b/src/main/java/com/server/capple/domain/board/mapper/BoardMapper.java @@ -1,6 +1,5 @@ package com.server.capple.domain.board.mapper; -import com.server.capple.domain.board.dto.BoardRequest; import com.server.capple.domain.board.dto.BoardResponse; import com.server.capple.domain.board.entity.Board; import com.server.capple.domain.board.entity.BoardType; @@ -23,7 +22,6 @@ public Board toBoard( .writer(member) .boardType(boardType) .content(content) - .heartCount(heartCount) .commentCount(commentCount) .build(); } @@ -45,13 +43,15 @@ public BoardResponse.BoardsGetByBoardType toBoardsGetByBoardType( } public BoardResponse.BoardsGetByBoardTypeBoardInfo toBoardsGetByBoardTypeBoardInfo( - Board board - ) { + Board board, + Integer boardHeartsCount) { return BoardResponse.BoardsGetByBoardTypeBoardInfo.builder() + .boardId(board.getId()) .writerId(board.getWriter().getId()) .content(board.getContent()) - .heartCount(board.getHeartCount()) - .commentCount(board.getCommentCount()) + .heartCount(boardHeartsCount) + // TODO : 댓글 작성 API 나오면 추후 구현 + .commentCount(0) .createAt(board.getCreatedAt()) .build(); } @@ -63,12 +63,14 @@ public BoardResponse.BoardDelete toBoardDelete(Board board) { } public BoardResponse.BoardsSearchByKeywordBoardInfo toBoardsSearchByKeywordBoardInfo( - Board board + Board board, + Integer heartCount ) { return BoardResponse.BoardsSearchByKeywordBoardInfo.builder() + .boardId(board.getId()) .writerId(board.getWriter().getId()) .content(board.getContent()) - .heartCount(board.getHeartCount()) + .heartCount(heartCount) .commentCount(board.getCommentCount()) .createAt(board.getCreatedAt()) .build(); diff --git a/src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java b/src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java new file mode 100644 index 00000000..1da32f2d --- /dev/null +++ b/src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java @@ -0,0 +1,60 @@ +package com.server.capple.domain.board.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.SetOperations; +import org.springframework.stereotype.Repository; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +@Repository +@RequiredArgsConstructor +public class BoardHeartRedisRepository implements Serializable { + public static final String BOARD_HEART_KEY_PREFIX = "boardHeart-"; + public static final String MEMBER_KEY_PREFIX = "member-"; + + private final RedisTemplate redisTemplate; + + // 게시판 좋아요 토글 + public Boolean toggleBoardHeart(Long memberId, Long boardId) { + String key = BOARD_HEART_KEY_PREFIX + boardId.toString(); + String member = MEMBER_KEY_PREFIX + memberId.toString(); + SetOperations setOperations = redisTemplate.opsForSet(); + + //해당 key에 member가 존재하지 않으면 추가, 존재하면 삭제 + if (FALSE.equals(setOperations.isMember(key, member))) { + setOperations.add(key, member); + return TRUE; + } else { + setOperations.remove(key, member); + return FALSE; + } + } + + // 게시판 좋아요 수 조회 + public Integer getBoardHeartsCount(Long boardId) { + String key = BOARD_HEART_KEY_PREFIX + boardId.toString(); + Set members = redisTemplate.opsForSet().members(key); + return members != null ? members.size() : 0; + } + + // 좋아요 누른 게시판 조회 + public Set getMemberHeartsBoard(Long memberId) { + String member = MEMBER_KEY_PREFIX + memberId.toString(); + Set keys = redisTemplate.keys(BOARD_HEART_KEY_PREFIX + "*"); // 모든 키 조회 + Set boardIds = new HashSet<>(); + + for (String key : keys) { + if (redisTemplate.opsForSet().isMember(key, member)) { + String boardId = key.substring(BOARD_HEART_KEY_PREFIX.length()); + boardIds.add(Long.parseLong(boardId)); + } + } + return boardIds; + } +} \ No newline at end of file diff --git a/src/main/java/com/server/capple/domain/board/service/BoardService.java b/src/main/java/com/server/capple/domain/board/service/BoardService.java index 1a213b0a..79e19f68 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardService.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardService.java @@ -13,4 +13,6 @@ public interface BoardService { BoardResponse.BoardDelete deleteBoard(Member member, Long boardId); BoardResponse.BoardsSearchByKeyword searchBoardsByKeyword(String keyword); + + BoardResponse.BoardToggleHeart toggleBoardHeart(Member member, Long boardId); } diff --git a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java index 6caf7c13..c938b458 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java @@ -4,18 +4,17 @@ import com.server.capple.domain.board.entity.Board; import com.server.capple.domain.board.entity.BoardType; import com.server.capple.domain.board.mapper.BoardMapper; +import com.server.capple.domain.board.repository.BoardHeartRedisRepository; import com.server.capple.domain.board.repository.BoardRepository; import com.server.capple.domain.member.entity.Member; import com.server.capple.global.exception.RestApiException; import com.server.capple.global.exception.errorCode.BoardErrorCode; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; -import java.util.Optional; @Service @RequiredArgsConstructor @@ -23,6 +22,7 @@ public class BoardServiceImpl implements BoardService { private final BoardRepository boardRepository; + private final BoardHeartRedisRepository boardHeartRedisRepository; private final BoardMapper boardMapper; @Override @@ -49,7 +49,7 @@ public BoardResponse.BoardsGetByBoardType getBoardsByBoardType(BoardType boardTy throw new RestApiException(BoardErrorCode.BOARD_BAD_REQUEST); } return boardMapper.toBoardsGetByBoardType(boards.stream() - .map(boardMapper::toBoardsGetByBoardTypeBoardInfo) + .map(board -> boardMapper.toBoardsGetByBoardTypeBoardInfo(board, boardHeartRedisRepository.getBoardHeartsCount(board.getId()))) .toList() ); } @@ -71,9 +71,18 @@ public BoardResponse.BoardDelete deleteBoard(Member member, Long boardId) { public BoardResponse.BoardsSearchByKeyword searchBoardsByKeyword(String keyword) { List boards = boardRepository.findBoardsByKeyword(keyword); return boardMapper.toBoardsSearchByKeyword(boards.stream() - .map(boardMapper::toBoardsSearchByKeywordBoardInfo) + .map(board -> boardMapper.toBoardsSearchByKeywordBoardInfo(board, boardHeartRedisRepository.getBoardHeartsCount(board.getId()))) .toList()); } + @Override + public BoardResponse.BoardToggleHeart toggleBoardHeart(Member member, Long boardId) { + Board board = boardRepository.findById(boardId) + .orElseThrow(() -> new RestApiException(BoardErrorCode.BOARD_NOT_FOUND)); + + Boolean isLiked = boardHeartRedisRepository.toggleBoardHeart(member.getId(), board.getId()); + return new BoardResponse.BoardToggleHeart(boardId, isLiked); + } + } From d7d61dee0f9126740836358c0ffaec3a5f0eef5b Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Tue, 20 Aug 2024 11:02:37 +0900 Subject: [PATCH 10/21] =?UTF-8?q?feat:=20#128=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/BoardHeartRedisRepository.java | 16 +++++++++++++++- .../domain/board/service/BoardServiceImpl.java | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java b/src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java index 1da32f2d..7f1432fa 100644 --- a/src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java +++ b/src/main/java/com/server/capple/domain/board/repository/BoardHeartRedisRepository.java @@ -3,9 +3,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.SetOperations; +import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Repository; import java.io.Serializable; +import java.time.LocalDateTime; import java.util.HashSet; import java.util.Set; @@ -21,21 +23,33 @@ public class BoardHeartRedisRepository implements Serializable { private final RedisTemplate redisTemplate; // 게시판 좋아요 토글 - public Boolean toggleBoardHeart(Long memberId, Long boardId) { + public Boolean toggleBoardHeart(Long boardId, Long memberId) { String key = BOARD_HEART_KEY_PREFIX + boardId.toString(); String member = MEMBER_KEY_PREFIX + memberId.toString(); + String createAtKey = key + ":" + member + ":createAt"; // member ID를 포함한 createAtKeyㄱ SetOperations setOperations = redisTemplate.opsForSet(); + ValueOperations valueOperations = redisTemplate.opsForValue(); //해당 key에 member가 존재하지 않으면 추가, 존재하면 삭제 if (FALSE.equals(setOperations.isMember(key, member))) { setOperations.add(key, member); + // 현재 시간을 createAtKey로 저장 + valueOperations.set(createAtKey, LocalDateTime.now().toString()); return TRUE; } else { setOperations.remove(key, member); + // 좋아요 취소 시 생성 시간도 삭제할 수 있음 + redisTemplate.delete(createAtKey); return FALSE; } } + // + public String getBoardHeartCreateAt(Long boardId, Long memberId) { + String createAtKey = BOARD_HEART_KEY_PREFIX + boardId.toString() + ":" + MEMBER_KEY_PREFIX + memberId.toString() + ":createAt"; + return redisTemplate.opsForValue().get(createAtKey); + } + // 게시판 좋아요 수 조회 public Integer getBoardHeartsCount(Long boardId) { String key = BOARD_HEART_KEY_PREFIX + boardId.toString(); diff --git a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java index c938b458..bb434e54 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java @@ -80,6 +80,8 @@ public BoardResponse.BoardToggleHeart toggleBoardHeart(Member member, Long board Board board = boardRepository.findById(boardId) .orElseThrow(() -> new RestApiException(BoardErrorCode.BOARD_NOT_FOUND)); + System.out.println(boardHeartRedisRepository.getBoardHeartCreateAt(board.getId(), member.getId())); + Boolean isLiked = boardHeartRedisRepository.toggleBoardHeart(member.getId(), board.getId()); return new BoardResponse.BoardToggleHeart(boardId, isLiked); } From 0c4ae5b8aca17ace8129ff0480bcc7984fa67d03 Mon Sep 17 00:00:00 2001 From: Jaewon Lee <58386334+jaewonLeeKOR@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:48:54 +0900 Subject: [PATCH 11/21] =?UTF-8?q?feat:=20#126=20APNs=20JWT=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=83=9D=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=BA=90=EC=8B=B1=20=EC=9E=91?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config | 2 +- .../com/server/capple/CappleApplication.java | 2 + .../com/server/capple/config/RedisConfig.java | 15 +++++++ .../security/jwt/service/JwtService.java | 1 + .../security/jwt/service/JwtServiceImpl.java | 39 ++++++++++++++++++- 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/config b/config index b3f9b9eb..aa26e0d5 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit b3f9b9eb11640d8a78212a230b45dc0356f8d8f2 +Subproject commit aa26e0d5147da13220e6505f619204812bc0586b diff --git a/src/main/java/com/server/capple/CappleApplication.java b/src/main/java/com/server/capple/CappleApplication.java index ed9f5bba..bcd0e14b 100644 --- a/src/main/java/com/server/capple/CappleApplication.java +++ b/src/main/java/com/server/capple/CappleApplication.java @@ -4,6 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; @@ -16,6 +17,7 @@ @EnableFeignClients @EnableConfigurationProperties @EnableScheduling +@EnableCaching public class CappleApplication { public static void main(String[] args) { diff --git a/src/main/java/com/server/capple/config/RedisConfig.java b/src/main/java/com/server/capple/config/RedisConfig.java index a330bdcd..f7eb58d3 100644 --- a/src/main/java/com/server/capple/config/RedisConfig.java +++ b/src/main/java/com/server/capple/config/RedisConfig.java @@ -1,14 +1,20 @@ package com.server.capple.config; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; +import java.time.Duration; + @Configuration public class RedisConfig { @@ -35,4 +41,13 @@ public RedisTemplate redisTemplate(RedisConnectionFactory redisC return redisTemplate; } + + @Bean + public CacheManager apnsJwtCacheManager(RedisConnectionFactory cf) { + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) + .entryTtl(Duration.ofMinutes(30)); + return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build(); + } } diff --git a/src/main/java/com/server/capple/config/security/jwt/service/JwtService.java b/src/main/java/com/server/capple/config/security/jwt/service/JwtService.java index 9d7934af..92f0fe13 100644 --- a/src/main/java/com/server/capple/config/security/jwt/service/JwtService.java +++ b/src/main/java/com/server/capple/config/security/jwt/service/JwtService.java @@ -16,4 +16,5 @@ public interface JwtService { Boolean isExpired(String token); Boolean checkJwt(String token); MemberResponse.Tokens refreshTokens(Long memberId, Role role); + String createApnsJwt(); } diff --git a/src/main/java/com/server/capple/config/security/jwt/service/JwtServiceImpl.java b/src/main/java/com/server/capple/config/security/jwt/service/JwtServiceImpl.java index 1267dff7..a138eaf2 100644 --- a/src/main/java/com/server/capple/config/security/jwt/service/JwtServiceImpl.java +++ b/src/main/java/com/server/capple/config/security/jwt/service/JwtServiceImpl.java @@ -11,6 +11,10 @@ import io.jsonwebtoken.*; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -18,16 +22,26 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.Security; +import java.util.Base64; import java.util.Date; @Component @RequiredArgsConstructor -public class JwtServiceImpl implements JwtService{ +public class JwtServiceImpl implements JwtService { private SecretKey secretKey; private final JwtProperties jwtProperties; private final JpaUserDetailService userDetailService; private final TokensMapper tokensMapper; + @Value("${apns.key-id}") + private String kid; + @Value("${apple-auth.team_id}") + private String iss; + @Value("${apns.key}") + private String apnsKeyString; + @PostConstruct protected void init() { secretKey = new SecretKeySpec(jwtProperties.getJwt_secret().getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS512.key().build().getAlgorithm()); @@ -119,4 +133,27 @@ public MemberResponse.Tokens refreshTokens(Long memberId, Role role) { String refreshToken = createJwt(memberId, role.getName(), "refresh"); return tokensMapper.toTokens(accessToken, refreshToken); } + + @Override + @Cacheable(value = "ApnsJwt", cacheManager = "apnsJwtCacheManager") + public String createApnsJwt() { + return Jwts.builder() + .header().add("alg", "ES256").add("kid", kid).and() + .issuer(iss) + .issuedAt(new Date(System.currentTimeMillis())) + .signWith(getPrivateKey(), SignatureAlgorithm.ES256) + .compact(); + } + + private PrivateKey getPrivateKey() { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + try { + byte[] privateKeyBytes = Base64.getDecoder().decode(apnsKeyString); + PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(privateKeyBytes); + return converter.getPrivateKey(privateKeyInfo); + } catch (Exception e) { + throw new RuntimeException("Error converting private key from String", e); + } + } } From 38c43d33ea4b169bf262e7edc9a4fe44f1fd6ca3 Mon Sep 17 00:00:00 2001 From: Jaewon Lee <58386334+jaewonLeeKOR@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:04:03 +0900 Subject: [PATCH 12/21] =?UTF-8?q?feat:=20#126=20APNs=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EB=B0=9C=EC=86=A1=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + config | 2 +- .../config/apns/config/ApnsClientConfig.java | 47 ++++++++++ .../config/apns/dto/ApnsClientRequest.java | 93 +++++++++++++++++++ .../config/apns/service/ApnsService.java | 7 ++ .../config/apns/service/ApnsServiceImpl.java | 74 +++++++++++++++ .../apns/service/ApnsServiceImplTest.java | 63 +++++++++++++ 7 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/server/capple/config/apns/config/ApnsClientConfig.java create mode 100644 src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java create mode 100644 src/main/java/com/server/capple/config/apns/service/ApnsService.java create mode 100644 src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java create mode 100644 src/test/java/com/server/capple/config/apns/service/ApnsServiceImplTest.java diff --git a/build.gradle b/build.gradle index a987461c..1d26fe12 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,8 @@ dependencies { // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 implementation 'com.google.code.findbugs:jsr305:3.0.2' +// webflux + implementation 'org.springframework.boot:spring-boot-starter-webflux' } dependencyManagement { diff --git a/config b/config index aa26e0d5..de326242 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit aa26e0d5147da13220e6505f619204812bc0586b +Subproject commit de3262429af531867ffdfba0b0fe6b152864982d diff --git a/src/main/java/com/server/capple/config/apns/config/ApnsClientConfig.java b/src/main/java/com/server/capple/config/apns/config/ApnsClientConfig.java new file mode 100644 index 00000000..c1003b09 --- /dev/null +++ b/src/main/java/com/server/capple/config/apns/config/ApnsClientConfig.java @@ -0,0 +1,47 @@ +package com.server.capple.config.apns.config; + +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.timeout.WriteTimeoutHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import reactor.netty.http.HttpProtocol; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; + +import javax.net.ssl.SSLException; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +@Configuration +public class ApnsClientConfig { + @Bean("apnsSslContext") + public SslContext getApnsSslContext() throws SSLException { + return SslContextBuilder.forClient().protocols("TLSv1.2").build(); // SSL 설정 + } + + @Bean("apnsConnectionProvider") + public ConnectionProvider getApnsConnectionProvider() { + return ConnectionProvider.builder("apns") + .maxConnections(10) // 최대 커낵션 수 + .pendingAcquireMaxCount(-1) // 재시도 횟수 (-1 : 무한대) + .pendingAcquireTimeout(java.time.Duration.ofSeconds(10)) // 커넥션 풀에 사용 가능한 커넥션 없을 때의 대기 시간 + .maxIdleTime(java.time.Duration.ofSeconds(5)) // 최대 유휴 시간 + .maxLifeTime(java.time.Duration.ofSeconds(300)) // 최대 생명 시간 + .lifo() // 후입선출 + .build(); + } + + @Bean("apnsH2HttpClient") + public HttpClient getApnsH2HttpClient(ConnectionProvider apnsConnectionProvider, SslContext apnsSslContext) { + return HttpClient.create(apnsConnectionProvider) // reactor HttpClient 생성 + .keepAlive(true) // keep-alive 활성화 + .protocol(HttpProtocol.H2) // HTTP/2 활성화 + .doOnConnected(connection -> connection.addHandlerLast(new WriteTimeoutHandler(10, TimeUnit.SECONDS))) // 쓰기 타임 아웃 + .doAfterRequest((req, conn) -> conn.addHandlerLast(new LoggingHandler(LogLevel.INFO))) + .responseTimeout(Duration.ofSeconds(10)) // 응답 타임 아웃 + .secure(sslSpec -> sslSpec.sslContext(apnsSslContext)); // SSL 활성화 + } +} diff --git a/src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java b/src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java new file mode 100644 index 00000000..1af62a3c --- /dev/null +++ b/src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java @@ -0,0 +1,93 @@ +package com.server.capple.config.apns.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +public class ApnsClientRequest { + + @Getter + @NoArgsConstructor + @ToString + public static class SimplePushBody { + private Aps aps; + + public SimplePushBody(String title, String subTitle, String body, Integer badge, String threaId, String targetContentId) { + this.aps = new Aps(new Aps.Alert(title, subTitle, body), badge, threaId, targetContentId); + } + + @ToString + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class Aps { + private Alert alert; + private Integer badge; + @JsonProperty("thread-id") + private String threadId; + @JsonProperty("target-content-id") + private String targetContentId; // 프론트 측 작업 필요함 + + @ToString + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class Alert { + private String title; + private String subtitle; + private String body; + } + } + } + + @Getter + @AllArgsConstructor + @NoArgsConstructor + @Builder + @ToString + public static class FullAlertBody { + private Aps aps; + + @Getter + @AllArgsConstructor + @NoArgsConstructor + @Builder + @ToString + public static class Aps { + private Alert alert; // alert 정보 + private Integer badge; // 앱 아이콘에 표시할 뱃지 숫자 + @Schema(defaultValue = "default") + private String sound; // Library/Sounds 폴더 내의 파일 이름 + @Schema(defaultValue = "thread-id") + private String threadId; // 알림 그룹화를 위한 thread id (UNNotificationContent 객체의 threadIdentifier와 일치해야 함) + private String category; // 알림 그룹화를 위한 category, (UNNotificationCategory 식별자와 일치해야 함) + @Schema(defaultValue = "0") + @JsonProperty("content-available") + private Integer contentAvailable; // 백그라운드 알림 여부, 1이면 백그라운드 알림, 0이면 포그라운드 알림 (백그라운드일 경우 alert, badge, sound는 넣으면 안됨) + @Schema(defaultValue = "0") + @JsonProperty("mutable-content") + private Integer mutableContent; // 알림 서비스 확장 플래그 + @Schema(defaultValue = "") + @JsonProperty("target-content-id") + private String targetContentId; // 알림이 클릭되었을 때 가져올 창의 식별자, UNNotificationContent 객체에 채워짐 + + @Getter + @AllArgsConstructor + @NoArgsConstructor + @Builder + @ToString + public static class Alert { + @Schema(defaultValue = "title") + private String title; + @Schema(defaultValue = "subTitle") + private String subtitle; + @Schema(defaultValue = "body") + private String body; + @Schema(defaultValue = "") + @JsonProperty("launch-image") + private String launchImage; // 실행시 보여줄 이미지 파일, 기본 실행 이미지 대신 입력한 이미지 또는 스토리보드가 켜짐 + } + } + } + +} diff --git a/src/main/java/com/server/capple/config/apns/service/ApnsService.java b/src/main/java/com/server/capple/config/apns/service/ApnsService.java new file mode 100644 index 00000000..ece346db --- /dev/null +++ b/src/main/java/com/server/capple/config/apns/service/ApnsService.java @@ -0,0 +1,7 @@ +package com.server.capple.config.apns.service; + +import java.util.List; + +public interface ApnsService { + Boolean sendApns(T request, List deviceToken); +} diff --git a/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java b/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java new file mode 100644 index 00000000..17ab9fb3 --- /dev/null +++ b/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java @@ -0,0 +1,74 @@ +package com.server.capple.config.apns.service; + +import com.server.capple.config.security.jwt.service.JwtService; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ApnsServiceImpl implements ApnsService { + private final JwtService jwtService; + private final HttpClient apnsH2HttpClient; + private WebClient defaultApnsWebClient; + + @Value("${apns.base-url}") + private String apnsBaseUrl; + @Value("${apns.base-sub-url}") + private String apnsBaseSubUrl; + @Value("${apple-auth.client_id}") + private String apnsTopic; + private final String apnsAlertPushType = "alert"; + + @PostConstruct + public void init() { + defaultApnsWebClient = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(apnsH2HttpClient)) + .baseUrl(apnsBaseUrl) + .defaultHeader("apns-topic", apnsTopic) + .defaultHeader("apns-push-type", apnsAlertPushType) + .build(); + } + + @Override + public Boolean sendApns(T request, List deviceToken) { + WebClient tmpWebClient = defaultApnsWebClient.mutate() + .defaultHeader("authorization", "bearer " + jwtService.createApnsJwt()) + .build(); + + WebClient tmpSubWebClient = tmpWebClient.mutate() + .baseUrl(apnsBaseSubUrl) + .build(); + + deviceToken.parallelStream().forEach(token -> { + tmpWebClient + .method(HttpMethod.POST) + .uri(token) + .bodyValue(request) + .retrieve() + .bodyToMono(String.class) + .doOnError(e -> { // 에러 발생 시 보조 채널로 재시도 + tmpSubWebClient + .method(HttpMethod.POST) + .uri(token) + .bodyValue(request) + .retrieve() + .bodyToMono(String.class) + .block(); + log.error("APNs 전송 중 오류 발생", e); + }) + .block(); + }); + + return true; + } +} diff --git a/src/test/java/com/server/capple/config/apns/service/ApnsServiceImplTest.java b/src/test/java/com/server/capple/config/apns/service/ApnsServiceImplTest.java new file mode 100644 index 00000000..58f1d90b --- /dev/null +++ b/src/test/java/com/server/capple/config/apns/service/ApnsServiceImplTest.java @@ -0,0 +1,63 @@ +package com.server.capple.config.apns.service; + +import com.server.capple.config.apns.dto.ApnsClientRequest.SimplePushBody; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + + +@DisplayName("APNs 서비스로 ") +@SpringBootTest +@ActiveProfiles("test") +class ApnsServiceImplTest { + @Autowired + private ApnsService apnsService; + + @Test + @Disabled + @DisplayName("메시지를 전송한다.") + void sendApns() { + //given + String simulatorDeviceToken = "{deviceToken}"; // 기기의 deviceToken을 넣어야 작동 + String title = "title"; + String subTitle = "subTitle"; + String body = "body"; + String threadId = "testApnsMessage"; + String targetContentId = "targetContentId"; + + //when + Boolean result = apnsService.sendApns(new SimplePushBody(title, subTitle, body, null, threadId, targetContentId), List.of(simulatorDeviceToken)); + + //then + assertTrue(result); + } + + @Test + @Disabled + @DisplayName("다수의 메시지를 전송한다.") + void sendApnsMessages() { + //given + String simulatorDeviceToken = "{deviceToken}"; // 기기의 deviceToken을 넣어야 작동 + String title = "title"; + String subTitle = "subTitle"; + String body = "body"; + String threadId = "multipleTestApnsMessages"; + String targetContentId = "targetContentId"; + ArrayList deviceTokens = new ArrayList<>(); + for (int i = 0; i < 100; i++) deviceTokens.add(simulatorDeviceToken); + + //when + Boolean result = apnsService.sendApns(new SimplePushBody(title, subTitle, body, null, threadId, targetContentId), deviceTokens); + + //then + assertTrue(result); + } +} \ No newline at end of file From ad683d96223c24845d82c1510300d8aff3eb7e30 Mon Sep 17 00:00:00 2001 From: Jaewon Lee <58386334+jaewonLeeKOR@users.noreply.github.com> Date: Fri, 23 Aug 2024 00:07:27 +0900 Subject: [PATCH 13/21] =?UTF-8?q?feat:=20#126=20APNs=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EB=B0=9C=EC=86=A1=20Non-Blocking=20request?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/apns/config/ApnsClientConfig.java | 3 -- .../config/apns/service/ApnsServiceImpl.java | 40 +++++++++---------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/server/capple/config/apns/config/ApnsClientConfig.java b/src/main/java/com/server/capple/config/apns/config/ApnsClientConfig.java index c1003b09..d05f103d 100644 --- a/src/main/java/com/server/capple/config/apns/config/ApnsClientConfig.java +++ b/src/main/java/com/server/capple/config/apns/config/ApnsClientConfig.java @@ -1,7 +1,5 @@ package com.server.capple.config.apns.config; -import io.netty.handler.logging.LogLevel; -import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.timeout.WriteTimeoutHandler; @@ -40,7 +38,6 @@ public HttpClient getApnsH2HttpClient(ConnectionProvider apnsConnectionProvider, .keepAlive(true) // keep-alive 활성화 .protocol(HttpProtocol.H2) // HTTP/2 활성화 .doOnConnected(connection -> connection.addHandlerLast(new WriteTimeoutHandler(10, TimeUnit.SECONDS))) // 쓰기 타임 아웃 - .doAfterRequest((req, conn) -> conn.addHandlerLast(new LoggingHandler(LogLevel.INFO))) .responseTimeout(Duration.ofSeconds(10)) // 응답 타임 아웃 .secure(sslSpec -> sslSpec.sslContext(apnsSslContext)); // SSL 활성화 } diff --git a/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java b/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java index 17ab9fb3..60d423a8 100644 --- a/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java +++ b/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java @@ -49,26 +49,26 @@ public Boolean sendApns(T request, List deviceToken) { .baseUrl(apnsBaseSubUrl) .build(); - deviceToken.parallelStream().forEach(token -> { - tmpWebClient - .method(HttpMethod.POST) - .uri(token) - .bodyValue(request) - .retrieve() - .bodyToMono(String.class) - .doOnError(e -> { // 에러 발생 시 보조 채널로 재시도 - tmpSubWebClient - .method(HttpMethod.POST) - .uri(token) - .bodyValue(request) - .retrieve() - .bodyToMono(String.class) - .block(); - log.error("APNs 전송 중 오류 발생", e); - }) - .block(); - }); - + deviceToken.parallelStream() + .forEach(token -> { + tmpWebClient + .method(HttpMethod.POST) + .uri(token) + .bodyValue(request) + .retrieve() + .bodyToMono(Void.class) + .doOnError(e -> { // 에러 발생 시 보조 채널로 재시도 + tmpSubWebClient + .method(HttpMethod.POST) + .uri(token) + .bodyValue(request) + .retrieve() + .bodyToMono(Void.class) + .subscribe(); + log.error("APNs 전송 중 오류 발생", e); + }) + .subscribe(); + }); return true; } } From df4af362e47996cf56906ee8114245776b6c2389 Mon Sep 17 00:00:00 2001 From: Jaewon Lee <58386334+jaewonLeeKOR@users.noreply.github.com> Date: Fri, 23 Aug 2024 00:12:23 +0900 Subject: [PATCH 14/21] =?UTF-8?q?feat:=20#126=20APNs=20deviceToken=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20key=20=ED=83=80=EC=9E=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20memberId=20=EC=9D=B4=EC=9A=A9=20APNs=20?= =?UTF-8?q?=EB=B0=9C=EC=8B=A0=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/apns/service/ApnsService.java | 1 + .../config/apns/service/ApnsServiceImpl.java | 7 ++++++ .../DeviceTokenRedisRepository.java | 16 +++++++------- .../member/service/MemberServiceImpl.java | 10 ++++----- .../DeviceTokenRedisRepositoryTest.java | 22 +++++++++---------- 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/server/capple/config/apns/service/ApnsService.java b/src/main/java/com/server/capple/config/apns/service/ApnsService.java index ece346db..4b262fa4 100644 --- a/src/main/java/com/server/capple/config/apns/service/ApnsService.java +++ b/src/main/java/com/server/capple/config/apns/service/ApnsService.java @@ -4,4 +4,5 @@ public interface ApnsService { Boolean sendApns(T request, List deviceToken); + Boolean sendApnsToMembers(T request, List memberIdList); } diff --git a/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java b/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java index 60d423a8..6be5cefa 100644 --- a/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java +++ b/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java @@ -1,6 +1,7 @@ package com.server.capple.config.apns.service; import com.server.capple.config.security.jwt.service.JwtService; +import com.server.capple.domain.member.repository.DeviceTokenRedisRepository; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,6 +20,7 @@ public class ApnsServiceImpl implements ApnsService { private final JwtService jwtService; private final HttpClient apnsH2HttpClient; + private final DeviceTokenRedisRepository deviceTokenRedisRepository; private WebClient defaultApnsWebClient; @Value("${apns.base-url}") @@ -71,4 +73,9 @@ public Boolean sendApns(T request, List deviceToken) { }); return true; } + + @Override + public Boolean sendApnsToMembers(T request, List memberIdList) { + return sendApns(request, deviceTokenRedisRepository.getDeviceTokens(memberIdList)); + } } diff --git a/src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java b/src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java index 0993ff2c..994218ef 100644 --- a/src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java +++ b/src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java @@ -18,19 +18,19 @@ public class DeviceTokenRedisRepository { private ValueOperations valueOperations; private final RedisTemplate redisTemplate; - public void saveDeviceToken(String key, String deviceToken) { - valueOperations.set(DEVICE_TOKEN_KEY + key, deviceToken); + public void saveDeviceToken(Long memberId, String deviceToken) { + valueOperations.set(DEVICE_TOKEN_KEY + memberId.toString(), deviceToken); } - public String getDeviceToken(String key) { - return valueOperations.get(DEVICE_TOKEN_KEY + key); + public String getDeviceToken(Long memberId) { + return valueOperations.get(DEVICE_TOKEN_KEY + memberId.toString()); } - public List getDeviceTokens(List keys) { - return valueOperations.multiGet(keys.stream().map(key -> DEVICE_TOKEN_KEY + key).toList()); + public List getDeviceTokens(List keys) { + return valueOperations.multiGet(keys.stream().map(key -> DEVICE_TOKEN_KEY + key.toString()).toList()); } - public void deleteDeviceToken(String key) { - redisTemplate.delete(DEVICE_TOKEN_KEY + key); + public void deleteDeviceToken(Long memberId) { + redisTemplate.delete(DEVICE_TOKEN_KEY + memberId.toString()); } } diff --git a/src/main/java/com/server/capple/domain/member/service/MemberServiceImpl.java b/src/main/java/com/server/capple/domain/member/service/MemberServiceImpl.java index 114b27fe..bc1d2073 100644 --- a/src/main/java/com/server/capple/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/com/server/capple/domain/member/service/MemberServiceImpl.java @@ -107,7 +107,7 @@ public MemberResponse.SignInResponse signIn(String authorizationCode, String dev String accessToken = jwtService.createJwt(memberId, role, "access"); String refreshToken = jwtService.createJwt(memberId, role, "refresh"); if(deviceToken != null) - deviceTokenRedisRepository.saveDeviceToken(memberId.toString(), deviceToken); + deviceTokenRedisRepository.saveDeviceToken(memberId, deviceToken); return memberMapper.toSignInResponse(accessToken, refreshToken, true); } @@ -125,7 +125,7 @@ public MemberResponse.Tokens signUp(String signUpToken, String email, String nic String accessToken = jwtService.createJwt(memberId, role, "access"); String refreshToken = jwtService.createJwt(memberId, role, "refresh"); if(deviceToken != null) - deviceTokenRedisRepository.saveDeviceToken(memberId.toString(), deviceToken); + deviceTokenRedisRepository.saveDeviceToken(memberId, deviceToken); return tokensMapper.toTokens(accessToken, refreshToken); } @@ -141,7 +141,7 @@ public MemberResponse.SignInResponse localSignIn(String testId, String deviceTok String accessToken = jwtService.createJwt(memberId, role, "access"); String refreshToken = jwtService.createJwt(memberId, role, "refresh"); if(deviceToken != null) - deviceTokenRedisRepository.saveDeviceToken(memberId.toString(), deviceToken); + deviceTokenRedisRepository.saveDeviceToken(memberId, deviceToken); return memberMapper.toSignInResponse(accessToken, refreshToken, true); } @@ -161,7 +161,7 @@ public MemberResponse.MemberId resignMember(Member member) { Member resignedMember = memberRepository.findById(member.getId()).orElseThrow( () -> new RestApiException(MemberErrorCode.MEMBER_NOT_FOUND)); resignedMember.resignMember(); - deviceTokenRedisRepository.deleteDeviceToken(member.getId().toString()); + deviceTokenRedisRepository.deleteDeviceToken(member.getId()); return memberMapper.toMemberId(member); } @@ -224,7 +224,7 @@ public Boolean checkCertCode(String signUpToken, String email, String certCode) @Override public Boolean logout(Member member) { - deviceTokenRedisRepository.deleteDeviceToken(member.getId().toString()); + deviceTokenRedisRepository.deleteDeviceToken(member.getId()); return true; } } \ No newline at end of file diff --git a/src/test/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepositoryTest.java b/src/test/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepositoryTest.java index fe9ed909..34e5c4ff 100644 --- a/src/test/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepositoryTest.java +++ b/src/test/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepositoryTest.java @@ -20,8 +20,8 @@ class DeviceTokenRedisRepositoryTest { @Autowired private DeviceTokenRedisRepository deviceTokenRedisRepository; - private String testKeyPrefix = "testKey-"; - private List testKeys = new ArrayList<>(); + private Long testKeyPrefix = 1_000_000_000L; + private List testKeys = new ArrayList<>(); @AfterEach void afterEach() { @@ -32,7 +32,7 @@ void afterEach() { @DisplayName("디바이스 토큰 저장, 조회 테스트") void saveDeviceTokenTest() { //given - String key = testKeyPrefix + UUID.randomUUID().toString(); + Long key = testKeyPrefix + 1; testKeys.add(key); String deviceToken = UUID.randomUUID().toString(); @@ -47,7 +47,7 @@ void saveDeviceTokenTest() { @DisplayName("디바이스 토큰 삭제 테스트") void deleteDeviceTokenTest() { //given - String key = testKeyPrefix + UUID.randomUUID().toString(); + Long key = testKeyPrefix + 1; String deviceToken = UUID.randomUUID().toString(); //when @@ -62,9 +62,9 @@ void deleteDeviceTokenTest() { @DisplayName("디바이스 토큰 여러개 조회 테스트") void getDeviceTokensTest() { //given - String key1 = testKeyPrefix + UUID.randomUUID().toString(); - String key2 = testKeyPrefix + UUID.randomUUID().toString(); - String key3 = testKeyPrefix + UUID.randomUUID().toString(); + Long key1 = testKeyPrefix + 1; + Long key2 = testKeyPrefix + 2; + Long key3 = testKeyPrefix + 3; testKeys.add(key1); testKeys.add(key2); testKeys.add(key3); @@ -89,9 +89,9 @@ void getDeviceTokensTest() { @DisplayName("디바이스 토큰 삭제 후 여러개 조회 테스트") void getDeviceTokensAfterDeleteTest() { //given - String key1 = testKeyPrefix + UUID.randomUUID().toString(); - String key2 = testKeyPrefix + UUID.randomUUID().toString(); - String key3 = testKeyPrefix + UUID.randomUUID().toString(); + Long key1 = testKeyPrefix + 1; + Long key2 = testKeyPrefix + 2; + Long key3 = testKeyPrefix + 3; testKeys.add(key1); testKeys.add(key2); testKeys.add(key3); @@ -118,7 +118,7 @@ void getDeviceTokensAfterDeleteTest() { @DisplayName("디바이스 토큰 중복 저장, 조회 테스트") void saveDuplicateDeviceTokenTest() { //given - String key = testKeyPrefix + UUID.randomUUID().toString(); + Long key = testKeyPrefix + 1; testKeys.add(key); String deviceToken1 = UUID.randomUUID().toString(); String deviceToken2 = UUID.randomUUID().toString(); From 782336223fd94a41202452c2519b0510487bde95 Mon Sep 17 00:00:00 2001 From: tnals2384 Date: Fri, 23 Aug 2024 01:36:38 +0900 Subject: [PATCH 15/21] =?UTF-8?q?feat:=20#133=20boardComment=20Entity=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boardComment/entity/BoardComment.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java diff --git a/src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java b/src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java new file mode 100644 index 00000000..b4f42862 --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java @@ -0,0 +1,31 @@ +package com.server.capple.domain.boardComment.entity; + +import com.server.capple.domain.board.entity.Board; +import com.server.capple.domain.member.entity.Member; +import com.server.capple.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.SQLRestriction; + +@Getter +@Builder +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@SQLRestriction("deleted_at is null") +public class BoardComment extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_id", nullable = false) + private Board board; + + @Column(nullable = false) + private String content; +} From 29d25e2631cbb60945b610f0bdf0d8f755d9791a Mon Sep 17 00:00:00 2001 From: tnals2384 Date: Fri, 23 Aug 2024 09:47:09 +0900 Subject: [PATCH 16/21] =?UTF-8?q?feat:=20#133=20boardComment=20CRUD,=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=ED=86=A0=EA=B8=80=20API=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/service/BoardService.java | 13 ++- .../board/service/BoardServiceImpl.java | 14 +-- .../controller/BoardCommentController.java | 59 ++++++++++ .../boardComment/dto/BoardCommentRequest.java | 16 +++ .../dto/BoardCommentResponse.java | 41 +++++++ .../boardComment/entity/BoardComment.java | 4 + .../mapper/BoardCommentMapper.java | 29 +++++ .../BoardCommentHeartRedisRepository.java | 53 +++++++++ .../repository/BoardCommentRepository.java | 10 ++ .../service/BoardCommentService.java | 17 +++ .../service/BoardCommentServiceImpl.java | 101 ++++++++++++++++++ .../exception/errorCode/CommentErrorCode.java | 27 +++++ 12 files changed, 373 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java create mode 100644 src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentRequest.java create mode 100644 src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentResponse.java create mode 100644 src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java create mode 100644 src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentHeartRedisRepository.java create mode 100644 src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentRepository.java create mode 100644 src/main/java/com/server/capple/domain/boardComment/service/BoardCommentService.java create mode 100644 src/main/java/com/server/capple/domain/boardComment/service/BoardCommentServiceImpl.java create mode 100644 src/main/java/com/server/capple/global/exception/errorCode/CommentErrorCode.java diff --git a/src/main/java/com/server/capple/domain/board/service/BoardService.java b/src/main/java/com/server/capple/domain/board/service/BoardService.java index 79e19f68..92c5a005 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardService.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardService.java @@ -1,18 +1,21 @@ package com.server.capple.domain.board.service; import com.server.capple.domain.board.dto.BoardResponse; +import com.server.capple.domain.board.dto.BoardResponse.*; +import com.server.capple.domain.board.entity.Board; import com.server.capple.domain.board.entity.BoardType; import com.server.capple.domain.member.entity.Member; import org.springframework.data.domain.Pageable; public interface BoardService { - BoardResponse.BoardCreate createBoard(Member member, BoardType boardType, String content); + BoardCreate createBoard(Member member, BoardType boardType, String content); - BoardResponse.BoardsGetByBoardType getBoardsByBoardType(BoardType boardType); + BoardsGetByBoardType getBoardsByBoardType(BoardType boardType); - BoardResponse.BoardDelete deleteBoard(Member member, Long boardId); + BoardDelete deleteBoard(Member member, Long boardId); - BoardResponse.BoardsSearchByKeyword searchBoardsByKeyword(String keyword); + BoardsSearchByKeyword searchBoardsByKeyword(String keyword); - BoardResponse.BoardToggleHeart toggleBoardHeart(Member member, Long boardId); + BoardToggleHeart toggleBoardHeart(Member member, Long boardId); + Board findBoard(Long boardId); } diff --git a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java index bb434e54..ad3bd6c4 100644 --- a/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java +++ b/src/main/java/com/server/capple/domain/board/service/BoardServiceImpl.java @@ -56,9 +56,7 @@ public BoardResponse.BoardsGetByBoardType getBoardsByBoardType(BoardType boardTy @Override public BoardResponse.BoardDelete deleteBoard(Member member, Long boardId) { - Board board = boardRepository.findById(boardId) - .orElseThrow(() -> new RestApiException(BoardErrorCode.BOARD_NOT_FOUND)); - + Board board = findBoard(boardId); if (board.getWriter().getId() != member.getId()) { throw new RestApiException(BoardErrorCode.BOARD_NO_AUTHORIZATION); } @@ -77,14 +75,18 @@ public BoardResponse.BoardsSearchByKeyword searchBoardsByKeyword(String keyword) @Override public BoardResponse.BoardToggleHeart toggleBoardHeart(Member member, Long boardId) { - Board board = boardRepository.findById(boardId) - .orElseThrow(() -> new RestApiException(BoardErrorCode.BOARD_NOT_FOUND)); - + Board board = findBoard(boardId); System.out.println(boardHeartRedisRepository.getBoardHeartCreateAt(board.getId(), member.getId())); Boolean isLiked = boardHeartRedisRepository.toggleBoardHeart(member.getId(), board.getId()); return new BoardResponse.BoardToggleHeart(boardId, isLiked); } + @Override + public Board findBoard(Long boardId) { + return boardRepository.findById(boardId) + .orElseThrow(() -> new RestApiException(BoardErrorCode.BOARD_NOT_FOUND)); + } + } diff --git a/src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java b/src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java new file mode 100644 index 00000000..641f69fe --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java @@ -0,0 +1,59 @@ +package com.server.capple.domain.boardComment.controller; + + +import com.server.capple.config.security.AuthMember; +import com.server.capple.domain.boardComment.dto.BoardCommentRequest; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentHeart; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentId; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfos; +import com.server.capple.domain.boardComment.service.BoardCommentService; +import com.server.capple.domain.member.entity.Member; +import com.server.capple.global.common.BaseResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "게시글 댓글 API", description = "게시글 댓글 API입니다.") +@RestController +@RequiredArgsConstructor +@RequestMapping("/boardComments") +public class BoardCommentController { + + private final BoardCommentService boardCommentService; + + @Operation(summary = "게시글 댓글 생성 API", description = " 게시글 댓글 생성 API 입니다. pathVariable 으로 boardId를 주세요.") + @PostMapping("/board/{boardId}") + public BaseResponse createAnswerComment(@AuthMember Member member, + @PathVariable(value = "boardId") Long boardId, + @RequestBody @Valid BoardCommentRequest request) { + return BaseResponse.onSuccess(boardCommentService.createBoardComment(member, boardId, request)); + } + + @Operation(summary = "게시글 댓글 수정 API", description = " 게시글 댓글 수정 API 입니다. pathVariable 으로 commentId를 주세요.") + @PatchMapping("/{commentId}") + public BaseResponse updateAnswerComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId, @RequestBody @Valid BoardCommentRequest request) { + return BaseResponse.onSuccess(boardCommentService.updateBoardComment(member, commentId, request)); + } + + @Operation(summary = "게시글 댓글 삭제 API", description = " 게시글 댓글 삭제 API 입니다. pathVariable 으로 commentId를 주세요.") + @DeleteMapping("/{commentId}") + public BaseResponse deleteBoardComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId) { + return BaseResponse.onSuccess(boardCommentService.deleteBoardComment(member, commentId)); + } + + + @Operation(summary = "게시글 댓글 좋아요/취소 토글 API", description = " 게시글 댓글 좋아요/취소 토글 API 입니다. pathVariable 으로 commentId를 주세요.") + @PatchMapping("/heart/{commentId}") + public BaseResponse heartAnswerComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId) { + return BaseResponse.onSuccess(boardCommentService.heartBoardComment(member, commentId)); + } + + @Operation(summary = "게시글 댓글 목록 조회 API", description = " 게시글 댓글 조회 API 입니다. pathVariable 으로 boardId를 주세요.") + @GetMapping("/{boardId}") + public BaseResponse getAnswerCommentInfos(@AuthMember Member member, @PathVariable(value = "boardId") Long boardId) { + return BaseResponse.onSuccess(boardCommentService.getBoardCommentInfos(member,boardId)); + } + +} diff --git a/src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentRequest.java b/src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentRequest.java new file mode 100644 index 00000000..0d93e062 --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentRequest.java @@ -0,0 +1,16 @@ +package com.server.capple.domain.boardComment.dto; + + +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class BoardCommentRequest { + + @NotEmpty + private String comment; +} diff --git a/src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentResponse.java b/src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentResponse.java new file mode 100644 index 00000000..b87ed7cd --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/dto/BoardCommentResponse.java @@ -0,0 +1,41 @@ +package com.server.capple.domain.boardComment.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +public class BoardCommentResponse { + + @Getter + @AllArgsConstructor + public static class BoardCommentId { + private Long BoardCommentId; + } + + @Getter + @AllArgsConstructor + public static class BoardCommentHeart { + private Long boardCommentId; + private Boolean isLiked; + } + + @Getter + @Builder + public static class BoardCommentInfo { + private Long boardCommentId; + private String writer; + private String content; + private Long heartCount; + private Boolean isLiked; + private LocalDateTime createdAt; + } + + @Getter + @AllArgsConstructor + public static class BoardCommentInfos { + private List boardCommentInfos; + } +} diff --git a/src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java b/src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java index b4f42862..b9dc2176 100644 --- a/src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java +++ b/src/main/java/com/server/capple/domain/boardComment/entity/BoardComment.java @@ -28,4 +28,8 @@ public class BoardComment extends BaseEntity { @Column(nullable = false) private String content; + + public void update(String content) { + this.content = content; + } } diff --git a/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java b/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java new file mode 100644 index 00000000..cae15b9a --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java @@ -0,0 +1,29 @@ +package com.server.capple.domain.boardComment.mapper; + +import com.server.capple.domain.board.entity.Board; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfo; +import com.server.capple.domain.boardComment.entity.BoardComment; +import com.server.capple.domain.member.entity.Member; +import org.springframework.stereotype.Component; + +@Component +public class BoardCommentMapper { + public BoardComment toBoardCommentEntity(Member member, Board board, String comment) { + return BoardComment.builder() + .member(member) + .board(board) + .content(comment) + .build(); + } + + public BoardCommentInfo toBoardCommentInfo(BoardComment comment, Long heartCount,Boolean isLiked) { + return BoardCommentInfo.builder() + .boardCommentId(comment.getId()) + .writer(comment.getMember().getNickname()) + .content(comment.getContent()) + .heartCount(heartCount) + .isLiked(isLiked) + .createdAt(comment.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentHeartRedisRepository.java b/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentHeartRedisRepository.java new file mode 100644 index 00000000..10c08801 --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentHeartRedisRepository.java @@ -0,0 +1,53 @@ +package com.server.capple.domain.boardComment.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.SetOperations; +import org.springframework.stereotype.Repository; + +import java.io.Serializable; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +@Repository +@RequiredArgsConstructor +public class BoardCommentHeartRedisRepository implements Serializable { + private static final String BOARD_COMMENT_HEART_KEY_PREFIX = "boardCommentHeart-"; + private static final String MEMBER_PREFIX = "member-"; + + private final RedisTemplate redisTemplate; + + public Boolean toggleBoardCommentHeart(Long commentId, Long memberId) { + String key = BOARD_COMMENT_HEART_KEY_PREFIX + commentId.toString(); + String member = MEMBER_PREFIX + memberId.toString(); + + SetOperations setOperations = redisTemplate.opsForSet(); + + Boolean isLiked = setOperations.isMember(key, member); + + if(FALSE.equals(isLiked)) { + setOperations.add(key, member); + return TRUE; + } + else { + setOperations.remove(key, member); + return FALSE; + } + } + + public Boolean isMemberLiked(Long commentId, Long memberId) { + String key = BOARD_COMMENT_HEART_KEY_PREFIX + commentId.toString(); + String member = MEMBER_PREFIX + memberId.toString(); + + SetOperations setOperations = redisTemplate.opsForSet(); + + return setOperations.isMember(key, member); + } + + public Long getBoardCommentsCount(Long commentId) { + String key = BOARD_COMMENT_HEART_KEY_PREFIX + commentId.toString(); + return redisTemplate.opsForSet().size(key); + } + +} diff --git a/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentRepository.java b/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentRepository.java new file mode 100644 index 00000000..a3df1223 --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/repository/BoardCommentRepository.java @@ -0,0 +1,10 @@ +package com.server.capple.domain.boardComment.repository; + +import com.server.capple.domain.boardComment.entity.BoardComment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface BoardCommentRepository extends JpaRepository { + List findBoardCommentByBoardIdOrderByCreatedAt(Long boardId); +} diff --git a/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentService.java b/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentService.java new file mode 100644 index 00000000..3255443d --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentService.java @@ -0,0 +1,17 @@ +package com.server.capple.domain.boardComment.service; + +import com.server.capple.domain.boardComment.dto.BoardCommentRequest; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentHeart; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentId; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfos; +import com.server.capple.domain.boardComment.entity.BoardComment; +import com.server.capple.domain.member.entity.Member; + +public interface BoardCommentService { + BoardCommentId createBoardComment(Member member, Long boardId, BoardCommentRequest request); + BoardCommentId updateBoardComment(Member member, Long commentId,BoardCommentRequest request); + BoardCommentId deleteBoardComment(Member member, Long commentId); + BoardCommentHeart heartBoardComment(Member member, Long commentId); + BoardCommentInfos getBoardCommentInfos(Member member, Long boardId); + BoardComment findBoardComment(Long commentId); +} diff --git a/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentServiceImpl.java b/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentServiceImpl.java new file mode 100644 index 00000000..1890632b --- /dev/null +++ b/src/main/java/com/server/capple/domain/boardComment/service/BoardCommentServiceImpl.java @@ -0,0 +1,101 @@ +package com.server.capple.domain.boardComment.service; + +import com.server.capple.domain.board.entity.Board; +import com.server.capple.domain.board.service.BoardService; +import com.server.capple.domain.boardComment.dto.BoardCommentRequest; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentHeart; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentId; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfo; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfos; +import com.server.capple.domain.boardComment.entity.BoardComment; +import com.server.capple.domain.boardComment.mapper.BoardCommentMapper; +import com.server.capple.domain.boardComment.repository.BoardCommentHeartRedisRepository; +import com.server.capple.domain.boardComment.repository.BoardCommentRepository; +import com.server.capple.domain.member.entity.Member; +import com.server.capple.domain.member.service.MemberService; +import com.server.capple.global.exception.RestApiException; +import com.server.capple.global.exception.errorCode.CommentErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BoardCommentServiceImpl implements BoardCommentService { + private final MemberService memberService; + private final BoardService boardService; + private final BoardCommentRepository boardCommentRepository; + private final BoardCommentHeartRedisRepository boardCommentHeartRedisRepository; + private final BoardCommentMapper boardCommentMapper; + + @Override + @Transactional + public BoardCommentId createBoardComment(Member member, Long boardId, BoardCommentRequest request) { + Member loginMember = memberService.findMember(member.getId()); + Board board = boardService.findBoard(boardId); + + BoardComment boardComment = boardCommentRepository.save( + boardCommentMapper.toBoardCommentEntity(loginMember, board, request.getComment())); + + return new BoardCommentId(boardComment.getId()); + } + + @Override + @Transactional + public BoardCommentId updateBoardComment(Member member, Long commentId, BoardCommentRequest request) { + BoardComment boardComment = findBoardComment(commentId); + checkPermission(member, boardComment); + + boardComment.update(request.getComment()); + return new BoardCommentId(commentId); + } + + @Override + @Transactional + public BoardCommentId deleteBoardComment(Member member, Long commentId) { + BoardComment boardComment = findBoardComment(commentId); + checkPermission(member, boardComment); + + boardComment.delete(); + + return new BoardCommentId(boardComment.getId()); + } + + @Override + @Transactional + public BoardCommentHeart heartBoardComment(Member member, Long commentId) { + Boolean isLiked = boardCommentHeartRedisRepository. + toggleBoardCommentHeart(commentId, member.getId()); + + return new BoardCommentHeart(commentId, isLiked); + } + + @Override + public BoardCommentInfos getBoardCommentInfos(Member member, Long boardId) { + List commentInfos = boardCommentRepository + .findBoardCommentByBoardIdOrderByCreatedAt(boardId).stream().map( + comment -> { + Long heartCount = boardCommentHeartRedisRepository.getBoardCommentsCount(comment.getId()); + Boolean isLiked = boardCommentHeartRedisRepository.isMemberLiked(comment.getId(), member.getId()); + return boardCommentMapper.toBoardCommentInfo(comment, heartCount, isLiked); + }).toList(); + + return new BoardCommentInfos(commentInfos); + } + + private void checkPermission(Member member, BoardComment boardComment) { + Member loginMember = memberService.findMember(member.getId()); + + if (!loginMember.getId().equals(boardComment.getMember().getId())) + throw new RestApiException(CommentErrorCode.COMMENT_NOT_FOUND); + } + + @Override + public BoardComment findBoardComment(Long commentId) { + return boardCommentRepository.findById(commentId).orElseThrow( + () -> new RestApiException(CommentErrorCode.COMMENT_NOT_FOUND)); + } +} diff --git a/src/main/java/com/server/capple/global/exception/errorCode/CommentErrorCode.java b/src/main/java/com/server/capple/global/exception/errorCode/CommentErrorCode.java new file mode 100644 index 00000000..5fd5a78b --- /dev/null +++ b/src/main/java/com/server/capple/global/exception/errorCode/CommentErrorCode.java @@ -0,0 +1,27 @@ +package com.server.capple.global.exception.errorCode; + +import com.server.capple.global.exception.ErrorCode; +import com.server.capple.global.exception.ErrorCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum CommentErrorCode implements ErrorCodeInterface { + COMMENT_NOT_FOUND("COMMENT001", "댓글을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + COMMENT_UNAUTHORIZED("COMMENT002", "댓글에 대한 권한이 없습니다.", HttpStatus.FORBIDDEN); + + private final String code; + private final String message; + private final HttpStatus httpStatus; + + @Override + public ErrorCode getErrorCode() { + return ErrorCode.builder() + .code(code) + .message(message) + .httpStatus(httpStatus) + .build(); + } +} \ No newline at end of file From c08ae38ec8ec1676d6a95b7a3c5ca55966d9471c Mon Sep 17 00:00:00 2001 From: tnals2384 Date: Fri, 23 Aug 2024 10:55:47 +0900 Subject: [PATCH 17/21] =?UTF-8?q?test:=20#133=20boardComment=20CRUD,=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=ED=86=A0=EA=B8=80=20API=20test?= =?UTF-8?q?=20code=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/security/SecurityConfig.java | 1 + .../controller/BoardCommentController.java | 10 +- .../mapper/BoardCommentMapper.java | 2 +- .../BoardCommentControllerTest.java | 157 ++++++++++++++++++ .../service/BoardCommentServiceTest.java | 111 +++++++++++++ .../capple/support/ControllerTestConfig.java | 32 +++- .../capple/support/ServiceTestConfig.java | 39 ++++- 7 files changed, 341 insertions(+), 11 deletions(-) create mode 100644 src/test/java/com/server/capple/domain/boardComment/controller/BoardCommentControllerTest.java create mode 100644 src/test/java/com/server/capple/domain/boardComment/service/BoardCommentServiceTest.java diff --git a/src/main/java/com/server/capple/config/security/SecurityConfig.java b/src/main/java/com/server/capple/config/security/SecurityConfig.java index a4b96fef..98a543ff 100644 --- a/src/main/java/com/server/capple/config/security/SecurityConfig.java +++ b/src/main/java/com/server/capple/config/security/SecurityConfig.java @@ -56,6 +56,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/questions","/questions/**").authenticated() .requestMatchers("/reports", "/reports/**").authenticated() .requestMatchers("/boards", "/boards/**").authenticated() + .requestMatchers("/boardComments", "/boardComments/**").authenticated() .anyRequest().denyAll()); http .addFilterBefore(new JwtFilter(jwtService), UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java b/src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java index 641f69fe..12ce6420 100644 --- a/src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java +++ b/src/main/java/com/server/capple/domain/boardComment/controller/BoardCommentController.java @@ -25,7 +25,7 @@ public class BoardCommentController { @Operation(summary = "게시글 댓글 생성 API", description = " 게시글 댓글 생성 API 입니다. pathVariable 으로 boardId를 주세요.") @PostMapping("/board/{boardId}") - public BaseResponse createAnswerComment(@AuthMember Member member, + public BaseResponse createBoardComment(@AuthMember Member member, @PathVariable(value = "boardId") Long boardId, @RequestBody @Valid BoardCommentRequest request) { return BaseResponse.onSuccess(boardCommentService.createBoardComment(member, boardId, request)); @@ -33,7 +33,7 @@ public BaseResponse createAnswerComment(@AuthMember Member membe @Operation(summary = "게시글 댓글 수정 API", description = " 게시글 댓글 수정 API 입니다. pathVariable 으로 commentId를 주세요.") @PatchMapping("/{commentId}") - public BaseResponse updateAnswerComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId, @RequestBody @Valid BoardCommentRequest request) { + public BaseResponse updateBoardComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId, @RequestBody @Valid BoardCommentRequest request) { return BaseResponse.onSuccess(boardCommentService.updateBoardComment(member, commentId, request)); } @@ -46,13 +46,13 @@ public BaseResponse deleteBoardComment(@AuthMember Member member @Operation(summary = "게시글 댓글 좋아요/취소 토글 API", description = " 게시글 댓글 좋아요/취소 토글 API 입니다. pathVariable 으로 commentId를 주세요.") @PatchMapping("/heart/{commentId}") - public BaseResponse heartAnswerComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId) { + public BaseResponse heartBoardComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId) { return BaseResponse.onSuccess(boardCommentService.heartBoardComment(member, commentId)); } - @Operation(summary = "게시글 댓글 목록 조회 API", description = " 게시글 댓글 조회 API 입니다. pathVariable 으로 boardId를 주세요.") + @Operation(summary = "게시글 댓글 리스트 조회 API", description = " 게시글 댓글 리스트 조회 API 입니다. pathVariable 으로 boardId를 주세요.") @GetMapping("/{boardId}") - public BaseResponse getAnswerCommentInfos(@AuthMember Member member, @PathVariable(value = "boardId") Long boardId) { + public BaseResponse getBoardCommentInfos(@AuthMember Member member, @PathVariable(value = "boardId") Long boardId) { return BaseResponse.onSuccess(boardCommentService.getBoardCommentInfos(member,boardId)); } diff --git a/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java b/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java index cae15b9a..dbc38ff7 100644 --- a/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java +++ b/src/main/java/com/server/capple/domain/boardComment/mapper/BoardCommentMapper.java @@ -16,7 +16,7 @@ public BoardComment toBoardCommentEntity(Member member, Board board, String comm .build(); } - public BoardCommentInfo toBoardCommentInfo(BoardComment comment, Long heartCount,Boolean isLiked) { + public BoardCommentInfo toBoardCommentInfo(BoardComment comment, Long heartCount, Boolean isLiked) { return BoardCommentInfo.builder() .boardCommentId(comment.getId()) .writer(comment.getMember().getNickname()) diff --git a/src/test/java/com/server/capple/domain/boardComment/controller/BoardCommentControllerTest.java b/src/test/java/com/server/capple/domain/boardComment/controller/BoardCommentControllerTest.java new file mode 100644 index 00000000..f1af70b0 --- /dev/null +++ b/src/test/java/com/server/capple/domain/boardComment/controller/BoardCommentControllerTest.java @@ -0,0 +1,157 @@ +package com.server.capple.domain.boardComment.controller; + +import com.server.capple.domain.boardComment.dto.BoardCommentRequest; +import com.server.capple.domain.boardComment.service.BoardCommentService; +import com.server.capple.domain.member.entity.Member; +import com.server.capple.support.ControllerTestConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.ResultActions; + +import static com.server.capple.domain.boardComment.dto.BoardCommentResponse.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayName("BoardComment 컨트롤러의") +@SpringBootTest +@AutoConfigureMockMvc +public class BoardCommentControllerTest extends ControllerTestConfig { + + @MockBean + private BoardCommentService boardCommentService; + + @Test + @DisplayName("게시글 댓글 생성 API 테스트") + public void createBoardCommentTest() throws Exception { + //given + final String url = "/boardComments/board/{boardId}"; + + BoardCommentRequest request = getBoardCommentRequest(); + BoardCommentId response = new BoardCommentId(1L); + + when(boardCommentService.createBoardComment(any(Member.class), any(Long.class), any(BoardCommentRequest.class))) + .thenReturn(response); + + //when + ResultActions resultActions = this.mockMvc.perform(post(url, 1L) + .contentType(APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(request)) + .header("Authorization", "Bearer " + jwt)); + + //then + resultActions.andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.boardCommentId").value(1L)); + } + + @Test + @DisplayName("게시글 댓글 수정 API 테스트") + public void updateBoardCommentTest() throws Exception { + //given + final String url = "/boardComments/{commentId}"; + + BoardCommentRequest request = getBoardCommentRequest(); + BoardCommentId response = new BoardCommentId(1L); + + doReturn(response).when(boardCommentService).updateBoardComment(any(Member.class), any(Long.class), any(BoardCommentRequest.class)); + + //when + ResultActions resultActions = this.mockMvc.perform(patch(url, 1L) + .contentType(APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(request)) + .header("Authorization", "Bearer " + jwt)); + + //then + resultActions. + andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.boardCommentId").value(1L)); + } + + @Test + @DisplayName("게시글 댓글 삭제 API 테스트") + public void deleteBoardCommentTest() throws Exception { + //given + final String url = "/boardComments/{commentId}"; + BoardCommentId response = new BoardCommentId(1L); + + doReturn(response).when(boardCommentService).deleteBoardComment(any(Member.class), any(Long.class)); + + //when + ResultActions resultActions = this.mockMvc.perform(delete(url, 1L) + .contentType(APPLICATION_JSON_VALUE) + .header("Authorization", "Bearer " + jwt)); + + //then + resultActions. + andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.boardCommentId").value(1L)); + } + + @Test + @DisplayName("게시글 댓글 좋아요/취소 API 테스트") + public void heartBoardCommentTest() throws Exception { + //given + final String url = "/boardComments/heart/{commentId}"; + BoardCommentHeart response = new BoardCommentHeart(1L, Boolean.TRUE); + + doReturn(response).when(boardCommentService).heartBoardComment(any(Member.class), any(Long.class)); + + //when + ResultActions resultActions = this.mockMvc.perform(patch(url, 1L) + .contentType(APPLICATION_JSON_VALUE) + .header("Authorization", "Bearer " + jwt)); + + //then + resultActions. + andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.boardCommentId").value(1L)) + .andExpect(jsonPath("$.result.isLiked").value(Boolean.TRUE)); + } + + @Test + @DisplayName("게시글 댓글 리스트 조회 API 테스트") + public void getBoardCommentInfosTest() throws Exception { + //given + final String url = "/boardComments/{boardId}"; + BoardCommentInfos response = getBoardCommentInfos(); + + doReturn(response).when(boardCommentService).getBoardCommentInfos(any(Member.class), any(Long.class)); + + //when + ResultActions resultActions = this.mockMvc.perform(get(url, 1L) + .contentType(APPLICATION_JSON_VALUE) + .header("Authorization", "Bearer " + jwt)); + + //then + resultActions. + andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) + .andExpect(jsonPath("$.result.boardCommentInfos[0].boardCommentId").value(1L)) + .andExpect(jsonPath("$.result.boardCommentInfos[0].writer").value("루시")) + .andExpect(jsonPath("$.result.boardCommentInfos[0].content").value("댓글")) + .andExpect(jsonPath("$.result.boardCommentInfos[0].heartCount").value(2L)) + .andExpect(jsonPath("$.result.boardCommentInfos[0].isLiked").value(true)); + } +} diff --git a/src/test/java/com/server/capple/domain/boardComment/service/BoardCommentServiceTest.java b/src/test/java/com/server/capple/domain/boardComment/service/BoardCommentServiceTest.java new file mode 100644 index 00000000..d1accde2 --- /dev/null +++ b/src/test/java/com/server/capple/domain/boardComment/service/BoardCommentServiceTest.java @@ -0,0 +1,111 @@ +package com.server.capple.domain.boardComment.service; + +import com.server.capple.domain.boardComment.dto.BoardCommentRequest; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentHeart; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfos; +import com.server.capple.domain.boardComment.entity.BoardComment; +import com.server.capple.support.ServiceTestConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DisplayName("BoardComment 서비스의 ") +@SpringBootTest +public class BoardCommentServiceTest extends ServiceTestConfig { + + @Autowired + private BoardCommentService boardCommentService; + + @Test + @DisplayName("게시글 댓글 생성 테스트") + @Transactional + public void createBoardCommentTest() { + //given + BoardCommentRequest request = getBoardCommentRequest(); + + //when + Long boardCommentId = boardCommentService.createBoardComment(member, board.getId(), request).getBoardCommentId(); + BoardComment comment = boardCommentService.findBoardComment(boardCommentId); + + //then + assertEquals("게시글 댓글", comment.getContent()); + } + + @Test + @DisplayName("게시글 댓글 수정 테스트") + @Transactional + public void updateBoardCommentTest() { + //given + BoardCommentRequest request = getBoardCommentRequest(); + Long boardCommentId = boardCommentService.createBoardComment(member, board.getId(), request).getBoardCommentId(); + + BoardCommentRequest updateRequest = new BoardCommentRequest("댓글 수정"); + + //when + boardCommentService.updateBoardComment(member, boardCommentId, updateRequest); + BoardComment comment = boardCommentService.findBoardComment(boardCommentId); + + //then + assertEquals("댓글 수정", comment.getContent()); + } + + @Test + @DisplayName("게시글 댓글 삭제 테스트") + @Transactional + public void deleteBoardCommentTest() { + //given + BoardCommentRequest request = getBoardCommentRequest(); + Long boardCommentId = boardCommentService.createBoardComment(member, board.getId(), request).getBoardCommentId(); + + //when + boardCommentService.deleteBoardComment(member, boardCommentId); + BoardComment comment = boardCommentService.findBoardComment(boardCommentId); + + //then + assertNotNull(comment.getDeletedAt()); + } + + @Test + @DisplayName("게시글 댓글 좋아요/취소 토글 테스트") + @Transactional + public void heartBoardCommentTest() { + //1. 좋아요 + //given & when + BoardCommentHeart liked = boardCommentService.heartBoardComment(member, boardComment.getId()); + BoardCommentInfos likedResponse = boardCommentService.getBoardCommentInfos(member, board.getId()); + //then + assertEquals(boardComment.getId(), liked.getBoardCommentId()); + assertEquals(true, liked.getIsLiked()); + assertEquals(true, likedResponse.getBoardCommentInfos().get(0).getIsLiked()); + + //2. 좋아요 취소 + //given & when + BoardCommentHeart unLiked = boardCommentService.heartBoardComment(member, boardComment.getId()); + BoardCommentInfos unLikedResponse = boardCommentService.getBoardCommentInfos(member, board.getId()); + + //then + assertEquals(boardComment.getId(), unLiked.getBoardCommentId()); + assertEquals(false, unLiked.getIsLiked()); + assertEquals(false, unLikedResponse.getBoardCommentInfos().get(0).getIsLiked()); + + } + + @Test + @DisplayName("게시판 댓글 리스트 조회 테스트") + @Transactional + public void getBoardCommentsTest() { + //when + BoardCommentInfos response = boardCommentService.getBoardCommentInfos(member, board.getId()); + + //then + assertEquals("루시", response.getBoardCommentInfos().get(0).getWriter()); + assertEquals("게시글 댓글", response.getBoardCommentInfos().get(0).getContent()); + assertEquals(0L, response.getBoardCommentInfos().get(0).getHeartCount()); + assertEquals(false, response.getBoardCommentInfos().get(0).getIsLiked()); + } +} \ No newline at end of file diff --git a/src/test/java/com/server/capple/support/ControllerTestConfig.java b/src/test/java/com/server/capple/support/ControllerTestConfig.java index be7a8b5b..f001bada 100644 --- a/src/test/java/com/server/capple/support/ControllerTestConfig.java +++ b/src/test/java/com/server/capple/support/ControllerTestConfig.java @@ -6,7 +6,11 @@ import com.server.capple.config.security.jwt.service.JwtService; import com.server.capple.domain.answer.dto.AnswerRequest; import com.server.capple.domain.answer.dto.AnswerResponse; +import com.server.capple.domain.answer.dto.AnswerResponse.MemberAnswerList; import com.server.capple.domain.answer.entity.Answer; +import com.server.capple.domain.boardComment.dto.BoardCommentRequest; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfo; +import com.server.capple.domain.boardComment.dto.BoardCommentResponse.BoardCommentInfos; import com.server.capple.domain.member.entity.Member; import com.server.capple.domain.member.entity.Role; import com.server.capple.domain.question.entity.Question; @@ -19,8 +23,10 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import java.time.LocalDateTime; import java.util.List; +import static java.lang.Boolean.TRUE; import static org.mockito.Mockito.when; @SpringBootTest @@ -31,12 +37,12 @@ public abstract class ControllerTestConfig { protected MockMvc mockMvc; @Autowired protected ObjectMapper objectMapper; - protected Member member; - protected String jwt; @Autowired JwtService jwtService; @MockBean JpaUserDetailService jpaUserDetailService; + protected Member member; + protected String jwt; protected Question question; protected Answer answer; @@ -88,7 +94,7 @@ protected AnswerRequest getAnswerRequest() { .build(); } - protected AnswerResponse.MemberAnswerList getMemberAnswerList () { + protected MemberAnswerList getMemberAnswerList () { List memberAnswerInfos = List.of(AnswerResponse.MemberAnswerInfo.builder() .questionId(answer.getQuestion().getId()) .answerId(answer.getId()) @@ -98,6 +104,24 @@ protected AnswerResponse.MemberAnswerList getMemberAnswerList () { .heartCount(1) .build()); - return new AnswerResponse.MemberAnswerList(memberAnswerInfos); + return new MemberAnswerList(memberAnswerInfos); + } + + protected BoardCommentRequest getBoardCommentRequest() { + return new BoardCommentRequest("게시글 댓글"); + } + + protected BoardCommentInfos getBoardCommentInfos() { + List commentInfos = + List.of(BoardCommentInfo.builder() + .boardCommentId(1L) + .writer(member.getNickname()) + .content("댓글") + .createdAt(LocalDateTime.now()) + .heartCount(2L) + .isLiked(TRUE) + .build()); + + return new BoardCommentInfos(commentInfos); } } diff --git a/src/test/java/com/server/capple/support/ServiceTestConfig.java b/src/test/java/com/server/capple/support/ServiceTestConfig.java index 42123400..68bd175e 100644 --- a/src/test/java/com/server/capple/support/ServiceTestConfig.java +++ b/src/test/java/com/server/capple/support/ServiceTestConfig.java @@ -3,6 +3,12 @@ import com.server.capple.domain.answer.dto.AnswerRequest; import com.server.capple.domain.answer.entity.Answer; import com.server.capple.domain.answer.repository.AnswerRepository; +import com.server.capple.domain.board.entity.Board; +import com.server.capple.domain.board.entity.BoardType; +import com.server.capple.domain.board.repository.BoardRepository; +import com.server.capple.domain.boardComment.dto.BoardCommentRequest; +import com.server.capple.domain.boardComment.entity.BoardComment; +import com.server.capple.domain.boardComment.repository.BoardCommentRepository; import com.server.capple.domain.member.entity.Member; import com.server.capple.domain.member.entity.Role; import com.server.capple.domain.member.repository.MemberRepository; @@ -26,12 +32,17 @@ public abstract class ServiceTestConfig { protected QuestionRepository questionRepository; @Autowired protected AnswerRepository answerRepository; - + @Autowired + protected BoardRepository boardRepository; + @Autowired + BoardCommentRepository boardCommentRepository; protected Member member; protected Question liveQuestion; protected Question pendingQuestion; protected Question oldQuestion; protected Answer answer; + protected Board board; + protected BoardComment boardComment; @Autowired private RedisTemplate redisTemplate; @@ -43,6 +54,8 @@ public void setUp() { pendingQuestion = createPendingQuestion(); oldQuestion = createOldQuestion(); answer = createAnswer(); + board = createBoard(); + boardComment = createBoardComment(); redisTemplate.getConnectionFactory().getConnection().flushAll(); } @@ -102,4 +115,28 @@ protected AnswerRequest getAnswerRequest() { .answer("나는 와플을 좋아하는 사람이 좋아") .build(); } + + protected Board createBoard() { + return boardRepository.save(Board.builder() + .id(1L) + .boardType(BoardType.FREEBOARD) + .writer(member) + .content("오늘 밥먹을 사람!") + .commentCount(2) + .build()); + } + protected BoardComment createBoardComment() { + return boardCommentRepository.save( + BoardComment.builder() + .member(member) + .board(board) + .content("게시글 댓글") + .build()); + } + + protected BoardCommentRequest getBoardCommentRequest() { + return new BoardCommentRequest("게시글 댓글"); + } + + } From de05d001fb046cd67bc499f9da8410d74f1455fe Mon Sep 17 00:00:00 2001 From: tnals2384 Date: Fri, 23 Aug 2024 10:59:31 +0900 Subject: [PATCH 18/21] chore: #133 config pull --- config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config b/config index 5736a35e..de326242 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 5736a35ee09ccb7b74f2a78b93770dbe5195dbae +Subproject commit de3262429af531867ffdfba0b0fe6b152864982d From ce0f663ada01604cbc1b72e69e4cf0971eebd76d Mon Sep 17 00:00:00 2001 From: Jaewon Lee <58386334+jaewonLeeKOR@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:20:57 +0900 Subject: [PATCH 19/21] =?UTF-8?q?feat:=20#126=20redis=20cloud=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config | 2 +- .../com/server/capple/config/RedisConfig.java | 31 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/config b/config index de326242..cdea0f32 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit de3262429af531867ffdfba0b0fe6b152864982d +Subproject commit cdea0f3251cf1bb6ec4b5d01939797da1b926510 diff --git a/src/main/java/com/server/capple/config/RedisConfig.java b/src/main/java/com/server/capple/config/RedisConfig.java index f7eb58d3..00582074 100644 --- a/src/main/java/com/server/capple/config/RedisConfig.java +++ b/src/main/java/com/server/capple/config/RedisConfig.java @@ -1,12 +1,15 @@ package com.server.capple.config; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; @@ -24,13 +27,25 @@ public class RedisConfig { private int port; @Value("${spring.data.redis.database}") private int database; + @Value("${redis-cloud.host}") + private String redisCloudHost; + @Value("${redis-cloud.port}") + private int redisCloudPort; + @Value("${redis-cloud.database}") + private int redisCloudDatabase; + @Value("${redis-cloud.username}") + private String redisCloudUsername; + @Value("${redis-cloud.password}") + private String redisCloudPassword; @Bean + @Primary public RedisConnectionFactory redisConnectionFactory() { LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(host, port); connectionFactory.setDatabase(database); return connectionFactory; } + @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate<>(); @@ -43,11 +58,23 @@ public RedisTemplate redisTemplate(RedisConnectionFactory redisC } @Bean - public CacheManager apnsJwtCacheManager(RedisConnectionFactory cf) { + public RedisConnectionFactory redisCloudConnectionFactory() { + RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration(redisCloudHost, redisCloudPort); + redisConfiguration.setUsername(redisCloudUsername); + redisConfiguration.setPassword(redisCloudPassword); + LettuceConnectionFactory apnsRedisConnectionFactory = new LettuceConnectionFactory(redisConfiguration); + apnsRedisConnectionFactory.setDatabase(redisCloudDatabase); + apnsRedisConnectionFactory.start(); + return apnsRedisConnectionFactory; + } + + @Bean + @Qualifier("redisCloudConnectionFactory") + public CacheManager apnsJwtCacheManager(RedisConnectionFactory redisCloudConnectionFactory) { RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) .entryTtl(Duration.ofMinutes(30)); - return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build(); + return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisCloudConnectionFactory).cacheDefaults(redisCacheConfiguration).build(); } } From 225db13b316574ee36961eedc70f07861d3621c8 Mon Sep 17 00:00:00 2001 From: Jaewon Lee <58386334+jaewonLeeKOR@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:24:44 +0900 Subject: [PATCH 20/21] =?UTF-8?q?feat:=20#126=20=EB=A9=A4=EB=B2=84id=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20APNs=20payload=20=EB=B0=9C=EC=86=A1=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/apns/dto/ApnsClientRequest.java | 4 ++-- .../config/apns/service/ApnsService.java | 4 +++- .../config/apns/service/ApnsServiceImpl.java | 20 +++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java b/src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java index 1af62a3c..eb804673 100644 --- a/src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java +++ b/src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java @@ -12,8 +12,8 @@ public class ApnsClientRequest { public static class SimplePushBody { private Aps aps; - public SimplePushBody(String title, String subTitle, String body, Integer badge, String threaId, String targetContentId) { - this.aps = new Aps(new Aps.Alert(title, subTitle, body), badge, threaId, targetContentId); + public SimplePushBody(String title, String subTitle, String body, Integer badge, String threadId, String targetContentId) { + this.aps = new Aps(new Aps.Alert(title, subTitle, body), badge, threadId, targetContentId); } @ToString diff --git a/src/main/java/com/server/capple/config/apns/service/ApnsService.java b/src/main/java/com/server/capple/config/apns/service/ApnsService.java index 4b262fa4..05032e3c 100644 --- a/src/main/java/com/server/capple/config/apns/service/ApnsService.java +++ b/src/main/java/com/server/capple/config/apns/service/ApnsService.java @@ -3,6 +3,8 @@ import java.util.List; public interface ApnsService { - Boolean sendApns(T request, List deviceToken); + Boolean sendApns(T request, String ... deviceTokens); + Boolean sendApns(T request, List deviceTokenList); + Boolean sendApnsToMembers(T request, Long ... memberIds); Boolean sendApnsToMembers(T request, List memberIdList); } diff --git a/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java b/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java index 6be5cefa..81acd50b 100644 --- a/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java +++ b/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java @@ -41,6 +41,11 @@ public void init() { .build(); } + @Override + public Boolean sendApns(T request, String... deviceToken) { + return sendApns(request, List.of(deviceToken)); + } + @Override public Boolean sendApns(T request, List deviceToken) { WebClient tmpWebClient = defaultApnsWebClient.mutate() @@ -59,6 +64,16 @@ public Boolean sendApns(T request, List deviceToken) { .bodyValue(request) .retrieve() .bodyToMono(Void.class) + .doOnDiscard(Void.class, response -> {// 거절 시 보조 채널로 재시도 + tmpSubWebClient + .method(HttpMethod.POST) + .uri(token) + .bodyValue(request) + .retrieve() + .bodyToMono(Void.class) + .subscribe(); + log.info("APNs 전송 거절 발생"); + }) .doOnError(e -> { // 에러 발생 시 보조 채널로 재시도 tmpSubWebClient .method(HttpMethod.POST) @@ -74,6 +89,11 @@ public Boolean sendApns(T request, List deviceToken) { return true; } + @Override + public Boolean sendApnsToMembers(T request, Long... memberIds) { + return sendApns(request, deviceTokenRedisRepository.getDeviceTokens(List.of(memberIds))); + } + @Override public Boolean sendApnsToMembers(T request, List memberIdList) { return sendApns(request, deviceTokenRedisRepository.getDeviceTokens(memberIdList)); From bdcddc36b875fa91c5b1d7de967ca6be5f3428cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9E=AC=EC=9B=90?= Date: Sun, 25 Aug 2024 20:56:50 +0900 Subject: [PATCH 21/21] =?UTF-8?q?Chore:=20=EB=B9=8C=EB=93=9C=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=20Jar=20=EC=B6=94=EC=B6=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 이재원 --- .github/workflows/cicd-release.yml | 45 ------------------------------ 1 file changed, 45 deletions(-) diff --git a/.github/workflows/cicd-release.yml b/.github/workflows/cicd-release.yml index e400cc37..16b0b89b 100644 --- a/.github/workflows/cicd-release.yml +++ b/.github/workflows/cicd-release.yml @@ -83,48 +83,3 @@ jobs: sudo docker image rm ${{ env.SPRING_IMAGE }} sudo docker-compose --env-file=config/env/release.env -f docker-compose.release.yml -p backend up -d sudo docker image prune -af - - releaseExport: - name: Export Release Jar - runs-on: ubuntu-latest - needs: releaseDeploy - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ env.GITHUB_ACTOR }} - password: ${{ secrets.TOKEN_GITHUB }} - - - name: lowercase the image tag & repository - run: | - echo "REPOSITORY=$(echo $REPOSITORY | tr '[:upper:]' '[:lower:]')" >> ${GITHUB_ENV} - - - name: Get Version - run: | - echo "VERSION=$( echo ${{ github.ref_name }} | cut -c 9- )" >> ${GITHUB_ENV} - - - name: Set Spring Image Environment Variable - run: | - echo "SPRING_IMAGE=${{ env.REGISTRY }}/${{ env.REPOSITORY }}-release:${{ env.VERSION }}" >> ${GITHUB_ENV} - - - name: Run Container - run: docker run -d --name container-capple ${{ env.SPRING_IMAGE }} - - - name: Extract .jar File From Container - run: docker cp container-capple:/app/app.jar . - - - name: Stop Container - run: docker rm -f container-capple - - - name: Rename Jar - run: mv app.jar capple-${{ env.VERSION }}.jar - - - name: Extract .jar File - uses: actions/upload-artifact@v4 - with: - name: capple-${{ env.VERSION }} - path: ./*.jar - compression-level: 9 - retention-days: 1 - overwrite: true