diff --git a/app-server/detekt-config.yml b/app-server/detekt-config.yml index cd9e87a5a..8b62d51f4 100644 --- a/app-server/detekt-config.yml +++ b/app-server/detekt-config.yml @@ -21,6 +21,8 @@ naming: active: false InvalidPackageDeclaration: active: false + ConstructorParameterNaming: + active: false performance: SpreadOperator: active: false diff --git a/app-server/subprojects/api_specification/api/scc-api/api-spec.yaml b/app-server/subprojects/api_specification/api/scc-api/api-spec.yaml index 4b1a1e257..13d3c9f4b 100644 --- a/app-server/subprojects/api_specification/api/scc-api/api-spec.yaml +++ b/app-server/subprojects/api_specification/api/scc-api/api-spec.yaml @@ -1651,21 +1651,29 @@ components: ToiletAccessibilityDetails: type: object properties: + image_url: + type: string gender: - type: boolean + type: string accessDesc: type: string availableDesc: type: string entranceDesc: type: string - stallDesc: + stallWidth: + type: string + stallDepth: type: string doorDesc: type: string - washStandDesc: + doorSideRoom: + type: string + washStandBelowRoom: + type: string + washStandHandle: type: string - extra: + extraDesc: type: string PlaceListItem: diff --git a/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/in/ExternalAccessibilityService.kt b/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/in/ExternalAccessibilityService.kt index 2b6076e9c..3e5937024 100644 --- a/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/in/ExternalAccessibilityService.kt +++ b/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/in/ExternalAccessibilityService.kt @@ -14,4 +14,10 @@ class ExternalAccessibilityService( return externalAccessibilityRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("External Accessibility with id $id does not exist.") } + + fun upsert( + list: List + ) { + return externalAccessibilityRepository.saveAll(list) + } } diff --git a/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/in/ToiletAccessibilitySyncUseCase.kt b/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/in/ToiletAccessibilitySyncUseCase.kt new file mode 100644 index 000000000..a46c54e11 --- /dev/null +++ b/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/in/ToiletAccessibilitySyncUseCase.kt @@ -0,0 +1,48 @@ +package club.staircrusher.external_accessibility.application.port.`in` + +import club.staircrusher.external_accessibility.application.port.out.web.ToiletInfoFetcher +import club.staircrusher.external_accessibility.domain.model.ExternalAccessibility +import club.staircrusher.external_accessibility.domain.model.ToiletAccessibilityDetails +import club.staircrusher.stdlib.di.annotation.Component +import club.staircrusher.stdlib.external_accessibility.ExternalAccessibilityCategory +import club.staircrusher.stdlib.geography.Location +import java.time.Instant + +@Component +class ToiletAccessibilitySyncUseCase( + private val externalAccessibilityService: ExternalAccessibilityService, + private val toiletInfoFetcher: ToiletInfoFetcher, +) { + + fun load() { + val records = toiletInfoFetcher.fetchRecords() + externalAccessibilityService.upsert( + records.map { + ExternalAccessibility( + id = it.toiletId, + name = it.toiletName, + location = Location(it.longitude, it.latitude), + address = it.addressNew ?: it.addressOld ?: "", + createdAt = Instant.now(), + updatedAt = Instant.now(), + category = ExternalAccessibilityCategory.TOILET, + toiletDetails = ToiletAccessibilityDetails( + imageUrl = it.imageUrl, + gender = it.이용성별, + accessDesc = it.추천접근로, + availableDesc = it.사용가능여부, + entranceDesc = it.화장실입구구조, + stallWidth = it.내부가로너비, + stallDepth = it.내부세로너비, + doorDesc = it.대변기출입문, + doorSideRoom = it.대변기옆공간, + washStandBelowRoom = it.세면대아래공간, + washStandHandle = it.세면대손잡이, + extraDesc = it.기타참고사항, + ) + ) + } + ) + + } +} diff --git a/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/out/ExternalAccessibilityRepository.kt b/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/out/persistence/ExternalAccessibilityRepository.kt similarity index 100% rename from app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/out/ExternalAccessibilityRepository.kt rename to app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/out/persistence/ExternalAccessibilityRepository.kt diff --git a/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/out/web/ToiletInfoFetcher.kt b/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/out/web/ToiletInfoFetcher.kt new file mode 100644 index 000000000..43805a305 --- /dev/null +++ b/app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/out/web/ToiletInfoFetcher.kt @@ -0,0 +1,28 @@ +package club.staircrusher.external_accessibility.application.port.out.web + +interface ToiletInfoFetcher { + fun fetchRecords(): List + + @Suppress("NonAsciiCharacters") + data class ToiletRow( + val toiletId: String, + val toiletName: String, + val imageUrl: String, + val addressOld: String?, + val addressNew: String?, + val 이용성별: String?, + val 추천접근로: String?, + val 사용가능여부: String?, + val 화장실입구구조: String?, + val 내부가로너비: String?, + val 내부세로너비: String?, + val 대변기출입문: String?, + val 대변기옆공간: String?, + val 세면대아래공간: String?, + val 세면대손잡이: String?, + val 기타참고사항: String?, + val 상세주소: String?, + val latitude: Double, + val longitude: Double, + ) +} diff --git a/app-server/subprojects/bounded_context/external_accessibility/domain/src/main/kotlin/club/staircrusher/external_accessibility/domain/model/ToiletAccessibilityDetails.kt b/app-server/subprojects/bounded_context/external_accessibility/domain/src/main/kotlin/club/staircrusher/external_accessibility/domain/model/ToiletAccessibilityDetails.kt index 001852b30..6e379d8a0 100644 --- a/app-server/subprojects/bounded_context/external_accessibility/domain/src/main/kotlin/club/staircrusher/external_accessibility/domain/model/ToiletAccessibilityDetails.kt +++ b/app-server/subprojects/bounded_context/external_accessibility/domain/src/main/kotlin/club/staircrusher/external_accessibility/domain/model/ToiletAccessibilityDetails.kt @@ -4,12 +4,16 @@ import kotlinx.serialization.Serializable @Serializable data class ToiletAccessibilityDetails( - val gender: Boolean? = null, + val imageUrl: String? = null, + val gender: String? = null, val accessDesc: String? = null, val availableDesc: String? = null, val entranceDesc: String? = null, - val stallDesc: String? = null, + val stallWidth: String? = null, + val stallDepth: String? = null, val doorDesc: String? = null, - val washStandDesc: String? = null, - val extra: String? = null, + val doorSideRoom: String? = null, + val washStandBelowRoom: String? = null, + val washStandHandle: String? = null, + val extraDesc: String? = null, ) diff --git a/app-server/subprojects/bounded_context/external_accessibility/infra/build.gradle.kts b/app-server/subprojects/bounded_context/external_accessibility/infra/build.gradle.kts index ae9ad5ca8..58a0b818c 100644 --- a/app-server/subprojects/bounded_context/external_accessibility/infra/build.gradle.kts +++ b/app-server/subprojects/bounded_context/external_accessibility/infra/build.gradle.kts @@ -8,6 +8,9 @@ dependencies { api(projects.apiSpecification.domainEvent) implementation(projects.crossCuttingConcern.infra.persistenceModel) implementation("org.springframework.boot:spring-boot-starter-web") + implementation("io.projectreactor.netty:reactor-netty") + implementation("org.springframework:spring-webflux") + implementation("org.apache.commons:commons-csv:1.11.0") integrationTestImplementation(projects.crossCuttingConcern.test.springIt) diff --git a/app-server/subprojects/bounded_context/external_accessibility/infra/src/integrationTest/kotlin/club/staircrusher/external_accessibility/infra/adapter/in/controller/ExternalAccessibilityControllerTest.kt b/app-server/subprojects/bounded_context/external_accessibility/infra/src/integrationTest/kotlin/club/staircrusher/external_accessibility/infra/adapter/in/controller/ExternalAccessibilityControllerTest.kt index e2460056c..e2c31c0b9 100644 --- a/app-server/subprojects/bounded_context/external_accessibility/infra/src/integrationTest/kotlin/club/staircrusher/external_accessibility/infra/adapter/in/controller/ExternalAccessibilityControllerTest.kt +++ b/app-server/subprojects/bounded_context/external_accessibility/infra/src/integrationTest/kotlin/club/staircrusher/external_accessibility/infra/adapter/in/controller/ExternalAccessibilityControllerTest.kt @@ -3,21 +3,17 @@ package club.staircrusher.external_accessibility.infra.adapter.`in`.controller import club.staircrusher.api.converter.toDTO import club.staircrusher.api.spec.dto.ExternalAccessibility import club.staircrusher.api.spec.dto.GetExternalAccessibilityPostRequest -import club.staircrusher.api.spec.dto.SearchExternalAccessibilitiesPostRequest import club.staircrusher.api.spec.dto.SearchExternalAccessibilitiesPost200Response -import club.staircrusher.user.domain.model.User +import club.staircrusher.api.spec.dto.SearchExternalAccessibilitiesPostRequest import club.staircrusher.external_accessibility.infra.adapter.`in`.controller.base.ExternalAccessibilityITBase import club.staircrusher.external_accessibility.infra.adapter.out.persistence.ExternalAccessibilityRepository import club.staircrusher.stdlib.geography.Length import club.staircrusher.stdlib.geography.Location import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired -import kotlin.random.Random class ExternalAccessibilityControllerTest : ExternalAccessibilityITBase() { diff --git a/app-server/subprojects/bounded_context/external_accessibility/infra/src/main/kotlin/club/staircrusher/external_accessibility/infra/adapter/in/controller/ExternalAccessibilityController.kt b/app-server/subprojects/bounded_context/external_accessibility/infra/src/main/kotlin/club/staircrusher/external_accessibility/infra/adapter/in/controller/ExternalAccessibilityController.kt index a40bb4e78..a404cf54e 100644 --- a/app-server/subprojects/bounded_context/external_accessibility/infra/src/main/kotlin/club/staircrusher/external_accessibility/infra/adapter/in/controller/ExternalAccessibilityController.kt +++ b/app-server/subprojects/bounded_context/external_accessibility/infra/src/main/kotlin/club/staircrusher/external_accessibility/infra/adapter/in/controller/ExternalAccessibilityController.kt @@ -7,6 +7,8 @@ import club.staircrusher.api.spec.dto.SearchExternalAccessibilitiesPost200Respon import club.staircrusher.api.spec.dto.ToiletAccessibilityDetails import club.staircrusher.external_accessibility.application.port.`in`.ExternalAccessibilitySearchService import club.staircrusher.external_accessibility.application.port.`in`.ExternalAccessibilityService +import club.staircrusher.external_accessibility.application.port.`in`.ToiletAccessibilitySyncUseCase +import club.staircrusher.stdlib.env.SccEnv import club.staircrusher.stdlib.external_accessibility.ExternalAccessibilityCategory import club.staircrusher.stdlib.geography.Length import club.staircrusher.stdlib.geography.Location @@ -17,7 +19,8 @@ import org.springframework.web.bind.annotation.RestController @RestController class ExternalAccessibilityController( private val externalAccessibilitySearchService: ExternalAccessibilitySearchService, - private val externalAccessibilityService: ExternalAccessibilityService + private val externalAccessibilityService: ExternalAccessibilityService, + private val toiletAccessibilitySyncUseCase: ToiletAccessibilitySyncUseCase, ) { @PostMapping("/searchExternalAccessibilities") fun search( @@ -43,6 +46,18 @@ class ExternalAccessibilityController( return externalAccessibilityService.get(request.externalAccessibilityId).toDTO() } + @PostMapping("/syncWithDataSource") + fun syncWithDataSource(): String { + when (SccEnv.getEnv()) { + SccEnv.TEST, + SccEnv.LOCAL, + SccEnv.DEV -> toiletAccessibilitySyncUseCase.load() + + else -> return "Not accessible" + } + return "OK" + } + private fun club.staircrusher.external_accessibility.domain.model.ExternalAccessibility.toDTO(): ExternalAccessibility { return ExternalAccessibility( id = this.id, @@ -56,14 +71,18 @@ class ExternalAccessibilityController( category = this.category.name, toiletDetails = this.toiletDetails?.run { ToiletAccessibilityDetails( + imageUrl = imageUrl, gender = gender, accessDesc = accessDesc, availableDesc = availableDesc, entranceDesc = entranceDesc, - stallDesc = stallDesc, + stallWidth = stallWidth, + stallDepth = stallDepth, doorDesc = doorDesc, - washStandDesc = washStandDesc, - extra = extra + doorSideRoom = doorSideRoom, + washStandBelowRoom = washStandBelowRoom, + washStandHandle = washStandHandle, + extraDesc = extraDesc, ) } ) diff --git a/app-server/subprojects/bounded_context/external_accessibility/infra/src/main/kotlin/club/staircrusher/external_accessibility/infra/adapter/out/web/ToiletInfoFetcherImpl.kt b/app-server/subprojects/bounded_context/external_accessibility/infra/src/main/kotlin/club/staircrusher/external_accessibility/infra/adapter/out/web/ToiletInfoFetcherImpl.kt new file mode 100644 index 000000000..4ffd7c8c9 --- /dev/null +++ b/app-server/subprojects/bounded_context/external_accessibility/infra/src/main/kotlin/club/staircrusher/external_accessibility/infra/adapter/out/web/ToiletInfoFetcherImpl.kt @@ -0,0 +1,72 @@ +package club.staircrusher.external_accessibility.infra.adapter.out.web + +import club.staircrusher.external_accessibility.application.port.out.web.ToiletInfoFetcher +import club.staircrusher.stdlib.di.annotation.Component +import club.staircrusher.stdlib.util.string.emptyToNull +import org.apache.commons.csv.CSVFormat +import org.apache.commons.csv.CSVParser +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.core.io.buffer.DataBufferUtils +import org.springframework.http.MediaType +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono +import java.nio.charset.Charset + +@Component +class ToiletInfoFetcherImpl : ToiletInfoFetcher { + override fun fetchRecords(): List { + val client = WebClient.builder() + .codecs { it.defaultCodecs().maxInMemorySize(4 * 1024 * 1024) } + .build() + return client.get() + .uri(CSV_BASE_URL) + .accept(MediaType.valueOf("text/csv")) + .exchangeToMono { response -> + if (response.statusCode().is2xxSuccessful) { + DataBufferUtils.join(response.bodyToFlux(DataBuffer::class.java)) + } else { + Mono.error(RuntimeException("Failed to fetch ${response.statusCode()}")) + } + } + .map { dataBuffer -> + val parser = CSVParser.parse( + dataBuffer.asInputStream(), + Charset.forName("UTF-8"), + CSVFormat.DEFAULT.withFirstRecordAsHeader() + ) + parser.stream() + .map { + ToiletInfoFetcher.ToiletRow( + it.get("cot_conts_id").emptyToNull()!!, + it.get("cot_conts_name").emptyToNull()!!, + IMAGE_BASE_URL + (it.get("cot_img_main_url").emptyToNull() ?: return@map null), + it.get("cot_addr_full_old").emptyToNull() ?: return@map null, + it.get("cot_addr_full_new").emptyToNull() ?: return@map null, + it.get("cot_value_01").emptyToNull(), + it.get("cot_value_02").emptyToNull(), + it.get("cot_value_03").emptyToNull(), + it.get("cot_value_04").emptyToNull(), + it.get("cot_value_05").emptyToNull(), + it.get("cot_value_06").emptyToNull(), + it.get("cot_value_07").emptyToNull(), + it.get("cot_value_08").emptyToNull(), + it.get("cot_value_09").emptyToNull(), + it.get("cot_value_10").emptyToNull(), + it.get("cot_value_11").emptyToNull(), + it.get("cot_value_12").emptyToNull(), + it.get("lat").toDoubleOrNull() ?: return@map null, + it.get("lng").toDoubleOrNull() ?: return@map null, + + ) + } + .toList() + .mapNotNull { it } + } + .block() ?: emptyList() + } + + companion object { + const val CSV_BASE_URL = "https://bucket-cxcjy2.s3.ap-northeast-2.amazonaws.com/toilet_info_with_latlng.csv" + const val IMAGE_BASE_URL = "https://map.seoul.go.kr" + } +} diff --git a/app-server/subprojects/cross_cutting_concern/test/spring_it/src/main/kotlin/club/staircrusher/testing/spring_it/ITDataGenerator.kt b/app-server/subprojects/cross_cutting_concern/test/spring_it/src/main/kotlin/club/staircrusher/testing/spring_it/ITDataGenerator.kt index f1d7f0154..0cff0b89d 100644 --- a/app-server/subprojects/cross_cutting_concern/test/spring_it/src/main/kotlin/club/staircrusher/testing/spring_it/ITDataGenerator.kt +++ b/app-server/subprojects/cross_cutting_concern/test/spring_it/src/main/kotlin/club/staircrusher/testing/spring_it/ITDataGenerator.kt @@ -174,7 +174,7 @@ class ITDataGenerator { createdAt = Instant.now(), updatedAt = Instant.now(), category = category, - toiletDetails = ToiletAccessibilityDetails(gender = true, availableDesc = "asdf"), + toiletDetails = ToiletAccessibilityDetails(gender = "남자화장실", availableDesc = "asdf"), ) ) }