Skip to content

Commit

Permalink
학교 조회 API
Browse files Browse the repository at this point in the history
  • Loading branch information
hhhello0507 committed Jan 8, 2025
1 parent b738678 commit d8e0a99
Show file tree
Hide file tree
Showing 15 changed files with 351 additions and 2 deletions.
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
spring.datasource.url: ${{ secrets.DB_URL }}
spring.datasource.username: ${{ secrets.DB_USERNAME }}
spring.datasource.password: ${{ secrets.DB_PASSWORD }}
neis.api-key: ${{ secrets.NEIS_API_KEY }}
spring.profiles.active: 'prd'
- name: Build with Gradle
run: |
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/MoyamoyaApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
import org.springframework.core.env.Environment
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.stereotype.Component

@SpringBootApplication
@EnableScheduling
@ConfigurationPropertiesScan
class MoyamoyaApplication

Expand Down
15 changes: 15 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/api/SchoolApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ohayo.moyamoya.api

import com.ohayo.moyamoya.core.SchoolEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("schools")
class SchoolApi(
private val schoolService: SchoolService
) {
@GetMapping
fun getSchools(): List<SchoolEntity> = schoolService.getSchools()
}
12 changes: 12 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/api/SchoolService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ohayo.moyamoya.api

import com.ohayo.moyamoya.core.SchoolEntity
import com.ohayo.moyamoya.core.SchoolRepository
import org.springframework.stereotype.Service

@Service
class SchoolService(
private val schoolRepository: SchoolRepository
) {
fun getSchools(): List<SchoolEntity> = schoolRepository.findAll()
}
10 changes: 10 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/api/UserApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ohayo.moyamoya.api

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

@RestController
@RequestMapping("user")
class UserApi {

}
35 changes: 35 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/common/TimeExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.ohayo.moyamoya.common

import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException

fun LocalDateTime.parse(format: String): String = this.format(DateTimeFormatter.ofPattern(format))
fun LocalDate.parse(format: String): String = this.format(DateTimeFormatter.ofPattern(format))
fun LocalTime.parse(format: String): String = this.format(DateTimeFormatter.ofPattern(format))

fun String.toLocalDate(pattern: String) =
try {
val formatter = DateTimeFormatter.ofPattern(pattern)
LocalDate.parse(this, formatter)
} catch (e: DateTimeParseException) {
null
}

fun String.toLocalTime(pattern: String) =
try {
val formatter = DateTimeFormatter.ofPattern(pattern)
LocalTime.parse(this, formatter)
} catch (e: DateTimeParseException) {
null
}

fun String.toLocalDateTime(pattern: String) =
try {
val formatter = DateTimeFormatter.ofPattern(pattern)
LocalDateTime.parse(this, formatter)
} catch (e: DateTimeParseException) {
null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.ohayo.moyamoya.core

import com.ohayo.moyamoya.global.CustomException
import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpStatus

fun <T> CrudRepository<T, Int>.findByIdSafety(id: Int) =
findByIdOrNull(id) ?: throw CustomException(HttpStatus.NOT_FOUND, "Not found entity")
42 changes: 42 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/core/SchoolEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.ohayo.moyamoya.core

import jakarta.persistence.*
import java.time.LocalDate

@Entity
@Table(name = "tbl_school")
class SchoolEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int = 0,

@Column(nullable = false)
val name: String,

@Enumerated(value = EnumType.STRING)
val type: SchoolType?,

@Column(nullable = false)
val cityName: String,

val postalCode: String?,
val address: String?,
val addressDetail: String?,

@Column(nullable = false)
val phone: String,

val website: String?,

@Column(nullable = false)
val createdAt: LocalDate,

@Column(nullable = false)
val anniversary: LocalDate,

@Column(nullable = false)
val schoolCode: String,

@Column(nullable = false)
val officeCode: String,
)
7 changes: 7 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/core/SchoolRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ohayo.moyamoya.core

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface SchoolRepository: JpaRepository<SchoolEntity, Int>
19 changes: 19 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/core/SchoolType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ohayo.moyamoya.core

enum class SchoolType(
val limit: Int
) {
HIGH(limit = 3),
MIDDLE(limit = 3),
ELEMENTARY(limit = 6);

companion object {
fun ofKorean(string: String) = entries.firstOrNull { it.korean() == string }
}

fun korean() = when (this) {
HIGH -> "고등학교"
MIDDLE -> "중학교"
ELEMENTARY -> "초등학교"
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/core/UserEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.ohayo.moyamoya.core

import jakarta.persistence.*

@Entity
@Table(name = "tbl_user")
class UserEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int,
phone: String,
schoolName: String,
schoolGrade: Int,
name: String,
password: String,
profileImageUrl: String,
)
6 changes: 6 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/global/GlobalConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ class GlobalConfig(
fun discordRestClient() = RestClient.builder()
.baseUrl(discordProperties.webhookUrl)
.build()

@Bean
@Qualifier("neis")
fun neisRestClient() = RestClient.builder()
.baseUrl("https://open.neis.go.kr")
.build()

@Bean
fun logger() = KotlinLogging.logger { }
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/global/Properties.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ import org.springframework.boot.context.properties.bind.ConstructorBinding
@ConfigurationProperties("discord")
class DiscordProperties @ConstructorBinding constructor(
val webhookUrl: String
)

@ConfigurationProperties("neis")
class NeisProperties @ConstructorBinding constructor(
val apiKey: String
)
170 changes: 170 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/infra/Neis.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package com.ohayo.moyamoya.infra

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.ohayo.moyamoya.core.SchoolEntity
import com.ohayo.moyamoya.core.SchoolRepository
import com.ohayo.moyamoya.core.SchoolType
import com.ohayo.moyamoya.global.CustomException
import com.ohayo.moyamoya.global.NeisProperties
import mu.KLogger
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.CommandLineRunner
import org.springframework.http.HttpStatus
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.client.RestClient
import org.springframework.web.client.body
import java.time.LocalDate
import java.time.format.DateTimeFormatter

@Component
class NeisSchoolClient(
private val logger: KLogger,
@Qualifier("neis")
private val restClient: RestClient,
private val neisProperties: NeisProperties,
private val schoolRepository: SchoolRepository
) : CommandLineRunner {
@Transactional
override fun run(vararg args: String?) {
run()
}

@Transactional
@Scheduled(cron = "0 0 0 1 * *")
fun run() {
schoolRepository.deleteAll()
schoolRepository.saveAll(getSchools())

logger.info("Loading schools success")
}

private fun getSchools(): List<SchoolEntity> = Array(15) { i -> i + 1 }
.mapNotNull { index ->
restClient.get()
.uri { uriBuilder ->
uriBuilder
.path("hub/schoolInfo")
.queryParam("KEY", neisProperties.apiKey)
.queryParam("Type", "json")
.queryParam("pIndex", index)
.queryParam("pSize", 1000) // Maximum value is 1000.
.build()
}
.retrieve()
.body<String>()
.let { it ?: throw CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "Neis Error") }
.let { jacksonObjectMapper().readValue<NeisSchoolRes>(it) }
.schoolInfo
?.mapNotNull { it.row }
?.flatten()
}
.flatten()
.map {
SchoolEntity(
officeCode = it.atptOfcdcScCode,
schoolCode = it.sdSchulCode,
name = it.schulNm,
type = it.schulKndScNm?.let(SchoolType::ofKorean),
cityName = it.lctnScNm,
postalCode = it.orgRdnzc,
address = it.orgRdnma,
addressDetail = it.orgRdnda,
phone = it.orgTelno,
website = it.hmpgAdres,
createdAt = LocalDate.parse(it.fondYmd, DateTimeFormatter.ofPattern("yyyyMMdd")),
anniversary = LocalDate.parse(it.foasMemrd, DateTimeFormatter.ofPattern("yyyyMMdd")),
)
}
}

@JsonIgnoreProperties(ignoreUnknown = true)
data class NeisSchoolRes(
val schoolInfo: List<SchoolInfo>?
) {
@JsonIgnoreProperties(ignoreUnknown = true)
data class SchoolInfo(
val row: List<Row>?
) {
data class Row(
/** 시도교육청코드 */
@JsonProperty("ATPT_OFCDC_SC_CODE") val atptOfcdcScCode: String,

/** 시도교육청명 */
@JsonProperty("ATPT_OFCDC_SC_NM") val atptOfcdcScNm: String,

/** 행정표준코드 */
@JsonProperty("SD_SCHUL_CODE") val sdSchulCode: String,

/** 학교명 */
@JsonProperty("SCHUL_NM") val schulNm: String,

/** 영문학교명 */
@JsonProperty("ENG_SCHUL_NM") val engSchulNm: String?,

/** 학교종류명 */
@JsonProperty("SCHUL_KND_SC_NM") val schulKndScNm: String?,

/** 시도명 */
@JsonProperty("LCTN_SC_NM") val lctnScNm: String,

/** 관할조직명 */
@JsonProperty("JU_ORG_NM") val juOrgNm: String,

/** 설립명 */
@JsonProperty("FOND_SC_NM") val fondScNm: String?,

/** 도로명우편번호 */
@JsonProperty("ORG_RDNZC") val orgRdnzc: String?,

/** 도로명주소 */
@JsonProperty("ORG_RDNMA") val orgRdnma: String?,

/** 도로명상세주소 */
@JsonProperty("ORG_RDNDA") val orgRdnda: String?,

/** 전화번호 */
@JsonProperty("ORG_TELNO") val orgTelno: String,

/** 홈페이지주소 */
@JsonProperty("HMPG_ADRES") val hmpgAdres: String?,

/** 남녀공학구분명 */
@JsonProperty("COEDU_SC_NM") val coeduScNm: String,

/** 팩스번호 */
@JsonProperty("ORG_FAXNO") val orgFaxno: String?,

/** 고등학교구분명 */
@JsonProperty("HS_SC_NM") val hsScNm: String?,

/** 산업체특별학급존재여부 */
@JsonProperty("INDST_SPECL_CCCCL_EXST_YN") val indstSpeclCcclExstYn: String,

/** 고등학교일반전문구분명 */
@JsonProperty("HS_GNRL_BUSNS_SC_NM") val hsGnrlBusnsScNm: String?,

/** 특수목적고등학교계열명 */
@JsonProperty("SPCLY_PURPS_HS_ORD_NM") val spclyPurpsHsOrdNm: String?,

/** 입시전후기구분명 */
@JsonProperty("ENE_BFE_SEHF_SC_NM") val eneBfeSehfScNm: String,

/** 주야구분명 */
@JsonProperty("DGHT_SC_NM") val dghtScNm: String,

/** 설립일자 */
@JsonProperty("FOND_YMD") val fondYmd: String,

/** 개교기념일 */
@JsonProperty("FOAS_MEMRD") val foasMemrd: String,

/** 수정일자 */
@JsonProperty("LOAD_DTM") val loadDtm: String
)
}
}
Loading

0 comments on commit d8e0a99

Please sign in to comment.