diff --git a/src/main/java/org/example/tackit/domain/admin/controller/AdminPostController.java b/src/main/java/org/example/tackit/domain/admin/controller/AdminPostController.java index a3dcae72..ec05ab15 100644 --- a/src/main/java/org/example/tackit/domain/admin/controller/AdminPostController.java +++ b/src/main/java/org/example/tackit/domain/admin/controller/AdminPostController.java @@ -1,68 +1,68 @@ -package org.example.tackit.domain.admin.controller; - -import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.admin.dto.ReportedPostDTO; -import org.example.tackit.domain.admin.service.ReportedPostService; -import org.example.tackit.domain.entity.Post; -import org.example.tackit.common.dto.PageResponseDTO; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PageableDefault; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -@RestController -@RequestMapping("/api/admin/report") -@RequiredArgsConstructor -public class AdminPostController { - private final Map reportedPostServices; - - // 신고된 게시글 조회 - @GetMapping("/{postType}/posts") - public ResponseEntity> getReportedPosts( - @PathVariable("postType") Post postType, - @PageableDefault(size = 5, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { - ReportedPostService service = reportedPostServices.get(postType); - - if (service == null) { - throw new IllegalArgumentException("지원하지 않는 게시글 유형입니다: " + postType); - } - - Page posts = service.getDeletedPosts(pageable); - return ResponseEntity.ok(PageResponseDTO.from(posts)); - } - - // 게시글 완전 삭제 - @DeleteMapping("/{postType}/posts/{postId}") - public ResponseEntity deleteReportedPost( - @PathVariable("postType") Post postType, - @PathVariable("postId") Long postId) { - - ReportedPostService service = reportedPostServices.get(postType); - if (service == null) { - throw new IllegalArgumentException("지원하지 않는 게시글 유형입니다: " + postType); - } - - service.deletePost(postId); - return ResponseEntity.noContent().build(); - } - - // 게시글 활성화 - @PatchMapping("/{postType}/posts/{postId}/activate") - public ResponseEntity activateReportedPost( - @PathVariable("postType") Post postType, - @PathVariable("postId") Long postId) { - - ReportedPostService service = reportedPostServices.get(postType); - if (service == null) { - throw new IllegalArgumentException("지원하지 않는 게시글 유형입니다: " + postType); - } - - service.activatePost(postId); - return ResponseEntity.ok("게시글이 활성화되었습니다."); - } - -} +//package org.example.tackit.domain.admin.controller; +// +//import lombok.RequiredArgsConstructor; +//import org.example.tackit.domain.admin.dto.ReportedPostDTO; +//import org.example.tackit.domain.admin.service.ReportedPostService; +//import org.example.tackit.domain.entity.postType; +//import org.example.tackit.common.dto.PageResponseDTO; +//import org.springframework.data.domain.Page; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.domain.Sort; +//import org.springframework.data.web.PageableDefault; +//import org.springframework.http.ResponseEntity; +//import org.springframework.web.bind.annotation.*; +// +//import java.util.Map; +// +//@RestController +//@RequestMapping("/api/admin/report") +//@RequiredArgsConstructor +//public class AdminPostController { +// private final Map reportedPostServices; +// +// // 신고된 게시글 조회 +// @GetMapping("/{postType}/posts") +// public ResponseEntity> getReportedPosts( +// @PathVariable("postType") Post postType, +// @PageableDefault(size = 5, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { +// ReportedPostService service = reportedPostServices.get(postType); +// +// if (service == null) { +// throw new IllegalArgumentException("지원하지 않는 게시글 유형입니다: " + postType); +// } +// +// Page posts = service.getDeletedPosts(pageable); +// return ResponseEntity.ok(PageResponseDTO.from(posts)); +// } +// +// // 게시글 완전 삭제 +// @DeleteMapping("/{postType}/posts/{postId}") +// public ResponseEntity deleteReportedPost( +// @PathVariable("postType") Post postType, +// @PathVariable("postId") Long postId) { +// +// ReportedPostService service = reportedPostServices.get(postType); +// if (service == null) { +// throw new IllegalArgumentException("지원하지 않는 게시글 유형입니다: " + postType); +// } +// +// service.deletePost(postId); +// return ResponseEntity.noContent().build(); +// } +// +// // 게시글 활성화 +// @PatchMapping("/{postType}/posts/{postId}/activate") +// public ResponseEntity activateReportedPost( +// @PathVariable("postType") Post postType, +// @PathVariable("postId") Long postId) { +// +// ReportedPostService service = reportedPostServices.get(postType); +// if (service == null) { +// throw new IllegalArgumentException("지원하지 않는 게시글 유형입니다: " + postType); +// } +// +// service.activatePost(postId); +// return ResponseEntity.ok("게시글이 활성화되었습니다."); +// } +// +//} diff --git a/src/main/java/org/example/tackit/domain/auth/login/service/AuthService.java b/src/main/java/org/example/tackit/domain/auth/login/service/AuthService.java index e2b583c0..ee42dd56 100644 --- a/src/main/java/org/example/tackit/domain/auth/login/service/AuthService.java +++ b/src/main/java/org/example/tackit/domain/auth/login/service/AuthService.java @@ -25,6 +25,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -82,51 +83,58 @@ public TokenDto signIn(SignInDto signInDto) { @Transactional public SignInResponse signIn(SignInDto signInDto) { - // 인증 토큰 생성 + UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(signInDto.getEmail(), signInDto.getPassword()); - - try { - log.info("로그인 시도: {}", signInDto.getEmail()); - Authentication authentication = authenticationManager.authenticate(authenticationToken); - log.info("로그인 성공: {}", authentication.getName()); - - TokenDto tokenDto = tokenProvider.generateTokenDto(authentication); - redisUtil.save(signInDto.getEmail(), tokenDto.getRefreshToken()); - - // 멀티 프로필 목록 조회 - List memberOrgs = memberOrgRepository.findAllByMemberEmail(signInDto.getEmail()); - - List profiles = memberOrgs.stream() - .map(org -> { - MultiProfileDto.MultiProfileDtoBuilder builder = MultiProfileDto.builder() - .memberOrgId(org.getId()) - .orgName(org.getOrganization().getName()) - .nickname(org.getNickname()) - .profileImage(org.getProfileImageUrl()) - .orgType(org.getOrgType().name()) - .memberRole(org.getMemberRole().name()) - .memberType(org.getMemberType().name()); - - if (OrgType.CLUB.name().equals(org.getOrgType().name()) - && org.getOrganization().getUniversity() != null) { - builder.universityName(org.getOrganization().getUniversity().getUniversityName()); - } - else { - builder.universityName(null); - } - - return builder.build(); - }).collect(Collectors.toList()); - - return new SignInResponse( - tokenDto, - profiles - ); - } catch (Exception e) { - log.error("로그인 실패: {}", signInDto.getEmail(), e); - throw e; - } + new UsernamePasswordAuthenticationToken( + signInDto.getEmail(), + signInDto.getPassword() + ); + + Authentication authentication = authenticationManager.authenticate(authenticationToken); + + return buildSignInResponse(signInDto.getEmail(), authentication); + } + + // 프로필 전환 + @Transactional + public SignInResponse switchProfile() { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + + return buildSignInResponse(email, authentication); + } + + // 로그인 응답 객체를 만드는 메서드 + public SignInResponse buildSignInResponse(String email, Authentication authentication) { + + TokenDto tokenDto = tokenProvider.generateTokenDto(authentication); + redisUtil.save(email, tokenDto.getRefreshToken()); + + List memberOrgs = memberOrgRepository.findAllByMemberEmail(email); + + List profiles = memberOrgs.stream() + .map(org -> { + MultiProfileDto.MultiProfileDtoBuilder builder = MultiProfileDto.builder() + .memberOrgId(org.getId()) + .orgName(org.getOrganization().getName()) + .nickname(org.getNickname()) + .profileImage(org.getProfileImageUrl()) + .orgType(org.getOrgType().name()) + .memberRole(org.getMemberRole().name()) + .memberType(org.getMemberType().name()); + + if (OrgType.CLUB.name().equals(org.getOrgType().name()) + && org.getOrganization().getUniversity() != null) { + builder.universityName(org.getOrganization().getUniversity().getUniversityName()); + } else { + builder.universityName(null); + } + + return builder.build(); + }).toList(); + + return new SignInResponse(tokenDto, profiles); } // Bearer 제거 및 형식 검증 @@ -254,7 +262,7 @@ public void resetPassword(String authorizationHeader, String newPassword) { .orElseThrow(() -> new UsernameNotFoundException(email + " not found")); String encodedNewPassword = passwordEncoder.encode(newPassword); - member.changePassword(encodedNewPassword); + member.updatePassword(encodedNewPassword); // 6. Redis에서 토큰 삭제 redisUtil.delete("reset:" + email); diff --git a/src/main/java/org/example/tackit/domain/entity/Member.java b/src/main/java/org/example/tackit/domain/entity/Member.java index f8d77655..eb687f28 100644 --- a/src/main/java/org/example/tackit/domain/entity/Member.java +++ b/src/main/java/org/example/tackit/domain/entity/Member.java @@ -57,7 +57,7 @@ public void updateNickname(String newNickname) { */ // 비밀번호 변경 책임은 Member 도메인 내부에 분리 - public void changePassword(String encodedNewPassword) { + public void updatePassword(String encodedNewPassword) { this.password = encodedNewPassword; } diff --git a/src/main/java/org/example/tackit/domain/entity/Post.java b/src/main/java/org/example/tackit/domain/entity/Post.java deleted file mode 100644 index 04642394..00000000 --- a/src/main/java/org/example/tackit/domain/entity/Post.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.example.tackit.domain.entity; - -public enum Post { - Free, QnA, Tip, Notice -} diff --git a/src/main/java/org/example/tackit/domain/entity/Report.java b/src/main/java/org/example/tackit/domain/entity/Report.java index 4be1efdf..cc259529 100644 --- a/src/main/java/org/example/tackit/domain/entity/Report.java +++ b/src/main/java/org/example/tackit/domain/entity/Report.java @@ -14,20 +14,17 @@ import jakarta.persistence.Table; import java.time.LocalDateTime; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.example.tackit.domain.entity.org.MemberOrg; -import org.example.tackit.domain.report.dto.ReportRequestDto; +import org.example.tackit.domain.entity.post.PostType; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Getter -@Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor @EntityListeners(AuditingEntityListener.class) @Table(name = "report") public class Report { @@ -46,6 +43,7 @@ public class Report { @JoinColumn(name = "targetMember_id", nullable = false) private MemberOrg targetMember; + // POST 혹은 COMMENT의 id @Column(nullable = false) private Long targetId; @@ -54,12 +52,29 @@ public class Report { @Column(nullable = false) private TargetType targetType; + // 타겟 대상 postId comment면 comment가 달린 post의 id, post는 본인. + @Column(nullable = false) + private Long postId; + + // 게시판 + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 50) + private PostType postType; + + // 신고 당시 게시글 제목 + @Column(nullable = false, length = 100) + private String reportedPostTitle; + + // 신고 당시 타겟 게시글/댓글 내용 + @Column(columnDefinition = "LONGTEXT", nullable = false) + private String reportedContent; + @Enumerated(EnumType.STRING) @Column(nullable = false) private ReportReason reportReason; @Column(columnDefinition = "TEXT") - private String customReason; + private String detailReason; @CreatedDate private LocalDateTime reportedAt; @@ -67,17 +82,21 @@ public class Report { @Enumerated(EnumType.STRING) private ActiveStatus activeStatus; - //TODO Entity 안에 DTO를 참조하는 메소드가 있는 것이 이상함. 분리해야할듯 - public static Report from(ReportRequestDto dto, MemberOrg reporter, MemberOrg targetMember) { - return Report.builder() - .reporter(reporter) - .targetMember(targetMember) - .targetId(dto.getTargetId()) - .targetType(dto.getTargetType()) - .reportReason(dto.getReason()) - .activeStatus(ActiveStatus.ACTIVE) // 초기값 설정 - .build(); + @Builder + public Report(MemberOrg reporter, MemberOrg targetMember, Long targetId, TargetType targetType, + Long postId, PostType postType, String reportedPostTitle, String reportedContent, + ReportReason reportReason, String detailReason, ActiveStatus activeStatus) { + this.reporter = reporter; + this.targetMember = targetMember; + this.targetId = targetId; + this.targetType = targetType; + this.postId = postId; + this.postType = postType; + this.reportedPostTitle = reportedPostTitle; + this.reportedContent = reportedContent; + this.reportReason = reportReason; + this.detailReason = detailReason; + this.activeStatus = (activeStatus != null) ? activeStatus : ActiveStatus.ACTIVE; } - } diff --git a/src/main/java/org/example/tackit/domain/entity/ReportReason.java b/src/main/java/org/example/tackit/domain/entity/ReportReason.java index a4339bd3..d8c7a15a 100644 --- a/src/main/java/org/example/tackit/domain/entity/ReportReason.java +++ b/src/main/java/org/example/tackit/domain/entity/ReportReason.java @@ -1,20 +1,20 @@ package org.example.tackit.domain.entity; public enum ReportReason { - ADVERTISEMENT("광고 및 홍보성 게시물"), - DUPLICATE("중복 또는 도배성 게시물"), - FALSE_INFO("허위 정보 또는 사실 왜곡"), - IRRELEVANT("게시판 주제와 관련 없는 내용"), - ETC("기타"); + ADVERTISEMENT("광고 및 홍보성 내용"), + DUPLICATE("중복 또는 도배성 내용"), + FALSE_INFO("허위 정보 또는 사실 왜곡"), + IRRELEVANT("게시판 주제와 관련 없는 내용"), + ETC("기타"); - private final String description; + private final String description; - ReportReason(String description) { - this.description = description; - } + ReportReason(String description) { + this.description = description; + } - public String getDescription() { - return description; - } + public String getDescription() { + return description; + } } diff --git a/src/main/java/org/example/tackit/domain/entity/TargetType.java b/src/main/java/org/example/tackit/domain/entity/TargetType.java index e1038994..4c41e685 100644 --- a/src/main/java/org/example/tackit/domain/entity/TargetType.java +++ b/src/main/java/org/example/tackit/domain/entity/TargetType.java @@ -1,10 +1,5 @@ package org.example.tackit.domain.entity; public enum TargetType { - TIP_POST, - FREE_POST, - QNA_POST, - TIP_COMMENT, - FREE_COMMENT, - QNA_COMMENT, + POST, COMMENT } diff --git a/src/main/java/org/example/tackit/domain/entity/org/MemberOrg.java b/src/main/java/org/example/tackit/domain/entity/org/MemberOrg.java index 0ee29756..2ffcfdb5 100644 --- a/src/main/java/org/example/tackit/domain/entity/org/MemberOrg.java +++ b/src/main/java/org/example/tackit/domain/entity/org/MemberOrg.java @@ -78,4 +78,13 @@ public int getActivityYear() { int currentYear = LocalDate.now().getYear(); return currentYear - this.joinedYear + 1; } + + // 프로필 수정 + public void updateNickname(String nickname) { + this.nickname = nickname; + } + + public void updateProfileImage(String profileImageUrl) { + this.profileImageUrl = profileImageUrl; + } } diff --git a/src/main/java/org/example/tackit/domain/entity/post/Comment.java b/src/main/java/org/example/tackit/domain/entity/post/Comment.java index 7e070170..9e2cf3d0 100644 --- a/src/main/java/org/example/tackit/domain/entity/post/Comment.java +++ b/src/main/java/org/example/tackit/domain/entity/post/Comment.java @@ -60,9 +60,12 @@ public class Comment { @Enumerated(EnumType.STRING) private ActiveStatus activeStatus = ActiveStatus.ACTIVE; + @Column(nullable = false) + private int reportCnt; + @CreatedDate private LocalDateTime createdAt; - + @LastModifiedDate private LocalDateTime updatedAt; @@ -72,6 +75,7 @@ public Comment(String content, Post post, MemberOrg writer, Comment parent) { this.post = post; this.writer = writer; this.parent = parent; + this.reportCnt = 0; } public void addReply(Comment reply) { @@ -79,6 +83,23 @@ public void addReply(Comment reply) { reply.setParent(this); } + // 관리자에 의한 활성화 + public void activate() { + if (this.activeStatus != ActiveStatus.DELETED) { + throw new IllegalStateException("삭제되지 않은 댓글은 활성화할 수 없습니다."); + } + this.activeStatus = ActiveStatus.ACTIVE; + this.reportCnt = 0; + } + + public void receiveReport() { + this.reportCnt++; + + if (this.reportCnt >= 3) { + this.activeStatus = ActiveStatus.DELETED; + } + } + private void setParent(Comment parent) { this.parent = parent; } diff --git a/src/main/java/org/example/tackit/domain/entity/post/Post.java b/src/main/java/org/example/tackit/domain/entity/post/Post.java index e3767545..c2a04c63 100644 --- a/src/main/java/org/example/tackit/domain/entity/post/Post.java +++ b/src/main/java/org/example/tackit/domain/entity/post/Post.java @@ -158,6 +158,14 @@ public void activate() { this.reportCnt = 0; } + public void receiveReport() { + this.reportCnt++; + + if (this.reportCnt >= 3) { + this.activeStatus = ActiveStatus.DELETED; + } + } + public void increaseViewCnt() { this.viewCnt++; } diff --git a/src/main/java/org/example/tackit/domain/executive/controller/ExecutiveReportController.java b/src/main/java/org/example/tackit/domain/executive/controller/ExecutiveReportController.java index 90ba2558..1ac0ccd4 100644 --- a/src/main/java/org/example/tackit/domain/executive/controller/ExecutiveReportController.java +++ b/src/main/java/org/example/tackit/domain/executive/controller/ExecutiveReportController.java @@ -1,71 +1,71 @@ -package org.example.tackit.domain.executive.controller; - -import lombok.RequiredArgsConstructor; -import org.example.tackit.common.dto.PageResponseDTO; -import org.example.tackit.domain.admin.dto.ReportedPostDTO; -import org.example.tackit.domain.entity.Post; -import org.example.tackit.domain.executive.dto.response.ReportedPostResDto; -import org.example.tackit.domain.executive.service.ExecutiveReportedPostService; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PageableDefault; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/executive/reports") -@RequiredArgsConstructor -public class ExecutiveReportController { - private final ExecutiveReportedPostService executiveReportedPostService; - - // [ 신고 게시글 전체 조회 ] - @GetMapping - public ResponseEntity> getReportList( - @RequestParam(value = "status", defaultValue = "ALL") String status, - @PageableDefault(size = 10, sort = "reportedAt", direction = Sort.Direction.DESC) Pageable pageable) { - - Page reports = executiveReportedPostService.getReportList(status, pageable); - return ResponseEntity.ok(PageResponseDTO.from(reports)); - } - - // [ 신고 게시글 상세 조회 ] - @GetMapping("/{reportId}") - public ResponseEntity getReportDetail(@PathVariable Long reportId) { - return ResponseEntity.ok(executiveReportedPostService.getReportDetail(reportId)); - } - - // [ 신고 게시글 복구 ] - - // [ 신고 게시글 완전 삭제 ] - /* - @DeleteMapping("/{postType}/posts/{postId}") - public ResponseEntity deleteReportedPost( - @PathVariable("postType") Post postType, - @PathVariable("postId") Long postId) { - - ReportedPostService service = reportedPostServices.get(postType); - if (service == null) { - throw new IllegalArgumentException("지원하지 않는 게시글 유형입니다: " + postType); - } - - service.deletePost(postId); - return ResponseEntity.noContent().build(); - } - - // 게시글 활성화 - @PatchMapping("/{postType}/posts/{postId}/activate") - public ResponseEntity activateReportedPost( - @PathVariable("postType") Post postType, - @PathVariable("postId") Long postId) { - - ReportedPostService service = reportedPostServices.get(postType); - if (service == null) { - throw new IllegalArgumentException("지원하지 않는 게시글 유형입니다: " + postType); - } - - service.activatePost(postId); - return ResponseEntity.ok("게시글이 활성화되었습니다."); - } - */ -} +//package org.example.tackit.domain.executive.controller; +// +//import lombok.RequiredArgsConstructor; +//import org.example.tackit.common.dto.PageResponseDTO; +//import org.example.tackit.domain.admin.dto.ReportedPostDTO; +//import org.example.tackit.domain.entity.Post; +//import org.example.tackit.domain.executive.dto.response.ReportedPostResDto; +//import org.example.tackit.domain.executive.service.ExecutiveReportedPostService; +//import org.springframework.data.domain.Page; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.domain.Sort; +//import org.springframework.data.web.PageableDefault; +//import org.springframework.http.ResponseEntity; +//import org.springframework.web.bind.annotation.*; +// +//@RestController +//@RequestMapping("/api/executive/reports") +//@RequiredArgsConstructor +//public class ExecutiveReportController { +// private final ExecutiveReportedPostService executiveReportedPostService; +// +// // [ 신고 게시글 전체 조회 ] +// @GetMapping +// public ResponseEntity> getReportList( +// @RequestParam(value = "status", defaultValue = "ALL") String status, +// @PageableDefault(size = 10, sort = "reportedAt", direction = Sort.Direction.DESC) Pageable pageable) { +// +// Page reports = executiveReportedPostService.getReportList(status, pageable); +// return ResponseEntity.ok(PageResponseDTO.from(reports)); +// } +// +// // [ 신고 게시글 상세 조회 ] +// @GetMapping("/{reportId}") +// public ResponseEntity getReportDetail(@PathVariable Long reportId) { +// return ResponseEntity.ok(executiveReportedPostService.getReportDetail(reportId)); +// } +// +// // [ 신고 게시글 복구 ] +// +// // [ 신고 게시글 완전 삭제 ] +// /* +// @DeleteMapping("/{postType}/posts/{postId}") +// public ResponseEntity deleteReportedPost( +// @PathVariable("postType") Post postType, +// @PathVariable("postId") Long postId) { +// +// ReportedPostService service = reportedPostServices.get(postType); +// if (service == null) { +// throw new IllegalArgumentException("지원하지 않는 게시글 유형입니다: " + postType); +// } +// +// service.deletePost(postId); +// return ResponseEntity.noContent().build(); +// } +// +// // 게시글 활성화 +// @PatchMapping("/{postType}/posts/{postId}/activate") +// public ResponseEntity activateReportedPost( +// @PathVariable("postType") Post postType, +// @PathVariable("postId") Long postId) { +// +// ReportedPostService service = reportedPostServices.get(postType); +// if (service == null) { +// throw new IllegalArgumentException("지원하지 않는 게시글 유형입니다: " + postType); +// } +// +// service.activatePost(postId); +// return ResponseEntity.ok("게시글이 활성화되었습니다."); +// } +// */ +//} diff --git a/src/main/java/org/example/tackit/domain/memberOrg/component/MemberOrgValidator.java b/src/main/java/org/example/tackit/domain/memberOrg/component/MemberOrgValidator.java index 6ffb7201..9316bcde 100644 --- a/src/main/java/org/example/tackit/domain/memberOrg/component/MemberOrgValidator.java +++ b/src/main/java/org/example/tackit/domain/memberOrg/component/MemberOrgValidator.java @@ -6,6 +6,8 @@ import org.example.tackit.domain.entity.org.MemberOrg; import org.example.tackit.domain.entity.org.OrgStatus; import org.example.tackit.domain.memberOrg.repository.MemberOrgRepository; +import org.example.tackit.global.exception.CustomBaseException; +import org.example.tackit.global.exception.ErrorCode; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -56,4 +58,11 @@ public MemberOrg validateSenior(Long memberOrgId) { } return memberOrg; } + + // 소유자 검증 메서드 + public void validateOwner(MemberOrg memberOrg, String email) { + if( !memberOrg.getMember().getEmail().equals(email)) { + throw new CustomBaseException(ErrorCode.ACCESS_DENIED_PROFILE); + } + } } \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/mypage/controller/MypageController.java b/src/main/java/org/example/tackit/domain/mypage/controller/MypageController.java new file mode 100644 index 00000000..b5d27174 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/mypage/controller/MypageController.java @@ -0,0 +1,88 @@ +package org.example.tackit.domain.mypage.controller; + +import lombok.RequiredArgsConstructor; +import org.example.tackit.domain.auth.login.dto.SignInResponse; +import org.example.tackit.domain.auth.login.security.CustomUserDetails; +import org.example.tackit.domain.auth.login.service.AuthService; +import org.example.tackit.domain.mypage.dto.request.UpdateProfileReq; +import org.example.tackit.domain.mypage.dto.response.MyCommentListResp; +import org.example.tackit.domain.mypage.dto.response.MyPostListResp; +import org.example.tackit.domain.mypage.dto.response.MyPageInfoResp; +import org.example.tackit.domain.mypage.dto.response.MyScrapListResp; +import org.example.tackit.domain.mypage.service.MyPageService; +import org.example.tackit.global.response.ApiResponse; +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.util.List; + +@RestController +@RequestMapping("/api/mypage") +@RequiredArgsConstructor +public class MypageController { + private final MyPageService myPageService; + private final AuthService authService; + + // 내 정보 조회(닉네임, 조직(동아리라면 대학), 이메일) + @GetMapping("/profiles/{memberOrgId}") + public ResponseEntity> getMyPageInfo( + @PathVariable Long memberOrgId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + MyPageInfoResp response = myPageService.getMypageInfo(memberOrgId, userDetails.getUsername()); + + return ApiResponse.success(HttpStatus.OK, "마이 프로필 조회 성공", response); + } + + // 비밀번호, 닉네임, 프로필 이미지 수정 + @PatchMapping("/profiles/{memberOrgId}") + public ResponseEntity> updateProfile( + @PathVariable Long memberOrgId, + @RequestBody UpdateProfileReq request, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + myPageService.updateProfile(memberOrgId, request, userDetails.getUsername()); + + return ApiResponse.success(HttpStatus.OK, "프로필 수정 성공", null); + } + + // 작성한 글 조회 + @GetMapping("/posts/{memberOrgId}") + public ResponseEntity>> getMyPosts( + @PathVariable Long memberOrgId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + List response = myPageService.getMyPosts(memberOrgId, userDetails.getUsername()); + + return ApiResponse.success(HttpStatus.OK, "작성한 글 목록 조회 성공", response); + } + + // 작성한 댓글 조회 + @GetMapping("/comments/{memberOrgId}") + public ResponseEntity>> getMyComments( + @PathVariable Long memberOrgId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + List response = myPageService.getMyComments(memberOrgId, userDetails.getUsername()); + return ApiResponse.success(HttpStatus.OK, "작성한 댓글 목록 조회 성공", response); + } + + // 스크랩 게시글 조회 + @GetMapping("/scraps/{memberOrgId}") + public ResponseEntity>> getMyScraps( + @PathVariable Long memberOrgId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + List response = myPageService.getMyScraps(memberOrgId, userDetails.getUsername()); + return ApiResponse.success(HttpStatus.OK, "스크랩한 글 목록 조회 성공", response); + } + + // 프로필 전환 + @PostMapping("/switch") + public ResponseEntity switchProfile() { + return ResponseEntity.ok(authService.switchProfile()); + } + +} diff --git a/src/main/java/org/example/tackit/domain/mypage/controller/MypageFreeController.java b/src/main/java/org/example/tackit/domain/mypage/controller/MypageFreeController.java deleted file mode 100644 index cdc69964..00000000 --- a/src/main/java/org/example/tackit/domain/mypage/controller/MypageFreeController.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.example.tackit.domain.mypage.controller; - -import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.auth.login.security.CustomUserDetails; -import org.example.tackit.domain.mypage.dto.response.*; -import org.example.tackit.domain.mypage.service.MyPageFreeService; -import org.example.tackit.common.dto.PageResponseDTO; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PageableDefault; -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.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/mypage") -@RequiredArgsConstructor -public class MypageFreeController { - private final MyPageFreeService myPageFreeService; - - /* - // 스크랩한 자유 게시글 - @GetMapping("/free-scraps") - public ResponseEntity> getMyFreeScraps( - @AuthenticationPrincipal CustomUserDetails user, - @PageableDefault(size = 5, sort = "savedAt", direction = Sort.Direction.DESC) Pageable pageable - ) { - return ResponseEntity.ok(myPageFreeService.getScrapListByMember(user.getEmail(), pageable)); - } - - // 내가 작성한 자유 게시글 - @GetMapping("/free-posts") - public ResponseEntity> getMyFreePosts( - @AuthenticationPrincipal CustomUserDetails user, - @PageableDefault(size = 5, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { - - return ResponseEntity.ok(myPageFreeService.getMyPosts(user.getUsername(), pageable)); - } - - // 내가 쓴 댓글 조회 - @GetMapping("/free-comments") - public ResponseEntity> getMyFreeComments( - @AuthenticationPrincipal CustomUserDetails user, - @PageableDefault(size = 5, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { - - return ResponseEntity.ok(myPageFreeService.getMyComments(user.getUsername(), pageable)); - - } - - */ - -} diff --git a/src/main/java/org/example/tackit/domain/mypage/dto/request/UpdateProfileReq.java b/src/main/java/org/example/tackit/domain/mypage/dto/request/UpdateProfileReq.java new file mode 100644 index 00000000..28642f6f --- /dev/null +++ b/src/main/java/org/example/tackit/domain/mypage/dto/request/UpdateProfileReq.java @@ -0,0 +1,10 @@ +package org.example.tackit.domain.mypage.dto.request; + +import lombok.Getter; + +@Getter +public class UpdateProfileReq { + private String nickname; + private String password; + private String profileImageUrl; +} diff --git a/src/main/java/org/example/tackit/domain/mypage/dto/response/FreeMyCommentResponseDto.java b/src/main/java/org/example/tackit/domain/mypage/dto/response/FreeMyCommentResponseDto.java index cab1ef1d..dfbca565 100644 --- a/src/main/java/org/example/tackit/domain/mypage/dto/response/FreeMyCommentResponseDto.java +++ b/src/main/java/org/example/tackit/domain/mypage/dto/response/FreeMyCommentResponseDto.java @@ -1,23 +1,23 @@ -package org.example.tackit.domain.mypage.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.example.tackit.domain.entity.Post; - -import java.time.LocalDateTime; - -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class FreeMyCommentResponseDto { - private Long commentId; - private Long postId; - private String writer; - private String profileImageUrl; - private String content; - private LocalDateTime createdAt; - private Post type; -} +//package org.example.tackit.domain.mypage.dto.response; +// +//import lombok.AllArgsConstructor; +//import lombok.Builder; +//import lombok.Getter; +//import lombok.NoArgsConstructor; +//import org.example.tackit.domain.entity.Post; +// +//import java.time.LocalDateTime; +// +//@Getter +//@Builder +//@AllArgsConstructor +//@NoArgsConstructor +//public class FreeMyCommentResponseDto { +// private Long commentId; +// private Long postId; +// private String writer; +// private String profileImageUrl; +// private String content; +// private LocalDateTime createdAt; +// private Post type; +//} diff --git a/src/main/java/org/example/tackit/domain/mypage/dto/response/MemberMypageResponse.java b/src/main/java/org/example/tackit/domain/mypage/dto/response/MemberMypageResponse.java deleted file mode 100644 index 84a049ea..00000000 --- a/src/main/java/org/example/tackit/domain/mypage/dto/response/MemberMypageResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.example.tackit.domain.mypage.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.example.tackit.domain.entity.MemberRole; -import org.example.tackit.domain.entity.MemberType; - -/* -@NoArgsConstructor -@AllArgsConstructor -@Getter -@Builder -public class MemberMypageResponse { - private String nickname; - private String email; - private String orgName; - private MemberRole memberRole; - private MemberType memberType; - private String profileImageUrl; - -} - - */ diff --git a/src/main/java/org/example/tackit/domain/mypage/dto/response/MyCommentListResp.java b/src/main/java/org/example/tackit/domain/mypage/dto/response/MyCommentListResp.java new file mode 100644 index 00000000..c0d479dd --- /dev/null +++ b/src/main/java/org/example/tackit/domain/mypage/dto/response/MyCommentListResp.java @@ -0,0 +1,37 @@ +package org.example.tackit.domain.mypage.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.example.tackit.domain.entity.post.Comment; +import org.example.tackit.domain.entity.post.Post; +import org.example.tackit.domain.entity.post.PostType; + +import java.time.LocalDateTime; + +@Getter +@Builder +@AllArgsConstructor +public class MyCommentListResp { + private Long commentId; + private Long postId; + private PostType postType; + private String postTitle; + private String commentContent; + private LocalDateTime createdAt; + + public static MyCommentListResp from(Comment comment) { + Post post = comment.getPost(); + + return MyCommentListResp.builder() + .commentId(comment.getId()) + .postId(post.getId()) + .postType(post.getPostType()) + .postTitle(post.getTitle()) + .commentContent(comment.getContent()) + .createdAt(comment.getCreatedAt()) + .build(); + } + + +} diff --git a/src/main/java/org/example/tackit/domain/mypage/dto/response/MyPageInfoResp.java b/src/main/java/org/example/tackit/domain/mypage/dto/response/MyPageInfoResp.java new file mode 100644 index 00000000..52ffd3f4 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/mypage/dto/response/MyPageInfoResp.java @@ -0,0 +1,43 @@ +package org.example.tackit.domain.mypage.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.example.tackit.domain.entity.MemberRole; +import org.example.tackit.domain.entity.MemberType; +import org.example.tackit.domain.entity.org.MemberOrg; +import org.example.tackit.domain.entity.org.OrgType; +import org.example.tackit.domain.entity.org.Organization; + +@Getter +@Builder +@AllArgsConstructor +public class MyPageInfoResp { + private String nickname; + private String email; + private OrgType orgType; + private String orgName; + private String universityName; + private String imageUrl; + private MemberType memberType; + private MemberRole memberRole; + + public static MyPageInfoResp from(MemberOrg memberOrg) { + Organization organization = memberOrg.getOrganization(); + + String universityName = (organization.getType() == OrgType.CLUB && organization.getUniversity() != null) + ? organization.getUniversity().getUniversityName() + : null; + + return MyPageInfoResp.builder() + .nickname(memberOrg.getNickname()) + .email(memberOrg.getMember().getEmail()) + .orgType(organization.getType()) + .orgName(organization.getName()) + .universityName(universityName) + .imageUrl(memberOrg.getProfileImageUrl()) + .memberType(memberOrg.getMemberType()) + .memberRole(memberOrg.getMemberRole()) + .build(); + } +} diff --git a/src/main/java/org/example/tackit/domain/mypage/dto/response/MyPostListResp.java b/src/main/java/org/example/tackit/domain/mypage/dto/response/MyPostListResp.java new file mode 100644 index 00000000..3bc331f4 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/mypage/dto/response/MyPostListResp.java @@ -0,0 +1,55 @@ +package org.example.tackit.domain.mypage.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.example.tackit.domain.entity.MemberRole; +import org.example.tackit.domain.entity.MemberType; +import org.example.tackit.domain.entity.org.MemberOrg; +import org.example.tackit.domain.entity.post.Post; +import org.example.tackit.domain.entity.post.PostCategory; +import org.example.tackit.domain.entity.post.PostType; + +import java.time.LocalDateTime; + +@Getter +@Builder +@AllArgsConstructor +public class MyPostListResp { + private Long postId; + private String title; + private String content; + private String nickname; + private String imageUrl; + private String postUrl; + private PostType postType; + private PostCategory category; + private MemberRole memberRole; + private MemberType memberType; + private LocalDateTime createdAt; + + public static MyPostListResp from(Post post) { + MemberOrg writer = post.getWriter(); + + String imageUrl = post.getImages().isEmpty() ? null + : post.getImages().get(0).getImageUrl(); + + String url = "/api/posts/" + post.getId(); + + String displayNickname = post.isAnonymous() ? "익명" : writer.getNickname(); + + return MyPostListResp.builder() + .postId(post.getId()) + .title(post.getTitle()) + .content(post.getContent()) + .nickname(displayNickname) + .imageUrl(imageUrl) + .postUrl(url) + .postType(post.getPostType()) + .category(post.getCategory()) + .memberRole(writer.getMemberRole()) + .memberType(writer.getMemberType()) + .createdAt(post.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/org/example/tackit/domain/mypage/dto/response/MyScrapListResp.java b/src/main/java/org/example/tackit/domain/mypage/dto/response/MyScrapListResp.java new file mode 100644 index 00000000..26db593d --- /dev/null +++ b/src/main/java/org/example/tackit/domain/mypage/dto/response/MyScrapListResp.java @@ -0,0 +1,61 @@ +package org.example.tackit.domain.mypage.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.example.tackit.domain.entity.MemberRole; +import org.example.tackit.domain.entity.MemberType; +import org.example.tackit.domain.entity.org.MemberOrg; +import org.example.tackit.domain.entity.post.Post; +import org.example.tackit.domain.entity.post.PostCategory; +import org.example.tackit.domain.entity.post.PostType; +import org.example.tackit.domain.entity.post.Scrap; + +import java.time.LocalDateTime; + +@Getter +@Builder +@AllArgsConstructor +public class MyScrapListResp { + private Long scrapId; + private Long postId; + private String title; + private String content; + private String writerNickname; + private MemberType memberType; + private MemberRole memberRole; + private PostType postType; + private PostCategory postCategory; + private String imageUrl; + private String postUrl; + + private LocalDateTime createdAt; + + public static MyScrapListResp from(Scrap scrap) { + Post post = scrap.getPost(); + MemberOrg writer = post.getWriter(); + + String imageUrl = !post.getImages().isEmpty() + ? post.getImages().get(0).getImageUrl() + : null; + + String url = "/api/posts/" + post.getId(); + + String displayNickname = post.isAnonymous() ? "익명" : writer.getNickname(); + + return MyScrapListResp.builder() + .scrapId(scrap.getId()) + .postId(post.getId()) + .title(post.getTitle()) + .content(post.getContent()) + .writerNickname(displayNickname) + .memberRole(post.getWriter().getMemberRole()) + .memberType(post.getWriter().getMemberType()) + .postType(post.getPostType()) + .postCategory(post.getCategory()) + .imageUrl(imageUrl) + .postUrl(url) + .createdAt(scrap.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/org/example/tackit/domain/mypage/service/MyPageFreeService.java b/src/main/java/org/example/tackit/domain/mypage/service/MyPageFreeService.java deleted file mode 100644 index 1260919d..00000000 --- a/src/main/java/org/example/tackit/domain/mypage/service/MyPageFreeService.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.example.tackit.domain.mypage.service; - -import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.admin.repository.AdminMemberRepository; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class MyPageFreeService { - - private final AdminMemberRepository adminMemberRepository; - - /* - // 스크랩한 자유 게시글 조회 - @Transactional - public PageResponseDTO getScrapListByMember(String email, Pageable pageable) { - Member member = adminMemberRepository.findByEmail(email) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - - Page page = freeScrapJPARepository.findByMemberAndType(member, Post.Free, pageable); - - - return PageResponseDTO.from(page, scrap -> { - FreePost post = scrap.getFreePost(); - - List tags = freePostTagMapRepository.findByFreePost(post).stream() - .map(mapping -> mapping.getTag().getTagName()) - .toList(); - - return FreeScrapResponse.from(scrap, tags); - }); - } - - // 내가 쓴 자유 게시글 조회 - @Transactional(readOnly = true) - public PageResponseDTO getMyPosts(String email, Pageable pageable) { - Member member = adminMemberRepository.findByEmail(email) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - - Page page = freePostJPARepository.findByWriterAndStatus(member, AccountStatus.ACTIVE, pageable); - - return PageResponseDTO.from(page, post -> { - List tags = freePostTagMapRepository.findByFreePost(post).stream() - .map(mapping -> mapping.getTag().getTagName()) - .toList(); - - return FreeMyPostResponseDto.from(post, tags); - }); - } - - // 내가 쓴 댓글 조회 - @Transactional(readOnly = true) - public PageResponseDTO getMyComments(String email, Pageable pageable) { - Member member = adminMemberRepository.findByEmail(email) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - - Page comments = freeCommentRepository.findByWriter(member, pageable); - - return PageResponseDTO.from(comments, comment -> FreeMyCommentResponseDto.builder() - .commentId(comment.getId()) - .postId(comment.getFreePost().getId()) - .writer(comment.getWriter().getNickname()) - .profileImageUrl(comment.getWriter().getProfileImageUrl()) - .content(comment.getContent()) - .createdAt(comment.getCreatedAt()) - .type(comment.getFreePost().getType()) - .build() - ); - } - - */ -} diff --git a/src/main/java/org/example/tackit/domain/mypage/service/MyPageService.java b/src/main/java/org/example/tackit/domain/mypage/service/MyPageService.java new file mode 100644 index 00000000..87b0ecd9 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/mypage/service/MyPageService.java @@ -0,0 +1,107 @@ +package org.example.tackit.domain.mypage.service; + +import lombok.RequiredArgsConstructor; +import org.example.tackit.domain.entity.Member; +import org.example.tackit.domain.entity.org.MemberOrg; +import org.example.tackit.domain.entity.post.Post; +import org.example.tackit.domain.entity.post.Scrap; +import org.example.tackit.domain.memberOrg.component.MemberOrgValidator; +import org.example.tackit.domain.memberOrg.repository.MemberOrgRepository; +import org.example.tackit.domain.mypage.dto.request.UpdateProfileReq; +import org.example.tackit.domain.mypage.dto.response.MyCommentListResp; +import org.example.tackit.domain.mypage.dto.response.MyPostListResp; +import org.example.tackit.domain.mypage.dto.response.MyPageInfoResp; +import org.example.tackit.domain.mypage.dto.response.MyScrapListResp; +import org.example.tackit.domain.post.repository.CommentRepository; +import org.example.tackit.domain.post.repository.PostRepository; +import org.example.tackit.domain.post.repository.ScrapRepository; +import org.example.tackit.global.exception.BusinessException; +import org.example.tackit.global.exception.ErrorCode; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class MyPageService { + private final MemberOrgRepository memberOrgRepository; + private final MemberOrgValidator memberOrgValidator; + private final PostRepository postRepository; + private final CommentRepository commentRepository; + private final ScrapRepository scrapRepository; + private final PasswordEncoder passwordEncoder; + + // 내 정보 조회(닉네임, 조직(동아리라면 대학), 이메일) + public MyPageInfoResp getMypageInfo(Long memberOrgId, String email) { + MemberOrg memberOrg = memberOrgValidator.validateActiveMembership(memberOrgId); + memberOrgValidator.validateOwner(memberOrg, email); + + return MyPageInfoResp.from(memberOrg); + } + + // 내 정보 수정 + public void updateProfile(Long memberOrgId, UpdateProfileReq request, String email) { + + MemberOrg memberOrg = memberOrgValidator.validateActiveMembership(memberOrgId); + memberOrgValidator.validateOwner(memberOrg, email); + + Member member = memberOrg.getMember(); + + // 같은 조직 내 닉네임 중복 방지 + if (request.getNickname() != null && !request.getNickname().equals(memberOrg.getNickname())) { + if (memberOrgRepository.existsByOrganizationIdAndNickname( + memberOrg.getOrganization().getId(), + request.getNickname() + )) { + throw new BusinessException(ErrorCode.DUPLICATE_NICKNAME); + } + memberOrg.updateNickname(request.getNickname()); + } + + if (request.getPassword() != null) { + member.updatePassword(passwordEncoder.encode(request.getPassword())); + } + + if (request.getProfileImageUrl() != null) { + memberOrg.updateProfileImage(request.getProfileImageUrl()); + } + } + + // 작성한 글 조회 + public List getMyPosts(Long memberOrgId, String email) { + MemberOrg memberOrg = memberOrgValidator.validateActiveMembership(memberOrgId); + memberOrgValidator.validateOwner(memberOrg, email); + + List posts = postRepository.findAllByWriterIdWithDetails(memberOrgId); + + return posts.stream() + .map(MyPostListResp::from) + .toList(); + } + + // 작성한 댓글 조회 + public List getMyComments(Long memberOrgId, String email) { + MemberOrg memberOrg = memberOrgValidator.validateActiveMembership(memberOrgId); + memberOrgValidator.validateOwner(memberOrg, email); + + return commentRepository.findAllByWriterIdWithPost(memberOrgId).stream() + .map(MyCommentListResp::from) + .toList(); + } + + // 스크랩한 게시글 조회 + public List getMyScraps(Long memberOrgId, String email) { + MemberOrg memberOrg = memberOrgValidator.validateActiveMembership(memberOrgId); + memberOrgValidator.validateOwner(memberOrg, email); + + List scraps = scrapRepository.findAllByMemberOrgIdWithPost(memberOrgId); + + return scraps.stream() + .map(MyScrapListResp::from) + .toList(); + } + +} diff --git a/src/main/java/org/example/tackit/domain/post/repository/CommentRepository.java b/src/main/java/org/example/tackit/domain/post/repository/CommentRepository.java index 820eda02..5c1531d6 100644 --- a/src/main/java/org/example/tackit/domain/post/repository/CommentRepository.java +++ b/src/main/java/org/example/tackit/domain/post/repository/CommentRepository.java @@ -1,6 +1,7 @@ package org.example.tackit.domain.post.repository; import java.util.List; +import java.util.Optional; import org.example.tackit.domain.entity.post.Comment; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -10,4 +11,13 @@ public interface CommentRepository extends JpaRepository { @Query("SELECT c FROM Comment c JOIN FETCH c.writer WHERE c.post.id = :postId ORDER BY c.createdAt ASC") List findAllByPostId(@Param("postId") Long postId); + + @Query("SELECT c FROM Comment c JOIN FETCH c.post WHERE c.id = :commentId") + Optional findByIdWithPost(@Param("commentId") Long commentId); + + @Query("select c from Comment c " + + "join fetch c.post p " + + "where c.writer.id = :memberOrgId " + + "order by c.createdAt desc") + List findAllByWriterIdWithPost(@Param("memberOrgId") Long memberOrgId); } \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/post/repository/PostRepository.java b/src/main/java/org/example/tackit/domain/post/repository/PostRepository.java index b059c330..69c87d23 100644 --- a/src/main/java/org/example/tackit/domain/post/repository/PostRepository.java +++ b/src/main/java/org/example/tackit/domain/post/repository/PostRepository.java @@ -1,5 +1,6 @@ package org.example.tackit.domain.post.repository; +import java.util.List; import java.util.Optional; import org.example.tackit.domain.entity.ActiveStatus; import org.example.tackit.domain.entity.post.Post; @@ -39,4 +40,12 @@ Page findAllByOrganizationIdAndPostTypeAndActiveStatus( // 관리자 신고 게시글 조회 Page findAllByActiveStatusAndReportCntGreaterThanEqual(ActiveStatus activeStatus, int reportCount, Pageable pageable); + + // 마이페이지 ) 작성한 글 조회 + @Query("select distinct p from Post p " + + "join fetch p.writer w " + + "left join fetch p.images " + + "where w.id = :memberOrgId " + + "order by p.createdAt desc") + List findAllByWriterIdWithDetails(@Param("memberOrgId") Long memberOrgId); } \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/post/repository/ScrapRepository.java b/src/main/java/org/example/tackit/domain/post/repository/ScrapRepository.java index c5c03033..41a8552a 100644 --- a/src/main/java/org/example/tackit/domain/post/repository/ScrapRepository.java +++ b/src/main/java/org/example/tackit/domain/post/repository/ScrapRepository.java @@ -1,8 +1,11 @@ package org.example.tackit.domain.post.repository; +import java.util.List; import java.util.Optional; import org.example.tackit.domain.entity.post.Scrap; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface ScrapRepository extends JpaRepository { @@ -12,4 +15,12 @@ public interface ScrapRepository extends JpaRepository { // 삭제 시 조회용 Optional findByMemberOrgIdAndPostId(Long memberOrgId, Long postId); + + // 마이페이지 ) 스크랩한 글 조회 + @Query("select s from Scrap s " + + "join fetch s.post p " + + "join fetch p.writer w " + + "where s.memberOrg.id = :memberOrgId " + + "order by s.createdAt desc") + List findAllByMemberOrgIdWithPost(@Param("memberOrgId") Long memberOrgId); } \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/report/controller/ReportController.java b/src/main/java/org/example/tackit/domain/report/controller/ReportController.java index 2801b9aa..cc66794f 100644 --- a/src/main/java/org/example/tackit/domain/report/controller/ReportController.java +++ b/src/main/java/org/example/tackit/domain/report/controller/ReportController.java @@ -1,19 +1,47 @@ package org.example.tackit.domain.report.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.auth.login.security.CustomUserDetails; -import org.example.tackit.domain.report.dto.ReportRequestDto; +import org.example.tackit.common.dto.ActiveProfile; +import org.example.tackit.common.dto.ProfileContext; +import org.example.tackit.domain.report.dto.ReportReqDto; import org.example.tackit.domain.report.service.ReportService; +import org.example.tackit.global.response.ApiResponse; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/reports") +@RequestMapping("/api") @RequiredArgsConstructor public class ReportController { - private final ReportService reportService; + private final ReportService reportService; + + @PostMapping("/posts/{postId}/report") + public ResponseEntity> reportPost( + @ActiveProfile ProfileContext profileContext, + @PathVariable Long postId, + @Valid @RequestBody ReportReqDto reqDto + ) { + reportService.reportPost(reqDto, profileContext.id(), postId); + return ApiResponse.success(HttpStatus.OK, "게시글 신고 접수 성공"); + } + + @PostMapping("/comments/{commentId}/report") + public ResponseEntity> reportComment( + @ActiveProfile ProfileContext profileContext, + @PathVariable Long commentId, + @Valid @RequestBody ReportReqDto reqDto + ) { + reportService.reportComment(reqDto, profileContext.id(), commentId); + return ApiResponse.success(HttpStatus.OK, "댓글 신고 접수 성공"); + } + /* @PostMapping("/create") diff --git a/src/main/java/org/example/tackit/domain/report/dto/ReportRequestDto.java b/src/main/java/org/example/tackit/domain/report/dto/ReportReqDto.java similarity index 58% rename from src/main/java/org/example/tackit/domain/report/dto/ReportRequestDto.java rename to src/main/java/org/example/tackit/domain/report/dto/ReportReqDto.java index c8a32ba8..908de5e4 100644 --- a/src/main/java/org/example/tackit/domain/report/dto/ReportRequestDto.java +++ b/src/main/java/org/example/tackit/domain/report/dto/ReportReqDto.java @@ -5,15 +5,13 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.example.tackit.domain.entity.ReportReason; -import org.example.tackit.domain.entity.TargetType; @Getter @Setter @NoArgsConstructor @AllArgsConstructor -public class ReportRequestDto { +public class ReportReqDto { - private Long targetId; - private TargetType targetType; // POST / COMMENT - private ReportReason reason; + private ReportReason reportReason; + private String detailReason; } diff --git a/src/main/java/org/example/tackit/domain/report/repository/ReportRepository.java b/src/main/java/org/example/tackit/domain/report/repository/ReportRepository.java index 353908fd..d8655afe 100644 --- a/src/main/java/org/example/tackit/domain/report/repository/ReportRepository.java +++ b/src/main/java/org/example/tackit/domain/report/repository/ReportRepository.java @@ -26,6 +26,7 @@ public interface ReportRepository extends JpaRepository { boolean existsByReporterIdAndTargetIdAndTargetType(Long reporterId, Long targetId, TargetType targetType); + // TODO 해당 부분은 별도 DTO로 빼는 게 좋을 것 같습니다 interface ReportedTargetInfo { Long getTargetId(); diff --git a/src/main/java/org/example/tackit/domain/report/service/ReportService.java b/src/main/java/org/example/tackit/domain/report/service/ReportService.java index e2c29161..e8dada41 100644 --- a/src/main/java/org/example/tackit/domain/report/service/ReportService.java +++ b/src/main/java/org/example/tackit/domain/report/service/ReportService.java @@ -1,17 +1,86 @@ package org.example.tackit.domain.report.service; import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.member.repository.MemberRepository; +import org.example.tackit.domain.entity.Report; +import org.example.tackit.domain.entity.TargetType; +import org.example.tackit.domain.entity.org.MemberOrg; +import org.example.tackit.domain.entity.post.Comment; +import org.example.tackit.domain.entity.post.Post; +import org.example.tackit.domain.memberOrg.component.MemberOrgValidator; +import org.example.tackit.domain.post.repository.CommentRepository; +import org.example.tackit.domain.post.repository.PostRepository; +import org.example.tackit.domain.report.dto.ReportReqDto; import org.example.tackit.domain.report.repository.ReportRepository; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class ReportService { private final ReportRepository reportRepository; - private final MemberRepository memberRepository; + private final MemberOrgValidator memberOrgValidator; + private final PostRepository postRepository; + private final CommentRepository commentRepository; + + @Transactional +// 🚨 게시글 신고 로직 + public void reportPost(ReportReqDto reqDto, Long requesterMemberOrgId, Long postId) { + MemberOrg reporter = memberOrgValidator.validateActiveMembership(requesterMemberOrgId); + + Post post = postRepository.findById(postId) + .orElseThrow(() -> new IllegalArgumentException("해당 게시글을 찾을 수 없습니다.")); + + if (post.getWriter().getId().equals(reporter.getId())) { + throw new IllegalStateException("자신의 게시글은 신고할 수 없습니다."); + } + + Report report = Report.builder() + .reporter(reporter) + .targetMember(post.getWriter()) + .targetId(post.getId()) + .targetType(TargetType.POST) + .postId(post.getId()) + .postType(post.getPostType()) + .reportedPostTitle(post.getTitle()) + .reportedContent(post.getContent()) + .reportReason(reqDto.getReportReason()) + .detailReason(reqDto.getDetailReason()) + .build(); + + post.receiveReport(); + reportRepository.save(report); + } + + // 댓글 신고 + @Transactional + public void reportComment(ReportReqDto reqDto, Long requesterMemberOrgId, Long commentId) { + MemberOrg reporter = memberOrgValidator.validateActiveMembership(requesterMemberOrgId); + + Comment comment = commentRepository.findByIdWithPost(commentId) + .orElseThrow(() -> new IllegalArgumentException("해당 댓글을 찾을 수 없습니다.")); + + Post post = comment.getPost(); + + Report report = Report.builder() + .reporter(reporter) + .targetMember(comment.getWriter()) + .targetId(comment.getId()) + .targetType(TargetType.COMMENT) + .postId(post.getId()) + .postType(post.getPostType()) + .reportedPostTitle(post.getTitle()) + .reportedContent(comment.getContent()) + .reportReason(reqDto.getReportReason()) + .detailReason(reqDto.getDetailReason()) + .build(); + + comment.receiveReport(); + reportRepository.save(report); + } + /* @Transactional @@ -23,6 +92,5 @@ public void createReport(ReportRequestDto dto, String email, String org) { reportRepository.save(report); } - - */ + */ } diff --git a/src/main/java/org/example/tackit/global/exception/ErrorCode.java b/src/main/java/org/example/tackit/global/exception/ErrorCode.java index 73c721bc..a20070bc 100644 --- a/src/main/java/org/example/tackit/global/exception/ErrorCode.java +++ b/src/main/java/org/example/tackit/global/exception/ErrorCode.java @@ -17,6 +17,7 @@ public enum ErrorCode { ACCESS_DENIED_ORGANIZATION(HttpStatus.FORBIDDEN, "A002", "해당 조직에 속하지 않은 게시글입니다."), ACCESS_DENIED_DELETE(HttpStatus.FORBIDDEN, "A003", "작성자 또는 관리자만 삭제할 수 있습니다."), ACCESS_DENIED_NOTICE(HttpStatus.FORBIDDEN, "A004", "운영진만 공지 게시글을 작성할 수 있습니다."), + ACCESS_DENIED_PROFILE(HttpStatus.FORBIDDEN, "A005", "해당 프로필에 대한 접근 권한이 없습니다."), // 400 POST_IS_INACTIVE(HttpStatus.BAD_REQUEST, "P002", "비활성화된 게시글입니다."), diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 37b394c2..51e84b67 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,48 +1,37 @@ spring.application.name=Tackit - # Import .env file spring.config.import=optional:file:.env[.properties] - # Mariadb spring.datasource.driver-class-name=org.mariadb.jdbc.Driver spring.datasource.url=${DB_URL} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD} - # Hibernate spring.jpa.database-platform=org.hibernate.dialect.MariaDBDialect - spring.jpa.show-sql=true logging.level.org.hibernate.SQL=DEBUG - #thymeleaf spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html - #jpa table spring.jpa.hibernate.ddl-auto=create #redis -spring.data.redis.port = ${REDIS_PORT} -spring.data.redis.host = ${REDIS_HOST} - +spring.data.redis.port=${REDIS_PORT} +spring.data.redis.host=${REDIS_HOST} #S3 -cloud.aws.s3.bucket=${S3_BUCKET_NAME} -cloud.aws.credentials.access-key=${ACCESS_KEY} -cloud.aws.credentials.secret-key=${SECRET_ACCESS_KEY} -cloud.aws.region.static=ap-northeast-2 -cloud.aws.region.auto=false -cloud.aws.stack.auto=false - +spring.cloud.aws.s3.bucket=${S3_BUCKET_NAME} +spring.cloud.aws.credentials.access-key=${ACCESS_KEY} +spring.cloud.aws.credentials.secret-key=${SECRET_ACCESS_KEY} +spring.cloud.aws.region.static=ap-northeast-2 +spring.cloud.aws.region.auto=false +spring.cloud.aws.stack.auto=false #jwt custom.jwt.secret=${JWT_SECRET} - #server-tomcat server.tomcat.mbeanregistry.enabled=true - #actuator management.endpoints.web.exposure.include=* management.endpoints.web.base-path=/api/actuator management.server.port=9090 - tackit.admin.email=${ADMIN_EMAIL:""} \ No newline at end of file