Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
3254a06
feat: ํ•€ ์กฐํšŒ ์‹œ Elastic Cache ๊ตฌ์ถ•
taerimiiii Jun 17, 2026
ea569c0
Merge remote-tracking branch 'origin/develop' into refactor/310-chache
taerimiiii Jun 17, 2026
03e21f1
feat: ์บ์‹œ ์ ์žฌ ํ™•์ธ API ๊ตฌํ˜„
taerimiiii Jun 17, 2026
1e1fa0e
merge: ์ถฉ๋Œ ์ฒ˜๋ฆฌ
taerimiiii Jun 17, 2026
56f259f
fix: Redis GEO bulk populate ํŒŒ์ดํ”„๋ผ์ธ ๊ฐœ์„ 
taerimiiii Jun 17, 2026
c82af3f
fix: ZSet ํฌ๊ธฐ ์„ค์ •์— ๋”ฐ๋ฅธ ๋…ผ๋ฆฌ ์˜ค๋ฅ˜ ํ•ด๊ฒฐ์„ ์œ„ํ•œ volatile boolean isReady ํ”Œ๋ž˜๊ทธ ๋„์ž…๊ณผ bulโ€ฆ
taerimiiii Jun 17, 2026
1e75b57
fix: addPin๊ณผ removePin์ด DB ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„ ๋‚ด์—์„œ ํ˜ธ์ถœ๋˜๋Š” ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด TransactionSyโ€ฆ
taerimiiii Jun 17, 2026
c92eecf
fix: ๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ์Šค์ผ€์ฅด๋ง ์ธ์Šคํ„ด์Šค ๋™์‹œ ์‹คํ–‰ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ Redis SET NX PX ๊ธฐ๋ฐ˜ ๋ถ„์‚ฐ ๋ฝ ์ ์šฉ
taerimiiii Jun 17, 2026
0620fc4
feat: ๋ฐ์ดํ„ฐ ์ •ํ™•์„ฑ ๋ณด์žฅ์„ ์œ„ํ•œ null ๊ฒฝ์šฐ ๋นˆ ์ฒ˜๋ฆฌ์™€, KEYS ๋ช…๋ น์–ด ์ „์ฒด ํƒ์ƒ‰ ๋Œ€์‹  ์นดํ…Œ๊ณ ๋ฆฌ ์—ด๊ฑฐ๊ฐ’ ๊ธฐ์ค€ ์‚ญ์ œ ์ ์šฉ
taerimiiii Jun 17, 2026
326942d
fix: ์žฌ๊ตฌ์„ฑ ์ค‘ ์•ˆ์ „๋ง ํ˜•์„ฑ์„ ์œ„ํ•ด isReady=true์ผ ๋•Œ๋„ ZCARD > 0 ์ฒดํฌ๋ฅผ ์œ ์ง€ํ•˜๊ณ , bulkPopulaโ€ฆ
taerimiiii Jun 17, 2026
e90e6b8
fix: ๋‹ค์ค‘ ์ธ์Šคํ„ด์Šค ํ™˜๊ฒฝ์„ ๊ณ ๋ คํ•œ ์ดˆ๊ธฐํ™” ๊ฑด๋„ˆ๋›ฐ๊ธฐ ์ถ”๊ฐ€
taerimiiii Jun 17, 2026
d8b4a12
fix: ์บ์‹œ ์žฌ๋นŒ๋“œ ์‹œ๊ฐ ์žฌ์„ค์ •
taerimiiii Jun 18, 2026
49ba5f0
Merge remote-tracking branch 'origin/develop' into refactor/310-chache
taerimiiii Jun 18, 2026
55a8d1c
fix: ์‚ฌ์šฉ์ž ์‹œ๋ฏผํ•ด๊ฒฐ์‚ฌ ์กฐํšŒ ์‹œ '์ด๋™์ค‘' ์ƒํƒœ ์ œ์•ฝ ์กฐ๊ฑด ํ•ด์ง€
taerimiiii Jun 18, 2026
4cb0248
Merge remote-tracking branch 'origin/bug/373-user-solver' into merge/โ€ฆ
taerimiiii Jun 18, 2026
a0921f3
Merge pull request #375 from IssueIssyu/merge/310-cache
taerimiiii Jun 18, 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 @@ -114,4 +114,33 @@ List<MapPinClusterView> findPinClustersInBoundingBox(
);

Optional<PinLocation> findFirstByPin_PinId(Long pinId);

// Redis GEO ์ดˆ๊ธฐ ์ ์žฌ / ์•ผ๊ฐ„ ์žฌ์ ์žฌ์šฉ: BBox ์ œ์•ฝ ์—†์ด ํ˜„์žฌ ํ™œ์„ฑ ํ•€์„ ๋ชจ๋‘ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
// ํ™œ์„ฑ ์กฐ๊ฑด์€ findPinsInBoundingBox ์™€ ๋™์ผ (ISSUE 1๋…„, COMMUNICATION 1๊ฐœ์›”, STORE/FESTIVAL ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„).
@Query(value = """
SELECT
p.pin_id AS pinId,
p.pin_type AS pinType,
ST_Y(pl.pin_point) AS lat,
ST_X(pl.pin_point) AS lng,
pl.detail_address AS detailAddress,
l.location AS region,
CASE WHEN p.pin_type = 'STORE' THEN ep.discount ELSE NULL END AS discount
FROM pin_location pl
INNER JOIN pin p ON pl.pin_id = p.pin_id
INNER JOIN location l ON pl.location_id = l.location_id
LEFT JOIN communication_pin cp ON cp.pin_id = p.pin_id
LEFT JOIN event_pin ep ON ep.pin_id = p.pin_id
WHERE p.created_at >= (NOW() AT TIME ZONE 'Asia/Seoul') - INTERVAL '1 year'
AND (
p.pin_type = 'ISSUE'
OR (p.pin_type = 'COMMUNICATION'
AND cp.communication_pin_id IS NOT NULL
AND COALESCE(cp.updated_at, cp.created_at) >= (NOW() AT TIME ZONE 'Asia/Seoul') - INTERVAL '1 month')
OR (p.pin_type IN ('STORE', 'FESTIVAL')
AND ep.event_pin_id IS NOT NULL
AND (NOW() AT TIME ZONE 'Asia/Seoul') BETWEEN ep.event_start_time AND ep.event_end_time)
)
""", nativeQuery = true)
List<MapPinView> findAllActivePins();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package issueissyu.backend.domain.map.cache;

import issueissyu.backend.domain.location.repository.PinLocationRepository;
import issueissyu.backend.domain.map.dto.res.MapPinView;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.List;

// ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ DB์˜ ํ™œ์„ฑ ํ•€์„ Redis GEO ์บ์‹œ์— ์ „์ฒด ์ ์žฌํ•ฉ๋‹ˆ๋‹ค.
// {@link ApplicationReadyEvent} ์ดํ›„ ๋น„๋™๊ธฐ๋กœ ์‹คํ–‰๋˜์–ด ์„œ๋ฒ„ ๊ธฐ๋™ ์ง€์—ฐ์ด ์—†์Šต๋‹ˆ๋‹ค.
// ์ดˆ๊ธฐํ™” ๋„์ค‘ ํ•€ ์กฐํšŒ API ํ˜ธ์ถœ์ด ์˜ค๋ฉด isGeoSetReady() == false ์ด๋ฏ€๋กœ DB์—์„œ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.

// ๋‹ค์ค‘ ์ธ์Šคํ„ด์Šค ํ™˜๊ฒฝ ๊ณ ๋ ค์‚ฌํ•ญ:
// - ๋กค๋ง ๋ฐฐํฌ์ฒ˜๋Ÿผ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ˆœ์ฐจ์ ์œผ๋กœ ๊ธฐ๋™๋˜๋Š” ๊ฒฝ์šฐ, geo:pins:ready ํ‚ค๋กœ ์ค‘๋ณต ์ดˆ๊ธฐํ™”๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
// - ์ธ์Šคํ„ด์Šค๊ฐ€ ์™„์ „ํžˆ ๋™์‹œ์— ๊ธฐ๋™๋˜๋”๋ผ๋„ bulkPopulate ๋Š” ๋ฉฑ๋“ฑ(idempotent)ํ•˜๊ฒŒ ์„ค๊ณ„๋˜์–ด
// ๋™์ผ ๋ฐ์ดํ„ฐ๋ฅผ ์ค‘๋ณต ์ ์žฌํ•  ๋ฟ ๋ฐ์ดํ„ฐ ์†์ƒ์€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
@Slf4j
@Component
@RequiredArgsConstructor
public class PinGeoRedisInitializer {

private final PinLocationRepository pinLocationRepository;
private final PinGeoRedisService pinGeoRedisService;

@Async
@Order(1)
@EventListener(ApplicationReadyEvent.class)
public void initialize() {
try {
// geo:pins:ready ํ‚ค๊ฐ€ ์กด์žฌํ•˜๋ฉด ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ด๋ฏธ ์บ์‹œ๋ฅผ ์ ์žฌํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.
// ๋กค๋ง ๋ฐฐํฌ ๋“ฑ ์ˆœ์ฐจ ๊ธฐ๋™ ํ™˜๊ฒฝ์—์„œ ๋ถˆํ•„์š”ํ•œ DB ์กฐํšŒ์™€ Redis ์ค‘๋ณต ์ ์žฌ๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
if (pinGeoRedisService.isGeoSetReady()) {
log.info("[Redis GEO] ์บ์‹œ๊ฐ€ ์ด๋ฏธ ์ดˆ๊ธฐํ™”๋˜์–ด ์žˆ์–ด ์ดˆ๊ธฐํ™”๋ฅผ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.");
return;
}
log.info("[Redis GEO] ์บ์‹œ ์ดˆ๊ธฐํ™” ์‹œ์ž‘ ...");
List<MapPinView> views = pinLocationRepository.findAllActivePins();
pinGeoRedisService.bulkPopulate(views);
log.info("[Redis GEO] ์บ์‹œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ: {}๊ฐœ ํ•€ ์ ์žฌ", views.size());
} catch (Exception e) {
log.warn("[Redis GEO] ์บ์‹œ ์ดˆ๊ธฐํ™” ์‹คํŒจ (์„œ๋ฒ„๋Š” ์ •์ƒ ๊ธฐ๋™, DB ํด๋ฐฑ ์‚ฌ์šฉ): {}", e.getMessage());
}
}
}
Comment on lines +26 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋™์‹œ์— ๊ธฐ๋™๋˜๋Š” ํ™˜๊ฒฝ(์˜ˆ: ๋ฌด์ค‘๋‹จ ๋ฐฐํฌ ์ค‘ ๋‹ค์ค‘ ์ปจํ…Œ์ด๋„ˆ ๊ธฐ๋™)์—์„œ isGeoSetReady() ์ฒดํฌ์™€ bulkPopulate() ์‹คํ–‰ ์‚ฌ์ด์— ๋ ˆ์ด์Šค ์ปจ๋””์…˜(Race Condition)์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‘ ์ธ์Šคํ„ด์Šค๊ฐ€ ๊ฑฐ์˜ ๋™์‹œ์— isGeoSetReady()๋ฅผ false๋กœ ํŒ๋‹จํ•˜๋ฉด, ๋‘˜ ๋‹ค bulkPopulate()๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๊ณ  ๋‚ด๋ถ€์ ์œผ๋กœ clearGeoKeys()๊ฐ€ ์‹คํ–‰๋˜์–ด ์บ์‹œ๊ฐ€ ๋น„์›Œ์ง€๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ์‹ค๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\n\n์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ดˆ๊ธฐํ™” ์‹œ์ ์—๋„ Redis ๋ถ„์‚ฐ ๋ฝ์„ ํ™œ์šฉํ•˜์—ฌ ๋‹จ ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค๋งŒ ์ดˆ๊ธฐํ™” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋ณด์žฅํ•˜๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

public class PinGeoRedisInitializer {\n\n    private final PinLocationRepository pinLocationRepository;\n    private final PinGeoRedisService pinGeoRedisService;\n    private final org.springframework.data.redis.core.RedisTemplate<String, String> redisTemplate;\n\n    @Async\n    @Order(1)\n    @EventListener(ApplicationReadyEvent.class)\n    public void initialize() {\n        try {\n            if (pinGeoRedisService.isGeoSetReady()) {\n                log.info(\"[Redis GEO] ์บ์‹œ๊ฐ€ ์ด๋ฏธ ์ดˆ๊ธฐํ™”๋˜์–ด ์žˆ์–ด ์ดˆ๊ธฐํ™”๋ฅผ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.\");\n                return;\n            }\n\n            Boolean acquired = redisTemplate.opsForValue().setIfAbsent(\"lock:geo:init\", \"1\", java.time.Duration.ofMinutes(5));\n            if (!Boolean.TRUE.equals(acquired)) {\n                log.info(\"[Redis GEO] ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ด๋ฏธ ์ดˆ๊ธฐํ™”๋ฅผ ์ง„ํ–‰ ์ค‘์ด๋ฏ€๋กœ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.\");\n                return;\n            }\n\n            try {\n                log.info(\"[Redis GEO] ์บ์‹œ ์ดˆ๊ธฐํ™” ์‹œ์ž‘ ...\");\n                List<MapPinView> views = pinLocationRepository.findAllActivePins();\n                pinGeoRedisService.bulkPopulate(views);\n                log.info(\"[Redis GEO] ์บ์‹œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ: {}๊ฐœ ํ•€ ์ ์žฌ\", views.size());\n            } finally {\n                redisTemplate.delete(\"lock:geo:init\");\n            }\n        } catch (Exception e) {\n            log.warn(\"[Redis GEO] ์บ์‹œ ์ดˆ๊ธฐํ™” ์‹คํŒจ (์„œ๋ฒ„๋Š” ์ •์ƒ ๊ธฐ๋™, DB ํด๋ฐฑ ์‚ฌ์šฉ): {}\", e.getMessage());\n        }\n    }\n}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package issueissyu.backend.domain.map.cache;

import issueissyu.backend.domain.location.repository.PinLocationRepository;
import issueissyu.backend.domain.map.dto.res.MapPinView;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.List;

// Redis GEO ์บ์‹œ ์ •ํ•ฉ์„ฑ ์œ ์ง€๋ฅผ ์œ„ํ•œ ์•ผ๊ฐ„ ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ.

// ๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ๋ชจ๋“  ์ธ์Šคํ„ด์Šค๊ฐ€ ๋™์‹œ์— ์‹คํ–‰๋˜๋Š” ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด
// Redis SET NX PX ๊ธฐ๋ฐ˜ ๋ถ„์‚ฐ ๋ฝ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
// ๋ฝ์„ ํš๋“ํ•œ ์ธ์Šคํ„ด์Šค๋งŒ ์žฌ๊ตฌ์„ฑ์„ ์ˆ˜ํ–‰ํ•˜๊ณ , ๋‚˜๋จธ์ง€๋Š” ์ฆ‰์‹œ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.
// ๋ฝ TTL ์ด ๋งŒ๋ฃŒ๋˜๋ฉด (ํ”„๋กœ์„ธ์Šค ๋น„์ •์ƒ ์ข…๋ฃŒ ๋“ฑ) ๋‹ค์Œ ์‹คํ–‰ ์‹œ ์ž๋™์œผ๋กœ ํš๋“ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
@Slf4j
@Component
@RequiredArgsConstructor
public class PinGeoScheduler {

private static final String REBUILD_LOCK_KEY = "lock:geo:rebuild";
// ์žฌ๊ตฌ์„ฑ ์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„๋ณด๋‹ค ์ถฉ๋ถ„ํžˆ ๊ธธ๊ฒŒ ์„ค์ • (๋น„์ •์ƒ ์ข…๋ฃŒ ์‹œ ์ž๋™ ๋งŒ๋ฃŒ์šฉ)
private static final Duration REBUILD_LOCK_TTL = Duration.ofMinutes(10);

private final PinLocationRepository pinLocationRepository;
private final PinGeoRedisService pinGeoRedisService;
private final RedisTemplate<String, String> redisTemplate;

@Scheduled(cron = "0 0 5 * * *", zone = "Asia/Seoul")
public void rebuildGeoCache() {
// SET lock:geo:rebuild 1 NX PX 600000 โ€” ์›์ž์  ๋ช…๋ น์œผ๋กœ ๋ ˆ์ด์Šค ์ปจ๋””์…˜ ์—†์Œ
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(REBUILD_LOCK_KEY, "1", REBUILD_LOCK_TTL);
if (!Boolean.TRUE.equals(acquired)) {
log.info("[Redis GEO] ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค๊ฐ€ ์•ผ๊ฐ„ ์บ์‹œ ์žฌ๊ตฌ์„ฑ ์ค‘์ด๋ฏ€๋กœ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.");
return;
}
try {
log.info("[Redis GEO] ์•ผ๊ฐ„ ์บ์‹œ ์žฌ๊ตฌ์„ฑ ์‹œ์ž‘ ...");
List<MapPinView> views = pinLocationRepository.findAllActivePins();
pinGeoRedisService.bulkPopulate(views);
log.info("[Redis GEO] ์•ผ๊ฐ„ ์บ์‹œ ์žฌ๊ตฌ์„ฑ ์™„๋ฃŒ: {}๊ฐœ ํ•€", views.size());
} catch (Exception e) {
log.error("[Redis GEO] ์•ผ๊ฐ„ ์บ์‹œ ์žฌ๊ตฌ์„ฑ ์‹คํŒจ: {}", e.getMessage(), e);
} finally {
// ์ •์ƒยท๋น„์ •์ƒ ์ข…๋ฃŒ ๋ชจ๋‘ ๋ฝ ์ฆ‰์‹œ ํ•ด์ œ (๋‹ค์Œ ์˜ˆ์•ฝ ์‹คํ–‰๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆด ํ•„์š” ์—†์Œ)
redisTemplate.delete(REBUILD_LOCK_KEY);
}
}
Comment on lines +47 to +53

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

์•ผ๊ฐ„ ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ์‹คํ–‰๋œ ํ›„ finally ๋ธ”๋ก์—์„œ ๋ฝ์„ ์ฆ‰์‹œ ํ•ด์ œ(redisTemplate.delete)ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ์„œ๋ฒ„ ๊ฐ„์˜ ๋ฏธ์„ธํ•œ ์‹œ๊ฐ„ ์ฐจ์ด(Clock Drift)๊ฐ€ ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋จผ์ € ์‹œ์ž‘ํ•œ ์„œ๋ฒ„๊ฐ€ ์ž‘์—…์„ ๋น ๋ฅด๊ฒŒ ๋๋‚ด๊ณ  ๋ฝ์„ ํ•ด์ œํ•˜๋ฉด, ์•ฝ๊ฐ„ ๋Šฆ๊ฒŒ ๊ธฐ๋™๋œ ๋‹ค๋ฅธ ์„œ๋ฒ„๊ฐ€ ๋‹ค์‹œ ๋ฝ์„ ํš๋“ํ•˜์—ฌ ์ค‘๋ณต์œผ๋กœ ์บ์‹œ ์žฌ๊ตฌ์„ฑ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\n\n์•ผ๊ฐ„ ๋ฐฐ์น˜๋Š” ํ•˜๋ฃจ์— ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋˜๋ฏ€๋กœ, finally ๋ธ”๋ก์—์„œ ๋ฝ์„ ์ˆ˜๋™์œผ๋กœ ์‚ญ์ œํ•˜์ง€ ์•Š๊ณ  REBUILD_LOCK_TTL (10๋ถ„) ๋™์•ˆ ์ž์—ฐ ๋งŒ๋ฃŒ๋˜๋„๋ก ๋‘๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์‹œ๊ฐ„ ์ฐจ์ด๊ฐ€ ์ˆ˜ ์ดˆ~์ˆ˜ ๋ถ„ ๋‚˜๋”๋ผ๋„ ์ค‘๋ณต ์‹คํ–‰์„ ํ™•์‹คํžˆ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

        } catch (Exception e) {\n            log.error(\"[Redis GEO] ์•ผ๊ฐ„ ์บ์‹œ ์žฌ๊ตฌ์„ฑ ์‹คํŒจ: {}\", e.getMessage(), e);\n        }\n    }\n}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import issueissyu.backend.domain.map.dto.res.MapNoticeListResDTO;
import issueissyu.backend.domain.map.dto.res.MapPinCacheStatusResDTO;
import issueissyu.backend.domain.map.dto.res.MapPinCardResDTO;
import issueissyu.backend.domain.map.dto.res.MapPinClusterResDTO;
import issueissyu.backend.domain.map.dto.res.MapPinResDTO;
Expand All @@ -12,6 +13,7 @@
import issueissyu.backend.domain.map.exception.code.MapErrorCode;
import issueissyu.backend.domain.map.exception.code.MapSuccessCode;
import issueissyu.backend.domain.map.service.query.MapNoticeQueryService;
import issueissyu.backend.domain.map.service.query.MapPinCacheQueryService;
import issueissyu.backend.domain.map.service.query.MapPinCardQueryService;
import issueissyu.backend.domain.map.service.query.MapPinQueryService;
import issueissyu.backend.domain.map.service.query.PatchNoteQueryService;
Expand All @@ -35,6 +37,7 @@ public class MapController {

private final MapPinQueryService mapPinQueryService;
private final MapPinCardQueryService mapPinCardQueryService;
private final MapPinCacheQueryService mapPinCacheQueryService;
private final MapNoticeQueryService mapNoticeQueryService;
private final PatchNoteQueryService patchNoteQueryService;

Expand Down Expand Up @@ -113,6 +116,16 @@ public ApiResponse<PatchNoteResDTO> getPatchNotes(
patchNoteQueryService.getPatchNotes(uid, locationId, size, cursor));
}

@Operation(
summary = "์ง€๋„ ํ•€ ์บ์‹œ ์ƒํƒœ ์กฐํšŒ (ADMIN)",
description = "Redis GEO Set(geo:pins)์— ์ €์žฅ๋œ ํ•€ ๊ฐœ์ˆ˜(ZCARD)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ADMIN ๊ถŒํ•œ ํ•„์š”.")
@GetMapping("/cache/status")
public ApiResponse<MapPinCacheStatusResDTO> getPinCacheStatus(@AuthenticationPrincipal String uid) {
return ApiResponse.onSuccess(
MapSuccessCode.MAP_CACHE_200,
mapPinCacheQueryService.getCacheStatus(uid));
}

@Operation(
summary = "๋‹จ์ผ ํ•€ ์นด๋“œ ์กฐํšŒ (๊ฒฝ๋กœ ๋ณ€์ˆ˜)",
description = "communityId๋Š” ์—ฐ๊ฒฐ๋œ ์ปค๋ฎค๋‹ˆํ‹ฐ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ๋ฐ˜ํ™˜.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package issueissyu.backend.domain.map.dto.res;

public record MapPinCacheStatusResDTO(long geoPinCount) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public enum MapErrorCode implements BaseErrorCode {
PATCHNOTE_400_1(HttpStatus.BAD_REQUEST, "PATCHNOTE_400_1", "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ง€์—ญ ์ž…๋‹ˆ๋‹ค."),
PATCHNOTE_400_2(HttpStatus.BAD_REQUEST, "PATCHNOTE_400_2", "์กฐํšŒ ๋ถˆ๊ฐ€๋Šฅํ•œ ์‚ฌ์ด์ฆˆ ์ž…๋‹ˆ๋‹ค."),
PATCHNOTE_400_3(HttpStatus.BAD_REQUEST, "PATCHNOTE_400_3", "์กฐํšŒ ๋ถˆ๊ฐ€๋Šฅํ•œ cursor ์ž…๋‹ˆ๋‹ค."),
PATCHNOTE_400_4(HttpStatus.BAD_REQUEST, "PATCHNOTE_400_4", "์‚ฌ์šฉ์ž์˜ ๋™๋„ค๊ฐ€ ๋“ฑ๋ก๋˜์ง€ ์•Š์•„ ํŒจ์น˜๋…ธํŠธ ์กฐํšŒ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.");
PATCHNOTE_400_4(HttpStatus.BAD_REQUEST, "PATCHNOTE_400_4", "์‚ฌ์šฉ์ž์˜ ๋™๋„ค๊ฐ€ ๋“ฑ๋ก๋˜์ง€ ์•Š์•„ ํŒจ์น˜๋…ธํŠธ ์กฐํšŒ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."),

MAP_CACHE_403(HttpStatus.FORBIDDEN, "MAP_CACHE_403", "์ง€๋„ ํ•€ ์บ์‹œ ์ƒํƒœ ์กฐํšŒ๋Š” ADMIN ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ public enum MapSuccessCode implements BaseSuccessCode {
MAP_NOTICE_200(HttpStatus.OK, "MAP_NOTICE_200", "๊ณต์ง€์‚ฌํ•ญ ์กฐํšŒ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค."),
MAP_NOTICE_204(HttpStatus.OK, "MAP_NOTICE_204", "๋“ฑ๋ก๋œ ๊ณต์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."),

PATCHNOTE_200(HttpStatus.OK, "PATCHNOTE_200", "ํ˜„์žฌ ์ง€์—ญ์˜ ํŒจ์น˜๋…ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค.");
PATCHNOTE_200(HttpStatus.OK, "PATCHNOTE_200", "ํ˜„์žฌ ์ง€์—ญ์˜ ํŒจ์น˜๋…ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค."),

MAP_CACHE_200(HttpStatus.OK, "MAP_CACHE_200", "์ง€๋„ ํ•€ ์บ์‹œ ์ƒํƒœ ์กฐํšŒ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package issueissyu.backend.domain.map.service.query;

import issueissyu.backend.domain.map.dto.res.MapPinCacheStatusResDTO;

public interface MapPinCacheQueryService {

MapPinCacheStatusResDTO getCacheStatus(String uid);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package issueissyu.backend.domain.map.service.query;

import issueissyu.backend.domain.map.cache.PinGeoRedisService;
import issueissyu.backend.domain.map.dto.res.MapPinCacheStatusResDTO;
import issueissyu.backend.domain.map.exception.MapException;
import issueissyu.backend.domain.map.exception.code.MapErrorCode;
import issueissyu.backend.domain.user.entity.User;
import issueissyu.backend.domain.user.enums.UserRole;
import issueissyu.backend.domain.user.repository.UserRepository;
import issueissyu.backend.global.api.code.GeneralErrorCode;
import issueissyu.backend.global.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MapPinCacheQueryServiceImpl implements MapPinCacheQueryService {

private final UserRepository userRepository;
private final PinGeoRedisService pinGeoRedisService;

@Override
public MapPinCacheStatusResDTO getCacheStatus(String uid) {
User user = userRepository.findById(uid)
.orElseThrow(() -> GeneralException.of(GeneralErrorCode.USER_NOT_FOUND));
if (user.getRole() != UserRole.ADMIN) {
throw MapException.of(MapErrorCode.MAP_CACHE_403);
}
return new MapPinCacheStatusResDTO(pinGeoRedisService.getGeoPinCount());
}
}
Loading
Loading