Skip to content

Commit

Permalink
Merge pull request #32 from dnd-side-project/feat/weather
Browse files Browse the repository at this point in the history
[BLOOM-030] 날씨에 맞는 식물 관리법 조회 API
  • Loading branch information
stophwan authored Aug 11, 2024
2 parents 356905a + 0c00fea commit 424182d
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dnd11th.blooming.api.controller.weather

import dnd11th.blooming.api.dto.weather.WeatherMessageResponse
import dnd11th.blooming.api.service.weather.WeatherMessage
import dnd11th.blooming.api.service.weather.WeatherService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.time.LocalDateTime

@RestController
@RequestMapping("/api/v1/weather-message")
class WeatherMessageController(
private val weatherService: WeatherService,
) {
@GetMapping
fun getWeatherMessage(): List<WeatherMessageResponse> {
val weatherMessages: List<WeatherMessage> =
weatherService.createWeatherMessage(LocalDateTime.now())
return weatherMessages.map { weatherMessage -> WeatherMessageResponse.from(weatherMessage) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dnd11th.blooming.api.dto.weather

import dnd11th.blooming.api.service.weather.WeatherMessage

data class WeatherMessageResponse(
val status: String,
val title: String,
val message: List<String>,
) {
companion object {
fun from(weatherMessages: WeatherMessage): WeatherMessageResponse {
return WeatherMessageResponse(
status = weatherMessages.name,
title = weatherMessages.title,
message = weatherMessages.message,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package dnd11th.blooming.api.service.user.oauth

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dnd11th.blooming.client.KakaoOauthClient
import dnd11th.blooming.client.dto.OidcPublicKeys
import dnd11th.blooming.client.kakao.KakaoOauthClient
import dnd11th.blooming.common.exception.ErrorType
import dnd11th.blooming.common.exception.UnAuthorizedException
import dnd11th.blooming.domain.entity.user.OidcUser
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dnd11th.blooming.api.service.weather

enum class WeatherMessage(val title: String, val message: List<String>) {
HUMIDITY("과습주의보", listOf("과습이에요", "과습입니다.")),
DRY("건조주의보", listOf("건조에요", "건조입니다")),
COLD("한파주의보", listOf("한파입니다", "한파에요")),
HOT("더위주의보", listOf("덥습니다", "더워요")),
}
104 changes: 104 additions & 0 deletions src/main/kotlin/dnd11th/blooming/api/service/weather/WeatherService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package dnd11th.blooming.api.service.weather

import dnd11th.blooming.client.dto.WeatherItem
import dnd11th.blooming.client.weather.WeatherInfoClient
import dnd11th.blooming.common.exception.ErrorType
import dnd11th.blooming.common.exception.NotFoundException
import dnd11th.blooming.domain.entity.user.User
import dnd11th.blooming.domain.repository.user.UserRepository
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter

@Service
@Transactional(readOnly = true)
class WeatherService(
@Value("\${weather.serviceKey}")
private val serviceKey: String,
private val weatherInfoClient: WeatherInfoClient,
private val userRepository: UserRepository,
) {
companion object {
private const val HUMIDITY_KEY = "REH"
private const val TEMPERATURE_KEY = "TMP"
private const val MAX_HUMIDITY = 100
private const val MIN_HUMIDITY = 0
private const val MAX_TEMPERATURE = 40
private const val MIN_TEMPERATURE = -20
private const val HIGH_HUMIDITY_THRESHOLD = 80
private const val LOW_HUMIDITY_THRESHOLD = 20
private const val LOW_TEMPERATURE_THRESHOLD = 5
private const val HIGH_TEMPERATURE_THRESHOLD = 30
}

fun createWeatherMessage(now: LocalDateTime): List<WeatherMessage> {
val user: User = userRepository.findById(1L).orElseThrow { throw NotFoundException(ErrorType.USER_NOT_FOUND) }

val weatherItems: List<WeatherItem> =
weatherInfoClient.getWeatherInfo(
serviceKey = serviceKey,
base_date = getBaseDate(now),
nx = user.nx,
ny = user.ny,
).toWeatherItems()

return determineWeatherMessages(weatherItems)
}

/**
* 과습 -> 그날 습도가 80 이상 넘어가는 날 있으면 HUMIDITY
* 건조 -> 그날 습도가 20 이하 내려가는 시간 있으면 DRY
* 한파 -> 최저 온도가 5도 이하로 내려가면 COLD
* 더위 -> 최고 온도가 30도 이상 올라가는 날 있으면 HOT
*/
private fun determineWeatherMessages(items: List<WeatherItem>): List<WeatherMessage> {
var maxHumidity = MIN_HUMIDITY
var minHumidity = MAX_HUMIDITY
var maxTemperature = MIN_TEMPERATURE
var minTemperature = MAX_TEMPERATURE

items.forEach { item ->
when (item.category) {
HUMIDITY_KEY -> {
val humidity = item.fcstValue.toInt()
maxHumidity = maxOf(maxHumidity, humidity)
minHumidity = minOf(minHumidity, humidity)
}
TEMPERATURE_KEY -> {
val temperature = item.fcstValue.toInt()
maxTemperature = maxOf(maxTemperature, temperature)
minTemperature = minOf(minTemperature, temperature)
}
}
}

return mutableListOf<WeatherMessage>().apply {
if (maxHumidity >= HIGH_HUMIDITY_THRESHOLD) add(WeatherMessage.HUMIDITY)
if (minHumidity <= LOW_HUMIDITY_THRESHOLD) add(WeatherMessage.DRY)
if (minTemperature <= LOW_TEMPERATURE_THRESHOLD) add(WeatherMessage.COLD)
if (maxTemperature >= HIGH_TEMPERATURE_THRESHOLD) add(WeatherMessage.HOT)
}
}

/**
* 현재 시간이 2시 10분을 지났다면, 현재 날짜를 반환합니다.
* 그렇지 않다면, 어제 날짜를 반환합니다.
*/
private fun getBaseDate(now: LocalDateTime): String {
val baseTime = LocalTime.of(2, 20)
return if (now.toLocalTime().isAfter(baseTime)) {
formatDate(now.toLocalDate())
} else {
formatDate(now.toLocalDate().minusDays(1))
}
}

private fun formatDate(date: LocalDate): String {
val formatter = DateTimeFormatter.ofPattern("yyyyMMdd")
return date.format(formatter)
}
}
48 changes: 48 additions & 0 deletions src/main/kotlin/dnd11th/blooming/client/dto/WeatherResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dnd11th.blooming.client.dto

import dnd11th.blooming.common.exception.ClientCallException
import dnd11th.blooming.common.exception.ErrorType

data class WeatherResponse(
val response: ResponseData,
) {
fun toWeatherItems(): List<WeatherItem> {
validate()
return response.body?.items?.item ?: emptyList()
}

private fun validate() {
if (response.header.resultCode != "00") {
throw ClientCallException(ErrorType.OPEN_API_CALL_EXCEPTION)
}
}
}

data class ResponseData(
val header: HeaderData,
val body: BodyData?,
)

data class HeaderData(
val resultCode: String,
val resultMsg: String,
)

data class BodyData(
val items: Items,
)

data class Items(
val item: List<WeatherItem>,
)

data class WeatherItem(
val baseDate: String,
val baseTime: String,
val category: String,
val fcstDate: String,
val fcstTime: String,
val fcstValue: String,
val nx: Int,
val ny: Int,
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package dnd11th.blooming.client
package dnd11th.blooming.client.kakao

import dnd11th.blooming.client.dto.OidcPublicKeys
import org.springframework.cloud.openfeign.FeignClient
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dnd11th.blooming.client.weather

import dnd11th.blooming.client.dto.WeatherResponse
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam

@FeignClient(
name = "WeatherInfoClient",
url = "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst",
)
interface WeatherInfoClient {
companion object {
const val FIXED_PAGE_NUMBER = 1
const val FIXED_NUMBER_OF_ROWS = 288
const val FIXED_DATA_TYPE = "JSON"
const val FIXED_BASE_TIME = "0200"
}

@GetMapping
fun getWeatherInfo(
@RequestParam serviceKey: String,
@RequestParam pageNo: Int = FIXED_PAGE_NUMBER,
@RequestParam numOfRows: Int = FIXED_NUMBER_OF_ROWS,
@RequestParam dataType: String = FIXED_DATA_TYPE,
@RequestParam base_date: String,
@RequestParam base_time: String = FIXED_BASE_TIME,
@RequestParam nx: Int,
@RequestParam ny: Int,
): WeatherResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package dnd11th.blooming.common.exception

class ClientCallException(errorType: ErrorType) : MyException(errorType)
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ enum class ErrorType(val status: HttpStatus, val message: String, val logLevel:
NOT_FOUND_MYPLANT(HttpStatus.NOT_FOUND, "존재하지 않는 내 식물입니다.", LogLevel.DEBUG),
NOT_FOUND_LOCATION(HttpStatus.NOT_FOUND, "존재하지 않는 위치입니다.", LogLevel.DEBUG),

// User
// Auth
INVALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다", LogLevel.DEBUG),
INVALID_ID_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 ID TOKEN입니다.", LogLevel.DEBUG),
INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "지원하지 않는 provider입니다", LogLevel.DEBUG),

// User
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않은 사용자입니다.", LogLevel.DEBUG),

// OpenAPI
OPEN_API_CALL_EXCEPTION(HttpStatus.BAD_REQUEST, "OpenAPI 호출에 실패했습니다", LogLevel.WARN),
}
4 changes: 4 additions & 0 deletions src/main/kotlin/dnd11th/blooming/domain/entity/user/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class User(

val nickname: String = nickname

val nx: Int = 0

val ny: Int = 0

companion object {
fun create(claims: UserClaims): User {
return User(claims.email, claims.nickname)
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ auth:
iss: https://kauth.kakao.com
aud: ${KAKAO_CLIENT_ID}
nonce: coffeecoffeecoffeecoffeecoffeecoffeecoffeevcoffeecoffeecoffeecoffeecoffeecoffee

weather:
serviceKey: ${OPEN_API_SERVICE_KEY}
3 changes: 3 additions & 0 deletions src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ spring:
properties:
hibernate.format_sql: true
dialect: org.hibernate.dialect.H2Dialect

weather:
serviceKey: "serviceKey"

0 comments on commit 424182d

Please sign in to comment.