From 28c0881a8a52e8a7b7ef5fd3179c0c9b1b843ba9 Mon Sep 17 00:00:00 2001 From: baseballclub Date: Fri, 6 Feb 2026 22:29:17 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[Feat]=20TOP3=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?,=20=ED=8D=BC=EC=84=BC=ED=8A=B8=20=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/demo/core/config/RedisConfig.java | 30 ++++++++ .../domain/gemini/service/GeminiService.java | 26 ++++++- .../ranking/controller/RankingController.java | 26 +++++++ .../ranking/service/RankingService.java | 70 +++++++++++++++++++ src/main/resources/application.yaml | 4 ++ 5 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/example/demo/core/config/RedisConfig.java create mode 100644 src/main/java/com/example/demo/domain/ranking/controller/RankingController.java create mode 100644 src/main/java/com/example/demo/domain/ranking/service/RankingService.java diff --git a/src/main/java/com/example/demo/core/config/RedisConfig.java b/src/main/java/com/example/demo/core/config/RedisConfig.java new file mode 100644 index 0000000..8c57083 --- /dev/null +++ b/src/main/java/com/example/demo/core/config/RedisConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/gemini/service/GeminiService.java b/src/main/java/com/example/demo/domain/gemini/service/GeminiService.java index 0c7bcf7..b1c66b7 100644 --- a/src/main/java/com/example/demo/domain/gemini/service/GeminiService.java +++ b/src/main/java/com/example/demo/domain/gemini/service/GeminiService.java @@ -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; @@ -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 @@ -27,14 +29,17 @@ public class GeminiService { private final RestTemplate restTemplate = new RestTemplate(); private final ObjectMapper objectMapper; + private final RankingService rankingService; public Map 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); @@ -52,7 +57,22 @@ public Map askGemini(String userContent) { try { String cleanedJson = rawContent.replaceAll("(?s)```json|```", "").trim(); - return objectMapper.readValue(cleanedJson, Map.class); + Map result = objectMapper.readValue(cleanedJson, Map.class); + + Object keywordObj = result.get("keyword"); + if (keywordObj instanceof List) { + List keywords = (List) 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); } diff --git a/src/main/java/com/example/demo/domain/ranking/controller/RankingController.java b/src/main/java/com/example/demo/domain/ranking/controller/RankingController.java new file mode 100644 index 0000000..c2507e1 --- /dev/null +++ b/src/main/java/com/example/demo/domain/ranking/controller/RankingController.java @@ -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>> getTop3() { + List> result = rankingService.getTop3WithPercentage(); + return ApiResponse.onSuccess(result, SuccessCode.OK); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/ranking/service/RankingService.java b/src/main/java/com/example/demo/domain/ranking/service/RankingService.java new file mode 100644 index 0000000..7600649 --- /dev/null +++ b/src/main/java/com/example/demo/domain/ranking/service/RankingService.java @@ -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> getTop3WithPercentage() { + String key = getRankingkey(); + + Set> top3WithScores = + redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 2); + + if (top3WithScores == null || top3WithScores.isEmpty()) return List.of(); + + // 현재 시간대의 전체 소각 횟수 합 + Double totalScore = 0.0; + Set> 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 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()); + } + + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index c914623..3406a39 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,6 +2,10 @@ spring: application: name: void + data: + redis: + host: localhost + port: 6379 gemini: api: key: ${KEY_hackerton} From 08942dded988fcf216d13624fd44725b92320967 Mon Sep 17 00:00:00 2001 From: baseballclub Date: Fri, 6 Feb 2026 23:02:10 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[Feat]=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../demo/core/config/SwaggerConfig.java | 22 +++++++++++++++++++ src/main/resources/application.yaml | 10 ++++----- 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/example/demo/core/config/SwaggerConfig.java diff --git a/src/main/java/com/example/demo/core/config/SwaggerConfig.java b/src/main/java/com/example/demo/core/config/SwaggerConfig.java new file mode 100644 index 0000000..2caa383 --- /dev/null +++ b/src/main/java/com/example/demo/core/config/SwaggerConfig.java @@ -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("/"))); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 3406a39..4a7bbdd 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -11,11 +11,9 @@ gemini: 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 \ No newline at end of file + disable-swagger-default-url: false \ No newline at end of file From 166fde9555858e95d69448883aa7db9ede3169e4 Mon Sep 17 00:00:00 2001 From: ygcho Date: Fri, 6 Feb 2026 23:02:34 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20=ED=83=80=EA=B2=9F=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 37d4157..5463e95 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 @@ -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