Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
12 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.env
/.idea
7 changes: 7 additions & 0 deletions SpringBoot/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ dependencies {

/* Swagger UI */
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0"

/* Prometheus */
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'

/* Redis-Cache */
implementation 'org.springframework.boot:spring-boot-starter-cache'
}

dependencyManagement {
Expand Down
6 changes: 5 additions & 1 deletion SpringBoot/database/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,8 @@ CREATE TABLE hot_topic

CREATE INDEX idx_news_title ON news(title); -- news ํ…Œ์ด๋ธ” DELETE ์‹œ title ๊ฒ€์ƒ‰ ์‹œ
CREATE INDEX idx_news_published_at ON news(published_at);
CREATE INDEX idx_news_publisher ON news(publisher);
CREATE INDEX idx_news_publisher ON news(publisher);
-- SELECT์— ์‚ฌ์šฉ๋˜๋Š” ์กฐ๊ฑด
CREATE INDEX idx_news_published_sections ON news(published_at, sections);
-- ์ค‘๋ณต ์‚ญ์ œ์— ํšจ์œจ์ ์ธ ๋ณตํ•ฉ ์ธ๋ฑ์Šค
CREATE INDEX idx_news_dup ON news(published_at, title, publisher, id);
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

## ๋„๋ฉ”์ธ ์„ค๋ช…

## ๊ธฐ๋Šฅ ์„ค๋ช…
## ๊ธฐ๋Šฅ ์„ค๋ช…


Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
import Baemin.News_Deliver.Domain.HotTopic.repository.HotTopicRepository;
import Baemin.News_Deliver.Global.News.ElasticSearch.dto.NewsEsDocument;
import Baemin.News_Deliver.Global.News.ElasticSearch.service.NewsEsService;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.aggregations.StringTermsBucket;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
Expand All @@ -39,12 +42,23 @@
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class HotTopicService {

private final ElasticsearchClient elasticsearchClient;
private final HotTopicRepository hotTopicRepository;
private final NewsEsService elasticSearchService;
private final RedisTemplate<String, Object> redisTemplate;

public HotTopicService(
HotTopicRepository hotTopicRepository,
NewsEsService elasticSearchService,
@Qualifier("redisCacheTemplate") RedisTemplate<String, Object> redisTemplate
) {
this.hotTopicRepository = hotTopicRepository;
this.elasticSearchService = elasticSearchService;
this.redisTemplate = redisTemplate;
}

private static final String CACHE_PREFIX = "keyword:";

/**
* DB์—์„œ "์–ด์ œ" ์ €์žฅ๋œ ํ•ซํ† ํ”ฝ ์ƒ์œ„ 10๊ฐœ ์กฐํšŒ ํ›„ DTO๋กœ ๋ฐ˜ํ™˜
Expand All @@ -55,18 +69,40 @@ public class HotTopicService {
* @return ์–ด์ œ์˜ ํ•ซํ† ํ”ฝ ๋ฆฌ์ŠคํŠธ (์ตœ๋Œ€ 10๊ฐœ)
*/
public List<HotTopicResponseDTO> getHotTopicList() {
long start = System.nanoTime();
String cacheKey = "hottopic:daily";

List<HotTopicResponseDTO> cached = (List<HotTopicResponseDTO>) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
long end = System.nanoTime();
log.info("โœ… ํ•ซํ† ํ”ฝ ์บ์‹œ์—์„œ ๊ฐ€์ ธ์˜ด ({} ms)", (end - start) / 1_000_000);
return cached;
}

LocalDate yesterday = LocalDate.now().minusDays(1);
LocalDateTime startOfYesterday = yesterday.atStartOfDay(); // ์–ด์ œ 00:00:00
LocalDateTime endOfYesterday = yesterday.atTime(LocalTime.MAX); // ์–ด์ œ 23:59:59.999999999
LocalDateTime startOfYesterday = yesterday.atStartOfDay();
LocalDateTime endOfYesterday = yesterday.atTime(LocalTime.MAX);

return hotTopicRepository.findTop10ByTopicDateBetweenOrderByTopicRankAsc(startOfYesterday, endOfYesterday).stream()
List<HotTopicResponseDTO> result = hotTopicRepository.findTop10ByTopicDateBetweenOrderByTopicRankAsc(startOfYesterday, endOfYesterday)
.stream()
.map(entity -> HotTopicResponseDTO.builder()
.topicRank(entity.getTopicRank())
.keyword(entity.getKeyword())
.keywordCount(entity.getKeywordCount())
.topicDate(entity.getTopicDate())
.build())
.toList();

long end = System.nanoTime();
log.info("๐Ÿ“ฆ ํ•ซํ† ํ”ฝ DB ์กฐํšŒ & ์บ์‹ฑ ์™„๋ฃŒ ({} ms)", (end - start) / 1_000_000);

long ttlSeconds = Duration.between(
LocalDateTime.now(),
LocalDate.now().plusDays(1).atStartOfDay()
).getSeconds();

redisTemplate.opsForValue().set(cacheKey, result, Duration.ofSeconds(ttlSeconds));
return result;
}

/**
Expand Down Expand Up @@ -116,6 +152,26 @@ public void getAndSaveHotTopic() throws IOException {
* @throws IOException Elasticsearch ํ†ต์‹  ์‹คํŒจ ์‹œ
*/
public List<NewsEsDocument> getNewsList(String keyword, int size) throws IOException {
return elasticSearchService.searchByKeyword(keyword, size);
long start = System.nanoTime();
String cacheKey = CACHE_PREFIX + keyword;

List<NewsEsDocument> cached = (List<NewsEsDocument>) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
long end = System.nanoTime();
log.info("โœ… ์บ์‹œ์—์„œ ๊ฐ€์ ธ์˜ด: {} ({} ms)", keyword, (end - start) / 1_000_000);
return cached;
}

List<NewsEsDocument> result = elasticSearchService.searchByKeyword(keyword, size);
long end = System.nanoTime();
log.info("๐Ÿ“ฆ ES ์กฐํšŒ & ์บ์‹ฑ: {} ์™„๋ฃŒ ({} ms)", keyword, (end - start) / 1_000_000);

long ttlSeconds = Duration.between(
LocalDateTime.now(),
LocalDate.now().plusDays(1).atStartOfDay()
).getSeconds();

redisTemplate.opsForValue().set(cacheKey, result, Duration.ofSeconds(ttlSeconds));
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ public ResponseEntity<Long> saveSetting(@RequestBody SettingDTO settingDTO,
Long userId = authService.findByKakaoId(kakaoId).getId();
settingDTO.setUserId(userId);

return settingService.saveSetting(settingDTO);
Long settingId = settingService.saveSetting(settingDTO);
return ResponseEntity.ok(settingId);

} catch (Exception e) {
log.error("์„ค์ • ์ €์žฅ ์‹คํŒจ: kakaoId={}, error={}",
Expand Down Expand Up @@ -127,7 +128,8 @@ public ResponseEntity<Void> updateSetting(@RequestBody SettingDTO settingDTO,
Long userId = authService.findByKakaoId(kakaoId).getId();
settingDTO.setUserId(userId);

return settingService.updateSetting(settingDTO);
settingService.updateSetting(settingDTO);
return ResponseEntity.noContent().build();

} catch (Exception e) {
log.error("์„ค์ • ์ˆ˜์ • ์‹คํŒจ: kakaoId={}, error={}",
Expand All @@ -154,19 +156,12 @@ public ResponseEntity<Void> updateSetting(@RequestBody SettingDTO settingDTO,
})
public ResponseEntity<Void> deleteSetting(@PathVariable("id") Long id,
Authentication authentication) {
try {
// ์ธ์ฆ ๊ฐ์ฒด์—์„œ ์นด์นด์˜ค ID ์ถ”์ถœ
String kakaoId = authentication.getName();

// ์นด์นด์˜ค ID๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ (์ธ์ฆ ์‹คํŒจ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ)
Long userId = authService.findByKakaoId(kakaoId).getId();

return settingService.deleteSetting(id, userId);
String kakaoId = authentication.getName();
Long userId = authService.findByKakaoId(kakaoId).getId();

} catch (Exception e) {
log.error("์„ค์ • ์‚ญ์ œ ์‹คํŒจ: kakaoId={}, error={}",
authentication.getName(), e.getMessage());
throw new SettingException(ErrorCode.SETTING_DELETE_FAILED);
}
settingService.deleteSetting(id, userId);
return ResponseEntity.noContent().build();
}

}
Loading