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
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
# spring-sunshine-precourse
# spring-sunshine-precourse

## 기능 요구 사항
- 주어진 도시 이름을 입력받아 외부 REST API를 호출해 해당 도시의 날씨 정보를 조회하고 이를 정리해 반환하는 간단한 웹 서비스를 스프링 프레임워크로 구현한다
- Git의 커밋 단위는 앞 단계에서 README.md 에 정리한 기능 목록 단위로 추가한다.
- AngularJS Git Commit Message Conventions을 참고해 커밋 메시지를 작성한다.
- AI 도구를 활용하였다면, README.md 에 활용한 방식과 코드를 어떻게 수정하였는지, 무엇을 학습하였는지 기록한다.
- 자세한 과제 진행 방법은 프리코스 진행 가이드 문서를 참고한다.
- 사용자는 도시 이름을 입력하면 해당 도시의 위도와 경도를 기반으로 날씨 정보를 조회할 수 있다.
- 최소 다섯 개 이상의 도시는 반드시 지원해야 한다.
e.g. Seoul, Tokyo, NewYork, Paris, London
- 도시 이름과 좌표를 매핑하는 자료 구조는 직접 설계한다.
- 서비스는 Open-Meteo API를 호출하여 아래 정보를 조회한다.
- 현재 온도
- 체감 온도
- 하늘 상태(맑음, 흐림 등)
- 습도
- 조회한 데이터를 기반으로 간단한 한 줄 요약 문장을 생성하는 기능을 추가한다.
e.g. "현재 서울의 기온은 3.4°C이며, 풍속은 5.7m/s입니다. 날씨는 흐림입니다."
7 changes: 5 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,6 +28,8 @@ 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")
runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")
Expand All @@ -44,3 +46,4 @@ kotlin {
tasks.withType<Test> {
useJUnitPlatform()
}

5 changes: 5 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
org.gradle.daemon=true
org.gradle.console=rich
org.gradle.parallel=true
org.gradle.user.home=C:/Users/chaeyj/gradle-home

5 changes: 2 additions & 3 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#Wed Dec 10 14:47:04 KST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
5 changes: 5 additions & 0 deletions src/main/java/study/AddDayRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package study;

public record AddDayRequest(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);
// }
//}
17 changes: 17 additions & 0 deletions src/main/java/study/DateResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package study;

import java.time.LocalDate;


public class DateResponse {
private LocalDate date;

public DateResponse(LocalDate date) {
this.date = date;
}

public LocalDate getDate() {
return date;
}

}
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.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import java.util.function.Function;

import java.time.LocalDate;

@Configuration
public class Functions {

@Description("Calculate a date after adding days from today")
@Bean
public Function<AddDayRequest, DateResponse> addDaysFromToday() {
return request ->
new DateResponse(
LocalDate.now().plusDays(request.days())
);
}
}

81 changes: 81 additions & 0 deletions src/main/java/study/JokeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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.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).call().content();
}
// public record ActorsFilms(String actor, List<String> movies) {
//
// }

// @GetMapping("/actors")
// public ActorFilms actors(@RequestParam(defaultValue = "Tom Cruise") String actor)
// ) {
//
// var beanOutputConverter = new BeanOutputConverter<>(ActorsFilms.class);
// var format = beanOutputConverter.getFormat();
//
// log.debug(format);
//
// var userMessage = """
// Generate the filmography of 5 movies for {actor}.
// {format}
// """;
// var promptTemplate = new PromptTemplate(userMessage, Map.of("actor", actor, "format", format));
// var prompt = promptTemplate.create();
// beanOutputConverter.convert(client.prompt(prompt).call().chatResponse().getResult().getOutput().getText());
//
//
// }


@GetMapping("/joke")
public ChatResponse joke(@RequestParam(defaultValue = "Bob") String name,
@RequestParam(defaultValue = "pirate") String voice
) {
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(List.of(userMessage, systemMessage));

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

}

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

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

import java.time.LocalDate;

@Configuration
public class Functions {

@Description("요청된 도시의 날씨 데이터를 요약 문장으로 생성합니다.")
@Bean("makeWeatherSummary")
public Function<WeatherFunctionRequest, WeatherFunctionResponse> makeWeatherSummary() {

return request -> {

String city = request.city();
float currentTemperature = request.currentTemperature();
float apparentTemperature = request.apparentTemperature();
int weatherCode = request.weatherCode();
float humidity = request.humidity();

// 자연스러운 한 줄 요약 생성
String summary = String.format(
"현재 %s의 기온은 %.1f°C이며, 체감온도는 %.1f°C입니다. 하늘 상태는 '%d'이며 습도는 %.0f%%입니다.",
city, currentTemperature, apparentTemperature, weatherCode, humidity
);

return new WeatherFunctionResponse(summary);
};
}

private boolean isRain(int code) {
return (code >= 51 && code <= 67) || (code >= 80 && code <= 82);
}

private boolean isSnow(int code) {
return (code >= 71 && code <= 77) || code == 85 || code == 86;
}

private boolean isClear(int code) {
return code == 0 || code == 1;
}


@Description("Open-Meteo 날씨 데이터를 기준으로 오늘 입기 좋은 복장을 추천한다.")
@Bean("recommendOutfit")
public Function<OutfitRecommendationRequest, OutfitRecommendationResponse> recommendOutfit() {

return request -> {

// 1. 기준 온도 선택
float baseTemp = request.apparentTemperature() > 0
? request.apparentTemperature()
: request.currentTemperature();

String outer;
String top;
String bottom;
String shoes = "운동화";
List<String> extras = new ArrayList<>();

// 2. 기온 기준 복장
if (baseTemp <= -5) {
outer = "두꺼운 패딩";
top = "기모 상의";
bottom = "기모 바지";
extras.add("목도리");
extras.add("장갑");
}
else if (baseTemp <= 4) {
outer = "코트나 패딩";
top = "니트";
bottom = "긴바지";
}
else if (baseTemp <= 9) {
outer = "자켓";
top = "후드나 니트";
bottom = "긴바지";
}
else if (baseTemp <= 16) {
outer = "얇은 자켓";
top = "맨투맨이나 셔츠";
bottom = "슬랙스나 청바지";
}
else if (baseTemp <= 22) {
outer = "가벼운 가디건";
top = "반팔이나 얇은 긴팔";
bottom = "면바지";
}
else if (baseTemp <= 27) {
outer = "겉옷 없이";
top = "반팔";
bottom = "가벼운 바지";
}
else {
outer = "겉옷 없이";
top = "민소매나 반팔";
bottom = "반바지";
extras.add("선크림");
}

// 3. 강수 보정 (weatherCode)
if (isRain(request.weatherCode())) {
outer += " (방수)";
shoes = "방수 운동화";
extras.add("우산");
}

if (isSnow(request.weatherCode())) {
shoes = "미끄럼 방지 신발";
extras.add("장갑");
}

// 4. 습도 보정
if (request.humidity() >= 75 && baseTemp >= 23) {
extras.add("통풍 잘 되는 옷");
}

// 5. 문장 생성
StringBuilder sb = new StringBuilder();
sb.append("오늘은 ");

if (!outer.contains("없이")) {
sb.append(outer).append("에 ");
}

sb.append(top)
.append("을 입고 ")
.append(bottom)
.append("를 입으면 좋아요. ")
.append(shoes)
.append("를 추천해요.");

if (!extras.isEmpty()) {
sb.append(" ");
sb.append(String.join(", ", extras))
.append("도 챙기세요.");
}

return new OutfitRecommendationResponse(sb.toString());
};
}


}

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

public record OutfitRecommendationRequest(
float currentTemperature,
float apparentTemperature,
int weatherCode,
float humidity
) {}
5 changes: 5 additions & 0 deletions src/main/java/sunshine/ai/OutfitRecommendationResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package sunshine.ai;

public record OutfitRecommendationResponse(
String recommendation
) {}
9 changes: 9 additions & 0 deletions src/main/java/sunshine/ai/WeatherFunctionRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package sunshine.ai;

public record WeatherFunctionRequest(
String city,
float currentTemperature,
float apparentTemperature,
int weatherCode,
float humidity
) {}
6 changes: 6 additions & 0 deletions src/main/java/sunshine/ai/WeatherFunctionResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package sunshine.ai;

public record WeatherFunctionResponse(
String summary
) {}

Loading