내역 저장 시 현재 월 모든 날에 기록이 있으면 월 1회 랜덤 1~100P 지급. 전월 소급 입력 파밍 방지(현재 월만 인정), 화면 안내 없음(포인트 내역에만). - AccountEntryMapper.countDistinctEntryDays, PointMapper.countByReason - PointService.awardMonthComplete(reason=MONTH_COMPLETE_yyyyMM 로 월 1회) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -96,4 +96,9 @@ public interface AccountEntryMapper {
|
||||
|
||||
/** 복수 항목의 태그를 한 번에 조회 — {entryId, tagName} 맵 반환 */
|
||||
List<Map<String, Object>> findTagsByEntryIds(@Param("entryIds") List<Long> entryIds);
|
||||
|
||||
/** 특정 연·월에 기록이 있는 서로 다른 날짜 수 (한 달 개근 판정용) */
|
||||
int countDistinctEntryDays(@Param("memberId") Long memberId,
|
||||
@Param("year") int year,
|
||||
@Param("month") int month);
|
||||
}
|
||||
|
||||
@@ -256,9 +256,28 @@ public class AccountService {
|
||||
mapper.insert(entry);
|
||||
applyTags(entry.getId(), req.getTagIds(), memberId);
|
||||
pointService.awardAccountEntry(memberId);
|
||||
awardIfMonthComplete(entry.getEntryDate(), memberId);
|
||||
return toResponse(mapper.findByIdAndMember(entry.getId(), memberId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 내역의 월이 '현재 진행 중인 달'이고 그 달 모든 날에 기록이 있으면 개근 보너스(랜덤 1~100P) 지급.
|
||||
* 지난달을 소급 입력해 채우는 파밍을 막기 위해 현재 월만 인정한다(전 월 제외). 판정 실패는 저장에 영향 없음.
|
||||
*/
|
||||
private void awardIfMonthComplete(java.time.LocalDate entryDate, Long memberId) {
|
||||
if (entryDate == null) return;
|
||||
try {
|
||||
java.time.LocalDate today = java.time.LocalDate.now();
|
||||
if (entryDate.getYear() != today.getYear() || entryDate.getMonthValue() != today.getMonthValue()) return;
|
||||
int recorded = mapper.countDistinctEntryDays(memberId, entryDate.getYear(), entryDate.getMonthValue());
|
||||
if (recorded >= entryDate.lengthOfMonth()) {
|
||||
pointService.awardMonthComplete(memberId, entryDate.getYear(), entryDate.getMonthValue());
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
// 개근 판정 오류는 무시(내역 저장은 이미 완료)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AccountEntryResponse update(Long id, AccountEntryRequest req, Long memberId) {
|
||||
AccountEntry entry = mustFind(id, memberId);
|
||||
|
||||
@@ -52,6 +52,10 @@ public class PointService {
|
||||
public static final int ACCOUNT_ENTRY_DAILY_LIMIT = 5; // 10P × 5 = 일 50P
|
||||
public static final String REASON_ACCOUNT_ENTRY = "ACCOUNT_ENTRY";
|
||||
|
||||
// 한 달 개근(그 달 모든 날에 기록) — 랜덤 1~100P, 월 1회. reason 에 연월 인코딩(MONTH_COMPLETE_yyyyMM)
|
||||
public static final int MONTH_COMPLETE_MAX_POINT = 100;
|
||||
public static final String REASON_MONTH_COMPLETE_PREFIX = "MONTH_COMPLETE_";
|
||||
|
||||
// 도배 방지 키워드 — 포함 시 작성 포인트 미지급
|
||||
private static final List<String> SPAM_KEYWORDS = List.of(
|
||||
"http://", "https://", "bit.ly", "goo.gl",
|
||||
@@ -143,6 +147,21 @@ public class PointService {
|
||||
return ACCOUNT_ENTRY_POINT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 한 달의 모든 날에 기록이 있을 때 주는 랜덤 보너스(1~100P). 월 1회만 지급한다.
|
||||
* reason 에 연월을 인코딩(MONTH_COMPLETE_yyyyMM)해 같은 달 중복 지급을 막는다.
|
||||
* @return 지급 포인트(이미 지급됐으면 0)
|
||||
*/
|
||||
@Transactional
|
||||
public int awardMonthComplete(Long memberId, int year, int month) {
|
||||
String reason = REASON_MONTH_COMPLETE_PREFIX + String.format("%04d%02d", year, month);
|
||||
if (pointMapper.countByReason(memberId, reason) > 0) return 0; // 이미 지급됨
|
||||
int amount = java.util.concurrent.ThreadLocalRandom.current().nextInt(1, MONTH_COMPLETE_MAX_POINT + 1); // 1~100
|
||||
pointMapper.insertHistory(memberId, amount, reason);
|
||||
pointMapper.addPoints(memberId, amount);
|
||||
return amount;
|
||||
}
|
||||
|
||||
public long getPoints(Long memberId) {
|
||||
Long p = pointMapper.getPoints(memberId);
|
||||
return p == null ? 0L : p;
|
||||
|
||||
@@ -15,6 +15,9 @@ public interface PointMapper {
|
||||
/** 오늘 특정 사유로 지급된 횟수 (일일 한도 계산용) */
|
||||
int countTodayGrants(@Param("memberId") Long memberId, @Param("reason") String reason);
|
||||
|
||||
/** 특정 사유로 지금까지 지급된 총 횟수 (월 1회 등 전체기간 중복 방지용) */
|
||||
int countByReason(@Param("memberId") Long memberId, @Param("reason") String reason);
|
||||
|
||||
int insertHistory(@Param("memberId") Long memberId, @Param("amount") int amount, @Param("reason") String reason);
|
||||
|
||||
int addPoints(@Param("memberId") Long memberId, @Param("amount") int amount);
|
||||
|
||||
@@ -246,4 +246,13 @@
|
||||
ORDER BY aet.entry_id, t.name
|
||||
</select>
|
||||
|
||||
<!-- 특정 연·월에 기록이 있는 서로 다른 날짜 수(한 달 개근 판정) -->
|
||||
<select id="countDistinctEntryDays" resultType="int">
|
||||
SELECT COUNT(DISTINCT DAY(entry_date))
|
||||
FROM account_entry
|
||||
WHERE member_id = #{memberId}
|
||||
AND YEAR(entry_date) = #{year}
|
||||
AND MONTH(entry_date) = #{month}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
WHERE member_id = #{memberId} AND reason = #{reason} AND created_at >= CURDATE()
|
||||
</select>
|
||||
|
||||
<select id="countByReason" resultType="int">
|
||||
SELECT COUNT(*) FROM point_history
|
||||
WHERE member_id = #{memberId} AND reason = #{reason}
|
||||
</select>
|
||||
|
||||
<insert id="insertHistory">
|
||||
INSERT INTO point_history (member_id, amount, reason, created_at)
|
||||
VALUES (#{memberId}, #{amount}, #{reason}, NOW())
|
||||
|
||||
Reference in New Issue
Block a user