@@ -104,6 +104,12 @@ public class AuthController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/** 현재 회원 전체 프로필 (아바타·포인트 포함) — 재로그인 없이 최신값 동기화 */
|
||||
@GetMapping("/profile")
|
||||
public MemberResponse profile(@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
return authService.getProfile(current.getId());
|
||||
}
|
||||
|
||||
/** 가입정보(이름/이메일) 변경 */
|
||||
@PutMapping("/profile")
|
||||
public MemberResponse updateProfile(
|
||||
|
||||
@@ -319,6 +319,15 @@ public class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 현재 회원 전체 프로필(아바타·포인트 포함) — 재로그인 없이 최신값 동기화용 */
|
||||
public MemberResponse getProfile(Long memberId) {
|
||||
Member member = memberMapper.findById(memberId);
|
||||
if (member == null) {
|
||||
throw new ApiException(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다.");
|
||||
}
|
||||
return MemberResponse.from(member);
|
||||
}
|
||||
|
||||
/**
|
||||
* 가입정보(이름/이메일) 변경. 비밀번호 인증을 통과한 화면에서 호출된다.
|
||||
* 변경 후 세션(Redis + DB 백업)의 표시 이름도 동기화한다.
|
||||
|
||||
@@ -23,6 +23,7 @@ public class CommentResponse {
|
||||
private Integer upCount; // 추천 수
|
||||
private Integer downCount; // 비추천 수
|
||||
private String myVote; // 현재 사용자의 투표: UP / DOWN / null
|
||||
private Boolean hot; // 인기 댓글(추천 50+ & 1일 이내) 상단 노출
|
||||
|
||||
public static CommentResponse from(Comment c) {
|
||||
return CommentResponse.builder()
|
||||
|
||||
@@ -19,7 +19,9 @@ public class PostSummary {
|
||||
private String authorProfileImage;
|
||||
private Integer viewCount;
|
||||
private Integer commentCount;
|
||||
private Integer upCount; // 추천 수
|
||||
private Integer upCount; // 추천 수
|
||||
private Integer downCount; // 비추천 수
|
||||
private Boolean hot; // 인기 글(추천 50+ & 1일 이내) 상단 노출
|
||||
private Boolean blocked;
|
||||
private Boolean notice;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@@ -22,6 +22,12 @@ public interface PostMapper {
|
||||
@Param("keyword") String keyword,
|
||||
@Param("searchType") String searchType);
|
||||
|
||||
/** 인기 글(추천 minUp 이상 & hours 시간 이내) 상단 노출용 — 추천 많은 순 limit 건 */
|
||||
List<PostSummary> findHotPosts(@Param("category") String category,
|
||||
@Param("hours") int hours,
|
||||
@Param("minUp") int minUp,
|
||||
@Param("limit") int limit);
|
||||
|
||||
Post findById(@Param("id") Long id);
|
||||
|
||||
int insert(Post post);
|
||||
|
||||
@@ -49,6 +49,11 @@ public class BoardService {
|
||||
private static final String UP = "UP";
|
||||
private static final String DOWN = "DOWN";
|
||||
|
||||
// 인기 글/댓글: 등록 1일 이내 + 추천 50건 이상이면 상단에 최대 3건 노출
|
||||
private static final int HOT_WITHIN_HOURS = 24;
|
||||
private static final int HOT_MIN_UP = 50;
|
||||
private static final int HOT_MAX = 3;
|
||||
|
||||
/* ===================== 게시글 ===================== */
|
||||
|
||||
public PageResponse<PostSummary> list(int page, int size, String category, String tag, String keyword, String searchType) {
|
||||
@@ -61,6 +66,20 @@ public class BoardService {
|
||||
// 목록에는 태그를 표시하지 않으므로 게시글별 태그 조회를 생략한다.
|
||||
List<PostSummary> content = postMapper.findPage(offset, s, cat, blankToNull(tag), blankToNull(keyword), st);
|
||||
long total = postMapper.countPage(cat, blankToNull(tag), blankToNull(keyword), st);
|
||||
|
||||
// 1페이지·검색 없을 때만 인기 글을 최상단에 노출 (중복 제거)
|
||||
if (p == 1 && blankToNull(tag) == null && blankToNull(keyword) == null) {
|
||||
List<PostSummary> hot = postMapper.findHotPosts(cat, HOT_WITHIN_HOURS, HOT_MIN_UP, HOT_MAX);
|
||||
if (!hot.isEmpty()) {
|
||||
java.util.Set<Long> hotIds = new java.util.HashSet<>();
|
||||
hot.forEach(h -> { h.setHot(true); hotIds.add(h.getId()); });
|
||||
List<PostSummary> merged = new java.util.ArrayList<>(hot);
|
||||
for (PostSummary ps : content) {
|
||||
if (!hotIds.contains(ps.getId())) merged.add(ps);
|
||||
}
|
||||
content = merged;
|
||||
}
|
||||
}
|
||||
return PageResponse.of(content, p, s, total);
|
||||
}
|
||||
|
||||
@@ -206,7 +225,10 @@ public class BoardService {
|
||||
/** 게시글 추천/비추천 토글. 같은 표 재요청 시 취소, 반대면 전환. */
|
||||
@Transactional
|
||||
public VoteResponse votePost(Long postId, String type, SessionUser user) {
|
||||
mustFindPost(postId);
|
||||
Post post = mustFindPost(postId);
|
||||
if (post.getAuthorId().equals(user.getId())) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "본인 글에는 추천/비추천할 수 없습니다.");
|
||||
}
|
||||
Long memberId = user.getId();
|
||||
String existing = voteMapper.findPostVote(postId, memberId);
|
||||
boolean cancel = type.equals(existing);
|
||||
@@ -228,6 +250,9 @@ public class BoardService {
|
||||
if (comment == null) {
|
||||
throw new ApiException(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다.");
|
||||
}
|
||||
if (comment.getAuthorId().equals(user.getId())) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "본인 댓글에는 추천/비추천할 수 없습니다.");
|
||||
}
|
||||
Long memberId = user.getId();
|
||||
String existing = voteMapper.findCommentVote(commentId, memberId);
|
||||
boolean cancel = type.equals(existing);
|
||||
@@ -266,7 +291,7 @@ public class BoardService {
|
||||
for (CommentVoteSummary s : voteMapper.findCommentVoteSummary(postId, viewerId)) {
|
||||
voteByComment.put(s.getCommentId(), s);
|
||||
}
|
||||
List<CommentResponse> comments = commentMapper.findByPostId(postId).stream().map(c -> {
|
||||
List<CommentResponse> comments = new java.util.ArrayList<>(commentMapper.findByPostId(postId).stream().map(c -> {
|
||||
CommentResponse cr = CommentResponse.from(c);
|
||||
CommentVoteSummary s = voteByComment.get(c.getId());
|
||||
if (s != null) {
|
||||
@@ -275,7 +300,8 @@ public class BoardService {
|
||||
cr.setMyVote(s.getMyVote());
|
||||
}
|
||||
return cr;
|
||||
}).toList();
|
||||
}).toList());
|
||||
comments = orderWithHot(comments);
|
||||
|
||||
PostDetail detail = PostDetail.of(post, tags, comments);
|
||||
detail.setUpCount(voteMapper.countPostVotes(postId, UP));
|
||||
@@ -285,6 +311,27 @@ public class BoardService {
|
||||
return detail;
|
||||
}
|
||||
|
||||
/** 인기 댓글(추천 50+ & 1일 이내) 최대 3건을 추천 많은 순으로 상단에 올리고 hot 표시 */
|
||||
private List<CommentResponse> orderWithHot(List<CommentResponse> comments) {
|
||||
java.time.LocalDateTime cutoff = java.time.LocalDateTime.now().minusHours(HOT_WITHIN_HOURS);
|
||||
List<CommentResponse> hot = comments.stream()
|
||||
.filter(c -> c.getUpCount() != null && c.getUpCount() >= HOT_MIN_UP
|
||||
&& c.getCreatedAt() != null && c.getCreatedAt().isAfter(cutoff))
|
||||
.sorted((a, b) -> Integer.compare(b.getUpCount(), a.getUpCount()))
|
||||
.limit(HOT_MAX)
|
||||
.toList();
|
||||
if (hot.isEmpty()) {
|
||||
return comments;
|
||||
}
|
||||
java.util.Set<Long> hotIds = new java.util.HashSet<>();
|
||||
hot.forEach(c -> { c.setHot(true); hotIds.add(c.getId()); });
|
||||
List<CommentResponse> ordered = new java.util.ArrayList<>(hot);
|
||||
for (CommentResponse c : comments) {
|
||||
if (!hotIds.contains(c.getId())) ordered.add(c);
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/** 선택된 태그 ID 중 실제 DB에 존재하는 것만 게시글에 연결 (신규 태그 생성 없음) */
|
||||
private void applyTags(Long postId, List<Long> tagIds) {
|
||||
if (tagIds == null || tagIds.isEmpty()) {
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
p.id, p.title, p.author_id, p.author_name, p.view_count, p.blocked, p.notice, p.created_at,
|
||||
m.google_picture AS author_google_picture, m.profile_image AS author_profile_image,
|
||||
(SELECT COUNT(*) FROM comment c WHERE c.post_id = p.id) AS comment_count,
|
||||
(SELECT COUNT(*) FROM post_vote v WHERE v.post_id = p.id AND v.vote_type = 'UP') AS up_count
|
||||
(SELECT COUNT(*) FROM post_vote v WHERE v.post_id = p.id AND v.vote_type = 'UP') AS up_count,
|
||||
(SELECT COUNT(*) FROM post_vote v WHERE v.post_id = p.id AND v.vote_type = 'DOWN') AS down_count
|
||||
FROM post p
|
||||
LEFT JOIN member m ON m.id = p.author_id
|
||||
<include refid="filter"/>
|
||||
@@ -46,6 +47,24 @@
|
||||
LIMIT #{size} OFFSET #{offset}
|
||||
</select>
|
||||
|
||||
<!-- 인기 글: 등록 #{hours}시간 이내 + 추천 #{minUp}건 이상, 추천 많은 순 #{limit}건 (상단 노출용) -->
|
||||
<select id="findHotPosts" resultType="com.sb.web.board.dto.PostSummary">
|
||||
SELECT
|
||||
p.id, p.title, p.author_id, p.author_name, p.view_count, p.blocked, p.notice, p.created_at,
|
||||
m.google_picture AS author_google_picture, m.profile_image AS author_profile_image,
|
||||
(SELECT COUNT(*) FROM comment c WHERE c.post_id = p.id) AS comment_count,
|
||||
(SELECT COUNT(*) FROM post_vote v WHERE v.post_id = p.id AND v.vote_type = 'UP') AS up_count,
|
||||
(SELECT COUNT(*) FROM post_vote v WHERE v.post_id = p.id AND v.vote_type = 'DOWN') AS down_count
|
||||
FROM post p
|
||||
LEFT JOIN member m ON m.id = p.author_id
|
||||
WHERE p.notice = 0
|
||||
AND (#{category} IS NULL OR p.category = #{category})
|
||||
AND p.created_at >= NOW() - INTERVAL #{hours} HOUR
|
||||
AND (SELECT COUNT(*) FROM post_vote v WHERE v.post_id = p.id AND v.vote_type = 'UP') >= #{minUp}
|
||||
ORDER BY (SELECT COUNT(*) FROM post_vote v WHERE v.post_id = p.id AND v.vote_type = 'UP') DESC, p.id DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<select id="countPage" resultType="long">
|
||||
SELECT COUNT(*)
|
||||
FROM post p
|
||||
|
||||
Reference in New Issue
Block a user