Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a3c9a37
#250 [Fix] 투표 비율 계산 기준을 preVoteOption에서 postVoteOption으로 수정 (#252)
HYH0804 May 27, 2026
a38c141
#254 [Refactor] 관점 등록/수정 시 GPT 검수 코드 제거 (#256)
HYH0804 Jun 3, 2026
cee2c5d
#255 [Refactor] VoteStatsResponse OptionStat에 title 필드 추가 (#257)
HYH0804 Jun 3, 2026
53533b8
#258 [Feat] 출석체크 API 추가
si-zero Jun 3, 2026
77db7a1
#260 [Feat] 스웨거 어드민/유저 분리
si-zero Jun 4, 2026
a9d4293
[Feat] 알림기능 (#263)
HYH0804 Jun 11, 2026
b5a681b
#262 [Fix] APNs 자격증명 S3 다운로드 시 임시파일 충돌 오류 수정
HYH0804 Jun 11, 2026
d281fa3
#264 [Fix] AdMob SSV 포인트 미적립 버그 수정
si-zero Jun 11, 2026
968d6a3
#262 merge (#266)
HYH0804 Jun 14, 2026
d041631
[Chore] adMob 크롤링 파일 설정: adMob 크롤링 파일 설정 #269
si-zero Jun 16, 2026
ec25882
#270 [Feat] 설정 파일 추가
si-zero Jun 16, 2026
4c45999
[Chore] JWT필터 설정 #271
si-zero Jun 16, 2026
e0c9c1c
#275 [Fix] AdMob SSV 콜백 파라미터 optional 처리 및 서명 검증 활성화
si-zero Jun 18, 2026
ab2cb78
#277 [Fix] AdMob SSV 콜백 URL 검증 요청 처리 실패
si-zero Jun 18, 2026
04d947c
#279 [Fix] AdMob SSV 엔드포인트 검증 실패 시 항상 200 반환
si-zero Jun 18, 2026
d9ea3b5
#283 [Fix] /me/content-activities voteSide null 반환 버그
si-zero Jun 19, 2026
ec27b01
#285 [Feat] content-activities 응답에 option_title 필드 추가
si-zero Jun 20, 2026
6663e7a
#287 [Fix] AdMob SSV 서명 검증 오류로 인한 포인트 미지급 버그
si-zero Jun 24, 2026
39aca84
#289 [Fix] 오늘의 배틀 개수 변경
si-zero Jul 5, 2026
11e2da1
#293 [Fix] 사전투표 철학자 아이콘 미노출
si-zero Jul 5, 2026
aa449f0
#297 [Fix] 댓글 작성 시각이 9시간 전으로 표시되는 버그
si-zero Jul 6, 2026
3259a1a
#301 [Fix] robots.txt 파일 부재로 AdMob app-ads.txt 크롤링 차단
si-zero Jul 6, 2026
e91819b
#305 [Fix] S3 오디오 조각 재다운로드 시 FileAlreadyExistsException으로 TTS 파이프라인 실패
si-zero Jul 6, 2026
d64d5ca
#309 [Fix] JwtFilter 자체 화이트리스트에 /robots.txt 누락으로 401 발생
si-zero Jul 6, 2026
ff16d85
#316 [Fix] 포인트 정책표 대비 코드 불일치 4건 수정
si-zero Jul 7, 2026
f50c749
Merge branch 'main' into dev
si-zero Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
public class AttendanceService {

private static final ZoneId SEOUL_ZONE = ZoneId.of("Asia/Seoul");
private static final int STREAK_BONUS_THRESHOLD = 7;
private static final int STREAK_REWARD_POINTS = CreditType.ATTENDANCE_STREAK.getDefaultAmount();

private final AttendanceRecordRepository attendanceRecordRepository;
Expand All @@ -54,7 +53,11 @@ public CheckResponse check() {
throw new CustomException(ErrorCode.ATTENDANCE_ALREADY_CHECKED);
}

int dailyPoints = CreditType.TODAY_CREDIT.getDefaultAmount();
// 월~토 개근 후 맞는 일요일에는 5P 대신 7P를 지급(대체), 그 외에는 매일 5P
boolean perfectWeekBonus = isSundayWithFullWeekAttendance(user.getId(), today);
CreditType creditType = perfectWeekBonus ? CreditType.ATTENDANCE_STREAK : CreditType.TODAY_CREDIT;
int dailyPoints = creditType.getDefaultAmount();

AttendanceRecord record = AttendanceRecord.builder()
.user(user)
.attendedDate(today)
Expand All @@ -63,19 +66,19 @@ public CheckResponse check() {
attendanceRecordRepository.save(record);

Long referenceId = toDateLong(today);
creditService.addCredit(user.getId(), CreditType.TODAY_CREDIT, referenceId);
creditService.addCredit(user.getId(), creditType, referenceId);

AttendanceStreak streak = getOrCreateStreak(user, today);
boolean streakBonusEarned = updateStreak(streak, today, user.getId());
updateStreak(streak, today);

int streakBonusPoints = streakBonusEarned ? STREAK_REWARD_POINTS : 0;
int streakBonusPoints = perfectWeekBonus ? dailyPoints : 0;
int totalPoints = creditService.getTotalPoints(user.getId());

return new CheckResponse(
user.getUserTag(),
LocalDateTime.now(SEOUL_ZONE),
dailyPoints,
streakBonusEarned,
perfectWeekBonus,
streakBonusPoints,
streak.getCurrentStreak(),
totalPoints
Expand Down Expand Up @@ -174,28 +177,28 @@ private AttendanceStreak getOrCreateStreak(User user, LocalDate today) {
});
}

private boolean updateStreak(AttendanceStreak streak, LocalDate today, Long userId) {
private void updateStreak(AttendanceStreak streak, LocalDate today) {
LocalDate yesterday = today.minusDays(1);
boolean continuedStreak = attendanceRecordRepository
.existsByUserIdAndAttendedDate(userId, yesterday);
.existsByUserIdAndAttendedDate(streak.getUserId(), yesterday);

if (continuedStreak || streak.getCurrentStreak() == 0) {
streak.attend(today);
} else {
LocalDate weekStart = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
streak.resetStreak(weekStart);
}
}

boolean justAchieved = streak.getCurrentStreak() >= STREAK_BONUS_THRESHOLD
&& streak.isStreakAchieved()
&& !creditService.existsHistory(userId, CreditType.ATTENDANCE_STREAK, toDateLong(streak.getStreakWeekStart()) + 7L);

if (justAchieved) {
Long streakReferenceId = toDateLong(streak.getStreakWeekStart()) + 7L;
creditService.addCredit(userId, CreditType.ATTENDANCE_STREAK, streakReferenceId);
private boolean isSundayWithFullWeekAttendance(Long userId, LocalDate today) {
if (today.getDayOfWeek() != DayOfWeek.SUNDAY) {
return false;
}

return justAchieved;
LocalDate monday = today.minusDays(6);
LocalDate saturday = today.minusDays(1);
List<AttendanceRecord> monToSatRecords =
attendanceRecordRepository.findByUserIdAndAttendedDateBetween(userId, monday, saturday);
return monToSatRecords.size() == 6;
}

private Long toDateLong(LocalDate date) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ public String processReward(AdMobRewardRequest request) {
log.info("[AdMob] 보상 이력 저장 완료: historyId={}", history.getId());

// 6. 크레딧 적립 (history.getId()를 referenceId로 사용해 unique 충돌 방지)
creditService.addCredit(user.getId(), CreditType.FREE_CHARGE, request.reward_amount(), history.getId());
// AdMob이 보낸 reward_amount는 콘솔 설정에 따라 달라질 수 있어 신뢰하지 않고, 정책상 고정값(FREE_CHARGE)만 지급한다
creditService.addCredit(user.getId(), CreditType.FREE_CHARGE, CreditType.FREE_CHARGE.getDefaultAmount(), history.getId());
log.info("[AdMob] 포인트 적립 완료: userId={}, amount={}, historyId={}",
user.getId(), request.reward_amount(), history.getId());
user.getId(), CreditType.FREE_CHARGE.getDefaultAmount(), history.getId());

return "OK";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
@Getter
public enum CreditType {
TODAY_CREDIT(5), // 출석체크: 하루 1회
ATTENDANCE_STREAK(7), // 7일 연속 출석 보너스
ATTENDANCE_STREAK(7), // 월~토 개근 후 일요일 출석체크 (TODAY_CREDIT 대신 지급)
DEFAULT_CREDIT(30), // 기본 크레딧: 회원가입 시 기본 지급
BATTLE_VOTE(5), // 배틀 참여 보상: 사후 투표 완료 시 즉시 지급
BATTLE_ENTRY(-5), // 지난 배틀 이용 비용: 사전 투표 최초 진입 시 차감
MAJORITY_WIN(10), // 다수결 보상: 월요일 배치, 2주 전 배틀 TOP≥10 대상
MAJORITY_WIN(5), // 다수결 보상: 월요일 배치, 2주 전 배틀 TOP≥10 대상
BEST_COMMENT(15), // 베댓 보상: 월요일 배치, 2주 전 배틀 좋아요 1위
WEEKLY_CHARGE(40), // 주간 자동 충전: 매주 월요일 00:00 활성 사용자 전체
FREE_CHARGE(20), // 광고/자유 충전: 가변 금액
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* 다수결 보상 배치.
* runDate(월요일) 기준 targetDate ∈ [runDate-20, runDate-14] 범위의 배틀 중
* 최다 득표 옵션(= 다수결 승자 옵션)을 선정하고,
* 그 옵션을 사전 투표한 사용자 전원에게 +10P (CreditType.MAJORITY_WIN) 를 지급한다.
* 그 옵션을 사전 투표한 사용자 전원에게 +5P (CreditType.MAJORITY_WIN) 를 지급한다.
*
* referenceId = battleId. CreditHistory 유니크 제약으로 같은 배틀 재실행 시 중복 지급 없음.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -49,6 +50,9 @@ List<BattleVote> findByUserIdAndPreVoteOptionDisplayOrderNotOrderByCreatedAtDesc

long countByUserId(Long userId);

// 오늘의 배틀 무료 참여(일 1회) 제한 체크용: 유저가 오늘 날짜인 배틀에 이미 참여했는지 확인
boolean existsByUserIdAndBattle_TargetDate(Long userId, LocalDate targetDate);

@Query("SELECT COUNT(v) FROM BattleVote v WHERE v.user.id = :userId AND v.preVoteOption.displayOrder = :displayOrder")
long countByUserIdAndPreVoteOptionDisplayOrder(@Param("userId") Long userId, @Param("displayOrder") Integer displayOrder);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public VoteResultResponse preVote(Long battleId, Long userId, VoteRequest reques
vote = existingVote.get();
vote.updatePreVote(option);
} else {
if (shouldChargeBattleEntryCredit(battle)) {
if (shouldChargeBattleEntryCredit(battle, user)) {
creditService.addCredit(user.getId(), CreditType.BATTLE_ENTRY, battle.getId());
}
vote = BattleVote.createPreVote(user, battle, option);
Expand Down Expand Up @@ -202,8 +202,13 @@ public void completeTts(Long battleId, Long userId) {
userBattleService.upsertStep(user, battle, UserBattleStep.POST_VOTE);
}

private boolean shouldChargeBattleEntryCredit(Battle battle) {
// 오늘의 배틀(targetDate == 오늘)은 하루 1회에 한해 무료 진입, 그 외 진입 또는 이미 오늘 무료 진입을 사용한 경우 -5P 차감
private boolean shouldChargeBattleEntryCredit(Battle battle, User user) {
LocalDate today = LocalDate.now(KST);
return battle.getTargetDate() == null || !battle.getTargetDate().isEqual(today);
boolean isTodayBattle = battle.getTargetDate() != null && battle.getTargetDate().isEqual(today);
if (!isTodayBattle) {
return true;
}
return battleVoteRepository.existsByUserIdAndBattle_TargetDate(user.getId(), today);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ void processReward_Success() throws Exception {
assertThat(result).isEqualTo("OK");

// // 4. 호출 검증
verify(creditService, times(1)).addCredit(eq(1L), eq(CreditType.FREE_CHARGE), eq(100), anyLong());
verify(creditService, times(1)).addCredit(eq(1L), eq(CreditType.FREE_CHARGE), eq(CreditType.FREE_CHARGE.getDefaultAmount()), anyLong());
verify(adRewardHistoryRepository, times(1)).saveAndFlush(any(AdRewardHistory.class));
verify(userService, times(1)).findByUserTag("pique-1cc4a030");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,27 @@ void preVote_doesNotChargeBattleEntryCreditForTodayBattle() {
verify(creditService, never()).addCredit(10L, CreditType.BATTLE_ENTRY, 100L);
}

@Test
@DisplayName("오늘의 배틀이라도 오늘 이미 무료 진입을 사용했다면 크레딧을 차감한다")
void preVote_chargesBattleEntryCreditWhenFreeEntryAlreadyUsedToday() {
Battle battle = battle(100L, LocalDate.now());
User user = user(10L);
BattleOption option = option(201L, battle, BattleOptionLabel.A);

when(battleService.findById(100L)).thenReturn(battle);
when(userRepository.findById(10L)).thenReturn(Optional.of(user));
when(battleOptionRepository.findById(201L)).thenReturn(Optional.of(option));
when(battleVoteRepository.findByBattleAndUser(battle, user)).thenReturn(Optional.empty());
when(battleVoteRepository.existsByUserIdAndBattle_TargetDate(10L, LocalDate.now())).thenReturn(true);
when(userBattleService.getUserBattleStatus(user, battle))
.thenReturn(new UserBattleStatusResponse(100L, UserBattleStep.NONE));

VoteResultResponse response = battleVoteService.preVote(100L, 10L, new VoteRequest(201L));

assertThat(response.status()).isEqualTo(UserBattleStep.PRE_VOTE);
verify(creditService).addCredit(10L, CreditType.BATTLE_ENTRY, 100L);
}

@Test
@DisplayName("이미 사전 투표한 배틀이면 옵션 변경 시 추가 차감하지 않는다")
void preVote_doesNotChargeAgainWhenVoteAlreadyExists() {
Expand Down
Loading