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

폐업 추정 장소 크롤링 구현 #391

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions app-server/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ coroutineVersion=1.6.4
jUnitJupiterVersion=5.9.1
wireVersion=4.4.1
geoToolsVersion=26.5
jtsVersion=1.19.0
proj4jVersion=1.3.0
postgresqlVersion=42.5.0
flywayVersion=9.4.0
kspVersion=1.8.20-1.0.10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dependencies {
implementation(projects.apiSpecification.domainEvent)
implementation("io.github.microutils:kotlin-logging-jvm:$kotlinLoggingVersion")

val jtsVersion: String by project
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.locationtech.jts:jts-core:1.19.0")
implementation("org.locationtech.jts:jts-core:$jtsVersion")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package club.staircrusher.place.application.port.`in`

import club.staircrusher.place.application.port.out.persistence.ClosedPlaceCandidateRepository
import club.staircrusher.place.application.port.out.web.OpenDataService
import club.staircrusher.place.domain.model.ClosedPlaceCandidate
import club.staircrusher.stdlib.persistence.TransactionManager
import org.springframework.stereotype.Service
import java.util.UUID

@Service
class CreateClosedPlaceCandidatesUseCase(
private val transactionManager: TransactionManager,
private val closedPlaceCandidateRepository: ClosedPlaceCandidateRepository,
private val placeApplicationService: PlaceApplicationService,
private val openDataService: OpenDataService,
) {
fun handle() {
val closedPlacesFromOpenData = openDataService.getClosedPlaces()

val closedPlaceCandidates = closedPlacesFromOpenData.mapNotNull { closedPlace ->
val nearbyPlaces = placeApplicationService.searchPlacesInCircle(closedPlace.location, SEARCH_RADIUS)
Zeniuus marked this conversation as resolved.
Show resolved Hide resolved
if (nearbyPlaces.isEmpty()) return@mapNotNull null

val similarPlace = nearbyPlaces
.minByOrNull { StringSimilarityComparator.getSimilarity(it.name, closedPlace.name) }
?: return@mapNotNull null

ClosedPlaceCandidate(
Zeniuus marked this conversation as resolved.
Show resolved Hide resolved
id = UUID.randomUUID().toString(),
placeId = similarPlace.id,
)
}

transactionManager.doInTransaction {
closedPlaceCandidateRepository.saveAll(closedPlaceCandidates)
}
}

companion object {
private const val SEARCH_RADIUS = 10
Zeniuus marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package club.staircrusher.place.application.port.`in`

import kotlin.math.max
import kotlin.math.min

@Suppress("ComplexMethod", "NestedBlockDepth")
object StringSimilarityComparator {
private const val N: Int = 2

/**
* https://github.com/tdebatty/java-string-similarity/blob/master/src/main/java/info/debatty/java/stringsimilarity/NGram.java
* Compute n-gram distance.
* @param s0 The first string to compare.
* @param s1 The second string to compare.
* @return The computed n-gram distance in the range [0, 1]
*/
fun getSimilarity(s0: String, s1: String): Double {
if (s0 == s1) {
return 0.0
}

val special = '\n'
val sl = s0.length
val tl = s1.length

if (sl == 0 || tl == 0) {
return 1.0
}

var cost = 0
if (sl < N || tl < N) {
var i = 0
val ni = min(sl.toDouble(), tl.toDouble()).toInt()
while (i < ni) {
if (s0[i] == s1[i]) {
cost++
}
i++
}
return cost.toFloat() / max(sl.toDouble(), tl.toDouble())
}

val sa = CharArray(sl + N - 1)
var p: FloatArray //'previous' cost array, horizontally
var d: FloatArray // cost array, horizontally
var d2: FloatArray //placeholder to assist in swapping p and d

//construct sa with prefix
for (i in sa.indices) {
if (i < N - 1) {
sa[i] = special //add prefix
} else {
sa[i] = s0[i - N + 1]
}
}
p = FloatArray(sl + 1)
d = FloatArray(sl + 1)

// indexes into strings s and t
var i: Int // iterates through source

var t_j = CharArray(N) // jth n-gram of t

i = 0
while (i <= sl) {
p[i] = i.toFloat()
i++
}

var j = 1 // iterates through target
while (j <= tl) {
//construct t_j n-gram
if (j < N) {
for (ti in 0 until N - j) {
t_j[ti] = special //add prefix
}
for (ti in N - j until N) {
t_j[ti] = s1[ti - (N - j)]
}
} else {
t_j = s1.substring(j - N, j).toCharArray()
}
d[0] = j.toFloat()
i = 1
while (i <= sl) {
cost = 0
var tn = N
//compare sa to t_j
for (ni in 0 until N) {
if (sa[i - 1 + ni] != t_j[ni]) {
cost++
} else if (sa[i - 1 + ni] == special) {
//discount matches on prefix
tn--
}
}
val ec = cost.toFloat() / tn
// minimum of cell to the left+1, to the top+1,
// diagonally left and up +cost
d[i] = min(
min((d[i - 1] + 1).toDouble(), (p[i] + 1).toDouble()), (p[i - 1] + ec).toDouble()
)
.toFloat()
i++
}
// copy current distance counts to 'previous row' distance counts
d2 = p
p = d
d = d2
j++
}

// our last action in the above loop was to switch d and p, so p now
// actually has the most recent cost counts
return p[sl] / max(tl.toDouble(), sl.toDouble())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package club.staircrusher.place.application.port.out.persistence

import club.staircrusher.place.domain.model.ClosedPlaceCandidate
import org.springframework.data.repository.CrudRepository

interface ClosedPlaceCandidateRepository : CrudRepository<ClosedPlaceCandidate, String>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package club.staircrusher.place.application.port.out.web

import club.staircrusher.place.application.result.ClosedPlaceResult

interface OpenDataService {
fun getClosedPlaces(): List<ClosedPlaceResult>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package club.staircrusher.place.application.result

import club.staircrusher.stdlib.geography.Location
import java.time.LocalDate

data class ClosedPlaceResult(
val name: String,
val postalCode: String,
val location: Location,
val phoneNumber: String?,
val closedDate: LocalDate,
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
dependencies {
val jtsVersion: String by project
implementation("org.hibernate:hibernate-spatial:6.1.7.Final")
implementation("org.locationtech.jts:jts-core:1.19.0")
implementation("org.locationtech.jts:jts-core:$jtsVersion")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package club.staircrusher.place.domain.model

import club.staircrusher.stdlib.persistence.jpa.TimeAuditingBaseEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import java.time.Instant

@Entity
class ClosedPlaceCandidate(
@Id
val id: String,

@Column(nullable = false)
val placeId: String,

@Column(nullable = true)
var acceptedAt: Instant? = null,

@Column(nullable = true)
var ignoredAt: Instant? = null,
) : TimeAuditingBaseEntity() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as ClosedPlaceCandidate

return id == other.id
}

override fun hashCode(): Int {
return id.hashCode()
}

override fun toString(): String {
return "ClosedPlaceCandidate(id='$id', placeId='$placeId', createdAt=$createdAt, updatedAt=$updatedAt)"
Zeniuus marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package club.staircrusher.place.domain.model

import club.staircrusher.stdlib.geography.Location
import club.staircrusher.stdlib.persistence.jpa.TimeAuditingBaseEntity
import club.staircrusher.stdlib.place.PlaceCategory
import jakarta.persistence.AttributeOverride
import jakarta.persistence.AttributeOverrides
Expand Down Expand Up @@ -37,7 +38,7 @@ class Place private constructor(
val category: PlaceCategory? = null,
isClosed: Boolean,
isNotAccessible: Boolean,
) {
) : TimeAuditingBaseEntity() {
val address: BuildingAddress
// FIXME
get() = building.address
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies {

implementation(projects.crossCuttingConcern.stdlib)
implementation(projects.crossCuttingConcern.infra.persistenceModel)
implementation(projects.crossCuttingConcern.infra.network)
implementation(projects.apiSpecification.domainEvent)

implementation("org.springframework.boot:spring-boot-starter-web")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package club.staircrusher.place.infra.adapter.`in`.controller

import club.staircrusher.place.application.port.`in`.CreateClosedPlaceCandidatesUseCase
import jakarta.servlet.http.HttpServletRequest
import org.springframework.security.web.util.matcher.IpAddressMatcher
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class PlaceController(
private val createClosedPlaceCandidatesUseCase: CreateClosedPlaceCandidatesUseCase,
) {
@PostMapping("/createClosedPlaceCandidates")
fun createClosedPlaceCandidates(request: HttpServletRequest) {
val clusterIpAddressMatcher = IpAddressMatcher("10.42.0.0/16")
val localIpAddressMatcher = IpAddressMatcher("127.0.0.1/32")
Zeniuus marked this conversation as resolved.
Show resolved Hide resolved
if (
!clusterIpAddressMatcher.matches(request)
&& !localIpAddressMatcher.matches(request)
) {
throw IllegalArgumentException("Unauthorized")
}
createClosedPlaceCandidatesUseCase.handle()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package club.staircrusher.place.infra.adapter.out.web

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties("scc.open-data")
data class GovernmentOpenDataProperties(
val apiKey: String,
)
Loading
Loading