Skip to content
Open
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
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
# spring-sunshine-precourse
# spring-sunshine-precourse


## getCoordinatesByCityName(enum city)
도시 이름을 넣으면 위도와 경도를 반환하는 메소드
output: coordinate double DTO

## ???
시/군, 구, 동 까지 볼 수 있는 어쩌고 저쩌고

## getWeatherByCoordinates(double latitude, double longitude)
위도와 경도를 넣으면 위치에 해당하는 온도, 습도, 체감온도, 날씨코드, 요약을 반환하는 메소드
output: current.temperature_2m, relative_humedity_2m, apperent_temperature, weather_code, summary DTO

## getSummaryByInfo(current.temperature_2m, relative_humedity_2m, apperent_temperature, weather_code)
온도, 습도, 체감온도, 날씨코드로 요약을 생성하여 반환하는 메소드
output: summary string

## getDescriptionByWeatherCode(int weather_code)
날씨코드와 매핑되는 설명을 반환하는 메소드
output: string

13 changes: 11 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id("org.springframework.boot") version "3.3.1"
id("io.spring.dependency-management") version "1.1.5"
id("org.springframework.boot") version "3.5.8"
id("io.spring.dependency-management") version "1.1.7"
kotlin("plugin.jpa") version "1.9.24"
kotlin("jvm") version "1.9.24"
kotlin("plugin.spring") version "1.9.24"
Expand Down Expand Up @@ -28,11 +28,20 @@ dependencies {
implementation("org.flywaydb:flyway-core")
implementation("org.flywaydb:flyway-mysql")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation(platform("org.springframework.ai:spring-ai-bom:1.1.2"))
implementation("org.springframework.ai:spring-ai-starter-model-google-genai")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
compileOnly("org.projectlombok:lombok:1.18.30")
annotationProcessor("org.projectlombok:lombok:1.18.30")

testCompileOnly("org.projectlombok:lombok:1.18.30")
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")

}

kotlin {
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/study/AddDaysRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package study;

public record AddDaysRequest(int days) {

}
11 changes: 11 additions & 0 deletions src/main/java/study/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//package study;
//
//import org.springframework.boot.SpringApplication;
//import org.springframework.boot.autoconfigure.SpringBootApplication;
//
//@SpringBootApplication
//public class Application {
// public static void main(String[] args) {
// SpringApplication.run(Application.class, args);
// }
//}
5 changes: 5 additions & 0 deletions src/main/java/study/DateResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package study;

public record DateResponse(String day) {

}
22 changes: 22 additions & 0 deletions src/main/java/study/Functions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package study;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

import java.time.LocalDate;
import java.util.function.Function;

@Configuration
public class Functions {

@Tool(description = "Calculate a date after adding days from today")
public DateResponse addDaysFromToday(AddDaysRequest request) {
var result = LocalDate.now().plusDays(request.days());
return new DateResponse(result.toString());

}
}
64 changes: 64 additions & 0 deletions src/main/java/study/JokeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package study;

import org.springframework.ai.chat.client.ChatClient;
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.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.template.TemplateRenderer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
public class JokeController {
private final ChatClient client;

public JokeController(ChatClient.Builder builder) {
this.client = builder.build();
}




@GetMapping("/addDays")
public String addDays(
@RequestParam(defaultValue = "0") int days
) {
var template = new PromptTemplate("오늘 기준으로 {days}일 뒤 날짜를 알려줘.");
var prompt = template.render(Map.of("days", days));
return client.prompt(prompt)
.tools(new Functions())
.call()
.content();
}



@GetMapping("/joke")
public ChatResponse joke(
@RequestParam(defaultValue = "HWANG SEONGHYUN") String name,
@RequestParam(defaultValue = "pirate") String voice
) {
var user = new UserMessage("""
Tell me about three famous pirates from the Golden Age of Piracy and what they did.
Write at least one sentence for each pirate.
"""
);
var template = new SystemPromptTemplate("""
You are a helpful AI assistant.
You are an AI assistant that helps people find information.
Your name is {name}.
You should reply to the user's request using your name and in the style of a {voice}.
"""
);
var system = template.createMessage(Map.of("name", name, "voice", voice));
var prompt = new Prompt(user, system);
return client.prompt(prompt).call().chatResponse();
}
}
11 changes: 11 additions & 0 deletions src/main/java/sunshine/Current.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package sunshine;


import com.fasterxml.jackson.annotation.JsonProperty;

public record Current(
@JsonProperty("temperature_2m") double temperature2m,
@JsonProperty("relative_humidity_2m") double relativeHumidity2m,
@JsonProperty("apparent_temperature") double apparentTemperature,
@JsonProperty("weather_code") int weatherCode
) {}
15 changes: 15 additions & 0 deletions src/main/java/sunshine/HttpClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package sunshine;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestClient;

@Configuration
public class HttpClientConfig {
@Bean
RestClient restClient(RestClient.Builder builder) {
return builder.build();
}
}
4 changes: 4 additions & 0 deletions src/main/java/sunshine/LatLngDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package sunshine;

public record LatLngDto(double lat, double lng) {
}
8 changes: 8 additions & 0 deletions src/main/java/sunshine/OutfitRecommendationController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package sunshine;

import org.springframework.stereotype.Controller;

@Controller
public class OutfitRecommendationController {

}
11 changes: 11 additions & 0 deletions src/main/java/sunshine/WeatherApiResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package sunshine;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

public record WeatherApiResponseDto(
double latitude,
double longitude,
@JsonProperty("current")
Current current
) {}
106 changes: 106 additions & 0 deletions src/main/java/sunshine/WeatherQueryController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package sunshine;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import study.Functions;

import java.util.Map;

@RestController
public class WeatherQueryController {
private final ChatClient client;
private final RestClient httpClient;
private static final Logger log = LoggerFactory.getLogger(WeatherQueryController.class);

public WeatherQueryController(ChatClient.Builder builder, RestClient httpClient) {
this.client = builder.build();
this.httpClient = httpClient;
}

@GetMapping("/getLatLngDto")
public String getLatLngDto(@RequestParam(defaultValue = "서울시 강서구") String city) {

var beanOutputConverter = new BeanOutputConverter<>(LatLngDto.class);
var format = beanOutputConverter.getFormat();

var user = """
{city}의 위도와 경도 알려줘. 답변 양식은 {format}으로 알려줘야해.;
""";
var prompt = new PromptTemplate(user).create(Map.of("city", city, "format", format));

try {
// ✅ ChatResponse를 명시적으로 받기 (메타데이터/토큰/ID 로깅용)
var chatResponse = client.prompt(prompt)
.call()
.chatResponse();

var metadata = chatResponse.getMetadata(); // ChatResponseMetadata :contentReference[oaicite:1]{index=1}
var usage = metadata.getUsage(); // Usage :contentReference[oaicite:2]{index=2}

// ✅ 호출 ID + 토큰 사용량 로그
log.info("llm.call id={} model={} promptTokens={} totalTokens={}",
metadata.getId(),
metadata.getModel(),
usage.getPromptTokens(),
usage.getTotalTokens()
);

// 기존 로직: content를 꺼내서 변환
String content = chatResponse.getResult().getOutput().getText(); // 또는 너가 쓰던 result.content() 방식
LatLngDto convert = beanOutputConverter.convert(content);

WeatherApiResponseDto weatherByCoordinates = getWeatherByCoordinates(convert);
log.info("weather={}", weatherByCoordinates);

return recommendOutfit(weatherByCoordinates);

} catch (Exception e) {
log.info("llm.call failureType={}", e.getClass().getSimpleName(), e);
return null;
}
}

public WeatherApiResponseDto getWeatherByCoordinates(LatLngDto latLngDto) {
RestClient.RequestHeadersSpec<?> uri = httpClient.get()
.uri(uriBuilder -> uriBuilder
.scheme("https")
.host("api.open-meteo.com")
.path("/v1/forecast")
.queryParam("latitude", latLngDto.lat())
.queryParam("longitude", latLngDto.lng())
.queryParam("current",
"temperature_2m,relative_humidity_2m,apparent_temperature,weather_code")
.build());

log.info(uri.retrieve().body(WeatherApiResponseDto.class).toString());
return uri.retrieve().body(WeatherApiResponseDto.class);
}

public String recommendOutfit(WeatherApiResponseDto weatherinfo) {
var template = """
기온은 {temperature} 이고,
습도는 {relativeHumidity} 이고,
체감온도는 {apparentTemperature} 이고,
날씨코드는 {weatherCode} 야.5
이 데이터에 맞는 옷차림을 추천해줘.
""";

var prompt = new PromptTemplate(template).create(Map.of("temperature", weatherinfo.current().temperature2m(),
"relativeHumidity", weatherinfo.current().relativeHumidity2m(),
"apparentTemperature", weatherinfo.current().apparentTemperature(),
"weatherCode", weatherinfo.current().weatherCode()));

ChatClient.CallResponseSpec call = client.prompt(prompt).call();
log.info(call.toString());

return call.content();
}
}
4 changes: 4 additions & 0 deletions src/main/java/sunshine/WeatherSummaryController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package sunshine;

public class WeatherSummaryController {
}
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
spring.application.name=spring-sunshine
spring.ai.google.genai.api-key=YOUR_API_KEY_HERE
spring.ai.google.genai.chat.options.model=gemini-2.5-flash-lite