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
130 changes: 129 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,129 @@
# spring-sunshine-precourse
# Spring Sunshine - 날씨 조회 서비스

스프링 프레임워크를 사용한 AI 기반 날씨 조회 및 복장 추천 웹 서비스입니다.

## 주요 기능

### 1. 다양한 위치 지원
- **도시**: Seoul, Tokyo, NewYork, Paris, London
- **권역**: 수도권(Sudogwon), 영남권(Yeongnam)
- **구/군**: 강남구(Gangnam), 종로구(Jongno), 마포구(Mapo)

### 2. AI 기반 날씨 요약
- Google Gemini AI를 사용한 자연어 날씨 요약
- 캐시 기능으로 비용 절약 및 성능 향상
- 토큰 사용량 및 비용 추정치 로깅

### 3. 스마트 복장 추천
- 기온, 체감온도, 날씨 상태 기반 복장 추천
- 바람, 습도 등 세부 기상 조건 고려

### 4. 사용량 추적
- 입력/출력 토큰 수 모니터링
- API 호출 비용 추정
- 캐시 사용 여부 추적

## API 엔드포인트

### 새로운 AI 기반 API

#### 1. 단일 위치 AI 요약 조회
```
GET /api/v1/weather/summary/{location}?useCache=true
```

**예시:**
```bash
curl "http://localhost:8080/api/v1/weather/summary/Seoul?useCache=true"
curl "http://localhost:8080/api/v1/weather/summary/Gangnam"
curl "http://localhost:8080/api/v1/weather/summary/Sudogwon"
```

**응답 예시:**
```json
{
"city": "Seoul",
"temperature": 15.2,
"apparentTemperature": 13.8,
"skyCondition": "부분 흐림",
"humidity": 65.0,
"windSpeed": 3.2,
"aiSummary": "현재 서울의 기온은 15.2°C이며, 체감온도는 13.8°C입니다. 하늘은 부분적으로 흐리고 바람은 약합니다.",
"clothingRecommendation": "얇은 가디건, 긴팔 티셔츠, 면바지, 청바지",
"fromCache": false,
"usageInfo": {
"inputTokens": 45,
"outputTokens": 32,
"totalTokens": 77,
"modelName": "gemini-2.5-flash-lite",
"cacheUsed": false,
"estimatedCost": 0.000013
}
}
```

#### 2. 다중 위치 AI 요약 조회
```
GET /api/v1/weather/summary?locations=Seoul,Gangnam,Sudogwon&useCache=true
```

#### 3. 캐시 설정 변경
```
POST /api/v1/weather/cache?enabled=true
```

### 기존 호환 API

#### 기본 날씨 정보 조회
```
GET /api/v1/weather/{city}
GET /api/v1/weather?cities=Seoul,Tokyo
```

## 지원 위치 목록

| 타입 | 위치명 | 설명 |
|------|--------|------|
| 도시 | Seoul | 서울 |
| 도시 | Tokyo | 도쿄 |
| 도시 | NewYork | 뉴욕 |
| 도시 | Paris | 파리 |
| 도시 | London | 런던 |
| 구/군 | Gangnam | 강남구 |
| 구/군 | Jongno | 종로구 |
| 구/군 | Mapo | 마포구 |
| 권역 | Sudogwon | 수도권 |
| 권역 | Yeongnam | 영남권 |

## 복장 추천 기준

| 체감온도 | 추천 복장 |
|----------|----------|
| 28°C 이상 | 민소매, 반팔, 반바지, 원피스 |
| 23-27°C | 반팔, 얇은 셔츠, 반바지, 면바지 |
| 20-22°C | 얇은 가디건, 긴팔 티셔츠, 면바지, 청바지 |
| 17-19°C | 얇은 니트, 맨투맨, 가디건, 청바지 |
| 12-16°C | 자켓, 가디건, 청바지, 면바지 |
| 9-11°C | 트렌치코트, 야상, 니트, 청바지, 스타킹 |
| 5-8°C | 울코트, 히트텍, 니트, 청바지 |
| 5°C 미만 | 패딩, 두꺼운 코트, 목도리, 장갑, 기모제품 |

**추가 조건:**
- 흐림/비: 우산 필수
- 강풍(7m/s 이상): 바람막이 추천
- 고습도(80% 이상): 통풍이 잘 되는 소재 권장

## 실행 방법

```bash
./gradlew bootRun
```

## 환경 설정

`application.properties`에서 다음 설정을 확인하세요:

```properties
spring.ai.google.genai.api-key=YOUR_GEMINI_API_KEY
spring.ai.google.genai.chat.options.model=gemini-2.5-flash-lite
```
10 changes: 8 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 @@ -33,6 +33,8 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation(platform("org.springframework.ai:spring-ai-bom:1.1.2"))
implementation("org.springframework.ai:spring-ai-starter-model-google-genai")
}

kotlin {
Expand All @@ -44,3 +46,7 @@ kotlin {
tasks.withType<Test> {
useJUnitPlatform()
}

springBoot {
mainClass.set("sunshine.Application")
}
27 changes: 27 additions & 0 deletions src/main/java/study/Function.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package study;

import java.time.LocalDate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

@Configuration
class Function {
@Bean
@Description("Calculate a date after adding days from today")
public java.util.function.Function<AddDaysRequest, DateResponse> addDaysFromToday() {
return request -> {
LocalDate result = LocalDate.now().plusDays(request.days());
return new DateResponse(result.toString());
};
}

public record AddDaysRequest(int days) {
}

public record DateResponse(String date) {
}


}

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

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 String joke(
@RequestParam(defaultValue = "Tell me a joke") String message,
@RequestParam(defaultValue = "Programming") String topic) {
var template = new PromptTemplate("Tell me a joke about {topic}");
var prompt = template.render(Map.of("topic", topic));
return client.prompt(prompt)
.call()
.content();
}


@GetMapping("/joke2")
public ChatResponse joke2(
@RequestParam(defaultValue = "Bob") String name,
@RequestParam(defaultValue = "pirate") String voice) {


var template = new PromptTemplate("Tell me a joke about {topic}");

var userMessage = 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 systemPromptTemplate = 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 systemMessage =
systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));
var prompt = new Prompt(userMessage, systemMessage);
client.prompt(prompt).call().chatResponse();

return client.prompt(prompt)
.call()
.chatResponse();
}



//
// @GetMapping("/actors")
// public ActorFilms actors(
// @RequestParam(defaultValue = "Bob") String actor
// ) {
//
// var beanOutputConverter = new BeanOutputConverter<>(ActorFilms.class);
// var format = beanOutputConverter.getFormat();
// var userMessage = """
// Generate the filmography of 5 movies for {actor}.
// {format}
// """;
// var promptTemplate = new PromptTemplate(Map.of("actor", actor, "format", format));
// var prompt = promptTemplate.create();
// beanOutputConverter.convert(chatClient.prompt(prompt).call().chatResponse().getResult().getOutput().getText());
//
//
// return
//
// }
//
// public Vase
}
12 changes: 12 additions & 0 deletions src/main/java/study/StudyApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package study;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "study")
class StudyApplication {
public static void main(String[] args) {
SpringApplication.run(StudyApplication.class, args);
}

}
2 changes: 1 addition & 1 deletion src/main/java/sunshine/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@SpringBootApplication(scanBasePackages = "sunshine")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/sunshine/RestTemplateConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package sunshine;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
* RestTemplate 설정
*/
@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
15 changes: 15 additions & 0 deletions src/main/java/sunshine/config/AiConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package sunshine.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.google.genai.GoogleGenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {

@Bean
public ChatClient chatClient(GoogleGenAiChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
}
Loading