From 24382cda3e5c3d942b741c127d4fcbcf027cff20 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 7 Dec 2023 12:47:27 +0900 Subject: [PATCH 01/45] =?UTF-8?q?Chore:=20builde.gradle=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이메일 인증 기능 구현을 위해 java mail sender 의존성 추가 --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 915bfee..4cc65b8 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,9 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + // mail + implementation 'org.springframework.boot:spring-boot-starter-mail' + //Querydsl 추가 implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" From 9007a153f0aeaf78bb871f3a0ee74fb7e2645c41 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 7 Dec 2023 12:57:06 +0900 Subject: [PATCH 02/45] =?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 - 인증 링크에는 회원가입 페이지에서 작성한 이메일과 인증용 토큰이 포함되고 해당 링크를 클릭하면 인증이 완료된다. - 회원 가입 시 메일 인증을 하지 않으면 가입이 처리되지 않도록 코드 수정 --- .../trip/domain/email/model/EmailAuth.java | 45 ++++++++++++++ .../email/repository/EmailAuthRepository.java | 12 ++++ .../repository/EmailAuthRepositoryCustom.java | 11 ++++ .../EmailAuthRepositoryCustomImpl.java | 32 ++++++++++ .../domain/email/service/EmailService.java | 62 +++++++++++++++++++ .../member/controller/MemberController.java | 24 +++---- .../member/controller/dto/EmailResponse.java | 19 ++++++ .../member/controller/dto/JoinRequest.java | 8 +++ .../api/trip/domain/member/model/Member.java | 12 ++-- .../domain/member/service/MemberService.java | 16 ++++- 10 files changed, 221 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/api/trip/domain/email/model/EmailAuth.java create mode 100644 src/main/java/com/api/trip/domain/email/repository/EmailAuthRepository.java create mode 100644 src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustom.java create mode 100644 src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustomImpl.java create mode 100644 src/main/java/com/api/trip/domain/email/service/EmailService.java create mode 100644 src/main/java/com/api/trip/domain/member/controller/dto/EmailResponse.java diff --git a/src/main/java/com/api/trip/domain/email/model/EmailAuth.java b/src/main/java/com/api/trip/domain/email/model/EmailAuth.java new file mode 100644 index 0000000..51ba76a --- /dev/null +++ b/src/main/java/com/api/trip/domain/email/model/EmailAuth.java @@ -0,0 +1,45 @@ +package com.api.trip.domain.email.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EmailAuth { + + private static final Long MAX_EXPIRE_TIME = 5L; // 링크 유효 기간 5분 + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String email; // 가입 이메일 + + private String authToken; // 인증 토큰 (UUID) + + private boolean expired; // 토큰 사용 여부 (인증 완료: true, 인증 미완료: false) + + private LocalDateTime expireDate; // 인증 토큰 만료일 (발급 날짜 + 5분) + + @Builder + public EmailAuth(String email, String authToken, boolean expired) { + this.email = email; + this.authToken = authToken; + this.expired = expired; + this.expireDate = LocalDateTime.now().plusMinutes(MAX_EXPIRE_TIME); + } + + public void useToken() { + this.expired = true; + } + +} diff --git a/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepository.java b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepository.java new file mode 100644 index 0000000..0bcb4a1 --- /dev/null +++ b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepository.java @@ -0,0 +1,12 @@ +package com.api.trip.domain.email.repository; + +import com.api.trip.domain.email.model.EmailAuth; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface EmailAuthRepository extends JpaRepository, EmailAuthRepositoryCustom { + + Optional findByEmail(String email); + +} diff --git a/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustom.java b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustom.java new file mode 100644 index 0000000..aa6be06 --- /dev/null +++ b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.api.trip.domain.email.repository; + +import com.api.trip.domain.email.model.EmailAuth; + +import java.time.LocalDateTime; +import java.util.Optional; + +public interface EmailAuthRepositoryCustom { + Optional findValidAuthByEmail(String email, String authToken, LocalDateTime now); + +} diff --git a/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustomImpl.java new file mode 100644 index 0000000..3d0936c --- /dev/null +++ b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustomImpl.java @@ -0,0 +1,32 @@ +package com.api.trip.domain.email.repository; + +import com.api.trip.domain.email.model.EmailAuth; +import com.api.trip.domain.email.model.QEmailAuth; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; + +import java.time.LocalDateTime; +import java.util.Optional; + +public class EmailAuthRepositoryCustomImpl implements EmailAuthRepositoryCustom{ + + private final JPAQueryFactory jpaQueryFactory; + + public EmailAuthRepositoryCustomImpl(EntityManager em) { + this.jpaQueryFactory = new JPAQueryFactory(em); + } + + @Override + public Optional findValidAuthByEmail(String email, String authToken, LocalDateTime now) { + + EmailAuth emailAuth = jpaQueryFactory + .selectFrom(QEmailAuth.emailAuth) + .where(QEmailAuth.emailAuth.email.eq(email), + QEmailAuth.emailAuth.authToken.eq(authToken), + QEmailAuth.emailAuth.expireDate.goe(now), + QEmailAuth.emailAuth.expired.eq(false)) + .fetchFirst(); + + return Optional.ofNullable(emailAuth); + } +} diff --git a/src/main/java/com/api/trip/domain/email/service/EmailService.java b/src/main/java/com/api/trip/domain/email/service/EmailService.java new file mode 100644 index 0000000..2d82561 --- /dev/null +++ b/src/main/java/com/api/trip/domain/email/service/EmailService.java @@ -0,0 +1,62 @@ +package com.api.trip.domain.email.service; + +import com.api.trip.domain.email.model.EmailAuth; +import com.api.trip.domain.email.repository.EmailAuthRepository; +import com.api.trip.domain.member.controller.dto.EmailResponse; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +@EnableAsync +@Transactional +@RequiredArgsConstructor +public class EmailService { + + private final JavaMailSender javaMailSender; + private final EmailAuthRepository emailAuthRepository; + + @Async + public void send(String email, String authToken) { + MimeMessage message = javaMailSender.createMimeMessage(); + String text = "http://localhost:8080/api/members/auth-email/%s/%s" .formatted(email, authToken); + + try { + message.setSubject("[Trip Trip] 회원가입 인증 메일"); + message.setRecipients(MimeMessage.RecipientType.TO, email); + message.setText(text, "UTF-8", "HTML"); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + + javaMailSender.send(message); + } + + // 인증 메일 검증 + public EmailResponse authEmail(String email, String authToken) { + EmailAuth emailAuth = emailAuthRepository.findValidAuthByEmail(email, authToken, LocalDateTime.now()) + .orElseThrow(() -> new RuntimeException("토큰 정보가 일치하지 않습니다!")); + + emailAuth.useToken(); // 토큰 사용 -> 만료 + return EmailResponse.of(emailAuth.isExpired()); + } + + public String createEmailAuth(String email) { + EmailAuth emailAuth = EmailAuth.builder() + .email(email) + .authToken(UUID.randomUUID().toString()) + .expired(false) + .build(); + + return emailAuthRepository.save(emailAuth).getAuthToken(); + } + +} diff --git a/src/main/java/com/api/trip/domain/member/controller/MemberController.java b/src/main/java/com/api/trip/domain/member/controller/MemberController.java index ccafff3..16518e2 100644 --- a/src/main/java/com/api/trip/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/trip/domain/member/controller/MemberController.java @@ -1,26 +1,22 @@ package com.api.trip.domain.member.controller; -import com.api.trip.common.security.dto.AuthenticationMember; +import com.api.trip.domain.email.service.EmailService; +import com.api.trip.domain.member.controller.dto.EmailResponse; import com.api.trip.domain.member.controller.dto.JoinRequest; import com.api.trip.domain.member.controller.dto.LoginRequest; import com.api.trip.domain.member.controller.dto.LoginResponse; import com.api.trip.domain.member.service.MemberService; -import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.data.jpa.repository.Query; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; -import java.security.Principal; - @RestController @RequestMapping("/api/members") @RequiredArgsConstructor public class MemberController { private final MemberService memberService; + private final EmailService emailService; @PostMapping("/join") public ResponseEntity joinMember(@RequestBody JoinRequest joinRequest) { @@ -34,10 +30,14 @@ public ResponseEntity loginMember(@RequestBody LoginRequest login return ResponseEntity.ok().body(loginResponse); } - // 이메일 값 확인을 위한 테스트 메서드 입니다. - @GetMapping("/info") - public ResponseEntity info() { - String email = SecurityContextHolder.getContext().getAuthentication().getName(); - return ResponseEntity.ok().body(email); + // 인증 메일 전송 + @PostMapping("/send-email/{email}") + public void sendAuthEmail(@PathVariable String email) { + emailService.send(email, emailService.createEmailAuth(email)); + } + + @GetMapping("/auth-email/{email}/{authToken}") + public ResponseEntity emailAndAuthToken(@PathVariable String email, @PathVariable String authToken) { + return ResponseEntity.ok().body(emailService.authEmail(email, authToken)); } } diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/EmailResponse.java b/src/main/java/com/api/trip/domain/member/controller/dto/EmailResponse.java new file mode 100644 index 0000000..db68daf --- /dev/null +++ b/src/main/java/com/api/trip/domain/member/controller/dto/EmailResponse.java @@ -0,0 +1,19 @@ +package com.api.trip.domain.member.controller.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class EmailResponse { + + private String message; + private boolean authEmail; + + public static EmailResponse of(boolean authEmail) { + return EmailResponse.builder() + .message("이메일 인증이 완료 되었습니다!") + .authEmail(authEmail) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java b/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java index 255e3b7..33e5629 100644 --- a/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java +++ b/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java @@ -10,4 +10,12 @@ public class JoinRequest { private String email; private String password; private String nickname; + + public static Member of(JoinRequest joinRequest, String password){ + return Member.builder() + .email(joinRequest.getEmail()) + .nickname(joinRequest.getNickname()) + .password(password) + .build(); + } } diff --git a/src/main/java/com/api/trip/domain/member/model/Member.java b/src/main/java/com/api/trip/domain/member/model/Member.java index bd121c0..43238c7 100644 --- a/src/main/java/com/api/trip/domain/member/model/Member.java +++ b/src/main/java/com/api/trip/domain/member/model/Member.java @@ -1,6 +1,5 @@ package com.api.trip.domain.member.model; -import com.api.trip.domain.member.controller.dto.JoinRequest; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -29,6 +28,8 @@ public class Member { @Enumerated(EnumType.STRING) private MemberRole role; + private boolean emailAuth; + @Builder private Member(String email, String nickname, String password){ this.email = email; @@ -37,11 +38,8 @@ private Member(String email, String nickname, String password){ this.role = MemberRole.MEMBER; } - public static Member of(JoinRequest joinRequest, String password){ - return Member.builder() - .email(joinRequest.getEmail()) - .nickname(joinRequest.getNickname()) - .password(password) - .build(); + // 이메일 인증 상태 변경 메서드 + public void emailVerifiedSuccess() { + this.emailAuth = true; } } diff --git a/src/main/java/com/api/trip/domain/member/service/MemberService.java b/src/main/java/com/api/trip/domain/member/service/MemberService.java index fc8796a..ff65985 100644 --- a/src/main/java/com/api/trip/domain/member/service/MemberService.java +++ b/src/main/java/com/api/trip/domain/member/service/MemberService.java @@ -2,6 +2,8 @@ import com.api.trip.common.security.JwtToken; import com.api.trip.common.security.JwtTokenProvider; +import com.api.trip.domain.email.model.EmailAuth; +import com.api.trip.domain.email.repository.EmailAuthRepository; import com.api.trip.domain.member.controller.dto.JoinRequest; import com.api.trip.domain.member.controller.dto.LoginRequest; import com.api.trip.domain.member.controller.dto.LoginResponse; @@ -11,6 +13,7 @@ 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.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,12 +26,23 @@ public class MemberService { private final MemberRepository memberRepository; + private final EmailAuthRepository emailAuthRepository; private final PasswordEncoder passwordEncoder; private final AuthenticationManagerBuilder authenticationManagerBuilder; private final JwtTokenProvider jwtTokenProvider; public void join(JoinRequest joinRequest) { - Member member = Member.of(joinRequest, passwordEncoder.encode(joinRequest.getPassword())); + // 중복 회원 체크 + memberRepository.findByEmail(joinRequest.getEmail()).ifPresent(it -> { + throw new RuntimeException("이미 존재하는 회원 입니다."); + }); + + EmailAuth emailAuth = emailAuthRepository.findByEmail(joinRequest.getEmail()) + .orElseThrow(() -> new RuntimeException("토큰 정보가 없습니다!")); + + Member member = JoinRequest.of(joinRequest, passwordEncoder.encode(joinRequest.getPassword())); + member.emailVerifiedSuccess(); + memberRepository.save(member); } From b4c2b07b93658045bf579115ec25d9d8199c0d55 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 7 Dec 2023 12:58:17 +0900 Subject: [PATCH 03/45] =?UTF-8?q?Feat:=20JwtTokenFilter=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이메일 인증 url은 토큰이 필요없으므로 필터를 타지 않도록 제외시킴. --- .../java/com/api/trip/common/security/JwtTokenFilter.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/api/trip/common/security/JwtTokenFilter.java b/src/main/java/com/api/trip/common/security/JwtTokenFilter.java index 72fffe1..8e87b6b 100644 --- a/src/main/java/com/api/trip/common/security/JwtTokenFilter.java +++ b/src/main/java/com/api/trip/common/security/JwtTokenFilter.java @@ -42,7 +42,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { - String[] excludePath = {"/api/members/join", "/api/members/login"}; + String[] excludePath = { + "/api/members/join", "/api/members/login", + "/api/members/send-email", "/api/members/auth-email" + }; + String path = request.getRequestURI(); return Arrays.stream(excludePath).anyMatch(path::startsWith); } From 3c6640d9133c462bb8122d4a1fa294855aaecc9a Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 7 Dec 2023 13:00:12 +0900 Subject: [PATCH 04/45] =?UTF-8?q?Docs:=20application.yml=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spring.mail.host 추가 특정 프로필을 가진 yml 파일에 작성하면 `No beans of 'JavaMailSender' type found.` 메세지 발생 기능 동작에는 문제가 없지만 해당 메세지를 없애기 위해 공통 yml에 추가함. --- src/main/resources/application.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5cff38d..bcb0593 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,7 +7,8 @@ spring: h2: console: enabled: true - + mail: + host: smtp.gmail.com logging: level: From 2e88e93761157c02782f0af00f7efaab6bcddcd0 Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Fri, 8 Dec 2023 01:04:47 +0900 Subject: [PATCH 05/45] =?UTF-8?q?Feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Entity 추가 - Repository 추가 --- .../trip/domain/article/model/Article.java | 43 +++++++++++++++++++ .../article/repository/ArticleRepository.java | 7 +++ 2 files changed, 50 insertions(+) create mode 100644 src/main/java/com/api/trip/domain/article/model/Article.java create mode 100644 src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java diff --git a/src/main/java/com/api/trip/domain/article/model/Article.java b/src/main/java/com/api/trip/domain/article/model/Article.java new file mode 100644 index 0000000..7999f9a --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/model/Article.java @@ -0,0 +1,43 @@ +package com.api.trip.domain.article.model; + +import com.api.trip.domain.member.model.Member; +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.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Article { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Member writer; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String content; + + private long viewCount; + + @Builder + private Article(Member writer, String title, String content, long viewCount) { + this.writer = writer; + this.title = title; + this.content = content; + this.viewCount = viewCount; + } +} diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java new file mode 100644 index 0000000..dfd7271 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java @@ -0,0 +1,7 @@ +package com.api.trip.domain.article.repository; + +import com.api.trip.domain.article.model.Article; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArticleRepository extends JpaRepository { +} From 47434ff18924a525ff9041b1f1a9ce523ca0a55c Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Fri, 8 Dec 2023 01:13:59 +0900 Subject: [PATCH 06/45] =?UTF-8?q?Feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=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 - crud 기능 구현 - 동적 쿼리 및 페이징 처리 --- .../article/controller/ArticleController.java | 70 ++++++++++++++++ .../controller/dto/CreateArticleRequest.java | 10 +++ .../controller/dto/ReadArticleResponse.java | 32 +++++++ .../controller/dto/UpdateArticleRequest.java | 10 +++ .../trip/domain/article/model/Article.java | 9 ++ .../article/repository/ArticleRepository.java | 7 +- .../repository/ArticleRepositoryCustom.java | 15 ++++ .../ArticleRepositoryCustomImpl.java | 78 +++++++++++++++++ .../article/service/ArticleService.java | 84 +++++++++++++++++++ 9 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/api/trip/domain/article/controller/ArticleController.java create mode 100644 src/main/java/com/api/trip/domain/article/controller/dto/CreateArticleRequest.java create mode 100644 src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java create mode 100644 src/main/java/com/api/trip/domain/article/controller/dto/UpdateArticleRequest.java create mode 100644 src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustom.java create mode 100644 src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java create mode 100644 src/main/java/com/api/trip/domain/article/service/ArticleService.java diff --git a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java new file mode 100644 index 0000000..a147fd4 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java @@ -0,0 +1,70 @@ +package com.api.trip.domain.article.controller; + +import com.api.trip.domain.article.controller.dto.CreateArticleRequest; +import com.api.trip.domain.article.controller.dto.ReadArticleResponse; +import com.api.trip.domain.article.controller.dto.UpdateArticleRequest; +import com.api.trip.domain.article.service.ArticleService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +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; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/articles") +@RequiredArgsConstructor +public class ArticleController { + + private final ArticleService articleService; + + @PostMapping + public ResponseEntity createArticle(@RequestBody CreateArticleRequest request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(articleService.createArticle(request, email)); + } + + @PatchMapping("/{articleId}") + public ResponseEntity updateArticle(@PathVariable("articleId") Long articleId, @RequestBody UpdateArticleRequest request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + articleService.updateArticle(articleId, request, email); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{articleId}") + public ResponseEntity deleteArticle(@PathVariable("articleId") Long articleId) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + articleService.deleteArticle(articleId, email); + return ResponseEntity.ok().build(); + } + + @GetMapping("/{articleId}") + public ResponseEntity readArticle(@PathVariable("articleId") Long articleId) { + return ResponseEntity.ok(articleService.readArticle(articleId)); + } + + @GetMapping + public ResponseEntity> getArticles( + @PageableDefault(size = 8, page = 0) Pageable pageable, + @RequestParam(value = "filter", required = false) String filter + ) { + return ResponseEntity.ok(articleService.getArticles(pageable, filter)); + } + + @GetMapping("/me") + public ResponseEntity> getMyArticles() { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(articleService.getMyArticles(email)); + } +} diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/CreateArticleRequest.java b/src/main/java/com/api/trip/domain/article/controller/dto/CreateArticleRequest.java new file mode 100644 index 0000000..7a16836 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/dto/CreateArticleRequest.java @@ -0,0 +1,10 @@ +package com.api.trip.domain.article.controller.dto; + +import lombok.Getter; + +@Getter +public class CreateArticleRequest { + + private String title; + private String content; +} diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java new file mode 100644 index 0000000..84d6d22 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java @@ -0,0 +1,32 @@ +package com.api.trip.domain.article.controller.dto; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ReadArticleResponse { + + private Long articleId; + private String title; + private Long writerId; + private String writerNickName; + private String writerRole; + private String content; + private long viewCount; + + public static ReadArticleResponse fromEntity(Article article) { + Member writer = article.getWriter(); + return builder() + .articleId(article.getId()) + .title(article.getTitle()) + .writerId(writer.getId()) + .writerNickName(writer.getNickname()) + .writerRole(writer.getRole().name()) + .content(article.getContent()) + .viewCount(article.getViewCount()) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/UpdateArticleRequest.java b/src/main/java/com/api/trip/domain/article/controller/dto/UpdateArticleRequest.java new file mode 100644 index 0000000..1eceb9d --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/dto/UpdateArticleRequest.java @@ -0,0 +1,10 @@ +package com.api.trip.domain.article.controller.dto; + +import lombok.Getter; + +@Getter +public class UpdateArticleRequest { + + private String title; + private String content; +} diff --git a/src/main/java/com/api/trip/domain/article/model/Article.java b/src/main/java/com/api/trip/domain/article/model/Article.java index 7999f9a..47de710 100644 --- a/src/main/java/com/api/trip/domain/article/model/Article.java +++ b/src/main/java/com/api/trip/domain/article/model/Article.java @@ -40,4 +40,13 @@ private Article(Member writer, String title, String content, long viewCount) { this.content = content; this.viewCount = viewCount; } + + public void modify(String title, String content) { + this.title = title; + this.content = content; + } + + public void increaseViewCount() { + this.viewCount++; + } } diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java index dfd7271..68c2f4d 100644 --- a/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java @@ -1,7 +1,12 @@ package com.api.trip.domain.article.repository; import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; import org.springframework.data.jpa.repository.JpaRepository; -public interface ArticleRepository extends JpaRepository { +import java.util.List; + +public interface ArticleRepository extends JpaRepository, ArticleRepositoryCustom { + + List
findAllByWriterOrderByIdDesc(Member writer); } diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustom.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustom.java new file mode 100644 index 0000000..723b199 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustom.java @@ -0,0 +1,15 @@ +package com.api.trip.domain.article.repository; + +import com.api.trip.domain.article.model.Article; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface ArticleRepositoryCustom { + + Optional
findArticle(@Param("articleId") Long articleId); + + Page
findArticles(Pageable pageable, String filter); +} diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java new file mode 100644 index 0000000..c6b8fc5 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java @@ -0,0 +1,78 @@ +package com.api.trip.domain.article.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.MemberRole; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.List; +import java.util.Optional; + +import static com.api.trip.domain.article.model.QArticle.article; +import static com.api.trip.domain.member.model.QMember.member; + +public class ArticleRepositoryCustomImpl implements ArticleRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + public ArticleRepositoryCustomImpl(EntityManager em) { + this.jpaQueryFactory = new JPAQueryFactory(em); + } + + @Override + public Optional
findArticle(Long id) { + return Optional.ofNullable( + jpaQueryFactory + .selectFrom(article) + .innerJoin(article.writer, member).fetchJoin() + .where(article.id.eq(id)) + .fetchOne() + ); + } + + @Override + public Page
findArticles(Pageable pageable, String filter) { + List
content = jpaQueryFactory + .selectFrom(article) + .innerJoin(article.writer, member).fetchJoin() + .where(eqFilter(filter)) + .orderBy(getOrderSpecifier(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = jpaQueryFactory + .select(article.count()) + .from(article) + .innerJoin(article.writer, member) + .where(eqFilter(filter)) + .fetchOne(); + + return new PageImpl<>(content, pageable, total); + } + + private BooleanExpression eqFilter(String filter) { + for (MemberRole role : MemberRole.values()) { + if (role.name().equals(filter)) { + return member.role.eq(role); + } + } + return null; + } + + private OrderSpecifier getOrderSpecifier(Pageable pageable) { + for (Sort.Order order : pageable.getSort()) { + if ("POPULAR".equals(order.getProperty())) { + return new OrderSpecifier<>(Order.DESC, article.viewCount); + } + } + return new OrderSpecifier<>(Order.DESC, article.id); + } +} diff --git a/src/main/java/com/api/trip/domain/article/service/ArticleService.java b/src/main/java/com/api/trip/domain/article/service/ArticleService.java new file mode 100644 index 0000000..00018f4 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/service/ArticleService.java @@ -0,0 +1,84 @@ +package com.api.trip.domain.article.service; + +import com.api.trip.domain.article.controller.dto.CreateArticleRequest; +import com.api.trip.domain.article.controller.dto.ReadArticleResponse; +import com.api.trip.domain.article.controller.dto.UpdateArticleRequest; +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.article.repository.ArticleRepository; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.repository.MemberRepository; +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; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class ArticleService { + + private final ArticleRepository articleRepository; + private final MemberRepository memberRepository; + + public Long createArticle(CreateArticleRequest request, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Article article = Article.builder() + .writer(member) + .title(request.getTitle()) + .content(request.getContent()) + .build(); + + return articleRepository.save(article).getId(); + } + + public void updateArticle(Long articleId, UpdateArticleRequest request, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Article article = articleRepository.findById(articleId).orElseThrow(); + if (article.getWriter() != member) { + throw new RuntimeException("수정 권한이 없습니다."); + } + + article.modify(request.getTitle(), request.getContent()); + } + + public void deleteArticle(Long articleId, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Article article = articleRepository.findById(articleId).orElseThrow(); + if (article.getWriter() != member) { + throw new RuntimeException("삭제 권한이 없습니다."); + } + + articleRepository.delete(article); + } + + public ReadArticleResponse readArticle(Long articleId) { + Article article = articleRepository.findArticle(articleId).orElseThrow(); + + article.increaseViewCount(); + + return ReadArticleResponse.fromEntity(article); + } + + @Transactional(readOnly = true) + public Page getArticles(Pageable pageable, String filter) { + return articleRepository.findArticles(pageable, filter) + .map(ReadArticleResponse::fromEntity); + } + + @Transactional(readOnly = true) + public List getMyArticles(String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + List
articles = articleRepository.findAllByWriterOrderByIdDesc(member); + + return articles.stream() + .map(ReadArticleResponse::fromEntity) + .toList(); + } +} From 803b2fb6971e640b831564cc8fbe45b125e9a2a7 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Fri, 8 Dec 2023 11:50:07 +0900 Subject: [PATCH 07/45] =?UTF-8?q?Feat:=20Jpa=20Auditing=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 데이터의 생성일, 수정일을 자동으로 관리하기 위해 auditing 클래스 추가함. --- .../auditing/config/AuditingConfig.java | 21 +++++++++++++++ .../auditing/entity/BaseTimeEntity.java | 26 +++++++++++++++++++ .../trip/domain/email/model/EmailAuth.java | 3 ++- .../api/trip/domain/member/model/Member.java | 3 ++- 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/api/trip/common/auditing/config/AuditingConfig.java create mode 100644 src/main/java/com/api/trip/common/auditing/entity/BaseTimeEntity.java diff --git a/src/main/java/com/api/trip/common/auditing/config/AuditingConfig.java b/src/main/java/com/api/trip/common/auditing/config/AuditingConfig.java new file mode 100644 index 0000000..c077f44 --- /dev/null +++ b/src/main/java/com/api/trip/common/auditing/config/AuditingConfig.java @@ -0,0 +1,21 @@ +package com.api.trip.common.auditing.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import java.util.Optional; + +@Configuration +@EnableJpaAuditing +public class AuditingConfig { + + // TODO: 회원가입 후 유저 정보를 가져와서 넣어주는 방법을 모르겠습니다... + /** + @Bean + public AuditorAware auditorAware() { + return () -> Optional.of("user1"); + } + */ +} diff --git a/src/main/java/com/api/trip/common/auditing/entity/BaseTimeEntity.java b/src/main/java/com/api/trip/common/auditing/entity/BaseTimeEntity.java new file mode 100644 index 0000000..7439cd4 --- /dev/null +++ b/src/main/java/com/api/trip/common/auditing/entity/BaseTimeEntity.java @@ -0,0 +1,26 @@ +package com.api.trip.common.auditing.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseTimeEntity { + + @CreatedDate + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime modifiedAt; + +} diff --git a/src/main/java/com/api/trip/domain/email/model/EmailAuth.java b/src/main/java/com/api/trip/domain/email/model/EmailAuth.java index 51ba76a..ee617b1 100644 --- a/src/main/java/com/api/trip/domain/email/model/EmailAuth.java +++ b/src/main/java/com/api/trip/domain/email/model/EmailAuth.java @@ -1,5 +1,6 @@ package com.api.trip.domain.email.model; +import com.api.trip.common.auditing.entity.BaseTimeEntity; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -14,7 +15,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class EmailAuth { +public class EmailAuth extends BaseTimeEntity { private static final Long MAX_EXPIRE_TIME = 5L; // 링크 유효 기간 5분 diff --git a/src/main/java/com/api/trip/domain/member/model/Member.java b/src/main/java/com/api/trip/domain/member/model/Member.java index 43238c7..aad8261 100644 --- a/src/main/java/com/api/trip/domain/member/model/Member.java +++ b/src/main/java/com/api/trip/domain/member/model/Member.java @@ -1,5 +1,6 @@ package com.api.trip.domain.member.model; +import com.api.trip.common.auditing.entity.BaseTimeEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -9,7 +10,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Member { +public class Member extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From 5e7956f4f4ec8f6fd7be15441fe0957d4e77b49b Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Fri, 8 Dec 2023 14:58:22 +0900 Subject: [PATCH 08/45] =?UTF-8?q?Feat:=20ArticleController=20@PathVariable?= =?UTF-8?q?=20=EC=86=8D=EC=84=B1=20=EA=B0=92=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trip/domain/article/controller/ArticleController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java index a147fd4..ae3c3e7 100644 --- a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java +++ b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java @@ -36,21 +36,21 @@ public ResponseEntity createArticle(@RequestBody CreateArticleRequest requ } @PatchMapping("/{articleId}") - public ResponseEntity updateArticle(@PathVariable("articleId") Long articleId, @RequestBody UpdateArticleRequest request) { + public ResponseEntity updateArticle(@PathVariable Long articleId, @RequestBody UpdateArticleRequest request) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); articleService.updateArticle(articleId, request, email); return ResponseEntity.ok().build(); } @DeleteMapping("/{articleId}") - public ResponseEntity deleteArticle(@PathVariable("articleId") Long articleId) { + public ResponseEntity deleteArticle(@PathVariable Long articleId) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); articleService.deleteArticle(articleId, email); return ResponseEntity.ok().build(); } @GetMapping("/{articleId}") - public ResponseEntity readArticle(@PathVariable("articleId") Long articleId) { + public ResponseEntity readArticle(@PathVariable Long articleId) { return ResponseEntity.ok(articleService.readArticle(articleId)); } From bb21742c9fe2ba0af441169b5f83cb1ad0856235 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Fri, 8 Dec 2023 16:14:01 +0900 Subject: [PATCH 09/45] =?UTF-8?q?Feat:=20SecurityUtils=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 SecurityContextHolder에서 현재 로그인한 회원의 이메일을 가져오는 메서드 구현 --- .../trip/common/security/util/SecurityUtils.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/main/java/com/api/trip/common/security/util/SecurityUtils.java diff --git a/src/main/java/com/api/trip/common/security/util/SecurityUtils.java b/src/main/java/com/api/trip/common/security/util/SecurityUtils.java new file mode 100644 index 0000000..256cdca --- /dev/null +++ b/src/main/java/com/api/trip/common/security/util/SecurityUtils.java @@ -0,0 +1,16 @@ +package com.api.trip.common.security.util; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public class SecurityUtils { + public static String getCurrentUsername() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || authentication.getName() == null) { + throw new RuntimeException("인증 정보가 없습니다!"); + } + return authentication.getName(); + } + +} From 421a1762ccdb732432b51ff82c833062a96a1bec Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Fri, 8 Dec 2023 16:37:00 +0900 Subject: [PATCH 10/45] =?UTF-8?q?Feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B0=BE=EA=B8=B0=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 - 임시 비밀번호를 메일로 전송하고 해당 회원의 비밀번호를 임시 비밀번호로 갱신함. - 메일로 전송된 임시 비밀번호로 로그인 - 메일 본문에 추후에 본인이 원하는 비밀번호로 변경하도록 안내 --- .../domain/email/service/EmailService.java | 53 ++++++++++++++++++- .../member/controller/MemberController.java | 15 ++++-- .../controller/dto/FindPasswordRequest.java | 9 ++++ .../api/trip/domain/member/model/Member.java | 4 ++ .../domain/member/service/MemberService.java | 12 +++++ 5 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/api/trip/domain/member/controller/dto/FindPasswordRequest.java diff --git a/src/main/java/com/api/trip/domain/email/service/EmailService.java b/src/main/java/com/api/trip/domain/email/service/EmailService.java index 2d82561..d51d229 100644 --- a/src/main/java/com/api/trip/domain/email/service/EmailService.java +++ b/src/main/java/com/api/trip/domain/email/service/EmailService.java @@ -3,6 +3,8 @@ import com.api.trip.domain.email.model.EmailAuth; import com.api.trip.domain.email.repository.EmailAuthRepository; import com.api.trip.domain.member.controller.dto.EmailResponse; +import com.api.trip.domain.member.controller.dto.FindPasswordRequest; +import com.api.trip.domain.member.service.MemberService; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; @@ -12,6 +14,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.security.SecureRandom; import java.time.LocalDateTime; import java.util.UUID; @@ -21,16 +24,19 @@ @RequiredArgsConstructor public class EmailService { + private final MemberService memberService; private final JavaMailSender javaMailSender; private final EmailAuthRepository emailAuthRepository; @Async public void send(String email, String authToken) { - MimeMessage message = javaMailSender.createMimeMessage(); + // TODO: 개발용, 서버용 링크 분리 예정 String text = "http://localhost:8080/api/members/auth-email/%s/%s" .formatted(email, authToken); + MimeMessage message = javaMailSender.createMimeMessage(); + try { - message.setSubject("[Trip Trip] 회원가입 인증 메일"); + message.setSubject("[TRIP TRIP] 회원가입 인증 링크 발급"); message.setRecipients(MimeMessage.RecipientType.TO, email); message.setText(text, "UTF-8", "HTML"); } catch (MessagingException e) { @@ -40,6 +46,31 @@ public void send(String email, String authToken) { javaMailSender.send(message); } + @Async + public void sendNewPassword(String email) { + if (email == null || email.isEmpty()) { + throw new RuntimeException("이메일 정보가 없습니다!"); + } + + // 가입 회원 여부 검사 + memberService.getMemberByEmail(email); + + String newPassword = getRandomPassword(); + String text = "회원님의 임시 비밀번호는 %s 입니다. 로그인 후에 비밀번호를 변경해주세요.".formatted(newPassword); + + MimeMessage message = javaMailSender.createMimeMessage(); + try { + message.setSubject("[TRIP TRIP] 임시 비밀번호 발급"); + message.setRecipients(MimeMessage.RecipientType.TO, email); + message.setText(text); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + + javaMailSender.send(message); + memberService.changePassword(email, newPassword); // 새 비밀번호로 업데이트 + } + // 인증 메일 검증 public EmailResponse authEmail(String email, String authToken) { EmailAuth emailAuth = emailAuthRepository.findValidAuthByEmail(email, authToken, LocalDateTime.now()) @@ -59,4 +90,22 @@ public String createEmailAuth(String email) { return emailAuthRepository.save(emailAuth).getAuthToken(); } + // TODO: 랜덤 비밀번호 생성 -> 임시로 8자리 랜덤 문자열 반환 + private String getRandomPassword() { + char[] charSet = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' + }; + + SecureRandom random = new SecureRandom(); + StringBuilder sb = new StringBuilder(); + + int rndAllCharactersLength = charSet.length; + for (int i = 0; i < 8; i++) { + sb.append(charSet[random.nextInt(rndAllCharactersLength)]); + } + + return sb.toString(); + } + } diff --git a/src/main/java/com/api/trip/domain/member/controller/MemberController.java b/src/main/java/com/api/trip/domain/member/controller/MemberController.java index 16518e2..010658f 100644 --- a/src/main/java/com/api/trip/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/trip/domain/member/controller/MemberController.java @@ -1,13 +1,12 @@ package com.api.trip.domain.member.controller; +import com.api.trip.common.security.util.SecurityUtils; import com.api.trip.domain.email.service.EmailService; -import com.api.trip.domain.member.controller.dto.EmailResponse; -import com.api.trip.domain.member.controller.dto.JoinRequest; -import com.api.trip.domain.member.controller.dto.LoginRequest; -import com.api.trip.domain.member.controller.dto.LoginResponse; +import com.api.trip.domain.member.controller.dto.*; import com.api.trip.domain.member.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @@ -31,6 +30,7 @@ public ResponseEntity loginMember(@RequestBody LoginRequest login } // 인증 메일 전송 + // TODO: 리팩토링 예정.. @PostMapping("/send-email/{email}") public void sendAuthEmail(@PathVariable String email) { emailService.send(email, emailService.createEmailAuth(email)); @@ -40,4 +40,11 @@ public void sendAuthEmail(@PathVariable String email) { public ResponseEntity emailAndAuthToken(@PathVariable String email, @PathVariable String authToken) { return ResponseEntity.ok().body(emailService.authEmail(email, authToken)); } + + @PreAuthorize("isAnonymous()") + @PostMapping("/find/password") + public ResponseEntity sendNewPassword(@RequestBody FindPasswordRequest findPasswordRequest) { + emailService.sendNewPassword(findPasswordRequest.getEmail()); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/FindPasswordRequest.java b/src/main/java/com/api/trip/domain/member/controller/dto/FindPasswordRequest.java new file mode 100644 index 0000000..0bd424f --- /dev/null +++ b/src/main/java/com/api/trip/domain/member/controller/dto/FindPasswordRequest.java @@ -0,0 +1,9 @@ +package com.api.trip.domain.member.controller.dto; + +import lombok.Getter; + +@Getter +public class FindPasswordRequest { + + private String email; +} diff --git a/src/main/java/com/api/trip/domain/member/model/Member.java b/src/main/java/com/api/trip/domain/member/model/Member.java index aad8261..a0018dc 100644 --- a/src/main/java/com/api/trip/domain/member/model/Member.java +++ b/src/main/java/com/api/trip/domain/member/model/Member.java @@ -43,4 +43,8 @@ private Member(String email, String nickname, String password){ public void emailVerifiedSuccess() { this.emailAuth = true; } + + public void changePassword(String password) { + this.password = password; + } } diff --git a/src/main/java/com/api/trip/domain/member/service/MemberService.java b/src/main/java/com/api/trip/domain/member/service/MemberService.java index ff65985..f574adf 100644 --- a/src/main/java/com/api/trip/domain/member/service/MemberService.java +++ b/src/main/java/com/api/trip/domain/member/service/MemberService.java @@ -18,6 +18,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.security.SecureRandom; import java.util.stream.Collectors; @Service @@ -57,4 +58,15 @@ public LoginResponse login(LoginRequest loginRequest) { JwtToken jwtToken = jwtTokenProvider.createJwtToken(loginRequest.getEmail(), authorities); return LoginResponse.of(jwtToken); } + + // 회원의 비밀번호를 메일로 전송한 임시 비밀번호로 변경 + public void changePassword(String email, String password) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + member.changePassword(passwordEncoder.encode(password)); + } + + @Transactional(readOnly = true) + public Member getMemberByEmail(String email) { + return memberRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("가입된 회원이 아닙니다!")); + } } From e38d0a72541e281e8c362e480846bf2af8bfa7fb Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Sat, 9 Dec 2023 17:20:00 +0900 Subject: [PATCH 11/45] =?UTF-8?q?Feat:=20=EB=8C=93=EA=B8=80=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Entity 추가 - Repository 추가 --- .../trip/domain/comment/model/Comment.java | 46 +++++++++++++++++++ .../comment/repository/CommentRepository.java | 7 +++ 2 files changed, 53 insertions(+) create mode 100644 src/main/java/com/api/trip/domain/comment/model/Comment.java create mode 100644 src/main/java/com/api/trip/domain/comment/repository/CommentRepository.java diff --git a/src/main/java/com/api/trip/domain/comment/model/Comment.java b/src/main/java/com/api/trip/domain/comment/model/Comment.java new file mode 100644 index 0000000..dacda14 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/model/Comment.java @@ -0,0 +1,46 @@ +package com.api.trip.domain.comment.model; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +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.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Comment extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Member writer; + + @ManyToOne(fetch = FetchType.LAZY) + private Article article; + + @Column(nullable = false) + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + private Comment parent; + + @Builder + public Comment(Member writer, Article article, String content, Comment parent) { + this.writer = writer; + this.article = article; + this.content = content; + this.parent = parent; + } +} diff --git a/src/main/java/com/api/trip/domain/comment/repository/CommentRepository.java b/src/main/java/com/api/trip/domain/comment/repository/CommentRepository.java new file mode 100644 index 0000000..56b1e5a --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package com.api.trip.domain.comment.repository; + +import com.api.trip.domain.comment.model.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} From 5d52d94f174ce954faa898609df9c75734445cbe Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Sat, 9 Dec 2023 17:23:12 +0900 Subject: [PATCH 12/45] =?UTF-8?q?Feat:=20=EB=8C=93=EA=B8=80=20=EA=B8=B0?= =?UTF-8?q?=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 - crud 기능 구현 --- .../comment/controller/CommentController.java | 58 ++++++++++ .../controller/dto/CommentResponse.java | 37 ++++++ .../controller/dto/CreateCommentRequest.java | 11 ++ .../controller/dto/UpdateCommentRequest.java | 9 ++ .../trip/domain/comment/model/Comment.java | 4 + .../comment/repository/CommentRepository.java | 7 +- .../repository/CommentRepositoryCustom.java | 11 ++ .../CommentRepositoryCustomImpl.java | 29 +++++ .../comment/service/CommentService.java | 108 ++++++++++++++++++ 9 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/api/trip/domain/comment/controller/CommentController.java create mode 100644 src/main/java/com/api/trip/domain/comment/controller/dto/CommentResponse.java create mode 100644 src/main/java/com/api/trip/domain/comment/controller/dto/CreateCommentRequest.java create mode 100644 src/main/java/com/api/trip/domain/comment/controller/dto/UpdateCommentRequest.java create mode 100644 src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustom.java create mode 100644 src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustomImpl.java create mode 100644 src/main/java/com/api/trip/domain/comment/service/CommentService.java diff --git a/src/main/java/com/api/trip/domain/comment/controller/CommentController.java b/src/main/java/com/api/trip/domain/comment/controller/CommentController.java new file mode 100644 index 0000000..04bd19b --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/controller/CommentController.java @@ -0,0 +1,58 @@ +package com.api.trip.domain.comment.controller; + +import com.api.trip.domain.comment.controller.dto.CommentResponse; +import com.api.trip.domain.comment.controller.dto.CreateCommentRequest; +import com.api.trip.domain.comment.controller.dto.UpdateCommentRequest; +import com.api.trip.domain.comment.service.CommentService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +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; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/comments") +@RequiredArgsConstructor +public class CommentController { + + private final CommentService commentService; + + @PostMapping + public ResponseEntity createComment(@RequestBody CreateCommentRequest request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(commentService.createComment(request, email)); + } + + @PatchMapping("/{commentId}") + public ResponseEntity updateComment(@PathVariable Long commentId, @RequestBody UpdateCommentRequest request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + commentService.updateComment(commentId, request, email); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{commentId}") + public ResponseEntity deleteComment(@PathVariable Long commentId) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + commentService.deleteComment(commentId, email); + return ResponseEntity.ok().build(); + } + + @GetMapping + public ResponseEntity> getComments(Long articleId) { + return ResponseEntity.ok(commentService.getComments(articleId)); + } + + @GetMapping("/me") + public ResponseEntity> getMyComments() { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(commentService.getMyComments(email)); + } +} diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/CommentResponse.java b/src/main/java/com/api/trip/domain/comment/controller/dto/CommentResponse.java new file mode 100644 index 0000000..28b5d8f --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/CommentResponse.java @@ -0,0 +1,37 @@ +package com.api.trip.domain.comment.controller.dto; + +import com.api.trip.domain.comment.model.Comment; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +public class CommentResponse { + + private Long commentId; + private Long writerId; + private String writerNickname; + private Long articleId; + private String content; + private Long parentId; + private LocalDateTime createdAt; + + @Setter + private List comments; + + public static CommentResponse fromEntity(Comment comment) { + return builder() + .commentId(comment.getId()) + .writerId(comment.getWriter().getId()) + .writerNickname(comment.getWriter().getNickname()) + .articleId(comment.getArticle().getId()) + .content(comment.getContent()) + .parentId(comment.getParent() != null ? comment.getParent().getId() : null) + .createdAt(comment.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/CreateCommentRequest.java b/src/main/java/com/api/trip/domain/comment/controller/dto/CreateCommentRequest.java new file mode 100644 index 0000000..c1ecaa7 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/CreateCommentRequest.java @@ -0,0 +1,11 @@ +package com.api.trip.domain.comment.controller.dto; + +import lombok.Getter; + +@Getter +public class CreateCommentRequest { + + private Long articleId; + private Long parentId; + private String content; +} diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/UpdateCommentRequest.java b/src/main/java/com/api/trip/domain/comment/controller/dto/UpdateCommentRequest.java new file mode 100644 index 0000000..879ca39 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/UpdateCommentRequest.java @@ -0,0 +1,9 @@ +package com.api.trip.domain.comment.controller.dto; + +import lombok.Getter; + +@Getter +public class UpdateCommentRequest { + + private String content; +} diff --git a/src/main/java/com/api/trip/domain/comment/model/Comment.java b/src/main/java/com/api/trip/domain/comment/model/Comment.java index dacda14..e991067 100644 --- a/src/main/java/com/api/trip/domain/comment/model/Comment.java +++ b/src/main/java/com/api/trip/domain/comment/model/Comment.java @@ -43,4 +43,8 @@ public Comment(Member writer, Article article, String content, Comment parent) { this.content = content; this.parent = parent; } + + public void modify(String content) { + this.content = content; + } } diff --git a/src/main/java/com/api/trip/domain/comment/repository/CommentRepository.java b/src/main/java/com/api/trip/domain/comment/repository/CommentRepository.java index 56b1e5a..6a9eaaf 100644 --- a/src/main/java/com/api/trip/domain/comment/repository/CommentRepository.java +++ b/src/main/java/com/api/trip/domain/comment/repository/CommentRepository.java @@ -1,7 +1,12 @@ package com.api.trip.domain.comment.repository; import com.api.trip.domain.comment.model.Comment; +import com.api.trip.domain.member.model.Member; import org.springframework.data.jpa.repository.JpaRepository; -public interface CommentRepository extends JpaRepository { +import java.util.List; + +public interface CommentRepository extends JpaRepository, CommentRepositoryCustom { + + List findAllByWriterOrderByIdDesc(Member writer); } diff --git a/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustom.java b/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustom.java new file mode 100644 index 0000000..da423e7 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.api.trip.domain.comment.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.comment.model.Comment; + +import java.util.List; + +public interface CommentRepositoryCustom { + + List findComments(Article article); +} diff --git a/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustomImpl.java new file mode 100644 index 0000000..e261c9f --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustomImpl.java @@ -0,0 +1,29 @@ +package com.api.trip.domain.comment.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.comment.model.Comment; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; + +import java.util.List; + +import static com.api.trip.domain.comment.model.QComment.comment; +import static com.api.trip.domain.member.model.QMember.member; + +public class CommentRepositoryCustomImpl implements CommentRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + public CommentRepositoryCustomImpl(EntityManager em) { + this.jpaQueryFactory = new JPAQueryFactory(em); + } + + @Override + public List findComments(Article article) { + return jpaQueryFactory + .selectFrom(comment) + .innerJoin(comment.writer, member).fetchJoin() + .where(comment.article.eq(article)) + .fetch(); + } +} diff --git a/src/main/java/com/api/trip/domain/comment/service/CommentService.java b/src/main/java/com/api/trip/domain/comment/service/CommentService.java new file mode 100644 index 0000000..29978d3 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/service/CommentService.java @@ -0,0 +1,108 @@ +package com.api.trip.domain.comment.service; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.article.repository.ArticleRepository; +import com.api.trip.domain.comment.controller.dto.CommentResponse; +import com.api.trip.domain.comment.controller.dto.CreateCommentRequest; +import com.api.trip.domain.comment.controller.dto.UpdateCommentRequest; +import com.api.trip.domain.comment.model.Comment; +import com.api.trip.domain.comment.repository.CommentRepository; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@Transactional +@RequiredArgsConstructor +public class CommentService { + + private final CommentRepository commentRepository; + private final ArticleRepository articleRepository; + private final MemberRepository memberRepository; + + public Long createComment(CreateCommentRequest request, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Article article = articleRepository.findById(request.getArticleId()).orElseThrow(); + + Comment parent = null; + if (request.getParentId() != null) { + parent = commentRepository.findById(request.getParentId()).orElseThrow(); + if (parent.getParent() != null || parent.getArticle() != article) { + throw new RuntimeException("잘못된 요청입니다."); + } + } + + Comment comment = Comment.builder() + .writer(member) + .article(article) + .content(request.getContent()) + .parent(parent) + .build(); + + return commentRepository.save(comment).getId(); + } + + public void updateComment(Long commentId, UpdateCommentRequest request, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Comment comment = commentRepository.findById(commentId).orElseThrow(); + if (comment.getWriter() != member) { + throw new RuntimeException("수정 권한이 없습니다."); + } + + comment.modify(request.getContent()); + } + + public void deleteComment(Long commentId, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Comment comment = commentRepository.findById(commentId).orElseThrow(); + if (comment.getWriter() != member) { + throw new RuntimeException("삭제 권한이 없습니다."); + } + + commentRepository.delete(comment); + } + + @Transactional(readOnly = true) + public List getComments(Long articleId) { + Article article = articleRepository.findById(articleId).orElseThrow(); + + List comments = commentRepository.findComments(article); + + List commentResponses = comments.stream() + .map(CommentResponse::fromEntity) + .toList(); + + Map> groupByParentId = commentResponses.stream() + .collect(Collectors.groupingBy(commentResponse -> commentResponse.getParentId() != null ? commentResponse.getParentId() : 0)); + + return commentResponses.stream() + .filter(commentResponse -> { + if (commentResponse.getParentId() == null) { + commentResponse.setComments(groupByParentId.get(commentResponse.getCommentId())); + return true; + } + return false; + }) + .toList(); + } + + @Transactional(readOnly = true) + public List getMyComments(String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + List comments = commentRepository.findAllByWriterOrderByIdDesc(member); + + return comments.stream() + .map(CommentResponse::fromEntity) + .toList(); + } +} From 3bfadc879e08249530dac6e5c481a65dd6f65847 Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Mon, 11 Dec 2023 15:34:41 +0900 Subject: [PATCH 13/45] =?UTF-8?q?Feat:=20Article=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20Auditing=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/api/trip/domain/article/model/Article.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/api/trip/domain/article/model/Article.java b/src/main/java/com/api/trip/domain/article/model/Article.java index 47de710..6738d8b 100644 --- a/src/main/java/com/api/trip/domain/article/model/Article.java +++ b/src/main/java/com/api/trip/domain/article/model/Article.java @@ -1,5 +1,6 @@ package com.api.trip.domain.article.model; +import com.api.trip.common.auditing.entity.BaseTimeEntity; import com.api.trip.domain.member.model.Member; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -16,7 +17,7 @@ @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -public class Article { +public class Article extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From 7679895c63191b8d221739d23f2c38e1df4b3b92 Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Mon, 11 Dec 2023 15:37:10 +0900 Subject: [PATCH 14/45] =?UTF-8?q?Feat:=20ArticleRepositoryCustom=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=95=A0?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/article/repository/ArticleRepositoryCustom.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustom.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustom.java index 723b199..fbdbe4f 100644 --- a/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustom.java +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustom.java @@ -3,13 +3,12 @@ import com.api.trip.domain.article.model.Article; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.repository.query.Param; import java.util.Optional; public interface ArticleRepositoryCustom { - Optional
findArticle(@Param("articleId") Long articleId); + Optional
findArticle(Long articleId); Page
findArticles(Pageable pageable, String filter); } From 28694e4f6c4f0137bdbce584e0baafb0a4e20a51 Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Mon, 11 Dec 2023 15:45:39 +0900 Subject: [PATCH 15/45] =?UTF-8?q?Feat:=20application.yml=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 - 요청을 Pageable로 받을 때, 시작 페이지 번호를 1로 사용할 수 있도록 변경 --- src/main/resources/application.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bcb0593..b30c3fc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,6 +9,10 @@ spring: enabled: true mail: host: smtp.gmail.com + data: + web: + pageable: + one-indexed-parameters: true logging: level: From 895223dd11ee030e013457fabce43b8b2b6449ed Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Mon, 11 Dec 2023 15:47:57 +0900 Subject: [PATCH 16/45] =?UTF-8?q?Feat:=20ReadArticleResponse=20=EC=98=A4?= =?UTF-8?q?=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 --- .../article/controller/dto/ReadArticleResponse.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java index 84d6d22..46ae5d9 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java @@ -5,6 +5,8 @@ import lombok.Builder; import lombok.Getter; +import java.time.LocalDateTime; + @Getter @Builder public class ReadArticleResponse { @@ -12,10 +14,11 @@ public class ReadArticleResponse { private Long articleId; private String title; private Long writerId; - private String writerNickName; + private String writerNickname; private String writerRole; private String content; private long viewCount; + private LocalDateTime createdAt; public static ReadArticleResponse fromEntity(Article article) { Member writer = article.getWriter(); @@ -23,10 +26,11 @@ public static ReadArticleResponse fromEntity(Article article) { .articleId(article.getId()) .title(article.getTitle()) .writerId(writer.getId()) - .writerNickName(writer.getNickname()) + .writerNickname(writer.getNickname()) .writerRole(writer.getRole().name()) .content(article.getContent()) .viewCount(article.getViewCount()) + .createdAt(article.getCreatedAt()) .build(); } } From e401faa8d75219f8e7367e0d9c520a02fea352bf Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Mon, 11 Dec 2023 15:50:59 +0900 Subject: [PATCH 17/45] =?UTF-8?q?Feat:=20GetArticlesResponse,=20GetMyArtic?= =?UTF-8?q?lesResponse=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dto/GetArticlesResponse.java | 62 +++++++++++++++++++ .../controller/dto/GetMyArticlesResponse.java | 49 +++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java create mode 100644 src/main/java/com/api/trip/domain/article/controller/dto/GetMyArticlesResponse.java diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java new file mode 100644 index 0000000..9a528d3 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java @@ -0,0 +1,62 @@ +package com.api.trip.domain.article.controller.dto; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +public class GetArticlesResponse { + + private int totalPages; + private long totalElements; + private int page; + private boolean hasNext; + private boolean hasPrevious; + private int requestSize; + private int resultSize; + private List result; + + public static GetArticlesResponse fromEntityPage(Page
articlePage) { + Page articleDtoPage = articlePage.map(ArticleDto::fromEntity); + return builder() + .totalPages(articleDtoPage.getTotalPages()) + .totalElements(articleDtoPage.getTotalElements()) + .page(articleDtoPage.getNumber() + 1) + .hasNext(articleDtoPage.hasNext()) + .hasPrevious(articleDtoPage.hasPrevious()) + .requestSize(articleDtoPage.getSize()) + .resultSize(articleDtoPage.getNumberOfElements()) + .result(articleDtoPage.getContent()) + .build(); + } + + @Getter + @Builder + private static class ArticleDto { + + private Long articleId; + private String title; + private Long writerId; + private String writerNickname; + private String writerRole; + private LocalDateTime createdAt; + + private static ArticleDto fromEntity(Article article) { + Member writer = article.getWriter(); + return builder() + .articleId(article.getId()) + .title(article.getTitle()) + .writerId(writer.getId()) + .writerNickname(writer.getNickname()) + .writerRole(writer.getRole().name()) + .createdAt(article.getCreatedAt()) + .build(); + } + } +} diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/GetMyArticlesResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/GetMyArticlesResponse.java new file mode 100644 index 0000000..4901fc8 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/dto/GetMyArticlesResponse.java @@ -0,0 +1,49 @@ +package com.api.trip.domain.article.controller.dto; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@AllArgsConstructor +public class GetMyArticlesResponse { + + private List articles; + + public static GetMyArticlesResponse fromEntities(List
articles) { + List articleDtos = articles.stream() + .map(ArticleDto::fromEntity) + .toList(); + + return new GetMyArticlesResponse(articleDtos); + } + + @Getter + @Builder + private static class ArticleDto { + + private Long articleId; + private String title; + private Long writerId; + private String writerNickname; + private String writerRole; + private LocalDateTime createdAt; + + private static ArticleDto fromEntity(Article article) { + Member writer = article.getWriter(); + return builder() + .articleId(article.getId()) + .title(article.getTitle()) + .writerId(writer.getId()) + .writerNickname(writer.getNickname()) + .writerRole(writer.getRole().name()) + .createdAt(article.getCreatedAt()) + .build(); + } + } +} From 6c80c67c7f55c6e8b6177c31a557694081ef15cc Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Mon, 11 Dec 2023 15:59:38 +0900 Subject: [PATCH 18/45] =?UTF-8?q?Feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C,=20=EB=82=B4=EA=B0=80?= =?UTF-8?q?=20=EC=93=B4=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B0=98=ED=99=98=20=EA=B0=92?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ArticleController.getArticles (ResponseEntity>에서 ResponseEntity) - ArticleController.getMyArticles (ResponseEntity>에서 ResponseEntity) - ArticleService.getArticles (Page>에서 GetArticlesResponse) - ArticleService.getMyArticles (List>에서 GetMyArticlesResponse) --- .../article/controller/ArticleController.java | 11 +++++------ .../domain/article/service/ArticleService.java | 15 ++++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java index ae3c3e7..14780b9 100644 --- a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java +++ b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java @@ -1,11 +1,12 @@ package com.api.trip.domain.article.controller; import com.api.trip.domain.article.controller.dto.CreateArticleRequest; +import com.api.trip.domain.article.controller.dto.GetArticlesResponse; +import com.api.trip.domain.article.controller.dto.GetMyArticlesResponse; import com.api.trip.domain.article.controller.dto.ReadArticleResponse; import com.api.trip.domain.article.controller.dto.UpdateArticleRequest; import com.api.trip.domain.article.service.ArticleService; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; @@ -20,8 +21,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - @RestController @RequestMapping("/api/articles") @RequiredArgsConstructor @@ -55,15 +54,15 @@ public ResponseEntity readArticle(@PathVariable Long articl } @GetMapping - public ResponseEntity> getArticles( - @PageableDefault(size = 8, page = 0) Pageable pageable, + public ResponseEntity getArticles( + @PageableDefault(size = 8) Pageable pageable, @RequestParam(value = "filter", required = false) String filter ) { return ResponseEntity.ok(articleService.getArticles(pageable, filter)); } @GetMapping("/me") - public ResponseEntity> getMyArticles() { + public ResponseEntity getMyArticles() { String email = SecurityContextHolder.getContext().getAuthentication().getName(); return ResponseEntity.ok(articleService.getMyArticles(email)); } diff --git a/src/main/java/com/api/trip/domain/article/service/ArticleService.java b/src/main/java/com/api/trip/domain/article/service/ArticleService.java index 00018f4..ed74e20 100644 --- a/src/main/java/com/api/trip/domain/article/service/ArticleService.java +++ b/src/main/java/com/api/trip/domain/article/service/ArticleService.java @@ -1,6 +1,8 @@ package com.api.trip.domain.article.service; import com.api.trip.domain.article.controller.dto.CreateArticleRequest; +import com.api.trip.domain.article.controller.dto.GetArticlesResponse; +import com.api.trip.domain.article.controller.dto.GetMyArticlesResponse; import com.api.trip.domain.article.controller.dto.ReadArticleResponse; import com.api.trip.domain.article.controller.dto.UpdateArticleRequest; import com.api.trip.domain.article.model.Article; @@ -66,19 +68,18 @@ public ReadArticleResponse readArticle(Long articleId) { } @Transactional(readOnly = true) - public Page getArticles(Pageable pageable, String filter) { - return articleRepository.findArticles(pageable, filter) - .map(ReadArticleResponse::fromEntity); + public GetArticlesResponse getArticles(Pageable pageable, String filter) { + Page
articlePage = articleRepository.findArticles(pageable, filter); + + return GetArticlesResponse.fromEntityPage(articlePage); } @Transactional(readOnly = true) - public List getMyArticles(String email) { + public GetMyArticlesResponse getMyArticles(String email) { Member member = memberRepository.findByEmail(email).orElseThrow(); List
articles = articleRepository.findAllByWriterOrderByIdDesc(member); - return articles.stream() - .map(ReadArticleResponse::fromEntity) - .toList(); + return GetMyArticlesResponse.fromEntities(articles); } } From 11436873eb6847614f464dd01405f0bfb86c3014 Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Mon, 11 Dec 2023 16:03:19 +0900 Subject: [PATCH 19/45] =?UTF-8?q?Feat:=20Comment=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=9D=98=20=EC=83=9D=EC=84=B1=EC=9E=90=EB=A5=BC=20pri?= =?UTF-8?q?vate=EC=9C=BC=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 --- src/main/java/com/api/trip/domain/comment/model/Comment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/api/trip/domain/comment/model/Comment.java b/src/main/java/com/api/trip/domain/comment/model/Comment.java index e991067..8408ed3 100644 --- a/src/main/java/com/api/trip/domain/comment/model/Comment.java +++ b/src/main/java/com/api/trip/domain/comment/model/Comment.java @@ -37,7 +37,7 @@ public class Comment extends BaseTimeEntity { private Comment parent; @Builder - public Comment(Member writer, Article article, String content, Comment parent) { + private Comment(Member writer, Article article, String content, Comment parent) { this.writer = writer; this.article = article; this.content = content; From a437884abfa6b085d85f2eaf7028af3eb0cf55f9 Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Mon, 11 Dec 2023 16:32:30 +0900 Subject: [PATCH 20/45] =?UTF-8?q?Feat:=20GetCommentsResponse,=20GetMyComme?= =?UTF-8?q?ntsResponse=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dto/GetCommentsResponse.java | 68 +++++++++++++++++++ .../controller/dto/GetMyCommentsResponse.java | 49 +++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java create mode 100644 src/main/java/com/api/trip/domain/comment/controller/dto/GetMyCommentsResponse.java diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java b/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java new file mode 100644 index 0000000..a532262 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java @@ -0,0 +1,68 @@ +package com.api.trip.domain.comment.controller.dto; + +import com.api.trip.domain.comment.model.Comment; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Getter +@AllArgsConstructor +public class GetCommentsResponse { + + private List comments; + + public static GetCommentsResponse fromEntities(List comments) { + List commentDtos = comments.stream() + .map(CommentDto::fromEntity) + .toList(); + + Map> groupByParentId = commentDtos.stream() + .collect(Collectors.groupingBy(commentDto -> commentDto.getParentId() != null ? commentDto.getParentId() : 0)); + + commentDtos = commentDtos.stream() + .filter(commentDto -> { + if (commentDto.getParentId() == null) { + commentDto.setChildren(groupByParentId.get(commentDto.getCommentId())); + return true; + } + return false; + }) + .toList(); + + return new GetCommentsResponse(commentDtos); + } + + @Getter + @Builder + private static class CommentDto { + + private Long commentId; + private Long writerId; + private String writerNickname; + private Long articleId; + private String content; + private Long parentId; + private LocalDateTime createdAt; + + @Setter + private List children; + + private static CommentDto fromEntity(Comment comment) { + return builder() + .commentId(comment.getId()) + .writerId(comment.getWriter().getId()) + .writerNickname(comment.getWriter().getNickname()) + .articleId(comment.getArticle().getId()) + .content(comment.getContent()) + .parentId(comment.getParent() != null ? comment.getParent().getId() : null) + .createdAt(comment.getCreatedAt()) + .build(); + } + } +} diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/GetMyCommentsResponse.java b/src/main/java/com/api/trip/domain/comment/controller/dto/GetMyCommentsResponse.java new file mode 100644 index 0000000..37cfbf3 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/GetMyCommentsResponse.java @@ -0,0 +1,49 @@ +package com.api.trip.domain.comment.controller.dto; + +import com.api.trip.domain.comment.model.Comment; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@AllArgsConstructor +public class GetMyCommentsResponse { + + private List comments; + + public static GetMyCommentsResponse fromEntities(List comments) { + List commentDtos = comments.stream() + .map(CommentDto::fromEntity) + .toList(); + + return new GetMyCommentsResponse(commentDtos); + } + + @Getter + @Builder + private static class CommentDto { + + private Long commentId; + private Long writerId; + private String writerNickname; + private Long articleId; + private String content; + private Long parentId; + private LocalDateTime createdAt; + + private static CommentDto fromEntity(Comment comment) { + return builder() + .commentId(comment.getId()) + .writerId(comment.getWriter().getId()) + .writerNickname(comment.getWriter().getNickname()) + .articleId(comment.getArticle().getId()) + .content(comment.getContent()) + .parentId(comment.getParent() != null ? comment.getParent().getId() : null) + .createdAt(comment.getCreatedAt()) + .build(); + } + } +} From 5f8c3a64f5c5d5bc65022b91fe7b622000ce1782 Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Mon, 11 Dec 2023 16:36:51 +0900 Subject: [PATCH 21/45] =?UTF-8?q?Feat:=20=EB=8C=93=EA=B8=80=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C,=20=EB=82=B4=EA=B0=80=20?= =?UTF-8?q?=EC=93=B4=20=EB=8C=93=EA=B8=80=20=EA=B4=80=EB=A0=A8=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EB=B0=98=ED=99=98=20=EA=B0=92=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 - CommentResponse 삭제 - CommentController.getComments (ResponseEntity>에서 ResponseEntity) - CommentController.getMyComments (ResponseEntity>에서 ResponseEntity) - CommentService.getComments (List에서 GetCommentsResponse) - CommentService.getMyComments (List에서 GetMyCommentsResponse) --- .../comment/controller/CommentController.java | 9 ++--- .../controller/dto/CommentResponse.java | 37 ------------------- .../comment/service/CommentService.java | 30 +++------------ 3 files changed, 10 insertions(+), 66 deletions(-) delete mode 100644 src/main/java/com/api/trip/domain/comment/controller/dto/CommentResponse.java diff --git a/src/main/java/com/api/trip/domain/comment/controller/CommentController.java b/src/main/java/com/api/trip/domain/comment/controller/CommentController.java index 04bd19b..3591398 100644 --- a/src/main/java/com/api/trip/domain/comment/controller/CommentController.java +++ b/src/main/java/com/api/trip/domain/comment/controller/CommentController.java @@ -1,7 +1,8 @@ package com.api.trip.domain.comment.controller; -import com.api.trip.domain.comment.controller.dto.CommentResponse; import com.api.trip.domain.comment.controller.dto.CreateCommentRequest; +import com.api.trip.domain.comment.controller.dto.GetCommentsResponse; +import com.api.trip.domain.comment.controller.dto.GetMyCommentsResponse; import com.api.trip.domain.comment.controller.dto.UpdateCommentRequest; import com.api.trip.domain.comment.service.CommentService; import lombok.RequiredArgsConstructor; @@ -16,8 +17,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - @RestController @RequestMapping("/api/comments") @RequiredArgsConstructor @@ -46,12 +45,12 @@ public ResponseEntity deleteComment(@PathVariable Long commentId) { } @GetMapping - public ResponseEntity> getComments(Long articleId) { + public ResponseEntity getComments(Long articleId) { return ResponseEntity.ok(commentService.getComments(articleId)); } @GetMapping("/me") - public ResponseEntity> getMyComments() { + public ResponseEntity getMyComments() { String email = SecurityContextHolder.getContext().getAuthentication().getName(); return ResponseEntity.ok(commentService.getMyComments(email)); } diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/CommentResponse.java b/src/main/java/com/api/trip/domain/comment/controller/dto/CommentResponse.java deleted file mode 100644 index 28b5d8f..0000000 --- a/src/main/java/com/api/trip/domain/comment/controller/dto/CommentResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.api.trip.domain.comment.controller.dto; - -import com.api.trip.domain.comment.model.Comment; -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; - -import java.time.LocalDateTime; -import java.util.List; - -@Getter -@Builder -public class CommentResponse { - - private Long commentId; - private Long writerId; - private String writerNickname; - private Long articleId; - private String content; - private Long parentId; - private LocalDateTime createdAt; - - @Setter - private List comments; - - public static CommentResponse fromEntity(Comment comment) { - return builder() - .commentId(comment.getId()) - .writerId(comment.getWriter().getId()) - .writerNickname(comment.getWriter().getNickname()) - .articleId(comment.getArticle().getId()) - .content(comment.getContent()) - .parentId(comment.getParent() != null ? comment.getParent().getId() : null) - .createdAt(comment.getCreatedAt()) - .build(); - } -} diff --git a/src/main/java/com/api/trip/domain/comment/service/CommentService.java b/src/main/java/com/api/trip/domain/comment/service/CommentService.java index 29978d3..c7387db 100644 --- a/src/main/java/com/api/trip/domain/comment/service/CommentService.java +++ b/src/main/java/com/api/trip/domain/comment/service/CommentService.java @@ -2,8 +2,9 @@ import com.api.trip.domain.article.model.Article; import com.api.trip.domain.article.repository.ArticleRepository; -import com.api.trip.domain.comment.controller.dto.CommentResponse; import com.api.trip.domain.comment.controller.dto.CreateCommentRequest; +import com.api.trip.domain.comment.controller.dto.GetCommentsResponse; +import com.api.trip.domain.comment.controller.dto.GetMyCommentsResponse; import com.api.trip.domain.comment.controller.dto.UpdateCommentRequest; import com.api.trip.domain.comment.model.Comment; import com.api.trip.domain.comment.repository.CommentRepository; @@ -14,8 +15,6 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; @Service @Transactional @@ -72,37 +71,20 @@ public void deleteComment(Long commentId, String email) { } @Transactional(readOnly = true) - public List getComments(Long articleId) { + public GetCommentsResponse getComments(Long articleId) { Article article = articleRepository.findById(articleId).orElseThrow(); List comments = commentRepository.findComments(article); - List commentResponses = comments.stream() - .map(CommentResponse::fromEntity) - .toList(); - - Map> groupByParentId = commentResponses.stream() - .collect(Collectors.groupingBy(commentResponse -> commentResponse.getParentId() != null ? commentResponse.getParentId() : 0)); - - return commentResponses.stream() - .filter(commentResponse -> { - if (commentResponse.getParentId() == null) { - commentResponse.setComments(groupByParentId.get(commentResponse.getCommentId())); - return true; - } - return false; - }) - .toList(); + return GetCommentsResponse.fromEntities(comments); } @Transactional(readOnly = true) - public List getMyComments(String email) { + public GetMyCommentsResponse getMyComments(String email) { Member member = memberRepository.findByEmail(email).orElseThrow(); List comments = commentRepository.findAllByWriterOrderByIdDesc(member); - return comments.stream() - .map(CommentResponse::fromEntity) - .toList(); + return GetMyCommentsResponse.fromEntities(comments); } } From 2db0a1c5b117422cd0d97eda2be94958fb11812f Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Mon, 11 Dec 2023 17:50:41 +0900 Subject: [PATCH 22/45] =?UTF-8?q?Feat:=20=EC=97=94=ED=8B=B0=ED=8B=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20DTO=EB=A1=9C=20=EB=B3=80=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=A0=95=EC=A0=81=20=ED=8C=A9=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9D=B4=EB=A6=84=EC=9D=84=20of?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/article/controller/dto/GetArticlesResponse.java | 6 +++--- .../article/controller/dto/GetMyArticlesResponse.java | 6 +++--- .../domain/article/controller/dto/ReadArticleResponse.java | 2 +- .../com/api/trip/domain/article/service/ArticleService.java | 6 +++--- .../domain/comment/controller/dto/GetCommentsResponse.java | 6 +++--- .../comment/controller/dto/GetMyCommentsResponse.java | 6 +++--- .../com/api/trip/domain/comment/service/CommentService.java | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java index 9a528d3..f1262af 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java @@ -22,8 +22,8 @@ public class GetArticlesResponse { private int resultSize; private List result; - public static GetArticlesResponse fromEntityPage(Page
articlePage) { - Page articleDtoPage = articlePage.map(ArticleDto::fromEntity); + public static GetArticlesResponse of(Page
articlePage) { + Page articleDtoPage = articlePage.map(ArticleDto::of); return builder() .totalPages(articleDtoPage.getTotalPages()) .totalElements(articleDtoPage.getTotalElements()) @@ -47,7 +47,7 @@ private static class ArticleDto { private String writerRole; private LocalDateTime createdAt; - private static ArticleDto fromEntity(Article article) { + private static ArticleDto of(Article article) { Member writer = article.getWriter(); return builder() .articleId(article.getId()) diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/GetMyArticlesResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/GetMyArticlesResponse.java index 4901fc8..b47627a 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/GetMyArticlesResponse.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/GetMyArticlesResponse.java @@ -15,9 +15,9 @@ public class GetMyArticlesResponse { private List articles; - public static GetMyArticlesResponse fromEntities(List
articles) { + public static GetMyArticlesResponse of(List
articles) { List articleDtos = articles.stream() - .map(ArticleDto::fromEntity) + .map(ArticleDto::of) .toList(); return new GetMyArticlesResponse(articleDtos); @@ -34,7 +34,7 @@ private static class ArticleDto { private String writerRole; private LocalDateTime createdAt; - private static ArticleDto fromEntity(Article article) { + private static ArticleDto of(Article article) { Member writer = article.getWriter(); return builder() .articleId(article.getId()) diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java index 46ae5d9..343183a 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java @@ -20,7 +20,7 @@ public class ReadArticleResponse { private long viewCount; private LocalDateTime createdAt; - public static ReadArticleResponse fromEntity(Article article) { + public static ReadArticleResponse of(Article article) { Member writer = article.getWriter(); return builder() .articleId(article.getId()) diff --git a/src/main/java/com/api/trip/domain/article/service/ArticleService.java b/src/main/java/com/api/trip/domain/article/service/ArticleService.java index ed74e20..a8ec7af 100644 --- a/src/main/java/com/api/trip/domain/article/service/ArticleService.java +++ b/src/main/java/com/api/trip/domain/article/service/ArticleService.java @@ -64,14 +64,14 @@ public ReadArticleResponse readArticle(Long articleId) { article.increaseViewCount(); - return ReadArticleResponse.fromEntity(article); + return ReadArticleResponse.of(article); } @Transactional(readOnly = true) public GetArticlesResponse getArticles(Pageable pageable, String filter) { Page
articlePage = articleRepository.findArticles(pageable, filter); - return GetArticlesResponse.fromEntityPage(articlePage); + return GetArticlesResponse.of(articlePage); } @Transactional(readOnly = true) @@ -80,6 +80,6 @@ public GetMyArticlesResponse getMyArticles(String email) { List
articles = articleRepository.findAllByWriterOrderByIdDesc(member); - return GetMyArticlesResponse.fromEntities(articles); + return GetMyArticlesResponse.of(articles); } } diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java b/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java index a532262..95f86d4 100644 --- a/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java @@ -17,9 +17,9 @@ public class GetCommentsResponse { private List comments; - public static GetCommentsResponse fromEntities(List comments) { + public static GetCommentsResponse of(List comments) { List commentDtos = comments.stream() - .map(CommentDto::fromEntity) + .map(CommentDto::of) .toList(); Map> groupByParentId = commentDtos.stream() @@ -53,7 +53,7 @@ private static class CommentDto { @Setter private List children; - private static CommentDto fromEntity(Comment comment) { + private static CommentDto of(Comment comment) { return builder() .commentId(comment.getId()) .writerId(comment.getWriter().getId()) diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/GetMyCommentsResponse.java b/src/main/java/com/api/trip/domain/comment/controller/dto/GetMyCommentsResponse.java index 37cfbf3..7469b6b 100644 --- a/src/main/java/com/api/trip/domain/comment/controller/dto/GetMyCommentsResponse.java +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/GetMyCommentsResponse.java @@ -14,9 +14,9 @@ public class GetMyCommentsResponse { private List comments; - public static GetMyCommentsResponse fromEntities(List comments) { + public static GetMyCommentsResponse of(List comments) { List commentDtos = comments.stream() - .map(CommentDto::fromEntity) + .map(CommentDto::of) .toList(); return new GetMyCommentsResponse(commentDtos); @@ -34,7 +34,7 @@ private static class CommentDto { private Long parentId; private LocalDateTime createdAt; - private static CommentDto fromEntity(Comment comment) { + private static CommentDto of(Comment comment) { return builder() .commentId(comment.getId()) .writerId(comment.getWriter().getId()) diff --git a/src/main/java/com/api/trip/domain/comment/service/CommentService.java b/src/main/java/com/api/trip/domain/comment/service/CommentService.java index c7387db..fe143d4 100644 --- a/src/main/java/com/api/trip/domain/comment/service/CommentService.java +++ b/src/main/java/com/api/trip/domain/comment/service/CommentService.java @@ -76,7 +76,7 @@ public GetCommentsResponse getComments(Long articleId) { List comments = commentRepository.findComments(article); - return GetCommentsResponse.fromEntities(comments); + return GetCommentsResponse.of(comments); } @Transactional(readOnly = true) @@ -85,6 +85,6 @@ public GetMyCommentsResponse getMyComments(String email) { List comments = commentRepository.findAllByWriterOrderByIdDesc(member); - return GetMyCommentsResponse.fromEntities(comments); + return GetMyCommentsResponse.of(comments); } } From 691a7c73a72bb303b4b1580a085e3aaf239c79af Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Mon, 11 Dec 2023 18:16:38 +0900 Subject: [PATCH 23/45] =?UTF-8?q?Feat:=20DTO=EC=97=90=EC=84=9C=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=EB=A1=9C=20=EB=B3=80=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=EC=9D=84=20Service=EC=97=90?= =?UTF-8?q?=EC=84=9C=20DTO=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/dto/CreateArticleRequest.java | 10 ++++++++++ .../trip/domain/article/service/ArticleService.java | 6 +----- .../comment/controller/dto/CreateCommentRequest.java | 12 ++++++++++++ .../trip/domain/comment/service/CommentService.java | 7 +------ 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/CreateArticleRequest.java b/src/main/java/com/api/trip/domain/article/controller/dto/CreateArticleRequest.java index 7a16836..da050a4 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/CreateArticleRequest.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/CreateArticleRequest.java @@ -1,5 +1,7 @@ package com.api.trip.domain.article.controller.dto; +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; import lombok.Getter; @Getter @@ -7,4 +9,12 @@ public class CreateArticleRequest { private String title; private String content; + + public Article toEntity(Member writer) { + return Article.builder() + .writer(writer) + .title(title) + .content(content) + .build(); + } } diff --git a/src/main/java/com/api/trip/domain/article/service/ArticleService.java b/src/main/java/com/api/trip/domain/article/service/ArticleService.java index a8ec7af..5380b7f 100644 --- a/src/main/java/com/api/trip/domain/article/service/ArticleService.java +++ b/src/main/java/com/api/trip/domain/article/service/ArticleService.java @@ -28,11 +28,7 @@ public class ArticleService { public Long createArticle(CreateArticleRequest request, String email) { Member member = memberRepository.findByEmail(email).orElseThrow(); - Article article = Article.builder() - .writer(member) - .title(request.getTitle()) - .content(request.getContent()) - .build(); + Article article = request.toEntity(member); return articleRepository.save(article).getId(); } diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/CreateCommentRequest.java b/src/main/java/com/api/trip/domain/comment/controller/dto/CreateCommentRequest.java index c1ecaa7..55408d3 100644 --- a/src/main/java/com/api/trip/domain/comment/controller/dto/CreateCommentRequest.java +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/CreateCommentRequest.java @@ -1,5 +1,8 @@ package com.api.trip.domain.comment.controller.dto; +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.comment.model.Comment; +import com.api.trip.domain.member.model.Member; import lombok.Getter; @Getter @@ -8,4 +11,13 @@ public class CreateCommentRequest { private Long articleId; private Long parentId; private String content; + + public Comment toEntity(Member writer, Article article, Comment parent) { + return Comment.builder() + .writer(writer) + .article(article) + .content(content) + .parent(parent) + .build(); + } } diff --git a/src/main/java/com/api/trip/domain/comment/service/CommentService.java b/src/main/java/com/api/trip/domain/comment/service/CommentService.java index fe143d4..239ae54 100644 --- a/src/main/java/com/api/trip/domain/comment/service/CommentService.java +++ b/src/main/java/com/api/trip/domain/comment/service/CommentService.java @@ -38,12 +38,7 @@ public Long createComment(CreateCommentRequest request, String email) { } } - Comment comment = Comment.builder() - .writer(member) - .article(article) - .content(request.getContent()) - .parent(parent) - .build(); + Comment comment = request.toEntity(member, article, parent); return commentRepository.save(comment).getId(); } From 173c97779ab47bd46f56c594e53d1037184a36aa Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Tue, 12 Dec 2023 12:39:12 +0900 Subject: [PATCH 24/45] =?UTF-8?q?Feat:=20soft=20delete=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit autiting 필드에 삭제일을 추가하고 - `@SQLDelete`를 사용해서 delete 쿼리가 실행될 때 삭제일을 현재 날짜로 update 하도록 설정 - @SQLRestriction를 사용해서 회원 조회시에 삭제일이 null인 회원들만 조회하도록 설정 - 삭제일 값이 null이 아닌 데이터들은 삭제된 데이터 --- .../com/api/trip/common/auditing/entity/BaseTimeEntity.java | 2 ++ src/main/java/com/api/trip/domain/member/model/Member.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/com/api/trip/common/auditing/entity/BaseTimeEntity.java b/src/main/java/com/api/trip/common/auditing/entity/BaseTimeEntity.java index 7439cd4..5f4016b 100644 --- a/src/main/java/com/api/trip/common/auditing/entity/BaseTimeEntity.java +++ b/src/main/java/com/api/trip/common/auditing/entity/BaseTimeEntity.java @@ -23,4 +23,6 @@ public class BaseTimeEntity { @Column(nullable = false) private LocalDateTime modifiedAt; + private LocalDateTime deletedAt; + } diff --git a/src/main/java/com/api/trip/domain/member/model/Member.java b/src/main/java/com/api/trip/domain/member/model/Member.java index a0018dc..b8b9034 100644 --- a/src/main/java/com/api/trip/domain/member/model/Member.java +++ b/src/main/java/com/api/trip/domain/member/model/Member.java @@ -6,10 +6,14 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE member SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") public class Member extends BaseTimeEntity { @Id From 9a2bf5bc14e9b7d219fef4364487b40352eb41f3 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Tue, 12 Dec 2023 12:40:53 +0900 Subject: [PATCH 25/45] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=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 - 탈퇴를 원하는 사용자의 현재 비밀번호를 입력받아서 탈퇴를 진행 - 회원이 아니거나, 비밀번호가 일치하지 않는 경우에는 탈퇴 불가능 --- .../domain/member/controller/MemberController.java | 10 ++++++++++ .../member/controller/dto/DeleteRequest.java | 14 ++++++++++++++ .../trip/domain/member/service/MemberService.java | 11 +++++++++++ 3 files changed, 35 insertions(+) create mode 100644 src/main/java/com/api/trip/domain/member/controller/dto/DeleteRequest.java diff --git a/src/main/java/com/api/trip/domain/member/controller/MemberController.java b/src/main/java/com/api/trip/domain/member/controller/MemberController.java index 010658f..d6a56d5 100644 --- a/src/main/java/com/api/trip/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/trip/domain/member/controller/MemberController.java @@ -47,4 +47,14 @@ public ResponseEntity sendNewPassword(@RequestBody FindPasswordRequest fin emailService.sendNewPassword(findPasswordRequest.getEmail()); return ResponseEntity.ok().build(); } + + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/me") + public ResponseEntity deleteMember(@RequestBody DeleteRequest deleteRequest) { + String email = SecurityUtils.getCurrentUsername(); + memberService.deleteMember(email, deleteRequest.getPassword()); + + return ResponseEntity.ok().build(); + } + } diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/DeleteRequest.java b/src/main/java/com/api/trip/domain/member/controller/dto/DeleteRequest.java new file mode 100644 index 0000000..db23e25 --- /dev/null +++ b/src/main/java/com/api/trip/domain/member/controller/dto/DeleteRequest.java @@ -0,0 +1,14 @@ +package com.api.trip.domain.member.controller.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class DeleteRequest { + + private String password; + +} diff --git a/src/main/java/com/api/trip/domain/member/service/MemberService.java b/src/main/java/com/api/trip/domain/member/service/MemberService.java index f574adf..ed05725 100644 --- a/src/main/java/com/api/trip/domain/member/service/MemberService.java +++ b/src/main/java/com/api/trip/domain/member/service/MemberService.java @@ -19,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import java.security.SecureRandom; +import java.util.List; import java.util.stream.Collectors; @Service @@ -59,6 +60,16 @@ public LoginResponse login(LoginRequest loginRequest) { return LoginResponse.of(jwtToken); } + public void deleteMember(String email, String password) { + Member member = getMemberByEmail(email); + + if (!passwordEncoder.matches(password, member.getPassword())) { + throw new RuntimeException("비밀번호가 일치하지 않습니다."); + } + + memberRepository.deleteById(member.getId()); + } + // 회원의 비밀번호를 메일로 전송한 임시 비밀번호로 변경 public void changePassword(String email, String password) { Member member = memberRepository.findByEmail(email).orElseThrow(); From d7f496ff66fcdfc3236e0ff9e68883228875d094 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Tue, 12 Dec 2023 12:41:29 +0900 Subject: [PATCH 26/45] =?UTF-8?q?Test:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컨트롤러, 서비스 간단하게 테스트 해봄. --- .../controller/MemberControllerTest.java | 77 +++++++++++++++++++ .../member/service/MemberServiceTest.java | 58 ++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/test/java/com/api/trip/domain/member/controller/MemberControllerTest.java create mode 100644 src/test/java/com/api/trip/domain/member/service/MemberServiceTest.java diff --git a/src/test/java/com/api/trip/domain/member/controller/MemberControllerTest.java b/src/test/java/com/api/trip/domain/member/controller/MemberControllerTest.java new file mode 100644 index 0000000..4037f02 --- /dev/null +++ b/src/test/java/com/api/trip/domain/member/controller/MemberControllerTest.java @@ -0,0 +1,77 @@ +package com.api.trip.domain.member.controller; + +import com.api.trip.domain.member.controller.dto.DeleteRequest; +import com.api.trip.domain.member.service.MemberService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@SpringBootTest +@AutoConfigureMockMvc +class MemberControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private MemberService memberService; + + @DisplayName("[API] - 회원 삭제 성공") + @WithMockUser + @Test + void successDeleteMember() throws Exception { + + String email = "test@email.com"; + String password = "1234"; + + doNothing().when(memberService).deleteMember(email, password); + + mvc.perform(delete("/api/members/me") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsBytes(new DeleteRequest(password))) + ) + .andExpect(status().isOk()) + .andDo(print()); + } + + /** + * 에러 처리가 안되있어서 응답 실패시에도 200 응답이 떨어지기 때문에 실패 테스트는 현재 진행 불가능 + @DisplayName("[API] - 회원 삭제 실패 - 현재 비밀번호를 잘못 입력한 경우") + @WithMockUser + @Test + void deleteDeleteMember() throws Exception { + + String email = "test@email.com"; + String password = "1234"; + String wrongPassword = "1111"; + + doThrow(new RuntimeException("비밀번호가 일치하지 않습니다.")).when(memberService).deleteMember(email, wrongPassword); + + mvc.perform(delete("/api/members/me") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsBytes(new DeleteRequest(wrongPassword))) + ) + .andExpect(status().isConflict()) + .andDo(print()); + } + */ + + +} \ No newline at end of file diff --git a/src/test/java/com/api/trip/domain/member/service/MemberServiceTest.java b/src/test/java/com/api/trip/domain/member/service/MemberServiceTest.java new file mode 100644 index 0000000..637152f --- /dev/null +++ b/src/test/java/com/api/trip/domain/member/service/MemberServiceTest.java @@ -0,0 +1,58 @@ +package com.api.trip.domain.member.service; + +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.repository.MemberRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.*; + +@DisplayName("비즈니스 로직 - 회원") +@ExtendWith(MockitoExtension.class) +class MemberServiceTest { + + @InjectMocks + private MemberService memberService; + + @Mock + private MemberRepository memberRepository; + + @Spy + private PasswordEncoder passwordEncoder; + + @DisplayName("현재 비밀번호를 입력하면 회원 탈퇴가 완료된다.") + @Test + void successDeleteMember() { + + // Given + String email = "test@email.com"; + String password = "1234"; + + Member member = Member.builder() + .password(passwordEncoder.encode(password)) + .build(); + + when(memberRepository.findByEmail(anyString())).thenReturn(Optional.of(mock(Member.class))); + when(passwordEncoder.matches(password, member.getPassword())).thenReturn(true); + doNothing().when(memberRepository).deleteById(anyLong()); + + // When + memberService.deleteMember(email, password); + + // Then + then(memberRepository).should().findByEmail(anyString()); + then(passwordEncoder).should().matches(password, member.getPassword()); + then(memberRepository).should().deleteById(anyLong()); + } + +} \ No newline at end of file From c4ddea72f2437b45671366d6c4eca0e9e778e50a Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Wed, 13 Dec 2023 11:32:25 +0900 Subject: [PATCH 27/45] =?UTF-8?q?Remove:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 불필요해서 삭제함. --- .../controller/MemberControllerTest.java | 77 ------------------- .../member/service/MemberServiceTest.java | 58 -------------- 2 files changed, 135 deletions(-) delete mode 100644 src/test/java/com/api/trip/domain/member/controller/MemberControllerTest.java delete mode 100644 src/test/java/com/api/trip/domain/member/service/MemberServiceTest.java diff --git a/src/test/java/com/api/trip/domain/member/controller/MemberControllerTest.java b/src/test/java/com/api/trip/domain/member/controller/MemberControllerTest.java deleted file mode 100644 index 4037f02..0000000 --- a/src/test/java/com/api/trip/domain/member/controller/MemberControllerTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.api.trip.domain.member.controller; - -import com.api.trip.domain.member.controller.dto.DeleteRequest; -import com.api.trip.domain.member.service.MemberService; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MockMvc; - -import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - - -@SpringBootTest -@AutoConfigureMockMvc -class MemberControllerTest { - - @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper objectMapper; - - @MockBean - private MemberService memberService; - - @DisplayName("[API] - 회원 삭제 성공") - @WithMockUser - @Test - void successDeleteMember() throws Exception { - - String email = "test@email.com"; - String password = "1234"; - - doNothing().when(memberService).deleteMember(email, password); - - mvc.perform(delete("/api/members/me") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes(new DeleteRequest(password))) - ) - .andExpect(status().isOk()) - .andDo(print()); - } - - /** - * 에러 처리가 안되있어서 응답 실패시에도 200 응답이 떨어지기 때문에 실패 테스트는 현재 진행 불가능 - @DisplayName("[API] - 회원 삭제 실패 - 현재 비밀번호를 잘못 입력한 경우") - @WithMockUser - @Test - void deleteDeleteMember() throws Exception { - - String email = "test@email.com"; - String password = "1234"; - String wrongPassword = "1111"; - - doThrow(new RuntimeException("비밀번호가 일치하지 않습니다.")).when(memberService).deleteMember(email, wrongPassword); - - mvc.perform(delete("/api/members/me") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes(new DeleteRequest(wrongPassword))) - ) - .andExpect(status().isConflict()) - .andDo(print()); - } - */ - - -} \ No newline at end of file diff --git a/src/test/java/com/api/trip/domain/member/service/MemberServiceTest.java b/src/test/java/com/api/trip/domain/member/service/MemberServiceTest.java deleted file mode 100644 index 637152f..0000000 --- a/src/test/java/com/api/trip/domain/member/service/MemberServiceTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.api.trip.domain.member.service; - -import com.api.trip.domain.member.model.Member; -import com.api.trip.domain.member.repository.MemberRepository; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.*; - -@DisplayName("비즈니스 로직 - 회원") -@ExtendWith(MockitoExtension.class) -class MemberServiceTest { - - @InjectMocks - private MemberService memberService; - - @Mock - private MemberRepository memberRepository; - - @Spy - private PasswordEncoder passwordEncoder; - - @DisplayName("현재 비밀번호를 입력하면 회원 탈퇴가 완료된다.") - @Test - void successDeleteMember() { - - // Given - String email = "test@email.com"; - String password = "1234"; - - Member member = Member.builder() - .password(passwordEncoder.encode(password)) - .build(); - - when(memberRepository.findByEmail(anyString())).thenReturn(Optional.of(mock(Member.class))); - when(passwordEncoder.matches(password, member.getPassword())).thenReturn(true); - doNothing().when(memberRepository).deleteById(anyLong()); - - // When - memberService.deleteMember(email, password); - - // Then - then(memberRepository).should().findByEmail(anyString()); - then(passwordEncoder).should().matches(password, member.getPassword()); - then(memberRepository).should().deleteById(anyLong()); - } - -} \ No newline at end of file From 9b8b42dfb398d9d81a13bc7428a6b5bb8c5c1dc5 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Wed, 13 Dec 2023 11:35:26 +0900 Subject: [PATCH 28/45] =?UTF-8?q?Refactor:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서비스 메서드 파라미터에 request 객체를 넣는 방식으로 변경함. --- .../domain/member/controller/MemberController.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/api/trip/domain/member/controller/MemberController.java b/src/main/java/com/api/trip/domain/member/controller/MemberController.java index d6a56d5..f3ea3ef 100644 --- a/src/main/java/com/api/trip/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/trip/domain/member/controller/MemberController.java @@ -1,6 +1,5 @@ package com.api.trip.domain.member.controller; -import com.api.trip.common.security.util.SecurityUtils; import com.api.trip.domain.email.service.EmailService; import com.api.trip.domain.member.controller.dto.*; import com.api.trip.domain.member.service.MemberService; @@ -18,13 +17,13 @@ public class MemberController { private final EmailService emailService; @PostMapping("/join") - public ResponseEntity joinMember(@RequestBody JoinRequest joinRequest) { + public ResponseEntity join(@RequestBody JoinRequest joinRequest) { memberService.join(joinRequest); return ResponseEntity.ok().build(); } @PostMapping("/login") - public ResponseEntity loginMember(@RequestBody LoginRequest loginRequest) { + public ResponseEntity login(@RequestBody LoginRequest loginRequest) { LoginResponse loginResponse = memberService.login(loginRequest); return ResponseEntity.ok().body(loginResponse); } @@ -44,16 +43,14 @@ public ResponseEntity emailAndAuthToken(@PathVariable String emai @PreAuthorize("isAnonymous()") @PostMapping("/find/password") public ResponseEntity sendNewPassword(@RequestBody FindPasswordRequest findPasswordRequest) { - emailService.sendNewPassword(findPasswordRequest.getEmail()); + emailService.sendNewPassword(findPasswordRequest); return ResponseEntity.ok().build(); } @PreAuthorize("isAuthenticated()") @DeleteMapping("/me") public ResponseEntity deleteMember(@RequestBody DeleteRequest deleteRequest) { - String email = SecurityUtils.getCurrentUsername(); - memberService.deleteMember(email, deleteRequest.getPassword()); - + memberService.deleteMember(deleteRequest); return ResponseEntity.ok().build(); } From eee793e41d60d8f4579c6855a90322985269e5c3 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Wed, 13 Dec 2023 11:36:58 +0900 Subject: [PATCH 29/45] =?UTF-8?q?Refactor:=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Request 객체를 서비스 메서드 안에서 풀어서 쓰는 방식으로 수정 - 현재 로그인한 회원의 이메일을 사용해서 회원 정보를 가져오는 `getAuthenticationMember` 메서드 추가 --- .../api/trip/domain/email/service/EmailService.java | 7 +++++-- .../trip/domain/member/service/MemberService.java | 13 ++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/api/trip/domain/email/service/EmailService.java b/src/main/java/com/api/trip/domain/email/service/EmailService.java index d51d229..b40dbf6 100644 --- a/src/main/java/com/api/trip/domain/email/service/EmailService.java +++ b/src/main/java/com/api/trip/domain/email/service/EmailService.java @@ -4,6 +4,7 @@ import com.api.trip.domain.email.repository.EmailAuthRepository; import com.api.trip.domain.member.controller.dto.EmailResponse; import com.api.trip.domain.member.controller.dto.FindPasswordRequest; +import com.api.trip.domain.member.model.Member; import com.api.trip.domain.member.service.MemberService; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; @@ -47,13 +48,15 @@ public void send(String email, String authToken) { } @Async - public void sendNewPassword(String email) { + public void sendNewPassword(FindPasswordRequest findPasswordRequest) { + String email = findPasswordRequest.getEmail(); + if (email == null || email.isEmpty()) { throw new RuntimeException("이메일 정보가 없습니다!"); } // 가입 회원 여부 검사 - memberService.getMemberByEmail(email); + Member member = memberService.getMemberByEmail(email); String newPassword = getRandomPassword(); String text = "회원님의 임시 비밀번호는 %s 입니다. 로그인 후에 비밀번호를 변경해주세요.".formatted(newPassword); diff --git a/src/main/java/com/api/trip/domain/member/service/MemberService.java b/src/main/java/com/api/trip/domain/member/service/MemberService.java index ed05725..c40f13f 100644 --- a/src/main/java/com/api/trip/domain/member/service/MemberService.java +++ b/src/main/java/com/api/trip/domain/member/service/MemberService.java @@ -2,8 +2,10 @@ import com.api.trip.common.security.JwtToken; import com.api.trip.common.security.JwtTokenProvider; +import com.api.trip.common.security.util.SecurityUtils; import com.api.trip.domain.email.model.EmailAuth; import com.api.trip.domain.email.repository.EmailAuthRepository; +import com.api.trip.domain.member.controller.dto.DeleteRequest; import com.api.trip.domain.member.controller.dto.JoinRequest; import com.api.trip.domain.member.controller.dto.LoginRequest; import com.api.trip.domain.member.controller.dto.LoginResponse; @@ -60,10 +62,10 @@ public LoginResponse login(LoginRequest loginRequest) { return LoginResponse.of(jwtToken); } - public void deleteMember(String email, String password) { - Member member = getMemberByEmail(email); + public void deleteMember(DeleteRequest deleteRequest) { + Member member = getAuthenticationMember(); - if (!passwordEncoder.matches(password, member.getPassword())) { + if (!passwordEncoder.matches(deleteRequest.getPassword(), member.getPassword())) { throw new RuntimeException("비밀번호가 일치하지 않습니다."); } @@ -80,4 +82,9 @@ public void changePassword(String email, String password) { public Member getMemberByEmail(String email) { return memberRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("가입된 회원이 아닙니다!")); } + + @Transactional(readOnly = true) + public Member getAuthenticationMember() { + return memberRepository.findByEmail(SecurityUtils.getCurrentUsername()).orElseThrow(() -> new UsernameNotFoundException("가입된 회원이 아닙니다!")); + } } From b33715e0671ff032fa1a4a1159a882f8da742f21 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Wed, 13 Dec 2023 11:37:52 +0900 Subject: [PATCH 30/45] Remove: Optimize Imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 불필요한 import 정리 --- .../common/auditing/config/AuditingConfig.java | 4 ---- .../api/trip/common/security/JwtTokenFilter.java | 1 - .../trip/common/security/JwtTokenProvider.java | 8 ++++---- .../api/trip/common/security/SecurityConfig.java | 1 - .../trip/common/security/util/SecurityUtils.java | 1 - .../article/controller/ArticleController.java | 16 ++-------------- .../api/trip/domain/article/model/Article.java | 8 +------- .../domain/article/service/ArticleService.java | 6 +----- .../comment/controller/CommentController.java | 9 +-------- .../api/trip/domain/comment/model/Comment.java | 8 +------- .../member/controller/dto/JoinRequest.java | 1 - .../domain/member/service/MemberService.java | 2 -- .../java/com/api/trip/TripApplicationTests.java | 1 - 13 files changed, 10 insertions(+), 56 deletions(-) diff --git a/src/main/java/com/api/trip/common/auditing/config/AuditingConfig.java b/src/main/java/com/api/trip/common/auditing/config/AuditingConfig.java index c077f44..bdd7310 100644 --- a/src/main/java/com/api/trip/common/auditing/config/AuditingConfig.java +++ b/src/main/java/com/api/trip/common/auditing/config/AuditingConfig.java @@ -1,12 +1,8 @@ package com.api.trip.common.auditing.config; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import java.util.Optional; - @Configuration @EnableJpaAuditing public class AuditingConfig { diff --git a/src/main/java/com/api/trip/common/security/JwtTokenFilter.java b/src/main/java/com/api/trip/common/security/JwtTokenFilter.java index 8e87b6b..c86047e 100644 --- a/src/main/java/com/api/trip/common/security/JwtTokenFilter.java +++ b/src/main/java/com/api/trip/common/security/JwtTokenFilter.java @@ -6,7 +6,6 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; diff --git a/src/main/java/com/api/trip/common/security/JwtTokenProvider.java b/src/main/java/com/api/trip/common/security/JwtTokenProvider.java index d6e829b..998ac4f 100644 --- a/src/main/java/com/api/trip/common/security/JwtTokenProvider.java +++ b/src/main/java/com/api/trip/common/security/JwtTokenProvider.java @@ -1,10 +1,10 @@ package com.api.trip.common.security; -import com.api.trip.common.security.dto.AuthenticationMember; -import com.api.trip.domain.member.model.Member; -import com.api.trip.domain.member.repository.MemberRepository; -import io.jsonwebtoken.*; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/api/trip/common/security/SecurityConfig.java b/src/main/java/com/api/trip/common/security/SecurityConfig.java index 8146dbb..8aa5d10 100644 --- a/src/main/java/com/api/trip/common/security/SecurityConfig.java +++ b/src/main/java/com/api/trip/common/security/SecurityConfig.java @@ -1,7 +1,6 @@ package com.api.trip.common.security; import lombok.RequiredArgsConstructor; -import org.springframework.boot.autoconfigure.security.servlet.PathRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; diff --git a/src/main/java/com/api/trip/common/security/util/SecurityUtils.java b/src/main/java/com/api/trip/common/security/util/SecurityUtils.java index 256cdca..0e1d970 100644 --- a/src/main/java/com/api/trip/common/security/util/SecurityUtils.java +++ b/src/main/java/com/api/trip/common/security/util/SecurityUtils.java @@ -1,6 +1,5 @@ package com.api.trip.common.security.util; -import org.springframework.context.annotation.Configuration; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; diff --git a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java index 14780b9..b18b4c9 100644 --- a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java +++ b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java @@ -1,25 +1,13 @@ package com.api.trip.domain.article.controller; -import com.api.trip.domain.article.controller.dto.CreateArticleRequest; -import com.api.trip.domain.article.controller.dto.GetArticlesResponse; -import com.api.trip.domain.article.controller.dto.GetMyArticlesResponse; -import com.api.trip.domain.article.controller.dto.ReadArticleResponse; -import com.api.trip.domain.article.controller.dto.UpdateArticleRequest; +import com.api.trip.domain.article.controller.dto.*; import com.api.trip.domain.article.service.ArticleService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; -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; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/articles") diff --git a/src/main/java/com/api/trip/domain/article/model/Article.java b/src/main/java/com/api/trip/domain/article/model/Article.java index 6738d8b..d34e154 100644 --- a/src/main/java/com/api/trip/domain/article/model/Article.java +++ b/src/main/java/com/api/trip/domain/article/model/Article.java @@ -2,13 +2,7 @@ import com.api.trip.common.auditing.entity.BaseTimeEntity; import com.api.trip.domain.member.model.Member; -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.ManyToOne; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/api/trip/domain/article/service/ArticleService.java b/src/main/java/com/api/trip/domain/article/service/ArticleService.java index 5380b7f..cbd32e7 100644 --- a/src/main/java/com/api/trip/domain/article/service/ArticleService.java +++ b/src/main/java/com/api/trip/domain/article/service/ArticleService.java @@ -1,10 +1,6 @@ package com.api.trip.domain.article.service; -import com.api.trip.domain.article.controller.dto.CreateArticleRequest; -import com.api.trip.domain.article.controller.dto.GetArticlesResponse; -import com.api.trip.domain.article.controller.dto.GetMyArticlesResponse; -import com.api.trip.domain.article.controller.dto.ReadArticleResponse; -import com.api.trip.domain.article.controller.dto.UpdateArticleRequest; +import com.api.trip.domain.article.controller.dto.*; import com.api.trip.domain.article.model.Article; import com.api.trip.domain.article.repository.ArticleRepository; import com.api.trip.domain.member.model.Member; diff --git a/src/main/java/com/api/trip/domain/comment/controller/CommentController.java b/src/main/java/com/api/trip/domain/comment/controller/CommentController.java index 3591398..6bdca89 100644 --- a/src/main/java/com/api/trip/domain/comment/controller/CommentController.java +++ b/src/main/java/com/api/trip/domain/comment/controller/CommentController.java @@ -8,14 +8,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; -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; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/comments") diff --git a/src/main/java/com/api/trip/domain/comment/model/Comment.java b/src/main/java/com/api/trip/domain/comment/model/Comment.java index 8408ed3..1a2b535 100644 --- a/src/main/java/com/api/trip/domain/comment/model/Comment.java +++ b/src/main/java/com/api/trip/domain/comment/model/Comment.java @@ -3,13 +3,7 @@ import com.api.trip.common.auditing.entity.BaseTimeEntity; import com.api.trip.domain.article.model.Article; import com.api.trip.domain.member.model.Member; -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.ManyToOne; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java b/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java index 33e5629..5fde0eb 100644 --- a/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java +++ b/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java @@ -2,7 +2,6 @@ import com.api.trip.domain.member.model.Member; import lombok.Getter; -import lombok.NoArgsConstructor; @Getter public class JoinRequest { diff --git a/src/main/java/com/api/trip/domain/member/service/MemberService.java b/src/main/java/com/api/trip/domain/member/service/MemberService.java index c40f13f..f162df7 100644 --- a/src/main/java/com/api/trip/domain/member/service/MemberService.java +++ b/src/main/java/com/api/trip/domain/member/service/MemberService.java @@ -20,8 +20,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.security.SecureRandom; -import java.util.List; import java.util.stream.Collectors; @Service diff --git a/src/test/java/com/api/trip/TripApplicationTests.java b/src/test/java/com/api/trip/TripApplicationTests.java index a0fcd5a..f4e3edb 100644 --- a/src/test/java/com/api/trip/TripApplicationTests.java +++ b/src/test/java/com/api/trip/TripApplicationTests.java @@ -1,7 +1,6 @@ package com.api.trip; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; //@SpringBootTest class TripApplicationTests { From b8e29eb3414156ca402983c226eb19a6d60ad777 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Wed, 13 Dec 2023 11:39:19 +0900 Subject: [PATCH 31/45] =?UTF-8?q?Refactor:=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/trip/common/security/{ => config}/SecurityConfig.java | 4 +++- .../java/com/api/trip/common/security/{ => jwt}/JwtToken.java | 2 +- .../api/trip/common/security/{ => jwt}/JwtTokenFilter.java | 2 +- .../api/trip/common/security/{ => jwt}/JwtTokenProvider.java | 2 +- .../common/security/{ => service}/MemberSecurityService.java | 2 +- .../api/trip/domain/member/controller/dto/LoginResponse.java | 2 +- .../com/api/trip/domain/member/service/MemberService.java | 4 ++-- 7 files changed, 10 insertions(+), 8 deletions(-) rename src/main/java/com/api/trip/common/security/{ => config}/SecurityConfig.java (92%) rename src/main/java/com/api/trip/common/security/{ => jwt}/JwtToken.java (82%) rename src/main/java/com/api/trip/common/security/{ => jwt}/JwtTokenFilter.java (97%) rename src/main/java/com/api/trip/common/security/{ => jwt}/JwtTokenProvider.java (98%) rename src/main/java/com/api/trip/common/security/{ => service}/MemberSecurityService.java (96%) diff --git a/src/main/java/com/api/trip/common/security/SecurityConfig.java b/src/main/java/com/api/trip/common/security/config/SecurityConfig.java similarity index 92% rename from src/main/java/com/api/trip/common/security/SecurityConfig.java rename to src/main/java/com/api/trip/common/security/config/SecurityConfig.java index 8aa5d10..342dbae 100644 --- a/src/main/java/com/api/trip/common/security/SecurityConfig.java +++ b/src/main/java/com/api/trip/common/security/config/SecurityConfig.java @@ -1,5 +1,7 @@ -package com.api.trip.common.security; +package com.api.trip.common.security.config; +import com.api.trip.common.security.jwt.JwtTokenFilter; +import com.api.trip.common.security.jwt.JwtTokenProvider; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/api/trip/common/security/JwtToken.java b/src/main/java/com/api/trip/common/security/jwt/JwtToken.java similarity index 82% rename from src/main/java/com/api/trip/common/security/JwtToken.java rename to src/main/java/com/api/trip/common/security/jwt/JwtToken.java index 7437fe7..6d14b7b 100644 --- a/src/main/java/com/api/trip/common/security/JwtToken.java +++ b/src/main/java/com/api/trip/common/security/jwt/JwtToken.java @@ -1,4 +1,4 @@ -package com.api.trip.common.security; +package com.api.trip.common.security.jwt; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/api/trip/common/security/JwtTokenFilter.java b/src/main/java/com/api/trip/common/security/jwt/JwtTokenFilter.java similarity index 97% rename from src/main/java/com/api/trip/common/security/JwtTokenFilter.java rename to src/main/java/com/api/trip/common/security/jwt/JwtTokenFilter.java index c86047e..1d88834 100644 --- a/src/main/java/com/api/trip/common/security/JwtTokenFilter.java +++ b/src/main/java/com/api/trip/common/security/jwt/JwtTokenFilter.java @@ -1,4 +1,4 @@ -package com.api.trip.common.security; +package com.api.trip.common.security.jwt; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/src/main/java/com/api/trip/common/security/JwtTokenProvider.java b/src/main/java/com/api/trip/common/security/jwt/JwtTokenProvider.java similarity index 98% rename from src/main/java/com/api/trip/common/security/JwtTokenProvider.java rename to src/main/java/com/api/trip/common/security/jwt/JwtTokenProvider.java index 998ac4f..5f288ac 100644 --- a/src/main/java/com/api/trip/common/security/JwtTokenProvider.java +++ b/src/main/java/com/api/trip/common/security/jwt/JwtTokenProvider.java @@ -1,4 +1,4 @@ -package com.api.trip.common.security; +package com.api.trip.common.security.jwt; import io.jsonwebtoken.Claims; diff --git a/src/main/java/com/api/trip/common/security/MemberSecurityService.java b/src/main/java/com/api/trip/common/security/service/MemberSecurityService.java similarity index 96% rename from src/main/java/com/api/trip/common/security/MemberSecurityService.java rename to src/main/java/com/api/trip/common/security/service/MemberSecurityService.java index 6e8ecb3..a9eb390 100644 --- a/src/main/java/com/api/trip/common/security/MemberSecurityService.java +++ b/src/main/java/com/api/trip/common/security/service/MemberSecurityService.java @@ -1,4 +1,4 @@ -package com.api.trip.common.security; +package com.api.trip.common.security.service; import com.api.trip.common.security.dto.AuthenticationMember; import com.api.trip.domain.member.model.Member; diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/LoginResponse.java b/src/main/java/com/api/trip/domain/member/controller/dto/LoginResponse.java index 0c37761..0943176 100644 --- a/src/main/java/com/api/trip/domain/member/controller/dto/LoginResponse.java +++ b/src/main/java/com/api/trip/domain/member/controller/dto/LoginResponse.java @@ -1,6 +1,6 @@ package com.api.trip.domain.member.controller.dto; -import com.api.trip.common.security.JwtToken; +import com.api.trip.common.security.jwt.JwtToken; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/api/trip/domain/member/service/MemberService.java b/src/main/java/com/api/trip/domain/member/service/MemberService.java index f162df7..6c95be0 100644 --- a/src/main/java/com/api/trip/domain/member/service/MemberService.java +++ b/src/main/java/com/api/trip/domain/member/service/MemberService.java @@ -1,7 +1,7 @@ package com.api.trip.domain.member.service; -import com.api.trip.common.security.JwtToken; -import com.api.trip.common.security.JwtTokenProvider; +import com.api.trip.common.security.jwt.JwtToken; +import com.api.trip.common.security.jwt.JwtTokenProvider; import com.api.trip.common.security.util.SecurityUtils; import com.api.trip.domain.email.model.EmailAuth; import com.api.trip.domain.email.repository.EmailAuthRepository; From d9afe7e61d966cbd1eade86d126a7622d51aefc6 Mon Sep 17 00:00:00 2001 From: gkfktkrh153 Date: Wed, 13 Dec 2023 14:33:07 +0900 Subject: [PATCH 32/45] =?UTF-8?q?Feature=20:=20item=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../item/controller/ItemController.java | 43 ++++++++++++ .../item/controller/dto/ItemResponse.java | 4 ++ .../item/controller/dto/ItemsResponse.java | 4 ++ .../com/api/trip/domain/item/model/Item.java | 53 +++++++++++++++ .../item/repository/ItemRepository.java | 7 ++ .../trip/domain/item/service/ItemService.java | 67 +++++++++++++++++++ 6 files changed, 178 insertions(+) create mode 100644 src/main/java/com/api/trip/domain/item/controller/ItemController.java create mode 100644 src/main/java/com/api/trip/domain/item/controller/dto/ItemResponse.java create mode 100644 src/main/java/com/api/trip/domain/item/controller/dto/ItemsResponse.java create mode 100644 src/main/java/com/api/trip/domain/item/model/Item.java create mode 100644 src/main/java/com/api/trip/domain/item/repository/ItemRepository.java create mode 100644 src/main/java/com/api/trip/domain/item/service/ItemService.java diff --git a/src/main/java/com/api/trip/domain/item/controller/ItemController.java b/src/main/java/com/api/trip/domain/item/controller/ItemController.java new file mode 100644 index 0000000..0a44ebc --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/controller/ItemController.java @@ -0,0 +1,43 @@ +package com.api.trip.domain.item.controller; +import com.api.trip.domain.item.controller.dto.ItemResponse; +import com.api.trip.domain.item.controller.dto.ItemsResponse; +import com.api.trip.domain.item.service.ItemService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/items") +@RequiredArgsConstructor +public class ItemController { + + private final ItemService itemService; + + + @GetMapping("/{ItemId}") + public ResponseEntity getItem(@PathVariable Long ItemId) { + return ResponseEntity.ok(itemService.getItem(ItemId)); + } + + @GetMapping + public ResponseEntity getItems( + @PageableDefault(size = 8) Pageable pageable, + @RequestParam(value = "filter", required = false) String filter + ) { + return ResponseEntity.ok(itemService.getItems(pageable)); + } + + @GetMapping("/me") + public ResponseEntity getMyItems() { + return ResponseEntity.ok(itemService.getMyItems()); + } + + @DeleteMapping("/{ItemId}") + public ResponseEntity deleteItem(@PathVariable Long ItemId) { + itemService.deleteItem(ItemId); + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/com/api/trip/domain/item/controller/dto/ItemResponse.java b/src/main/java/com/api/trip/domain/item/controller/dto/ItemResponse.java new file mode 100644 index 0000000..ccf13d8 --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/controller/dto/ItemResponse.java @@ -0,0 +1,4 @@ +package com.api.trip.domain.item.controller.dto; + +public class ItemResponse { +} diff --git a/src/main/java/com/api/trip/domain/item/controller/dto/ItemsResponse.java b/src/main/java/com/api/trip/domain/item/controller/dto/ItemsResponse.java new file mode 100644 index 0000000..3c9d59c --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/controller/dto/ItemsResponse.java @@ -0,0 +1,4 @@ +package com.api.trip.domain.item.controller.dto; + +public class ItemsResponse { +} diff --git a/src/main/java/com/api/trip/domain/item/model/Item.java b/src/main/java/com/api/trip/domain/item/model/Item.java new file mode 100644 index 0000000..a6fa0b1 --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/model/Item.java @@ -0,0 +1,53 @@ +package com.api.trip.domain.item.model; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Item extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long productId; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String shopName; + + @Column(nullable = false) + private String buyUrl; + + @Column(nullable = false) + private long maxPrice; + + @Column(nullable = false) + private long minPrice; + + @Column(nullable = false) + private long viewCount; + + @Column + private boolean isDeleted; + + @ManyToOne(fetch = FetchType.LAZY) + private Member writer; + + + + + public void increaseViewCount() { + this.viewCount++; + } +} \ No newline at end of file diff --git a/src/main/java/com/api/trip/domain/item/repository/ItemRepository.java b/src/main/java/com/api/trip/domain/item/repository/ItemRepository.java new file mode 100644 index 0000000..4c7d786 --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/repository/ItemRepository.java @@ -0,0 +1,7 @@ +package com.api.trip.domain.item.repository; + +import com.api.trip.domain.item.model.Item; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ItemRepository extends JpaRepository { +} diff --git a/src/main/java/com/api/trip/domain/item/service/ItemService.java b/src/main/java/com/api/trip/domain/item/service/ItemService.java new file mode 100644 index 0000000..5b5173c --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/service/ItemService.java @@ -0,0 +1,67 @@ +package com.api.trip.domain.item.service; + + +import com.api.trip.common.security.util.SecurityUtils; +import com.api.trip.domain.item.controller.dto.ItemResponse; +import com.api.trip.domain.item.controller.dto.ItemsResponse; +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.item.repository.ItemRepository; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.service.MemberService; +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; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class ItemService { + + private final MemberService memberService; + private final ItemRepository itemRepository; + + public Long createItem(CreateItemRequest itemRequest) { + Member member = memberService.getAuthenticationMember(); + Item item = itemRequest.toEntity(member); + + return itemRepository.save(item).getId(); + } + + public ItemResponse getItem(Long ItemId) { + Item item = itemRepository.findById(ItemId).orElseThrow(); + + item.increaseViewCount(); + return ItemResponse.of(item); + } + + @Transactional(readOnly = true) + public ItemsResponse getItems(Pageable pageable) { + Page itemPage = itemRepository.findItems(pageable); + + return ItemsResponse.of(itemPage); + } + + @Transactional(readOnly = true) + public ItemsResponse getMyItems() { + Member member = memberService.getAuthenticationMember(); + + List Items = itemRepository.findAllByWriterOrderByIdDesc(member); + + return ItemsResponse.of(Items); + } + + public void deleteItem(Long ItemId) { + Member member = memberService.getAuthenticationMember(); + + Item item = itemRepository.findById(ItemId).orElseThrow(); + if (item.getWriter() != member) { + throw new RuntimeException("삭제 권한이 없습니다."); + } + item.delete(); + } + +} From 46be9df4b18bedacfa0950e29b1f0d07657295b1 Mon Sep 17 00:00:00 2001 From: gkfktkrh153 Date: Wed, 13 Dec 2023 16:21:17 +0900 Subject: [PATCH 33/45] =?UTF-8?q?Feature=20:=20item=20=EC=A0=95=EB=A0=AC?= =?UTF-8?q?=20=EC=A0=9C=EC=99=B8=20=EC=9E=91=EC=97=85=20=EB=81=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../item/controller/ItemController.java | 18 ++++--- .../controller/dto/CreateItemRequest.java | 36 ++++++++++++++ .../item/controller/dto/GetItemResponse.java | 40 +++++++++++++++ .../item/controller/dto/GetItemsResponse.java | 30 ++++++++++++ .../item/controller/dto/ItemResponse.java | 4 -- .../item/controller/dto/ItemsResponse.java | 4 -- .../com/api/trip/domain/item/model/Item.java | 17 ++++++- .../item/repository/ItemRepository.java | 2 +- .../item/repository/ItemRepositoryCustom.java | 10 ++++ .../repository/ItemRepositoryCustomImpl.java | 49 +++++++++++++++++++ .../trip/domain/item/service/ItemService.java | 23 +++------ 11 files changed, 200 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/api/trip/domain/item/controller/dto/CreateItemRequest.java create mode 100644 src/main/java/com/api/trip/domain/item/controller/dto/GetItemResponse.java create mode 100644 src/main/java/com/api/trip/domain/item/controller/dto/GetItemsResponse.java delete mode 100644 src/main/java/com/api/trip/domain/item/controller/dto/ItemResponse.java delete mode 100644 src/main/java/com/api/trip/domain/item/controller/dto/ItemsResponse.java create mode 100644 src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustom.java create mode 100644 src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustomImpl.java diff --git a/src/main/java/com/api/trip/domain/item/controller/ItemController.java b/src/main/java/com/api/trip/domain/item/controller/ItemController.java index 0a44ebc..2690201 100644 --- a/src/main/java/com/api/trip/domain/item/controller/ItemController.java +++ b/src/main/java/com/api/trip/domain/item/controller/ItemController.java @@ -1,11 +1,13 @@ package com.api.trip.domain.item.controller; -import com.api.trip.domain.item.controller.dto.ItemResponse; -import com.api.trip.domain.item.controller.dto.ItemsResponse; +import com.api.trip.domain.item.controller.dto.CreateItemRequest; +import com.api.trip.domain.item.controller.dto.GetItemResponse; +import com.api.trip.domain.item.controller.dto.GetItemsResponse; import com.api.trip.domain.item.service.ItemService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; @RestController @@ -15,24 +17,24 @@ public class ItemController { private final ItemService itemService; + @PostMapping + public ResponseEntity createItem(@RequestBody CreateItemRequest itemRequest) { + return ResponseEntity.ok(itemService.createItem(itemRequest)); + } @GetMapping("/{ItemId}") - public ResponseEntity getItem(@PathVariable Long ItemId) { + public ResponseEntity getItem(@PathVariable Long ItemId) { return ResponseEntity.ok(itemService.getItem(ItemId)); } @GetMapping - public ResponseEntity getItems( + public ResponseEntity getItems( @PageableDefault(size = 8) Pageable pageable, @RequestParam(value = "filter", required = false) String filter ) { return ResponseEntity.ok(itemService.getItems(pageable)); } - @GetMapping("/me") - public ResponseEntity getMyItems() { - return ResponseEntity.ok(itemService.getMyItems()); - } @DeleteMapping("/{ItemId}") public ResponseEntity deleteItem(@PathVariable Long ItemId) { diff --git a/src/main/java/com/api/trip/domain/item/controller/dto/CreateItemRequest.java b/src/main/java/com/api/trip/domain/item/controller/dto/CreateItemRequest.java new file mode 100644 index 0000000..d81f1eb --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/controller/dto/CreateItemRequest.java @@ -0,0 +1,36 @@ +package com.api.trip.domain.item.controller.dto; + +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.Column; +import lombok.Getter; + +@Getter +public class CreateItemRequest { + + + private Long productId; + + private String title; + + private String shopName; + + private String buyUrl; + + private long maxPrice; + + private long minPrice; + + public Item toEntity(Member writer){ + return Item.builder() + .productId(productId) + .title(title) + .shopName(shopName) + .buyUrl(buyUrl) + .maxPrice(maxPrice) + .minPrice(minPrice) + .writer(writer) + .build(); + + } +} diff --git a/src/main/java/com/api/trip/domain/item/controller/dto/GetItemResponse.java b/src/main/java/com/api/trip/domain/item/controller/dto/GetItemResponse.java new file mode 100644 index 0000000..1541c4a --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/controller/dto/GetItemResponse.java @@ -0,0 +1,40 @@ +package com.api.trip.domain.item.controller.dto; + +import com.api.trip.domain.item.model.Item; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class GetItemResponse { + + private Long id; + + private Long productId; + + private String title; + + private String shopName; + + private String buyUrl; + + private long maxPrice; + + private long minPrice; + + private String writerNickname; + + public static GetItemResponse of(Item item){ + return GetItemResponse.builder() + .id(item.getId()) + .productId(item.getProductId()) + .title(item.getTitle()) + .shopName(item.getShopName()) + .buyUrl(item.getBuyUrl()) + .maxPrice(item.getMaxPrice()) + .minPrice(item.getMinPrice()) + .writerNickname(item.getWriter().getNickname()) + .build(); + + } +} diff --git a/src/main/java/com/api/trip/domain/item/controller/dto/GetItemsResponse.java b/src/main/java/com/api/trip/domain/item/controller/dto/GetItemsResponse.java new file mode 100644 index 0000000..9d13fcf --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/controller/dto/GetItemsResponse.java @@ -0,0 +1,30 @@ +package com.api.trip.domain.item.controller.dto; + +import com.api.trip.domain.item.model.Item; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@Builder +public class GetItemsResponse { + + private long totalCount; + private int currentPage; + private int totalPage; + private List itemList; + + public static GetItemsResponse of(Page page) + { + return GetItemsResponse.builder() + .currentPage(page.getNumber()) + .totalCount(page.getTotalElements()) + .totalPage(page.getTotalPages()) + .itemList(page.getContent().stream().map(GetItemResponse::of).collect(Collectors.toList())) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/item/controller/dto/ItemResponse.java b/src/main/java/com/api/trip/domain/item/controller/dto/ItemResponse.java deleted file mode 100644 index ccf13d8..0000000 --- a/src/main/java/com/api/trip/domain/item/controller/dto/ItemResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.api.trip.domain.item.controller.dto; - -public class ItemResponse { -} diff --git a/src/main/java/com/api/trip/domain/item/controller/dto/ItemsResponse.java b/src/main/java/com/api/trip/domain/item/controller/dto/ItemsResponse.java deleted file mode 100644 index 3c9d59c..0000000 --- a/src/main/java/com/api/trip/domain/item/controller/dto/ItemsResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.api.trip.domain.item.controller.dto; - -public class ItemsResponse { -} diff --git a/src/main/java/com/api/trip/domain/item/model/Item.java b/src/main/java/com/api/trip/domain/item/model/Item.java index a6fa0b1..7e808a3 100644 --- a/src/main/java/com/api/trip/domain/item/model/Item.java +++ b/src/main/java/com/api/trip/domain/item/model/Item.java @@ -45,9 +45,24 @@ public class Item extends BaseTimeEntity { private Member writer; - + @Builder + private Item(Long productId, String title, String shopName, String buyUrl, long maxPrice, long minPrice, Member writer) { + this.productId = productId; + this.title = title; + this.shopName = shopName; + this.buyUrl = buyUrl; + this.maxPrice = maxPrice; + this.minPrice = minPrice; + this.viewCount = 0; + this.writer = writer; + } public void increaseViewCount() { this.viewCount++; } + public void delete(){ + if(this.isDeleted == true) + new RuntimeException("이미 삭제된 아이템입니다."); + this.isDeleted = true; + } } \ No newline at end of file diff --git a/src/main/java/com/api/trip/domain/item/repository/ItemRepository.java b/src/main/java/com/api/trip/domain/item/repository/ItemRepository.java index 4c7d786..34fd1dc 100644 --- a/src/main/java/com/api/trip/domain/item/repository/ItemRepository.java +++ b/src/main/java/com/api/trip/domain/item/repository/ItemRepository.java @@ -3,5 +3,5 @@ import com.api.trip.domain.item.model.Item; import org.springframework.data.jpa.repository.JpaRepository; -public interface ItemRepository extends JpaRepository { +public interface ItemRepository extends JpaRepository, ItemRepositoryCustom { } diff --git a/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustom.java b/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustom.java new file mode 100644 index 0000000..015c942 --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.api.trip.domain.item.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.item.model.Item; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface ItemRepositoryCustom { + Page findItems(Pageable pageable); +} diff --git a/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustomImpl.java new file mode 100644 index 0000000..c8051f3 --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustomImpl.java @@ -0,0 +1,49 @@ +package com.api.trip.domain.item.repository; + +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.member.model.QMember; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.support.PageableExecutionUtils; + +import java.util.List; + +import static com.api.trip.domain.item.model.QItem.item; + +public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{ + + private final JPAQueryFactory jpaQueryFactory; + + public ItemRepositoryCustomImpl(EntityManager em) { + this.jpaQueryFactory = new JPAQueryFactory(em); + } + + @Override + public Page findItems(Pageable pageable) { + + + List result = jpaQueryFactory.selectFrom(item) + .join(item.writer).fetchJoin() + .where( + item.isDeleted.eq(false) + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + //.orderBy() + .fetch(); + + JPAQuery countQuery = jpaQueryFactory.select(item.count()) + .from(item) + .where( + item.isDeleted.eq(false) + ); + + return PageableExecutionUtils.getPage(result, pageable, countQuery::fetchOne); + } + + +} diff --git a/src/main/java/com/api/trip/domain/item/service/ItemService.java b/src/main/java/com/api/trip/domain/item/service/ItemService.java index 5b5173c..62de899 100644 --- a/src/main/java/com/api/trip/domain/item/service/ItemService.java +++ b/src/main/java/com/api/trip/domain/item/service/ItemService.java @@ -1,9 +1,9 @@ package com.api.trip.domain.item.service; -import com.api.trip.common.security.util.SecurityUtils; -import com.api.trip.domain.item.controller.dto.ItemResponse; -import com.api.trip.domain.item.controller.dto.ItemsResponse; +import com.api.trip.domain.item.controller.dto.CreateItemRequest; +import com.api.trip.domain.item.controller.dto.GetItemResponse; +import com.api.trip.domain.item.controller.dto.GetItemsResponse; import com.api.trip.domain.item.model.Item; import com.api.trip.domain.item.repository.ItemRepository; import com.api.trip.domain.member.model.Member; @@ -31,28 +31,21 @@ public Long createItem(CreateItemRequest itemRequest) { return itemRepository.save(item).getId(); } - public ItemResponse getItem(Long ItemId) { + public GetItemResponse getItem(Long ItemId) { Item item = itemRepository.findById(ItemId).orElseThrow(); item.increaseViewCount(); - return ItemResponse.of(item); + return GetItemResponse.of(item); } @Transactional(readOnly = true) - public ItemsResponse getItems(Pageable pageable) { + public GetItemsResponse getItems(Pageable pageable) { Page itemPage = itemRepository.findItems(pageable); + itemPage.getContent(); - return ItemsResponse.of(itemPage); + return GetItemsResponse.of(itemPage); } - @Transactional(readOnly = true) - public ItemsResponse getMyItems() { - Member member = memberService.getAuthenticationMember(); - - List Items = itemRepository.findAllByWriterOrderByIdDesc(member); - - return ItemsResponse.of(Items); - } public void deleteItem(Long ItemId) { Member member = memberService.getAuthenticationMember(); From 7b4312947016f0a0aa983cc3f84167cf7d88bce6 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Wed, 13 Dec 2023 16:45:26 +0900 Subject: [PATCH 34/45] =?UTF-8?q?Chore:=20build.gradle=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `aws s3`, `tika` 의존성 추가 - 관련 있는 implement 끼리 묶어서 정리 --- build.gradle | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index 4cc65b8..986cb45 100644 --- a/build.gradle +++ b/build.gradle @@ -26,37 +26,37 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' - - // mail implementation 'org.springframework.boot:spring-boot-starter-mail' - //Querydsl 추가 + // aws + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.429' + implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' + + // tika + implementation 'org.apache.tika:tika-core:2.9.1' + + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + // Jwt + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' - // mysql runtimeOnly 'com.mysql:mysql-connector-j' - - // h2 runtimeOnly 'com.h2database:h2' - // lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.projectlombok:lombok' - - // Jwt - implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' - runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' - runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' - - // Swagger - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' } From d5f6f3eeb307f2f4161e2e7499846931cbd4d6c0 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Wed, 13 Dec 2023 16:50:28 +0900 Subject: [PATCH 35/45] =?UTF-8?q?Feat:=20aws=20s3=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - s3에 업로드 할 때 `members/현재날짜/파일 이름` 형식으로 저장해서 관리 --- .../domain/aws/config/AmazonS3Config.java | 33 ++++++++++ .../domain/aws/service/AmazonS3Service.java | 65 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/main/java/com/api/trip/domain/aws/config/AmazonS3Config.java create mode 100644 src/main/java/com/api/trip/domain/aws/service/AmazonS3Service.java diff --git a/src/main/java/com/api/trip/domain/aws/config/AmazonS3Config.java b/src/main/java/com/api/trip/domain/aws/config/AmazonS3Config.java new file mode 100644 index 0000000..37b2f18 --- /dev/null +++ b/src/main/java/com/api/trip/domain/aws/config/AmazonS3Config.java @@ -0,0 +1,33 @@ +package com.api.trip.domain.aws.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AmazonS3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3Client() { + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + + return AmazonS3ClientBuilder + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/aws/service/AmazonS3Service.java b/src/main/java/com/api/trip/domain/aws/service/AmazonS3Service.java new file mode 100644 index 0000000..7d8f6be --- /dev/null +++ b/src/main/java/com/api/trip/domain/aws/service/AmazonS3Service.java @@ -0,0 +1,65 @@ +package com.api.trip.domain.aws.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import java.util.UUID; + +@Service +@Slf4j +@RequiredArgsConstructor +public class AmazonS3Service { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Getter + @Value("${cloud.aws.default-image}") + private String defaultProfileImg; + + public String upload(MultipartFile profileImg) { + + // 파일 확장자 분리 (.png, .jpg, .gif) + String ext = Optional.ofNullable(profileImg.getOriginalFilename()) + .filter(f -> f.contains(".")) + .map(f -> f.split("\\.")[1]) + .orElse(""); + + // 파일 이름은 UUID.ext 형식으로 업로드 + String fileName = "%s.%s".formatted(UUID.randomUUID(), ext); + String formatDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy_MM_dd")); + String s3location = bucket + "/members/" + formatDate; + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(profileImg.getSize()); + metadata.setContentType(profileImg.getContentType()); + + try { + log.debug("uploading aws s3.. upload path: {}", s3location + fileName); + amazonS3.putObject(s3location, fileName, profileImg.getInputStream(), metadata); + } catch (IOException e) { + // TODO: add exception + throw new RuntimeException("Fail AWS S3 Upload.. {}", e); + } + + return String.valueOf(amazonS3.getUrl(s3location, fileName)); + } + + // TODO: 회원 정보 수정 구현시 필요 + public void delete(String key) { + amazonS3.deleteObject(bucket, key); + } + +} From 34d83c1f71bda21ea34cc08ee87c9e468a79cc54 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Wed, 13 Dec 2023 16:52:31 +0900 Subject: [PATCH 36/45] =?UTF-8?q?Feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B8=B0?= =?UTF-8?q?=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 - s3에 업로드한 파일의 url을 profileImg 컬럼에 넣어주는 방식으로 구현 - 프로필 이미지를 따로 설정하지 않은 회원은 기본 이미지가 들어가도록 구현 - application.yml에서 최대 파일 크기 설정 (추후 변경 가능) --- .../domain/aws/util/MultipartFileUtils.java | 18 ++++++++++ .../api/trip/domain/member/model/Member.java | 18 ++++++++-- .../domain/member/service/MemberService.java | 36 ++++++++++++++++--- src/main/resources/application.yml | 4 +++ 4 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/api/trip/domain/aws/util/MultipartFileUtils.java diff --git a/src/main/java/com/api/trip/domain/aws/util/MultipartFileUtils.java b/src/main/java/com/api/trip/domain/aws/util/MultipartFileUtils.java new file mode 100644 index 0000000..1a83db1 --- /dev/null +++ b/src/main/java/com/api/trip/domain/aws/util/MultipartFileUtils.java @@ -0,0 +1,18 @@ +package com.api.trip.domain.aws.util; + +import org.apache.tika.Tika; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +public class MultipartFileUtils { + + final static String[] PERMISSION_PROFILE_FILE_MIME_TYPE = {"image/jpeg", "image/png", "image/gif"}; + + // 파일 변조 여부 검사 + public static boolean isPermission(InputStream inputStream) throws IOException { + String mimeType = new Tika().detect(inputStream); + return Arrays.stream(PERMISSION_PROFILE_FILE_MIME_TYPE).anyMatch(type -> type.equalsIgnoreCase(mimeType)); + } +} diff --git a/src/main/java/com/api/trip/domain/member/model/Member.java b/src/main/java/com/api/trip/domain/member/model/Member.java index b8b9034..4eb8e2b 100644 --- a/src/main/java/com/api/trip/domain/member/model/Member.java +++ b/src/main/java/com/api/trip/domain/member/model/Member.java @@ -1,6 +1,7 @@ package com.api.trip.domain.member.model; import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.member.controller.dto.JoinRequest; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -29,6 +30,9 @@ public class Member extends BaseTimeEntity { @Column(nullable = false) private String password; + @Column(nullable = false) // 기본 이미지가 무조건 들어갈 예정. + private String profileImg; + @Column(nullable = false) @Enumerated(EnumType.STRING) private MemberRole role; @@ -36,13 +40,23 @@ public class Member extends BaseTimeEntity { private boolean emailAuth; @Builder - private Member(String email, String nickname, String password){ + private Member(String email, String password, String nickname, String profileImg){ this.email = email; - this.nickname = nickname; this.password = password; + this.nickname = nickname; + this.profileImg = profileImg; this.role = MemberRole.MEMBER; } + public static Member of(String email, String password, String nickname, String profileImg) { + return Member.builder() + .email(email) + .password(password) + .nickname(nickname) + .profileImg(profileImg) + .build(); + } + // 이메일 인증 상태 변경 메서드 public void emailVerifiedSuccess() { this.emailAuth = true; diff --git a/src/main/java/com/api/trip/domain/member/service/MemberService.java b/src/main/java/com/api/trip/domain/member/service/MemberService.java index 6c95be0..63b4fbd 100644 --- a/src/main/java/com/api/trip/domain/member/service/MemberService.java +++ b/src/main/java/com/api/trip/domain/member/service/MemberService.java @@ -3,6 +3,8 @@ import com.api.trip.common.security.jwt.JwtToken; import com.api.trip.common.security.jwt.JwtTokenProvider; import com.api.trip.common.security.util.SecurityUtils; +import com.api.trip.domain.aws.util.MultipartFileUtils; +import com.api.trip.domain.aws.service.AmazonS3Service; import com.api.trip.domain.email.model.EmailAuth; import com.api.trip.domain.email.repository.EmailAuthRepository; import com.api.trip.domain.member.controller.dto.DeleteRequest; @@ -19,7 +21,9 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.stream.Collectors; @Service @@ -30,21 +34,45 @@ public class MemberService { private final MemberRepository memberRepository; private final EmailAuthRepository emailAuthRepository; private final PasswordEncoder passwordEncoder; + private final AmazonS3Service amazonS3Service; private final AuthenticationManagerBuilder authenticationManagerBuilder; private final JwtTokenProvider jwtTokenProvider; - public void join(JoinRequest joinRequest) { - // 중복 회원 체크 + public void join(JoinRequest joinRequest, MultipartFile profileImg) throws IOException { + + // 중복된 회원이 있는지 검사 memberRepository.findByEmail(joinRequest.getEmail()).ifPresent(it -> { throw new RuntimeException("이미 존재하는 회원 입니다."); }); + // 이메일 인증이 완료 여부 검사 EmailAuth emailAuth = emailAuthRepository.findByEmail(joinRequest.getEmail()) - .orElseThrow(() -> new RuntimeException("토큰 정보가 없습니다!")); + .orElseThrow(() -> new RuntimeException("이메일 인증 토큰 정보가 없습니다!")); + + String profileImgUrl = ""; + if (profileImg == null || profileImg.isEmpty()) { + // 프로필 사진 데이터가 없으면 기본 이미지 세팅 + profileImgUrl = amazonS3Service.getDefaultProfileImg(); + } else { + // 파일 변조 여부 체크 + if (!MultipartFileUtils.isPermission(profileImg.getInputStream())) { + throw new RuntimeException("프로필 사진은 이미지 파일만 선택 가능합니다!"); + } + + // 요청 파일 이미지가 있는 경우 s3 업로드 + profileImgUrl = amazonS3Service.upload(profileImg); + } + + Member member = Member.of( + joinRequest.getEmail(), + passwordEncoder.encode(joinRequest.getPassword()), + joinRequest.getNickname(), + profileImgUrl + ); - Member member = JoinRequest.of(joinRequest, passwordEncoder.encode(joinRequest.getPassword())); member.emailVerifiedSuccess(); + // TODO: 회원 가입이 실패하는 경우 s3에 있는 파일 삭제 처리 고민 해보기 memberRepository.save(member); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b30c3fc..366d765 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,6 +13,10 @@ spring: web: pageable: one-indexed-parameters: true + servlet: + multipart: + max-file-size: 20MB + max-request-size: 50MB logging: level: From 94b4396057a2fe5eec09125cc72ec0306df9c909 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Wed, 13 Dec 2023 16:53:38 +0900 Subject: [PATCH 37/45] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=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 - `JSON`, `MultipartFile` 2가지 타입을 받기 때문에 @RequestPart로 받도록 수정함. - 프로필 이미지는 안들어올수도 있기 때문에 `required = false` 속성 추가 --- .../member/controller/MemberController.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/api/trip/domain/member/controller/MemberController.java b/src/main/java/com/api/trip/domain/member/controller/MemberController.java index f3ea3ef..e21aaa3 100644 --- a/src/main/java/com/api/trip/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/trip/domain/member/controller/MemberController.java @@ -1,12 +1,24 @@ package com.api.trip.domain.member.controller; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; import com.api.trip.domain.email.service.EmailService; import com.api.trip.domain.member.controller.dto.*; import com.api.trip.domain.member.service.MemberService; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/members") @@ -16,9 +28,10 @@ public class MemberController { private final MemberService memberService; private final EmailService emailService; - @PostMapping("/join") - public ResponseEntity join(@RequestBody JoinRequest joinRequest) { - memberService.join(joinRequest); + @PostMapping(value = "/join", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) + public ResponseEntity join(@RequestPart JoinRequest joinRequest, @RequestPart(required = false) MultipartFile profileImg) throws IOException { + + memberService.join(joinRequest, profileImg); return ResponseEntity.ok().build(); } From 399efb233a4fa2c4e1e4000908fd1cf627ece5d9 Mon Sep 17 00:00:00 2001 From: gkfktkrh153 Date: Wed, 13 Dec 2023 19:09:49 +0900 Subject: [PATCH 38/45] =?UTF-8?q?Feature=20:=20=EA=B4=80=EC=8B=AC=EC=83=81?= =?UTF-8?q?=ED=92=88=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 --- .../domain/interestitem/InterestItem.java | 34 ++++++++++++ .../controller/InterestItemController.java | 43 +++++++++++++++ .../dto/CreateInterestItemRequest.java | 12 +++++ .../dto/GetInterestItemsResponse.java | 33 ++++++++++++ .../repository/InterestItemRepository.java | 15 ++++++ .../service/InterestItemService.java | 53 +++++++++++++++++++ .../item/controller/ItemController.java | 7 ++- .../trip/domain/item/service/ItemService.java | 10 +++- 8 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/api/trip/domain/interestitem/InterestItem.java create mode 100644 src/main/java/com/api/trip/domain/interestitem/controller/InterestItemController.java create mode 100644 src/main/java/com/api/trip/domain/interestitem/controller/dto/CreateInterestItemRequest.java create mode 100644 src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java create mode 100644 src/main/java/com/api/trip/domain/interestitem/repository/InterestItemRepository.java create mode 100644 src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java diff --git a/src/main/java/com/api/trip/domain/interestitem/InterestItem.java b/src/main/java/com/api/trip/domain/interestitem/InterestItem.java new file mode 100644 index 0000000..7c08783 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/InterestItem.java @@ -0,0 +1,34 @@ +package com.api.trip.domain.interestitem; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class InterestItem extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + private Item item; + + @Builder + private InterestItem(Member member, Item item) { + this.member = member; + this.item = item; + } +} diff --git a/src/main/java/com/api/trip/domain/interestitem/controller/InterestItemController.java b/src/main/java/com/api/trip/domain/interestitem/controller/InterestItemController.java new file mode 100644 index 0000000..77e3722 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/controller/InterestItemController.java @@ -0,0 +1,43 @@ +package com.api.trip.domain.interestitem.controller; + +import com.api.trip.domain.interestitem.controller.dto.CreateInterestItemRequest; +import com.api.trip.domain.interestitem.service.InterestItemService; +import com.api.trip.domain.item.controller.dto.CreateItemRequest; +import com.api.trip.domain.item.controller.dto.GetItemResponse; +import com.api.trip.domain.item.controller.dto.GetItemsResponse; +import com.api.trip.domain.item.service.ItemService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/interest-items") +@RequiredArgsConstructor +public class InterestItemController { + + private final InterestItemService interestItemService; + + + @PostMapping + public ResponseEntity createInterestItem(@RequestBody CreateInterestItemRequest itemRequest) { + + interestItemService.createInterestItem(itemRequest); + return ResponseEntity.ok().build(); + } + + @GetMapping + public ResponseEntity getInterestItems( + @PageableDefault(size = 8) Pageable pageable + ) { + return ResponseEntity.ok(interestItemService.getInterestItems(pageable)); + } + + + @DeleteMapping("/{ItemId}") + public ResponseEntity cancelInterestItem(@PathVariable Long ItemId) { + interestItemService.cancelInterestItem(ItemId); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/api/trip/domain/interestitem/controller/dto/CreateInterestItemRequest.java b/src/main/java/com/api/trip/domain/interestitem/controller/dto/CreateInterestItemRequest.java new file mode 100644 index 0000000..7a7629d --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/controller/dto/CreateInterestItemRequest.java @@ -0,0 +1,12 @@ +package com.api.trip.domain.interestitem.controller.dto; + +import com.api.trip.domain.interestitem.InterestItem; +import com.api.trip.domain.member.model.Member; +import lombok.Getter; + +@Getter +public class CreateInterestItemRequest { + + private long itemId; + +} diff --git a/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java b/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java new file mode 100644 index 0000000..ad91aae --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java @@ -0,0 +1,33 @@ +package com.api.trip.domain.interestitem.controller.dto; + +import com.api.trip.domain.interestitem.InterestItem; +import com.api.trip.domain.item.controller.dto.GetItemResponse; +import com.api.trip.domain.item.controller.dto.GetItemsResponse; +import com.api.trip.domain.item.model.Item; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@Builder +public class GetInterestItemsResponse { + + private long totalCount; + private int currentPage; + private int totalPage; + private List itemList; + + public static GetItemsResponse of(Page page) + { + return GetItemsResponse.builder() + .currentPage(page.getNumber()) + .totalCount(page.getTotalElements()) + .totalPage(page.getTotalPages()) + .itemList(page.getContent().stream().map(InterestItem::getItem).map(GetItemResponse::of).collect(Collectors.toList())) + .build(); + } + +} diff --git a/src/main/java/com/api/trip/domain/interestitem/repository/InterestItemRepository.java b/src/main/java/com/api/trip/domain/interestitem/repository/InterestItemRepository.java new file mode 100644 index 0000000..9641209 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/repository/InterestItemRepository.java @@ -0,0 +1,15 @@ +package com.api.trip.domain.interestitem.repository; + +import com.api.trip.domain.interestitem.InterestItem; +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.member.model.Member; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InterestItemRepository extends JpaRepository { + + Page findByMember(Member member, Pageable pageable); + + InterestItem findByItem_Id(Long itemId); +} diff --git a/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java b/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java new file mode 100644 index 0000000..d33c785 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java @@ -0,0 +1,53 @@ +package com.api.trip.domain.interestitem.service; + +import com.api.trip.domain.interestitem.InterestItem; +import com.api.trip.domain.interestitem.controller.dto.CreateInterestItemRequest; +import com.api.trip.domain.interestitem.controller.dto.GetInterestItemsResponse; +import com.api.trip.domain.interestitem.repository.InterestItemRepository; +import com.api.trip.domain.item.controller.dto.CreateItemRequest; +import com.api.trip.domain.item.controller.dto.GetItemsResponse; +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.item.service.ItemService; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.service.MemberService; +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; + +import java.util.stream.Collectors; + +@Service +@Transactional +@RequiredArgsConstructor +public class InterestItemService { + + private final MemberService memberService; + private final ItemService itemService; + private final InterestItemRepository interestItemRepository; + + public void createInterestItem(CreateInterestItemRequest itemRequest) { + Member member = memberService.getAuthenticationMember(); + Item item = itemService.getItem(itemRequest.getItemId()); + + InterestItem interestItem = InterestItem.builder() + .item(item) + .member(member).build(); + + interestItemRepository.save(interestItem); + + } + + public GetItemsResponse getInterestItems(Pageable pageable) { + Member member = memberService.getAuthenticationMember(); + Page page = interestItemRepository.findByMember(member, pageable); + + return GetInterestItemsResponse.of(page); + } + + public void cancelInterestItem(Long itemId) { + InterestItem interestItem = interestItemRepository.findByItem_Id(itemId); + interestItemRepository.delete(interestItem); + } +} diff --git a/src/main/java/com/api/trip/domain/item/controller/ItemController.java b/src/main/java/com/api/trip/domain/item/controller/ItemController.java index 2690201..1e115f1 100644 --- a/src/main/java/com/api/trip/domain/item/controller/ItemController.java +++ b/src/main/java/com/api/trip/domain/item/controller/ItemController.java @@ -24,15 +24,14 @@ public ResponseEntity createItem(@RequestBody CreateItemRequest itemReques } @GetMapping("/{ItemId}") public ResponseEntity getItem(@PathVariable Long ItemId) { - return ResponseEntity.ok(itemService.getItem(ItemId)); + return ResponseEntity.ok(itemService.getItemDetail(ItemId)); } @GetMapping public ResponseEntity getItems( - @PageableDefault(size = 8) Pageable pageable, - @RequestParam(value = "filter", required = false) String filter + @PageableDefault(size = 8) Pageable pageable ) { - return ResponseEntity.ok(itemService.getItems(pageable)); + return ResponseEntity.ok(itemService.getItemsDetail(pageable)); } diff --git a/src/main/java/com/api/trip/domain/item/service/ItemService.java b/src/main/java/com/api/trip/domain/item/service/ItemService.java index 62de899..c18be09 100644 --- a/src/main/java/com/api/trip/domain/item/service/ItemService.java +++ b/src/main/java/com/api/trip/domain/item/service/ItemService.java @@ -31,7 +31,13 @@ public Long createItem(CreateItemRequest itemRequest) { return itemRepository.save(item).getId(); } - public GetItemResponse getItem(Long ItemId) { + @Transactional(readOnly = true) + public Item getItem(Long itemId){ + return itemRepository.findById(itemId).orElseThrow(); + } + + @Transactional(readOnly = true) + public GetItemResponse getItemDetail(Long ItemId) { Item item = itemRepository.findById(ItemId).orElseThrow(); item.increaseViewCount(); @@ -39,7 +45,7 @@ public GetItemResponse getItem(Long ItemId) { } @Transactional(readOnly = true) - public GetItemsResponse getItems(Pageable pageable) { + public GetItemsResponse getItemsDetail(Pageable pageable) { Page itemPage = itemRepository.findItems(pageable); itemPage.getContent(); From 6a95d4dd1bff9e43df0d4bd2351b2ec91981bd2a Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Wed, 13 Dec 2023 21:18:55 +0900 Subject: [PATCH 39/45] =?UTF-8?q?Feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94,=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=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 - 좋아요와 좋아요 취소는 엔드포인트를 분리하여 설계 - member_id, article_id 쌍에 대한 Unique 제약조건 설정 --- .../controller/InterestArticleController.java | 29 +++++++++++ .../dto/CreateInterestArticleRequest.java | 19 ++++++++ .../model/InterestArticle.java | 33 +++++++++++++ .../repository/InterestArticleRepository.java | 11 +++++ .../service/InterestArticleService.java | 48 +++++++++++++++++++ 5 files changed, 140 insertions(+) create mode 100644 src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java create mode 100644 src/main/java/com/api/trip/domain/interestarticle/controller/dto/CreateInterestArticleRequest.java create mode 100644 src/main/java/com/api/trip/domain/interestarticle/model/InterestArticle.java create mode 100644 src/main/java/com/api/trip/domain/interestarticle/repository/InterestArticleRepository.java create mode 100644 src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java diff --git a/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java b/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java new file mode 100644 index 0000000..a46bbb9 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java @@ -0,0 +1,29 @@ +package com.api.trip.domain.interestarticle.controller; + +import com.api.trip.domain.interestarticle.controller.dto.CreateInterestArticleRequest; +import com.api.trip.domain.interestarticle.service.InterestArticleService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/interest-articles") +@RequiredArgsConstructor +public class InterestArticleController { + + private final InterestArticleService interestArticleService; + + @PostMapping + public ResponseEntity createInterestArticle(@RequestBody CreateInterestArticleRequest request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(interestArticleService.createInterestArticle(request, email)); + } + + @DeleteMapping("/{interestArticleId}") + public ResponseEntity deleteInterestArticle(@PathVariable Long interestArticleId) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + interestArticleService.deleteInterestArticle(interestArticleId, email); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/controller/dto/CreateInterestArticleRequest.java b/src/main/java/com/api/trip/domain/interestarticle/controller/dto/CreateInterestArticleRequest.java new file mode 100644 index 0000000..ed21e3d --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/controller/dto/CreateInterestArticleRequest.java @@ -0,0 +1,19 @@ +package com.api.trip.domain.interestarticle.controller.dto; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.member.model.Member; +import lombok.Getter; + +@Getter +public class CreateInterestArticleRequest { + + private Long articleId; + + public InterestArticle toEntity(Member member, Article article) { + return InterestArticle.builder() + .member(member) + .article(article) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/model/InterestArticle.java b/src/main/java/com/api/trip/domain/interestarticle/model/InterestArticle.java new file mode 100644 index 0000000..5cd6066 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/model/InterestArticle.java @@ -0,0 +1,33 @@ +package com.api.trip.domain.interestarticle.model; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "article_id"})}) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class InterestArticle extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + private Article article; + + @Builder + private InterestArticle(Member member, Article article) { + this.member = member; + this.article = article; + } +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/repository/InterestArticleRepository.java b/src/main/java/com/api/trip/domain/interestarticle/repository/InterestArticleRepository.java new file mode 100644 index 0000000..04bb2ed --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/repository/InterestArticleRepository.java @@ -0,0 +1,11 @@ +package com.api.trip.domain.interestarticle.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.member.model.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InterestArticleRepository extends JpaRepository { + + InterestArticle findByMemberAndArticle(Member member, Article article); +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java b/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java new file mode 100644 index 0000000..31aa33f --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java @@ -0,0 +1,48 @@ +package com.api.trip.domain.interestarticle.service; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.article.repository.ArticleRepository; +import com.api.trip.domain.interestarticle.controller.dto.CreateInterestArticleRequest; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.interestarticle.repository.InterestArticleRepository; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class InterestArticleService { + + private final InterestArticleRepository interestArticleRepository; + private final ArticleRepository articleRepository; + private final MemberRepository memberRepository; + + public Long createInterestArticle(CreateInterestArticleRequest request, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Article article = articleRepository.findById(request.getArticleId()).orElseThrow(); + + InterestArticle interestArticle = interestArticleRepository.findByMemberAndArticle(member, article); + if (interestArticle != null) { + throw new RuntimeException("잘못된 요청입니다."); + } + + interestArticle = request.toEntity(member, article); + + return interestArticleRepository.save(interestArticle).getId(); + } + + public void deleteInterestArticle(Long interestArticleId, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + InterestArticle interestArticle = interestArticleRepository.findById(interestArticleId).orElseThrow(); + if (interestArticle.getMember() != member) { + throw new RuntimeException("삭제 권한이 없습니다."); + } + + interestArticleRepository.delete(interestArticle); + } +} From 1dae46dd1eaf69a926a96926fdd8e40714be5cc4 Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Wed, 13 Dec 2023 21:37:40 +0900 Subject: [PATCH 40/45] =?UTF-8?q?Feat:=20Article=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20likeCount(=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=88=98)=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 좋아요 또는 좋아요 취소 시 Article.likeCount를 update --- .../com/api/trip/domain/article/model/Article.java | 5 ++++- .../domain/article/repository/ArticleRepository.java | 11 +++++++++++ .../service/InterestArticleService.java | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/api/trip/domain/article/model/Article.java b/src/main/java/com/api/trip/domain/article/model/Article.java index d34e154..c81f067 100644 --- a/src/main/java/com/api/trip/domain/article/model/Article.java +++ b/src/main/java/com/api/trip/domain/article/model/Article.java @@ -28,12 +28,15 @@ public class Article extends BaseTimeEntity { private long viewCount; + private long likeCount; + @Builder - private Article(Member writer, String title, String content, long viewCount) { + private Article(Member writer, String title, String content, long viewCount, long likeCount) { this.writer = writer; this.title = title; this.content = content; this.viewCount = viewCount; + this.likeCount = likeCount; } public void modify(String title, String content) { diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java index 68c2f4d..fb3a3a0 100644 --- a/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java @@ -3,10 +3,21 @@ import com.api.trip.domain.article.model.Article; import com.api.trip.domain.member.model.Member; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; public interface ArticleRepository extends JpaRepository, ArticleRepositoryCustom { List
findAllByWriterOrderByIdDesc(Member writer); + + @Modifying + @Query("UPDATE Article a SET a.likeCount = a.likeCount + 1 WHERE a = :article") + void increaseLikeCount(@Param("article") Article article); + + @Modifying + @Query("UPDATE Article a SET a.likeCount = a.likeCount - 1 WHERE a = :article") + void decreaseLikeCount(@Param("article") Article article); } diff --git a/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java b/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java index 31aa33f..93265c1 100644 --- a/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java +++ b/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java @@ -32,6 +32,8 @@ public Long createInterestArticle(CreateInterestArticleRequest request, String e interestArticle = request.toEntity(member, article); + articleRepository.increaseLikeCount(interestArticle.getArticle()); + return interestArticleRepository.save(interestArticle).getId(); } @@ -43,6 +45,8 @@ public void deleteInterestArticle(Long interestArticleId, String email) { throw new RuntimeException("삭제 권한이 없습니다."); } + articleRepository.decreaseLikeCount(interestArticle.getArticle()); + interestArticleRepository.delete(interestArticle); } } From 2e86a15fbf7b191ed722eb35749bd9e141eb8b5b Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Wed, 13 Dec 2023 21:43:14 +0900 Subject: [PATCH 41/45] =?UTF-8?q?Feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20API?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EA=B0=92=EC=97=90=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=88=98=20=EB=98=90=EB=8A=94=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 3 ++- .../controller/dto/GetArticlesResponse.java | 2 ++ .../controller/dto/ReadArticleResponse.java | 5 ++++- .../domain/article/service/ArticleService.java | 14 ++++++++++++-- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java index b18b4c9..0bcb45d 100644 --- a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java +++ b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java @@ -38,7 +38,8 @@ public ResponseEntity deleteArticle(@PathVariable Long articleId) { @GetMapping("/{articleId}") public ResponseEntity readArticle(@PathVariable Long articleId) { - return ResponseEntity.ok(articleService.readArticle(articleId)); + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(articleService.readArticle(articleId, email)); } @GetMapping diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java index f1262af..c8f9f73 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java @@ -46,6 +46,7 @@ private static class ArticleDto { private String writerNickname; private String writerRole; private LocalDateTime createdAt; + private long likeCount; private static ArticleDto of(Article article) { Member writer = article.getWriter(); @@ -56,6 +57,7 @@ private static ArticleDto of(Article article) { .writerNickname(writer.getNickname()) .writerRole(writer.getRole().name()) .createdAt(article.getCreatedAt()) + .likeCount(article.getLikeCount()) .build(); } } diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java index 343183a..f3ae41f 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java @@ -1,6 +1,7 @@ package com.api.trip.domain.article.controller.dto; import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.interestarticle.model.InterestArticle; import com.api.trip.domain.member.model.Member; import lombok.Builder; import lombok.Getter; @@ -19,8 +20,9 @@ public class ReadArticleResponse { private String content; private long viewCount; private LocalDateTime createdAt; + private Long interestArticleId; - public static ReadArticleResponse of(Article article) { + public static ReadArticleResponse of(Article article, InterestArticle interestArticle) { Member writer = article.getWriter(); return builder() .articleId(article.getId()) @@ -31,6 +33,7 @@ public static ReadArticleResponse of(Article article) { .content(article.getContent()) .viewCount(article.getViewCount()) .createdAt(article.getCreatedAt()) + .interestArticleId(interestArticle != null ? interestArticle.getId() : null) .build(); } } diff --git a/src/main/java/com/api/trip/domain/article/service/ArticleService.java b/src/main/java/com/api/trip/domain/article/service/ArticleService.java index cbd32e7..b910c66 100644 --- a/src/main/java/com/api/trip/domain/article/service/ArticleService.java +++ b/src/main/java/com/api/trip/domain/article/service/ArticleService.java @@ -3,6 +3,8 @@ import com.api.trip.domain.article.controller.dto.*; import com.api.trip.domain.article.model.Article; import com.api.trip.domain.article.repository.ArticleRepository; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.interestarticle.repository.InterestArticleRepository; import com.api.trip.domain.member.model.Member; import com.api.trip.domain.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; @@ -12,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Objects; @Service @Transactional @@ -20,6 +23,7 @@ public class ArticleService { private final ArticleRepository articleRepository; private final MemberRepository memberRepository; + private final InterestArticleRepository interestArticleRepository; public Long createArticle(CreateArticleRequest request, String email) { Member member = memberRepository.findByEmail(email).orElseThrow(); @@ -51,12 +55,18 @@ public void deleteArticle(Long articleId, String email) { articleRepository.delete(article); } - public ReadArticleResponse readArticle(Long articleId) { + public ReadArticleResponse readArticle(Long articleId, String email) { Article article = articleRepository.findArticle(articleId).orElseThrow(); article.increaseViewCount(); - return ReadArticleResponse.of(article); + InterestArticle interestArticle = null; + if (!Objects.equals(email, "anonymousUser")) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + interestArticle = interestArticleRepository.findByMemberAndArticle(member, article); + } + + return ReadArticleResponse.of(article, interestArticle); } @Transactional(readOnly = true) From 776a568a3cb9861fdc6d0e2fae9f5008d678419f Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Wed, 13 Dec 2023 21:48:21 +0900 Subject: [PATCH 42/45] =?UTF-8?q?Fix:=20=EB=8F=99=EC=8B=9C=EC=84=B1=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=EB=A5=BC=20=EA=B3=A0=EB=A0=A4=ED=95=B4=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A1=B0=ED=9A=8C=EC=88=98=20?= =?UTF-8?q?=EC=A6=9D=EA=B0=80=20=EB=A1=9C=EC=A7=81=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/trip/domain/article/model/Article.java | 4 ---- .../api/trip/domain/article/repository/ArticleRepository.java | 4 ++++ .../com/api/trip/domain/article/service/ArticleService.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/api/trip/domain/article/model/Article.java b/src/main/java/com/api/trip/domain/article/model/Article.java index c81f067..c017dd9 100644 --- a/src/main/java/com/api/trip/domain/article/model/Article.java +++ b/src/main/java/com/api/trip/domain/article/model/Article.java @@ -43,8 +43,4 @@ public void modify(String title, String content) { this.title = title; this.content = content; } - - public void increaseViewCount() { - this.viewCount++; - } } diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java index fb3a3a0..5dbb977 100644 --- a/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java @@ -13,6 +13,10 @@ public interface ArticleRepository extends JpaRepository, Article List
findAllByWriterOrderByIdDesc(Member writer); + @Modifying + @Query("UPDATE Article a SET a.viewCount = a.viewCount + 1 WHERE a = :article") + void increaseViewCount(@Param("article") Article article); + @Modifying @Query("UPDATE Article a SET a.likeCount = a.likeCount + 1 WHERE a = :article") void increaseLikeCount(@Param("article") Article article); diff --git a/src/main/java/com/api/trip/domain/article/service/ArticleService.java b/src/main/java/com/api/trip/domain/article/service/ArticleService.java index b910c66..24509d8 100644 --- a/src/main/java/com/api/trip/domain/article/service/ArticleService.java +++ b/src/main/java/com/api/trip/domain/article/service/ArticleService.java @@ -58,7 +58,7 @@ public void deleteArticle(Long articleId, String email) { public ReadArticleResponse readArticle(Long articleId, String email) { Article article = articleRepository.findArticle(articleId).orElseThrow(); - article.increaseViewCount(); + articleRepository.increaseViewCount(article); InterestArticle interestArticle = null; if (!Objects.equals(email, "anonymousUser")) { From ae55c333678da8f524d0d3dd5d5880be096df0b3 Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Wed, 13 Dec 2023 21:52:35 +0900 Subject: [PATCH 43/45] =?UTF-8?q?Feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=EC=88=9C=20=EC=A0=95=EB=A0=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 동적 정렬 로직 (ArticleRepositoryCustomImpl.getOrderSpecifiers) 수정 --- .../repository/ArticleRepositoryCustomImpl.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java index c6b8fc5..aff3711 100644 --- a/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java @@ -12,6 +12,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -27,12 +28,12 @@ public ArticleRepositoryCustomImpl(EntityManager em) { } @Override - public Optional
findArticle(Long id) { + public Optional
findArticle(Long articleId) { return Optional.ofNullable( jpaQueryFactory .selectFrom(article) .innerJoin(article.writer, member).fetchJoin() - .where(article.id.eq(id)) + .where(article.id.eq(articleId)) .fetchOne() ); } @@ -43,7 +44,7 @@ public Page
findArticles(Pageable pageable, String filter) { .selectFrom(article) .innerJoin(article.writer, member).fetchJoin() .where(eqFilter(filter)) - .orderBy(getOrderSpecifier(pageable)) + .orderBy(getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); @@ -67,12 +68,14 @@ private BooleanExpression eqFilter(String filter) { return null; } - private OrderSpecifier getOrderSpecifier(Pageable pageable) { + private OrderSpecifier[] getOrderSpecifiers(Pageable pageable) { + List> orderSpecifierList = new ArrayList<>(); for (Sort.Order order : pageable.getSort()) { - if ("POPULAR".equals(order.getProperty())) { - return new OrderSpecifier<>(Order.DESC, article.viewCount); + if ("popular".equals(order.getProperty())) { + orderSpecifierList.add(new OrderSpecifier<>(Order.DESC, article.likeCount)); } } - return new OrderSpecifier<>(Order.DESC, article.id); + orderSpecifierList.add(new OrderSpecifier<>(Order.DESC, article.id)); + return orderSpecifierList.toArray(OrderSpecifier[]::new); } } From 3ee93f6427e61357bf7862077657908b9cd1fe83 Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Thu, 14 Dec 2023 00:21:22 +0900 Subject: [PATCH 44/45] =?UTF-8?q?Feat:=20=EB=88=84=EB=9D=BD=EB=90=9C=20API?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/article/controller/dto/GetArticlesResponse.java | 6 ++++-- .../domain/article/controller/dto/ReadArticleResponse.java | 6 +++++- .../domain/comment/controller/dto/GetCommentsResponse.java | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java index c8f9f73..d4c86f6 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java @@ -45,8 +45,9 @@ private static class ArticleDto { private Long writerId; private String writerNickname; private String writerRole; - private LocalDateTime createdAt; + private long viewCount; private long likeCount; + private LocalDateTime createdAt; private static ArticleDto of(Article article) { Member writer = article.getWriter(); @@ -56,8 +57,9 @@ private static ArticleDto of(Article article) { .writerId(writer.getId()) .writerNickname(writer.getNickname()) .writerRole(writer.getRole().name()) - .createdAt(article.getCreatedAt()) + .viewCount(article.getViewCount()) .likeCount(article.getLikeCount()) + .createdAt(article.getCreatedAt()) .build(); } } diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java index f3ae41f..f853c14 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java @@ -17,8 +17,10 @@ public class ReadArticleResponse { private Long writerId; private String writerNickname; private String writerRole; + private String writerProfileImg; private String content; private long viewCount; + private long likeCount; private LocalDateTime createdAt; private Long interestArticleId; @@ -29,9 +31,11 @@ public static ReadArticleResponse of(Article article, InterestArticle interestAr .title(article.getTitle()) .writerId(writer.getId()) .writerNickname(writer.getNickname()) + .writerProfileImg(writer.getProfileImg()) .writerRole(writer.getRole().name()) .content(article.getContent()) - .viewCount(article.getViewCount()) + .viewCount(article.getViewCount() + 1) + .likeCount(article.getLikeCount()) .createdAt(article.getCreatedAt()) .interestArticleId(interestArticle != null ? interestArticle.getId() : null) .build(); diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java b/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java index 95f86d4..d615b8d 100644 --- a/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java @@ -45,6 +45,7 @@ private static class CommentDto { private Long commentId; private Long writerId; private String writerNickname; + private String writerProfileImg; private Long articleId; private String content; private Long parentId; @@ -58,6 +59,7 @@ private static CommentDto of(Comment comment) { .commentId(comment.getId()) .writerId(comment.getWriter().getId()) .writerNickname(comment.getWriter().getNickname()) + .writerProfileImg(comment.getWriter().getProfileImg()) .articleId(comment.getArticle().getId()) .content(comment.getContent()) .parentId(comment.getParent() != null ? comment.getParent().getId() : null) From d6e0ed7611bde349dc9c2d2403c0143bdefb3934 Mon Sep 17 00:00:00 2001 From: seungnYong <77851079+gkfktkrh153@users.noreply.github.com> Date: Thu, 14 Dec 2023 18:05:48 +0900 Subject: [PATCH 45/45] Update work.yml --- .github/workflows/work.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/work.yml b/.github/workflows/work.yml index 78eefc3..8a4f79e 100644 --- a/.github/workflows/work.yml +++ b/.github/workflows/work.yml @@ -71,4 +71,4 @@ jobs: docker rm trip_app || true docker pull ${{ secrets.DOCKER_REPO }}/trip docker run --name=trip_app --restart unless-stopped \ - -p 80:8080 -e TZ=Asia/Seoul -d ${{ secrets.DOCKER_REPO }}/trip + -p 8080:8080 -e TZ=Asia/Seoul -d ${{ secrets.DOCKER_REPO }}/trip