Skip to content

Commit

Permalink
Merge pull request #181 from mash-up-kr/fix/url-parsing
Browse files Browse the repository at this point in the history
fix: URL 파싱 정규식 추가, URL 검증 로직 추가
  • Loading branch information
K-Diger authored Sep 4, 2024
2 parents f5e43d9 + 10ca06f commit 656cef8
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 100 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
package com.piikii.application.port.input.dto.request

import com.piikii.common.exception.ExceptionCode
import com.piikii.common.exception.PiikiiException
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.NotBlank
import java.net.URI

data class PlaceAutoCompleteUrlRequest(
@field:NotNull(message = "URL 입력은 필수 입니다.")
@field:NotBlank(message = "URL 입력은 필수 입니다.")
@field:Schema(description = "장소 자동완성을 위한 지도 URL", example = "https://test.com/1231421")
val url: String,
)
var url: String,
) {
init {
val validatedUrl =
Regex(REGEX_PATTERN).find(url)?.value ?: throw PiikiiException(
exceptionCode = ExceptionCode.ILLEGAL_ARGUMENT_EXCEPTION,
detailMessage = "URL이 요청 형식에 맞지 않습니다. : $url",
)
url = URI(validatedUrl).toString()
}

companion object {
private const val REGEX_PATTERN = "(https://\\S+)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@ import com.piikii.application.domain.place.OriginPlace
import com.piikii.application.port.output.web.OriginPlaceAutoCompletePort
import com.piikii.common.exception.ExceptionCode
import com.piikii.common.exception.PiikiiException
import com.piikii.output.web.lemon.parser.LemonOriginMapIdParser
import com.piikii.output.web.lemon.parser.LemonOriginMapIdParserStrategy
import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient
import org.springframework.web.client.body

@Component
class LemonPlaceAutoCompleteAdapter(
private val lemonOriginMapIdParser: LemonOriginMapIdParser,
private val lemonOriginMapIdParserStrategy: LemonOriginMapIdParserStrategy,
private val lemonApiClient: RestClient,
) : OriginPlaceAutoCompletePort {
override fun isAutoCompleteSupportedUrl(url: String): Boolean {
return lemonOriginMapIdParser.isAutoCompleteSupportedUrl(url)
return lemonOriginMapIdParserStrategy.getParserBySupportedUrl(url) != null
}

override fun extractOriginMapId(url: String): OriginMapId {
return lemonOriginMapIdParser.parseOriginMapId(url)
return lemonOriginMapIdParserStrategy.getParserBySupportedUrl(url)?.parseOriginMapId(url)
?: throw PiikiiException(ExceptionCode.NOT_SUPPORT_AUTO_COMPLETE_URL)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import com.piikii.application.domain.place.OriginPlace

@JsonIgnoreProperties(ignoreUnknown = true)
data class LemonPlaceInfoResponse(
val isMapUser: String?,
val isExist: Boolean?,
val basicInfo: BasicInfo,
@JsonProperty("ismapuser") val isMapUser: String?,
@JsonProperty("isexist") val isExist: Boolean?,
@JsonProperty("basicinfo") val basicInfo: BasicInfo?,
val comment: Comment?,
val menuInfo: MenuInfo,
val photo: Photo,
@JsonProperty("menuinfo") val menuInfo: MenuInfo?,
val photo: Photo?,
) {
fun toOriginPlace(url: String): OriginPlace {
val fullAddress = "${basicInfo.address.region.newaddrfullname} ${basicInfo.address.newaddr.newaddrfull}".trim()
requireNotNull(basicInfo) { "BasicInfo is required" }
val fullAddress = "${basicInfo.address.region.newAddrFullName} ${basicInfo.address.newAddr.newAddrFull}".trim()
return OriginPlace(
id = LongTypeId(0L),
name = basicInfo.name,
Expand All @@ -40,83 +41,70 @@ data class LemonPlaceInfoResponse(
@JsonIgnoreProperties(ignoreUnknown = true)
data class BasicInfo(
val cid: Long,
@JsonProperty("placenamefull")
val name: String,
@JsonProperty("mainphotourl")
val mainPhotoUrl: String,
@JsonProperty("phonenum")
val phoneNumber: String?,
@JsonProperty("placenamefull") val name: String,
@JsonProperty("mainphotourl") val mainPhotoUrl: String,
@JsonProperty("phonenum") val phoneNumber: String?,
val address: Address,
val homepage: String?,
val category: Category,
val feedback: Feedback,
val openHour: OpenHour,
@JsonProperty("openhour") val openHour: OpenHour,
val tags: List<String>?,
@JsonProperty("x")
val longitude: Double?,
@JsonProperty("y")
val latitude: Double?,
@JsonProperty("x") val longitude: Double?,
@JsonProperty("y") val latitude: Double?,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class Address(
val newaddr: NewAddress,
@JsonProperty("newaddr") val newAddr: NewAddress,
val region: Region,
val addrbunho: String? = null,
@JsonProperty("addrbunho") val addrBunho: String?,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class NewAddress(
val newaddrfull: String,
val bsizonno: String,
@JsonProperty("newaddrfull") val newAddrFull: String,
@JsonProperty("bsizonno") val bsiZonNo: String,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class Region(
val name3: String,
val fullname: String,
val newaddrfullname: String,
@JsonProperty("fullname") val fullName: String,
@JsonProperty("newaddrfullname") val newAddrFullName: String,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class Category(
@JsonProperty("catename")
val firstCategoryName: String,
@JsonProperty("cate1name")
val secondCategoryName: String,
@JsonProperty("catename") val firstCategoryName: String,
@JsonProperty("cate1name") val secondCategoryName: String,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class Feedback(
@JsonProperty("scoresum")
val sumOfScore: Int,
@JsonProperty("scorecnt")
val countOfScore: Int,
@JsonProperty("blogrvwcnt")
val countOfBlogReview: Int,
@JsonProperty("comntcnt")
val countOfReviewComment: Int,
@JsonProperty("allphotocnt")
val countOfAllPhoto: Int,
@JsonProperty("reviewphotocnt")
val countOfPhotoReview: Int,
@JsonProperty("scoresum") val sumOfScore: Int = 0,
@JsonProperty("scorecnt") val countOfScore: Int = 0,
@JsonProperty("blogrvwcnt") val countOfBlogReview: Int = 0,
@JsonProperty("comntcnt") val countOfReviewComment: Int = 0,
@JsonProperty("allphotocnt") val countOfAllPhoto: Int = 0,
@JsonProperty("reviewphotocnt") val countOfPhotoReview: Int = 0,
) {
fun calculateStarGrade(): Double? = if (countOfScore > 0) sumOfScore.toDouble() / countOfScore else null
}

@JsonIgnoreProperties(ignoreUnknown = true)
data class OpenHour(
val periodList: List<Period>?,
val offdayList: List<Offday>?,
@JsonProperty("periodlist") val periodList: List<Period>?,
@JsonProperty("offdaylist") val offDayList: List<OffDay>?,
) {
fun toPrintFormat(): String? {
val openingHour =
periodList?.first { it.periodName == OPEN_HOUR_PERIOD_NAME }
?.toPrintFormat()
val offdaySchedule =
offdayList?.map { it.toPrintFormat() }
?.joinToString { JOINER }
return if (openingHour == null && offdaySchedule == null) null else "$openingHour$JOINER$offdaySchedule"
val openingHour = periodList?.firstOrNull { it.periodName == OPEN_HOUR_PERIOD_NAME }?.toPrintFormat()
val offDaySchedule = offDayList?.mapNotNull { it.toPrintFormat() }?.joinToString(JOINER)
return if (openingHour == null && offDaySchedule.isNullOrEmpty()) {
null
} else {
"$openingHour$JOINER$offDaySchedule".trim()
}
}

companion object {
Expand All @@ -127,51 +115,55 @@ data class LemonPlaceInfoResponse(

@JsonIgnoreProperties(ignoreUnknown = true)
data class Period(
val periodName: String,
val timeList: List<Time>?,
@JsonProperty("periodname") val periodName: String,
@JsonProperty("timelist") val timeList: List<Time>?,
) {
fun toPrintFormat(): String? {
return timeList?.joinToString(OpenHour.JOINER) { "${it.timeName}: ${it.dayOfWeek} ${it.timeSE}" }
}
fun toPrintFormat(): String? =
timeList?.joinToString(OpenHour.JOINER) {
"${it.timeName}: ${it.dayOfWeek} ${it.timeSE}"
}
}

@JsonIgnoreProperties(ignoreUnknown = true)
data class Time(
val timeName: String,
val timeSE: String,
val dayOfWeek: String,
@JsonProperty("timename") val timeName: String,
@JsonProperty("timese") val timeSE: String,
@JsonProperty("dayofweek") val dayOfWeek: String,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class Offday(
val holidayName: String,
val weekAndDay: String,
val temporaryHolidays: String,
data class OffDay(
@JsonProperty("holidayname") val holidayName: String,
@JsonProperty("weekandday") val weekAndDay: String,
@JsonProperty("temporaryholidays") val temporaryHolidays: String?,
) {
fun toPrintFormat(): String {
return "$holidayName: $weekAndDay"
}
fun toPrintFormat(): String? =
if (holidayName.isNotBlank() && weekAndDay.isNotBlank()) {
"$holidayName: $weekAndDay"
} else {
null
}
}

@JsonIgnoreProperties(ignoreUnknown = true)
data class Comment(
val kamapComntcnt: Int,
val scoresum: Int,
val scorecnt: Int,
@JsonProperty("kamapcomntcnt") val kamapComntCnt: Int = 0,
@JsonProperty("scoresum") val scoreSum: Int = 0,
@JsonProperty("scorecnt") val scoreCnt: Int = 0,
val list: List<CommentItem>?,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class CommentItem(
val contents: String,
val point: Int,
val username: String,
@JsonProperty("username") val userName: String,
val date: String,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class MenuInfo(
val menuList: List<MenuItem>?,
@JsonProperty("menulist") val menuList: List<MenuItem>?,
)

@JsonIgnoreProperties(ignoreUnknown = true)
Expand All @@ -184,13 +176,13 @@ data class LemonPlaceInfoResponse(

@JsonIgnoreProperties(ignoreUnknown = true)
data class Photo(
val photoCount: Int,
val photoList: List<PhotoItem>?,
@JsonProperty("photocount") val photoCount: Int = 0,
@JsonProperty("photolist") val photoList: List<PhotoItem>?,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class PhotoItem(
val photoid: String,
val orgurl: String,
@JsonProperty("photoid") val photoId: String,
@JsonProperty("orgurl") val orgUrl: String,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ data class LemonUrl(
data class Regex(
val web: String,
val mobileWeb: String,
val mobileApp: String,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,63 @@ import com.piikii.application.domain.generic.LongTypeId
import com.piikii.application.domain.place.Origin
import com.piikii.application.domain.place.OriginMapId
import com.piikii.output.web.lemon.config.LemonProperties
import com.piikii.output.web.lemon.parser.LemonOriginMapIdParser.Companion.ANY_REGEX
import com.piikii.output.web.lemon.parser.LemonOriginMapIdParser.Companion.NUMBER_REGEX
import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient

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

fun isAutoCompleteSupportedUrl(url: String): Boolean = regexes.any { it.matches(url) }

fun parseOriginMapId(url: String): OriginMapId? {
return regexes.firstOrNull { it.matches(url) }
?.find(url)
?.groupValues
?.getOrNull(1)
?.toLongOrNull()
?.let { OriginMapId.of(id = LongTypeId(it), origin = Origin.LEMON) }
}
class LemonOriginMapIdParserStrategy(private val parsers: List<LemonOriginMapIdParser>) {
fun getParserBySupportedUrl(url: String): LemonOriginMapIdParser? =
parsers.firstOrNull { it.getParserBySupportedUrl(url) != null }
}

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

fun parseOriginMapId(url: String): OriginMapId?

fun MatchResult?.parseFromMatchResult(): OriginMapId? =
this?.groupValues?.getOrNull(1)?.toLongOrNull()?.let {
OriginMapId.of(id = LongTypeId(it), origin = Origin.LEMON)
}

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

@Component
class WebUrlParser(properties: LemonProperties) : LemonOriginMapIdParser {
private val regex = "${properties.url.regex.web}($NUMBER_REGEX)".toRegex()

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

override fun parseOriginMapId(url: String): OriginMapId? = regex.find(url).parseFromMatchResult()
}

@Component
class MobileWebUrlParser(properties: LemonProperties) : LemonOriginMapIdParser {
private val regex = "${properties.url.regex.mobileWeb}($NUMBER_REGEX)".toRegex()

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

override fun parseOriginMapId(url: String): OriginMapId? = regex.find(url).parseFromMatchResult()
}

@Component
class MobileAppUrlParser(properties: LemonProperties) : LemonOriginMapIdParser {
private val regex = "^${properties.url.regex.mobileApp}($ANY_REGEX)$".toRegex()
private val idParameterRegex = "itemId=(\\d+)".toRegex()
private val client: RestClient = RestClient.builder().build()

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

override fun parseOriginMapId(url: String): OriginMapId? {
val response = client.get().uri(url).retrieve().toEntity(String::class.java)
return response.headers.location?.toString()?.let { location ->
idParameterRegex.find(location).parseFromMatchResult()
}.takeIf { response.statusCode.is3xxRedirection }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ lemon:
regex:
web: ${LEMON_WEB_URL_REGEX}
mobile-web: ${LEMON_MOBILE_WEB_URL_REGEX}
mobile-app: ${LEMON_MOBILE_APP_URL_REGEX}
api: ${LEMON_API_URL}

0 comments on commit 656cef8

Please sign in to comment.