Skip to content

Commit

Permalink
Merge pull request #9 from Nexters/feature/2-weather-api
Browse files Browse the repository at this point in the history
[#2] 날씨 정보 조회 API
  • Loading branch information
jun108059 authored Oct 3, 2024
2 parents 9647c17 + 4ab64aa commit e154f07
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ data class SkiResortResponseDto(
currentWeather = currentWeather?.let {
SimpleCurrentWeatherDto(
temperature = it.temperature,
description = it.condition.name
description = it.condition
)
} ?: SimpleCurrentWeatherDto(0, "정보 없음"),
weeklyWeather = weeklyWeather.map {
WeeklyWeatherDto(
day = it.dayOfWeek,
maxTemperature = it.maxTemp,
minTemperature = it.minTemp,
description = it.dayCondition.name
description = it.condition
)
}
)
Expand Down
4 changes: 1 addition & 3 deletions src/main/kotlin/nexters/weski/weather/CurrentWeather.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ data class CurrentWeather(
val minTemp: Int,
val feelsLike: Int,
val description: String,

@Enumerated(EnumType.STRING)
val condition: WeatherCondition,
val condition: String,

@OneToOne
@MapsId
Expand Down
7 changes: 1 addition & 6 deletions src/main/kotlin/nexters/weski/weather/DailyWeather.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@ data class DailyWeather(
val precipitationChance: Int,
val maxTemp: Int,
val minTemp: Int,

@Enumerated(EnumType.STRING)
val dayCondition: WeatherCondition,

@Enumerated(EnumType.STRING)
val nightCondition: WeatherCondition,
val condition: String,

@ManyToOne
@JoinColumn(name = "resort_id")
Expand Down
23 changes: 23 additions & 0 deletions src/main/kotlin/nexters/weski/weather/HourlyWeather.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nexters.weski.weather

import jakarta.persistence.*
import nexters.weski.common.BaseEntity
import nexters.weski.ski_resort.SkiResort
import java.time.LocalDateTime

@Entity
@Table(name = "hourly_weather")
data class HourlyWeather(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,

val forecastTime: LocalDateTime,
val temperature: Int,
val precipitationChance: Int,
val condition: String,

@ManyToOne
@JoinColumn(name = "resort_id")
val skiResort: SkiResort
) : BaseEntity()
12 changes: 12 additions & 0 deletions src/main/kotlin/nexters/weski/weather/HourlyWeatherRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package nexters.weski.weather

import org.springframework.data.jpa.repository.JpaRepository
import java.time.LocalDateTime

interface HourlyWeatherRepository : JpaRepository<HourlyWeather, Long> {
fun findAllBySkiResortResortIdAndForecastTimeBetween(
resortId: Long,
startTime: LocalDateTime,
endTime: LocalDateTime
): List<HourlyWeather>
}
17 changes: 17 additions & 0 deletions src/main/kotlin/nexters/weski/weather/WeatherController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package nexters.weski.weather

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/weather")
class WeatherController(
private val weatherService: WeatherService
) {
@GetMapping("/{resortId}")
fun getWeatherByResortId(@PathVariable resortId: Long): WeatherDto? {
return weatherService.getWeatherByResortId(resortId)
}
}
67 changes: 45 additions & 22 deletions src/main/kotlin/nexters/weski/weather/WeatherDto.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package nexters.weski.weather

import java.time.LocalDateTime

data class WeatherDto(
val resortId: Long,
val currentWeather: CurrentWeatherDto,
Expand All @@ -24,8 +26,8 @@ data class WeatherDto(

data class CurrentWeatherDto(
val temperature: Int,
val maxTemp: Int,
val minTemp: Int,
val maxTemperature: Int,
val minTemperature: Int,
val feelsLike: Int,
val description: String,
val condition: String
Expand All @@ -34,11 +36,11 @@ data class CurrentWeatherDto(
fun fromEntity(entity: CurrentWeather): CurrentWeatherDto {
return CurrentWeatherDto(
temperature = entity.temperature,
maxTemp = entity.maxTemp,
minTemp = entity.minTemp,
maxTemperature = entity.maxTemp,
minTemperature = entity.minTemp,
feelsLike = entity.feelsLike,
description = entity.description,
condition = entity.condition.name
condition = entity.condition
)
}
}
Expand All @@ -47,16 +49,16 @@ data class CurrentWeatherDto(
data class HourlyWeatherDto(
val time: String,
val temperature: Int,
val precipitationChance: Int,
val precipitationChance: String,
val condition: String
) {
companion object {
fun fromEntity(entity: HourlyWeather): HourlyWeatherDto {
return HourlyWeatherDto(
time = entity.forecastTime.toLocalTime().toString(),
time = entity.forecastTime.toLocalTimeString(),
temperature = entity.temperature,
precipitationChance = entity.precipitationChance,
condition = entity.condition.name
precipitationChance = entity.precipitationChance.toPercentString(),
condition = entity.condition
)
}
}
Expand All @@ -65,22 +67,20 @@ data class HourlyWeatherDto(
data class DailyWeatherDto(
val day: String,
val date: String,
val precipitationChance: Int,
val maxTemp: Int,
val minTemp: Int,
val dayCondition: String,
val nightCondition: String
val precipitationChance: String,
val maxTemperature: Int,
val minTemperature: Int,
val condition: String,
) {
companion object {
fun fromEntity(entity: DailyWeather): DailyWeatherDto {
return DailyWeatherDto(
day = entity.dayOfWeek,
date = entity.forecastDate.toString(),
precipitationChance = entity.precipitationChance,
maxTemp = entity.maxTemp,
minTemp = entity.minTemp,
dayCondition = entity.dayCondition.name,
nightCondition = entity.nightCondition.name
date = entity.forecastDate.toString().toShortDate(),
precipitationChance = entity.precipitationChance.toPercentString(),
maxTemperature = entity.maxTemp,
minTemperature = entity.minTemp,
condition = entity.condition,
)
}
}
Expand Down Expand Up @@ -109,11 +109,34 @@ data class WeeklyWeatherDto(
companion object {
fun fromEntity(entity: DailyWeather): WeeklyWeatherDto {
return WeeklyWeatherDto(
day = TODO(),
day = entity.dayOfWeek,
maxTemperature = entity.maxTemp,
minTemperature = entity.minTemp,
description = entity.dayCondition.name
description = entity.condition
)
}
}
}

// 2024-08-01 형태에서 8.1 형태로 변경하는 메서드 호출
fun String.toShortDate(): String {
val date = this.split("-")
return "${date[1]}.${date[2]}"
}

// Int 데이터를 Int+% String으로 변환하는 메서드
fun Int.toPercentString(): String {
return "$this%"
}

// LocalDateTime 데이터를 오전/오후 n시로 변경하는 메서드
fun LocalDateTime.toLocalTimeString(): String {
val hour = this.hour
return if (hour < 12) {
"오전 ${hour}"
} else if (hour == 12) {
"오후 ${hour}"
} else {
"오후 ${hour - 12}"
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/nexters/weski/weather/WeatherService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package nexters.weski.weather

import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.time.LocalTime

@Service
class WeatherService(
private val currentWeatherRepository: CurrentWeatherRepository,
private val hourlyWeatherRepository: HourlyWeatherRepository,
private val dailyWeatherRepository: DailyWeatherRepository
) {
fun getWeatherByResortId(resortId: Long): WeatherDto? {
val currentWeather = currentWeatherRepository.findBySkiResortResortId(resortId) ?: return null
// 오늘, 내일 날짜의 날씨 정보만 가져옴
val startTime = LocalDateTime.now().with(LocalTime.MIN)
val endTime = startTime.plusDays(2).with(LocalTime.MAX)
val hourlyWeather = hourlyWeatherRepository.findAllBySkiResortResortIdAndForecastTimeBetween(
resortId, startTime, endTime
)
val dailyWeather = dailyWeatherRepository.findAllBySkiResortResortId(resortId)

return WeatherDto.fromEntities(currentWeather, hourlyWeather, dailyWeather)
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/nexters/weski/weather/WeatherUpdateService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package nexters.weski.weather


import nexters.weski.ski_resort.SkiResortRepository
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service

@Service
class WeatherUpdateService(
private val skiResortRepository: SkiResortRepository,
private val currentWeatherRepository: CurrentWeatherRepository,
) {
// @Scheduled(fixedRate = 3600000) // 1시간마다 실행
fun updateWeatherData() {
val skiResorts = skiResortRepository.findAll()
skiResorts.forEach { resort ->
println(resort)
// 외부 API 호출하여 날씨 정보 가져오기
// currentWeatherRepository.save(업데이트된 데이터)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
package nexters.weski.ski_resort

import com.ninjasquad.springmockk.MockkBean
import io.mockk.every
import nexters.weski.common.config.JpaConfig
import nexters.weski.weather.SimpleCurrentWeatherDto
import nexters.weski.weather.WeeklyWeatherDto
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.FilterType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*

import com.ninjasquad.springmockk.MockkBean
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@WebMvcTest(SkiResortController::class)
@ComponentScan(
excludeFilters = [ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = [JpaConfig::class]
)]
)
class SkiResortControllerTest @Autowired constructor(
private val mockMvc: MockMvc
) {
Expand All @@ -20,12 +31,23 @@ class SkiResortControllerTest @Autowired constructor(

@Test
fun `GET api_ski-resorts should return list of ski resorts`() {
val currentWeather = SimpleCurrentWeatherDto(-1, "맑음")
val weeklyWeather = listOf(
WeeklyWeatherDto("", 5, -3, "맑음"),
WeeklyWeatherDto("", 6, -2, "맑음"),
WeeklyWeatherDto("", 7, -1, "맑음"),
WeeklyWeatherDto("", 8, 0, "맑음"),
WeeklyWeatherDto("", 9, 1, "맑음"),
WeeklyWeatherDto("", 10, 2, "맑음"),
WeeklyWeatherDto("", 11, 3, "맑음")
)
// Given
val skiResorts = listOf(
SkiResortDto(1, "스키장 A", ResortStatus.운영중, "2023-12-01", "2024-03-01", 5, 10),
SkiResortDto(2, "스키장 B", ResortStatus.예정, "2023-12-15", null, 0, 8)
SkiResortResponseDto(1, "스키장 A", ResortStatus.운영중.name, 3, currentWeather, weeklyWeather),
SkiResortResponseDto(2, "스키장 B", ResortStatus.운영중.name, 4, currentWeather, weeklyWeather),
)
every { skiResortService.getAllSkiResorts() } returns skiResorts
every { skiResortService.getAllSkiResortsAndWeather() } returns skiResorts


// When & Then
mockMvc.perform(get("/api/ski-resorts"))
Expand Down
14 changes: 12 additions & 2 deletions src/test/kotlin/nexters/weski/ski_resort/SkiResortServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ package nexters.weski.ski_resort

import io.mockk.every
import io.mockk.mockk
import nexters.weski.weather.CurrentWeatherRepository
import nexters.weski.weather.DailyWeatherRepository
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test

class SkiResortServiceTest {

private val skiResortRepository: SkiResortRepository = mockk()
private val skiResortService = SkiResortService(skiResortRepository)
private val currentWeatherRepository: CurrentWeatherRepository = mockk()
private val dailyWeatherRepository: DailyWeatherRepository = mockk()
private val skiResortService = SkiResortService(
skiResortRepository,
currentWeatherRepository,
dailyWeatherRepository,
)

@Test
fun `getAllSkiResorts should return list of SkiResortDto`() {
Expand All @@ -18,9 +26,11 @@ class SkiResortServiceTest {
SkiResort(2, "스키장 B", ResortStatus.예정, null, null, 0, 8)
)
every { skiResortRepository.findAll() } returns skiResorts
every { currentWeatherRepository.findBySkiResortResortId(any()) } returns null
every { dailyWeatherRepository.findAllBySkiResortResortId(any()) } returns emptyList()

// When
val result = skiResortService.getAllSkiResorts()
val result = skiResortService.getAllSkiResortsAndWeather()

// Then
assertEquals(2, result.size)
Expand Down
Loading

0 comments on commit e154f07

Please sign in to comment.