Skip to content

Commit

Permalink
feat: 포토북 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
HyungJu committed Aug 10, 2024
1 parent de60a16 commit 96eff82
Show file tree
Hide file tree
Showing 25 changed files with 780 additions and 24 deletions.
2 changes: 1 addition & 1 deletion CONFIG
10 changes: 7 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ dependencies {
implementation("org.hibernate:hibernate-core:6.2.4.Final")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")

implementation("org.asynchttpclient:async-http-client:3.0.0")
}

allOpen {
Expand Down Expand Up @@ -85,11 +87,13 @@ jib {
to {
image = "ggsdh/ggsdh-operate:latest"
container {
jvmFlags = listOf(
jvmFlags =
listOf(
"-Dspring.profiles.active=real",
"-Dserver.port=8080",
"-XX:+UseContainerSupport"
)
"-XX:+UseContainerSupport",
)

ports = listOf("8080")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum class GlobalError(
override val status: HttpStatus,
override val code: String,
) : ErrorCode {
PHOTOBOOK_NOT_FOUND("포토북이 존재하지 않습니다.", HttpStatus.NOT_FOUND, "P_001"),
GLOBAL_NOT_FOUND("리소스가 존재하지 않습니다.", HttpStatus.NOT_FOUND, "G_001"),
INVALID_REQUEST_PARAM("요청 파라미터가 유효하지 않습니다.", HttpStatus.BAD_REQUEST, "G_002"),
INTERNAL_SERVER_ERROR("서버 내부 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR, "G_003"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,30 @@ import org.springframework.stereotype.Component
@Component
class CustomRequestMatcher {
fun authEndpoints(): RequestMatcher =
OrRequestMatcher(
AntPathRequestMatcher("/"), // Actuator Health Checker
AntPathRequestMatcher("/api/v1/dummy"),
AntPathRequestMatcher("/api/v1/trip/onboarding/themes"),
AntPathRequestMatcher("/api/v1/oauth/**/**"), // Oauth Login
AntPathRequestMatcher("/api/v1/actuator"),
AntPathRequestMatcher("/api/v1/member/**/**"),
AntPathRequestMatcher("/api/v1/restaurant"),
AntPathRequestMatcher("/api/v1/ranking"),
AntPathRequestMatcher("/api/v1/lane"),
)
OrRequestMatcher(
AntPathRequestMatcher("/"), // Actuator Health Checker
AntPathRequestMatcher("/api/v1/dummy"),
AntPathRequestMatcher("/api/v1/trip/onboarding/themes"),
AntPathRequestMatcher("/api/v1/oauth/**/**"), // Oauth Login
AntPathRequestMatcher("/api/v1/actuator"),
AntPathRequestMatcher("/api/v1/member/**/**"),
AntPathRequestMatcher("/api/v1/restaurant"),
AntPathRequestMatcher("/api/v1/ranking"),
AntPathRequestMatcher("/api/v1/lane"),
)

fun tempUserEndpoints(): RequestMatcher =
OrRequestMatcher(
AntPathRequestMatcher("/api/v1/trip/onboarding"),
AntPathRequestMatcher("/api/v1/member/signup"),
AntPathRequestMatcher("/api/v1/member/nickname"),
AntPathRequestMatcher("/api/v1/member/"),
)
OrRequestMatcher(
AntPathRequestMatcher("/api/v1/trip/onboarding"),
AntPathRequestMatcher("/api/v1/member/signup"),
AntPathRequestMatcher("/api/v1/member/nickname"),
AntPathRequestMatcher("/api/v1/member/"),
AntPathRequestMatcher("/api/v1/photobook"),
AntPathRequestMatcher("/api/v1/photobook/**"),
)

fun userEndpoints(): RequestMatcher =
OrRequestMatcher(
AntPathRequestMatcher("/api/v1/search/**"),
)
OrRequestMatcher(
AntPathRequestMatcher("/api/v1/search/**"),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.ggsdh.backend.photobook.application

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.ggsdh.backend.photobook.application.dto.ApiResponse
import com.ggsdh.backend.photobook.application.dto.GetLocationInputDto
import com.ggsdh.backend.photobook.domain.Location
import org.asynchttpclient.Dsl.asyncHttpClient
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.util.concurrent.CompletableFuture

@Component
class NaverGcService(
private val objectMapper: ObjectMapper,
@Value("\${naver.gc.client-id}")
private val clientId: String,
@Value("\${naver.gc.client-secret}")
private val clientSecret: String,
) {
fun getLocation(input: GetLocationInputDto): CompletableFuture<Location?> {
if (input.lat == null || input.lon == null) {
return CompletableFuture.completedFuture(null)
}

return asyncHttpClient()
.prepareGet(
"https://naveropenapi.apigw.ntruss.com/map-reversegeocode/v2/gc?coords=${input.lon},${input.lat}&output=json&orders=roadaddr",
).setHeader("X-NCP-APIGW-API-KEY", clientSecret)
.setHeader("X-NCP-APIGW-API-KEY-ID", clientId)
.execute()
.toCompletableFuture()
.thenApply {
val body = it.responseBody
val response = objectMapper.readValue<ApiResponse>(body)

if (response.status.code == 3) {
return@thenApply null
}

val buildingName =
response.results[0]
.land.addition0.value

val city =
response.results[0]
.region.area1.name

if (buildingName == "") {
val roadName =
response.results[0]
.land.name
return@thenApply Location(input.lat, input.lon, "$roadName 근처 어딘가", city)
} else {
return@thenApply Location(input.lat, input.lon, buildingName, city)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.ggsdh.backend.photobook.application

import com.ggsdh.backend.global.exception.error.BusinessException
import com.ggsdh.backend.global.exception.error.GlobalError
import com.ggsdh.backend.photobook.domain.Photo
import com.ggsdh.backend.photobook.domain.PhotoBook
import com.ggsdh.backend.photobook.domain.PhotoBookRepository
import com.ggsdh.backend.photobook.presentation.dto.CreatePhotobookRequest
import jakarta.transaction.Transactional
import org.springframework.stereotype.Component
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.CompletableFuture

@Component
class PhotoBookService(
private val naverGcService: NaverGcService,
private val photoBookRepository: PhotoBookRepository,
) {
fun deletePhotoBook(
memberId: Long,
photobookId: Long,
) = photoBookRepository.delete(memberId, photobookId)

fun getAllPhotoBooks(memberId: Long): List<PhotoBook> = photoBookRepository.getAllByMemberId(memberId)

fun getPhotoBook(
memberId: Long,
photobookId: Long,
): PhotoBook {
val photoBook =
photoBookRepository.find(memberId, photobookId) ?: throw BusinessException(
GlobalError.PHOTOBOOK_NOT_FOUND,
)

return photoBook
}

@Transactional
fun getPhoto(
memberId: Long,
input: CreatePhotobookRequest,
): PhotoBook {
val photos: MutableList<Photo> = mutableListOf()

CompletableFuture
.allOf(
*input.photos
.map { photo ->
naverGcService.getLocation(photo.toGetLocationInputDto()).thenApply {
photos.add(
Photo(
id = -1,
path = photo.path,
location = it,
dateTime = photo.toGetLocationInputDto().date,
),
)
}
}.toTypedArray(),
).join()

photos.filter {
it.location?.city == "경기도"
}

val photoBook =
PhotoBook.create(
id = -1,
input.title,
LocalDateTime.parse(input.startDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")),
LocalDateTime.parse(input.endDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")),
photos,
)
val saved =
photoBookRepository.save(
memberId,
photoBook,
)

return saved
}

fun deletePhoto(
memberId: Long,
photoId: List<Long>,
) = photoBookRepository.deletePhotos(memberId, photoId)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ggsdh.backend.photobook.application.dto

import java.time.LocalDateTime

class GetLocationInputDto(
val path: String,
val lat: Double?,
val lon: Double?, // 0.0
val date: LocalDateTime,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.ggsdh.backend.photobook.application.dto

data class ApiResponse(
val status: Status,
val results: List<Result>,
)

data class Status(
val code: Int,
val name: String,
val message: String,
)

data class Result(
val name: String,
val code: Code,
val region: Region,
val land: Land,
)

data class Code(
val id: String,
val type: String,
val mappingId: String,
)

data class Region(
val area0: Area,
val area1: AreaWithAlias,
val area2: Area,
val area3: Area,
val area4: Area,
)

data class Area(
val name: String,
val coords: Coords,
)

data class AreaWithAlias(
val name: String,
val coords: Coords,
val alias: String,
)

data class Coords(
val center: Center,
)

data class Center(
val crs: String,
val x: Double,
val y: Double,
)

data class Land(
val type: String,
val number1: String,
val number2: String,
val addition0: Addition,
val addition1: Addition,
val addition2: Addition,
val addition3: Addition,
val addition4: Addition,
val name: String,
val coords: Coords,
)

data class Addition(
val type: String,
val value: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.ggsdh.backend.photobook.domain

import java.time.LocalDate

class DailyPhotoGroup(
val day: Int,
val dateTime: LocalDate,
val hourlyPhotoGroups: List<HourlyPhotoGroup>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ggsdh.backend.photobook.domain

data class Location(
val lat: Double,
val lon: Double,
val name: String?,
val city: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.ggsdh.backend.photobook.domain

import java.time.LocalDateTime

// 2024-08-09 23:38:21.000
class HourlyPhotoGroup(
val dateTime: LocalDateTime,
val photos: List<Photo>,
) {
fun getPhotosCount(): Number = photos.size

fun getLocalizedTime(): String {
val prefix = if (dateTime.hour < 12) "오전" else "오후"
return "$prefix ${dateTime.hour % 12}"
}

fun getDominantLocation(): Location? {
val locationMap = mutableMapOf<Location, Int>()
photos.forEach { photo ->
val location = photo.location

if (location != null) {
locationMap[location] = (locationMap[location] ?: 0) + 1
}
}
return locationMap.maxByOrNull { it.value }?.key
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/com/ggsdh/backend/photobook/domain/Photo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ggsdh.backend.photobook.domain

import java.time.LocalDateTime

class Photo(
val id: Long,
val path: String,
val location: Location?,
val dateTime: LocalDateTime,
) {
companion object {
fun create(
path: String,
dateTime: LocalDateTime,
): Photo = Photo(-1, path, null, dateTime)
}
}
Loading

0 comments on commit 96eff82

Please sign in to comment.