체크인 시 PostgreSQL(통계용 영구 기록)과 Redis(실시간 활성 체크인)에 함께 저장. 활성 메이트 조회를 Redis ZSet(member=dogId, score=체크인시각, 24h TTL)에서 최근 24시간·최신순으로 수행. Postgres 는 이벤트마다 누적(통계). - CheckInRedisStore: ZSet + 24h 키 TTL, 조회 시 만료 정리 - application.yml: spring.data.redis (env 주입, 기본 localhost:6379) - Redis 미기동 시에도 컨텍스트 로드(Lettuce 지연연결) → CI/테스트 안전 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,3 +13,7 @@ build/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Redis 로컬 실행 시 생성
|
||||
dump.rdb
|
||||
*.rdb
|
||||
|
||||
@@ -25,6 +25,9 @@ dependencies {
|
||||
// 실시간 인앱 채팅 (STOMP over WebSocket)
|
||||
implementation 'org.springframework.boot:spring-boot-starter-websocket'
|
||||
|
||||
// 실시간 스팟 체크인(24h TTL) — 활성 메이트 조회. 통계는 PostgreSQL.
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
|
||||
// PostGIS / 공간 데이터 (스팟 위경도 좌표)
|
||||
implementation 'org.hibernate.orm:hibernate-spatial'
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.dog.dognation.domain.spot;
|
||||
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 스팟 실시간 체크인 저장소 (Redis).
|
||||
* 스팟별 ZSet(member=dogId, score=체크인 epoch초)으로 관리한다.
|
||||
* - 조회 시 24시간이 지난 항목은 정리하고, 키에도 24h TTL 을 걸어 유휴 시 자동 삭제.
|
||||
* - 영구 통계는 PostgreSQL check_ins 테이블이 담당(이 저장소와 별개).
|
||||
*/
|
||||
@Component
|
||||
public class CheckInRedisStore {
|
||||
|
||||
/** 실시간 체크인 유효시간. 이 시간이 지나면 활성 메이트에서 비노출. */
|
||||
static final Duration TTL = Duration.ofHours(24);
|
||||
|
||||
private final StringRedisTemplate redis;
|
||||
|
||||
public CheckInRedisStore(StringRedisTemplate redis) {
|
||||
this.redis = redis;
|
||||
}
|
||||
|
||||
private String key(Long spotId) {
|
||||
return "spot:checkin:" + spotId;
|
||||
}
|
||||
|
||||
/** 체크인 기록 — dogId 를 현재 시각 점수로 추가하고 키 TTL 을 24h 로 갱신한다. */
|
||||
public void add(Long spotId, Long dogId) {
|
||||
String k = key(spotId);
|
||||
redis.opsForZSet().add(k, String.valueOf(dogId), Instant.now().getEpochSecond());
|
||||
redis.expire(k, TTL);
|
||||
}
|
||||
|
||||
/** 최근 24시간 내 체크인한 dogId 목록(최신순). 만료 항목은 조회 시 정리. */
|
||||
public List<Long> activeDogIds(Long spotId) {
|
||||
String k = key(spotId);
|
||||
double cutoff = Instant.now().minus(TTL).getEpochSecond();
|
||||
redis.opsForZSet().removeRangeByScore(k, Double.NEGATIVE_INFINITY, cutoff);
|
||||
Set<String> members = redis.opsForZSet()
|
||||
.reverseRangeByScore(k, cutoff, Double.POSITIVE_INFINITY);
|
||||
if (members == null || members.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return members.stream().map(Long::valueOf).toList();
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,31 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class SpotService {
|
||||
|
||||
/** 체크인 유효시간(분). 이 시간이 지나면 스팟에서 자동 비노출. */
|
||||
/** 체크인 통계 기록의 만료 표기(분). 실시간 노출은 Redis(24h)가 담당. */
|
||||
private static final int CHECKIN_TTL_MINUTES = 120;
|
||||
|
||||
private final SpotRepository spotRepository;
|
||||
private final CheckInRepository checkInRepository;
|
||||
private final CheckInRedisStore checkInRedisStore;
|
||||
private final SpotReviewRepository reviewRepository;
|
||||
private final DogRepository dogRepository;
|
||||
|
||||
public SpotService(SpotRepository spotRepository,
|
||||
CheckInRepository checkInRepository,
|
||||
CheckInRedisStore checkInRedisStore,
|
||||
SpotReviewRepository reviewRepository,
|
||||
DogRepository dogRepository) {
|
||||
this.spotRepository = spotRepository;
|
||||
this.checkInRepository = checkInRepository;
|
||||
this.checkInRedisStore = checkInRedisStore;
|
||||
this.reviewRepository = reviewRepository;
|
||||
this.dogRepository = dogRepository;
|
||||
}
|
||||
@@ -43,17 +50,25 @@ public class SpotService {
|
||||
if (!dogRepository.existsById(dogId)) {
|
||||
throw new EntityNotFoundException("강아지를 찾을 수 없습니다: " + dogId);
|
||||
}
|
||||
// 통계용 영구 기록(PostgreSQL)
|
||||
OffsetDateTime expiresAt = OffsetDateTime.now().plusMinutes(CHECKIN_TTL_MINUTES);
|
||||
return checkInRepository.save(new CheckIn(spotId, userId, dogId, expiresAt));
|
||||
CheckIn saved = checkInRepository.save(new CheckIn(spotId, userId, dogId, expiresAt));
|
||||
// 실시간 활성 체크인(Redis, 24h TTL) — 활성 메이트 노출용
|
||||
checkInRedisStore.add(spotId, dogId);
|
||||
return saved;
|
||||
}
|
||||
|
||||
/** 스팟에 현재 체크인 중인 강아지(메이트) 목록. */
|
||||
/** 스팟에 현재 체크인 중인 강아지(메이트) 목록 — 최근 24시간(Redis), 최신순. */
|
||||
@Transactional(readOnly = true)
|
||||
public List<Dog> activeMates(Long spotId) {
|
||||
List<CheckIn> active = checkInRepository
|
||||
.findBySpotIdAndExpiresAtAfterOrderByCheckedInAtDesc(spotId, OffsetDateTime.now());
|
||||
List<Long> dogIds = active.stream().map(CheckIn::getDogId).distinct().toList();
|
||||
return dogRepository.findAllById(dogIds);
|
||||
List<Long> dogIds = checkInRedisStore.activeDogIds(spotId);
|
||||
if (dogIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
// Redis 최신순 유지 — findAllById 는 순서를 보장하지 않으므로 매핑 후 재정렬.
|
||||
Map<Long, Dog> byId = dogRepository.findAllById(dogIds).stream()
|
||||
.collect(Collectors.toMap(Dog::getId, Function.identity()));
|
||||
return dogIds.stream().map(byId::get).filter(Objects::nonNull).toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
||||
@@ -23,6 +23,16 @@ spring:
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
# 실시간 스팟 체크인(24h TTL) 저장소. 값은 .env 에서 주입, 미설정 시 로컬 기본값.
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
# StringRedisTemplate 만 사용(RedisHash 리포지토리 없음) → JPA 리포지토리 오분류 스캔 비활성.
|
||||
repositories:
|
||||
enabled: false
|
||||
|
||||
# 스케줄러/시간 기준: 한국 시간(KST) 자정
|
||||
app:
|
||||
scheduler:
|
||||
|
||||
Reference in New Issue
Block a user