Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_KEY }}
source: "build/libs/app.jar"
target: "/home/ubuntu/app"
target: "/home/ubuntu/app/"

# 5️⃣ EC2에서 앱 재시작
- name: Run app on EC2
Expand All @@ -46,6 +46,6 @@ jobs:
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_KEY }}
script: |
cd /home/ubuntu/app
cd /home/ubuntu/app/
chmod +x run.sh
./run.sh
30 changes: 30 additions & 0 deletions src/main/java/com/example/demo/core/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.demo.core.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;

@Configuration
public class RedisConfig {

@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private String port;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, Integer.parseInt(port));
}

@Bean
public StringRedisTemplate stringRedisTemplate() {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
return stringRedisTemplate;
}
}
22 changes: 22 additions & 0 deletions src/main/java/com/example/demo/core/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.demo.core.config;

import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class SwaggerConfig {

@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("void")
.version("3.0.1"))
.servers(List.of(new Server().url("/")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.example.demo.domain.gemini.dto.GeminiRequest;
import com.example.demo.domain.gemini.dto.GeminiResponse;
import com.example.demo.domain.gemini.exception.GeminiErrorCode;
import com.example.demo.domain.ranking.service.RankingService;
import com.example.demo.shared.exception.CustomException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -16,6 +17,7 @@
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

import java.util.List;
import java.util.Map;

@Service
Expand All @@ -27,14 +29,17 @@ public class GeminiService {

private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper;
private final RankingService rankingService;

public Map<String, Object> askGemini(String userContent) {
String cleanKey = apiKey.trim();

String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=" + cleanKey;

String systemPrompt = "유저의 고민을 듣고 다정하게 위로해줘. 핵심 키워드 3개 정도를 욕설 제외하고 뽑아줘. 갯수에 제한 받기 보다는, 정말 중요하다 생각되는 키워드를 뽑아."
+ "반드시 JSON 형식으로만 대답해. 형식: {\"keyword\": \"단어\", \"answer\": \"내용\"}";
String systemPrompt = "유저의 고민을 듣고 다정하게 위로해줘." +
"너무 ai 같지 않게 사람답게 말하고, 유저를 절대 비난해서는 안돼." +
"무조건적인 공감과 지지를 보내줘. 핵심 키워드 3개 정도를 욕설 제외하고 뽑아줘. 갯수에 제한 받기 보다는, 정말 중요하다 생각되는 키워드를 뽑아."
+ "반드시 JSON 형식으로만 대답해. 형식: {\"keyword\": [\"단어1\", \"단어2\", \"단어3\"], \"answer\": \"내용\"}";

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Expand All @@ -52,7 +57,22 @@ public Map<String, Object> askGemini(String userContent) {

try {
String cleanedJson = rawContent.replaceAll("(?s)```json|```", "").trim();
return objectMapper.readValue(cleanedJson, Map.class);
Map<String, Object> result = objectMapper.readValue(cleanedJson, Map.class);

Object keywordObj = result.get("keyword");
if (keywordObj instanceof List) {
List<String> keywords = (List<String>) keywordObj;
for (String kw : keywords) {
rankingService.incrementKeywordCount(kw.trim());
}
} else if (keywordObj instanceof String) {
String[] splitKeywords = ((String) keywordObj).split(",");
for (String kw : splitKeywords) {
rankingService.incrementKeywordCount(kw.trim());
}
}

return result;
} catch (JsonProcessingException e) {
throw new CustomException(GeminiErrorCode.GEMINI_PARSE_ERROR);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example.demo.domain.ranking.controller;

import com.example.demo.domain.ranking.service.RankingService;
import com.example.demo.shared.response.ApiResponse;
import com.example.demo.shared.response.SuccessCode;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/ranking")
@RequiredArgsConstructor
public class RankingController {

private final RankingService rankingService;

@GetMapping("/top3")
public ApiResponse<List<Map<String, Object>>> getTop3() {
List<Map<String, Object>> result = rankingService.getTop3WithPercentage();
return ApiResponse.onSuccess(result, SuccessCode.OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.example.demo.domain.ranking.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class RankingService {

private final StringRedisTemplate redisTemplate;

// 시간별 redis key 생성
private String getRankingkey() {
String timestamp = LocalDateTime.now().format(
DateTimeFormatter.ofPattern("yyyyMMddHH")
);

return "ranking:" + timestamp;
}

// 키워드 소각 횟수 증가
public void incrementKeywordCount(String keyword) {
if (keyword == null || keyword.isBlank()) return;
String key = getRankingkey();
redisTemplate.opsForZSet().incrementScore(key, keyword, 1);
}

public List<Map<String, Object>> getTop3WithPercentage() {
String key = getRankingkey();

Set<ZSetOperations.TypedTuple<String>> top3WithScores =
redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 2);

if (top3WithScores == null || top3WithScores.isEmpty()) return List.of();

// 현재 시간대의 전체 소각 횟수 합
Double totalScore = 0.0;
Set<ZSetOperations.TypedTuple<String>> allItems =
redisTemplate.opsForZSet().rangeWithScores(key, 0, -1);

if (allItems != null) {
for (var item : allItems) {
totalScore += (item.getScore() != null ? item.getScore() : 0.0);
}
}
final Double finalTotalScore = totalScore;

return top3WithScores.stream().map(tuple -> {
Map<String, Object> map = new LinkedHashMap<>();
double score = tuple.getScore() != null ? tuple.getScore() : 0.0;
double percentage = (score / finalTotalScore) * 100;

map.put("keyword", tuple.getValue());
map.put("percentage", Math.round(percentage) + "%");
return map;
}).collect(Collectors.toList());
}


}
14 changes: 8 additions & 6 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ spring:
application:
name: void

data:
redis:
host: localhost
port: 6379
gemini:
api:
key: ${KEY_hackerton}

springdoc:
default-consumes-media-type: application/json
default-produces-media-type: application/json
api-docs:
version: openapi_3_0
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
disable-swagger-default-url: true
display-request-duration: true
api-docs:
path: /api-docs
disable-swagger-default-url: false