diff --git a/build.gradle.kts b/build.gradle.kts index 3f75395..d49af70 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,11 +19,22 @@ repositories { mavenCentral() } +extra["springCloudVersion"] = "2023.0.2" + +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") + } +} + dependencies { + implementation(platform("org.springframework.ai:spring-ai-bom:1.1.2")) implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.cloud:spring-cloud-starter-openfeign") + implementation("org.springframework.ai:spring-ai-starter-model-google-genai") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.flywaydb:flyway-core") implementation("org.flywaydb:flyway-mysql") @@ -33,6 +44,10 @@ dependencies { 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") + annotationProcessor("org.projectlombok:lombok") + } kotlin { diff --git a/src/main/java/sunshine/Application.java b/src/main/java/sunshine/Application.java index b5d46a4..366a120 100644 --- a/src/main/java/sunshine/Application.java +++ b/src/main/java/sunshine/Application.java @@ -2,10 +2,19 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; +@EnableFeignClients @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } } diff --git a/src/main/java/sunshine/application/WeatherService.java b/src/main/java/sunshine/application/WeatherService.java new file mode 100644 index 0000000..4a1effa --- /dev/null +++ b/src/main/java/sunshine/application/WeatherService.java @@ -0,0 +1,40 @@ +package sunshine.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.stereotype.Service; +import sunshine.domain.ClothingRecommendationService; +import sunshine.domain.GeocodingResponse; +import sunshine.domain.GeocodingService; +import sunshine.domain.WeatherDomainService; +import sunshine.domain.WeatherResponseDto; + +@Slf4j +@Service +@RequiredArgsConstructor +public class WeatherService { + + private final GeocodingService geocodingService; + private final WeatherDomainService weatherDomainService; + private final ClothingRecommendationService clothingRecommendationService; + + public ChatResponse getWeather(String cityName) { + // 1. 권역 조회 + GeocodingResponse geocode = geocodingService.geocode(cityName); + + // 2. 날씨 조회 + WeatherResponseDto weather = weatherDomainService.getWeather(geocode); + + // 3. 복장 추천 (llm) + ChatResponse response = clothingRecommendationService.recommendClothing(weather); + + // 토큰 사용량 로그 출력 + log.info("Gemini API 토큰 사용량 - 입력 토큰: {}, 출력 토큰: {}, 총 토큰: {}", + response.getMetadata().getUsage().getPromptTokens(), + response.getMetadata().getUsage().getCompletionTokens(), + response.getMetadata().getUsage().getTotalTokens()); + + return response; + } +} diff --git a/src/main/java/sunshine/domain/City.java b/src/main/java/sunshine/domain/City.java new file mode 100644 index 0000000..4a3f953 --- /dev/null +++ b/src/main/java/sunshine/domain/City.java @@ -0,0 +1,42 @@ +package sunshine.domain; + +import lombok.Getter; + +@Getter +public enum City { + + Seoul("서울", "37.5665", "126.9780"), + Tokyo("도쿄", "35.6762", "139.6503"), + NewYork("뉴욕", "40.7128", "-74.0060"), + Paris("파리", "48.8566", "2.3522"), + London("런던", "51.5074", "-0.1278"); + + private final String cityKorName; + private final String latitude; + private final String longitude; + + City(String cityKorName, String latitude, String longitude) { + this.cityKorName = cityKorName; + this.latitude = latitude; + this.longitude = longitude; + } + + public static City getCityInfo(String cityName) { + for (City city : City.values()) { + if (city.name().equalsIgnoreCase(cityName)) { + return city; + } + } + throw new IllegalArgumentException("Invalid city name: " + cityName); + } + + public static String getCityKorName(String cityName) { + for (City city : City.values()) { + if (city.name().equalsIgnoreCase(cityName)) { + return city.cityKorName; + } + } + throw new IllegalArgumentException("Invalid city name: " + cityName); + } + +} diff --git a/src/main/java/sunshine/domain/ClothingRecommendationService.java b/src/main/java/sunshine/domain/ClothingRecommendationService.java new file mode 100644 index 0000000..b92824f --- /dev/null +++ b/src/main/java/sunshine/domain/ClothingRecommendationService.java @@ -0,0 +1,56 @@ +package sunshine.domain; + +import java.util.Map; +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.SystemPromptTemplate; +import org.springframework.stereotype.Service; + +@Service +public class ClothingRecommendationService { + + private final ChatClient client; + + public ClothingRecommendationService(ChatClient.Builder builder) { + this.client = builder.build(); + } + + public ChatResponse recommendClothing(WeatherResponseDto weather) { + var systemPrompt = new SystemPromptTemplate(""" + You are a helpful fashion advisor AI assistant. + You provide clothing recommendations based on weather conditions. + Your name is {name}. + You should reply in Korean and be friendly and practical. + """); + var system = systemPrompt.createMessage(Map.of("name", "StyleHelper")); + + var userMessage = new UserMessage(String.format(""" + 현재 날씨 정보를 바탕으로 어떤 옷을 입으면 좋을지 추천해주세요. + + - 기온: %s°C + - 체감온도: %s°C + - 습도: %s%% + - 날씨 상태: %s + - 강수량: %smm + - 비: %smm + - 눈: %smm + + 위 날씨 정보를 고려하여 상의, 하의, 신발, 필요한 액세서리를 추천해주세요. + """, + weather.getCurrent().getTemperature2m(), + weather.getCurrent().getApparentTemperature(), + weather.getCurrent().getRelativeHumidity2m(), + WeatherCode.getDescription(weather.getCurrent().getWeatherCode()), + weather.getCurrent().getPrecipitation(), + weather.getCurrent().getRain(), + weather.getCurrent().getSnowfall() + )); + var prompt = new Prompt(userMessage, system); + + return client.prompt(prompt) + .call() + .chatResponse(); + } +} diff --git a/src/main/java/sunshine/domain/GeocodingResponse.java b/src/main/java/sunshine/domain/GeocodingResponse.java new file mode 100644 index 0000000..6482226 --- /dev/null +++ b/src/main/java/sunshine/domain/GeocodingResponse.java @@ -0,0 +1,12 @@ +package sunshine.domain; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class GeocodingResponse { + private double lat; + private double lon; + private String displayName; +} \ No newline at end of file diff --git a/src/main/java/sunshine/domain/GeocodingService.java b/src/main/java/sunshine/domain/GeocodingService.java new file mode 100644 index 0000000..8b6648a --- /dev/null +++ b/src/main/java/sunshine/domain/GeocodingService.java @@ -0,0 +1,78 @@ +package sunshine.domain; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import sunshine.domain.exception.ExternalApiException; +import sunshine.domain.exception.InvalidLocationException; + +@Service +@RequiredArgsConstructor +public class GeocodingService { + private final RestTemplate restTemplate; + + public GeocodingResponse geocode(String location) { + if (location == null || location.trim().isEmpty()) { + throw new InvalidLocationException("지역명이 비어있습니다."); + } + + String url = "https://nominatim.openstreetmap.org/search?q=" + location + "&format=json&limit=1"; + + try { + HttpHeaders headers = new HttpHeaders(); + headers.set("User-Agent", "Spring-Sunshine-Weather-App"); + HttpEntity entity = new HttpEntity<>(headers); + + JsonNode[] response = restTemplate.exchange( + url, + HttpMethod.GET, + entity, + JsonNode[].class + ).getBody(); + + // 응답이 null이거나 비어있는 경우 + if (response == null || response.length == 0) { + throw new InvalidLocationException("지역을 찾을 수 없습니다: " + location); + } + + JsonNode firstResult = response[0]; + + // 필수 필드 검증 + if (!firstResult.has("lat") || !firstResult.has("lon") || !firstResult.has("display_name")) { + throw new ExternalApiException("Geocoding API 응답에 필수 필드가 없습니다."); + } + + double lat = firstResult.get("lat").asDouble(); + double lon = firstResult.get("lon").asDouble(); + String displayName = firstResult.get("display_name").asText(); + + return new GeocodingResponse(lat, lon, displayName); + + } catch (HttpClientErrorException e) { + // 4xx 에러: 클라이언트 오류 (재시도 불필요) + throw new ExternalApiException("Geocoding API 클라이언트 오류: " + e.getStatusCode(), e); + } catch (HttpServerErrorException e) { + // 5xx 에러: 서버 오류 (재시도 가능) + throw new ExternalApiException("Geocoding API 서버 오류: " + e.getStatusCode(), e); + } catch (ResourceAccessException e) { + // 네트워크 오류, 타임아웃 + throw new ExternalApiException("Geocoding API 네트워크 오류 또는 타임아웃", e); + } catch (RestClientException e) { + // 기타 RestTemplate 예외 + throw new ExternalApiException("Geocoding API 호출 실패", e); + } catch (Exception e) { + // JSON 파싱 오류 등 기타 예외 + throw new ExternalApiException("Geocoding API 응답 처리 실패", e); + } + } +} + + diff --git a/src/main/java/sunshine/domain/WeatherCode.java b/src/main/java/sunshine/domain/WeatherCode.java new file mode 100644 index 0000000..92976c6 --- /dev/null +++ b/src/main/java/sunshine/domain/WeatherCode.java @@ -0,0 +1,54 @@ +package sunshine.domain; + +public enum WeatherCode { + CLEAR_SKY(0, "맑음"), + MAINLY_CLEAR(1, "대체로 맑음"), + PARTLY_CLOUDY(2, "부분적으로 흐림"), + OVERCAST(3, "흐림"), + FOG(45, "안개"), + DEPOSITING_RIME_FOG(48, "서리 안개"), + DRIZZLE_LIGHT(51, "가벼운 이슬비"), + DRIZZLE_MODERATE(53, "이슬비"), + DRIZZLE_DENSE(55, "강한 이슬비"), + FREEZING_DRIZZLE_LIGHT(56, "가벼운 어는 이슬비"), + FREEZING_DRIZZLE_DENSE(57, "강한 어는 이슬비"), + RAIN_SLIGHT(61, "약한 비"), + RAIN_MODERATE(63, "비"), + RAIN_HEAVY(65, "강한 비"), + FREEZING_RAIN_LIGHT(66, "약한 어는 비"), + FREEZING_RAIN_HEAVY(67, "강한 어는 비"), + SNOW_SLIGHT(71, "약한 눈"), + SNOW_MODERATE(73, "눈"), + SNOW_HEAVY(75, "강한 눈"), + SNOW_GRAINS(77, "눈알갱이"), + RAIN_SHOWERS_SLIGHT(80, "약한 소나기"), + RAIN_SHOWERS_MODERATE(81, "소나기"), + RAIN_SHOWERS_VIOLENT(82, "강한 소나기"), + SNOW_SHOWERS_SLIGHT(85, "약한 눈 소나기"), + SNOW_SHOWERS_HEAVY(86, "강한 눈 소나기"), + THUNDERSTORM(95, "천둥번개"), + THUNDERSTORM_SLIGHT_HAIL(96, "약한 우박을 동반한 천둥번개"), + THUNDERSTORM_HEAVY_HAIL(99, "강한 우박을 동반한 천둥번개"); + + private final int code; + private final String description; + + WeatherCode(int code, String description) { + this.code = code; + this.description = description; + } + + public static String getDescription(String codeStr) { + try { + int code = Integer.parseInt(codeStr); + for (WeatherCode weatherCode : WeatherCode.values()) { + if (weatherCode.code == code) { + return weatherCode.description; + } + } + return "알 수 없는 날씨"; + } catch (NumberFormatException e) { + return "알 수 없는 날씨"; + } + } +} diff --git a/src/main/java/sunshine/domain/WeatherDomainClient.java b/src/main/java/sunshine/domain/WeatherDomainClient.java new file mode 100644 index 0000000..e5d696a --- /dev/null +++ b/src/main/java/sunshine/domain/WeatherDomainClient.java @@ -0,0 +1,7 @@ +package sunshine.domain; + +public interface WeatherDomainClient { + + WeatherResponseDto getWeather(double latitude, double longitude, String current); + +} diff --git a/src/main/java/sunshine/domain/WeatherDomainService.java b/src/main/java/sunshine/domain/WeatherDomainService.java new file mode 100644 index 0000000..3ebc106 --- /dev/null +++ b/src/main/java/sunshine/domain/WeatherDomainService.java @@ -0,0 +1,16 @@ +package sunshine.domain; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class WeatherDomainService { + + private final WeatherDomainClient weatherDomainClient; + + public WeatherResponseDto getWeather(GeocodingResponse geo) { + String current = "temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,rain,showers,snowfall,weather_code"; + return weatherDomainClient.getWeather(geo.getLat(), geo.getLon(), current); + } +} diff --git a/src/main/java/sunshine/domain/WeatherResponseDto.java b/src/main/java/sunshine/domain/WeatherResponseDto.java new file mode 100644 index 0000000..d614f58 --- /dev/null +++ b/src/main/java/sunshine/domain/WeatherResponseDto.java @@ -0,0 +1,69 @@ +package sunshine.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class WeatherResponseDto { + + @JsonProperty("latitude") + private String latitude; + + @JsonProperty("longitude") + private String longitude; + + @JsonProperty("generationtime_ms") + private String generationTimeMs; + + @JsonProperty("utc_offset_seconds") + private String utcOffsetSeconds; + + @JsonProperty("timezone") + private String timezone; + + @JsonProperty("timezone_abbreviation") + private String timezoneAbbreviation; + + @JsonProperty("elevation") + private String elevation; + + @JsonProperty("current_units") + private CurrentUnits currentUnits; + + @JsonProperty("current") + private CurrentUnits current; + + @Data + public static class CurrentUnits { + + @JsonProperty("time") + private String time; + + @JsonProperty("interval") + private String interval; + + @JsonProperty("temperature_2m") + private String temperature2m; + + @JsonProperty("relative_humidity_2m") + private String relativeHumidity2m; + + @JsonProperty("apparent_temperature") + private String apparentTemperature; + + @JsonProperty("precipitation") + private String precipitation; + + @JsonProperty("rain") + private String rain; + + @JsonProperty("showers") + private String showers; + + @JsonProperty("snowfall") + private String snowfall; + + @JsonProperty("weather_code") + private String weatherCode; + } +} \ No newline at end of file diff --git a/src/main/java/sunshine/domain/exception/ExternalApiException.java b/src/main/java/sunshine/domain/exception/ExternalApiException.java new file mode 100644 index 0000000..566d0b2 --- /dev/null +++ b/src/main/java/sunshine/domain/exception/ExternalApiException.java @@ -0,0 +1,15 @@ +package sunshine.domain.exception; + +public class ExternalApiException extends RuntimeException { + public ExternalApiException() { + super("External API call failed."); + } + + public ExternalApiException(String message) { + super(message); + } + + public ExternalApiException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/sunshine/domain/exception/InvalidLocationException.java b/src/main/java/sunshine/domain/exception/InvalidLocationException.java new file mode 100644 index 0000000..8bf9ce7 --- /dev/null +++ b/src/main/java/sunshine/domain/exception/InvalidLocationException.java @@ -0,0 +1,7 @@ +package sunshine.domain.exception; + +public class InvalidLocationException extends RuntimeException { + public InvalidLocationException(String message) { + super(message); + } +} diff --git a/src/main/java/sunshine/domain/exception/WebExceptionHandler.java b/src/main/java/sunshine/domain/exception/WebExceptionHandler.java new file mode 100644 index 0000000..64ee1e5 --- /dev/null +++ b/src/main/java/sunshine/domain/exception/WebExceptionHandler.java @@ -0,0 +1,27 @@ +package sunshine.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; +import sunshine.dto.ErrorResponse; + +@RestControllerAdvice +public class WebExceptionHandler { + + @ExceptionHandler(InvalidLocationException.class) + public ResponseEntity handleInvalidLocation(InvalidLocationException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse("INVALID_LOCATION", e.getMessage())); + } + + @ExceptionHandler(ExternalApiException.class) + public ResponseEntity handleExternalApi(ExternalApiException e) { + return ResponseEntity.status(HttpStatus.BAD_GATEWAY) + .body(new ErrorResponse("EXTERNAL_API_ERROR", e.getMessage())); + } +} + diff --git a/src/main/java/sunshine/infrastructure/WeatherDomainFeignClient.java b/src/main/java/sunshine/infrastructure/WeatherDomainFeignClient.java new file mode 100644 index 0000000..c3fc3d4 --- /dev/null +++ b/src/main/java/sunshine/infrastructure/WeatherDomainFeignClient.java @@ -0,0 +1,46 @@ +package sunshine.infrastructure; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClientException; +import sunshine.domain.WeatherDomainClient; +import sunshine.domain.WeatherResponseDto; + +@Slf4j +@Component +@RequiredArgsConstructor +public class WeatherDomainFeignClient implements WeatherDomainClient { + + private final WeatherFeignClient weatherFeignClient; + + @Override + public WeatherResponseDto getWeather(double latitude, double longitude, String current) { + WeatherResponseDto response = null; + try { + response = weatherFeignClient.getWeather(latitude, longitude, current); + } catch (HttpClientErrorException e) { + // 4xx 클라이언트 에러 (잘못된 요청, 권한 없음 등) + log.error("HttpClientErrorException: {}", e.getMessage()); + } catch (HttpServerErrorException e) { + // 5xx 서버 에러 + log.error("HttpServerErrorException: {}", e.getMessage()); + } catch (ResourceAccessException e) { + // 네트워크 연결 실패, 타임아웃 + log.error("ResourceAccessException: {}", e.getMessage()); + } catch (RestClientException e) { + // 기타 REST 클라이언트 예외 + log.error("RestClientException: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + // 잘못된 파라미터 + log.error("IllegalArgumentException: {}", e.getMessage()); + } catch (Exception e) { + // 기타 모든 예외 + log.error("Exception: {}", e.getMessage()); + } + return response; + } +} diff --git a/src/main/java/sunshine/infrastructure/WeatherFeignClient.java b/src/main/java/sunshine/infrastructure/WeatherFeignClient.java new file mode 100644 index 0000000..b6fa28d --- /dev/null +++ b/src/main/java/sunshine/infrastructure/WeatherFeignClient.java @@ -0,0 +1,17 @@ +package sunshine.infrastructure; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import sunshine.domain.WeatherResponseDto; + +// https://api.open-meteo.com/v1/forecast?latitude=37.566&longitude=126.9784¤t=temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,rain,showers,snowfall,weather_code +@FeignClient(name = "weather", url = "https://api.open-meteo.com/v1") +public interface WeatherFeignClient { + + @GetMapping("/forecast") + WeatherResponseDto getWeather(@RequestParam("latitude") double latitude, + @RequestParam("longitude") double longitude, + @RequestParam("current") String current); + +} diff --git a/src/main/java/sunshine/presentation/JokeController.java b/src/main/java/sunshine/presentation/JokeController.java new file mode 100644 index 0000000..c72951e --- /dev/null +++ b/src/main/java/sunshine/presentation/JokeController.java @@ -0,0 +1,63 @@ +package sunshine.presentation; + +import java.util.Map; +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.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class JokeController { + + private final ChatClient client; + + public JokeController(ChatClient.Builder builder) { + this.client = builder.build(); + } + + @GetMapping("/joke") + public ChatResponse getJoke(@RequestParam(defaultValue = "eong") 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(); + } + + @GetMapping("/joke2") + public String getJoke2(@RequestParam(defaultValue = "Tell me a joke about {topic}") String message) { + return client.prompt(message) + .call() + .content(); + } + + @GetMapping("/joke3") + public ChatResponse getJoke3(@RequestParam(defaultValue = "Tell me a joke about {topic}") String message, + @RequestParam(defaultValue = "programming") String topic) { + var template = new PromptTemplate(message); + var prompt = template.render(Map.of("topic", topic)); + return client.prompt(prompt) + .call() + .chatResponse(); + } +} + diff --git a/src/main/java/sunshine/presentation/WeatherController.java b/src/main/java/sunshine/presentation/WeatherController.java new file mode 100644 index 0000000..2610c5b --- /dev/null +++ b/src/main/java/sunshine/presentation/WeatherController.java @@ -0,0 +1,24 @@ +package sunshine.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import sunshine.application.WeatherService; + +@RestController +@RequiredArgsConstructor +public class WeatherController { + + private final WeatherService weatherService; + + @GetMapping("/getWeather") + public String getWeather(@RequestParam("city") String cityName) { + + ChatResponse response = weatherService.getWeather(cityName); + + return response.getResult().getOutput().getText(); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 10fffc9..4ee6e6a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,3 @@ spring.application.name=spring-sunshine +spring.ai.google.genai.api-key=MY_TOKEN +spring.ai.google.genai.chat.options.model=gemini-2.5-flash-lite \ No newline at end of file