@@ -104,6 +104,12 @@ public class AuthController {
|
|||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 현재 회원 전체 프로필 (아바타·포인트 포함) — 재로그인 없이 최신값 동기화 */
|
||||||
|
@GetMapping("/profile")
|
||||||
|
public MemberResponse profile(@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||||
|
return authService.getProfile(current.getId());
|
||||||
|
}
|
||||||
|
|
||||||
/** 가입정보(이름/이메일) 변경 */
|
/** 가입정보(이름/이메일) 변경 */
|
||||||
@PutMapping("/profile")
|
@PutMapping("/profile")
|
||||||
public MemberResponse updateProfile(
|
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 백업)의 표시 이름도 동기화한다.
|
* 변경 후 세션(Redis + DB 백업)의 표시 이름도 동기화한다.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public class CommentResponse {
|
|||||||
private Integer upCount; // 추천 수
|
private Integer upCount; // 추천 수
|
||||||
private Integer downCount; // 비추천 수
|
private Integer downCount; // 비추천 수
|
||||||
private String myVote; // 현재 사용자의 투표: UP / DOWN / null
|
private String myVote; // 현재 사용자의 투표: UP / DOWN / null
|
||||||
|
private Boolean hot; // 인기 댓글(추천 50+ & 1일 이내) 상단 노출
|
||||||
|
|
||||||
public static CommentResponse from(Comment c) {
|
public static CommentResponse from(Comment c) {
|
||||||
return CommentResponse.builder()
|
return CommentResponse.builder()
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ public class PostSummary {
|
|||||||
private Integer viewCount;
|
private Integer viewCount;
|
||||||
private Integer commentCount;
|
private Integer commentCount;
|
||||||
private Integer upCount; // 추천 수
|
private Integer upCount; // 추천 수
|
||||||
|
private Integer downCount; // 비추천 수
|
||||||
|
private Boolean hot; // 인기 글(추천 50+ & 1일 이내) 상단 노출
|
||||||
private Boolean blocked;
|
private Boolean blocked;
|
||||||
private Boolean notice;
|
private Boolean notice;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ public interface PostMapper {
|
|||||||
@Param("keyword") String keyword,
|
@Param("keyword") String keyword,
|
||||||
@Param("searchType") String searchType);
|
@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);
|
Post findById(@Param("id") Long id);
|
||||||
|
|
||||||
int insert(Post post);
|
int insert(Post post);
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ public class BoardService {
|
|||||||
private static final String UP = "UP";
|
private static final String UP = "UP";
|
||||||
private static final String DOWN = "DOWN";
|
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) {
|
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);
|
List<PostSummary> content = postMapper.findPage(offset, s, cat, blankToNull(tag), blankToNull(keyword), st);
|
||||||
long total = postMapper.countPage(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);
|
return PageResponse.of(content, p, s, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +225,10 @@ public class BoardService {
|
|||||||
/** 게시글 추천/비추천 토글. 같은 표 재요청 시 취소, 반대면 전환. */
|
/** 게시글 추천/비추천 토글. 같은 표 재요청 시 취소, 반대면 전환. */
|
||||||
@Transactional
|
@Transactional
|
||||||
public VoteResponse votePost(Long postId, String type, SessionUser user) {
|
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();
|
Long memberId = user.getId();
|
||||||
String existing = voteMapper.findPostVote(postId, memberId);
|
String existing = voteMapper.findPostVote(postId, memberId);
|
||||||
boolean cancel = type.equals(existing);
|
boolean cancel = type.equals(existing);
|
||||||
@@ -228,6 +250,9 @@ public class BoardService {
|
|||||||
if (comment == null) {
|
if (comment == null) {
|
||||||
throw new ApiException(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다.");
|
throw new ApiException(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다.");
|
||||||
}
|
}
|
||||||
|
if (comment.getAuthorId().equals(user.getId())) {
|
||||||
|
throw new ApiException(HttpStatus.BAD_REQUEST, "본인 댓글에는 추천/비추천할 수 없습니다.");
|
||||||
|
}
|
||||||
Long memberId = user.getId();
|
Long memberId = user.getId();
|
||||||
String existing = voteMapper.findCommentVote(commentId, memberId);
|
String existing = voteMapper.findCommentVote(commentId, memberId);
|
||||||
boolean cancel = type.equals(existing);
|
boolean cancel = type.equals(existing);
|
||||||
@@ -266,7 +291,7 @@ public class BoardService {
|
|||||||
for (CommentVoteSummary s : voteMapper.findCommentVoteSummary(postId, viewerId)) {
|
for (CommentVoteSummary s : voteMapper.findCommentVoteSummary(postId, viewerId)) {
|
||||||
voteByComment.put(s.getCommentId(), s);
|
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);
|
CommentResponse cr = CommentResponse.from(c);
|
||||||
CommentVoteSummary s = voteByComment.get(c.getId());
|
CommentVoteSummary s = voteByComment.get(c.getId());
|
||||||
if (s != null) {
|
if (s != null) {
|
||||||
@@ -275,7 +300,8 @@ public class BoardService {
|
|||||||
cr.setMyVote(s.getMyVote());
|
cr.setMyVote(s.getMyVote());
|
||||||
}
|
}
|
||||||
return cr;
|
return cr;
|
||||||
}).toList();
|
}).toList());
|
||||||
|
comments = orderWithHot(comments);
|
||||||
|
|
||||||
PostDetail detail = PostDetail.of(post, tags, comments);
|
PostDetail detail = PostDetail.of(post, tags, comments);
|
||||||
detail.setUpCount(voteMapper.countPostVotes(postId, UP));
|
detail.setUpCount(voteMapper.countPostVotes(postId, UP));
|
||||||
@@ -285,6 +311,27 @@ public class BoardService {
|
|||||||
return detail;
|
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에 존재하는 것만 게시글에 연결 (신규 태그 생성 없음) */
|
/** 선택된 태그 ID 중 실제 DB에 존재하는 것만 게시글에 연결 (신규 태그 생성 없음) */
|
||||||
private void applyTags(Long postId, List<Long> tagIds) {
|
private void applyTags(Long postId, List<Long> tagIds) {
|
||||||
if (tagIds == null || tagIds.isEmpty()) {
|
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,
|
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,
|
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 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
|
FROM post p
|
||||||
LEFT JOIN member m ON m.id = p.author_id
|
LEFT JOIN member m ON m.id = p.author_id
|
||||||
<include refid="filter"/>
|
<include refid="filter"/>
|
||||||
@@ -46,6 +47,24 @@
|
|||||||
LIMIT #{size} OFFSET #{offset}
|
LIMIT #{size} OFFSET #{offset}
|
||||||
</select>
|
</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 id="countPage" resultType="long">
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM post p
|
FROM post p
|
||||||
|
|||||||
Reference in New Issue
Block a user