Skip to content
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
Expand Up @@ -50,8 +50,23 @@ class PlaceCongestionController (
return ApiResponse.onSuccess(place)
}

@GetMapping("/place/{placeId}/real-time")
@Operation(summary = "명소 실시간 혼잡도 조회")
@GetMapping("/place/{placeId}/congestions")
@Operation(summary = "명소 혼잡도 조회",
description =
"""
* congestions_by_day : 각 요일별 현재시간 기준 혼잡도
- index [ 0:월, 1:화 ... 5:토, 6:일 ]

* congestions_by_time : 각 요일별 시간별 혼잡도 ( 0~23시 제공 )
- 명소의 모든 요일의 혼잡도 제공
- 총 7개의 배열, 각 배열에는 24개의 혼잡도 정보 담김

* standard_day와 standard_time 활용해서 현재시간 판단 가능합니다.
이거도 마찬가지로 0:월 ~ 6:일

* 현재(실시간) 혼잡도는 real_time_congestion_level 이용하면 됩니다.
"""
)
fun placeRealTimeCongestion(
@PathVariable("placeId") placeId: Long): ApiResponse<PlaceMapResponseDTO.PlaceCongestionDto>{
val congestion = placeCongestionQueryService.getCongestion(placeId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ class PlaceMapResponseDTO {

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
data class PlaceCongestionDto(
val standardDay: Int,
val standardTime: Int,
val realTimeCongestionLevel: Int,
val byTimePercent: List<Float>
val congestionsByDay: List<Float>,
val congestionsByTime: List<List<Float>>
)

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,28 +119,36 @@ class PlaceCongestionQueryService(
@Transactional(readOnly = true)
fun getCongestion(placeId: Long): PlaceMapResponseDTO.PlaceCongestionDto {

val current = LocalDateTime.now()
log.info("현재 시간: ${current}시")

val roundedBase = (current.hour / 3) * 3

// 최근 6개 3시간 단위 시간 생성 (기준시간 포함 총 7개)
val hours = (-3 .. 3).map { i -> (roundedBase - i * 3 + 24) % 24 }
// 현재시간 기준 LocalDateTime
val currentDateTime = LocalDateTime.now()

Comment on lines +123 to +124
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix timezone source: use KST or inject Clock.

LocalDateTime.now() depends on server default TZ; day/hour can drift if infra runs in UTC. Use Asia/Seoul or inject Clock for testability.

-        val currentDateTime = LocalDateTime.now()
+        val currentDateTime = LocalDateTime.now(java.time.ZoneId.of("Asia/Seoul"))

Alternative (preferred for tests):

// ctor
class PlaceCongestionQueryService(..., private val clock: java.time.Clock) { ... }

// usage
val currentDateTime = LocalDateTime.now(clock)
🤖 Prompt for AI Agents
In
src/main/kotlin/busanVibe/busan/domain/place/service/PlaceCongestionQueryService.kt
around lines 123-124, replace the direct call to LocalDateTime.now() (which
relies on server default TZ) with a testable, timezone-correct source: inject a
java.time.Clock into the service constructor and call LocalDateTime.now(clock);
register a Clock bean (e.g., Clock.system(ZoneId.of("Asia/Seoul"))) in your
Spring/Koin config for production and use Clock.fixed(...) in tests so
timestamps are consistently in KST and deterministic for testing.

// 혼잡도 리스트
val congestionsByDay: MutableList<Float> = mutableListOf() // 요일별
val congestionsByTime: MutableList<List<Float>> = mutableListOf() // 시간대별

// 시간대별 혼잡도 ( 월~일, 0~23시 )
for( day in 1..7){
val congestions = mutableListOf<Float>() // 각 요일의 혼잡도 정보 담을 List
for( hour in 0..23){
congestions.add(placeRedisUtil.getTimeCongestion(placeId, day, hour))
}
congestionsByTime.add(congestions)
}

val byTimePercent: List<Float> = hours.map { hour ->
val adjustedDateTime = current.withHour(hour)
.withMinute(0).withSecond(0).withNano(0)
.let {
if (hour > current.hour) it.minusDays(1) else it
}
placeRedisUtil.getTimeCongestion(placeId, adjustedDateTime)
// 요일별 현재 시간의 혼잡도
for( day in 1..7){
congestionsByDay.add(placeRedisUtil.getTimeCongestion(placeId, day, currentDateTime.hour))
}

// DTO 생성 및 반환
return PlaceMapResponseDTO.PlaceCongestionDto(
standardTime = roundedBase,
realTimeCongestionLevel = placeRedisUtil.getTimeCongestion(placeId, current).toInt(),
byTimePercent = byTimePercent
standardDay = currentDateTime.dayOfWeek.value-1, // 원래 1:월~7:일인데, 0:월~6일로 변경해서 반환
standardTime = currentDateTime.hour, // 0~23
realTimeCongestionLevel = placeRedisUtil.getTimeCongestion(placeId).toInt(),
congestionsByDay = congestionsByDay,
congestionsByTime = congestionsByTime
Comment on lines +145 to +149
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Clarify day indexing contract and align all arrays.

standardDay is 0=월..6=일. Ensure congestionsByDay[standardDay] and congestionsByTime[standardDay][standardTime] follow the same zero-based mapping; verify PlaceRedisUtil(day, hour) expects 1..7 or 0..6 to avoid off-by-one.

Run to confirm signatures and usages:


🏁 Script executed:

#!/bin/bash
# Verify PlaceRedisUtil day/hour semantics and DTO usage
rg -nP -C2 'getTimeCongestion\s*\(.*day\s*:\s*Int.*hour\s*:\s*Int' --type=kt
rg -nP -C2 'getTimeCongestion\s*\(.*placeId' --type=kt
rg -nP -C2 'PlaceCongestionDto' --type=kt

Length of output: 80


🏁 Script executed:

#!/bin/bash
# Locate PlaceRedisUtil implementation and inspect getTimeCongestion signature
rg -l 'class PlaceRedisUtil' -g '*.kt'
rg -n 'class PlaceRedisUtil' -g '*.kt' -C3
rg -n 'fun getTimeCongestion' -g '*.kt' -C3

# Locate PlaceCongestionDto and its fields
rg -l 'data class PlaceCongestionDto' -g '*.kt'
rg -n 'PlaceCongestionDto' -g '*.kt' -C3

Length of output: 6165


🏁 Script executed:

#!/bin/bash
sed -n '1,80p' src/main/kotlin/busanVibe/busan/domain/place/util/PlaceRedisUtil.kt

Length of output: 2445


🏁 Script executed:

#!/bin/bash
rg -n 'congestionsByDay' -g '*.kt'
rg -n 'congestionsByTime' -g '*.kt'

Length of output: 1174


Refactor getTimeCongestion(day,hour) to use true weekday semantics
The getTimeCongestion(placeId, day, hour) overload treats its day: Int (1=Monday…7=Sunday) as a day-of-month when calling LocalDateTime.of(year, month, day, hour, …), so the resulting dateTime.dayOfWeek may not match the requested weekday. Replace this with logic that sets the date to the desired weekday (for example, using LocalDate.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.of(day))).atTime(hour, dateTime.minute)) so that both congestionsByDay[standardDay] and congestionsByTime[standardDay][standardTime] pull from the correct Redis key.
PlaceRedisUtil.kt:24–32

)

}

@Transactional(readOnly = false)
Expand Down
71 changes: 41 additions & 30 deletions src/main/kotlin/busanVibe/busan/domain/place/util/PlaceRedisUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,46 +13,55 @@ class PlaceRedisUtil(

private val log = LoggerFactory.getLogger("busanVibe.busan.domain.place")

// 임의로 혼잡도 생성하여 반환. 레디스 키 값으로 저장함.
// fun getRedisCongestion(placeId: Long?): Int{
//
// val key = "place:congestion:$placeId"
// val randomCongestion = getRandomCongestion().toInt().toString()
//
// redisTemplate.opsForValue()
// .set(key, randomCongestion)
//
// return Integer.parseInt(randomCongestion)
// }
// 현재 시간 기준 혼잡도 구하는 메서드
// 현재에 해당하는 요일과 시간의 혼잡도 반환
fun getTimeCongestion(placeId: Long?):Float{
return getTimeCongestion(placeId, LocalDateTime.now())
}

/**
* [요일(1=Mon..7=Sun), 시간]으로 혼잡도 구하는 메서드
* @param placeId 조회 대상 장소 ID
* @param dayOfWeek 1..7 (1=월요일, 7=일요일)
* @param hour 0..23
*/
fun getTimeCongestion(placeId: Long?, day: Int, hour: Int): Float{
val now = LocalDateTime.now()
return getTimeCongestion(
placeId,
LocalDateTime.of(
now.year,
now.month,
day,
hour,
now.minute
)
)
}

// 지정 시간 혼잡도 조회
// null이면 현재시간 기준
fun getTimeCongestion(placeId: Long?, dateTime: LocalDateTime?): Float {

// DateTime
val dateTime = dateTime ?: LocalDateTime.now()

val roundedHour = (dateTime.hour / 3) * 3
val key = "place:congestion:$placeId-${dateTime.year}-${dateTime.monthValue}-${dateTime.dayOfMonth}-$roundedHour"

val key = getCongestionRedisKey(placeId, dateTime)
val value = redisTemplate.opsForValue().get(key)

return (if (value != null) {
value.toFloatOrNull() ?: 0
} else {
setPlaceTimeCongestion(placeId, dateTime.withHour(roundedHour))
val newValue = redisTemplate.opsForValue().get(key)
newValue?.toFloatOrNull() ?: 0
}) as Float
}
val parsed = value?.toFloatOrNull()

fun getTimeCongestion(placeId: Long?):Float{
return getTimeCongestion(placeId, LocalDateTime.now())
// 값이 존재하교 유효하다면
if (parsed != null)return parsed // 그대로 반환

// 값이 없다면
setPlaceTimeCongestion(placeId, dateTime) // 새로 만들어서 할당
val newValue = redisTemplate.opsForValue().get(key)?.toFloatOrNull() // 새로 설정한 값 조회
return newValue ?: 0f // 반환, 값이 이상하다면 0 반환
}

// 시간 혼잡도 설정
private fun setPlaceTimeCongestion(placeId: Long?, dateTime: LocalDateTime) {
val roundedHour = (dateTime.hour / 3) * 3
val key = "place:congestion:$placeId-${dateTime.year}-${dateTime.monthValue}-${dateTime.dayOfMonth}-$roundedHour"
val key = getCongestionRedisKey(placeId, dateTime)
val congestion = getRandomCongestion().toString()
val success = redisTemplate.opsForValue().setIfAbsent(key, congestion, Duration.ofHours(24))

Expand All @@ -65,9 +74,11 @@ class PlaceRedisUtil(
}

// 혼잡도 생성 (1.0 ~ 5.0 사이의 Float)
private fun getRandomCongestion(): Float {
return (Math.random() * 4 + 1).toFloat()
}
private fun getRandomCongestion(): Float = (Math.random() * 4 + 1).toFloat()


// redis에 저장할 key 생성
private fun getCongestionRedisKey(placeId: Long?, dateTime: LocalDateTime): String
= "place:congestion:${placeId}-${dateTime.dayOfWeek}-${dateTime.hour}"

}