작성: 10P, 일 50P 한도(5회), 스팸 키워드 포함 시 미지급 추천 받음(작성자): +5P, 일 50P 한도 비추천 받음(작성자): -1P 남의 글 추천(투표자): +1P, 일 10P 한도 추천 취소(투표자): -1P addPoints에 GREATEST(0,...) 적용 — 잔액 음수 방지
This commit is contained in:
@@ -191,7 +191,7 @@ public class BoardService {
|
|||||||
.build();
|
.build();
|
||||||
postMapper.insert(post);
|
postMapper.insert(post);
|
||||||
applyTags(post.getId(), req.getTagIds());
|
applyTags(post.getId(), req.getTagIds());
|
||||||
pointService.awardBoardWrite(author.getId()); // 글 작성 보상(일 3회 한도)
|
pointService.awardBoardWrite(author.getId(), req.getContent());
|
||||||
return assemble(mustFindPost(post.getId()), author);
|
return assemble(mustFindPost(post.getId()), author);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ public class BoardService {
|
|||||||
.content(req.getContent())
|
.content(req.getContent())
|
||||||
.build();
|
.build();
|
||||||
commentMapper.insert(comment);
|
commentMapper.insert(comment);
|
||||||
pointService.awardBoardWrite(author.getId()); // 댓글 작성 보상(일 3회 한도, 글과 합산)
|
pointService.awardBoardWrite(author.getId(), req.getContent());
|
||||||
return CommentResponse.from(commentMapper.findById(comment.getId()));
|
return CommentResponse.from(commentMapper.findById(comment.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +328,7 @@ public class BoardService {
|
|||||||
} else {
|
} else {
|
||||||
voteMapper.upsertPostVote(postId, memberId, type);
|
voteMapper.upsertPostVote(postId, memberId, type);
|
||||||
}
|
}
|
||||||
|
pointService.applyVotePoints(post.getAuthorId(), memberId, existing, type);
|
||||||
return new VoteResponse(
|
return new VoteResponse(
|
||||||
voteMapper.countPostVotes(postId, UP),
|
voteMapper.countPostVotes(postId, UP),
|
||||||
voteMapper.countPostVotes(postId, DOWN),
|
voteMapper.countPostVotes(postId, DOWN),
|
||||||
@@ -352,6 +353,7 @@ public class BoardService {
|
|||||||
} else {
|
} else {
|
||||||
voteMapper.upsertCommentVote(commentId, memberId, type);
|
voteMapper.upsertCommentVote(commentId, memberId, type);
|
||||||
}
|
}
|
||||||
|
pointService.applyVotePoints(comment.getAuthorId(), memberId, existing, type);
|
||||||
return new VoteResponse(
|
return new VoteResponse(
|
||||||
voteMapper.countCommentVotes(commentId, UP),
|
voteMapper.countCommentVotes(commentId, UP),
|
||||||
voteMapper.countCommentVotes(commentId, DOWN),
|
voteMapper.countCommentVotes(commentId, DOWN),
|
||||||
|
|||||||
@@ -5,9 +5,18 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 활동 포인트 적립/조회.
|
* 활동 포인트 적립/조회.
|
||||||
* 게시판 글/댓글 작성 시 10점(하루 3회), 가계부 내역 작성 시 10점(하루 50점 한도).
|
*
|
||||||
|
* 정책 요약:
|
||||||
|
* - 게시글/댓글 작성: 10P, 일 5회(50P) 한도, 스팸 키워드 포함 시 미지급
|
||||||
|
* - 추천 받음(글 작성자): +5P, 일 50P 한도
|
||||||
|
* - 비추천 받음(글 작성자): -1P
|
||||||
|
* - 남의 글 추천(투표자): +1P, 일 10P 한도
|
||||||
|
* - 추천 취소(투표자): -1P
|
||||||
|
* - 가계부 내역 작성: 10P, 일 5회(50P) 한도
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -15,31 +24,112 @@ public class PointService {
|
|||||||
|
|
||||||
private final PointMapper pointMapper;
|
private final PointMapper pointMapper;
|
||||||
|
|
||||||
public static final int BOARD_WRITE_POINT = 10;
|
// 게시글/댓글 작성
|
||||||
public static final int BOARD_WRITE_DAILY_LIMIT = 3;
|
public static final int BOARD_WRITE_POINT = 10;
|
||||||
public static final String REASON_BOARD_WRITE = "BOARD_WRITE";
|
public static final int BOARD_WRITE_DAILY_LIMIT = 5; // 10P × 5 = 일 50P
|
||||||
|
public static final String REASON_BOARD_WRITE = "BOARD_WRITE";
|
||||||
|
|
||||||
public static final int ACCOUNT_ENTRY_POINT = 10;
|
// 추천 받음 (글 작성자)
|
||||||
public static final int ACCOUNT_ENTRY_DAILY_LIMIT = 5; // 10pt × 5 = 일 50pt 한도
|
public static final int POST_UPVOTED_POINT = 5;
|
||||||
public static final String REASON_ACCOUNT_ENTRY = "ACCOUNT_ENTRY";
|
public static final int POST_UPVOTED_DAILY_CAP = 10; // 5P × 10 = 일 50P
|
||||||
|
public static final String REASON_POST_UPVOTED = "POST_UPVOTED";
|
||||||
|
|
||||||
|
// 비추천 받음 (글 작성자)
|
||||||
|
public static final int POST_DOWNVOTED_POINT = -1;
|
||||||
|
public static final String REASON_POST_DOWNVOTED = "POST_DOWNVOTED";
|
||||||
|
|
||||||
|
// 남의 글 추천 (투표자)
|
||||||
|
public static final int VOTE_REWARD_POINT = 1;
|
||||||
|
public static final int VOTE_REWARD_DAILY_CAP = 10; // 1P × 10 = 일 10P
|
||||||
|
public static final String REASON_VOTE_REWARD = "VOTE_REWARD";
|
||||||
|
|
||||||
|
// 추천 취소 (투표자)
|
||||||
|
public static final int VOTE_CANCEL_POINT = -1;
|
||||||
|
public static final String REASON_VOTE_CANCEL = "VOTE_CANCEL";
|
||||||
|
|
||||||
|
// 가계부 내역 작성
|
||||||
|
public static final int ACCOUNT_ENTRY_POINT = 10;
|
||||||
|
public static final int ACCOUNT_ENTRY_DAILY_LIMIT = 5; // 10P × 5 = 일 50P
|
||||||
|
public static final String REASON_ACCOUNT_ENTRY = "ACCOUNT_ENTRY";
|
||||||
|
|
||||||
|
// 도배 방지 키워드 — 포함 시 작성 포인트 미지급
|
||||||
|
private static final List<String> SPAM_KEYWORDS = List.of(
|
||||||
|
"http://", "https://", "bit.ly", "goo.gl",
|
||||||
|
"무료지급", "무료쿠폰", "공짜", "홍보", "광고", "클릭하세요",
|
||||||
|
"카지노", "토토", "바카라", "도박"
|
||||||
|
);
|
||||||
|
|
||||||
|
public boolean isSpam(String content) {
|
||||||
|
if (content == null || content.isBlank()) return false;
|
||||||
|
String lower = content.toLowerCase();
|
||||||
|
return SPAM_KEYWORDS.stream().anyMatch(lower::contains);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 게시판 글/댓글 작성 보상. 오늘 지급 횟수가 한도 미만일 때만 지급한다.
|
* 게시글/댓글 작성 보상. 스팸 키워드 포함 또는 일 한도 초과 시 미지급.
|
||||||
* @return 지급되면 지급 포인트, 한도 초과로 미지급이면 0
|
* @return 지급 포인트 (미지급이면 0)
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public int awardBoardWrite(Long memberId) {
|
public int awardBoardWrite(Long memberId, String content) {
|
||||||
|
if (isSpam(content)) return 0;
|
||||||
int today = pointMapper.countTodayGrants(memberId, REASON_BOARD_WRITE);
|
int today = pointMapper.countTodayGrants(memberId, REASON_BOARD_WRITE);
|
||||||
if (today >= BOARD_WRITE_DAILY_LIMIT) {
|
if (today >= BOARD_WRITE_DAILY_LIMIT) return 0;
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
pointMapper.insertHistory(memberId, BOARD_WRITE_POINT, REASON_BOARD_WRITE);
|
pointMapper.insertHistory(memberId, BOARD_WRITE_POINT, REASON_BOARD_WRITE);
|
||||||
pointMapper.addPoints(memberId, BOARD_WRITE_POINT);
|
pointMapper.addPoints(memberId, BOARD_WRITE_POINT);
|
||||||
return BOARD_WRITE_POINT;
|
return BOARD_WRITE_POINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 가계부 내역 작성 보상. 건당 10점, 하루 5건(50점)까지만 지급한다.
|
* 투표 포인트 처리. 이전 투표(existing)와 새 요청(newType)을 비교해 적절한 포인트를 지급/차감한다.
|
||||||
|
*
|
||||||
|
* 케이스:
|
||||||
|
* - existing==newType (취소): UP 취소 → 투표자 -1P / DOWN 취소 → 변동 없음
|
||||||
|
* - 새 UP: 작성자 +5P(일 50P 한도), 투표자 +1P(일 10P 한도)
|
||||||
|
* - 새 DOWN: 작성자 -1P
|
||||||
|
* - UP→DOWN 전환: 투표자 -1P(취소), 작성자 -1P(비추)
|
||||||
|
* - DOWN→UP 전환: 작성자 +5P, 투표자 +1P
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void applyVotePoints(Long authorId, Long voterId, String existing, String newType) {
|
||||||
|
boolean cancel = newType.equals(existing);
|
||||||
|
|
||||||
|
if (cancel) {
|
||||||
|
// 같은 타입 재요청 = 취소
|
||||||
|
if ("UP".equals(existing)) {
|
||||||
|
// 투표자가 받았던 +1P 회수
|
||||||
|
pointMapper.insertHistory(voterId, VOTE_CANCEL_POINT, REASON_VOTE_CANCEL);
|
||||||
|
pointMapper.addPoints(voterId, VOTE_CANCEL_POINT);
|
||||||
|
}
|
||||||
|
// DOWN 취소: 포인트 변동 없음
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UP→DOWN 전환: 투표자의 추천 보상 먼저 회수
|
||||||
|
if ("UP".equals(existing)) {
|
||||||
|
pointMapper.insertHistory(voterId, VOTE_CANCEL_POINT, REASON_VOTE_CANCEL);
|
||||||
|
pointMapper.addPoints(voterId, VOTE_CANCEL_POINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("UP".equals(newType)) {
|
||||||
|
// 작성자: +5P (일 한도)
|
||||||
|
if (pointMapper.countTodayGrants(authorId, REASON_POST_UPVOTED) < POST_UPVOTED_DAILY_CAP) {
|
||||||
|
pointMapper.insertHistory(authorId, POST_UPVOTED_POINT, REASON_POST_UPVOTED);
|
||||||
|
pointMapper.addPoints(authorId, POST_UPVOTED_POINT);
|
||||||
|
}
|
||||||
|
// 투표자: +1P (일 한도)
|
||||||
|
if (pointMapper.countTodayGrants(voterId, REASON_VOTE_REWARD) < VOTE_REWARD_DAILY_CAP) {
|
||||||
|
pointMapper.insertHistory(voterId, VOTE_REWARD_POINT, REASON_VOTE_REWARD);
|
||||||
|
pointMapper.addPoints(voterId, VOTE_REWARD_POINT);
|
||||||
|
}
|
||||||
|
} else if ("DOWN".equals(newType)) {
|
||||||
|
// 작성자: -1P
|
||||||
|
pointMapper.insertHistory(authorId, POST_DOWNVOTED_POINT, REASON_POST_DOWNVOTED);
|
||||||
|
pointMapper.addPoints(authorId, POST_DOWNVOTED_POINT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 가계부 내역 작성 보상. 건당 10P, 하루 5건(50P)까지만 지급한다.
|
||||||
* @return 지급되면 지급 포인트, 한도 초과로 미지급이면 0
|
* @return 지급되면 지급 포인트, 한도 초과로 미지급이면 0
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<update id="addPoints">
|
<update id="addPoints">
|
||||||
UPDATE member SET points = points + #{amount}, updated_at = NOW() WHERE id = #{memberId}
|
UPDATE member SET points = GREATEST(0, points + #{amount}), updated_at = NOW() WHERE id = #{memberId}
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
<select id="getPoints" parameterType="long" resultType="long">
|
<select id="getPoints" parameterType="long" resultType="long">
|
||||||
|
|||||||
Reference in New Issue
Block a user