From 848cd9b40841a2b32820eb4922821afdde1dead7 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Wed, 6 Dec 2023 18:19:56 +0900 Subject: [PATCH 01/32] =?UTF-8?q?Feat:=20MessageService=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20#124=20verifyAuthNum2?= =?UTF-8?q?=20-=20=EA=B8=B0=EC=A1=B4=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EC=99=80=20=EB=8B=AC=EB=A6=AC=20=EC=9D=B8=EC=A6=9D=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EB=90=9C=20=EB=B2=88=ED=98=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=EC=B2=98=EB=9F=BC=20=EC=9D=B4=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/message/MessageService.java | 4 ++ .../global/message/MessageServiceImpl.java | 54 ++++++++++++++----- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/example/DoroServer/global/message/MessageService.java b/src/main/java/com/example/DoroServer/global/message/MessageService.java index 3e912957..c415c3ea 100644 --- a/src/main/java/com/example/DoroServer/global/message/MessageService.java +++ b/src/main/java/com/example/DoroServer/global/message/MessageService.java @@ -2,9 +2,13 @@ import com.example.DoroServer.global.message.dto.SendAuthNumReq; import com.example.DoroServer.global.message.dto.VerifyAuthNumReq; +import com.example.DoroServer.global.message.dto.VerifyAuthNumRes; public interface MessageService { void sendAuthNum(SendAuthNumReq sendAuthNumReq); + void verifyAuthNum(VerifyAuthNumReq verifyAuthNumReq); + + VerifyAuthNumRes verifyAuthNum2(VerifyAuthNumReq verifyAuthNumReq); } diff --git a/src/main/java/com/example/DoroServer/global/message/MessageServiceImpl.java b/src/main/java/com/example/DoroServer/global/message/MessageServiceImpl.java index 9da4e0a7..1d65c5ec 100644 --- a/src/main/java/com/example/DoroServer/global/message/MessageServiceImpl.java +++ b/src/main/java/com/example/DoroServer/global/message/MessageServiceImpl.java @@ -6,7 +6,10 @@ import com.example.DoroServer.global.jwt.RedisService; import com.example.DoroServer.global.message.dto.SendAuthNumReq; import com.example.DoroServer.global.message.dto.VerifyAuthNumReq; +import com.example.DoroServer.global.message.dto.VerifyAuthNumRes; + import java.time.Duration; +import java.util.UUID; import java.util.HashMap; import java.util.Random; import lombok.extern.slf4j.Slf4j; @@ -16,12 +19,13 @@ import net.nurigo.sdk.message.request.SingleMessageSendingRequest; import net.nurigo.sdk.message.service.DefaultMessageService; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Slf4j -public class MessageServiceImpl implements MessageService{ +public class MessageServiceImpl implements MessageService { private final DefaultMessageService defaultMessageService; private final RedisService redisService; @@ -30,31 +34,32 @@ public class MessageServiceImpl implements MessageService{ private final String templateId; public MessageServiceImpl(@Value("${solapi.api-key}") String apiKey, - @Value("${solapi.api-secret}") String apiSecret, - @Value("${solapi.from-number}") String fromNumber, - @Value("${solapi.domain}") String domain, - @Value("${solapi.pfid}") String pfid, - @Value("${solapi.templateId}") String templateId, - RedisService redisService) { + @Value("${solapi.api-secret}") String apiSecret, + @Value("${solapi.from-number}") String fromNumber, + @Value("${solapi.domain}") String domain, + @Value("${solapi.pfid}") String pfid, + @Value("${solapi.templateId}") String templateId, + RedisService redisService) { this.defaultMessageService = NurigoApp.INSTANCE.initialize(apiKey, apiSecret, domain); this.fromNumber = fromNumber; this.pfId = pfid; this.templateId = templateId; this.redisService = redisService; } + @Transactional @Override public void sendAuthNum(SendAuthNumReq sendAuthNumReq) { - try{ + try { String authNum = numberGen(6); Message message = makeKakaoMessage(sendAuthNumReq, authNum); redisService.setValues(sendAuthNumReq.getMessageType() + sendAuthNumReq.getPhone(), - authNum, Duration.ofSeconds(300)); + authNum, Duration.ofSeconds(300)); log.info("Redis 저장 성공"); defaultMessageService.sendOne(new SingleMessageSendingRequest(message)); log.info("메시지 전송 성공"); - }catch (Exception e){ + } catch (Exception e) { throw new MessageException(Code.MESSAGE_SEND_FAILED); } } @@ -78,13 +83,30 @@ private Message makeKakaoMessage(SendAuthNumReq sendAuthNumReq, String authNum) @Override public void verifyAuthNum(VerifyAuthNumReq verifyAuthNumReq) { String redisAuthNum = redisService.getValues(verifyAuthNumReq.getMessageType() + - verifyAuthNumReq.getPhone()); - if(redisAuthNum == null || !redisAuthNum.equals(verifyAuthNumReq.getAuthNum())){ + verifyAuthNumReq.getPhone()); + if (redisAuthNum == null || !redisAuthNum.equals(verifyAuthNumReq.getAuthNum())) { throw new BaseException(Code.VERIFICATION_DID_NOT_MATCH); } redisService.deleteValues(verifyAuthNumReq.getPhone()); redisService.setValues(verifyAuthNumReq.getMessageType() + - verifyAuthNumReq.getPhone(), "Verified", Duration.ofMinutes(30)); + verifyAuthNumReq.getPhone(), "Verified", Duration.ofMinutes(30)); + } + + @Override + public VerifyAuthNumRes verifyAuthNum2(VerifyAuthNumReq verifyAuthNumReq) { + String redisAuthNum = redisService.getValues(verifyAuthNumReq.getMessageType() + + verifyAuthNumReq.getPhone()); + if (redisAuthNum == null || !redisAuthNum.equals(verifyAuthNumReq.getAuthNum())) { + throw new BaseException(Code.VERIFICATION_DID_NOT_MATCH); + } + redisService.deleteValues(verifyAuthNumReq.getPhone()); + + // Redis에 5분간 유효한 세션(세션 ID: 전화번호) 생성 + String sessionId = generateSessionId(); + redisService.setValues(sessionId, verifyAuthNumReq.getPhone(), Duration.ofMinutes(5)); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("Session-Id", sessionId); + return VerifyAuthNumRes.builder().sessionId(httpHeaders).build(); } /** @@ -94,10 +116,14 @@ private String numberGen(int len) { Random rand = new Random(); StringBuilder numStr = new StringBuilder(); - for(int i = 0; i < len; i++) { + for (int i = 0; i < len; i++) { String ran = Integer.toString(rand.nextInt(10)); numStr.append(ran); } return numStr.toString(); } + + public static String generateSessionId() { + return UUID.randomUUID().toString(); + } } From 5cdf109246a2b89141af02b3cbd30dc05e4e7056 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Wed, 6 Dec 2023 18:24:47 +0900 Subject: [PATCH 02/32] =?UTF-8?q?Feat:=20Message=20=EA=B4=80=EB=A0=A8=20DT?= =?UTF-8?q?O=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20#124=20-=20SendAuthN?= =?UTF-8?q?umReq=EC=9D=98=20MessageType=EC=97=90=20TEMP=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20VerifyAuthNumReq,=20VerifyAuthNumRes=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/message/dto/SendAuthNumReq.java | 4 ++-- .../global/message/dto/VerifyAuthNumReq.java | 8 ++++++++ .../global/message/dto/VerifyAuthNumRes.java | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/DoroServer/global/message/dto/VerifyAuthNumRes.java diff --git a/src/main/java/com/example/DoroServer/global/message/dto/SendAuthNumReq.java b/src/main/java/com/example/DoroServer/global/message/dto/SendAuthNumReq.java index cc06fbc6..8311169a 100644 --- a/src/main/java/com/example/DoroServer/global/message/dto/SendAuthNumReq.java +++ b/src/main/java/com/example/DoroServer/global/message/dto/SendAuthNumReq.java @@ -14,7 +14,7 @@ public class SendAuthNumReq { @NotNull private MessageType messageType; - public enum MessageType{ - JOIN, ACCOUNT, PASSWORD, UPDATE + public enum MessageType { + JOIN, ACCOUNT, PASSWORD, UPDATE, TEMP // TEMP는 교육 신청시 사용 } } diff --git a/src/main/java/com/example/DoroServer/global/message/dto/VerifyAuthNumReq.java b/src/main/java/com/example/DoroServer/global/message/dto/VerifyAuthNumReq.java index dfe9cb3f..ee41e1b6 100644 --- a/src/main/java/com/example/DoroServer/global/message/dto/VerifyAuthNumReq.java +++ b/src/main/java/com/example/DoroServer/global/message/dto/VerifyAuthNumReq.java @@ -1,12 +1,20 @@ package com.example.DoroServer.global.message.dto; import com.example.DoroServer.global.message.dto.SendAuthNumReq.MessageType; + import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; + +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor public class VerifyAuthNumReq { @NotBlank @Pattern(regexp = "^01([016789])([0-9]{3,4})([0-9]{4})$", message = "올바른 휴대폰 번호 형식이 아닙니다.") diff --git a/src/main/java/com/example/DoroServer/global/message/dto/VerifyAuthNumRes.java b/src/main/java/com/example/DoroServer/global/message/dto/VerifyAuthNumRes.java new file mode 100644 index 00000000..f98fda0b --- /dev/null +++ b/src/main/java/com/example/DoroServer/global/message/dto/VerifyAuthNumRes.java @@ -0,0 +1,16 @@ +package com.example.DoroServer.global.message.dto; + +import org.springframework.http.HttpHeaders; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VerifyAuthNumRes { + HttpHeaders sessionId; +} \ No newline at end of file From a8c0e7c9744649cd1e50ee6097ed0ad03caeba02 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Wed, 6 Dec 2023 18:26:33 +0900 Subject: [PATCH 03/32] =?UTF-8?q?Feat:=20Message=20API=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20#124=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EB=90=9C=20=EC=A0=84=ED=99=94=EB=B2=88=ED=98=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=84=B8=EC=85=98=EC=9D=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=B4=EC=A3=BC=EB=8A=94=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94=EA=B0=80("message/verif?= =?UTF-8?q?y2")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/message/MessageController.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/DoroServer/global/message/MessageController.java b/src/main/java/com/example/DoroServer/global/message/MessageController.java index b423eed9..1c0d91bb 100644 --- a/src/main/java/com/example/DoroServer/global/message/MessageController.java +++ b/src/main/java/com/example/DoroServer/global/message/MessageController.java @@ -3,11 +3,16 @@ import com.example.DoroServer.global.common.SuccessResponse; import com.example.DoroServer.global.message.dto.SendAuthNumReq; import com.example.DoroServer.global.message.dto.VerifyAuthNumReq; +import com.example.DoroServer.global.message.dto.VerifyAuthNumRes; + import io.swagger.annotations.Api; import io.swagger.v3.oas.annotations.Operation; + import javax.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; + +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -21,16 +26,24 @@ public class MessageController { @Operation(summary = "002_01", description = "인증번호 전송") @PostMapping("/message/send") - public SuccessResponse sendAuthNum(@RequestBody @Valid SendAuthNumReq sendAuthNumReq){ + public SuccessResponse sendAuthNum(@RequestBody @Valid SendAuthNumReq sendAuthNumReq) { messageService.sendAuthNum(sendAuthNumReq); return SuccessResponse.successResponse("인증번호가 전송되었습니다."); } - @Operation(summary = "002_01", description = "인증번호 확인") @PostMapping("/message/verify") - public SuccessResponse verifyAuthNum(@RequestBody @Valid VerifyAuthNumReq verifyAuthNumReq){ + public SuccessResponse verifyAuthNum(@RequestBody @Valid VerifyAuthNumReq verifyAuthNumReq) { messageService.verifyAuthNum(verifyAuthNumReq); return SuccessResponse.successResponse("인증 성공"); } + + @Operation(summary = "인증번호를 확인하고 세션이 생성됩니다", description = "인증번호 확인") + @PostMapping("/message/verify2") + public ResponseEntity verifyAuthNum2(@RequestBody @Valid VerifyAuthNumReq verifyAuthNumReq) { + VerifyAuthNumRes result = messageService.verifyAuthNum2(verifyAuthNumReq); + return ResponseEntity.ok() + .headers(result.getSessionId()) + .body("인증 성공"); + } } From a77ac9701fcf4cbac2450618e332584bf94c8f79 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Wed, 6 Dec 2023 18:27:36 +0900 Subject: [PATCH 04/32] =?UTF-8?q?Test:=20Message=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20=20#124?= =?UTF-8?q?=20-=20MessageControllerTest.java=20-=20MessageServiceImplTest.?= =?UTF-8?q?java=20-=20MessageServiceTestSetup.java?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/message/MessageControllerTest.java | 58 +++++++++++++++++++ .../message/MessageServiceImplTest.java | 51 ++++++++++++++++ .../message/MessageServiceTestSetup.java | 29 ++++++++++ 3 files changed, 138 insertions(+) create mode 100644 src/test/java/com/example/DoroServer/global/message/MessageControllerTest.java create mode 100644 src/test/java/com/example/DoroServer/global/message/MessageServiceImplTest.java create mode 100644 src/test/java/com/example/DoroServer/global/message/MessageServiceTestSetup.java diff --git a/src/test/java/com/example/DoroServer/global/message/MessageControllerTest.java b/src/test/java/com/example/DoroServer/global/message/MessageControllerTest.java new file mode 100644 index 00000000..dc3d8491 --- /dev/null +++ b/src/test/java/com/example/DoroServer/global/message/MessageControllerTest.java @@ -0,0 +1,58 @@ +package com.example.DoroServer.global.message; + +import static com.example.DoroServer.global.message.MessageServiceTestSetup.getVerifyAuthNumReq; +import static com.example.DoroServer.global.message.MessageServiceTestSetup.getVerifyAuthNumRes; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.example.DoroServer.global.message.dto.VerifyAuthNumReq; +import com.example.DoroServer.global.message.dto.VerifyAuthNumRes; +import com.fasterxml.jackson.databind.ObjectMapper; + +@ExtendWith(MockitoExtension.class) +public class MessageControllerTest { + + @InjectMocks + private MessageController messageController; + + @Mock + private MessageServiceImpl messageService; + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(messageController).build(); + } + + @Test + @DisplayName("MockMvc를 통한 휴대폰 인증 세션 테스트") + public void verifyAuthNum2Test() throws Exception { + // given + VerifyAuthNumReq verifyAuthNumReq = getVerifyAuthNumReq(); + VerifyAuthNumRes verifyAuthNumRes = getVerifyAuthNumRes(); + + given(messageService.verifyAuthNum2(any(VerifyAuthNumReq.class))).willReturn(verifyAuthNumRes); + + // when & then + mockMvc.perform(post("/message/verify2") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(verifyAuthNumReq))) + .andExpect(status().isOk()) + .andExpect(header().exists("Session-Id")); + } +} diff --git a/src/test/java/com/example/DoroServer/global/message/MessageServiceImplTest.java b/src/test/java/com/example/DoroServer/global/message/MessageServiceImplTest.java new file mode 100644 index 00000000..3ef9726b --- /dev/null +++ b/src/test/java/com/example/DoroServer/global/message/MessageServiceImplTest.java @@ -0,0 +1,51 @@ +package com.example.DoroServer.global.message; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.example.DoroServer.global.jwt.RedisService; +import com.example.DoroServer.global.message.dto.VerifyAuthNumReq; +import com.example.DoroServer.global.message.dto.VerifyAuthNumRes; + +import static com.example.DoroServer.global.message.MessageServiceTestSetup.getVerifyAuthNumReq; + +@ExtendWith(MockitoExtension.class) +public class MessageServiceImplTest { + + private MessageServiceImpl messageService; + + @Mock + private RedisService redisService; + + @BeforeEach + void setUp() { + messageService = new MessageServiceImpl("apiKey", "apiSecret", "fromNumber", "http://example.com", "pfid", + "templateId", redisService); + } + + @Test + @DisplayName("verifyAuthNum2 테스트") + void verifyAuthNum2Test() { + // given + VerifyAuthNumReq verifyAuthNumReq = getVerifyAuthNumReq(); + + given(redisService.getValues(verifyAuthNumReq.getMessageType() + verifyAuthNumReq.getPhone())) + .willReturn(verifyAuthNumReq.getAuthNum()); // redisService는 모두 정상적으로 작동한다고 가정 + + // when + VerifyAuthNumRes result = messageService.verifyAuthNum2(verifyAuthNumReq); + + // then + verify(redisService, Mockito.times(1)) + .getValues(verifyAuthNumReq.getMessageType() + verifyAuthNumReq.getPhone()); + assert (result.getSessionId().get("Session-Id") != null); + } +} diff --git a/src/test/java/com/example/DoroServer/global/message/MessageServiceTestSetup.java b/src/test/java/com/example/DoroServer/global/message/MessageServiceTestSetup.java new file mode 100644 index 00000000..9ff3c380 --- /dev/null +++ b/src/test/java/com/example/DoroServer/global/message/MessageServiceTestSetup.java @@ -0,0 +1,29 @@ +package com.example.DoroServer.global.message; + +import com.example.DoroServer.global.message.dto.SendAuthNumReq.MessageType; + +import org.springframework.http.HttpHeaders; + +import com.example.DoroServer.global.message.dto.VerifyAuthNumReq; +import com.example.DoroServer.global.message.dto.VerifyAuthNumRes; + +public class MessageServiceTestSetup { + + public static VerifyAuthNumReq getVerifyAuthNumReq() { + return VerifyAuthNumReq.builder() + .phone("01012345678") + .authNum("123456") + .messageType(MessageType.TEMP) + .build(); + + } + + public static VerifyAuthNumRes getVerifyAuthNumRes() { + HttpHeaders headers = new HttpHeaders(); + headers.add("Session-Id", "some-session-id"); + return VerifyAuthNumRes.builder() + .sessionId(headers) + .build(); + } + +} \ No newline at end of file From d99e56b3ba85d27de5e7ee7d03924ef750385829 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 09:16:45 +0900 Subject: [PATCH 05/32] =?UTF-8?q?Style:=20=EA=B8=B0=EB=B3=B8=20=EA=B0=92?= =?UTF-8?q?=EC=9D=B4=20=EC=9E=88=EB=8A=94=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EC=97=90=20@Builder.Default=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95=20-=20User=20-=20Lecture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DoroServer/domain/lecture/entity/Lecture.java | 13 +++++++------ .../DoroServer/domain/user/entity/User.java | 14 ++++---------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/example/DoroServer/domain/lecture/entity/Lecture.java b/src/main/java/com/example/DoroServer/domain/lecture/entity/Lecture.java index d08e26be..9352ae95 100644 --- a/src/main/java/com/example/DoroServer/domain/lecture/entity/Lecture.java +++ b/src/main/java/com/example/DoroServer/domain/lecture/entity/Lecture.java @@ -50,19 +50,20 @@ public class Lecture extends BaseEntity { private String staff; // 강의 스태프 수 - private String mainPayment; //강사 급여 + private String mainPayment; // 강사 급여 private String subPayment; private String staffPayment; - private String transportCost;//교통비 + private String transportCost;// 교통비 private String time; // 시간 private String remark; // 강의 기타 사항 - @ElementCollection() + @Builder.Default + @ElementCollection @CollectionTable(name = "lecture_date", joinColumns = @JoinColumn(name = "lecture_id")) private List lectureDates = new ArrayList<>(); // 강의 날짜 @@ -72,7 +73,7 @@ public class Lecture extends BaseEntity { @Embedded private LectureDate lectureDate; // 강의 날짜 관련 [enrollStateDate, enrollEndDate] - //== 연관관계 매핑 ==// + // == 연관관계 매핑 ==// // Lecture와 LectureContent는 다대일(Many-to-One) 관계 @ManyToOne(fetch = LAZY) @@ -88,8 +89,8 @@ public void setLectureContent(LectureContent lectureContent) { this.lectureContent = lectureContent; } - public void changeLectureStatus(LectureStatus lectureStatus){ - this.status=lectureStatus; + public void changeLectureStatus(LectureStatus lectureStatus) { + this.status = lectureStatus; } } diff --git a/src/main/java/com/example/DoroServer/domain/user/entity/User.java b/src/main/java/com/example/DoroServer/domain/user/entity/User.java index 565c7be1..3540f0ae 100644 --- a/src/main/java/com/example/DoroServer/domain/user/entity/User.java +++ b/src/main/java/com/example/DoroServer/domain/user/entity/User.java @@ -4,18 +4,11 @@ import com.example.DoroServer.domain.token.entity.Token; import java.time.LocalDate; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; + import org.hibernate.annotations.ColumnDefault; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; import java.util.ArrayList; import java.util.Collection; import com.example.DoroServer.domain.chat.entity.Chat; @@ -71,6 +64,7 @@ public class User extends BaseEntity implements UserDetails { private String profileImg; // 사용자 이미 + @Builder.Default @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List tokens = new ArrayList<>(); // 사용자 보유 FCM 토큰 @@ -128,7 +122,7 @@ public void updateGeneration(int generation) { this.generation = generation; } - public void updateBirth(LocalDate birth){ + public void updateBirth(LocalDate birth) { this.birth = birth; } @@ -136,7 +130,7 @@ public void updatePhone(String phone) { this.phone = phone; } - public void toInactive(){ + public void toInactive() { this.isActive = false; } From 1e1f04117f5956fa1b6b95a50205b7f9dfd08385 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 09:17:50 +0900 Subject: [PATCH 06/32] =?UTF-8?q?Fix:=20LectureRepository,UserLectureServi?= =?UTF-8?q?ce=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20LectureRepository=20-=20UserLectureService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/LectureRepositoryTest.java | 55 ++++---- .../service/UserLectureServiceTest.java | 132 ++++++++++-------- 2 files changed, 101 insertions(+), 86 deletions(-) diff --git a/src/test/java/com/example/DoroServer/domain/lecture/repository/LectureRepositoryTest.java b/src/test/java/com/example/DoroServer/domain/lecture/repository/LectureRepositoryTest.java index 59f008c3..252a7e1d 100644 --- a/src/test/java/com/example/DoroServer/domain/lecture/repository/LectureRepositoryTest.java +++ b/src/test/java/com/example/DoroServer/domain/lecture/repository/LectureRepositoryTest.java @@ -1,7 +1,10 @@ package com.example.DoroServer.domain.lecture.repository; +import java.util.Arrays; +import java.util.Collections; import com.example.DoroServer.domain.lecture.entity.Lecture; import com.example.DoroServer.domain.lectureContent.entity.LectureContent; +import com.example.DoroServer.domain.lectureContent.repository.LectureContentRepository; import com.example.DoroServer.global.config.QueryDslConfig; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -26,27 +29,39 @@ @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@Import({QueryDslConfig.class}) +@Import({ QueryDslConfig.class }) class LectureRepositoryTest { @Autowired LectureRepository lectureRepository; + @Autowired + LectureContentRepository lectureContentRepository; + @PersistenceContext EntityManager em; + LectureContent lectureContent; + + @BeforeEach + void setUp() { + lectureContent = LectureContent.builder().kit("테스트 키트").detail("세부사항").requirement("고졸").build(); + lectureContentRepository.save(lectureContent); + } @DisplayName("findLectureById_Success_Test") @Test void findLectureByIdTest() { // given - LectureContent lectureContent = LectureContent.builder().id(1L).build(); Lecture lecture = Lecture.builder() + .mainTitle("강의 제목") .lectureContent(lectureContent) .build(); Lecture saved = lectureRepository.save(lecture); + // when Optional optionalLecture = lectureRepository.findLectureById(saved.getId()); + // then assertThat(em.getEntityManagerFactory().getPersistenceUnitUtil() .isLoaded(optionalLecture.get().getLectureContent())).isTrue(); @@ -56,39 +71,23 @@ void findLectureByIdTest() { @Test void findLecturesByFinishedDateTest() { // given - LectureContent lectureContent = LectureContent.builder().id(1L).build(); - - ArrayList> localDatesList = new ArrayList<>(); - - ArrayList localDates = new ArrayList<>(); - localDates.add(LocalDate.of(2023,10,10)); - localDates.add(LocalDate.of(2023,10,11)); - - - ArrayList localDates2 = new ArrayList<>(); - localDates2.add(LocalDate.of(2023,10,10)); - localDates2.add(LocalDate.of(2023,10,9)); - - ArrayList localDates3 = new ArrayList<>(); - localDates3.add(LocalDate.of(2023,10,10)); - - - localDatesList.add(localDates); - localDatesList.add(localDates2); - localDatesList.add(localDates3); - - for(ArrayList inputList : localDatesList){ - Lecture lecture1 = Lecture.builder() + List> localDatesList = Arrays.asList( + Arrays.asList(LocalDate.of(2023, 10, 10), LocalDate.of(2023, 10, 11)), + Arrays.asList(LocalDate.of(2023, 10, 10), LocalDate.of(2023, 10, 9)), + Collections.singletonList(LocalDate.of(2023, 10, 10))); + for (List inputList : localDatesList) { + Lecture lecture = Lecture.builder() .lectureDates(inputList) .lectureContent(lectureContent) .build(); - Lecture saved = lectureRepository.save(lecture1); + lectureRepository.save(lecture); } - Integer finishedLecturesCount=2; + Integer finishedLecturesCount = 2; + LocalDate finishedDate = LocalDate.of(2023, 10, 10); - LocalDate finishedDate=LocalDate.of(2023, 10, 10); // when List lecturesByFinishedDate = lectureRepository.findLecturesByFinishedDate(finishedDate); + // then assertThat(lecturesByFinishedDate.size()).isEqualTo(finishedLecturesCount); } diff --git a/src/test/java/com/example/DoroServer/domain/userLecture/service/UserLectureServiceTest.java b/src/test/java/com/example/DoroServer/domain/userLecture/service/UserLectureServiceTest.java index 2b88587e..c17ecb04 100644 --- a/src/test/java/com/example/DoroServer/domain/userLecture/service/UserLectureServiceTest.java +++ b/src/test/java/com/example/DoroServer/domain/userLecture/service/UserLectureServiceTest.java @@ -4,6 +4,8 @@ import com.example.DoroServer.domain.lecture.entity.LectureDate; import com.example.DoroServer.domain.lecture.entity.LectureStatus; import com.example.DoroServer.domain.lecture.repository.LectureRepository; +import com.example.DoroServer.domain.notification.dto.NotificationContentReq; +import com.example.DoroServer.domain.notification.service.NotificationServiceRefact; import com.example.DoroServer.domain.user.entity.Degree; import com.example.DoroServer.domain.user.entity.Gender; import com.example.DoroServer.domain.user.entity.StudentStatus; @@ -23,7 +25,6 @@ import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; - import java.lang.reflect.Field; import java.time.LocalDate; import java.util.ArrayList; @@ -36,7 +37,6 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class UserLectureServiceTest { @InjectMocks @@ -51,11 +51,13 @@ class UserLectureServiceTest { @Mock private LectureRepository lectureRepository; + @Mock + private NotificationServiceRefact notificationService; + @Spy private UserLectureMapper userLectureMapper = Mappers.getMapper(UserLectureMapper.class); - - private User setUpUser(Long userId){ + private User setUpUser(Long userId) { return User.builder() .id(userId) .name("name") @@ -73,7 +75,7 @@ private User setUpUser(Long userId){ .build(); } - private Lecture setUpLecture(Long lectureId){ + private Lecture setUpLecture(Long lectureId) { ArrayList dates = new ArrayList<>(); LocalDate now = LocalDate.now(); dates.add(now); @@ -103,11 +105,11 @@ private Lecture setUpLecture(Long lectureId){ .build(); } - private UserLecture setUpUserLecture(Long userLectureId){ - Long userId=3L; + private UserLecture setUpUserLecture(Long userLectureId) { + Long userId = 3L; User user = setUpUser(userId); - Long lectureId=2L; + Long lectureId = 2L; Lecture lecture = setUpLecture(lectureId); return UserLecture.builder() @@ -119,21 +121,21 @@ private UserLecture setUpUserLecture(Long userLectureId){ .build(); } - private List setUpUserLectureList(int length){ + private List setUpUserLectureList(int length) { ArrayList userLectureArrayList = new ArrayList<>(); - for(int i=0;i userLectures = setUpUserLectureList(userLectureCount); given(userLectureRepository.findAllTutors(any(Long.class))).willReturn(userLectures); FindAllTutorsRes findAllTutorsRes = setUpFindAllTutorsRes(); - given(userLectureMapper.toFindAllTutorsRes(any(UserLecture.class),any(User.class))).willReturn(findAllTutorsRes); + given(userLectureMapper.toFindAllTutorsRes(any(UserLecture.class), any(User.class))) + .willReturn(findAllTutorsRes); // when - Long lectureId=3L; + Long lectureId = 3L; List allTutors = userLectureService.findAllTutors(lectureId); // then - verify(userLectureRepository,times(1)).findAllTutors(any(Long.class)); + verify(userLectureRepository, times(1)).findAllTutors(any(Long.class)); assertThat(allTutors.size()).isEqualTo(userLectureCount); } @@ -269,11 +276,12 @@ void findAllTutorsTest() { @Test void findMyLecturesMapperTest() throws IllegalAccessException { // given - Long userLectureId=4L; + Long userLectureId = 4L; UserLecture userLecture = setUpUserLecture(userLectureId); // when - FindMyLecturesRes findMyLecturesRes = userLectureMapper.toFindMyLecturesRes(userLecture.getLecture(), userLecture); + FindMyLecturesRes findMyLecturesRes = userLectureMapper.toFindMyLecturesRes(userLecture.getLecture(), + userLecture); // then for (Field field : findMyLecturesRes.getClass().getDeclaredFields()) { field.setAccessible(true); @@ -292,14 +300,15 @@ void findMyLectureTest() { given(userLectureRepository.findMyLectures(any(Long.class))).willReturn(userLectures); FindMyLecturesRes findMyLecturesRes = setUpFindMyLectures(); - given(userLectureMapper.toFindMyLecturesRes(any(Lecture.class),any(UserLecture.class))).willReturn(findMyLecturesRes); + given(userLectureMapper.toFindMyLecturesRes(any(Lecture.class), any(UserLecture.class))) + .willReturn(findMyLecturesRes); // when - Long userId=4L; + Long userId = 4L; List myLectures = userLectureService.findMyLectures(userId); // then - verify(userLectureRepository,times(1)).findMyLectures(any(Long.class)); + verify(userLectureRepository, times(1)).findMyLectures(any(Long.class)); assertThat(myLectures.size()).isEqualTo(userLectureCount); } @@ -308,8 +317,8 @@ void findMyLectureTest() { @Test void selectTutorNoTutorExceptionTest() { // given - given(userLectureRepository.findUserLecture(any(Long.class),any(Long.class),any(TutorRole.class))).willReturn(Optional.empty()); - + given(userLectureRepository.findUserLecture(any(Long.class), any(Long.class), any(TutorRole.class))) + .willReturn(Optional.empty()); Long lectureId = 1L; SelectTutorReq selectTutorReq = SelectTutorReq.builder() @@ -329,9 +338,10 @@ void selectTutorNoTutorExceptionTest() { @Test void selectTutorNoUserExceptionTest() { // given - Long userLectureId=4L; + Long userLectureId = 4L; UserLecture userLecture = setUpUserLecture(userLectureId); - given(userLectureRepository.findUserLecture(any(Long.class),any(Long.class),any(TutorRole.class))).willReturn(Optional.of(userLecture)); + given(userLectureRepository.findUserLecture(any(Long.class), any(Long.class), any(TutorRole.class))) + .willReturn(Optional.of(userLecture)); given(userRepository.findById(any(Long.class))).willReturn(Optional.empty()); Long lectureId = 1L; @@ -352,9 +362,10 @@ void selectTutorNoUserExceptionTest() { @Test void selectTutorNoLectureExceptionTest() { // given - Long userLectureId=4L; + Long userLectureId = 4L; UserLecture userLecture = setUpUserLecture(userLectureId); - given(userLectureRepository.findUserLecture(any(Long.class),any(Long.class),any(TutorRole.class))).willReturn(Optional.of(userLecture)); + given(userLectureRepository.findUserLecture(any(Long.class), any(Long.class), any(TutorRole.class))) + .willReturn(Optional.of(userLecture)); given(userRepository.findById(any(Long.class))).willReturn(Optional.of(userLecture.getUser())); given(lectureRepository.findById(any(Long.class))).willReturn(Optional.empty()); @@ -378,18 +389,22 @@ void selectTutorNoLectureExceptionTest() { @Test void selectTutorTest() { // given - Long userLectureId=4L; + Long userLectureId = 4L; UserLecture userLecture = setUpUserLecture(userLectureId); - given(userLectureRepository.findUserLecture(any(Long.class),any(Long.class),any(TutorRole.class))).willReturn(Optional.of(userLecture)); + given(userLectureRepository.findUserLecture(any(Long.class), any(Long.class), any(TutorRole.class))) + .willReturn(Optional.of(userLecture)); - Long userId=3L; + Long userId = 3L; User user = setUpUser(userId); given(userRepository.findById(any(Long.class))).willReturn(Optional.of(user)); - Long lectureId=4L; + Long lectureId = 4L; Lecture lecture = setUpLecture(lectureId); given(lectureRepository.findById(any(Long.class))).willReturn(Optional.of(lecture)); + given(notificationService.sendNotificationToOne(any(Long.class), any(Long.class), + any(NotificationContentReq.class))) + .willReturn(userId); SelectTutorReq selectTutorReq = SelectTutorReq.builder() .userId(userId) @@ -397,21 +412,22 @@ void selectTutorTest() { .build(); // when - userLectureService.selectTutor(lectureId,selectTutorReq); + userLectureService.selectTutor(lectureId, selectTutorReq); + // then assertThat(userLecture.getTutorStatus()).isEqualTo(TutorStatus.ASSIGNED); } -// @DisplayName("강의 신청 취소 테스트") -// @Test -// void deleteLectureTest() { -// // given -// doNothing().when(userLectureRepository).deleteById(any(Long.class)); -// // when -// Long userLectureId=1L; -// userLectureService.deleteUserLecture(userLectureId,); -// // then -// verify(userLectureRepository,times(1)).deleteById(any(Long.class)); -// -// } + // @DisplayName("강의 신청 취소 테스트") + // @Test + // void deleteLectureTest() { + // // given + // doNothing().when(userLectureRepository).deleteById(any(Long.class)); + // // when + // Long userLectureId=1L; + // userLectureService.deleteUserLecture(userLectureId,); + // // then + // verify(userLectureRepository,times(1)).deleteById(any(Long.class)); + // + // } } \ No newline at end of file From 01b570f1401603c5aece85c9995e6e6b13a38242 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 20:04:03 +0900 Subject: [PATCH 07/32] =?UTF-8?q?Test:=20RedisService=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20-=20RedisSer?= =?UTF-8?q?viceTest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/jwt/RedisServiceTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/test/java/com/example/DoroServer/global/jwt/RedisServiceTest.java diff --git a/src/test/java/com/example/DoroServer/global/jwt/RedisServiceTest.java b/src/test/java/com/example/DoroServer/global/jwt/RedisServiceTest.java new file mode 100644 index 00000000..efcc4c9a --- /dev/null +++ b/src/test/java/com/example/DoroServer/global/jwt/RedisServiceTest.java @@ -0,0 +1,34 @@ +package com.example.DoroServer.global.jwt; + +import static org.assertj.core.api.Assertions.assertThat; + +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; + +@SpringBootTest +public class RedisServiceTest { + + @Autowired + private RedisService redisService; + + @Test + @DisplayName("setValues, getValues 테스트") + public void testRedisSetAndGet() { + // given + String key = "test"; + String value = "test"; + + // when + redisService.setValues(key, value); + String returnedValue = redisService.getValues(key); + + // then + assertThat(returnedValue).isEqualTo(value); + + // after + redisService.deleteValues(key); + + } +} \ No newline at end of file From d9a05e61808916147d6eb57d356117b4fd10f5e7 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 20:22:52 +0900 Subject: [PATCH 08/32] =?UTF-8?q?Feat:=20=ED=95=84=ED=84=B0=EC=B2=B4?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20=EC=84=B8=EC=85=98=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=20#124=20=EC=84=B8=EC=85=98?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=8A=94=20"/education-application/**"=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=EC=97=90=EB=A7=8C=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=EB=90=A8=20=EC=9A=94=EC=B2=AD=20=ED=97=A4=EB=8D=94=EC=9D=98=20?= =?UTF-8?q?Session-Id=EB=A5=BC=20=ED=86=B5=ED=95=B4=20=ED=9C=B4=EB=8C=80?= =?UTF-8?q?=ED=8F=B0=20=EB=B2=88=ED=98=B8=EB=A5=BC=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=EC=97=90=20=EC=A0=84=EB=8B=AC=20-=20SessionF?= =?UTF-8?q?ilter=20-=20SessionFilterHandler=20-=20SessionException=20-=20S?= =?UTF-8?q?ecurityConfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/SessionFilter.java | 50 +++++++++++++++++++ .../api/SessionFilterHandler.java | 46 +++++++++++++++++ .../global/config/SecurityConfig.java | 6 ++- .../global/exception/SessionIdException.java | 8 +++ 4 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/DoroServer/domain/educationApplication/api/SessionFilter.java create mode 100644 src/main/java/com/example/DoroServer/domain/educationApplication/api/SessionFilterHandler.java create mode 100644 src/main/java/com/example/DoroServer/global/exception/SessionIdException.java diff --git a/src/main/java/com/example/DoroServer/domain/educationApplication/api/SessionFilter.java b/src/main/java/com/example/DoroServer/domain/educationApplication/api/SessionFilter.java new file mode 100644 index 00000000..b450c94a --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/educationApplication/api/SessionFilter.java @@ -0,0 +1,50 @@ +package com.example.DoroServer.domain.educationApplication.api; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.filter.GenericFilterBean; + +import com.example.DoroServer.global.exception.Code; +import com.example.DoroServer.global.exception.SessionIdException; +import com.example.DoroServer.global.jwt.RedisService; + +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class SessionFilter extends GenericFilterBean { + // 해당 필터는 "/education-application"에 대한 요청에 대해서만 적용 + + private final RedisService redisService; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + String requestURI = httpRequest.getRequestURI(); + + if (requestURI.startsWith("/education-application")) { + String sessionId = httpRequest.getHeader("Session-Id"); + if (sessionId == null) { + log.debug("유효한 Session-Id가 없습니다, uri: {}", requestURI); + throw new SessionIdException(Code.SESSION_ID_NOT_FOUND); + } + String phoneNumber = redisService.getValues(sessionId); + if (phoneNumber == null) { + log.debug("유효한 Session-Id가 아닙니다, uri: {}", requestURI); + throw new SessionIdException(Code.SESSION_ID_NOT_VALID); + } + httpRequest.setAttribute("phoneNumber", phoneNumber); + + } + + chain.doFilter(request, response); + } +} diff --git a/src/main/java/com/example/DoroServer/domain/educationApplication/api/SessionFilterHandler.java b/src/main/java/com/example/DoroServer/domain/educationApplication/api/SessionFilterHandler.java new file mode 100644 index 00000000..8e68d78c --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/educationApplication/api/SessionFilterHandler.java @@ -0,0 +1,46 @@ +package com.example.DoroServer.domain.educationApplication.api; + +import javax.servlet.FilterChain; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletException; + +import java.io.IOException; + +import org.springframework.web.filter.GenericFilterBean; + +import com.example.DoroServer.global.common.AuthErrorResponse; +import com.example.DoroServer.global.exception.Code; +import com.example.DoroServer.global.exception.SessionIdException; + +public class SessionFilterHandler extends GenericFilterBean { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + try { + chain.doFilter(request, response); + } catch (SessionIdException e) { + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.setContentType("application/json"); + httpResponse.setCharacterEncoding("utf-8"); + Code code = e.getMessage().equals("SESSION_ID_NOT_FOUND") ? Code.SESSION_ID_NOT_FOUND + : Code.SESSION_ID_NOT_VALID; + httpResponse.setStatus(code.getHttpStatus().value()); + AuthErrorResponse authErrorResponse = buildAuthErrorResponse(code); + httpResponse.getWriter().write(authErrorResponse.toString()); + + return; + } + } + + // 리팩터링 필요 - static 메서드가 아닌 별개의 클래스에서 메서드 생성 + public static AuthErrorResponse buildAuthErrorResponse(Code errorCode) { + return AuthErrorResponse.builder() + .errorCode(errorCode) + .message(errorCode.getMessage()) + .cause(SessionFilter.class.getName()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/DoroServer/global/config/SecurityConfig.java b/src/main/java/com/example/DoroServer/global/config/SecurityConfig.java index 500560bd..cfdf63a7 100644 --- a/src/main/java/com/example/DoroServer/global/config/SecurityConfig.java +++ b/src/main/java/com/example/DoroServer/global/config/SecurityConfig.java @@ -1,5 +1,7 @@ package com.example.DoroServer.global.config; +import com.example.DoroServer.domain.educationApplication.api.SessionFilter; +import com.example.DoroServer.domain.educationApplication.api.SessionFilterHandler; import com.example.DoroServer.global.jwt.*; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -50,7 +52,6 @@ public WebSecurityCustomizer webSecurityCustomizer() { "/check/phone", "/find/account", "/change/password", - "/education-application/**", "/", "/swagger-ui.html", "/v3/api-docs/**", @@ -82,10 +83,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .and() .authorizeRequests() + .antMatchers("/education-application/**").permitAll() // 훚대폰 인증 세션 필터 적용 .anyRequest().authenticated() .and() .addFilter(corsConfig.corsFilter()) + .addFilterBefore(new SessionFilter(redisService), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new SessionFilterHandler(), SessionFilter.class) .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, redisService), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtAuthenticationExceptionHandler(), JwtAuthenticationFilter.class); diff --git a/src/main/java/com/example/DoroServer/global/exception/SessionIdException.java b/src/main/java/com/example/DoroServer/global/exception/SessionIdException.java new file mode 100644 index 00000000..a6bddb3b --- /dev/null +++ b/src/main/java/com/example/DoroServer/global/exception/SessionIdException.java @@ -0,0 +1,8 @@ +package com.example.DoroServer.global.exception; + +public class SessionIdException extends RuntimeException { + + public SessionIdException(Code code) { + super(code.name()); + } +} From b67d391adbdc741f58b797ab8610cec5891e2ffc Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 20:25:42 +0900 Subject: [PATCH 09/32] =?UTF-8?q?Fix:=20JwtAuthenticationFilter=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EC=A7=80=EC=A0=95=20#124=20@Order?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=B4=20JwtAuthenticationFilt?= =?UTF-8?q?er=20=EC=88=9C=EC=84=9C=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DoroServer/global/jwt/JwtAuthenticationFilter.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/DoroServer/global/jwt/JwtAuthenticationFilter.java b/src/main/java/com/example/DoroServer/global/jwt/JwtAuthenticationFilter.java index d11006a9..c2dba415 100644 --- a/src/main/java/com/example/DoroServer/global/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/DoroServer/global/jwt/JwtAuthenticationFilter.java @@ -3,6 +3,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.StringUtils; @@ -16,6 +18,7 @@ import java.io.IOException; @Slf4j +@Order(Ordered.HIGHEST_PRECEDENCE) @RequiredArgsConstructor public class JwtAuthenticationFilter extends GenericFilterBean { @@ -23,13 +26,14 @@ public class JwtAuthenticationFilter extends GenericFilterBean { private final RedisService redisService; @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String jwtToken = jwtTokenProvider.resolveToken(httpServletRequest); String requestURI = httpServletRequest.getRequestURI(); if (StringUtils.hasText(jwtToken) && jwtTokenProvider.validateToken(jwtToken)) { - if(redisService.getValues(jwtToken) == null) { + if (redisService.getValues(jwtToken) == null) { Authentication authentication = jwtTokenProvider.getAuthentication(jwtToken); SecurityContextHolder.getContext().setAuthentication(authentication); log.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI); @@ -38,7 +42,6 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha log.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI); } - chain.doFilter(request, response); } } From ba591f03cb2e2ecc423f5efac561efbc9da45670 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 20:28:05 +0900 Subject: [PATCH 10/32] =?UTF-8?q?Fix:=20DTO=EC=97=90=EC=84=9C=20=ED=9C=B4?= =?UTF-8?q?=EB=8C=80=ED=8F=B0=20=EB=B2=88=ED=98=B8=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#124=20-=20EducationApplicationReq=20-=20R?= =?UTF-8?q?etrieveApplicationReq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../educationApplication/dto/EducationApplicationReq.java | 2 ++ .../domain/educationApplication/dto/RetrieveApplicationReq.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/com/example/DoroServer/domain/educationApplication/dto/EducationApplicationReq.java b/src/main/java/com/example/DoroServer/domain/educationApplication/dto/EducationApplicationReq.java index 26af9194..83a56cd7 100644 --- a/src/main/java/com/example/DoroServer/domain/educationApplication/dto/EducationApplicationReq.java +++ b/src/main/java/com/example/DoroServer/domain/educationApplication/dto/EducationApplicationReq.java @@ -3,6 +3,7 @@ import javax.validation.constraints.Email; import javax.validation.constraints.Min; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Getter; @@ -27,6 +28,7 @@ public class EducationApplicationReq { private String position; // 신청자 직위 @NotBlank + @Pattern(regexp = "^01([016789])([0-9]{3,4})([0-9]{4})$", message = "올바른 휴대폰 번호 형식이 아닙니다.") private String phoneNumber; // 신청자 전화번호 @Email diff --git a/src/main/java/com/example/DoroServer/domain/educationApplication/dto/RetrieveApplicationReq.java b/src/main/java/com/example/DoroServer/domain/educationApplication/dto/RetrieveApplicationReq.java index d1976350..b07539e5 100644 --- a/src/main/java/com/example/DoroServer/domain/educationApplication/dto/RetrieveApplicationReq.java +++ b/src/main/java/com/example/DoroServer/domain/educationApplication/dto/RetrieveApplicationReq.java @@ -1,6 +1,7 @@ package com.example.DoroServer.domain.educationApplication.dto; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Getter; @@ -15,5 +16,6 @@ @AllArgsConstructor public class RetrieveApplicationReq { @NotBlank + @Pattern(regexp = "^01([016789])([0-9]{3,4})([0-9]{4})$", message = "올바른 휴대폰 번호 형식이 아닙니다.") private String phoneNumber; // 신청자 전화번호 } From ded0ef059a199c5798a6aaa0454ce2a965be40c4 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 20:29:20 +0900 Subject: [PATCH 11/32] =?UTF-8?q?Feat:=20=EC=98=A4=EB=A5=98=20Code=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#124=20=EC=84=B8=EC=85=98=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=EA=B3=BC=20=EA=B4=80=EB=A0=A8=EB=90=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20enum=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DoroServer/global/exception/Code.java | 161 +++++++++--------- 1 file changed, 82 insertions(+), 79 deletions(-) diff --git a/src/main/java/com/example/DoroServer/global/exception/Code.java b/src/main/java/com/example/DoroServer/global/exception/Code.java index b6a25122..a1cd77d5 100644 --- a/src/main/java/com/example/DoroServer/global/exception/Code.java +++ b/src/main/java/com/example/DoroServer/global/exception/Code.java @@ -11,83 +11,86 @@ @RequiredArgsConstructor public enum Code { - SUCCESS(OK, "SUCCESS", "OK"), - // Common - BAD_REQUEST(HttpStatus.BAD_REQUEST, "AUTH001", "잘못된 요청입니다."), - // 인증 관련 오류 AUTH001,AUTH002... - FORBIDDEN(HttpStatus.FORBIDDEN, "AUTH001", "접근 권한이 없습니다."), - UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "AUTH002", "인증정보가 유효하지 않습니다."), - JWT_BAD_REQUEST(HttpStatus.UNAUTHORIZED, "AUTH003", "잘못된 JWT 서명입니다."), - JWT_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "AUTH004", "토큰이 만료되었습니다."), - JWT_UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH006", "지원하지 않는 JWT 토큰입니다."), - JWT_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "AUTH007", "유효한 JWT 토큰이 없습니다."), - UNAUTHORIZED_PHONE_NUMBER(HttpStatus.UNAUTHORIZED, "AUTH008", "인증되지 않은 전화번호입니다."), - EXIST_ACCOUNT(HttpStatus.CONFLICT, "AUTH009", "이미 존재하는 아이디입니다."), - PASSWORD_DID_NOT_MATCH(HttpStatus.BAD_REQUEST, "AUTH010", "비밀번호가 일치하지 않습니다."), - DORO_ADMIN_AUTH_FAILED(HttpStatus.UNAUTHORIZED, "AUTH011", "관리자 인증번호가 일치하지 않습니다."), - DORO_USER_AUTH_FAILED(HttpStatus.UNAUTHORIZED, "AUTH012", "도로 인증번호가 일치하지 않습니다."), - ACCOUNT_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH013", "가입된 아이디가 없습니다."), - EXIST_PHONE(HttpStatus.CONFLICT, "AUTH014", "이미 존재하는 휴대폰 번호입니다."), - REFRESH_TOKEN_DID_NOT_MATCH(HttpStatus.BAD_REQUEST, "AUTH015", "RefreshToken 정보가 일치하지 않습니다."), - - WITHDRAWAL_FAILED(HttpStatus.BAD_REQUEST, "AUTH016", "회원 탈퇴에 실패했습니다."), - - // USER 관련 오류 U001,U002... - USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER001", "사용자가 존재하지 않습니다"), - - // 강의 관련 오류 L001,L002.. - LECTURE_NOT_FOUND(HttpStatus.BAD_REQUEST, "LEC001", "강의가 존재하지 않습니다."), - LECTURE_CONTENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "LEC002", "강의 컨텐츠가 존재하지 않습니다."), - - // 신청 강사 관련 오류 - TUTOR_NOT_FOUND(HttpStatus.BAD_REQUEST, "TUTOR001", "신청하지 않은 강사입니다."), - ALREADY_EXIST(HttpStatus.BAD_REQUEST, "TUTOR002", "이미 신청하셨습니다"), - USER_LECTURE_NOT_FOUND(HttpStatus.BAD_REQUEST, "TUTOR003", "신청내역이 존재하지 않습니다"), - - // 알림톡 관련 오류 M001, M002... - MESSAGE_SEND_FAILED(HttpStatus.BAD_REQUEST, "M001", "메시지 전송이 실패했습니다. 올바른 번호인지 확인하세요."), - VERIFICATION_DID_NOT_MATCH(HttpStatus.BAD_REQUEST, "M002", "인증 번호가 일치하지 않습니다."), - - // Notification 관련 오류 - NOTIFICATION_PUSH_FAIL(HttpStatus.BAD_REQUEST, "NOTI001", "FCM 알림 푸쉬에 실패했습니다."), - NOTIFICATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "NOTI002", "알림을 찾을 수 없습니다."), - - // 공지 관련 오류 - ANNOUNCEMENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "ANNO001", "공지를 찾을 수 없습니다."), - - // S3 관련 오류 S001, S002... - RESIZE_FAILED(HttpStatus.BAD_REQUEST, "S001", "파일 리사이징을 실패했습니다."), - EMPTY_FILE(HttpStatus.NO_CONTENT, "S002", "업로드 파일이 없습니다."), - UPLOAD_FAILED(HttpStatus.BAD_REQUEST, "S003", "업로드 중 오류가 발생했습니다."), - FILE_DELETE_FAILED(HttpStatus.BAD_REQUEST, "S004", "파일 삭제 중 오류가 발생했습니다."), - - // UserNotification 관련 오류 - USER_NOTIFICATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "USERNOTI001", "UserNotification을 찾을 수 없습니다."), - - // fcm 관련 오류 - FCM_UNSPECIFIED_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "FCM001", - "현재 알림을 보낼 수 없습니다. 다시 한번 시도하시고 안되면 관리자에게 문의 바랍니다."), - FCM_CERTIFICATION_IS_NOT_VALID(HttpStatus.SERVICE_UNAVAILABLE, "FCM002", - "FCM 계정을 확인해주세요 apns 혹은 web 인증서에 문제가 있습니다."), - FCM_INTERNAL_SERVER_ERROR(HttpStatus.SERVICE_UNAVAILABLE, "FCM003", - "현재 FCM 서버에 문제가 있습니다, 다시 한번 시도해보시고 계속 문제가 있으면 FCM 측에 문의를 해주세요"), - FCM_INVALID_ARGUMENT(HttpStatus.INTERNAL_SERVER_ERROR, "FCM004", - "어플리케이션 이용이 오래되어 토큰이 삭제되었습니다, 로그아웃 혹은 앱을 지웠다가 다시 접속해주세요."), - FCM_UNREGISTERED(HttpStatus.INTERNAL_SERVER_ERROR, "FCM005", - "어플리케이션 이용이 오래되어 토큰이 삭제되었습니다, 로그아웃 혹은 앱을 지웠다가 다시 접속해주세요"), - - // 강의(교육) 신청 관련 오류 - EDUCATION_APPLICATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "EDU001", "교육 신청서를 찾을 수 없습니다."), - EDUCATION_CLASS_GROUP_NOT_FOUND(HttpStatus.BAD_REQUEST, "EDU002", "교육 학급을 찾을 수 없습니다."), - - METHOD_ARGUMENT_NOT_VALID(HttpStatus.BAD_REQUEST, "C-0004", "요청 인자가 유효하지 않음"), - METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "C-0002", "허용되지 않은 Request Method 호출"), - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "C-0003", "내부 서버 오류"), - INVALID_TYPE_VALUE(HttpStatus.BAD_REQUEST, "C005", "유효하지 않은 값 타입"), - JSON_SYNTAX_ERROR(HttpStatus.BAD_REQUEST, "C-006", "json 형식이 잘못되었습니다."), - NOT_SUPPORTED_BODY_TYPE(HttpStatus.BAD_REQUEST, "C-007", "지원하지 않는 형식의 Request Body 형식입니다. json 형식으로 보내주세요"); - - private final HttpStatus httpStatus; - private final String code; - private final String message; + SUCCESS(OK, "SUCCESS", "OK"), + // Common + BAD_REQUEST(HttpStatus.BAD_REQUEST, "AUTH001", "잘못된 요청입니다."), + // 인증 관련 오류 AUTH001,AUTH002... + FORBIDDEN(HttpStatus.FORBIDDEN, "AUTH001", "접근 권한이 없습니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "AUTH002", "인증정보가 유효하지 않습니다."), + JWT_BAD_REQUEST(HttpStatus.UNAUTHORIZED, "AUTH003", "잘못된 JWT 서명입니다."), + JWT_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "AUTH004", "토큰이 만료되었습니다."), + JWT_UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH006", "지원하지 않는 JWT 토큰입니다."), + JWT_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "AUTH007", "유효한 JWT 토큰이 없습니다."), + UNAUTHORIZED_PHONE_NUMBER(HttpStatus.UNAUTHORIZED, "AUTH008", "인증되지 않은 전화번호입니다."), + EXIST_ACCOUNT(HttpStatus.CONFLICT, "AUTH009", "이미 존재하는 아이디입니다."), + PASSWORD_DID_NOT_MATCH(HttpStatus.BAD_REQUEST, "AUTH010", "비밀번호가 일치하지 않습니다."), + DORO_ADMIN_AUTH_FAILED(HttpStatus.UNAUTHORIZED, "AUTH011", "관리자 인증번호가 일치하지 않습니다."), + DORO_USER_AUTH_FAILED(HttpStatus.UNAUTHORIZED, "AUTH012", "도로 인증번호가 일치하지 않습니다."), + ACCOUNT_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH013", "가입된 아이디가 없습니다."), + EXIST_PHONE(HttpStatus.CONFLICT, "AUTH014", "이미 존재하는 휴대폰 번호입니다."), + REFRESH_TOKEN_DID_NOT_MATCH(HttpStatus.BAD_REQUEST, "AUTH015", "RefreshToken 정보가 일치하지 않습니다."), + SESSION_ID_NOT_FOUND(HttpStatus.UNAUTHORIZED, "AUTH016", "SessionId 정보가 없습니다."), + SESSION_ID_NOT_VALID(HttpStatus.UNAUTHORIZED, "AUTH017", "SessionId 정보가 유효하지 않습니다."), + + WITHDRAWAL_FAILED(HttpStatus.BAD_REQUEST, "AUTH016", "회원 탈퇴에 실패했습니다."), + + // USER 관련 오류 U001,U002... + USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER001", "사용자가 존재하지 않습니다"), + + // 강의 관련 오류 L001,L002.. + LECTURE_NOT_FOUND(HttpStatus.BAD_REQUEST, "LEC001", "강의가 존재하지 않습니다."), + LECTURE_CONTENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "LEC002", "강의 컨텐츠가 존재하지 않습니다."), + + // 신청 강사 관련 오류 + TUTOR_NOT_FOUND(HttpStatus.BAD_REQUEST, "TUTOR001", "신청하지 않은 강사입니다."), + ALREADY_EXIST(HttpStatus.BAD_REQUEST, "TUTOR002", "이미 신청하셨습니다"), + USER_LECTURE_NOT_FOUND(HttpStatus.BAD_REQUEST, "TUTOR003", "신청내역이 존재하지 않습니다"), + + // 알림톡 관련 오류 M001, M002... + MESSAGE_SEND_FAILED(HttpStatus.BAD_REQUEST, "M001", "메시지 전송이 실패했습니다. 올바른 번호인지 확인하세요."), + VERIFICATION_DID_NOT_MATCH(HttpStatus.BAD_REQUEST, "M002", "인증 번호가 일치하지 않습니다."), + + // Notification 관련 오류 + NOTIFICATION_PUSH_FAIL(HttpStatus.BAD_REQUEST, "NOTI001", "FCM 알림 푸쉬에 실패했습니다."), + NOTIFICATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "NOTI002", "알림을 찾을 수 없습니다."), + + // 공지 관련 오류 + ANNOUNCEMENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "ANNO001", "공지를 찾을 수 없습니다."), + + // S3 관련 오류 S001, S002... + RESIZE_FAILED(HttpStatus.BAD_REQUEST, "S001", "파일 리사이징을 실패했습니다."), + EMPTY_FILE(HttpStatus.NO_CONTENT, "S002", "업로드 파일이 없습니다."), + UPLOAD_FAILED(HttpStatus.BAD_REQUEST, "S003", "업로드 중 오류가 발생했습니다."), + FILE_DELETE_FAILED(HttpStatus.BAD_REQUEST, "S004", "파일 삭제 중 오류가 발생했습니다."), + + // UserNotification 관련 오류 + USER_NOTIFICATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "USERNOTI001", "UserNotification을 찾을 수 없습니다."), + + // fcm 관련 오류 + FCM_UNSPECIFIED_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "FCM001", + "현재 알림을 보낼 수 없습니다. 다시 한번 시도하시고 안되면 관리자에게 문의 바랍니다."), + FCM_CERTIFICATION_IS_NOT_VALID(HttpStatus.SERVICE_UNAVAILABLE, "FCM002", + "FCM 계정을 확인해주세요 apns 혹은 web 인증서에 문제가 있습니다."), + FCM_INTERNAL_SERVER_ERROR(HttpStatus.SERVICE_UNAVAILABLE, "FCM003", + "현재 FCM 서버에 문제가 있습니다, 다시 한번 시도해보시고 계속 문제가 있으면 FCM 측에 문의를 해주세요"), + FCM_INVALID_ARGUMENT(HttpStatus.INTERNAL_SERVER_ERROR, "FCM004", + "어플리케이션 이용이 오래되어 토큰이 삭제되었습니다, 로그아웃 혹은 앱을 지웠다가 다시 접속해주세요."), + FCM_UNREGISTERED(HttpStatus.INTERNAL_SERVER_ERROR, "FCM005", + "어플리케이션 이용이 오래되어 토큰이 삭제되었습니다, 로그아웃 혹은 앱을 지웠다가 다시 접속해주세요"), + + // 강의(교육) 신청 관련 오류 + EDUCATION_APPLICATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "EDU001", "교육 신청서를 찾을 수 없습니다."), + EDUCATION_CLASS_GROUP_NOT_FOUND(HttpStatus.BAD_REQUEST, "EDU002", "교육 학급을 찾을 수 없습니다."), + EDUCATION_APPLICATION_DIFFERENT_PHONE(HttpStatus.UNAUTHORIZED, "EDU003", "휴대폰 번호가 동일하지 않습니다."), + + METHOD_ARGUMENT_NOT_VALID(HttpStatus.BAD_REQUEST, "C-0004", "요청 인자가 유효하지 않음"), + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "C-0002", "허용되지 않은 Request Method 호출"), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "C-0003", "내부 서버 오류"), + INVALID_TYPE_VALUE(HttpStatus.BAD_REQUEST, "C005", "유효하지 않은 값 타입"), + JSON_SYNTAX_ERROR(HttpStatus.BAD_REQUEST, "C-006", "json 형식이 잘못되었습니다."), + NOT_SUPPORTED_BODY_TYPE(HttpStatus.BAD_REQUEST, "C-007", "지원하지 않는 형식의 Request Body 형식입니다. json 형식으로 보내주세요"); + + private final HttpStatus httpStatus; + private final String code; + private final String message; } From 88002648dcc36109d8129d5e4e090eb49585d962 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 20:33:52 +0900 Subject: [PATCH 12/32] =?UTF-8?q?Feat:=20EducationApplication=20API?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9D=B8=EC=A6=9D=EB=90=9C=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=82=AC=EC=9A=A9=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20#124=20=EC=84=B8=EC=85=98=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=B4=20phoneNumber=EB=A5=BC=20?= =?UTF-8?q?=EC=96=BB=EC=96=B4=20EducationApplication=20API=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=A9=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/EducationApplicationApi.java | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/example/DoroServer/domain/educationApplication/api/EducationApplicationApi.java b/src/main/java/com/example/DoroServer/domain/educationApplication/api/EducationApplicationApi.java index 06c828e2..a9f2b303 100644 --- a/src/main/java/com/example/DoroServer/domain/educationApplication/api/EducationApplicationApi.java +++ b/src/main/java/com/example/DoroServer/domain/educationApplication/api/EducationApplicationApi.java @@ -2,10 +2,12 @@ import java.util.List; +import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -38,35 +40,46 @@ public class EducationApplicationApi { private final EducationApplicationService applicationService; private final ClassGroupService classGroupService; + @ModelAttribute("phoneNumber") + public String getPhoneNumber(HttpServletRequest request) { + // SessionFilter에서 넣어준 phoneNumber를 가져온다 + return (String) request.getAttribute("phoneNumber"); + } + /* Education Application */ @ApiOperation(value = "교육 신청 작성", notes = "교육을 신청합니다.") @PostMapping - public SuccessResponse create(@RequestBody @Valid EducationApplicationReq applicationReq) { - EducationApplicationRes savedApplication = applicationService.save(applicationReq); + public SuccessResponse create(@ModelAttribute("phoneNumber") String phoneNumber, + @RequestBody @Valid EducationApplicationReq applicationReq) { + EducationApplicationRes savedApplication = applicationService.save(applicationReq, phoneNumber); return SuccessResponse.successResponse(savedApplication); } @ApiOperation(value = "휴대폰 번호로 작성한 교육 신청 조회", notes = "신청한 교육을 핸드폰 번호를 통해 조회합니다.") @GetMapping - public SuccessResponse read(@RequestBody @Valid RetrieveApplicationReq retrieveApplicationReq) { + public SuccessResponse read(@ModelAttribute("phoneNumber") String phoneNumber, + @RequestBody @Valid RetrieveApplicationReq retrieveApplicationReq) { List applications = applicationService - .findByPhoneNumber(retrieveApplicationReq); + .findByPhoneNumber(retrieveApplicationReq, phoneNumber); return SuccessResponse.successResponse(applications); } @ApiOperation(value = "신청한 교육 수정", notes = "신청한 교육을 수정합니다.") @PutMapping("/{applicationId}") - public SuccessResponse update(@PathVariable Long applicationId, + public SuccessResponse update(@ModelAttribute("phoneNumber") String phoneNumber, + @PathVariable Long applicationId, @RequestBody @Valid EducationApplicationReq applicationReq) { - EducationApplicationRes updatedApplication = applicationService.update(applicationId, applicationReq); + EducationApplicationRes updatedApplication = applicationService.update(applicationId, applicationReq, + phoneNumber); return SuccessResponse.successResponse(updatedApplication); } @ApiOperation(value = "신청한 교육 삭제", notes = "신청한 교육을 삭제합니다. 학급정보도 같이 삭제됩니다.") @DeleteMapping("/{applicationId}") - public SuccessResponse delete(@PathVariable Long applicationId) { - applicationService.delete(applicationId); + public SuccessResponse delete(@ModelAttribute("phoneNumber") String phoneNumber, + @PathVariable Long applicationId) { + applicationService.delete(applicationId, phoneNumber); return SuccessResponse.successResponse("신청이 삭제되었습니다."); } @@ -74,25 +87,29 @@ public SuccessResponse delete(@PathVariable Long applicationId) { @ApiOperation(value = "신청한 교육에 학급정보 추가", notes = "우선 교육을 신청해야 합니다.") @PostMapping("/{applicationId}/classGroup") - public SuccessResponse addClassGroup(@PathVariable Long applicationId, + public SuccessResponse addClassGroup(@ModelAttribute("phoneNumber") String phoneNumber, + @PathVariable Long applicationId, @RequestBody @Valid ClassGroupReq classGroupReq) { - ClassGroupRes savedClassGroup = classGroupService.addClassGroupToApplication(applicationId, classGroupReq); + ClassGroupRes savedClassGroup = classGroupService.addClassGroupToApplication(applicationId, classGroupReq, + phoneNumber); return SuccessResponse.successResponse(savedClassGroup); } @ApiOperation(value = "학급정보 업데이트", notes = "우선 교육을 신청해야 합니다.") @PutMapping("/{applicationId}/classGroup/{classGroupId}") - public SuccessResponse updateClassGroup(@PathVariable Long applicationId, @PathVariable Long classGroupId, + public SuccessResponse updateClassGroup(@ModelAttribute("phoneNumber") String phoneNumber, + @PathVariable Long applicationId, @PathVariable Long classGroupId, @RequestBody @Valid ClassGroupReq classGroupReq) { ClassGroupRes updatedClassGroup = classGroupService.updateClassGroup(applicationId, classGroupId, - classGroupReq); + classGroupReq, phoneNumber); return SuccessResponse.successResponse(updatedClassGroup); } @ApiOperation(value = "학급정보 삭제", notes = "학급정보가 삭제됩니다.") @DeleteMapping("/{applicationId}/classGroup/{classGroupId}") - public SuccessResponse deleteClassGroup(@PathVariable Long applicationId, @PathVariable Long classGroupId) { - classGroupService.deleteClassGroup(applicationId, classGroupId); + public SuccessResponse deleteClassGroup(@ModelAttribute("phoneNumber") String phoneNumber, + @PathVariable Long applicationId, @PathVariable Long classGroupId) { + classGroupService.deleteClassGroup(applicationId, classGroupId, phoneNumber); return SuccessResponse.successResponse("학급정보가 삭제되었습니다."); } } \ No newline at end of file From e5c313d63744ba6dfb104ef8238255103fc56475 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 20:37:43 +0900 Subject: [PATCH 13/32] =?UTF-8?q?Feat:=20EducationApplication,=20ClassGrou?= =?UTF-8?q?p=20Service=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=20#124=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EB=90=9C=20=ED=95=B8=EB=93=9C=ED=8F=B0=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=EC=99=80=20=EA=B0=99=EC=9D=80=EC=A7=80=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20EducationApplicationService=20-=20Clas?= =?UTF-8?q?sGroupService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/EducationApplicationService.java | 34 ++++++++++++++----- .../service/ClassGroupService.java | 32 ++++++++++++----- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/example/DoroServer/domain/educationApplication/service/EducationApplicationService.java b/src/main/java/com/example/DoroServer/domain/educationApplication/service/EducationApplicationService.java index 0ed102f3..798047ba 100644 --- a/src/main/java/com/example/DoroServer/domain/educationApplication/service/EducationApplicationService.java +++ b/src/main/java/com/example/DoroServer/domain/educationApplication/service/EducationApplicationService.java @@ -25,7 +25,9 @@ public class EducationApplicationService { /* Education Application */ // create - public EducationApplicationRes save(EducationApplicationReq applicationReq) { + public EducationApplicationRes save(EducationApplicationReq applicationReq, String phoneNumber) { + throwExceptionIfNotEqual(applicationReq.getPhoneNumber(), phoneNumber); + EducationApplication educationApplication = mapper.toEntity(applicationReq); EducationApplication savedEducationApplication = repository.save(educationApplication); @@ -33,17 +35,21 @@ public EducationApplicationRes save(EducationApplicationReq applicationReq) { } // read - public List findByPhoneNumber(RetrieveApplicationReq retrieveApplicationReq) { - List educationApplications = repository - .findByPhoneNumber(retrieveApplicationReq.getPhoneNumber()); + public List findByPhoneNumber(RetrieveApplicationReq retrieveApplicationReq, + String phoneNumber) { + throwExceptionIfNotEqual(retrieveApplicationReq.getPhoneNumber(), phoneNumber); + + List educationApplications = repository.findByPhoneNumber(phoneNumber); return mapper.toDTO(educationApplications); } // update - public EducationApplicationRes update(Long id, EducationApplicationReq applicationReq) { - EducationApplication educationApplication = repository.findById(id) - .orElseThrow(() -> new BaseException(Code.EDUCATION_APPLICATION_NOT_FOUND)); + public EducationApplicationRes update(Long id, EducationApplicationReq applicationReq, String phoneNumber) { + throwExceptionIfNotEqual(applicationReq.getPhoneNumber(), phoneNumber); + + EducationApplication educationApplication = repository.findByIdAndPhoneNumber(id, phoneNumber).orElseThrow( + () -> new BaseException(Code.EDUCATION_APPLICATION_NOT_FOUND)); mapper.toEntity(applicationReq, educationApplication); EducationApplication updatedEducationApplication = repository.save(educationApplication); @@ -52,8 +58,18 @@ public EducationApplicationRes update(Long id, EducationApplicationReq applicati } // delete - public void delete(Long id) { - repository.deleteById(id); + public void delete(Long id, String phoneNumber) { + EducationApplication educationApplication = repository.findByIdAndPhoneNumber(id, phoneNumber).orElseThrow( + () -> new BaseException(Code.EDUCATION_APPLICATION_NOT_FOUND)); + + repository.delete(educationApplication); + } + + public static void throwExceptionIfNotEqual(String phoneNumber1, String phoneNumber2) { + // 두 번호가 같지 않으면 예외 발생 + if (!phoneNumber2.equals(phoneNumber1)) { + throw new BaseException(Code.EDUCATION_APPLICATION_DIFFERENT_PHONE); + } } } diff --git a/src/main/java/com/example/DoroServer/domain/educationApplicationClassGroup/service/ClassGroupService.java b/src/main/java/com/example/DoroServer/domain/educationApplicationClassGroup/service/ClassGroupService.java index 7d749d6b..caaca117 100644 --- a/src/main/java/com/example/DoroServer/domain/educationApplicationClassGroup/service/ClassGroupService.java +++ b/src/main/java/com/example/DoroServer/domain/educationApplicationClassGroup/service/ClassGroupService.java @@ -25,8 +25,10 @@ public class ClassGroupService { /* Class Group */ // create - public ClassGroupRes addClassGroupToApplication(Long applicationId, ClassGroupReq classGroupReq) { - EducationApplication educationApplication = educationApplicationRepository.findById(applicationId) + public ClassGroupRes addClassGroupToApplication(Long applicationId, ClassGroupReq classGroupReq, + String phoneNumber) { + EducationApplication educationApplication = educationApplicationRepository + .findByIdAndPhoneNumber(applicationId, phoneNumber) .orElseThrow(() -> new BaseException(Code.EDUCATION_APPLICATION_NOT_FOUND)); ClassGroup classGroup = mapper.toEntity(classGroupReq); @@ -37,21 +39,33 @@ public ClassGroupRes addClassGroupToApplication(Long applicationId, ClassGroupRe } // update - public ClassGroupRes updateClassGroup(Long id, Long classGroupId, ClassGroupReq classGroupReq) { - ClassGroup target = classGroupRepository.findById(classGroupId) + public ClassGroupRes updateClassGroup(Long applicationId, Long classGroupId, ClassGroupReq classGroupReq, + String phoneNumber) { + EducationApplication educationApplication = educationApplicationRepository + .findByIdAndPhoneNumber(applicationId, phoneNumber) + .orElseThrow(() -> new BaseException(Code.EDUCATION_APPLICATION_NOT_FOUND)); + + ClassGroup classGroup = educationApplication.getClassGroups().stream() + .filter(group -> group.getId().equals(classGroupId)) + .findFirst() .orElseThrow(() -> new BaseException(Code.EDUCATION_CLASS_GROUP_NOT_FOUND)); - mapper.toEntity(classGroupReq, target); - ClassGroup updatedClassGroup = classGroupRepository.save(target); + mapper.toEntity(classGroupReq, classGroup); + ClassGroup updatedClassGroup = classGroupRepository.save(classGroup); return mapper.toDTO(updatedClassGroup); } // delete - public void deleteClassGroup(Long id, Long classGroupId) { - ClassGroup classGroup = classGroupRepository.findById(classGroupId) + public void deleteClassGroup(Long applicationId, Long classGroupId, String phoneNumber) { + EducationApplication educationApplication = educationApplicationRepository + .findByIdAndPhoneNumber(applicationId, phoneNumber) + .orElseThrow(() -> new BaseException(Code.EDUCATION_APPLICATION_NOT_FOUND)); + + educationApplication.getClassGroups().stream() + .filter(group -> group.getId().equals(classGroupId)) + .findFirst() .orElseThrow(() -> new BaseException(Code.EDUCATION_CLASS_GROUP_NOT_FOUND)); - classGroup.getEducationApplication().getId().equals(id); classGroupRepository.deleteById(classGroupId); } From 8137f30bba69d9acdc3d3b0eb644f8b738fbde2b Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 20:38:50 +0900 Subject: [PATCH 14/32] =?UTF-8?q?Feat:=20EducationApplicationRepository=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20#124=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=EB=90=9C=20=ED=95=B8=EB=93=9C=ED=8F=B0=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EC=99=80=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EB=94=94=EB=A5=BC=20=ED=95=9C=EB=B2=88=EC=97=90=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=ED=95=98=EB=8A=94=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=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 --- .../repository/EducationApplicationRepository.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/DoroServer/domain/educationApplication/repository/EducationApplicationRepository.java b/src/main/java/com/example/DoroServer/domain/educationApplication/repository/EducationApplicationRepository.java index 2fae4bca..f290ae27 100644 --- a/src/main/java/com/example/DoroServer/domain/educationApplication/repository/EducationApplicationRepository.java +++ b/src/main/java/com/example/DoroServer/domain/educationApplication/repository/EducationApplicationRepository.java @@ -1,7 +1,7 @@ package com.example.DoroServer.domain.educationApplication.repository; import java.util.List; - +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import com.example.DoroServer.domain.educationApplication.entity.EducationApplication; @@ -10,4 +10,6 @@ public interface EducationApplicationRepository extends JpaRepository findByPhoneNumber(String phoneNumber); + Optional findByIdAndPhoneNumber(Long id, String phoneNumber); + } From 01fda0ace723425f792b5ebc028367a6fa980460 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 20:39:47 +0900 Subject: [PATCH 15/32] =?UTF-8?q?Test:=20SessionFilter=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20#12?= =?UTF-8?q?4=20-=20SessionFilterTest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/SessionFilterTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/test/java/com/example/DoroServer/domain/educationApplication/api/SessionFilterTest.java diff --git a/src/test/java/com/example/DoroServer/domain/educationApplication/api/SessionFilterTest.java b/src/test/java/com/example/DoroServer/domain/educationApplication/api/SessionFilterTest.java new file mode 100644 index 00000000..2b0e7cb3 --- /dev/null +++ b/src/test/java/com/example/DoroServer/domain/educationApplication/api/SessionFilterTest.java @@ -0,0 +1,86 @@ +package com.example.DoroServer.domain.educationApplication.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import com.example.DoroServer.global.exception.SessionIdException; +import com.example.DoroServer.global.jwt.RedisService; + +@ExtendWith(MockitoExtension.class) +public class SessionFilterTest { + + @Mock + private RedisService redisService; + + @InjectMocks + private SessionFilter sessionFilter; + + @Test + @DisplayName("doFilter 메서드 테스트 - 세션 ID가 유효한 경우") + public void testDoFilter_ValidSessionId_Success() throws Exception { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/education-application"); + request.addHeader("Session-Id", "valid-session-id"); + String phoneNumber = "01012345678"; // valid-session-id: 01012345678 + MockHttpServletResponse response = new MockHttpServletResponse(); + + given(redisService.getValues("valid-session-id")).willReturn(phoneNumber); + + // when + MockFilterChain filterChain = new MockFilterChain(); + sessionFilter.doFilter(request, response, filterChain); + + // then + verify(redisService, times(1)).getValues("valid-session-id"); + assertEquals(phoneNumber, request.getAttribute("phoneNumber")); + } + + @Test + @DisplayName("doFilter 메서드 테스트 - 세션 ID가 없이 요청한 경우") + public void testDoFilter_InvalidSessionId_ExceptionThrown() throws Exception { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/education-application"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when + MockFilterChain filterChain = new MockFilterChain(); + + // then + assertThrows(SessionIdException.class, () -> sessionFilter.doFilter(request, response, filterChain)); + } + + @Test + @DisplayName("doFilter 메서드 테스트 - 세션 ID가 유효하지 않은 경우") + public void testDoFilter_InvalidSessionId_ExceptionThrown2() throws Exception { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/education-application"); + request.addHeader("Session-Id", "invalid-session-id"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + given(redisService.getValues("invalid-session-id")).willReturn(null); + + // when + MockFilterChain filterChain = new MockFilterChain(); + + // then + assertThrows(SessionIdException.class, () -> sessionFilter.doFilter(request, response, filterChain)); + } +} From 6da4d0943213323970824756cdf028263b5eac9b Mon Sep 17 00:00:00 2001 From: hot666666 Date: Thu, 7 Dec 2023 20:41:45 +0900 Subject: [PATCH 16/32] =?UTF-8?q?Fix:=20=EA=B0=95=EC=9D=98(=EA=B5=90?= =?UTF-8?q?=EC=9C=A1)=20=EC=8B=A0=EC=B2=AD=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20#124=20=EA=B8=B0=EC=A1=B4=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=9D=B8=EC=A6=9D=EB=90=9C=20?= =?UTF-8?q?=ED=95=B8=EB=93=9C=ED=8F=B0=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20-=20EducationApplicationTestSetup=20-=20Ed?= =?UTF-8?q?ucationApplicationApiTest=20-=20EducationApplicationServiceTest?= =?UTF-8?q?=20-=20ClassGroupServiceTest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EducationApplicationTestSetup.java | 2 +- .../api/EducationApplicationApiTest.java | 372 ++++++++++-------- .../EducationApplicationServiceTest.java | 35 +- .../service/ClassGroupServiceTest.java | 179 +++++---- 4 files changed, 344 insertions(+), 244 deletions(-) diff --git a/src/test/java/com/example/DoroServer/domain/educationApplication/EducationApplicationTestSetup.java b/src/test/java/com/example/DoroServer/domain/educationApplication/EducationApplicationTestSetup.java index 42e55b39..473b2819 100644 --- a/src/test/java/com/example/DoroServer/domain/educationApplication/EducationApplicationTestSetup.java +++ b/src/test/java/com/example/DoroServer/domain/educationApplication/EducationApplicationTestSetup.java @@ -12,7 +12,7 @@ public class EducationApplicationTestSetup { public static Long ID = 1L; public static String NAME = "홍길동"; public static String UPDATED_NAME = "고길동"; - public static String PHONE_NUMBER = "010-1234-5678"; + public static String PHONE_NUMBER = "01012345678"; public static String INSTITUTION_NAME = "냉장고등학교"; public static String POSITION = "교장선생님"; public static String UPDATED_POSITION = "교감선생님"; diff --git a/src/test/java/com/example/DoroServer/domain/educationApplication/api/EducationApplicationApiTest.java b/src/test/java/com/example/DoroServer/domain/educationApplication/api/EducationApplicationApiTest.java index 8ebc148c..b6cb9211 100644 --- a/src/test/java/com/example/DoroServer/domain/educationApplication/api/EducationApplicationApiTest.java +++ b/src/test/java/com/example/DoroServer/domain/educationApplication/api/EducationApplicationApiTest.java @@ -1,5 +1,6 @@ package com.example.DoroServer.domain.educationApplication.api; +import static com.example.DoroServer.domain.educationApplication.EducationApplicationTestSetup.getEducationApplication; import static com.example.DoroServer.domain.educationApplication.EducationApplicationTestSetup.getEducationApplicationReq; import static com.example.DoroServer.domain.educationApplication.EducationApplicationTestSetup.getEducationApplicationRes; import static com.example.DoroServer.domain.educationApplication.EducationApplicationTestSetup.getRetrieveApplicationReq; @@ -10,6 +11,8 @@ import static com.example.DoroServer.domain.educationApplicationClassGroup.ClassGroupTestSetup.getUpdateClassGroupReq; import static com.example.DoroServer.domain.educationApplicationClassGroup.ClassGroupTestSetup.getUpdateClassGroupRes; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -23,9 +26,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -36,178 +38,208 @@ import com.example.DoroServer.domain.educationApplicationClassGroup.dto.ClassGroupReq; import com.example.DoroServer.domain.educationApplicationClassGroup.dto.ClassGroupRes; import com.example.DoroServer.domain.educationApplicationClassGroup.service.ClassGroupService; +import com.example.DoroServer.global.jwt.RedisService; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -@WebMvcTest(EducationApplicationApi.class) -@AutoConfigureMockMvc(addFilters = false) // SecurityConfig를 적용하지 않음 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc public class EducationApplicationApiTest { - @Autowired - private MockMvc mockMvc; - - @MockBean - private EducationApplicationService educationApplicationService; - - @MockBean - private ClassGroupService classGroupService; - - @MockBean - private JpaMetamodelMappingContext jpaMetamodelMappingContext; // JPA auditing을 위한 Bean - - private static ObjectMapper mapper; - - @BeforeAll - static void setUp() { - mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - } - - @Test - @DisplayName("MockMvc를 통한 교육 신청 작성 테스트") - public void createEducationApplicationTest() throws Exception { - // given - EducationApplicationReq applicationReq = getEducationApplicationReq(); - EducationApplicationRes educationApplicationRes = getEducationApplicationRes(); - - given(educationApplicationService.save(any(EducationApplicationReq.class))) - .willReturn(educationApplicationRes); - - // when & then - mockMvc.perform(post("/education-application") - .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(applicationReq))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.id").value(educationApplicationRes.getId())) - .andExpect(jsonPath("$.data.name").value(educationApplicationRes.getName())) - .andExpect(jsonPath("$.data.institutionName") - .value(educationApplicationRes.getInstitutionName())) - .andExpect(jsonPath("$.data.position").value(educationApplicationRes.getPosition())) - .andExpect(jsonPath("$.data.phoneNumber") - .value(educationApplicationRes.getPhoneNumber())) - .andExpect(jsonPath("$.data.email").value(educationApplicationRes.getEmail())) - .andExpect(jsonPath("$.data.numberOfStudents") - .value(educationApplicationRes.getNumberOfStudents())) - .andExpect(jsonPath("$.data.studentRank") - .value(educationApplicationRes.getStudentRank())) - .andExpect(jsonPath("$.data.budget").value(educationApplicationRes.getBudget())) - .andExpect(jsonPath("$.data.overallRemark") - .value(educationApplicationRes.getOverallRemark())); - } - - @Test - @DisplayName("MockMvc를 통한 신청한 교육 불러오기 테스트") - public void retrieveApplicationTest() throws Exception { - // given - RetrieveApplicationReq retrieveApplicationReq = getRetrieveApplicationReq(); - EducationApplicationRes educationApplicationRes = getEducationApplicationRes(); - - given(educationApplicationService.findByPhoneNumber(any(RetrieveApplicationReq.class))) - .willReturn(List.of(educationApplicationRes)); - - // when & then - mockMvc.perform(get("/education-application") - .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(retrieveApplicationReq))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data[0].id").value(educationApplicationRes.getId())) - .andExpect(jsonPath("$.data[0].name").value(educationApplicationRes.getName())) - .andExpect(jsonPath("$.data[0].institutionName") - .value(educationApplicationRes.getInstitutionName())) - .andExpect(jsonPath("$.data[0].position").value(educationApplicationRes.getPosition())) - .andExpect(jsonPath("$.data[0].phoneNumber") - .value(educationApplicationRes.getPhoneNumber())) - .andExpect(jsonPath("$.data[0].email").value(educationApplicationRes.getEmail())) - .andExpect(jsonPath("$.data[0].numberOfStudents") - .value(educationApplicationRes.getNumberOfStudents())) - .andExpect(jsonPath("$.data[0].studentRank") - .value(educationApplicationRes.getStudentRank())) - .andExpect(jsonPath("$.data[0].budget").value(educationApplicationRes.getBudget())) - .andExpect(jsonPath("$.data[0].overallRemark") - .value(educationApplicationRes.getOverallRemark())); - - } - - @Test - @DisplayName("MockMvc를 통한 신청한 교육 수정 테스트") - void updateEducationApplicationTest() throws Exception { - // given - EducationApplicationReq applicationReq = getUpdateEducationApplicationReq(); - EducationApplicationRes educationApplicationRes = getUpdateEducationApplicationRes(); - - given(educationApplicationService.update(any(Long.class), any(EducationApplicationReq.class))) - .willReturn(educationApplicationRes); - - // when & then - mockMvc.perform(put("/education-application/1") - .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(applicationReq))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.id").value(educationApplicationRes.getId())) - .andExpect(jsonPath("$.data.name").value(educationApplicationRes.getName())) - .andExpect(jsonPath("$.data.institutionName") - .value(educationApplicationRes.getInstitutionName())) - .andExpect(jsonPath("$.data.position").value(educationApplicationRes.getPosition())) - .andExpect(jsonPath("$.data.phoneNumber") - .value(educationApplicationRes.getPhoneNumber())) - .andExpect(jsonPath("$.data.email").value(educationApplicationRes.getEmail())) - .andExpect(jsonPath("$.data.numberOfStudents") - .value(educationApplicationRes.getNumberOfStudents())) - .andExpect(jsonPath("$.data.studentRank") - .value(educationApplicationRes.getStudentRank())) - .andExpect(jsonPath("$.data.budget").value(educationApplicationRes.getBudget())) - .andExpect(jsonPath("$.data.overallRemark") - .value(educationApplicationRes.getOverallRemark())); - } - - @Test - @DisplayName("MockMvc를 통한 신청한 교육에 학급정보 추가 테스트") - void addClassGroupToApplicationTest() throws Exception { - // given - ClassGroupReq classGroupReq = getClassGroupReq(); - ClassGroupRes classGroupRes = getClassGroupRes(); - - given(classGroupService.addClassGroupToApplication(any(Long.class), any(ClassGroupReq.class))) - .willReturn(classGroupRes); - - // when & then - mockMvc.perform(post("/education-application/1/classGroup") - .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(classGroupReq))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.id").value(classGroupRes.getId())) - .andExpect(jsonPath("$.data.className").value(classGroupRes.getClassName())) - .andExpect(jsonPath("$.data.educationConcept") - .value(classGroupRes.getEducationConcept())) - .andExpect(jsonPath("$.data.numberOfStudents") - .value(classGroupRes.getNumberOfStudents())) - .andExpect(jsonPath("$.data.remark").value(classGroupRes.getRemark())) - .andExpect(jsonPath("$.data.unfixed").value(classGroupRes.isUnfixed())); - } - - @Test - @DisplayName("MockMvc를 통한 신청한 교육에 학급정보 업데이트 테스트") - void updateClassGroupTest() throws Exception { - // given - ClassGroupReq classGroupReq = getUpdateClassGroupReq(); - ClassGroupRes classGroupRes = getUpdateClassGroupRes(); - - given(classGroupService.updateClassGroup(any(Long.class), any(Long.class), any(ClassGroupReq.class))) - .willReturn(classGroupRes); - - // when & then - mockMvc.perform(put("/education-application/1/classGroup/1") - .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(classGroupReq))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.id").value(classGroupRes.getId())) - .andExpect(jsonPath("$.data.className").value(classGroupRes.getClassName())) - .andExpect(jsonPath("$.data.educationConcept") - .value(classGroupRes.getEducationConcept())) - .andExpect(jsonPath("$.data.numberOfStudents") - .value(classGroupRes.getNumberOfStudents())) - .andExpect(jsonPath("$.data.remark").value(classGroupRes.getRemark())) - .andExpect(jsonPath("$.data.unfixed").value(classGroupRes.isUnfixed())); - } + @Autowired + private MockMvc mockMvc; + + @MockBean + private RedisService redisService; + + @MockBean + private EducationApplicationService educationApplicationService; + + @MockBean + private ClassGroupService classGroupService; + + private static ObjectMapper mapper; + + @BeforeAll + static void setUp() { + mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + } + + @Test + @DisplayName("MockMvc를 통한 교육 신청 작성 테스트") + public void createEducationApplicationTest() throws Exception { + // given + EducationApplicationReq applicationReq = getEducationApplicationReq(); + EducationApplicationRes educationApplicationRes = getEducationApplicationRes(); + String phoneNumber = applicationReq.getPhoneNumber(); + + given(redisService.getValues("some-session-id")).willReturn(phoneNumber); + given(educationApplicationService.save(any(EducationApplicationReq.class), anyString())) + .willReturn(educationApplicationRes); + + // when & then + mockMvc.perform(post("/education-application") + .contentType(MediaType.APPLICATION_JSON) + .header("Session-Id", "some-session-id") + .content(mapper.writeValueAsString(applicationReq))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(educationApplicationRes.getId())) + .andExpect(jsonPath("$.data.name").value(educationApplicationRes.getName())) + .andExpect(jsonPath("$.data.institutionName") + .value(educationApplicationRes.getInstitutionName())) + .andExpect(jsonPath("$.data.position").value(educationApplicationRes.getPosition())) + .andExpect(jsonPath("$.data.phoneNumber") + .value(educationApplicationRes.getPhoneNumber())) + .andExpect(jsonPath("$.data.email").value(educationApplicationRes.getEmail())) + .andExpect(jsonPath("$.data.numberOfStudents") + .value(educationApplicationRes.getNumberOfStudents())) + .andExpect(jsonPath("$.data.studentRank") + .value(educationApplicationRes.getStudentRank())) + .andExpect(jsonPath("$.data.budget").value(educationApplicationRes.getBudget())) + .andExpect(jsonPath("$.data.overallRemark") + .value(educationApplicationRes.getOverallRemark())); + } + + @Test + @DisplayName("MockMvc를 통한 신청한 교육 불러오기 테스트") + public void retrieveApplicationTest() throws Exception { + // given + RetrieveApplicationReq retrieveApplicationReq = getRetrieveApplicationReq(); + EducationApplicationRes educationApplicationRes = getEducationApplicationRes(); + String phoneNumber = retrieveApplicationReq.getPhoneNumber(); + + given(redisService.getValues("some-session-id")).willReturn(phoneNumber); + given(educationApplicationService.findByPhoneNumber(any(RetrieveApplicationReq.class), anyString())) + .willReturn(List.of(educationApplicationRes)); + + // when & then + mockMvc.perform(get("/education-application") + .contentType(MediaType.APPLICATION_JSON) + .header("Session-Id", "some-session-id") + .content(mapper.writeValueAsString(retrieveApplicationReq))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].id").value(educationApplicationRes.getId())) + .andExpect(jsonPath("$.data[0].name").value(educationApplicationRes.getName())) + .andExpect(jsonPath("$.data[0].institutionName") + .value(educationApplicationRes.getInstitutionName())) + .andExpect(jsonPath("$.data[0].position").value(educationApplicationRes.getPosition())) + .andExpect(jsonPath("$.data[0].phoneNumber") + .value(educationApplicationRes.getPhoneNumber())) + .andExpect(jsonPath("$.data[0].email").value(educationApplicationRes.getEmail())) + .andExpect(jsonPath("$.data[0].numberOfStudents") + .value(educationApplicationRes.getNumberOfStudents())) + .andExpect(jsonPath("$.data[0].studentRank") + .value(educationApplicationRes.getStudentRank())) + .andExpect(jsonPath("$.data[0].budget").value(educationApplicationRes.getBudget())) + .andExpect(jsonPath("$.data[0].overallRemark") + .value(educationApplicationRes.getOverallRemark())); + + } + + @Test + @DisplayName("MockMvc를 통한 신청한 교육 수정 테스트") + void updateEducationApplicationTest() throws Exception { + // given + EducationApplicationReq applicationReq = getUpdateEducationApplicationReq(); + EducationApplicationRes educationApplicationRes = getUpdateEducationApplicationRes(); + String phoneNumber = applicationReq.getPhoneNumber(); + + given(redisService.getValues("some-session-id")).willReturn(phoneNumber); + given(educationApplicationService.update(anyLong(), any(EducationApplicationReq.class), anyString())) + .willReturn(educationApplicationRes); + + // when & then + mockMvc.perform(put("/education-application/1") + .contentType(MediaType.APPLICATION_JSON) + .header("Session-Id", "some-session-id") + .content(mapper.writeValueAsString(applicationReq))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(educationApplicationRes.getId())) + .andExpect(jsonPath("$.data.name").value(educationApplicationRes.getName())) + .andExpect(jsonPath("$.data.institutionName") + .value(educationApplicationRes.getInstitutionName())) + .andExpect(jsonPath("$.data.position").value(educationApplicationRes.getPosition())) + .andExpect(jsonPath("$.data.phoneNumber") + .value(educationApplicationRes.getPhoneNumber())) + .andExpect(jsonPath("$.data.email").value(educationApplicationRes.getEmail())) + .andExpect(jsonPath("$.data.numberOfStudents") + .value(educationApplicationRes.getNumberOfStudents())) + .andExpect(jsonPath("$.data.studentRank") + .value(educationApplicationRes.getStudentRank())) + .andExpect(jsonPath("$.data.budget").value(educationApplicationRes.getBudget())) + .andExpect(jsonPath("$.data.overallRemark") + .value(educationApplicationRes.getOverallRemark())); + } + + @Test + @DisplayName("MockMvc를 통한 신청한 교육에 학급정보 추가 테스트") + void addClassGroupToApplicationTest() throws Exception { + // given + ClassGroupReq classGroupReq = getClassGroupReq(); + ClassGroupRes classGroupRes = getClassGroupRes(); + String phoneNumber = getEducationApplication().getPhoneNumber(); + + given(redisService.getValues("some-session-id")).willReturn(phoneNumber); + given(classGroupService.addClassGroupToApplication(any(Long.class), any(ClassGroupReq.class), anyString())) + .willReturn(classGroupRes); + + // when & then + mockMvc.perform(post("/education-application/1/classGroup") + .contentType(MediaType.APPLICATION_JSON) + .header("Session-Id", "some-session-id") + .content(mapper.writeValueAsString(classGroupReq))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(classGroupRes.getId())) + .andExpect(jsonPath("$.data.className").value(classGroupRes.getClassName())) + .andExpect(jsonPath("$.data.educationConcept") + .value(classGroupRes.getEducationConcept())) + .andExpect(jsonPath("$.data.numberOfStudents") + .value(classGroupRes.getNumberOfStudents())) + .andExpect(jsonPath("$.data.remark").value(classGroupRes.getRemark())) + .andExpect(jsonPath("$.data.unfixed").value(classGroupRes.isUnfixed())); + } + + @Test + @DisplayName("MockMvc를 통한 신청한 교육에 학급정보 업데이트 테스트") + void updateClassGroupTest() throws Exception { + // given + ClassGroupReq classGroupReq = getUpdateClassGroupReq(); + ClassGroupRes classGroupRes = getUpdateClassGroupRes(); + String phoneNumber = getEducationApplication().getPhoneNumber(); + + given(redisService.getValues("some-session-id")).willReturn(phoneNumber); + given(classGroupService.updateClassGroup(any(Long.class), any(Long.class), any(ClassGroupReq.class), + anyString())) + .willReturn(classGroupRes); + + // when & then + mockMvc.perform(put("/education-application/1/classGroup/1") + .contentType(MediaType.APPLICATION_JSON) + .header("Session-Id", "some-session-id") + .content(mapper.writeValueAsString(classGroupReq))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(classGroupRes.getId())) + .andExpect(jsonPath("$.data.className").value(classGroupRes.getClassName())) + .andExpect(jsonPath("$.data.educationConcept") + .value(classGroupRes.getEducationConcept())) + .andExpect(jsonPath("$.data.numberOfStudents") + .value(classGroupRes.getNumberOfStudents())) + .andExpect(jsonPath("$.data.remark").value(classGroupRes.getRemark())) + .andExpect(jsonPath("$.data.unfixed").value(classGroupRes.isUnfixed())); + } + + @Test + @DisplayName("MockMvc를 통한 교육 신청서 작성 세션헤더 없이 접근 테스트") + public void createEducationApplicationWithoutAuthenticationTest() throws Exception { + // given + EducationApplicationReq applicationReq = getEducationApplicationReq(); + + // when & then + mockMvc.perform(post("/education-application") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(applicationReq))) + .andExpect(status().isUnauthorized()); + } } diff --git a/src/test/java/com/example/DoroServer/domain/educationApplication/service/EducationApplicationServiceTest.java b/src/test/java/com/example/DoroServer/domain/educationApplication/service/EducationApplicationServiceTest.java index c836af45..6b43dc28 100644 --- a/src/test/java/com/example/DoroServer/domain/educationApplication/service/EducationApplicationServiceTest.java +++ b/src/test/java/com/example/DoroServer/domain/educationApplication/service/EducationApplicationServiceTest.java @@ -1,6 +1,8 @@ package com.example.DoroServer.domain.educationApplication.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -19,6 +21,7 @@ import com.example.DoroServer.domain.educationApplication.dto.RetrieveApplicationReq; import com.example.DoroServer.domain.educationApplication.entity.EducationApplication; import com.example.DoroServer.domain.educationApplication.repository.EducationApplicationRepository; +import com.example.DoroServer.global.exception.BaseException; import static com.example.DoroServer.domain.educationApplication.EducationApplicationTestSetup.getEducationApplication; import static com.example.DoroServer.domain.educationApplication.EducationApplicationTestSetup.getEducationApplicationReq; @@ -49,13 +52,14 @@ void createEducationApplicationTest() { EducationApplicationReq applicationReq = getEducationApplicationReq(); EducationApplication educationApplication = getEducationApplication(); EducationApplicationRes applicationRes = getEducationApplicationRes(); + String phoneNumber = applicationReq.getPhoneNumber(); given(mapper.toEntity(applicationReq)).willReturn(educationApplication); given(educationApplicationRepository.save(educationApplication)).willReturn(educationApplication); given(mapper.toDTO(educationApplication)).willReturn(applicationRes); // when - EducationApplicationRes result = educationApplicationService.save(applicationReq); + EducationApplicationRes result = educationApplicationService.save(applicationReq, phoneNumber); // then verify(mapper, times(1)).toEntity(applicationReq); @@ -64,6 +68,17 @@ void createEducationApplicationTest() { assertThat(result.getId()).isEqualTo(ID); } + @DisplayName("다른번호로 교육 신청 생성 테스트 - 예외 발생") + @Test + void createEducationApplicationWithDifferentPhoneNumberTest() { + // given + EducationApplicationReq applicationReq = getEducationApplicationReq(); + String phoneNumber = "01087654321"; + + // when & then + assertThrows(BaseException.class, () -> educationApplicationService.save(applicationReq, phoneNumber)); + } + @DisplayName("교육 신청 조회 테스트") @Test void retrieveEducationApplicationTest() { @@ -72,17 +87,21 @@ void retrieveEducationApplicationTest() { EducationApplication educationApplication = getEducationApplication(); EducationApplicationRes applicationRes = getEducationApplicationRes(); List applicationResList = List.of(applicationRes); + String phoneNumber = retrieveApplicationReq.getPhoneNumber(); + given(educationApplicationRepository.findByPhoneNumber(educationApplication.getPhoneNumber())) .willReturn(List.of(educationApplication)); given(mapper.toDTO(List.of(educationApplication))).willReturn(applicationResList); // when - List result = educationApplicationService - .findByPhoneNumber(retrieveApplicationReq); + List result = educationApplicationService.findByPhoneNumber( + retrieveApplicationReq, + phoneNumber); // then verify(mapper, times(1)).toDTO(List.of(educationApplication)); - verify(educationApplicationRepository, times(1)).findByPhoneNumber(educationApplication.getPhoneNumber()); + verify(educationApplicationRepository, times(1)) + .findByPhoneNumber(educationApplication.getPhoneNumber()); verify(mapper, times(1)).toDTO(List.of(educationApplication)); assertThat(result.get(0).getId()).isEqualTo(ID); @@ -95,8 +114,9 @@ void updateEducationApplicationTest() { EducationApplicationReq applicationReq = getUpdateEducationApplicationReq(); EducationApplication educationApplication = getEducationApplication(); EducationApplicationRes applicationRes = getUpdateEducationApplicationRes(); + String phoneNumber = applicationReq.getPhoneNumber(); - given(educationApplicationRepository.findById(educationApplication.getId())) + given(educationApplicationRepository.findByIdAndPhoneNumber(educationApplication.getId(), phoneNumber)) .willReturn(java.util.Optional.of(educationApplication)); given(mapper.toEntity(applicationReq, educationApplication)).willReturn(educationApplication); given(educationApplicationRepository.save(educationApplication)).willReturn(educationApplication); @@ -104,10 +124,11 @@ void updateEducationApplicationTest() { // when EducationApplicationRes result = educationApplicationService.update(educationApplication.getId(), - applicationReq); + applicationReq, phoneNumber); // then - verify(educationApplicationRepository, times(1)).findById(educationApplication.getId()); + verify(educationApplicationRepository, times(1)).findByIdAndPhoneNumber(educationApplication.getId(), + phoneNumber); verify(mapper, times(1)).toEntity(applicationReq, educationApplication); verify(educationApplicationRepository, times(1)).save(educationApplication); verify(mapper, times(1)).toDTO(educationApplication); diff --git a/src/test/java/com/example/DoroServer/domain/educationApplicationClassGroup/service/ClassGroupServiceTest.java b/src/test/java/com/example/DoroServer/domain/educationApplicationClassGroup/service/ClassGroupServiceTest.java index c331f771..54d870bb 100644 --- a/src/test/java/com/example/DoroServer/domain/educationApplicationClassGroup/service/ClassGroupServiceTest.java +++ b/src/test/java/com/example/DoroServer/domain/educationApplicationClassGroup/service/ClassGroupServiceTest.java @@ -3,9 +3,11 @@ import static com.example.DoroServer.domain.educationApplicationClassGroup.ClassGroupTestSetup.getClassGroup; import static com.example.DoroServer.domain.educationApplicationClassGroup.ClassGroupTestSetup.getClassGroupReq; import static com.example.DoroServer.domain.educationApplicationClassGroup.ClassGroupTestSetup.getClassGroupRes; +import static com.example.DoroServer.domain.educationApplicationClassGroup.ClassGroupTestSetup.getUpdateClassGroup; import static com.example.DoroServer.domain.educationApplicationClassGroup.ClassGroupTestSetup.getUpdateClassGroupReq; import static com.example.DoroServer.domain.educationApplicationClassGroup.ClassGroupTestSetup.getUpdateClassGroupRes; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -28,75 +30,120 @@ import com.example.DoroServer.domain.educationApplicationClassGroup.dto.ClassGroupRes; import com.example.DoroServer.domain.educationApplicationClassGroup.entity.ClassGroup; import com.example.DoroServer.domain.educationApplicationClassGroup.repository.ClassGroupRepository; +import com.example.DoroServer.global.exception.BaseException; @ExtendWith(MockitoExtension.class) public class ClassGroupServiceTest { - @InjectMocks - private ClassGroupService classGroupService; - - @Mock - private EducationApplicationRepository educationApplicationRepository; - - @Mock - private ClassGroupRepository classGroupRepository; - - @Mock - private ClassGroupMapper mapper; - - @Test - @DisplayName("교육 신청서에 학급 정보 추가 테스트") - void testAddClassGroup() { - // given - EducationApplication educationApplication = getEducationApplication(); - ClassGroupReq classGroupReq = getClassGroupReq(); - ClassGroup classGroup = getClassGroup(); - ClassGroupRes classGroupRes = getClassGroupRes(); - - given(educationApplicationRepository.findById(educationApplication.getId())) - .willReturn(Optional.of(educationApplication)); - given(mapper.toEntity(classGroupReq)).willReturn(classGroup); - given(classGroupRepository.save(classGroup)).willReturn(classGroup); - given(mapper.toDTO(classGroup)).willReturn(classGroupRes); - - // when - ClassGroupRes savedClassGroup = classGroupService.addClassGroupToApplication( - educationApplication.getId(), - classGroupReq); - - // then - verify(educationApplicationRepository, times(1)).findById(educationApplication.getId()); - verify(mapper, times(1)).toEntity(classGroupReq); - verify(classGroupRepository, times(1)).save(classGroup); - verify(mapper, times(1)).toDTO(classGroup); - assertThat(savedClassGroup.getId()).isEqualTo(1L); - } - - @Test - @DisplayName("교육 신청서에 학급 정보 수정 테스트") - void testUpdateClassGroup() { - // given - EducationApplication educationApplication = getEducationApplication(); - ClassGroupReq classGroupReq = getUpdateClassGroupReq(); - ClassGroup classGroup = getClassGroup(); - ClassGroupRes classGroupRes = getUpdateClassGroupRes(); - - given(classGroupRepository.findById(classGroup.getId())).willReturn(Optional.of(classGroup)); - given(mapper.toEntity(classGroupReq, classGroup)).willReturn(classGroup); - given(classGroupRepository.save(classGroup)).willReturn(classGroup); - given(mapper.toDTO(classGroup)).willReturn(classGroupRes); - - // when - ClassGroupRes updatedClassGroup = classGroupService.updateClassGroup(educationApplication.getId(), - classGroup.getId(), classGroupReq); - - // then - verify(classGroupRepository, times(1)).findById(classGroup.getId()); - verify(mapper, times(1)).toEntity(classGroupReq, classGroup); - verify(classGroupRepository, times(1)).save(classGroup); - verify(mapper, times(1)).toDTO(classGroup); - - assertThat(updatedClassGroup.getNumberOfStudents()).isEqualTo(classGroupRes.getNumberOfStudents()); - } + @InjectMocks + private ClassGroupService classGroupService; + + @Mock + private EducationApplicationRepository educationApplicationRepository; + + @Mock + private ClassGroupRepository classGroupRepository; + + @Mock + private ClassGroupMapper mapper; + + @Test + @DisplayName("교육 신청서에 학급 정보 추가 테스트") + void addClassGroupToApplicationTest() { + // given + EducationApplication educationApplication = getEducationApplication(); + ClassGroupReq classGroupReq = getClassGroupReq(); + ClassGroup classGroup = getClassGroup(); + ClassGroup savedClassGroup = getClassGroup(); + ClassGroupRes classGroupRes = getClassGroupRes(); + Long applicationId = educationApplication.getId(); + String phoneNumber = educationApplication.getPhoneNumber(); + + given(educationApplicationRepository.findByIdAndPhoneNumber(applicationId, phoneNumber)) + .willReturn(Optional.of(educationApplication)); + given(mapper.toEntity(classGroupReq)).willReturn(classGroup); + given(classGroupRepository.save(classGroup)).willReturn(savedClassGroup); + given(mapper.toDTO(savedClassGroup)).willReturn(classGroupRes); + + // when + ClassGroupRes result = classGroupService.addClassGroupToApplication(applicationId, classGroupReq, phoneNumber); + + // then + assertThat(result).isEqualTo(classGroupRes); + verify(educationApplicationRepository, times(1)).findByIdAndPhoneNumber(applicationId, phoneNumber); + verify(mapper, times(1)).toEntity(classGroupReq); + verify(classGroupRepository, times(1)).save(classGroup); + verify(mapper, times(1)).toDTO(savedClassGroup); + } + + @Test + @DisplayName("교육 신청서에 학급 정보 추가 테스트") + void updateClassGroupToApplicationTest() { + // given + EducationApplication educationApplication = getEducationApplication(); + ClassGroupReq updateClassGroupReq = getUpdateClassGroupReq(); + ClassGroup classGroup = getClassGroup(); + ClassGroup updateClassGroup = getUpdateClassGroup(); + ClassGroupRes classGroupRes = getUpdateClassGroupRes(); + Long applicationId = educationApplication.getId(); + Long classGroupId = classGroup.getId(); + String phoneNumber = educationApplication.getPhoneNumber(); + + educationApplication.getClassGroups().add(classGroup); + given(educationApplicationRepository.findByIdAndPhoneNumber(applicationId, phoneNumber)) + .willReturn(Optional.of(educationApplication)); + given(mapper.toEntity(updateClassGroupReq, classGroup)).willReturn(updateClassGroup); + given(classGroupRepository.save(classGroup)).willReturn(classGroup); + given(mapper.toDTO(classGroup)).willReturn(classGroupRes); + + // when + ClassGroupRes result = classGroupService.updateClassGroup(applicationId, classGroupId, updateClassGroupReq, + phoneNumber); + + // then + assertThat(result).isEqualTo(classGroupRes); + verify(educationApplicationRepository, times(1)).findByIdAndPhoneNumber(applicationId, phoneNumber); + verify(mapper, times(1)).toEntity(updateClassGroupReq, classGroup); + verify(classGroupRepository, times(1)).save(classGroup); + verify(mapper, times(1)).toDTO(classGroup); + } + + @Test + @DisplayName("교육 신청서에 학급 정보 업데이트 실패 테스트 - 교육 신청서가 존재하지 않음") + void updateClassGroupErrorTest1() { + // given + EducationApplication educationApplication = getEducationApplication(); + ClassGroupReq updateClassGroupReq = getUpdateClassGroupReq(); + ClassGroup classGroup = getClassGroup(); + Long applicationId = educationApplication.getId(); + Long classGroupId = classGroup.getId(); + String phoneNumber = educationApplication.getPhoneNumber(); + + given(educationApplicationRepository.findByIdAndPhoneNumber(applicationId, phoneNumber)) + .willReturn(Optional.of(educationApplication)); + + // when & then + assertThrows(BaseException.class, () -> classGroupService.updateClassGroup(applicationId, classGroupId, + updateClassGroupReq, phoneNumber)); + verify(educationApplicationRepository, times(1)).findByIdAndPhoneNumber(applicationId, phoneNumber); + + } + + @Test + @DisplayName("교육 신청서에 학급 정보 업데이트 실패 테스트 - 핸드폰 번호가 일치하지 않음") + void updateClassGroupErrorTest2() { + // given + EducationApplication educationApplication = getEducationApplication(); + ClassGroupReq updateClassGroupReq = getUpdateClassGroupReq(); + ClassGroup classGroup = getClassGroup(); + Long applicationId = educationApplication.getId(); + Long classGroupId = classGroup.getId(); + String phoneNumber = "01011112222"; // 01012345678 + + // when & then + assertThrows(BaseException.class, () -> classGroupService.updateClassGroup(applicationId, classGroupId, + updateClassGroupReq, phoneNumber)); + + } } From d8a58317e6784ec4554698bd49a40604b3d6fd83 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Fri, 8 Dec 2023 18:07:50 +0900 Subject: [PATCH 17/32] =?UTF-8?q?Style:=20=EA=B8=B0=EB=B3=B8=EA=B0=92=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=ED=95=84=EB=93=9C=EC=97=90=20Builder.Defa?= =?UTF-8?q?ult=20=EC=B6=94=EA=B0=80=20-=20CreateLectureReq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DoroServer/domain/lecture/dto/CreateLectureReq.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/DoroServer/domain/lecture/dto/CreateLectureReq.java b/src/main/java/com/example/DoroServer/domain/lecture/dto/CreateLectureReq.java index e612db9b..741a2255 100644 --- a/src/main/java/com/example/DoroServer/domain/lecture/dto/CreateLectureReq.java +++ b/src/main/java/com/example/DoroServer/domain/lecture/dto/CreateLectureReq.java @@ -50,7 +50,7 @@ public class CreateLectureReq { @NotBlank private String staff; // 강의 스태프 수 @NotBlank - private String mainPayment; //강사 급여 + private String mainPayment; // 강사 급여 @NotBlank private String subPayment; @NotBlank @@ -61,6 +61,8 @@ public class CreateLectureReq { private String time; // 시간 @NotBlank private String remark; + + @Builder.Default @NotNull private List lectureDates = new ArrayList<>(); // 강의 날짜 @NotNull From 2fc2ffb34e2852f915017a9284442e247500f0a6 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sat, 9 Dec 2023 19:32:21 +0900 Subject: [PATCH 18/32] =?UTF-8?q?Feat:=20=EA=B0=95=EC=9D=98=20=EC=BB=A8?= =?UTF-8?q?=ED=85=90=EC=B8=A0=EC=9D=98=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=A4=EB=A5=98=20Code=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#120=20LectureContentApi=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EC=8B=9C=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20Code=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/DoroServer/global/exception/Code.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/example/DoroServer/global/exception/Code.java b/src/main/java/com/example/DoroServer/global/exception/Code.java index a1cd77d5..26b3630b 100644 --- a/src/main/java/com/example/DoroServer/global/exception/Code.java +++ b/src/main/java/com/example/DoroServer/global/exception/Code.java @@ -40,6 +40,9 @@ public enum Code { // 강의 관련 오류 L001,L002.. LECTURE_NOT_FOUND(HttpStatus.BAD_REQUEST, "LEC001", "강의가 존재하지 않습니다."), LECTURE_CONTENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "LEC002", "강의 컨텐츠가 존재하지 않습니다."), + LECTURE_CONTENT_IMAGE_SIZE_OVER(HttpStatus.BAD_REQUEST, "LEC003", "이미지 사이즈가 10MB를 초과했습니다."), + LECTURE_CONTENT_IMAGE_COUNT_OVER(HttpStatus.BAD_REQUEST, "LEC004", "이미지는 100장 이하로 업로드해주세요."), + LECTURE_CONTENT_IMAGE_NOT_FOUND(HttpStatus.BAD_REQUEST, "LEC005", "이미지가 존재하지 않습니다."), // 신청 강사 관련 오류 TUTOR_NOT_FOUND(HttpStatus.BAD_REQUEST, "TUTOR001", "신청하지 않은 강사입니다."), From f48afc43cf43f7924bcf60942458431af63aed04 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sat, 9 Dec 2023 19:34:37 +0900 Subject: [PATCH 19/32] =?UTF-8?q?Style:=20CreateLectureReq=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B4=80=EB=A0=A8=20=EC=97=86=EB=8A=94=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20import=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/DoroServer/domain/lecture/dto/CreateLectureReq.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/example/DoroServer/domain/lecture/dto/CreateLectureReq.java b/src/main/java/com/example/DoroServer/domain/lecture/dto/CreateLectureReq.java index 741a2255..ac05ffd4 100644 --- a/src/main/java/com/example/DoroServer/domain/lecture/dto/CreateLectureReq.java +++ b/src/main/java/com/example/DoroServer/domain/lecture/dto/CreateLectureReq.java @@ -3,7 +3,6 @@ import com.example.DoroServer.domain.lecture.entity.Lecture; import com.example.DoroServer.domain.lecture.entity.LectureDate; import com.example.DoroServer.domain.lecture.entity.LectureStatus; -import com.example.DoroServer.domain.lectureContent.dto.LectureContentDto; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; From 097f73a647be929a0166240ead95b61ebd731f6c Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sat, 9 Dec 2023 19:37:55 +0900 Subject: [PATCH 20/32] =?UTF-8?q?Feat:=20=EA=B0=95=EC=9D=98=20=EC=BB=A8?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EA=B4=80=EB=A0=A8=20DTO=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20#120=20-=20LectureContentDtoReq=20=20=20-=20CreateL?= =?UTF-8?q?ectureContentReq=20=20=20-=20UpdateLectureContentReq=20=20=20-?= =?UTF-8?q?=20LectureContentRes=20-=20LectureContentImageDto=20=20=20-=20L?= =?UTF-8?q?ectureContentImageReq=20=20=20-=20LectureContentImageRes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/CreateLectureContentReq.java | 36 +++++++++++++++++++ .../lectureContent/dto/LectureContentDto.java | 15 +++----- .../lectureContent/dto/LectureContentRes.java | 22 ++++++++++++ .../dto/UpdateLectureContentReq.java | 8 +++-- .../dto/LectureContentImageDto.java | 17 +++++++++ .../dto/LectureContentImageReq.java | 18 ++++++++++ .../dto/LectureContentImageRes.java | 13 +++++++ 7 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/example/DoroServer/domain/lectureContent/dto/CreateLectureContentReq.java create mode 100644 src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentRes.java create mode 100644 src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageDto.java create mode 100644 src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java create mode 100644 src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageRes.java diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/dto/CreateLectureContentReq.java b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/CreateLectureContentReq.java new file mode 100644 index 00000000..f52c1b7a --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/CreateLectureContentReq.java @@ -0,0 +1,36 @@ +package com.example.DoroServer.domain.lectureContent.dto; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import org.springframework.web.multipart.MultipartFile; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +@Setter +public class CreateLectureContentReq { + + @NotBlank + private String kit; // 강의 사용 키트 + + @NotBlank + private String detail; // 강의 세부 구성 + + @NotNull + private String requirement; // 강의 자격 요건 + + @NotNull + private String content; // 강의 컨텐츠 + + @NotNull + private MultipartFile[] files; // 강의 컨텐츠 이미지 파일들 + +} diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentDto.java b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentDto.java index 71a4ceb3..2fa0b267 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentDto.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentDto.java @@ -1,12 +1,12 @@ package com.example.DoroServer.domain.lectureContent.dto; -import com.example.DoroServer.domain.lecture.entity.Lecture; -import com.example.DoroServer.domain.lectureContent.entity.LectureContent; +import java.util.List; import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; +import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageDto; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -28,16 +28,11 @@ public class LectureContentDto { @NotBlank private String detail; // 강의 세부 구성 - @NotNull private String requirement; // 강의 자격 요건 @NotNull - private String content; - - - - - + private String content; // 강의 내용 + private List images; // 강의 자료 이미지 } diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentRes.java b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentRes.java new file mode 100644 index 00000000..2d77e01c --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentRes.java @@ -0,0 +1,22 @@ +package com.example.DoroServer.domain.lectureContent.dto; + +import java.util.List; + +import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageRes; + +import lombok.Builder; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@Builder +@AllArgsConstructor +public class LectureContentRes { + + private Long id; // PK + private String kit; // 강의 사용 키트 + private String detail; // 강의 세부 구성 + private String requirement; // 강의 자격 요건 + private String content; // 강의 컨텐츠 + private List images; // 강의 컨텐츠 이미지 파일들 +} diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/dto/UpdateLectureContentReq.java b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/UpdateLectureContentReq.java index b0b59a82..a1c94e5a 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/dto/UpdateLectureContentReq.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/UpdateLectureContentReq.java @@ -1,15 +1,16 @@ package com.example.DoroServer.domain.lectureContent.dto; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Getter -@Builder @NoArgsConstructor +@Builder @AllArgsConstructor +@Setter public class UpdateLectureContentReq { private String kit; // 강의 사용 키트 @@ -17,5 +18,6 @@ public class UpdateLectureContentReq { private String remark; // 강의 기타 사항 - private String content; + private String content; // 강의 컨텐츠 + } diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageDto.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageDto.java new file mode 100644 index 00000000..c2bbad87 --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageDto.java @@ -0,0 +1,17 @@ +package com.example.DoroServer.domain.lectureContentImage.dto; + +import javax.validation.constraints.NotNull; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@NoArgsConstructor +@Setter +public class LectureContentImageDto { + private Long id; + + @NotNull + private String url; +} diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java new file mode 100644 index 00000000..46ed3f54 --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java @@ -0,0 +1,18 @@ +package com.example.DoroServer.domain.lectureContentImage.dto; + +import javax.validation.constraints.NotNull; + +import org.springframework.web.multipart.MultipartFile; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@NoArgsConstructor +@Setter +public class LectureContentImageReq { + @NotNull + private MultipartFile[] files; // 강의 컨텐츠 이미지 파일들 -> 하나만 보내면 무시됨 + +} diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageRes.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageRes.java new file mode 100644 index 00000000..6ed5519e --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageRes.java @@ -0,0 +1,13 @@ +package com.example.DoroServer.domain.lectureContentImage.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@NoArgsConstructor +@Setter +public class LectureContentImageRes { + private Long id; // PK + private String url; // uploaded image url +} From 4942ff0c584280e21cb60ca00622edba5fff28b2 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sat, 9 Dec 2023 19:42:24 +0900 Subject: [PATCH 21/32] =?UTF-8?q?Feat:=20=EA=B0=95=EC=9D=98=20=EC=BB=A8?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EA=B4=80=EB=A0=A8=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=9E=91=EC=84=B1=20#120=20=EC=9D=B4=EC=A0=84=20Le?= =?UTF-8?q?ctureContent=EC=97=90=EC=84=9C=20images=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80,=20=EC=9D=B4=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20LectureContentImage=EC=99=80=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=20-=20LectureContent(One-To-Many)=20-=20LectureConten?= =?UTF-8?q?tImage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lectureContent/entity/LectureContent.java | 18 +++++++++++-- .../entity/LectureContentImage.java | 25 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/DoroServer/domain/lectureContentImage/entity/LectureContentImage.java diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/entity/LectureContent.java b/src/main/java/com/example/DoroServer/domain/lectureContent/entity/LectureContent.java index becd08ae..d1ff8017 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/entity/LectureContent.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/entity/LectureContent.java @@ -3,11 +3,17 @@ import javax.persistence.GenerationType; import lombok.*; +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; -import javax.validation.constraints.NotBlank; +import javax.persistence.OneToMany; + +import com.example.DoroServer.domain.lectureContentImage.entity.LectureContentImage; @Entity @Getter @@ -27,5 +33,13 @@ public class LectureContent { private String requirement; // 강의 자격 요건 - private String content; + private String content; // 강의 컨텐츠 + + // == 연관관계 매핑 ==// + + // LectureContent과 LectureContentImage는 일대다(One-to-Many) 관계 + @Builder.Default + @OneToMany(cascade = CascadeType.ALL) + private List images = new ArrayList<>(); + } diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/entity/LectureContentImage.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/entity/LectureContentImage.java new file mode 100644 index 00000000..64f6aeab --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/entity/LectureContentImage.java @@ -0,0 +1,25 @@ +package com.example.DoroServer.domain.lectureContentImage.entity; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LectureContentImage { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String url; + +} From 5dda67f0fe9c1d129e45ee3bce0daa02390d131f Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sat, 9 Dec 2023 19:42:43 +0900 Subject: [PATCH 22/32] =?UTF-8?q?Feat:=20=EA=B0=95=EC=9D=98=20=EC=BB=A8?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EA=B4=80=EB=A0=A8=20Mapper=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20#120=20-=20LectureContentMapper=20-=20LectureConten?= =?UTF-8?q?tImageMapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lectureContent/dto/LectureContentMapper.java | 12 ++++++++++-- .../dto/LectureContentImageMapper.java | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageMapper.java diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentMapper.java b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentMapper.java index 22e990b8..26240d23 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentMapper.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/LectureContentMapper.java @@ -1,11 +1,19 @@ package com.example.DoroServer.domain.lectureContent.dto; import com.example.DoroServer.domain.lectureContent.entity.LectureContent; + +import java.util.List; + import org.mapstruct.Mapper; @Mapper(componentModel = "spring") public interface LectureContentMapper { - //LectureContent -> LectureContentDto - LectureContentDto toLectureContentDto(LectureContent lectureContent); LectureContent toLectureContent(LectureContentDto lectureContentDto); + + LectureContentDto toLectureContentDto(LectureContent lectureContent); + + LectureContentRes toLectureContentRes(LectureContent lectureContent); + + List toLectureContentResList(List lectureContentList); + } diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageMapper.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageMapper.java new file mode 100644 index 00000000..8c9fbff5 --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageMapper.java @@ -0,0 +1,15 @@ +package com.example.DoroServer.domain.lectureContentImage.dto; + +import java.util.List; + +import org.mapstruct.Mapper; + +import com.example.DoroServer.domain.lectureContentImage.entity.LectureContentImage; + +@Mapper(componentModel = "spring") +public interface LectureContentImageMapper { + + LectureContentImageRes toLectureContentImageRes(LectureContentImage lectureContentImage); + + List toLectureContentImageResList(List lectureContentImageList); +} From 19e00ce9c08e9742d32e4ea59cd54556abc298a7 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sat, 9 Dec 2023 19:45:02 +0900 Subject: [PATCH 23/32] =?UTF-8?q?Feat:=20=EA=B0=95=EC=9D=98=20=EC=BB=A8?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=A0=88?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=84=B0=EB=A6=AC=20=EC=B6=94=EA=B0=80=20#12?= =?UTF-8?q?0=20-=20LectureContentImageRepository?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/LectureContentImageRepository.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/com/example/DoroServer/domain/lectureContentImage/repository/LectureContentImageRepository.java diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/repository/LectureContentImageRepository.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/repository/LectureContentImageRepository.java new file mode 100644 index 00000000..30a15fd5 --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/repository/LectureContentImageRepository.java @@ -0,0 +1,8 @@ +package com.example.DoroServer.domain.lectureContentImage.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.example.DoroServer.domain.lectureContentImage.entity.LectureContentImage; + +public interface LectureContentImageRepository extends JpaRepository { +} \ No newline at end of file From 6d7291c5a2edcfea5c30bdb042d0d07b724c7484 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sat, 9 Dec 2023 19:47:34 +0900 Subject: [PATCH 24/32] =?UTF-8?q?Feat:=20=EA=B0=95=EC=9D=98=20=EC=BB=A8?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B0=8F=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80=20#120=20-=20LectureContentService,=20?= =?UTF-8?q?LectureContentImageService=20-=20LectureContentApi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lectureContent/api/LectureContentApi.java | 68 ++++++++--- .../service/LectureContentService.java | 108 ++++++++++++++--- .../service/LectureContentImageService.java | 112 ++++++++++++++++++ 3 files changed, 256 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/api/LectureContentApi.java b/src/main/java/com/example/DoroServer/domain/lectureContent/api/LectureContentApi.java index 618d8e76..ff56a7e4 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/api/LectureContentApi.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/api/LectureContentApi.java @@ -1,9 +1,12 @@ package com.example.DoroServer.domain.lectureContent.api; - -import com.example.DoroServer.domain.lectureContent.dto.LectureContentDto; +import com.example.DoroServer.domain.lectureContent.dto.CreateLectureContentReq; +import com.example.DoroServer.domain.lectureContent.dto.LectureContentRes; import com.example.DoroServer.domain.lectureContent.dto.UpdateLectureContentReq; import com.example.DoroServer.domain.lectureContent.service.LectureContentService; +import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageReq; +import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageRes; +import com.example.DoroServer.domain.lectureContentImage.service.LectureContentImageService; import com.example.DoroServer.global.common.SuccessResponse; import java.util.List; @@ -11,15 +14,17 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.access.annotation.Secured; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; + @Api(tags = "강의 자료") @RestController @RequestMapping("/lecture-contents") @@ -27,24 +32,55 @@ @Validated public class LectureContentApi { private final LectureContentService lectureContentService; - @Secured("ROLE_ADMIN") - @PostMapping() - public SuccessResponse createLectureContent(@RequestBody @Valid LectureContentDto lectureContentDto){ - Long lectureId = lectureContentService.createLecture(lectureContentDto); - return SuccessResponse.successResponse(lectureId); - } - @GetMapping() - public SuccessResponse findAllLectureContents(){ - List allLectureContents = lectureContentService.findAllLectureContents(); + private final LectureContentImageService lectureContentImageService; + + @GetMapping + public SuccessResponse findAllLectureContents() { + List allLectureContents = lectureContentService.findAllLectureContents(); return SuccessResponse.successResponse(allLectureContents); } + + @Secured("ROLE_ADMIN") + @PostMapping + public SuccessResponse createLectureContent(@ModelAttribute @Valid CreateLectureContentReq lectureContentReq) { + LectureContentRes createdLectureContent = lectureContentService.createLectureContent(lectureContentReq); + return SuccessResponse.successResponse(createdLectureContent); + } + @Secured("ROLE_ADMIN") @PatchMapping("/{id}") - public SuccessResponse updateLectureContent( + public SuccessResponse updateLectureContent( // 강의컨텐츠에서 이미지를 제외한 나머지 필드들만 수정 @PathVariable("id") Long id, - @RequestBody UpdateLectureContentReq updateLectureContentReq){ - Long updateLectureContentId = lectureContentService.updateLectureContent(id, updateLectureContentReq); - return SuccessResponse.successResponse(updateLectureContentId + "is updated"); + @ModelAttribute UpdateLectureContentReq updateLectureContentReq) { + LectureContentRes updatedLectureContent = lectureContentService.updateLectureContent(id, + updateLectureContentReq); + return SuccessResponse.successResponse(updatedLectureContent); + } + + @Secured("ROLE_ADMIN") + @DeleteMapping("/{id}") + public SuccessResponse deleteLectureContent(@PathVariable("id") Long id) { + lectureContentService.deleteLectureContent(id); + return SuccessResponse.successResponse("강의 자료 삭제 성공"); + } + + /* 강의 컨텐츠의 이미지 추가 및 삭제 API */ + + @Secured("ROLE_ADMIN") + @PostMapping("/{id}/images") + public SuccessResponse addLectureContentImages(@PathVariable("id") Long id, + @ModelAttribute @Valid LectureContentImageReq lectureContentImageReq) { + List addedLectureContentImages = lectureContentImageService.addLectureContentImages(id, + lectureContentImageReq); + return SuccessResponse.successResponse(addedLectureContentImages); + } + + @Secured("ROLE_ADMIN") + @DeleteMapping("/{id}/images/{imageId}") + public SuccessResponse deleteLectureContentImage(@PathVariable("id") Long id, + @PathVariable("imageId") Long imageId) { + lectureContentImageService.deleteLectureContentImage(id, imageId); + return SuccessResponse.successResponse("강의 컨텐츠 이미지 삭제 성공"); } } diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java b/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java index f18c88c2..aa594cb8 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java @@ -1,44 +1,120 @@ package com.example.DoroServer.domain.lectureContent.service; -import com.example.DoroServer.domain.lecture.repository.LectureRepository; -import com.example.DoroServer.domain.lectureContent.dto.LectureContentDto; import com.example.DoroServer.domain.lectureContent.dto.LectureContentMapper; +import com.example.DoroServer.domain.lectureContent.dto.CreateLectureContentReq; +import com.example.DoroServer.domain.lectureContent.dto.LectureContentRes; import com.example.DoroServer.domain.lectureContent.dto.UpdateLectureContentReq; import com.example.DoroServer.domain.lectureContent.entity.LectureContent; import com.example.DoroServer.domain.lectureContent.repository.LectureContentRepository; +import com.example.DoroServer.domain.lectureContentImage.entity.LectureContentImage; import com.example.DoroServer.global.exception.BaseException; import com.example.DoroServer.global.exception.Code; +import com.example.DoroServer.global.s3.AwsS3ServiceImpl; + +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; import org.modelmapper.ModelMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor @Transactional public class LectureContentService { + + private final AwsS3ServiceImpl awsS3Service; private final LectureContentRepository lectureContentRepository; private final LectureContentMapper lectureContentMapper; private final ModelMapper modelMapper; - public Long createLecture(LectureContentDto lectureContentDto){ - LectureContent lectureContent = lectureContentMapper.toLectureContent(lectureContentDto); + + /* 서비스 코드 */ + + public List findAllLectureContents() { + List lectureContentList = lectureContentRepository.findAll(); + + return lectureContentMapper.toLectureContentResList(lectureContentList); + } + + public LectureContentRes createLectureContent(CreateLectureContentReq lectureContentReq) { + validateDtoFiles(lectureContentReq.getFiles()); + + LectureContent lectureContent = modelMapper.map(lectureContentReq, LectureContent.class); + + List uploadedUrls = uploadFilesAndGetUrls(lectureContentReq.getFiles()); + + List lectureContentImages = createLectureContentImagesWith(uploadedUrls); + lectureContent.getImages().addAll(lectureContentImages); LectureContent savedLectureContent = lectureContentRepository.save(lectureContent); - return savedLectureContent.getId(); + + return lectureContentMapper.toLectureContentRes(savedLectureContent); } - public List findAllLectureContents(){ - List lectureContentList = lectureContentRepository.findAll(); - List lectureContentResList = lectureContentList.stream() - .map(lectureContent -> lectureContentMapper.toLectureContentDto(lectureContent)) - .collect(Collectors.toList()); - return lectureContentResList; + + public LectureContentRes updateLectureContent(Long id, UpdateLectureContentReq updateLectureContentReq) { + LectureContent lectureContent = findLectureContentById(id); + + modelMapper.map(updateLectureContentReq, lectureContent); // 필드있고없고차이 MapStruct와 비교 + + LectureContent updatedLectureContent = lectureContentRepository.save(lectureContent); + + return lectureContentMapper.toLectureContentRes(updatedLectureContent); + } + + public void deleteLectureContent(Long id) { + LectureContent lectureContent = findLectureContentById(id); + + lectureContent.getImages().forEach(this::deleteS3Image); + + lectureContentRepository.delete(lectureContent); } - public Long updateLectureContent(Long id, UpdateLectureContentReq updateLectureContentReq){ - LectureContent lectureContent = lectureContentRepository.findById(id) + /* 서비스 코드에서 사용되는 메서드 */ + + void validateDtoFiles(MultipartFile[] files) { + if (files == null) { + // Multipart가 한 개라도 배열에 넣어서 요청해야함 + throw new BaseException(Code.JSON_SYNTAX_ERROR); + } + if (files.length > 100) { + throw new BaseException(Code.LECTURE_CONTENT_IMAGE_COUNT_OVER); + } + for (MultipartFile file : files) { + if (file.getSize() > 10485760) { // 10MB + throw new BaseException(Code.LECTURE_CONTENT_IMAGE_SIZE_OVER); + } + } + } + + LectureContent findLectureContentById(Long id) { + return lectureContentRepository.findById(id) .orElseThrow(() -> new BaseException(Code.LECTURE_CONTENT_NOT_FOUND)); - modelMapper.map(updateLectureContentReq,lectureContent); - return lectureContent.getId(); } + + List createLectureContentImagesWith(List uploadedUrls) { + // 업로드된 파일들의 URL로 LectureContentImage를 만들어 리스트로 반환 + return uploadedUrls.stream() + .map(url -> LectureContentImage.builder().url(url).build()) + .collect(Collectors.toList()); + } + + List uploadFilesAndGetUrls(MultipartFile[] files) { + // S3 BUCKET/lecture-content에 파일 업로드 후, 업로드된 파일들의 URL 반환 + return Arrays.stream(files) + .map(file -> awsS3Service.upload(file, "lecture-content")) + .collect(Collectors.toList()); + } + + void deleteS3Image(LectureContentImage lectureContentImage) { + String uploadedUrl = lectureContentImage.getUrl(); + String fileName = getFileNameFrom(uploadedUrl); + awsS3Service.deleteImage(fileName); + } + + String getFileNameFrom(String url) { + // UUID(36자) + .jpg(4자) = 파일명(40자) + return url.substring(40); + } + } diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java new file mode 100644 index 00000000..5adcd3c1 --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java @@ -0,0 +1,112 @@ +package com.example.DoroServer.domain.lectureContentImage.service; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import javax.transaction.Transactional; + +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import com.example.DoroServer.domain.lectureContent.entity.LectureContent; +import com.example.DoroServer.domain.lectureContent.repository.LectureContentRepository; +import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageMapper; +import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageReq; +import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageRes; +import com.example.DoroServer.domain.lectureContentImage.entity.LectureContentImage; +import com.example.DoroServer.domain.lectureContentImage.repository.LectureContentImageRepository; +import com.example.DoroServer.global.exception.BaseException; +import com.example.DoroServer.global.exception.Code; +import com.example.DoroServer.global.s3.AwsS3ServiceImpl; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional +public class LectureContentImageService { + private final AwsS3ServiceImpl awsS3Service; + private final LectureContentImageRepository lectureContentImageRepository; + private final LectureContentRepository lectureContentRepository; + private final LectureContentImageMapper lectureContentImageMapper; + + public List addLectureContentImages(Long id, + LectureContentImageReq lectureContentImageReq) { + LectureContent lectureContent = findLectureContentById(id); + + validateDtoFiles(lectureContentImageReq.getFiles()); + + List uploadedUrls = uploadFilesAndGetUrls(lectureContentImageReq.getFiles()); + + List lectureContentImages = createLectureContentImagesWith(uploadedUrls); + lectureContentImages = lectureContentImageRepository.saveAll(lectureContentImages); + lectureContent.getImages().addAll(lectureContentImages); + return lectureContentImageMapper.toLectureContentImageResList(lectureContentImages); + } + + public void deleteLectureContentImage(Long contentId, Long imageId) { + LectureContent lectureContent = findLectureContentById(contentId); + LectureContentImage lectureContentImage = getLectureContentImage(imageId, lectureContent); + + deleteS3Image(lectureContentImage); + lectureContent.getImages().remove(lectureContentImage); + lectureContentImageRepository.delete(lectureContentImage); + } + + /* 서비스 코드에서 사용되는 메서드 */ + + void validateDtoFiles(MultipartFile[] files) { + // MultipartFile이 한 개라도 배열에 넣어서 요청해야함 + if (files == null || files.length == 0) { + throw new BaseException(Code.JSON_SYNTAX_ERROR); + } + if (files.length > 100) { + throw new BaseException(Code.LECTURE_CONTENT_IMAGE_COUNT_OVER); + } + for (MultipartFile file : files) { + if (file.getSize() > 10485760) { // 10MB + throw new BaseException(Code.LECTURE_CONTENT_IMAGE_SIZE_OVER); + } + } + } + + private LectureContentImage getLectureContentImage(Long imageId, LectureContent lectureContent) { + LectureContentImage lectureContentImage = lectureContent.getImages().stream() + .filter(image -> image.getId().equals(imageId)) + .findFirst() + .orElseThrow(() -> new BaseException(Code.LECTURE_CONTENT_IMAGE_NOT_FOUND)); + return lectureContentImage; + } + + LectureContent findLectureContentById(Long id) { + return lectureContentRepository.findById(id) + .orElseThrow(() -> new BaseException(Code.LECTURE_CONTENT_NOT_FOUND)); + } + + List createLectureContentImagesWith(List uploadedUrls) { + // 업로드된 파일들의 URL로 LectureContentImage를 만들어 리스트로 반환 + return uploadedUrls.stream() + .map(url -> LectureContentImage.builder().url(url).build()) + .collect(Collectors.toList()); + } + + List uploadFilesAndGetUrls(MultipartFile[] files) { + // S3 BUCKET/lecture-content에 파일 업로드 후, 업로드된 파일들의 URL 반환 + return Arrays.stream(files) + .map(file -> awsS3Service.upload(file, "lecture-content")) + .collect(Collectors.toList()); + } + + void deleteS3Image(LectureContentImage lectureContentImage) { + String uploadedUrl = lectureContentImage.getUrl(); + String fileName = getFileNameFrom(uploadedUrl); + awsS3Service.deleteImage(fileName); + } + + String getFileNameFrom(String url) { + // UUID(36자) + .jpg(4자) = 파일명(40자) + return url.substring(40); + } + +} From bd722ff39099e444ba3af063a8e5e4a951788fdd Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sat, 9 Dec 2023 19:50:43 +0900 Subject: [PATCH 25/32] Chore: chmod +x ./gradlew --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From a0971cf3ee4d292471b33bd5cd2181281f57056c Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sat, 9 Dec 2023 21:03:28 +0900 Subject: [PATCH 26/32] =?UTF-8?q?Refact:=20=EA=B0=95=EC=9D=98=20=EC=BB=A8?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20#120=20ContentImageS3Service?= =?UTF-8?q?=EB=A5=BC=20=EB=A7=8C=EB=93=A4=EC=96=B4=20=EA=B0=95=EC=9D=98=20?= =?UTF-8?q?=EC=BB=A8=ED=85=90=EC=B8=A0=EC=99=80=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=ED=95=9C=20S3=20=EC=9D=B4=EC=9A=A9=EA=B8=B0=EB=8A=A5=EC=9D=84?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=ED=95=A8=20-=20ContentImageS3Service,=20C?= =?UTF-8?q?ontentImageS3ServiceImpl=20-=20LecturecontentService,=20Lecture?= =?UTF-8?q?contentImageService=20-=20LectureContentImage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ContentImageS3Service.java | 12 ++++ .../service/ContentImageS3ServiceImpl.java | 59 ++++++++++++++++++ .../service/LectureContentService.java | 60 +++---------------- .../entity/LectureContentImage.java | 4 ++ .../service/LectureContentImageService.java | 58 +++--------------- 5 files changed, 93 insertions(+), 100 deletions(-) create mode 100644 src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3Service.java create mode 100644 src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3Service.java b/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3Service.java new file mode 100644 index 00000000..1d076282 --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3Service.java @@ -0,0 +1,12 @@ +package com.example.DoroServer.domain.lectureContent.service; + +import org.springframework.web.multipart.MultipartFile; +import java.util.List; + +import com.example.DoroServer.domain.lectureContentImage.entity.LectureContentImage; + +public interface ContentImageS3Service { + List uploadS3Images(MultipartFile[] files); + + void deleteS3Image(LectureContentImage lectureContentImage); +} diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java b/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java new file mode 100644 index 00000000..47634c61 --- /dev/null +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java @@ -0,0 +1,59 @@ +package com.example.DoroServer.domain.lectureContent.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import com.example.DoroServer.domain.lectureContentImage.entity.LectureContentImage; +import com.example.DoroServer.global.exception.BaseException; +import com.example.DoroServer.global.exception.Code; +import com.example.DoroServer.global.s3.AwsS3ServiceImpl; + +import java.util.Arrays; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +@Service +public class ContentImageS3ServiceImpl implements ContentImageS3Service { + private final AwsS3ServiceImpl awsS3Service; + + void validate(MultipartFile[] files) { + if (files == null) { + // Multipart가 한 개라도 배열에 넣어서 요청해야함 + throw new BaseException(Code.JSON_SYNTAX_ERROR); + } + if (files.length > 100) { + throw new BaseException(Code.LECTURE_CONTENT_IMAGE_COUNT_OVER); + } + for (MultipartFile file : files) { + if (file.getSize() > 10485760) { // 10MB + throw new BaseException(Code.LECTURE_CONTENT_IMAGE_SIZE_OVER); + } + } + } + + String getFileNameFrom(String url) { + // UUID(36자) + .jpg(4자) = 파일명(40자) + return url.substring(40); + } + + @Override + public List uploadS3Images(MultipartFile[] files) { + validate(files); + + // S3 BUCKET/lecture-content에 파일 업로드 후, 업로드된 파일들의 URL 반환 + return Arrays.stream(files) + .map(file -> awsS3Service.upload(file, "lecture-content")) + .collect(Collectors.toList()); + } + + @Override + public void deleteS3Image(LectureContentImage lectureContentImage) { + String uploadedUrl = lectureContentImage.getUrl(); + String fileName = getFileNameFrom(uploadedUrl); + awsS3Service.deleteImage(fileName); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java b/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java index aa594cb8..e746f604 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java @@ -9,15 +9,12 @@ import com.example.DoroServer.domain.lectureContentImage.entity.LectureContentImage; import com.example.DoroServer.global.exception.BaseException; import com.example.DoroServer.global.exception.Code; -import com.example.DoroServer.global.s3.AwsS3ServiceImpl; -import java.util.Arrays; -import java.util.List; import java.util.stream.Collectors; +import java.util.List; import org.modelmapper.ModelMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; import lombok.RequiredArgsConstructor; @Service @@ -25,10 +22,10 @@ @Transactional public class LectureContentService { - private final AwsS3ServiceImpl awsS3Service; private final LectureContentRepository lectureContentRepository; private final LectureContentMapper lectureContentMapper; private final ModelMapper modelMapper; + private final ContentImageS3ServiceImpl contentImageS3Service; /* 서비스 코드 */ @@ -39,13 +36,14 @@ public List findAllLectureContents() { } public LectureContentRes createLectureContent(CreateLectureContentReq lectureContentReq) { - validateDtoFiles(lectureContentReq.getFiles()); - LectureContent lectureContent = modelMapper.map(lectureContentReq, LectureContent.class); - List uploadedUrls = uploadFilesAndGetUrls(lectureContentReq.getFiles()); + // lectureContentReq의 files를 S3에 업로드 후, 업로드된 파일들의 URL을 LectureContentImage로 변환 + List uploadedUrls = contentImageS3Service.uploadS3Images(lectureContentReq.getFiles()); + List lectureContentImages = uploadedUrls.stream() + .map(LectureContentImage::new) + .collect(Collectors.toList()); - List lectureContentImages = createLectureContentImagesWith(uploadedUrls); lectureContent.getImages().addAll(lectureContentImages); LectureContent savedLectureContent = lectureContentRepository.save(lectureContent); @@ -55,7 +53,7 @@ public LectureContentRes createLectureContent(CreateLectureContentReq lectureCon public LectureContentRes updateLectureContent(Long id, UpdateLectureContentReq updateLectureContentReq) { LectureContent lectureContent = findLectureContentById(id); - modelMapper.map(updateLectureContentReq, lectureContent); // 필드있고없고차이 MapStruct와 비교 + modelMapper.map(updateLectureContentReq, lectureContent); LectureContent updatedLectureContent = lectureContentRepository.save(lectureContent); @@ -65,56 +63,16 @@ public LectureContentRes updateLectureContent(Long id, UpdateLectureContentReq u public void deleteLectureContent(Long id) { LectureContent lectureContent = findLectureContentById(id); - lectureContent.getImages().forEach(this::deleteS3Image); + lectureContent.getImages().forEach(contentImageS3Service::deleteS3Image); lectureContentRepository.delete(lectureContent); } /* 서비스 코드에서 사용되는 메서드 */ - void validateDtoFiles(MultipartFile[] files) { - if (files == null) { - // Multipart가 한 개라도 배열에 넣어서 요청해야함 - throw new BaseException(Code.JSON_SYNTAX_ERROR); - } - if (files.length > 100) { - throw new BaseException(Code.LECTURE_CONTENT_IMAGE_COUNT_OVER); - } - for (MultipartFile file : files) { - if (file.getSize() > 10485760) { // 10MB - throw new BaseException(Code.LECTURE_CONTENT_IMAGE_SIZE_OVER); - } - } - } - LectureContent findLectureContentById(Long id) { return lectureContentRepository.findById(id) .orElseThrow(() -> new BaseException(Code.LECTURE_CONTENT_NOT_FOUND)); } - List createLectureContentImagesWith(List uploadedUrls) { - // 업로드된 파일들의 URL로 LectureContentImage를 만들어 리스트로 반환 - return uploadedUrls.stream() - .map(url -> LectureContentImage.builder().url(url).build()) - .collect(Collectors.toList()); - } - - List uploadFilesAndGetUrls(MultipartFile[] files) { - // S3 BUCKET/lecture-content에 파일 업로드 후, 업로드된 파일들의 URL 반환 - return Arrays.stream(files) - .map(file -> awsS3Service.upload(file, "lecture-content")) - .collect(Collectors.toList()); - } - - void deleteS3Image(LectureContentImage lectureContentImage) { - String uploadedUrl = lectureContentImage.getUrl(); - String fileName = getFileNameFrom(uploadedUrl); - awsS3Service.deleteImage(fileName); - } - - String getFileNameFrom(String url) { - // UUID(36자) + .jpg(4자) = 파일명(40자) - return url.substring(40); - } - } diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/entity/LectureContentImage.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/entity/LectureContentImage.java index 64f6aeab..d0a07923 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContentImage/entity/LectureContentImage.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/entity/LectureContentImage.java @@ -22,4 +22,8 @@ public class LectureContentImage { private String url; + public LectureContentImage(String url) { + this.url = url; + } + } diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java index 5adcd3c1..2a4ccb4b 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java @@ -1,16 +1,15 @@ package com.example.DoroServer.domain.lectureContentImage.service; -import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import javax.transaction.Transactional; import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; import com.example.DoroServer.domain.lectureContent.entity.LectureContent; import com.example.DoroServer.domain.lectureContent.repository.LectureContentRepository; +import com.example.DoroServer.domain.lectureContent.service.ContentImageS3ServiceImpl; import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageMapper; import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageReq; import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageRes; @@ -18,7 +17,6 @@ import com.example.DoroServer.domain.lectureContentImage.repository.LectureContentImageRepository; import com.example.DoroServer.global.exception.BaseException; import com.example.DoroServer.global.exception.Code; -import com.example.DoroServer.global.s3.AwsS3ServiceImpl; import lombok.RequiredArgsConstructor; @@ -26,22 +24,24 @@ @RequiredArgsConstructor @Transactional public class LectureContentImageService { - private final AwsS3ServiceImpl awsS3Service; private final LectureContentImageRepository lectureContentImageRepository; private final LectureContentRepository lectureContentRepository; private final LectureContentImageMapper lectureContentImageMapper; + private final ContentImageS3ServiceImpl contentImageS3Service; public List addLectureContentImages(Long id, LectureContentImageReq lectureContentImageReq) { LectureContent lectureContent = findLectureContentById(id); - validateDtoFiles(lectureContentImageReq.getFiles()); - - List uploadedUrls = uploadFilesAndGetUrls(lectureContentImageReq.getFiles()); + // lectureContentReq의 files를 S3에 업로드 후, 업로드된 파일들의 URL을 LectureContentImage로 변환 + List uploadedUrls = contentImageS3Service.uploadS3Images(lectureContentImageReq.getFiles()); + List lectureContentImages = uploadedUrls.stream() + .map(LectureContentImage::new) + .collect(Collectors.toList()); - List lectureContentImages = createLectureContentImagesWith(uploadedUrls); lectureContentImages = lectureContentImageRepository.saveAll(lectureContentImages); lectureContent.getImages().addAll(lectureContentImages); + return lectureContentImageMapper.toLectureContentImageResList(lectureContentImages); } @@ -49,28 +49,13 @@ public void deleteLectureContentImage(Long contentId, Long imageId) { LectureContent lectureContent = findLectureContentById(contentId); LectureContentImage lectureContentImage = getLectureContentImage(imageId, lectureContent); - deleteS3Image(lectureContentImage); + contentImageS3Service.deleteS3Image(lectureContentImage); lectureContent.getImages().remove(lectureContentImage); lectureContentImageRepository.delete(lectureContentImage); } /* 서비스 코드에서 사용되는 메서드 */ - void validateDtoFiles(MultipartFile[] files) { - // MultipartFile이 한 개라도 배열에 넣어서 요청해야함 - if (files == null || files.length == 0) { - throw new BaseException(Code.JSON_SYNTAX_ERROR); - } - if (files.length > 100) { - throw new BaseException(Code.LECTURE_CONTENT_IMAGE_COUNT_OVER); - } - for (MultipartFile file : files) { - if (file.getSize() > 10485760) { // 10MB - throw new BaseException(Code.LECTURE_CONTENT_IMAGE_SIZE_OVER); - } - } - } - private LectureContentImage getLectureContentImage(Long imageId, LectureContent lectureContent) { LectureContentImage lectureContentImage = lectureContent.getImages().stream() .filter(image -> image.getId().equals(imageId)) @@ -84,29 +69,4 @@ LectureContent findLectureContentById(Long id) { .orElseThrow(() -> new BaseException(Code.LECTURE_CONTENT_NOT_FOUND)); } - List createLectureContentImagesWith(List uploadedUrls) { - // 업로드된 파일들의 URL로 LectureContentImage를 만들어 리스트로 반환 - return uploadedUrls.stream() - .map(url -> LectureContentImage.builder().url(url).build()) - .collect(Collectors.toList()); - } - - List uploadFilesAndGetUrls(MultipartFile[] files) { - // S3 BUCKET/lecture-content에 파일 업로드 후, 업로드된 파일들의 URL 반환 - return Arrays.stream(files) - .map(file -> awsS3Service.upload(file, "lecture-content")) - .collect(Collectors.toList()); - } - - void deleteS3Image(LectureContentImage lectureContentImage) { - String uploadedUrl = lectureContentImage.getUrl(); - String fileName = getFileNameFrom(uploadedUrl); - awsS3Service.deleteImage(fileName); - } - - String getFileNameFrom(String url) { - // UUID(36자) + .jpg(4자) = 파일명(40자) - return url.substring(40); - } - } From 649a8f27492d80053077b598ed462ab5372fc10f Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sun, 10 Dec 2023 14:13:27 +0900 Subject: [PATCH 27/32] =?UTF-8?q?Fix:=20=EA=B0=95=EC=9D=98=EC=BB=A8?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20Code=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20#1?= =?UTF-8?q?20=20-=20Code.LECTURE=5FCONTENT=5FINVAILD=5FIMAGE=5FCOUNT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/DoroServer/global/exception/Code.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/DoroServer/global/exception/Code.java b/src/main/java/com/example/DoroServer/global/exception/Code.java index 26b3630b..245413e0 100644 --- a/src/main/java/com/example/DoroServer/global/exception/Code.java +++ b/src/main/java/com/example/DoroServer/global/exception/Code.java @@ -41,7 +41,7 @@ public enum Code { LECTURE_NOT_FOUND(HttpStatus.BAD_REQUEST, "LEC001", "강의가 존재하지 않습니다."), LECTURE_CONTENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "LEC002", "강의 컨텐츠가 존재하지 않습니다."), LECTURE_CONTENT_IMAGE_SIZE_OVER(HttpStatus.BAD_REQUEST, "LEC003", "이미지 사이즈가 10MB를 초과했습니다."), - LECTURE_CONTENT_IMAGE_COUNT_OVER(HttpStatus.BAD_REQUEST, "LEC004", "이미지는 100장 이하로 업로드해주세요."), + LECTURE_CONTENT_INVAILD_IMAGE_COUNT(HttpStatus.BAD_REQUEST, "LEC004", "이미지는 1장 이상 100장 이하로 업로드해주세요."), LECTURE_CONTENT_IMAGE_NOT_FOUND(HttpStatus.BAD_REQUEST, "LEC005", "이미지가 존재하지 않습니다."), // 신청 강사 관련 오류 From 4f3691c40574cd4bf79f8d093e1d4fe7a43df822 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sun, 10 Dec 2023 14:18:53 +0900 Subject: [PATCH 28/32] =?UTF-8?q?Test:=20ContentImageS3ServiceImpl=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=20#120=20-=20ContentImageS3ServiceImplTest=20-=20C?= =?UTF-8?q?ontentImageS3Service,=20ContentImageS3ServiceImpl=20-=20AwsS3Se?= =?UTF-8?q?rviceImpl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ContentImageS3ServiceImpl.java | 10 +- .../global/s3/AwsS3ServiceImpl.java | 27 +++-- .../ContentImageS3ServiceImplTest.java | 112 ++++++++++++++++++ 3 files changed, 136 insertions(+), 13 deletions(-) create mode 100644 src/test/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImplTest.java diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java b/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java index 47634c61..547f0f27 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java @@ -21,11 +21,11 @@ public class ContentImageS3ServiceImpl implements ContentImageS3Service { void validate(MultipartFile[] files) { if (files == null) { - // Multipart가 한 개라도 배열에 넣어서 요청해야함 + // Multipart가 1개라도 배열에 넣어서 요청해야함 throw new BaseException(Code.JSON_SYNTAX_ERROR); } - if (files.length > 100) { - throw new BaseException(Code.LECTURE_CONTENT_IMAGE_COUNT_OVER); + if (files.length > 100 || files.length < 1) { + throw new BaseException(Code.LECTURE_CONTENT_INVAILD_IMAGE_COUNT); } for (MultipartFile file : files) { if (file.getSize() > 10485760) { // 10MB @@ -36,7 +36,7 @@ void validate(MultipartFile[] files) { String getFileNameFrom(String url) { // UUID(36자) + .jpg(4자) = 파일명(40자) - return url.substring(40); + return url.substring(url.length() - 40); } @Override @@ -53,7 +53,7 @@ public List uploadS3Images(MultipartFile[] files) { public void deleteS3Image(LectureContentImage lectureContentImage) { String uploadedUrl = lectureContentImage.getUrl(); String fileName = getFileNameFrom(uploadedUrl); - awsS3Service.deleteImage(fileName); + awsS3Service.deleteImage2(fileName); } } \ No newline at end of file diff --git a/src/main/java/com/example/DoroServer/global/s3/AwsS3ServiceImpl.java b/src/main/java/com/example/DoroServer/global/s3/AwsS3ServiceImpl.java index 7431bfba..794e2359 100644 --- a/src/main/java/com/example/DoroServer/global/s3/AwsS3ServiceImpl.java +++ b/src/main/java/com/example/DoroServer/global/s3/AwsS3ServiceImpl.java @@ -1,6 +1,5 @@ package com.example.DoroServer.global.s3; -import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.ObjectMetadata; @@ -19,7 +18,7 @@ @Service @RequiredArgsConstructor -public class AwsS3ServiceImpl implements AwsS3Service{ +public class AwsS3ServiceImpl implements AwsS3Service { @Value("${cloud.aws.s3.bucket}") private String bucket; @@ -30,8 +29,8 @@ public class AwsS3ServiceImpl implements AwsS3Service{ private final AmazonS3Client amazonS3Client; @Override - public String upload(MultipartFile multipartFile, String dirName){ - String fileName = dirName+"/"+ UUID.randomUUID() + ".jpg"; + public String upload(MultipartFile multipartFile, String dirName) { + String fileName = dirName + "/" + UUID.randomUUID() + ".jpg"; if (multipartFile.isEmpty()) { throw new BaseException(Code.BAD_REQUEST); } @@ -45,7 +44,7 @@ public String upload(MultipartFile multipartFile, String dirName){ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, byteArrayInputStream, objectMetadata) - .withCannedAcl(CannedAccessControlList.PublicRead)); + .withCannedAcl(CannedAccessControlList.PublicRead)); } catch (IOException e) { throw new BaseException(Code.UPLOAD_FAILED); } @@ -55,12 +54,24 @@ public String upload(MultipartFile multipartFile, String dirName){ @Override public void deleteImage(String fileName) { - int index=fileName.indexOf(url); - String fileRoute=fileName.substring(index + url.length()+1); + int index = fileName.indexOf(url); + String fileRoute = fileName.substring(index + url.length() + 1); try { boolean isObjectExist = amazonS3Client.doesObjectExist(bucket, fileRoute); if (isObjectExist) { - amazonS3Client.deleteObject(bucket,fileRoute); + amazonS3Client.deleteObject(bucket, fileRoute); + } + } catch (Exception e) { + throw new BaseException(Code.BAD_REQUEST); + } + } + + public void deleteImage2(String fileName) { + String objectName = "lecture-content/" + fileName; // BUCKET_FOLDER/UUID.jpg + try { + boolean isObjectExist = amazonS3Client.doesObjectExist(bucket, objectName); + if (isObjectExist) { + amazonS3Client.deleteObject(bucket, objectName); } } catch (Exception e) { throw new BaseException(Code.BAD_REQUEST); diff --git a/src/test/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImplTest.java b/src/test/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImplTest.java new file mode 100644 index 00000000..ab90a5d5 --- /dev/null +++ b/src/test/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImplTest.java @@ -0,0 +1,112 @@ +package com.example.DoroServer.domain.lectureContent.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.calls; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.List; +import java.util.Objects; +import java.util.Arrays; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import com.example.DoroServer.domain.lectureContent.dto.CreateLectureContentReq; +import com.example.DoroServer.domain.lectureContentImage.entity.LectureContentImage; +import com.example.DoroServer.global.exception.BaseException; +import com.example.DoroServer.global.s3.AwsS3ServiceImpl; + +@ExtendWith(MockitoExtension.class) +public class ContentImageS3ServiceImplTest { + @InjectMocks + private ContentImageS3ServiceImpl contentImageS3Service; + + @Mock + private AwsS3ServiceImpl awsS3Service; + + private String FILE_URL = "https://rodu-s3/lecture-content/052d05f0-e3aa-4088-9b6d-ed3bc42485b2.jpg"; + + private CreateLectureContentReq setUpLectureContentReq0() { + return CreateLectureContentReq.builder() + .kit("kit") + .detail("detail") + .requirement("requirement") + .content("content") + .build(); + } + + private CreateLectureContentReq setUpLectureContentReq1() { + MockMultipartFile mockFile = new MockMultipartFile("file", "Hello, World!".getBytes()); + return CreateLectureContentReq.builder() + .kit("kit") + .detail("detail") + .requirement("requirement") + .content("content") + .files(new MultipartFile[] { mockFile }) + .build(); + } + + private LectureContentImage setUpLectureContentImage() { + return LectureContentImage.builder() + .url(FILE_URL).build(); + } + + @Test + @DisplayName("validate 테스트 - 이미지 파일이 없는 경우 예외발생") + void validateTest0() { + // given + CreateLectureContentReq lectureContentReq = setUpLectureContentReq0(); + + // when & then + assertThrows(BaseException.class, () -> contentImageS3Service.validate(lectureContentReq.getFiles())); + + } + + @Test + @DisplayName("uploadS3Images 테스트") + void uploadS3ImagesTest() { + // given + CreateLectureContentReq lectureContentReq = setUpLectureContentReq1(); + + given(awsS3Service.upload(any(MultipartFile.class), eq("lecture-content"))) + .willReturn(FILE_URL); + + // when + List urls = contentImageS3Service.uploadS3Images(lectureContentReq.getFiles()); + + // then + verify(awsS3Service, times(1)).upload(any(MultipartFile.class), eq("lecture-content")); + assertThat(urls.get(0)).isEqualTo(FILE_URL); + + } + + @Test + @DisplayName("deleteS3Image 테스트") + void deleteS3ImageTest() { + // given + LectureContentImage lectureContentImage = setUpLectureContentImage(); + + // when + contentImageS3Service.deleteS3Image(lectureContentImage); + + // then + verify(awsS3Service, times(1)).deleteImage2(anyString()); + + } + +} From 56a17e498182649aaac0dbaec8f28d1f7eb653e5 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sun, 10 Dec 2023 14:22:38 +0900 Subject: [PATCH 29/32] =?UTF-8?q?Test:=20=EA=B0=95=EC=9D=98=20=EC=BB=A8?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#120=20-=20LectureContentServiceTest=20-?= =?UTF-8?q?=20LectureContentImageServiceTest=20-=20LectureRepositoryTest?= =?UTF-8?q?=20-=20LectureContentImageReq,=20LectureContentImageRes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/LectureContentImageReq.java | 4 + .../dto/LectureContentImageRes.java | 4 + .../repository/LectureRepositoryTest.java | 2 +- .../service/LectureContentServiceTest.java | 180 ++++++++++++------ .../LectureContentImageServiceTest.java | 167 ++++++++++++++++ 5 files changed, 298 insertions(+), 59 deletions(-) create mode 100644 src/test/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageServiceTest.java diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java index 46ed3f54..aba76e73 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java @@ -4,12 +4,16 @@ import org.springframework.web.multipart.MultipartFile; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @NoArgsConstructor +@Builder +@AllArgsConstructor @Setter public class LectureContentImageReq { @NotNull diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageRes.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageRes.java index 6ed5519e..429a79e4 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageRes.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageRes.java @@ -1,5 +1,7 @@ package com.example.DoroServer.domain.lectureContentImage.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -7,6 +9,8 @@ @Getter @NoArgsConstructor @Setter +@AllArgsConstructor +@Builder public class LectureContentImageRes { private Long id; // PK private String url; // uploaded image url diff --git a/src/test/java/com/example/DoroServer/domain/lecture/repository/LectureRepositoryTest.java b/src/test/java/com/example/DoroServer/domain/lecture/repository/LectureRepositoryTest.java index 252a7e1d..8451a578 100644 --- a/src/test/java/com/example/DoroServer/domain/lecture/repository/LectureRepositoryTest.java +++ b/src/test/java/com/example/DoroServer/domain/lecture/repository/LectureRepositoryTest.java @@ -45,7 +45,7 @@ class LectureRepositoryTest { @BeforeEach void setUp() { - lectureContent = LectureContent.builder().kit("테스트 키트").detail("세부사항").requirement("고졸").build(); + lectureContent = LectureContent.builder().kit("테스트 키트").detail("세부사항").requirement("고졸").content("컨텐츠").build(); lectureContentRepository.save(lectureContent); } diff --git a/src/test/java/com/example/DoroServer/domain/lectureContent/service/LectureContentServiceTest.java b/src/test/java/com/example/DoroServer/domain/lectureContent/service/LectureContentServiceTest.java index f5810c26..eef158b7 100644 --- a/src/test/java/com/example/DoroServer/domain/lectureContent/service/LectureContentServiceTest.java +++ b/src/test/java/com/example/DoroServer/domain/lectureContent/service/LectureContentServiceTest.java @@ -1,12 +1,13 @@ package com.example.DoroServer.domain.lectureContent.service; - -import com.example.DoroServer.domain.lectureContent.dto.LectureContentDto; +import com.example.DoroServer.domain.lectureContent.dto.CreateLectureContentReq; import com.example.DoroServer.domain.lectureContent.dto.LectureContentMapper; +import com.example.DoroServer.domain.lectureContent.dto.LectureContentRes; import com.example.DoroServer.domain.lectureContent.dto.UpdateLectureContentReq; import com.example.DoroServer.domain.lectureContent.entity.LectureContent; import com.example.DoroServer.domain.lectureContent.repository.LectureContentRepository; import com.example.DoroServer.global.exception.BaseException; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -16,9 +17,8 @@ import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.modelmapper.ModelMapper; - - -import javax.annotation.meta.When; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; import java.lang.reflect.Field; import java.util.ArrayList; @@ -28,13 +28,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class LectureContentServiceTest { - @InjectMocks private LectureContentService lectureContentService; @@ -42,13 +42,17 @@ class LectureContentServiceTest { private LectureContentRepository lectureContentRepository; @Mock - private ModelMapper modelMapper; + private ContentImageS3ServiceImpl contentImageS3Service; + + @Spy + private ModelMapper modelMapper = new ModelMapper(); @Spy private LectureContentMapper lectureContentMapper = Mappers.getMapper(LectureContentMapper.class); - private LectureContentDto setUpLectureContentDto(){ - return LectureContentDto.builder() + private LectureContent setUpLectureContent() { + return LectureContent.builder() + .id(1L) .kit("kit") .detail("detail") .requirement("requirement") @@ -56,43 +60,77 @@ private LectureContentDto setUpLectureContentDto(){ .build(); } - private LectureContent setUpLectureContent(){ - return LectureContent.builder() - .id(1L) + private CreateLectureContentReq setUpLectureContentReq() { + MockMultipartFile mockFile = new MockMultipartFile("file", "Hello, World!".getBytes()); + return CreateLectureContentReq.builder() .kit("kit") .detail("detail") .requirement("requirement") .content("content") + .files(new MultipartFile[] { mockFile }) .build(); } - private UpdateLectureContentReq setUpUpdateLectureContentReq(){ + + private UpdateLectureContentReq setUpUpdateLectureContentReq() { return UpdateLectureContentReq.builder() .kit("updated") .content("updated") .build(); } - private List setUpLectureContentList(int listSize){ - List allContents= new ArrayList(); - for(int i=0;i()) + .build(); + } + + private LectureContentRes setUpUpdatedLectureContentRes() { + return LectureContentRes.builder() + .id(1L) + .kit("updated") + .detail("detail") + .requirement("requirement") + .content("updated") + .images(new ArrayList<>()) + .build(); + } + + private List setUpLectureContentList(int listSize) { + List allContents = new ArrayList(); + for (int i = 0; i < listSize; i++) { allContents.add(setUpLectureContent()); } return allContents; } + private List setUpLectureContentResList(int listSize) { + List allContents = new ArrayList(); + for (int i = 0; i < listSize; i++) { + allContents.add(setUpLectureContentRes()); + } + return allContents; + } @DisplayName("강의자료 생성 Mapper 테스트") @Test void toLectureContentMapperTest() throws IllegalAccessException { - //given - LectureContentDto lectureContentDto = setUpLectureContentDto(); - //when - LectureContent lectureContent = lectureContentMapper.toLectureContent(lectureContentDto); + // given + CreateLectureContentReq createLectureContentReq = setUpLectureContentReq(); + LectureContent lectureContent = setUpLectureContent(); + + // when + LectureContent lectureContentMapped = modelMapper.map(createLectureContentReq, LectureContent.class); - //then - for(Field field :lectureContent.getClass().getDeclaredFields() ){ + // then + for (Field field : lectureContentMapped.getClass().getDeclaredFields()) { field.setAccessible(true); Object value = field.get(lectureContent); - if(field.getName().equals("id")){ + if (field.getName().equals("id")) { continue; } assertThat(value).isNotNull(); @@ -103,56 +141,74 @@ void toLectureContentMapperTest() throws IllegalAccessException { @DisplayName("강의 자료 생성 테스트") @Test void createLectureContentTest() { - // given - LectureContentDto lectureContentDto = setUpLectureContentDto(); + CreateLectureContentReq createLectureContentReq = setUpLectureContentReq(); LectureContent lectureContent = setUpLectureContent(); + LectureContentRes lectureContentRes = setUpLectureContentRes(); + + given(modelMapper.map(createLectureContentReq, LectureContent.class)).willReturn(lectureContent); + given(contentImageS3Service.uploadS3Images(any(MultipartFile[].class))).willReturn(new ArrayList()); + given(lectureContentRepository.save(any(LectureContent.class))).willReturn(setUpLectureContent()); + given(lectureContentMapper.toLectureContentRes(any(LectureContent.class))).willReturn(lectureContentRes); - given(lectureContentMapper.toLectureContent(any(LectureContentDto.class))).willReturn(lectureContent); - given(lectureContentRepository.save(any(LectureContent.class))).willReturn(lectureContent); // when - Long lectureID = lectureContentService.createLecture(lectureContentDto); + LectureContentRes savedLectureContent = lectureContentService.createLectureContent(createLectureContentReq); + // then - assertThat(lectureID).isEqualTo(lectureContent.getId()); - verify(lectureContentMapper,times(1)).toLectureContent(lectureContentDto); - verify(lectureContentRepository,times(1)).save(lectureContent); + verify(modelMapper, times(1)).map(createLectureContentReq, LectureContent.class); + verify(contentImageS3Service, times(1)).uploadS3Images(any(MultipartFile[].class)); + verify(lectureContentRepository, times(1)).save(any(LectureContent.class)); + verify(lectureContentMapper, times(1)).toLectureContentRes(any(LectureContent.class)); + assertThat(savedLectureContent).isNotNull(); } @DisplayName("강의 자료 조회 Mapper 테스트") @Test - void toLectureContentDtoMapperTest() throws IllegalAccessException { + void toLectureContentResListMapperTest() throws IllegalAccessException { // given - LectureContent lectureContent = setUpLectureContent(); + int contentCount = 5; + List lectureContentList = setUpLectureContentList(contentCount); + List lectureContentResList = setUpLectureContentResList(contentCount); // when - LectureContentDto lectureContentDto = lectureContentMapper.toLectureContentDto(lectureContent); + List mappedlectureContentResList = lectureContentMapper + .toLectureContentResList(lectureContentList); + // then - for(Field field :lectureContentDto.getClass().getDeclaredFields() ){ - field.setAccessible(true); - Object value = field.get(lectureContentDto); - assertThat(value).isNotNull(); + for (int i = 0; i < contentCount; i++) { + LectureContentRes mappedRes = mappedlectureContentResList.get(i); + LectureContentRes originalRes = lectureContentResList.get(i); + + for (Field field : mappedRes.getClass().getDeclaredFields()) { + field.setAccessible(true); + Object mappedValue = field.get(mappedRes); + Object originalValue = field.get(originalRes); + + assertThat(mappedValue).isEqualTo(originalValue); + } } } - @DisplayName("모든 강의 자료 조회 테스트") @Test - void findAllLectureContents() { + @DisplayName("강의 자료 조회 테스트") + void findAllLectureContentsTest() { // given - int contentCount=5; - List lectureContents = setUpLectureContentList(contentCount); - given(lectureContentRepository.findAll()).willReturn(lectureContents); - given(lectureContentMapper.toLectureContentDto(any(LectureContent.class))).willReturn(setUpLectureContentDto()); + int contentCount = 5; + List lectureContentList = setUpLectureContentList(contentCount); + List lectureContentResList = setUpLectureContentResList(contentCount); + + given(lectureContentRepository.findAll()).willReturn(lectureContentList); + given(lectureContentMapper.toLectureContentResList(anyList())).willReturn(lectureContentResList); // when - List allLectureContents = lectureContentService.findAllLectureContents(); + List result = lectureContentService.findAllLectureContents(); // then - verify(lectureContentRepository,times(1)).findAll(); - verify(lectureContentMapper,times(contentCount)).toLectureContentDto(any(LectureContent.class)); - assertThat(allLectureContents.size()).isEqualTo(contentCount); - + assertEquals(contentCount, result.size()); + verify(lectureContentRepository, times(1)).findAll(); + verify(lectureContentMapper, times(1)).toLectureContentResList(anyList()); } @DisplayName("강의 자료 업데이트 예외 테스트") @@ -161,13 +217,12 @@ void updateLectureContentExceptionTest() { // given LectureContent lectureContent = setUpLectureContent(); UpdateLectureContentReq updateLectureContentReq = setUpUpdateLectureContentReq(); - given(lectureContentRepository.findById(any(Long.class))).willReturn(Optional.empty()); - // when + given(lectureContentRepository.findById(any(Long.class))).willReturn(Optional.empty()); - // then - assertThrows(BaseException.class,()->{ - lectureContentService.updateLectureContent(lectureContent.getId(),updateLectureContentReq); + // when & then + assertThrows(BaseException.class, () -> { + lectureContentService.updateLectureContent(lectureContent.getId(), updateLectureContentReq); }); } @@ -177,14 +232,23 @@ void updateLectureContentTest() { // given LectureContent lectureContent = setUpLectureContent(); UpdateLectureContentReq updateLectureContentReq = setUpUpdateLectureContentReq(); + LectureContentRes updatedLectureContentRes = setUpUpdatedLectureContentRes(); + given(lectureContentRepository.findById(any(Long.class))).willReturn(Optional.of(lectureContent)); - doNothing().when(modelMapper).map(any(UpdateLectureContentReq.class),any(LectureContent.class)); + given(lectureContentRepository.save(any(LectureContent.class))).willReturn(lectureContent); + given(lectureContentMapper.toLectureContentRes(any(LectureContent.class))) + .willReturn(updatedLectureContentRes); // when - lectureContentService.updateLectureContent(lectureContent.getId(),updateLectureContentReq); + LectureContentRes updatedLectureContent = lectureContentService + .updateLectureContent(lectureContent.getId(), updateLectureContentReq); + // then - verify(lectureContentRepository,times(1)).findById(any(Long.class)); - verify(modelMapper,times(1)).map(any(UpdateLectureContentReq.class),any(LectureContent.class));; + verify(lectureContentRepository, times(1)).findById(any(Long.class)); + verify(lectureContentRepository, times(1)).save(any(LectureContent.class)); + verify(lectureContentMapper, times(1)).toLectureContentRes(any(LectureContent.class)); + assertThat(updatedLectureContent).isNotNull(); + assertThat(updatedLectureContent.getKit()).isEqualTo(updateLectureContentReq.getKit()); } diff --git a/src/test/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageServiceTest.java b/src/test/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageServiceTest.java new file mode 100644 index 00000000..3c39232c --- /dev/null +++ b/src/test/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageServiceTest.java @@ -0,0 +1,167 @@ +package com.example.DoroServer.domain.lectureContentImage.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mapstruct.factory.Mappers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import com.example.DoroServer.domain.lectureContent.dto.LectureContentRes; +import com.example.DoroServer.domain.lectureContent.entity.LectureContent; +import com.example.DoroServer.domain.lectureContent.repository.LectureContentRepository; +import com.example.DoroServer.domain.lectureContent.service.ContentImageS3ServiceImpl; +import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageMapper; +import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageReq; +import com.example.DoroServer.domain.lectureContentImage.dto.LectureContentImageRes; +import com.example.DoroServer.domain.lectureContentImage.entity.LectureContentImage; +import com.example.DoroServer.domain.lectureContentImage.repository.LectureContentImageRepository; +import com.example.DoroServer.global.exception.BaseException; + +import java.util.List; + +@ExtendWith(MockitoExtension.class) +public class LectureContentImageServiceTest { + + @InjectMocks + private LectureContentImageService lectureContentImageService; + + @Mock + private LectureContentImageRepository lectureContentImageRepository; + + @Mock + private LectureContentRepository lectureContentRepository; + + @Mock + private ContentImageS3ServiceImpl contentImageS3Service; + + @Spy + private LectureContentImageMapper lectureContentImageMapper = Mappers.getMapper(LectureContentImageMapper.class); + + LectureContentImageReq setUplectureContentImageReq() { + MockMultipartFile mockFile = new MockMultipartFile("file", "Hello, World!".getBytes()); + return LectureContentImageReq.builder() + .files(new MultipartFile[] { mockFile }) + .build(); + } + + String UPLOADED_URL = "url"; + + LectureContent setUplectureContent() { + return LectureContent.builder() + .id(1L) + .kit("kit") + .detail("detail") + .requirement("requirement") + .content("content") + .build(); + } + + LectureContentImage setUplectureContentImage() { + return LectureContentImage.builder() + .id(1L) + .url(UPLOADED_URL) + .build(); + } + + LectureContentImageRes setUplectureContentImageRes() { + return LectureContentImageRes.builder() + .id(1L) + .url(UPLOADED_URL) + .build(); + } + + LectureContentRes setUplectureContentRes() { + return LectureContentRes.builder() + .id(1L) + .kit("kit") + .detail("detail") + .requirement("requirement") + .content("content") + .images(List.of(setUplectureContentImageRes())) + .build(); + } + + @Test + @DisplayName("강의 컨텐츠 이미지 추가 테스트") + void addLectureContentImages() { + // given + LectureContentImageReq lectureContentImageReq = setUplectureContentImageReq(); + Long id = 1L; + LectureContent lectureContent = setUplectureContent(); + List lectureContentImageResList = List.of(setUplectureContentImageRes()); + + given(lectureContentRepository.findById(id)).willReturn(Optional.of(lectureContent)); + given(contentImageS3Service.uploadS3Images(lectureContentImageReq.getFiles())) + .willReturn(List.of(UPLOADED_URL)); + given(lectureContentImageRepository.saveAll(anyList())) + .willReturn(List.of(setUplectureContentImage())); + given(lectureContentImageMapper.toLectureContentImageResList(lectureContent.getImages())) + .willReturn(lectureContentImageResList); + + // when + List result = lectureContentImageService.addLectureContentImages(id, + lectureContentImageReq); + + // then + assertThat(result).isEqualTo(lectureContentImageResList); + verify(lectureContentRepository, times(1)).findById(id); + verify(contentImageS3Service, times(1)).uploadS3Images(lectureContentImageReq.getFiles()); + verify(lectureContentImageRepository, times(1)).saveAll(anyList()); + verify(lectureContentImageMapper, times(1)).toLectureContentImageResList(lectureContent.getImages()); + + } + + @Test + @DisplayName("강의 컨텐츠 이미지 삭제 테스트") + void deleteLectureContentImage() { + // given + Long id = 1L; + Long imageId = 1L; + LectureContent lectureContent = setUplectureContent(); + LectureContentImage lectureContentImage = setUplectureContentImage(); + lectureContent.getImages().add(lectureContentImage); + + given(lectureContentRepository.findById(id)).willReturn(Optional.of(lectureContent)); + doNothing().when(contentImageS3Service).deleteS3Image(lectureContentImage); + willDoNothing().given(lectureContentImageRepository).delete(lectureContentImage); + + // when + lectureContentImageService.deleteLectureContentImage(id, imageId); + + // then + verify(lectureContentRepository, times(1)).findById(id); + verify(contentImageS3Service, times(1)).deleteS3Image(lectureContentImage); + verify(lectureContentImageRepository, times(1)).delete(lectureContentImage); + } + + @Test + @DisplayName("유효하지 않은 강의 컨텐츠 이미지 삭제 예외 발생 테스트") + void deleteLectureContentImageWithInvalidId() { + // given + Long id = -1L; + Long imageId = 1L; + + given(lectureContentRepository.findById(id)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> lectureContentImageService.deleteLectureContentImage(id, imageId)) + .isInstanceOf(BaseException.class); + } + +} From e852059ce283f03c82b2378af3af5310d95669c9 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Sun, 10 Dec 2023 15:16:08 +0900 Subject: [PATCH 30/32] =?UTF-8?q?Docs:=20=EA=B0=95=EC=9D=98=20=EC=BB=A8?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20API=20=EC=84=A4=EB=AA=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#120=20-=20LectureContentApi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/lectureContent/api/LectureContentApi.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/api/LectureContentApi.java b/src/main/java/com/example/DoroServer/domain/lectureContent/api/LectureContentApi.java index ff56a7e4..1965900c 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/api/LectureContentApi.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/api/LectureContentApi.java @@ -11,6 +11,7 @@ import java.util.List; import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import org.springframework.security.access.annotation.Secured; import org.springframework.validation.annotation.Validated; @@ -25,7 +26,7 @@ import javax.validation.Valid; -@Api(tags = "강의 자료") +@Api(tags = "강의 컨텐츠") @RestController @RequestMapping("/lecture-contents") @RequiredArgsConstructor @@ -34,12 +35,14 @@ public class LectureContentApi { private final LectureContentService lectureContentService; private final LectureContentImageService lectureContentImageService; + @ApiOperation(value = "모든 강의 컨텐츠 조회", notes = "인증된 사용자만 조회 가능합니다.") @GetMapping public SuccessResponse findAllLectureContents() { List allLectureContents = lectureContentService.findAllLectureContents(); return SuccessResponse.successResponse(allLectureContents); } + @ApiOperation(value = "강의 컨텐츠 추가", notes = "관리자만 생성 가능합니다. 이미지는 1개라도 MultipartFile[]로 보내야 합니다.") @Secured("ROLE_ADMIN") @PostMapping public SuccessResponse createLectureContent(@ModelAttribute @Valid CreateLectureContentReq lectureContentReq) { @@ -47,9 +50,10 @@ public SuccessResponse createLectureContent(@ModelAttribute @Valid CreateLect return SuccessResponse.successResponse(createdLectureContent); } + @ApiOperation(value = "강의 컨텐츠 수정", notes = "관리자만 수정 가능합니다. 해당 엔드포인트에선 이미지를 제외한 나머지 필드들만 수정 가능합니다.") @Secured("ROLE_ADMIN") @PatchMapping("/{id}") - public SuccessResponse updateLectureContent( // 강의컨텐츠에서 이미지를 제외한 나머지 필드들만 수정 + public SuccessResponse updateLectureContent( @PathVariable("id") Long id, @ModelAttribute UpdateLectureContentReq updateLectureContentReq) { LectureContentRes updatedLectureContent = lectureContentService.updateLectureContent(id, @@ -57,6 +61,7 @@ public SuccessResponse updateLectureContent( // 강의컨텐츠에서 이미 return SuccessResponse.successResponse(updatedLectureContent); } + @ApiOperation(value = "강의 컨텐츠 삭제", notes = "관리자만 삭제 가능합니다.") @Secured("ROLE_ADMIN") @DeleteMapping("/{id}") public SuccessResponse deleteLectureContent(@PathVariable("id") Long id) { @@ -66,6 +71,7 @@ public SuccessResponse deleteLectureContent(@PathVariable("id") Long id) { /* 강의 컨텐츠의 이미지 추가 및 삭제 API */ + @ApiOperation(value = "강의 컨텐츠 이미지 추가", notes = "관리자만 추가 가능합니다. 이미지는 1개라도 MultipartFile[]로 보내야 합니다.") @Secured("ROLE_ADMIN") @PostMapping("/{id}/images") public SuccessResponse addLectureContentImages(@PathVariable("id") Long id, @@ -75,6 +81,7 @@ public SuccessResponse addLectureContentImages(@PathVariable("id") Long id, return SuccessResponse.successResponse(addedLectureContentImages); } + @ApiOperation(value = "강의 컨텐츠 이미지 삭제", notes = "관리자만 삭제 가능합니다. 강의 컨텐츠와 강의 컨텐츠의 이미지 id를 통해 삭제합니다.") @Secured("ROLE_ADMIN") @DeleteMapping("/{id}/images/{imageId}") public SuccessResponse deleteLectureContentImage(@PathVariable("id") Long id, From eec1148d737c3059bd91a2b1094a3eeeb98eb5c7 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Mon, 18 Dec 2023 18:43:51 +0900 Subject: [PATCH 31/32] =?UTF-8?q?Refact:=20=EB=A7=A4=EC=A7=81=EB=84=98?= =?UTF-8?q?=EB=B2=84=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20validate=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC=20-=20ContentImageS3Ser?= =?UTF-8?q?viceImpl=20-=20MultipartFileUtils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ContentImageS3ServiceImpl.java | 22 ++----------------- .../global/s3/MultipartFileUtils.java | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/example/DoroServer/global/s3/MultipartFileUtils.java diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java b/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java index 547f0f27..aaaf313d 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImpl.java @@ -6,8 +6,6 @@ import org.springframework.web.multipart.MultipartFile; import com.example.DoroServer.domain.lectureContentImage.entity.LectureContentImage; -import com.example.DoroServer.global.exception.BaseException; -import com.example.DoroServer.global.exception.Code; import com.example.DoroServer.global.s3.AwsS3ServiceImpl; import java.util.Arrays; @@ -19,30 +17,14 @@ public class ContentImageS3ServiceImpl implements ContentImageS3Service { private final AwsS3ServiceImpl awsS3Service; - void validate(MultipartFile[] files) { - if (files == null) { - // Multipart가 1개라도 배열에 넣어서 요청해야함 - throw new BaseException(Code.JSON_SYNTAX_ERROR); - } - if (files.length > 100 || files.length < 1) { - throw new BaseException(Code.LECTURE_CONTENT_INVAILD_IMAGE_COUNT); - } - for (MultipartFile file : files) { - if (file.getSize() > 10485760) { // 10MB - throw new BaseException(Code.LECTURE_CONTENT_IMAGE_SIZE_OVER); - } - } - } + private static final int fileNameLength = 40; // UUID(36)+확장자(4) String getFileNameFrom(String url) { - // UUID(36자) + .jpg(4자) = 파일명(40자) - return url.substring(url.length() - 40); + return url.substring(url.length() - fileNameLength); } @Override public List uploadS3Images(MultipartFile[] files) { - validate(files); - // S3 BUCKET/lecture-content에 파일 업로드 후, 업로드된 파일들의 URL 반환 return Arrays.stream(files) .map(file -> awsS3Service.upload(file, "lecture-content")) diff --git a/src/main/java/com/example/DoroServer/global/s3/MultipartFileUtils.java b/src/main/java/com/example/DoroServer/global/s3/MultipartFileUtils.java new file mode 100644 index 00000000..44d8dab1 --- /dev/null +++ b/src/main/java/com/example/DoroServer/global/s3/MultipartFileUtils.java @@ -0,0 +1,22 @@ +package com.example.DoroServer.global.s3; + +import org.springframework.web.multipart.MultipartFile; + +import com.example.DoroServer.global.exception.BaseException; +import com.example.DoroServer.global.exception.Code; + +public class MultipartFileUtils { + public static void validateMultipartFiles(MultipartFile[] files) { + if (files == null || files.length == 0) { + throw new BaseException(Code.JSON_SYNTAX_ERROR); + } + if (files.length > 100 || files.length < 1) { + throw new BaseException(Code.LECTURE_CONTENT_INVAILD_IMAGE_COUNT); + } + for (MultipartFile file : files) { + if (file.getSize() > 10485760) { // 10MB + throw new BaseException(Code.LECTURE_CONTENT_IMAGE_SIZE_OVER); + } + } + } +} From 7d14a33196e31445dff49cd4080b1c2931cefaa0 Mon Sep 17 00:00:00 2001 From: hot666666 Date: Mon, 18 Dec 2023 18:49:11 +0900 Subject: [PATCH 32/32] =?UTF-8?q?Fix:=20validate=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20CreateLectureContentReq=20-=20LectureContentImageReq=20-=20?= =?UTF-8?q?LectureContentService=20-=20LectureContentImageService=20-=20Co?= =?UTF-8?q?ntentImageS3ServiceImplTest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lectureContent/dto/CreateLectureContentReq.java | 6 ++++++ .../lectureContent/service/LectureContentService.java | 1 + .../lectureContentImage/dto/LectureContentImageReq.java | 5 +++++ .../service/LectureContentImageService.java | 1 + .../service/ContentImageS3ServiceImplTest.java | 9 +-------- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/dto/CreateLectureContentReq.java b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/CreateLectureContentReq.java index f52c1b7a..2723d082 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/dto/CreateLectureContentReq.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/dto/CreateLectureContentReq.java @@ -5,6 +5,8 @@ import org.springframework.web.multipart.MultipartFile; +import com.example.DoroServer.global.s3.MultipartFileUtils; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -33,4 +35,8 @@ public class CreateLectureContentReq { @NotNull private MultipartFile[] files; // 강의 컨텐츠 이미지 파일들 + public void validateFiles() { + MultipartFileUtils.validateMultipartFiles(this.files); + } + } diff --git a/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java b/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java index e746f604..3dd8fa7e 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContent/service/LectureContentService.java @@ -36,6 +36,7 @@ public List findAllLectureContents() { } public LectureContentRes createLectureContent(CreateLectureContentReq lectureContentReq) { + lectureContentReq.validateFiles(); LectureContent lectureContent = modelMapper.map(lectureContentReq, LectureContent.class); // lectureContentReq의 files를 S3에 업로드 후, 업로드된 파일들의 URL을 LectureContentImage로 변환 diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java index aba76e73..4ca25b2f 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/dto/LectureContentImageReq.java @@ -4,6 +4,8 @@ import org.springframework.web.multipart.MultipartFile; +import com.example.DoroServer.global.s3.MultipartFileUtils; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -19,4 +21,7 @@ public class LectureContentImageReq { @NotNull private MultipartFile[] files; // 강의 컨텐츠 이미지 파일들 -> 하나만 보내면 무시됨 + public void validateFiles() { + MultipartFileUtils.validateMultipartFiles(this.files); + } } diff --git a/src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java b/src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java index 2a4ccb4b..6518f365 100644 --- a/src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java +++ b/src/main/java/com/example/DoroServer/domain/lectureContentImage/service/LectureContentImageService.java @@ -31,6 +31,7 @@ public class LectureContentImageService { public List addLectureContentImages(Long id, LectureContentImageReq lectureContentImageReq) { + lectureContentImageReq.validateFiles(); LectureContent lectureContent = findLectureContentById(id); // lectureContentReq의 files를 S3에 업로드 후, 업로드된 파일들의 URL을 LectureContentImage로 변환 diff --git a/src/test/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImplTest.java b/src/test/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImplTest.java index ab90a5d5..8ef57875 100644 --- a/src/test/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImplTest.java +++ b/src/test/java/com/example/DoroServer/domain/lectureContent/service/ContentImageS3ServiceImplTest.java @@ -1,22 +1,15 @@ package com.example.DoroServer.domain.lectureContent.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.calls; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import java.util.List; -import java.util.Objects; -import java.util.Arrays; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -73,7 +66,7 @@ void validateTest0() { CreateLectureContentReq lectureContentReq = setUpLectureContentReq0(); // when & then - assertThrows(BaseException.class, () -> contentImageS3Service.validate(lectureContentReq.getFiles())); + assertThrows(BaseException.class, () -> lectureContentReq.validateFiles()); }