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
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ configurations {

repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
maven {
name = 'Central Portal Snapshots'
url = 'https://central.sonatype.com/repository/maven-snapshots/'
}

}

dependencies {
Expand Down Expand Up @@ -64,6 +71,9 @@ dependencies {
// cache
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine:3.2.1'

implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-SNAPSHOT")
implementation 'org.springframework.ai:spring-ai-openai'
}

// Q파일 생성 위치
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/jiwon/mylog/domain/gpt/FortuneResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.jiwon.mylog.domain.gpt;

import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Builder
@Getter
public class FortuneResponse {
private final String content;
private final LocalDateTime createdAt;
}
22 changes: 22 additions & 0 deletions src/main/java/com/jiwon/mylog/domain/gpt/OpenAiController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.jiwon.mylog.domain.gpt;

import com.jiwon.mylog.global.security.auth.annotation.LoginUser;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RequestMapping("/api/openai")
@RestController
public class OpenAiController {

private final OpenAiService openAiService;

@GetMapping("/fortune")
public ResponseEntity<FortuneResponse> getDailyFortune(@LoginUser Long userId) {
FortuneResponse response = openAiService.getDailyFortune(userId);
return ResponseEntity.ok(response);
}
}
88 changes: 88 additions & 0 deletions src/main/java/com/jiwon/mylog/domain/gpt/OpenAiService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.jiwon.mylog.domain.gpt;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Service
public class OpenAiService {

private final OpenAiChatModel chatModel;
private final OpenAiChatOptions openAiChatOptions;

@Cacheable(value = "dailyFortune", key = "#userId + '_' + T(java.time.LocalDate).now()")
public FortuneResponse getDailyFortune(Long userId) {
try {
ChatResponse response = callOpenAiApi(userId);
String content = extractContent(response);
return FortuneResponse.builder()
.content(content)
.createdAt(LocalDateTime.now())
.build();
} catch (Exception e) {
return getDefaultFortune();
}
}

@CacheEvict(value = "dailyFortune", allEntries = true)
public void evictAllFortunes() {
log.info("All dailyFortune Cache deleted");
}

private ChatResponse callOpenAiApi(Long userId) {
SystemMessage systemMessage = new SystemMessage("""
넌 오늘의 운세를 나폴리탄 괴담 형식으로 알려주는 예언자야.
기묘한 단어를 조금 섞어서 무서운 괴담 느낌 가득하게 운세를 말해줘.
추가로 운세에는 아래 내용을 짧게 포함해야 해.
- 행운의 색, 행운의 물건, 나폴리탄 형식 괴담 주의사항 하나
""");
String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일"));
UserMessage userMessage = new UserMessage(
String.format("사용자 %d, %s 기준으로 오늘의 외계인 운세 알려줘.", userId, date));

List<Message> messages = Arrays.asList(systemMessage, userMessage);

Prompt prompt = Prompt.builder()
.chatOptions(openAiChatOptions)
.messages(messages)
.build();

return chatModel.call(prompt);
}

private String extractContent(ChatResponse response) {
if (response == null || response.getResult() == null) {
throw new IllegalArgumentException("GPT 응답 오류");
}

String content = response.getResult().getOutput().getText();
if (content == null || content.trim().isBlank()) {
throw new IllegalArgumentException("GPT 응답 오류");
}
return content;
}

private FortuneResponse getDefaultFortune() {
return FortuneResponse.builder()
.content("외계 통신 오 류! 외 계인을 구 해줘...")
.createdAt(LocalDateTime.now())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
Caffeine<Object, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(10))
.maximumSize(1_000);

CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(cache);

cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofHours(1)));

cacheManager.registerCustomCache("dailyFortune",
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofHours(1))
.build());

return cacheManager;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.jiwon.mylog.global.common.config;

import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenAiConfig {

@Value("${spring.ai.openai.api-key}")
private String apiKey;

@Value("${spring.ai.openai.chat.model}")
private String model;

@Bean
public OpenAiApi openAiApi() {
return OpenAiApi.builder()
.apiKey(apiKey)
.build();
}

@Bean
public OpenAiChatOptions openAiChatOptions() {
return OpenAiChatOptions.builder()
.model(model)
.temperature(0.7)
.maxTokens(800)
.build();
}

@Bean
public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi) {
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtService jwtService)
// 단순 조회 (권한X)
.requestMatchers(HttpMethod.GET, "/api/users/**", "/api/posts/**", "/api/categories/**", "/api/images/**", "/api/points/**", "/api/items/**", "/api/sse/**", "/api/likes/**").permitAll()
// 블로그 사용자
.requestMatchers("/api/users/**", "/api/posts/**", "/api/categories/**", "/api/comments/**", "/api/images/**", "/api/notifications/**", "/api/likes/**", "/api/readme/**").authenticated()
.requestMatchers("/api/users/**", "/api/posts/**", "/api/categories/**", "/api/comments/**", "/api/images/**", "/api/notifications/**", "/api/likes/**", "/api/readme/**", "/api/openai/**").authenticated()
// 관리자 전용
.requestMatchers("/api/admin/**").hasRole("ADMIN"));

Expand Down