diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index af3587d..cfa1d8b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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: | diff --git a/src/main/kotlin/com/ohayo/moyamoya/MoyamoyaApplication.kt b/src/main/kotlin/com/ohayo/moyamoya/MoyamoyaApplication.kt index 17c7941..ef06b82 100644 --- a/src/main/kotlin/com/ohayo/moyamoya/MoyamoyaApplication.kt +++ b/src/main/kotlin/com/ohayo/moyamoya/MoyamoyaApplication.kt @@ -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 diff --git a/src/main/kotlin/com/ohayo/moyamoya/api/SchoolApi.kt b/src/main/kotlin/com/ohayo/moyamoya/api/SchoolApi.kt new file mode 100644 index 0000000..6922501 --- /dev/null +++ b/src/main/kotlin/com/ohayo/moyamoya/api/SchoolApi.kt @@ -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 = schoolService.getSchools() +} \ No newline at end of file diff --git a/src/main/kotlin/com/ohayo/moyamoya/api/SchoolService.kt b/src/main/kotlin/com/ohayo/moyamoya/api/SchoolService.kt new file mode 100644 index 0000000..9f81820 --- /dev/null +++ b/src/main/kotlin/com/ohayo/moyamoya/api/SchoolService.kt @@ -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 = schoolRepository.findAll() +} \ No newline at end of file diff --git a/src/main/kotlin/com/ohayo/moyamoya/api/UserApi.kt b/src/main/kotlin/com/ohayo/moyamoya/api/UserApi.kt new file mode 100644 index 0000000..81d285b --- /dev/null +++ b/src/main/kotlin/com/ohayo/moyamoya/api/UserApi.kt @@ -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 { + +} \ No newline at end of file diff --git a/src/main/kotlin/com/ohayo/moyamoya/common/TimeExt.kt b/src/main/kotlin/com/ohayo/moyamoya/common/TimeExt.kt new file mode 100644 index 0000000..2f29d3d --- /dev/null +++ b/src/main/kotlin/com/ohayo/moyamoya/common/TimeExt.kt @@ -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 + } \ No newline at end of file diff --git a/src/main/kotlin/com/ohayo/moyamoya/core/CrudRepositoryExtension.kt b/src/main/kotlin/com/ohayo/moyamoya/core/CrudRepositoryExtension.kt new file mode 100644 index 0000000..91e2203 --- /dev/null +++ b/src/main/kotlin/com/ohayo/moyamoya/core/CrudRepositoryExtension.kt @@ -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 CrudRepository.findByIdSafety(id: Int) = + findByIdOrNull(id) ?: throw CustomException(HttpStatus.NOT_FOUND, "Not found entity") \ No newline at end of file diff --git a/src/main/kotlin/com/ohayo/moyamoya/core/SchoolEntity.kt b/src/main/kotlin/com/ohayo/moyamoya/core/SchoolEntity.kt new file mode 100644 index 0000000..87f5c72 --- /dev/null +++ b/src/main/kotlin/com/ohayo/moyamoya/core/SchoolEntity.kt @@ -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, +) \ No newline at end of file diff --git a/src/main/kotlin/com/ohayo/moyamoya/core/SchoolRepository.kt b/src/main/kotlin/com/ohayo/moyamoya/core/SchoolRepository.kt new file mode 100644 index 0000000..ab38201 --- /dev/null +++ b/src/main/kotlin/com/ohayo/moyamoya/core/SchoolRepository.kt @@ -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 \ No newline at end of file diff --git a/src/main/kotlin/com/ohayo/moyamoya/core/SchoolType.kt b/src/main/kotlin/com/ohayo/moyamoya/core/SchoolType.kt new file mode 100644 index 0000000..f748ae4 --- /dev/null +++ b/src/main/kotlin/com/ohayo/moyamoya/core/SchoolType.kt @@ -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 -> "초등학교" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ohayo/moyamoya/core/UserEntity.kt b/src/main/kotlin/com/ohayo/moyamoya/core/UserEntity.kt new file mode 100644 index 0000000..c31417c --- /dev/null +++ b/src/main/kotlin/com/ohayo/moyamoya/core/UserEntity.kt @@ -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, +) \ No newline at end of file diff --git a/src/main/kotlin/com/ohayo/moyamoya/global/GlobalConfig.kt b/src/main/kotlin/com/ohayo/moyamoya/global/GlobalConfig.kt index 899a4ea..fe95ee9 100644 --- a/src/main/kotlin/com/ohayo/moyamoya/global/GlobalConfig.kt +++ b/src/main/kotlin/com/ohayo/moyamoya/global/GlobalConfig.kt @@ -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 { } diff --git a/src/main/kotlin/com/ohayo/moyamoya/global/Properties.kt b/src/main/kotlin/com/ohayo/moyamoya/global/Properties.kt index 1dc7584..9f341ec 100644 --- a/src/main/kotlin/com/ohayo/moyamoya/global/Properties.kt +++ b/src/main/kotlin/com/ohayo/moyamoya/global/Properties.kt @@ -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 ) \ No newline at end of file diff --git a/src/main/kotlin/com/ohayo/moyamoya/infra/Neis.kt b/src/main/kotlin/com/ohayo/moyamoya/infra/Neis.kt new file mode 100644 index 0000000..27a7c67 --- /dev/null +++ b/src/main/kotlin/com/ohayo/moyamoya/infra/Neis.kt @@ -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 = 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() + .let { it ?: throw CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "Neis Error") } + .let { jacksonObjectMapper().readValue(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? +) { + @JsonIgnoreProperties(ignoreUnknown = true) + data class SchoolInfo( + val row: List? + ) { + 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 + ) + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 31d7798..18290ce 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,5 +19,5 @@ server: port: 1234 discord: webhook-url: ${DISCORD_WEBHOOK_URL} -#jwt: -# secret-key: ${JWT_SECRET} \ No newline at end of file +neis: + api-key: ${NEIS_API_KEY} \ No newline at end of file