From 31f6c3c1f483d83a0f950d0e6edb9930030d3c20 Mon Sep 17 00:00:00 2001 From: jeondui Date: Wed, 27 Mar 2024 14:03:33 +0900 Subject: [PATCH 01/33] =?UTF-8?q?Refactor:=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EA=B2=80=EC=82=AC=20API=20=EB=B3=84=EB=8F=84=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 7 +++++++ .../dto/request/JoinNicknameRequest.java | 19 +++++++++++++++++ .../domain/member/service/MemberService.java | 21 ++++++++++--------- 3 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/api/readinglog/domain/member/controller/dto/request/JoinNicknameRequest.java diff --git a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java index 0577a88..9fb4c62 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java @@ -5,6 +5,7 @@ import com.api.readinglog.common.security.CustomUserDetail; import com.api.readinglog.common.security.util.CookieUtils; import com.api.readinglog.domain.member.controller.dto.request.DeleteRequest; +import com.api.readinglog.domain.member.controller.dto.request.JoinNicknameRequest; import com.api.readinglog.domain.member.controller.dto.request.JoinRequest; import com.api.readinglog.domain.member.controller.dto.request.LoginRequest; import com.api.readinglog.domain.member.controller.dto.request.UpdateProfileRequest; @@ -35,6 +36,12 @@ public class MemberController { private final MemberService memberService; + @PostMapping("/join-nickname") + public Response join_nickname(@ModelAttribute @Valid JoinNicknameRequest request) { + memberService.joinNickname(request); + return Response.success(HttpStatus.OK, "닉네임 검사 통과!"); + } + @PostMapping("/join") public Response join(@ModelAttribute @Valid JoinRequest request) { memberService.join(request); diff --git a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/JoinNicknameRequest.java b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/JoinNicknameRequest.java new file mode 100644 index 0000000..a56ebd4 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/JoinNicknameRequest.java @@ -0,0 +1,19 @@ +package com.api.readinglog.domain.member.controller.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class JoinNicknameRequest { + + @NotBlank(message = "닉네임은 필수 입력 값입니다.") + @Size(max = 8, message = "닉네임은 8자 이하로 입력해야 합니다.") + @Pattern(regexp = "^[\\p{L}0-9]+$", message = "닉네임에는 특수 문자를 사용할 수 없습니다.") + private String nickname; +} diff --git a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java index 2397a57..6bd8f01 100644 --- a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java +++ b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java @@ -2,34 +2,28 @@ import com.api.readinglog.common.aws.AmazonS3Service; import com.api.readinglog.common.exception.ErrorCode; -import com.api.readinglog.common.exception.custom.JwtException; import com.api.readinglog.common.exception.custom.MemberException; import com.api.readinglog.common.jwt.JwtToken; import com.api.readinglog.common.jwt.JwtTokenProvider; import com.api.readinglog.common.oauth.OAuth2RevokeService; -import com.api.readinglog.common.security.CustomUserDetail; import com.api.readinglog.common.security.util.JwtUtils; import com.api.readinglog.domain.member.controller.dto.request.DeleteRequest; +import com.api.readinglog.domain.member.controller.dto.request.JoinNicknameRequest; import com.api.readinglog.domain.member.controller.dto.request.JoinRequest; import com.api.readinglog.domain.member.controller.dto.request.LoginRequest; import com.api.readinglog.domain.member.controller.dto.request.UpdateProfileRequest; import com.api.readinglog.domain.member.controller.dto.response.MemberDetailsResponse; import com.api.readinglog.domain.member.entity.Member; import com.api.readinglog.domain.member.entity.MemberRole; -import com.api.readinglog.domain.token.repository.RefreshTokenRepository; import com.api.readinglog.domain.token.repository.SocialAccessTokenRepository; import com.api.readinglog.domain.member.repository.MemberRepository; import jakarta.servlet.http.HttpServletResponse; -import java.util.Collection; -import java.util.Collections; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -51,8 +45,13 @@ public class MemberService { private final OAuth2RevokeService oAuth2RevokeService; private final JwtUtils jwtUtils; + public void joinNickname(JoinNicknameRequest request) { + validateExistingNickname(request.getNickname()); + } + public void join(JoinRequest request) { - validateExistingMember(request.getEmail(), request.getNickname()); + validateExistingMember(request.getEmail()); + validateExistingNickname(request.getNickname()); validatePassword(request.getPassword(), request.getPasswordConfirm()); String encodedPassword = passwordEncoder.encode(request.getPassword()); @@ -139,12 +138,14 @@ private void revokeSocialAccessToken(Member member, String socialAccessToken) { } } - private void validateExistingMember(String email, String nickname) { + private void validateExistingMember(String email) { if (memberRepository.findByEmailAndRole(email, MemberRole.MEMBER_NORMAL).isPresent()) { throw new MemberException(ErrorCode.MEMBER_ALREADY_EXISTS); } + } - if (memberRepository.findByNickname(nickname).isPresent()) { + private void validateExistingNickname(String nickname) { + if(memberRepository.findByNickname(nickname).isPresent()) { throw new MemberException(ErrorCode.NICKNAME_ALREADY_EXISTS); } } From 3bc8c1b51575028c5c9b2ab964e2462217a1ca21 Mon Sep 17 00:00:00 2001 From: jeondui Date: Wed, 27 Mar 2024 14:39:33 +0900 Subject: [PATCH 02/33] =?UTF-8?q?Feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/ErrorCode.java | 1 + .../member/controller/MemberController.java | 10 ++++++++- .../dto/request/UpdatePasswordRequest.java | 21 +++++++++++++++++++ .../domain/member/entity/Member.java | 4 ++++ .../domain/member/service/MemberService.java | 12 +++++++++++ 5 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java diff --git a/src/main/java/com/api/readinglog/common/exception/ErrorCode.java b/src/main/java/com/api/readinglog/common/exception/ErrorCode.java index 100d506..86a8409 100644 --- a/src/main/java/com/api/readinglog/common/exception/ErrorCode.java +++ b/src/main/java/com/api/readinglog/common/exception/ErrorCode.java @@ -18,6 +18,7 @@ public enum ErrorCode { UNSUPPORTED_TOKEN("지원하지 않는 형식의 토큰입니다.", HttpStatus.UNAUTHORIZED), NOT_FOUND_TOKEN("토큰을 찾을 수 없습니다.", HttpStatus.UNAUTHORIZED), NOT_FOUND_REFRESH_TOKEN("해당 사용자의 리프레시 토큰을 찾을 수 없습니다.", HttpStatus.UNAUTHORIZED), + INVALID_CURRENT_PASSWORD("현재 비밀번호와 일치하지 않습니다.", HttpStatus.BAD_REQUEST), // 400 EMPTY_SEARCH_KEYWORD("검색어를 입력해주세요!", HttpStatus.BAD_REQUEST), diff --git a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java index 9fb4c62..8a0a535 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java @@ -8,6 +8,7 @@ import com.api.readinglog.domain.member.controller.dto.request.JoinNicknameRequest; import com.api.readinglog.domain.member.controller.dto.request.JoinRequest; import com.api.readinglog.domain.member.controller.dto.request.LoginRequest; +import com.api.readinglog.domain.member.controller.dto.request.UpdatePasswordRequest; import com.api.readinglog.domain.member.controller.dto.request.UpdateProfileRequest; import com.api.readinglog.domain.member.controller.dto.response.MemberDetailsResponse; import com.api.readinglog.domain.member.service.MemberService; @@ -100,7 +101,14 @@ public Response reissue(HttpServletRequest request, HttpServletResponse re // 재발급된 토큰 반환 response.addHeader("Authorization", newToken.getAccessToken()); CookieUtils.addCookie(response, "refreshToken", newToken.getRefreshToken(), 24 * 60 * 60 * 7); - return Response.success(HttpStatus.OK, "토큰 재발급 성공!"); + return Response.success(HttpStatus.OK, "토큰 재발급 성공!"); + } + + @PatchMapping("/password") + public Response updatePassword(@AuthenticationPrincipal CustomUserDetail user, + @RequestBody @Valid UpdatePasswordRequest request) { + memberService.updatePassword(user.getId(), request); + return Response.success(HttpStatus.OK, "비밀번호 변경 성공!"); } // HttpServletRequest에서 리프레시 토큰을 추출하는 메서드 diff --git a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java new file mode 100644 index 0000000..00ee8c6 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java @@ -0,0 +1,21 @@ +package com.api.readinglog.domain.member.controller.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class UpdatePasswordRequest { + private String currentPassword; + + @NotBlank(message = "비밀번호는 필수 입력 값입니다.") + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&]).{8,20}$", message = "비밀번호는 8~20자의 영문 대소문자, 숫자, 특수문자를 포함해야 합니다.") + private String newPassword; + + @NotBlank(message = "비밀번호 확인은 필수입니다.") + private String newPasswordConfirm; +} diff --git a/src/main/java/com/api/readinglog/domain/member/entity/Member.java b/src/main/java/com/api/readinglog/domain/member/entity/Member.java index dbdd5dd..beffcea 100644 --- a/src/main/java/com/api/readinglog/domain/member/entity/Member.java +++ b/src/main/java/com/api/readinglog/domain/member/entity/Member.java @@ -77,4 +77,8 @@ public void updateProfile(String nickname, String picture) { this.nickname = nickname; this.profileImg = picture; } + + public void updatePassword(String password) { + this.password = password; + } } diff --git a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java index 6bd8f01..2ac3371 100644 --- a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java +++ b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java @@ -11,6 +11,7 @@ import com.api.readinglog.domain.member.controller.dto.request.JoinNicknameRequest; import com.api.readinglog.domain.member.controller.dto.request.JoinRequest; import com.api.readinglog.domain.member.controller.dto.request.LoginRequest; +import com.api.readinglog.domain.member.controller.dto.request.UpdatePasswordRequest; import com.api.readinglog.domain.member.controller.dto.request.UpdateProfileRequest; import com.api.readinglog.domain.member.controller.dto.response.MemberDetailsResponse; import com.api.readinglog.domain.member.entity.Member; @@ -129,6 +130,17 @@ public JwtToken reissueToken(String refreshToken) { return jwtTokenProvider.reissueTokenByRefreshToken(refreshToken); } + public void updatePassword(Long memberId, UpdatePasswordRequest request) { + Member member = getMemberById(memberId); + + if (!passwordEncoder.matches(request.getCurrentPassword(), member.getPassword())) { + throw new MemberException(ErrorCode.INVALID_CURRENT_PASSWORD); + } + + validatePassword(request.getNewPassword(), request.getNewPasswordConfirm()); + member.updatePassword(passwordEncoder.encode(request.getNewPassword())); + } + // 소셜 계정 연동 해제 처리를 별도의 메서드로 추출 private void revokeSocialAccessToken(Member member, String socialAccessToken) { switch (member.getRole()) { From 7d29c4af12c001f15680c7fdefdadd3e119e4adf Mon Sep 17 00:00:00 2001 From: jeondui Date: Thu, 28 Mar 2024 01:09:38 +0900 Subject: [PATCH 03/33] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 ++ .../common/exception/ErrorCode.java | 4 +- .../exception/custom/EmailException.java | 11 ++ .../domain/email/config/EmailConfig.java | 40 +++++++ .../domain/email/config/SchedulerConfig.java | 9 ++ .../domain/email/service/EmailService.java | 105 ++++++++++++++++++ 6 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/api/readinglog/common/exception/custom/EmailException.java create mode 100644 src/main/java/com/api/readinglog/domain/email/config/EmailConfig.java create mode 100644 src/main/java/com/api/readinglog/domain/email/config/SchedulerConfig.java create mode 100644 src/main/java/com/api/readinglog/domain/email/service/EmailService.java diff --git a/build.gradle b/build.gradle index df75ad5..66df5c0 100644 --- a/build.gradle +++ b/build.gradle @@ -53,6 +53,13 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + + /* Email */ + implementation 'org.springframework.boot:spring-boot-starter-mail' + + /* Thymeleaf */ + implementation'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' } tasks.named('bootBuildImage') { diff --git a/src/main/java/com/api/readinglog/common/exception/ErrorCode.java b/src/main/java/com/api/readinglog/common/exception/ErrorCode.java index 86a8409..eb65e67 100644 --- a/src/main/java/com/api/readinglog/common/exception/ErrorCode.java +++ b/src/main/java/com/api/readinglog/common/exception/ErrorCode.java @@ -19,6 +19,7 @@ public enum ErrorCode { NOT_FOUND_TOKEN("토큰을 찾을 수 없습니다.", HttpStatus.UNAUTHORIZED), NOT_FOUND_REFRESH_TOKEN("해당 사용자의 리프레시 토큰을 찾을 수 없습니다.", HttpStatus.UNAUTHORIZED), INVALID_CURRENT_PASSWORD("현재 비밀번호와 일치하지 않습니다.", HttpStatus.BAD_REQUEST), + INVALID_AUTH_CODE("이메일 인증에 실패했습니다.", HttpStatus.BAD_REQUEST), // 400 EMPTY_SEARCH_KEYWORD("검색어를 입력해주세요!", HttpStatus.BAD_REQUEST), @@ -46,7 +47,8 @@ public enum ErrorCode { // 500 INTERNAL_SERVER_ERROR("서버 에러 발생!", HttpStatus.INTERNAL_SERVER_ERROR), AWS_S3_FILE_UPLOAD_FAIL("AWS S3 파일 업로드 실패", HttpStatus.INTERNAL_SERVER_ERROR), - AWS_S3_FILE_DELETE_FAIL("AWS S3 파일 삭제 실패", HttpStatus.INTERNAL_SERVER_ERROR),; + AWS_S3_FILE_DELETE_FAIL("AWS S3 파일 삭제 실패", HttpStatus.INTERNAL_SERVER_ERROR), + EMAIL_SEND_FAILED("이메일 발송 실패", HttpStatus.INTERNAL_SERVER_ERROR); private final String message; private final HttpStatus status; diff --git a/src/main/java/com/api/readinglog/common/exception/custom/EmailException.java b/src/main/java/com/api/readinglog/common/exception/custom/EmailException.java new file mode 100644 index 0000000..26539e1 --- /dev/null +++ b/src/main/java/com/api/readinglog/common/exception/custom/EmailException.java @@ -0,0 +1,11 @@ +package com.api.readinglog.common.exception.custom; + +import com.api.readinglog.common.exception.CustomException; +import com.api.readinglog.common.exception.ErrorCode; + +public class EmailException extends CustomException { + + public EmailException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/api/readinglog/domain/email/config/EmailConfig.java b/src/main/java/com/api/readinglog/domain/email/config/EmailConfig.java new file mode 100644 index 0000000..af4aa77 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/email/config/EmailConfig.java @@ -0,0 +1,40 @@ +package com.api.readinglog.domain.email.config; + +import java.util.Properties; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +@Configuration +public class EmailConfig { + @Value("${spring.mail.host}") + private String mailServerHost; + + @Value("${spring.mail.port}") + private String mailServerPort; + + @Value("${spring.mail.username}") + private String mailServerUsername; + + @Value("${spring.mail.password}") + private String mailServerPassword; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(mailServerHost); + mailSender.setPort(Integer.parseInt(mailServerPort)); + + mailSender.setUsername(mailServerUsername); + mailSender.setPassword(mailServerPassword); + + Properties properties = mailSender.getJavaMailProperties(); + properties.put("mail.transport.protocol", "smtp"); + properties.put("mail.smtp.auth", "true"); + properties.put("mail.smtp.starttls.enable", "true"); + + return mailSender; + } +} diff --git a/src/main/java/com/api/readinglog/domain/email/config/SchedulerConfig.java b/src/main/java/com/api/readinglog/domain/email/config/SchedulerConfig.java new file mode 100644 index 0000000..96d9ba6 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/email/config/SchedulerConfig.java @@ -0,0 +1,9 @@ +package com.api.readinglog.domain.email.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@Configuration +public class SchedulerConfig { +} diff --git a/src/main/java/com/api/readinglog/domain/email/service/EmailService.java b/src/main/java/com/api/readinglog/domain/email/service/EmailService.java new file mode 100644 index 0000000..af2a0b2 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/email/service/EmailService.java @@ -0,0 +1,105 @@ +package com.api.readinglog.domain.email.service; + +import com.api.readinglog.common.exception.ErrorCode; +import com.api.readinglog.common.exception.custom.EmailException; +import com.api.readinglog.domain.email.entity.EmailAuth; +import com.api.readinglog.domain.email.repository.EmailAuthRepository; +import com.api.readinglog.domain.member.entity.Member; +import com.api.readinglog.domain.member.service.MemberService; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import java.time.LocalDateTime; +import java.util.Random; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; + +@Service +@Slf4j +@RequiredArgsConstructor +@Transactional +public class EmailService { + + private final JavaMailSender javaMailSender; + private final SpringTemplateEngine templateEngine; + private final MemberService memberService; + private final EmailAuthRepository emailAuthRepository; + private final PasswordEncoder passwordEncoder; + + + public void sendAuthCode(String toEmail) { + String authCode = createRandomCode(); + emailAuthRepository.save(EmailAuth.of(toEmail, authCode)); + sendEmail(toEmail, authCode, "[리딩 로그] 이메일 인증 코드", "authCode.html"); + } + + public void sendTemporaryPassword(Long memberId, String toEmail) { + String tempPassword = createRandomCode(); + sendEmail(toEmail, tempPassword, "[리딩 로그] 임시 비밀번호", "tempPassword.html"); + + Member member = memberService.getMemberById(memberId); + member.updatePassword(passwordEncoder.encode(tempPassword)); + log.info("새로 발급 받은 비밀번호: {}", tempPassword); + } + + @Scheduled(fixedDelay = 60000) + public void deleteExpiredAuthCodes() { + emailAuthRepository.deleteByExpiryTimeBefore(LocalDateTime.now()); + } + + private void sendEmail(String toEmail, String code, String subject, String templateName) { + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + try { + MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8"); + messageHelper.setTo(toEmail); + messageHelper.setSubject(subject); + messageHelper.setText(setContext(code, templateName), true); + + javaMailSender.send(mimeMessage); + + } catch (MessagingException e) { + throw new EmailException(ErrorCode.EMAIL_SEND_FAILED); + } + } + + public void verifyAuthCode(String email, String authCode) { + emailAuthRepository.findByEmailAndAuthCode(email, authCode) + .orElseThrow(() -> new EmailException(ErrorCode.INVALID_AUTH_CODE)); + } + + // 인증번호 및 임시 비밀번호 생성 + private String createRandomCode() { + Random random = new Random(); + StringBuilder key = new StringBuilder(); + for (int i = 0; i < 8; i++) { + switch (random.nextInt(3)) { + case 0: + key.append((char) ((int) random.nextInt(26) + 97)); // 소문자 + break; + case 1: + key.append((char) ((int) random.nextInt(26) + 65)); // 대문자 + break; + case 2: + key.append(random.nextInt(10)); // 숫자 + break; + } + } + return key.toString(); + } + + private String setContext(String code, String templateName) { + Context context = new Context(); + log.info(code); + context.setVariable("code", code); + return templateEngine.process(templateName, context); + } + +} + From 9b6963d74170e2d5ecd721b36298cf06f59adb5a Mon Sep 17 00:00:00 2001 From: jeondui Date: Thu, 28 Mar 2024 01:09:55 +0900 Subject: [PATCH 04/33] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/templates/authCode.html | 25 ++++++++++++++++++ .../resources/templates/tempPassword.html | 26 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/main/resources/templates/authCode.html create mode 100644 src/main/resources/templates/tempPassword.html diff --git a/src/main/resources/templates/authCode.html b/src/main/resources/templates/authCode.html new file mode 100644 index 0000000..da9c93e --- /dev/null +++ b/src/main/resources/templates/authCode.html @@ -0,0 +1,25 @@ + + + + 이메일 인증 코드 + + +
+
+ Logo +
+
+

인증 코드 발급

+

안녕하세요! 리딩 로그 서비스 이용을 위한 인증 코드입니다.

+

아래 코드를 입력하여 이메일 인증을 완료해주세요.

+
+ 인증 코드 +
+
+
+

감사합니다.

+

리딩 로그 팀

+
+
+ + diff --git a/src/main/resources/templates/tempPassword.html b/src/main/resources/templates/tempPassword.html new file mode 100644 index 0000000..1337990 --- /dev/null +++ b/src/main/resources/templates/tempPassword.html @@ -0,0 +1,26 @@ + + + + 임시 비밀번호 안내 + + +
+
+ Logo +
+
+

임시 비밀번호 발급

+

안녕하세요! 리딩 로그 서비스의 보안을 위해 임시 비밀번호를 발급해 드립니다.

+

아래의 임시 비밀번호로 로그인한 후, 반드시 비밀번호를 변경해 주세요.

+
+ 임시 비밀번호 +
+
+
+

비밀번호 변경 후에도 계속해서 문제가 발생한다면, 고객 지원 센터로 문의해 주세요.

+

감사합니다.

+

리딩 로그 팀

+
+
+ + From eb792ad368bfe6746588e6957f61bbc3acd7875d Mon Sep 17 00:00:00 2001 From: jeondui Date: Thu, 28 Mar 2024 01:11:23 +0900 Subject: [PATCH 05/33] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=BD=94=EB=93=9C=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/AuthCodeVerificationRequest.java | 13 +++++++ .../domain/email/dto/EmailRequest.java | 11 ++++++ .../domain/email/entity/EmailAuth.java | 39 +++++++++++++++++++ .../email/repository/EmailAuthRepository.java | 12 ++++++ .../member/controller/MemberController.java | 16 ++++++++ .../domain/member/service/MemberService.java | 2 +- 6 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java create mode 100644 src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java create mode 100644 src/main/java/com/api/readinglog/domain/email/entity/EmailAuth.java create mode 100644 src/main/java/com/api/readinglog/domain/email/repository/EmailAuthRepository.java diff --git a/src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java b/src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java new file mode 100644 index 0000000..343afdc --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java @@ -0,0 +1,13 @@ +package com.api.readinglog.domain.email.dto; + +import jakarta.validation.constraints.Email; +import lombok.Getter; + +@Getter +public class AuthCodeVerificationRequest { + + @Email(message = "이메일 형식이 올바르지 않습니다.") + private String email; + + private String authCode; +} diff --git a/src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java b/src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java new file mode 100644 index 0000000..56e4aad --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java @@ -0,0 +1,11 @@ +package com.api.readinglog.domain.email.dto; + +import jakarta.validation.constraints.Email; +import lombok.Getter; + +@Getter +public class EmailRequest { + + @Email(message = "이메일 형식이 올바르지 않습니다.") + private String email; +} diff --git a/src/main/java/com/api/readinglog/domain/email/entity/EmailAuth.java b/src/main/java/com/api/readinglog/domain/email/entity/EmailAuth.java new file mode 100644 index 0000000..c99b600 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/email/entity/EmailAuth.java @@ -0,0 +1,39 @@ +package com.api.readinglog.domain.email.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EmailAuth { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String email; + private String authCode; + private LocalDateTime expiryTime; + + @Builder + private EmailAuth(String email, String authCode, LocalDateTime expiryTime) { + this.email = email; + this.authCode = authCode; + this.expiryTime = expiryTime; + } + + public static EmailAuth of(String email, String authCode) { + return EmailAuth.builder() + .email(email) + .authCode(authCode) + .expiryTime(LocalDateTime.now().plusMinutes(5)) // 5분 후 만료 + .build(); + } +} diff --git a/src/main/java/com/api/readinglog/domain/email/repository/EmailAuthRepository.java b/src/main/java/com/api/readinglog/domain/email/repository/EmailAuthRepository.java new file mode 100644 index 0000000..d16d1e6 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/email/repository/EmailAuthRepository.java @@ -0,0 +1,12 @@ +package com.api.readinglog.domain.email.repository; + +import com.api.readinglog.domain.email.entity.EmailAuth; +import java.time.LocalDateTime; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EmailAuthRepository extends JpaRepository { + Optional findByEmailAndAuthCode(String email, String authCode); + + void deleteByExpiryTimeBefore(LocalDateTime now); +} diff --git a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java index 8a0a535..b1ee27c 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java @@ -4,6 +4,9 @@ import com.api.readinglog.common.response.Response; import com.api.readinglog.common.security.CustomUserDetail; import com.api.readinglog.common.security.util.CookieUtils; +import com.api.readinglog.domain.email.dto.AuthCodeVerificationRequest; +import com.api.readinglog.domain.email.dto.EmailRequest; +import com.api.readinglog.domain.email.service.EmailService; import com.api.readinglog.domain.member.controller.dto.request.DeleteRequest; import com.api.readinglog.domain.member.controller.dto.request.JoinNicknameRequest; import com.api.readinglog.domain.member.controller.dto.request.JoinRequest; @@ -36,6 +39,7 @@ public class MemberController { private final MemberService memberService; + private final EmailService emailService; @PostMapping("/join-nickname") public Response join_nickname(@ModelAttribute @Valid JoinNicknameRequest request) { @@ -111,6 +115,18 @@ public Response updatePassword(@AuthenticationPrincipal CustomUserDetail u return Response.success(HttpStatus.OK, "비밀번호 변경 성공!"); } + @PostMapping("/send-authCode") + public Response sendEmailAuthCode(@RequestBody @Valid EmailRequest request) { + emailService.sendAuthCode(request.getEmail()); + return Response.success(HttpStatus.OK, "이메일 인증 코드 전송 완료!"); + } + + @PostMapping("/verify-authCode") + public Response verifyAuthCode(@RequestBody @Valid AuthCodeVerificationRequest request) { + emailService.verifyAuthCode(request.getEmail(), request.getAuthCode()); + return Response.success(HttpStatus.OK, "이메일 인증 성공!"); + } + // HttpServletRequest에서 리프레시 토큰을 추출하는 메서드 private String extractRefreshToken(HttpServletRequest request) { // 쿠키에서 리프레`시 토큰 이름으로 검색 diff --git a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java index 2ac3371..f2b8577 100644 --- a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java +++ b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java @@ -157,7 +157,7 @@ private void validateExistingMember(String email) { } private void validateExistingNickname(String nickname) { - if(memberRepository.findByNickname(nickname).isPresent()) { + if (memberRepository.findByNickname(nickname).isPresent()) { throw new MemberException(ErrorCode.NICKNAME_ALREADY_EXISTS); } } From 02b6edb39aa07bd72383ca9ac382cb0d8100d204 Mon Sep 17 00:00:00 2001 From: jeondui Date: Thu, 28 Mar 2024 01:11:52 +0900 Subject: [PATCH 06/33] =?UTF-8?q?Feat:=20=EC=9E=84=EC=8B=9C=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EB=B0=9C=EA=B8=89=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/controller/MemberController.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java index b1ee27c..2fed8a7 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java @@ -127,6 +127,13 @@ public Response verifyAuthCode(@RequestBody @Valid AuthCodeVerificationReq return Response.success(HttpStatus.OK, "이메일 인증 성공!"); } + @PostMapping("/send-temporaryPassword") + public Response sendEmailTempPassword(@AuthenticationPrincipal CustomUserDetail user, + @RequestBody @Valid EmailRequest request) { + emailService.sendTemporaryPassword(user.getId(), request.getEmail()); + return Response.success(HttpStatus.OK, "임시 비밀번호 전송 완료!"); + } + // HttpServletRequest에서 리프레시 토큰을 추출하는 메서드 private String extractRefreshToken(HttpServletRequest request) { // 쿠키에서 리프레`시 토큰 이름으로 검색 From 61ad2e9631809ff85c17be2eacf1022e0f2658b7 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 28 Mar 2024 15:09:43 +0900 Subject: [PATCH 07/33] =?UTF-8?q?Feat:=20=EC=B1=85=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제목 + 저자 검색에서 제목 검색으로 변경 - 한 페이지당 10개씩 조회 --- .../readinglog/domain/book/dto/BookSearchApiResponse.java | 1 - .../api/readinglog/domain/book/service/BookService.java | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/api/readinglog/domain/book/dto/BookSearchApiResponse.java b/src/main/java/com/api/readinglog/domain/book/dto/BookSearchApiResponse.java index 2ccffdc..3a39de5 100644 --- a/src/main/java/com/api/readinglog/domain/book/dto/BookSearchApiResponse.java +++ b/src/main/java/com/api/readinglog/domain/book/dto/BookSearchApiResponse.java @@ -9,7 +9,6 @@ @Setter public class BookSearchApiResponse { - // TODO: 무한 스크롤 구현에 필요한 데이터 추가 private int totalResults; // 전체 데이터 개수 private int startIndex; // 시작 페이지 private int itemsPerPage; // 페이지당 아이템 개수 diff --git a/src/main/java/com/api/readinglog/domain/book/service/BookService.java b/src/main/java/com/api/readinglog/domain/book/service/BookService.java index 2c1f92d..a5c9fc9 100644 --- a/src/main/java/com/api/readinglog/domain/book/service/BookService.java +++ b/src/main/java/com/api/readinglog/domain/book/service/BookService.java @@ -51,15 +51,14 @@ public BookSearchApiResponse searchBooks(String query, int start) { throw new BookException(ErrorCode.EMPTY_SEARCH_KEYWORD); } - // TODO: 독서 기록이 있는 책인 경우 독서 기록도 응답에 포함시켜 전달. BookSearchApiResponse response = webClient.get() .uri(uriBuilder -> uriBuilder .path("/ItemSearch.aspx") - .queryParam("Query", query) // 검색어 - .queryParam("QueryType", "Keyword") // 제목 + 저자로 검색 + .queryParam("Query", query) + .queryParam("QueryType", "Title") // 제목으로 검색 .queryParam("SearchTarget", "Book") // 검색 대상: 도서 .queryParam("Start", start) // 시작 페이지: 1 - .queryParam("MaxResults", "20") // 페이지 당 검색 결과: 20개 + .queryParam("MaxResults", "10") // 페이지 당 검색 결과: 10개 .queryParam("Sort", "Accuracy") // 관련도순 정렬 .build() ) From ad4bcf61305889d3e9507320ae0c90516a57eba9 Mon Sep 17 00:00:00 2001 From: jeondui Date: Thu, 28 Mar 2024 15:28:56 +0900 Subject: [PATCH 08/33] =?UTF-8?q?Refactor:=20=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=20=EC=A0=84=EC=86=A1=20=EB=B0=A9=EC=8B=9D=20=EB=B9=84=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../readinglog/domain/email/service/EmailService.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/api/readinglog/domain/email/service/EmailService.java b/src/main/java/com/api/readinglog/domain/email/service/EmailService.java index af2a0b2..b7c017a 100644 --- a/src/main/java/com/api/readinglog/domain/email/service/EmailService.java +++ b/src/main/java/com/api/readinglog/domain/email/service/EmailService.java @@ -14,6 +14,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -23,6 +25,7 @@ @Service @Slf4j +@EnableAsync @RequiredArgsConstructor @Transactional public class EmailService { @@ -33,20 +36,20 @@ public class EmailService { private final EmailAuthRepository emailAuthRepository; private final PasswordEncoder passwordEncoder; - + @Async public void sendAuthCode(String toEmail) { String authCode = createRandomCode(); emailAuthRepository.save(EmailAuth.of(toEmail, authCode)); sendEmail(toEmail, authCode, "[리딩 로그] 이메일 인증 코드", "authCode.html"); } + @Async public void sendTemporaryPassword(Long memberId, String toEmail) { String tempPassword = createRandomCode(); sendEmail(toEmail, tempPassword, "[리딩 로그] 임시 비밀번호", "tempPassword.html"); Member member = memberService.getMemberById(memberId); member.updatePassword(passwordEncoder.encode(tempPassword)); - log.info("새로 발급 받은 비밀번호: {}", tempPassword); } @Scheduled(fixedDelay = 60000) @@ -54,7 +57,8 @@ public void deleteExpiredAuthCodes() { emailAuthRepository.deleteByExpiryTimeBefore(LocalDateTime.now()); } - private void sendEmail(String toEmail, String code, String subject, String templateName) { + @Async + public void sendEmail(String toEmail, String code, String subject, String templateName) { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); try { MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8"); From b4cf04d718640ede21e91032233ebaa2abdb8d9d Mon Sep 17 00:00:00 2001 From: jeondui Date: Thu, 28 Mar 2024 15:29:24 +0900 Subject: [PATCH 09/33] =?UTF-8?q?Refactor:=20=EC=BF=A0=ED=82=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B0=92=20=EC=B6=94=EC=B6=9C=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/ErrorCode.java | 5 ++-- .../common/security/util/CookieUtils.java | 29 +++++-------------- .../member/controller/MemberController.java | 24 ++------------- 3 files changed, 12 insertions(+), 46 deletions(-) diff --git a/src/main/java/com/api/readinglog/common/exception/ErrorCode.java b/src/main/java/com/api/readinglog/common/exception/ErrorCode.java index eb65e67..333a886 100644 --- a/src/main/java/com/api/readinglog/common/exception/ErrorCode.java +++ b/src/main/java/com/api/readinglog/common/exception/ErrorCode.java @@ -10,6 +10,9 @@ public enum ErrorCode { // 400 PASSWORD_MISMATCH("비밀번호가 일치하지 않습니다.", HttpStatus.BAD_REQUEST), + INVALID_CURRENT_PASSWORD("현재 비밀번호와 일치하지 않습니다.", HttpStatus.BAD_REQUEST), + INVALID_AUTH_CODE("이메일 인증에 실패했습니다.", HttpStatus.BAD_REQUEST), + NOT_FOUND_REFRESH_TOKEN_IN_COOKIE("리프레시 토큰이 쿠키에 없습니다.", HttpStatus.BAD_REQUEST), // 401 UNAUTHORIZED_LOGIN("로그인 실패: 인증에 실패하였습니다.", HttpStatus.UNAUTHORIZED), @@ -18,8 +21,6 @@ public enum ErrorCode { UNSUPPORTED_TOKEN("지원하지 않는 형식의 토큰입니다.", HttpStatus.UNAUTHORIZED), NOT_FOUND_TOKEN("토큰을 찾을 수 없습니다.", HttpStatus.UNAUTHORIZED), NOT_FOUND_REFRESH_TOKEN("해당 사용자의 리프레시 토큰을 찾을 수 없습니다.", HttpStatus.UNAUTHORIZED), - INVALID_CURRENT_PASSWORD("현재 비밀번호와 일치하지 않습니다.", HttpStatus.BAD_REQUEST), - INVALID_AUTH_CODE("이메일 인증에 실패했습니다.", HttpStatus.BAD_REQUEST), // 400 EMPTY_SEARCH_KEYWORD("검색어를 입력해주세요!", HttpStatus.BAD_REQUEST), diff --git a/src/main/java/com/api/readinglog/common/security/util/CookieUtils.java b/src/main/java/com/api/readinglog/common/security/util/CookieUtils.java index 852eba6..a863f07 100644 --- a/src/main/java/com/api/readinglog/common/security/util/CookieUtils.java +++ b/src/main/java/com/api/readinglog/common/security/util/CookieUtils.java @@ -1,27 +1,15 @@ package com.api.readinglog.common.security.util; +import com.api.readinglog.common.exception.ErrorCode; +import com.api.readinglog.common.exception.custom.JwtException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Base64; -import java.util.Optional; import org.springframework.util.SerializationUtils; public class CookieUtils { - public static Optional getCookie(HttpServletRequest request, String name) { - Cookie[] cookies = request.getCookies(); - - if (cookies != null && cookies.length > 0) { - for (Cookie cookie : cookies) { - if (name.equals(cookie.getName())) { - return Optional.of(cookie); - } - } - } - return Optional.empty(); - } - public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) { Cookie cookie = new Cookie(name, value); cookie.setPath("/"); @@ -31,19 +19,16 @@ public static void addCookie(HttpServletResponse response, String name, String v response.addCookie(cookie); } - public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { + public static String extractRefreshToken(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); - - if (cookies != null && cookies.length > 0) { + if (cookies != null) { for (Cookie cookie : cookies) { - if (name.equals(cookie.getName())) { - cookie.setValue(""); - cookie.setPath("/"); - cookie.setMaxAge(0); - response.addCookie(cookie); + if (cookie.getName().equals("refreshToken")) { + return cookie.getValue(); } } } + throw new JwtException(ErrorCode.NOT_FOUND_REFRESH_TOKEN_IN_COOKIE); } public static String serialize(Object obj) { diff --git a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java index 2fed8a7..4360dd1 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java @@ -15,7 +15,6 @@ import com.api.readinglog.domain.member.controller.dto.request.UpdateProfileRequest; import com.api.readinglog.domain.member.controller.dto.response.MemberDetailsResponse; import com.api.readinglog.domain.member.service.MemberService; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -76,7 +75,7 @@ public Response updateProfile(@AuthenticationPrincipal CustomUserDetail us @PostMapping("/logout") public Response logout(HttpServletRequest request, HttpServletResponse response) { - String refreshToken = extractRefreshToken(request); + String refreshToken = CookieUtils.extractRefreshToken(request); memberService.logout(refreshToken, response); return Response.success(HttpStatus.OK, "로그아웃 성공!"); } @@ -96,13 +95,8 @@ public Response deleteSocialMember(@AuthenticationPrincipal CustomUserDeta @GetMapping("/reissue") public Response reissue(HttpServletRequest request, HttpServletResponse response) { - // 쿠키에서 리프레시 토큰 가져오기 - String refreshToken = extractRefreshToken(request); - - // 리프레시 토큰을 사용하여 새로운 토큰 재발급 + String refreshToken = CookieUtils.extractRefreshToken(request); JwtToken newToken = memberService.reissueToken(refreshToken); - - // 재발급된 토큰 반환 response.addHeader("Authorization", newToken.getAccessToken()); CookieUtils.addCookie(response, "refreshToken", newToken.getRefreshToken(), 24 * 60 * 60 * 7); return Response.success(HttpStatus.OK, "토큰 재발급 성공!"); @@ -133,18 +127,4 @@ public Response sendEmailTempPassword(@AuthenticationPrincipal CustomUserD emailService.sendTemporaryPassword(user.getId(), request.getEmail()); return Response.success(HttpStatus.OK, "임시 비밀번호 전송 완료!"); } - - // HttpServletRequest에서 리프레시 토큰을 추출하는 메서드 - private String extractRefreshToken(HttpServletRequest request) { - // 쿠키에서 리프레`시 토큰 이름으로 검색 - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals("refreshToken")) { - return cookie.getValue(); - } - } - } - throw new IllegalStateException("리프레시 토큰이 쿠키에 없습니다."); - } } From 7df84ce97abc38dfb5461e21f312c2e0420c723e Mon Sep 17 00:00:00 2001 From: jeondui Date: Thu, 28 Mar 2024 16:16:01 +0900 Subject: [PATCH 10/33] =?UTF-8?q?Refactor:=20=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=20=EC=BD=94=EB=93=9C=20Redis=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Scheduler 삭제 - 만료시간 5분 설정 --- build.gradle | 3 ++ .../common/redis/config/RedisConfig.java | 36 +++++++++++++++++ .../common/redis/service/RedisService.java | 34 ++++++++++++++++ .../domain/email/config/SchedulerConfig.java | 9 ----- .../domain/email/entity/EmailAuth.java | 39 ------------------- .../email/repository/EmailAuthRepository.java | 12 ------ .../domain/email/service/EmailService.java | 27 +++++++------ 7 files changed, 88 insertions(+), 72 deletions(-) create mode 100644 src/main/java/com/api/readinglog/common/redis/config/RedisConfig.java create mode 100644 src/main/java/com/api/readinglog/common/redis/service/RedisService.java delete mode 100644 src/main/java/com/api/readinglog/domain/email/config/SchedulerConfig.java delete mode 100644 src/main/java/com/api/readinglog/domain/email/entity/EmailAuth.java delete mode 100644 src/main/java/com/api/readinglog/domain/email/repository/EmailAuthRepository.java diff --git a/build.gradle b/build.gradle index 66df5c0..3db55e1 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,9 @@ dependencies { /* Thymeleaf */ implementation'org.springframework.boot:spring-boot-starter-thymeleaf' implementation'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' + + /* Redis */ + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('bootBuildImage') { diff --git a/src/main/java/com/api/readinglog/common/redis/config/RedisConfig.java b/src/main/java/com/api/readinglog/common/redis/config/RedisConfig.java new file mode 100644 index 0000000..37192e9 --- /dev/null +++ b/src/main/java/com/api/readinglog/common/redis/config/RedisConfig.java @@ -0,0 +1,36 @@ +package com.api.readinglog.common.redis.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@EnableRedisRepositories +public class RedisConfig { + + @Value("${spring.redis.host}") + private String redisHost; + + @Value("${spring.redis.port}") + private int redisPort; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisHost, redisPort); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + return redisTemplate; + } +} diff --git a/src/main/java/com/api/readinglog/common/redis/service/RedisService.java b/src/main/java/com/api/readinglog/common/redis/service/RedisService.java new file mode 100644 index 0000000..7f7e936 --- /dev/null +++ b/src/main/java/com/api/readinglog/common/redis/service/RedisService.java @@ -0,0 +1,34 @@ +package com.api.readinglog.common.redis.service; + +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor + +public class RedisService { + + private final RedisTemplate redisTemplate; + + + public void setData(String key, Object value, Long time, TimeUnit timeUnit) { + redisTemplate.opsForValue().set(key, value.toString(), time, timeUnit); + } + + public Object getData(String key) { + return redisTemplate.opsForValue().get(key); + } + + + public void deleteData(String key) { + redisTemplate.delete(key); + } + + + public void increaseData(String key) { + redisTemplate.opsForValue().increment(key); + } + +} diff --git a/src/main/java/com/api/readinglog/domain/email/config/SchedulerConfig.java b/src/main/java/com/api/readinglog/domain/email/config/SchedulerConfig.java deleted file mode 100644 index 96d9ba6..0000000 --- a/src/main/java/com/api/readinglog/domain/email/config/SchedulerConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.api.readinglog.domain.email.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableScheduling; - -@EnableScheduling -@Configuration -public class SchedulerConfig { -} diff --git a/src/main/java/com/api/readinglog/domain/email/entity/EmailAuth.java b/src/main/java/com/api/readinglog/domain/email/entity/EmailAuth.java deleted file mode 100644 index c99b600..0000000 --- a/src/main/java/com/api/readinglog/domain/email/entity/EmailAuth.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.api.readinglog.domain.email.entity; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import java.time.LocalDateTime; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class EmailAuth { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - private String email; - private String authCode; - private LocalDateTime expiryTime; - - @Builder - private EmailAuth(String email, String authCode, LocalDateTime expiryTime) { - this.email = email; - this.authCode = authCode; - this.expiryTime = expiryTime; - } - - public static EmailAuth of(String email, String authCode) { - return EmailAuth.builder() - .email(email) - .authCode(authCode) - .expiryTime(LocalDateTime.now().plusMinutes(5)) // 5분 후 만료 - .build(); - } -} diff --git a/src/main/java/com/api/readinglog/domain/email/repository/EmailAuthRepository.java b/src/main/java/com/api/readinglog/domain/email/repository/EmailAuthRepository.java deleted file mode 100644 index d16d1e6..0000000 --- a/src/main/java/com/api/readinglog/domain/email/repository/EmailAuthRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.api.readinglog.domain.email.repository; - -import com.api.readinglog.domain.email.entity.EmailAuth; -import java.time.LocalDateTime; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface EmailAuthRepository extends JpaRepository { - Optional findByEmailAndAuthCode(String email, String authCode); - - void deleteByExpiryTimeBefore(LocalDateTime now); -} diff --git a/src/main/java/com/api/readinglog/domain/email/service/EmailService.java b/src/main/java/com/api/readinglog/domain/email/service/EmailService.java index b7c017a..4e61e83 100644 --- a/src/main/java/com/api/readinglog/domain/email/service/EmailService.java +++ b/src/main/java/com/api/readinglog/domain/email/service/EmailService.java @@ -2,21 +2,20 @@ import com.api.readinglog.common.exception.ErrorCode; import com.api.readinglog.common.exception.custom.EmailException; -import com.api.readinglog.domain.email.entity.EmailAuth; -import com.api.readinglog.domain.email.repository.EmailAuthRepository; +import com.api.readinglog.common.redis.service.RedisService; import com.api.readinglog.domain.member.entity.Member; import com.api.readinglog.domain.member.service.MemberService; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; -import java.time.LocalDateTime; +import java.util.Optional; import java.util.Random; +import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,13 +32,13 @@ public class EmailService { private final JavaMailSender javaMailSender; private final SpringTemplateEngine templateEngine; private final MemberService memberService; - private final EmailAuthRepository emailAuthRepository; + private final RedisService redisService; private final PasswordEncoder passwordEncoder; @Async public void sendAuthCode(String toEmail) { String authCode = createRandomCode(); - emailAuthRepository.save(EmailAuth.of(toEmail, authCode)); + saveAuthCode(toEmail, authCode); sendEmail(toEmail, authCode, "[리딩 로그] 이메일 인증 코드", "authCode.html"); } @@ -52,11 +51,6 @@ public void sendTemporaryPassword(Long memberId, String toEmail) { member.updatePassword(passwordEncoder.encode(tempPassword)); } - @Scheduled(fixedDelay = 60000) - public void deleteExpiredAuthCodes() { - emailAuthRepository.deleteByExpiryTimeBefore(LocalDateTime.now()); - } - @Async public void sendEmail(String toEmail, String code, String subject, String templateName) { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); @@ -74,7 +68,8 @@ public void sendEmail(String toEmail, String code, String subject, String templa } public void verifyAuthCode(String email, String authCode) { - emailAuthRepository.findByEmailAndAuthCode(email, authCode) + findByEmailAndAuthCode(authCode) + .filter(e -> e.equals(email)) .orElseThrow(() -> new EmailException(ErrorCode.INVALID_AUTH_CODE)); } @@ -105,5 +100,13 @@ private String setContext(String code, String templateName) { return templateEngine.process(templateName, context); } + private void saveAuthCode(String email, String authCode) { + redisService.setData(authCode, email, 5L, TimeUnit.MINUTES); // 5분 설정 + } + + private Optional findByEmailAndAuthCode(String authCode) { + Object email = redisService.getData(authCode); + return Optional.ofNullable(email != null ? email.toString() : null); + } } From acd68107cddb857ad8d84eacbf3e995f86cb5ca8 Mon Sep 17 00:00:00 2001 From: jeondui Date: Thu, 28 Mar 2024 17:03:38 +0900 Subject: [PATCH 11/33] =?UTF-8?q?Refactor:=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=82=AC=EC=A7=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../readinglog/common/aws/AmazonS3Service.java | 5 +++++ .../domain/member/service/MemberService.java | 15 +++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/api/readinglog/common/aws/AmazonS3Service.java b/src/main/java/com/api/readinglog/common/aws/AmazonS3Service.java index 43f2a0d..83c843f 100644 --- a/src/main/java/com/api/readinglog/common/aws/AmazonS3Service.java +++ b/src/main/java/com/api/readinglog/common/aws/AmazonS3Service.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -26,6 +27,10 @@ public class AmazonS3Service { @Value("${cloud.aws.s3.bucket}") private String bucket; + @Getter + @Value("${cloud.aws.s3.default.profile.image}") + private String defaultProfileImg; + public String uploadFile(MultipartFile profileImg) { String fileName = generateFileName(profileImg.getOriginalFilename()); diff --git a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java index f2b8577..181827f 100644 --- a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java +++ b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java @@ -56,10 +56,9 @@ public void join(JoinRequest request) { validatePassword(request.getPassword(), request.getPasswordConfirm()); String encodedPassword = passwordEncoder.encode(request.getPassword()); - String uploadFileName = determineProfileImageUrl(request.getProfileImage()); + String uploadFileName = determineProfileImgUrl(request.getProfileImage()); Member member = Member.of(request, encodedPassword, uploadFileName); - log.debug("회원 프로필 사진 이름: {}", uploadFileName); memberRepository.save(member); } @@ -81,7 +80,6 @@ public MemberDetailsResponse getMemberDetails(Long memberId) { Member member = getMemberById(memberId); String profileImg = member.getProfileImg(); - // 소셜 로그인 한 회원이 아닌 경우에만 S3에서 이미지 객체 URL을 가져옴 if (member.getRole().equals(MemberRole.MEMBER_NORMAL)) { profileImg = amazonS3Service.getFileUrl(member.getProfileImg()); } @@ -91,7 +89,6 @@ public MemberDetailsResponse getMemberDetails(Long memberId) { public void updateProfile(Long memberId, UpdateProfileRequest request) { Member member = getMemberById(memberId); - // 기존 이미지를 기본 값으로 설정 String updatedFileName = member.getProfileImg(); if (!isEmptyProfileImg(request.getProfileImg())) { @@ -141,7 +138,6 @@ public void updatePassword(Long memberId, UpdatePasswordRequest request) { member.updatePassword(passwordEncoder.encode(request.getNewPassword())); } - // 소셜 계정 연동 해제 처리를 별도의 메서드로 추출 private void revokeSocialAccessToken(Member member, String socialAccessToken) { switch (member.getRole()) { case MEMBER_KAKAO -> oAuth2RevokeService.revokeKakao(socialAccessToken); @@ -178,12 +174,11 @@ private Authentication getUserAuthentication(LoginRequest request) { } } - private String determineProfileImageUrl(MultipartFile profileImage) { - /* TODO: 프로필 사진 요청이 없는 경우, 기본 프로필 저장 */ - if (profileImage == null || profileImage.isEmpty()) { - return "기본 프로필 이미지 URL"; + private String determineProfileImgUrl(MultipartFile profileImg) { + if (profileImg == null || profileImg.isEmpty()) { + return amazonS3Service.getDefaultProfileImg(); } else { - return amazonS3Service.uploadFile(profileImage); + return amazonS3Service.uploadFile(profileImg); } } From fde55030af228489dc92b9d48674456c71fa3313 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 28 Mar 2024 17:58:43 +0900 Subject: [PATCH 12/33] =?UTF-8?q?Feat:=20S3=20=EC=97=85=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B3=B5=ED=86=B5=ED=99=94=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원, 책 도메인에서 함께 사용할 수 있게 수정 - 도메인 타입으로 폴더 이름 결정하도록 수정 --- .../common/aws/AmazonS3Service.java | 59 ++++++++----------- .../api/readinglog/common/aws/DomainType.java | 13 ++++ 2 files changed, 37 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/api/readinglog/common/aws/DomainType.java diff --git a/src/main/java/com/api/readinglog/common/aws/AmazonS3Service.java b/src/main/java/com/api/readinglog/common/aws/AmazonS3Service.java index 43f2a0d..ef221de 100644 --- a/src/main/java/com/api/readinglog/common/aws/AmazonS3Service.java +++ b/src/main/java/com/api/readinglog/common/aws/AmazonS3Service.java @@ -7,9 +7,11 @@ import com.amazonaws.services.s3.model.PutObjectRequest; import com.api.readinglog.common.exception.ErrorCode; import com.api.readinglog.common.exception.custom.AwsS3Exception; +import com.api.readinglog.common.image.ImageUtil; import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -26,61 +28,48 @@ public class AmazonS3Service { @Value("${cloud.aws.s3.bucket}") private String bucket; - public String uploadFile(MultipartFile profileImg) { - String fileName = generateFileName(profileImg.getOriginalFilename()); + public String uploadFile(MultipartFile file, DomainType type) { + + String ext = ImageUtil.getExt(file.getOriginalFilename()); // 확장자 + String fileName = UUID.randomUUID() + "." + ext; // 파일 이름 + 확장자 + String imageFilePath = generateFilePath(fileName, type); // 타입/날짜/파일이름 try { ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(profileImg.getSize()); - metadata.setContentType(profileImg.getContentType()); + metadata.setContentLength(file.getSize()); + metadata.setContentType(file.getContentType()); - uploadToS3(bucket, fileName, profileImg, metadata); - return fileName; + uploadToS3(bucket, imageFilePath, file, metadata); + return imageFilePath; } catch (IOException e) { throw new AwsS3Exception(ErrorCode.AWS_S3_FILE_UPLOAD_FAIL); } } - // TODO: 회원, 책 이미지 업로드 시 공통으로 사용할 수 있게 리팩토링 필요함. - public String uploadBookCover(MultipartFile profileImg) { - String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); - String fileName = String.format("books/%s/%s", currentDate, profileImg.getOriginalFilename()); - - try { - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(profileImg.getSize()); - metadata.setContentType(profileImg.getContentType()); - - uploadToS3(bucket, fileName, profileImg, metadata); - return fileName; - - } catch (IOException e) { - throw new AwsS3Exception(ErrorCode.AWS_S3_FILE_UPLOAD_FAIL); - } + private void uploadToS3(String bucket, String filePath, MultipartFile file, ObjectMetadata metadata) + throws IOException { + s3Client.putObject(new PutObjectRequest(bucket, filePath, file.getInputStream(), metadata)); } - public void deleteFile(String fileName) { + public void deleteFile(String filePath) { try { - s3Client.deleteObject(new DeleteObjectRequest(bucket, fileName)); - log.debug("삭제한 이미지 파일 이름: {}", fileName); + s3Client.deleteObject(new DeleteObjectRequest(bucket, filePath)); + log.debug("삭제한 이미지 파일의 경로: {}", filePath); } catch (AmazonServiceException e) { throw new AwsS3Exception(ErrorCode.AWS_S3_FILE_DELETE_FAIL); } } - public String getFileUrl(String fileName) { - return s3Client.getUrl(bucket, fileName).toString(); - } - - private String generateFileName(String originalFileName) { - String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); - return String.format("members/%s/%s", currentDate, originalFileName); + // [type]/[yyyy_MM_dd]/[파일 이름] 형식의 경로 반환 메서드 + private String generateFilePath(String fileName, DomainType type) { + String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy_MM_dd")); + return String.format("%s/%s/%s", type.getType(), currentDate, fileName); } - private void uploadToS3(String bucket, String fileName, MultipartFile file, ObjectMetadata metadata) - throws IOException { - s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), metadata)); + // 전체 이미지 주소 반환 메서드 + public String getFileUrl(String fileName) { + return s3Client.getUrl(bucket, fileName).toString(); } } diff --git a/src/main/java/com/api/readinglog/common/aws/DomainType.java b/src/main/java/com/api/readinglog/common/aws/DomainType.java new file mode 100644 index 0000000..22c609b --- /dev/null +++ b/src/main/java/com/api/readinglog/common/aws/DomainType.java @@ -0,0 +1,13 @@ +package com.api.readinglog.common.aws; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum DomainType { + + MEMBERS("members"), BOOK("books"); + + private final String type; +} From 2d6e32122a8156ec6722406c8d006e4fcafc07df Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 28 Mar 2024 17:59:18 +0900 Subject: [PATCH 13/33] =?UTF-8?q?Feat:=20image=20util=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미지 파일과 관련된 처리를 해주는 유틸 클래스 구현 --- .../readinglog/common/image/ImageUtil.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/com/api/readinglog/common/image/ImageUtil.java diff --git a/src/main/java/com/api/readinglog/common/image/ImageUtil.java b/src/main/java/com/api/readinglog/common/image/ImageUtil.java new file mode 100644 index 0000000..ff405e6 --- /dev/null +++ b/src/main/java/com/api/readinglog/common/image/ImageUtil.java @@ -0,0 +1,21 @@ +package com.api.readinglog.common.image; + +import java.util.Optional; +import org.springframework.web.multipart.MultipartFile; + +public class ImageUtil { + + // 파일의 확장자를 반환해주는 메서드 + public static String getExt(String fileName) { + return Optional.ofNullable(fileName) + .filter(f -> f.contains(".")) + .map(f -> f.substring(fileName.lastIndexOf(".") + 1)) + .orElse(""); + } + + // 이미지 존재 여부 확인 + public static boolean isEmptyProfileImg(MultipartFile file) { + return (file == null || file.isEmpty()); + } + +} From 5dcd92b743df669fbdc420dddda95faea121526e Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 28 Mar 2024 18:00:14 +0900 Subject: [PATCH 14/33] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 변경된 s3 업로드 메서드로 교체 --- .../readinglog/domain/book/service/BookService.java | 11 ++++++----- .../domain/member/service/MemberService.java | 11 +++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/api/readinglog/domain/book/service/BookService.java b/src/main/java/com/api/readinglog/domain/book/service/BookService.java index a5c9fc9..d9b1604 100644 --- a/src/main/java/com/api/readinglog/domain/book/service/BookService.java +++ b/src/main/java/com/api/readinglog/domain/book/service/BookService.java @@ -1,9 +1,11 @@ package com.api.readinglog.domain.book.service; import com.api.readinglog.common.aws.AmazonS3Service; +import com.api.readinglog.common.aws.DomainType; import com.api.readinglog.common.exception.ErrorCode; import com.api.readinglog.common.exception.custom.BookException; import com.api.readinglog.common.exception.custom.MemberException; +import com.api.readinglog.common.image.ImageUtil; import com.api.readinglog.domain.book.dto.BookDetailResponse; import com.api.readinglog.domain.book.dto.BookDirectRequest; import com.api.readinglog.domain.book.dto.BookModifyRequest; @@ -17,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; import org.springframework.web.reactive.function.client.WebClient; @Service @@ -83,7 +84,7 @@ public void registerBookAfterSearch(Long memberId, BookRegisterRequest request) // 책 직접 등록 public void registerBookDirect(Long memberId, BookDirectRequest request) { Member member = memberService.getMemberById(memberId); - String cover = amazonS3Service.uploadBookCover(request.getCover()); + String cover = amazonS3Service.uploadFile(request.getCover(), DomainType.BOOK); bookRepository.save(Book.of(member, request, cover)); } @@ -94,10 +95,10 @@ public void modifyBook(Long memberId, Long bookId, BookModifyRequest bookModifyR // 파일이 존재하면 기존 이미지 삭제 후 새로운 이미지 업로드 String cover = book.getCover(); - MultipartFile coverImg = bookModifyRequest.getCover(); - if (!(coverImg == null || coverImg.isEmpty())) { + + if (!ImageUtil.isEmptyProfileImg(bookModifyRequest.getCover())) { amazonS3Service.deleteFile(cover); - cover = amazonS3Service.uploadBookCover(bookModifyRequest.getCover()); + cover = amazonS3Service.uploadFile(bookModifyRequest.getCover(), DomainType.BOOK); } book.modify(bookModifyRequest, cover); diff --git a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java index f2b8577..877f7c0 100644 --- a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java +++ b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java @@ -1,8 +1,10 @@ package com.api.readinglog.domain.member.service; import com.api.readinglog.common.aws.AmazonS3Service; +import com.api.readinglog.common.aws.DomainType; import com.api.readinglog.common.exception.ErrorCode; import com.api.readinglog.common.exception.custom.MemberException; +import com.api.readinglog.common.image.ImageUtil; import com.api.readinglog.common.jwt.JwtToken; import com.api.readinglog.common.jwt.JwtTokenProvider; import com.api.readinglog.common.oauth.OAuth2RevokeService; @@ -94,10 +96,10 @@ public void updateProfile(Long memberId, UpdateProfileRequest request) { // 기존 이미지를 기본 값으로 설정 String updatedFileName = member.getProfileImg(); - if (!isEmptyProfileImg(request.getProfileImg())) { + if (!ImageUtil.isEmptyProfileImg(request.getProfileImg())) { // 수정할 이미지 데이터가 존재할 경우, 기존 이미지 삭제 후 새 이미지 업로드 amazonS3Service.deleteFile(member.getProfileImg()); - updatedFileName = amazonS3Service.uploadFile(request.getProfileImg()); + updatedFileName = amazonS3Service.uploadFile(request.getProfileImg(), DomainType.MEMBERS); } member.updateProfile(request.getNickname(), updatedFileName); } @@ -183,11 +185,8 @@ private String determineProfileImageUrl(MultipartFile profileImage) { if (profileImage == null || profileImage.isEmpty()) { return "기본 프로필 이미지 URL"; } else { - return amazonS3Service.uploadFile(profileImage); + return amazonS3Service.uploadFile(profileImage, DomainType.MEMBERS); } } - private boolean isEmptyProfileImg(MultipartFile profileImg) { - return (profileImg == null || profileImg.isEmpty()); - } } From 6eab6fe0df82814f7c620bed08e410bb58f9c9aa Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 28 Mar 2024 18:14:43 +0900 Subject: [PATCH 15/33] =?UTF-8?q?Feat:=20ImageUtil=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 메서드 이름과 결과가 반대되기 때문에 이름을 명확하게 수정 --- src/main/java/com/api/readinglog/common/image/ImageUtil.java | 4 ++-- .../com/api/readinglog/domain/book/service/BookService.java | 4 ++-- .../api/readinglog/domain/member/service/MemberService.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/api/readinglog/common/image/ImageUtil.java b/src/main/java/com/api/readinglog/common/image/ImageUtil.java index ff405e6..8268ead 100644 --- a/src/main/java/com/api/readinglog/common/image/ImageUtil.java +++ b/src/main/java/com/api/readinglog/common/image/ImageUtil.java @@ -14,8 +14,8 @@ public static String getExt(String fileName) { } // 이미지 존재 여부 확인 - public static boolean isEmptyProfileImg(MultipartFile file) { - return (file == null || file.isEmpty()); + public static boolean isNotEmptyImageFile(MultipartFile file) { + return !(file == null || file.isEmpty()); } } diff --git a/src/main/java/com/api/readinglog/domain/book/service/BookService.java b/src/main/java/com/api/readinglog/domain/book/service/BookService.java index d9b1604..3adca68 100644 --- a/src/main/java/com/api/readinglog/domain/book/service/BookService.java +++ b/src/main/java/com/api/readinglog/domain/book/service/BookService.java @@ -93,10 +93,10 @@ public void modifyBook(Long memberId, Long bookId, BookModifyRequest bookModifyR Member member = memberService.getMemberById(memberId); Book book = getBookById(bookId); - // 파일이 존재하면 기존 이미지 삭제 후 새로운 이미지 업로드 String cover = book.getCover(); - if (!ImageUtil.isEmptyProfileImg(bookModifyRequest.getCover())) { + // 파일이 존재하면 기존 이미지 삭제 후 새로운 이미지 업로드 + if (ImageUtil.isNotEmptyImageFile(bookModifyRequest.getCover())) { amazonS3Service.deleteFile(cover); cover = amazonS3Service.uploadFile(bookModifyRequest.getCover(), DomainType.BOOK); } diff --git a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java index 877f7c0..57ef57e 100644 --- a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java +++ b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java @@ -96,7 +96,7 @@ public void updateProfile(Long memberId, UpdateProfileRequest request) { // 기존 이미지를 기본 값으로 설정 String updatedFileName = member.getProfileImg(); - if (!ImageUtil.isEmptyProfileImg(request.getProfileImg())) { + if (ImageUtil.isNotEmptyImageFile(request.getProfileImg())) { // 수정할 이미지 데이터가 존재할 경우, 기존 이미지 삭제 후 새 이미지 업로드 amazonS3Service.deleteFile(member.getProfileImg()); updatedFileName = amazonS3Service.uploadFile(request.getProfileImg(), DomainType.MEMBERS); From 3560819a9d69655ee894d4abe390b38defa41cbd Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 28 Mar 2024 18:15:35 +0900 Subject: [PATCH 16/33] =?UTF-8?q?Feat:=20data.sql=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 개발 환경에서 쓸 회원, 책, 한줄평 데이터 쿼리 작성 --- src/main/resources/data.sql | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/main/resources/data.sql diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..027b23c --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,67 @@ +insert into member(member_id, member_email, member_nickname, member_password, member_profile_img, member_role, + created_at, modified_at, deleted_at) +values (1, 'dongmin@naver.com', '동민', '{noop}1234', 'default.png', 'member_normal', now(), now(), null); + +# 책 +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (1, 1, null, 'Report, The (Gozaresh)', 'hmival0', 'Anheuser-Busch Inbev SA', 'Oyondu', 'http://dummyimage.com/159x100.png/ff4444/ffffff', '2024-03-13 07:37:09', '2024-03-05 20:23:56', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (2, 1, null, 'Formula, The', 'tcrookshank1', 'Viad Corp', 'Mydeo', 'http://dummyimage.com/116x100.png/dddddd/000000', '2024-03-12 23:27:43', '2024-03-09 00:25:25', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (3, 1, null, 'He Got Game', 'afouch2', 'CyberOptics Corporation', 'Quimm', 'http://dummyimage.com/145x100.png/5fa2dd/ffffff', '2024-03-07 00:11:29', '2024-03-14 14:18:40', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (4, 1, null, 'Lone Ranger and the Lost City of Gold, The', 'mshanks3', 'Amedica Corporation', 'Skinte', 'http://dummyimage.com/131x100.png/ff4444/ffffff', '2024-03-07 04:37:17', '2024-03-06 01:11:36', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (5, 1, null, 'Detropia', 'uweld4', 'Civitas Solutions, Inc.', 'Skiba', 'http://dummyimage.com/229x100.png/5fa2dd/ffffff', '2024-03-20 01:48:37', '2024-03-21 13:40:16', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (6, 1, null, 'Murph: The Protector', 'ddovydenas5', 'Deluxe Corporation', 'Jatri', 'http://dummyimage.com/142x100.png/ff4444/ffffff', '2024-03-03 17:31:32', '2024-03-24 04:39:18', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (7, 1, null, 'But Forever in My Mind', 'mnewsham6', 'TFS Financial Corporation', 'Centizu', 'http://dummyimage.com/131x100.png/ff4444/ffffff', '2024-03-21 17:54:32', '2024-03-22 17:02:55', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (8, 1, null, 'Always', 'bterrelly7', 'Biostage, Inc.', 'Chatterbridge', 'http://dummyimage.com/181x100.png/5fa2dd/ffffff', '2024-03-21 16:53:22', '2024-03-11 02:39:23', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (9, 1, null, 'Not Fade Away', 'jstrainge8', 'Rush Enterprises, Inc.', 'Yakitri', 'http://dummyimage.com/189x100.png/5fa2dd/ffffff', '2024-03-08 00:40:02', '2024-03-03 14:06:00', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (10, 1, null, 'Caddyshack', 'jwhalebelly9', 'Capitol Federal Financial, Inc.', 'Photolist', 'http://dummyimage.com/134x100.png/5fa2dd/ffffff', '2024-03-21 05:26:57', '2024-03-02 01:27:43', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (11, 1, null, 'Marina Abramovic: The Artist Is Present', 'swalasa', 'Diana Shipping inc.', 'Blogtag', 'http://dummyimage.com/131x100.png/ff4444/ffffff', '2024-03-12 09:37:19', '2024-03-06 02:40:11', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (12, 1, null, 'The Legend of Sarila', 'bmccrillisb', 'Duke Energy Corporation', 'Livepath', 'http://dummyimage.com/134x100.png/ff4444/ffffff', '2024-03-19 19:21:41', '2024-03-20 13:44:13', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (13, 1, null, 'No Man''s Land', 'abeckenhamc', 'Frontier Communications Corporation', 'Buzzdog', 'http://dummyimage.com/103x100.png/ff4444/ffffff', '2024-03-10 13:03:22', '2024-03-21 10:17:48', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (14, 1, null, 'Submarine', 'mlindd', 'CECO Environmental Corp.', 'Zava', 'http://dummyimage.com/238x100.png/ff4444/ffffff', '2024-03-09 16:28:03', '2024-03-04 04:59:15', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (15, 1, null, 'Hard Way, The', 'dhuburne', 'SuperValu Inc.', 'Izio', 'http://dummyimage.com/241x100.png/dddddd/000000', '2024-03-21 21:56:51', '2024-03-23 06:28:12', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (16, 1, null, 'Beerfest', 'epeppinf', 'Galmed Pharmaceuticals Ltd.', 'Meembee', 'http://dummyimage.com/119x100.png/dddddd/000000', '2024-03-01 05:48:07', '2024-03-25 22:05:39', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (17, 1, null, 'Trailer Park Boys: Countdown to Liquor Day', 'bdunstallg', 'CBIZ, Inc.', 'Zoombox', 'http://dummyimage.com/219x100.png/5fa2dd/ffffff', '2024-03-21 17:50:09', '2024-03-25 23:47:07', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (18, 1, null, 'Watcher in the Woods, The', 'tbanbrookh', 'Cushing Renaissance Fund (The)', 'Jamia', 'http://dummyimage.com/144x100.png/cc0000/ffffff', '2024-03-25 07:25:33', '2024-03-19 02:03:49', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (19, 1, null, 'Zouzou', 'dcuddyi', 'Morgan Stanley Emerging Markets Debt Fund, Inc.', 'Zoomdog', 'http://dummyimage.com/201x100.png/dddddd/000000', '2024-03-22 11:21:04', '2024-03-11 01:10:12', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (20, 1, null, 'Movie Days (Bíódagar)', 'flanchberyj', 'Arconic Inc.', 'Flipopia', 'http://dummyimage.com/237x100.png/5fa2dd/ffffff', '2024-03-09 01:08:44', '2024-03-21 00:34:04', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (21, 1, null, 'Mon Oncle Antoine', 'bmeakesk', 'KKR & Co. L.P.', 'Meevee', 'http://dummyimage.com/118x100.png/5fa2dd/ffffff', '2024-03-25 18:52:16', '2024-03-02 13:05:48', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (22, 1, null, 'Bandslam', 'aromneyl', 'Martin Midstream Partners L.P.', 'Midel', 'http://dummyimage.com/126x100.png/cc0000/ffffff', '2024-03-26 14:44:33', '2024-03-17 23:21:25', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (23, 1, null, 'Nil By Mouth', 'mbagniukm', 'BJ''s Restaurants, Inc.', 'Quaxo', 'http://dummyimage.com/142x100.png/cc0000/ffffff', '2024-03-27 21:33:25', '2024-03-17 02:53:59', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (24, 1, null, 'Birds of America', 'mstarfordn', 'Wingstop Inc.', 'Voolia', 'http://dummyimage.com/222x100.png/cc0000/ffffff', '2024-03-05 05:13:12', '2024-03-03 21:03:16', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (25, 1, null, 'Sex Ed', 'boxenhamo', 'Oasmia Pharmaceutical AB', 'Tagpad', 'http://dummyimage.com/207x100.png/ff4444/ffffff', '2024-03-05 20:36:18', '2024-03-27 04:01:20', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (26, 1, null, 'An Amazing Couple', 'mbeekp', 'Kelly Services, Inc.', 'Roombo', 'http://dummyimage.com/244x100.png/5fa2dd/ffffff', '2024-03-16 23:05:46', '2024-03-26 06:17:03', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (27, 1, null, 'House of Cards', 'hbeevorsq', 'Valeant Pharmaceuticals International, Inc.', 'Latz', 'http://dummyimage.com/189x100.png/5fa2dd/ffffff', '2024-03-11 07:56:31', '2024-03-16 16:40:43', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (28, 1, null, 'Calling, The', 'obullentr', 'Amec Plc Ord', 'Buzzster', 'http://dummyimage.com/198x100.png/cc0000/ffffff', '2024-03-01 19:56:07', '2024-03-21 14:44:27', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (29, 1, null, 'Rugrats Go Wild!', 'cvains', 'Applied Industrial Technologies, Inc.', 'Tagfeed', 'http://dummyimage.com/110x100.png/cc0000/ffffff', '2024-03-26 05:43:02', '2024-03-09 14:44:01', null); +insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (30, 1, null, 'Postal', 'tmarchellot', 'SMTC Corporation', 'Skalith', 'http://dummyimage.com/118x100.png/ff4444/ffffff', '2024-03-01 09:03:05', '2024-03-17 02:16:17', null); + +# 한줄평 +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (1, 1, 1, 'Reduced responsive capability', '2024-03-23 12:38:32', '2024-03-11 05:27:00', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (2, 1, 2, 'Progressive discrete function', '2024-03-17 21:47:54', '2024-03-24 03:19:14', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (3, 1, 3, 'Polarised executive alliance', '2024-03-24 05:58:57', '2024-03-09 16:40:30', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (4, 1, 4, 'Visionary demand-driven task-force', '2024-03-01 16:45:31', '2024-03-22 15:01:29', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (5, 1, 5, 'Down-sized secondary extranet', '2024-03-01 00:36:25', '2024-03-13 10:05:25', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (6, 1, 6, 'Adaptive dynamic encryption', '2024-03-11 10:11:26', '2024-03-08 12:31:54', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (7, 1, 7, 'Centralized foreground infrastructure', '2024-03-19 17:21:49', '2024-03-27 22:25:53', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (8, 1, 8, 'Self-enabling attitude-oriented infrastructure', '2024-03-06 12:54:45', '2024-03-11 17:25:18', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (9, 1, 9, 'Synergistic composite collaboration', '2024-03-01 13:34:40', '2024-03-09 02:21:06', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (10, 1, 10, 'Reverse-engineered composite alliance', '2024-03-03 22:52:57', '2024-03-10 13:10:55', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (11, 1, 11, 'User-friendly directional framework', '2024-03-27 08:24:11', '2024-03-16 22:26:56', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (12, 1, 12, 'Vision-oriented web-enabled implementation', '2024-03-04 11:42:17', '2024-03-13 21:51:15', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (13, 1, 13, 'Cross-group responsive system engine', '2024-03-24 02:37:51', '2024-03-21 01:48:50', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (14, 1, 14, 'Programmable 3rd generation complexity', '2024-03-27 21:27:27', '2024-03-05 09:36:17', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (15, 1, 15, 'Reactive demand-driven flexibility', '2024-03-15 16:13:26', '2024-03-01 21:10:14', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (16, 1, 16, 'Expanded client-driven alliance', '2024-03-05 21:17:27', '2024-03-26 15:59:06', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (17, 1, 17, 'Distributed systematic strategy', '2024-03-18 08:15:56', '2024-03-06 13:41:46', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (18, 1, 18, 'Persevering national leverage', '2024-03-17 12:04:24', '2024-03-05 23:20:15', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (19, 1, 19, 'Multi-layered dedicated customer loyalty', '2024-03-19 16:56:50', '2024-03-21 21:40:20', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (20, 1, 20, 'Self-enabling full-range budgetary management', '2024-03-20 02:34:51', '2024-03-26 19:56:27', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (21, 1, 21, 'Intuitive systematic benchmark', '2024-03-14 00:30:15', '2024-03-20 22:50:15', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (22, 1, 22, 'Ergonomic mission-critical service-desk', '2024-03-16 11:53:23', '2024-03-19 10:39:26', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (23, 1, 23, 'Progressive heuristic leverage', '2024-03-04 09:16:11', '2024-03-14 22:24:36', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (24, 1, 24, 'Extended attitude-oriented support', '2024-03-02 13:26:35', '2024-03-22 19:57:15', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (25, 1, 25, 'Monitored dedicated toolset', '2024-03-18 05:57:08', '2024-03-26 22:23:44', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (26, 1, 26, 'Synergistic attitude-oriented firmware', '2024-03-04 02:24:47', '2024-03-01 14:56:16', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (27, 1, 27, 'Ameliorated impactful firmware', '2024-03-24 21:39:35', '2024-03-27 08:10:35', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (28, 1, 28, 'Streamlined mobile capacity', '2024-03-24 12:31:10', '2024-03-11 19:23:06', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (29, 1, 29, 'Cross-group intermediate superstructure', '2024-03-19 07:58:29', '2024-03-17 03:57:50', null); +insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (30, 1, 30, 'De-engineered demand-driven open architecture', '2024-03-13 14:06:05', '2024-03-02 14:13:32', null); From 99b1d6fdc54a6c8d4ae49fa062e78db6e4db32dc Mon Sep 17 00:00:00 2001 From: jeondui Date: Mon, 1 Apr 2024 01:51:56 +0900 Subject: [PATCH 17/33] =?UTF-8?q?fix:=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/api/readinglog/domain/member/service/MemberService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java index aedf3bd..0e80293 100644 --- a/src/main/java/com/api/readinglog/domain/member/service/MemberService.java +++ b/src/main/java/com/api/readinglog/domain/member/service/MemberService.java @@ -180,7 +180,7 @@ private String determineProfileImgUrl(MultipartFile profileImg) { if (profileImg == null || profileImg.isEmpty()) { return amazonS3Service.getDefaultProfileImg(); } else { - return amazonS3Service.uploadFile(profileImage, DomainType.MEMBERS); + return amazonS3Service.uploadFile(profileImg, DomainType.MEMBERS); } } From 3d07617ac63d4c8a81715c33cd6d6c2f0ee8cfb8 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Mon, 1 Apr 2024 16:39:53 +0900 Subject: [PATCH 18/33] =?UTF-8?q?Feat:=20=EB=8F=85=EC=84=9C=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EC=9E=91=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 시작일, 종료일을 입력받아 독서한 날짜를 추가 --- .../record/controller/RecordController.java | 36 ++++++++++ .../controller/dto/RecordWriteRequest.java | 20 ++++++ .../domain/record/entity/Record.java | 67 +++++++++++++++++++ .../record/repository/RecordRepository.java | 7 ++ .../domain/record/service/RecordService.java | 31 +++++++++ 5 files changed, 161 insertions(+) create mode 100644 src/main/java/com/api/readinglog/domain/record/controller/RecordController.java create mode 100644 src/main/java/com/api/readinglog/domain/record/controller/dto/RecordWriteRequest.java create mode 100644 src/main/java/com/api/readinglog/domain/record/entity/Record.java create mode 100644 src/main/java/com/api/readinglog/domain/record/repository/RecordRepository.java create mode 100644 src/main/java/com/api/readinglog/domain/record/service/RecordService.java diff --git a/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java b/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java new file mode 100644 index 0000000..d2b0ca6 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java @@ -0,0 +1,36 @@ +package com.api.readinglog.domain.record.controller; + +import com.api.readinglog.common.response.Response; +import com.api.readinglog.common.security.CustomUserDetail; +import com.api.readinglog.domain.record.controller.dto.RecordWriteRequest; +import com.api.readinglog.domain.record.service.RecordService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/records") +@RequiredArgsConstructor +@Slf4j +public class RecordController { + + private final RecordService recordService; + + @PostMapping("/{bookId}") + public Response addRecord(@AuthenticationPrincipal CustomUserDetail user, + @PathVariable Long bookId, + @RequestBody @Valid RecordWriteRequest recordWriteRequest) { + + recordService.write(user.getId(), bookId, recordWriteRequest); + return Response.success(HttpStatus.OK, "독서 기록 추가 성공"); + } +} diff --git a/src/main/java/com/api/readinglog/domain/record/controller/dto/RecordWriteRequest.java b/src/main/java/com/api/readinglog/domain/record/controller/dto/RecordWriteRequest.java new file mode 100644 index 0000000..5a8792b --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/record/controller/dto/RecordWriteRequest.java @@ -0,0 +1,20 @@ +package com.api.readinglog.domain.record.controller.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class RecordWriteRequest { + + @NotNull(message = "독서 시작일은 필수값 입니다.") + private LocalDateTime startDate; + + @NotNull(message = "독서 종료일은 필수값 입니다.") + private LocalDateTime endDate; +} diff --git a/src/main/java/com/api/readinglog/domain/record/entity/Record.java b/src/main/java/com/api/readinglog/domain/record/entity/Record.java new file mode 100644 index 0000000..70e0035 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/record/entity/Record.java @@ -0,0 +1,67 @@ +package com.api.readinglog.domain.record.entity; + + +import com.api.readinglog.common.base.BaseTimeEntity; +import com.api.readinglog.domain.book.entity.Book; +import com.api.readinglog.domain.member.entity.Member; +import com.api.readinglog.domain.record.controller.dto.RecordWriteRequest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE record SET deleted_at = NOW() WHERE record_id = ?") +@Where(clause = "deleted_at IS NULL") +public class Record extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "record_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "book_id") + private Book book; + + @Column(name = "record_start_date", nullable = false) + private LocalDateTime startDate; + + @Column(name = "record_end_date", nullable = false) + private LocalDateTime endDate; + + @Builder + public Record(Member member, Book book, LocalDateTime startDate, LocalDateTime endDate) { + this.member = member; + this.book = book; + this.startDate = startDate; + this.endDate = endDate; + } + + public static Record of(Member member, Book book, RecordWriteRequest request) { + return Record.builder() + .member(member) + .book(book) + .startDate(request.getStartDate()) + .endDate(request.getEndDate()) + .build(); + } + +} diff --git a/src/main/java/com/api/readinglog/domain/record/repository/RecordRepository.java b/src/main/java/com/api/readinglog/domain/record/repository/RecordRepository.java new file mode 100644 index 0000000..46e80a7 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/record/repository/RecordRepository.java @@ -0,0 +1,7 @@ +package com.api.readinglog.domain.record.repository; + +import com.api.readinglog.domain.record.entity.Record; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RecordRepository extends JpaRepository { +} diff --git a/src/main/java/com/api/readinglog/domain/record/service/RecordService.java b/src/main/java/com/api/readinglog/domain/record/service/RecordService.java new file mode 100644 index 0000000..87f6e30 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/record/service/RecordService.java @@ -0,0 +1,31 @@ +package com.api.readinglog.domain.record.service; + +import com.api.readinglog.domain.book.entity.Book; +import com.api.readinglog.domain.book.service.BookService; +import com.api.readinglog.domain.member.entity.Member; +import com.api.readinglog.domain.member.service.MemberService; +import com.api.readinglog.domain.record.controller.dto.RecordWriteRequest; +import com.api.readinglog.domain.record.entity.Record; +import com.api.readinglog.domain.record.repository.RecordRepository; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class RecordService { + + private final RecordRepository recordRepository; + private final MemberService memberService; + private final BookService bookService; + + public void write(long memberId, long bookId, RecordWriteRequest request) { + Member member = memberService.getMemberById(memberId); + Book book = bookService.getBookById(bookId); + + recordRepository.save(Record.of(member, book, request)); + } + +} From 5746643a41709b4c56e2313a375a5dcc877d14cf Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Mon, 1 Apr 2024 16:40:47 +0900 Subject: [PATCH 19/33] =?UTF-8?q?Feat:=20data.sql=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 주석이 다음줄 까지 영향을 줘서 삭제 --- src/main/resources/data.sql | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 027b23c..9825368 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -2,7 +2,6 @@ insert into member(member_id, member_email, member_nickname, member_password, me created_at, modified_at, deleted_at) values (1, 'dongmin@naver.com', '동민', '{noop}1234', 'default.png', 'member_normal', now(), now(), null); -# 책 insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (1, 1, null, 'Report, The (Gozaresh)', 'hmival0', 'Anheuser-Busch Inbev SA', 'Oyondu', 'http://dummyimage.com/159x100.png/ff4444/ffffff', '2024-03-13 07:37:09', '2024-03-05 20:23:56', null); insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (2, 1, null, 'Formula, The', 'tcrookshank1', 'Viad Corp', 'Mydeo', 'http://dummyimage.com/116x100.png/dddddd/000000', '2024-03-12 23:27:43', '2024-03-09 00:25:25', null); insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (3, 1, null, 'He Got Game', 'afouch2', 'CyberOptics Corporation', 'Quimm', 'http://dummyimage.com/145x100.png/5fa2dd/ffffff', '2024-03-07 00:11:29', '2024-03-14 14:18:40', null); @@ -34,7 +33,6 @@ insert into book (book_id, member_id, book_item_id, book_title, book_author, boo insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (29, 1, null, 'Rugrats Go Wild!', 'cvains', 'Applied Industrial Technologies, Inc.', 'Tagfeed', 'http://dummyimage.com/110x100.png/cc0000/ffffff', '2024-03-26 05:43:02', '2024-03-09 14:44:01', null); insert into book (book_id, member_id, book_item_id, book_title, book_author, book_publisher, book_category, book_cover, created_at, modified_at, deleted_at) values (30, 1, null, 'Postal', 'tmarchellot', 'SMTC Corporation', 'Skalith', 'http://dummyimage.com/118x100.png/ff4444/ffffff', '2024-03-01 09:03:05', '2024-03-17 02:16:17', null); -# 한줄평 insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (1, 1, 1, 'Reduced responsive capability', '2024-03-23 12:38:32', '2024-03-11 05:27:00', null); insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (2, 1, 2, 'Progressive discrete function', '2024-03-17 21:47:54', '2024-03-24 03:19:14', null); insert into summary (summary_id, member_id, book_id, summary_content, created_at, modified_at, deleted_at) values (3, 1, 3, 'Polarised executive alliance', '2024-03-24 05:58:57', '2024-03-09 16:40:30', null); From 6e79bfd552d3af9a52e603b509d5eebd7c8ad01c Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Mon, 1 Apr 2024 16:58:29 +0900 Subject: [PATCH 20/33] =?UTF-8?q?Feat:=20=EB=8F=85=EC=84=9C=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 책에 기록한 독서 날짜를 조회 - 조회 결과가 없는 경우에는 404 예외 반환 --- .../common/exception/ErrorCode.java | 1 + .../exception/custom/RecordException.java | 10 +++++++ .../record/controller/RecordController.java | 14 +++++++--- .../dto/{ => request}/RecordWriteRequest.java | 5 +--- .../dto/response/RecordResponse.java | 27 +++++++++++++++++++ .../domain/record/entity/Record.java | 2 +- .../record/repository/RecordRepository.java | 5 ++++ .../domain/record/service/RecordService.java | 23 ++++++++++++++-- 8 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/api/readinglog/common/exception/custom/RecordException.java rename src/main/java/com/api/readinglog/domain/record/controller/dto/{ => request}/RecordWriteRequest.java (73%) create mode 100644 src/main/java/com/api/readinglog/domain/record/controller/dto/response/RecordResponse.java diff --git a/src/main/java/com/api/readinglog/common/exception/ErrorCode.java b/src/main/java/com/api/readinglog/common/exception/ErrorCode.java index 333a886..0d8408c 100644 --- a/src/main/java/com/api/readinglog/common/exception/ErrorCode.java +++ b/src/main/java/com/api/readinglog/common/exception/ErrorCode.java @@ -38,6 +38,7 @@ public enum ErrorCode { NOT_FOUND_HIGHLIGHT("등록된 책이 존재하지 않습니다!", HttpStatus.NOT_FOUND), NOT_FOUND_SUMMARY("등록된 한줄평이 존재하지 않습니다!", HttpStatus.NOT_FOUND), NOT_FOUND_REVIEW("서평이 존재하지 않습니다!", HttpStatus.NOT_FOUND), + NOT_FOUND_RECORD("독서 기록이 존재하지 않습니다!", HttpStatus.NOT_FOUND), NOT_FOUND_FEED("피드 목록이 존재하지 않습니다!", HttpStatus.NOT_FOUND), // 409 diff --git a/src/main/java/com/api/readinglog/common/exception/custom/RecordException.java b/src/main/java/com/api/readinglog/common/exception/custom/RecordException.java new file mode 100644 index 0000000..4b3f619 --- /dev/null +++ b/src/main/java/com/api/readinglog/common/exception/custom/RecordException.java @@ -0,0 +1,10 @@ +package com.api.readinglog.common.exception.custom; + +import com.api.readinglog.common.exception.CustomException; +import com.api.readinglog.common.exception.ErrorCode; + +public class RecordException extends CustomException { + public RecordException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java b/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java index d2b0ca6..c853c16 100644 --- a/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java +++ b/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java @@ -2,15 +2,16 @@ import com.api.readinglog.common.response.Response; import com.api.readinglog.common.security.CustomUserDetail; -import com.api.readinglog.domain.record.controller.dto.RecordWriteRequest; +import com.api.readinglog.domain.record.controller.dto.request.RecordWriteRequest; +import com.api.readinglog.domain.record.controller.dto.response.RecordResponse; import com.api.readinglog.domain.record.service.RecordService; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; -import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -33,4 +34,11 @@ public Response addRecord(@AuthenticationPrincipal CustomUserDetail user, recordService.write(user.getId(), bookId, recordWriteRequest); return Response.success(HttpStatus.OK, "독서 기록 추가 성공"); } + + @GetMapping("/{bookId}") + public Response> getRecord(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) { + + List response = recordService.getRecord(user.getId(), bookId); + return Response.success(HttpStatus.OK, "독서 기록 조회 성공", response); + } } diff --git a/src/main/java/com/api/readinglog/domain/record/controller/dto/RecordWriteRequest.java b/src/main/java/com/api/readinglog/domain/record/controller/dto/request/RecordWriteRequest.java similarity index 73% rename from src/main/java/com/api/readinglog/domain/record/controller/dto/RecordWriteRequest.java rename to src/main/java/com/api/readinglog/domain/record/controller/dto/request/RecordWriteRequest.java index 5a8792b..a4ff84d 100644 --- a/src/main/java/com/api/readinglog/domain/record/controller/dto/RecordWriteRequest.java +++ b/src/main/java/com/api/readinglog/domain/record/controller/dto/request/RecordWriteRequest.java @@ -1,15 +1,12 @@ -package com.api.readinglog.domain.record.controller.dto; +package com.api.readinglog.domain.record.controller.dto.request; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import lombok.Getter; import lombok.Setter; -import lombok.ToString; @Getter @Setter -@ToString public class RecordWriteRequest { @NotNull(message = "독서 시작일은 필수값 입니다.") diff --git a/src/main/java/com/api/readinglog/domain/record/controller/dto/response/RecordResponse.java b/src/main/java/com/api/readinglog/domain/record/controller/dto/response/RecordResponse.java new file mode 100644 index 0000000..7b567e1 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/record/controller/dto/response/RecordResponse.java @@ -0,0 +1,27 @@ +package com.api.readinglog.domain.record.controller.dto.response; + +import com.api.readinglog.domain.record.entity.Record; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class RecordResponse { + + private Long memberId; + private Long recordId; + private Long bookId; + private LocalDateTime startDate; + private LocalDateTime endDate; + + public static RecordResponse fromEntity(Record record) { + return RecordResponse.builder() + .memberId((record.getMember().getId())) + .recordId(record.getId()) + .bookId(record.getBook().getId()) + .startDate(record.getStartDate()) + .endDate(record.getEndDate()) + .build(); + } +} diff --git a/src/main/java/com/api/readinglog/domain/record/entity/Record.java b/src/main/java/com/api/readinglog/domain/record/entity/Record.java index 70e0035..c1f4353 100644 --- a/src/main/java/com/api/readinglog/domain/record/entity/Record.java +++ b/src/main/java/com/api/readinglog/domain/record/entity/Record.java @@ -4,7 +4,7 @@ import com.api.readinglog.common.base.BaseTimeEntity; import com.api.readinglog.domain.book.entity.Book; import com.api.readinglog.domain.member.entity.Member; -import com.api.readinglog.domain.record.controller.dto.RecordWriteRequest; +import com.api.readinglog.domain.record.controller.dto.request.RecordWriteRequest; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; diff --git a/src/main/java/com/api/readinglog/domain/record/repository/RecordRepository.java b/src/main/java/com/api/readinglog/domain/record/repository/RecordRepository.java index 46e80a7..48a9236 100644 --- a/src/main/java/com/api/readinglog/domain/record/repository/RecordRepository.java +++ b/src/main/java/com/api/readinglog/domain/record/repository/RecordRepository.java @@ -1,7 +1,12 @@ package com.api.readinglog.domain.record.repository; +import com.api.readinglog.domain.book.entity.Book; +import com.api.readinglog.domain.member.entity.Member; import com.api.readinglog.domain.record.entity.Record; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface RecordRepository extends JpaRepository { + + List findAllByMemberAndBook(Member member, Book book); } diff --git a/src/main/java/com/api/readinglog/domain/record/service/RecordService.java b/src/main/java/com/api/readinglog/domain/record/service/RecordService.java index 87f6e30..423969c 100644 --- a/src/main/java/com/api/readinglog/domain/record/service/RecordService.java +++ b/src/main/java/com/api/readinglog/domain/record/service/RecordService.java @@ -1,13 +1,16 @@ package com.api.readinglog.domain.record.service; +import com.api.readinglog.common.exception.ErrorCode; +import com.api.readinglog.common.exception.custom.RecordException; import com.api.readinglog.domain.book.entity.Book; import com.api.readinglog.domain.book.service.BookService; import com.api.readinglog.domain.member.entity.Member; import com.api.readinglog.domain.member.service.MemberService; -import com.api.readinglog.domain.record.controller.dto.RecordWriteRequest; +import com.api.readinglog.domain.record.controller.dto.request.RecordWriteRequest; +import com.api.readinglog.domain.record.controller.dto.response.RecordResponse; import com.api.readinglog.domain.record.entity.Record; import com.api.readinglog.domain.record.repository.RecordRepository; -import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,4 +31,20 @@ public void write(long memberId, long bookId, RecordWriteRequest request) { recordRepository.save(Record.of(member, book, request)); } + public List getRecord(Long memberId, Long bookId) { + Member member = memberService.getMemberById(memberId); + Book book = bookService.getBookById(bookId); + + List records = recordRepository.findAllByMemberAndBook(member, book) + .stream() + + .map(RecordResponse::fromEntity) + .toList(); + + if (records.isEmpty()) { + throw new RecordException(ErrorCode.NOT_FOUND_RECORD); + } + + return records; + } } From 5bbd4622c1009697123c66a78959f18a29c4978e Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Mon, 1 Apr 2024 17:22:44 +0900 Subject: [PATCH 21/33] =?UTF-8?q?Feat:=20=EB=8F=85=EC=84=9C=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../record/controller/RecordController.java | 17 ++++++++--- .../dto/response/RecordResponse.java | 4 +-- .../domain/record/service/RecordService.java | 30 ++++++++++++++----- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java b/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java index c853c16..4dacd0f 100644 --- a/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java +++ b/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java @@ -11,6 +11,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -26,6 +27,13 @@ public class RecordController { private final RecordService recordService; + @GetMapping("/{bookId}") + public Response> getRecord(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) { + + List response = recordService.getRecord(user.getId(), bookId); + return Response.success(HttpStatus.OK, "독서 기록 조회 성공", response); + } + @PostMapping("/{bookId}") public Response addRecord(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId, @@ -35,10 +43,11 @@ public Response addRecord(@AuthenticationPrincipal CustomUserDetail user, return Response.success(HttpStatus.OK, "독서 기록 추가 성공"); } - @GetMapping("/{bookId}") - public Response> getRecord(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) { + @DeleteMapping("/{recordId}") + public Response delete(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long recordId) { - List response = recordService.getRecord(user.getId(), bookId); - return Response.success(HttpStatus.OK, "독서 기록 조회 성공", response); + recordService.delete(user.getId(), recordId); + return Response.success(HttpStatus.OK, "독서 기록 삭제 성공"); } + } diff --git a/src/main/java/com/api/readinglog/domain/record/controller/dto/response/RecordResponse.java b/src/main/java/com/api/readinglog/domain/record/controller/dto/response/RecordResponse.java index 7b567e1..5da3e02 100644 --- a/src/main/java/com/api/readinglog/domain/record/controller/dto/response/RecordResponse.java +++ b/src/main/java/com/api/readinglog/domain/record/controller/dto/response/RecordResponse.java @@ -10,16 +10,16 @@ public class RecordResponse { private Long memberId; - private Long recordId; private Long bookId; + private Long recordId; private LocalDateTime startDate; private LocalDateTime endDate; public static RecordResponse fromEntity(Record record) { return RecordResponse.builder() .memberId((record.getMember().getId())) - .recordId(record.getId()) .bookId(record.getBook().getId()) + .recordId(record.getId()) .startDate(record.getStartDate()) .endDate(record.getEndDate()) .build(); diff --git a/src/main/java/com/api/readinglog/domain/record/service/RecordService.java b/src/main/java/com/api/readinglog/domain/record/service/RecordService.java index 423969c..5fbf3c0 100644 --- a/src/main/java/com/api/readinglog/domain/record/service/RecordService.java +++ b/src/main/java/com/api/readinglog/domain/record/service/RecordService.java @@ -24,13 +24,7 @@ public class RecordService { private final MemberService memberService; private final BookService bookService; - public void write(long memberId, long bookId, RecordWriteRequest request) { - Member member = memberService.getMemberById(memberId); - Book book = bookService.getBookById(bookId); - - recordRepository.save(Record.of(member, book, request)); - } - + @Transactional(readOnly = true) public List getRecord(Long memberId, Long bookId) { Member member = memberService.getMemberById(memberId); Book book = bookService.getBookById(bookId); @@ -47,4 +41,26 @@ public List getRecord(Long memberId, Long bookId) { return records; } + + public void write(long memberId, long bookId, RecordWriteRequest request) { + Member member = memberService.getMemberById(memberId); + Book book = bookService.getBookById(bookId); + + recordRepository.save(Record.of(member, book, request)); + } + + public void delete(Long memberId, Long recordId) { + Member member = memberService.getMemberById(memberId); + Record record = getRecordById(recordId); + + if (record.getMember() != member) { + throw new RecordException(ErrorCode.FORBIDDEN_DELETE); + } + + recordRepository.delete(record); + } + + public Record getRecordById(Long recordId) { + return recordRepository.findById(recordId).orElseThrow(() -> new RecordException(ErrorCode.NOT_FOUND_RECORD)); + } } From 2e6005a01ac7438df1045e0b45057bfe20998e76 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Mon, 1 Apr 2024 17:34:57 +0900 Subject: [PATCH 22/33] =?UTF-8?q?Feat:=20=EB=8F=85=EC=84=9C=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../record/controller/RecordController.java | 11 +++++++++++ .../dto/request/RecordModifyRequest.java | 15 +++++++++++++++ .../dto/request/RecordWriteRequest.java | 2 -- .../readinglog/domain/record/entity/Record.java | 5 +++++ .../domain/record/service/RecordService.java | 13 +++++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/api/readinglog/domain/record/controller/dto/request/RecordModifyRequest.java diff --git a/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java b/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java index 4dacd0f..8d42ade 100644 --- a/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java +++ b/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java @@ -2,6 +2,7 @@ import com.api.readinglog.common.response.Response; import com.api.readinglog.common.security.CustomUserDetail; +import com.api.readinglog.domain.record.controller.dto.request.RecordModifyRequest; import com.api.readinglog.domain.record.controller.dto.request.RecordWriteRequest; import com.api.readinglog.domain.record.controller.dto.response.RecordResponse; import com.api.readinglog.domain.record.service.RecordService; @@ -13,6 +14,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +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; @@ -43,6 +45,15 @@ public Response addRecord(@AuthenticationPrincipal CustomUserDetail user, return Response.success(HttpStatus.OK, "독서 기록 추가 성공"); } + @PatchMapping("/{recordId}") + public Response modify(@AuthenticationPrincipal CustomUserDetail user, + @PathVariable Long recordId, + @RequestBody RecordModifyRequest recordModifyRequest) { + + recordService.modify(user.getId(), recordId, recordModifyRequest); + return Response.success(HttpStatus.OK, "독서 기록 수정 성공"); + } + @DeleteMapping("/{recordId}") public Response delete(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long recordId) { diff --git a/src/main/java/com/api/readinglog/domain/record/controller/dto/request/RecordModifyRequest.java b/src/main/java/com/api/readinglog/domain/record/controller/dto/request/RecordModifyRequest.java new file mode 100644 index 0000000..7b58981 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/record/controller/dto/request/RecordModifyRequest.java @@ -0,0 +1,15 @@ +package com.api.readinglog.domain.record.controller.dto.request; + +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.Getter; + +@Getter +public class RecordModifyRequest { + + @NotNull(message = "독서 시작일은 필수값 입니다.") + private LocalDateTime startDate; + + @NotNull(message = "독서 종료일은 필수값 입니다.") + private LocalDateTime endDate; +} diff --git a/src/main/java/com/api/readinglog/domain/record/controller/dto/request/RecordWriteRequest.java b/src/main/java/com/api/readinglog/domain/record/controller/dto/request/RecordWriteRequest.java index a4ff84d..9e9b09d 100644 --- a/src/main/java/com/api/readinglog/domain/record/controller/dto/request/RecordWriteRequest.java +++ b/src/main/java/com/api/readinglog/domain/record/controller/dto/request/RecordWriteRequest.java @@ -3,10 +3,8 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import lombok.Getter; -import lombok.Setter; @Getter -@Setter public class RecordWriteRequest { @NotNull(message = "독서 시작일은 필수값 입니다.") diff --git a/src/main/java/com/api/readinglog/domain/record/entity/Record.java b/src/main/java/com/api/readinglog/domain/record/entity/Record.java index c1f4353..15c984e 100644 --- a/src/main/java/com/api/readinglog/domain/record/entity/Record.java +++ b/src/main/java/com/api/readinglog/domain/record/entity/Record.java @@ -4,6 +4,7 @@ import com.api.readinglog.common.base.BaseTimeEntity; import com.api.readinglog.domain.book.entity.Book; import com.api.readinglog.domain.member.entity.Member; +import com.api.readinglog.domain.record.controller.dto.request.RecordModifyRequest; import com.api.readinglog.domain.record.controller.dto.request.RecordWriteRequest; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -64,4 +65,8 @@ public static Record of(Member member, Book book, RecordWriteRequest request) { .build(); } + public void modify(RecordModifyRequest request) { + this.startDate = request.getStartDate(); + this.endDate = request.getEndDate(); + } } diff --git a/src/main/java/com/api/readinglog/domain/record/service/RecordService.java b/src/main/java/com/api/readinglog/domain/record/service/RecordService.java index 5fbf3c0..97ed92d 100644 --- a/src/main/java/com/api/readinglog/domain/record/service/RecordService.java +++ b/src/main/java/com/api/readinglog/domain/record/service/RecordService.java @@ -6,6 +6,7 @@ import com.api.readinglog.domain.book.service.BookService; import com.api.readinglog.domain.member.entity.Member; import com.api.readinglog.domain.member.service.MemberService; +import com.api.readinglog.domain.record.controller.dto.request.RecordModifyRequest; import com.api.readinglog.domain.record.controller.dto.request.RecordWriteRequest; import com.api.readinglog.domain.record.controller.dto.response.RecordResponse; import com.api.readinglog.domain.record.entity.Record; @@ -49,6 +50,17 @@ public void write(long memberId, long bookId, RecordWriteRequest request) { recordRepository.save(Record.of(member, book, request)); } + public void modify(Long memberId, Long recordId, RecordModifyRequest request) { + Member member = memberService.getMemberById(memberId); + Record record = getRecordById(recordId); + + if (record.getMember() != member) { + throw new RecordException(ErrorCode.FORBIDDEN_MODIFY); + } + + record.modify(request); + } + public void delete(Long memberId, Long recordId) { Member member = memberService.getMemberById(memberId); Record record = getRecordById(recordId); @@ -63,4 +75,5 @@ public void delete(Long memberId, Long recordId) { public Record getRecordById(Long recordId) { return recordRepository.findById(recordId).orElseThrow(() -> new RecordException(ErrorCode.NOT_FOUND_RECORD)); } + } From 0e54b89ffc0771eb87042f5ba3320cb51b4fdd07 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Mon, 1 Apr 2024 18:37:50 +0900 Subject: [PATCH 23/33] =?UTF-8?q?Feat:=20=EC=B1=85=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 책 삭제시 관련 글과 기록도 함께 삭제되도록 수정 - 양방향 매핑 후 cascade, orphanRemoval 옵션 추가 --- .../readinglog/domain/book/entity/Book.java | 20 +++++++++++++++++++ .../hightlight/service/HighlightService.java | 5 +++-- .../domain/record/service/RecordService.java | 3 ++- .../domain/review/service/ReviewService.java | 4 ++-- .../summary/service/SummaryService.java | 4 ++-- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/api/readinglog/domain/book/entity/Book.java b/src/main/java/com/api/readinglog/domain/book/entity/Book.java index fc82d82..5b690c9 100644 --- a/src/main/java/com/api/readinglog/domain/book/entity/Book.java +++ b/src/main/java/com/api/readinglog/domain/book/entity/Book.java @@ -4,7 +4,12 @@ import com.api.readinglog.domain.book.dto.BookDirectRequest; import com.api.readinglog.domain.book.dto.BookModifyRequest; import com.api.readinglog.domain.book.dto.BookRegisterRequest; +import com.api.readinglog.domain.hightlight.entity.Highlight; import com.api.readinglog.domain.member.entity.Member; +import com.api.readinglog.domain.record.entity.Record; +import com.api.readinglog.domain.review.entity.Review; +import com.api.readinglog.domain.summary.entity.Summary; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -13,6 +18,9 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -54,6 +62,18 @@ public class Book extends BaseTimeEntity { @Column(name = "book_cover") private String cover; + @OneToMany(mappedBy = "book", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List summaryList = new ArrayList<>(); + + @OneToMany(mappedBy = "book", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List HighlightList = new ArrayList<>(); + + @OneToMany(mappedBy = "book", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List reviewList = new ArrayList<>(); + + @OneToMany(mappedBy = "book", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List recordList = new ArrayList<>(); + @Builder private Book(Member member, Integer itemId, String title, String author, String publisher, String category, String cover) { this.member = member; diff --git a/src/main/java/com/api/readinglog/domain/hightlight/service/HighlightService.java b/src/main/java/com/api/readinglog/domain/hightlight/service/HighlightService.java index 301f63d..b1fff19 100644 --- a/src/main/java/com/api/readinglog/domain/hightlight/service/HighlightService.java +++ b/src/main/java/com/api/readinglog/domain/hightlight/service/HighlightService.java @@ -31,6 +31,7 @@ public Page highlights(Long memberId, Long bookId, Pageable p Member member = memberService.getMemberById(memberId); Book book = bookService.getBookById(bookId); + // TODO: 결과가 없는 경우에 404 처리 return highlightRepository.findAllByMemberAndBook(member, book, pageable) .map(HighlightResponse::fromEntity); } @@ -40,8 +41,8 @@ public void write(Long memberId, Long bookId, WriteRequest request) { Member member = memberService.getMemberById(memberId); Book book = bookService.getBookById(bookId); - Highlight highlight = Highlight.of(member, book, request); - highlightRepository.save(highlight); + Highlight highlight = highlightRepository.save(Highlight.of(member, book, request)); + book.getHighlightList().add(highlight); } public void modify(Long memberId, Long highlightId, ModifyRequest request) { diff --git a/src/main/java/com/api/readinglog/domain/record/service/RecordService.java b/src/main/java/com/api/readinglog/domain/record/service/RecordService.java index 97ed92d..0f855ed 100644 --- a/src/main/java/com/api/readinglog/domain/record/service/RecordService.java +++ b/src/main/java/com/api/readinglog/domain/record/service/RecordService.java @@ -47,7 +47,8 @@ public void write(long memberId, long bookId, RecordWriteRequest request) { Member member = memberService.getMemberById(memberId); Book book = bookService.getBookById(bookId); - recordRepository.save(Record.of(member, book, request)); + Record record = recordRepository.save(Record.of(member, book, request)); + book.getRecordList().add(record); } public void modify(Long memberId, Long recordId, RecordModifyRequest request) { diff --git a/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java b/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java index 91d6cf8..2cf4ee2 100644 --- a/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java +++ b/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java @@ -45,8 +45,8 @@ public void write(Long memberId, Long bookId, WriteRequest request) { Member member = memberService.getMemberById(memberId); Book book = bookService.getBookById(bookId); - Review review = Review.of(member, book, request); - reviewRepository.save(review); + Review review = reviewRepository.save(Review.of(member, book, request)); + book.getReviewList().add(review); } public void modify(Long memberId, Long reviewId, ModifyRequest request) { diff --git a/src/main/java/com/api/readinglog/domain/summary/service/SummaryService.java b/src/main/java/com/api/readinglog/domain/summary/service/SummaryService.java index d231e3e..772565d 100644 --- a/src/main/java/com/api/readinglog/domain/summary/service/SummaryService.java +++ b/src/main/java/com/api/readinglog/domain/summary/service/SummaryService.java @@ -60,8 +60,8 @@ public void write(Long memberId, Long bookId, WriteRequest request) { throw new SummaryException(ErrorCode.SUMMARY_ALREADY_EXISTS); }); - Summary summary = Summary.of(member, book, request); - summaryRepository.save(summary); + Summary summary = summaryRepository.save(Summary.of(member, book, request)); + book.getSummaryList().add(summary); } public void modify(Long memberId, Long summaryId, ModifyRequest request) { From 029f173fbfc4a0f0d2053e517d25811d52d478da Mon Sep 17 00:00:00 2001 From: jeondui Date: Mon, 1 Apr 2024 21:24:37 +0900 Subject: [PATCH 24/33] =?UTF-8?q?Refactor:=20=EB=82=B4=EA=B0=80=20?= =?UTF-8?q?=EC=93=B4=20=EC=84=9C=ED=8F=89=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EB=B0=98=ED=99=98=20=ED=98=95=EC=8B=9D=20Lis?= =?UTF-8?q?t=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/controller/ReviewController.java | 12 ++++-------- .../domain/review/repository/ReviewRepository.java | 5 ++--- .../domain/review/service/ReviewService.java | 14 ++++++++------ 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/api/readinglog/domain/review/controller/ReviewController.java b/src/main/java/com/api/readinglog/domain/review/controller/ReviewController.java index 91bf54e..7512917 100644 --- a/src/main/java/com/api/readinglog/domain/review/controller/ReviewController.java +++ b/src/main/java/com/api/readinglog/domain/review/controller/ReviewController.java @@ -7,11 +7,8 @@ import com.api.readinglog.domain.review.controller.dto.response.ReviewResponse; import com.api.readinglog.domain.review.service.ReviewService; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort.Direction; -import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; @@ -31,11 +28,10 @@ public class ReviewController { private final ReviewService reviewService; @GetMapping("/{bookId}/me") - public Response> reviews(@AuthenticationPrincipal CustomUserDetail user, - @PathVariable Long bookId, - @PageableDefault(sort = "createdAt", direction = Direction.DESC) Pageable pageable) { + public Response> reviews(@AuthenticationPrincipal CustomUserDetail user, + @PathVariable Long bookId) { - Page response = reviewService.reviews(user.getId(), bookId, pageable); + List response = reviewService.reviews(user.getId(), bookId); return Response.success(HttpStatus.OK, "내가 쓴 서평 목록 조회 성공", response); } diff --git a/src/main/java/com/api/readinglog/domain/review/repository/ReviewRepository.java b/src/main/java/com/api/readinglog/domain/review/repository/ReviewRepository.java index b540c86..ddf2c43 100644 --- a/src/main/java/com/api/readinglog/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/api/readinglog/domain/review/repository/ReviewRepository.java @@ -3,11 +3,10 @@ import com.api.readinglog.domain.book.entity.Book; import com.api.readinglog.domain.member.entity.Member; import com.api.readinglog.domain.review.entity.Review; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface ReviewRepository extends JpaRepository { - Page findAllByMemberAndBook(Member member, Book book, Pageable pageable); + List findAllByMemberAndBook(Member member, Book book); } diff --git a/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java b/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java index 91d6cf8..f7815b3 100644 --- a/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java +++ b/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java @@ -11,9 +11,9 @@ import com.api.readinglog.domain.review.controller.dto.response.ReviewResponse; import com.api.readinglog.domain.review.entity.Review; import com.api.readinglog.domain.review.repository.ReviewRepository; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,14 +27,16 @@ public class ReviewService { private final BookService bookService; @Transactional(readOnly = true) - public Page reviews(Long memberId, Long bookId, Pageable pageable) { + public List reviews(Long memberId, Long bookId) { Member member = memberService.getMemberById(memberId); Book book = bookService.getBookById(bookId); - Page reviews = reviewRepository.findAllByMemberAndBook(member, book, pageable) - .map(ReviewResponse::fromEntity); + List reviews = reviewRepository.findAllByMemberAndBook(member, book) + .stream() + .map(ReviewResponse::fromEntity) + .collect(Collectors.toList()); - if (reviews.getContent().isEmpty()) { + if (reviews.isEmpty()) { throw new ReviewException(ErrorCode.NOT_FOUND_REVIEW); } From 8e44252869b17afc4cde27ec706bc00acc4a07ac Mon Sep 17 00:00:00 2001 From: jeondui Date: Mon, 1 Apr 2024 21:31:04 +0900 Subject: [PATCH 25/33] =?UTF-8?q?Refactor:=20=EB=82=B4=EA=B0=80=20?= =?UTF-8?q?=EC=93=B4=20=ED=95=98=EC=9D=B4=EB=9D=BC=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20=ED=98=95=EC=8B=9D=20List=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 하이라이트가 존재하지 않는 경우 예외 처리 추가 - 패키지명 오타 수정 --- .../common/exception/ErrorCode.java | 4 +-- .../controller/HighlightController.java | 25 ++++++--------- .../controller/dto/request/ModifyRequest.java | 2 +- .../controller/dto/request/WriteRequest.java | 2 +- .../dto/response/HighlightResponse.java | 4 +-- .../entity/Highlight.java | 8 ++--- .../repository/HighlightRepository.java | 12 +++++++ .../service/HighlightService.java | 31 ++++++++++++------- .../repository/HighlightRepository.java | 13 -------- 9 files changed, 50 insertions(+), 51 deletions(-) rename src/main/java/com/api/readinglog/domain/{hightlight => highlight}/controller/HighlightController.java (69%) rename src/main/java/com/api/readinglog/domain/{hightlight => highlight}/controller/dto/request/ModifyRequest.java (87%) rename src/main/java/com/api/readinglog/domain/{hightlight => highlight}/controller/dto/request/WriteRequest.java (87%) rename src/main/java/com/api/readinglog/domain/{hightlight => highlight}/controller/dto/response/HighlightResponse.java (79%) rename src/main/java/com/api/readinglog/domain/{hightlight => highlight}/entity/Highlight.java (87%) create mode 100644 src/main/java/com/api/readinglog/domain/highlight/repository/HighlightRepository.java rename src/main/java/com/api/readinglog/domain/{hightlight => highlight}/service/HighlightService.java (69%) delete mode 100644 src/main/java/com/api/readinglog/domain/hightlight/repository/HighlightRepository.java diff --git a/src/main/java/com/api/readinglog/common/exception/ErrorCode.java b/src/main/java/com/api/readinglog/common/exception/ErrorCode.java index 333a886..9247c39 100644 --- a/src/main/java/com/api/readinglog/common/exception/ErrorCode.java +++ b/src/main/java/com/api/readinglog/common/exception/ErrorCode.java @@ -35,10 +35,10 @@ public enum ErrorCode { NOT_FOUND_MEMBER("회원이 존재하지 않습니다!", HttpStatus.NOT_FOUND), NOT_FOUND_SEARCH("검색 결과가 존재하지 않습니다!", HttpStatus.NOT_FOUND), NOT_FOUND_BOOK("등록된 책이 존재하지 않습니다!", HttpStatus.NOT_FOUND), - NOT_FOUND_HIGHLIGHT("등록된 책이 존재하지 않습니다!", HttpStatus.NOT_FOUND), + NOT_FOUND_HIGHLIGHT("등록된 하이라이트가 존재하지 않습니다!", HttpStatus.NOT_FOUND), NOT_FOUND_SUMMARY("등록된 한줄평이 존재하지 않습니다!", HttpStatus.NOT_FOUND), NOT_FOUND_REVIEW("서평이 존재하지 않습니다!", HttpStatus.NOT_FOUND), - NOT_FOUND_FEED("피드 목록이 존재하지 않습니다!", HttpStatus.NOT_FOUND), + NOT_FOUND_BOOK_LOGS("북로그 목록이 존재하지 않습니다!", HttpStatus.NOT_FOUND), // 409 MEMBER_ALREADY_EXISTS("이미 존재하는 회원입니다.", HttpStatus.CONFLICT), diff --git a/src/main/java/com/api/readinglog/domain/hightlight/controller/HighlightController.java b/src/main/java/com/api/readinglog/domain/highlight/controller/HighlightController.java similarity index 69% rename from src/main/java/com/api/readinglog/domain/hightlight/controller/HighlightController.java rename to src/main/java/com/api/readinglog/domain/highlight/controller/HighlightController.java index 9f6d451..a167c48 100644 --- a/src/main/java/com/api/readinglog/domain/hightlight/controller/HighlightController.java +++ b/src/main/java/com/api/readinglog/domain/highlight/controller/HighlightController.java @@ -1,19 +1,15 @@ -package com.api.readinglog.domain.hightlight.controller; +package com.api.readinglog.domain.highlight.controller; import com.api.readinglog.common.response.Response; import com.api.readinglog.common.security.CustomUserDetail; -import com.api.readinglog.domain.hightlight.controller.dto.request.ModifyRequest; -import com.api.readinglog.domain.hightlight.controller.dto.request.WriteRequest; -import com.api.readinglog.domain.hightlight.controller.dto.response.HighlightResponse; -import com.api.readinglog.domain.hightlight.service.HighlightService; +import com.api.readinglog.domain.highlight.controller.dto.request.ModifyRequest; +import com.api.readinglog.domain.highlight.controller.dto.request.WriteRequest; +import com.api.readinglog.domain.highlight.controller.dto.response.HighlightResponse; +import com.api.readinglog.domain.highlight.service.HighlightService; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.Sort.Direction; -import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; @@ -34,12 +30,11 @@ public class HighlightController { private final HighlightService highlightService; @GetMapping("/{bookId}/me") - public Response> highlights(@AuthenticationPrincipal CustomUserDetail user, - @PathVariable Long bookId, - @PageableDefault(sort = "createdAt", direction = Direction.DESC) Pageable pageable) { + public Response> highlights(@AuthenticationPrincipal CustomUserDetail user, + @PathVariable Long bookId) { - Page response = highlightService.highlights(user.getId(), bookId, pageable); - return Response.success(HttpStatus.OK, "하이라이트 목록 조회 성공", response); + List response = highlightService.highlights(user.getId(), bookId); + return Response.success(HttpStatus.OK, "내가 쓴 하이라이트 목록 조회 성공", response); } @PostMapping("/{bookId}") diff --git a/src/main/java/com/api/readinglog/domain/hightlight/controller/dto/request/ModifyRequest.java b/src/main/java/com/api/readinglog/domain/highlight/controller/dto/request/ModifyRequest.java similarity index 87% rename from src/main/java/com/api/readinglog/domain/hightlight/controller/dto/request/ModifyRequest.java rename to src/main/java/com/api/readinglog/domain/highlight/controller/dto/request/ModifyRequest.java index 53d8bc7..062a20e 100644 --- a/src/main/java/com/api/readinglog/domain/hightlight/controller/dto/request/ModifyRequest.java +++ b/src/main/java/com/api/readinglog/domain/highlight/controller/dto/request/ModifyRequest.java @@ -1,4 +1,4 @@ -package com.api.readinglog.domain.hightlight.controller.dto.request; +package com.api.readinglog.domain.highlight.controller.dto.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/api/readinglog/domain/hightlight/controller/dto/request/WriteRequest.java b/src/main/java/com/api/readinglog/domain/highlight/controller/dto/request/WriteRequest.java similarity index 87% rename from src/main/java/com/api/readinglog/domain/hightlight/controller/dto/request/WriteRequest.java rename to src/main/java/com/api/readinglog/domain/highlight/controller/dto/request/WriteRequest.java index dc40698..8c1c913 100644 --- a/src/main/java/com/api/readinglog/domain/hightlight/controller/dto/request/WriteRequest.java +++ b/src/main/java/com/api/readinglog/domain/highlight/controller/dto/request/WriteRequest.java @@ -1,4 +1,4 @@ -package com.api.readinglog.domain.hightlight.controller.dto.request; +package com.api.readinglog.domain.highlight.controller.dto.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/api/readinglog/domain/hightlight/controller/dto/response/HighlightResponse.java b/src/main/java/com/api/readinglog/domain/highlight/controller/dto/response/HighlightResponse.java similarity index 79% rename from src/main/java/com/api/readinglog/domain/hightlight/controller/dto/response/HighlightResponse.java rename to src/main/java/com/api/readinglog/domain/highlight/controller/dto/response/HighlightResponse.java index a901c49..150fec0 100644 --- a/src/main/java/com/api/readinglog/domain/hightlight/controller/dto/response/HighlightResponse.java +++ b/src/main/java/com/api/readinglog/domain/highlight/controller/dto/response/HighlightResponse.java @@ -1,6 +1,6 @@ -package com.api.readinglog.domain.hightlight.controller.dto.response; +package com.api.readinglog.domain.highlight.controller.dto.response; -import com.api.readinglog.domain.hightlight.entity.Highlight; +import com.api.readinglog.domain.highlight.entity.Highlight; import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/api/readinglog/domain/hightlight/entity/Highlight.java b/src/main/java/com/api/readinglog/domain/highlight/entity/Highlight.java similarity index 87% rename from src/main/java/com/api/readinglog/domain/hightlight/entity/Highlight.java rename to src/main/java/com/api/readinglog/domain/highlight/entity/Highlight.java index 603fd71..4103a04 100644 --- a/src/main/java/com/api/readinglog/domain/hightlight/entity/Highlight.java +++ b/src/main/java/com/api/readinglog/domain/highlight/entity/Highlight.java @@ -1,14 +1,12 @@ -package com.api.readinglog.domain.hightlight.entity; +package com.api.readinglog.domain.highlight.entity; import com.api.readinglog.common.base.BaseTimeEntity; import com.api.readinglog.domain.book.entity.Book; -import com.api.readinglog.domain.hightlight.controller.dto.request.ModifyRequest; -import com.api.readinglog.domain.hightlight.controller.dto.request.WriteRequest; +import com.api.readinglog.domain.highlight.controller.dto.request.ModifyRequest; +import com.api.readinglog.domain.highlight.controller.dto.request.WriteRequest; import com.api.readinglog.domain.member.entity.Member; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; diff --git a/src/main/java/com/api/readinglog/domain/highlight/repository/HighlightRepository.java b/src/main/java/com/api/readinglog/domain/highlight/repository/HighlightRepository.java new file mode 100644 index 0000000..0ca9f2c --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/highlight/repository/HighlightRepository.java @@ -0,0 +1,12 @@ +package com.api.readinglog.domain.highlight.repository; + +import com.api.readinglog.domain.book.entity.Book; +import com.api.readinglog.domain.highlight.entity.Highlight; +import com.api.readinglog.domain.member.entity.Member; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface HighlightRepository extends JpaRepository { + + List findAllByMemberAndBook(Member member, Book book); +} diff --git a/src/main/java/com/api/readinglog/domain/hightlight/service/HighlightService.java b/src/main/java/com/api/readinglog/domain/highlight/service/HighlightService.java similarity index 69% rename from src/main/java/com/api/readinglog/domain/hightlight/service/HighlightService.java rename to src/main/java/com/api/readinglog/domain/highlight/service/HighlightService.java index 301f63d..5a09ac9 100644 --- a/src/main/java/com/api/readinglog/domain/hightlight/service/HighlightService.java +++ b/src/main/java/com/api/readinglog/domain/highlight/service/HighlightService.java @@ -1,19 +1,18 @@ -package com.api.readinglog.domain.hightlight.service; +package com.api.readinglog.domain.highlight.service; import com.api.readinglog.common.exception.ErrorCode; import com.api.readinglog.common.exception.custom.HighlightException; import com.api.readinglog.domain.book.entity.Book; import com.api.readinglog.domain.book.service.BookService; -import com.api.readinglog.domain.hightlight.controller.dto.request.ModifyRequest; -import com.api.readinglog.domain.hightlight.controller.dto.request.WriteRequest; -import com.api.readinglog.domain.hightlight.controller.dto.response.HighlightResponse; -import com.api.readinglog.domain.hightlight.entity.Highlight; -import com.api.readinglog.domain.hightlight.repository.HighlightRepository; +import com.api.readinglog.domain.highlight.controller.dto.request.ModifyRequest; +import com.api.readinglog.domain.highlight.controller.dto.request.WriteRequest; +import com.api.readinglog.domain.highlight.controller.dto.response.HighlightResponse; +import com.api.readinglog.domain.highlight.entity.Highlight; +import com.api.readinglog.domain.highlight.repository.HighlightRepository; import com.api.readinglog.domain.member.entity.Member; import com.api.readinglog.domain.member.service.MemberService; +import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,14 +26,22 @@ public class HighlightService { private final BookService bookService; @Transactional(readOnly = true) - public Page highlights(Long memberId, Long bookId, Pageable pageable) { + public List highlights(Long memberId, Long bookId) { Member member = memberService.getMemberById(memberId); Book book = bookService.getBookById(bookId); - return highlightRepository.findAllByMemberAndBook(member, book, pageable) - .map(HighlightResponse::fromEntity); - } + List highlights = highlightRepository.findAllByMemberAndBook(member, book) + .stream() + .map(HighlightResponse::fromEntity) + .toList(); + + // 하이라이트가 존재하지 않는 경우 예외 처리 + if(highlights.isEmpty()) { + throw new HighlightException(ErrorCode.NOT_FOUND_HIGHLIGHT); + } + return highlights; + } public void write(Long memberId, Long bookId, WriteRequest request) { Member member = memberService.getMemberById(memberId); diff --git a/src/main/java/com/api/readinglog/domain/hightlight/repository/HighlightRepository.java b/src/main/java/com/api/readinglog/domain/hightlight/repository/HighlightRepository.java deleted file mode 100644 index d34cdf4..0000000 --- a/src/main/java/com/api/readinglog/domain/hightlight/repository/HighlightRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.api.readinglog.domain.hightlight.repository; - -import com.api.readinglog.domain.book.entity.Book; -import com.api.readinglog.domain.hightlight.entity.Highlight; -import com.api.readinglog.domain.member.entity.Member; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface HighlightRepository extends JpaRepository { - - Page findAllByMemberAndBook(Member member, Book book, Pageable pageable); -} From be77943eb37fa69e453d19f46165bf7ddbd0e4de Mon Sep 17 00:00:00 2001 From: jeondui Date: Mon, 1 Apr 2024 21:31:34 +0900 Subject: [PATCH 26/33] =?UTF-8?q?Refactor:=20=EC=8A=A4=ED=8A=B8=EB=A6=BC?= =?UTF-8?q?=20=EB=B0=98=ED=99=98=20=ED=98=95=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/readinglog/domain/review/service/ReviewService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java b/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java index f7815b3..716e769 100644 --- a/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java +++ b/src/main/java/com/api/readinglog/domain/review/service/ReviewService.java @@ -12,7 +12,6 @@ import com.api.readinglog.domain.review.entity.Review; import com.api.readinglog.domain.review.repository.ReviewRepository; import java.util.List; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,7 +33,7 @@ public List reviews(Long memberId, Long bookId) { List reviews = reviewRepository.findAllByMemberAndBook(member, book) .stream() .map(ReviewResponse::fromEntity) - .collect(Collectors.toList()); + .toList(); if (reviews.isEmpty()) { throw new ReviewException(ErrorCode.NOT_FOUND_REVIEW); From db241b4386c5322c9776fd77abd3c25bfa25c835 Mon Sep 17 00:00:00 2001 From: jeondui Date: Mon, 1 Apr 2024 21:36:41 +0900 Subject: [PATCH 27/33] =?UTF-8?q?Feat:=20=EB=B6=81=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 나의 로그 조회 API SummaryController에서 BookLogController로 이동 --- .../booklog/controller/BookLogController.java | 39 ++++++++ .../controller/dto/BookLogResponse.java | 29 ++++++ .../booklog/service/BookLogService.java | 92 +++++++++++++++++++ .../summary/controller/SummaryController.java | 21 ----- .../summary/service/SummaryService.java | 28 ------ 5 files changed, 160 insertions(+), 49 deletions(-) create mode 100644 src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java create mode 100644 src/main/java/com/api/readinglog/domain/booklog/controller/dto/BookLogResponse.java create mode 100644 src/main/java/com/api/readinglog/domain/booklog/service/BookLogService.java diff --git a/src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java b/src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java new file mode 100644 index 0000000..881ff84 --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java @@ -0,0 +1,39 @@ +package com.api.readinglog.domain.booklog.controller; + +import com.api.readinglog.common.response.Response; +import com.api.readinglog.common.security.CustomUserDetail; +import com.api.readinglog.domain.booklog.controller.dto.BookLogResponse; +import com.api.readinglog.domain.booklog.service.BookLogService; +import com.api.readinglog.domain.summary.controller.dto.response.SummaryResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/book-logs") +public class BookLogController { + + private final BookLogService bookLogService; + + @GetMapping("/{bookId}/me") + public Response myLogs(@AuthenticationPrincipal CustomUserDetail user, + @PathVariable Long bookId) { + return Response.success(HttpStatus.OK, "나의 로그 조회 성공", bookLogService.myLogs(user.getId(), bookId)); + } + + @GetMapping + public Response> bookLogs(@PageableDefault(sort = "createdAt", direction = Direction.DESC) + Pageable pageable) { + // TODO: querydsl 동적 쿼리 처리 + return Response.success(HttpStatus.OK, "북로그 조회 성공", bookLogService.bookLogs(pageable)); + } +} \ No newline at end of file diff --git a/src/main/java/com/api/readinglog/domain/booklog/controller/dto/BookLogResponse.java b/src/main/java/com/api/readinglog/domain/booklog/controller/dto/BookLogResponse.java new file mode 100644 index 0000000..c35ab5e --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/booklog/controller/dto/BookLogResponse.java @@ -0,0 +1,29 @@ +package com.api.readinglog.domain.booklog.controller.dto; + +import com.api.readinglog.domain.book.dto.BookDetailResponse; +import com.api.readinglog.domain.highlight.controller.dto.response.HighlightResponse; +import com.api.readinglog.domain.review.controller.dto.response.ReviewResponse; +import com.api.readinglog.domain.summary.controller.dto.response.MySummaryResponse; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class BookLogResponse { + + private final BookDetailResponse bookInfo; + private final MySummaryResponse summary; + private final List reviews; + private final List highlights; + + public static BookLogResponse of(BookDetailResponse bookInfo, MySummaryResponse summary, + List reviews, List highlights) { + return BookLogResponse.builder() + .bookInfo(bookInfo) + .summary(summary) + .reviews(reviews) + .highlights(highlights) + .build(); + } +} diff --git a/src/main/java/com/api/readinglog/domain/booklog/service/BookLogService.java b/src/main/java/com/api/readinglog/domain/booklog/service/BookLogService.java new file mode 100644 index 0000000..35e246e --- /dev/null +++ b/src/main/java/com/api/readinglog/domain/booklog/service/BookLogService.java @@ -0,0 +1,92 @@ +package com.api.readinglog.domain.booklog.service; + +import com.api.readinglog.common.exception.ErrorCode; +import com.api.readinglog.common.exception.custom.SummaryException; +import com.api.readinglog.domain.book.dto.BookDetailResponse; +import com.api.readinglog.domain.book.entity.Book; +import com.api.readinglog.domain.book.service.BookService; +import com.api.readinglog.domain.booklog.controller.dto.BookLogResponse; +import com.api.readinglog.domain.highlight.controller.dto.response.HighlightResponse; +import com.api.readinglog.domain.highlight.repository.HighlightRepository; +import com.api.readinglog.domain.member.entity.Member; +import com.api.readinglog.domain.member.service.MemberService; +import com.api.readinglog.domain.review.controller.dto.response.ReviewResponse; +import com.api.readinglog.domain.review.repository.ReviewRepository; +import com.api.readinglog.domain.summary.controller.dto.response.MySummaryResponse; +import com.api.readinglog.domain.summary.controller.dto.response.SummaryResponse; +import com.api.readinglog.domain.summary.repository.SummaryRepository; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class BookLogService { + + private final MemberService memberService; + private final BookService bookService; + private final SummaryRepository summaryRepository; + private final ReviewRepository reviewRepository; + private final HighlightRepository highlightRepository; + + @Transactional(readOnly = true) + public BookLogResponse myLogs(Long memberId, Long bookId) { + Member member = getMember(memberId); + Book book = getBook(bookId); + BookDetailResponse bookDetailResponse = bookService.getBookInfo(memberId, bookId); + + // 한 줄평은 반드시 존재 + MySummaryResponse summary = findSummaryResponse(member, book); + + // 서평과 하이라이트가 존재하지 않은 경우는 빈 리스트 반환 + List reviews = findReviewsResponse(member, book); + List highlights = findHighlightsResponse(member, book); + + return BookLogResponse.of(bookDetailResponse, summary, reviews, highlights); + } + + @Transactional(readOnly = true) + public Page bookLogs(Pageable pageable) { + Page bookLogs = summaryRepository.findAllBy(pageable).map(SummaryResponse::fromEntity); + + // 북로그가 존재하지 않는 경우 예외 처리 + if (bookLogs.getContent().isEmpty()) { + throw new SummaryException(ErrorCode.NOT_FOUND_BOOK_LOGS); + } + + return bookLogs; + } + + private Member getMember(Long memberId) { + return memberService.getMemberById(memberId); + } + + private Book getBook(Long bookId) { + return bookService.getBookById(bookId); + } + + private MySummaryResponse findSummaryResponse(Member member, Book book) { + return summaryRepository.findByMemberAndBook(member, book) + .map(MySummaryResponse::fromEntity) + .orElseThrow(() -> new SummaryException(ErrorCode.NOT_FOUND_SUMMARY)); // 한 줄평이 존재하지 않는 경우 예외 처리 + } + + private List findReviewsResponse(Member member, Book book) { + return reviewRepository.findAllByMemberAndBook(member, book) + .stream() + .map(ReviewResponse::fromEntity) + .collect(Collectors.toList()); + } + + private List findHighlightsResponse(Member member, Book book) { + return highlightRepository.findAllByMemberAndBook(member, book) + .stream() + .map(HighlightResponse::fromEntity) + .collect(Collectors.toList()); + } +} + diff --git a/src/main/java/com/api/readinglog/domain/summary/controller/SummaryController.java b/src/main/java/com/api/readinglog/domain/summary/controller/SummaryController.java index 6a699d9..7aed2af 100644 --- a/src/main/java/com/api/readinglog/domain/summary/controller/SummaryController.java +++ b/src/main/java/com/api/readinglog/domain/summary/controller/SummaryController.java @@ -4,19 +4,12 @@ import com.api.readinglog.common.security.CustomUserDetail; import com.api.readinglog.domain.summary.controller.dto.request.ModifyRequest; import com.api.readinglog.domain.summary.controller.dto.request.WriteRequest; -import com.api.readinglog.domain.summary.controller.dto.response.SummaryResponse; -import com.api.readinglog.domain.summary.controller.dto.response.MySummaryResponse; import com.api.readinglog.domain.summary.service.SummaryService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort.Direction; -import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -31,20 +24,6 @@ public class SummaryController { private final SummaryService summaryService; - @GetMapping("/feed") - public Response> feed(@PageableDefault(sort = "createdAt", direction = Direction.DESC) Pageable pageable) { - // TODO: querydsl 동적 쿼리 처리 - return Response.success(HttpStatus.OK, "피드 목록 조회 성공", summaryService.feed(pageable)); - } - - @GetMapping("/{bookId}/me") - public Response mySummary(@AuthenticationPrincipal CustomUserDetail user, - @PathVariable Long bookId) { - - MySummaryResponse response = summaryService.mySummary(user.getId(), bookId); - return Response.success(HttpStatus.OK, "내 한줄평 조회 성공", response); - } - @PostMapping("/{bookId}") public Response write(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId, diff --git a/src/main/java/com/api/readinglog/domain/summary/service/SummaryService.java b/src/main/java/com/api/readinglog/domain/summary/service/SummaryService.java index d231e3e..a58c9bd 100644 --- a/src/main/java/com/api/readinglog/domain/summary/service/SummaryService.java +++ b/src/main/java/com/api/readinglog/domain/summary/service/SummaryService.java @@ -8,13 +8,9 @@ import com.api.readinglog.domain.member.service.MemberService; import com.api.readinglog.domain.summary.controller.dto.request.ModifyRequest; import com.api.readinglog.domain.summary.controller.dto.request.WriteRequest; -import com.api.readinglog.domain.summary.controller.dto.response.SummaryResponse; -import com.api.readinglog.domain.summary.controller.dto.response.MySummaryResponse; import com.api.readinglog.domain.summary.entity.Summary; import com.api.readinglog.domain.summary.repository.SummaryRepository; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,30 +23,6 @@ public class SummaryService { private final MemberService memberService; private final BookService bookService; - @Transactional(readOnly = true) - public Page feed(Pageable pageable) { - Page feed = summaryRepository.findAllBy(pageable).map(SummaryResponse::fromEntity); - - // 피드가 존재하지 않는 경우 예외 처리 - if (feed.getContent().isEmpty()) { - throw new SummaryException(ErrorCode.NOT_FOUND_FEED); - } - - return feed; - } - - @Transactional(readOnly = true) - public MySummaryResponse mySummary(Long memberId, Long bookId) { - Member member = memberService.getMemberById(memberId); - Book book = bookService.getBookById(bookId); - - // 해당 책에 대한 한줄평이 존재하면 반환 - Summary summary = summaryRepository.findByMemberAndBook(member, book) - .orElseThrow(() -> new SummaryException(ErrorCode.NOT_FOUND_SUMMARY)); - - return MySummaryResponse.fromEntity(summary); - } - public void write(Long memberId, Long bookId, WriteRequest request) { Member member = memberService.getMemberById(memberId); Book book = bookService.getBookById(bookId); From beb9e3fe29f243bff81b8989e53bcd039cc7a2a8 Mon Sep 17 00:00:00 2001 From: jeondui Date: Mon, 1 Apr 2024 21:41:20 +0900 Subject: [PATCH 28/33] =?UTF-8?q?Fix:=20=ED=95=98=EC=9D=B4=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/api/readinglog/domain/book/entity/Book.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/api/readinglog/domain/book/entity/Book.java b/src/main/java/com/api/readinglog/domain/book/entity/Book.java index 5b690c9..7a180f6 100644 --- a/src/main/java/com/api/readinglog/domain/book/entity/Book.java +++ b/src/main/java/com/api/readinglog/domain/book/entity/Book.java @@ -4,7 +4,8 @@ import com.api.readinglog.domain.book.dto.BookDirectRequest; import com.api.readinglog.domain.book.dto.BookModifyRequest; import com.api.readinglog.domain.book.dto.BookRegisterRequest; -import com.api.readinglog.domain.hightlight.entity.Highlight; +import com.api.readinglog.domain.highlight.entity.Highlight; +import com.api.readinglog.domain.highlight.entity.Highlight; import com.api.readinglog.domain.member.entity.Member; import com.api.readinglog.domain.record.entity.Record; import com.api.readinglog.domain.review.entity.Review; From 817d8e506860a7aba5a898297a0a3b41b0546994 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Tue, 2 Apr 2024 17:06:06 +0900 Subject: [PATCH 29/33] =?UTF-8?q?Feat:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/swagger/SwaggerConfig.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/main/java/com/api/readinglog/common/swagger/SwaggerConfig.java diff --git a/src/main/java/com/api/readinglog/common/swagger/SwaggerConfig.java b/src/main/java/com/api/readinglog/common/swagger/SwaggerConfig.java new file mode 100644 index 0000000..bcbb95c --- /dev/null +++ b/src/main/java/com/api/readinglog/common/swagger/SwaggerConfig.java @@ -0,0 +1,46 @@ +package com.api.readinglog.common.swagger; + + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.servers.Server; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import java.util.Arrays; +import java.util.Collections; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@OpenAPIDefinition( + servers = {@Server(url = "/")}, + info = @Info( + title = "리딩로그 API 명세서", + description = "독서 기록 서비스 리딩로그의 API 명세서", + contact = @Contact(name = "the developer", email = "ddmkim94@gmail.com"), + version = "v1.0") +) +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI(){ + String securityRequirementName = "Bearer를 제외한 accessToken값을 넣어주세요."; + SecurityRequirement securityRequirement = new SecurityRequirement().addList(securityRequirementName); + + Components components = new Components() + .addSecuritySchemes(securityRequirementName, new SecurityScheme() + .name(securityRequirementName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT")); + + return new OpenAPI() + .components(components) + .addSecurityItem(securityRequirement); + } +} From 8db9d35d04a93d9a96ef1fe3b34ab4d4c39ca4bc Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Tue, 2 Apr 2024 17:06:34 +0900 Subject: [PATCH 30/33] =?UTF-8?q?Feat:=20API=EC=97=90=20=EC=84=A4=EB=AA=85?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스웨거에서 제공하는 어노테이션을 사용해서 API에 대한 설명 추가 --- .../domain/book/controller/BookController.java | 11 ++++++++++- .../highlight/controller/HighlightController.java | 7 +++++++ .../domain/member/controller/MemberController.java | 12 ++++++++++-- .../domain/record/controller/RecordController.java | 7 +++++++ .../domain/review/controller/ReviewController.java | 7 +++++++ .../domain/summary/controller/SummaryController.java | 6 ++++++ 6 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/api/readinglog/domain/book/controller/BookController.java b/src/main/java/com/api/readinglog/domain/book/controller/BookController.java index a7dfd7a..194ae4e 100644 --- a/src/main/java/com/api/readinglog/domain/book/controller/BookController.java +++ b/src/main/java/com/api/readinglog/domain/book/controller/BookController.java @@ -8,6 +8,8 @@ import com.api.readinglog.domain.book.dto.BookSearchApiResponse; import com.api.readinglog.domain.book.dto.BookModifyRequest; import com.api.readinglog.domain.book.service.BookService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,20 +27,23 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -@RestController +@Tag(name = "Book", description = "Book API") @Slf4j +@RestController @RequestMapping("/api/books") @RequiredArgsConstructor public class BookController { private final BookService bookService; + @Operation(summary = "Find book by ID", description = "사용자가 등록한 책 정보 조회") @GetMapping("/{bookId}") public Response getBookInfo(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) { return Response.success(HttpStatus.OK, String.format("%d번 책 정보 응답 성공", bookId), bookService.getBookInfo(user.getId(), bookId)); } + @Operation(summary = "Search book", description = "책 검색") @GetMapping("/search") public Response searchBooks(@RequestParam(required = false) String q, @RequestParam(defaultValue = "1") int start) { @@ -46,6 +51,7 @@ public Response searchBooks(@RequestParam(required = fals return Response.success(HttpStatus.OK, "책 검색 성공", bookService.searchBooks(q, start)); } + @Operation(summary = "Add a new book after search", description = "책 검색 후 등록") @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) public Response registerBookAfterSearch(@AuthenticationPrincipal CustomUserDetail user, @RequestBody @Valid BookRegisterRequest request) { @@ -54,6 +60,7 @@ public Response registerBookAfterSearch(@AuthenticationPrincipal CustomUse return Response.success(HttpStatus.CREATED, "책 등록 성공"); } + @Operation(summary = "Add a new book direct registration", description = "책 직접 등록") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public Response registerBookDirect(@AuthenticationPrincipal CustomUserDetail user, @ModelAttribute @Valid BookDirectRequest request) { @@ -62,6 +69,7 @@ public Response registerBookDirect(@AuthenticationPrincipal CustomUserDeta return Response.success(HttpStatus.CREATED, "책 등록 성공"); } + @Operation(summary = "Modify book info", description = "책 정보 수정") @PatchMapping("/{bookId}") public Response modifyBook(@AuthenticationPrincipal CustomUserDetail user, @ModelAttribute BookModifyRequest bookModifyRequest, @@ -71,6 +79,7 @@ public Response modifyBook(@AuthenticationPrincipal CustomUserDetail user, return Response.success(HttpStatus.OK, "책 수정 성공"); } + @Operation(summary = "Delete book", description = "책 삭제") @DeleteMapping("/{bookId}") public Response deleteBook(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) { // TODO: 책이 삭제될 때 해당 책에 관한 기록, 포스트 함께 삭제? diff --git a/src/main/java/com/api/readinglog/domain/highlight/controller/HighlightController.java b/src/main/java/com/api/readinglog/domain/highlight/controller/HighlightController.java index a167c48..e55a109 100644 --- a/src/main/java/com/api/readinglog/domain/highlight/controller/HighlightController.java +++ b/src/main/java/com/api/readinglog/domain/highlight/controller/HighlightController.java @@ -6,6 +6,8 @@ import com.api.readinglog.domain.highlight.controller.dto.request.WriteRequest; import com.api.readinglog.domain.highlight.controller.dto.response.HighlightResponse; import com.api.readinglog.domain.highlight.service.HighlightService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; @@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Highlight", description = "Highlight API") @Slf4j @RestController @RequestMapping("/api/highlights") @@ -29,6 +32,7 @@ public class HighlightController { private final HighlightService highlightService; + @Operation(summary = "Find highlights", description = "내가 쓴 하이라이트 목록 조회") @GetMapping("/{bookId}/me") public Response> highlights(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) { @@ -37,6 +41,7 @@ public Response> highlights(@AuthenticationPrincipal Cus return Response.success(HttpStatus.OK, "내가 쓴 하이라이트 목록 조회 성공", response); } + @Operation(summary = "Add a new highlight", description = "하이라이트 작성") @PostMapping("/{bookId}") public Response write(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId, @@ -47,6 +52,7 @@ public Response write(@AuthenticationPrincipal CustomUserDetail user, return Response.success(HttpStatus.CREATED, "하이라이트 작성 성공"); } + @Operation(summary = "Modify highlight", description = "하이라이트 수정") @PatchMapping("/{highlightId}") public Response modify(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long highlightId, @@ -56,6 +62,7 @@ public Response modify(@AuthenticationPrincipal CustomUserDetail user, return Response.success(HttpStatus.OK, "하이라이트 수정 성공"); } + @Operation(summary = "Delete highlight", description = "하이라이트 삭제") @DeleteMapping("/{highlightId}") public Response delete(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long highlightId) { diff --git a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java index 4360dd1..5d6891c 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java @@ -15,6 +15,8 @@ import com.api.readinglog.domain.member.controller.dto.request.UpdateProfileRequest; import com.api.readinglog.domain.member.controller.dto.response.MemberDetailsResponse; import com.api.readinglog.domain.member.service.MemberService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -31,10 +33,11 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@RestController +@Tag(name = "Member", description = "Member API") @Slf4j -@RequiredArgsConstructor +@RestController @RequestMapping("/api/members") +@RequiredArgsConstructor public class MemberController { private final MemberService memberService; @@ -46,12 +49,14 @@ public Response join_nickname(@ModelAttribute @Valid JoinNicknameRequest r return Response.success(HttpStatus.OK, "닉네임 검사 통과!"); } + @Operation(summary = "Create member", description = "일반 회원가입") @PostMapping("/join") public Response join(@ModelAttribute @Valid JoinRequest request) { memberService.join(request); return Response.success(HttpStatus.CREATED, "회원 가입 완료"); } + @Operation(summary = "Login member into the system", description = "일반 로그인") @PostMapping("/login") public Response login(@RequestBody LoginRequest request, HttpServletResponse response) { JwtToken jwtToken = memberService.login(request); @@ -66,6 +71,7 @@ public Response findMember(@AuthenticationPrincipal Custo return Response.success(HttpStatus.OK, "회원 조회 성공!", member); } + @Operation(summary = "Updated member", description = "회원정보 수정") @PatchMapping("/me") public Response updateProfile(@AuthenticationPrincipal CustomUserDetail user, @ModelAttribute @Valid UpdateProfileRequest request) { @@ -73,6 +79,7 @@ public Response updateProfile(@AuthenticationPrincipal CustomUserDetail us return Response.success(HttpStatus.OK, "회원 수정 성공!"); } + @Operation(summary = "Logout member into the system", description = "로그아웃") @PostMapping("/logout") public Response logout(HttpServletRequest request, HttpServletResponse response) { String refreshToken = CookieUtils.extractRefreshToken(request); @@ -80,6 +87,7 @@ public Response logout(HttpServletRequest request, HttpServletResponse res return Response.success(HttpStatus.OK, "로그아웃 성공!"); } + @Operation(summary = "Deleted member", description = "일반 회원 탈퇴") @DeleteMapping("/me") public Response deleteMember(@AuthenticationPrincipal CustomUserDetail user, @RequestBody DeleteRequest request) { diff --git a/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java b/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java index 8d42ade..3ee2189 100644 --- a/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java +++ b/src/main/java/com/api/readinglog/domain/record/controller/RecordController.java @@ -6,6 +6,8 @@ import com.api.readinglog.domain.record.controller.dto.request.RecordWriteRequest; import com.api.readinglog.domain.record.controller.dto.response.RecordResponse; import com.api.readinglog.domain.record.service.RecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; @@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Record", description = "Record API") @RestController @RequestMapping("/api/records") @RequiredArgsConstructor @@ -29,6 +32,7 @@ public class RecordController { private final RecordService recordService; + @Operation(summary = "Find records", description = "독서 기록 조회") @GetMapping("/{bookId}") public Response> getRecord(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) { @@ -36,6 +40,7 @@ public Response> getRecord(@AuthenticationPrincipal CustomU return Response.success(HttpStatus.OK, "독서 기록 조회 성공", response); } + @Operation(summary = "Add a new record", description = "독서 기록 추가") @PostMapping("/{bookId}") public Response addRecord(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId, @@ -45,6 +50,7 @@ public Response addRecord(@AuthenticationPrincipal CustomUserDetail user, return Response.success(HttpStatus.OK, "독서 기록 추가 성공"); } + @Operation(summary = "Modify record", description = "독서 기록 수정") @PatchMapping("/{recordId}") public Response modify(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long recordId, @@ -54,6 +60,7 @@ public Response modify(@AuthenticationPrincipal CustomUserDetail user, return Response.success(HttpStatus.OK, "독서 기록 수정 성공"); } + @Operation(summary = "Delete record", description = "독서 기록 삭제") @DeleteMapping("/{recordId}") public Response delete(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long recordId) { diff --git a/src/main/java/com/api/readinglog/domain/review/controller/ReviewController.java b/src/main/java/com/api/readinglog/domain/review/controller/ReviewController.java index 7512917..f9090c9 100644 --- a/src/main/java/com/api/readinglog/domain/review/controller/ReviewController.java +++ b/src/main/java/com/api/readinglog/domain/review/controller/ReviewController.java @@ -6,6 +6,8 @@ import com.api.readinglog.domain.review.controller.dto.request.WriteRequest; import com.api.readinglog.domain.review.controller.dto.response.ReviewResponse; import com.api.readinglog.domain.review.service.ReviewService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; @@ -20,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Review", description = "Review API") @RestController @RequestMapping("/api/reviews") @RequiredArgsConstructor @@ -27,6 +30,7 @@ public class ReviewController { private final ReviewService reviewService; + @Operation(summary = "Find reviews", description = "내가 쓴 서평 목록 조회") @GetMapping("/{bookId}/me") public Response> reviews(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) { @@ -35,6 +39,7 @@ public Response> reviews(@AuthenticationPrincipal CustomUse return Response.success(HttpStatus.OK, "내가 쓴 서평 목록 조회 성공", response); } + @Operation(summary = "Add a new review", description = "서평 작성") @PostMapping("/{bookId}") public Response write(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId, @@ -44,6 +49,7 @@ public Response write(@AuthenticationPrincipal CustomUserDetail user, return Response.success(HttpStatus.CREATED, "서평 작성 성공"); } + @Operation(summary = "Modify review", description = "서평 수정") @PatchMapping("/{reviewId}") public Response modify(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long reviewId, @@ -53,6 +59,7 @@ public Response modify(@AuthenticationPrincipal CustomUserDetail user, return Response.success(HttpStatus.OK, "서평 수정 성공"); } + @Operation(summary = "Delete review", description = "서평 삭제") @DeleteMapping("/{reviewId}") public Response modify(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long reviewId) { diff --git a/src/main/java/com/api/readinglog/domain/summary/controller/SummaryController.java b/src/main/java/com/api/readinglog/domain/summary/controller/SummaryController.java index 7aed2af..8da1b69 100644 --- a/src/main/java/com/api/readinglog/domain/summary/controller/SummaryController.java +++ b/src/main/java/com/api/readinglog/domain/summary/controller/SummaryController.java @@ -5,6 +5,8 @@ import com.api.readinglog.domain.summary.controller.dto.request.ModifyRequest; import com.api.readinglog.domain.summary.controller.dto.request.WriteRequest; import com.api.readinglog.domain.summary.service.SummaryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Summary", description = "Summary API") @RestController @RequestMapping("/api/summaries") @RequiredArgsConstructor @@ -24,6 +27,7 @@ public class SummaryController { private final SummaryService summaryService; + @Operation(summary = "Add a new summary", description = "한줄평 작성") @PostMapping("/{bookId}") public Response write(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId, @@ -33,6 +37,7 @@ public Response write(@AuthenticationPrincipal CustomUserDetail user, return Response.success(HttpStatus.CREATED, "한줄평 작성 성공"); } + @Operation(summary = "Modify summary", description = "한줄평 수정") @PatchMapping("/{summaryId}") public Response modify(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long summaryId, @@ -42,6 +47,7 @@ public Response modify(@AuthenticationPrincipal CustomUserDetail user, return Response.success(HttpStatus.OK, "한줄평 수정 성공"); } + @Operation(summary = "Delete summary", description = "한줄평 삭제") @DeleteMapping("/{summaryId}") public Response delete(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long summaryId) { From 5899c1d10f7b00ed5f208d22320ac9fc0de142a6 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Tue, 2 Apr 2024 17:09:44 +0900 Subject: [PATCH 31/33] =?UTF-8?q?Feat:=20concat=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/api/readinglog/common/swagger/SwaggerConfig.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/api/readinglog/common/swagger/SwaggerConfig.java b/src/main/java/com/api/readinglog/common/swagger/SwaggerConfig.java index bcbb95c..014d687 100644 --- a/src/main/java/com/api/readinglog/common/swagger/SwaggerConfig.java +++ b/src/main/java/com/api/readinglog/common/swagger/SwaggerConfig.java @@ -21,7 +21,6 @@ info = @Info( title = "리딩로그 API 명세서", description = "독서 기록 서비스 리딩로그의 API 명세서", - contact = @Contact(name = "the developer", email = "ddmkim94@gmail.com"), version = "v1.0") ) @Configuration From c0070772f5a0447ca4f5a0a7e1efba3668057e65 Mon Sep 17 00:00:00 2001 From: jeondui Date: Tue, 2 Apr 2024 18:34:33 +0900 Subject: [PATCH 32/33] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=20API=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/AuthCodeVerificationRequest.java | 3 + .../domain/email/dto/EmailRequest.java | 2 + .../member/controller/MemberController.java | 131 +++++++++++++++--- .../controller/dto/request/DeleteRequest.java | 3 + .../controller/dto/request/LoginRequest.java | 4 + .../dto/request/UpdatePasswordRequest.java | 3 + 6 files changed, 127 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java b/src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java index 343afdc..df526e4 100644 --- a/src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java +++ b/src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java @@ -1,5 +1,6 @@ package com.api.readinglog.domain.email.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import lombok.Getter; @@ -7,7 +8,9 @@ public class AuthCodeVerificationRequest { @Email(message = "이메일 형식이 올바르지 않습니다.") + @Schema(description = "인증에 사용한 이메일") private String email; + @Schema(description = "이메일로 발급 받은 인증 코드") private String authCode; } diff --git a/src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java b/src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java index 56e4aad..c49aa94 100644 --- a/src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java +++ b/src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java @@ -1,5 +1,6 @@ package com.api.readinglog.domain.email.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import lombok.Getter; @@ -7,5 +8,6 @@ public class EmailRequest { @Email(message = "이메일 형식이 올바르지 않습니다.") + @Schema(description = "사용자 이메일") private String email; } diff --git a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java index 5d6891c..1f71ea5 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java @@ -16,7 +16,12 @@ import com.api.readinglog.domain.member.controller.dto.response.MemberDetailsResponse; import com.api.readinglog.domain.member.service.MemberService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -33,7 +38,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "Member", description = "Member API") +@Tag(name = "Members", description = "회원 API 목록입니다.") @Slf4j @RestController @RequestMapping("/api/members") @@ -43,96 +48,184 @@ public class MemberController { private final MemberService memberService; private final EmailService emailService; + @Operation(summary = "닉네임 중복 검사", description = "회원 가입 전, 닉네임 중복을 검사합니다.", + parameters = { + @Parameter(name = "nickname", description = "닉네임", required = true, + schema = @Schema(type = "string", implementation = String.class)) + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "닉네임 중복 검사 통과", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "닉네임 중복 검사 실패") + }) @PostMapping("/join-nickname") public Response join_nickname(@ModelAttribute @Valid JoinNicknameRequest request) { memberService.joinNickname(request); - return Response.success(HttpStatus.OK, "닉네임 검사 통과!"); + return Response.success(HttpStatus.OK, "닉네임 중복 검사 통과"); } - @Operation(summary = "Create member", description = "일반 회원가입") + @Operation(summary = "회원 가입", description = "일반 이메일 회원 가입입니다.", + parameters = { + @Parameter(name = "email", description = "이메일", example = "test@test.com", required = true, + schema = @Schema(type = "string", implementation = String.class)), + @Parameter(name = "password", description = "비밀번호", example = "Password123!", required = true, + schema = @Schema(type = "string", implementation = String.class)), + @Parameter(name = "passwordConfirm", description = "비밀번호 확인", example = "Password123!", required = true, + schema = @Schema(type = "string", implementation = String.class)), + @Parameter(name = "nickname", description = "닉네임", example = "테스트닉네임", required = true, + schema = @Schema(type = "string", implementation = String.class)), + @Parameter(name = "profileImage", description = "프로필 이미지", required = false, + schema = @Schema(type = "string", implementation = String.class)), + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "회원 가입 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "회원 가입 실패 (필수 입력값을 입력하지 않은 경우, 비밀번호와 비밀번호 확인이 일치하지 않는 경우)") + }) @PostMapping("/join") public Response join(@ModelAttribute @Valid JoinRequest request) { memberService.join(request); - return Response.success(HttpStatus.CREATED, "회원 가입 완료"); + return Response.success(HttpStatus.CREATED, "회원 가입 성공"); } - @Operation(summary = "Login member into the system", description = "일반 로그인") + @Operation(summary = "로그인", description = "일반 로그인") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "401", description = "로그인 실패: 인증에 실패하였습니다.") + }) @PostMapping("/login") public Response login(@RequestBody LoginRequest request, HttpServletResponse response) { JwtToken jwtToken = memberService.login(request); response.addHeader("Authorization", jwtToken.getAccessToken()); CookieUtils.addCookie(response, "refreshToken", jwtToken.getRefreshToken(), 24 * 60 * 60 * 7); - return Response.success(HttpStatus.OK, "로그인 성공!"); + return Response.success(HttpStatus.OK, "로그인 성공"); } + @Operation(summary = "회원 정보 조회", description = "인증 토큰을 사용하여 회원 정보를 조회합니다.") @GetMapping("/me") public Response findMember(@AuthenticationPrincipal CustomUserDetail user) { MemberDetailsResponse member = memberService.getMemberDetails(user.getId()); - return Response.success(HttpStatus.OK, "회원 조회 성공!", member); + return Response.success(HttpStatus.OK, "회원 조회 성공", member); } - @Operation(summary = "Updated member", description = "회원정보 수정") + @Operation(summary = "회원 정보 수정", description = "회원 정보를 수정합니다.", + parameters = { + @Parameter(name = "nickname", description = "닉네임", example = "새로운닉네임", required = true, + schema = @Schema(type = "string", implementation = String.class)), + @Parameter(name = "profileImage", description = "프로필 이미지", required = false, + schema = @Schema(type = "string", implementation = String.class)) + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "회원 수정 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "401", description = "로그인 실패: 인증에 실패하였습니다.") + }) @PatchMapping("/me") public Response updateProfile(@AuthenticationPrincipal CustomUserDetail user, @ModelAttribute @Valid UpdateProfileRequest request) { memberService.updateProfile(user.getId(), request); - return Response.success(HttpStatus.OK, "회원 수정 성공!"); + return Response.success(HttpStatus.OK, "회원 수정 성공"); } - @Operation(summary = "Logout member into the system", description = "로그아웃") + @Operation(summary = "로그아웃", description = "쿠키에 저장된 리프레시 토큰을 통해 로그아웃 합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그아웃 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "리프레시 토큰이 쿠키에 없습니다.") + }) @PostMapping("/logout") public Response logout(HttpServletRequest request, HttpServletResponse response) { String refreshToken = CookieUtils.extractRefreshToken(request); memberService.logout(refreshToken, response); - return Response.success(HttpStatus.OK, "로그아웃 성공!"); + return Response.success(HttpStatus.OK, "로그아웃 성공"); } - @Operation(summary = "Deleted member", description = "일반 회원 탈퇴") + @Operation(summary = "일반 회원 탈퇴", description = "일반 회원은 비밀번호 확인을 통해 회원 검증 후, 서비스를 탈퇴할 수 있습니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "일반 회원 탈퇴 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "회원이 존재하지 않습니다!") + }) @DeleteMapping("/me") public Response deleteMember(@AuthenticationPrincipal CustomUserDetail user, @RequestBody DeleteRequest request) { memberService.deleteMember(user.getId(), request); - return Response.success(HttpStatus.OK, "일반 회원 탈퇴 성공!"); + return Response.success(HttpStatus.OK, "일반 회원 탈퇴 성공"); } + @Operation(summary = "소셜 회원 탈퇴", description = "소셜 회원은 재로그인을 통해 회원 검증 후, 재발급 받은 액세스 토큰을 통해 서비스를 탈퇴할 수 있습니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "소셜 회원 탈퇴 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "회원이 존재하지 않습니다!") + }) @DeleteMapping("/social/me") public Response deleteSocialMember(@AuthenticationPrincipal CustomUserDetail user) { memberService.deleteSocialMember(user.getId()); - return Response.success(HttpStatus.OK, "소셜 회원 탈퇴 성공!"); + return Response.success(HttpStatus.OK, "소셜 회원 탈퇴 성공"); } + @Operation(summary = "토큰 재발급", description = "액세스 토큰이 만료된 경우, 리프레시 토큰을 이용하여 재발급 받을 수 있습니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "토큰 재발급 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "리프레시 토큰이 쿠키에 없습니다.") + }) @GetMapping("/reissue") public Response reissue(HttpServletRequest request, HttpServletResponse response) { String refreshToken = CookieUtils.extractRefreshToken(request); JwtToken newToken = memberService.reissueToken(refreshToken); response.addHeader("Authorization", newToken.getAccessToken()); CookieUtils.addCookie(response, "refreshToken", newToken.getRefreshToken(), 24 * 60 * 60 * 7); - return Response.success(HttpStatus.OK, "토큰 재발급 성공!"); + return Response.success(HttpStatus.OK, "토큰 재발급 성공"); } + @Operation(summary = "비밀번호 변경", description = "비밀번호 변경입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "비밀번호 변경 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "비밀번호 변경 실패") + }) @PatchMapping("/password") public Response updatePassword(@AuthenticationPrincipal CustomUserDetail user, @RequestBody @Valid UpdatePasswordRequest request) { memberService.updatePassword(user.getId(), request); - return Response.success(HttpStatus.OK, "비밀번호 변경 성공!"); + return Response.success(HttpStatus.OK, "비밀번호 변경 성공"); } + @Operation(summary = "이메일 인증 코드 전송", description = "사용자 이메일로 인증 코드를 전송합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "이메일 인증 코드 전송 완료", + content = {@Content(schema = @Schema(implementation = Response.class))}) + }) @PostMapping("/send-authCode") public Response sendEmailAuthCode(@RequestBody @Valid EmailRequest request) { emailService.sendAuthCode(request.getEmail()); - return Response.success(HttpStatus.OK, "이메일 인증 코드 전송 완료!"); + return Response.success(HttpStatus.OK, "이메일 인증 코드 전송 완료"); } + @Operation(summary = "이메일 인증", description = "사용자 이메일로 보낸 인증 코드를 검증하여 이메일을 인증합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "이메일 인증 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "이메일 인증에 실패하였습니다.") + }) @PostMapping("/verify-authCode") public Response verifyAuthCode(@RequestBody @Valid AuthCodeVerificationRequest request) { emailService.verifyAuthCode(request.getEmail(), request.getAuthCode()); - return Response.success(HttpStatus.OK, "이메일 인증 성공!"); + return Response.success(HttpStatus.OK, "이메일 인증 성공"); } + @Operation(summary = "임시 비밀번호 전송", description = "사용자 이메일로 임시 비밀번호를 전송합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "임시 비밀번호 전송 완료", + content = {@Content(schema = @Schema(implementation = Response.class))}) + }) @PostMapping("/send-temporaryPassword") public Response sendEmailTempPassword(@AuthenticationPrincipal CustomUserDetail user, @RequestBody @Valid EmailRequest request) { emailService.sendTemporaryPassword(user.getId(), request.getEmail()); - return Response.success(HttpStatus.OK, "임시 비밀번호 전송 완료!"); + return Response.success(HttpStatus.OK, "임시 비밀번호 전송 완료"); } } diff --git a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/DeleteRequest.java b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/DeleteRequest.java index b4b4d1f..0345890 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/DeleteRequest.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/DeleteRequest.java @@ -1,5 +1,6 @@ package com.api.readinglog.domain.member.controller.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -8,5 +9,7 @@ @NoArgsConstructor @AllArgsConstructor public class DeleteRequest { + + @Schema(description = "회원 탈퇴에 필요한 비밀번호") private String password; } diff --git a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/LoginRequest.java b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/LoginRequest.java index 13a0516..0d1405f 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/LoginRequest.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/LoginRequest.java @@ -1,10 +1,14 @@ package com.api.readinglog.domain.member.controller.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @Getter public class LoginRequest { + @Schema(description = "이메일", example = "test@test.com") private String email; + + @Schema(description = "비밀번호", example = "Password123!") private String password; } diff --git a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java index 00ee8c6..2099f4e 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java @@ -1,5 +1,6 @@ package com.api.readinglog.domain.member.controller.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.Getter; @@ -14,8 +15,10 @@ public class UpdatePasswordRequest { @NotBlank(message = "비밀번호는 필수 입력 값입니다.") @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&]).{8,20}$", message = "비밀번호는 8~20자의 영문 대소문자, 숫자, 특수문자를 포함해야 합니다.") + @Schema(description = "새로운 비밀번호") private String newPassword; @NotBlank(message = "비밀번호 확인은 필수입니다.") + @Schema(description = "새로운 비밀번호 확인") private String newPasswordConfirm; } From 59388d2fa9d8315a4ab1b3549d98f030e38dce7e Mon Sep 17 00:00:00 2001 From: jeondui Date: Tue, 2 Apr 2024 18:35:01 +0900 Subject: [PATCH 33/33] =?UTF-8?q?Feat:=20=EB=B6=81=EB=A1=9C=EA=B7=B8=20API?= =?UTF-8?q?=20=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ .../booklog/controller/BookLogController.java | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/build.gradle b/build.gradle index 3db55e1..168305e 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,9 @@ dependencies { /* Redis */ implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + /* Swagger */ + implementation group: 'io.swagger.core.v3', name: 'swagger-core-jakarta', version: '2.2.7' } tasks.named('bootBuildImage') { diff --git a/src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java b/src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java index 881ff84..9f0549f 100644 --- a/src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java +++ b/src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java @@ -5,6 +5,12 @@ import com.api.readinglog.domain.booklog.controller.dto.BookLogResponse; import com.api.readinglog.domain.booklog.service.BookLogService; import com.api.readinglog.domain.summary.controller.dto.response.SummaryResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -17,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "BookLogs", description = "북로그 API 목록입니다.") @RestController @RequiredArgsConstructor @RequestMapping("/api/book-logs") @@ -24,12 +31,24 @@ public class BookLogController { private final BookLogService bookLogService; + @Operation(summary = "나의 로그 조회", description = "인증 토큰을 통해 특정 사용자가 작성한 로그 정보를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "나의 로그 조회 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "나의 로그 조회 실패") + }) @GetMapping("/{bookId}/me") public Response myLogs(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) { return Response.success(HttpStatus.OK, "나의 로그 조회 성공", bookLogService.myLogs(user.getId(), bookId)); } + @Operation(summary = "북로그 조회", description = "리딩 로그 서비스의 모든 북로그를 조회합니다. 비회원도 조회가 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "북로그 조회 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "404", description = "북로그 목록이 존재하지 않습니다!") + }) @GetMapping public Response> bookLogs(@PageableDefault(sort = "createdAt", direction = Direction.DESC) Pageable pageable) {