Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 장소 입력자동화 정보 DB 저장 #110

Merged
merged 16 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
KimDoubleB marked this conversation as resolved.
Show resolved Hide resolved
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,
thguss marked this conversation as resolved.
Show resolved Hide resolved
@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
*/
thguss marked this conversation as resolved.
Show resolved Hide resolved
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