diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/api/meal/MealService.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/api/meal/MealService.kt index aa4454b..dd45fb2 100644 --- a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/api/meal/MealService.kt +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/api/meal/MealService.kt @@ -1,14 +1,11 @@ package com.bestswlkh0310.graduating.graduatingserver.api.meal import com.bestswlkh0310.graduating.graduatingserver.api.meal.res.MealRes -import com.bestswlkh0310.graduating.graduatingserver.core.meal.MealEntity +import com.bestswlkh0310.graduating.graduatingserver.core.global.safeSaveAll import com.bestswlkh0310.graduating.graduatingserver.core.meal.MealRepository import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolRepository import com.bestswlkh0310.graduating.graduatingserver.core.school.getBy -import com.bestswlkh0310.graduating.graduatingserver.infra.neis.NeisMealHelper -import jakarta.persistence.PersistenceException -import kotlinx.coroutines.runBlocking -import mu.KLogger +import com.bestswlkh0310.graduating.graduatingserver.infra.neis.meal.NeisMealClient import org.springframework.stereotype.Service import java.time.LocalDate @@ -16,8 +13,7 @@ import java.time.LocalDate class MealService( private val mealRepository: MealRepository, private val schoolRepository: SchoolRepository, - private val neisMealService: NeisMealHelper, - private val logger: KLogger, + private val neisMealClient: NeisMealClient, ) { fun getMeals(schoolId: Long): List { @@ -25,33 +21,18 @@ class MealService( val currentTime = LocalDate.now() val schools = mealRepository.findBySchoolIdAndMealDate(schoolId, currentTime) - val included = schools.any { - it.mealDate == currentTime && it.school.id == schoolId - } - if (included) { + if (schools.isNotEmpty()) { return schools.map { MealRes.of(it) } } - // This code is really suck. - return runBlocking { - val result = arrayListOf() - neisMealService.getMeals( - school = school, - ).forEach { - try { - result.add( - mealRepository.save(it) - ) - } catch (e: PersistenceException) { - logger.error("duplicate mealEntity.") - } - } - return@runBlocking result - .filter { it.mealDate == currentTime } - .map { MealRes.of(it) } - } + + val meals = neisMealClient.getMeals(school = school) + mealRepository.safeSaveAll(meals) + + return mealRepository.findBySchoolIdAndMealDate(schoolId, currentTime) + .map { MealRes.of(it) } } fun deleteMealAll() { - mealRepository.deleteAll() + mealRepository.deleteOldRecords() } } \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/api/school/MealController.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/api/school/MealController.kt index e46c21f..6586b45 100644 --- a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/api/school/MealController.kt +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/api/school/MealController.kt @@ -10,5 +10,6 @@ class MealController( private val mealService: MealService ) { @GetMapping("/{schoolId}", "/{schoolId}/") - fun getMeals(@PathVariable("schoolId") schoolId: Long, ) = mealService.getMeals(schoolId) + fun getMeals(@PathVariable("schoolId") schoolId: Long) = + mealService.getMeals(schoolId) } \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/core/global/safeSaveAll.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/core/global/safeSaveAll.kt new file mode 100644 index 0000000..2c65f87 --- /dev/null +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/core/global/safeSaveAll.kt @@ -0,0 +1,12 @@ +package com.bestswlkh0310.graduating.graduatingserver.core.global + +import org.springframework.data.jpa.repository.JpaRepository + +fun JpaRepository.safeSaveAll(entities: Iterable) { + entities.forEach { + try { + save(it) + } catch (_: Exception) { + } + } +} \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/core/meal/MealRepository.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/core/meal/MealRepository.kt index 3cb9b3c..fc3121f 100644 --- a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/core/meal/MealRepository.kt +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/core/meal/MealRepository.kt @@ -1,10 +1,16 @@ package com.bestswlkh0310.graduating.graduatingserver.core.meal import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository import java.time.LocalDate @Repository interface MealRepository: JpaRepository { fun findBySchoolIdAndMealDate(schoolId: Long, mealDate: LocalDate): List + + @Modifying + @Query("DELETE FROM MealEntity e WHERE e.mealDate < CURRENT_DATE") + fun deleteOldRecords() } \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/global/config/RestClientConfig.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/global/config/RestClientConfig.kt index 7dd4244..46289ed 100644 --- a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/global/config/RestClientConfig.kt +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/global/config/RestClientConfig.kt @@ -3,6 +3,7 @@ package com.bestswlkh0310.graduating.graduatingserver.global.config import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.http.converter.json.GsonHttpMessageConverter import org.springframework.web.client.RestClient @Configuration diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/global/loader/NeisSchoolLoader.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/global/loader/NeisSchoolLoader.kt index c08c12c..a1dbc93 100644 --- a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/global/loader/NeisSchoolLoader.kt +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/global/loader/NeisSchoolLoader.kt @@ -1,26 +1,38 @@ package com.bestswlkh0310.graduating.graduatingserver.global.loader -import com.bestswlkh0310.graduating.graduatingserver.infra.neis.NeisScheduleHelper +import com.bestswlkh0310.graduating.graduatingserver.core.graduating.GraduatingRepository +import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolRepository +import com.bestswlkh0310.graduating.graduatingserver.infra.neis.schedule.NeisScheduleClient +import com.bestswlkh0310.graduating.graduatingserver.infra.neis.school.NeisSchoolClient import kotlinx.coroutines.runBlocking import org.springframework.boot.CommandLineRunner //@Component -class DataLoader( - private val neisService: NeisScheduleHelper +class FetchSchoolRunner( + private val neisSchoolClient: NeisSchoolClient, + private val schoolRepository: SchoolRepository ) : CommandLineRunner { override fun run(vararg args: String?) { - neisService.importCsv("/Users/hhhello0507/Downloads/school.csv") + val schools = neisSchoolClient.importCsv("/Users/hhhello0507/Downloads/school.csv") + schoolRepository.saveAll(schools) } } //@Component -class FetchSchool( - private val neisService: NeisScheduleHelper +class FetchScheduleRunner( + private val neisScheduleClient: NeisScheduleClient, + private val schoolRepository: SchoolRepository, + private val graduatingRepository: GraduatingRepository, ) : CommandLineRunner { override fun run(vararg args: String?) { runBlocking { - neisService.getSchoolsDate() + val graduatingEntities = schoolRepository.findAll() + .map { school -> + neisScheduleClient.getSchoolGraduatingDays(school) + } + .flatten() + graduatingRepository.saveAll(graduatingEntities) } } } \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisMealHelper.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisMealHelper.kt deleted file mode 100644 index fd8a9a8..0000000 --- a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisMealHelper.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.bestswlkh0310.graduating.graduatingserver.infra.neis - -import com.bestswlkh0310.graduating.graduatingserver.common.parse -import com.bestswlkh0310.graduating.graduatingserver.core.meal.MealEntity -import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolEntity -import com.bestswlkh0310.graduating.graduatingserver.core.meal.MealRepository -import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolRepository -import mu.KLogger -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.stereotype.Component -import org.springframework.web.client.RestClient -import java.time.LocalDateTime - -@Component -class NeisMealHelper( - private val schoolRepository: SchoolRepository, - private val neisProperties: NeisProperties, - private val mealRepository: MealRepository, - @Qualifier("neis") - private val restClient: RestClient, - private val logger: KLogger, -) { - - suspend fun getMeals(school: SchoolEntity): List { - val currentDate = LocalDateTime.now() - val nextDate = currentDate.plusDays(6) // A week - - val response = restClient.get() - .uri { uriBuilder -> - uriBuilder - .path("hub/mealServiceDietInfo") - .queryParam("KEY", neisProperties.apiKey) - .queryParam("Type", "json") - .queryParam("ATPT_OFCDC_SC_CODE", school.officeCode) - .queryParam("SD_SCHUL_CODE", school.code) - .queryParam("MLSV_FROM_YMD", currentDate.parse("yyyyMMdd")) - .queryParam("MLSV_TO_YMD", nextDate.parse("yyyyMMdd")) - .build() - } - .retrieve() - .toEntity(NeisMealsRes::class.java) - .body - - val result = arrayListOf() - - response?.mealServiceDietInfo?.let { res -> - res.mapNotNull { it?.row } - .forEach { rows -> - for (meal in rows) { - try { - if (meal == null) continue - val entity = meal.toMealEntity(school = school) - if (entity == null) { - println(meal) - continue - } - result.add(entity) - } catch (e: Exception) { - logger.error("Neis Error") - } - } - } - } - - return result - } - -// suspend fun saveMeals() { -// val meals = schoolRepository.findAll().flatMapIndexed { idx, school -> -// val meals = getMeals(school) -// println("[$idx] school.id - ${school.id} , size - ${meals.size}") -// return@flatMapIndexed meals -// } -// println(meals.size) -// mealRepository.saveAll(meals) -// } -} \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisMealRes.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisMealRes.kt deleted file mode 100644 index 3836450..0000000 --- a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisMealRes.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.bestswlkh0310.graduating.graduatingserver.infra.neis - -data class NeisMealRowRes( - val ATPT_OFCDC_SC_CODE: String, - val ATPT_OFCDC_SC_NM: String, - val SD_SCHUL_CODE: String, - val SCHUL_NM: String, - val MMEAL_SC_CODE: String, - val MMEAL_SC_NM: String, - val MLSV_YMD: String, - val MLSV_FGR: Double, - val DDISH_NM: String, - val ORPLC_INFO: String, - val CAL_INFO: String, - val NTR_INFO: String, - val MLSV_FROM_YMD: String, - val MLSV_TO_YMD: String, - val LOAD_DTM: String -) - -data class NeisMealRes( - val row: List -) - -data class NeisMealsRes( - val mealServiceDietInfo: List?, -) \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisMealRowResMapper.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisMealRowResMapper.kt deleted file mode 100644 index 1149c48..0000000 --- a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisMealRowResMapper.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.bestswlkh0310.graduating.graduatingserver.infra.neis - -import com.bestswlkh0310.graduating.graduatingserver.common.toLocalDate -import com.bestswlkh0310.graduating.graduatingserver.core.meal.MealEntity -import com.bestswlkh0310.graduating.graduatingserver.core.meal.MealType -import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolEntity - -fun NeisMealRowRes.toMealEntity(school: SchoolEntity): MealEntity? { - val time = this.MLSV_TO_YMD.toLocalDate("yyyyMMdd") ?: return null - return MealEntity( - mealType = MealType.ofKorean(this.MMEAL_SC_NM), - menu = this.DDISH_NM, - calorie = this.CAL_INFO.substringBefore(" Kcal").toDoubleOrNull() ?: 0.0, - mealInfo = this.ORPLC_INFO, - mealDate = time, - school = school - ) -} \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisScheduleHelper.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisScheduleHelper.kt deleted file mode 100644 index 4c8a529..0000000 --- a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisScheduleHelper.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.bestswlkh0310.graduating.graduatingserver.infra.neis - -import com.bestswlkh0310.graduating.graduatingserver.core.graduating.GraduatingEntity -import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolEntity -import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolType -import com.bestswlkh0310.graduating.graduatingserver.core.graduating.GraduatingRepository -import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolRepository -import mu.KLogger -import org.apache.commons.csv.CSVFormat -import org.apache.commons.csv.CSVParser -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.stereotype.Component -import org.springframework.web.client.RestClient -import java.io.File -import java.nio.charset.StandardCharsets -import java.time.LocalDate -import java.time.format.DateTimeFormatter - -// TODO: 코드 개선 (함수) -// 다음 연도가 되면 개선할 듯....... -@Component -class NeisScheduleHelper( - private val schoolRepository: SchoolRepository, - private val graduatingRepository: GraduatingRepository, - @Qualifier("neis") - private val restClient: RestClient, - private val neisProperties: NeisProperties, - private val logger: KLogger -) { - - fun importCsv(filePath: String) { - val file = File(filePath) - val parser = CSVParser.parse(file, StandardCharsets.UTF_8, CSVFormat.DEFAULT.withFirstRecordAsHeader()) - - parser.mapNotNull { record -> - val v = record.toList() - try { - SchoolEntity( - officeCode = v[0], - code = v[1], - name = v[2], - type = SchoolType.ofKorean(v[3]), - cityName = v[4], - postalCode = v[5], - address = v[6], - addressDetail = v[7], - phone = v[8], - website = v[9], - createdAt = LocalDate.parse(v[10], DateTimeFormatter.ofPattern("yyyyMMdd")), - anniversary = LocalDate.parse(v[11], DateTimeFormatter.ofPattern("yyyyMMdd")), - ) - } catch (e: Exception) { - logger.error("Neis Error", e) - null - } - }.let { - schoolRepository.saveAll(it) - } - - parser.close() - } - - private suspend fun getSchoolDate(school: SchoolEntity): List { - val result = arrayListOf() - try { - val response = restClient.get() - .uri { uriBuilder -> - uriBuilder - .path("hub/SchoolSchedule") - .queryParam("KEY", neisProperties.apiKey) - .queryParam("Type", "json") - .queryParam("ATPT_OFCDC_SC_CODE", school.officeCode) - .queryParam("SD_SCHUL_CODE", school.code) - .queryParam("AA_FROM_YMD", "20241201") - .queryParam("AA_TO_YMD", "20250301") - .build() - } - .retrieve() - .toEntity(NeisSchedulesRes::class.java) - .body - - var includeGraduating = false - response?.SchoolSchedule?.let { res -> - res.mapNotNull { it?.row } - .forEach { rows -> - rows.forEach { row -> - if (row.EVENT_NM.contains("졸업")) { - println("✅ - ${school.name} - ${row.EVENT_NM}") - includeGraduating = true - val entity = GraduatingEntity( - school = school, - graduatingDay = row.AA_YMD, - ) - result.add(entity) - } - } - } - } - - if (!includeGraduating) { - println("❌ - ${school.name} - 알 수 없음 ${response?.SchoolSchedule}") - } - } catch (e: Exception) { - logger.error("Neis Error", e) - } - return result - } - - suspend fun getSchoolsDate() { - val schools = schoolRepository.findAll() - schools.map { - getSchoolDate(it) - }.flatMap { - graduatingRepository.saveAll(it) - } - } -} \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisSchedulesRes.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisSchedulesRes.kt deleted file mode 100644 index 3da8dcf..0000000 --- a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/NeisSchedulesRes.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.bestswlkh0310.graduating.graduatingserver.infra.neis - -data class NeisScheduleRowRes( - val ATPT_OFCDC_SC_CODE: String, - val SD_SCHUL_CODE: String, - val AY: String, - val AA_YMD: String, - val ATPT_OFCDC_SC_NM: String, - val SCHUL_NM: String, - val DGHT_CRSE_SC_NM: String, - val SCHUL_CRSE_SC_NM: String, - val EVENT_NM: String, - val EVENT_CNTNT: String, - val ONE_GRADE_EVENT_YN: String, - val TW_GRADE_EVENT_YN: String, - val THREE_GRADE_EVENT_YN: String, - val FR_GRADE_EVENT_YN: String, - val FIV_GRADE_EVENT_YN: String, - val SIX_GRADE_EVENT_YN: String, - val SBTR_DD_SC_NM: String, - val LOAD_DTM: String -) - -data class NeisScheduleRes( - val row: List -) - -data class NeisSchedulesRes( - val SchoolSchedule: List? -) \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/meal/NeisMealClient.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/meal/NeisMealClient.kt new file mode 100644 index 0000000..e339bcc --- /dev/null +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/meal/NeisMealClient.kt @@ -0,0 +1,56 @@ +package com.bestswlkh0310.graduating.graduatingserver.infra.neis.meal + +import java.time.LocalDateTime +import com.bestswlkh0310.graduating.graduatingserver.common.parse +import com.bestswlkh0310.graduating.graduatingserver.core.meal.MealEntity +import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolEntity +import com.bestswlkh0310.graduating.graduatingserver.global.exception.CustomException +import com.bestswlkh0310.graduating.graduatingserver.infra.neis.NeisProperties +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Component +import org.springframework.web.client.RestClient +import org.springframework.web.client.body +import mu.KLogger +import org.springframework.http.HttpStatus + +@Component +class NeisMealClient( + private val neisProperties: NeisProperties, + @Qualifier("neis") + private val restClient: RestClient, + private val logger: KLogger, +) { + fun getMeals(school: SchoolEntity): List { + val currentDate = LocalDateTime.now() + val nextDate = currentDate.plusDays(6) // A week + return restClient.get() + .uri { uriBuilder -> + uriBuilder + .path("hub/mealServiceDietInfo") + .queryParam("KEY", neisProperties.apiKey) + .queryParam("Type", "json") + .queryParam("ATPT_OFCDC_SC_CODE", school.officeCode) + .queryParam("SD_SCHUL_CODE", school.code) + .queryParam("MLSV_FROM_YMD", currentDate.parse("yyyyMMdd")) + .queryParam("MLSV_TO_YMD", nextDate.parse("yyyyMMdd")) + .build() + } + .retrieve() + .body() + .let { it ?: throw CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "Neis Error") } + .let { jacksonObjectMapper().readValue(it) } + .mealServiceDietInfo + .mapNotNull { it?.row } + .flatten() + .mapNotNull { meal -> + try { + meal.toEntity(school = school) + } catch (e: Exception) { + logger.error("Neis Error") + null + } + } + } +} \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/meal/NeisMealsRes.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/meal/NeisMealsRes.kt new file mode 100644 index 0000000..e73f07e --- /dev/null +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/meal/NeisMealsRes.kt @@ -0,0 +1,106 @@ +package com.bestswlkh0310.graduating.graduatingserver.infra.neis.meal + +import com.bestswlkh0310.graduating.graduatingserver.common.toLocalDate +import com.bestswlkh0310.graduating.graduatingserver.core.meal.MealEntity +import com.bestswlkh0310.graduating.graduatingserver.core.meal.MealType +import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolEntity +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +data class NeisMealsRes( + val mealServiceDietInfo: List +) { + @JsonIgnoreProperties(ignoreUnknown = true) + data class MealServiceDietInfo( + val row: List? + ) { + data class Row( + /** + * 시도교육청코드 + */ + @JsonProperty("ATPT_OFCDC_SC_CODE") val atptOfcdcScCode: String, + + /** + * 시도교육청명 + */ + @JsonProperty("ATPT_OFCDC_SC_NM") val atptOfcdcScNm: String, + + /** + * 칼로리정보 + */ + @JsonProperty("CAL_INFO") val calInfo: String, + + /** + * 요리명 + */ + @JsonProperty("DDISH_NM") val ddishNm: String, + + /** + * 수정일자 + */ + @JsonProperty("LOAD_DTM") val loadDtm: String, + + /** + * 급식인원수 + */ + @JsonProperty("MLSV_FGR") val mlsvFgr: Int, + + /** + * 급식시작일자 + */ + @JsonProperty("MLSV_FROM_YMD") val mlsvFromYmd: String, + + /** + * 급식종료일자 + */ + @JsonProperty("MLSV_TO_YMD") val mlsvToYmd: String, + + /** + * 급식일자 + */ + @JsonProperty("MLSV_YMD") val mlsvYmd: String, + + /** + * 식사코드 + */ + @JsonProperty("MMEAL_SC_CODE") val mmealScCode: String, + + /** + * 식사명 + */ + @JsonProperty("MMEAL_SC_NM") val mmealScNm: String, + + /** + * 영양정보 + */ + @JsonProperty("NTR_INFO") val ntrInfo: String, + + /** + * 원산지정보 + */ + @JsonProperty("ORPLC_INFO") val orplcInfo: String, + + /** + * 학교명 + */ + @JsonProperty("SCHUL_NM") val schulNm: String, + + /** + * 행정표준코드 + */ + @JsonProperty("SD_SCHUL_CODE") val sdSchulCode: String + ) { + fun toEntity(school: SchoolEntity): MealEntity? { + val time = this.mlsvToYmd.toLocalDate("yyyyMMdd") ?: return null + return MealEntity( + mealType = MealType.ofKorean(this.mmealScNm), + menu = this.ddishNm, + calorie = this.calInfo.substringBefore(" Kcal").toDoubleOrNull() ?: 0.0, + mealInfo = this.orplcInfo, + mealDate = time, + school = school + ) + } + } + } +} \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/schedule/NeisScheduleClient.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/schedule/NeisScheduleClient.kt new file mode 100644 index 0000000..63562ca --- /dev/null +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/schedule/NeisScheduleClient.kt @@ -0,0 +1,58 @@ +package com.bestswlkh0310.graduating.graduatingserver.infra.neis.schedule + +import com.bestswlkh0310.graduating.graduatingserver.core.graduating.GraduatingEntity +import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolEntity +import com.bestswlkh0310.graduating.graduatingserver.core.graduating.GraduatingRepository +import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolRepository +import com.bestswlkh0310.graduating.graduatingserver.global.exception.CustomException +import com.bestswlkh0310.graduating.graduatingserver.infra.neis.NeisProperties +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import mu.KLogger +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import org.springframework.web.client.RestClient +import org.springframework.web.client.body + +@Component +class NeisScheduleClient( + @Qualifier("neis") + private val restClient: RestClient, + private val neisProperties: NeisProperties, + private val logger: KLogger +) { + + suspend fun getSchoolGraduatingDays(school: SchoolEntity): List { + val response = restClient.get() + .uri { uriBuilder -> + uriBuilder + .path("hub/SchoolSchedule") + .queryParam("KEY", neisProperties.apiKey) + .queryParam("Type", "json") + .queryParam("ATPT_OFCDC_SC_CODE", school.officeCode) + .queryParam("SD_SCHUL_CODE", school.code) + .queryParam("AA_FROM_YMD", "20241201") + .queryParam("AA_TO_YMD", "20250301") + .build() + } + .retrieve() + .body() + .let { it ?: throw CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "Neis Error") } + .let { jacksonObjectMapper().readValue(it, NeisSchedulesRes::class.java) } + + val result = response.schoolSchedule + .mapNotNull { it.row } + .flatten() + .filter { it.eventNm.contains("졸업") } + .map { GraduatingEntity(school = school, graduatingDay = it.aaYmd) } + + if (result.isEmpty()) { + logger.info("❌ - ${school.name} - 알 수 없음 ${response.schoolSchedule}") + } else { + result.forEach { + logger.info("✅ - ${school.name}: ${it.graduatingDay}") + } + } + return result + } +} \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/schedule/NeisSchedulesRes.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/schedule/NeisSchedulesRes.kt new file mode 100644 index 0000000..b7bf454 --- /dev/null +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/schedule/NeisSchedulesRes.kt @@ -0,0 +1,106 @@ +package com.bestswlkh0310.graduating.graduatingserver.infra.neis.schedule + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + + +data class NeisSchedulesRes( + @JsonProperty("SchoolSchedule") val schoolSchedule: List +) { + @JsonIgnoreProperties(ignoreUnknown = true) + data class SchoolSchedule( + val row: List? + ) { + data class Row( + /** + * 시도교육청코드 + */ + @JsonProperty("ATPT_OFCDC_SC_CODE") val atptOfcdcScCode: String, + + /** + * 행정표준코드 + */ + @JsonProperty("SD_SCHUL_CODE") val sdSchulCode: String, + + /** + * 학년도 + */ + @JsonProperty("AY") val ay: String, + + /** + * 학사일자 + */ + @JsonProperty("AA_YMD") val aaYmd: String, + + /** + * 시도교육청명 + */ + @JsonProperty("ATPT_OFCDC_SC_NM") val atptOfcdcScNm: String, + + /** + * 학교명 + */ + @JsonProperty("SCHUL_NM") val schulNm: String, + + /** + * 주야과정명 + */ + @JsonProperty("DGHT_CRSE_SC_NM") val dghtCrseScNm: String, + + /** + * 학교과정명 + */ + @JsonProperty("SCHUL_CRSE_SC_NM") val schulCrseScNm: String, + + /** + * 행사명 + */ + @JsonProperty("EVENT_NM") val eventNm: String, + + /** + * 행사내용 + */ + @JsonProperty("EVENT_CNTNT") val eventCntnt: String, + + /** + * 1학년행사여부 + */ + @JsonProperty("ONE_GRADE_EVENT_YN") val oneGradeEventYn: String, + + /** + * 2학년행사여부 + */ + @JsonProperty("TW_GRADE_EVENT_YN") val twGradeEventYn: String, + + /** + * 3학년행사여부 + */ + @JsonProperty("THREE_GRADE_EVENT_YN") val threeGradeEventYn: String, + + /** + * 4학년행사여부 + */ + @JsonProperty("FR_GRADE_EVENT_YN") val frGradeEventYn: String, + + /** + * 5학년행사여부 + */ + @JsonProperty("FIV_GRADE_EVENT_YN") val fivGradeEventYn: String, + + /** + * 6학년행사여부 + */ + @JsonProperty("SIX_GRADE_EVENT_YN") val sixGradeEventYn: String, + + /** + * 수업공제일명 + */ + @JsonProperty("SBTR_DD_SC_NM") val sbtrDdScNm: String, + + /** + * 수정일자 + */ + @JsonProperty("LOAD_DTM") val loadDtm: String + ) + } +} \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/school/NeisSchoolClient.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/school/NeisSchoolClient.kt new file mode 100644 index 0000000..9f90bd3 --- /dev/null +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/neis/school/NeisSchoolClient.kt @@ -0,0 +1,50 @@ +package com.bestswlkh0310.graduating.graduatingserver.infra.neis.school + +import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolEntity +import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolRepository +import com.bestswlkh0310.graduating.graduatingserver.core.school.SchoolType +import mu.KLogger +import org.apache.commons.csv.CSVFormat +import org.apache.commons.csv.CSVParser +import org.springframework.stereotype.Component +import java.io.File +import java.nio.charset.StandardCharsets +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +@Component +class NeisSchoolClient( + private val logger: KLogger, +) { + fun importCsv(filePath: String): List { + val file = File(filePath) + val parser = CSVParser.parse(file, StandardCharsets.UTF_8, CSVFormat.DEFAULT.withFirstRecordAsHeader()) + + val ret = parser.mapNotNull { record -> + val v = record.toList() + try { + SchoolEntity( + officeCode = v[0], + code = v[1], + name = v[2], + type = SchoolType.ofKorean(v[3]), + cityName = v[4], + postalCode = v[5], + address = v[6], + addressDetail = v[7], + phone = v[8], + website = v[9], + createdAt = LocalDate.parse(v[10], DateTimeFormatter.ofPattern("yyyyMMdd")), + anniversary = LocalDate.parse(v[11], DateTimeFormatter.ofPattern("yyyyMMdd")), + ) + } catch (e: Exception) { + logger.error("Neis Error", e) + null + } + } + + parser.close() + + return ret + } +} \ No newline at end of file diff --git a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/oauth2/apple/AppleOAuth2Client.kt b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/oauth2/apple/AppleOAuth2Client.kt index ec51887..dd203e3 100644 --- a/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/oauth2/apple/AppleOAuth2Client.kt +++ b/Graduating-Server/src/main/kotlin/com/bestswlkh0310/graduating/graduatingserver/infra/oauth2/apple/AppleOAuth2Client.kt @@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Qualifier import org.springframework.http.HttpStatus import org.springframework.stereotype.Component import org.springframework.web.client.RestClient +import org.springframework.web.client.body import org.springframework.web.client.toEntity import java.security.PrivateKey import java.security.Security @@ -24,7 +25,7 @@ class AppleOAuth2Client( private val restClient: RestClient, private val properties: AppleOAuth2Properties, ) { - + fun getToken(code: String) = restClient.post() .uri { it.path("/auth/token") @@ -35,14 +36,14 @@ class AppleOAuth2Client( .build() } .retrieve() - .toEntity() - .body ?: throw CustomException(HttpStatus.BAD_REQUEST, "Apple client error") + .body() + ?: throw CustomException(HttpStatus.BAD_REQUEST, "Apple client error") fun getPublicKeys() = restClient.get() .uri("/auth/keys") .retrieve() - .toEntity(AppleJWKSet::class.java) - .body ?: throw CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "Apple client error") + .body(AppleJWKSet::class.java) + ?: throw CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "Apple client error") private fun generateClientSecret(): String { diff --git a/Graduating-iOS/Graduating/Feature/Main/MainView.swift b/Graduating-iOS/Graduating/Feature/Main/MainView.swift index d5b73f3..1a5e7c2 100644 --- a/Graduating-iOS/Graduating/Feature/Main/MainView.swift +++ b/Graduating-iOS/Graduating/Feature/Main/MainView.swift @@ -34,8 +34,7 @@ let data = [ struct MainPath: Hashable {} -struct MainView: View { - +struct MainView { @EnvironmentObject private var router: Router @EnvironmentObject private var appState: AppState @EnvironmentObject private var dialogProvider: DialogProvider @@ -50,7 +49,9 @@ struct MainView: View { init(_ path: MainPath) { self.path = path } - +} + +extension MainView: View { var body: some View { MyBottomAppBar(data, selection: selectedTab) { selectedTab = $0