Skip to content

Commit

Permalink
Merge pull request #110 from mash-up-kr/feature/auto-complete-save
Browse files Browse the repository at this point in the history
feat: 장소 입력자동화 정보 DB 저장
  • Loading branch information
KimDoubleB authored Jul 27, 2024
2 parents 3a90359 + 6a72ef9 commit bb5f701
Show file tree
Hide file tree
Showing 26 changed files with 206 additions and 195 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.piikii.application.domain.generic

enum class Origin {
AVOCADO,
LEMON,
MANUAL,
enum class Origin(val prefix: String) {
AVOCADO("A"),
LEMON("L"),
MANUAL("M"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.piikii.application.domain.generic.ThumbnailLinks
data class OriginPlace(
val id: Long?,
val name: String,
val originMapId: Long,
val originMapId: OriginMapId,
val url: String,
val thumbnailLinks: ThumbnailLinks,
val address: String? = null,
Expand All @@ -18,3 +18,21 @@ data class OriginPlace(
val category: String?,
val origin: Origin,
)

@JvmInline
value class OriginMapId(val value: String) {
fun toId(): String {
return value.split(SEPARATOR).last()
}

companion object {
private const val SEPARATOR: String = "_"

fun of(
id: Long,
origin: Origin,
): OriginMapId {
return OriginMapId("${origin.prefix}$SEPARATOR$id")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ class OriginPlaceService(
ExceptionCode.NOT_SUPPORT_AUTO_COMPLETE_URL,
"No AutoComplete client found for $url",
)
val placeId = originPlaceAutoCompleteClient.extractPlaceId(plainUrl)
return originPlaceAutoCompleteClient.getAutoCompletedPlace(url = plainUrl, placeId = placeId)
val originMapId = originPlaceAutoCompleteClient.extractOriginMapId(plainUrl)
return originPlaceQueryPort.findByOriginMapId(originMapId)
?: originPlaceAutoCompleteClient.getAutoCompletedPlace(url = plainUrl, originMapId = originMapId)
.let { originPlaceCommandPort.save(it) }
}

private fun getUrlOfRemovedParameters(url: String): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,6 @@ data class AddPlaceRequest(
@field:Max(value = 5, message = "별점은 5 이하여야 합니다.")
@field:Schema(description = "별점 (0-5)", example = "4.5")
val starGrade: Float?,
@field:NotNull(message = "장소 정보 제공처")
@field:Schema(
description = "장소 정보 제공처",
allowableValues = [
"AVOCADO",
"LEMON",
"MANUAL",
],
example = "MANUAL",
)
val origin: Origin,
@field:NotBlank(message = "메모는 필수이며 빈 문자열이 허용되지 않습니다.")
@field:Size(max = 50, message = "메모는 50자를 초과할 수 없습니다.")
@field:Schema(description = "메모", example = "맛있는 레스토랑")
Expand All @@ -83,7 +72,7 @@ data class AddPlaceRequest(
address = address,
phoneNumber = phoneNumber,
starGrade = starGrade,
origin = origin,
origin = Origin.MANUAL,
memo = memo,
)
}
Expand Down Expand Up @@ -116,16 +105,6 @@ data class ModifyPlaceRequest(
@field:Max(value = 5, message = "별점은 5 이하여야 합니다.")
@field:Schema(description = "별점 (0-5)", example = "4.5")
val starGrade: Float?,
@field:Schema(
description = "장소 정보 제공처",
allowableValues = [
"AVOCADO",
"LEMON",
"MANUAL",
],
example = "MANUAL",
)
val origin: Origin,
@field:NotBlank(message = "메모는 필수이며 빈 문자열이 허용되지 않습니다.")
@field:Size(max = 50, message = "메모는 50자를 초과할 수 없습니다.")
@field:Schema(description = "메모", example = "맛있는 레스토랑")
Expand Down Expand Up @@ -153,7 +132,7 @@ data class ModifyPlaceRequest(
address = address,
phoneNumber = phoneNumber,
starGrade = starGrade,
origin = origin,
origin = Origin.MANUAL,
memo = memo,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
package com.piikii.application.port.output.persistence

import com.piikii.application.domain.place.OriginMapId
import com.piikii.application.domain.place.OriginPlace

interface OriginPlaceQueryPort {
fun retrieve(id: Long): OriginPlace

fun retrieveAll(ids: List<Long>): List<OriginPlace>
fun findByOriginMapId(originMapId: OriginMapId): OriginPlace?
}

interface OriginPlaceCommandPort {
fun save(originPlace: OriginPlace): OriginPlace

fun update(
originPlace: OriginPlace,
id: Long,
)

fun delete(id: Long)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.piikii.application.port.output.web

import com.piikii.application.domain.place.OriginMapId
import com.piikii.application.domain.place.OriginPlace

interface OriginPlaceAutoCompleteClient {
fun isAutoCompleteSupportedUrl(url: String): Boolean

fun extractPlaceId(url: String): String
fun extractOriginMapId(url: String): OriginMapId

fun getAutoCompletedPlace(
url: String,
placeId: String,
originMapId: OriginMapId,
): OriginPlace
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.piikii.output.persistence.postgresql.adapter

import com.piikii.application.domain.place.OriginMapId
import com.piikii.application.domain.place.OriginPlace
import com.piikii.application.port.output.persistence.OriginPlaceCommandPort
import com.piikii.application.port.output.persistence.OriginPlaceQueryPort
import com.piikii.output.persistence.postgresql.persistence.entity.OriginPlaceEntity
import com.piikii.output.persistence.postgresql.persistence.repository.OriginPlaceRepository
import jakarta.persistence.EntityNotFoundException
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional

Expand All @@ -16,34 +16,11 @@ class OriginPlaceAdapter(
) : OriginPlaceCommandPort, OriginPlaceQueryPort {
@Transactional
override fun save(originPlace: OriginPlace): OriginPlace {
val entity = OriginPlaceEntity.from(originPlace)
originPlaceRepository.save(entity)
return entity.toDomain()
return originPlaceRepository.save(OriginPlaceEntity.from(originPlace))
.toDomain()
}

@Transactional
override fun update(
originPlace: OriginPlace,
id: Long,
) {
TODO("Not yet implemented")
}

@Transactional
override fun delete(id: Long) {
TODO("Not yet implemented")
}

override fun retrieve(id: Long): OriginPlace {
val originPlaceEntity = originPlaceRepository.findById(id)
if (originPlaceEntity.isPresent) {
return originPlaceEntity.get().toDomain()
}
// TODO 예외 정의
throw EntityNotFoundException()
}

override fun retrieveAll(ids: List<Long>): List<OriginPlace> {
TODO("Not yet implemented")
override fun findByOriginMapId(originMapId: OriginMapId): OriginPlace? {
return originPlaceRepository.findByOriginMapId(originMapId)?.toDomain()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.piikii.output.persistence.postgresql.persistence.entity

import com.piikii.application.domain.generic.Origin
import com.piikii.application.domain.generic.ThumbnailLinks
import com.piikii.application.domain.place.OriginMapId
import com.piikii.application.domain.place.OriginPlace
import com.piikii.output.persistence.postgresql.persistence.common.BaseEntity
import jakarta.persistence.Column
Expand All @@ -19,13 +20,13 @@ import org.hibernate.annotations.SQLRestriction
@SQLDelete(sql = "UPDATE piikii.origin_place SET is_deleted = true WHERE id = ?")
@DynamicUpdate
class OriginPlaceEntity(
@Column(name = "origin_map_id", nullable = false)
val originMapId: Long,
@Column(name = "origin_map_id", nullable = false, unique = true)
val originMapId: OriginMapId,
@Column(name = "name", length = 255, nullable = false)
var name: String,
@Column(name = "url", nullable = false, length = 255)
val url: String,
@Column(name = "thumbnail_links", nullable = false, length = 255)
@Column(name = "thumbnail_links", columnDefinition = "TEXT")
val thumbnailLinks: String,
@Column(name = "address", length = 255)
val address: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class PlaceEntity(
var name: String,
@Column(name = "url", length = 255)
var url: String?,
@Column(name = "thumbnail_links", length = 255, nullable = false)
@Column(name = "thumbnail_links", columnDefinition = "TEXT")
var thumbnailLinks: String?,
@Column(name = "address", length = 255)
var address: String?,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.piikii.output.persistence.postgresql.persistence.repository

import com.piikii.application.domain.place.OriginMapId
import com.piikii.output.persistence.postgresql.persistence.entity.OriginPlaceEntity
import org.springframework.data.jpa.repository.JpaRepository

interface OriginPlaceRepository : JpaRepository<OriginPlaceEntity, Long>
interface OriginPlaceRepository : JpaRepository<OriginPlaceEntity, Long> {
fun findByOriginMapId(originMapId: OriginMapId): OriginPlaceEntity?
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
package com.piikii.output.web.avocado.adapter

import com.piikii.application.domain.place.OriginMapId
import com.piikii.application.domain.place.OriginPlace
import com.piikii.application.port.output.web.OriginPlaceAutoCompleteClient
import com.piikii.common.exception.ExceptionCode
import com.piikii.common.exception.PiikiiException
import com.piikii.output.web.avocado.parser.AvocadoPlaceIdParserStrategy
import com.piikii.output.web.avocado.parser.AvocadoOriginMapIdParserStrategy
import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient
import org.springframework.web.client.body

@Component
class AvocadoPlaceAutoCompleteClient(
private val avocadoPlaceIdParserStrategy: AvocadoPlaceIdParserStrategy,
private val avocadoOriginMapIdParserStrategy: AvocadoOriginMapIdParserStrategy,
private val avocadoApiClient: RestClient,
) : OriginPlaceAutoCompleteClient {
override fun isAutoCompleteSupportedUrl(url: String): Boolean {
return avocadoPlaceIdParserStrategy.getParserBySupportedUrl(url) != null
return avocadoOriginMapIdParserStrategy.getParserBySupportedUrl(url) != null
}

override fun extractPlaceId(url: String): String {
return avocadoPlaceIdParserStrategy.getParserBySupportedUrl(url)?.parsePlaceId(url)
override fun extractOriginMapId(url: String): OriginMapId {
return avocadoOriginMapIdParserStrategy.getParserBySupportedUrl(url)?.parseOriginMapId(url)
?: throw PiikiiException(ExceptionCode.NOT_SUPPORT_AUTO_COMPLETE_URL)
}

override fun getAutoCompletedPlace(
url: String,
placeId: String,
originMapId: OriginMapId,
): OriginPlace {
return avocadoApiClient.get()
.uri("/$placeId")
.uri("/${originMapId.toId()}")
.retrieve()
.body<AvocadoPlaceInfoResponse>()
?.toOriginPlace(url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.piikii.application.domain.generic.Origin
import com.piikii.application.domain.generic.ThumbnailLinks
import com.piikii.application.domain.place.OriginMapId
import com.piikii.application.domain.place.OriginPlace

@JsonIgnoreProperties(ignoreUnknown = true)
Expand All @@ -27,7 +28,7 @@ data class AvocadoPlaceInfoResponse(
fun toOriginPlace(url: String): OriginPlace {
return OriginPlace(
id = null,
originMapId = id,
originMapId = OriginMapId.of(id = id, origin = Origin.AVOCADO),
name = name,
url = url,
thumbnailLinks = ThumbnailLinks(images ?: emptyList()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ data class AvocadoUrl(
) {
data class Regex(
val web: String,
val mobileWeb: String,
val share: String,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.piikii.output.web.avocado.parser

import com.piikii.application.domain.generic.Origin
import com.piikii.application.domain.place.OriginMapId
import com.piikii.output.web.avocado.config.AvocadoProperties
import com.piikii.output.web.avocado.parser.AvocadoOriginMapIdParser.Companion.ORIGIN_MAP_IP_REGEX
import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient

@Component
class AvocadoOriginMapIdParserStrategy(private val parsers: List<AvocadoOriginMapIdParser>) {
fun getParserBySupportedUrl(url: String): AvocadoOriginMapIdParser? {
return parsers.firstOrNull { it.getParserBySupportedUrl(url) != null }
}
}

interface AvocadoOriginMapIdParser {
fun getParserBySupportedUrl(url: String): AvocadoOriginMapIdParser?

fun parseOriginMapId(url: String): OriginMapId?

/**
* Regex를 이용해 OriginMapId 반환
* - Regex Match 결과로부터 첫 번째 값을 꺼내 OriginMapId 변환 및 반환
*
* @return OriginMapId
*/
fun MatchResult?.parseFromMatchResult(): OriginMapId? {
return this?.groupValues
?.getOrNull(1)
?.toLongOrNull()
?.let { OriginMapId.of(id = it, origin = Origin.AVOCADO) }
}

companion object {
const val ORIGIN_MAP_IP_REGEX = "\\d+"
}
}

@Component
class MapUrlIdParser(properties: AvocadoProperties) : AvocadoOriginMapIdParser {
private val regexes: List<Regex> =
listOf(
"${properties.url.regex.web}($ORIGIN_MAP_IP_REGEX)".toRegex(),
"${properties.url.regex.mobileWeb}($ORIGIN_MAP_IP_REGEX)/home".toRegex(),
)

override fun getParserBySupportedUrl(url: String): AvocadoOriginMapIdParser? =
takeIf { regexes.any { regex -> regex.matches(url) } }

override fun parseOriginMapId(url: String): OriginMapId? {
return regexes.first { it.matches(url) }.find(url).parseFromMatchResult()
}
}

@Component
class ShareUrlIdParser(
properties: AvocadoProperties,
) : AvocadoOriginMapIdParser {
private val regex: Regex = properties.url.regex.share.toRegex()
private val idParameterRegex: Regex = "id=(\\d+)".toRegex()
private val client: RestClient = RestClient.builder().build()

override fun getParserBySupportedUrl(url: String): AvocadoOriginMapIdParser? = takeIf { regex.matches(url) }

override fun parseOriginMapId(url: String): OriginMapId? {
val response =
client.get().uri(url)
.retrieve()
.toEntity(Map::class.java)
if (response.statusCode.is3xxRedirection && response.headers.location != null) {
return idParameterRegex.find(response.headers.location.toString())
.parseFromMatchResult()
}
return null
}
}
Loading

0 comments on commit bb5f701

Please sign in to comment.