Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
41c994c
docs: README.md에 기능 목록 및 구현 전략 작성
kimheonseung Dec 12, 2025
6ae0108
feat: 도시 좌표 매핑 City enum 구현
kimheonseung Dec 12, 2025
ff2821a
feat: WMO weather code 한국어 변환기 구현
kimheonseung Dec 12, 2025
ceee752
feat: Open-Meteo API 연동 및 날씨 조회 기능 구현
kimheonseung Dec 12, 2025
8f050a6
test: 도메인 클래스 단위 테스트 추가
kimheonseung Dec 12, 2025
ffb14c4
refactor: WeatherService 메서드 15줄 이하로 분리
kimheonseung Dec 12, 2025
4be290f
docs: Claude Code 프로젝트 지침 파일 추가
kimheonseung Dec 12, 2025
75ac06d
test: WeatherController 테스트 추가
kimheonseung Dec 12, 2025
2503a85
chore: 설정 파일 정리
kimheonseung Dec 12, 2025
28917e6
refactor: GlobalExceptionHandler로 예외 처리 분리
kimheonseung Dec 12, 2025
046379b
docs: README.md에 AI 활용 방법 및 수정 내용 상세 작성
kimheonseung Dec 12, 2025
0534b12
docs: README.md 1차/2차 작업으로 구분하여 재구성
kimheonseung Dec 12, 2025
e8a0edd
feat: MySQL Docker 환경 및 DB 마이그레이션 추가
kimheonseung Dec 12, 2025
d0639d9
feat: 도시 및 날씨 코드 매핑을 DB로 전환
kimheonseung Dec 12, 2025
19fa4bf
docs: README.md에 2차 작업 내용 추가
kimheonseung Dec 12, 2025
5ed6c2d
refactor: Controller에서 Repository 직접 의존 제거
kimheonseung Dec 12, 2025
46cea80
refactor: WeatherCodeTranslator에 readOnly 트랜잭션 적용
kimheonseung Dec 12, 2025
c0d50d8
docs: README.md 2차 작업 내용 보강
kimheonseung Dec 12, 2025
3655e2a
docs: 과제 진행 소감 추가
kimheonseung Dec 12, 2025
72f9216
feat: 날씨조회(LLM)
kimheonseung Dec 16, 2025
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
70 changes: 70 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Claude Code 프로젝트 지침

## 프로그래밍 요구사항

### 코드 스타일
- **Google Java Style Guide** 준수
- **들여쓰기**: 4 spaces (2 spaces 아님)

### 코드 구조 제약
- **들여쓰기 단계**: 최대 2단계까지만 허용 (3단계 이상 금지)
- 힌트: 함수를 분리하여 들여쓰기 단계를 줄일 것
- **메서드 길이**: 15줄 이하
- 메서드가 길어지면 작은 메서드로 분리할 것
- **함수 단일 책임**: 함수는 한 가지 일만 수행

### 금지 사항
- `else` 키워드 사용 금지
- 힌트: if문에서 값을 반환하는 방식으로 구현
- `switch`문 사용 금지
- 삼항 연산자(`? :`) 사용 금지

### 테스트
- **JUnit 5**와 **AssertJ**로 테스트 작성
- 기능 목록이 정상 작동하는지 테스트로 검증

---

## Git Commit Message Convention (AngularJS)

### 형식
```
<type>: <subject>

<body>

<footer>
```

### Type 종류
| Type | 설명 |
|------|------|
| `feat` | 새로운 기능 추가 |
| `fix` | 버그 수정 |
| `docs` | 문서 변경 (README 등) |
| `style` | 코드 포맷팅, 세미콜론 누락 등 (기능 변경 없음) |
| `refactor` | 코드 리팩토링 (기능 변경 없음) |
| `test` | 테스트 추가 또는 수정 |
| `chore` | 빌드, 패키지 매니저 설정 등 |

### 작성 규칙
- **subject**: 50자 이내, 명령형으로 작성
- **body**: 변경 이유와 내용을 bullet point로 설명
- 커밋 단위: README.md에 정리한 **기능 목록 단위**로 커밋

### 예시
```
feat: 도시 좌표 매핑 City enum 구현

- 5개 도시(Seoul, Tokyo, NewYork, Paris, London) 지원
- 도시 이름으로 City 조회 기능 추가
```

---

## 작업 순서

1. `README.md`에 구현할 **기능 목록** 먼저 작성
2. 기능 목록 단위로 구현 및 커밋
3. 테스트 작성으로 기능 검증
4. 코드 작성 후 프로그래밍 요구사항 준수 여부 검증
177 changes: 176 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,176 @@
# spring-sunshine-precourse
# spring-sunshine-precourse

## 기능 목록

### 1. 도시 좌표 매핑
- 도시 이름을 위도/경도 좌표로 매핑하는 기능
- 지원 도시: Seoul, Tokyo, NewYork, Paris, London (최소 5개)

### 2. Open-Meteo API 연동
- Open-Meteo API를 호출하여 날씨 정보 조회
- 조회 항목: 현재 온도, 체감 온도, 하늘 상태(weather code), 습도

### 3. 하늘 상태 변환
- WMO weather code를 한국어 하늘 상태로 변환
- 예: 0 → 맑음, 3 → 흐림, 61 → 비

### 4. 날씨 요약 문장 생성
- 조회한 데이터를 기반으로 한 줄 요약 문장 생성
- 예: "현재 서울의 기온은 3.4°C이며, 체감 온도는 1.2°C입니다. 날씨는 흐림입니다."

### 5. REST API 엔드포인트
- GET /api/weather?city={도시명} 형태로 날씨 정보 조회 API 제공

---

## 구현 전략

### 1차 구현
1. `City` enum으로 도시 이름과 좌표를 매핑
2. `WeatherCodeTranslator`로 WMO 코드를 한국어로 변환
3. `WeatherService`에서 Open-Meteo API 호출 및 응답 파싱
4. `WeatherSummaryGenerator`로 요약 문장 생성
5. `WeatherController`에서 REST API 제공

### 2차 구현
1. 도시 좌표 매핑을 DB 테이블로 관리 (추가/수정/삭제 용이)
2. WMO weather code 매핑을 DB 테이블로 관리
3. Docker Compose로 MySQL 환경 구성
4. Flyway로 DB 마이그레이션 관리

---

## AI 활용 내역

Claude Code를 활용하여 대화형으로 프로젝트를 진행했습니다.

---

### 1차 작업

#### 진행 흐름

1. **과제 분석 및 계획 수립**
- 과제 PDF 문서를 읽고 요구사항 파악
- README.md에 기능 목록 및 구현 전략 작성

2. **외부 API 조사**
- Open-Meteo API 문서를 웹에서 조회하여 필요한 파라미터 파악
- WMO weather code 매핑 정보 조사

3. **기능 단위 구현**
- 기능 목록 순서대로 구현 및 커밋
- AngularJS 커밋 컨벤션에 맞춰 커밋 메시지 작성

4. **요구사항 검증 및 리팩토링**
- 프로그래밍 요구사항 준수 여부 검증 요청
- 위반 사항 발견 시 즉시 수정

5. **테스트 보완 및 구조 개선**
- 부족한 테스트 케이스 추가
- 코드 구조 개선 (예외 처리 분리 등)

#### AI 활용으로 수정한 내용

**프로그래밍 요구사항 준수**
- `WeatherService.buildWeatherResponse` 메서드가 15줄을 초과하여 `createSummary`, `createWeatherResponse`로 분리

**브라우저 호환성 이슈 해결**
- Safari에서 JSON 응답의 한글이 깨지는 문제 발생
- 원인: Spring Boot 3.x는 RFC 7159에 따라 JSON 응답에 charset을 명시하지 않음
- 해결: `application.yml`에 `server.servlet.encoding.force-response: true` 설정 추가

**테스트 코드 보완**
- 도메인 클래스 단위 테스트만 있던 상태에서 `WeatherControllerTest` 추가
- MockMvc를 활용한 컨트롤러 레이어 테스트

**예외 처리 구조 개선**
- `WeatherController`에 있던 `@ExceptionHandler`를 `GlobalExceptionHandler`로 분리
- `ErrorResponse` DTO로 공통 에러 응답 규격 정의

**프로젝트 지침 문서화**
- `CLAUDE.md` 파일 생성하여 프로그래밍 요구사항 및 커밋 컨벤션 정리
- 다음 세션에서도 동일한 규칙 적용 가능하도록 문서화

---

### 2차 작업

#### 진행 흐름

1. **DB 환경 구성**
- Docker Compose로 MySQL 컨테이너 설정
- Flyway 마이그레이션 스크립트 작성

2. **엔티티 및 Repository 구현**
- City 엔티티와 CityRepository 구현
- WeatherCode 엔티티와 WeatherCodeRepository 구현

3. **기존 코드 리팩토링**
- WeatherCodeTranslator가 WeatherCodeRepository를 사용하도록 변경
- 기존 City enum 삭제

4. **레이어 구조 개선**
- Controller에서 Repository 직접 의존 제거
- CityReadService 추가 (readOnly 트랜잭션)
- WeatherCodeTranslator에 readOnly 트랜잭션 적용

5. **테스트 환경 구성**
- 테스트용 H2 인메모리 DB 설정
- data.sql로 테스트 데이터 초기화
- 테스트 코드를 DB 기반으로 수정

#### AI 활용으로 수정한 내용

**Docker 환경 구성**
- `environment/docker-compose.yml`로 MySQL 8.0 컨테이너 설정
- username/password를 `weather`로 설정

**DB 스키마 설계**
- `city` 테이블: id, name, korean_name, latitude, longitude
- `weather_code` 테이블: id, code, description
- Flyway 마이그레이션으로 테이블 생성 및 초기 데이터 삽입

**테스트 환경 분리**
- 운영: MySQL, 테스트: H2 인메모리 DB
- `spring.jpa.defer-datasource-initialization`으로 data.sql 실행 순서 조정

**레이어 구조 개선**
- Controller → Service → Repository 의존 관계 정립
- CityReadService로 도시 조회 로직 분리
- `@Transactional(readOnly = true)`로 읽기 전용 트랜잭션 적용

---

## 실행 방법

### 1. MySQL 컨테이너 실행
```bash
cd environment
docker-compose up -d
```

### 2. 애플리케이션 실행
```bash
./gradlew bootRun
```

### 3. API 호출
```bash
curl "http://localhost:8080/api/weather?city=Seoul"
```

### 4. 테스트 실행
```bash
./gradlew test
```

---

## 과제 진행 소감

Claude Code를 활용하여 프로젝트를 진행하면서 많은 시간을 단축할 수 있었습니다. 대화형으로 요구사항을 전달하고, 코드 검증과 리팩토링을 반복하는 과정이 효율적이었습니다.

추후 이 프로젝트가 LLM과 연동하여 어떻게 발전할 수 있을지 고민해보았습니다. 단순히 날씨를 질의하는 챗봇부터, 여러 지역의 날씨를 참고하여 오늘 입고 나갈 옷을 제안해주는 에이전트까지 다양한 활용이 가능해 보였습니다.

항상 OpenWeatherMap만 사용해보았는데, Open-Meteo API를 통해 API 키 없이도 특정 지역의 날씨를 간단하게 조회할 수 있다는 것을 알게 되었습니다.
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 All @@ -24,6 +24,8 @@ dependencies {
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(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")
implementation("org.flywaydb:flyway-core")
implementation("org.flywaydb:flyway-mysql")
Expand All @@ -41,6 +43,10 @@ kotlin {
}
}

tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}

tasks.withType<Test> {
useJUnitPlatform()
}
17 changes: 17 additions & 0 deletions environment/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
services:
mysql:
image: mysql:8.0
container_name: weather-mysql
environment:
MYSQL_ROOT_PASSWORD: weather
MYSQL_DATABASE: weather
MYSQL_USER: weather
MYSQL_PASSWORD: weather
ports:
- "3306:3306"
volumes:
- weather-mysql-data:/var/lib/mysql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

volumes:
weather-mysql-data:
19 changes: 19 additions & 0 deletions src/main/java/sunshine/config/StudyAiFunctionConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package sunshine.config;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.annotation.Configuration;
import sunshine.study.AddDayRequest;
import sunshine.study.DateResponse;

import java.time.LocalDate;

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


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

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.annotation.Configuration;
import sunshine.data.WeatherData;
import sunshine.service.WeatherProviderService;

@Configuration
public class WeatherToolConfiguration {
private final WeatherProviderService weatherProviderService;

public WeatherToolConfiguration(WeatherProviderService weatherProviderService) {
this.weatherProviderService = weatherProviderService;
}

@Tool(description = "open-meteo 날씨를 위도, 경도를 통해 조회하는 기능")
public WeatherData searchWeather(double latitude, double longitude) {
System.out.println("::::: searchWeather: latitude: " + latitude + ", longitude: " + longitude);
return weatherProviderService.getCurrent(latitude, longitude);
}
}
Loading