Skip to content

Commit b7ca3bd

Browse files
authored
Merge pull request #264 from CodIN-INU/develop
Develop
2 parents dfd2154 + aacf077 commit b7ca3bd

91 files changed

Lines changed: 5752 additions & 1230 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ CodIN 백엔드 Main API 서버 Repository 입니다.
1212
- [교과목 API 서버](https://github.com/CodIN-INU/codin-lecture-api)
1313

1414
---
15-
## 전체 아키텍처
16-
17-
<img width="1411" height="979" alt="image" src="https://github.com/user-attachments/assets/21768a5d-cfa0-46d8-9e16-2502520ad70c" />
1815

1916
## 주요 화면
2017

src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
import inu.codin.codin.common.security.exception.SecurityErrorCode;
55
import inu.codin.codin.domain.user.entity.UserRole;
66
import inu.codin.codin.domain.user.security.CustomUserDetails;
7+
import lombok.extern.slf4j.Slf4j;
78
import org.bson.types.ObjectId;
9+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
810
import org.springframework.security.core.Authentication;
911
import org.springframework.security.core.context.SecurityContextHolder;
1012

1113
/**
1214
* SecurityContext와 관련된 유틸리티 클래스.
1315
*/
16+
@Slf4j
1417
public class SecurityUtils {
1518

1619
/**
@@ -20,15 +23,33 @@ public class SecurityUtils {
2023
* @throws JwtException 인증 정보가 없는 경우 예외 발생
2124
*/
2225
public static ObjectId getCurrentUserId() {
26+
log.info("getCurrentUserId.");
2327
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
24-
28+
log.info("auth={} / principalClass={}", authentication, (authentication!=null ? authentication.getPrincipal().getClass() : null));
2529
if (authentication == null || !(authentication.getPrincipal() instanceof CustomUserDetails userDetails)) {
2630
throw new JwtException(SecurityErrorCode.ACCESS_DENIED);
2731
}
2832

2933
return userDetails.getId();
3034
}
3135

36+
37+
/**
38+
* 현재 인증된 사용자의 ID를 반환 (nullable 안전 버전)
39+
* - 인증이 없거나 익명이면 null 반환
40+
*/
41+
public static ObjectId getCurrentUserIdOrNull() {
42+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
43+
44+
if (authentication == null || !authentication.isAuthenticated()
45+
|| authentication instanceof AnonymousAuthenticationToken
46+
|| !(authentication.getPrincipal() instanceof CustomUserDetails userDetails)) {
47+
return null;
48+
}
49+
50+
return userDetails.getId();
51+
}
52+
3253
public static UserRole getCurrentUserRole(){
3354
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
3455

@@ -46,4 +67,10 @@ public static void validateUser(ObjectId id){
4667
}
4768
}
4869

70+
public static void validateOwners(ObjectId currentUserId, ObjectId ownerId) {
71+
validateUser(currentUserId);
72+
if (!ownerId.equals(currentUserId)) {
73+
throw new JwtException(SecurityErrorCode.ACCESS_DENIED, "본인 리소스가 아닙니다. ");
74+
}
75+
}
4976
}

src/main/java/inu/codin/codin/domain/block/service/BlockService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,15 @@ public void unblockUser(String strBlockedUserId) {
7878

7979
/**
8080
* 현재 유저의 차단된 유저 목록 반환
81+
* 인증이 없거나 익명이면 빈 리스트 반환
8182
* @return 차단한 유저 목록 (빈 리스트가 제공될 수 있음)
8283
*/
8384
public List<ObjectId> getBlockedUsers() {
85+
ObjectId currentUserId = SecurityUtils.getCurrentUserIdOrNull();
86+
if (currentUserId == null) {
87+
return List.of();
88+
}
89+
8490
return blockRepository.findByUserId(SecurityUtils.getCurrentUserId())
8591
.map(BlockEntity::getBlockedUsers)
8692
.orElse(List.of());

src/main/java/inu/codin/codin/domain/board/notice/controller/NoticeController.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import inu.codin.codin.domain.board.notice.dto.response.NoticeDetailResponseDto;
77
import inu.codin.codin.domain.board.notice.dto.response.NoticePageResponse;
88
import inu.codin.codin.domain.board.notice.service.NoticeService;
9-
import inu.codin.codin.domain.post.service.PostService;
9+
import inu.codin.codin.domain.post.service.PostCommandService;
1010
import io.swagger.v3.oas.annotations.Operation;
1111
import io.swagger.v3.oas.annotations.tags.Tag;
1212
import jakarta.validation.Valid;
@@ -28,7 +28,7 @@
2828
public class NoticeController {
2929

3030
private final NoticeService noticeService;
31-
private final PostService postService;
31+
private final PostCommandService postCommandService;
3232

3333
/*
3434
===================
@@ -119,7 +119,7 @@ public ResponseEntity<SingleResponse<?>> deleteNoticeImage(
119119
@PathVariable String postId,
120120
@RequestParam String imageUrl) {
121121

122-
postService.deletePostImage(postId, imageUrl);
122+
postCommandService.deletePostImage(postId, imageUrl);
123123
return ResponseEntity.ok()
124124
.body(new SingleResponse<>(200, "공지사항 이미지가 삭제되었습니다.", null));
125125
}
@@ -130,7 +130,7 @@ public ResponseEntity<SingleResponse<?>> deleteNoticeImage(
130130
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
131131
@DeleteMapping("/{postId}")
132132
public ResponseEntity<SingleResponse<?>> softDeleteNotice(@PathVariable String postId) {
133-
postService.softDeletePost(postId);
133+
postCommandService.softDeletePost(postId);
134134
return ResponseEntity.ok()
135135
.body(new SingleResponse<>(200, "공지사항이 삭제되었습니다.", null));
136136
}

src/main/java/inu/codin/codin/domain/like/service/LikeService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import inu.codin.codin.domain.like.entity.LikeEntity;
99
import inu.codin.codin.domain.like.entity.LikeType;
1010
import inu.codin.codin.domain.like.repository.LikeRepository;
11-
import inu.codin.codin.domain.post.domain.comment.domain.reply.repository.ReplyCommentRepository;
1211
import inu.codin.codin.domain.post.domain.comment.repository.CommentRepository;
12+
import inu.codin.codin.domain.post.domain.comment.reply.repository.ReplyCommentRepository;
1313
import inu.codin.codin.domain.post.repository.PostRepository;
1414
import inu.codin.codin.infra.redis.config.RedisHealthChecker;
1515
import inu.codin.codin.infra.redis.service.RedisBestService;

src/main/java/inu/codin/codin/domain/notification/service/NotificationService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import inu.codin.codin.domain.notification.repository.NotificationRepository;
99
import inu.codin.codin.domain.post.domain.comment.entity.CommentEntity;
1010
import inu.codin.codin.domain.post.domain.comment.repository.CommentRepository;
11-
import inu.codin.codin.domain.post.domain.comment.domain.reply.entity.ReplyCommentEntity;
12-
import inu.codin.codin.domain.post.domain.comment.domain.reply.repository.ReplyCommentRepository;
11+
import inu.codin.codin.domain.post.domain.comment.reply.entity.ReplyCommentEntity;
12+
import inu.codin.codin.domain.post.domain.comment.reply.repository.ReplyCommentRepository;
1313
import inu.codin.codin.domain.post.entity.PostCategory;
1414
import inu.codin.codin.domain.post.entity.PostEntity;
1515
import inu.codin.codin.domain.post.repository.PostRepository;

src/main/java/inu/codin/codin/domain/post/controller/PostController.java

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
import inu.codin.codin.domain.post.dto.request.PostContentUpdateRequestDTO;
77
import inu.codin.codin.domain.post.dto.request.PostCreateRequestDTO;
88
import inu.codin.codin.domain.post.dto.request.PostStatusUpdateRequestDTO;
9-
import inu.codin.codin.domain.post.dto.response.PostDetailResponseDTO;
109
import inu.codin.codin.domain.post.dto.response.PostPageResponse;
10+
import inu.codin.codin.domain.post.dto.response.PostPageItemResponseDTO;
1111
import inu.codin.codin.domain.post.entity.PostCategory;
12-
import inu.codin.codin.domain.post.service.PostService;
12+
import inu.codin.codin.domain.post.service.PostCommandService;
13+
import inu.codin.codin.domain.post.service.PostQueryService;
1314
import io.swagger.v3.oas.annotations.Operation;
1415
import io.swagger.v3.oas.annotations.tags.Tag;
1516
import jakarta.validation.Valid;
1617
import jakarta.validation.constraints.NotNull;
1718
import jakarta.validation.constraints.Size;
19+
import lombok.RequiredArgsConstructor;
1820
import org.springframework.http.HttpStatus;
1921
import org.springframework.http.MediaType;
2022
import org.springframework.http.ResponseEntity;
@@ -27,40 +29,39 @@
2729
@RestController
2830
@RequestMapping("/posts")
2931
@Validated
32+
@RequiredArgsConstructor
3033
@Tag(name = "POST API", description = "게시글 API")
3134
public class PostController {
3235

33-
private final PostService postService;
34-
35-
public PostController(PostService postService) {
36-
this.postService = postService;
37-
}
36+
private final PostCommandService postCommandService;
37+
private final PostQueryService postQueryService;
3838

3939
@Operation(
4040
summary = "게시물 작성",
4141
description = "JSON 형식의 게시물 데이터(postContent)와 이미지 파일(postImages) 업로드"
4242
)
4343
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
44-
public ResponseEntity<SingleResponse<?>> createPost(
44+
public ResponseEntity<SingleResponse<Void>> createPost(
4545
@RequestPart("postContent") @Valid PostCreateRequestDTO postCreateRequestDTO,
4646
@RequestPart(value = "postImages", required = false) List<MultipartFile> postImages) {
4747

4848
// postImages가 null이면 빈 리스트로 처리
4949
if (postImages == null) postImages = List.of();
50+
postCommandService.createPost(postCreateRequestDTO, postImages);
5051
return ResponseEntity.status(HttpStatus.CREATED)
51-
.body(new SingleResponse<>(201, "게시물이 작성되었습니다.", postService.createPost(postCreateRequestDTO, postImages)));
52+
.body(new SingleResponse<>(201, "게시물이 작성되었습니다.", null));
5253
}
5354

5455
@Operation(
5556
summary = "게시물 내용 수정 및 이미지 수정&추가"
5657
)
5758
@PatchMapping(value = "/{postId}/content", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
58-
public ResponseEntity<SingleResponse<?>> updatePostContent(
59+
public ResponseEntity<SingleResponse<Void>> updatePostContent(
5960
@PathVariable String postId,
6061
@RequestPart("postContent") @Valid PostContentUpdateRequestDTO requestDTO,
6162
@RequestPart(value = "postImages", required = false) List<MultipartFile> postImages) {
6263

63-
postService.updatePostContent(postId, requestDTO, postImages);
64+
postCommandService.updatePostContent(postId, requestDTO, postImages);
6465
return ResponseEntity.status(HttpStatus.OK)
6566
.body(new SingleResponse<>(200, "게시물 내용이 수정되었습니다.", null));
6667
}
@@ -69,21 +70,21 @@ public ResponseEntity<SingleResponse<?>> updatePostContent(
6970
summary = "상태 수정"
7071
)
7172
@PatchMapping("/{postId}/status")
72-
public ResponseEntity<SingleResponse<?>> updatePostStatus(
73+
public ResponseEntity<SingleResponse<Void>> updatePostStatus(
7374
@PathVariable String postId,
7475
@RequestBody PostStatusUpdateRequestDTO requestDTO) {
75-
postService.updatePostStatus(postId, requestDTO);
76+
postCommandService.updatePostStatus(postId, requestDTO);
7677
return ResponseEntity.status(HttpStatus.OK)
7778
.body(new SingleResponse<>(200, "게시물 상태가 수정되었습니다.", null));
7879
}
7980

8081

8182
@Operation(summary = "게시물 익명 설정 수정")
8283
@PatchMapping("/{postId}/anonymous")
83-
public ResponseEntity<SingleResponse<?>> updatePostAnonymous(
84+
public ResponseEntity<SingleResponse<Void>> updatePostAnonymous(
8485
@PathVariable String postId,
8586
@RequestBody @Valid PostAnonymousUpdateRequestDTO requestDTO) {
86-
postService.updatePostAnonymous(postId, requestDTO);
87+
postCommandService.updatePostAnonymous(postId, requestDTO);
8788
return ResponseEntity.status(HttpStatus.OK)
8889
.body(new SingleResponse<>(200, "게시물 익명 설정이 수정되었습니다.", null));
8990
}
@@ -95,36 +96,36 @@ public ResponseEntity<SingleResponse<?>> updatePostAnonymous(
9596
@GetMapping("/category")
9697
public ResponseEntity<SingleResponse<PostPageResponse>> getAllPosts(@RequestParam PostCategory postCategory,
9798
@RequestParam("page") @NotNull int pageNumber) {
98-
PostPageResponse postpages= postService.getAllPosts(postCategory, pageNumber);
99+
PostPageResponse postpages= postQueryService.getAllPosts(postCategory, pageNumber);
99100
return ResponseEntity.ok()
100101
.body(new SingleResponse<>(200, "카테고리별 삭제 되지 않은 모든 게시물 조회 성공", postpages));
101102
}
102103

103104

104105
@Operation(summary = "해당 게시물 상세 조회 (댓글 조회는 Comment에서 따로 조회)")
105106
@GetMapping("/{postId}")
106-
public ResponseEntity<SingleResponse<PostDetailResponseDTO>> getPostWithDetail(@PathVariable String postId) {
107-
PostDetailResponseDTO post = postService.getPostWithDetail(postId);
107+
public ResponseEntity<SingleResponse<PostPageItemResponseDTO>> getPostWithDetail(@PathVariable String postId) {
108+
PostPageItemResponseDTO post = postQueryService.getPostWithDetail(postId);
108109
return ResponseEntity.ok()
109110
.body(new SingleResponse<>(200, "게시물 상세 조회 성공", post));
110111
}
111112

112113

113114
@Operation(summary = "게시물 이미지 삭제")
114115
@DeleteMapping("/{postId}/images")
115-
public ResponseEntity<SingleResponse<?>> deletePostImage(
116+
public ResponseEntity<SingleResponse<Void>> deletePostImage(
116117
@PathVariable String postId,
117118
@RequestParam String imageUrl) {
118119

119-
postService.deletePostImage(postId, imageUrl);
120+
postCommandService.deletePostImage(postId, imageUrl);
120121
return ResponseEntity.ok()
121122
.body(new SingleResponse<>(200, "게시물 이미지가 삭제되었습니다.", null));
122123
}
123124

124125
@Operation(summary = "게시물 삭제 (Soft Delete)")
125126
@DeleteMapping("/{postId}")
126-
public ResponseEntity<SingleResponse<?>> softDeletePost(@PathVariable String postId) {
127-
postService.softDeletePost(postId);
127+
public ResponseEntity<SingleResponse<Void>> softDeletePost(@PathVariable String postId) {
128+
postCommandService.softDeletePost(postId);
128129
return ResponseEntity.ok()
129130
.body(new SingleResponse<>(200, "게시물이 삭제되었습니다.", null));
130131
}
@@ -133,23 +134,23 @@ public ResponseEntity<SingleResponse<?>> softDeletePost(@PathVariable String pos
133134
summary = "검색 엔진"
134135
)
135136
@GetMapping("/search")
136-
public ResponseEntity<SingleResponse<?>> searchPosts(@RequestParam("keyword") @Size(min = 2) String keyword,
137+
public ResponseEntity<SingleResponse<PostPageResponse>> searchPosts(@RequestParam("keyword") @Size(min = 2) String keyword,
137138
@RequestParam("pageNumber") @NotNull int pageNumber){
138139
return ResponseEntity.ok()
139-
.body(new SingleResponse<>(200, "'"+keyword+"'"+"으로 검색된 게시글 반환 완료", postService.searchPosts(keyword, pageNumber)));
140+
.body(new SingleResponse<>(200, "'"+keyword+"'"+"으로 검색된 게시글 반환 완료", postQueryService.searchPosts(keyword, pageNumber)));
140141
}
141142

142143
@Operation(summary = "Top 3 베스트 게시글 가져오기")
143144
@GetMapping("/top3")
144-
public ResponseEntity<ListResponse<?>> getTop3BestPosts(){
145+
public ResponseEntity<ListResponse<PostPageItemResponseDTO>> getTop3BestPosts(){
145146
return ResponseEntity.ok()
146-
.body(new ListResponse<>(200, "Top3 베스트 게시글 반환 완료", postService.getTop3BestPosts()));
147+
.body(new ListResponse<>(200, "Top3 베스트 게시글 반환 완료", postQueryService.getTop3BestPosts()));
147148
}
148149

149150
@Operation(summary = "Top3로 선정된 게시글들 모두 가져오기")
150151
@GetMapping("/best")
151-
public ResponseEntity<SingleResponse<?>> getBestPosts(@RequestParam("pageNumber") int pageNumber){
152+
public ResponseEntity<SingleResponse<PostPageResponse>> getBestPosts(@RequestParam("pageNumber") int pageNumber){
152153
return ResponseEntity.ok()
153-
.body(new SingleResponse<>(200, "Top3로 선정된 게시글들 모두 반환 완료", postService.getBestPosts(pageNumber)));
154+
.body(new SingleResponse<>(200, "Top3로 선정된 게시글들 모두 반환 완료", postQueryService.getBestPosts(pageNumber)));
154155
}
155156
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package inu.codin.codin.domain.post.domain.best;
2+
3+
import inu.codin.codin.infra.redis.service.RedisBestService;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.bson.types.ObjectId;
7+
import org.springframework.data.domain.Page;
8+
import org.springframework.data.domain.PageRequest;
9+
import org.springframework.data.domain.Sort;
10+
import org.springframework.stereotype.Service;
11+
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.stream.Collectors;
16+
17+
@Slf4j
18+
@Service
19+
@RequiredArgsConstructor
20+
public class BestService {
21+
private final RedisBestService redisBestService;
22+
private final BestRepository bestRepository;
23+
24+
// [BestService] - Top 3 베스트 postId 목록 반환
25+
public List<String> getTop3BestPostIds() {
26+
Map<String, Double> bestPosts = redisBestService.getBests();
27+
return bestPosts.keySet()
28+
.stream()
29+
.limit(3)
30+
.collect(Collectors.toList());
31+
}
32+
33+
// [BestService] - BestEntity 페이지 반환
34+
public Page<BestEntity> getBestEntities(int pageNumber) {
35+
PageRequest pageRequest = PageRequest.of(pageNumber, 20, Sort.by("createdAt").descending());
36+
return bestRepository.findAll(pageRequest);
37+
}
38+
39+
// Redis에서 특정 베스트 게시물 삭제
40+
public void deleteBestPost(String postId) {
41+
redisBestService.deleteBest(postId);
42+
}
43+
44+
// [BestService] - 베스트 점수 적용 처리 래핑
45+
public void applyBestScore(ObjectId postId) {
46+
redisBestService.applyBestScore(1, postId);
47+
log.info("베스트 점수 적용. PostId: {}", postId);
48+
}
49+
50+
}

0 commit comments

Comments
 (0)