Skip to content

Commit

Permalink
[화장실지도] 화장실 정보 Fetch 받아 싱크하기 (#358)
Browse files Browse the repository at this point in the history
* finish fetcher

* on test only

* remove test code

* fix lint
  • Loading branch information
sanggggg authored Aug 4, 2024
1 parent b805813 commit 990a4ff
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 18 deletions.
2 changes: 2 additions & 0 deletions app-server/detekt-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ naming:
active: false
InvalidPackageDeclaration:
active: false
ConstructorParameterNaming:
active: false
performance:
SpreadOperator:
active: false
Expand Down
16 changes: 12 additions & 4 deletions app-server/subprojects/api_specification/api/scc-api/api-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ class ExternalAccessibilityService(
return externalAccessibilityRepository.findByIdOrNull(id)
?: throw IllegalArgumentException("External Accessibility with id $id does not exist.")
}

fun upsert(
list: List<ExternalAccessibility>
) {
return externalAccessibilityRepository.saveAll(list)
}
}
Original file line number Diff line number Diff line change
@@ -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.기타참고사항,
)
)
}
)

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package club.staircrusher.external_accessibility.application.port.out.web

interface ToiletInfoFetcher {
fun fetchRecords(): List<ToiletRow>

@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,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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,
)
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ToiletInfoFetcher.ToiletRow> {
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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
)
)
}
Expand Down

0 comments on commit 990a4ff

Please sign in to comment.