diff --git a/.DS_Store b/.DS_Store index 281e3db..e34aeb9 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 4a2e44e..7fb9fca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ +# General HELP.md .gradle build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ +.vscode/ +*.yml + ### STS ### .apt_generated @@ -33,6 +34,4 @@ out/ /nbdist/ /.nb-gradle/ -### VS Code ### -.vscode/ -*.yml \ No newline at end of file +/src/main/resources/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..93ea0ef --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM openjdk:17-jdk-slim + +# 작업 디렉토리 설정 +WORKDIR /usr/src/app + +# Spring Boot JAR 파일을 이미지에 복사 +COPY ./build/libs/*.jar app.jar + +# 환경 변수 설정 +ENV SPRING_PROFILES_ACTIVE=deploy + +# Spring Boot 애플리케이션 실행 +CMD ["java", "-jar", "app.jar"] diff --git a/README.md b/README.md index 3344319..1d0071a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,61 @@ -# Rremoa +# Remoa -remoa BE +image -# description here -- + +## ❓배경 +image +image + + + +## 👥 대상 +수상에 실패한 사람이나, 수상하였어도 피드백을 원하는 사람들 + + + +## 📖 프로젝트 소개 +image + + + +## ⌨️ 기술 스택 +### Environment +![Visual Studio Code](https://img.shields.io/badge/Visual%20Studio%20Code-0078d7.svg?style=for-the-badge&logo=visual-studio-code&logoColor=white) +![IntelliJ IDEA](https://img.shields.io/badge/IntelliJIDEA-000000.svg?style=for-the-badge&logo=intellij-idea&logoColor=white) + + + + + +### 💫 Front-end + + + +### 💫 Back-end + + + + +## 💻 화면 구성 + + + + + + + + + + + + + + +
메인 페이지
상세 피드백 뷰어
imageimage
내 작업 관리
마이 페이지
imageimage +
+ + + +## 〰 UI/UX +[Figma](https://www.figma.com/file/afTvihibzwDCoa5oJZBJE1/%EB%A0%88%EB%AA%A8%EC%95%84_GUI_230111?node-id=0-1&t=U9zbbPGCBEqYvbmx-0) diff --git a/build.gradle b/build.gradle index 2ff8e85..bda1d11 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,15 @@ plugins { id 'java' - id 'org.springframework.boot' version '2.7.7' - id 'io.spring.dependency-management' version '1.0.15.RELEASE' + id 'org.springframework.boot' version '3.2.3' + id 'io.spring.dependency-management' version '1.1.4' } -group = 'Remoa' +group = 'toward' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '1.8' + +java { + sourceCompatibility = '17' +} configurations { compileOnly { @@ -21,24 +24,78 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' - //json simple - implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1' + //SQL파라미터 + implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' + + //타임리프 + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' + + //jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' + + //swagger +// implementation 'io.springfox:springfox-boot-starter:3.0.0' 스프링 3 자바 17과 맞지 않음 +// implementation 'io.springfox:springfox-swagger-ui:3.0.0' + + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + + //java-jwt + implementation 'com.auth0:java-jwt:4.4.0' // S3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' - // h2 db 추가 - implementation "com.h2database:h2" + //modelmapper + implementation 'org.modelmapper:modelmapper:3.1.0' + + // Querydsl 추가 + 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" + + //pdf 처리 + implementation 'org.apache.pdfbox:pdfbox:2.0.29' + + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-core', version: '3.8.2' + + //json simple + implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-cache:3.1.2' + + // 직렬화 관련 + implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2' + implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.2" + + //ReadFrom + implementation 'io.lettuce:lettuce-core:6.2.6.RELEASE' + + // @ConfigurationProperties + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:3.1.2" + // aop 관련 + implementation 'org.springframework.boot:spring-boot-starter-aop:2.3.1.RELEASE' + implementation 'org.aspectj:aspectjweaver' } tasks.named('test') { useJUnitPlatform() -} \ No newline at end of file +} + +jar { + enabled = false +} diff --git a/build/libs/BE-ver_230228.jar b/build/libs/BE-ver_230228.jar deleted file mode 100644 index cc38cb3..0000000 Binary files a/build/libs/BE-ver_230228.jar and /dev/null differ diff --git a/build/libs/BE-ver_230301.jar b/build/libs/BE-ver_230301.jar deleted file mode 100644 index 980901d..0000000 Binary files a/build/libs/BE-ver_230301.jar and /dev/null differ diff --git a/src/.DS_Store b/src/.DS_Store index 65a6baf..9ac4b93 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/main/.DS_Store b/src/main/.DS_Store index 9e7cfed..f0c5aa3 100644 Binary files a/src/main/.DS_Store and b/src/main/.DS_Store differ diff --git a/src/main/java/Remoa/BE/BeApplication.java b/src/main/java/Remoa/BE/BeApplication.java index 7083f99..de5d397 100644 --- a/src/main/java/Remoa/BE/BeApplication.java +++ b/src/main/java/Remoa/BE/BeApplication.java @@ -5,7 +5,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.PropertySource; -import javax.annotation.PostConstruct; + +import jakarta.annotation.PostConstruct; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -14,23 +15,15 @@ @SpringBootApplication public class BeApplication { - @Value("${uploadFolder}") - private String uploadFolder; + static { + System.setProperty("com.amazonaws.sdk.disableEc2Metadata", "true"); + } + public static void main(String[] args) { SpringApplication.run(BeApplication.class, args); } - @PostConstruct - public void createUploadFolder() { - Path upload = Paths.get(uploadFolder); - try { - if (!Files.exists(upload)) { - Files.createDirectory(upload); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } + +//깃허브 테스트 diff --git a/src/main/java/Remoa/BE/Member/Controller/FollowController.java b/src/main/java/Remoa/BE/Member/Controller/FollowController.java deleted file mode 100644 index 9e20bdd..0000000 --- a/src/main/java/Remoa/BE/Member/Controller/FollowController.java +++ /dev/null @@ -1,88 +0,0 @@ -package Remoa.BE.Member.Controller; - -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Member.Service.FollowService; -import Remoa.BE.Member.Service.MemberService; -import Remoa.BE.exception.CustomMessage; -import Remoa.BE.exception.response.ErrorResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.*; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -import static Remoa.BE.exception.CustomBody.errorResponse; -import static Remoa.BE.exception.CustomBody.successResponse; -import static Remoa.BE.utill.MemberInfo.authorized; -import static Remoa.BE.utill.MemberInfo.getMemberId; - -@RestController -@Slf4j -@RequiredArgsConstructor -@CrossOrigin(origins = "*") -public class FollowController { - - private final FollowService followService; - private final MemberService memberService; - - /** - * Session에서 로그인한 사용자(Follow를 거는 사용자)의 Member와 Follow 받는 대상의 memberId를 통해 - * Follow가 이미 되었는지 확인하고, 안 되어 있으면 Follow, 되어있다면 Unfollow를 할 수 있도록 기능합니다. - * @param memberId - * @param request - * @return ResponseEntity - */ - @PostMapping("/follow/{member_id}") - public ResponseEntity follow(@PathVariable("member_id") Long memberId, HttpServletRequest request) { - - if(authorized(request)){ - //나 자신을 팔로우 하는 경우 - Long myMemberId = getMemberId(); - if(Objects.equals(memberId, myMemberId)){ - - return errorResponse(CustomMessage.FOLLOW_ME); - } - else{ - Member member = memberService.findOne(myMemberId); - boolean check = followService.followFunction(memberId, member); - //팔로우 - if(check){ - return successResponse(CustomMessage.OK_FOLLOW,followService.showFollowId(member)); - } - //언팔로우 - else{ - return successResponse(CustomMessage.OK_UNFOLLOW,followService.showFollowId(member)); - } - - } - - } - - return errorResponse(CustomMessage.UNAUTHORIZED); - } - - /** - * Login한 Member가 Follow하는 멤버들의 Member 필드들을 List로 보여줌. - * 현재는 모든 필드들을 다 보여주지만, 이후 프론트와의 협의 후에 필요한 필드들만 가져올 수 있게 구현해야함. - * @param request - * @return ResponseEntity - */ - @GetMapping("/user/follow") - public ResponseEntity showFollowers(HttpServletRequest request) { - if(authorized(request)){ - Long myMemberId = getMemberId(); - Member member = memberService.findOne(myMemberId); - List members = followService.showFollows(member); - return successResponse(CustomMessage.OK,members); - } - return errorResponse(CustomMessage.UNAUTHORIZED); - } -} diff --git a/src/main/java/Remoa/BE/Member/Controller/KakaoController.java b/src/main/java/Remoa/BE/Member/Controller/KakaoController.java deleted file mode 100644 index be74e54..0000000 --- a/src/main/java/Remoa/BE/Member/Controller/KakaoController.java +++ /dev/null @@ -1,153 +0,0 @@ -package Remoa.BE.Member.Controller; - -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Member.Dto.Req.ReqSignupDto; -import Remoa.BE.Member.Dto.Res.ResSignupDto; -import Remoa.BE.Member.Service.KakaoService; -import Remoa.BE.Member.Service.MemberService; -import Remoa.BE.exception.CustomMessage; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.io.IOException; -import java.util.Map; -import java.util.Optional; - -import static Remoa.BE.exception.CustomBody.*; -import static Remoa.BE.utill.MemberInfo.*; - - -@RestController -@Slf4j -@RequiredArgsConstructor -@CrossOrigin(origins = "*") // 프론트에서 추가 요청 -public class KakaoController { - - private final KakaoService ks; - private final MemberService memberService; - private final HttpSession httpSession; - - /** - * 카카오 로그인을 통해 code를 query string으로 받아오면, 코드를 통해 토큰, 토큰을 통해 사용자 정보를 얻어와 db에 해당 사용자가 존재하는지 여부를 - * 파악해 존재할 때는 로그인, 없을 땐 회원가입 페이지로 넘어가게 해줌. - */ - @GetMapping("/login/kakao") - public ResponseEntity getCI(@RequestParam String code, HttpServletRequest request) throws IOException { - log.info("code = " + code); - - // 액세스 토큰과 유저정보 받기 - String access_token = ks.getToken(code); - Map userInfo = ks.getUserInfo(access_token); - - log.info("userInfo = {}", userInfo.values()); - - Long kakaoId = Long.parseLong((String) userInfo.get("id")); - Optional member = memberService.findByKakaoId(kakaoId); - - log.info("kakaoId = {}", kakaoId); - - /* - * 백에서 보낼 게 없을 때에도 정상 처리됐다는 메세지 정도는 같이 보내주는 게 좋습니다. - * 상태메세지는 body에, 상태코드는 head에 들어갑니다. - * 에러메세지를 exception 패키지처럼 한곳에 모아놓고 쓰는 것도 좋습니다. - */ - if (member.isPresent()) { - securityLoginWithoutLoginForm(member.get(), request); - //if문에 걸리지 않았다면 이미 회원가입이 진행돼 db에 kakaoId가 있는 유저이므로 kakaoMember가 존재하므로 LoginController처럼 로그인 처리 하면 됩니다. - return successResponse(CustomMessage.OK, userInfo); - - } else { - //kakaoId가 db에 없으므로 kakaoMember가 null이므로 회원가입하지 않은 회원. 따라서 회원가입이 필요하므로 회원가입하는 uri로 redirect 시켜주어야 함. - return successResponse(CustomMessage.OK_SIGNUP, userInfo); - } - } - - /** - * 카카오 로그인을 우회해 테스트 하기 위한 용도로 추가됨. - * @param kakaoId - * @return ResponseEntity - */ - @PostMapping("/login/kakao/test") - public ResponseEntity testLogin(@RequestBody Integer kakaoId, HttpServletRequest request) { - log.warn("kakaoId = {}", kakaoId); - Optional findMember = memberService.findByKakaoId(Long.valueOf(kakaoId)); - if (findMember.isPresent()) { - Member member = findMember.get(); - securityLoginWithoutLoginForm(member, request); - return successResponse(CustomMessage.OK, member); - } - return failResponse(CustomMessage.VALIDATED, "User Not Exist"); - } - - /** - * front-end에서 회원가입에 필요한 정보를 넘겨주면 KakaoSignupForm으로 받아 회원가입을 진행시켜줌 - */ - @PostMapping("/signup/kakao") - public ResponseEntity signupKakaoMember(@RequestBody @Validated ReqSignupDto form, HttpServletRequest request) { - - Member member = new Member(); - member.setKakaoId(form.getKakaoId()); - member.setEmail(form.getEmail()); - member.setNickname(form.getNickname()); - member.setProfileImage(form.getProfileImage()); - member.setTermConsent(form.getTermConsent()); - - memberService.join(member); - securityLoginWithoutLoginForm(member, request); - - ResSignupDto result = ResSignupDto.builder(). - kakaoId(member.getKakaoId()). - email(member.getEmail()). - nickname(member.getNickname()). - profileImage(member.getProfileImage()). - termConsent(member.getTermConsent()). - build(); - - return successResponse(CustomMessage.OK,result); - } - - /** - *자동 로그인 추후에 - */ -/* - @GetMapping("/login") - public ResponseEntity autoLogin(){ - Long kaKaoId = getKaKaoId(); - Optional member = memberService.findByKakaoId(kaKaoId); - if(member.isPresent()){ - return successResponse(CustomMessage.OK,member); - } - else{ - return errorResponse(CustomMessage.UNAUTHORIZED); - } - - } -*/ - - /** - * 로그아웃 기능 - 세션무효화, jsession쿠키를 제거, - */ - @PostMapping("/user/logout") - public ResponseEntity logout(HttpServletResponse response){ - - SecurityContextHolder.clearContext(); // 현재 SecurityContext를 제거합니다. - httpSession.invalidate(); // HttpSession을 무효화합니다. - - Cookie myCookie = new Cookie("JSESSIONID", null); - myCookie.setMaxAge(0); // 쿠키의 expiration 타임을 0으로 하여 없앤다. - myCookie.setPath("/"); // 모든 경로에서 삭제 됬음을 알린다. - response.addCookie(myCookie); - return successResponse(CustomMessage.OK,myCookie); - } - - -} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Member/Controller/ProfileController.java b/src/main/java/Remoa/BE/Member/Controller/ProfileController.java deleted file mode 100644 index c8bebb4..0000000 --- a/src/main/java/Remoa/BE/Member/Controller/ProfileController.java +++ /dev/null @@ -1,85 +0,0 @@ -package Remoa.BE.Member.Controller; - -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Member.Dto.Req.EditProfileForm; -import Remoa.BE.Member.Service.MemberService; -import Remoa.BE.Member.Service.ProfileService; -import Remoa.BE.exception.CustomMessage; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - -import static Remoa.BE.exception.CustomBody.*; - -@Slf4j -@RestController -@RequiredArgsConstructor -@CrossOrigin(origins = "*") -public class ProfileController { - - private final ProfileService profileService; - - private final MemberService memberService; - - // 프로필 수정 범위 : 닉네임(중복확인), 핸드폰번호, 대학교, 한줄소개 - @GetMapping("/user") - public ResponseEntity userHome(HttpServletRequest request) { - - HttpSession session = request.getSession(); - // 현재 로그인한 사용자의 세션 가져오기 - Member loginMember = (Member) session.getAttribute("loginMember"); - - // 세션이 없으면 로그인 페이지로 이동 - if (loginMember == null) { - return failResponse(CustomMessage.VALIDATED, "redirect:/login/kakao"); - } - - // 로그인된 사용자의 정보를 db에서 다시 불러와 띄워줌. - Member member = memberService.findOne(loginMember.getMemberId()); - return successResponse(CustomMessage.OK, member); - } - - // RESTful API에서 PUT 매핑은 수정할 리소스를 명확하게 지정해야 하는데 이 경우에는 URL에 리소스 ID를 명시하는 것이 일반적이다. - // 그런데 우리는 수정할 사용자의 정보를 모두 입력받아 수정하는 형태이기 때문에 - // URL에 리소스 ID를 명시할 필요가 없어서 PUT대신 POST 매핑을 사용하였습니다. - @PostMapping("/user") - public ResponseEntity editProfile(@RequestBody EditProfileForm form, HttpServletRequest request) { - HttpSession session = request.getSession(); - Member loginMember = (Member) session.getAttribute("loginMember"); - - if (loginMember == null) { - // 로그인되어 있지 않은 경우 로그인 페이지로 이동 - return failResponse(CustomMessage.VALIDATED, "로그인하지 않은 회원입니다. 로 redirect"); - } - - if (memberService.isNicknameDuplicate(form.getNickname())) { - return failResponse(CustomMessage.VALIDATED, "닉네임이 중복됩니다."); - } - - // 사용자의 입력 정보를 DTO에 담아 서비스로 전달 - EditProfileForm profileInfo = new EditProfileForm( - form.getNickname(), form.getPhoneNumber(), form.getUniversity(), form.getOneLineIntroduction()); - profileService.editProfile(loginMember.getMemberId(), profileInfo); - - // 수정이 완료되면 프로필 페이지로 이동 - return successResponse(CustomMessage.OK, "redirect:/user"); - } - - /** - * 프론트에서 닉네임 중복 검사를 할 때 사용할 메서드 - * @param nickname - * @return ResponseEntity - */ - @GetMapping("/nickname") - public ResponseEntity checkNicknameDuplicate(@RequestParam String nickname) { - if (memberService.isNicknameDuplicate(nickname)) { - return successResponse(CustomMessage.OK, nickname + "은(는) 이미 사용중인 닉네임입니다."); - } else { - return successResponse(CustomMessage.OK, nickname + "은(는) 사용 가능한 닉네임입니다."); - } - } -} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Member/Controller/WithdrewController.java b/src/main/java/Remoa/BE/Member/Controller/WithdrewController.java deleted file mode 100644 index e6922b7..0000000 --- a/src/main/java/Remoa/BE/Member/Controller/WithdrewController.java +++ /dev/null @@ -1,89 +0,0 @@ -package Remoa.BE.Member.Controller; - -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Member.Service.MemberService; -import Remoa.BE.Member.Service.WithdrewService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - -@RestController -@Slf4j -@RequiredArgsConstructor -@CrossOrigin(origins = "*") -public class WithdrewController { - - private final WithdrewService withdrewService; - private final MemberService memberService; - - /** - * PathVariable을 이용한 회원 탈퇴 uri. - * 로그인 된 사용자인지, 해당 사용자의 탈퇴 요청이 맞는지 확인 후 탈퇴 처리. - * @param memberId - * @param request - * @return 로그인 되지 않은 상태면 403(forbidden), 다른 id 값을 통한 잘못된 요청을 하면 401(Unauthorized), 올바른 탈퇴 요청이면 200(OK) - */ - @DeleteMapping("/delete/{member_id}") - public ResponseEntity withdrewRemoa(@PathVariable("member_id") Long memberId, HttpServletRequest request) { - - HttpSession session = request.getSession(false); - if (session == null) { - return new ResponseEntity<>("잘못된 접근입니다.", HttpStatus.FORBIDDEN); - } - - //해당 객체로 탈퇴 수행해야 dirty checking 통한 soft delete 적용 가능. - Member member = memberService.findOne(memberId); - //PathVariable의 id와 로그인된 사용자의 id가 같은지 확인하기 위한 용도. - Member loginMember = (Member) session.getAttribute("loginMember"); - - if (loginMember.getMemberId() != memberId) { - return new ResponseEntity<>("회원정보가 일치하지 않습니다.", HttpStatus.UNAUTHORIZED); - } - - withdrewService.withdrewRemoa(member); - - return new ResponseEntity<>("회원 탈퇴가 완료되었습니다.", HttpStatus.OK); - } - - /** - * Session 정보만을 활용해서 PathVariable 없이 구현한 회원 탈퇴 uri. 로그인 된 사용자인지만 확인하면 됨. - * @param request - * @return 로그인 되지 않은 상태면 403(forbidden), 올바른 탈퇴 요청이면 200(OK) - */ - @DeleteMapping("/delete") - public ResponseEntity withdrewRemoaWithoutPathVariable(HttpServletRequest request) { - - HttpSession session = request.getSession(false); - if (session == null) { - return new ResponseEntity<>("잘못된 접근입니다.", HttpStatus.FORBIDDEN); - } - - Member member = (Member) session.getAttribute("loginMember"); - Member findMember = memberService.findOne(member.getMemberId()); - - withdrewService.withdrewRemoa(findMember); - - return new ResponseEntity<>("회원 탈퇴가 완료되었습니다.", HttpStatus.OK); - } - - /** - * 탈퇴 처리가 되어 deleted 필드가 true인 member가 조회되는지 확인하기 위한 테스트 uri. - * @param memberId - * @return 조회 결과가 없으면 418(I am a tea pot), 있으면 200(OK) - */ - @GetMapping("/find/{member_id}") - public ResponseEntity findMemberTest(@PathVariable("member_id") Long memberId) { - Member member = memberService.findOne(memberId); - - if (member == null) { - return new ResponseEntity<>("회원 정보가 없습니다.", HttpStatus.I_AM_A_TEAPOT); - } - - return new ResponseEntity<>("memberId 번호 <" + memberId + ">는 " + member.getName() + "입니다.", HttpStatus.OK); - } -} diff --git a/src/main/java/Remoa/BE/Member/Domain/Comment.java b/src/main/java/Remoa/BE/Member/Domain/Comment.java deleted file mode 100644 index d9a1e07..0000000 --- a/src/main/java/Remoa/BE/Member/Domain/Comment.java +++ /dev/null @@ -1,56 +0,0 @@ -package Remoa.BE.Member.Domain; - -import Remoa.BE.Post.Domain.Post; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; - -import javax.persistence.*; - -import static javax.persistence.FetchType.LAZY; - -@Entity -@Getter -@Setter -@Where(clause = "deleted = false") -public class Comment { - - @Id - @GeneratedValue - @Column(name = "comment_id") - private Long commentId; - - /** - * Comment가 속해있는 Post - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; - - /** - * Comment를 작성한 Member - */ - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "member_id") - private Member member; - - /** - * Comment의 내용 - */ - private String comment; - - /** - * Comment가 작성된 시간 - */ - @Column(name = "commented_time") - private String commentedTime; - - /** - * Comment의 좋아요 숫자 - */ - @Column(name = "comment_like_count") - private Integer commentLikeCount; - - private Boolean deleted = Boolean.FALSE; -} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Member/Domain/Feedback.java b/src/main/java/Remoa/BE/Member/Domain/Feedback.java deleted file mode 100644 index 1bcb965..0000000 --- a/src/main/java/Remoa/BE/Member/Domain/Feedback.java +++ /dev/null @@ -1,58 +0,0 @@ -package Remoa.BE.Member.Domain; - -import Remoa.BE.Post.Domain.Post; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.Where; - -import javax.persistence.*; - -import static javax.persistence.FetchType.LAZY; - -@Entity -@Getter -@Setter -@Where(clause = "deleted = false") -public class Feedback { - - @Id - @GeneratedValue - @Column(name = "feedback_id") - private Long feedbackId; - - /** - * Feedback이 속해있는 Post - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; - - /** - * Feedback을 작성한 Member - */ - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "member_id") - private Member member; - - @Column(name = "page_number") - private Integer pageNumber; - - /** - * Feedback의 내용 - */ - private String feedback; - - /** - * Feedback이 작성된 시간 - */ - @Column(name = "feedback_time") - private String feedbackTime; - - /** - * Feedback의 좋아요 숫자 - */ - @Column(name = "feedback_like_count") - private Integer feedbackLikeCount; - - private Boolean deleted = Boolean.FALSE; -} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Member/Dto/Req/ReqSignupDto.java b/src/main/java/Remoa/BE/Member/Dto/Req/ReqSignupDto.java deleted file mode 100644 index d7dfbfe..0000000 --- a/src/main/java/Remoa/BE/Member/Dto/Req/ReqSignupDto.java +++ /dev/null @@ -1,26 +0,0 @@ -package Remoa.BE.Member.Dto.Req; - -import lombok.Getter; -import lombok.Setter; - -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; - -@Getter -@Setter -public class ReqSignupDto { - - @NotBlank(message = "이메일은 필수값입니다.") - private String email; - - @NotNull(message = "카카오에서 발급받은 id값이 누락되었습니다.") - private Long kakaoId; - - @NotBlank(message = "이름은 필수값입니다.") - private String nickname; - - private String profileImage; - - @NotNull(message = "선택 동의사항 값은 필수입니다.") - private Boolean termConsent; -} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Member/Dto/Res/ResSignupDto.java b/src/main/java/Remoa/BE/Member/Dto/Res/ResSignupDto.java deleted file mode 100644 index b4b390c..0000000 --- a/src/main/java/Remoa/BE/Member/Dto/Res/ResSignupDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package Remoa.BE.Member.Dto.Res; - -import lombok.Builder; -import lombok.Getter; - -import javax.validation.constraints.NotBlank; - -@Builder -@Getter -public class ResSignupDto { - - private Long kakaoId; - private String email; - - private String nickname; - - private String profileImage; - - private boolean termConsent; -} diff --git a/src/main/java/Remoa/BE/Member/Service/FollowService.java b/src/main/java/Remoa/BE/Member/Service/FollowService.java deleted file mode 100644 index a90ae23..0000000 --- a/src/main/java/Remoa/BE/Member/Service/FollowService.java +++ /dev/null @@ -1,51 +0,0 @@ -package Remoa.BE.Member.Service; - -import Remoa.BE.Member.Domain.Follow; -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Member.Repository.MemberRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Optional; - -@Service -@Slf4j -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class FollowService { - - private final MemberRepository memberRepository; - private final MemberService memberService; - - //false 언팔 true 팔로우 - @Transactional - public boolean followFunction(Long toMemberId, Member fromMember) { - - Member toMember = memberService.findOne(toMemberId); - - if (memberRepository.isFollow(fromMember, toMember)) { - memberRepository.unfollowByFollowId(fromMember,toMember); - return false; - } - - Follow follow = new Follow(); - follow.setFromMember(fromMember); - follow.setToMember(toMember); - - memberRepository.follow(follow); - - return true; - } - - - public List showFollows(Member fromMember) { - return memberRepository.loadFollows(fromMember); - } - - public List showFollowId(Member fromMember){ - return memberRepository.loadFollowsId(fromMember); - } -} diff --git a/src/main/java/Remoa/BE/Member/Service/KakaoService.java b/src/main/java/Remoa/BE/Member/Service/KakaoService.java deleted file mode 100644 index 79a2ed0..0000000 --- a/src/main/java/Remoa/BE/Member/Service/KakaoService.java +++ /dev/null @@ -1,213 +0,0 @@ -package Remoa.BE.Member.Service; - -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Member.Repository.MemberRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; -import org.springframework.stereotype.Service; - -import java.io.*; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.ProtocolException; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; - -@Service -@Slf4j -@RequiredArgsConstructor -public class KakaoService { - - //카카오 로그인시 접속해야 할 링크 : https://kauth.kakao.com/oauth/authorize?client_id=139febf9e13da4d124d1c1faafcf3f86&redirect_uri=http://localhost:8080/login/kakao&response_type=code - - private final MemberRepository MemberRepository; - - /** - * 카카오 인증 서버에 code를 보내고 token을 발급받는 메서드 - * @param code - * @return token - * @throws IOException - */ - public String getToken(String code) throws IOException { - //토큰을 받아올 카카오 인증 서버. 레모아 서버가 클리아언트로, 카카오 인증 서버가 서버로 동작한다고 보면 됩니다. - String host = "https://kauth.kakao.com/oauth/token"; - //카카오 인증 서버와 통신하기 위한 설정 - URL url = new URL(host); - HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); - String token = ""; - - try { - // OutputStream으로 POST 데이터를 넘겨주겠다는 옵션 - urlConnection.setRequestMethod("POST"); - urlConnection.setDoOutput(true); - - //x-www-form-urlencoded 타입으로 Body에 담아 카카오 인증 서버에 Post로 요청하기 위한 버퍼 스트림 생성 - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(urlConnection.getOutputStream())); - StringBuilder sb = new StringBuilder(); - sb.append("grant_type=authorization_code"); - sb.append("&client_id=139febf9e13da4d124d1c1faafcf3f86"); - - // 02.26. 프론트와 연동하는데 여기 3000으로 바꿔달라고 하셔서 바꿔놓았습니다 -광휘 - sb.append("&redirect_uri=http://localhost:3000/login/kakao"); - sb.append("&code=" + code); - sb.append("&client_secret=5IueqXws75WoH1e3gCSI2aNxQgOGMdBG"); - - bw.write(sb.toString()); - - //write 되어 버퍼에 있던 데이터를 flush를 통해 출력 스트림으로 출력하고, 카카오 인증 서버에 요청 전송 - bw.flush(); - - //========카카오 인증 서버에 요청 후 응답 받음========// - - //카카오 인증 서버에서 응답으로 받은 response code 값 - int responseCode = urlConnection.getResponseCode(); - log.debug("responseCode = {}", responseCode); - - //카카오 인증 서버에서 받은 응답을 받기 위한 버퍼 스트림 생성(참고-요청과는 달리 JSON 데이터를 보내줌) - BufferedReader br = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); - String line = ""; - String result = ""; - //다양한 형식(한 줄 이상의 JSON 데이터)를 받기 위한 작업 - while ((line = br.readLine()) != null) { - result += line; - } - - //JSON parsing - JSONParser parser = new JSONParser(); - JSONObject elem = (JSONObject) parser.parse(result); - - //access 토큰값 -> 카카오 api 서버에 사용자 정보를 받아오기 위해서 사용될 토큰 - String access_token = elem.get("access_token").toString(); - //refresh 토큰은 현재 서비스 구조상 카카오 api 서버에서 사용자 정보만 가져오면 되므로 필요하지 않음 -// String refresh_token = elem.get("refresh_token").toString(); - - token = access_token; - - //버퍼스트림 닫기 - br.close(); - bw.close(); - } catch (IOException | ParseException e) { - e.printStackTrace(); - } - - - return token; - } - - /** - * 카카오 api 서버에 token을 보내고 사용자 정보를 발급받는 메서드 - * @param access_token - * @return 카카오 사용자 정보((kakao)id, nickname, email, profileImage) - * @throws IOException - */ - public Map getUserInfo(String access_token) throws IOException { - //사용자 정보를 받아올 카카오 api 서버. 레모아 서버가 클리아언트로, 카카오 api 서버가 서버로 동작한다고 보면 됩니다. - String host = "https://kapi.kakao.com/v2/user/me"; - //사용자 정보를 받을 Map 객체 생성 - Map result = new HashMap<>(); - try { - URL url = new URL(host); - HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); - //Request Header에 토큰 인증 관련 값을 설정하고, GET을 통해 카카오 api 서버에 요청한다는 옵션 - urlConnection.setRequestProperty("Authorization", "Bearer " + access_token); - urlConnection.setRequestMethod("GET"); - - //========카카오 api 서버에 요청 후 응답 받음========// - - int responseCode = urlConnection.getResponseCode(); - log.debug("responseCode = {}", responseCode); - - BufferedReader br = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); - String line = ""; - String res = ""; - //다양한 형식(한 줄 이상의 JSON 데이터)를 받기 위한 작업 - while ((line=br.readLine()) != null) - { - res+=line; - } - - //JSON parsing - JSONParser parser = new JSONParser(); - JSONObject obj = (JSONObject) parser.parse(res); - JSONObject properties = (JSONObject) obj.get("properties"); - - String id = obj.get("id").toString(); - String nickname = properties.get("nickname").toString(); - - String profileImage = properties.get("profile_image").toString(); - JSONObject kakao_account = (JSONObject) obj.get("kakao_account"); - String email = kakao_account.get("email").toString(); - - result.put("id", id); - result.put("nickname", nickname); - result.put("image", profileImage); - result.put("email", email); - - br.close(); - - - } catch (IOException | ParseException e) { - e.printStackTrace(); - } - - return result; - } - - /** - * 사용자의 카카오 api 동의 내역을 확인하는 메서드. - * kakao developers 공식문서 <- 참고 - * @param access_token - * @return 사용자의 동의항목 JSON 데이터 - */ - public String getAgreementInfo(String access_token) - { - String result = ""; - String host = "https://kapi.kakao.com/v2/user/scopes"; - try{ - URL url = new URL(host); - HttpURLConnection urlConnection = (HttpURLConnection)url.openConnection(); - urlConnection.setRequestMethod("GET"); - urlConnection.setRequestProperty("Authorization", "Bearer " + access_token); - - BufferedReader br = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); - String line = ""; - while((line=br.readLine())!=null) - { - result += line; - } - - int responseCode = urlConnection.getResponseCode(); - log.debug("responseCode = {}", responseCode); - - // result는 json 포멧. - br.close(); - - } catch (MalformedURLException e) { - e.printStackTrace(); - } catch (ProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return result; - } - - /** - * kakaoId가 db에 있는지 확인해주는 메서드 - * @param kakaoId - * @return db에 존재 -> Member, 없으면 -> null - */ - public Member distinguishKakaoId(Long kakaoId) { - - if (!MemberRepository.findByKakaoId(kakaoId).isPresent()) { - return null; - } - Member kakaoMember = MemberRepository.findByKakaoId(kakaoId).get(); - - return kakaoMember; - } -} diff --git a/src/main/java/Remoa/BE/Member/Service/MemberService.java b/src/main/java/Remoa/BE/Member/Service/MemberService.java deleted file mode 100644 index d007af6..0000000 --- a/src/main/java/Remoa/BE/Member/Service/MemberService.java +++ /dev/null @@ -1,64 +0,0 @@ -package Remoa.BE.Member.Service; - -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Member.Repository.MemberRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.server.ResponseStatusException; - -import java.util.List; -import java.util.Optional; - -@Service -@Transactional(readOnly = true) -@Slf4j -@RequiredArgsConstructor -public class MemberService { - private final MemberRepository memberRepository; - private final PasswordEncoder bCryptPasswordEncoder; - - @Transactional - public Long join(Member member) { - // validateDuplicateMember(member); -// member.hashPassword(this.bCryptPasswordEncoder); - memberRepository.save(member); - return member.getMemberId(); - - } - - private void validateDuplicateMember(Member member) { - log.info("member={}", member.getEmail()); - Optional findMembers = memberRepository.findByEmail(member.getEmail()); - if (findMembers.isPresent()) { - throw new IllegalStateException("이미 존재하는 회원입니다."); - } - } - - public Boolean isNicknameDuplicate(String nickname) { - List findMembers = memberRepository.findByNickname(nickname); - if (findMembers.isEmpty()) { - return false; - } else { - return true; - } - } - - - public Member findOne(Long memberId) { - Optional member = memberRepository.findOne(memberId); - return member.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "User not found")); - } - - public Optional findByKakaoId(Long kakaoId) { - return memberRepository.findByKakaoId(kakaoId); - } - - - public Boolean isAdminExist() { - return memberRepository.findByEmail("spparta@gmail.com").isPresent(); - } -} diff --git a/src/main/java/Remoa/BE/Member/Service/ProfileService.java b/src/main/java/Remoa/BE/Member/Service/ProfileService.java deleted file mode 100644 index 5755031..0000000 --- a/src/main/java/Remoa/BE/Member/Service/ProfileService.java +++ /dev/null @@ -1,37 +0,0 @@ -package Remoa.BE.Member.Service; - -import Remoa.BE.Member.Dto.Req.EditProfileForm; -import Remoa.BE.Member.Repository.MemberRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - - -import Remoa.BE.Member.Domain.Member; - - -@Service -@Slf4j -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class ProfileService { - - private final MemberService memberService; - - - @Transactional - public void editProfile(Long memberId, EditProfileForm profile) { - Member member = memberService.findOne(memberId); - - if (member == null) { - throw new IllegalArgumentException("Invalid member id"); - } - - // 사용자의 프로필 정보 수정 - member.setNickname(profile.getNickname()); - member.setPhoneNumber(profile.getPhoneNumber()); - member.setUniversity(profile.getUniversity()); - member.setOneLineIntroduction(profile.getOneLineIntroduction()); - } -} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Post/Controller/PostController.java b/src/main/java/Remoa/BE/Post/Controller/PostController.java deleted file mode 100644 index 8d81335..0000000 --- a/src/main/java/Remoa/BE/Post/Controller/PostController.java +++ /dev/null @@ -1,77 +0,0 @@ -package Remoa.BE.Post.Controller; - -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Member.Service.MemberService; -import Remoa.BE.Post.Domain.Post; -import Remoa.BE.Post.Service.FileService; -import Remoa.BE.Post.Service.PostService; -import Remoa.BE.Post.form.Request.UploadPostForm; -import Remoa.BE.exception.CustomBody; -import Remoa.BE.exception.CustomMessage; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; -import java.io.IOException; -import java.util.List; - -import static Remoa.BE.exception.CustomBody.errorResponse; -import static Remoa.BE.exception.CustomBody.successResponse; -import static Remoa.BE.utill.MemberInfo.authorized; -import static Remoa.BE.utill.MemberInfo.getMemberId; - -@Slf4j -@RestController -@RequiredArgsConstructor -@CrossOrigin(origins = "*") -public class PostController { - - private final FileService fileService; - - private final PostService postService; - - private final MemberService memberService; - - //Todo 게시글 작성중 파일 업로드만 작성 - @PostMapping("/post") - public void posting(Post post, @RequestParam("files") List multipartFile){ - fileService.saveUploadFiles(post,multipartFile); - } - - /** - * @param fileId file PK - * @return file이 저장된 url 반환 - */ - @GetMapping("/post/file/{fileId}/url") - public String getFileUrl(@PathVariable("fileId") Long fileId ){ - return fileService.getUrl(fileId); - } - - /** - * @param fileId file PK - * @return file을 바로 다운로드 할 수 있다 - */ - @GetMapping("/post/file/{fileId}") - public ResponseEntity getFileDownload(@PathVariable("fileId") Long fileId ) throws IOException { - return fileService.getObject(fileId); - } - - @PostMapping("/reference") // 게시물 등록 - public ResponseEntity share(@RequestPart UploadPostForm uploadPostForm, - @RequestPart List uploadFiles, HttpServletRequest request){ - //TODO postingTime 설정. 로그인 여부 거르는 건 Spring Security 설정으로 가능해서 우선 없어도 괜찮을듯함. - if(authorized(request)){ - Long memberId = getMemberId(); - Member myMember = memberService.findOne(memberId); - postService.registPost(uploadPostForm,uploadFiles,myMember); - return successResponse(CustomMessage.OK,myMember); - } - - return errorResponse(CustomMessage.UNAUTHORIZED); - } - -} diff --git a/src/main/java/Remoa/BE/Post/Domain/Post.java b/src/main/java/Remoa/BE/Post/Domain/Post.java deleted file mode 100644 index e9753b4..0000000 --- a/src/main/java/Remoa/BE/Post/Domain/Post.java +++ /dev/null @@ -1,106 +0,0 @@ -package Remoa.BE.Post.Domain; - -import Remoa.BE.Member.Domain.Comment; -import Remoa.BE.Member.Domain.Member; -import lombok.*; -import org.hibernate.annotations.Where; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -import static javax.persistence.FetchType.LAZY; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Getter -@Setter -@Entity -@Where(clause = "deleted = false") -public class Post { - - @Id - @GeneratedValue - @Column(name = "post_id") - private Long postId; - - /** - * 해당 Post를 쓴 작성자(Member) - */ - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "member_id") - private Member member; - - /** - * Post 제목 - */ - private String title; - - /** - * 참여 공모전의 이름 - */ - @Column(name = "contest_name") - private String contestName; - - /** - * 참여한 공모전의 마감 기한 - */ - private String deadline; - - /** - * 참여한 공모전의 수상 내역 - */ - @Column(name = "contest_award") - private Boolean ContestAward; - - /** - * pm쪽에 문의해야할듯. - */ - @Column(name = "contest_aware_type") - private String contestAwareType; - - /** - * Post에 대한 좋아요 수 - */ - @Column(name = "like_count") - private Integer likeCount; - - /** - * Post가 작성된 시간 - */ - @Column(name = "posting_time") - private String postingTime; - - /** - * Post의 조회수 - */ - private Integer views = 0; - - /** - * Post에 작성되어진 Comment - */ - @OneToMany(mappedBy = "post") - private List comments = new ArrayList<>(); - - /** - * Post에서 쓰인 files - */ - @OneToMany(mappedBy = "post") - private List uploadFiles = new ArrayList<>(); - - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) - private List postScarps = new ArrayList<>(); - - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) - private List postLikes = new ArrayList<>(); - - /** - * 작성한 Post의 카테고리 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "category_id") - private Category category; - - private Boolean deleted = Boolean.FALSE; -} diff --git a/src/main/java/Remoa/BE/Post/Repository/CommentRepository.java b/src/main/java/Remoa/BE/Post/Repository/CommentRepository.java deleted file mode 100644 index cf7049d..0000000 --- a/src/main/java/Remoa/BE/Post/Repository/CommentRepository.java +++ /dev/null @@ -1,90 +0,0 @@ -package Remoa.BE.Post.Repository; - -import Remoa.BE.Member.Domain.Comment; -import Remoa.BE.Member.Domain.CommentBookmark; -import Remoa.BE.Member.Domain.CommentLike; -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Post.Domain.Post; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Repository; - -import javax.persistence.EntityManager; -import java.util.List; -import java.util.Optional; - -@Slf4j -@Repository -@RequiredArgsConstructor -public class CommentRepository { - - private final EntityManager em; - - public void saveComment(Comment comment) { - em.persist(comment); - } - - public Comment findByCommentId(Long commentId) { - return em.find(Comment.class, commentId); - } - - /** - * 포스트 별 댓글을 찾아오기 위한 메서드 - * @param post - * @return List - */ - public List findByPost(Post post) { - return em.createQuery("select c from Comment c where c.post = :post", Comment.class) - .setParameter("post", post) - .getResultList(); - } - - public void saveCommentLike(CommentLike commentLike) { - em.persist(commentLike); - } - - /** - * commentLikeAction 메서드를 실행하기 전 이미 해당 댓글에 대한 좋아요를 했는지 검증->service 단에서 return 값의 null이면 좋아요 가능. - * 혹은 좋아요 취소를 위해 사용할 수도 있다. - */ - public Optional findMemberCommendLike(Member member, Comment comment) { - return em.createQuery("select cl from CommentLike cl " + - "where cl.comment = :comment and cl.member = :member", CommentLike.class) - .setParameter("comment", comment) - .setParameter("member", member) - .getResultStream() - .findAny(); - } - - public Integer findCommentLike(Comment comment) { - return em.createQuery("select cl from CommentLike cl where cl.comment = :comment", CommentLike.class) - .setParameter("comment", comment) - .getResultList() - .size(); - } - - public void saveCommentBookmark(CommentBookmark commentBookmark) { - em.persist(commentBookmark); - } - - /** - * commentBookmarkAction 메서드를 실행하기 전 이미 해당 댓글에 대한 북마크를 했는지 검증->service 단에서 return 값의 null이면 북마크 가능. - * 혹은 북마크 해제를 위해 사용할 수도 있다. - */ - public Optional findMemberCommendBookmark(Member member, Comment comment) { - return em.createQuery("select cb from CommentBookmark cb " + - "where cb.comment = :comment and cb.member = :member", CommentBookmark.class) - .setParameter("comment", comment) - .setParameter("member", member) - .getResultStream() - .findAny(); - } - - /* //필요없을 거 같아서 주석처리... - public Integer findCommentBookmark(Comment comment) { - return em.createQuery("select cb from CommentBookmark cb where cb.comment = :comment", CommentBookmark.class) - .setParameter("comment", comment) - .getResultList() - .size(); - }*/ -} diff --git a/src/main/java/Remoa/BE/Post/Repository/FeedbackRepository.java b/src/main/java/Remoa/BE/Post/Repository/FeedbackRepository.java deleted file mode 100644 index 703c21c..0000000 --- a/src/main/java/Remoa/BE/Post/Repository/FeedbackRepository.java +++ /dev/null @@ -1,90 +0,0 @@ -package Remoa.BE.Post.Repository; - -import Remoa.BE.Member.Domain.Feedback; -import Remoa.BE.Member.Domain.FeedbackBookmark; -import Remoa.BE.Member.Domain.FeedbackLike; -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Post.Domain.Post; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Repository; - -import javax.persistence.EntityManager; -import java.util.List; -import java.util.Optional; - -@Slf4j -@Repository -@RequiredArgsConstructor -public class FeedbackRepository { - - private final EntityManager em; - - public void saveFeedback(Feedback feedback) { - em.persist(feedback); - } - - public Feedback findByFeedbackId(Long feedbackId) { - return em.find(Feedback.class, feedbackId); - } - - /** - * 포스트 별 댓글을 찾아오기 위한 메서드 - * @param post - * @return List - */ - public List findByPost(Post post) { - return em.createQuery("select c from Feedback c where c.post = :post", Feedback.class) - .setParameter("post", post) - .getResultList(); - } - - public void saveFeedbackLike(FeedbackLike feedbackLike) { - em.persist(feedbackLike); - } - - /** - * feedbackLikeAction 메서드를 실행하기 전 이미 해당 댓글에 대한 좋아요를 했는지 검증->service 단에서 return 값의 null이면 좋아요 가능. - * 혹은 좋아요 취소를 위해 사용할 수도 있다. - */ - public Optional findMemberCommendLike(Member member, Feedback feedback) { - return em.createQuery("select cl from FeedbackLike cl " + - "where cl.feedback = :feedback and cl.member = :member", FeedbackLike.class) - .setParameter("feedback", feedback) - .setParameter("member", member) - .getResultStream() - .findAny(); - } - - public Integer findFeedbackLike(Feedback feedback) { - return em.createQuery("select cl from FeedbackLike cl where cl.feedback = :feedback", FeedbackLike.class) - .setParameter("feedback", feedback) - .getResultList() - .size(); - } - - public void saveFeedbackBookmark(FeedbackBookmark feedbackBookmark) { - em.persist(feedbackBookmark); - } - - /** - * feedbackBookmarkAction 메서드를 실행하기 전 이미 해당 댓글에 대한 북마크를 했는지 검증->service 단에서 return 값의 null이면 북마크 가능. - * 혹은 북마크 해제를 위해 사용할 수도 있다. - */ - public Optional findMemberCommendBookmark(Member member, Feedback feedback) { - return em.createQuery("select cb from FeedbackBookmark cb " + - "where cb.feedback = :feedback and cb.member = :member", FeedbackBookmark.class) - .setParameter("feedback", feedback) - .setParameter("member", member) - .getResultStream() - .findAny(); - } - - /* //필요없을 거 같아서 주석처리... - public Integer findFeedbackBookmark(Feedback feedback) { - return em.createQuery("select cb from FeedbackBookmark cb where cb.feedback = :feedback", FeedbackBookmark.class) - .setParameter("feedback", feedback) - .getResultList() - .size(); - }*/ -} diff --git a/src/main/java/Remoa/BE/Post/Repository/PostRepository.java b/src/main/java/Remoa/BE/Post/Repository/PostRepository.java deleted file mode 100644 index a31258a..0000000 --- a/src/main/java/Remoa/BE/Post/Repository/PostRepository.java +++ /dev/null @@ -1,68 +0,0 @@ -package Remoa.BE.Post.Repository; - -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Post.Domain.Category; -import Remoa.BE.Post.Domain.Post; -import Remoa.BE.Post.Domain.PostLike; -import Remoa.BE.Post.Domain.PostScarp; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Repository; - -import javax.persistence.EntityManager; -import java.util.List; -import java.util.Optional; - -@Slf4j -@Repository -@RequiredArgsConstructor -public class PostRepository { - - private final EntityManager em; - - public void savePost(Post post) { - em.persist(post); - } - - public Post findByPostId(Long postId) { - return em.find(Post.class, postId); - } - - public Optional findByMemberId(Member member) { - return em.createQuery("select p from Post p where p.member = :member", Post.class) - .setParameter("member", member) - .getResultStream() - .findAny(); - } - - public void savePostScrap(PostScarp postScarp) { - em.persist(postScarp); - } - - public Post findScrapedPost(Member member) { - return em.createQuery("select ps from PostScarp ps where ps.member = :member", PostScarp.class) - .setParameter("member", member) - .getResultStream() - .findAny() - .get() - .getPost(); - } - - public Post findLikedPost(Member member) { - return em.createQuery("select pl from PostLike pl where pl.member = :member", PostLike.class) - .setParameter("member", member) - .getResultStream() - .findAny() - .get() - .getPost(); - } - - public List findPostsByCategory(Category category) { - return em.createQuery("select p from Post p where p.category = :category", Post.class) - .setParameter("category", category) - .getResultList(); - } - - - -} diff --git a/src/main/java/Remoa/BE/Post/Repository/UploadFileRepository.java b/src/main/java/Remoa/BE/Post/Repository/UploadFileRepository.java deleted file mode 100644 index 1056ddb..0000000 --- a/src/main/java/Remoa/BE/Post/Repository/UploadFileRepository.java +++ /dev/null @@ -1,37 +0,0 @@ -package Remoa.BE.Post.Repository; - -import Remoa.BE.Post.Domain.Post; -import Remoa.BE.Post.Domain.UploadFile; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Repository; - -import javax.persistence.EntityManager; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -@Repository -@RequiredArgsConstructor -@Slf4j -public class UploadFileRepository { - - private final EntityManager em; - - public Optional findById(Long fileId){ - return Optional.ofNullable(em.find(UploadFile.class, fileId)); - } - - public void saveFile(UploadFile file) { - em.persist(file); - } - - public List findFilesByPost(Post post) { - return em.createQuery("select uf from UploadFile uf where uf.post = :post", UploadFile.class) - .setParameter("post", post) - .getResultList() - .stream() - .map(file -> file.getSaveFileName()) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/Remoa/BE/Post/Service/CommentService.java b/src/main/java/Remoa/BE/Post/Service/CommentService.java deleted file mode 100644 index 93744ed..0000000 --- a/src/main/java/Remoa/BE/Post/Service/CommentService.java +++ /dev/null @@ -1,48 +0,0 @@ -package Remoa.BE.Post.Service; - -import Remoa.BE.Member.Domain.Comment; -import Remoa.BE.Member.Domain.CommentBookmark; -import Remoa.BE.Member.Domain.CommentLike; -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Post.Domain.Post; -import Remoa.BE.Post.Repository.CommentRepository; -import Remoa.BE.Post.Repository.PostRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -public class CommentService { - - private final CommentRepository commentRepository; - private final PostRepository postRepository; - - @Transactional - public Long writeComment(Comment comment) { - commentRepository.saveComment(comment); - return comment.getCommentId(); - } - - public List loadCommentsByPostId(Long postId) { - Post post = postRepository.findByPostId(postId); - return commentRepository.findByPost(post); - } - - @Transactional - public Long commentLikeAction(Comment comment, Member member) { - CommentLike commentLike = CommentLike.createCommentLike(member, comment); - commentRepository.saveCommentLike(commentLike); - return commentLike.getCommentLikeId(); - } - - @Transactional - public Long commentBookmarkAction(Comment comment, Member member) { - CommentBookmark commentBookmark = CommentBookmark.createCommentBookmark(member, comment); - commentRepository.saveCommentBookmark(commentBookmark); - return commentBookmark.getCommentBookmarkId(); - } -} diff --git a/src/main/java/Remoa/BE/Post/Service/FileService.java b/src/main/java/Remoa/BE/Post/Service/FileService.java deleted file mode 100644 index 14d7553..0000000 --- a/src/main/java/Remoa/BE/Post/Service/FileService.java +++ /dev/null @@ -1,151 +0,0 @@ -package Remoa.BE.Post.Service; - -import Remoa.BE.Post.Domain.Post; -import Remoa.BE.Post.Domain.UploadFile; -import Remoa.BE.Post.Repository.PostRepository; -import Remoa.BE.Post.Repository.UploadFileRepository; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.*; -import com.amazonaws.util.IOUtils; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class FileService { - - private final AmazonS3 amazonS3; - private final UploadFileRepository uploadFileRepository; - private final PostRepository postRepository; - private final List uploadFileList; - - @Value("${cloud.aws.s3.bucket}") - private String bucket; - - /** - * - * @param post 게시글 - * @param multipartFile 해당 게시글의 파일 리스트 - * 파일들을 저장해준다 - */ - @Transactional - public void saveUploadFiles(Post post,List multipartFile){ - - multipartFile.forEach(item -> saveUploadFile(post,item)); - - post.setUploadFiles(uploadFileList); - postRepository.savePost(post); - - uploadFileList.clear(); - } - - /** - * - * @param post 게시글 - * @param multipartFile 파일 - * saveUploadFiles 에서 파일 하나씩 가져와서 s3에 넣는다 - */ - @Transactional - public void saveUploadFile(Post post, MultipartFile multipartFile){ - - //파일 타입과 사이즈 저장 - ObjectMetadata objectMetadata = new ObjectMetadata(); - objectMetadata.setContentType(multipartFile.getContentType()); - objectMetadata.setContentLength(multipartFile.getSize()); - - //파일 이름 - String originalFilename = multipartFile.getOriginalFilename(); - - //파일 이름이 비어있으면 (assert 오류 반환) - assert originalFilename != null; - //확장자 - String ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); - - //파일 이름이 겹치지 않게 - String uuid = UUID.randomUUID().toString(); - - //postId 폴더에 따로 넣어서 보관 - String s3name = uuid+"_"+originalFilename; - - try (InputStream inputStream = multipartFile.getInputStream()) { - amazonS3.putObject(new PutObjectRequest(bucket, s3name, inputStream, objectMetadata) - .withCannedAcl(CannedAccessControlList.PublicRead)); - } catch (IOException e) { - //파일을 제대로 받아오지 못했을때 - //Todo 예외처리 custom 따로 만들기 - throw new RuntimeException(e); - } - - //파일 보관 url - String storeFileUrl = amazonS3.getUrl(bucket,s3name).toString().replaceAll("\\+", "+"); - UploadFile uploadFile = new UploadFile(); - uploadFile.setPost(post); - uploadFile.setOriginalFileName(originalFilename); - uploadFile.setSaveFileName(s3name); - uploadFile.setStoreFileUrl(storeFileUrl); - uploadFile.setExtension(ext); - uploadFileList.add(uploadFile); - log.info(storeFileUrl); - uploadFileRepository.saveFile(uploadFile); - } - - public String getUrl(Long fileId){ - Optional file = uploadFileRepository.findById(fileId); - if(file.isPresent()){ - return file.get().getStoreFileUrl(); - } - else{ - //해당 파일이 없을떄 예외처리 - //Todo 예외처리 custom 따로 만들기 - throw new RuntimeException(); - } - } - - - public ResponseEntity getObject(Long fileId) throws IOException { - - Optional file = uploadFileRepository.findById(fileId); - if(file.isPresent()){ - String s3name = file.get().getSaveFileName(); - - S3Object o = amazonS3.getObject(new GetObjectRequest(bucket, s3name)); - S3ObjectInputStream objectInputStream = o.getObjectContent(); - byte[] bytes = IOUtils.toByteArray(objectInputStream); - - String fileOriginalName = file.get().getOriginalFileName(); - //encode 메서드에 두 번째 파라메터에 StandardCharsets.UTF_8만 쓰면 오류가 나서 뒤에 name을 임사방편으로 붙임. 기능상 문제는 없을듯 함 - String fileNameFix = URLEncoder.encode(fileOriginalName, StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20"); - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM); - httpHeaders.setContentLength(bytes.length); - httpHeaders.setContentDispositionFormData("attachment", fileNameFix); - - return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK); - } - else{ - //해당 파일이 없을떄 예외처리 - //Todo 예외처리 custom 따로 만들기 - throw new RuntimeException(); - } - - - } -} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Post/Service/PostService.java b/src/main/java/Remoa/BE/Post/Service/PostService.java deleted file mode 100644 index 9f962fa..0000000 --- a/src/main/java/Remoa/BE/Post/Service/PostService.java +++ /dev/null @@ -1,56 +0,0 @@ -package Remoa.BE.Post.Service; - -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Post.Domain.Category; -import Remoa.BE.Post.Domain.Post; -import Remoa.BE.Post.Domain.UploadFile; -import Remoa.BE.Post.Repository.PostRepository; -import Remoa.BE.Post.Repository.UploadFileRepository; -import Remoa.BE.Post.Repository.CategoryRepository; -import Remoa.BE.Post.form.Request.UploadPostForm; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class PostService { - - private final UploadFileRepository uploadFileRepository; - - private final PostRepository postRepository; - - private final CategoryRepository categoryRepository; - - private final List uploadFileList; - - private final FileService fileService; - - public Post dtoToEntity(UploadPostForm uploadPostForm, Member member){ // dto를 db에 저장하기 위해 entity로 변환 - - // String category를 이용해 Category 엔티티를 찾기 - Category category = categoryRepository.findByCategoryName(uploadPostForm.getCategory()); - - return Post.builder() - .title(uploadPostForm.getTitle()) - .member(member) - .contestName(uploadPostForm.getContestName()) - .category(category) - .contestAwareType(uploadPostForm.getContestAward()) - .build(); - } - - @Transactional - public void registPost(UploadPostForm uploadPostForm, List uploadFiles, Member member){ - - Post post = dtoToEntity(uploadPostForm, member); - postRepository.savePost(post); - fileService.saveUploadFiles(post, uploadFiles); - } - -} diff --git a/src/main/java/Remoa/BE/Post/form/Request/UploadPostForm.java b/src/main/java/Remoa/BE/Post/form/Request/UploadPostForm.java deleted file mode 100644 index bcecf18..0000000 --- a/src/main/java/Remoa/BE/Post/form/Request/UploadPostForm.java +++ /dev/null @@ -1,27 +0,0 @@ -package Remoa.BE.Post.form.Request; - -import Remoa.BE.Post.Domain.Category; -import Remoa.BE.Post.Domain.Post; -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; -import org.springframework.web.multipart.MultipartFile; - -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.Size; -import java.util.List; - -@Getter -@Setter -public class UploadPostForm { - - private String title; // Post name - - private String contestName; - - private String category; // Category name - - private String contestAward; - - //private List uploadFiles; -} diff --git a/src/main/java/Remoa/BE/Web/ADMIN/Controller/AdminController.java b/src/main/java/Remoa/BE/Web/ADMIN/Controller/AdminController.java new file mode 100644 index 0000000..08287a3 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/ADMIN/Controller/AdminController.java @@ -0,0 +1,121 @@ +package Remoa.BE.Web.ADMIN.Controller; + + +import Remoa.BE.Web.ADMIN.Service.AdminService; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "어드민 기능", description = "어드민 기능 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/admin") +@Slf4j +public class AdminController { + + private final AdminService adminService; + + + // 포스트 삭제 + @DeleteMapping("/post/{postId}") + @Operation(summary = "ADMIN 포스트 삭제", description = "ADMIN 포스트를 삭제합니다. " + + "
응답데이터 정의 필요") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "포스트를 성공적으로 삭제했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = MessageUtils.FORBIDDEN, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity deletePost(@PathVariable Long postId) { + log.info("EndPoint Delete /admin/post/{postId}"); + + adminService.deletePost(postId); + return ResponseEntity.ok().build(); + } + + // 코멘트 삭제 + @DeleteMapping("/comment/{commentId}") + @Operation(summary = "ADMIN 코멘트 삭제", description = "ADMIN 코멘트를 삭제합니다." + + "
응답데이터 정의 필요") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "코멘트를 성공적으로 삭제했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = MessageUtils.FORBIDDEN, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity deleteComment(@PathVariable Long commentId) { + log.info("EndPoint Delete /admin/comment/{commentId}"); + + adminService.deleteComment(commentId); + return ResponseEntity.ok().build(); + } + + // 코멘트 답글 삭제 + @DeleteMapping("/comment-reply/{commentReplyId}") + @Operation(summary = "ADMIN 코멘트 대댓글 삭제", description = "ADMIN 코멘트 대댓글을 삭제합니다." + + "
응답데이터 정의 필요") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "코멘트 답글을 성공적으로 삭제했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = MessageUtils.FORBIDDEN, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity deleteCommentReply(@PathVariable Long commentReplyId) { + log.info("EndPoint Delete /admin/comment-reply/{commentReplyId}"); + + adminService.deleteCommentReply(commentReplyId); + return ResponseEntity.ok().build(); + } + + // 피드백 삭제 + @DeleteMapping("/feedback/{feedbackId}") + @Operation(summary = "ADMIN 피드백 삭제", description = "ADMIN 피드백을 삭제합니다." + + "
응답데이터 정의 필요") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "피드백을 성공적으로 삭제했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = MessageUtils.FORBIDDEN, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity deleteFeedback(@PathVariable Long feedbackId) { + log.info("EndPoint Delete /admin/feedback/{feedbackId}"); + + adminService.deleteFeedback(feedbackId); + return ResponseEntity.ok().build(); + } + + // 피드백 답글 삭제 + @DeleteMapping("/feedback-reply/{feedbackReplyId}") + @Operation(summary = "ADMIN 피드백 대댓글 삭제", description = "ADMIN 피드백 대댓글을 삭제합니다." + + "
응답데이터 정의 필요") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "피드백 답글을 성공적으로 삭제했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = MessageUtils.FORBIDDEN, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity deleteFeedbackReply(@PathVariable Long feedbackReplyId) { + log.info("EndPoint Delete /admin/feedback-reply/{feedbackReplyId}"); + + adminService.deleteFeedbackReply(feedbackReplyId); + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/Remoa/BE/Web/ADMIN/Service/AdminService.java b/src/main/java/Remoa/BE/Web/ADMIN/Service/AdminService.java new file mode 100644 index 0000000..e6082b1 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/ADMIN/Service/AdminService.java @@ -0,0 +1,65 @@ +package Remoa.BE.Web.ADMIN.Service; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Comment.Domain.CommentReply; +import Remoa.BE.Web.Comment.Repository.CommentReplyRepository; +import Remoa.BE.Web.Comment.Repository.CommentRepository; +import Remoa.BE.Web.CommentFeedback.Repository.CommentFeedbackRepository; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Feedback.Domain.FeedbackReply; +import Remoa.BE.Web.Feedback.Repository.FeedbackReplyRepository; +import Remoa.BE.Web.Feedback.Repository.FeedbackRepository; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Repository.PostRepository; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@Service +@RequiredArgsConstructor +@Transactional +public class AdminService { + + private final PostRepository postRepository; + private final CommentRepository commentRepository; + private final CommentReplyRepository commentReplyRepository; + private final FeedbackRepository feedbackRepository; + private final FeedbackReplyRepository feedBackReplyRepository; + private final CommentFeedbackRepository commentFeedbackRepository; + + public void deletePost(Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + postRepository.delete(post); + } + + public void deleteComment(Long commentId) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + commentFeedbackRepository.deleteByComment(comment); + commentRepository.delete(comment); + } + + public void deleteCommentReply(Long commentReplyId) { + CommentReply commentReply = commentReplyRepository.findById(commentReplyId) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + commentReplyRepository.delete(commentReply); + } + + public void deleteFeedback(Long feedbackId) { + Feedback feedback = feedbackRepository.findById(feedbackId) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + commentFeedbackRepository.deleteByFeedback(feedback); + feedbackRepository.delete(feedback); + } + + public void deleteFeedbackReply(Long feedbackReplyId) { + FeedbackReply feedbackReply = feedBackReplyRepository.findById(feedbackReplyId) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + feedBackReplyRepository.delete(feedbackReply); + } +} + diff --git a/src/main/java/Remoa/BE/Web/Comment/Controller/CommentController.java b/src/main/java/Remoa/BE/Web/Comment/Controller/CommentController.java new file mode 100644 index 0000000..74024b1 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Controller/CommentController.java @@ -0,0 +1,143 @@ +package Remoa.BE.Web.Comment.Controller; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Comment.Dto.Req.ReqCommentDto; +import Remoa.BE.Web.Comment.Dto.Res.ResCommentDto; +import Remoa.BE.Web.Comment.Dto.Res.ResCommentLikeDto; +import Remoa.BE.Web.Comment.Service.CommentService; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.MemberUtils; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Objects; + +@Tag(name = "레퍼런스 코멘트 기능 Test completed", description = "레퍼런스 코멘트 기능 API") +@Slf4j +@RestController +@RequiredArgsConstructor +public class CommentController { + + private final MemberService memberService; + private final CommentService commentService; + private final MemberUtils memberUtils; + + //코멘트 작성 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "코멘트을 성공적으로 등록했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/reference/{reference_id}/comment") + @Operation(summary = "코멘트 작성 Test Completed", description = "코멘트를 작성합니다.") + public ResponseEntity>> registerComment(@RequestBody ReqCommentDto req, + @PathVariable("reference_id") Long postId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint POST /reference/{reference_id}/comment"); + + String content = req.getComment(); + Member myMember = memberService.findOne(memberDetails.getMemberId()); + commentService.registerComment(myMember, content, postId); + + // 조회한 post의 comment 조회 및 각 comment에 대한 commentReply 조회 -> 이후 ResCommentDto로 매핑 + List resCommentDtos = memberUtils.commentList(postId, myMember); + + BaseResponse> response = new BaseResponse<>(CustomMessage.OK, resCommentDtos); + return ResponseEntity.ok(response); + } + + + // 코멘트 수정 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "코멘트을 성공적으로 수정했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PutMapping("/reference/comment/{comment_id}") + @Operation(summary = "코멘트 수정 Test Completed", description = "작성한 코멘트를 수정합니다.") + public ResponseEntity>> modifyComment(@RequestBody ReqCommentDto req, + @PathVariable("comment_id") Long commentId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint PUT /reference/comment/{comment_id}"); + + String content = req.getComment(); + Comment c = commentService.findOne(commentId); + + if (!Objects.equals(c.getMember().getMemberId(), memberDetails.getMemberId())) { + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + commentService.modifyComment(content, commentId); //내용 변경 + + Member myMember = memberService.findOne(memberDetails.getMemberId()); + // 조회한 post의 comment 조회 및 각 comment에 대한 commentReply 조회 -> 이후 ResCommentDto로 매핑 + List resCommentDtos = memberUtils.commentList(c.getPost().getPostId(), myMember); + + BaseResponse> response = new BaseResponse<>(CustomMessage.OK, resCommentDtos); + return ResponseEntity.ok(response); + } + + //코멘트 삭제 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "코멘트를 성공적으로 삭제했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/reference/comment/{comment_id}") + @Operation(summary = "코멘트 삭제 Test Completed", description = "작성한 코멘트를 삭제합니다.") + public ResponseEntity>> deleteComment(@PathVariable("comment_id") Long commentId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Delete /reference/comment/{comment_id}"); + + Comment c = commentService.findOne(commentId); + + if (!Objects.equals(c.getMember().getMemberId(), memberDetails.getMemberId())) { + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + commentService.deleteComment(commentId); + + Member myMember = memberService.findOne(memberDetails.getMemberId()); + // 조회한 post의 comment 조회 및 각 comment에 대한 commentReply 조회 -> 이후 ResCommentDto로 매핑 + List resCommentDtos = memberUtils.commentList(c.getPost().getPostId(), myMember); + + BaseResponse> response = new BaseResponse<>(CustomMessage.OK, resCommentDtos); + return ResponseEntity.ok(response); + } + + // 코멘트 좋아요 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "코멘트에 성공적으로 좋아요를 눌렀습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/reference/comment/{comment_id}/like") // 코멘트 좋아요 + @Operation(summary = "코멘트 좋아요 Test Completed", description = "코멘트에 좋아요를 누릅니다.") + public ResponseEntity likeComment(@PathVariable("comment_id") Long commentId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Post /reference/comment/{comment_id}/like"); + + Long memberId = memberDetails.getMemberId(); + Member member = memberService.findOne(memberId); + commentService.likeComment(member, commentId); + int count = commentService.commentLikeCount(commentId); + ResCommentLikeDto responseDto = new ResCommentLikeDto(count); + return ResponseEntity.ok(responseDto); + } + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Comment/Controller/CommentReplyController.java b/src/main/java/Remoa/BE/Web/Comment/Controller/CommentReplyController.java new file mode 100644 index 0000000..29213cd --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Controller/CommentReplyController.java @@ -0,0 +1,156 @@ +package Remoa.BE.Web.Comment.Controller; + +import Remoa.BE.Web.Comment.Domain.CommentReply; +import Remoa.BE.Web.Comment.Dto.Req.ReqCommentReplyDto; +import Remoa.BE.Web.Comment.Dto.Res.ResCommentDto; +import Remoa.BE.Web.Comment.Dto.Res.ResCommentLikeDto; +import Remoa.BE.Web.Comment.Dto.Res.ResCommentReplyDto; +import Remoa.BE.Web.Comment.Dto.Res.ResCommentReplyLikeDto; +import Remoa.BE.Web.Comment.Service.CommentReplyService; +import Remoa.BE.Web.Comment.Service.CommentService; +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.MemberUtils; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Service.PostService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Objects; + +@Tag(name = "레퍼런스 코멘트 대댓글 기능 Test Completed", description = "레퍼런스 코멘트 대댓글 기능 API") +@Slf4j +@RestController +@RequiredArgsConstructor +public class CommentReplyController { + + private final MemberService memberService; + private final CommentReplyService commentReplyService; + private final MemberUtils memberUtils; + + //대댓글 작성 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "코멘트 대댓글을 성공적으로 등록했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/reference/{reference_id}/comment/{comment_id}") + @Operation(summary = "코멘트 대댓글 작성 Test completed", description = "코멘트에 대댓글을 작성합니다.") + public ResponseEntity>> registerCommentReply(@RequestBody ReqCommentReplyDto req, + @PathVariable("reference_id") Long postId, + @PathVariable("comment_id") Long commentId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Post /reference/{reference_id}/comment/{comment_id}"); + + String content = req.getCommentReply(); + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + commentReplyService.registerCommentReply(myMember, content, postId, commentId); + + + // 조회한 post의 comment 조회 및 각 comment에 대한 commentReply 조회 -> 이후 ResCommentDto로 매핑 + List resCommentDtos = memberUtils.commentList(postId, myMember); + + BaseResponse> response = new BaseResponse<>(CustomMessage.OK, resCommentDtos); + return ResponseEntity.ok(response); + } + + + // 대댓글 수정 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "코멘트 대댓글을 성공적으로 수정했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PutMapping("/reference/comment/{comment_id}/reply/{reply_id}") + @Operation(summary = "코멘트 대댓글 수정 Test completed", description = "작성한 코멘트 대댓글을 수정합니다.") + public ResponseEntity>> modifyCommentReply(@RequestBody ReqCommentReplyDto req, + @PathVariable("comment_id") Long commentId, + @PathVariable("reply_id") Long replyId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Put /reference/comment/{comment_id}/reply/{reply_id}"); + + String content = req.getCommentReply(); + CommentReply reply = commentReplyService.findOne(replyId); + + if (!Objects.equals(reply.getMember().getMemberId(), memberDetails.getMemberId())) { + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + commentReplyService.modifyCommentReply(content, replyId); + + Member myMember = memberService.findOne(memberDetails.getMemberId()); + // 조회한 post의 comment 조회 및 각 comment에 대한 commentReply 조회 -> 이후 ResCommentDto로 매핑 + List resCommentDtos = memberUtils.commentList(reply.getPost().getPostId(), myMember); + + BaseResponse> response = new BaseResponse<>(CustomMessage.OK, resCommentDtos); + return ResponseEntity.ok(response); + } + + // 대댓글 삭제 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "코멘트 대댓글을 성공적으로 삭제했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/reference/comment/{comment_id}/reply/{reply_id}") + @Operation(summary = "코멘트 대댓글 삭제 Test Completed", description = "작성한 코멘트 대댓글을 삭제합니다.") + public ResponseEntity>> deleteCommentReply(@PathVariable("comment_id") Long commentId, + @PathVariable("reply_id") Long replyId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Delete /reference/comment/{comment_id}/reply/{reply_id}"); + + CommentReply reply = commentReplyService.findOne(replyId); + + if (!Objects.equals(reply.getMember().getMemberId(), memberDetails.getMemberId())) { + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + + commentReplyService.deleteCommentReply(replyId); + + Member myMember = memberService.findOne(memberDetails.getMemberId()); + // 조회한 post의 comment 조회 및 각 comment에 대한 commentReply 조회 -> 이후 ResCommentDto로 매핑 + List resCommentDtos = memberUtils.commentList(reply.getPost().getPostId(), myMember); + + BaseResponse> response = new BaseResponse<>(CustomMessage.OK, resCommentDtos); + return ResponseEntity.ok(response); + } + + // 코멘트 좋아요 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "코멘트 대댓글에 성공적으로 좋아요를 눌렀습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/reference/comment_reply/{reply_id}/like") // 코멘트 좋아요 + @Operation(summary = "코멘트 대댓글 좋아요 Test Completed", description = "코멘트 대댓글에 좋아요를 누릅니다.") + public ResponseEntity likeComment(@PathVariable("reply_id") Long replyId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Post /reference/comment_reply/{reply_id}/like"); + + Long memberId = memberDetails.getMemberId(); + Member member = memberService.findOne(memberId); + commentReplyService.likeCommentReply(member, replyId); + int count = commentReplyService.commentReplyLikeCount(replyId); + ResCommentReplyLikeDto responseDto = new ResCommentReplyLikeDto(count); + return ResponseEntity.ok(responseDto); + } + +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Domain/Comment.java b/src/main/java/Remoa/BE/Web/Comment/Domain/Comment.java new file mode 100644 index 0000000..b1841c3 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Domain/Comment.java @@ -0,0 +1,96 @@ +package Remoa.BE.Web.Comment.Domain; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +import java.time.LocalDateTime; +import java.util.List; + +import static jakarta.persistence.FetchType.LAZY; + +@Entity +@Getter +@Setter +@SQLDelete(sql = "UPDATE comment SET deleted = true WHERE comment_id = ?") +@SQLRestriction("deleted = false") // 검색시 deleted = false 조건을 where 절에 추가 +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_id") + private Long commentId; + + /** + * Comment가 속해있는 Post + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + /** + * Comment를 작성한 Member + */ + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id") + private Member member; + + /** + * Comment의 내용 + */ + @Lob + @Column(name = "content", length = 300) + private String content; + + + /** + * Comment가 작성된 시간 + */ + @Column(name = "commented_time") + private LocalDateTime commentedTime; + + /** + * Comment의 좋아요 숫자 + */ + @Column(name = "like_count") + private Integer LikeCount = 0; + + + @OneToMany(mappedBy = "comment", cascade = {CascadeType.REMOVE}, fetch = LAZY) + //@OnDelete(action = OnDeleteAction.CASCADE) + private List commentLikes; + + + @OneToMany(mappedBy = "comment", cascade = {CascadeType.REMOVE}, fetch = LAZY) + //@OnDelete(action = OnDeleteAction.CASCADE) + private List commentReplies; + + private Boolean deleted = Boolean.FALSE; + + + public static Comment createComment(Post post, Member member, String content, LocalDateTime time) { + if(content.length() > 300){ + throw new BaseException(CustomMessage.INVALID_CONTENT_LENGTH); + } + Comment comment = new Comment(); + comment.setPost(post); + comment.setMember(member); + comment.setContent(content); + comment.setLikeCount(0); + comment.setCommentedTime(time); + return comment; + } + + public void setContent(String content){ + if(content.length() > 300){ + throw new BaseException(CustomMessage.INVALID_CONTENT_LENGTH); + } + this.content = content; + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Member/Domain/CommentLike.java b/src/main/java/Remoa/BE/Web/Comment/Domain/CommentLike.java similarity index 88% rename from src/main/java/Remoa/BE/Member/Domain/CommentLike.java rename to src/main/java/Remoa/BE/Web/Comment/Domain/CommentLike.java index 00785ef..01793e1 100644 --- a/src/main/java/Remoa/BE/Member/Domain/CommentLike.java +++ b/src/main/java/Remoa/BE/Web/Comment/Domain/CommentLike.java @@ -1,11 +1,12 @@ -package Remoa.BE.Member.Domain; +package Remoa.BE.Web.Comment.Domain; +import Remoa.BE.Web.Member.Domain.Member; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; +import jakarta.persistence.*; /** * Member가 Comment를 Like할 때 사용 @@ -33,7 +34,6 @@ public static CommentLike createCommentLike(Member member, Comment comment) { CommentLike commentLike = new CommentLike(); commentLike.setComment(comment); commentLike.setMember(member); - return commentLike; } } \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Comment/Domain/CommentReply.java b/src/main/java/Remoa/BE/Web/Comment/Domain/CommentReply.java new file mode 100644 index 0000000..146dcb5 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Domain/CommentReply.java @@ -0,0 +1,100 @@ +package Remoa.BE.Web.Comment.Domain; + +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +import java.time.LocalDateTime; +import java.util.List; + +import static jakarta.persistence.FetchType.LAZY; + +@Entity +@Getter +@Setter +@SQLDelete(sql = "UPDATE comment_reply SET deleted = true WHERE comment_reply_id = ?") +@SQLRestriction("deleted = false") // 검색시 deleted = false 조건을 where 절에 추가 +public class CommentReply { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_reply_id") + private Long commentReplyId; + + /** + * CommentReply 속해있는 Post + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + /** + * CommentReply 작성한 Member + */ + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id") + private Member member; + + /** + * 대댓글 기능을 위해 부모 댓글과의 연관관계 세팅. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_id") + private Comment comment; + + + /** + * CommentReply 내용 + */ + @Lob + @Column(name = "content", length = 300) + private String content; + + /** + * CommentReply 작성된 시간 + */ + @Column(name = "comment_replied_time") + private LocalDateTime commentRepliedTime; + + /** + * CommentReply 좋아요 숫자 + */ + @Column(name = "like_count") + private Integer likeCount = 0; + + + @OneToMany(mappedBy = "commentReply", cascade = {CascadeType.REMOVE}, fetch = FetchType.LAZY) +// @OnDelete(action = OnDeleteAction.CASCADE) + private List commentReplyLikes; + + private Boolean deleted = Boolean.FALSE; + + public static CommentReply createCommentReply(Post post, Member member, String content, Comment parentComment) { + if(content.length() > 300){ + throw new BaseException(CustomMessage.INVALID_CONTENT_LENGTH); + } + CommentReply commentReply = new CommentReply(); + commentReply.setPost(post); + commentReply.setMember(member); + commentReply.setContent(content); + commentReply.setComment(parentComment); + commentReply.setLikeCount(0); + commentReply.setCommentRepliedTime(LocalDateTime.now()); + return commentReply; + } + + public void setContent(String content){ + if(content.length() > 300){ + throw new BaseException(CustomMessage.INVALID_CONTENT_LENGTH); + } + this.content = content; + } + +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Domain/CommentReplyLike.java b/src/main/java/Remoa/BE/Web/Comment/Domain/CommentReplyLike.java new file mode 100644 index 0000000..3359a35 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Domain/CommentReplyLike.java @@ -0,0 +1,35 @@ +package Remoa.BE.Web.Comment.Domain; + +import Remoa.BE.Web.Member.Domain.Member; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +/** + * Member가 CommentReply(대댓글)을 Like할 때 사용 + */ +@Getter +@Setter +@Entity +public class CommentReplyLike { + + @Id + @GeneratedValue + @Column(name = "comment_reply_like_id") + private Long commentReplyLikeId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_reply_id") + private CommentReply commentReply; + + public static CommentReplyLike createCommentReplyLike(Member member, CommentReply commentReply) { + CommentReplyLike commentReplyLike = new CommentReplyLike(); + commentReplyLike.setCommentReply(commentReply); + commentReplyLike.setMember(member); + return commentReplyLike; + } +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Dto/Req/ReqCommentDto.java b/src/main/java/Remoa/BE/Web/Comment/Dto/Req/ReqCommentDto.java new file mode 100644 index 0000000..46c218c --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Dto/Req/ReqCommentDto.java @@ -0,0 +1,14 @@ +package Remoa.BE.Web.Comment.Dto.Req; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class ReqCommentDto { + private String comment; +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Dto/Req/ReqCommentReplyDto.java b/src/main/java/Remoa/BE/Web/Comment/Dto/Req/ReqCommentReplyDto.java new file mode 100644 index 0000000..5634add --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Dto/Req/ReqCommentReplyDto.java @@ -0,0 +1,14 @@ +package Remoa.BE.Web.Comment.Dto.Req; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class ReqCommentReplyDto { + private String commentReply; +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentDto.java b/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentDto.java new file mode 100644 index 0000000..9e5fbf5 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentDto.java @@ -0,0 +1,58 @@ +package Remoa.BE.Web.Comment.Dto.Res; + +import Remoa.BE.Web.Comment.Domain.CommentReply; +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ResCommentDto { + + @Schema(description = "코멘트 ID", example = "4") + private Long commentId; + + @Schema(description = "작성자 정보") + private ResMemberInfoDto member; + + @Schema(description = "코멘트 내용", example = "안녕") + private String content; + + @Schema(description = "좋아요 수", example = "0") + private Integer likeCount; + + @Schema(description = "좋아요 여부") + private Boolean isLiked; + + @Schema(description = "삭제여부", example = "false") + private Boolean isDeleted; + + @Schema(description = "코멘트 작성 시간", example = "2023-03-27T23:18:47") + private LocalDateTime commentedTime; + + @Schema(description = "대댓글 목록") + private List commentReplies; + + public ResCommentDto(Comment comment, Boolean isLiked, Boolean isFollow, List commentReplyDtos) { + this.commentId = comment.getCommentId(); + this.member = new ResMemberInfoDto(comment.getMember(), isFollow); + this.content = comment.getContent(); + this.likeCount = comment.getLikeCount(); + this.isLiked = isLiked; + this.isDeleted = comment.getDeleted(); + this.commentedTime = comment.getCommentedTime(); + this.commentReplies = commentReplyDtos; + } + + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentLikeDto.java b/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentLikeDto.java new file mode 100644 index 0000000..2c4256f --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentLikeDto.java @@ -0,0 +1,14 @@ +package Remoa.BE.Web.Comment.Dto.Res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ResCommentLikeDto { + @Schema(description = "좋아요 수", example = "21") + private int likeCount; +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentReplyDto.java b/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentReplyDto.java new file mode 100644 index 0000000..65993ca --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentReplyDto.java @@ -0,0 +1,50 @@ +package Remoa.BE.Web.Comment.Dto.Res; + +import Remoa.BE.Web.Comment.Domain.CommentReply; +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ResCommentReplyDto { + + @Schema(description = "대댓글 ID", example = "4") + private Long commentReplyId; + + @Schema(description = "작성자 정보") + private ResMemberInfoDto member; + + @Schema(description = "대댓글 내용", example = "안녕") + private String content; + + @Schema(description = "좋아요 수", example = "0") + private Integer likeCount; + + @Schema(description = "좋아요 여부") // 수정 필요 + private Boolean isLiked; + + @Schema(description = "대댓글 작성 시간", example = "2023-03-27T23:18:47") + private LocalDateTime commentRepliedTime; + + @Schema(description = "삭제여부", example = "false") + private Boolean isDeleted; + + public ResCommentReplyDto(CommentReply commentReply, Boolean isLiked, Boolean isFollow) { + this.commentReplyId = commentReply.getCommentReplyId(); + this.member = new ResMemberInfoDto(commentReply.getMember(), isFollow); + this.content = commentReply.getContent(); + this.likeCount = commentReply.getLikeCount(); + this.isLiked = isLiked; + this.isDeleted = commentReply.getDeleted(); + this.commentRepliedTime = commentReply.getCommentRepliedTime(); + } + +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentReplyLikeDto.java b/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentReplyLikeDto.java new file mode 100644 index 0000000..96dcf22 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Dto/Res/ResCommentReplyLikeDto.java @@ -0,0 +1,16 @@ +package Remoa.BE.Web.Comment.Dto.Res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ResCommentReplyLikeDto { + @Schema(description = "좋아요 수", example = "21") + private int likeCount; + +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Repository/CommentLikeRepository.java b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentLikeRepository.java new file mode 100644 index 0000000..b0536f4 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentLikeRepository.java @@ -0,0 +1,21 @@ +package Remoa.BE.Web.Comment.Repository; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Comment.Domain.CommentLike; +import Remoa.BE.Web.Member.Domain.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 org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CommentLikeRepository extends JpaRepository { + Optional findByMemberAndComment(Member member, Comment comment); + + @Modifying + @Query(value = "delete from Comment_Like cl where comment_id = :commentId", nativeQuery = true) + void deleteByCommentHard(@Param("commentId") Long commentId); +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Comment/Repository/CommentReplyLikeRepository.java b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentReplyLikeRepository.java new file mode 100644 index 0000000..d534ab1 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentReplyLikeRepository.java @@ -0,0 +1,14 @@ +package Remoa.BE.Web.Comment.Repository; + +import Remoa.BE.Web.Comment.Domain.CommentReply; +import Remoa.BE.Web.Comment.Domain.CommentReplyLike; +import Remoa.BE.Web.Member.Domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import javax.swing.text.html.Option; +import java.util.Optional; + +public interface CommentReplyLikeRepository extends JpaRepository { + + Optional findByMemberAndCommentReply(Member member, CommentReply commentReply); +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Repository/CommentReplyRepository.java b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentReplyRepository.java new file mode 100644 index 0000000..2823f6c --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentReplyRepository.java @@ -0,0 +1,24 @@ +package Remoa.BE.Web.Comment.Repository; + +import Remoa.BE.Web.Comment.Domain.CommentReply; +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Member.Domain.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 CommentReplyRepository extends JpaRepository { + + List findByCommentOrderByCommentRepliedTimeAsc(Comment comment); + + @Modifying + @Query(value = "delete from CommentReply cr where comment_id = :commentId", nativeQuery = true) + void deleteByCommentHard(@Param("commentId") Long commentId); + + @Modifying + @Query(value = "delete from comment_reply cr where member_id = :memberId", nativeQuery = true) + void deleteByMemberHard(@Param("memberId") Long memberId); +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Repository/CommentReplyRepositoryCustom.java b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentReplyRepositoryCustom.java new file mode 100644 index 0000000..a11da11 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentReplyRepositoryCustom.java @@ -0,0 +1,6 @@ +package Remoa.BE.Web.Comment.Repository; + +public interface CommentReplyRepositoryCustom { + + +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Repository/CommentRepository.java b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentRepository.java new file mode 100644 index 0000000..e7d5416 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentRepository.java @@ -0,0 +1,50 @@ +package Remoa.BE.Web.Comment.Repository; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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 CommentRepository extends JpaRepository, CommentRepositoryCustom { + + Page findByPost(Pageable pageable, Post post); + + @Query(value = "select * from comment c where post_id =: postId", nativeQuery = true) + List findByPostHard(@Param("postId")Long postId); + + @Modifying + @Query(value = "delete from Comment c where post_id = :postId", nativeQuery = true) + void deleteByPostHard(@Param("postId") Long postId); + + Page findByMemberOrderByCommentedTimeDesc(Pageable pageable, Member member); + + boolean existsByPostAndMember(Post post, Member member); + + @Query("SELECT c FROM Comment c " + + "INNER JOIN FETCH c.post p " + + "WHERE c.member = :member " + + "AND c.commentedTime = (SELECT MAX(c2.commentedTime) FROM Comment c2 WHERE c2.post.postId = c.post.postId) " + + "ORDER BY c.commentedTime DESC") + Page findNewestComment(Member member, Pageable pageable); + + @Query("SELECT c FROM Comment c " + + "INNER JOIN FETCH c.post p " + + "WHERE c.member = :member " + + "AND c.commentedTime = (SELECT MIN(c2.commentedTime) FROM Comment c2 " + + "WHERE c2.post.postId = c.post.postId AND c2.member = :member) " + // + "ORDER BY c.commentedTime ASC") + Page findOldestComment(Member member, Pageable pageable); + + + @Modifying + @Query(value = "delete from comment c where member_id = :memberId", nativeQuery = true) + void deleteCommentByMemberHard(@Param("memberId") Long memberId); + +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Repository/CommentRepositoryCustom.java b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentRepositoryCustom.java new file mode 100644 index 0000000..3eec052 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentRepositoryCustom.java @@ -0,0 +1,34 @@ +package Remoa.BE.Web.Comment.Repository; + +import Remoa.BE.Web.Comment.Domain.CommentLike; +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Member.Domain.CommentBookmark; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import com.querydsl.core.Tuple; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +public interface CommentRepositoryCustom { + + Optional findOne(Long id); + void saveComment(Comment comment); + Optional findByCommentId(Long commentId); + List findByPost(Post post); + void saveCommentLike(CommentLike commentLike); + Optional findMemberCommendLike(Member member, Comment comment); + Integer findCommentLike(Comment comment); + void saveCommentBookmark(CommentBookmark commentBookmark); + Optional findMemberCommendBookmark(Member member, Comment comment); + List findRepliesOfParentComment(Comment parentComment); + void updateComment(Comment newComment); + void deleteComment(Comment comment); + List findAllByMember(Member member); + + void deleteChildCommentByParentFeedback(Comment comment); + + Page findMyComment(Member member, Pageable pageable, String sort); +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Repository/CommentRepositoryCustomImpl.java b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentRepositoryCustomImpl.java new file mode 100644 index 0000000..04ac1da --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Repository/CommentRepositoryCustomImpl.java @@ -0,0 +1,186 @@ +package Remoa.BE.Web.Comment.Repository; + +import Remoa.BE.Web.Comment.Domain.CommentLike; +import Remoa.BE.Web.Comment.Domain.QComment; +import Remoa.BE.Web.Member.Domain.QMember; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Member.Domain.CommentBookmark; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.QPost; +import com.querydsl.core.QueryResults; +import com.querydsl.core.Tuple; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.EntityManager; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class CommentRepositoryCustomImpl implements CommentRepositoryCustom { + + private final EntityManager em; + + public Optional findOne(Long id) { + return Optional.ofNullable(em.find(Comment.class, id)); + } + + public void saveComment(Comment comment) { + em.persist(comment); + } + + public Optional findByCommentId(Long commentId) { + return Optional.ofNullable(em.find(Comment.class, commentId)); + } + + /** + * 포스트 별 코멘트을 찾아오기 위한 메서드 + * + * @param post + * @return List + */ + public List findByPost(Post post) { + return em.createQuery("select c from Comment c where c.post = :post order by c.commentedTime asc", + Comment.class) + .setParameter("post", post) + .getResultList(); + } + + public void saveCommentLike(CommentLike commentLike) { + em.persist(commentLike); + } + + /** + * commentLikeAction 메서드를 실행하기 전 이미 해당 코멘트에 대한 좋아요를 했는지 검증->service 단에서 return 값의 null이면 좋아요 가능. + * 혹은 좋아요 취소를 위해 사용할 수도 있다. + */ + public Optional findMemberCommendLike(Member member, Comment comment) { + return em.createQuery("select cl from CommentLike cl " + + "where cl.comment = :comment and cl.member = :member", CommentLike.class) + .setParameter("comment", comment) + .setParameter("member", member) + .getResultStream() + .findAny(); + } + + public Integer findCommentLike(Comment comment) { + return em.createQuery("select cl from CommentLike cl where cl.comment = :comment", CommentLike.class) + .setParameter("comment", comment) + .getResultList() + .size(); + } + + public void saveCommentBookmark(CommentBookmark commentBookmark) { + em.persist(commentBookmark); + } + + /** + * commentBookmarkAction 메서드를 실행하기 전 이미 해당 코멘트에 대한 북마크를 했는지 검증->service 단에서 return 값의 null이면 북마크 가능. + * 혹은 북마크 해제를 위해 사용할 수도 있다. + */ + public Optional findMemberCommendBookmark(Member member, Comment comment) { + return em.createQuery("select cb from CommentBookmark cb " + + "where cb.comment = :comment and cb.member = :member", CommentBookmark.class) + .setParameter("comment", comment) + .setParameter("member", member) + .getResultStream() + .findAny(); + } + + /* //필요없을 거 같아서 주석처리... + public Integer findCommentBookmark(Comment comment) { + return em.createQuery("select cb from CommentBookmark cb where cb.comment = :comment", CommentBookmark.class) + .setParameter("comment", comment) + .getResultList() + .size(); + }*/ + + public List findRepliesOfParentComment(Comment parentComment) { + return em.createQuery("select c from Comment c " + + "where c.parentComment = :comment order by c.commentedTime desc", Comment.class) + .setParameter("comment", parentComment) + .getResultList(); + } + + public void updateComment(Comment newComment) { + em.merge(newComment); + } + + public void deleteComment(Comment comment) { + em.remove(comment); + } + + public List findAllByMember(Member member) { + return em.createQuery("select c from Comment c " + + "where c.member = :member", Comment.class) + .setParameter("member", member) + .getResultList(); + } + + + public void deleteChildCommentByParentFeedback(Comment comment) { + em.createQuery("delete from Comment c where c.parentComment = :comment") + .setParameter("comment", comment) + .executeUpdate(); + } + + private final JPAQueryFactory jpaQueryFactory; + QComment comment = QComment.comment; + QMember member = QMember.member; + QPost post = QPost.post; + + + @Override + public Page findMyComment(Member myMember, Pageable pageable, String sort) { + + + // Subquery to find the latest commented time per post by the member + JPAQuery subQuery = jpaQueryFactory + .select(comment.commentedTime.max()) + .from(comment) + .where(comment.post.postId.eq(post.postId) + .and(comment.member.eq(myMember))); + + // Main query to fetch comments with latest commented time per post + JPAQuery query = jpaQueryFactory + .select(comment) + .from(comment) + .innerJoin(comment.post, post).fetchJoin() + .innerJoin(comment.member, member).fetchJoin() + .where(comment.member.eq(myMember) + .and(comment.commentedTime.eq(subQuery))); + + // Apply sorting based on the sort parameter + OrderSpecifier orderSpecifier; + if ("asc".equalsIgnoreCase(sort)) { + orderSpecifier = comment.commentedTime.asc(); + } else { + orderSpecifier = comment.commentedTime.desc(); + } + query.orderBy(orderSpecifier); + + // Fetch total count for pagination + long total = query.fetchCount(); + + // Apply pagination to the query + List fetch = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // Convert to Page + return new PageImpl<>(fetch, pageable, total); + } +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Service/CommentReplyService.java b/src/main/java/Remoa/BE/Web/Comment/Service/CommentReplyService.java new file mode 100644 index 0000000..f0636fa --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Service/CommentReplyService.java @@ -0,0 +1,111 @@ +package Remoa.BE.Web.Comment.Service; + +import Remoa.BE.Web.Comment.Domain.CommentReply; +import Remoa.BE.Web.Comment.Domain.CommentReplyLike; +import Remoa.BE.Web.Comment.Repository.CommentReplyLikeRepository; +import Remoa.BE.Web.Comment.Repository.CommentReplyRepository; +import Remoa.BE.Web.Comment.Repository.CommentRepository; +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Repository.PostRepository; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class CommentReplyService { + + private final CommentReplyRepository commentReplyRepository; + private final CommentReplyLikeRepository commentReplyLikeRepository; + private final CommentRepository commentRepository; + private final PostRepository postRepository; + + public List findCommentReplies(Comment comment) { + List commentReplies = commentReplyRepository.findByCommentOrderByCommentRepliedTimeAsc(comment); + return commentReplies; + } + + @Transactional + public void deleteByMember(Member member) { + commentReplyRepository.deleteByMemberHard(member.getMemberId()); + } + + @Transactional + public void deleteByComment(Comment comment) { + commentReplyRepository.deleteByCommentHard(comment.getCommentId()); + } + + private void validateContent(String content) { + if (content == null || content.isEmpty()) { + throw new BaseException(CustomMessage.EMPTY_CONTENT); + } + } + + @Transactional + public CommentReply registerCommentReply(Member member, String content, Long postId, Long commentId) { + validateContent(content); + Post post = postRepository.findById(postId).orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + + CommentReply commentReply = CommentReply.createCommentReply(post, member, content, comment); + commentReplyRepository.save(commentReply); + return commentReply; + } + + public CommentReply findOne(Long replyId) { + Optional reply = commentReplyRepository.findById(replyId); + return reply.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Comment reply not found")); + } + + @Transactional + public void modifyCommentReply(String content, Long commentReplyId) { + validateContent(content); + CommentReply commentReply = commentReplyRepository.findById(commentReplyId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Comment reply not found")); + commentReply.setContent(content); // 변경 감지 + } + + @Transactional + public void deleteCommentReply(Long commentReplyId) { + CommentReply commentReply = commentReplyRepository.findById(commentReplyId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Comment reply not found")); + commentReplyRepository.delete(commentReply); + } + + public Optional findCommentReplyLike(Member member, CommentReply commentReply) { + return commentReplyLikeRepository.findByMemberAndCommentReply(member, commentReply); + } + + @Transactional + public void likeCommentReply(Member member, Long commentReplyId) { + CommentReply commentReplyObj = findOne(commentReplyId); + Integer commentReplyLikeCount = commentReplyObj.getLikeCount(); + + // CommentReplyLike를 db에서 조회해보고 조회 결과가 null이면 like+=1, CommentReplyLike 엔티티 생성 + // null이 아니면 like -= 1, 조회결과인 해당 CommentReplyLike 엔티티 삭제 + Optional commentReplyLike = findCommentReplyLike(member, commentReplyObj); + if (commentReplyLike.isEmpty()) { + commentReplyObj.setLikeCount(commentReplyLikeCount + 1); // 좋아요 수 1 증가 + CommentReplyLike commentReplyLikeObj = CommentReplyLike.createCommentReplyLike(member, commentReplyObj); + commentReplyLikeRepository.save(commentReplyLikeObj); + } else { + commentReplyObj.setLikeCount(commentReplyLikeCount - 1); // 좋아요 수 1 차감 + commentReplyLikeRepository.deleteById(commentReplyLike.get().getCommentReplyLikeId()); // db에서 삭제 + } + } + + public int commentReplyLikeCount(Long commentReplyId) { + CommentReply commentReply = findOne(commentReplyId); + return commentReply.getLikeCount(); + } +} diff --git a/src/main/java/Remoa/BE/Web/Comment/Service/CommentService.java b/src/main/java/Remoa/BE/Web/Comment/Service/CommentService.java new file mode 100644 index 0000000..8fda513 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Comment/Service/CommentService.java @@ -0,0 +1,193 @@ +package Remoa.BE.Web.Comment.Service; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Comment.Domain.CommentLike; +import Remoa.BE.Web.Comment.Repository.CommentRepository; +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Comment.Repository.CommentLikeRepository; +import Remoa.BE.Web.CommentFeedback.Service.CommentFeedbackService; +import Remoa.BE.Web.Post.Repository.PostRepository; +import Remoa.BE.Web.Member.Domain.*; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static Remoa.BE.utill.Constant.CONTENT_PAGE_SIZE; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class CommentService { + + private final CommentRepository commentRepository; + + private final CommentLikeRepository commentLikeRepository; + private final PostRepository postRepository; + private final CommentFeedbackService commentFeedbackService; + private final MemberService memberService; + + @Transactional + public void deleteByMember(Member member){ + commentRepository.deleteCommentByMemberHard(member.getMemberId()); + } + + @Transactional + public Long writeComment(Comment comment) { + commentRepository.saveComment(comment); + return comment.getCommentId(); + } + + public boolean existsMyComment(Post post, Member member){ + return commentRepository.existsByPostAndMember(post, member); + } + + @Transactional + public void deleteCommentLikeByComment(Comment comment){ + commentLikeRepository.deleteByCommentHard(comment.getCommentId()); + } + + @Transactional + public void deleteByPost(Post post){ + commentRepository.deleteByPostHard(post.getPostId()); + } + + public List findAllCommentsOfPost(Long postId) { + Post post = postRepository.findById(postId).orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + return commentRepository.findByPost(post); + } + + public List findCommentsByPostHard(Post post) { + return commentRepository.findByPostHard(post.getPostId()); + } + + @Transactional + public Long commentLikeAction(Comment comment, Member member) { + CommentLike commentLike = CommentLike.createCommentLike(member, comment); + commentRepository.saveCommentLike(commentLike); + return commentLike.getCommentLikeId(); + } + + public Comment findOne(Long commentId) { + Optional comment = commentRepository.findOne(commentId); + return comment.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Comment not found")); + } + + public int commentLikeCount(Long commentId) { + Comment comment = findOne(commentId); + return comment.getLikeCount(); + } + + + public Page getMyComments(int page, Member member, String sortDirection) { + Pageable pageable = PageRequest.of(page, CONTENT_PAGE_SIZE); + if (sortDirection.equalsIgnoreCase("desc")) { + return commentRepository.findMyComment(member, pageable, "desc"); + } else { + return commentRepository.findMyComment(member, pageable, "asc"); + } + } + + @Transactional + public Long commentBookmarkAction(Comment comment, Member member) { + CommentBookmark commentBookmark = CommentBookmark.createCommentBookmark(member, comment); + commentRepository.saveCommentBookmark(commentBookmark); + return commentBookmark.getCommentBookmarkId(); + } + + /** + * 코멘트 등록 + */ + @Transactional + public Comment registerComment(Member member, String content, Long postId) { + validateContent(content); + LocalDateTime time = LocalDateTime.now(); + Post post = postRepository.findById(postId).orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + + Comment commentObj = Comment.createComment(post, member, content, time); + commentRepository.saveComment(commentObj); + + //코멘트 - 피드백 동시관리 + commentFeedbackService.saveCommentFeedback(commentObj, null, ContentType.COMMENT, member, post, time); + + return commentObj; + } + + /** + * 코멘트 수정 + */ + + private void validateContent(String content){ + if(content == null || content.isEmpty()){ + throw new BaseException(CustomMessage.EMPTY_CONTENT); + } + } + @Transactional + public void modifyComment(String comment, Long commentId) { + validateContent(comment); + Comment commentObj = findOne(commentId); + commentObj.setContent(comment); + + // commentFeedbackService.findComment(commentObj).getComment().setContent(comment); + commentFeedbackService.findComment(commentObj).setComment(commentObj); + + commentRepository.updateComment(commentObj); + } + + + @Transactional + public void deleteComment(Long commentId) { + Comment commentObj = findOne(commentId); + + CommentFeedback commentOfCommentFeedback = commentFeedbackService.findComment(commentObj); + commentOfCommentFeedback.setDeleted(true); + + commentRepository.delete(commentObj); + } + + + public List getRecentThreeComments(Post post) { + PageRequest pageable = PageRequest.of(0, 3, Sort.by("commentedTime").descending()); + return commentRepository.findByPost(pageable, post).getContent(); + } + + + public List getParentCommentsReply(Comment parentComment) { + return commentRepository.findRepliesOfParentComment(parentComment); + } + + public Optional findCommentLike(Member member, Comment comment) { + return commentLikeRepository.findByMemberAndComment(member, comment); + } + + @Transactional + public void likeComment(Member member, Long commentId) { + Comment commentObj = findOne(commentId); + Integer commentLikeCount = commentObj.getLikeCount(); + + // CommentLike를 db에서 조회해보고 조회 결과가 null이면 like+=1, CommentLike 엔티티 생성 + // null이 아니면 like -= 1, 조회결과인 해당 CommentLike 엔티티 삭제 + Optional commentLike = findCommentLike(member, commentObj); + if (commentLike.isEmpty()) { + commentObj.setLikeCount(commentLikeCount + 1); // 좋아요 수 1 증가 + CommentLike commentLikeObj = CommentLike.createCommentLike(member, commentObj); + commentLikeRepository.save(commentLikeObj); + } else { + commentObj.setLikeCount(commentLikeCount - 1); // 좋아요 수 1 차감 + commentLikeRepository.deleteById(commentLike.get().getCommentLikeId()); // db에서 삭제 + } + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/CommentFeedback/Controller/MyFeedbackAndCommentController.java b/src/main/java/Remoa/BE/Web/CommentFeedback/Controller/MyFeedbackAndCommentController.java new file mode 100644 index 0000000..8421198 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/CommentFeedback/Controller/MyFeedbackAndCommentController.java @@ -0,0 +1,133 @@ +package Remoa.BE.Web.CommentFeedback.Controller; + +import Remoa.BE.Web.Comment.Service.CommentReplyService; +import Remoa.BE.Web.Comment.Service.CommentService; +import Remoa.BE.Web.CommentFeedback.Repository.CommentFeedbackRepository; +import Remoa.BE.Web.CommentFeedback.Service.CommentFeedbackService; +import Remoa.BE.Web.Member.MemberUtils; +import Remoa.BE.Web.Member.Service.FollowService; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Post.Service.PostService; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "받은 피드백 기능 ?? 쓰이는지 ?", description = "받은 피드백 기능 API") +@RestController +@RequiredArgsConstructor +@Slf4j +public class MyFeedbackAndCommentController { + + private final MemberService memberService; + private final PostService postService; + private final CommentService commentService; + private final CommentFeedbackService commentFeedbackService; + private final CommentFeedbackRepository commentFeedbackRepository; + private final FollowService followService; + private final CommentReplyService commentReplyService; + private final MemberUtils memberUtils; + + /* @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "내가 받은 피드백과 코멘트를 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "400", description = "페이지 번호가 잘못되었습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/user/feedback") + @Operation(summary = "내가 받은 피드백 코멘트 조회", description = "내가 받은 최신 피드백 코멘트들을 조회합니다.") + public ResponseEntity receivedFeedback(HttpServletRequest request, + @RequestParam(required = false, defaultValue = "all") String category, + @RequestParam(required = false, defaultValue = "1", name = "page") int pageNumber) { + + if (authorized(request)) { + Long memberId = getMemberId(); + Member myMember = memberService.findOne(memberId); + + pageNumber -= 1; + if (pageNumber < 0) { + return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + +// Page posts = myPostService.getNewestThreePosts(pageNumber, myMember); + Page posts; + if (categoryList.contains(category)) { + + posts = myPostService.getNewestThreePostsSortCategory(pageNumber, myMember, category); + + } else { + posts = myPostService.getNewestThreePosts(pageNumber, myMember); + } + + + if ((posts.getContent().isEmpty()) && (posts.getTotalElements() > 0)) { + return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + Map result = new HashMap<>(); + List res = new ArrayList<>(); + + for (Post post : posts) { //조회한 post + Map commentInfo = new HashMap<>(); + + List parentComments = commentService.getRecentThreeCommentsExceptReply(post); + + int commentNumber = 1; + for (Comment parentComment : parentComments) { //조회한 post의 parent comment + List parentCommentsReply = commentService.getParentCommentsReply(parentComment); + + + + List replies = new ArrayList<>(); + for (Comment reply : parentCommentsReply) { + + replies.add(new ResReplyDto( + reply.getCommentId(), + new ResMemberInfoDto(reply.getMember().getMemberId(), + reply.getMember().getNickname(), + reply.getMember().getProfileImage(), + followService.isMyMemberFollowMember(myMember, reply.getMember())), + reply.getComment(), + reply.getCommentLikeCount(), + commentService.findCommentLike(myMember.getMemberId(), reply.getCommentId()).isPresent(), + reply.getCommentedTime())); + } + + commentInfo.put("comment_" + commentNumber, new ResCommentDto( + parentComment.getCommentId(), + new ResMemberInfoDto(parentComment.getMember().getMemberId(), + parentComment.getMember().getNickname(), + parentComment.getMember().getProfileImage(), + followService.isMyMemberFollowMember(myMember, parentComment.getMember())), + parentComment.getComment(), + parentComment.getCommentLikeCount(), + commentService.findCommentLike(myMember.getMemberId(), + parentComment.getCommentId()).isPresent(), + parentComment.getCommentedTime(), + replies)); + + commentNumber++; + } + + ResReceivedCommentDto map = ResReceivedCommentDto.builder() + .title(post.getTitle()) + .thumbnail(post.getThumbnailUrl()) + .postId(post.getPostId()) + .commentInfo(commentInfo) + .build(); + + res.add(map); + + } + result.put("post",res); + result.put("totalPages", posts.getTotalPages()); //전체 페이지의 수 + result.put("totalOfAllComments", posts.getTotalElements()); //모든 코멘트의 수 + result.put("totalOfPageElements", posts.getNumberOfElements()); //현 페이지 피드백의 수 + + return successResponse(CustomMessage.OK, result); + + } + return errorResponse(CustomMessage.UNAUTHORIZED); + }*/ +} diff --git a/src/main/java/Remoa/BE/Web/CommentFeedback/Domain/CommentFeedback.java b/src/main/java/Remoa/BE/Web/CommentFeedback/Domain/CommentFeedback.java new file mode 100644 index 0000000..18e3b88 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/CommentFeedback/Domain/CommentFeedback.java @@ -0,0 +1,57 @@ +package Remoa.BE.Web.CommentFeedback.Domain; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Member.Domain.ContentType; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; +import org.hibernate.annotations.Where; + +import java.time.LocalDateTime; + +import static jakarta.persistence.FetchType.LAZY; + +/** + * 마이페이지-내 활동 관리에 쓰이는 Comment와 Feedback을 구분 없이 최신순으로 조회하기 위한 entity. + */ +@Builder +@Entity +@Getter +@Setter +@SQLRestriction("deleted = false") +@SQLDelete(sql = "UPDATE comment_feedback SET deleted = true WHERE comment_feedback_id = ?") +@NoArgsConstructor +@AllArgsConstructor +public class CommentFeedback { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_feedback_id") + private Long commentFeedbackId; + + @Enumerated(EnumType.STRING) + private ContentType type; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @OneToOne + private Comment comment; + + @OneToOne + private Feedback feedback; + + private LocalDateTime time; + + @Builder.Default + private Boolean deleted = Boolean.FALSE; +} diff --git a/src/main/java/Remoa/BE/Web/CommentFeedback/Dto/ReceivedFeedbackResponse.java b/src/main/java/Remoa/BE/Web/CommentFeedback/Dto/ReceivedFeedbackResponse.java new file mode 100644 index 0000000..ded4eb4 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/CommentFeedback/Dto/ReceivedFeedbackResponse.java @@ -0,0 +1,23 @@ +package Remoa.BE.Web.CommentFeedback.Dto; + +/* +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ReceivedFeedbackResponse { + + @Schema(description = "포스트의 댓글 목록") + private List posts; + + @Schema(description = "총 페이지 수") + private int totalPages; + + @Schema(description = "총 댓글 수") + private long totalOfAllComments; + + @Schema(description = "현재 페이지의 댓글 수") + private int totalOfPageElements; + + // 생성자, 게터, 세터 +}*/ diff --git a/src/main/java/Remoa/BE/Web/CommentFeedback/Dto/ResReceivedCommentFeedbackDto.java b/src/main/java/Remoa/BE/Web/CommentFeedback/Dto/ResReceivedCommentFeedbackDto.java new file mode 100644 index 0000000..fb2d813 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/CommentFeedback/Dto/ResReceivedCommentFeedbackDto.java @@ -0,0 +1,29 @@ +package Remoa.BE.Web.CommentFeedback.Dto; + + +import Remoa.BE.Web.Post.Dto.Response.ResCommentFeedbackDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ResReceivedCommentFeedbackDto { + @Schema(description = "내가 받은 최신 코멘트/피드백들의 목록") + private List contents; + + @Schema(description = "전체 페이지 수") + private int totalPages; + + @Schema(description = "모든 코멘트의 수") + private long totalOfAllComments; + + @Schema(description = "현재 페이지의 코멘트/피드백 수") + private int totalOfPageElements; +} diff --git a/src/main/java/Remoa/BE/Web/CommentFeedback/Dto/ResReceivedFeedbackAndComment.java b/src/main/java/Remoa/BE/Web/CommentFeedback/Dto/ResReceivedFeedbackAndComment.java new file mode 100644 index 0000000..1074288 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/CommentFeedback/Dto/ResReceivedFeedbackAndComment.java @@ -0,0 +1,18 @@ +package Remoa.BE.Web.CommentFeedback.Dto; + +import Remoa.BE.Web.Comment.Dto.Res.ResCommentDto; +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +/*@Data +@Builder +public class ResReceivedFeedbackAndComment { + + private Long postId; + private String thumbnail; + private Map commentInfo; + private String title; + +}*/ diff --git a/src/main/java/Remoa/BE/Web/CommentFeedback/Repository/CommentFeedbackCustomRepository.java b/src/main/java/Remoa/BE/Web/CommentFeedback/Repository/CommentFeedbackCustomRepository.java new file mode 100644 index 0000000..9b709a5 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/CommentFeedback/Repository/CommentFeedbackCustomRepository.java @@ -0,0 +1,26 @@ +package Remoa.BE.Web.CommentFeedback.Repository; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +import java.util.Optional; + +public interface CommentFeedbackCustomRepository { + Optional findByMemberOrderByTime(Member member); + Optional findByComment(Comment comment); + Optional findByFeedback(Feedback feedback); + + Page findRecentReceivedCommentFeedback(Member member, Pageable pageable, Category category); + Page findMyCommentOrFeedback(Member member, Pageable pageable, String sort); + + +} diff --git a/src/main/java/Remoa/BE/Web/CommentFeedback/Repository/CommentFeedbackCustomRepositoryImpl.java b/src/main/java/Remoa/BE/Web/CommentFeedback/Repository/CommentFeedbackCustomRepositoryImpl.java new file mode 100644 index 0000000..e6048b2 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/CommentFeedback/Repository/CommentFeedbackCustomRepositoryImpl.java @@ -0,0 +1,160 @@ +package Remoa.BE.Web.CommentFeedback.Repository; + + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Comment.Domain.QComment; +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.CommentFeedback.Domain.QCommentFeedback; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Feedback.Domain.QFeedback; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.QMember; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Domain.QPost; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class CommentFeedbackCustomRepositoryImpl implements CommentFeedbackCustomRepository{ + + + + private final JPAQueryFactory jpaQueryFactory; + + QCommentFeedback commentFeedback = QCommentFeedback.commentFeedback; + QMember member = QMember.member; + QComment comment = QComment.comment; + QFeedback feedback = QFeedback.feedback; + QPost post = QPost.post; + + @Override + public Page findMyCommentOrFeedback(Member myMember, Pageable pageable, String sort) { + + // Subquery to find the latest commented time per post by the member + JPAQuery subQuery = jpaQueryFactory + .select(commentFeedback.time.max()) + .from(commentFeedback) + .where(commentFeedback.post.postId.eq(post.postId) + .and(commentFeedback.member.eq(myMember))); + + // Main query to fetch comments with latest commented time per post + JPAQuery query = jpaQueryFactory + .select(commentFeedback) + .from(commentFeedback) + .innerJoin(commentFeedback.post, post).fetchJoin() + .leftJoin(commentFeedback.comment, comment).fetchJoin() + .leftJoin(commentFeedback.feedback, feedback).fetchJoin() + .innerJoin(commentFeedback.member, member).fetchJoin() + .where(commentFeedback.member.eq(myMember) + .and(commentFeedback.time.eq(subQuery))); + + // Apply sorting based on the sort parameter + OrderSpecifier orderSpecifier; + if ("asc".equalsIgnoreCase(sort)) { + orderSpecifier = commentFeedback.time.asc(); + } else { + orderSpecifier = commentFeedback.time.desc(); + } + query.orderBy(orderSpecifier); + + // Fetch total count for pagination + long total = query.fetchCount(); + + // Apply pagination to the query + List fetch = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // Convert to Page + return new PageImpl<>(fetch, pageable, total); + } + + private final JdbcTemplate jdbcTemplate; + +// @Override +// public void deleteByPost(Post post) { +// String sql = "DELETE FROM Comment_Feedback cf WHERE post_id = ?"; +// jdbcTemplate.update(sql, post.getPostId()); +// } + + @Override + public Optional findByMemberOrderByTime(Member member) { + return jpaQueryFactory.select(commentFeedback) + .from(commentFeedback) + .join(commentFeedback.member, this.member) + .where(this.member.eq(member)) + .orderBy(commentFeedback.time.desc()) + .limit(1L).stream().findAny(); + } + + @Override + public Optional findByComment(Comment comment) { + return jpaQueryFactory.select(commentFeedback) + .from(commentFeedback) + .join(commentFeedback.comment, this.comment) + .where(this.comment.eq(comment)) + .stream().findAny(); + } + + @Override + public Optional findByFeedback(Feedback feedback) { + return jpaQueryFactory.select(commentFeedback) + .from(commentFeedback) + .join(commentFeedback.feedback, this.feedback) + .where(this.feedback.eq(feedback)) + .stream().findAny(); + } + + + public Page findRecentReceivedCommentFeedback(Member member, Pageable pageable, Category category) { + boolean isCategoryExists = category != null; + List resultCommentFeedbacks; + if(isCategoryExists) { + resultCommentFeedbacks = jpaQueryFactory.select(commentFeedback) + .from(commentFeedback) + .join(commentFeedback.post, this.post) + .where( + this.post.member.eq(member), // 작성자 본인 + this.post.category.eq(category) // 카테고리 + ) + .orderBy(commentFeedback.time.desc()) + .offset(pageable.getOffset()) // 페이지 번호 + .limit(pageable.getPageSize()) // 페이지 사이즈 + .fetch(); + } else { // 카테고리 구분없이 전체 조회 + resultCommentFeedbacks = jpaQueryFactory.select(commentFeedback) + .from(commentFeedback) + .join(commentFeedback.post, this.post) + .where(this.post.member.eq(member)) // 작성자 본인 + .orderBy(commentFeedback.time.desc()) + .offset(pageable.getOffset()) // 페이지 번호 + .limit(pageable.getPageSize()) // 페이지 사이즈 + .fetch(); + } + // 페이지네이션 기능을 위한 쿼리 + JPAQuery countQuery = jpaQueryFactory // 총 개수 + .selectFrom(commentFeedback) + .join(commentFeedback.post, this.post) + .where(this.post.member.eq(member)); + // count 쿼리가 필요없는 경우는 실행하지 않는다 + return PageableExecutionUtils.getPage(resultCommentFeedbacks, pageable, () -> countQuery.fetch().size()); + } + +} diff --git a/src/main/java/Remoa/BE/Web/CommentFeedback/Repository/CommentFeedbackRepository.java b/src/main/java/Remoa/BE/Web/CommentFeedback/Repository/CommentFeedbackRepository.java new file mode 100644 index 0000000..bba3bb9 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/CommentFeedback/Repository/CommentFeedbackRepository.java @@ -0,0 +1,48 @@ +package Remoa.BE.Web.CommentFeedback.Repository; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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 org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CommentFeedbackRepository extends JpaRepository, CommentFeedbackCustomRepository { + + Page findByMemberOrderByTimeDesc(Pageable pageable, Member member); + + @Modifying + @Query(value = "delete from comment_feedback cf where post_id = :postId", nativeQuery = true) + void deleteByPostHard(@Param("postId") Long postId); + + @Modifying + @Query(value = "delete from comment_feedback cf where member_id = :memberId", nativeQuery = true) + void deleteByMemberHard(@Param("memberId") Long memberId); + + @Modifying + @Query(value = "delete from comment_feedback cf where feedback_id = :feedbackId", nativeQuery = true) + void deleteByFeedbackHard(@Param("feedbackId") Long feedbackId); + + @Modifying + @Query(value = "delete from CommentFeedback cf where cf.feedback = :feedback") + void deleteByFeedback(@Param("feedback") Feedback feedback); + + @Modifying + @Query(value = "delete from CommentFeedback cf where cf.comment = :comment") + void deleteByComment(Comment comment); + + @Modifying + @Query(value = "delete from Comment_Feedback cf where comment_id = :commentId", nativeQuery = true) + void deleteByCommentHard(Long commentId); + +} diff --git a/src/main/java/Remoa/BE/Web/CommentFeedback/Service/CommentFeedbackService.java b/src/main/java/Remoa/BE/Web/CommentFeedback/Service/CommentFeedbackService.java new file mode 100644 index 0000000..5e8eeae --- /dev/null +++ b/src/main/java/Remoa/BE/Web/CommentFeedback/Service/CommentFeedbackService.java @@ -0,0 +1,104 @@ +package Remoa.BE.Web.CommentFeedback.Service; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.CommentFeedback.Repository.CommentFeedbackRepository; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Member.Domain.ContentType; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Repository.CategoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static Remoa.BE.utill.Constant.CONTENT_PAGE_SIZE; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CommentFeedbackService { + private final CommentFeedbackRepository commentFeedbackRepository; + private final CategoryRepository categoryRepository; + + @Transactional + public void deleteByPost(Post post){ + commentFeedbackRepository.deleteByPostHard(post.getPostId()); + } + + @Transactional + public void deleteByComment(Comment comment){ + commentFeedbackRepository.deleteByCommentHard(comment.getCommentId()); + } + + @Transactional + public void deleteByFeedback(Feedback feedback){ + commentFeedbackRepository.deleteByFeedbackHard(feedback.getFeedbackId()); + } + + @Transactional + public void deleteByMember(Member member){ + commentFeedbackRepository.deleteByMemberHard(member.getMemberId()); + } + + @Transactional + public CommentFeedback saveCommentFeedback(Comment comment, Feedback feedback, ContentType type, + Member member, Post post, LocalDateTime time) { + + CommentFeedback commentFeedback = CommentFeedback.builder() + .type(type) + .member(member) + .post(post) + .comment(comment) + .feedback(feedback) + .time(time) + .deleted(false).build(); + return commentFeedbackRepository.save(commentFeedback); + } + + public Page getMyCommentOrFeedback(int page, Member member, String sortDirection) { + Pageable pageable = PageRequest.of(page, CONTENT_PAGE_SIZE); + if (sortDirection.equalsIgnoreCase("desc")) { + return commentFeedbackRepository.findMyCommentOrFeedback(member, pageable, "desc"); + } else { + return commentFeedbackRepository.findMyCommentOrFeedback(member, pageable, "asc"); + } + } + + + public CommentFeedback findNewestCommentFeedback(Member member) { + return commentFeedbackRepository.findByMemberOrderByTime(member).orElse(null); + } + + + public CommentFeedback findComment(Comment comment) { + return commentFeedbackRepository.findByComment(comment) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Comment not found")); + } + + public CommentFeedback findFeedback(Feedback feedback) { + return commentFeedbackRepository.findByFeedback(feedback) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Feedback not found")); + } + + public Page findReceivedCommentOrFeedback(Member myMember, int pageNum, String categoryString) { + Category category = null; + List categoryList = Arrays.asList("idea", "marketing", "design", "video", "digital", "etc"); + if (categoryList.contains(categoryString)) { + category = categoryRepository.findByCategoryName(categoryString); + } + PageRequest pageable = PageRequest.of(pageNum, CONTENT_PAGE_SIZE); + return commentFeedbackRepository.findRecentReceivedCommentFeedback(myMember, pageable, category); + } + +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Controller/FeedbackController.java b/src/main/java/Remoa/BE/Web/Feedback/Controller/FeedbackController.java new file mode 100644 index 0000000..1124808 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Controller/FeedbackController.java @@ -0,0 +1,164 @@ +package Remoa.BE.Web.Feedback.Controller; + +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Feedback.Dto.ResFeedbackDto2; +import Remoa.BE.Web.Feedback.Dto.ResFeedbackLikeDto; +import Remoa.BE.Web.Feedback.Service.FeedbackReplyService; +import Remoa.BE.Web.Feedback.Service.FeedbackService; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.MemberUtils; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Dto.Request.ReqFeedbackDto; +import Remoa.BE.Web.Post.Service.PostService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Objects; + +@Tag(name = "레퍼런스 피드백 기능 Test completed", description = "레퍼런스 피드백 기능 API") +@Slf4j +@RestController +@RequiredArgsConstructor +public class FeedbackController { + + private final FeedbackService feedbackService; + private final MemberService memberService; + private final PostService postService; + private final FeedbackReplyService feedbackReplyService; + private final MemberUtils memberUtils; + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "피드백을 성공적으로 등록했습니다."), + @ApiResponse(responseCode = "400", description = "게시물 페이지 번호가 잘못되었습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/reference/feedback/{reference_id}/{page_number}") // 레퍼런스에 피드백 등록 + @Operation(summary = "피드백 등록 Test completed", description = "특정 게시물 페이지에 피드백을 등록합니다.") + public ResponseEntity>> registerFeedback(@RequestBody ReqFeedbackDto req, + @PathVariable("reference_id") Long postId, + @PathVariable("page_number") Integer pageNumber, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Post /reference/feedback/{reference_id}/{page_number}"); + + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + Post post = postService.findOne(postId); + + // 피드백을 등록하려는 게시물의 페이지 수가 없을 경우 예외 처리 + if (post.getPageCount() < pageNumber || pageNumber < 1) { + throw new BaseException(CustomMessage.BAD_PAGE_NUM); + } + + String content = req.getFeedback(); + feedbackService.registerFeedback(myMember, content, postId, pageNumber); + + // 조회한 post의 feedback 조회 및 각 feedback에 대한 feedbackReply 조회 -> 이후 ResFeedbackDto로 매핑 + List resFeedbackDto2s = memberUtils.feedbackList(postId, myMember); + + + return ResponseEntity.ok(new BaseResponse<>(CustomMessage.OK, resFeedbackDto2s)); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "피드백을 성공적으로 수정했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PutMapping("/reference/feedback/{reference_id}/{feedback_id}") // 피드백 수정 + @Operation(summary = "피드백 수정 Test completed", description = "작성한 피드백을 수정합니다.") + public ResponseEntity>> modifyFeedback(@RequestBody ReqFeedbackDto req, + @PathVariable("reference_id") Long postId, + @PathVariable("feedback_id") Long feedbackId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Put /reference/feedback/{reference_id}/{feedback_id}"); + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); // 멤버 확인 + Post post = postService.findOne(postId); // 포스트 확인 + + String myFeedback = req.getFeedback(); + feedbackService.modifyFeedback(myMember, post, myFeedback, feedbackId); + + + // 조회한 post의 feedback 조회 및 각 feedback에 대한 feedbackReply 조회 -> 이후 ResFeedbackDto로 매핑 + List resFeedbackDto2s = memberUtils.feedbackList(post.getPostId(), myMember); + + return ResponseEntity.ok(new BaseResponse<>(CustomMessage.OK, resFeedbackDto2s)); + // return successResponse(CustomMessage.OK, feedbacks); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "피드백을 성공적으로 삭제했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/reference/feedback/{reference_id}/{feedback_id}") + @Operation(summary = "피드백 삭제 Test Completed", description = "작성한 피드백을 삭제합니다.") + public ResponseEntity>> deleteFeedback(@PathVariable("reference_id") Long postId, + @PathVariable("feedback_id") Long feedbackId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Delete /reference/feedback/{reference_id}/{feedback_id}"); + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + Post post = postService.findOne(postId); + + feedbackService.deleteFeedback(myMember, post, feedbackId); // 삭제 + + // 조회한 post의 feedback 조회 및 각 feedback에 대한 feedbackReply 조회 -> 이후 ResFeedbackDto로 매핑 + List resFeedbackDtos = memberUtils.feedbackList(post.getPostId(), myMember); + + + return ResponseEntity.ok(new BaseResponse<>(CustomMessage.OK, resFeedbackDtos)); + // return successResponse(CustomMessage.OK, feedbacks); + + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "피드백에 좋아요를 성공적으로 등록했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/reference/feedback/{reference_id}/{feedback_member_id}/like") // 피드백 좋아요 + @Operation(summary = "피드백 좋아요 Test Completed", description = "피드백에 좋아요를 누릅니다.") + public ResponseEntity> likeFeedback(@PathVariable("reference_id") Long postId, + @PathVariable("feedback_member_id") Long feedbackMemberId, + @AuthenticationPrincipal MemberDetails memberDetails) { + + log.info("EndPoint Post /reference/feedback/{reference_id}/{feedback_member_id}/like"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + Post post = postService.findOne(postId); + Member feedbackMember = memberService.findOne(feedbackMemberId); + + int count = feedbackService.likeFeedback(myMember, post, feedbackMember); + + ResFeedbackLikeDto dto = new ResFeedbackLikeDto(count); + BaseResponse response = new BaseResponse<>(CustomMessage.OK, dto); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, map); + } + + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Feedback/Controller/FeedbackReplyController.java b/src/main/java/Remoa/BE/Web/Feedback/Controller/FeedbackReplyController.java new file mode 100644 index 0000000..7e74060 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Controller/FeedbackReplyController.java @@ -0,0 +1,154 @@ +package Remoa.BE.Web.Feedback.Controller; + +import Remoa.BE.Web.Feedback.Domain.FeedbackReply; +import Remoa.BE.Web.Feedback.Dto.ResFeedbackDto2; +import Remoa.BE.Web.Feedback.Dto.ResFeedbackLikeDto; +import Remoa.BE.Web.Feedback.Dto.ResFeedbackReplyLikeDto; +import Remoa.BE.Web.Feedback.Service.FeedbackReplyService; +import Remoa.BE.Web.Feedback.Service.FeedbackService; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.MemberUtils; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Post.Dto.Request.ReqFeedbackDto; +import Remoa.BE.Web.Post.Dto.Request.ReqFeedbackReplyDto; +import Remoa.BE.Web.Post.Service.PostService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Objects; + +@Tag(name = "레퍼런스 피드백 대댓글 기능 Test Completed", description = "레퍼런스 피드백 대댓글 기능 API") +@Slf4j +@RestController +@RequiredArgsConstructor +public class FeedbackReplyController { + + + private final MemberService memberService; + private final FeedbackReplyService feedbackReplyService; + private final MemberUtils memberUtils; + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "피드백 대댓글을 성공적으로 등록했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/reference/{reference_id}/feedback/{feedback_member_log_id}") // 레퍼런스에 피드백 대댓글 등록 + @Operation(summary = "피드백 대댓글 등록 Test Completed", description = "특정 피드백에 대댓글을 등록합니다.") + public ResponseEntity>> registerFeedbackReply(@RequestBody ReqFeedbackReplyDto req, + @PathVariable("reference_id") Long postId, + @PathVariable("feedback_member_log_id") Long feedbackMemberLogId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Post /reference/{reference_id}/feedback/{feedback_member_log_id}"); + + String content = req.getFeedbackReply(); + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + feedbackReplyService.registerFeedbackReply(myMember, postId, feedbackMemberLogId, content); + + // 조회한 post의 feedback 조회 및 각 feedback에 대한 feedbackReply 조회 -> 이후 ResFeedbackDto로 매핑 + List resFeedbackDtos = memberUtils.feedbackList(postId, myMember); + + return ResponseEntity.ok(new BaseResponse<>(CustomMessage.OK, resFeedbackDtos)); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "피드백 대댓글을 성공적으로 수정했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PutMapping("/reference/feedback/{feedback_member_log_id}/reply/{reply_id}") // 피드백 대댓글 수정 + @Operation(summary = "피드백 대댓글 수정 Test Completed", description = "작성한 피드백 대댓글을 수정합니다.") + public ResponseEntity>> modifyFeedbackReply(@RequestBody ReqFeedbackReplyDto req, + @PathVariable("feedback_member_log_id") Long feedbackMemberLogId, + @PathVariable("reply_id") Long replyId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Put /reference/feedback/{feedback_member_log_id}/reply/{reply_id}"); + + String content = req.getFeedbackReply(); + FeedbackReply reply = feedbackReplyService.findOne(replyId); + Long myMemberId = memberDetails.getMemberId(); + if (!Objects.equals(reply.getMember().getMemberId(), myMemberId)) { + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + + feedbackReplyService.modifyFeedbackReply(content, replyId); + + Member myMember = memberService.findOne(myMemberId); + // 조회한 post의 feedback 조회 및 각 feedback에 대한 feedbackReply 조회 -> 이후 ResFeedbackDto로 매핑 + List resFeedbackDtos = memberUtils.feedbackList(reply.getPost().getPostId(), myMember); + + + return ResponseEntity.ok(new BaseResponse<>(CustomMessage.OK, resFeedbackDtos)); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "피드백 대댓글을 성공적으로 삭제했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/reference/feedback/{feedback_member_log_id}/reply/{reply_id}") // 피드백 대댓글 삭제 + @Operation(summary = "피드백 대댓글 삭제 Test Completed", description = "작성한 피드백 대댓글을 삭제합니다.") + public ResponseEntity>> deleteFeedbackReply(@PathVariable("feedback_member_log_id") Long feedbackMemberLogId, + @PathVariable("reply_id") Long replyId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Delete /reference/feedback/{feedback_member_log_id}/reply/{reply_id}"); + + FeedbackReply reply = feedbackReplyService.findOne(replyId); + + Long myMemberId = memberDetails.getMemberId(); + if (!Objects.equals(reply.getMember().getMemberId(), myMemberId)) { + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + + feedbackReplyService.deleteFeedbackReply(replyId); + + Member myMember = memberService.findOne(myMemberId); + // 조회한 post의 feedback 조회 및 각 feedback에 대한 feedbackReply 조회 -> 이후 ResFeedbackDto로 매핑 + List resFeedbackDto2s = memberUtils.feedbackList(reply.getPost().getPostId(), myMember); + + + return ResponseEntity.ok(new BaseResponse<>(CustomMessage.OK, resFeedbackDto2s)); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "피드백 대댓글에 좋아요를 성공적으로 등록했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/reference/feedback_reply/{reply_id}/like") // 피드백 좋아요 + @Operation(summary = "피드백 대댓글 좋아요", description = "피드백 대댓글에 좋아요를 누릅니다.") + public ResponseEntity> likeFeedback(@PathVariable("reply_id") Long replyId, + @AuthenticationPrincipal MemberDetails memberDetails) { + + log.info("EndPoint Post /reference/feedback/{feedback_member_log_id}/like"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + feedbackReplyService.likeFeedbackReply(myMember, replyId); + int count = feedbackReplyService.feedbackReplyLikeCount(replyId); + ResFeedbackReplyLikeDto dto = new ResFeedbackReplyLikeDto(count); + return ResponseEntity.ok(new BaseResponse<>(CustomMessage.OK, dto)); + } + + +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Domain/Feedback.java b/src/main/java/Remoa/BE/Web/Feedback/Domain/Feedback.java new file mode 100644 index 0000000..f483c94 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Domain/Feedback.java @@ -0,0 +1,94 @@ +package Remoa.BE.Web.Feedback.Domain; + +import Remoa.BE.Web.Comment.Domain.CommentReply; +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import jakarta.persistence.CascadeType; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.*; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.List; + +import static jakarta.persistence.FetchType.LAZY; + +@Entity +@Getter +@Setter +@SQLDelete(sql = "UPDATE feedback SET deleted = true WHERE feedback_id = ?") +@SQLRestriction("deleted = false") // 검색시 deleted = false 조건을 where 절에 추가 +public class Feedback { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "feedback_id") + private Long feedbackId; + + /** + * Feedback이 속해있는 Post + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + /** + * Feedback을 작성한 Member + */ + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Column(name = "page_number") + private Integer pageNumber; + + /** + * Feedback의 내용 + */ + @Lob + @Column(name = "content", length = 400) + private String content; + + /** + * Feedback이 작성된 시간 + */ + @Column(name = "feedback_time") + private LocalDateTime feedbackTime; + + /** + * Feedback의 좋아요 숫자 + */ + @Column(name = "like_count") + private Integer likeCount = 0; + + + private Boolean deleted = Boolean.FALSE; + + + public static Feedback createFeedback(Post post, Member member, Integer pageNumber, String content, LocalDateTime time) { + if (content.length() > 300) { + throw new BaseException(CustomMessage.INVALID_CONTENT_LENGTH); + } + Feedback feedbackObj = new Feedback(); + feedbackObj.setPost(post); + feedbackObj.setMember(member); + feedbackObj.setPageNumber(pageNumber); + feedbackObj.setContent(content); + feedbackObj.setLikeCount(0); + feedbackObj.setFeedbackTime(time); + return feedbackObj; + } + + public void setContent(String content) { + if (content.length() > 300) { + throw new BaseException(CustomMessage.INVALID_CONTENT_LENGTH); + } + this.content = content; + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Member/Domain/FeedbackLike.java b/src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackLike.java similarity index 69% rename from src/main/java/Remoa/BE/Member/Domain/FeedbackLike.java rename to src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackLike.java index 3279b09..1c52188 100644 --- a/src/main/java/Remoa/BE/Member/Domain/FeedbackLike.java +++ b/src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackLike.java @@ -1,11 +1,12 @@ -package Remoa.BE.Member.Domain; +package Remoa.BE.Web.Feedback.Domain; +import Remoa.BE.Web.Member.Domain.Member; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; +import jakarta.persistence.*; /** * Member가 Feedback을 Like할 때 사용 @@ -26,12 +27,12 @@ public class FeedbackLike { private Member member; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "feedback_id") - private Feedback feedback; + @JoinColumn(name = "feedback_member_log_id") + private FeedbackMemberLog feedbackMemberLog; - public static FeedbackLike createFeedbackLike(Member member, Feedback feedback) { + public static FeedbackLike createFeedbackLike(Member member, FeedbackMemberLog feedbackMemberLog) { FeedbackLike feedbackLike = new FeedbackLike(); - feedbackLike.setFeedback(feedback); + feedbackLike.setFeedbackMemberLog(feedbackMemberLog); feedbackLike.setMember(member); return feedbackLike; diff --git a/src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackMemberLog.java b/src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackMemberLog.java new file mode 100644 index 0000000..b3055a2 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackMemberLog.java @@ -0,0 +1,63 @@ +package Remoa.BE.Web.Feedback.Domain; + + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +import java.util.List; + +import static jakarta.persistence.FetchType.LAZY; + +@Entity +@Getter +@SQLDelete(sql = "UPDATE feedback_member_log SET deleted = true WHERE feedback_member_log_id = ?") +@SQLRestriction("deleted = false") // 검색시 deleted = false 조건을 where 절에 추가 +@NoArgsConstructor +public class FeedbackMemberLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "feedback_member_log_id") + Long feedbackMemberLogId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Column(name = "like_count") + private Integer likeCount = 0; + + private Boolean deleted = Boolean.FALSE; + + + @OneToMany(mappedBy = "feedbackMemberLog", cascade = {CascadeType.REMOVE}, fetch = LAZY) + //@OnDelete(action = OnDeleteAction.CASCADE) + private List feedbackLikes; + + @OneToMany(mappedBy = "feedbackMemberLog", cascade = {CascadeType.REMOVE}, fetch = LAZY) + //@OnDelete(action = OnDeleteAction.CASCADE) + private List feedbackReplies; + + public void increaseLikeCount(){ + this.likeCount++; + } + + public void decreaseLikeCount(){ + this.likeCount--; + } + + public FeedbackMemberLog(Member member, Post post) { + this.member = member; + this.post = post; + } +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackReply.java b/src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackReply.java new file mode 100644 index 0000000..00d0c5f --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackReply.java @@ -0,0 +1,96 @@ +package Remoa.BE.Web.Feedback.Domain; + +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +import java.time.LocalDateTime; +import java.util.List; + +import static jakarta.persistence.FetchType.LAZY; + +@Entity +@Getter +@Setter +@SQLDelete(sql = "UPDATE feedback_reply SET deleted = true WHERE feedback_reply_id = ?") +@SQLRestriction("deleted = false") // 검색시 deleted = false 조건을 where 절에 추가 +public class FeedbackReply { + + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "feedback_reply_id") + private Long feedbackReplyId; + + /** + * FeedbackReply 속해있는 Post + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + /** + * FeedbackReply 작성한 Member + */ + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "feedback_member_log_id") + private FeedbackMemberLog feedbackMemberLog; + + /** + * FeedbackReply 내용 + */ + private String content; + + /** + * FeedbackReply 좋아요 숫자 + */ + @Column(name = "like_count") + private Integer likeCount = 0; + + /** + * FeedbackReply 작성된 시간 + */ + @Column(name = "feedback_reply_time") + private LocalDateTime feedbackReplyTime; + + + @OneToMany(mappedBy = "feedbackReply", cascade = {CascadeType.REMOVE}, fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + private List feedbackReplyLikes; + + private Boolean deleted = Boolean.FALSE; + + public static FeedbackReply createFeedbackReply(Post post, Member member, FeedbackMemberLog feedbackMemberLog, String content) { + if (content.length() > 300) { + throw new BaseException(CustomMessage.INVALID_CONTENT_LENGTH); + } + FeedbackReply feedbackReply = new FeedbackReply(); + feedbackReply.setPost(post); + feedbackReply.setMember(member); + feedbackReply.setFeedbackMemberLog(feedbackMemberLog); + feedbackReply.setContent(content); + feedbackReply.setLikeCount(0); + feedbackReply.setFeedbackReplyTime(LocalDateTime.now()); + return feedbackReply; + } + + public void setContent(String content) { + if (content.length() > 300) { + throw new BaseException(CustomMessage.INVALID_CONTENT_LENGTH); + } + this.content = content; + } +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackReplyLike.java b/src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackReplyLike.java new file mode 100644 index 0000000..7236b86 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Domain/FeedbackReplyLike.java @@ -0,0 +1,32 @@ +package Remoa.BE.Web.Feedback.Domain; + +import Remoa.BE.Web.Member.Domain.Member; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +public class FeedbackReplyLike { + + @Id + @GeneratedValue + @Column(name = "feedback_reply_like_id") + private Long feedbackReplyLikeId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "feedback_reply_id") + private FeedbackReply feedbackReply; + + public static FeedbackReplyLike createFeedbackReplyLike(Member member, FeedbackReply feedbackReply) { + FeedbackReplyLike feedbackReplyLike = new FeedbackReplyLike(); + feedbackReplyLike.setFeedbackReply(feedbackReply); + feedbackReplyLike.setMember(member); + return feedbackReplyLike; + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackDto2.java b/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackDto2.java new file mode 100644 index 0000000..1d8be66 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackDto2.java @@ -0,0 +1,53 @@ +package Remoa.BE.Web.Feedback.Dto; + + +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Feedback.Domain.FeedbackMemberLog; +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class ResFeedbackDto2 { + + @Schema(description = "피드백 작성자 정보") + private ResMemberInfoDto member; + + @Schema(description = "피드백-회원 로깅 ID") + private Long feedbackMemberLogId; + + @Schema(description = "좋아요 수", example = "10") + private Integer likeCount; + + @Schema(description = "현재 사용자가 피드백 작성자를 좋아하는지 여부", example = "true") + private Boolean isLiked; + + @Schema(description = "해당 회원의 피드백 정보") + private List feedbackInfos; + + @Schema(description = "피드백에 대한 답글 목록") + private List replies; + + + public ResFeedbackDto2(ResMemberInfoDto member, + FeedbackMemberLog feedbackMemberLog, + Boolean isLiked, + List feedbackInfos, + List replies) { + this.member = member; + this.feedbackMemberLogId = feedbackMemberLog.getFeedbackMemberLogId(); + this.likeCount = feedbackMemberLog.getLikeCount(); + this.isLiked = isLiked; + this.feedbackInfos = feedbackInfos; + this.replies = replies; + } +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackInfoDto.java b/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackInfoDto.java new file mode 100644 index 0000000..964e627 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackInfoDto.java @@ -0,0 +1,39 @@ +package Remoa.BE.Web.Feedback.Dto; + +import Remoa.BE.Web.Feedback.Domain.Feedback; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +public class ResFeedbackInfoDto { + + @Schema(description = "피드백 ID", example = "123") + private Long feedbackId; + + @Schema(description = "피드백 내용", example = "좋은 정보 감사합니다.") + private String feedback; + + @Schema(description = "페이지 번호", example = "1") + private Integer page; + + @Schema(description = "삭제여부", example = "false") + private Boolean isDeleted; + + @Schema(description = "피드백 작성 시간", example = "2024-04-05T08:30:00") + private LocalDateTime feedbackTime; + + + + public ResFeedbackInfoDto(Feedback feedback) { + this.feedbackId = feedback.getFeedbackId(); + this.feedback = feedback.getContent(); + this.page = feedback.getPageNumber(); + this.isDeleted = feedback.getDeleted(); + this.feedbackTime = feedback.getFeedbackTime(); + + } +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackLikeDto.java b/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackLikeDto.java new file mode 100644 index 0000000..bcbf832 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackLikeDto.java @@ -0,0 +1,14 @@ +package Remoa.BE.Web.Feedback.Dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ResFeedbackLikeDto { + @Schema(description = "좋아요 수", example = "21") + private int likeCount; +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackReplyDto.java b/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackReplyDto.java new file mode 100644 index 0000000..81c4c40 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackReplyDto.java @@ -0,0 +1,47 @@ +package Remoa.BE.Web.Feedback.Dto; + +import Remoa.BE.Web.Feedback.Domain.FeedbackReply; +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ResFeedbackReplyDto { + + @Schema(description = "피드백 답글 ID", example = "4") + private Long feedbackReplyId; + + @Schema(description = "작성자 정보") + private ResMemberInfoDto member; + + @Schema(description = "피드백 답글 내용", example = "안녕하세요") + private String content; + + @Schema(description = "좋아요 수", example = "0") + private Integer likeCount; + + @Schema(description = "좋아요 여부") + private Boolean isLiked; + + @Schema(description = "삭제여부", example = "false") + private Boolean isDeleted; + + @Schema(description = "피드백 답글 작성 시간", example = "2023-03-27T23:18:47") + private LocalDateTime feedbackReplyTime; + + public ResFeedbackReplyDto(FeedbackReply feedbackReply, Boolean isLiked, Boolean isFollow) { + this.feedbackReplyId = feedbackReply.getFeedbackReplyId(); + this.member = new ResMemberInfoDto(feedbackReply.getMember(), isFollow); + this.content = feedbackReply.getContent(); + this.likeCount = feedbackReply.getLikeCount(); + this.isLiked = isLiked; + this.isDeleted = feedbackReply.getDeleted(); + this.feedbackReplyTime = feedbackReply.getFeedbackReplyTime(); + } +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackReplyLikeDto.java b/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackReplyLikeDto.java new file mode 100644 index 0000000..bcf348e --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Dto/ResFeedbackReplyLikeDto.java @@ -0,0 +1,14 @@ +package Remoa.BE.Web.Feedback.Dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ResFeedbackReplyLikeDto { + @Schema(description = "좋아요 수", example = "21") + private int likeCount; +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackLikeRepository.java b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackLikeRepository.java new file mode 100644 index 0000000..817984e --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackLikeRepository.java @@ -0,0 +1,21 @@ +package Remoa.BE.Web.Feedback.Repository; + +import Remoa.BE.Web.Feedback.Domain.FeedbackLike; +import Remoa.BE.Web.Feedback.Domain.FeedbackMemberLog; +import Remoa.BE.Web.Member.Domain.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 org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface FeedbackLikeRepository extends JpaRepository { + Optional findByMemberAndFeedbackMemberLog(Member member, FeedbackMemberLog feedbackMemberLog); + + @Modifying + @Query(value = "delete from feedback_like fl where feedback_member_log_id = :feedbackMemberLogId", nativeQuery = true) + void deleteByFeedbackHard(@Param("feedbackMemberLogId") Long feedbackMemberLogId); +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackMemberLogRepository.java b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackMemberLogRepository.java new file mode 100644 index 0000000..1fdeb85 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackMemberLogRepository.java @@ -0,0 +1,32 @@ +package Remoa.BE.Web.Feedback.Repository; + +import Remoa.BE.Web.Feedback.Domain.FeedbackMemberLog; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +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; +import java.util.Optional; + +public interface FeedbackMemberLogRepository extends JpaRepository { + + @Query(value = "select * from feedback_member_log where post_id = :postId", nativeQuery = true) + List findByPostHard(@Param("postId") Long postId); + + @Modifying + @Query(value = "delete from feedback_member_log fml where post_id = :postId", nativeQuery = true) + void deleteFeedbackLogByPostHard(@Param("postId") Long postId); + + @Modifying + @Query(value = "delete from feedback_member_log fml where member_id = :memberId", nativeQuery = true) + void deleteByMemberHard(@Param("memberId") Long memberId); + + boolean existsByMemberAndPost(Member member, Post post); + + Optional findByMemberAndPost(Member member, Post post); + + void deleteByMemberAndPost(Member member, Post post); +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackReplyLikeRepository.java b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackReplyLikeRepository.java new file mode 100644 index 0000000..c37bda1 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackReplyLikeRepository.java @@ -0,0 +1,13 @@ +package Remoa.BE.Web.Feedback.Repository; + +import Remoa.BE.Web.Feedback.Domain.FeedbackReply; +import Remoa.BE.Web.Feedback.Domain.FeedbackReplyLike; +import Remoa.BE.Web.Member.Domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface FeedbackReplyLikeRepository extends JpaRepository { + + OptionalfindByMemberAndFeedbackReply(Member member, FeedbackReply feedbackReply); +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackReplyRepository.java b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackReplyRepository.java new file mode 100644 index 0000000..4b44c44 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackReplyRepository.java @@ -0,0 +1,26 @@ +package Remoa.BE.Web.Feedback.Repository; + +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Feedback.Domain.FeedbackMemberLog; +import Remoa.BE.Web.Feedback.Domain.FeedbackReply; +import Remoa.BE.Web.Member.Domain.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 FeedbackReplyRepository extends JpaRepository { + + List findByFeedbackMemberLogOrderByFeedbackReplyTimeAsc(FeedbackMemberLog feedbackMemberLog); + + + @Modifying + @Query(value = "delete from feedback_reply fr where fr.member_id = :memberId", nativeQuery = true) + void deleteByMemberHard(@Param("memberId") Long memberId); + + @Modifying + @Query(value = "delete from feedback_reply fr where fr.feedback_member_log_id = :feedbackMemberLogId", nativeQuery = true) + void deleteByFeedbackMemberLogHard(@Param("feedbackMemberLogId")Long feedbackMemberLogId); +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackRepository.java b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackRepository.java new file mode 100644 index 0000000..28bc7cb --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackRepository.java @@ -0,0 +1,54 @@ +package Remoa.BE.Web.Feedback.Repository; + +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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 org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +public interface FeedbackRepository extends JpaRepository, FeedbackRepositoryCustom { + + @Query(value = "select * from Feedback f where post_id = :postId", nativeQuery = true) + List findFeedbackByPostHard(@Param("postId") Long postId); + + @Modifying + @Query(value = "delete from feedback f where post_id = :postId", nativeQuery = true) + void deleteFeedbackByPostHard(@Param("postId") Long postId); + + Page findByMemberOrderByFeedbackTimeDesc(Pageable pageable, Member member); + + @Modifying + @Transactional + @Query("DELETE FROM Feedback f WHERE f.post = :post") + void deleteByPost(Post post); + + boolean existsByMemberAndPostAndPageNumber(Member member, Post post, Integer pageNumber); + boolean existsByMemberAndPost(Member member, Post post); + + @Query("SELECT f FROM Feedback f " + + "INNER JOIN FETCH f.post p " + + "WHERE f.member = :member " + + "AND f.feedbackTime = (SELECT MAX(f2.feedbackTime) FROM Feedback f2 WHERE f2.post.postId = f.post.postId) " + + "ORDER BY f.feedbackTime DESC") + Page findNewestFeedback(Member member, Pageable pageable); + + + @Query("SELECT f FROM Feedback f " + + "INNER JOIN FETCH f.post p " + + "WHERE f.member = :member " + + "AND f.feedbackTime = (SELECT MIN(f2.feedbackTime) FROM Feedback f2 WHERE f2.post.postId = f.post.postId) " + + "ORDER BY f.feedbackTime ASC") + Page findOldestFeedback(Member member, Pageable pageable); + + + @Modifying + @Query(value = "delete from feedback f where member_id = :memberId", nativeQuery = true) + void deleteFeedbackByMemberHard(@Param("memberId") Long memberId); +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackRepositoryCustom.java b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackRepositoryCustom.java new file mode 100644 index 0000000..3abede6 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackRepositoryCustom.java @@ -0,0 +1,47 @@ +package Remoa.BE.Web.Feedback.Repository; + +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Feedback.Domain.FeedbackLike; +import Remoa.BE.Web.Member.Domain.FeedbackBookmark; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +public interface FeedbackRepositoryCustom { + + Optional findOne(Long id); + + void saveFeedback(Feedback feedback); + + Optional findByFeedbackId(Long feedbackId); + + List findByPost(Post post); + + List findRepliesOfParentFeedback(Feedback parentFeedback); + + void saveFeedbackLike(FeedbackLike feedbackLike); + + Optional findMemberCommendLike(Member member, Feedback feedback); + + Integer findFeedbackLike(Feedback feedback); + + void saveFeedbackBookmark(FeedbackBookmark feedbackBookmark); + + Optional findMemberCommendBookmark(Member member, Feedback feedback); + + void updateFeedback(Feedback newFeedback); + + void deleteFeedback(Feedback feedback); + + List findAllByMember(Member member); + + + + void deleteChildFeedbackByParentFeedback(Feedback feedback); + + Page findMyFeedback(Member member, Pageable pageable, String sort); +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackRepositoryCustomImpl.java b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackRepositoryCustomImpl.java new file mode 100644 index 0000000..8845c38 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Repository/FeedbackRepositoryCustomImpl.java @@ -0,0 +1,184 @@ +package Remoa.BE.Web.Feedback.Repository; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Comment.Domain.QComment; +import Remoa.BE.Web.Feedback.Domain.QFeedback; +import Remoa.BE.Web.Member.Domain.QMember; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Member.Domain.FeedbackBookmark; +import Remoa.BE.Web.Feedback.Domain.FeedbackLike; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.QPost; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.EntityManager; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class FeedbackRepositoryCustomImpl implements FeedbackRepositoryCustom { + + private final EntityManager em; + public Optional findOne(Long id){ + return Optional.ofNullable(em.find(Feedback.class, id)); + } + public void saveFeedback(Feedback feedback) { + em.persist(feedback); + } + + public Optional findByFeedbackId(Long feedbackId) { + return Optional.ofNullable(em.find(Feedback.class, feedbackId)); + } + + /** + * 포스트 별 코멘트을 찾아오기 위한 메서드 + * @param post + * @return List + */ + public List findByPost(Post post) { + return em.createQuery("select f from Feedback f where f.post = :post order by f.feedbackTime asc", + Feedback.class) + .setParameter("post", post) + .getResultList(); + } + + public List findRepliesOfParentFeedback(Feedback parentFeedback) { + return em.createQuery("select f from Feedback f " + + "where f.parentFeedback = :feedback order by f.feedbackTime desc", Feedback.class) + .setParameter("feedback", parentFeedback) + .getResultList(); + } + + public void saveFeedbackLike(FeedbackLike feedbackLike) { + em.persist(feedbackLike); + } + + /** + * feedbackLikeAction 메서드를 실행하기 전 이미 해당 댓글에 대한 좋아요를 했는지 검증->service 단에서 return 값의 null이면 좋아요 가능. + * 혹은 좋아요 취소를 위해 사용할 수도 있다. + */ + public Optional findMemberCommendLike(Member member, Feedback feedback) { + return em.createQuery("select cl from FeedbackLike cl " + + "where cl.feedback = :feedback and cl.member = :member", FeedbackLike.class) + .setParameter("feedback", feedback) + .setParameter("member", member) + .getResultStream() + .findAny(); + } + + public Integer findFeedbackLike(Feedback feedback) { + return em.createQuery("select cl from FeedbackLike cl where cl.feedback = :feedback", FeedbackLike.class) + .setParameter("feedback", feedback) + .getResultList() + .size(); + } + + public void saveFeedbackBookmark(FeedbackBookmark feedbackBookmark) { + em.persist(feedbackBookmark); + } + + /** + * feedbackBookmarkAction 메서드를 실행하기 전 이미 해당 코멘트에 대한 북마크를 했는지 검증->service 단에서 return 값의 null이면 북마크 가능. + * 혹은 북마크 해제를 위해 사용할 수도 있다. + */ + public Optional findMemberCommendBookmark(Member member, Feedback feedback) { + return em.createQuery("select cb from FeedbackBookmark cb " + + "where cb.feedback = :feedback and cb.member = :member", FeedbackBookmark.class) + .setParameter("feedback", feedback) + .setParameter("member", member) + .getResultStream() + .findAny(); + } + + public void updateFeedback(Feedback newFeedback){ + em.merge(newFeedback); + } + + /* //필요없을 거 같아서 주석처리... + public Integer findFeedbackBookmark(Feedback feedback) { + return em.createQuery("select cb from FeedbackBookmark cb where cb.feedback = :feedback", FeedbackBookmark.class) + .setParameter("feedback", feedback) + .getResultList() + .size(); + }*/ + + public void deleteFeedback(Feedback feedback){ + em.remove(feedback); + } + + public List findAllByMember(Member member) { + return em.createQuery("select f from Feedback f " + + "where f.member = :member", Feedback.class) + .setParameter("member", member) + .getResultList(); + } + + + public void deleteChildFeedbackByParentFeedback(Feedback feedback){ + em.createQuery("delete from Feedback f where f.parentFeedback = :feedback") + .setParameter("feedback", feedback) + .executeUpdate(); + } + + + + private final JPAQueryFactory jpaQueryFactory; + QFeedback feedback = QFeedback.feedback; + QMember member = QMember.member; + QPost post = QPost.post; + + @Override + public Page findMyFeedback(Member myMember, Pageable pageable, String sort) { + + + // Subquery to find the latest commented time per post by the member + JPAQuery subQuery = jpaQueryFactory + .select(feedback.feedbackTime.max()) + .from(feedback) + .where(feedback.post.postId.eq(post.postId) + .and(feedback.member.eq(myMember))); + + // Main query to fetch comments with latest commented time per post + JPAQuery query = jpaQueryFactory + .select(feedback) + .from(feedback) + .innerJoin(feedback.post, post).fetchJoin() + .innerJoin(feedback.member, member).fetchJoin() + .where(feedback.member.eq(myMember) + .and(feedback.feedbackTime.eq(subQuery))); + + // Apply sorting based on the sort parameter + OrderSpecifier orderSpecifier; + if ("asc".equalsIgnoreCase(sort)) { + orderSpecifier = feedback.feedbackTime.asc(); + } else { + orderSpecifier = feedback.feedbackTime.desc(); + } + query.orderBy(orderSpecifier); + + // Fetch total count for pagination + long total = query.fetchCount(); + + // Apply pagination to the query + List fetch = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // Convert to Page + return new PageImpl<>(fetch, pageable, total); + } +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Service/FeedbackReplyService.java b/src/main/java/Remoa/BE/Web/Feedback/Service/FeedbackReplyService.java new file mode 100644 index 0000000..c056941 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Service/FeedbackReplyService.java @@ -0,0 +1,115 @@ +package Remoa.BE.Web.Feedback.Service; + + +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Feedback.Domain.FeedbackMemberLog; +import Remoa.BE.Web.Feedback.Domain.FeedbackReply; +import Remoa.BE.Web.Feedback.Domain.FeedbackReplyLike; +import Remoa.BE.Web.Feedback.Repository.FeedbackMemberLogRepository; +import Remoa.BE.Web.Feedback.Repository.FeedbackReplyLikeRepository; +import Remoa.BE.Web.Feedback.Repository.FeedbackReplyRepository; +import Remoa.BE.Web.Feedback.Repository.FeedbackRepository; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Repository.PostRepository; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class FeedbackReplyService { + + private final PostRepository postRepository; + private final FeedbackReplyRepository feedbackReplyRepository; + private final FeedbackReplyLikeRepository feedbackReplyLikeRepository; + private final FeedbackMemberLogRepository feedbackMemberLogRepository; + + + @Transactional + public void deleteByMember(Member member){ + feedbackReplyRepository.deleteByMemberHard(member.getMemberId()); + } + + @Transactional + public FeedbackReply registerFeedbackReply(Member member, Long postId, Long feedbackMemberLogId, String content) { + validateContent(content); + Post post = postRepository.findById(postId).orElseThrow(() -> new BaseException(CustomMessage.NO_ID));; + FeedbackMemberLog feedbackMemberLog = feedbackMemberLogRepository.findById(feedbackMemberLogId).orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + FeedbackReply feedbackReply = FeedbackReply.createFeedbackReply(post, member, feedbackMemberLog, content); + feedbackReplyRepository.save(feedbackReply); + return feedbackReply; + } + + private void validateContent(String content){ + if(content == null || content.isEmpty()){ + throw new BaseException(CustomMessage.EMPTY_CONTENT); + } + } + + @Transactional + public void deleteByFeedbackMemberLog(FeedbackMemberLog feedbackMemberLog){ + feedbackReplyRepository.deleteByFeedbackMemberLogHard(feedbackMemberLog.getFeedbackMemberLogId()); + } + + + public List findFeedbackReplies(FeedbackMemberLog feedbackMemberLog) { + return feedbackReplyRepository.findByFeedbackMemberLogOrderByFeedbackReplyTimeAsc(feedbackMemberLog); + } + + public FeedbackReply findOne(Long replyId) { + return feedbackReplyRepository.findById(replyId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Feedback reply not found")); + } + + @Transactional + public void modifyFeedbackReply(String content, Long feedbackReplyId) { + validateContent(content); + FeedbackReply feedbackReply = feedbackReplyRepository.findById(feedbackReplyId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Feedback reply not found")); + feedbackReply.setContent(content); // 변경 감지 + } + + @Transactional + public void deleteFeedbackReply(Long feedbackReplyId) { + FeedbackReply feedbackReply = feedbackReplyRepository.findById(feedbackReplyId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Feedback reply not found")); + feedbackReplyRepository.delete(feedbackReply); + } + + public Optional findFeedbackReplyLike(Member member, FeedbackReply feedbackReply) { + return feedbackReplyLikeRepository.findByMemberAndFeedbackReply(member, feedbackReply); + } + + @Transactional + public void likeFeedbackReply(Member myMember, Long feedbackReplyId) { + FeedbackReply feedbackReplyObj = findOne(feedbackReplyId); + Integer feedbackReplyLikeCount = feedbackReplyObj.getLikeCount(); + + // FeedbackReplyLike를 db에서 조회해보고 조회 결과가 null이면 like+=1, FeedbackReplyLike 엔티티 추가 + // null이 아니면 like -=1, 조회결과인 해당 FeedbackReplyLike 엔티티 삭제 + Optional feedbackReplyLike = findFeedbackReplyLike(myMember, feedbackReplyObj); + if (feedbackReplyLike.isEmpty()) { + feedbackReplyObj.setLikeCount(feedbackReplyLikeCount + 1); // 좋아요 수 1 증가 + FeedbackReplyLike feedbackReplyLikeObj = FeedbackReplyLike.createFeedbackReplyLike(myMember, feedbackReplyObj); + feedbackReplyLikeRepository.save(feedbackReplyLikeObj); + } else { + feedbackReplyObj.setLikeCount(feedbackReplyLikeCount - 1); // 좋아요 수 1 차감 + feedbackReplyLikeRepository.deleteById(feedbackReplyLike.get().getFeedbackReplyLikeId()); // db에서 삭제 + } + } + + public int feedbackReplyLikeCount(Long feedbackReplyId) { + FeedbackReply feedbackReply = findOne(feedbackReplyId); + return feedbackReply.getLikeCount(); + } + +} diff --git a/src/main/java/Remoa/BE/Web/Feedback/Service/FeedbackService.java b/src/main/java/Remoa/BE/Web/Feedback/Service/FeedbackService.java new file mode 100644 index 0000000..b0327c3 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Feedback/Service/FeedbackService.java @@ -0,0 +1,201 @@ +package Remoa.BE.Web.Feedback.Service; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Feedback.Domain.FeedbackLike; +import Remoa.BE.Web.Feedback.Domain.FeedbackMemberLog; +import Remoa.BE.Web.Feedback.Repository.FeedbackMemberLogRepository; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Feedback.Repository.FeedbackLikeRepository; +import Remoa.BE.Web.Feedback.Repository.FeedbackRepository; +import Remoa.BE.Web.Member.Domain.*; +import Remoa.BE.Web.CommentFeedback.Service.CommentFeedbackService; +import Remoa.BE.Web.Post.Repository.PostRepository; +import Remoa.BE.Web.Post.Service.PostService; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static Remoa.BE.utill.Constant.CONTENT_PAGE_SIZE; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FeedbackService { + private final FeedbackRepository feedbackRepository; + private final FeedbackLikeRepository feedbackLikeRepository; + private final PostRepository postRepository; + private final CommentFeedbackService commentFeedbackService; + private final FeedbackMemberLogRepository feedbackMemberLogRepository; + + @Transactional + public void deleteFeedbackByMember(Member member){ + feedbackRepository.deleteFeedbackByMemberHard(member.getMemberId()); + } + + @Transactional + public void deleteFeedbackLogByMember(Member member){ + feedbackMemberLogRepository.deleteByMemberHard(member.getMemberId()); + } + + @Transactional + public Feedback findOne(Long feedbackId) { + Optional feedback = feedbackRepository.findOne(feedbackId); + return feedback.orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + } + + @Transactional + public void deleteFeedbackByPost(Post post){ + feedbackRepository.deleteFeedbackByPostHard(post.getPostId()); + } + + @Transactional + public void deleteFeedbackLogByPost(Post post){ + feedbackMemberLogRepository.deleteFeedbackLogByPostHard(post.getPostId()); + } + + @Transactional + public void deleteFeedbackLikeByFeedBack(FeedbackMemberLog feedbackMemberLog){ + feedbackLikeRepository.deleteByFeedbackHard(feedbackMemberLog.getFeedbackMemberLogId()); + } + + @Transactional + public List findFeedbackByPostHard(Post post){ + return feedbackRepository.findFeedbackByPostHard(post.getPostId()); + } + + public List findFeedbackMemberLogByPostHard(Post post){ + return feedbackMemberLogRepository.findByPostHard(post.getPostId()); + } + + @Transactional + public void deleteByPost(Post post){ + feedbackRepository.deleteByPost(post); + } + + public Page getMyFeedback(int page, Member member, String sortDirection) { + Pageable pageable = PageRequest.of(page, CONTENT_PAGE_SIZE); + if (sortDirection.equalsIgnoreCase("desc")) { + return feedbackRepository.findMyFeedback(member, pageable, "desc"); + } else { + return feedbackRepository.findMyFeedback(member, pageable, "asc"); + } + } + + public int feedbackLikeCount(Long feedbackId) { + Feedback feedback = findOne(feedbackId); + return feedback.getLikeCount(); + } + + public List findAllFeedbacksOfPost(Long postId) { + Post post = postRepository.findById(postId).orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + ; + return feedbackRepository.findByPost(post); + } + + + public Optional findFeedbackMemberLike(Member member, FeedbackMemberLog feedbackMemberLog) { + return feedbackLikeRepository.findByMemberAndFeedbackMemberLog(member, feedbackMemberLog); + } + + public FeedbackMemberLog findFeedbackMemberLog(Member member, Post post) { + return feedbackMemberLogRepository.findByMemberAndPost(member, post) + .orElseThrow(() -> new BaseException(CustomMessage.POST_MEMBER_FEEDBACK_NOT_EXIST)); + } + + private void validateContent(String content){ + if(content == null || content.isEmpty()){ + throw new BaseException(CustomMessage.EMPTY_CONTENT); + } + } + + @Transactional + public void registerFeedback(Member member, String content, Long postId, Integer pageNumber) { + validateContent(content); + Post post = postRepository.findById(postId).orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + + if (feedbackRepository.existsByMemberAndPostAndPageNumber(member, post, pageNumber)) { + throw new BaseException(CustomMessage.PAGE_FEEDBACK_ALREADY_EXISTS); + } + + if (!feedbackMemberLogRepository.existsByMemberAndPost(member, post)) { // 포스트멤버피드백 없으면 등록 + feedbackMemberLogRepository.save(new FeedbackMemberLog(member, post)); + } + + LocalDateTime time = LocalDateTime.now(); + Feedback feedbackObj = Feedback.createFeedback(post, member, pageNumber, content, time); + + feedbackRepository.saveFeedback(feedbackObj); + + commentFeedbackService.saveCommentFeedback(null, feedbackObj, ContentType.FEEDBACK, member, post, time); + + } + + @Transactional + public void modifyFeedback(Member member, Post post, String content, Long feedbackId) { + validateContent(content); + Feedback feedbackObj = findOne(feedbackId); + if (!Objects.equals(feedbackObj.getMember().getMemberId(), member.getMemberId())) { // 자신이 적은 피드백 여부 확인 + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + + feedbackObj.setContent(content); + commentFeedbackService.findFeedback(feedbackObj).setFeedback(feedbackObj); + + // feedbackRepository.updateFeedback(feedbackObj); //알아서 수정 됨 + } + + @Transactional + public void deleteFeedback(Member member, Post post, Long feedbackId) { + Feedback feedbackObj = findOne(feedbackId); + + if (!Objects.equals(feedbackObj.getMember().getMemberId(), member.getMemberId())) { // 자신이 적은 피드백 여부 확인 + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + + CommentFeedback feedbackOfCommentFeedback = commentFeedbackService.findFeedback(feedbackObj); + feedbackOfCommentFeedback.setDeleted(true); + + feedbackRepository.delete(feedbackObj); + if (!feedbackRepository.existsByMemberAndPost(member, post)) { //더이상 해당 포스트에 작성한 피드백 존재하지 않는다면 + feedbackMemberLogRepository.deleteByMemberAndPost(member, post); // 포스트멤버피드백로그 삭제 + } + } + + @Transactional + public int likeFeedback(Member myMember, Post post , Member feedbackMember) { + + FeedbackMemberLog feedbackMemberLog = feedbackMemberLogRepository.findByMemberAndPost(feedbackMember, post) + .orElseThrow(() -> new BaseException(CustomMessage.POST_MEMBER_FEEDBACK_NOT_EXIST)); //포스트에 피드백 없으면 에러 + + //FeedbackLike를 db에서 조회해보고 조회 결과가 null이면 like+=1, FeedbackLike 엔티티 추가 + // null이 아니면 like -=1, 조회결과인 해당 FeedbackLike 엔티티 삭제 + Optional feedbackLike = findFeedbackMemberLike(myMember, feedbackMemberLog); + if (feedbackLike.isEmpty()) { + FeedbackLike feedbackLikeObj = FeedbackLike.createFeedbackLike(myMember, feedbackMemberLog); // 좋아요 생성 + feedbackMemberLog.increaseLikeCount(); // 대상 피드백 멤버 좋아요 수 1 증가 + feedbackLikeRepository.save(feedbackLikeObj); + return feedbackMemberLog.getLikeCount(); + } else { + feedbackMemberLog.decreaseLikeCount(); // 좋아요 수 1 차감 + feedbackLikeRepository.deleteById(feedbackLike.get().getFeedbackLikeId()); // db에서 삭제 + return feedbackMemberLog.getLikeCount(); + } + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Controller/InquiryController.java b/src/main/java/Remoa/BE/Web/Inquiry/Controller/InquiryController.java new file mode 100644 index 0000000..629b80a --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Controller/InquiryController.java @@ -0,0 +1,136 @@ +package Remoa.BE.Web.Inquiry.Controller; + +import Remoa.BE.Web.Inquiry.Dto.Req.ReqInquiryDto; +import Remoa.BE.Web.Inquiry.Dto.Res.ResInquiryDetailDto; +import Remoa.BE.Web.Inquiry.Dto.Res.ResInquiryPaging; +import Remoa.BE.Web.Inquiry.Service.InquiryService; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "문의 기능 Test Completed", description = "문의 기능 API") +@RestController +@RequiredArgsConstructor +@Slf4j +public class InquiryController { + + private final InquiryService inquiryService; + private final MemberService memberService; + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "문의가 성공적으로 등록되었습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/inquiry") + @Operation(summary = "문의 등록 Test Completed", description = "문의를 등록합니다.") + public ResponseEntity postInquiry(@Validated @RequestBody ReqInquiryDto inquiryDto, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Post /inquiry"); + + Member myMember = memberService.findOne(memberDetails.getMemberId()); + inquiryService.registerInquiry(inquiryDto, myMember.getNickname()); + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "문의가 성공적으로 수정되었습니다."), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PutMapping("/inquiry/{id}") + @Operation(summary = "문의 수정 Test Completed", description = "문의를 수정합니다.") + public ResponseEntity updateInquiry(@PathVariable("id") Long inquiryId, + @Validated @RequestBody ReqInquiryDto inquiryDto, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Put /inquiry/{id}"); + + Member myMember = memberService.findOne(memberDetails.getMemberId()); + inquiryService.updateInquiry(inquiryId, inquiryDto, myMember); + + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "문의가 성공적으로 수정되었습니다."), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/inquiry/{id}") + @Operation(summary = "문의 삭제 Test Completed", description = "문의를 삭제합니다.") + public ResponseEntity deleteInquiry(@PathVariable("id") Long inquiryId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Put /inquiry/{id}"); + + Member myMember = memberService.findOne(memberDetails.getMemberId()); + inquiryService.deleteInquiry(inquiryId, myMember); + + return new ResponseEntity<>(HttpStatus.OK); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "문의 목록을 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/inquiry") + @Operation(summary = "문의 목록 조회 Test Completed", description = "페이지별 문의 목록을 조회합니다.") + public ResponseEntity> getInquiry(@RequestParam(required = false, defaultValue = "1", name = "page") int pageNumber) { + log.info("EndPoint Get /inquiry"); + + pageNumber -= 1; + if (pageNumber < 0) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, inquiryService.getInquiry(pageNumber)); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, inquiryService.getInquiry(pageNumber)); + } + + + /** + * 리턴할 경우 문의 답변도 함께 리턴해야 하는지 결정해야 함 + */ + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "문의 상세 정보를 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "400", description = "해당 문의가 존재하지 않습니다.") + }) + @GetMapping("/inquiry/view") + @Operation(summary = "문의 상세 조회 Test Completed", description = "특정 문의의 상세 정보를 조회합니다.") + public ResponseEntity> getInquiryDetail(@RequestParam("view") int inquiryId, + HttpSession session) { + log.info("EndPoint Get /inquiry/view"); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, inquiryService.getInquiryView(inquiryId, session)); + return ResponseEntity.ok(response); + } + + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Controller/InquiryReplyController.java b/src/main/java/Remoa/BE/Web/Inquiry/Controller/InquiryReplyController.java new file mode 100644 index 0000000..20d3f26 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Controller/InquiryReplyController.java @@ -0,0 +1,83 @@ +package Remoa.BE.Web.Inquiry.Controller; + +import Remoa.BE.Web.Inquiry.Domain.InquiryReply; +import Remoa.BE.Web.Inquiry.Dto.Req.ReqInquiryDto; +import Remoa.BE.Web.Inquiry.Dto.Req.ReqInquiryReplyDto; +import Remoa.BE.Web.Inquiry.Service.InquiryReplyService; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "문의 답변 기능 Test Completed", description = "문의 답변 기능 API") +@RestController +@RequiredArgsConstructor +@Slf4j +public class InquiryReplyController { + + private final MemberService memberService; + private final InquiryReplyService inquiryReplyService; + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "문의 답글이 성공적으로 등록되었습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = MessageUtils.FORBIDDEN, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/inquiry/{inquiry_id}/reply") + @Operation(summary = "문의 답글 등록 Test Completed", description = "문의 답글을 등록합니다." + + "
응답데이터 정의 필요") + public ResponseEntity postInquiry(@Validated @RequestBody ReqInquiryReplyDto inquiryReplyDto, + @PathVariable("inquiry_id") Long inquiryId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Post /inquiry/{inquiry_id}/reply"); + + Member myMember = memberService.findOne(memberDetails.getMemberId()); + inquiryReplyService.registerInquiryReply(inquiryReplyDto, inquiryId, myMember.getNickname()); + return new ResponseEntity<>(HttpStatus.OK); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "문의 답변이 성공적으로 수정되었습니다."), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = MessageUtils.FORBIDDEN, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PutMapping("/inquiry/reply/{reply_id}") + @Operation(summary = "문의 답변 수정 Test Completed", description = "기존 문의 답변을 수정합니다." + + "
응답데이터 정의 필요") + public ResponseEntity updateInquiryReply(@PathVariable("reply_id") Long replyId, + @Validated @RequestBody ReqInquiryReplyDto updatedReplyDto, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Put /inquiry/reply/{reply_id}"); + + // 요청한 멤버 정보 조회 + Member myMember = memberService.findOne(memberDetails.getMemberId()); + + // 수정된 내용으로 문의 답변 업데이트 + inquiryReplyService.updateInquiryReply(replyId, updatedReplyDto, myMember); + + return new ResponseEntity<>(HttpStatus.OK); + } + +} diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Domain/Inquiry.java b/src/main/java/Remoa/BE/Web/Inquiry/Domain/Inquiry.java new file mode 100644 index 0000000..a709dfe --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Domain/Inquiry.java @@ -0,0 +1,63 @@ +package Remoa.BE.Web.Inquiry.Domain; + +import Remoa.BE.Web.Inquiry.Dto.Req.ReqInquiryDto; +import lombok.*; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@SQLRestriction("deleted = false") +@SQLDelete(sql = "UPDATE inquiry SET deleted = true WHERE inquiry_id = ?") +public class Inquiry { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long inquiryId; + + private String author; + + private String title; + + private String content; + + private LocalDateTime postingTime; + + private LocalDateTime modifiedTime; + + @Builder.Default + private Boolean modified = Boolean.FALSE; + + @Builder.Default + private Boolean replied = Boolean.FALSE; + + @Builder.Default + private Boolean deleted = Boolean.FALSE; + + private int view; + + public void addViewCount() { + this.view++; + } + + public void updateInquiry(ReqInquiryDto updateDto) { + this.title = updateDto.getTitle(); + this.content = updateDto.getContent(); + this.modified = true; + this.modifiedTime = LocalDateTime.now(); + // 필요한 경우 다른 필드도 업데이트할 수 있습니다. + } + + +} diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Domain/InquiryReply.java b/src/main/java/Remoa/BE/Web/Inquiry/Domain/InquiryReply.java new file mode 100644 index 0000000..a4cd9d3 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Domain/InquiryReply.java @@ -0,0 +1,54 @@ +package Remoa.BE.Web.Inquiry.Domain; + + +import Remoa.BE.Web.Inquiry.Dto.Req.ReqInquiryDto; +import Remoa.BE.Web.Inquiry.Dto.Req.ReqInquiryReplyDto; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@SQLRestriction("deleted = false") +@SQLDelete(sql = "UPDATE inquiry_reply_id SET deleted = true WHERE inquiry_reply_id = ?") +public class InquiryReply { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long inquiryReplyId; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "inquiry_id") + Inquiry inquiry; + + private String author; + + private String replyTitle; + + private String replyContent; + + private LocalDateTime postingTime; + + private LocalDateTime modifiedTime; + + @Builder.Default + private Boolean modified = Boolean.FALSE; + + @Builder.Default + private Boolean deleted = Boolean.FALSE; + + public void updateInquiry(ReqInquiryReplyDto updateReplyDto) { + this.replyTitle = updateReplyDto.getReplyTitle(); + this.replyContent = updateReplyDto.getReplyContent(); + this.modified = true; + this.modifiedTime = LocalDateTime.now(); + // 필요한 경우 다른 필드도 업데이트할 수 있습니다. + } +} diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Dto/Req/ReqInquiryDto.java b/src/main/java/Remoa/BE/Web/Inquiry/Dto/Req/ReqInquiryDto.java new file mode 100644 index 0000000..1e180b1 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Dto/Req/ReqInquiryDto.java @@ -0,0 +1,29 @@ +package Remoa.BE.Web.Inquiry.Dto.Req; + +import Remoa.BE.Web.Inquiry.Domain.Inquiry; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; +@Data +public class ReqInquiryDto { + + @Schema(description = "제목", example = "문의사항 제목") + @NotNull + private String title; + + @Schema(description = "내용", example = "이 문의사항은 내용이 이거입니다.") + @NotNull + private String content; + + public Inquiry toEntityInquiry(String enrollNickname) { + return Inquiry.builder() + .author(enrollNickname) + .title(title) + .content(content) + .postingTime(LocalDateTime.now()) + .view(0) + .build(); + } +} diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Dto/Req/ReqInquiryReplyDto.java b/src/main/java/Remoa/BE/Web/Inquiry/Dto/Req/ReqInquiryReplyDto.java new file mode 100644 index 0000000..3580ac2 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Dto/Req/ReqInquiryReplyDto.java @@ -0,0 +1,31 @@ +package Remoa.BE.Web.Inquiry.Dto.Req; + +import Remoa.BE.Web.Inquiry.Domain.Inquiry; +import Remoa.BE.Web.Inquiry.Domain.InquiryReply; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class ReqInquiryReplyDto { + + @Schema(description = "답글 제목", example = "문의사항 답글 제목") + @NotNull + private String replyTitle; + + @Schema(description = "답글 내용", example = "이 문의사항 답글 내용이 이거입니다.") + @NotNull + private String replyContent; + + public InquiryReply toEntityInquiryReply(String enrollNickname, Inquiry inquiry) { + return InquiryReply.builder() + .inquiry(inquiry) + .author(enrollNickname) + .replyTitle(replyTitle) + .replyContent(replyContent) + .postingTime(LocalDateTime.now()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Dto/Res/ResInquiryDetailDto.java b/src/main/java/Remoa/BE/Web/Inquiry/Dto/Res/ResInquiryDetailDto.java new file mode 100644 index 0000000..237f5de --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Dto/Res/ResInquiryDetailDto.java @@ -0,0 +1,57 @@ +package Remoa.BE.Web.Inquiry.Dto.Res; + + +import Remoa.BE.Web.Inquiry.Domain.Inquiry; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Builder +@Getter +@AllArgsConstructor +public class ResInquiryDetailDto { + + @Schema(description = "문의 ID", example = "1") + private Long inquiryId; + + @Schema(description = "작성자", example = "Alice Smith") + private String author; + + @Schema(description = "제목", example = "Product Inquiry") + private String title; + + @Schema(description = "내용", example = "I have some questions regarding your product...") + private String content; + + @Schema(description = "작성일", example = "2024-04-06") + private LocalDate postingTime; + + @Schema(description = "조회 수", example = "50") + private int view; + + @Schema(description = "답변 여부", example = "true") + private Boolean isReplied; + + @Schema(description = "수정 여부", example = "true") + private boolean modified; // 수정 여부 표시 + + @Schema(description = "수정 시각", example = "2024-04-08T10:30:00") + private LocalDateTime modifiedTime; // 수정 시각 + + + public ResInquiryDetailDto(Inquiry entity) { + this.inquiryId = entity.getInquiryId(); + this.author = entity.getAuthor(); + this.title = entity.getTitle(); + this.content = entity.getContent(); + this.postingTime = entity.getPostingTime().toLocalDate(); + this.view = entity.getView(); + this.isReplied = entity.getReplied(); + this.modified = entity.getModified(); + this.modifiedTime = entity.getModifiedTime(); + } +} diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Dto/Res/ResInquiryDto.java b/src/main/java/Remoa/BE/Web/Inquiry/Dto/Res/ResInquiryDto.java new file mode 100644 index 0000000..3c95bfe --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Dto/Res/ResInquiryDto.java @@ -0,0 +1,53 @@ +package Remoa.BE.Web.Inquiry.Dto.Res; + +import Remoa.BE.Web.Inquiry.Domain.Inquiry; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Builder +@Getter +@AllArgsConstructor +public class ResInquiryDto { + + @Schema(description = "문의 ID", example = "1") + private Long id; + + @Schema(description = "작성자", example = "Alice Smith") + private String author; + + @Schema(description = "제목", example = "Product Inquiry") + private String title; + + @Schema(description = "작성일", example = "2024-04-06") + private LocalDate postingTime; + + @Schema(description = "답변 여부", example = "true") + private Boolean isReplied; + + @Schema(description = "조회 수", example = "50") + private int view; + + @Schema(description = "수정 여부", example = "true") + private boolean modified; // 수정 여부 표시 + + @Schema(description = "수정 시각", example = "2024-04-08T10:30:00") + private LocalDateTime modifiedTime; // 수정 시각 + + public ResInquiryDto(Inquiry inquiry) { + this.id = inquiry.getInquiryId(); + this.author = inquiry.getAuthor(); + this.title = inquiry.getTitle(); + this.postingTime = inquiry.getPostingTime().toLocalDate(); + this.isReplied = inquiry.getReplied(); + this.view = inquiry.getView(); + this.modified = inquiry.getModified(); + this.modifiedTime = inquiry.getModifiedTime(); + + } + +} diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Dto/Res/ResInquiryPaging.java b/src/main/java/Remoa/BE/Web/Inquiry/Dto/Res/ResInquiryPaging.java new file mode 100644 index 0000000..7c4725c --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Dto/Res/ResInquiryPaging.java @@ -0,0 +1,21 @@ +package Remoa.BE.Web.Inquiry.Dto.Res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ResInquiryPaging { + + private List inquiries; + private int totalPages; + private long totalOfAllInquiries; + private int totalOfPageElements; + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Repository/InquiryReplyRepository.java b/src/main/java/Remoa/BE/Web/Inquiry/Repository/InquiryReplyRepository.java new file mode 100644 index 0000000..20ba547 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Repository/InquiryReplyRepository.java @@ -0,0 +1,16 @@ +package Remoa.BE.Web.Inquiry.Repository; + +import Remoa.BE.Web.Inquiry.Domain.InquiryReply; +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 org.springframework.transaction.annotation.Transactional; + +public interface InquiryReplyRepository extends JpaRepository { + + @Modifying + @Query("UPDATE InquiryReply ir SET ir.author = :newNick WHERE ir.author = :oldNick") + void modifyingInquiryReplyAuthor(@Param("oldNick") String oldNick, @Param("newNick") String newNick); + +} diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Repository/InquiryRepository.java b/src/main/java/Remoa/BE/Web/Inquiry/Repository/InquiryRepository.java new file mode 100644 index 0000000..f541da1 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Repository/InquiryRepository.java @@ -0,0 +1,18 @@ +package Remoa.BE.Web.Inquiry.Repository; + +import Remoa.BE.Web.Inquiry.Domain.Inquiry; +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 org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public interface InquiryRepository extends JpaRepository { + + @Modifying + @Transactional + @Query("UPDATE Inquiry i SET i.author = :newNick WHERE i.author = :oldNick") + void modifyingInquiryAuthor(@Param("oldNick") String oldNick, @Param("newNick")String newNick); +} diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Service/InquiryReplyService.java b/src/main/java/Remoa/BE/Web/Inquiry/Service/InquiryReplyService.java new file mode 100644 index 0000000..db29fd2 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Service/InquiryReplyService.java @@ -0,0 +1,55 @@ +package Remoa.BE.Web.Inquiry.Service; + + +import Remoa.BE.Web.Inquiry.Domain.Inquiry; +import Remoa.BE.Web.Inquiry.Domain.InquiryReply; +import Remoa.BE.Web.Inquiry.Dto.Req.ReqInquiryDto; +import Remoa.BE.Web.Inquiry.Dto.Req.ReqInquiryReplyDto; +import Remoa.BE.Web.Inquiry.Repository.InquiryReplyRepository; +import Remoa.BE.Web.Inquiry.Repository.InquiryRepository; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class InquiryReplyService { + + private final InquiryReplyRepository inquiryReplyRepository; + private final InquiryRepository inquiryRepository; + + @Transactional + public void registerInquiryReply(ReqInquiryReplyDto req, Long inquiryId, String enrollNickname) { + Inquiry inquiry = inquiryRepository.findById(inquiryId) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + + InquiryReply inquiryReply = req.toEntityInquiryReply(enrollNickname, inquiry); + inquiryReplyRepository.save(inquiryReply); + + inquiry.setReplied(true); + } + + + @Transactional + public void updateInquiryReply(Long replyId, ReqInquiryReplyDto updateReplyDto, Member member) { + InquiryReply inquiryReply = inquiryReplyRepository.findById(replyId) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + + + if (!inquiryReply.getAuthor().equals(member.getNickname())) { + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + + inquiryReply.updateInquiry(updateReplyDto); + } + + @Transactional + public void modifying_Inquiry_Reply_NickName(String newNick, String oldNick) { + inquiryReplyRepository.modifyingInquiryReplyAuthor(newNick, oldNick); + } + +} diff --git a/src/main/java/Remoa/BE/Web/Inquiry/Service/InquiryService.java b/src/main/java/Remoa/BE/Web/Inquiry/Service/InquiryService.java new file mode 100644 index 0000000..ce5f126 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Inquiry/Service/InquiryService.java @@ -0,0 +1,105 @@ +package Remoa.BE.Web.Inquiry.Service; + +import Remoa.BE.Web.Inquiry.Domain.Inquiry; +import Remoa.BE.Web.Inquiry.Dto.Req.ReqInquiryDto; +import Remoa.BE.Web.Inquiry.Dto.Res.ResInquiryDetailDto; +import Remoa.BE.Web.Inquiry.Dto.Res.ResInquiryDto; +import Remoa.BE.Web.Inquiry.Dto.Res.ResInquiryPaging; +import Remoa.BE.Web.Inquiry.Repository.InquiryRepository; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Slf4j +public class InquiryService { + + private static final int INQUIRY_NUMBER = 5; + + private final InquiryRepository inquiryRepository; + + @Transactional + public void registerInquiry(ReqInquiryDto reqInquiryDto, String enrollNickname) { + + inquiryRepository.save(reqInquiryDto.toEntityInquiry(enrollNickname)); + } + + @Transactional + public void deleteInquiry(Long inquiryId, Member member) { + Inquiry inquiry = inquiryRepository.findById(inquiryId) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + + // 공지를 작성한 회원과 현재 로그인한 회원이 같은 경우에만 삭제를 허용합니다. + if (!inquiry.getAuthor().equals(member.getNickname())) { + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + inquiryRepository.delete(inquiry); + } + + @Transactional + public void updateInquiry(Long inquiryId, ReqInquiryDto inquiryDto, Member member) { + Inquiry inquiry = inquiryRepository.findById(inquiryId) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + + // 문의를 작성한 회원과 현재 로그인한 회원이 같은 경우에만 수정을 허용합니다. + if (!inquiry.getAuthor().equals(member.getNickname())) { + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + // 요청으로 받은 데이터로 문의를 업데이트합니다. + inquiry.updateInquiry(inquiryDto); + } + + public ResInquiryPaging getInquiry(int pageNumber) { + + Page inquiries = inquiryRepository.findAll(PageRequest.of(pageNumber, INQUIRY_NUMBER, Sort.by("postingTime").descending())); + List contents = inquiries.stream().map(ResInquiryDto::new).toList(); + + ResInquiryPaging resInquiryPaging = ResInquiryPaging.builder() + .inquiries(contents) + .totalPages(inquiries.getTotalPages()) + .totalOfAllInquiries(inquiries.getTotalElements()) + .totalOfPageElements(inquiries.getNumberOfElements()) + .build(); + + return resInquiryPaging; + } + + @Transactional + public ResInquiryDetailDto getInquiryView(int inquiryId, HttpSession session) { + Inquiry inquiry = inquiryRepository.findById((long) inquiryId).orElseThrow(() -> + new BaseException(CustomMessage.NO_ID)); + handleViewCount(inquiry, session); + return new ResInquiryDetailDto(inquiry); + } + + + @Transactional + public void modifying_Inquiry_NickName(String newNick, String oldNick) { + inquiryRepository.modifyingInquiryAuthor(newNick, oldNick); + } + + private void handleViewCount(Inquiry inquiry, HttpSession session) { + Long inquiryId = inquiry.getInquiryId(); + + String sessionKey = "InquiryViewed" + inquiryId; + log.info("sessionKey = {}", sessionKey); + + if (session.getAttribute(sessionKey) == null) { + inquiry.addViewCount(); + session.setAttribute(sessionKey, true); + } + } +} diff --git a/src/main/java/Remoa/BE/Web/Member/Controller/FollowController.java b/src/main/java/Remoa/BE/Web/Member/Controller/FollowController.java new file mode 100644 index 0000000..543ccce --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Controller/FollowController.java @@ -0,0 +1,114 @@ +package Remoa.BE.Web.Member.Controller; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Res.ResFollowerAndFollowingDto; +import Remoa.BE.Web.Member.Service.FollowService; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Objects; + +@Tag(name = "팔로우 기능 Test Completed", description = "팔로우 기능 API") +@RestController +@Slf4j +@RequiredArgsConstructor +public class FollowController { + + private final FollowService followService; + private final MemberService memberService; + + /** + * Session에서 로그인한 사용자(Follow를 거는 사용자)의 Member와 Follow 받는 대상의 memberId를 통해 + * Follow가 이미 되었는지 확인하고, 안 되어 있으면 Follow, 되어있다면 Unfollow를 할 수 있도록 기능합니다. + * + * @param toMemberId + * @param memberDetails + * @return ResponseEntity + */ + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "언팔로우 성공적으로 수행되었습니다."), + @ApiResponse(responseCode = "201", description = "팔로우가 성공적으로 수행되었습니다."), + @ApiResponse(responseCode = "400", description = "자기 자신을 팔로우하는 경우입니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/follow/{member_id}") + @Operation(summary = "팔로우 기능 Test completed", description = "특정 회원을 팔로우하거나 언팔로우합니다.") + public ResponseEntity follow(@PathVariable("member_id") Long toMemberId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Post /follow/{member_id}"); + + //나 자신을 팔로우 하는 경우 + Long myMemberId = memberDetails.getMemberId(); + if (Objects.equals(myMemberId, toMemberId)) { + throw new BaseException(CustomMessage.SELF_FOLLOW); + // return errorResponse(CustomMessage.SELF_FOLLOW); + } else { + boolean check = followService.followFunction(myMemberId, toMemberId); + //팔로우 + if (check) { + return new ResponseEntity<>(HttpStatus.CREATED); + } + //언팔로우 + else { + return new ResponseEntity<>(HttpStatus.OK); + } + } + } + + /** + * @return ResponseEntity + */ + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "팔로워 및 팔로잉 수 성공적 조회"), + }) + @GetMapping("/follow/{member_id}") + @Operation(summary = "팔로워 및 팔로잉 조회 Test completed", description = "특정 회원의 팔로워 및 팔로잉 수를 조회합니다.") + public ResponseEntity> showFollowers(@AuthenticationPrincipal MemberDetails memberDetails, + @PathVariable("member_id") Long memberId) { + log.info("EndPoint Get /follow/{member_id}"); + + Member myMember = null; + + if (memberDetails != null) { + Long myMemberId = memberDetails.getMemberId(); + myMember = memberService.findOne(myMemberId); + } + + Member member = memberService.findOne(memberId); + List count = followService.followerAndFollowing(member); + ResFollowerAndFollowingDto result = ResFollowerAndFollowingDto.builder() + .follower(count.get(0)) + .following(count.get(1)) + .isFollow(myMember != null ? followService.isMyMemberFollowMember(myMember, member) : null) + .build(); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, result); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, result); + } +} diff --git a/src/main/java/Remoa/BE/Web/Member/Controller/GeneralLoginController.java b/src/main/java/Remoa/BE/Web/Member/Controller/GeneralLoginController.java new file mode 100644 index 0000000..2acb9fd --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Controller/GeneralLoginController.java @@ -0,0 +1,126 @@ +package Remoa.BE.Web.Member.Controller; + +import Remoa.BE.Web.Member.Dto.GerneralLoginDto.GeneralLoginReq; +import Remoa.BE.Web.Member.Dto.GerneralLoginDto.GeneralLoginRes; +import Remoa.BE.Web.Member.Dto.GerneralLoginDto.GeneralSignUpReq; +import Remoa.BE.Web.Member.Dto.GerneralLoginDto.GeneralSignUpRes; +import Remoa.BE.Web.Member.Dto.Res.ResReIssue; +import Remoa.BE.Web.Member.Service.AuthService; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.config.jwt.JwtTokenProvider; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Enumeration; +import java.util.Random; + + +@Tag(name = "일반 로그인 Test Completed", description = "실제 사용하지 않지만 테스트용 토큰 얻기 위한 가입 및 로그인 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/member") +@Slf4j +public class GeneralLoginController { + + private final MemberService memberService; + private final JwtTokenProvider jwtTokenProvider; + private final AuthService authService; + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = MessageUtils.SUCCESS), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @Operation(summary = "테스트용 일반 회원가입 Test completed", description = "account, password 기반 일반 회원가입입니다.
리턴 데이터는 회원번호입니다") + @PostMapping("/signUp") + public ResponseEntity> signUp(@Parameter(name = "회원가입 위한 회원 정보들", required = true) @Valid @RequestBody GeneralSignUpReq signUpReq) { + log.info("EndPoint Post /api/member/signUp"); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, memberService.generalSignUp(signUpReq)); + + return ResponseEntity.ok(response); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = MessageUtils.SUCCESS), + @ApiResponse(responseCode = "404", description = MessageUtils.NOT_FOUND, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + + @Operation(summary = "테스트용 일반 로그인 Test completed", description = "account, password 기반 일반 로그인입니다. ") + @PostMapping("/login") + public ResponseEntity> login(@Parameter(name = "로그인 위한 회원 정보들", required = true) @RequestBody GeneralLoginReq loginRequestDto) { + log.info("EndPoint Post /api/member/login"); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, memberService.generalLogin(loginRequestDto)); + return ResponseEntity.ok(response); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = MessageUtils.SUCCESS), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @Operation(summary = "로그아웃", description = "로그아웃입니다. ") + @PutMapping("/logout") + public ResponseEntity logout(HttpServletRequest request) { + log.info("EndPoint Post /api/member/logout"); + + authService.logout(request); + + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = MessageUtils.SUCCESS), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "400", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @Operation(summary = "토큰 재발급", description = "Header " + + "
Authorization : Bearer 만료토큰" + + "
refresh-token : Bearer 리프레시토큰" + + "
재발급 요청 횟수 10회로 제한됨. 다시 로그인 한 경우 횟수 리셋") + @PutMapping("/reissue") + public ResponseEntity> reissue(HttpServletRequest request, + HttpServletResponse response, + @Parameter(description = "refresh-token", in = ParameterIn.HEADER, schema = @Schema(type = "string")) + @RequestHeader(value = "refresh-token", required = false) String refreshToken) { + log.info("PUT /api/member/reissue"); + + + // 모든 헤더 값 출력 +// Enumeration headerNames = request.getHeaderNames(); +// if (headerNames != null) { +// while (headerNames.hasMoreElements()) { +// String headerName = headerNames.nextElement(); +// String headerValue = request.getHeader(headerName); +// log.info("Header: {} = {}", headerName, headerValue); +// } +// } + + ResReIssue resReIssue = authService.reissueAccessToken(request, response); + return ResponseEntity.ok(new BaseResponse<>(CustomMessage.OK, resReIssue)); + } +} diff --git a/src/main/java/Remoa/BE/Web/Member/Controller/KakaoController.java b/src/main/java/Remoa/BE/Web/Member/Controller/KakaoController.java new file mode 100644 index 0000000..aff62b0 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Controller/KakaoController.java @@ -0,0 +1,138 @@ +package Remoa.BE.Web.Member.Controller; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Req.ReqSignupDto; +import Remoa.BE.Web.Member.Dto.Res.KakaoLoginResponseDto; +import Remoa.BE.Web.Member.Dto.Res.ResSignupDto; +import Remoa.BE.Web.Member.Service.KakaoService; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.Random; + +import static Remoa.BE.utill.MemberInfo.securityLoginWithoutLoginForm; + + +@Tag(name = "kakao", description = "카카오 로그인 API") +@RestController +@Slf4j +@RequiredArgsConstructor +public class KakaoController { + + private final KakaoService kakaoService; + private final MemberService memberService; + + /** + * 카카오 로그인을 통해 code를 query string으로 받아오면, 코드를 통해 토큰, 토큰을 통해 사용자 정보를 얻어와 db에 해당 사용자가 존재하는지 여부를 + * 파악해 존재할 때는 로그인, 없을 땐 회원가입 페이지로 넘어가게 해줌. + */ + // 프론트에서 인가코드 받아오는 url + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "기존 회원 로그인"), + @ApiResponse(responseCode = "201", description = "첫 로그인 회원가입"), + @ApiResponse(responseCode = "400", description = MessageUtils.ERROR, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/login/kakao") + @Operation(summary = "카카오 로그인", description = "카카오 로그인을 통해 사용자를 식별하고 로그인 또는 회원가입 처리합니다.") + public ResponseEntity> getCI(@RequestParam String code, + @RequestParam("redirect_uri") String redirectUri){ + log.info("EndPoint Get /login/kakao"); + + log.info("code = " + code); + KakaoLoginResponseDto kakaoLoginResponseDto = kakaoService.kakaoLogin(code, redirectUri); + + if(kakaoLoginResponseDto.isSignup()){ + BaseResponse response = new BaseResponse<>(CustomMessage.OK, kakaoLoginResponseDto); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, kakaoLoginResponseDto); + return new ResponseEntity<>(response, HttpStatus.OK); +// return SuccessResponse.builder() +// .message(CustomMessage.OK.getMessage()) +// .detail(CustomMessage.OK.getDetail()) +// .data(kakaoLoginResponseDto) +// .build(); + } + + /* *//** + * front-end에서 회원가입에 필요한 정보를 넘겨주면 KakaoSignupForm으로 받아 회원가입을 진행시켜줌 + *//* + @PostMapping("/signup/kakao") + @Operation(summary = "카카오 회원가입", description = "카카오에서 제공하는 사용자 정보를 이용하여 회원가입을 진행합니다.") + public ResponseEntity> signupKakaoMember(@RequestBody @Validated ReqSignupDto form, HttpServletRequest request) throws IOException { + + Member member = new Member(); + Random random = new Random(); + + //닉네임 사용 가능하면 그대로 진행, 불가능하면 임의 닉네임 "유저-{randomInt}로 지정. + String randomNumber = Integer.toString((random.nextInt(900_000) + 100_000)); // 100_000 ~ 999_999 + boolean nicknameDuplicate = memberService.isNicknameDuplicate("유저-" + randomNumber); + while (nicknameDuplicate) { //특수문자는 닉네임에 사용할 수 없으나 임의로 지정하는 닉네임에는 사용 가능하게 해서 또 다른 중복 문제 없게끔. + randomNumber = Integer.toString((random.nextInt(900_000) + 100_000)); // 100_000 ~ 999_999 + nicknameDuplicate = memberService.isNicknameDuplicate("유저-" + randomNumber); + } + member.setNickname("유저-" + randomNumber); + + + //카카오에서 받은 프로필 사진 url 링크를 토대로 s3에 저장 + if (memberService.findByKakaoId(form.getKakaoId()).isPresent()) { + throw new BaseException(CustomMessage.VALIDATED); + //return failResponse(CustomMessage.VALIDATED, "kakaoId가 이미 가입되어 있습니다."); + } + member.setKakaoId(form.getKakaoId()); + member.setEmail(form.getEmail()); + member.setTermConsent(form.getTermConsent()); + + memberService.join(member); + securityLoginWithoutLoginForm(member); + + ResSignupDto result = ResSignupDto.builder(). + kakaoId(member.getKakaoId()). + email(member.getEmail()). + nickname(member.getNickname()). + profileImage(member.getProfileImage()). + termConsent(member.getTermConsent()). + build(); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, result); + return ResponseEntity.ok().body(response); + } + + *//** + * 로그아웃 기능 + * 세션무효화, jsession쿠키를 제거, + *//* + @PostMapping("/user/logout") + @Operation(summary = "로그아웃", description = "현재 로그인된 사용자를 로그아웃 처리합니다.") + public ResponseEntity logout(HttpServletRequest request) { + SecurityContextHolder.clearContext(); + request.getSession().invalidate(); + + return new ResponseEntity<>(HttpStatus.OK); + + }*/ + + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Member/Controller/MyFollowingController.java b/src/main/java/Remoa/BE/Web/Member/Controller/MyFollowingController.java new file mode 100644 index 0000000..d47ac53 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Controller/MyFollowingController.java @@ -0,0 +1,74 @@ +package Remoa.BE.Web.Member.Controller; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Res.ResMypageFollowing; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Member.Service.MyFollowingService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "마이페이지 팔로잉 Test Completed", description = "마이페이지 팔로잉 기능 API") +@RestController +@Slf4j +@RequiredArgsConstructor +public class MyFollowingController { + + private final MemberService memberService; + + private final MyFollowingService myFollowingService; + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인된 사용자의 팔로잉 목록 조회 성공"), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/following") // 마이페이지 팔로잉 관리화면 + @Operation(summary = "마이페이지 팔로잉 관리화면 Test Completed", description = "현재 로그인된 사용자의 팔로잉 목록을 조회합니다.") + public ResponseEntity> mypageFollowing(@AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Get /following"); + + Long myMemberId = memberDetails.getMemberId(); + + Member myMember = memberService.findOne(myMemberId); + ResMypageFollowing resMypageFollowing = myFollowingService.mypageFollowing(myMember); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, resMypageFollowing); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, resMypageFollowing); + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인된 사용자의 팔로워 목록 조회 성공"), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/follower") // 마이페이지 팔로워 관리화면 + @Operation(summary = "마이페이지 팔로워 관리화면 Test Completed", description = "현재 로그인된 사용자의 팔로워 목록을 조회합니다.") + public ResponseEntity> mypageFollower(@AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Get /follower"); + + Long myMemberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(myMemberId); + ResMypageFollowing resMypageFollower = myFollowingService.mypageFollower(myMember); + BaseResponse response = new BaseResponse<>(CustomMessage.OK, resMypageFollower); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, resMypageFollower); + + } + +} diff --git a/src/main/java/Remoa/BE/Web/Member/Controller/ProfileController.java b/src/main/java/Remoa/BE/Web/Member/Controller/ProfileController.java new file mode 100644 index 0000000..2583de8 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Controller/ProfileController.java @@ -0,0 +1,283 @@ +package Remoa.BE.Web.Member.Controller; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Req.EditProfileForm; +import Remoa.BE.Web.Member.Dto.Res.ResUserInfoDto; +import Remoa.BE.Web.Member.Service.AwsS3Service; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Member.Service.ProfileService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static Remoa.BE.exception.CustomBody.errorResponse; +import static Remoa.BE.exception.CustomBody.failResponse; +import static Remoa.BE.utill.FileExtension.fileExtension; + + +@Tag(name = "프로필 기능 Test Completed", description = "프로필 기능 API") +@Slf4j +@RestController +@RequiredArgsConstructor +public class ProfileController { + + private final ProfileService profileService; + private final MemberService memberService; + private final AwsS3Service awsS3Service; + private static final long PROFILE_IMG_MAX_SIZE = 2097152L; + private static final int PROFILE_IMG_MIN_WIDTH_PIXEL = 110; + private static final int PROFILE_IMG_MIN_HEIGHT_PIXEL = 110; + private final RestTemplate restTemplate; + + // 프로필 수정 범위 : 닉네임(중복확인), 핸드폰번호, 대학교, 한줄소개 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인한 사용자의 정보 조회 성공"), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/user") + @Operation(summary = "사용자 정보 조회 Test Completed", description = "현재 로그인한 사용자의 정보를 조회합니다.") + public ResponseEntity> userInfo(@AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Get /user"); + + Long memberId = memberDetails.getMemberId(); + // 로그인된 사용자의 정보를 db에서 다시 불러와 띄워줌. + Member member = memberService.findOne(memberId); + ResUserInfoDto resUserInfoDto = ResUserInfoDto.builder() + .email(member.getEmail()) + .nickname(member.getNickname()) + .phoneNumber(member.getPhoneNumber()) + .university(member.getUniversity()) + .oneLineIntroduction(member.getOneLineIntroduction()) + .build(); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, resUserInfoDto); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, resUserInfoDto); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "대학교 데이터 조회 성공"), + @ApiResponse(responseCode = "401", description = "Unauthorized", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/university") + @Operation(summary = "대학교 찾기 API", description = "전체 대학교 데이터 OPEN API") + public ResponseEntity> getUniversities( + @AuthenticationPrincipal MemberDetails memberDetails, + @RequestParam String searchSchulNm) { + log.info("EndPoint GET /university"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + log.info(myMember.getNickname()); + + String apiKey = "1519ec0b6437aa464a3737f919af3ac1"; + String url = "http://www.career.go.kr/cnet/openapi/getOpenApi?apiKey=" + apiKey + + "&svcType=api&svcCode=SCHOOL&contentType=json&gubun=univ_list&thisPage=1&perPage=500" + + "&searchSchulNm=" + searchSchulNm; + + try { + ResponseEntity response = restTemplate.getForEntity(url, Map.class); + BaseResponse baseResponse = new BaseResponse<>(CustomMessage.OK, response.getBody()); + return ResponseEntity.ok(baseResponse); + } catch (Exception e) { + log.error("Error fetching university data", e); + throw new BaseException(CustomMessage.SERVER_ERROR); + } + } + + + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인한 사용자의 프로필 정보 수정 성공"), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PutMapping("/user") + @Operation(summary = "프로필 수정 Test Completed", description = "현재 로그인한 사용자의 프로필 정보를 수정합니다.") + public ResponseEntity> editProfile(@RequestBody EditProfileForm form, @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Put /user"); + + Long memberId = memberDetails.getMemberId(); + + Member myMember = memberService.findOne(memberId); + log.info(myMember.getNickname()); + if (memberService.isNicknameDuplicate(myMember.getNickname())) { + + // 사용자의 입력 정보를 DTO에 담아 서비스로 전달 + profileService.editProfile(memberId, form); + ResUserInfoDto resUserInfoDto = ResUserInfoDto.builder() + .email(myMember.getEmail()) + .nickname(myMember.getNickname()) + .phoneNumber(myMember.getPhoneNumber()) + .university(myMember.getUniversity()) + .oneLineIntroduction(myMember.getOneLineIntroduction()) + .build(); + BaseResponse response = new BaseResponse<>(CustomMessage.OK, resUserInfoDto); + return ResponseEntity.ok(response); + //return successResponse(CustomMessage.OK, resUserInfoDto); + } + // 수정이 완료되면 프로필 페이지로 이동 + throw new BaseException(CustomMessage.BAD_DUPLICATE); + } + + + // 프로필 사진 불러오기 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인한 사용자의 프로필 사진 URL을 조회 성공"), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/user/img") + @Operation(summary = "프로필 사진 조회 Test Completed", description = "현재 로그인한 사용자의 프로필 사진 URL을 조회합니다.") + public ResponseEntity> showImage(@AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Get /user/img"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + BaseResponse response = new BaseResponse<>(CustomMessage.OK, myMember.getProfileImage()); + return ResponseEntity.ok(response); + //return successResponse(CustomMessage.OK, myMember.getProfileImage()); + } + + // 프로필 사진 업로드 + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인한 사용자의 프로필 사진을 업로드 성공"), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PutMapping(value = "/user/img", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "프로필 사진 업로드 Test Completed", description = "현재 로그인한 사용자의 프로필 사진을 업로드합니다.") + public ResponseEntity upload(@RequestPart("file") MultipartFile multipartFile, @AuthenticationPrincipal MemberDetails memberDetails) throws IOException { + log.info("EndPoint Put /user/img"); + + // 확장자는 jpg, png만 가능 + String extension = fileExtension(multipartFile); + if (!"png".equals(extension) && !"jpg".equals(extension)) { + return failResponse(CustomMessage.BAD_FILE + , "이미지 파일은 jpg, png만 지원합니다. 현재 이미지 파일은 " + extension + "입니다"); + } + + // 이미지 용량 체크 + if (PROFILE_IMG_MAX_SIZE < multipartFile.getSize()) { + return failResponse(CustomMessage.FILE_SIZE_OVER, "프로필 사진은 2MB를 초과할 수 없습니다."); + } + + // 이미지 사이즈(픽셀) 체크 + Map imageSizeMap = checkImageSize(multipartFile); // 필요시 IOException 핸들링 할 것 + int widthPixel = imageSizeMap.get("width"); + int heightPixel = imageSizeMap.get("height"); + if (widthPixel < PROFILE_IMG_MIN_WIDTH_PIXEL || heightPixel < PROFILE_IMG_MIN_HEIGHT_PIXEL) { + return failResponse(CustomMessage.IMAGE_PIXEL_LACK + , "가로/세로 110픽셀 이상만 가능합니다. 현재 가로 : " + widthPixel + " / 세로 : " + heightPixel + "입니다."); + } + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + String editProfileImg = awsS3Service.editProfileImg(myMember.getProfileImage(), multipartFile); + myMember.setProfileImage(editProfileImg); + memberService.join(myMember); + + return new ResponseEntity<>(HttpStatus.OK); + + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인한 사용자의 프로필 사진을 삭제 성공"), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/user/img") + @Operation(summary = "프로필 사진 삭제 Test Completed", description = "현재 로그인한 사용자의 프로필 사진을 삭제합니다.") + public ResponseEntity remove(@AuthenticationPrincipal MemberDetails memberDetails) throws MalformedURLException { + log.info("EndPoint Delete /user/img"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + //유저가 기본프로필이 아니라면 + if (!Objects.equals(myMember.getProfileImage(), "https://remoa.s3.ap-northeast-2.amazonaws.com/img/flow_noname_image.png")) { + // awsS3Service.removeProfileUrl(myMember.getProfileImage()); + myMember.setProfileImage("https://remoa.s3.ap-northeast-2.amazonaws.com/img/flow_noname_image.png"); + memberService.join(myMember); + } + + return new ResponseEntity<>(HttpStatus.OK); + + } + + + /** + * 프론트에서 닉네임 중복 검사를 할 때 사용할 메서드 + * + * @param nickname + * @return ResponseEntity + */ + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "닉네임 사용 중인지 확인 성공"), + }) + @GetMapping("/nickname") + @Operation(summary = "닉네임 중복 확인 Test Completed", description = "사용자가 입력한 닉네임이 이미 사용 중인지 확인합니다." + + "
true : 사용가능" + + "
false : 사용불가") + public ResponseEntity> checkNicknameDuplicate(@RequestParam String nickname) { + log.info("EndPoint Get /nickname"); + + + if (memberService.isNicknameDuplicate(nickname)) { + BaseResponse response = new BaseResponse<>(CustomMessage.OK, false); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, false); + } else { + BaseResponse response = new BaseResponse<>(CustomMessage.OK, true); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, true); + } + } + + private Map checkImageSize(MultipartFile file) throws IOException { + BufferedImage bufferedImage = ImageIO.read(file.getInputStream()); + int width = bufferedImage.getWidth(); + int height = bufferedImage.getHeight(); + log.warn("width = " + width); + log.warn("height = " + height); + + Map imageSizeMap = new HashMap<>(); + imageSizeMap.put("width", width); + imageSizeMap.put("height", height); + return imageSizeMap; + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Member/Controller/WithdrewController.java b/src/main/java/Remoa/BE/Web/Member/Controller/WithdrewController.java new file mode 100644 index 0000000..bf64546 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Controller/WithdrewController.java @@ -0,0 +1,136 @@ +package Remoa.BE.Web.Member.Controller; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Comment.Service.CommentReplyService; +import Remoa.BE.Web.Comment.Service.CommentService; +import Remoa.BE.Web.CommentFeedback.Service.CommentFeedbackService; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Feedback.Domain.FeedbackMemberLog; +import Remoa.BE.Web.Feedback.Service.FeedbackReplyService; +import Remoa.BE.Web.Feedback.Service.FeedbackService; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Member.Service.WithdrewService; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Repository.UploadFileRepository; +import Remoa.BE.Web.Post.Service.FileService; +import Remoa.BE.Web.Post.Service.PostService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.lettuce.core.event.command.CommandStartedEvent; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.io.File; +import java.util.List; +import java.util.Objects; + +@Tag(name = "탈퇴 기능", description = "탈퇴 기능 API") +@RestController +@Slf4j +@RequiredArgsConstructor +public class WithdrewController { + + private final WithdrewService withdrewService; + private final MemberService memberService; + private final PostService postService; + private final FeedbackService feedbackService; + private final FeedbackReplyService feedbackReplyService; + private final CommentService commentService; + private final CommentReplyService commentReplyService; + private final CommentFeedbackService commentFeedbackService; + private final FileService fileService; + + + /** + * PathVariable을 이용한 회원 탈퇴 uri. + * 로그인 된 사용자인지, 해당 사용자의 탈퇴 요청이 맞는지 확인 후 탈퇴 처리. + * + * @return 로그인 되지 않은 상태면 403(forbidden), 다른 id 값을 통한 잘못된 요청을 하면 401(Unauthorized), 올바른 탈퇴 요청이면 200(OK) + */ + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인한 사용자가 자신의 계정을 탈퇴 성공"), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/remove") + @Operation(summary = "회원 DB 제거 ", description = "데이터 베이스 영구삭제 (개발 전용)") + public ResponseEntity withdrewRemoa(@AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Delete /remove"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + List postsByMember = postService.findPostsByMemberHard(myMember); + postsByMember.forEach(post -> { + List commentsByPost = commentService.findCommentsByPostHard(post); //코멘트 조회 + commentsByPost.forEach(commentReplyService::deleteByComment); //코멘트 대댓글 삭제 + commentsByPost.forEach(commentService::deleteCommentLikeByComment); // 코멘트 좋아요 삭제 + commentsByPost.forEach(commentFeedbackService::deleteByComment); //코멘트-피드백 삭제 + commentService.deleteByPost(post); // 코멘트 삭제 + + List feedbackMemberLogByPost = feedbackService.findFeedbackMemberLogByPostHard(post);//피드백로그 조회 + feedbackMemberLogByPost.forEach(feedbackReplyService::deleteByFeedbackMemberLog); //피드백 대댓글 삭제 + feedbackMemberLogByPost.forEach(feedbackService::deleteFeedbackLikeByFeedBack); //피드백 좋아요 + feedbackService.deleteFeedbackLogByPost(post);//피드백 로그 삭제 + + List feedbackByPost = feedbackService.findFeedbackByPostHard(post); //피드백 조회 + feedbackByPost.forEach(commentFeedbackService::deleteByFeedback); //코멘트-피드백 삭제 + feedbackService.deleteFeedbackByPost(post); //피드백 삭제 + + + commentFeedbackService.deleteByPost(post); //코멘트 피드백 삭제 by Post + fileService.deleteByPost(post); + postService.deletePostScrapByPost(post); + }); + postService.deleteByMember(myMember); //해당 포스트 삭제 + + commentFeedbackService.deleteByMember(myMember); //코멘트-피드백 삭제 + commentService.deleteByMember(myMember); + commentReplyService.deleteByMember(myMember); + feedbackService.deleteFeedbackByMember(myMember); + feedbackService.deleteFeedbackLogByMember(myMember); + feedbackReplyService.deleteByMember(myMember); + + //데이터베이스 영구 삭제 + memberService.deleteMemberFromDB(myMember); + + + return new ResponseEntity<>("회원 탈퇴가 완료되었습니다.", HttpStatus.OK); + } + + /** + * Session 정보만을 활용해서 PathVariable 없이 구현한 회원 탈퇴 uri. 로그인 된 사용자인지만 확인하면 됨. + * + * @return 로그인 되지 않은 상태면 403(forbidden), 올바른 탈퇴 요청이면 200(OK) + */ + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인한 사용자가 자신의 계정을 탈퇴 성공"), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/delete") + @Operation(summary = "회원 탈퇴", description = "로그인한 사용자가 자신의 계정을 탈퇴합니다.") + public ResponseEntity withdrewRemoaWithoutPathVariable(@AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Delete /delete"); + + Member findMember = memberService.findOne(memberDetails.getMemberId()); + + withdrewService.withdrewRemoa(findMember); + + return new ResponseEntity<>("회원 탈퇴가 완료되었습니다.", HttpStatus.OK); + } + +} diff --git a/src/main/java/Remoa/BE/Web/Member/Domain/AccessToken.java b/src/main/java/Remoa/BE/Web/Member/Domain/AccessToken.java new file mode 100644 index 0000000..417767c --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Domain/AccessToken.java @@ -0,0 +1,28 @@ +package Remoa.BE.Web.Member.Domain; + +import jakarta.persistence.*; +import lombok.Builder; + +import java.util.Date; + +@Entity +@Builder +public class AccessToken { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "member_id") + private Member member; + + @Column(length = 1000) + private String token; + + @Temporal(TemporalType.TIMESTAMP) + private Date expirationDate; + + private boolean blacklisted; + + // Getters and setters +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Member/Domain/AwsS3.java b/src/main/java/Remoa/BE/Web/Member/Domain/AwsS3.java new file mode 100644 index 0000000..a1c3723 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Domain/AwsS3.java @@ -0,0 +1,22 @@ +package Remoa.BE.Web.Member.Domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AwsS3 { + private String key; + private String path; + + public AwsS3() { + + } + + @Builder + public AwsS3(String key, String path) { + this.key = key; + this.path = path; + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Member/Domain/CommentBookmark.java b/src/main/java/Remoa/BE/Web/Member/Domain/CommentBookmark.java similarity index 87% rename from src/main/java/Remoa/BE/Member/Domain/CommentBookmark.java rename to src/main/java/Remoa/BE/Web/Member/Domain/CommentBookmark.java index b90ffc6..18eae4b 100644 --- a/src/main/java/Remoa/BE/Member/Domain/CommentBookmark.java +++ b/src/main/java/Remoa/BE/Web/Member/Domain/CommentBookmark.java @@ -1,11 +1,12 @@ -package Remoa.BE.Member.Domain; +package Remoa.BE.Web.Member.Domain; +import Remoa.BE.Web.Comment.Domain.Comment; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; +import jakarta.persistence.*; /** * Member가 Comment를 Bookmark할 때 사용 @@ -14,6 +15,7 @@ @Setter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Deprecated public class CommentBookmark { @Id diff --git a/src/main/java/Remoa/BE/Web/Member/Domain/ContentType.java b/src/main/java/Remoa/BE/Web/Member/Domain/ContentType.java new file mode 100644 index 0000000..b1e0658 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Domain/ContentType.java @@ -0,0 +1,14 @@ +package Remoa.BE.Web.Member.Domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * CommentFeedback에 comment와 Feedback 구분을 위한 enum 클래스. + */ +@AllArgsConstructor +@Getter +public enum ContentType { + COMMENT, + FEEDBACK; +} diff --git a/src/main/java/Remoa/BE/Member/Domain/FeedbackBookmark.java b/src/main/java/Remoa/BE/Web/Member/Domain/FeedbackBookmark.java similarity index 87% rename from src/main/java/Remoa/BE/Member/Domain/FeedbackBookmark.java rename to src/main/java/Remoa/BE/Web/Member/Domain/FeedbackBookmark.java index be94dfb..c816df2 100644 --- a/src/main/java/Remoa/BE/Member/Domain/FeedbackBookmark.java +++ b/src/main/java/Remoa/BE/Web/Member/Domain/FeedbackBookmark.java @@ -1,11 +1,12 @@ -package Remoa.BE.Member.Domain; +package Remoa.BE.Web.Member.Domain; +import Remoa.BE.Web.Feedback.Domain.Feedback; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; +import jakarta.persistence.*; /** * Member가 Feedback을Bookmark할 때 사용 @@ -14,6 +15,7 @@ @Setter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Deprecated public class FeedbackBookmark { @Id diff --git a/src/main/java/Remoa/BE/Member/Domain/Follow.java b/src/main/java/Remoa/BE/Web/Member/Domain/Follow.java similarity index 75% rename from src/main/java/Remoa/BE/Member/Domain/Follow.java rename to src/main/java/Remoa/BE/Web/Member/Domain/Follow.java index f84b161..b548224 100644 --- a/src/main/java/Remoa/BE/Member/Domain/Follow.java +++ b/src/main/java/Remoa/BE/Web/Member/Domain/Follow.java @@ -1,16 +1,19 @@ -package Remoa.BE.Member.Domain; +package Remoa.BE.Web.Member.Domain; import lombok.Getter; import lombok.Setter; -import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.Where; -import javax.persistence.*; +import jakarta.persistence.*; @Entity @Getter @Setter -@Where(clause = "deleted = false") +@SQLRestriction("deleted = false") +@Table(name = "follow", uniqueConstraints = { + @UniqueConstraint(columnNames = {"from_member_id", "to_member_id"}) +}) public class Follow { @Id diff --git a/src/main/java/Remoa/BE/Member/Domain/Member.java b/src/main/java/Remoa/BE/Web/Member/Domain/Member.java similarity index 65% rename from src/main/java/Remoa/BE/Member/Domain/Member.java rename to src/main/java/Remoa/BE/Web/Member/Domain/Member.java index 2d71b83..5276a54 100644 --- a/src/main/java/Remoa/BE/Member/Domain/Member.java +++ b/src/main/java/Remoa/BE/Web/Member/Domain/Member.java @@ -1,42 +1,49 @@ -package Remoa.BE.Member.Domain; - -import Remoa.BE.Post.Domain.Post; -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.SQLDelete; +package Remoa.BE.Web.Member.Domain; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Comment.Domain.CommentLike; +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Post.Domain.Post; +import lombok.*; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.Where; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; -import javax.persistence.*; +import jakarta.persistence.*; + import java.util.ArrayList; -import java.util.Collection; import java.util.List; + @Getter @Setter @Entity -@Where(clause = "deleted = false") -public class Member implements UserDetails { +@NoArgsConstructor +@AllArgsConstructor +@SQLRestriction("deleted = false") +public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "member_id") private Long memberId; + private String account; + /** + * 카카오 api에서 받아오는 카카오 이메일 + */ + private String email; + + //23.1.19 추가 + private String nickname; /** * 카카오 api에서 받아오는 고유 id값 */ @Column(name = "kakao_id") private Long kakaoId; - /** - * 카카오 api에서 받아오는 카카오 이메일 - */ - private String email; + /** * 23.02.04 카카오 로그인 단독 개발로 인해 삭제 예정. @@ -51,8 +58,7 @@ public class Member implements UserDetails { /** * 카카오 api에서 받아오는 nickname 값 */ - //23.1.19 추가 - private String nickname; + private String birth; @@ -67,7 +73,8 @@ public class Member implements UserDetails { * 한 줄 소개 */ @Column(name = "one_line_introduction") - private String oneLineIntroduction; + @Lob + private String oneLineIntroduction = ""; /** * 레모아 서비스 이용 약관 동의 여부 @@ -79,39 +86,27 @@ public class Member implements UserDetails { * 카카오 api에서 받아오는 카카오 프로필 사진 uri */ @Column(name = "profile_image") - private String profileImage; + private String profileImage = "https://remoa.s3.ap-northeast-2.amazonaws.com/img/flow_noname_image.png"; - @OneToMany(mappedBy = "member") - private List posts = new ArrayList<>(); - @OneToMany(mappedBy = "member") - private List comments = new ArrayList<>(); - - @OneToMany(mappedBy = "member") - private List feedbacks = new ArrayList<>(); - - @OneToMany(mappedBy = "member", cascade = {CascadeType.ALL}) + @OneToMany(mappedBy = "member", orphanRemoval = true, cascade = {CascadeType.REMOVE}, fetch = FetchType.LAZY) private List memberCategories = new ArrayList<>(); - @OneToMany(mappedBy = "member", cascade = {CascadeType.ALL}) - private List commentBookmarks = new ArrayList<>(); - - @OneToMany(mappedBy = "member", cascade = {CascadeType.ALL}) - private List commentLikes = new ArrayList<>(); - - @OneToMany(mappedBy = "fromMember", cascade = {CascadeType.ALL}) + @OneToMany(mappedBy = "fromMember", orphanRemoval = true, cascade = {CascadeType.REMOVE}, fetch = FetchType.LAZY) private List follows = new ArrayList<>(); /** * ADMIN과 일반 USER를 구분하기 위해 존재. Spring Security 이용하기 위함 */ - private String role = "ROLE_USER"; + @Enumerated(EnumType.STRING) + private Role role; private Boolean deleted = Boolean.FALSE; /** * SecureConfig를 통해 Bean에 등록된 passwordEncoder를 이용해 회원가입시 패스워드 암호화. * 현재(23.02.13 기준) 카카오 로그인으로 통합되어 kakaoId를 암호화 하는 방향으로 쓰이거나 사용하지 않을 예정 + * * @param passwordEncoder * @return */ @@ -137,15 +132,6 @@ public Boolean checkPassword(String plainPassword, PasswordEncoder passwordEncod return passwordEncoder.matches(plainPassword, this.password); } - @Override - public Collection getAuthorities() { - Collection authorities = new ArrayList<>(); - - for(String role : role.split(",")){ - authorities.add(new SimpleGrantedAuthority(role)); - } - return authorities; - } public String getUsername() { return null; diff --git a/src/main/java/Remoa/BE/Member/Domain/MemberCategory.java b/src/main/java/Remoa/BE/Web/Member/Domain/MemberCategory.java similarity index 88% rename from src/main/java/Remoa/BE/Member/Domain/MemberCategory.java rename to src/main/java/Remoa/BE/Web/Member/Domain/MemberCategory.java index 480d398..216c835 100644 --- a/src/main/java/Remoa/BE/Member/Domain/MemberCategory.java +++ b/src/main/java/Remoa/BE/Web/Member/Domain/MemberCategory.java @@ -1,12 +1,12 @@ -package Remoa.BE.Member.Domain; +package Remoa.BE.Web.Member.Domain; -import Remoa.BE.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.Category; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; +import jakarta.persistence.*; @Getter @Setter diff --git a/src/main/java/Remoa/BE/Member/Domain/Role.java b/src/main/java/Remoa/BE/Web/Member/Domain/Role.java similarity index 83% rename from src/main/java/Remoa/BE/Member/Domain/Role.java rename to src/main/java/Remoa/BE/Web/Member/Domain/Role.java index a93dc7e..85d1845 100644 --- a/src/main/java/Remoa/BE/Member/Domain/Role.java +++ b/src/main/java/Remoa/BE/Web/Member/Domain/Role.java @@ -1,4 +1,4 @@ -package Remoa.BE.Member.Domain; +package Remoa.BE.Web.Member.Domain; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/AdminSignUpReq.java b/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/AdminSignUpReq.java new file mode 100644 index 0000000..55ec423 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/AdminSignUpReq.java @@ -0,0 +1,37 @@ +package Remoa.BE.Web.Member.Dto.GerneralLoginDto; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.Role; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@Data +@NoArgsConstructor +@Schema(description = "어드민 회원가입 요청") +public class AdminSignUpReq { + + @NotBlank + private String account; + + @NotBlank + private String password; + + @NotBlank + private String name; + + public Member toEntity() { + Member member = new Member(); + member.setAccount(this.account); + member.setNickname("유저"); + member.setPassword(this.password); + member.setName(this.name); + member.setRole(Role.ADMIN); + return member; + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralLoginReq.java b/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralLoginReq.java new file mode 100644 index 0000000..0091abd --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralLoginReq.java @@ -0,0 +1,17 @@ +package Remoa.BE.Web.Member.Dto.GerneralLoginDto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@Schema(description = "일반 테스트 로그인 요청") +public class GeneralLoginReq { + + @Schema(description = "계정", example = "test1@gmail.com") + String account; + + @Schema(description = "회원 비밀번호", example = "testPassword1") + String password; +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralLoginRes.java b/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralLoginRes.java new file mode 100644 index 0000000..2212de8 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralLoginRes.java @@ -0,0 +1,40 @@ +package Remoa.BE.Web.Member.Dto.GerneralLoginDto; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Res.RemoaToken; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "일반 테스트 로그인 응답") +public class GeneralLoginRes { + + @Schema(description = "토큰 정보") + RemoaToken remoaToken; + + @Schema(description = "회원 닉네임", example = "testNickname1") + String nickname; + + @Schema(description = "회원 이름", example = "김김김") + String name; + + @Schema(description = "회원 번호", example = "1") + Long memberId; + + @Schema(description = "회원 역할", example = "USER") + String role; + + + public GeneralLoginRes(String accessToken, String refreshToken, Member member) { + this.remoaToken = new RemoaToken(accessToken, refreshToken); + this.nickname = member.getNickname(); + this.name = member.getName(); + this.memberId = member.getMemberId(); + this.role = member.getRole().toString(); + } +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralSignUpReq.java b/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralSignUpReq.java new file mode 100644 index 0000000..a33fc6a --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralSignUpReq.java @@ -0,0 +1,41 @@ +package Remoa.BE.Web.Member.Dto.GerneralLoginDto; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.Role; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Builder +@AllArgsConstructor +@Data +@NoArgsConstructor +@Schema(description = "일반 회원가입 요청") +public class GeneralSignUpReq { + + @NotBlank + @Schema(description = "계정", example = "test1@gmail.com") + private String account; + + @NotBlank + @Schema(description = "비밀번호", example = "testPassword1") + private String password; + + @NotBlank + @Schema(description = "이름", example = "김김김") + private String name; + + public Member toEntity() { + Member member = new Member(); + member.setAccount(this.account); + member.setNickname(""); + member.setPassword(this.password); + member.setName(this.name); + member.setRole(Role.USER); + return member; + } +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralSignUpRes.java b/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralSignUpRes.java new file mode 100644 index 0000000..4b6f896 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/GerneralLoginDto/GeneralSignUpRes.java @@ -0,0 +1,18 @@ +package Remoa.BE.Web.Member.Dto.GerneralLoginDto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@Schema(description = "일반 회원가입 응답") +public class GeneralSignUpRes { + + @Schema(description = "회원번호", example = "1") + Long memberId; + + public GeneralSignUpRes(Long memberId) { + this.memberId = memberId; + } +} diff --git a/src/main/java/Remoa/BE/Member/Dto/Req/EditProfileForm.java b/src/main/java/Remoa/BE/Web/Member/Dto/Req/EditProfileForm.java similarity index 82% rename from src/main/java/Remoa/BE/Member/Dto/Req/EditProfileForm.java rename to src/main/java/Remoa/BE/Web/Member/Dto/Req/EditProfileForm.java index ab4c678..20ed317 100644 --- a/src/main/java/Remoa/BE/Member/Dto/Req/EditProfileForm.java +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Req/EditProfileForm.java @@ -1,11 +1,11 @@ -package Remoa.BE.Member.Dto.Req; +package Remoa.BE.Web.Member.Dto.Req; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Getter @Setter diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/Req/KakaoLoginRequestDto.java b/src/main/java/Remoa/BE/Web/Member/Dto/Req/KakaoLoginRequestDto.java new file mode 100644 index 0000000..0179397 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Req/KakaoLoginRequestDto.java @@ -0,0 +1,49 @@ +package Remoa.BE.Web.Member.Dto.Req; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.Role; +import Remoa.BE.Web.Member.Dto.kakaoLoginDto.KakaoProfile; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor //컨트롤러에서 사용 안 됌. +public class KakaoLoginRequestDto { + private String account; + + private String nickname; + + private String password; + + private String name; + + private String profileImageFileName; + + private Long kakaoIdentifier; + + private String email; + + public KakaoLoginRequestDto(KakaoProfile profile, String uniqueNickname) { + this.account = profile.getProperties().getNickname() + profile.getId(); //닉네임과 카카오 식별자를 가지고 임의로 account를 만듦. + this.nickname = uniqueNickname; + this.password = "password"; + this.name = profile.getProperties().getNickname(); + this.profileImageFileName = profile.getProperties().getProfile_image(); + this.kakaoIdentifier = profile.getId(); + this.email = profile.getKakao_account().getEmail(); + } + + public Member toEntity() { + Member member = new Member(); + member.setAccount(this.account); + member.setEmail(this.email); + member.setNickname(this.nickname); + member.setPassword(this.password); + member.setName(this.name); + member.setKakaoId(this.kakaoIdentifier); + member.setRole(Role.USER); + return member; + + } + +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/Req/ReqSignupDto.java b/src/main/java/Remoa/BE/Web/Member/Dto/Req/ReqSignupDto.java new file mode 100644 index 0000000..b4f1a39 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Req/ReqSignupDto.java @@ -0,0 +1,22 @@ +package Remoa.BE.Web.Member.Dto.Req; + +import lombok.Getter; +import lombok.Setter; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Getter +@Setter +public class ReqSignupDto { + + @NotBlank(message = "계정은 필수값입니다.") + private String account; + + @NotNull(message = "카카오에서 발급받은 id값이 누락되었습니다.") + private Long kakaoId; + + + @NotNull(message = "선택 동의사항 값은 필수입니다.") + private Boolean termConsent; +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/Res/KakaoLoginResponseDto.java b/src/main/java/Remoa/BE/Web/Member/Dto/Res/KakaoLoginResponseDto.java new file mode 100644 index 0000000..b0f4e43 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Res/KakaoLoginResponseDto.java @@ -0,0 +1,36 @@ +package Remoa.BE.Web.Member.Dto.Res; + +import Remoa.BE.Web.Member.Domain.Member; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class KakaoLoginResponseDto { + + @Schema(description = "토큰 정보") + RemoaToken remoaToken; + + @Schema(description = "회원 닉네임", example = "test_nickname") + String nickname; + + @Schema(description = "회원 이름", example = "이원준") + String name; + + @Schema(description = "회원 번호", example = "1") + Long memberId; + + @Schema(description = "회원 역할", example = "USER") + String role; + + @Schema(description = "첫 로그인 시 회원 가입 여부", example = "true") + boolean isSignup; + + public KakaoLoginResponseDto(String accessToken, String refreshToken, Member member, boolean isSignup) { + this.remoaToken = new RemoaToken(accessToken, refreshToken); + this.nickname = member.getNickname(); + this.name = member.getName(); + this.memberId = member.getMemberId(); + this.role = member.getRole().toString(); + this.isSignup = isSignup; + } +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/Res/RemoaToken.java b/src/main/java/Remoa/BE/Web/Member/Dto/Res/RemoaToken.java new file mode 100644 index 0000000..2f81f4a --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Res/RemoaToken.java @@ -0,0 +1,25 @@ +package Remoa.BE.Web.Member.Dto.Res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@NoArgsConstructor +@Schema(description = "인증 토큰 정보") +public class RemoaToken { + + @Schema(description = "JWT 인증 토큰", example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0QWNjb3VudCIsImFjY291bnQiOiJ0ZXN0QWNjb3VudCIsImlhdCI6MTcxMDIyMTI1MCwiZXhwIjoxNzEwODI2MDUwfQ.wpMIUytr8MpqxGpFAJIlF8kG9OSm2KJE7xeUWQHVnAU") + private String accessToken; + + @Schema(description = "리프레시 토큰", example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0QWNjb3VudDEiLCJhY2NvdW50IjoidGVzdEFjY291bnQxIiwiaWF0IjoxNzE2MzY1MTY0LCJleHAiOjE3MTY0NTE1NjR9.Pl5iyo2TlZHqjfZmTpSgssZ_Gcz7ElnPtqq6PmzTlYY") + private String refreshToken; + + // 필요한 생성자, getter, setter 등이 Lombok의 @Data와 @NoArgsConstructor에 의해 자동 생성됩니다. + + public RemoaToken(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResFollowerAndFollowingDto.java b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResFollowerAndFollowingDto.java new file mode 100644 index 0000000..d5138ef --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResFollowerAndFollowingDto.java @@ -0,0 +1,19 @@ +package Remoa.BE.Web.Member.Dto.Res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ResFollowerAndFollowingDto { + + private Integer follower; + + private Integer following; + + Boolean isFollow; +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResMemberInfoDto.java b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResMemberInfoDto.java new file mode 100644 index 0000000..a9dfe73 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResMemberInfoDto.java @@ -0,0 +1,30 @@ +package Remoa.BE.Web.Member.Dto.Res; + +import Remoa.BE.Web.Member.Domain.Member; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ResMemberInfoDto { + + @Schema(description = "회원 ID", example = "1") + private Long memberId; + + @Schema(description = "닉네임", example = "유저-250900") + private String nickname; + + @Schema(description = "프로필 이미지 URL") + private String profileImage; + + @Schema(description = "팔로우 여부") + private Boolean isFollow; + + public ResMemberInfoDto(Member member, Boolean isFollow) { + this.memberId = member.getMemberId(); + this.nickname = member.getNickname(); + this.profileImage = member.getProfileImage(); + this.isFollow = isFollow; + } +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResMypageFollowing.java b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResMypageFollowing.java new file mode 100644 index 0000000..8a4d544 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResMypageFollowing.java @@ -0,0 +1,25 @@ +package Remoa.BE.Web.Member.Dto.Res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class ResMypageFollowing { + + @Schema(description = "회원 ID", example = "123") + private Long memberId; + + @Schema(description = "사용자 이름", example = "유저-980584") + private String userName; + + @Schema(description = "팔로우 수", example = "3") + private int followNum; + + @Schema(description = "팔로우 목록") + private List resMypageList; + +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResMypageList.java b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResMypageList.java new file mode 100644 index 0000000..4a02a2a --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResMypageList.java @@ -0,0 +1,31 @@ +package Remoa.BE.Web.Member.Dto.Res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ResMypageList { + + @Schema(description = "프로필 이미지 URL", example = "https://remoa.s3.ap-northeast-2.amazonaws.com/img/profile_img.png") + private String profileImage; + + @Schema(description = "사용자 이름", example = "유저-741885") + private String userName; + + @Schema(description = "팔로잉 수", example = "0") + private int followingNum; + + @Schema(description = "팔로워 수", example = "1") + private int followerNum; + + @Schema(description = "한 줄 소개", example = "") + private String oneLineIntroduction; + + @Schema(description = "회원 ID", example = "1") + private Long memberId; + + @Schema(description = "팔로잉 여부", example = "true") + private Boolean isFollow; // 팔로워 목록에서 내가 팔로워를 팔로잉하는지 확인하기 위함. 팔로잉 목록에선 필요 없으므로 null +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResReIssue.java b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResReIssue.java new file mode 100644 index 0000000..27d88d0 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResReIssue.java @@ -0,0 +1,15 @@ +package Remoa.BE.Web.Member.Dto.Res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class ResReIssue { + + @Schema(description = "토큰 정보") + RemoaToken remoaToken; + + public ResReIssue(String accessToken, String refreshToken) { + remoaToken = new RemoaToken(accessToken, refreshToken); + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResSignupDto.java b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResSignupDto.java new file mode 100644 index 0000000..e088b65 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResSignupDto.java @@ -0,0 +1,25 @@ +package Remoa.BE.Web.Member.Dto.Res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class ResSignupDto { + + @Schema(description = "카카오 아이디", example = "2670589") + private Long kakaoId; + + @Schema(description = "이메일", example = "biba99@naver.com") + private String email; + + @Schema(description = "닉네임", example = "오민택") + private String nickname; + + @Schema(description = "프로필 이미지 URL", example = "https://remoa.s3.ap-northeast-2.amazonaws.com/img/7e8b62e7-4fbe-4039-a084-8a01a08ee35b-%EC%98%A4%EB%AF%BC%ED%83%9D.jpg") + private String profileImage; + + @Schema(description = "이용약관 동의 여부", example = "true") + private boolean termConsent; +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResUserInfoDto.java b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResUserInfoDto.java new file mode 100644 index 0000000..eb56425 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/Res/ResUserInfoDto.java @@ -0,0 +1,19 @@ +package Remoa.BE.Web.Member.Dto.Res; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ResUserInfoDto { + + private String email; + + private String nickname; + private String phoneNumber; + + private String university; + + private String oneLineIntroduction; + +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/kakaoLoginDto/KakaoProfile.java b/src/main/java/Remoa/BE/Web/Member/Dto/kakaoLoginDto/KakaoProfile.java new file mode 100644 index 0000000..1bfdced --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/kakaoLoginDto/KakaoProfile.java @@ -0,0 +1,40 @@ +package Remoa.BE.Web.Member.Dto.kakaoLoginDto; + +import lombok.Data; + +@Data +public class KakaoProfile { + + public Long id; + public String connected_at; + public Properties properties; + public KakaoAccount kakao_account; + + @Data + public class Properties { + public String nickname; + public String profile_image; + public String thumbnail_image; + } + + @Data + public class KakaoAccount { + public Boolean profile_nickname_needs_agreement; + public Boolean profile_image_needs_agreement; + public Profile profile; + public Boolean has_email; + public Boolean email_needs_agreement; + public Boolean is_email_valid; + public Boolean is_email_verified; + public String email; + + @Data + public class Profile { + public String nickname; + public String thumbnail_image_url; + public String profile_image_url; + public Boolean is_default_image; + } + } + +} diff --git a/src/main/java/Remoa/BE/Web/Member/Dto/kakaoLoginDto/OAuthToken.java b/src/main/java/Remoa/BE/Web/Member/Dto/kakaoLoginDto/OAuthToken.java new file mode 100644 index 0000000..fe1800b --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Dto/kakaoLoginDto/OAuthToken.java @@ -0,0 +1,14 @@ +package Remoa.BE.Web.Member.Dto.kakaoLoginDto; + +import lombok.Data; + +@Data +public class OAuthToken { + private String access_token; + private String token_type; + private String refresh_token; + private int expires_in; + private String scope; + private int refresh_token_expires_in; + +} diff --git a/src/main/java/Remoa/BE/Web/Member/MemberUtils.java b/src/main/java/Remoa/BE/Web/Member/MemberUtils.java new file mode 100644 index 0000000..a6e0962 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/MemberUtils.java @@ -0,0 +1,147 @@ +package Remoa.BE.Web.Member; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Comment.Domain.CommentReply; +import Remoa.BE.Web.Comment.Dto.Res.ResCommentDto; +import Remoa.BE.Web.Comment.Dto.Res.ResCommentReplyDto; +import Remoa.BE.Web.Comment.Service.CommentReplyService; +import Remoa.BE.Web.Comment.Service.CommentService; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Feedback.Domain.FeedbackReply; +import Remoa.BE.Web.Feedback.Domain.FeedbackMemberLog; +import Remoa.BE.Web.Feedback.Dto.ResFeedbackDto2; +import Remoa.BE.Web.Feedback.Dto.ResFeedbackInfoDto; +import Remoa.BE.Web.Feedback.Dto.ResFeedbackReplyDto; +import Remoa.BE.Web.Feedback.Repository.FeedbackMemberLogRepository; +import Remoa.BE.Web.Feedback.Service.FeedbackReplyService; +import Remoa.BE.Web.Feedback.Service.FeedbackService; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import Remoa.BE.Web.Member.Service.FollowService; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Service.PostService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class MemberUtils { + + private final FollowService followService; + private final PostService postService; + private final CommentService commentService; + private final CommentReplyService commentReplyService; + private final FeedbackService feedbackService; + private final FeedbackReplyService feedbackReplyService; + + + public Boolean isMyMemberFollowMember(Member myMember, Member toMember) { + return myMember != null ? followService.isMyMemberFollowMember(myMember, toMember) : null; + } + + public Boolean isLikedPost(Member myMember, Post post) { + return myMember != null && postService.isThisPostLiked(myMember, post); + } + + public Boolean isScrapedPost(Member myMember, Post post) { + return myMember != null && postService.isThisPostScraped(myMember, post); + } + + private Boolean isLikedComment(Member myMember, Comment comment) { + return myMember != null && commentService.findCommentLike(myMember, comment).isPresent(); + } + + private Boolean isLikedCommentReply(Member myMember, CommentReply commentReply) { + return myMember != null && commentReplyService.findCommentReplyLike(myMember, commentReply).isPresent(); + } + + /** + * 내가 피드백 단 유저에 좋아요를 눌렀는지 여부 + */ + private Boolean isLikedFeedbackMember(Member myMember, FeedbackMemberLog feedbackMemberLog) { + return myMember != null && feedbackService.findFeedbackMemberLike(myMember, feedbackMemberLog).isPresent(); + } + + private Boolean isLikedFeedbackReply(Member myMember, FeedbackReply feedbackReply) { + return myMember != null && feedbackReplyService.findFeedbackReplyLike(myMember, feedbackReply).isPresent(); + } + + // 추가적인 유틸리티 메서드들... + + public List feedbackList(Long postId, Member myMember) { + Post post = postService.findOne(postId); + List feedbacks = feedbackService.findAllFeedbacksOfPost(postId); + + // 피드백을 멤버별로 그룹화 + Map> feedbacksByMember = feedbacks.stream() + .collect(Collectors.groupingBy(Feedback::getMember)); + + // 멤버별 피드백을 첫 번째 피드백의 작성 시간 기준으로 정렬 + List>> sortedFeedbacksByMember = feedbacksByMember.entrySet().stream() + .sorted(Comparator.comparing(entry -> entry.getValue().get(0).getFeedbackTime())) + .toList(); + + // 각 그룹 내의 피드백을 pageNumber 순으로 정렬 + sortedFeedbacksByMember.forEach(entry -> + entry.setValue(entry.getValue().stream() + .sorted(Comparator.comparing(Feedback::getPageNumber)) + .collect(Collectors.toList())) + ); + + List resFeedbackDtos = sortedFeedbacksByMember.stream() + .map(entry -> { + Member feedbackMember = entry.getKey(); // 피드백 작성자 + List memberFeedbacks = entry.getValue(); + + List feedbackInfos = memberFeedbacks.stream() + .map(ResFeedbackInfoDto::new) + .collect(Collectors.toList()); + + ResMemberInfoDto memberInfoDto = new ResMemberInfoDto(feedbackMember, isMyMemberFollowMember(myMember, feedbackMember)); + FeedbackMemberLog feedbackMemberLog = feedbackService.findFeedbackMemberLog(feedbackMember, post); + + return new ResFeedbackDto2(memberInfoDto, + feedbackMemberLog, + isLikedFeedbackMember(myMember, feedbackMemberLog), + feedbackInfos, + feedbackReplyService.findFeedbackReplies(feedbackMemberLog).stream().map( + feedbackReply -> new ResFeedbackReplyDto( + feedbackReply, + isLikedFeedbackReply(myMember, feedbackReply), + isMyMemberFollowMember(myMember, feedbackReply.getMember()))).collect(Collectors.toList()) + ); + }) + .collect(Collectors.toList()); + + return resFeedbackDtos; + } + + + public List commentList(Long postId, Member myMember) { + + List comments = commentService.findAllCommentsOfPost(postId); + List resCommentDtos = comments.stream() + .map(comment -> { + List replies = commentReplyService.findCommentReplies(comment); + List resReplies = replies.stream() + .map(reply -> new ResCommentReplyDto(reply, + isLikedCommentReply(myMember, reply), + isMyMemberFollowMember(myMember, reply.getMember()))) + .collect(Collectors.toList()); + + return new ResCommentDto(comment, + isLikedComment(myMember, comment), + isMyMemberFollowMember(myMember, comment.getMember()), + resReplies); + }) + .toList(); + + return resCommentDtos; + } +} diff --git a/src/main/java/Remoa/BE/Web/Member/Repository/AccessTokenRepository.java b/src/main/java/Remoa/BE/Web/Member/Repository/AccessTokenRepository.java new file mode 100644 index 0000000..29007b7 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Repository/AccessTokenRepository.java @@ -0,0 +1,12 @@ +package Remoa.BE.Web.Member.Repository; + +import Remoa.BE.Web.Member.Domain.AccessToken; +import org.aspectj.apache.bcel.classfile.Module; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AccessTokenRepository extends JpaRepository { + + Optional findByToken(String token); +} diff --git a/src/main/java/Remoa/BE/Web/Member/Repository/FollowRepository.java b/src/main/java/Remoa/BE/Web/Member/Repository/FollowRepository.java new file mode 100644 index 0000000..34f90a6 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Repository/FollowRepository.java @@ -0,0 +1,52 @@ +package Remoa.BE.Web.Member.Repository; + +import Remoa.BE.Web.Member.Domain.Follow; +import Remoa.BE.Web.Member.Domain.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 FollowRepository extends JpaRepository { + + @Query("select f from Follow f where f.fromMember = :fromMember and f.toMember = :toMember") + List findFollows(@Param("fromMember") Member fromMember, @Param("toMember") Member toMember); + + @Query("SELECT f.toMember FROM Follow f JOIN f.toMember WHERE f.fromMember = :member " + + "ORDER BY CASE " + + "WHEN ASCII(f.toMember.nickname) BETWEEN 48 AND 57 THEN 1 " + // 숫자 + "WHEN ASCII(f.toMember.nickname) BETWEEN 97 AND 122 THEN ASCII(f.toMember.nickname) " + // 소문자 + "WHEN ASCII(f.toMember.nickname) BETWEEN 65 AND 90 THEN ASCII(f.toMember.nickname) + 32 " + // 대문자 -> 소문자로 바꿈 + "ELSE 999 END") + List loadFollows(@Param("member") Member member); + + @Query("SELECT f.fromMember FROM Follow f JOIN f.fromMember WHERE f.toMember = :member " + + "ORDER BY CASE " + + "WHEN ASCII(f.fromMember.nickname) BETWEEN 48 AND 57 THEN 1 " + + "WHEN ASCII(f.fromMember.nickname) BETWEEN 97 AND 122 THEN ASCII(f.fromMember.nickname) " + // 소문자 + "WHEN ASCII(f.fromMember.nickname) BETWEEN 65 AND 90 THEN ASCII(f.fromMember.nickname) + 32 " + // 대문자 -> 소문자로 바꿈 + "ELSE 999 END") + List loadFollowers(@Param("member") Member member); + + + @Query("select f.toMember.memberId from Follow f where f.fromMember = :member") + List loadFollowsId(@Param("member") Member member); + + @Query("select count(f) > 0 from Follow f where f.fromMember = :fromMember and f.toMember = :toMember") + boolean isFollow(@Param("fromMember") Member fromMember, @Param("toMember") Member toMember); + + @Modifying + @Query("delete from Follow f where f.fromMember = :fromMember and f.toMember = :toMember") + void unfollowByMembers(@Param("fromMember") Member fromMember, @Param("toMember") Member toMember); + + @Modifying + @Query("delete from Follow f where f.fromMember = :fromMember and f.toMember = :toMember") + void deleteByMembers(@Param("fromMember") Member fromMember, @Param("toMember") Member toMember); + + // Follow 엔티티와의 관계에 따른 삭제 메소드 추가 + @Modifying + @Query("delete from Follow f where f.fromMember = :member or f.toMember = :member") + void deleteFollowsByMember(@Param("member") Member member); +} diff --git a/src/main/java/Remoa/BE/Web/Member/Repository/MemberRepository.java b/src/main/java/Remoa/BE/Web/Member/Repository/MemberRepository.java new file mode 100644 index 0000000..e398859 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Repository/MemberRepository.java @@ -0,0 +1,47 @@ +package Remoa.BE.Web.Member.Repository; + +import Remoa.BE.Web.Member.Domain.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 org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + + boolean existsByNickname(String nickname); + + + boolean existsByAccount(String account); + + Optional findByAccount(String account); + + @Modifying + @Transactional + @Query(value = "DELETE FROM Member m WHERE member_id = :memberId", nativeQuery = true) + void deleteMemberFromDB(@Param("memberId") Long memberId); + + @Query("select m from Member m where m.memberId = :id") + Optional findOne(@Param("id") Long id); + + @Query("select m from Member m") + List findAll(); + + @Query("select m from Member m where m.email = :email") + Optional findByEmail(@Param("email") String email); + + @Query("select m from Member m where m.nickname = :nickname") + List mfindByNickname(@Param("nickname") String nickname); + + Optional findByNickname(String nickName); + + @Query("select m from Member m where m.kakaoId = :kakaoId") + Optional findByKakaoId(@Param("kakaoId") Long kakaoId); + + // 나머지 메소드들도 유사하게 @Query를 이용하여 변경 가능 + + +} diff --git a/src/main/java/Remoa/BE/Member/Repository/MemberRepository.java b/src/main/java/Remoa/BE/Web/Member/Repository/MemberRepositoy.java similarity index 72% rename from src/main/java/Remoa/BE/Member/Repository/MemberRepository.java rename to src/main/java/Remoa/BE/Web/Member/Repository/MemberRepositoy.java index 3f07bcd..46ce9fb 100644 --- a/src/main/java/Remoa/BE/Member/Repository/MemberRepository.java +++ b/src/main/java/Remoa/BE/Web/Member/Repository/MemberRepositoy.java @@ -1,31 +1,23 @@ -package Remoa.BE.Member.Repository; +package Remoa.BE.Web.Member.Repository; -import Remoa.BE.Member.Domain.Follow; -import Remoa.BE.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.Follow; +import Remoa.BE.Web.Member.Domain.Member; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import javax.persistence.EntityManager; -import javax.persistence.EntityNotFoundException; +import jakarta.persistence.EntityManager; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @Repository @Slf4j @RequiredArgsConstructor -public class MemberRepository { +public class MemberRepositoy { public final EntityManager em; - public void save(Member member) { - this.em.persist(member); - log.info("member save ok = {}", member); - } - public Optional findOne(Long id) { return Optional.ofNullable(em.find(Member.class, id)); } @@ -40,7 +32,7 @@ public Optional findByEmail(String email) { .getResultStream().findAny(); } - public List findByNickname(String nickname) { + public List mfindByNickname(String nickname) { return em.createQuery("select m from Member m where m.nickname = :nickname", Member.class) .setParameter("nickname", nickname) .getResultList(); @@ -81,7 +73,7 @@ public Boolean isFollow(Member fromMember, Member toMember) { */ public Follow loadFollow(Member fromMember, Member toMember) { return em.createQuery("select f from Follow f " + - "where f.fromMember = :fromMember and f.toMember = :toMember", Follow.class) + "where f.fromMember = :fromMember and f.toMember = :toMember", Follow.class) .setParameter("fromMember", fromMember) .setParameter("toMember", toMember) .getResultList() @@ -95,7 +87,21 @@ public Follow loadFollow(Member fromMember, Member toMember) { */ public List loadFollows(Member member) { return em.createQuery("select f.toMember from Follow f " + - "where f.fromMember = :member", Member.class) + "where f.fromMember = :member " + + "order by f.toMember.nickname", Member.class) + .setParameter("member", member) + .getResultList(); + } + + /** + * 팔로워 Member 객체 리스트 반환 + * @param member + * @return List + */ + public List loadFollowers(Member member){ + return em.createQuery("select f.fromMember from Follow f " + + "where f.toMember = :member " + + "order by f.fromMember.nickname", Member.class) .setParameter("member", member) .getResultList(); } @@ -106,7 +112,7 @@ public List loadFollows(Member member) { * @return */ public List loadFollowsId(Member member) { - return em.createQuery("select f.toMember.id from Follow f " + + return em.createQuery("select f.toMember.memberId from Follow f " + "where f.fromMember = :member", Long.class) .setParameter("member", member) .getResultList(); @@ -124,13 +130,28 @@ public void unfollowByFollowId(Member fromMember, Member toMember) { em.remove(result); } + /** + * 회원 탈퇴시 팔로우 관계를 모두 삭제. + * @param member + */ + public void deleteAllFollowshipByMember(Member member){ + List results = em.createQuery("select f from Follow f " + + "where (f.fromMember = :member or f.toMember = :member)", Follow.class) + .setParameter("member", member) + .getResultList(); + results.stream().forEach(res -> { + em.remove(res); + }); + } + //soft delete 메소드로 사용하려 하였으나, 영속성 컨텍스트를 통한 엔티티의 deleted 필드값 교체만으로도 동작이 가능해서 현재 잠정 폐기 -/* @Query("update Member m set m.deleted = true where m.memberId = :id") - @Modifying*/ - /*public void deleteSoftlyById(Member member) { + @Deprecated + @Query("update Member m set m.deleted = true where m.memberId = :id") + @Modifying + public void deleteSoftlyById(Member member) { log.info("delete member..."); em.createQuery("update Member m set m.deleted = true where m.memberId = :id") .setParameter("id", member.getMemberId()); - }*/ + } } diff --git a/src/main/java/Remoa/BE/Web/Member/Service/AuthService.java b/src/main/java/Remoa/BE/Web/Member/Service/AuthService.java new file mode 100644 index 0000000..5ef382d --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Service/AuthService.java @@ -0,0 +1,96 @@ +package Remoa.BE.Web.Member.Service; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Res.ResReIssue; +import Remoa.BE.Web.Member.Repository.MemberRepository; +import Remoa.BE.config.jwt.JwtTokenProvider; +import Remoa.BE.config.redis.RedisUtils; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class AuthService { + private final JwtTokenProvider jwtTokenProvider; + private final MemberRepository memberRepository; + private final RedisUtils redisUtils; + + + @Transactional + public ResReIssue reissueAccessToken(HttpServletRequest request, HttpServletResponse response) { + String newAccessToken = null; + String refreshToken = null; + System.out.println("reissueAccessToken 진입"); + String oldAccessToken = parseBearerToken(request, HttpHeaders.AUTHORIZATION); + if (oldAccessToken == null || oldAccessToken.isEmpty()) { + System.out.println("만료 엑세스 토큰 없음"); + throw new BaseException(CustomMessage.EXPIRED_TOKEN_NOT_EXIST); + } + refreshToken = parseBearerToken(request, "refresh-token"); + if (refreshToken == null || refreshToken.isEmpty()) { + System.out.println("리프레시 토큰 없음"); + throw new BaseException(CustomMessage.REFRESH_TOKEN_NOT_EXIST); + } + + + log.info("==============================================================="); + log.info("oldAccessToken : {}", oldAccessToken); + log.info("refreshToken : {}", refreshToken); + log.info("==============================================================="); + + jwtTokenProvider.validateRefreshToken(refreshToken, oldAccessToken); + newAccessToken = jwtTokenProvider.recreateAccessToken(oldAccessToken); + + log.info("==============================================================="); + log.info("new AccessToken 발급 = " + newAccessToken); + log.info("==============================================================="); +// Authentication auth = jwtTokenProvider.getAuthentication(newAccessToken); +// SecurityContextHolder.getContext().setAuthentication(auth); + + return new ResReIssue(newAccessToken, refreshToken); + } + + private String parseBearerToken(HttpServletRequest request, String headerName) { + return Optional.ofNullable(request.getHeader(headerName)) + .filter(token -> token.substring(0, 7).equalsIgnoreCase("Bearer ")) + .map(token -> token.substring(7)) + .orElse(null); + } + + + public void logout(HttpServletRequest request) { + String accessToken = jwtTokenProvider.resolveToken(request); + String account = getAccountFromAccessToken(accessToken); + //해당 액세스 토큰의 남은 유효 시간 + long time = jwtTokenProvider.getAccessTokenExpirationDate(accessToken).getTime() - System.currentTimeMillis(); + + // AccessToken을 블랙리스트에 추가, 남은 유효시간만큼만 블랙리스트에 저장 + redisUtils.setBlackList(accessToken, account, time); + // 리프레시 토큰도 무효화 + // refreshTokenRepository.deleteById(account); + + // RedisUtils를 사용하여 리프레시 토큰 삭제 + redisUtils.deleteRefreshToken(account); + } + + + private String getAccountFromAccessToken(String accessToken) { + return jwtTokenProvider.getUserAccount(accessToken); + } + + private Member findMemberByAccount(String account) { + return memberRepository.findByAccount(account) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Member/Service/AwsS3Service.java b/src/main/java/Remoa/BE/Web/Member/Service/AwsS3Service.java new file mode 100644 index 0000000..bb42214 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Service/AwsS3Service.java @@ -0,0 +1,70 @@ +package Remoa.BE.Web.Member.Service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +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.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Objects; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Slf4j +public class AwsS3Service { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public String editProfileImg(String profileImgUrl,MultipartFile multipartFile) throws IOException { + + if(!Objects.equals(profileImgUrl, "https://remoa.s3.ap-northeast-2.amazonaws.com/img/flow_noname_image.png")) { + //기존 프로필 사진 s3에서 삭제 + // removeProfileUrl(profileImgUrl); + } + + //파일 타입과 사이즈 저장 + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType("image/jpeg"); + log.info(multipartFile.getContentType()); + objectMetadata.setContentLength(multipartFile.getSize()); + + //파일 이름 + String originalFilename = multipartFile.getOriginalFilename(); + + //파일 이름이 겹치지 않게 + String uuid = UUID.randomUUID().toString(); + + //post 폴더에 따로 넣어서 보관 + String s3name = "img/"+uuid+"_"+originalFilename; + + try (InputStream inputStream = multipartFile.getInputStream()) { + amazonS3.putObject(new PutObjectRequest(bucket, s3name, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch (IOException e) { + //파일을 제대로 받아오지 못했을때 + //Todo 예외처리 custom 따로 만들기 + throw new RuntimeException(e); + } + + return amazonS3.getUrl(bucket,s3name).toString().replaceAll("\\+", "+"); + + } + + public void removeProfileUrl(String profileImgUrl) throws MalformedURLException { + URL fileUrl = new URL(profileImgUrl); + String objectKey = fileUrl.getPath().replaceAll("^/", ""); + amazonS3.deleteObject(bucket,objectKey); + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Member/Service/FollowService.java b/src/main/java/Remoa/BE/Web/Member/Service/FollowService.java new file mode 100644 index 0000000..fbd2ff5 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Service/FollowService.java @@ -0,0 +1,71 @@ +package Remoa.BE.Web.Member.Service; + +import Remoa.BE.Web.Member.Domain.Follow; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Repository.FollowRepository; +import Remoa.BE.Web.Member.Repository.MemberRepository; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FollowService { + + private final FollowRepository followRepository; + private final MemberRepository memberRepository; + + //false 언팔 true 팔로우 + @Transactional + public boolean followFunction(Long fromMemberId, Long toMemberId) { + + Member fromMember = memberRepository.getReferenceById(fromMemberId); + Member toMember = memberRepository.findById(toMemberId).orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + + System.out.println("fromMember = " + fromMember); + System.out.println("toMember = " + toMember); + + if (followRepository.isFollow(fromMember, toMember)) { + followRepository.unfollowByMembers(fromMember,toMember); + return false; + } + + Follow follow = new Follow(); + follow.setFromMember(fromMember); + follow.setToMember(toMember); + + //memberRepository.follow(follow); + followRepository.save(follow); + + return true; + } + + public Boolean isMyMemberFollowMember(Member myMember, Member member) { + //return memberRepository.isFollow(myMember, member); + return followRepository.isFollow(myMember, member); + } + + public List followerAndFollowing(Member member){ + ArrayList arr = new ArrayList<>(); + arr.add(followRepository.loadFollowers(member).size()); + arr.add((member.getFollows().size())); + return arr; + } + + + public List showFollows(Member fromMember) { + return followRepository.loadFollows(fromMember); + } + + public List showFollowId(Member fromMember){ + return followRepository.loadFollowsId(fromMember); + } +} diff --git a/src/main/java/Remoa/BE/Web/Member/Service/KakaoService.java b/src/main/java/Remoa/BE/Web/Member/Service/KakaoService.java new file mode 100644 index 0000000..e1d9f06 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Service/KakaoService.java @@ -0,0 +1,147 @@ +package Remoa.BE.Web.Member.Service; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Req.KakaoLoginRequestDto; +import Remoa.BE.Web.Member.Dto.Res.KakaoLoginResponseDto; +import Remoa.BE.Web.Member.Dto.kakaoLoginDto.KakaoProfile; +import Remoa.BE.Web.Member.Dto.kakaoLoginDto.OAuthToken; +import Remoa.BE.Web.Member.Repository.MemberRepository; +import Remoa.BE.config.auth.RefreshToken; +import Remoa.BE.config.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Service +@Slf4j +@RequiredArgsConstructor +public class KakaoService { + + @Value("${security.jwt.token.refresh-expiration-minutes}") + public long refreshExpirationMinutes; + + + private final Random random = new Random(); + private final RedisTemplate redisTemplate; + + //카카오 로그인시 접속해야 할 링크 : https://kauth.kakao.com/oauth/authorize?client_id=139febf9e13da4d124d1c1faafcf3f86&redirect_uri=http://localhost:8080/login/kakao&response_type=code + + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + + public KakaoLoginResponseDto kakaoLogin(String code, String redirectUri) { + boolean isSignUp = false; + + OAuthToken accessToken = getAccessToken(code, redirectUri); + KakaoProfile kakaoProfile = getKakaoProfile(accessToken); + + log.info("kakaoProfile = {}", kakaoProfile); + + String uniqueNickname = generateUniqueNickname(); + + KakaoLoginRequestDto kakaoLoginRequestDto = new KakaoLoginRequestDto(kakaoProfile, uniqueNickname); + Member member = memberRepository.findByKakaoId(kakaoLoginRequestDto.getKakaoIdentifier()).orElseGet(() -> null); + if (member == null) { + log.info("카카오로 회원가입"); + isSignUp = true; + member = memberRepository.save(kakaoLoginRequestDto.toEntity()); + } + String token = jwtTokenProvider.createToken(member.getAccount()); //임의로 만든 account로 토큰 생성. + String refreshToken = jwtTokenProvider.createRefreshToken(member.getAccount()); // 리프레시 토큰 생성 + + log.info("==============================================================="); + log.info("token : {}", token); + log.info("refreshToken : {}", refreshToken); + log.info("==============================================================="); + + updateRefreshToken(member, refreshToken); + + return new KakaoLoginResponseDto(token, refreshToken, member, isSignUp); + } // 그냥 회원 가입 할 경우는 로그인을 따로 진행해야 토큰을 주고, 카카오 로그인을 할 경우 처음 등록시에도 토큰을 부여? -> yes + + private void updateRefreshToken(Member member, String refreshToken) { + // 새로운 RefreshToken 객체를 생성하고 바로 저장 + RefreshToken refreshTokenObj = new RefreshToken(member.getAccount(), refreshToken); + redisTemplate.opsForValue().set(member.getAccount(), refreshTokenObj, refreshExpirationMinutes, TimeUnit.MINUTES); + } + + + private String generateUniqueNickname() { + String randomNumber; + boolean nicknameDuplicate; + do { + randomNumber = Integer.toString((random.nextInt(900_000) + 100_000)); + nicknameDuplicate = memberRepository.existsByNickname("유저" + randomNumber); + } while (nicknameDuplicate); + return "유저" + randomNumber; + } + + //(2) + // 발급 받은 accessToken 으로 카카오 회원 정보 얻기 + public KakaoProfile getKakaoProfile(OAuthToken oAuthToken) { + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("Authorization", "Bearer " + oAuthToken.getAccess_token()); + httpHeaders.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); + + HttpEntity> kakaoProfileRequest2 = new HttpEntity<>(httpHeaders); //헤더만 가지고 요청헤더를 만들 수 있다. + KakaoProfile kakaoProfile = restTemplate.exchange("https://kapi.kakao.com/v2/user/me", HttpMethod.POST, kakaoProfileRequest2, KakaoProfile.class).getBody(); + + return kakaoProfile; + } + + // (1)넘어온 인가 코드를 통해 access_token 발급 + public OAuthToken getAccessToken(String code, String redirectUri) { + //POST 방식으로 key=value 데이터를 요청 + //Post 요청을 하는 다양한 라이브러리가 있다 Retrofit(안드로이드), OkHttp, RestTempate + RestTemplate restTemplate = new RestTemplate(); + + + //Haeder 오브젝트 생성 + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); + + log.info("before"); + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", "415ea3ee0124fa3538ce37d80adc94c2"); + if (redirectUri.equals("https://d197wa6gufmlpc.cloudfront.net/login/kakao")) { + log.info("redirect_uri = {}", "https://d197wa6gufmlpc.cloudfront.net/login/kakao"); + params.add("redirect_uri", "https://d197wa6gufmlpc.cloudfront.net/login/kakao"); + } else if (redirectUri.equals("http://localhost:3000/login/kakao")) { + log.info("redirect_uri = {}", "http://localhost:3000/login/kakao"); + params.add("redirect_uri", "http://localhost:3000/login/kakao"); + } + params.add("code", code); + //params.add("client_secret", client_secret); + + log.info("after"); + /** + * 토큰을 발급할때 좀 더 보안을 강화하기 위해 Client Secret을 사용할 수 있다. + * Client Secret을 받는 위치는 내 애플리케이션 -> 제품 설정 -> 카카오로그인 -> 보안 입니다. + * Client Secret을 받고난후 밑에 활성화 상태를 사용함으로 변경해주어야한다. + */ + + //HttpHeader와 HttpBody를 하나의 오브젝트에 담는다 + //HttpHeader와 HttpBody 담기기 + HttpEntity> kakaoTokenRequest = new HttpEntity<>(params, httpHeaders); // + + //Http요청하기 그리고 response 변수의 응답 받음. + OAuthToken oAuthToken = restTemplate.exchange("https://kauth.kakao.com/oauth/token", HttpMethod.POST, kakaoTokenRequest, OAuthToken.class).getBody(); + + return oAuthToken; + } + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Member/Service/MemberService.java b/src/main/java/Remoa/BE/Web/Member/Service/MemberService.java new file mode 100644 index 0000000..9a775af --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Service/MemberService.java @@ -0,0 +1,138 @@ +package Remoa.BE.Web.Member.Service; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.GerneralLoginDto.*; +import Remoa.BE.Web.Member.Repository.MemberRepository; +import Remoa.BE.config.auth.RefreshToken; +import Remoa.BE.config.jwt.JwtTokenProvider; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Service +@Transactional(readOnly = true) +@Slf4j +@RequiredArgsConstructor +public class MemberService { + @Value("${security.jwt.token.refresh-expiration-minutes}") + public long refreshExpirationMinutes; + + private final Random random = new Random(); + + private final JwtTokenProvider jwtTokenProvider; + private final MemberRepository memberRepository; + private final PasswordEncoder bCryptPasswordEncoder; + private final RedisTemplate redisTemplate; + + @Transactional + public Long join(Member member) { + // validateDuplicateMember(member); +// member.hashPassword(this.bCryptPasswordEncoder); + memberRepository.save(member); + return member.getMemberId(); + + } + + @Transactional + public void deleteMemberFromDB(Member member){ + memberRepository.deleteMemberFromDB(member.getMemberId()); + } + + + public GeneralLoginRes generalLogin(GeneralLoginReq loginReq) { + Member member = memberRepository.findByAccount(loginReq.getAccount()).orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + if (!bCryptPasswordEncoder.matches(loginReq.getPassword(), member.getPassword())) { + throw new BaseException(CustomMessage.UNAUTHORIZED); + } + String token = jwtTokenProvider.createToken(member.getAccount()); + String refreshToken = jwtTokenProvider.createRefreshToken(member.getAccount()); + + log.info("==============================================================="); + log.info("token : {}", token); + log.info("refreshToken : {}", refreshToken); + log.info("==============================================================="); + + updateRefreshToken(member, refreshToken); + + return new GeneralLoginRes(token, refreshToken, member); + } + + private void updateRefreshToken(Member member, String refreshToken) { + // 새로운 RefreshToken 객체를 생성하고 바로 저장 + RefreshToken refreshTokenObj = new RefreshToken(member.getAccount(), refreshToken); + redisTemplate.opsForValue().set(member.getAccount(), refreshTokenObj, refreshExpirationMinutes, TimeUnit.MINUTES); + } + + private void validateDuplicateMember(Member member) { + log.info("member={}", member.getEmail()); + Optional findMembers = memberRepository.findByAccount(member.getAccount()); + if (findMembers.isPresent()) { + throw new IllegalStateException("이미 존재하는 회원입니다."); + } + } + + @Transactional + public void adminSignUp(AdminSignUpReq adminSignUpReq) { + adminSignUpReq.setPassword(bCryptPasswordEncoder.encode(adminSignUpReq.getPassword())); + if (memberRepository.existsByAccount(adminSignUpReq.getAccount())) { + throw new BaseException(CustomMessage.BAD_DUPLICATE); + } + memberRepository.save(adminSignUpReq.toEntity()); + } + + @Transactional + public GeneralSignUpRes generalSignUp(GeneralSignUpReq signUpReq) { + signUpReq.setPassword(bCryptPasswordEncoder.encode(signUpReq.getPassword())); + if (memberRepository.existsByAccount(signUpReq.getAccount())) { + throw new BaseException(CustomMessage.BAD_DUPLICATE); + } + + String uniqueNickname = generateUniqueNickname(); + + Member member = signUpReq.toEntity(); + member.setNickname(uniqueNickname); + Member savedMember = memberRepository.save(member); + + return new GeneralSignUpRes(savedMember.getMemberId()); + } + + private String generateUniqueNickname() { + String randomNumber; + boolean nicknameDuplicate; + do { + randomNumber = Integer.toString((random.nextInt(900_000) + 100_000)); + nicknameDuplicate = memberRepository.existsByNickname("유저" + randomNumber); + } while (nicknameDuplicate); + return "유저" + randomNumber; + } + + public Boolean isNicknameDuplicate(String nickname) { + List findMembers = memberRepository.mfindByNickname(nickname); + return !(findMembers.size() == 0); + } + + + public Member findOne(Long memberId) { + Optional member = memberRepository.findOne(memberId); + return member.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "User not found")); + } + + public Optional findByKakaoId(Long kakaoId) { + return memberRepository.findByKakaoId(kakaoId); + } + + +} diff --git a/src/main/java/Remoa/BE/Web/Member/Service/MyFollowingService.java b/src/main/java/Remoa/BE/Web/Member/Service/MyFollowingService.java new file mode 100644 index 0000000..6682f89 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Service/MyFollowingService.java @@ -0,0 +1,109 @@ +package Remoa.BE.Web.Member.Service; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Res.ResMypageFollowing; +import Remoa.BE.Web.Member.Dto.Res.ResMypageList; +import Remoa.BE.Web.Member.MemberUtils; +import Remoa.BE.Web.Member.Repository.FollowRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class MyFollowingService { + + private final FollowRepository followRepository; + + private final FollowService followService; + + public List findResMypageList(Member member, int isFollowing) { + List resMypageLists = new ArrayList<>(); + List memberList; + + if (isFollowing == 1) { // 마이페이지 팔로잉 관리 화면 + memberList = followRepository.loadFollows(member); // member가 팔로우하는 유저 확인 + log.warn("followList : "); + memberList.forEach(m -> log.warn(m.getNickname())); + } else { // 마이페이지 팔로워 관리 화면 + memberList = followRepository.loadFollowers(member); + log.warn("followerList : "); + memberList.forEach(m -> log.warn(m.getNickname())); + } + + for (Member followMember : memberList) { + // followMember가 팔로잉하는 유저 구하기 + List followingMemberFollowing = followRepository.loadFollows(followMember); + // followMember를 팔로우하는 유저 구하기(팔로워) + List followingMemberFollower = followRepository.loadFollowers(followMember); + if (isFollowing == 1) { // 마이페이지 팔로잉 관리 화면 + ResMypageList resMypageList = ResMypageList.builder() + .profileImage(followMember.getProfileImage()) + .userName(followMember.getNickname()) + .followingNum(followingMemberFollowing.size()) + .followerNum(followingMemberFollower.size()) + .oneLineIntroduction(followMember.getOneLineIntroduction()) + .memberId(followMember.getMemberId()) + // 팔로잉 목록에서는 팔로워를 팔로잉하는지 확인할 필요가 없으므로 null처리 + .isFollow(Boolean.TRUE) + .build(); + resMypageLists.add(resMypageList); + } else { // 마이페이지 팔로워 관리 화면 + ResMypageList resMypageList = ResMypageList.builder() + .profileImage(followMember.getProfileImage()) + .userName(followMember.getNickname()) + .followingNum(followingMemberFollowing.size()) + .followerNum(followingMemberFollower.size()) + .oneLineIntroduction(followMember.getOneLineIntroduction()) + .memberId(followMember.getMemberId()) + // 팔로워를 팔로잉하는지 확인 - ture : 팔로우 함 / false : 팔로우 안 함 + .isFollow(followService.isMyMemberFollowMember(member, followMember)) + .build(); + resMypageLists.add(resMypageList); + } + } + + return resMypageLists; + } + + /** + * 마이페이지 팔로잉 관리 화면에 사용 + * + * @param member + * @return ResMypageFollowing + */ + @Transactional + public ResMypageFollowing mypageFollowing(Member member) { + + return ResMypageFollowing.builder() + .memberId(member.getMemberId()) + .userName(member.getNickname()) + .followNum(member.getFollows().size()) // 내가 팔로우하고 있는 유저 수 + .resMypageList(findResMypageList(member, 1)) + .build(); + } + + /** + * 마이페이지 팔로워 관리 화면에 사용 + * + * @param member + * @return ResMypageFollowing + */ + @Transactional + public ResMypageFollowing mypageFollower(Member member) { + + return ResMypageFollowing.builder() + .memberId(member.getMemberId()) + .userName(member.getNickname()) + .followNum(followRepository.loadFollowers(member).size()) // 나를 팔로우하고 있는 유저 수 + .resMypageList(findResMypageList(member, 0)) + .build(); + } + + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Member/Service/ProfileService.java b/src/main/java/Remoa/BE/Web/Member/Service/ProfileService.java new file mode 100644 index 0000000..e6a9551 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Member/Service/ProfileService.java @@ -0,0 +1,88 @@ +package Remoa.BE.Web.Member.Service; + +import Remoa.BE.Web.Inquiry.Service.InquiryReplyService; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Req.EditProfileForm; +import Remoa.BE.Web.Inquiry.Service.InquiryService; +import Remoa.BE.Web.Notice.Service.NoticeService; +import Remoa.BE.utill.MemberInfo; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.UUID; + + +@Service +@Slf4j +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ProfileService { + + private final MemberService memberService; + private final InquiryService inquiryService; + private final NoticeService noticeService; + private final InquiryReplyService inquiryReplyService; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + private final AmazonS3 amazonS3; + + @Transactional + public void editProfile(Long memberId, EditProfileForm profile) { + Member member = memberService.findOne(memberId); + + String oldNickname = member.getNickname(); + String newNickname = profile.getNickname(); + + //공지사항 및 문의사항에 변경된 닉네임 반영 + noticeService.modifying_Notice_NickName(oldNickname, newNickname); + inquiryService.modifying_Inquiry_NickName(oldNickname, newNickname); + inquiryReplyService.modifying_Inquiry_Reply_NickName(oldNickname, newNickname); + + // 사용자의 프로필 정보 수정 + member.setNickname(profile.getNickname()); + member.setPhoneNumber(profile.getPhoneNumber()); + member.setUniversity(profile.getUniversity()); + member.setOneLineIntroduction(profile.getOneLineIntroduction()); + + //변경된 정보 contextholder에 반영 + MemberInfo.securityLoginWithoutLoginForm(member); + } + + public String editProfileImg(String nickname, String profileImageUrl) throws IOException { + // 프로필사진을 jpg로 변환하기 + URL profileURL = new URL(profileImageUrl); + InputStream is = profileURL.openStream(); + + // 이미지 파일 생성 + BufferedImage image = ImageIO.read(is); + File outputFile = new File(nickname + ".jpg"); + ImageIO.write(image, "jpg", outputFile); + + // 사진으로 바꾼뒤 바로 S3로 업로드하기 + String url = uploadProfileImg(outputFile); + outputFile.delete(); + return url; + } + + // 프로필 사진 초기설정 - S3에 저장하기 + public String uploadProfileImg(File file) { + String s3FileName = UUID.randomUUID() + "-" + file.getName(); + ObjectMetadata objMeta = new ObjectMetadata(); + objMeta.setContentLength(file.length()); + amazonS3.putObject(bucket, "img/" + s3FileName, file); + return amazonS3.getUrl(bucket, "img/" + s3FileName).toString().replaceAll("\\+", "+"); + + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Member/Service/WithdrewService.java b/src/main/java/Remoa/BE/Web/Member/Service/WithdrewService.java similarity index 60% rename from src/main/java/Remoa/BE/Member/Service/WithdrewService.java rename to src/main/java/Remoa/BE/Web/Member/Service/WithdrewService.java index bc8d43c..62682a9 100644 --- a/src/main/java/Remoa/BE/Member/Service/WithdrewService.java +++ b/src/main/java/Remoa/BE/Web/Member/Service/WithdrewService.java @@ -1,6 +1,6 @@ -package Remoa.BE.Member.Service; +package Remoa.BE.Web.Member.Service; -import Remoa.BE.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.Member; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -12,12 +12,19 @@ @RequiredArgsConstructor public class WithdrewService { + private final FollowService followService; + /** * Dirty Checking 기능을 통한 엔티티의 deleted 필드를 true로 만들어 soft delete 시켜줌. * @param member */ @Transactional public void withdrewRemoa(Member member) { - member.setDeleted(true); + member.setName("탈퇴한 멤버"); + member.setNickname("탈퇴한 멤버"); + member.setKakaoId(0L); + member.setOneLineIntroduction(null); + member.setUniversity(null); + } } diff --git a/src/main/java/Remoa/BE/Web/MyPage/Controller/MyActivityController.java b/src/main/java/Remoa/BE/Web/MyPage/Controller/MyActivityController.java new file mode 100644 index 0000000..a864794 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/MyPage/Controller/MyActivityController.java @@ -0,0 +1,483 @@ +package Remoa.BE.Web.MyPage.Controller; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Comment.Service.CommentService; +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.CommentFeedback.Dto.ResReceivedCommentFeedbackDto; +import Remoa.BE.Web.CommentFeedback.Service.CommentFeedbackService; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Feedback.Service.FeedbackService; +import Remoa.BE.Web.Member.Domain.ContentType; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import Remoa.BE.Web.Member.Service.FollowService; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.MyPage.Dto.Res.*; +import Remoa.BE.Web.Post.Domain.PostScrap; +import Remoa.BE.Web.Post.Dto.Response.*; +import Remoa.BE.Web.Post.Service.PostService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.*; +import java.util.stream.Collectors; + +@Tag(name = "내 활동 기능 Test Completed", description = "내 활동 기능 API") +@RestController +@Slf4j +@RequiredArgsConstructor +public class MyActivityController { + + private final MemberService memberService; + private final CommentFeedbackService commentFeedbackService; + private final PostService postService; + private final FollowService followService; + private final CommentService commentService; + private final FeedbackService feedbackService; + + /** + * 내 활동 관리 + * + * @param + * @return Map + * "contents" : 내가 작성한 최신 댓글(Comment, Feedback 무관 1개) + * "posts" : 내가 스크랩한 post들을 가장 최근 스크랩한 순서의 List(12개). + */ + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "내 활동을 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/user/activity") + @Operation(summary = "내 활동 조회 Test Completed", description = "내가 작성한 최신 코멘트/피드백(Comment, Feedback 무관 1개)와 스크랩한 게시물들을 조회합니다.") + public ResponseEntity> myActivity(@AuthenticationPrincipal MemberDetails memberDetails) { + + log.info("EndPoint Get /user/activity"); + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + ResMyActivityDto result = new ResMyActivityDto(); + + CommentFeedback commentFeedback = commentFeedbackService.findNewestCommentFeedback(myMember); + + ResCommentFeedbackDto commentOrFeedback = null; + if (commentFeedback != null && commentFeedback.getType().equals(ContentType.FEEDBACK)) { + commentOrFeedback = feedbackBuilder(commentFeedback); + } else if (commentFeedback != null && commentFeedback.getType().equals(ContentType.COMMENT)) { + commentOrFeedback = commentBuilder(commentFeedback); + } + result.setContent(commentOrFeedback); + + /** + * 조회한 최근에 스크랩한 12개의 post들을 dto로 mapping. + */ + List posts = postService.findRecentTwelveScrapedPost(myMember).stream() + .map(post -> ResPostDto.builder() + .postId(post.getPostId()) + .postMember(new ResMemberInfoDto(post.getMember().getMemberId(), + post.getMember().getNickname(), + post.getMember().getProfileImage(), + followService.isMyMemberFollowMember(myMember, post.getMember()))) + .thumbnail(post.getThumbnailUrl()) + .title(post.getTitle()) + .likeCount(post.getLikeCount()) + .isLikedPost((myMember != null && !post.getMember().getMemberId().equals(myMember.getMemberId())) ? postService.isThisPostLiked(myMember, post) : null) + .postingTime(post.getPostingTime().toString()) + .views(post.getViews()) + .scrapCount(post.getScrapCount()) + .isScrapedPost((myMember != null && !post.getMember().getMemberId().equals(myMember.getMemberId())) ? postService.isThisPostScraped(myMember, post) : null) + .categoryName(post.getCategory().getName()) + .build()).collect(Collectors.toList()); + result.setPosts(posts); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, result); + return ResponseEntity.ok(response); + //return successResponse(CustomMessage.OK, result); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "내가 작성한 코멘트/피드백을 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "400", description = "페이지 번호가 잘못되었습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/user/comment-feedback") + @Operation(summary = "내가 작성한 코멘트/피드백 조회 Test Completed", description = "내가 작성한 최신 코멘트/피드백들을 조회합니다." + + "
asc : 오래된순" + + "
desc : 최신순(default)") + public ResponseEntity> myCommentFeedback(@RequestParam(name = "page", defaultValue = "1", required = false) int pageNum, + @RequestParam(name = "sort", defaultValue = "desc", required = false) String sortDirection, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Get /user/comment-feedback"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + pageNum -= 1; + if (pageNum < 0) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + Map result = new HashMap<>(); + + Page commentOrFeedback = commentFeedbackService.getMyCommentOrFeedback(pageNum, myMember, sortDirection); + + //조회할 레퍼런스가 db에 있으나, 현재 페이지에 조회할 데이터가 없는 경우 == 페이지 번호를 잘못 입력 + if ((commentOrFeedback.getContent().isEmpty()) && (commentOrFeedback.getTotalElements() > 0)) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + /** + * 조회한 가장 최근에 작성한 댓글들을 dto로 mapping + * 하나의 게시물에 여러 코멘트, 피드백 모두 단 경우 하나의 최신 하나만 보여주도록 구현 + */ + + List contents = commentOrFeedback.stream() + .map(commentFeedback -> { + ResCommentFeedbackDto map = null; + if (commentFeedback.getType().equals(ContentType.FEEDBACK)) { + map = feedbackBuilder(commentFeedback); + } else if (commentFeedback.getType().equals(ContentType.COMMENT)) { + map = commentBuilder(commentFeedback); + } + return map; + }).collect(Collectors.toList()); + + ResMyCommentFeedbackPaging myCommentFeedbackPaging = ResMyCommentFeedbackPaging.builder() + .contents(contents) + .totalPages(commentOrFeedback.getTotalPages()) + .totalOfAllComments(commentOrFeedback.getTotalElements()) + .totalOfPageElements(commentOrFeedback.getNumberOfElements()) + .build(); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, myCommentFeedbackPaging); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, result); + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "내가 작성한 코멘트 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "400", description = "페이지 번호가 잘못되었습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/user/comment") + @Operation(summary = "내가 작성한 코멘트 조회 Test Completed", description = "내가 작성한 코멘트 조회합니다." + + "
asc : 오래된순" + + "
desc : 최신순(default)") + public ResponseEntity> myComment(@RequestParam(name = "page", defaultValue = "1", required = false) int pageNum, + @RequestParam(name = "sort", defaultValue = "desc", required = false) String sortDirection, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Get /user/comment"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + pageNum -= 1; + if (pageNum < 0) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + Map result = new HashMap<>(); + + Page comments = commentService.getMyComments(pageNum, myMember, sortDirection); + + //조회할 레퍼런스가 db에 있으나, 현재 페이지에 조회할 데이터가 없는 경우 == 페이지 번호를 잘못 입력 + if ((comments.getContent().isEmpty()) && (comments.getTotalElements() > 0)) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + /** + * 조회한 가장 최근에 작성한 댓글들을 dto로 mapping + * 하나의 게시물에 여러 코멘트 단 경우 하나의 정렬 asc, desc에 따라 가장 오래된 것 또는 최신의 것 하나만 보여주도록 구현 + */ + List contents = comments.stream() + .map(comment -> { + ResMyCommentDto resMyCommentDto = commentBuilder(comment); + return resMyCommentDto; + }).collect(Collectors.toList()); + + ResMyCommentPaging myCommentPaging = ResMyCommentPaging.builder() + .contents(contents) + .totalPages(comments.getTotalPages()) + .totalOfAllComments(comments.getTotalElements()) + .totalOfPageElements(comments.getNumberOfElements()) + .build(); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, myCommentPaging); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, result); + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "내가 작성한 피드백 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "400", description = "페이지 번호가 잘못되었습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/user/feedback") + @Operation(summary = "내가 작성한 피드백 조회 Test Completed", description = "내가 작성한 피드백 조회합니다." + + "
asc : 오래된순" + + "
desc : 최신순(default)") + public ResponseEntity> myFeedback(@RequestParam(name = "page", defaultValue = "1", required = false) int pageNum, + @RequestParam(name = "sort", defaultValue = "desc", required = false) String sortDirection, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Get /user/feedback"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + pageNum -= 1; + if (pageNum < 0) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + + Page feedbacks = feedbackService.getMyFeedback(pageNum, myMember, sortDirection); + + //조회할 레퍼런스가 db에 있으나, 현재 페이지에 조회할 데이터가 없는 경우 == 페이지 번호를 잘못 입력 + if ((feedbacks.getContent().isEmpty()) && (feedbacks.getTotalElements() > 0)) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + } + + /** + * 조회한 가장 최근에 작성한 피드백들을 dto로 mapping + * 하나의 게시물에 여러 피드백 모두 단 경우 하나의 최신 하나만 보여주도록 구현 + */ + List contents = feedbacks.stream() + .map(this::feedbackBuilder).toList(); + + ResMyFeedbackPaging myFeedbackPaging = ResMyFeedbackPaging.builder() + .contents(contents) + .totalPages(feedbacks.getTotalPages()) + .totalOfAllFeedbacks(feedbacks.getTotalElements()) + .totalOfPageElements(feedbacks.getNumberOfElements()) + .build(); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, myFeedbackPaging); + return ResponseEntity.ok(response); + + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "내가 받은 코멘트/피드백을 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "400", description = "페이지 번호가 잘못되었습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/user/receive") + @Operation(summary = "내가 받은 코멘트/피드백 조회 Test Completed", description = "내가 받은 최신 코멘트/피드백들을 조회합니다." + + " \"
category : \"idea\", \"marketing\", \"design\", \"video\", \"digital\", \"etc\"") + public ResponseEntity> receivedCommentFeedback(@RequestParam(required = false, defaultValue = "all") String category, + @RequestParam(required = false, defaultValue = "1", name = "page") int pageNum, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Get /user/receive"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + pageNum -= 1; + if (pageNum < 0) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + + Page commentOrFeedbacks = commentFeedbackService.findReceivedCommentOrFeedback(myMember, pageNum, category); + + List contents = commentOrFeedbacks + .stream() + .map(commentFeedback -> { + ResCommentFeedbackDto map = null; + if (commentFeedback.getType().equals(ContentType.FEEDBACK)) { + map = feedbackBuilder(commentFeedback); + } else if (commentFeedback.getType().equals(ContentType.COMMENT)) { + map = commentBuilder(commentFeedback); + } + return map; + }).collect(Collectors.toList()); + + + ResReceivedCommentFeedbackDto responseDto = ResReceivedCommentFeedbackDto.builder() + .contents(contents) + .totalPages(commentOrFeedbacks.getTotalPages()) + .totalOfAllComments(commentOrFeedbacks.getTotalElements()) + .totalOfPageElements(commentOrFeedbacks.getNumberOfElements()) + .build(); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, responseDto); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, result); + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "내가 스크랩한 게시글을 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "400", description = "페이지 번호가 잘못되었습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/user/scrap") // 내가 스크랩한 게시글 확인 + @Operation(summary = "내가 스크랩한 게시글 조회 Test Completed", description = "내가 스크랩한 게시글들을 확인합니다." + + "
category : \"idea\", \"marketing\", \"design\", \"video\", \"digital\", \"etc\" " + + "
asc : 오래된순" + + "
desc : 최신순(default)") + public ResponseEntity> myScrap( + @RequestParam(required = false, defaultValue = "all") String category, + @RequestParam(required = false, defaultValue = "desc") String sort, + @RequestParam(name = "page", defaultValue = "1", required = false) int pageNum, + @AuthenticationPrincipal MemberDetails memberDetails + ) { + log.info("EndPoint Get /user/scrap"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + + ResMyScrapDto resMyScrapDto = new ResMyScrapDto(); + + pageNum -= 1; + if (pageNum < 0) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + //return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + /** + * 조회한 최근에 스크랩한 12개의 post들을 dto로 mapping. + */ + Page posts = postService.findScrapedPost(pageNum, myMember, category, sort); + + //조회할 레퍼런스가 db에 있으나, 현재 페이지에 조회할 데이터가 없는 경우 == 페이지 번호를 잘못 입력 + if ((posts.getContent().isEmpty()) && (posts.getTotalElements() > 0)) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + List postDtoList = posts.stream() + .map(PostScrap::getPost) + .toList() + .stream() + .map(post -> ResPostDto.builder() + .postId(post.getPostId()) + .postMember(new ResMemberInfoDto(post.getMember().getMemberId(), + post.getMember().getNickname(), + post.getMember().getProfileImage(), + followService.isMyMemberFollowMember(myMember, post.getMember()))) + .thumbnail(post.getThumbnailUrl()) + .title(post.getTitle()) + .likeCount(post.getLikeCount()) + .isLikedPost((myMember != null && !post.getMember().getMemberId().equals(myMember.getMemberId())) ? postService.isThisPostLiked(myMember, post) : null) + .postingTime(post.getPostingTime().toString()) + .views(post.getViews()) + .scrapCount(post.getScrapCount()) + .isScrapedPost((myMember != null && !post.getMember().getMemberId().equals(myMember.getMemberId())) ? postService.isThisPostScraped(myMember, post) : null) + .categoryName(post.getCategory().getName()).build()) + .collect(Collectors.toList()); + + ResMyScrapDto myScrapDto = ResMyScrapDto.builder() + .posts(postDtoList) + .totalPages(posts.getTotalPages()) //전체 페이지의 수 + .totalOfAllPosts(posts.getTotalElements())//모든 게시글 수 + .totalOfPageElements(posts.getNumberOfElements())//현 페이지 게시글 수 + .build(); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, myScrapDto); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, result); + } + + private ResMyCommentDto commentBuilder(Comment comment) { + return ResMyCommentDto.builder() + .title(comment.getPost().getTitle()) + .postId(comment.getPost().getPostId()) + .commentId(comment.getCommentId()) + .thumbnail(comment.getPost().getThumbnailUrl()) + .member(new ResMemberInfoDto(comment.getMember().getMemberId(), + comment.getMember().getNickname(), + comment.getMember().getProfileImage(), + null)) + .content(comment.getContent()) + .likeCount(comment.getLikeCount()) + .build(); + } + + private ResMyFeedbackDto feedbackBuilder(Feedback feedback) { + return ResMyFeedbackDto.builder() + .title(feedback.getPost().getTitle()) + .postId(feedback.getPost().getPostId()) + .feedbackId(feedback.getFeedbackId()) + .thumbnail(feedback.getPost().getThumbnailUrl()) + .member(new ResMemberInfoDto(feedback.getMember().getMemberId(), + feedback.getMember().getNickname(), + feedback.getMember().getProfileImage(), + null)) + .content(feedback.getContent()) + .likeCount(feedback.getLikeCount()).build(); + } + + + private ResCommentFeedbackDto commentBuilder(CommentFeedback commentFeedback) { + return ResCommentFeedbackDto.builder() + .title(commentFeedback.getPost().getTitle()) + .postId(commentFeedback.getPost().getPostId()) + .commentId(commentFeedback.getComment().getCommentId()) + .thumbnail(commentFeedback.getPost().getThumbnailUrl()) + .member(new ResMemberInfoDto(commentFeedback.getMember().getMemberId(), + commentFeedback.getMember().getNickname(), + commentFeedback.getMember().getProfileImage(), + null)) + .content(commentFeedback.getComment().getContent()) + .likeCount(commentFeedback.getComment().getLikeCount()) + .isComment(true) + .build(); + } + + private ResCommentFeedbackDto feedbackBuilder(CommentFeedback commentFeedback) { + return ResCommentFeedbackDto.builder() + .title(commentFeedback.getPost().getTitle()) + .postId(commentFeedback.getPost().getPostId()) + .feedbackId(commentFeedback.getFeedback().getFeedbackId()) + .thumbnail(commentFeedback.getPost().getThumbnailUrl()) + .member(new ResMemberInfoDto(commentFeedback.getMember().getMemberId(), + commentFeedback.getMember().getNickname(), + commentFeedback.getMember().getProfileImage(), + null)) + .content(commentFeedback.getFeedback().getContent()) + .likeCount(commentFeedback.getFeedback().getLikeCount()) + .isComment(commentService.existsMyComment(commentFeedback.getPost(), commentFeedback.getMember())) + .build(); + + } + + +} diff --git a/src/main/java/Remoa/BE/Web/MyPage/Controller/MyPostController.java b/src/main/java/Remoa/BE/Web/MyPage/Controller/MyPostController.java new file mode 100644 index 0000000..e75a124 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/MyPage/Controller/MyPostController.java @@ -0,0 +1,217 @@ +package Remoa.BE.Web.MyPage.Controller; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import Remoa.BE.Web.Member.Service.FollowService; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Dto.Response.PostPageResponseDto; +import Remoa.BE.Web.Post.Dto.Response.ResPostDto; +import Remoa.BE.Web.MyPage.Service.MyPostService; +import Remoa.BE.Web.Post.Service.PostService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +import static Remoa.BE.config.DbInit.categoryList; + +@Tag(name = "나의 레퍼런스 조회 기능 Test Completed", description = "나의 레퍼런스 조회 기능 API") +@Slf4j +@RestController +@RequiredArgsConstructor +public class MyPostController { + + private final MemberService memberService; + private final PostService postService; + private final MyPostService myPostService; + private final FollowService followService; + + // Entity <-> DTO 간의 변환을 편리하게 하고자 ModelMapper 사용.(build.gradle에 의존성 주입 완료) +// private final ModelMapper modelMapper = new ModelMapper(); + + + /** + * 내 작업물 목록 페이지 + */ + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "내가 레퍼런스를 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "400", description = "페이지 번호가 잘못되었습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/user/reference") + @Operation(summary = "내 레퍼런스 목록 조회 Test Completed", description = "내가 작성한 레퍼런스 목록을 조회합니다. " + + "
category : \"idea\", \"marketing\", \"design\", \"video\", \"digital\", \"etc\"" + + "
sort : \"views\", \"likes\", \"scrap\"") + public ResponseEntity> myReference(HttpServletRequest request, + @RequestParam(required = false, defaultValue = "all") String category, + @RequestParam(required = false, defaultValue = "1", name = "page") int pageNumber, + @RequestParam(required = false, defaultValue = "newest") String sort, + @RequestParam(required = false, defaultValue = "") String title, + @AuthenticationPrincipal MemberDetails memberDetails) { + + log.info("EndPoint Get /user/reference"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + pageNumber -= 1; + if (pageNumber < 0) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + Page posts; + + if (categoryList.contains(category)) { + posts = myPostService.sortAndPaginatePostsByCategoryAndMember(category, pageNumber, sort, myMember, title); + } else { + posts = myPostService.sortAndPaginatePostsByMember(pageNumber, sort, myMember, title); + } + + //조회할 레퍼런스가 db에 있으나, 현재 페이지에 조회할 데이터가 없는 경우 == 페이지 번호를 잘못 입력 + if ((posts.getContent().isEmpty()) && (posts.getTotalElements() > 0)) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + List result = new ArrayList<>(); + + for (Post post : posts) { + ResPostDto map = ResPostDto.builder() + .postingTime(post.getPostingTime().toString()) + .postMember(new ResMemberInfoDto(post.getMember().getMemberId(), + post.getMember().getNickname(), + post.getMember().getProfileImage(), + null)) + .postId(post.getPostId()) + .views(post.getViews()) + .categoryName(post.getCategory().getName()) + .likeCount(post.getLikeCount()) + .thumbnail(post.getThumbnailUrl()) + .scrapCount(post.getScrapCount()) + .title(post.getTitle()).build(); + result.add(map); + } + //프론트에서 쓰일 조회한 레퍼런스들과 페이지 관련한 값들 map에 담아서 return. + + PostPageResponseDto responseDto = PostPageResponseDto.builder() + .references(result) //조회한 레퍼런스들 + .totalPages(posts.getTotalPages()) //전체 페이지의 수 + .totalOfAllReferences(posts.getTotalElements()) //모든 레퍼런스의 수 + .totalOfPageElements(posts.getNumberOfElements()) //현 페이지의 레퍼런스 수 + .build(); + + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, responseDto); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, referencesAndPageInfo); + + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "다른 사용자의 레퍼런스를 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "400", description = "페이지 번호가 잘못되었습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/user/reference/{member_id}") + @Operation(summary = "다른 사용자의 레퍼런스 목록 조회 Test Completed", description = "다른 사용자가 작성한 레퍼런스 목록을 조회합니다." + + "
category : \"idea\", \"marketing\", \"design\", \"video\", \"digital\", \"etc\"" + + "
sort : \"views\", \"likes\", \"scrap\"") + public ResponseEntity> otherReference(HttpServletRequest request, + @PathVariable("member_id") Long memberId, + @RequestParam(required = false, defaultValue = "all") String category, + @RequestParam(required = false, defaultValue = "1", name = "page") int pageNumber, + @RequestParam(required = false, defaultValue = "newest") String sort, + @RequestParam(required = false, defaultValue = "") String title, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Get /user/reference/{member_id}"); + + Member myMember = null; + if (memberDetails != null) { + Long myMemberId = memberDetails.getMemberId(); + myMember = memberService.findOne(myMemberId); + } + + + Member selectedMember = memberService.findOne(memberId); + + pageNumber -= 1; + if (pageNumber < 0) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + Page posts; + + if (categoryList.contains(category)) { + posts = myPostService.sortAndPaginatePostsByCategoryAndMember(category, pageNumber, sort, selectedMember, title); + } else { + posts = myPostService.sortAndPaginatePostsByMember(pageNumber, sort, selectedMember, title); + } + + //조회할 레퍼런스가 db에 있으나, 현재 페이지에 조회할 데이터가 없는 경우 == 페이지 번호를 잘못 입력 + if ((posts.getContent().isEmpty()) && (posts.getTotalElements() > 0)) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + List result = new ArrayList<>(); + + for (Post post : posts) { + ResPostDto map = ResPostDto.builder() + .postingTime(post.getPostingTime().toString()) + .postMember(new ResMemberInfoDto(post.getMember().getMemberId(), + post.getMember().getNickname(), + post.getMember().getProfileImage(), + myMember != null ? followService.isMyMemberFollowMember(myMember, post.getMember()) : null)) + .postId(post.getPostId()) + .views(post.getViews()) + .categoryName(post.getCategory().getName()) + .likeCount(post.getLikeCount()) + .isLikedPost((myMember != null && !post.getMember().getMemberId().equals(myMember.getMemberId())) ? postService.isThisPostLiked(myMember, post) : null) + .thumbnail(post.getThumbnailUrl()) + .scrapCount(post.getScrapCount()) + .isScrapedPost((myMember != null && !post.getMember().getMemberId().equals(myMember.getMemberId())) ? postService.isThisPostScraped(myMember, post) : null) + .title(post.getTitle()).build(); + result.add(map); + } + //프론트에서 쓰일 조회한 레퍼런스들과 페이지 관련한 값들 map에 담아서 return. + PostPageResponseDto responseDto = PostPageResponseDto.builder() + .references(result) //조회한 레퍼런스들 + .totalPages(posts.getTotalPages()) //전체 페이지의 수 + .totalOfAllReferences(posts.getTotalElements()) //모든 레퍼런스의 수 + .totalOfPageElements(posts.getNumberOfElements()) //현 페이지의 레퍼런스 수 + .build(); + BaseResponse response = new BaseResponse<>(CustomMessage.OK, responseDto); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, referencesAndPageInfo); + } + + +} diff --git a/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyActivityDto.java b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyActivityDto.java new file mode 100644 index 0000000..1cca414 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyActivityDto.java @@ -0,0 +1,24 @@ +package Remoa.BE.Web.MyPage.Dto.Res; + +import Remoa.BE.Web.Post.Dto.Response.ResCommentFeedbackDto; +import Remoa.BE.Web.Post.Dto.Response.ResPostDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ResMyActivityDto { + + @Schema(description = "최신 댓글 또는 피드백 정보") + private ResCommentFeedbackDto content; + + @Schema(description = "최근에 스크랩한 게시물 리스트") + private List posts; +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyCommentDto.java b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyCommentDto.java new file mode 100644 index 0000000..7e624d6 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyCommentDto.java @@ -0,0 +1,32 @@ +package Remoa.BE.Web.MyPage.Dto.Res; + +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ResMyCommentDto { + + @Schema(description = "게시물 제목", example = "서울 빅데이터 공모전 최우수상") + private String title; + + @Schema(description = "게시물 ID", example = "2") + private Long postId; + + @Schema(description = "코멘트 ID") + private Long commentId; + + @Schema(description = "썸네일 URL", example = "https://remoa.s3.ap-northeast-2.amazonaws.com/thumbnail/3d6655e8-d922-4da4-aeac-5edc2a5cc0ca_basic_profile.png") + private String thumbnail; + + @Schema(description = "게시물 작성자 정보") + private ResMemberInfoDto member; + + @Schema(description = "코멘트 내용", example = "잘했어요") + private String content; + + @Schema(description = "좋아요 수", example = "0") + private Integer likeCount; +} diff --git a/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyCommentFeedbackPaging.java b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyCommentFeedbackPaging.java new file mode 100644 index 0000000..a01af8f --- /dev/null +++ b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyCommentFeedbackPaging.java @@ -0,0 +1,29 @@ +package Remoa.BE.Web.MyPage.Dto.Res; + + +import Remoa.BE.Web.Post.Dto.Response.ResCommentFeedbackDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ResMyCommentFeedbackPaging { + @Schema(description = "내가 작성한 최신 코멘트/피드백들의 목록") + private List contents; + + @Schema(description = "전체 페이지 수") + private int totalPages; + + @Schema(description = "모든 코멘트의 수") + private long totalOfAllComments; + + @Schema(description = "현재 페이지의 코멘트/피드백 수") + private int totalOfPageElements; +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyCommentPaging.java b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyCommentPaging.java new file mode 100644 index 0000000..55811b3 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyCommentPaging.java @@ -0,0 +1,30 @@ +package Remoa.BE.Web.MyPage.Dto.Res; + +import Remoa.BE.Web.Post.Dto.Response.ResCommentFeedbackDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ResMyCommentPaging { + + @Schema(description = "내가 작성한 최신 코멘트들의 목록") + private List contents; + + @Schema(description = "전체 페이지 수") + private int totalPages; + + @Schema(description = "모든 코멘트의 수") + private long totalOfAllComments; + + @Schema(description = "현재 페이지의 코멘트 수") + private int totalOfPageElements; +} diff --git a/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyFeedbackDto.java b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyFeedbackDto.java new file mode 100644 index 0000000..d3d65f9 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyFeedbackDto.java @@ -0,0 +1,32 @@ +package Remoa.BE.Web.MyPage.Dto.Res; + +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ResMyFeedbackDto { + + @Schema(description = "게시물 제목", example = "서울 빅데이터 공모전 최우수상") + private String title; + + @Schema(description = "게시물 ID", example = "2") + private Long postId; + + @Schema(description = "피드백 ID") + private Long feedbackId; + + @Schema(description = "썸네일 URL", example = "https://remoa.s3.ap-northeast-2.amazonaws.com/thumbnail/3d6655e8-d922-4da4-aeac-5edc2a5cc0ca_basic_profile.png") + private String thumbnail; + + @Schema(description = "게시물 작성자 정보") + private ResMemberInfoDto member; + + @Schema(description = "피드백 내용", example = "잘했어요") + private String content; + + @Schema(description = "좋아요 수", example = "0") + private Integer likeCount; +} diff --git a/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyFeedbackPaging.java b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyFeedbackPaging.java new file mode 100644 index 0000000..409dddd --- /dev/null +++ b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyFeedbackPaging.java @@ -0,0 +1,29 @@ +package Remoa.BE.Web.MyPage.Dto.Res; + +import Remoa.BE.Web.Post.Dto.Response.ResCommentFeedbackDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ResMyFeedbackPaging { + + @Schema(description = "내가 작성한 최신 피드백들의 목록") + private List contents; + + @Schema(description = "전체 페이지 수") + private int totalPages; + + @Schema(description = "모든 피드백의 수") + private long totalOfAllFeedbacks; + + @Schema(description = "현재 페이지의 피드백 수") + private int totalOfPageElements; +} diff --git a/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyScrapDto.java b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyScrapDto.java new file mode 100644 index 0000000..172ec59 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/MyPage/Dto/Res/ResMyScrapDto.java @@ -0,0 +1,29 @@ +package Remoa.BE.Web.MyPage.Dto.Res; + +import Remoa.BE.Web.Post.Dto.Response.ResPostDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ResMyScrapDto { + + @Schema(description = "스크랩한 게시글 리스트") + private List posts; + + @Schema(description = "전체 페이지의 수") + private int totalPages; + + @Schema(description = "모든 게시글의 수") + private long totalOfAllPosts; + + @Schema(description = "현재 페이지의 게시글 수") + private int totalOfPageElements; +} diff --git a/src/main/java/Remoa/BE/Web/MyPage/Service/MyPostService.java b/src/main/java/Remoa/BE/Web/MyPage/Service/MyPostService.java new file mode 100644 index 0000000..c56452d --- /dev/null +++ b/src/main/java/Remoa/BE/Web/MyPage/Service/MyPostService.java @@ -0,0 +1,127 @@ +package Remoa.BE.Web.MyPage.Service; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Repository.CategoryRepository; +import Remoa.BE.Web.Post.Repository.MyReferenceRepository; +import Remoa.BE.Web.Post.Repository.PostPagingRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static Remoa.BE.utill.Constant.HOME_PAGE_SIZE; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MyPostService { + + private final PostPagingRepository postPagingRepository; + private final CategoryRepository categoryRepository; + + private final MyReferenceRepository myReferenceRepository; + + + private static final int RECEIVED_COMMENT_PAGE_SIZE = 3; + + public Page sortAndPaginatePostsByMember(int pageNumber, String sort, Member myMember, String title) { + Page posts; + PageRequest pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE); + //switch문을 통해 각 옵션에 맞게 sorting + switch (sort) { + case "views": + posts = postPagingRepository.findByMemberAndTitleContainingOrderByViewsDesc(pageable, myMember,title); + break; + case "likes": + posts = postPagingRepository.findByMemberAndTitleContainingOrderByLikeCountDesc(pageable, myMember,title); + break; + case "scrap": + posts = postPagingRepository.findByMemberAndTitleContainingOrderByScrapCountDesc(pageable, myMember,title); + break; + default: + //sort 문자열이 잘못됐을 경우 default인 최신순으로 정렬 + posts = postPagingRepository.findByMemberAndTitleContainingOrderByPostingTimeDesc(pageable, myMember,title); + break; + } + return posts; + } + + public Page sortAndPaginatePostsByCategoryAndMember(String category, int pageNumber, String sort, Member myMember,String title) { + Page posts; + PageRequest pageable; + switch (sort) { + case "views": + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("views").descending()); + break; + case "likes": + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("likeCount").descending()); + break; + case "scrap": + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("scrapCount").descending()); + break; + default: + //sort 문자열이 잘못됐을 경우 default인 최신순으로 정렬 + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("postingTime").descending()); + break; + } + posts = postPagingRepository.findByMemberAndCategoryAndTitleContaining(pageable, myMember, categoryRepository.findByCategoryName(category),title); + return posts; + } + + + + /* *//** + * 받은 피드백 관리에서 쓰이는 최신 3개순 포스트 + * @param page + * @param member + * @param category + * @return member가 작성한 최신 3개의 Post. + *//* + public Page getNewestThreePostsSortCategory(int page, Member member, String category) { + PageRequest pageable = PageRequest.of(page, RECEIVED_COMMENT_PAGE_SIZE, Sort.by("postingTime").descending()); + return postPagingRepository + .findByMemberAndCategoryAndCommentsIsNotEmpty(pageable, member, categoryRepository.findByCategoryName(category)); + } + + *//** + * 받은 피드백 관리에서 쓰이는 최신 3개순 포스트 + * @param page + * @param member + * @return member가 작성한 최신 3개의 Post. + *//* + public Page getNewestThreePosts(int page, Member member) { + PageRequest pageable = PageRequest.of(page, RECEIVED_COMMENT_PAGE_SIZE, Sort.by("postingTime").descending()); + return postPagingRepository.findByMemberAndCommentsIsNotEmpty(pageable, member); + } + + *//** + * 내 활동 관리에 쓰이는 코멘트 및 피드백을 단 작업물 + * @return Post + *//* + public Page getCommentedPost(int size, Member member) { + PageRequest pageable = PageRequest.of(0, size, Sort.by("postingTime").descending()); + return postPagingRepository.findByMemberAndCommentsIsNotEmpty(pageable, member); + }*/ + + @Transactional + public void deleteReferenceCategory(Long memberId, Long categoryId){ + try { + List postList = myReferenceRepository.findByMemberMemberIdAndCategoryCategoryId(memberId,categoryId); + myReferenceRepository.deleteAll(postList); + } + catch (Exception e) + { + throw e; + } + + } + + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Notice/Controller/NoticeController.java b/src/main/java/Remoa/BE/Web/Notice/Controller/NoticeController.java new file mode 100644 index 0000000..553d6e8 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Notice/Controller/NoticeController.java @@ -0,0 +1,132 @@ +package Remoa.BE.Web.Notice.Controller; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Notice.Dto.Req.ReqNoticeDto; +import Remoa.BE.Web.Notice.Dto.Res.NoticeResponseDto; +import Remoa.BE.Web.Notice.Dto.Res.ResNoticeDetailDto; +import Remoa.BE.Web.Notice.Service.NoticeService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "공지 기능 Test Completed", description = "공지 기능 API") +@RestController +@RequiredArgsConstructor +@Slf4j +public class NoticeController { + + private final NoticeService noticeService; + private final MemberService memberService; + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "공지가 성공적으로 등록되었습니다."), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = MessageUtils.FORBIDDEN, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/notice") + @Operation(summary = "공지 등록 Test Completed", description = "공지를 등록합니다.") + public ResponseEntity postNotice(@Validated @RequestBody ReqNoticeDto reqNoticeDto, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Post /notice"); + + Member myMember = memberService.findOne(memberDetails.getMemberId()); + noticeService.registerNotice(reqNoticeDto, myMember.getNickname()); + + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "공지가 성공적으로 수정되었습니다."), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = MessageUtils.FORBIDDEN, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PutMapping("/notice/{id}") + @Operation(summary = "공지 수정 Test Completed", description = "공지를 수정합니다.") + public ResponseEntity updateNotice(@PathVariable("id") Long noticeId, + @Validated @RequestBody ReqNoticeDto reqNoticeDto, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Put /notice/{id}"); + + Member myMember = memberService.findOne(memberDetails.getMemberId()); + noticeService.updateNotice(noticeId, reqNoticeDto, myMember.getNickname()); + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "공지가 성공적으로 삭제되었습니다."), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = MessageUtils.FORBIDDEN, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/notice/{id}") + @Operation(summary = "공지 삭제 Test Completed", description = "공지를 삭제합니다.") + public ResponseEntity deleteNotice(@PathVariable("id") Long noticeId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Delete /notice/{id}"); + + Member myMember = memberService.findOne(memberDetails.getMemberId()); + noticeService.deleteNotice(noticeId, myMember); + + return new ResponseEntity<>(HttpStatus.OK); + } + + @GetMapping("/notice") + @Operation(summary = "공지 목록 조회 Test Completed", description = "페이지별 공지 목록을 조회합니다.") + public ResponseEntity> getNotice(@RequestParam(required = false, defaultValue = "1", name = "page") int pageNumber) { + log.info("EndPoint Get /notice"); + + pageNumber -= 1; + if (pageNumber < 0) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + BaseResponse response = new BaseResponse<>(CustomMessage.OK, noticeService.getNotice(pageNumber)); + return ResponseEntity.ok(response); + //return successResponse(CustomMessage.OK, noticeService.getNotice(pageNumber)); + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "공지 상세를 성공적으로 조회했습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @GetMapping("/notice/view") + @Operation(summary = "공지 상세 조회 Test Completed", description = "특정 공지의 상세 정보를 조회합니다.") + public ResponseEntity> getNoticeDetail(@RequestParam("view") int noticeId, + HttpSession session) { + log.info("EndPoint Get /notice/view"); + + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, noticeService.getNoticeView(noticeId,session)); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, noticeService.getNoticeView(view)); + } +} diff --git a/src/main/java/Remoa/BE/Web/Notice/Dto/Req/ReqNoticeDto.java b/src/main/java/Remoa/BE/Web/Notice/Dto/Req/ReqNoticeDto.java new file mode 100644 index 0000000..4c17769 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Notice/Dto/Req/ReqNoticeDto.java @@ -0,0 +1,33 @@ +package Remoa.BE.Web.Notice.Dto.Req; + +import Remoa.BE.Web.Notice.domain.Notice; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Data +public class ReqNoticeDto { + + @Schema(description = "제목", example = "공지사항 제목") + @NotNull + private String title; + + @Schema(description = "내용", example = "이 공지사항은 중요한 내용을 포함합니다.") + @NotNull + private String content; + + public Notice toEntityNotice(String enrollNickname) { + + return Notice.builder() + .author(enrollNickname) + .title(title) + .content(content) + .postingTime(LocalDateTime.now()) + .view(0) + .build(); + } + + +} diff --git a/src/main/java/Remoa/BE/Web/Notice/Dto/Res/NoticeResponseDto.java b/src/main/java/Remoa/BE/Web/Notice/Dto/Res/NoticeResponseDto.java new file mode 100644 index 0000000..409db16 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Notice/Dto/Res/NoticeResponseDto.java @@ -0,0 +1,30 @@ +package Remoa.BE.Web.Notice.Dto.Res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +@AllArgsConstructor +public class NoticeResponseDto { + + @Schema(description = "게시물 목록") + private List notices; + + @Schema(description = "전체 페이지 수", example = "5") + private int totalPages; + + @Schema(description = "전체 게시물 수", example = "100") + private long totalOfAllNotices; + + @Schema(description = "현재 페이지의 게시물 수", example = "10") + private int totalOfPageElements; + + + + // 생성자, getter, setter 등 +} diff --git a/src/main/java/Remoa/BE/Web/Notice/Dto/Res/ResNoticeDetailDto.java b/src/main/java/Remoa/BE/Web/Notice/Dto/Res/ResNoticeDetailDto.java new file mode 100644 index 0000000..143b282 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Notice/Dto/Res/ResNoticeDetailDto.java @@ -0,0 +1,53 @@ +package Remoa.BE.Web.Notice.Dto.Res; + + +import Remoa.BE.Web.Notice.domain.Notice; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Builder +@Getter +@AllArgsConstructor +public class ResNoticeDetailDto { + + + @Schema(description = "공지 ID", example = "1") + private Long noticeId; + + @Schema(description = "작성자", example = "관리자") + private String author; + + @Schema(description = "제목", example = "중요한 공지 제목") + private String title; + + @Schema(description = "내용", example = "이것은 중요한 공지 내용 입니다....") + private String content; + + @Schema(description = "작성일", example = "2024-04-06") + private LocalDate postingTime; + + @Schema(description = "조회 수", example = "100") + private int view; + + @Schema(description = "수정 여부", example = "true") + private boolean modified; // 수정 여부 표시 + + @Schema(description = "수정 시각", example = "2024-04-08T10:30:00") + private LocalDateTime modifiedTime; // 수정 시각 + + public ResNoticeDetailDto(Notice entity) { + this.noticeId = entity.getNoticeId(); + this.author = entity.getAuthor(); + this.title = entity.getTitle(); + this.content = entity.getContent(); + this.postingTime = entity.getPostingTime().toLocalDate(); + this.view = entity.getView(); + this.modified = entity.getModified(); + this.modifiedTime = entity.getModifiedTime(); + } +} diff --git a/src/main/java/Remoa/BE/Web/Notice/Dto/Res/ResNoticeDto.java b/src/main/java/Remoa/BE/Web/Notice/Dto/Res/ResNoticeDto.java new file mode 100644 index 0000000..6b94dbb --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Notice/Dto/Res/ResNoticeDto.java @@ -0,0 +1,47 @@ +package Remoa.BE.Web.Notice.Dto.Res; + +import Remoa.BE.Web.Notice.domain.Notice; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Builder +@Data +@AllArgsConstructor +public class ResNoticeDto { + + @Schema(description = "게시물 ID", example = "1") + private Long id; + + @Schema(description = "작성자", example = "John Doe") + private String author; + + @Schema(description = "제목", example = "공지사항 제목") + private String title; + + @Schema(description = "작성 시간", example = "2023-04-01") + private LocalDate postingTime; + + @Schema(description = "조회수", example = "100") + private int view; + + @Schema(description = "수정 여부", example = "true") + private boolean modified; // 수정 여부 표시 + + @Schema(description = "수정 시각", example = "2024-04-08T10:30:00") + private LocalDateTime modifiedTime; // 수정 시각 + + public ResNoticeDto(Notice entity) { + this.id = entity.getNoticeId(); + this.author = entity.getAuthor(); + this.title = entity.getTitle(); + this.postingTime = entity.getPostingTime().toLocalDate(); + this.view = entity.getView(); + this.modified = entity.getModified(); + this.modifiedTime = entity.getModifiedTime(); + } +} diff --git a/src/main/java/Remoa/BE/Web/Notice/Repository/NoticeRepository.java b/src/main/java/Remoa/BE/Web/Notice/Repository/NoticeRepository.java new file mode 100644 index 0000000..6607865 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Notice/Repository/NoticeRepository.java @@ -0,0 +1,19 @@ +package Remoa.BE.Web.Notice.Repository; + +import Remoa.BE.Web.Notice.domain.Notice; +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 org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public interface NoticeRepository extends JpaRepository { + + @Modifying + @Transactional + @Query("UPDATE Notice n SET n.author = :newNick WHERE n.author = :oldNick") + void modifyingNoticeAuthor(@Param("oldNick") String oldNick, @Param("newNick")String newNick); + +} diff --git a/src/main/java/Remoa/BE/Web/Notice/Service/NoticeService.java b/src/main/java/Remoa/BE/Web/Notice/Service/NoticeService.java new file mode 100644 index 0000000..7414458 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Notice/Service/NoticeService.java @@ -0,0 +1,98 @@ +package Remoa.BE.Web.Notice.Service; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Notice.Dto.Req.ReqNoticeDto; +import Remoa.BE.Web.Notice.Dto.Res.NoticeResponseDto; +import Remoa.BE.Web.Notice.Dto.Res.ResNoticeDetailDto; +import Remoa.BE.Web.Notice.Dto.Res.ResNoticeDto; +import Remoa.BE.Web.Notice.Repository.NoticeRepository; +import Remoa.BE.Web.Notice.domain.Notice; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class NoticeService { + + private final NoticeRepository noticeRepository; + + @Transactional + public void registerNotice(ReqNoticeDto reqNoticeDto, String enrollNickname) { + noticeRepository.save(reqNoticeDto.toEntityNotice(enrollNickname)); //builder를 이용해 객체를 직접 생성하지 않고 Notice 저장 + } + + @Transactional + public void updateNotice(Long noticeId, ReqNoticeDto reqNoticeDto, String enrollNickname) { + Notice notice = noticeRepository.findById(noticeId) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + notice.updateNotice(reqNoticeDto, enrollNickname); + } + + @Transactional + public void deleteNotice(Long noticeId, Member member) { + Notice notice = noticeRepository.findById(noticeId) + .orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + + // 공지를 작성한 회원과 현재 로그인한 회원이 같은 경우에만 삭제를 허용합니다. + if (!notice.getAuthor().equals(member.getNickname())) { + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + } + noticeRepository.delete(notice); + } + + public NoticeResponseDto getNotice(int pageNumber) { + + HashMap resultMap = new HashMap<>(); + + int NOTICE_NUMBER = 5; + + Page notices = noticeRepository.findAll(PageRequest.of(pageNumber, NOTICE_NUMBER, Sort.by("postingTime").descending())); + + List noticeDtos = notices.stream().map(ResNoticeDto::new).toList(); + + return NoticeResponseDto.builder() + .notices(noticeDtos) + .totalPages(notices.getTotalPages()) + .totalOfAllNotices(notices.getTotalElements()) + .totalOfPageElements(notices.getNumberOfElements()) + .build(); + } + + @Transactional + public ResNoticeDetailDto getNoticeView(int noticeId, HttpSession session) { + Notice notice = noticeRepository.findById((long) noticeId).orElseThrow(() -> + new BaseException(CustomMessage.NO_ID)); + + handleViewCount(notice, session); + return new ResNoticeDetailDto(notice); + } + + private void handleViewCount(Notice notice, HttpSession session) { + Long noticeId = notice.getNoticeId(); + + String sessionKey = "NoticeViewed" + noticeId; + log.info("sessionKey = {}", sessionKey); + + if (session.getAttribute(sessionKey) == null) { + notice.addViewCount(); + session.setAttribute(sessionKey, true); + } + } + + @Transactional + public void modifying_Notice_NickName(String newNick, String oldNick) { + noticeRepository.modifyingNoticeAuthor(newNick, oldNick); + } +} diff --git a/src/main/java/Remoa/BE/Web/Notice/domain/Notice.java b/src/main/java/Remoa/BE/Web/Notice/domain/Notice.java new file mode 100644 index 0000000..a26e97c --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Notice/domain/Notice.java @@ -0,0 +1,58 @@ +package Remoa.BE.Web.Notice.domain; + +import Remoa.BE.Web.Notice.Dto.Req.ReqNoticeDto; +import lombok.*; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@SQLRestriction("deleted = false") +@SQLDelete(sql = "UPDATE notice SET deleted = true WHERE notice_id = ?") +public class Notice { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long noticeId; + + private String author; + + private String title; + + private String content; + + private LocalDateTime postingTime; + + private int view; + + @Builder.Default + private Boolean modified = Boolean.FALSE; // 수정 여부 표시 + + private LocalDateTime modifiedTime; // 수정 시각 + + @Builder.Default + private Boolean deleted = Boolean.FALSE; + + public void addViewCount() { + this.view++; + } + + public void updateNotice(ReqNoticeDto updateDto, String author) { + this.title = updateDto.getTitle(); + this.content = updateDto.getContent(); + this.author = author; + this.modified = Boolean.TRUE; // 수정됨을 표시 + this.modifiedTime = LocalDateTime.now(); // 현재 시각으로 수정 시각 업데이트 + // 여기에 필요한 필드를 업데이트하는 코드를 추가할 수 있습니다. + } +} diff --git a/src/main/java/Remoa/BE/Web/Post/Controller/PostController.java b/src/main/java/Remoa/BE/Web/Post/Controller/PostController.java new file mode 100644 index 0000000..0c4054a --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Controller/PostController.java @@ -0,0 +1,335 @@ +package Remoa.BE.Web.Post.Controller; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import Remoa.BE.Web.Member.Service.FollowService; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Domain.UploadFile; +import Remoa.BE.Web.Post.Dto.Request.UploadPostForm; +import Remoa.BE.Web.MyPage.Service.MyPostService; +import Remoa.BE.Web.Post.Service.PostService; +import Remoa.BE.Web.Post.Dto.Response.*; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.CommonFunction; +import Remoa.BE.utill.MessageUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.modelmapper.ModelMapper; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +import static Remoa.BE.config.DbInit.categoryList; + +@Tag(name = "레퍼런스 기능 Test Completed", description = "레퍼런스 기능 API") +@Slf4j +@RestController +@RequiredArgsConstructor +public class PostController { + + private final PostService postService; + private final MyPostService myPostService; + private final MemberService memberService; + private final FollowService followService; + private final ModelMapper modelMapper; + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "레퍼런스를 성공적으로 검색했습니다."), + @ApiResponse(responseCode = "400", description = "페이지 번호가 잘못되었습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/reference") + @Operation(summary = "레퍼런스 검색 Test completed", description = "레퍼런스를 검색합니다." + + "
category : \"idea\", \"marketing\", \"design\", \"video\", \"digital\", \"etc\"" + + "
sort : \"views\", \"likes\", \"scrap\", \"newest(default)\" " ) + public ResponseEntity> searchPost(HttpServletRequest request, + @RequestParam(required = false, defaultValue = "all") String category, + @RequestParam(required = false, defaultValue = "newest") String sort, + @RequestParam(required = false, defaultValue = "1", name = "page") int pageNumber, + @RequestParam(required = false, defaultValue = "") String searchQuery, + @AuthenticationPrincipal MemberDetails memberDetails + ) { + log.info("EndPoint Get /reference"); + Member myMember = null; //로그인 한 경우 좋아요, 스크랩 표시하기 위한 분기. -> 토큰 방식으로 바뀌어 수정 필요. + if (memberDetails != null) { + Long myMemberId = memberDetails.getMemberId(); + myMember = memberService.findOne(myMemberId); + } + + Map responseData = new HashMap<>(); + + pageNumber -= 1; + if (pageNumber < 0) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + Page allPosts; + if (categoryList.contains(category)) { + //sort -> 최신순 : newest, 좋아요순 : like, 스크랩순 : scrap, 조회순 : view + allPosts = postService.sortAndPaginatePostsByCategory(category, sort, pageNumber, searchQuery); + } else { + //sort -> 최신순 : newest, 좋아요순 : like, 스크랩순 : scrap, 조회순 : view + allPosts = postService.sortAndPaginatePosts(sort, pageNumber, searchQuery); + } + + if ((allPosts.getContent().isEmpty()) && (allPosts.getTotalElements() > 0)) { + throw new BaseException(CustomMessage.PAGE_NUM_OVER); + // return errorResponse(CustomMessage.PAGE_NUM_OVER); + } + + List result = new ArrayList<>(); + + for (Post post : allPosts) { + ResHomeReferenceDto map = ResHomeReferenceDto.builder() + .postThumbnail(post.getThumbnailUrl()) + .postId(post.getPostId()) + .title(post.getTitle()) + .views(post.getViews()) + .likeCount(post.getLikeCount()) + .isLikedPost(isLikedPost(myMember, post)) + .scrapCount(post.getScrapCount()) + .isScrapedPost(isScrapedPost(myMember, post)) + .postMember(new ResMemberInfoDto(post.getMember().getMemberId(), + post.getMember().getNickname(), + post.getMember().getProfileImage(), + myMember != null ? followService.isMyMemberFollowMember(myMember, post.getMember()) : null)) + .build(); + + result.add(map); + } + + SearchPostResponseDto responseDto = SearchPostResponseDto.builder() + .references(result) //조회한 레퍼런스들 + .totalPages(allPosts.getTotalPages()) //전체 페이지의 수 + .totalOfAllReferences(allPosts.getTotalElements()) //모든 레퍼런스의 수 + .totalOfPageElements(allPosts.getNumberOfElements()) //현 페이지의 레퍼런스 수 + .build(); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, responseDto); + return ResponseEntity.ok(response); + // return successResponse(CustomMessage.OK, responseData); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "레퍼런스 등록 성공"), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping(value = "/reference", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) //게시물 등록 + @Operation(summary = "레퍼런스 등록 Test completed", description = "레퍼런스를 등록합니다.") + public ResponseEntity> share(@RequestPart("data") UploadPostForm uploadPostForm, + @RequestPart("thumbnail") MultipartFile thumbnail, + @RequestPart(value = "file", required = false) List uploadFiles, + @AuthenticationPrincipal MemberDetails memberDetails) throws IOException { + log.info("EndPoint Post /reference"); + + Long memberId = memberDetails.getMemberId(); + Post savePost = postService.registerPost(uploadPostForm, thumbnail, uploadFiles, memberId); + Post post = postService.findOne(savePost.getPostId()); + + ResReferenceRegisterDto resReferenceRegisterDto = ResReferenceRegisterDto.builder() + .postId(post.getPostId()) + .title(post.getTitle()) + .category(post.getCategory().getName()) + .contestAwardType(post.getContestAwardType()) + .contestName(post.getContestName()) + .youtubeLink(post.getYoutubeLink()) + .pageCount(post.getPageCount()) + .build(); + if (post.getUploadFiles() != null) { + resReferenceRegisterDto.setFileNames(post.getUploadFiles().stream() + .map(UploadFile::getOriginalFileName) + .collect(Collectors.toList())); + } + BaseResponse response = new BaseResponse<>(CustomMessage.OK, resReferenceRegisterDto); + return ResponseEntity.ok(response); + //return successResponse(CustomMessage.OK, resReferenceRegisterDto); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "레퍼런스 수정 성공"), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PutMapping(value = "/reference/{reference_id}" , consumes = MediaType.MULTIPART_FORM_DATA_VALUE) // 게시물 수정 + @Operation(summary = "레퍼런스 수정 Test completed", description = "레퍼런스를 수정합니다.") + public ResponseEntity> modify(@PathVariable("reference_id") Long referenceId, + @RequestPart("data") UploadPostForm uploadPostForm, + @RequestPart("thumbnail") MultipartFile thumbnail, + @RequestPart(value = "file", required = false) List uploadFiles, + @AuthenticationPrincipal MemberDetails memberDetails) throws IOException { + log.info("EndPoint Put /reference/{reference_id}"); + + Long memberId = memberDetails.getMemberId(); + Post post = postService.findOne(referenceId); + if (!Objects.equals(memberId, post.getMember().getMemberId())) { + throw new BaseException(CustomMessage.CAN_NOT_ACCESS); + //return errorResponse(CustomMessage.CAN_NOT_ACCESS); + } + Post modifiedPost = postService.modifyPost(uploadPostForm, thumbnail, uploadFiles, post); + + + ResReferenceRegisterDto resReferenceRegisterDto = ResReferenceRegisterDto.builder() + .postId(modifiedPost.getPostId()) + .title(modifiedPost.getTitle()) + .category(modifiedPost.getCategory().getName()) + .contestAwardType(modifiedPost.getContestAwardType()) + .contestName(modifiedPost.getContestName()) + .youtubeLink(modifiedPost.getYoutubeLink()) + .pageCount(modifiedPost.getPageCount()) + .build(); + if (modifiedPost.getUploadFiles() != null) { + resReferenceRegisterDto.setFileNames(modifiedPost.getUploadFiles().stream() + .map(UploadFile::getOriginalFileName) + .collect(Collectors.toList())); + } + BaseResponse response = new BaseResponse<>(CustomMessage.OK, resReferenceRegisterDto); + return ResponseEntity.ok(response); + //return successResponse(CustomMessage.OK, resReferenceRegisterDto); + + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "레퍼런스 좋아요 성공"), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/reference/{reference_id}/like") + @Operation(summary = "레퍼런스 좋아요 Test completed", description = "레퍼런스에 좋아요를 합니다.") + public ResponseEntity> likeReference(@PathVariable("reference_id") Long referenceId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Post /reference/{reference_id}/like"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + Member postedMember = postService.getPostedMember(referenceId); + if (myMember.equals(postedMember)) { + throw new BaseException(CustomMessage.SELF_LIKE); + //return errorResponse(CustomMessage.SELF_LIKE); + } + Post post = postService.likePost(memberId, myMember, referenceId); + LikePostResponseDto responseDto = LikePostResponseDto.builder() + .likeCount(post.getLikeCount()) + .build(); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, responseDto); + return ResponseEntity.ok(response); + //return successResponse(CustomMessage.OK, map); + + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "레퍼런스 스크랩 성공"), + @ApiResponse(responseCode = "201", description = "레퍼런스 스크랩 해제"), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping("/reference/{reference_id}/scrap") + @Operation(summary = "레퍼런스 스크랩 Test completed", description = "레퍼런스를 스크랩합니다.") + public ResponseEntity> scrapReference(@PathVariable("reference_id") Long referenceId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Post /reference/{reference_id}/scrap"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + Member postedMember = postService.getPostedMember(referenceId); + if (myMember.equals(postedMember)) { + throw new BaseException(CustomMessage.SELF_SCRAP); + //return errorResponse(CustomMessage.SELF_SCRAP); + } + boolean isScrapAction = postService.scrapPost(memberId, myMember, referenceId); + Post post = postService.findOne(referenceId); + ScrapReferenceResponseDto responseDto = ScrapReferenceResponseDto.builder() + .scrapCount(post.getScrapCount()) + .build(); + // 스크랩의 경우 : 200 OK, 스크랩 해제의 경우 : 201 CREATED + CustomMessage customMessage = isScrapAction ? CustomMessage.OK_SCRAP : CustomMessage.OK_UNSCRAP; + BaseResponse response = new BaseResponse<>(customMessage, responseDto); + return ResponseEntity.ok(response); + //return successResponse(customMessage, map); + } + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "레퍼런스 삭제 성공"), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/user/reference/{reference_id}") + @Operation(summary = "레퍼런스 삭제 Test completed", description = "레퍼런스를 삭제합니다.") + public ResponseEntity deleteReference(@PathVariable("reference_id") Long[] postId, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Delete /user/reference/{reference_id}"); + + Long memberId = memberDetails.getMemberId(); + Member myMember = memberService.findOne(memberId); + + // 현재 로그인한 사용자가 올린 게시글이 맞는지 확인/예외처리 + for (int i = 0; i < postId.length; i++) { + if (postService.checkMemberPost(myMember, postId[i])) { + postService.deleteReference(postId[i]); + } + } + return new ResponseEntity<>(HttpStatus.OK); + } + + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "레퍼런스 삭제 성공"), + @ApiResponse(responseCode = "400", description = MessageUtils.BAD_REQUEST, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = MessageUtils.UNAUTHORIZED, + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @DeleteMapping("/user/referenceCategory/{category}") + @Operation(summary = "레퍼런스 카테고리 삭제", description = "레퍼런스 카테고리를 삭제합니다.") + public ResponseEntity deleteReferenceCategory(@PathVariable("category") String category, + @AuthenticationPrincipal MemberDetails memberDetails) { + log.info("EndPoint Delete /user/referenceCategory/{category}"); + + Long categoryId = CommonFunction.getCategoryId(category); // 카테고리 id 추출. + Long memberId = memberDetails.getMemberId(); + myPostService.deleteReferenceCategory(memberId, categoryId); + + return new ResponseEntity<>(HttpStatus.OK); + } + + private Boolean isLikedPost(Member myMember, Post post) { + return myMember != null && postService.isThisPostLiked(myMember, post); + } + + private Boolean isScrapedPost(Member myMember, Post post) { + return myMember != null && postService.isThisPostScraped(myMember, post); + } + + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Post/Controller/ViewerController.java b/src/main/java/Remoa/BE/Web/Post/Controller/ViewerController.java new file mode 100644 index 0000000..2e74e8f --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Controller/ViewerController.java @@ -0,0 +1,78 @@ +package Remoa.BE.Web.Post.Controller; + +import Remoa.BE.Web.Comment.Dto.Res.ResCommentDto; +import Remoa.BE.Web.Feedback.Dto.ResFeedbackDto2; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.MemberUtils; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Dto.Response.ResReferenceViewerDto; +import Remoa.BE.Web.Post.Service.PostService; +import Remoa.BE.config.auth.MemberDetails; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Tag(name = "레퍼런스 상세 기능 Test Completed", description = "레퍼런스 상세 기능 API") +@Slf4j +@RestController +@RequiredArgsConstructor +public class ViewerController { + + private final MemberUtils memberUtils; + private final PostService postService; + private final MemberService memberService; + + + @GetMapping("/reference/{reference_id}") + @Operation(summary = "레퍼런스 조회 Test Completed", description = "특정 레퍼런스의 상세 정보를 조회합니다.") + public ResponseEntity> referenceViewer(@PathVariable("reference_id") Long referenceId, + @AuthenticationPrincipal MemberDetails memberDetails, + HttpSession session) { + + log.info("EndPoint Get /reference/{reference_id}"); + + Long myMemberId; + + Member myMember = null; + if (memberDetails != null) { + myMemberId = memberDetails.getMemberId(); + myMember = memberService.findOne(myMemberId); + } + + // query parameter로 넘어온 id값의 post 조회 + Post post = postService.findOneViewPlus(referenceId, session); + + // 조회한 post의 comment 조회 및 각 comment에 대한 commentReply 조회 -> 이후 ResCommentDto로 매핑 + List comments = memberUtils.commentList(post.getPostId(), myMember); + + // 조회한 post의 feedback 조회 및 각 feedback에 대한 feedbackReply 조회 -> 이후 ResFeedbackDto로 매핑 + List feedbacks = memberUtils.feedbackList(post.getPostId(), myMember); + + // 위에 생성한 CommentDto, FeedbackDto를 이용해 ReferenceViewerDto 매핑. + ResReferenceViewerDto resReferenceViewerDto = new ResReferenceViewerDto(post, + post.getMember(), + memberUtils.isMyMemberFollowMember(myMember, post.getMember()), + memberUtils.isLikedPost(myMember, post), + memberUtils.isScrapedPost(myMember, post), + comments, + feedbacks + ); + + BaseResponse response = new BaseResponse<>(CustomMessage.OK, resReferenceViewerDto); + return ResponseEntity.ok(response); + } + + +} diff --git a/src/main/java/Remoa/BE/Post/Domain/Category.java b/src/main/java/Remoa/BE/Web/Post/Domain/Category.java similarity index 89% rename from src/main/java/Remoa/BE/Post/Domain/Category.java rename to src/main/java/Remoa/BE/Web/Post/Domain/Category.java index 7cf9aeb..7a4bceb 100644 --- a/src/main/java/Remoa/BE/Post/Domain/Category.java +++ b/src/main/java/Remoa/BE/Web/Post/Domain/Category.java @@ -1,9 +1,9 @@ -package Remoa.BE.Post.Domain; +package Remoa.BE.Web.Post.Domain; import lombok.Getter; import lombok.NoArgsConstructor; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/Remoa/BE/Web/Post/Domain/Post.java b/src/main/java/Remoa/BE/Web/Post/Domain/Post.java new file mode 100644 index 0000000..e67d5a9 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Domain/Post.java @@ -0,0 +1,152 @@ +package Remoa.BE.Web.Post.Domain; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.Feedback.Domain.Feedback; +import Remoa.BE.Web.Member.Domain.Member; +import jakarta.persistence.CascadeType; +import lombok.*; +import org.hibernate.annotations.*; + +import jakarta.persistence.*; +import org.hibernate.annotations.processing.SQL; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.FetchType.LAZY; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@Entity +@SQLRestriction("deleted = false") +@SQLDelete(sql = "UPDATE post SET deleted = true WHERE post_id = ?") // 사용자 정의 SQL DELETE 문 설정 sofe delete로 구현 +public class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long postId; + + /** + * 해당 Post를 쓴 작성자(Member) + */ + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id") + private Member member; + + /** + * Post 제목 + */ + private String title; + + + private String thumbnailUrl; + + /** + * 참여 공모전의 이름 + */ + @Column(name = "contest_name") + private String contestName; + + /** + * 유튜브 링크 + */ + @Builder.Default + private String youtubeLink = ""; + + /** + * 참여한 공모전의 마감 기한 + */ + private String deadline; + + + /** + * pm쪽에 문의해야할듯. + */ + @Column(name = "contest_aware_type") + private String contestAwardType; + + /** + * Post에 대한 좋아요 수 + */ + @Builder.Default + @Column(name = "like_count") + private Integer likeCount = 0; + + /** + * Post가 작성된 시간 + */ + @Column(name = "posting_time") + private LocalDateTime postingTime; + + /** + * Post의 조회수 + */ + @Builder.Default + private Integer views = 0; + + @Builder.Default + private Integer scrapCount = 0; + + /* @Builder.Default + private Integer commentCount = 0; + + @Builder.Default + private Integer feedbackCount = 0;*/ + + @Builder.Default + private Integer pageCount = 1; + + /** + * Post에 작성되어진 Comment + */ + @Builder.Default + @OneToMany(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.REMOVE, fetch = LAZY) + //@OnDelete(action = OnDeleteAction.CASCADE) + private List comments = new ArrayList<>(); + + @Builder.Default + @OneToMany(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.REMOVE, fetch = LAZY) + //@OnDelete(action = OnDeleteAction.CASCADE) + private List feedbacks = new ArrayList<>(); + + @Builder.Default + @OneToMany(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.REMOVE, fetch = LAZY) + //@OnDelete(action = OnDeleteAction.CASCADE) + private List commentFeedbacks = new ArrayList<>(); + + + @Builder.Default + @OneToMany(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.REMOVE, fetch = LAZY) + //@OnDelete(action = OnDeleteAction.CASCADE) + private List postScraps = new ArrayList<>(); + + @Builder.Default + @OneToMany(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.REMOVE, fetch = LAZY) + //@OnDelete(action = OnDeleteAction.CASCADE) + private List postLikes = new ArrayList<>(); + /** + * Post에서 쓰인 files + */ + @OneToMany(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.REMOVE, fetch = LAZY) + private List uploadFiles; + + /** + * 작성한 Post의 카테고리 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id") + private Category category; + + @Builder.Default + private Boolean deleted = Boolean.FALSE; + + + public void addViewCount() { + this.views++; + } +} diff --git a/src/main/java/Remoa/BE/Post/Domain/PostLike.java b/src/main/java/Remoa/BE/Web/Post/Domain/PostLike.java similarity index 75% rename from src/main/java/Remoa/BE/Post/Domain/PostLike.java rename to src/main/java/Remoa/BE/Web/Post/Domain/PostLike.java index d2d112e..5b56a49 100644 --- a/src/main/java/Remoa/BE/Post/Domain/PostLike.java +++ b/src/main/java/Remoa/BE/Web/Post/Domain/PostLike.java @@ -1,20 +1,22 @@ -package Remoa.BE.Post.Domain; +package Remoa.BE.Web.Post.Domain; -import Remoa.BE.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.Member; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.Where; -import javax.persistence.*; +import jakarta.persistence.*; @Getter @Setter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Where(clause = "deleted = false") +@SQLDelete(sql = "UPDATE post_like SET deleted = true WHERE post_like_id = ?") +@SQLRestriction("deleted = false") public class PostLike { @Id diff --git a/src/main/java/Remoa/BE/Post/Domain/PostScarp.java b/src/main/java/Remoa/BE/Web/Post/Domain/PostScrap.java similarity index 56% rename from src/main/java/Remoa/BE/Post/Domain/PostScarp.java rename to src/main/java/Remoa/BE/Web/Post/Domain/PostScrap.java index 49fc0ac..0b978b1 100644 --- a/src/main/java/Remoa/BE/Post/Domain/PostScarp.java +++ b/src/main/java/Remoa/BE/Web/Post/Domain/PostScrap.java @@ -1,21 +1,24 @@ -package Remoa.BE.Post.Domain; +package Remoa.BE.Web.Post.Domain; -import Remoa.BE.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.Member; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.Where; -import javax.persistence.*; +import jakarta.persistence.*; +import java.time.LocalDateTime; @Getter @Setter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Where(clause = "deleted = false") -public class PostScarp { +@SQLDelete(sql = "UPDATE post_scrap SET deleted = true WHERE post_scrap_id = ?") +@SQLRestriction("deleted = false") +public class PostScrap { @Id @GeneratedValue @@ -30,11 +33,15 @@ public class PostScarp { @JoinColumn(name = "post_id") private Post post; + @Column(name = "scrap_time") + private LocalDateTime scrapTime; + private Boolean deleted = Boolean.FALSE; - public static PostScarp createPostScrap(Member member, Post post) { - PostScarp postScrap = new PostScarp(); + public static PostScrap createPostScrap(Member member, Post post) { + PostScrap postScrap = new PostScrap(); postScrap.setPost(post); + postScrap.setScrapTime(LocalDateTime.now()); postScrap.setMember(member); return postScrap; diff --git a/src/main/java/Remoa/BE/Post/Domain/UploadFile.java b/src/main/java/Remoa/BE/Web/Post/Domain/UploadFile.java similarity index 80% rename from src/main/java/Remoa/BE/Post/Domain/UploadFile.java rename to src/main/java/Remoa/BE/Web/Post/Domain/UploadFile.java index eeb155f..d544b50 100644 --- a/src/main/java/Remoa/BE/Post/Domain/UploadFile.java +++ b/src/main/java/Remoa/BE/Web/Post/Domain/UploadFile.java @@ -1,27 +1,23 @@ -package Remoa.BE.Post.Domain; +package Remoa.BE.Web.Post.Domain; import lombok.Getter; import lombok.Setter; -import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.Where; -import javax.persistence.*; +import jakarta.persistence.*; @Entity @Getter @Setter @Table(name = "FILE") -@Where(clause = "deleted = false") +@SQLRestriction("deleted = false") public class UploadFile { @Id @GeneratedValue - @Column(name = "file_id") private Long uploadFileId; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; /** * 업로드된 파일의 원본 이름 @@ -32,7 +28,7 @@ public class UploadFile { /** * UUID.randomUUID()를 통해 받은 랜덤값을 통해 S3파일 서버에 저장할 파일의 이름 */ - @Column(name = "save_file_name") + @Column(name = "save_file_name", length = 500) private String saveFileName; /** @@ -45,9 +41,13 @@ public class UploadFile { * Lob 는 긴 문자열을 처리해 줍니다. */ @Lob - @Column(name = "store_file_url") + @Column(name = "store_file_url", length = 500) private String storeFileUrl; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + private Boolean deleted = Boolean.FALSE; //이후 업로드 날짜 및 시간, 컨텐츠 타입, 사이즈 등의 필드등이 필요할 때 손봐야할듯. diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Request/ReqFeedbackDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Request/ReqFeedbackDto.java new file mode 100644 index 0000000..4893d7d --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Request/ReqFeedbackDto.java @@ -0,0 +1,14 @@ +package Remoa.BE.Web.Post.Dto.Request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class ReqFeedbackDto { + private String feedback; +} diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Request/ReqFeedbackReplyDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Request/ReqFeedbackReplyDto.java new file mode 100644 index 0000000..e4d4bc8 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Request/ReqFeedbackReplyDto.java @@ -0,0 +1,14 @@ +package Remoa.BE.Web.Post.Dto.Request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class ReqFeedbackReplyDto { + private String feedbackReply; +} diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Request/UploadPostForm.java b/src/main/java/Remoa/BE/Web/Post/Dto/Request/UploadPostForm.java new file mode 100644 index 0000000..e3b421e --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Request/UploadPostForm.java @@ -0,0 +1,29 @@ +package Remoa.BE.Web.Post.Dto.Request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +@Schema(description = "게시물 등록 양식") +public class UploadPostForm { + + @Schema(description = "게시물 제목", example = "서울 빅데이터 공모전 최우수상") + private String title; // 게시물 제목 + + @Schema(description = "공모전명", example = "서울 빅데이터 공모전") + private String contestName; // 공모전명 + + @Schema(description = "수상 유형", example = "최우수상") + private String contestAwardType; // 수상 유형 + + @Schema(description = "카테고리", example = "etc") + private String category; // 카테고리 이름 + + @Schema(description = "YouTube 링크", example = "https://www.youtube.com/watch?v=video_id") + private String youtubeLink; // YouTube 링크 + +} diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Response/LikePostResponseDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Response/LikePostResponseDto.java new file mode 100644 index 0000000..a789ddc --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Response/LikePostResponseDto.java @@ -0,0 +1,15 @@ +package Remoa.BE.Web.Post.Dto.Response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@Getter +@Setter +public class LikePostResponseDto { + + @Schema(description = "좋아요 수", example = "10") + private Integer likeCount; +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Response/PostPageResponseDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Response/PostPageResponseDto.java new file mode 100644 index 0000000..0f54af8 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Response/PostPageResponseDto.java @@ -0,0 +1,29 @@ +package Remoa.BE.Web.Post.Dto.Response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PostPageResponseDto { + + @Schema(description = "게시물 레퍼런스 리스트") + private List references; + + @Schema(description = "전체 페이지 수", example = "5") + private int totalPages; + + @Schema(description = "전체 레퍼런스 수", example = "50") + private long totalOfAllReferences; + + @Schema(description = "현재 페이지의 레퍼런스 수", example = "10") + private int totalOfPageElements; +} diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResCommentFeedbackDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResCommentFeedbackDto.java new file mode 100644 index 0000000..43fdf15 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResCommentFeedbackDto.java @@ -0,0 +1,42 @@ +package Remoa.BE.Web.Post.Dto.Response; + +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * 내 활동 관리에 쓰이는 Comment와 Feedback을 구분 없이 최신순으로 볼러오는 데 쓰이는 dto. + */ +@Data +@Builder +public class ResCommentFeedbackDto { + + @Schema(description = "게시물 제목", example = "서울 빅데이터 공모전 최우수상") + private String title; + + @Schema(description = "게시물 ID", example = "2") + private Long postId; + + @Schema(description = "코멘트 ID") + private Long commentId; + + @Schema(description = "피드백 ID") + private Long feedbackId; + + @Schema(description = "썸네일 URL", example = "https://remoa.s3.ap-northeast-2.amazonaws.com/thumbnail/3d6655e8-d922-4da4-aeac-5edc2a5cc0ca_basic_profile.png") + private String thumbnail; + + @Schema(description = "게시물 작성자 정보") + private ResMemberInfoDto member; + + @Schema(description = "코멘트/피드백 내용", example = "잘했어요") + private String content; + + @Schema(description = "좋아요 수", example = "0") + private Integer likeCount; + + @Schema(description = "코멘트인지 여부", example = "false") + private Boolean isComment; + +} diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResHomeReferenceDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResHomeReferenceDto.java new file mode 100644 index 0000000..c9efa2c --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResHomeReferenceDto.java @@ -0,0 +1,20 @@ +package Remoa.BE.Web.Post.Dto.Response; + +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ResHomeReferenceDto { + + private String postThumbnail; + private Long postId; + private String title; + private int views; + private int likeCount; + private Boolean isLikedPost; + private int scrapCount; + private Boolean isScrapedPost; + private ResMemberInfoDto postMember; +} diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResPostDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResPostDto.java new file mode 100644 index 0000000..a3fcbad --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResPostDto.java @@ -0,0 +1,49 @@ +package Remoa.BE.Web.Post.Dto.Response; + +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +/** + * 작업물 목록을 보여줄 때 쓰일 Post의 간단한 정보만을 담은 Dto. + */ +@Setter +@Getter +@Builder +public class ResPostDto { + + + @Schema(description = "게시물 ID", example = "12345") + public Long postId; + + @Schema(description = "게시물 작성자 정보") + public ResMemberInfoDto postMember; + + @Schema(description = "썸네일 이미지 URL", example = "https://example.com/thumbnail.jpg") + public String thumbnail; + + @Schema(description = "게시물 제목", example = "이것이 게시물 제목입니다.") + public String title; + + @Schema(description = "좋아요 수", example = "100") + public Integer likeCount; + + @Schema(description = "현재 사용자가 해당 게시물을 좋아하는지 여부", example = "true") + public Boolean isLikedPost; + + @Schema(description = "게시물 작성 시간", example = "2024-04-05T08:30:00Z") + public String postingTime; + + @Schema(description = "조회수", example = "500") + public Integer views; + + @Schema(description = "스크랩 수", example = "50") + public Integer scrapCount; + + @Schema(description = "현재 사용자가 해당 게시물을 스크랩했는지 여부", example = "false") + public Boolean isScrapedPost; + + @Schema(description = "게시물 카테고리 이름", example = "기술") + public String categoryName; + +} diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResReferenceRegisterDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResReferenceRegisterDto.java new file mode 100644 index 0000000..1084b1c --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResReferenceRegisterDto.java @@ -0,0 +1,38 @@ +package Remoa.BE.Web.Post.Dto.Response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Builder +@Getter +@Setter +public class ResReferenceRegisterDto { + + @Schema(description = "게시물 ID", example = "1") + private Long postId; + + @Schema(description = "게시물 제목", example = "서울 빅데이터 공모전 수상작") + private String title; + + @Schema(description = "공모전명", example = "서울 빅데이터 공모전") + private String contestName; + + @Schema(description = "카테고리", example = "빅데이터") + private String category; + + @Schema(description = "수상 종류", example = "우수상") + private String contestAwardType; + + @Schema(description = "유튜브 링크", example = "https://www.youtube.com/watch?v=video_id") + private String youtubeLink; + + @Schema(description = "총 페이지 수", example = "10") + private Integer pageCount; + + @Schema(description = "파일명 목록", example = "[\"file1.pdf\", \"file2.pdf\"]") + private List fileNames; +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResReferenceViewerDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResReferenceViewerDto.java new file mode 100644 index 0000000..1d430fb --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResReferenceViewerDto.java @@ -0,0 +1,94 @@ +package Remoa.BE.Web.Post.Dto.Response; + +import Remoa.BE.Web.Comment.Dto.Res.ResCommentDto; +import Remoa.BE.Web.Feedback.Dto.ResFeedbackDto2; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Domain.UploadFile; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.stream.Collectors; + +@Schema(description = "레퍼런스 뷰어 응답 DTO") +@Data +public class ResReferenceViewerDto { + + @Schema(description = "게시물 ID", example = "1") + private Long postId; + + @Schema(description = "게시물 작성자 정보") + private ResMemberInfoDto postMember; + + @Schema(description = "게시물 썸네일 이미지 URL") + private String thumbnail; + + @Schema(description = "공모전명", example = "서울 빅데이터 공모전") + private String contestName; + + @Schema(description = "수상 유형", example = "최우수상") + private String contestAwardType; + + @Schema(description = "카테고리", example = "etc") + private String category; + + @Schema(description = "게시물 제목", example = "서울 빅데이터 공모전 최우수상") + private String title; + + @Schema(description = "게시물 좋아요 수", example = "1") + private Integer likeCount; + + @Schema(description = "내가 게시물을 좋아요 했는지 여부", example = "true") + private Boolean isLiked; + + @Schema(description = "게시물 스크랩 수", example = "0") + private Integer scrapCount; + + @Schema(description = "내가 게시물을 스크랩했는지 여부", example = "false") + private Boolean isScraped; + + @Schema(description = "게시물 작성 시간", example = "2023-03-28T10:45:26") + private String postingTime; + + @Schema(description = "게시물 조회 수", example = "4") + private Integer views; + + @Schema(description = "페이지 수", example = "1") + private Integer pageCount; + + @Schema(description = "YouTube 링크") + private String youtubeLink; + + @Schema(description = "첨부 파일명 목록") + private List fileNames; + + @Schema(description = "게시물 댓글 목록") + private List comments; + + @Schema(description = "게시물 피드백 목록") + private List feedbacks; + + public ResReferenceViewerDto(Post post, Member postMember, Boolean isFollow, Boolean isLiked, Boolean isScraped, List comments, List feedbacks) { + this.postId = post.getPostId(); + this.postMember = new ResMemberInfoDto(postMember, isFollow); + this.thumbnail = post.getThumbnailUrl(); + this.contestName = post.getContestName(); + this.contestAwardType = post.getContestAwardType(); + this.category = post.getCategory().getName(); + this.title = post.getTitle(); + this.likeCount = post.getLikeCount(); + this.isLiked = isLiked; + this.scrapCount = post.getScrapCount(); + this.isScraped = isScraped; + this.postingTime = post.getPostingTime().toString(); + this.views = post.getViews(); + this.pageCount = post.getPageCount(); + this.youtubeLink = post.getYoutubeLink(); + this.fileNames = post.getUploadFiles() != null ? + post.getUploadFiles().stream().map(UploadFile::getStoreFileUrl).collect(Collectors.toList()) : null; + this.comments = comments; + this.feedbacks = feedbacks; + } +} diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResReplyDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResReplyDto.java new file mode 100644 index 0000000..1edde83 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ResReplyDto.java @@ -0,0 +1,36 @@ +package Remoa.BE.Web.Post.Dto.Response; + +import Remoa.BE.Web.Member.Dto.Res.ResMemberInfoDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * comment, feedback의 대댓글에 모두 사용할 수 있음. + */ +@Data +@AllArgsConstructor +@Builder +public class ResReplyDto { + + @Schema(description = "대댓글 ID", example = "3") + private Long replyId; + + @Schema(description = "작성자 정보") + private ResMemberInfoDto member; + + @Schema(description = "대댓글 내용", example = "대박") + private String content; + + @Schema(description = "좋아요 수", example = "0") + private Integer likeCount; + + @Schema(description = "좋아요 여부") + private Boolean isLiked; + + @Schema(description = "대댓글 작성 시간", example = "2023-03-27T23:18:38") + private LocalDateTime repliedTime; +} diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Response/ScrapReferenceResponseDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ScrapReferenceResponseDto.java new file mode 100644 index 0000000..20146b9 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Response/ScrapReferenceResponseDto.java @@ -0,0 +1,15 @@ +package Remoa.BE.Web.Post.Dto.Response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@Getter +@Setter +public class ScrapReferenceResponseDto { + + @Schema(description = "스크랩 수", example = "10") + private Integer scrapCount; +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Post/Dto/Response/SearchPostResponseDto.java b/src/main/java/Remoa/BE/Web/Post/Dto/Response/SearchPostResponseDto.java new file mode 100644 index 0000000..97d51fc --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Dto/Response/SearchPostResponseDto.java @@ -0,0 +1,30 @@ +package Remoa.BE.Web.Post.Dto.Response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SearchPostResponseDto { + + @Schema(description = "검색된 레퍼런스 목록") + private List references; + + @Schema(description = "전체 페이지의 수", example = "5") + private int totalPages; + + @Schema(description = "모든 레퍼런스의 수", example = "50") + private long totalOfAllReferences; + + @Schema(description = "현재 페이지의 레퍼런스 수", example = "10") + private int totalOfPageElements; + + // 생성자, 게터, 세터 등은 생략하였습니다. +} diff --git a/src/main/java/Remoa/BE/Post/Repository/CategoryRepository.java b/src/main/java/Remoa/BE/Web/Post/Repository/CategoryRepository.java similarity index 80% rename from src/main/java/Remoa/BE/Post/Repository/CategoryRepository.java rename to src/main/java/Remoa/BE/Web/Post/Repository/CategoryRepository.java index 5b21ed6..ebe9ce2 100644 --- a/src/main/java/Remoa/BE/Post/Repository/CategoryRepository.java +++ b/src/main/java/Remoa/BE/Web/Post/Repository/CategoryRepository.java @@ -1,12 +1,14 @@ -package Remoa.BE.Post.Repository; +package Remoa.BE.Web.Post.Repository; -import Remoa.BE.Post.Domain.Category; -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Member.Domain.MemberCategory; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.MemberCategory; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import javax.persistence.EntityManager; +import jakarta.persistence.EntityManager; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -36,7 +38,7 @@ public Category findByCategoryName(String categoryName) { if (category.isPresent()) { return category.get(); } else { - throw new RuntimeException("해당 카테고리는 존재하지 않습니다."); + throw new BaseException(CustomMessage.NO_CATEGORY); } } diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/MyReferenceRepository.java b/src/main/java/Remoa/BE/Web/Post/Repository/MyReferenceRepository.java new file mode 100644 index 0000000..ed3a156 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/MyReferenceRepository.java @@ -0,0 +1,17 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Post.Domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + + +@Repository +public interface MyReferenceRepository extends JpaRepository { + + List findByMemberMemberIdAndCategoryCategoryId(Long memberId ,Long categoryId); + + @Override + void deleteAll(Iterable entities); +} diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/PostCustomRepository.java b/src/main/java/Remoa/BE/Web/Post/Repository/PostCustomRepository.java new file mode 100644 index 0000000..f854d82 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/PostCustomRepository.java @@ -0,0 +1,16 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface PostCustomRepository { + + List findByMemberRecentTwelve(Member member); + + +} diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/PostCustomRepositoryImpl.java b/src/main/java/Remoa/BE/Web/Post/Repository/PostCustomRepositoryImpl.java new file mode 100644 index 0000000..86c39e3 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/PostCustomRepositoryImpl.java @@ -0,0 +1,39 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Member.Domain.Member; + +import Remoa.BE.Web.Member.Domain.QMember; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Domain.PostScrap; +import Remoa.BE.Web.Post.Domain.QPost; +import Remoa.BE.Web.Post.Domain.QPostScrap; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.stream.Collectors; + +@Repository +@RequiredArgsConstructor +public class PostCustomRepositoryImpl implements PostCustomRepository { + + private final JPAQueryFactory jpaQueryFactory; + QMember member = QMember.member; + QPost post = QPost.post; + QPostScrap postScrap = QPostScrap.postScrap; + + @Override + public List findByMemberRecentTwelve(Member member) { + return jpaQueryFactory.select(postScrap) + .from(postScrap) + .join(postScrap.member, this.member) + .where(this.member.eq(member)) + .orderBy(postScrap.scrapTime.desc()) + .limit(12L) + .fetch() + .stream().map(PostScrap::getPost) + .collect(Collectors.toList()); + + } +} diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/PostLikeRepository.java b/src/main/java/Remoa/BE/Web/Post/Repository/PostLikeRepository.java new file mode 100644 index 0000000..ec9a8f0 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/PostLikeRepository.java @@ -0,0 +1,10 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Post.Domain.PostLike; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PostLikeRepository extends JpaRepository { + PostLike findByMemberMemberIdAndPostPostId(Long memberId, Long postId); +} diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/PostPagingRepository.java b/src/main/java/Remoa/BE/Web/Post/Repository/PostPagingRepository.java new file mode 100644 index 0000000..a9b4408 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/PostPagingRepository.java @@ -0,0 +1,46 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * JPA Query Creation을 사용해서 post data sorting & slicing + */ +@Repository +public interface PostPagingRepository extends PagingAndSortingRepository, PostCustomRepository { + + @Query("SELECT p FROM Post p WHERE (p.member.nickname LIKE %:searchQuery% OR p.title LIKE %:searchQuery%) AND p.category = :category") + Page findByMemberNameOrTitleContainingAndCategory(Pageable pageable, String searchQuery, Category category); + + Page findByMemberAndTitleContainingOrderByPostingTimeDesc(Pageable pageable, Member member, String title); + + Page findByMemberAndTitleContainingOrderByViewsDesc(Pageable pageable, Member member, String title); + + Page findByMemberAndTitleContainingOrderByLikeCountDesc(Pageable pageable, Member member, String title); + + Page findByMemberAndTitleContainingOrderByScrapCountDesc(Pageable pageable, Member member, String title); + + Page findByMemberAndCategoryAndTitleContaining(Pageable pageable, Member member, Category category, String title); + + //"IsNotEmpty" 부분에 "Cannot resolve property 'isNotEmpty'"경고가 나오는 건 JPA의 isNotEmpty 예약어를 intellij가 인식하지 못하고 자바의 프로퍼티로 인식하기 때문. 즉, 무시해도 됨. + // Page findByMemberAndCategoryAndCommentsIsNotEmpty(Pageable pageable, Member member, Category category); + + //"IsNotEmpty" 부분에 "Cannot resolve property 'isNotEmpty'"경고가 나오는 건 JPA의 isNotEmpty 예약어를 intellij가 인식하지 못하고 자바의 프로퍼티로 인식하기 때문. 즉, 무시해도 됨. + // Page findByMemberAndCommentsIsNotEmpty(Pageable pageable, Member member); + + Page findByTitleContaining(Pageable pageable, String title); + + @Query("SELECT p FROM Post p WHERE p.member.nickname LIKE %:searchQuery% OR p.title LIKE %:searchQuery%") + Page findByMemberNameOrTitleContaining(Pageable pageable, String searchQuery); + + List findByMemberRecentTwelve(Member member); + +} diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/PostRepository.java b/src/main/java/Remoa/BE/Web/Post/Repository/PostRepository.java new file mode 100644 index 0000000..00dfd10 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/PostRepository.java @@ -0,0 +1,26 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Post; +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; +import java.util.Optional; + +public interface PostRepository extends JpaRepository, PostRepositoryCustom { + + List findByMember(Member member); + + @Query(value = "select * from post p where member_id = :memberId", nativeQuery = true) + List findByMemberHard(@Param("memberId")Long memberId); + + @Modifying + @Query(value = "delete from post p where member_id = :memberId", nativeQuery = true) + void deletePostByMemberHard(@Param("memberId") Long memberId); + + + +} diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/PostRepositoryCustom.java b/src/main/java/Remoa/BE/Web/Post/Repository/PostRepositoryCustom.java new file mode 100644 index 0000000..dadc45c --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/PostRepositoryCustom.java @@ -0,0 +1,42 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Domain.PostLike; +import Remoa.BE.Web.Post.Domain.PostScrap; +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; +import java.util.Optional; + +public interface PostRepositoryCustom { + void savePost(Post post); + + void modifyPost(Post post); + + List findAll(); + + Optional findOne(Long postId); + + List findByMember(Member member); + + List findByTitleContaining(String name); + + void savePostScrap(PostScrap postScrap); + + Optional findScrapedPost(Member member, Post post); + + Optional findLikedPost(Member myMember, Post post); + + List findPostsByCategory(Category category); + + void saveComment(Comment comment); + + void deletePost(Long postId); + + Optional findPostedMember(Long postId); +} diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/PostRepositoryCustomImpl.java b/src/main/java/Remoa/BE/Web/Post/Repository/PostRepositoryCustomImpl.java new file mode 100644 index 0000000..7d0187d --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/PostRepositoryCustomImpl.java @@ -0,0 +1,105 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Comment.Domain.Comment; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Domain.PostLike; +import Remoa.BE.Web.Post.Domain.PostScrap; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import java.util.List; +import java.util.Optional; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class PostRepositoryCustomImpl implements PostRepositoryCustom { + + private final EntityManager em; + + public void savePost(Post post) { + em.persist(post); + } + + public void modifyPost(Post post) { + em.merge(post); + } + + public List findAll(){ + return em.createQuery("SELECT p FROM Post p", Post.class).getResultList(); + } + + + public Optional findOne(Long postId) { // 위 findByPostId와 거의 같음 반환형을 Optional로 하기 위함 + return Optional.ofNullable(em.find(Post.class, postId)); + } + + public List findByMember(Member member) { + return em.createQuery("select p from Post p where p.member = :member", Post.class) + .setParameter("member", member) + .getResultList(); + } + + public List findByTitleContaining(String name){ + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(Post.class); + Root root = query.from(Post.class); + query.select(root).where(cb.like(root.get("title"), "%" + name + "%")); + return em.createQuery(query).getResultList(); + } + + public void savePostScrap(PostScrap postScrap) { + em.persist(postScrap); + } + + public Optional findScrapedPost(Member member, Post post) { + return em.createQuery("select ps from PostScrap ps where ps.member = :member and ps.post = :post", PostScrap.class) + .setParameter("member", member) + .setParameter("post", post) + .getResultStream() + .findAny(); + } + + public Optional findLikedPost(Member myMember, Post post) { + return em.createQuery("select pl from PostLike pl where pl.member = :member and pl.post = :post", PostLike.class) + .setParameter("member", myMember) + .setParameter("post", post) + .getResultStream() + .findAny(); + } + + public List findPostsByCategory(Category category) { + return em.createQuery("select p from Post p where p.category = :category", Post.class) + .setParameter("category", category) + .getResultList(); + } + + public void saveComment(Comment comment) { + em.persist(comment); + } + + public void deletePost(Long postId) { + Post post = em.find(Post.class, postId); + em.remove(post); + } + + public void deletePostByMember(Member member) { + em.createQuery("delete from Post p where p.member = :member") + .setParameter("member", member) + .executeUpdate(); + } + + public Optional findPostedMember(Long postId) { + return em.createQuery("select p.member from Post p where p.postId = :postId", Member.class) + .setParameter("postId", postId) + .getResultStream() + .findAny(); + } +} diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/PostScrapRepository.java b/src/main/java/Remoa/BE/Web/Post/Repository/PostScrapRepository.java new file mode 100644 index 0000000..ab0dc06 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/PostScrapRepository.java @@ -0,0 +1,21 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.PostScrap; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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 org.springframework.stereotype.Repository; + +@Repository +public interface PostScrapRepository extends JpaRepository, PostScrapRepositoryCustom { + PostScrap findByMemberMemberIdAndPostPostId(Long memberId, Long postId); + + @Modifying + @Query(value = "delete from post_scrap ps where post_id = :postId", nativeQuery = true) + void deleteByPostHard(@Param("postId")Long postId); +} diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/PostScrapRepositoryCustom.java b/src/main/java/Remoa/BE/Web/Post/Repository/PostScrapRepositoryCustom.java new file mode 100644 index 0000000..f1e0800 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/PostScrapRepositoryCustom.java @@ -0,0 +1,14 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.PostScrap; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface PostScrapRepositoryCustom { + + + Page findMyScrapedPost(Member member, Pageable pageable, Category category, String sort); + +} diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/PostScrapRepositoryCustomImpl.java b/src/main/java/Remoa/BE/Web/Post/Repository/PostScrapRepositoryCustomImpl.java new file mode 100644 index 0000000..cf64186 --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/PostScrapRepositoryCustomImpl.java @@ -0,0 +1,74 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Comment.Domain.QComment; +import Remoa.BE.Web.CommentFeedback.Domain.CommentFeedback; +import Remoa.BE.Web.CommentFeedback.Domain.QCommentFeedback; +import Remoa.BE.Web.Feedback.Domain.QFeedback; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.QMember; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.PostScrap; +import Remoa.BE.Web.Post.Domain.QPost; +import Remoa.BE.Web.Post.Domain.QPostScrap; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class PostScrapRepositoryCustomImpl implements PostScrapRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + QCommentFeedback commentFeedback = QCommentFeedback.commentFeedback; + QMember member = QMember.member; + QPost post = QPost.post; + QPostScrap postScrap = QPostScrap.postScrap; + + @Override + public Page findMyScrapedPost(Member myMember, Pageable pageable, Category category, String sort) { + boolean isCategoryExists = category != null; + + JPAQuery query = jpaQueryFactory.select(postScrap) + .from(postScrap) + .innerJoin(postScrap.post, post).fetchJoin() + .innerJoin(postScrap.post.member, member).fetchJoin() + .where( + postScrap.member.eq(myMember) + .and(isCategoryExists ? postScrap.post.category.eq(category) : null) + ); + // Apply sorting based on the sort parameter + OrderSpecifier orderSpecifier; + if ("asc".equalsIgnoreCase(sort)) { + orderSpecifier = postScrap.scrapTime.asc(); + } else { + orderSpecifier = postScrap.scrapTime.desc(); + } + + List result = query.orderBy(orderSpecifier) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // 페이지네이션 기능을 위한 카운트 쿼리 + JPAQuery countQuery = jpaQueryFactory + .select(postScrap.count()) + .from(postScrap) + .innerJoin(postScrap.post, post) + .where( + postScrap.member.eq(myMember) + .and(isCategoryExists ? postScrap.post.category.eq(category) : null) + ); + + // 페이지네이션 적용 + return PageableExecutionUtils.getPage(result, pageable, countQuery::fetchOne); + } + +} diff --git a/src/main/java/Remoa/BE/Web/Post/Repository/UploadFileRepository.java b/src/main/java/Remoa/BE/Web/Post/Repository/UploadFileRepository.java new file mode 100644 index 0000000..25e0bda --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Repository/UploadFileRepository.java @@ -0,0 +1,54 @@ +package Remoa.BE.Web.Post.Repository; + +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Domain.UploadFile; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.EntityManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Repository +@RequiredArgsConstructor +@Slf4j +public class UploadFileRepository { + + private final EntityManager em; + + + public Optional findById(Long fileId){ + return Optional.ofNullable(em.find(UploadFile.class, fileId)); + } + + public void saveFile(UploadFile file) { + em.persist(file); + } + + public void modifyFile(UploadFile file) { + em.merge(file); + } + + public List findFilesByPost(Post post) { + return new ArrayList<>(em.createQuery("select uf from UploadFile uf where uf.post = :post", UploadFile.class) + .setParameter("post", post) + .getResultList()); + } + + public void deleteById(UploadFile file) { + em.createQuery("delete from UploadFile u where u.uploadFileId = :id") + .setParameter("id", file.getUploadFileId()) + .executeUpdate(); + } + + public void deleteByPost(Post post) { + em.createQuery("delete from UploadFile u where u.post = :post") + .setParameter("post", post) + .executeUpdate(); + } + +} diff --git a/src/main/java/Remoa/BE/Post/Service/CategoryService.java b/src/main/java/Remoa/BE/Web/Post/Service/CategoryService.java similarity index 86% rename from src/main/java/Remoa/BE/Post/Service/CategoryService.java rename to src/main/java/Remoa/BE/Web/Post/Service/CategoryService.java index 87771a5..a55c944 100644 --- a/src/main/java/Remoa/BE/Post/Service/CategoryService.java +++ b/src/main/java/Remoa/BE/Web/Post/Service/CategoryService.java @@ -1,9 +1,9 @@ -package Remoa.BE.Post.Service; +package Remoa.BE.Web.Post.Service; -import Remoa.BE.Post.Domain.Category; -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Member.Domain.MemberCategory; -import Remoa.BE.Post.Repository.CategoryRepository; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.MemberCategory; +import Remoa.BE.Web.Post.Repository.CategoryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/Remoa/BE/Web/Post/Service/FileService.java b/src/main/java/Remoa/BE/Web/Post/Service/FileService.java new file mode 100644 index 0000000..d896f2f --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Service/FileService.java @@ -0,0 +1,235 @@ +package Remoa.BE.Web.Post.Service; + +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Domain.UploadFile; +import Remoa.BE.Web.Post.Repository.PostRepository; +import Remoa.BE.Web.Post.Repository.UploadFileRepository; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.*; +import com.amazonaws.util.IOUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FileService { + + private final AmazonS3 amazonS3; + private final UploadFileRepository uploadFileRepository; + private final PostRepository postRepository; + private final List uploadFileList; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Transactional + public void deleteByPost(Post post){ + uploadFileRepository.deleteByPost(post); + } + + /** + * @param post 게시글 + * @param multipartFile 해당 게시글의 파일 리스트 + * 파일들을 저장해준다 + */ + @Transactional + public void saveUploadFiles(Post post, MultipartFile thumbnail, List multipartFile) { + + //썸네일 파일 저장 추가 + saveUploadFile(thumbnail, post, "thumbnail"); + if (multipartFile != null) { + multipartFile.forEach(file -> saveUploadFile(file, post, "post")); + } + + //새로운 인스턴스 만들어서 set하지 않으면 clear 되면서 null이 계속 저장됨. + UploadFile uploadFile = uploadFileList.get(0); + post.setThumbnailUrl(uploadFile.getStoreFileUrl()); + + post.setUploadFiles(new ArrayList<>(uploadFileList.subList(1, uploadFileList.size()))); + postRepository.savePost(post); + + uploadFileList.clear(); + } + + /** + * @param post 수정할 게시글 + * @param multipartFile 해당 게시글의 수정할 파일 리스트 + * Post 엔티티의 file들을 수정해준다 + */ + @Transactional + public void modifyUploadFiles(Post post, MultipartFile thumbnail, List multipartFile) { + + List recentFiles = uploadFileRepository.findFilesByPost(post); + // 이미 해당하는 post에 파일 정보를 삭제처리 + if (recentFiles.size() > 0) { + recentFiles.forEach(file -> { + file.setDeleted(true); // DB 삭제 처리(delete 컬럼 update 1) + uploadFileRepository.modifyFile(file); + // amazonS3.deleteObject(new DeleteObjectRequest(bucket, file.getSaveFileName())); // S3에서 삭제처리 + }); + } + + //썸네일 파일 저장 추가 + saveUploadFile(thumbnail, post, "thumbnail"); + if (multipartFile != null) { + multipartFile.forEach(file -> saveUploadFile(file, post, "post")); + } + + //새로운 인스턴스 만들어서 set하지 않으면 clear 되면서 null이 계속 저장됨. + UploadFile uploadFile = uploadFileList.get(0); + post.setThumbnailUrl(uploadFile.getStoreFileUrl()); + + post.setUploadFiles(new ArrayList<>(uploadFileList.subList(1, uploadFileList.size()))); + postRepository.modifyPost(post); + + uploadFileList.clear(); + } + + /** + * @param multipartFile 파일 + */ + @Transactional + public void saveUploadFile(MultipartFile multipartFile, Post post, String folderName) { + String originalFilename = validateAndAdjustFileName(multipartFile); + + String s3name = generateUniqueFileName(folderName, originalFilename); + + uploadToS3(multipartFile, s3name); + + String storeFileUrl = getAmazonS3Url(bucket, s3name); + + validateAndSaveUploadFile(originalFilename, s3name, storeFileUrl, post); + } + + private String validateAndAdjustFileName(MultipartFile multipartFile) { + // 파일 이름 + String originalFilename = multipartFile.getOriginalFilename(); + assert originalFilename != null; + + // 파일 이름 길이 검사 및 조정 + if (originalFilename.length() > 255) { + log.info("originalFilename too long: {}", originalFilename); + originalFilename = originalFilename.substring(0, 255); + } + return originalFilename; + } + + private String generateUniqueFileName(String folderName, String originalFilename) { + // 파일 이름이 겹치지 않게 + String uuid = UUID.randomUUID().toString(); + String s3name = folderName + "/" + uuid + "_" + originalFilename; + + // 파일 이름 길이 검사 + if (s3name.length() > 500) { + log.info("s3name too long: {}", s3name); + throw new BaseException(CustomMessage.INVALID_FILE_LENGTH); + } + return s3name; + } + + private void uploadToS3(MultipartFile multipartFile, String s3name) { + try (InputStream inputStream = multipartFile.getInputStream()) { + // 파일 업로드 + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(multipartFile.getContentType()); + objectMetadata.setContentLength(multipartFile.getSize()); + + amazonS3.putObject(new PutObjectRequest(bucket, s3name, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch (IOException e) { + // 파일을 제대로 받아오지 못했을 때 예외 처리 + throw new RuntimeException(e); + } + } + + private String getAmazonS3Url(String bucket, String s3name) { + // 파일 보관 URL + String storeFileUrl = amazonS3.getUrl(bucket, s3name).toString().replaceAll("\\+", "+"); + + // URL 길이 검사 + if (storeFileUrl.length() > 500) { + log.info("storeFileUrl too long: {}", storeFileUrl); + throw new BaseException(CustomMessage.INVALID_FILE_LENGTH); + } + return storeFileUrl; + } + + private void validateAndSaveUploadFile(String originalFilename, String s3name, String storeFileUrl, Post post) { + UploadFile uploadFile = new UploadFile(); + uploadFile.setOriginalFileName(originalFilename); + uploadFile.setSaveFileName(s3name); + uploadFile.setStoreFileUrl(storeFileUrl); + uploadFile.setExtension(originalFilename.substring(originalFilename.lastIndexOf(".") + 1)); + uploadFile.setPost(post); + + // 데이터베이스에 저장 + uploadFileRepository.saveFile(uploadFile); + uploadFileList.add(uploadFile); + // 로깅 + log.info("Stored file URL: {}", storeFileUrl); + } + + + public String getUrl(Long fileId) { + Optional file = uploadFileRepository.findById(fileId); + if (file.isPresent()) { + return file.get().getStoreFileUrl(); + } else { + //해당 파일이 없을떄 예외처리 + //Todo 예외처리 custom 따로 만들기 + throw new RuntimeException(); + } + } + + + public ResponseEntity getObject(Long fileId) throws IOException { + + Optional file = uploadFileRepository.findById(fileId); + if (file.isPresent()) { + String s3name = file.get().getSaveFileName(); + + S3Object o = amazonS3.getObject(new GetObjectRequest(bucket, s3name)); + S3ObjectInputStream objectInputStream = o.getObjectContent(); + byte[] bytes = IOUtils.toByteArray(objectInputStream); + + String fileOriginalName = file.get().getOriginalFileName(); + //encode 메서드에 두 번째 파라메터에 StandardCharsets.UTF_8만 쓰면 오류가 나서 뒤에 name을 임사방편으로 붙임. 기능상 문제는 없을듯 함 + String fileNameFix = URLEncoder.encode(fileOriginalName, StandardCharsets.UTF_8.name()) + .replaceAll("\\+", "%20"); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM); + httpHeaders.setContentLength(bytes.length); + httpHeaders.setContentDispositionFormData("attachment", fileNameFix); + + return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK); + } else { + //해당 파일이 없을떄 예외처리 + //Todo 예외처리 custom 따로 만들기 + throw new RuntimeException(); + } + + + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/Web/Post/Service/PostService.java b/src/main/java/Remoa/BE/Web/Post/Service/PostService.java new file mode 100644 index 0000000..9cc104a --- /dev/null +++ b/src/main/java/Remoa/BE/Web/Post/Service/PostService.java @@ -0,0 +1,348 @@ +package Remoa.BE.Web.Post.Service; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Service.MemberService; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Domain.Post; +import Remoa.BE.Web.Post.Domain.PostLike; +import Remoa.BE.Web.Post.Domain.PostScrap; +import Remoa.BE.Web.Post.Dto.Request.UploadPostForm; +import Remoa.BE.Web.Post.Repository.*; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import javax.swing.plaf.SpinnerUI; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static Remoa.BE.utill.Constant.HOME_PAGE_SIZE; +import static Remoa.BE.utill.FileExtension.fileExtension; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PostService { + + + private final MemberService memberService; + private final PostRepository postRepository; + private final CategoryRepository categoryRepository; + private final FileService fileService; + private final PostPagingRepository postPagingRepository; + private final PostScrapRepository postScrapRepository; + private final PostLikeRepository postLikeRepository; + + + public List findPostsByMemberHard(Member member) { + return postRepository.findByMemberHard(member.getMemberId()); + } + + @Transactional + public void deleteByMember(Member myMember) { + postRepository.deletePostByMemberHard(myMember.getMemberId()); + } + + @Transactional + public void deletePostScrapByPost(Post post) { + postScrapRepository.deleteByPostHard(post.getPostId()); + } + + public Post findOne(Long postId) { + Optional post = postRepository.findOne(postId); + return post.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Post not found")); + } + + @Transactional + public Post findOneViewPlus(Long postId, HttpSession session) { + Post post = postRepository.findOne(postId).orElseThrow(() -> new BaseException(CustomMessage.NO_ID)); + handleViewCount(post, session); + return post; + } + + private void handleViewCount(Post post, HttpSession session) { + Long postId = post.getPostId(); + String sessionKey = "PostViewed_" + postId; + log.info("sessionKey = {}", sessionKey); + + if (session.getAttribute(sessionKey) == null) { + post.addViewCount(); + session.setAttribute(sessionKey, true); + } + } + +// public int findScrapCount(Long postId){ +// Post findPost = findOne(postId); +// return findPost.getPostScraps().size(); +// } + +// public int findLikeCount(Long postId){ +// Post findPost = findOne(postId); +// return findPost.getPostLikes().size(); +// } + + @Transactional + public Post likePost(Long memberId, Member myMember, Long referenceId) { + Post post = findOne(referenceId); + Integer postLikeCount = post.getLikeCount(); // 이 게시물을 좋아요한 수 + + PostLike postLike = postLikeRepository.findByMemberMemberIdAndPostPostId(memberId, referenceId); + if (postLike == null) { + post.setLikeCount(postLikeCount + 1); // 좋아요 + 1 + PostLike postLikeObj = PostLike.createPostLike(myMember, post); + postLikeRepository.save(postLikeObj); + } else { + post.setLikeCount(post.getLikeCount() - 1); // 좋아요 - 1 + postLikeRepository.deleteById(postLike.getPostLikeId()); + } + return post; + } + + @Transactional + public Post registerPost(UploadPostForm uploadPostForm, MultipartFile thumbnail, List uploadFiles, Long memberId) throws IOException { + + Category category = categoryRepository.findByCategoryName(uploadPostForm.getCategory()); + + Member member = memberService.findOne(memberId); + + Post post; + + // 비디오만 따로 처리 + if (category.getName().equals("video")) { + if (uploadFiles != null) { + throw new IOException(); + } + post = Post.builder() + .title(uploadPostForm.getTitle()) + .member(member) + .contestName(uploadPostForm.getContestName()) + .category(category) + .youtubeLink(uploadPostForm.getYoutubeLink()) + .contestAwardType(uploadPostForm.getContestAwardType()) + .pageCount(1) + .postingTime(LocalDateTime.now()) + .likeCount(0) + .views(0) + .scrapCount(0) + .deleted(false) + .build(); + fileService.saveUploadFiles(post, thumbnail, null); + } else { + if (uploadFiles == null) { + throw new IOException(); + } + //확장자 확인 + String extension = fileExtension(uploadFiles.get(0)); + if (extension.equals("pdf") || extension.equals("jpg") || extension.equals("png")) { + int pageCount; + if (extension.equals("pdf")) { + PDDocument document = PDDocument.load(uploadFiles.get(0).getInputStream()); + pageCount = document.getNumberOfPages(); + } else { + pageCount = uploadFiles.size(); + } + post = Post.builder() + .title(uploadPostForm.getTitle()) + .member(member) + .contestName(uploadPostForm.getContestName()) + .category(category) + .youtubeLink(uploadPostForm.getYoutubeLink()) + .contestAwardType(uploadPostForm.getContestAwardType()) + .pageCount(pageCount) + .postingTime(LocalDateTime.now()) + .likeCount(0) + .views(0) + .scrapCount(0) + .deleted(false) + .build(); + + fileService.saveUploadFiles(post, thumbnail, uploadFiles); + } else { + throw new IOException(); + } + } + + return post; + } + + @Transactional + public Post modifyPost(UploadPostForm uploadPostForm, MultipartFile thumbnail, List uploadFiles, Post originPost) throws IOException { + Category category = categoryRepository.findByCategoryName(uploadPostForm.getCategory()); + + Member member = memberService.findOne(originPost.getMember().getMemberId()); + + log.info("thumbnail = " + thumbnail.getOriginalFilename()); + + // 비디오만 따로 처리 + if (category.getName().equals("video")) { + if (uploadFiles != null) { + throw new IOException(); + } + + originPost.setTitle(uploadPostForm.getTitle()); + originPost.setContestName(uploadPostForm.getContestName()); + originPost.setCategory(category); + originPost.setYoutubeLink(uploadPostForm.getYoutubeLink()); + originPost.setContestAwardType(uploadPostForm.getContestAwardType()); + originPost.setPageCount(1); + + + fileService.modifyUploadFiles(originPost, thumbnail, null); + } else { + if (uploadFiles == null) { + throw new IOException(); + } + //확장자 확인 + String extension = fileExtension(uploadFiles.get(0)); + if (extension.equals("pdf") || extension.equals("jpg") || extension.equals("png")) { + int pageCount; + if (extension.equals("pdf")) { + PDDocument document = PDDocument.load(uploadFiles.get(0).getInputStream()); + pageCount = document.getNumberOfPages(); + } else { + pageCount = uploadFiles.size(); + } + + originPost.setTitle(uploadPostForm.getTitle()); + originPost.setContestName(uploadPostForm.getContestName()); + originPost.setCategory(category); + originPost.setYoutubeLink(uploadPostForm.getYoutubeLink()); + originPost.setContestAwardType(uploadPostForm.getContestAwardType()); + originPost.setPageCount(pageCount); + + fileService.modifyUploadFiles(originPost, thumbnail, uploadFiles); + } else { + throw new IOException(); + } + } + return originPost; + } + + public Page sortAndPaginatePosts(String sort, int pageNumber, String searchQuery) { + Page Posts; + Pageable pageable; + switch (sort) { + case "likes": + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("likeCount").descending()); + Posts = postPagingRepository.findByMemberNameOrTitleContaining(pageable, searchQuery); + break; + case "scrap": + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("scrapCount").descending()); + Posts = postPagingRepository.findByMemberNameOrTitleContaining(pageable, searchQuery); + break; + case "views": + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("views").descending()); + Posts = postPagingRepository.findByMemberNameOrTitleContaining(pageable, searchQuery); + break; + default: + //sort 문자열이 잘못됐을 경우 default인 최신순으로 정렬 + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("postingTime").descending()); + Posts = postPagingRepository.findByMemberNameOrTitleContaining(pageable, searchQuery); + break; + } + return Posts; + } + + public Page sortAndPaginatePostsByCategory(String category, String sort, int pageNumber, String searchQuery) { + Page Posts; + Pageable pageable; + switch (sort) { + case "likes": + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("likeCount").descending()); + break; + case "scrap": + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("scrapCount").descending()); + break; + case "views": + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("views").descending()); + break; + default: + //sort 문자열이 잘못됐을 경우 default인 최신순으로 정렬 + pageable = PageRequest.of(pageNumber, HOME_PAGE_SIZE, Sort.by("postingTime").descending()); + break; + } + Posts = postPagingRepository.findByMemberNameOrTitleContainingAndCategory(pageable, searchQuery, categoryRepository.findByCategoryName(category)); + return Posts; + } + + public PostScrap getPostScrapByMemberIdAndPostId(Long memberId, Long postId) { + return postScrapRepository.findByMemberMemberIdAndPostPostId(memberId, postId); + } + + @Transactional + public boolean scrapPost(Long memberId, Member myMember, Long referenceId) { + boolean isScrapAction = true; + Post post = findOne(referenceId); + Integer postScrapCount = post.getScrapCount(); // 이 게시물을 스크랩한 수 + + // scrapPost를 db에서 조회해보고 조회 결과가 null이면 scrapCount += 1, PostScrap 생성 + // null이 아니면 scrapCount -= 1, 조회결과인 해당 PostScrap 삭제 + PostScrap postScrap = getPostScrapByMemberIdAndPostId(memberId, referenceId); + if (postScrap == null) { + post.setScrapCount(postScrapCount + 1); // 스크랩 수 1 증가 + PostScrap postScrapObj = PostScrap.createPostScrap(myMember, post); + postScrapRepository.save(postScrapObj); + isScrapAction = true; + } else { + post.setScrapCount(post.getScrapCount() - 1); // 스크랩 수 1 차감 + postScrapRepository.deleteById(postScrap.getPostScrapId()); // db에서 삭제 + isScrapAction = false; + } + return isScrapAction; + } + + public Page findScrapedPost(int page, Member member, String categoryString, String sort) { + Category category = null; + List categoryList = Arrays.asList("idea", "marketing", "design", "video", "digital", "etc"); + if (categoryList.contains(categoryString)) { + category = categoryRepository.findByCategoryName(categoryString); + } + Pageable pageable = PageRequest.of(page, HOME_PAGE_SIZE); + return postScrapRepository.findMyScrapedPost(member, pageable, category, sort); + } + + public List findRecentTwelveScrapedPost(Member member) { + return postPagingRepository.findByMemberRecentTwelve(member); + } + + public boolean checkMemberPost(Member myMember, Long postId) { + // postId를 이용해 Post 엔티티에 등록된 Member를 불러오고 이 Member가 컨트롤러에서 불러온 Member와 맞는지 확인하고 true/false 반환 + Post post = findOne(postId); + Member member = post.getMember(); + return myMember == member; + } + + public boolean isThisPostScraped(Member myMember, Post post) { + return postRepository.findScrapedPost(myMember, post).isPresent(); + } + + public boolean isThisPostLiked(Member myMember, Post post) { + return postRepository.findLikedPost(myMember, post).isPresent(); + } + + @Transactional + public void deleteReference(Long postId) { + postRepository.deletePost(postId); + } + + public Member getPostedMember(Long postId) { + return postRepository.findPostedMember(postId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Post not found")); + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/AppConfig.java b/src/main/java/Remoa/BE/config/AppConfig.java new file mode 100644 index 0000000..80b45c7 --- /dev/null +++ b/src/main/java/Remoa/BE/config/AppConfig.java @@ -0,0 +1,20 @@ +package Remoa.BE.config; + +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public ModelMapper modelMapper() { + return new ModelMapper(); + } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/CorsConfig.java b/src/main/java/Remoa/BE/config/CorsConfig.java index a369d88..ade0a09 100644 --- a/src/main/java/Remoa/BE/config/CorsConfig.java +++ b/src/main/java/Remoa/BE/config/CorsConfig.java @@ -11,14 +11,16 @@ public class CorsConfig { @Bean public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); - config.addAllowedOrigin("*"); + config.addAllowedOriginPattern("*"); // addAllowedOriginPattern("*") 대신 사용 config.addAllowedHeader("*"); config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); - source.registerCorsConfiguration("*", config); return new CorsFilter(source); } } \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/DbInit.java b/src/main/java/Remoa/BE/config/DbInit.java index e1350be..bd63aba 100644 --- a/src/main/java/Remoa/BE/config/DbInit.java +++ b/src/main/java/Remoa/BE/config/DbInit.java @@ -1,47 +1,31 @@ package Remoa.BE.config; -import Remoa.BE.Member.Domain.Role; -import Remoa.BE.Post.Domain.Category; -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Post.Service.CategoryService; -import Remoa.BE.Member.Service.MemberService; +import Remoa.BE.Web.Member.Dto.GerneralLoginDto.AdminSignUpReq; +import Remoa.BE.Web.Member.Dto.GerneralLoginDto.GeneralSignUpReq; +import Remoa.BE.Web.Member.Dto.GerneralLoginDto.GeneralSignUpRes; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Post.Service.CategoryService; +import Remoa.BE.Web.Member.Service.MemberService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; +import jakarta.annotation.PostConstruct; + +import java.util.List; + + + @Slf4j -@Component +//@Component @RequiredArgsConstructor public class DbInit { - private final MemberService memberService; private final CategoryService categoryService; + private final MemberService memberService; - /** - * @PostConstruct로 Admin 계정을 생성해주는 메서드 - */ - @PostConstruct - public void createAdminUser() { - if (memberService.isAdminExist()) { - //do nothing - log.info("============Admin is already exist============"); - } else { - Member adminMember = new Member(); - adminMember.setEmail("spparta@gmail.com"); - adminMember.setPassword("admin"); - adminMember.setName("admin"); - adminMember.setBirth("00000000"); - adminMember.setSex(true); - adminMember.setPhoneNumber("01000000000"); - adminMember.setOneLineIntroduction("관리자입니다."); - adminMember.setTermConsent(true); - - adminMember.setRole("ROLE_ADMIN,ROLE_USER"); - memberService.join(adminMember); - log.info("============Add Admin user completely============"); - } - } + public static final List categoryList = List.of("idea", "marketing", "design", "video", "digital", "etc"); /** * @PostConstruct로 Category를 세팅해주는 메서드 @@ -52,14 +36,61 @@ public void initCategories() { //do nothing log.info("==========Categories are already set=========="); } else { - Category idea = new Category("idea"); - Category marketing = new Category("marketing"); - Category design = new Category("design"); - Category video = new Category("video"); - Category etc = new Category("etc"); - this.categoryService.persistCategory(idea, marketing, design, video, etc); + Category idea = new Category(categoryList.get(0)); + Category marketing = new Category(categoryList.get(1)); + Category design = new Category(categoryList.get(2)); + Category video = new Category(categoryList.get(3)); + Category digital = new Category(categoryList.get(4)); + Category etc = new Category(categoryList.get(5)); + this.categoryService.persistCategory(idea, marketing, design, video, digital, etc); log.info("==========Setting Categories completely=========="); } } + @PostConstruct + public void initAdmin() { + AdminSignUpReq adminSignUpReq = AdminSignUpReq.builder() + .account("referencemoa") + .password("fpahdk2023!") + .name("관리자") + .build(); + + memberService.adminSignUp(adminSignUpReq); + } + + + @PostConstruct + public void initMembers() { + + GeneralSignUpReq signUpReq1 = GeneralSignUpReq.builder() + .account("test1@gmail.com") + .password("testPassword1") + .name("김김김") + .build(); + + GeneralSignUpReq signUpReq2 = GeneralSignUpReq.builder() + .account("test2@gmail.com") + .password("testPassword2") + .name("이이이") + .build(); + + GeneralSignUpReq signUpReq3 = GeneralSignUpReq.builder() + .account("test3@gmail.com") + .password("testPassword3") + .name("박박박") + .build(); + + GeneralSignUpReq signUpReq4 = GeneralSignUpReq.builder() + .account("test4@gmail.com") + .password("testPassword4") + .name("최최최") + .build(); + + + memberService.generalSignUp(signUpReq1); + memberService.generalSignUp(signUpReq2); + memberService.generalSignUp(signUpReq3); + memberService.generalSignUp(signUpReq4); + } + } diff --git a/src/main/java/Remoa/BE/config/QueryDslConfig.java b/src/main/java/Remoa/BE/config/QueryDslConfig.java new file mode 100644 index 0000000..eee1b9c --- /dev/null +++ b/src/main/java/Remoa/BE/config/QueryDslConfig.java @@ -0,0 +1,20 @@ +package Remoa.BE.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +@Configuration +public class QueryDslConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/Remoa/BE/config/SecureConfig.java b/src/main/java/Remoa/BE/config/SecureConfig.java index 8bff517..b464520 100644 --- a/src/main/java/Remoa/BE/config/SecureConfig.java +++ b/src/main/java/Remoa/BE/config/SecureConfig.java @@ -1,63 +1,78 @@ package Remoa.BE.config; +import Remoa.BE.Web.Member.Domain.Role; +import Remoa.BE.config.jwt.CustomAuthenticationEntryPoint; +import Remoa.BE.config.jwt.JwtAccessDeniedHandler; +import Remoa.BE.config.jwt.JwtAuthenticationFilter; +import Remoa.BE.config.jwt.JwtTokenProvider; +import Remoa.BE.config.redis.RedisUtils; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.filter.CorsFilter; +import static Remoa.BE.config.auth.AuthConstant.*; + @RequiredArgsConstructor @EnableWebSecurity @Configuration public class SecureConfig { - private final CorsFilter corsFilter; + public static final String FRONT_URL = "http://localhost:3000"; - /** - * Spring Security에서 사용할 password encoder로 BCryptPasswordEncoder 지정 - */ - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + private final CorsFilter corsFilter; + private final JwtTokenProvider jwtTokenProvider; + private final RedisUtils redisUtils; - /** - * Spring Security 설정 - */ @Bean - public SecurityFilterChain configure(HttpSecurity http) throws Exception { - - http.addFilter(corsFilter); - - http.formLogin().disable(); - - http - .authorizeRequests() - //api 명세 확정 후 재확인 핋요 - .antMatchers("/**").permitAll() - /*.antMatchers("/").permitAll() - .antMatchers("/login").permitAll() - .antMatchers("/logout").permitAll() - .antMatchers("/upload").permitAll() - .antMatchers("/download").permitAll() - .antMatchers("/signup/**").permitAll() - .antMatchers("/mypage").authenticated()*/ - .antMatchers("/admin").hasRole("ADMIN") - .antMatchers("/user").hasAnyRole("ADMIN", "USER") - .anyRequest().authenticated() - .and() - - .sessionManagement() - .maximumSessions(-1) //세션 제한 없음 - .maxSessionsPreventsLogin(false); //중복 접속시 마지막 세션만 유지 - - http.csrf().disable(); + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + http.csrf(CsrfConfigurer::disable) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .addFilter(corsFilter); + + http.authorizeHttpRequests(request -> request + // .requestMatchers(AUTH_BLACKLIST).authenticated() + .requestMatchers(HttpMethod.GET, GET_AUTH_BLACKLIST).authenticated() + .requestMatchers(HttpMethod.POST, POST_AUTH_BLACKLIST).authenticated() + .requestMatchers(HttpMethod.PUT, PUT_AUTH_BLACKLIST).authenticated() + .requestMatchers(HttpMethod.DELETE, DELETE_AUTH_BLACKLIST).authenticated() + .requestMatchers(HttpMethod.POST, ADMIN_POST_AUTH_BLACKLIST).hasAuthority(Role.ADMIN.toString()) // ADMIN 권한 + .requestMatchers(HttpMethod.PUT, ADMIN_PUT_AUTH_BLACKLIST).hasAuthority(Role.ADMIN.toString()) // ADMIN 권한 + .requestMatchers(HttpMethod.DELETE, ADMIN_DELETE_AUTH_BLACKLIST).hasAuthority(Role.ADMIN.toString()) // ADMIN 권한 + //인증되어야 들어갈 수 있다. + .anyRequest().permitAll()) + // 나머지는 모두 허용 + .exceptionHandling(exceptionHandling -> exceptionHandling + .authenticationEntryPoint(new CustomAuthenticationEntryPoint())) + .exceptionHandling(exceptionHandling -> exceptionHandling + .accessDeniedHandler(new JwtAccessDeniedHandler())); //권한 403 관련 + /** + AuthenticationEntryPoint + 인증이 되지않은 유저가 요청을 했을때 동작함 + */ + + http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider,redisUtils), UsernamePasswordAuthenticationFilter.class); return http.build(); } -} + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/SwaggerConfig.java b/src/main/java/Remoa/BE/config/SwaggerConfig.java new file mode 100644 index 0000000..bb4dbb3 --- /dev/null +++ b/src/main/java/Remoa/BE/config/SwaggerConfig.java @@ -0,0 +1,40 @@ +package Remoa.BE.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.servers.Server; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@OpenAPIDefinition( + servers = {@Server(url = "/", description = "Default Server URL")}, + info = @Info( + title = "레모아 API 명세서", + description = "레모아 API 명세서입니다", + version = "v1")) +@Configuration +public class SwaggerConfig { + + + @Bean + // 운영 환경에는 Swagger를 비활성화하기 위해 추가했습니다. + public OpenAPI api() { + SecurityScheme apiKey = new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .in(SecurityScheme.In.HEADER) + .name("Authorization") + .scheme("bearer") + .bearerFormat("JWT"); + + SecurityRequirement securityRequirement = new SecurityRequirement() + .addList("Bearer Token"); + + return new OpenAPI() + .components(new Components().addSecuritySchemes("Bearer Token", apiKey)) + .addSecurityItem(securityRequirement); + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/auth/AuthConstant.java b/src/main/java/Remoa/BE/config/auth/AuthConstant.java new file mode 100644 index 0000000..0834d14 --- /dev/null +++ b/src/main/java/Remoa/BE/config/auth/AuthConstant.java @@ -0,0 +1,57 @@ +package Remoa.BE.config.auth; + +public class AuthConstant { + + // 인증이 필요하지 않은 경로 + public static final String[] AUTH_WHITELIST = { + "/api/**", "/graphiql", "/graphql", + "/swagger-ui/**", "/api-docs", "/swagger-ui-custom.html", + "/v3/api-docs/**", "/api-docs/**", "/swagger-ui.html" + }; + + // 인증이 필요한 경로 + public static final String[] AUTH_BLACKLIST = { + "/user/logout", "/following", "/follower", "/follow/{member_id}", "/user", "/user/img", "/reference/{reference_id}/comment", + "/reference/{reference_id}/comment/{comment_id}", "/reference/comment/{comment_id}", "/reference/comment/{comment_id}", "/comment/{comment_id}/like", + "/reference/{reference_id}/{page_number}", "/reference/{reference_id}/feedback/{feedback_id}", "/reference/feedback/{feedback_id}", "/reference/feedback/{feedback_id}/like" + , "/user/activity", "/user/scrap", "/user/comment", "/user/receive", "/user/feedback", "/user/reference", "/reference", "/reference", "/reference/{reference_id}", + "/reference/{reference_id}/like", "/reference/{reference_id}/scrap", "/user/reference/{reference_id}", "/user/referenceCategory/{category}", + "/delete", "/delete/{member_id}", "/inquiry" + }; + + // GET 메서드에 대한 인증이 필요한 경로 + public static final String[] GET_AUTH_BLACKLIST + = { "/following", "/follower", "/user", "/user/img", + "/inquiry", "/inquiry/view", "/user/activity", "/user/scrap", "/user/comment", + "/user/receive", "/user/feedback", "/user/reference", "/user/comment-feedback", "/university"}; + + // POST 메서드에 대한 인증이 필요한 경로 + public static final String[] POST_AUTH_BLACKLIST + = {"/follow/{member_id}", "/inquiry", "/reference/{reference_id}/comment", + "/reference/{reference_id}/comment/{comment_id}", "/comment/{comment_id}/like", "/reference/{reference_id}/feedback/{feedback_id}", + "/reference/feedback/{feedback_id}/like", "/reference", "/reference/{reference_id}/like", + "/reference/{reference_id}/scrap", "/reference/feedback-reply/{feedback_reply_id}/like", + "/reference/comment-reply/{comment_reply_id}/like", "/reference/feedback/{reference_id}/{page_number}", + "/reference/feedback/{reference_id}/{feedback_member_id}/like", "/reference/comment_reply/{reply_id}/like", "/reference/comment/{comment_id}/like"}; + + // PUT 메서드에 대한 인증이 필요한 경로 + public static final String[] PUT_AUTH_BLACKLIST + = {"/user", "/user/img", "/reference/comment/{comment_id}", "/reference/feedback/{feedback_id}", + "/reference/{reference_id}", "/inquiry/{id}", "/api/member/logout", "/reference/feedback/{feedback_id}/reply/{reply_id}", + "/reference/comment/{comment_id}/reply/{reply_id}", + "/reference/feedback/{reference_id}/{page_number}"}; + + // DELETE 메서드에 대한 인증이 필요한 경로 + public static final String[] DELETE_AUTH_BLACKLIST + = {"/user/img", "/delete/{member_id}", "/delete", "/reference/comment/{comment_id}", + "/reference/feedback/{feedback_id}", "/user/reference/{reference_id}", "/user/referenceCategory/{category}", + "/inquiry/{id}", "/reference/comment/{comment_id}/reply/{reply_id}", "/reference/feedback/{feedback_id}/reply/{reply_id}", + "/reference/feedback/{reference_id}/{page_number}", "/remove" }; + + // ADMIN 역할에 대한 인증이 필요한 경로 + public static final String[] ADMIN_POST_AUTH_BLACKLIST = {"/notice", "/inquiry/{inquiry_id}/reply", "/admin/**"}; + + public static final String[] ADMIN_PUT_AUTH_BLACKLIST = {"/notice/{id}", "/inquiry/reply/{reply_id}", "/admin/**"}; + + public static final String[] ADMIN_DELETE_AUTH_BLACKLIST = {"/notice/{id}", "/admin/**"}; +} diff --git a/src/main/java/Remoa/BE/config/auth/MemberDetails.java b/src/main/java/Remoa/BE/config/auth/MemberDetails.java new file mode 100644 index 0000000..6bf7bfd --- /dev/null +++ b/src/main/java/Remoa/BE/config/auth/MemberDetails.java @@ -0,0 +1,70 @@ +package Remoa.BE.config.auth; + +import Remoa.BE.Web.Member.Domain.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * + */ + +@RequiredArgsConstructor +@Getter +public class MemberDetails implements UserDetails { + + private final Long memberId; + private final String account; + private final String password; + private final String role; + + public MemberDetails(Member member) { + this.memberId = member.getMemberId(); + this.account = member.getAccount(); + this.password = member.getPassword(); + this.role = member.getRole().toString(); + } + + @Override + public Collection getAuthorities() { + List authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(role)); + return authorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return account; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/Remoa/BE/config/auth/MemberDetailsService.java b/src/main/java/Remoa/BE/config/auth/MemberDetailsService.java new file mode 100644 index 0000000..f3d3c14 --- /dev/null +++ b/src/main/java/Remoa/BE/config/auth/MemberDetailsService.java @@ -0,0 +1,22 @@ +package Remoa.BE.config.auth; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MemberDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Member member = memberRepository.findByAccount(username).orElseThrow(() -> new UsernameNotFoundException("user name not found!")); + return new MemberDetails(member); + } +} diff --git a/src/main/java/Remoa/BE/config/auth/RefreshToken.java b/src/main/java/Remoa/BE/config/auth/RefreshToken.java new file mode 100644 index 0000000..215a538 --- /dev/null +++ b/src/main/java/Remoa/BE/config/auth/RefreshToken.java @@ -0,0 +1,36 @@ +package Remoa.BE.config.auth; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; + +/*@RedisHash(value = "refreshToken", timeToLive = 14440)*/ +@Getter +@NoArgsConstructor +public class RefreshToken { + + + private String refreshToken; + + @Id + private String account; + + + public RefreshToken(String account, String refreshToken) { + this.account = account; + this.refreshToken = refreshToken; + } + + // 게터, 세터, 기타 메서드들... + + public void updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public boolean validateRefreshToken(String refreshToken) { + return this.refreshToken.equals(refreshToken); + } + + public void increaseReissueCount() { + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/auth/http/MultipartJackson2HttpMessageConverter.java b/src/main/java/Remoa/BE/config/auth/http/MultipartJackson2HttpMessageConverter.java new file mode 100644 index 0000000..9128f32 --- /dev/null +++ b/src/main/java/Remoa/BE/config/auth/http/MultipartJackson2HttpMessageConverter.java @@ -0,0 +1,31 @@ +package Remoa.BE.config.auth.http; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Type; + +@Component +public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { + + public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, MediaType.APPLICATION_OCTET_STREAM); + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return false; + } + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { + return false; + } + + @Override + protected boolean canWrite(MediaType mediaType) { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/jwt/CustomAuthenticationEntryPoint.java b/src/main/java/Remoa/BE/config/jwt/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..8b10d8f --- /dev/null +++ b/src/main/java/Remoa/BE/config/jwt/CustomAuthenticationEntryPoint.java @@ -0,0 +1,70 @@ +package Remoa.BE.config.jwt; + +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.ErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + + +import java.io.IOException; + +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + /* + 이 곳은 인증이 필요한 api에 접근 시 인증에 실패할 경우 이곳으로 진입하여 Response객체를 정의한다. + 토큰이 없거나, 만료되었거나, 유효하지 않거나 셋 중에 하나이다. + @ControllerAdvice에서 에러를 처리하는 건 서블릿 대상이다. + 필터의 에러는 이 곳에서 처리할 수 있다. 필터에서 에러가 발생한 경우 + */ + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + String exception = (String) request.getAttribute(JwtProperties.HEADER_STRING); + String errorCode; + + if (exception == null) { //토큰이 없고, 인증 못해서 에러 발생한 경우 + setErrorResponse(response, CustomMessage.NO_TOKEN_FOUND); + return; + } + + if(exception.equals("토큰이 만료되었습니다.")) { + setErrorResponse(response, CustomMessage.TOKEN_EXPIRED); + return; + } + + if (exception.equals("유효하지 않은 토큰입니다.")) { //토큰이 있지만, 로그인을(인증) 하지 못한 경우 + setErrorResponse(response, CustomMessage.NOT_VALID_TOKEN); + return; + } + } + + private void setErrorResponse( // 여기서 에러의 경우 반환 데이터를 정의 + HttpServletResponse response, + CustomMessage customMessage + ) { + ObjectMapper objectMapper = new ObjectMapper(); + response.setStatus(customMessage.getHttpStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + ErrorResponse errorResponse = ErrorResponse.builder() + .detail(customMessage.getMessage()) + .message(customMessage.getDetail()) + .build(); + + try { + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + + private void setResponse(HttpServletResponse response, String errorCode) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().println(JwtProperties.HEADER_STRING + " : " + errorCode); + } +} diff --git a/src/main/java/Remoa/BE/config/jwt/JwtAccessDeniedHandler.java b/src/main/java/Remoa/BE/config/jwt/JwtAccessDeniedHandler.java new file mode 100644 index 0000000..df3f76e --- /dev/null +++ b/src/main/java/Remoa/BE/config/jwt/JwtAccessDeniedHandler.java @@ -0,0 +1,42 @@ +package Remoa.BE.config.jwt; + +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.ErrorResponse; +import Remoa.BE.utill.MessageUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + //필요한 권한이 없이 접근하려 할때 403 + setErrorResponse(response, CustomMessage.FORBIDDEN); + } + + private void setErrorResponse( // 여기서 에러의 경우 반환 데이터를 정의 + HttpServletResponse response, + CustomMessage customMessage + ) { + ObjectMapper objectMapper = new ObjectMapper(); + response.setStatus(customMessage.getHttpStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + ErrorResponse errorResponse = ErrorResponse.builder() + .detail(customMessage.getMessage()) + .message(customMessage.getDetail()) + .build(); + + try { + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/jwt/JwtAuthenticationFilter.java b/src/main/java/Remoa/BE/config/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..d85b576 --- /dev/null +++ b/src/main/java/Remoa/BE/config/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,69 @@ +package Remoa.BE.config.jwt; + +import Remoa.BE.config.redis.RedisUtils; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +/** + * 만들어둔 jwt 패키지에 OncePerRequestFilter를 상속받는 유효성 체크용 필터를 만든다. + * 해당 필터는 이름에서도 짐작 가능 하듯, 한번의 요청마다 한번씩 실행되는 필터이다. -> GenericFilterBean으로 변경 + * 프론트 측에서 요청 헤더에 토큰을 넣어 보내면 이 필터가 검증해 줄 것이다. + */ + +//빈 주입을 하면 OncePerRequestFilter을 상속했더라도 SecurityConfig에서 한 번, 빈 주입에 의해 한 번 더 필터가 등록된다. +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends GenericFilterBean { + + private final JwtTokenProvider jwtTokenProvider; + // private final AccessTokenRepository accessTokenRepository; + private final RedisUtils redisUtils; + + + // Request로 들어오는 Jwt Token의 유효성을 검증하는 filter를 filterChain에 등록합니다. + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + log.info("JwtAuthenticationFilter 진입"); + + // header에서 JWT를 받아온다. //없을 수도 있다 없어도 접근 가능한 곳은 있으니. + String token = jwtTokenProvider.resolveToken((HttpServletRequest) servletRequest); + + // token이 있을 경우에는 유효성 확인. token이 없는데 인증 필요한 api 접근시 예외 발생 + // 매 진입마다 토큰 검사가 이뤄진다. -> 토큰으로부터 + try { + if (token != null) { + jwtTokenProvider.validateToken(token); //여기서 만료, 유효성 예외 발생. + if (doNotLogout(token)) { // 토큰이 블랙리스트에 없는 경우에만 인증 처리 + Authentication auth = jwtTokenProvider.getAuthentication(token); // 인증 객체 생성 + SecurityContextHolder.getContext().setAuthentication(auth); // SecurityContext 에 Authentication 객체를 저장 + log.info("인증 처리 함");//인증 객체를 통해 인증 처리 + } + } + } catch (ExpiredJwtException e) { + log.info("토큰이 있지만 기간이 만료 됨, 인증 필요한 api 접근시 예외 발생"); + servletRequest.setAttribute(JwtProperties.HEADER_STRING, "토큰이 만료되었습니다."); + } catch (Exception e) { + log.info("토큰이 있지만 유효하지 않음, 인증 필요한 api 접근시 예외 발생"); + servletRequest.setAttribute(JwtProperties.HEADER_STRING, "유효하지 않은 토큰입니다."); + } + + filterChain.doFilter(servletRequest, servletResponse); //토큰이 없는 경우는 바로 다음 필터로 넘어감. + //토큰 검증에서 예외가 발생하여도 인증이 필요 없는 경로로 요청한 경우 서블릿으로 진입가능하다. -> 정상적인 응답이 가능. + //토큰 검증에서 예외가 발생하고 인증이 필요한 경로로 요청한 경우 서블릿 진입이 불가하고, -> 인증 예외 발생 -> 예외를 AuthenticationEntryPoint에서 다룬다. + } + + private boolean doNotLogout(String accessToken) { + return !redisUtils.hasKeyBlackList(accessToken); + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/jwt/JwtProperties.java b/src/main/java/Remoa/BE/config/jwt/JwtProperties.java new file mode 100644 index 0000000..701d0ce --- /dev/null +++ b/src/main/java/Remoa/BE/config/jwt/JwtProperties.java @@ -0,0 +1,17 @@ +package Remoa.BE.config.jwt; + +public interface JwtProperties { + String SECRET = "remoa"; + int EXPIRATION_TIME = 864000000; //60000 1분 //864000000 10일 + String TOKEN_PREFIX = "Bearer "; + String HEADER_STRING = "Authorization"; +} + +//interface에 있는 것들은 private static이다. +/** + * 인터페이스 내에 정의되는 필드는 자동으로 public static final이 붙는다. + * JWT 의 Signatuer 를 해싱할 때 사용되는 비밀 키이다. 영어로 원하는 단어를 적어주면 된다. + * 토큰의 만료 기간이다. 초단위로 계산된다. 해당 프로젝트에서는 리프레시 토큰을 사용하지 않기 때문에 길게(10일) 설정해줬다. + * 토큰 앞에 붙는 정해진 형식이다. 꼭 Bearer 뒤에 한 칸 공백을 넣어줘야 한다. + * 헤더의 Authorization 이라는 항목에 토큰을 넣어줄 것이다. + */ diff --git a/src/main/java/Remoa/BE/config/jwt/JwtTokenProvider.java b/src/main/java/Remoa/BE/config/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..16dbd65 --- /dev/null +++ b/src/main/java/Remoa/BE/config/jwt/JwtTokenProvider.java @@ -0,0 +1,182 @@ +package Remoa.BE.config.jwt; + +import Remoa.BE.config.auth.MemberDetailsService; +import Remoa.BE.config.auth.RefreshToken; +import Remoa.BE.config.redis.RedisUtils; +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + + +import java.security.Key; +import java.util.Base64; +import java.util.Date; +import java.util.Optional; + +@Component +@Slf4j +public class JwtTokenProvider { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + private Key key; + private final long expirationMinutes; + private final long refreshExpirationMinutes; + private final MemberDetailsService memberDetailsService; + private final RedisUtils redisUtils; + + public JwtTokenProvider(@Value("${security.jwt.token.secret-key}") String secretKey, + @Value("${security.jwt.token.expiration-minutes}") long expirationMinutes, // hours -> minutes + @Value("${security.jwt.token.refresh-expiration-minutes}") long refreshExpirationMinutes, // 추가 + MemberDetailsService memberDetailsService, + RedisUtils redisUtils) { + + byte[] keyBytes = Base64.getDecoder().decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + this.expirationMinutes = expirationMinutes; + this.refreshExpirationMinutes = refreshExpirationMinutes; + this.memberDetailsService = memberDetailsService; + this.redisUtils = redisUtils; + } + + @Transactional(readOnly = true) + public void validateRefreshToken(String refreshToken, String oldAccessToken) { + try { + log.debug("Validating refresh token: {}", refreshToken); + validateToken(refreshToken); + log.debug("Extracting account from old access token: {}", oldAccessToken); + String account = getUserAccountFromOldToken(oldAccessToken); + log.debug("Finding refresh token for account: {}", account); + Optional byAccount = Optional.ofNullable((RefreshToken) redisUtils.get(account)); + + log.debug("Validating refresh token for account: {}", account); + Optional refreshToken1 = byAccount.filter(memberRefreshToken -> memberRefreshToken.validateRefreshToken(refreshToken)); + + log.debug("Checking if refresh token is valid for account: {}", account); + refreshToken1.orElseThrow(() -> new ExpiredJwtException(null, null, "Refresh token expired.")); + } catch (ExpiredJwtException e) { + log.error("ExpiredJwtException: {}", e.getMessage(), e); + throw new BaseException(CustomMessage.REFRESH_TOKEN_EXPIRED); + } catch (Exception e) { + log.error("Unexpected Exception: {}", e.getMessage(), e); + throw new BaseException(CustomMessage.SERVER_ERROR); + } + } + + public String getUserAccountFromOldToken(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } catch (ExpiredJwtException e) { + return e.getClaims().getSubject(); + } + } + + @Transactional + public String recreateAccessToken(String oldAccessToken) { + if (oldAccessToken == null) { + throw new BaseException(CustomMessage.BAD_REQUEST); + } + String account = getUserAccountFromOldToken(oldAccessToken); + return createToken(account); + } + + public String createToken(String account) { //email 받음 + Claims claims = Jwts.claims().setSubject(account); // JWT payload에 저장되는 정보 단위 + claims.put("account", account); // key/ value 쌍으로 저장 + + Date now = new Date(); + Date validity = new Date(now.getTime() + expirationMinutes * 60000); // set Expire Time +// log.info("now: {}", now); +// log.info("validity: {}", validity); + + return Jwts.builder() + .setClaims(claims) // sub 설정 (정보 저장) + .setIssuedAt(now) // 토큰 발행 시간 정보 + .setExpiration(validity) // Set Expire Time + .signWith(key, SignatureAlgorithm.HS256) //서명하는 값은 우리가 임의로 설정 -> YAML파일 + // 사용할 암호화 알고리즘과 signature에 들어갈 secret값 세팅 + .compact(); + } + + public String createRefreshToken(String account) { //email 받음 + Claims claims = Jwts.claims().setSubject(account); // JWT payload에 저장되는 정보 단위 + claims.put("account", account); // key/ value 쌍으로 저장 + + Date now = new Date(); + // 리프레시 토큰의 만료 시간 설정 (액세스 토큰의 만료 시간보다 더 길게 설정) + Date validity = new Date(now.getTime() + refreshExpirationMinutes * 60000); // hours -> milliseconds + + return Jwts.builder() + .setClaims(claims) // sub 설정 (정보 저장) + .setIssuedAt(now) // 토큰 발행 시간 정보 + .setExpiration(validity) // Set Expire Time + .signWith(key, SignatureAlgorithm.HS256) //서명하는 값은 우리가 임의로 설정 -> YAML파일 + // 사용할 암호화 알고리즘과 signature에 들어갈 secret값 세팅 + .compact(); + } + + public Long getSubject(String token) { + return Long.valueOf(Jwts.parserBuilder() + .setSigningKey(key).build() + .parseClaimsJws(token) + .getBody() + .getSubject()); + } + + // Jwt Token에서 account 추출 + public String getUserAccount(String token) { + return Jwts.parserBuilder().setSigningKey(key).build() + .parseClaimsJws(token).getBody().getSubject(); + } + + /* + 인증 성공시 SecurityContextHolder에 저장할 Authentication 객체 생성 + jwt토큰으로부터 이걸 디코딩 할 경우 account를 얻을 수 있다. account를 얻고 + MemberDetailsService에서 account를 통해 MemberDetails객체를 생성. + */ + public Authentication getAuthentication(String token) { + UserDetails userDetails = memberDetailsService.loadUserByUsername(this.getUserAccount(token)); + log.info("Jwt 토큰으로부터 account 얻어 냄"); // 토큰으로부터 MemberDetails를 생성하고 이를 토대로 인증 객체를 리턴 + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + public String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); //헤더 없을 경우 null + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(JwtProperties.TOKEN_PREFIX)) { + return bearerToken.substring(7); + } //토큰만 파싱해서 리턴 + return null; + } + + // Token의 유효성 + 만료 기간 검사 + public void validateToken(String jwtToken) { + Jws claims = Jwts.parserBuilder().setSigningKey(key).build() + .parseClaimsJws(jwtToken); + } + + public Date getAccessTokenExpirationDate(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + return claims.getExpiration(); + } + +} diff --git a/src/main/java/Remoa/BE/config/redis/RedisConfig.java b/src/main/java/Remoa/BE/config/redis/RedisConfig.java new file mode 100644 index 0000000..7b49496 --- /dev/null +++ b/src/main/java/Remoa/BE/config/redis/RedisConfig.java @@ -0,0 +1,175 @@ +package Remoa.BE.config.redis; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.lettuce.core.ClientOptions; +import io.lettuce.core.ReadFrom; +import io.lettuce.core.cluster.ClusterClientOptions; +import io.lettuce.core.cluster.ClusterTopologyRefreshOptions; +import io.lettuce.core.internal.HostAndPort; +import io.lettuce.core.resource.ClientResources; +import io.lettuce.core.resource.DnsResolvers; +import io.lettuce.core.resource.MappingSocketAddressResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachingConfigurer; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.*; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@Slf4j +@EnableCaching +@Configuration +@RequiredArgsConstructor +@EnableAspectJAutoProxy +public class RedisConfig implements CachingConfigurer { + + private final RedisInfo redisInfo; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + List nodes = redisInfo.getNodes(); + String connectIp = redisInfo.getConnectIp(); + int maxRedirects = redisInfo.getMaxRedirects(); + System.out.println("nodes = " + nodes); + System.out.println("connectIp = " + connectIp); + System.out.println("maxRedirects = " + maxRedirects); + + RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(nodes); + + redisClusterConfiguration.setMaxRedirects(redisInfo.getMaxRedirects()); + + ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder() + .enableAllAdaptiveRefreshTriggers() + .enablePeriodicRefresh(Duration.ofHours(1L)) + .build(); + ClientOptions clientOptions = ClusterClientOptions.builder() + .topologyRefreshOptions(clusterTopologyRefreshOptions) + .build(); + + MappingSocketAddressResolver resolver = MappingSocketAddressResolver.create(DnsResolvers.UNRESOLVED, + hostAndPort -> { + HostAndPort andPort = HostAndPort.of(redisInfo.getConnectIp(), hostAndPort.getPort()); + return andPort; + } + ); + + ClientResources clientResources = ClientResources.builder() + .socketAddressResolver(resolver) + .build(); + + LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder() + .commandTimeout(Duration.of(10, ChronoUnit.SECONDS)) + .clientOptions(clientOptions) + .clientResources(clientResources) //<--- 추가 + .readFrom(ReadFrom.REPLICA_PREFERRED) + .build(); + + LettuceConnectionFactory connectionFactory = + new LettuceConnectionFactory(redisClusterConfiguration, clientConfiguration); + + return connectionFactory; + } + + //JSON 직렬화/역직렬화 관련 + private ObjectMapper redisObjectMapper() { + PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator + .builder() + .allowIfSubType(Object.class) + .build(); + + return new ObjectMapper() + .findAndRegisterModules() + .enable(SerializationFeature.INDENT_OUTPUT) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .registerModule(new JavaTimeModule()) + .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL); + } + + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + // JSON 직렬화 설정 + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(redisObjectMapper()); + + + // Key는 String, Value는 ResRegisteredTag로 설정 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(serializer); + + // Hash 데이터 구조를 위한 설정 (필요에 따라 추가) + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(serializer); + + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } + + + @Bean + public RedisCacheConfiguration cacheConfiguration() { + return RedisCacheConfiguration.defaultCacheConfig() + .disableCachingNullValues() + .computePrefixWith(cacheName -> cacheName.concat(":")) + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer(redisObjectMapper()) + )); + } + + @Bean + public CacheManager cacheManager(LettuceConnectionFactory lettuceConnectionFactory) { + return RedisCacheManager.builder(lettuceConnectionFactory) // 자동 주입된 LettuceConnectionFactory 사용 + .cacheDefaults(this.cacheConfiguration()) + .build(); + } + + @Override + public CacheErrorHandler errorHandler() { + return new CacheErrorHandler() { + @Override + public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) { + log.warn(exception.getMessage(), exception); + } + + @Override + public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) { + log.warn(exception.getMessage(), exception); + } + + @Override + public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) { + log.warn(exception.getMessage(), exception); + } + + @Override + public void handleCacheClearError(RuntimeException exception, Cache cache) { + log.warn(exception.getMessage(), exception); + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/redis/RedisInfo.java b/src/main/java/Remoa/BE/config/redis/RedisInfo.java new file mode 100644 index 0000000..9f13184 --- /dev/null +++ b/src/main/java/Remoa/BE/config/redis/RedisInfo.java @@ -0,0 +1,21 @@ +package Remoa.BE.config.redis; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@ConfigurationProperties(prefix = "spring.data.redis.cluster") +@Configuration +public class RedisInfo { + private int maxRedirects; + private String password; + private String connectIp; + private List nodes; +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/redis/RedisUtils.java b/src/main/java/Remoa/BE/config/redis/RedisUtils.java new file mode 100644 index 0000000..4789ea7 --- /dev/null +++ b/src/main/java/Remoa/BE/config/redis/RedisUtils.java @@ -0,0 +1,55 @@ +package Remoa.BE.config.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +public class RedisUtils { + private final RedisTemplate redisTemplate; + private final RedisTemplate redisBlackListTemplate; + + public void set(String key, Object o, int minutes) { + redisTemplate.opsForValue().set(key, o, minutes, TimeUnit.MINUTES); + } + + public Object get(String key) { + return redisTemplate.opsForValue().get(key); + } + + + public void deleteRefreshToken(String account) { + redisTemplate.delete(account); // 해당 계정의 리프레시 토큰을 삭제 + } + public boolean delete(String key) { + return Boolean.TRUE.equals(redisTemplate.delete(key)); + } + + public boolean hasKey(String key) { + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } + + public void setBlackList(String key, Object o, Long milliSeconds) { + redisBlackListTemplate.opsForValue().set(key, o, milliSeconds, TimeUnit.MILLISECONDS); + } + + public Object getBlackList(String key) { + return redisBlackListTemplate.opsForValue().get(key); + } + + public boolean deleteBlackList(String key) { + return Boolean.TRUE.equals(redisBlackListTemplate.delete(key)); + } + + public boolean hasKeyBlackList(String key) { + return Boolean.TRUE.equals(redisBlackListTemplate.hasKey(key)); + } + + public void deleteAll() { + redisTemplate.delete(Objects.requireNonNull(redisTemplate.keys("*"))); + } +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/redis/cache/EvictRedisCache.java b/src/main/java/Remoa/BE/config/redis/cache/EvictRedisCache.java new file mode 100644 index 0000000..5eb661f --- /dev/null +++ b/src/main/java/Remoa/BE/config/redis/cache/EvictRedisCache.java @@ -0,0 +1,12 @@ +package Remoa.BE.config.redis.cache; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface EvictRedisCache { + String cacheName(); +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/config/redis/cache/RedisCacheAspect.java b/src/main/java/Remoa/BE/config/redis/cache/RedisCacheAspect.java new file mode 100644 index 0000000..aba1861 --- /dev/null +++ b/src/main/java/Remoa/BE/config/redis/cache/RedisCacheAspect.java @@ -0,0 +1,156 @@ +package Remoa.BE.config.redis.cache; + +import Remoa.BE.exception.CustomMessage; +import Remoa.BE.exception.response.BaseException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; + +@Component +@Aspect +@RequiredArgsConstructor +@Slf4j +public class RedisCacheAspect { + + private final RedisTemplate redisTemplate; + + @Around("@annotation(RedisCacheable)") + public Object cacheableProcess(ProceedingJoinPoint joinPoint) throws Throwable { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + RedisCacheable redisCacheable = getCacheableAnnotation(method); + + final String cacheKey = generateCacheKey(redisCacheable, method, joinPoint); + + if (redisTemplate.hasKey(cacheKey) && !shouldBypassCache()) { + log.info("Getting data from cache: {}", cacheKey); + return redisTemplate.opsForValue().get(cacheKey); + } + + log.info("Cache miss for key: {}, executing method", cacheKey); + final Object methodReturnValue = joinPoint.proceed(); + final long cacheTTL = redisCacheable.expireTime(); + + if (cacheTTL < 0) { + redisTemplate.opsForValue().set(cacheKey, methodReturnValue); + log.info("Storing data in cache: {}", cacheKey); + } else { + redisTemplate.opsForValue().set(cacheKey, methodReturnValue, cacheTTL, TimeUnit.MINUTES); + log.info("Storing data in cache: {}, TTL: {} minutes", cacheKey, cacheTTL); + } + + return methodReturnValue; + } + + private RedisCacheable getCacheableAnnotation(Method method) { + return AnnotationUtils.getAnnotation(method, RedisCacheable.class); + } + + private String generateCacheKey(RedisCacheable redisCacheable, Method method, ProceedingJoinPoint joinPoint) { + StringBuilder keyBuilder = new StringBuilder(redisCacheable.cacheName()); + + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + Object[] args = joinPoint.getArgs(); + boolean hasRedisCachedKeyParam = false; + + for (int i = 0; i < parameterAnnotations.length; i++) { + for (Annotation annotation : parameterAnnotations[i]) { + if (annotation instanceof RedisCachedKeyParam) { + hasRedisCachedKeyParam = true; + RedisCachedKeyParam keyParam = (RedisCachedKeyParam) annotation; + keyBuilder.append(":").append(keyParam.key()).append("="); + + if (keyParam.fields().length > 0) { + for (String field : keyParam.fields()) { + keyBuilder.append(getFieldValue(args[i], field)).append(":"); + } + // 마지막 콜론 제거 + if (keyBuilder.charAt(keyBuilder.length() - 1) == ':') { + keyBuilder.deleteCharAt(keyBuilder.length() - 1); + } + } else { + // 필드가 지정되지 않은 경우 매개변수 값을 직접 사용 + keyBuilder.append(args[i]); + } + } + } + } + if (!hasRedisCachedKeyParam) { + keyBuilder.append(":").append(method.getName()); + } + return keyBuilder.toString(); + } + + private boolean isComplexObjectType(Class clazz) { + return !clazz.isPrimitive() && !clazz.equals(String.class) && !Number.class.isAssignableFrom(clazz); + } + + private Object getFieldValue(Object obj, String fieldName) { + try { + Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new BaseException(CustomMessage.ANNOTATION_ERROR); + } + } + + private boolean shouldBypassCache() { + return false; // Default implementation + } + + @Around("@annotation(EvictRedisCache)") + public Object evictRedisCache(ProceedingJoinPoint joinPoint) throws Throwable { + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + EvictRedisCache evictRedisCache = method.getAnnotation(EvictRedisCache.class); + + String cacheKey = generateEvictionCacheKey(evictRedisCache, method, joinPoint); + redisTemplate.delete(cacheKey); + log.info("Evicted cache for key: {}", cacheKey); + + return joinPoint.proceed(); + } + + private String generateEvictionCacheKey(EvictRedisCache evictRedisCache, Method method, ProceedingJoinPoint joinPoint) { + StringBuilder keyBuilder = new StringBuilder(evictRedisCache.cacheName()); + + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + Object[] args = joinPoint.getArgs(); + boolean hasRedisCachedKeyParam = false; + + for (int i = 0; i < parameterAnnotations.length; i++) { + for (Annotation annotation : parameterAnnotations[i]) { + if (annotation instanceof RedisCachedKeyParam) { + hasRedisCachedKeyParam = true; + RedisCachedKeyParam keyParam = (RedisCachedKeyParam) annotation; + keyBuilder.append(":").append(keyParam.key()).append("="); + + for (String field : keyParam.fields()) { + keyBuilder.append(getFieldValue(args[i], field)).append(":"); + } + + if (keyBuilder.length() > 0 && keyBuilder.charAt(keyBuilder.length() - 1) == ':') { + keyBuilder.deleteCharAt(keyBuilder.length() - 1); + } + } + } + } + + if (!hasRedisCachedKeyParam) { + keyBuilder.append(":").append(method.getName()); + } + + return keyBuilder.toString(); + } +} diff --git a/src/main/java/Remoa/BE/config/redis/cache/RedisCacheable.java b/src/main/java/Remoa/BE/config/redis/cache/RedisCacheable.java new file mode 100644 index 0000000..b397e23 --- /dev/null +++ b/src/main/java/Remoa/BE/config/redis/cache/RedisCacheable.java @@ -0,0 +1,14 @@ +package Remoa.BE.config.redis.cache; + +import java.lang.annotation.*; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface RedisCacheable { + + String cacheName(); + long expireTime() default -1; + +} diff --git a/src/main/java/Remoa/BE/config/redis/cache/RedisCachedKeyParam.java b/src/main/java/Remoa/BE/config/redis/cache/RedisCachedKeyParam.java new file mode 100644 index 0000000..0a1ead3 --- /dev/null +++ b/src/main/java/Remoa/BE/config/redis/cache/RedisCachedKeyParam.java @@ -0,0 +1,14 @@ +package Remoa.BE.config.redis.cache; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface RedisCachedKeyParam { + String key(); + String[] fields() default {}; +} + diff --git a/src/main/java/Remoa/BE/exception/CustomBody.java b/src/main/java/Remoa/BE/exception/CustomBody.java index 03dd4be..58e9e4b 100644 --- a/src/main/java/Remoa/BE/exception/CustomBody.java +++ b/src/main/java/Remoa/BE/exception/CustomBody.java @@ -5,6 +5,7 @@ import Remoa.BE.exception.response.SuccessResponse; import lombok.Getter; import lombok.Setter; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @Getter @@ -16,14 +17,16 @@ public class CustomBody { get 메소드는 data에 프론트에게 필요한 데이터를 전달 post 메소드는 data에 저장된 값을 전달 */ - public static ResponseEntity successResponse(CustomMessage customMessage, Object data) { + public static ResponseEntity successResponse(CustomMessage customMessage, Object data) { + SuccessResponse response = SuccessResponse.builder() + .message(customMessage.getMessage()) + .detail(customMessage.getDetail()) + .data(data) + .build(); return ResponseEntity .status(customMessage.getHttpStatus()) - .body(SuccessResponse.builder(). - message(customMessage.getMessage()). - detail(customMessage.getDetail()). - data(data).build()); + .body(response); } /** @@ -34,6 +37,7 @@ public static ResponseEntity errorResponse(CustomMessage customMessage) return ResponseEntity .status(customMessage.getHttpStatus()) + .contentType(MediaType.APPLICATION_JSON) .body(ErrorResponse.builder(). message(customMessage.getMessage()). detail(customMessage.getDetail()) @@ -48,6 +52,7 @@ public static ResponseEntity failResponse(CustomMessage customMessage, O return ResponseEntity .status(customMessage.getHttpStatus()) + .contentType(MediaType.APPLICATION_JSON) .body(FailResponse.builder(). message(customMessage.getMessage()). detail(customMessage.getDetail()). diff --git a/src/main/java/Remoa/BE/exception/CustomMessage.java b/src/main/java/Remoa/BE/exception/CustomMessage.java index e1da644..4acb0a2 100644 --- a/src/main/java/Remoa/BE/exception/CustomMessage.java +++ b/src/main/java/Remoa/BE/exception/CustomMessage.java @@ -2,30 +2,77 @@ import lombok.Getter; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; @Getter public enum CustomMessage { //200 정상처리 상태코드 - OK(HttpStatus.OK,"올바른 요청","정상적으로 처리되었습니다"), + OK(HttpStatus.OK, "올바른 요청", "정상적으로 처리되었습니다"), - OK_UNFOLLOW(HttpStatus.OK,"올바른 요청","회원을 언팔로잉 합니다"), + OK_DUPLICATE(HttpStatus.OK, "올바른 요청", "닉네임이 중복되었습니다"), + + OK_UN_DUPLICATE(HttpStatus.OK, "올바른 요청", "사용가능한 닉네임 입니다"), + + OK_UNFOLLOW(HttpStatus.OK, "올바른 요청", "회원을 언팔로잉 합니다"), + + OK_SCRAP(HttpStatus.OK, "올바른 요청", "정상적으로 스크랩 되었습니다."), //201 한 api에서 정상처리상 구분이 필요할떄 사용 - OK_FOLLOW(HttpStatus.CREATED,"올바른 요청","회원을 팔로잉 합니다"), + OK_FOLLOW(HttpStatus.CREATED, "올바른 요청", "회원을 팔로잉 합니다"), + + OK_UNSCRAP(HttpStatus.CREATED, "올바른 요청", "정상적으로 스크랩이 해제됐습니다."), - OK_SIGNUP(HttpStatus.CREATED,"올바른 요청","회원가입하는 회원입니다"), + OK_SIGNUP(HttpStatus.CREATED, "올바른 요청", "회원가입하는 회원입니다"), //400 잘못된 요청 - VALIDATED(HttpStatus.BAD_REQUEST,"잘못된 요청","요청한 값이 유효성검사를 통과하지 못했습니다"), - NO_MEMBER(HttpStatus.BAD_REQUEST,"잘못된 요청","요청한 memberId가 존재하지 않습니다"), + VALIDATED(HttpStatus.BAD_REQUEST, "잘못된 요청", "요청한 값이 유효성검사를 통과하지 못했습니다"), + NO_ID(HttpStatus.BAD_REQUEST, "잘못된 요청", "요청한 Id가 존재하지 않습니다"), + NO_CATEGORY(HttpStatus.BAD_REQUEST, "잘못된 요청", "요청한 카테고리가 존재하지 않습니다"), + BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청", "잘 못된 요청입니다"), + + SELF_FOLLOW(HttpStatus.BAD_REQUEST, "서비스 로직상 오류", "자신을 팔로우할 수 없습니다"), + + SELF_LIKE(HttpStatus.BAD_REQUEST, "서비스 로직상 오류", "자신의 게시물을 좋아요할 수 없습니다"), + + SELF_SCRAP(HttpStatus.BAD_REQUEST, "서비스 로직상 오류", "자신을 게시물을 스크랩할 수 없습니다"), + + BAD_DUPLICATE(HttpStatus.BAD_REQUEST, "서비스 로직상 오류", "닉네임이 중복되었습니다"), + + BAD_PROFILE_IMG(HttpStatus.BAD_REQUEST, "서비스 로직상 오류", "해당 멤버의 프로필사진이 존재하지 않습니다"), + + BAD_FILE(HttpStatus.BAD_REQUEST, "서비스 로직상 오류", "해당 파일은 지원하지 않습니다."), + + BAD_PAGE_NUM(HttpStatus.BAD_REQUEST, "피드백 등록시 페이지 넘버 오류", "존재하지 않는 페이지에 피드백을 등록하려 합니다"), + PAGE_FEEDBACK_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "피드백 등록시 페이지 중복 등록 오류", "해당 페이지에 이미 피드백이 존재합니다"), + POST_MEMBER_FEEDBACK_NOT_EXIST(HttpStatus.BAD_REQUEST, "해당 멤버는 피드백 없음", "해당 멤버는 피드백 해당 포스트에 피드백을 달지 않았습니다"), + + PAGE_NUM_OVER(HttpStatus.BAD_REQUEST, "레퍼런스 조회시 페이지 넘버 오류", "올바르지 않는 페이지 번호입니다."), + + FILE_SIZE_OVER(HttpStatus.BAD_REQUEST, "파일 업/다운로드 오류", "파일 사이즈가 최대 허용 크기보다 큽니다."), + IMAGE_PIXEL_LACK(HttpStatus.BAD_REQUEST, "파일 업/다운로드 오류", "이미지 픽셀이 최소 규격에 미달합니다."), + INVALID_FILE_LENGTH(HttpStatus.BAD_REQUEST, "파일 길이 초과", "업로드하려는 파일의 길이가 너무 깁니다."), + INVALID_CONTENT_LENGTH(HttpStatus.BAD_REQUEST, "댓글 길이 초과", "등록하려는 문자열의 길이가 너무 깁니다."), + EMPTY_CONTENT(HttpStatus.BAD_REQUEST, "빈 문자 또는 null 등록", "빈 문자 또는 null 등록할 수 없습니다."), + CANNOT_REISSUE_TOKEN(HttpStatus.BAD_REQUEST, "토큰 재발급 실패", "토큰 재발급 실패하였습니다"), + //401권한오류 - UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"권한이 없습니다","인증에 필요한 쿠키 정보가 없습니다"), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "권한이 없습니다", "인증에 필요한 쿠키 정보가 없습니다"), + NOT_VALID_TOKEN(HttpStatus.UNAUTHORIZED, "권한이 없습니다", "토큰이 유효하지 않습니다."), + NO_TOKEN_FOUND(HttpStatus.UNAUTHORIZED, "권한이 없습니다", "토큰이 없습니다."), + EXPIRED_TOKEN_NOT_EXIST(HttpStatus.UNAUTHORIZED, "권한이 없습니다", "만료 토큰이 없습니다."), + TOKEN_EXPIRED(HttpStatus.LOCKED, "엑세스 토큰 만료", "토큰이 만료되었습니다."), + REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "리프레시 토큰 만료", "리프레시 토큰이 만료되었습니다."), + REFRESH_TOKEN_NOT_EXIST(HttpStatus.BAD_REQUEST, "리프레시 토큰 없음", "리프레시 토큰이 존재하지 않습니다."), + + + // 403 권한오류 + CAN_NOT_ACCESS(HttpStatus.FORBIDDEN, "권한이 없습니다", "다른 사람이 작성한 글에 접근할 수 없습니다"), + FORBIDDEN(HttpStatus.FORBIDDEN, "권한이 없습니다", "ADMIN 계정만 접근이 가능합니다"), - //409 상태 충돌 - FOLLOW_ME(HttpStatus.CONFLICT,"서비스 로직상 오류","자신을 팔로우할 수 없습니다"); + // 500번대 + SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류", "서버에서 오류가 발생했습니다."), + ANNOTATION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "애노테이션 에러", "애노테이션 에러입니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/Remoa/BE/exception/CustomizedExceptionHandler.java b/src/main/java/Remoa/BE/exception/CustomizedExceptionHandler.java index 922f7f7..0fb3343 100644 --- a/src/main/java/Remoa/BE/exception/CustomizedExceptionHandler.java +++ b/src/main/java/Remoa/BE/exception/CustomizedExceptionHandler.java @@ -1,13 +1,17 @@ package Remoa.BE.exception; -import Remoa.BE.exception.response.FailResponse; +import Remoa.BE.exception.response.BaseException; +import Remoa.BE.exception.response.BaseResponse; +import Remoa.BE.exception.response.ErrorResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.server.ResponseStatusException; +import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -21,6 +25,13 @@ public class CustomizedExceptionHandler { /** 유효성 검사 에러 처리 */ + @ExceptionHandler(value = {BaseException.class}) + protected ResponseEntity handleBaseException(BaseException e) { + return ResponseEntity.status(e.customMessage.getHttpStatus()) + .contentType(MediaType.APPLICATION_JSON) + .body(new ErrorResponse(e.customMessage.getMessage(), e.customMessage.getDetail())); + } + @ExceptionHandler public ResponseEntity methodValidException(MethodArgumentNotValidException ex){ @@ -31,6 +42,12 @@ public ResponseEntity methodValidException(MethodArgumentNotValidExcepti @ExceptionHandler public ResponseEntity responseStatusException(ResponseStatusException ex){ - return errorResponse(CustomMessage.NO_MEMBER); + return errorResponse(CustomMessage.NO_ID); + } + + //파일 유형 에러 + @ExceptionHandler + public ResponseEntity ioException(IOException ex){ + return errorResponse(CustomMessage.BAD_FILE); } } diff --git a/src/main/java/Remoa/BE/exception/response/BaseException.java b/src/main/java/Remoa/BE/exception/response/BaseException.java new file mode 100644 index 0000000..ab7f6c1 --- /dev/null +++ b/src/main/java/Remoa/BE/exception/response/BaseException.java @@ -0,0 +1,9 @@ +package Remoa.BE.exception.response; + +import Remoa.BE.exception.CustomMessage; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class BaseException extends RuntimeException{ + public final CustomMessage customMessage; +} diff --git a/src/main/java/Remoa/BE/exception/response/BaseResponse.java b/src/main/java/Remoa/BE/exception/response/BaseResponse.java new file mode 100644 index 0000000..98ba58c --- /dev/null +++ b/src/main/java/Remoa/BE/exception/response/BaseResponse.java @@ -0,0 +1,37 @@ +package Remoa.BE.exception.response; + +import Remoa.BE.exception.CustomMessage; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.MappedSuperclass; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter + +public class BaseResponse { + + @Schema(description = "전달 메시지") + private String message; + + @Schema(description = "상세 설명") + private String detail; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @Schema(description = "전달 데이터") + private T data; + + public BaseResponse(CustomMessage customMessage, T data){ + this.message = customMessage.getMessage(); + this.detail = customMessage.getDetail(); + this.data = data; + } + /** + * @JsonInclude(JsonInclude.Include.NON_NULL) 어노테이션은 Jackson 라이브러리에서 사용되며, + * JSON 직렬화 시에 data 필드가 null인 경우 JSON에서 생략되도록 설정합니다. + * 이렇게 함으로써, 클라이언트에게 null 값이 포함된 데이터를 전달하지 않아도 됩니다. + */ + +} \ No newline at end of file diff --git a/src/main/java/Remoa/BE/exception/response/ErrorResponse.java b/src/main/java/Remoa/BE/exception/response/ErrorResponse.java index 8d4988d..bb88f2f 100644 --- a/src/main/java/Remoa/BE/exception/response/ErrorResponse.java +++ b/src/main/java/Remoa/BE/exception/response/ErrorResponse.java @@ -1,5 +1,7 @@ package Remoa.BE.exception.response; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.Setter; @@ -11,9 +13,12 @@ @Builder @Getter @Setter +@AllArgsConstructor public class ErrorResponse { - + @Schema(description = "전달 메시지", example = "에러 메시지") private String message; + + @Schema(description = "디테일 설명", example = "이러이러한 이유로 안 됨") private String detail; } diff --git a/src/main/java/Remoa/BE/exception/response/SuccessResponse.java b/src/main/java/Remoa/BE/exception/response/SuccessResponse.java index b24e551..3852645 100644 --- a/src/main/java/Remoa/BE/exception/response/SuccessResponse.java +++ b/src/main/java/Remoa/BE/exception/response/SuccessResponse.java @@ -1,5 +1,6 @@ package Remoa.BE.exception.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; import lombok.Setter; @@ -12,9 +13,15 @@ @Getter @Setter @Builder -public class SuccessResponse { +public class SuccessResponse{ + + @Schema(description = "전달 메시지", example = "올바른 요청") private String message; + + @Schema(description = "상세 설명", example = "이러이러한 이유입니다") private String detail; + + @Schema(description = "전달 데이터") private Object data; } diff --git a/src/main/java/Remoa/BE/utill/CommonFunction.java b/src/main/java/Remoa/BE/utill/CommonFunction.java new file mode 100644 index 0000000..a4b3fd0 --- /dev/null +++ b/src/main/java/Remoa/BE/utill/CommonFunction.java @@ -0,0 +1,24 @@ +package Remoa.BE.utill; + +public final class CommonFunction { + + + public CommonFunction() { + throw new AssertionError(); + } + + + public static Long getCategoryId(String name){ + Long catagoryId = (long) 0; // 0 은 all 로 인식 + + if("idea".equals(name)){catagoryId = (long)1;} + if("marketing".equals(name)){catagoryId = (long)2;} + if("design".equals(name)){catagoryId = (long)3;} + if("video".equals(name)){catagoryId = (long)4;} + if("digital".equals(name)){catagoryId = (long)5;} + if("etc".equals(name)){catagoryId = (long)6;} + + return catagoryId; + } + +} diff --git a/src/main/java/Remoa/BE/utill/Constant.java b/src/main/java/Remoa/BE/utill/Constant.java new file mode 100644 index 0000000..f652a6d --- /dev/null +++ b/src/main/java/Remoa/BE/utill/Constant.java @@ -0,0 +1,8 @@ +package Remoa.BE.utill; + +public class Constant { + + public static final int CONTENT_PAGE_SIZE = 5; + + public static final int HOME_PAGE_SIZE = 20; +} diff --git a/src/main/java/Remoa/BE/utill/FileExtension.java b/src/main/java/Remoa/BE/utill/FileExtension.java new file mode 100644 index 0000000..ffcc1df --- /dev/null +++ b/src/main/java/Remoa/BE/utill/FileExtension.java @@ -0,0 +1,13 @@ +package Remoa.BE.utill; + +import org.springframework.web.multipart.MultipartFile; + +public class FileExtension { + public static String fileExtension(MultipartFile multipartFile){ + String fileName = multipartFile.getOriginalFilename(); + assert fileName != null; + int lastIndex = fileName.lastIndexOf("."); + return fileName.substring(lastIndex + 1); + } + +} diff --git a/src/main/java/Remoa/BE/utill/MemberInfo.java b/src/main/java/Remoa/BE/utill/MemberInfo.java index b04ff16..4cae0ef 100644 --- a/src/main/java/Remoa/BE/utill/MemberInfo.java +++ b/src/main/java/Remoa/BE/utill/MemberInfo.java @@ -1,23 +1,19 @@ package Remoa.BE.utill; -import Remoa.BE.Member.Domain.Member; +import Remoa.BE.Web.Member.Domain.Member; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.web.context.HttpSessionSecurityContextRepository; -import org.springframework.web.bind.annotation.GetMapping; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; import java.util.ArrayList; import java.util.List; -import java.util.Optional; +@Slf4j public class MemberInfo { - public static Long getMemberId() { + /* public static Long getMemberId() { Member member = (Member) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return member.getMemberId(); } @@ -27,13 +23,13 @@ public static boolean authorized(HttpServletRequest request){ HttpSession session = request.getSession(false); return session != null && context.getAuthentication() != null; } - +*/ /** * Spring Security가 기본값으로 form data를 사용해 로그인을 진행하는데, Rest API를 이용해 json을 주고받는 방식으로 로그인을 처리하기 위해 * 우회적인 방식으로 Spring Security를 이용할 수 있게 해주는 메서드. */ - public static void securityLoginWithoutLoginForm(Member member, HttpServletRequest request) { + public static void securityLoginWithoutLoginForm(Member member) { //로그인 세션에 들어갈 권한을 설정합니다. List list = new ArrayList<>(); diff --git a/src/main/java/Remoa/BE/utill/MessageUtils.java b/src/main/java/Remoa/BE/utill/MessageUtils.java new file mode 100644 index 0000000..f90760b --- /dev/null +++ b/src/main/java/Remoa/BE/utill/MessageUtils.java @@ -0,0 +1,13 @@ +package Remoa.BE.utill; + +public class MessageUtils { + + public static final String SUCCESS = "요청이 성공적으로 처리되었습니다."; + public static final String ERROR = "요청 처리 중 오류가 발생하였습니다."; + public static final String NOT_FOUND = "요청한 리소스를 찾을 수 없습니다."; + public static final String UNAUTHORIZED = "인증 필요합니다. 토큰을 제공해주세요"; + public static final String FORBIDDEN = "권한이 없습니다. ADMIN 계정만 접근 가능합니다 "; + public static final String BAD_REQUEST = "잘못된 요청입니다."; + // 추가적인 메시지 정의 가능 +} + diff --git a/src/main/resources/application-deploy.yml b/src/main/resources/application-deploy.yml new file mode 100644 index 0000000..a6ef147 --- /dev/null +++ b/src/main/resources/application-deploy.yml @@ -0,0 +1,78 @@ +server: + port: 8080 + +spring: + config: + activate: + on-profile: deploy + + datasource: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driver-class-name: ${SPRING_DATASOURCE_DRIVER} + + jpa: + hibernate: + ddl-auto: update #create update none + properties: + hibernate: + default_batch_fetch_size: 100 + #show-sql: true + format_sql: true + dialect: org.hibernate.dialect.MySQLDialect + data: + redis: + cluster: + max-redirects: 3 + #password: 1111 + connect-ip: ${REDIS_CLUSTER_IP} + nodes: ${REDIS_CLUSTER_NODES} + servlet: + multipart: + maxFileSize: 30MB # 파일 하나의 최대 크기 + maxRequestSize: 30MB # 한 번에 최대 업로드 가능 용량 + +security: + jwt: + token: + secret-key: "remoaremoaremoaremoaremoaremoaremoaremoaremoaremoaremoaremoaremoaremoaremoaremoaremoaremoa" + expiration-minutes: 30 + refresh-expiration-minutes: 10080 + +springdoc: + version: '@project.version@' + api-docs: + path: /api-docs + default-consumes-media-type: application/json + default-produces-media-type: application/json + swagger-ui: + groups-order: DESC + operations-sorter: alpha + tags-sorter: alpha + path: /swagger-ui.html + disable-swagger-default-url: true + display-query-params-without-oauth2: true + doc-expansion: none # doc-expansion: Swagger UI에서 문서 확장을 비활성화한다. + paths-to-match: # 해당 패턴에 매칭되는 controller만 swagger-ui에 노출한다. + - /** # 전체 노출 + +cloud: + aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + region: + static: ap-northeast-2 + s3: + bucket: remoa + stack: + auto: false + + +logging: + level: + com: + amazonaws: + util: + EC2MetadataUtils: ERROR \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..d963877 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,4 @@ +spring: + profiles: + active: + - local diff --git a/src/test/java/Remoa/BE/Firebase/FirebaseTest.java b/src/test/java/Remoa/BE/Firebase/FirebaseTest.java index d277a5a..cca0aad 100644 --- a/src/test/java/Remoa/BE/Firebase/FirebaseTest.java +++ b/src/test/java/Remoa/BE/Firebase/FirebaseTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import javax.annotation.Resource; +import jakarta.annotation.Resource; @SpringBootTest public class FirebaseTest { diff --git a/src/test/java/Remoa/BE/Web/Comment/Repository/CommentRepositoryCustomImplTest.java b/src/test/java/Remoa/BE/Web/Comment/Repository/CommentRepositoryCustomImplTest.java new file mode 100644 index 0000000..50ad0c3 --- /dev/null +++ b/src/test/java/Remoa/BE/Web/Comment/Repository/CommentRepositoryCustomImplTest.java @@ -0,0 +1,29 @@ +package Remoa.BE.Web.Comment.Repository; + +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Member.Service.MemberService; +import jdk.jfr.StackTrace; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.jupiter.api.Assertions.*; + + +@SpringBootTest +@Transactional +class CommentRepositoryCustomImplTest { + + @Autowired + CommentRepository commentRepository; + + @Autowired + MemberService memberService; + + @Test + void sqlTest() { + Member one = memberService.findOne(1L); + // commentRepository.findMyComment(one) + } +} \ No newline at end of file diff --git a/src/test/java/Remoa/BE/Web/Post/Repository/PostRepositoryTest.java b/src/test/java/Remoa/BE/Web/Post/Repository/PostRepositoryTest.java new file mode 100644 index 0000000..677c136 --- /dev/null +++ b/src/test/java/Remoa/BE/Web/Post/Repository/PostRepositoryTest.java @@ -0,0 +1,24 @@ +package Remoa.BE.Web.Post.Repository; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + + +@SpringBootTest +@Transactional +class PostRepositoryTest { + + @Autowired + PostRepository postRepository; + + @Test + void QueryTest() { + assertThat( postRepository.existsById(1L)).isFalse(); + } + +} \ No newline at end of file diff --git a/src/test/java/Remoa/BE/category/CategoryTest.java b/src/test/java/Remoa/BE/category/CategoryTest.java index 0065eb7..5e83927 100644 --- a/src/test/java/Remoa/BE/category/CategoryTest.java +++ b/src/test/java/Remoa/BE/category/CategoryTest.java @@ -1,11 +1,11 @@ package Remoa.BE.category; -import Remoa.BE.Member.Repository.MemberRepository; -import Remoa.BE.Post.Domain.Category; -import Remoa.BE.Member.Domain.Member; -import Remoa.BE.Post.Service.CategoryService; -import Remoa.BE.Member.Service.MemberService; -import org.junit.Test; +import Remoa.BE.Web.Member.Repository.MemberRepository; +import Remoa.BE.Web.Post.Domain.Category; +import Remoa.BE.Web.Member.Domain.Member; +import Remoa.BE.Web.Post.Service.CategoryService; +import Remoa.BE.Web.Member.Service.MemberService; +import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -47,7 +47,7 @@ public class CategoryTest { //email로 testMember를 찾은 후 category 찾아오기 - Member findMember = MemberRepository.findByEmail("tester@test.com").get(); + Member findMember = MemberRepository.findByAccount("tester@test.com").get(); List findMemberCategories = categoryService.findMemberCategory(findMember); //then @@ -56,7 +56,7 @@ public class CategoryTest { private Member createMember() { Member member = new Member(); - member.setEmail("tester@test.com"); + member.setAccount("tester@test.com"); member.setName("test"); member.setPassword("test"); member.setBirth("20101010"); diff --git a/src/test/java/Remoa/BE/withdrew/WithdrewTest.java b/src/test/java/Remoa/BE/withdrew/WithdrewTest.java index 3d76598..1f9e987 100644 --- a/src/test/java/Remoa/BE/withdrew/WithdrewTest.java +++ b/src/test/java/Remoa/BE/withdrew/WithdrewTest.java @@ -15,7 +15,7 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.transaction.annotation.Transactional; -import javax.persistence.EntityManager; +import jakarta.persistence.EntityManager; import java.util.List;