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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ client/captures
.cxx
local.properties
client/app/release/
client/app/google-services.json

HELP.md
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
server/.env

### STS ###
.apt_generated
Expand Down
7 changes: 6 additions & 1 deletion server/.envsample
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
DB_URL=
DB_NAME=
DB_USERNAME=
DB_PASSWORD=
DB_PASSWORD=

NAVER_S3_ACCESS_KEY=
NAVER_S3_SECRET_KEY=
NAVER_S3_BUCKET=
4 changes: 4 additions & 0 deletions server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")

// Naver, Storage
implementation("software.amazon.awssdk:s3:2.25.60")
implementation("software.amazon.awssdk:auth:2.25.60")
}

kotlin {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.andone.memorip.common.config

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.S3Configuration
import java.net.URI

@Configuration
class StorageConfig {

@Bean
fun s3Client(
@Value("\${naver.s3.access-key}") accessKey: String,
@Value("\${naver.s3.secret-key}") secretKey: String,
@Value("\${naver.s3.endpoint}") endpoint: String,
@Value("\${naver.s3.region}") region: String
): S3Client {
val credentials = AwsBasicCredentials.create(accessKey, secretKey)

return S3Client.builder()
.endpointOverride(URI.create(endpoint))
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.serviceConfiguration(
S3Configuration.builder()
.pathStyleAccessEnabled(true)
.build()
)
.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ class GlobalExceptionHandler {
.body(ApiResult.error(code.name, code.message))
}

@ExceptionHandler(IllegalArgumentException::class)
fun handleIllegalArgumentException(
e: IllegalArgumentException
): ResponseEntity<ApiResult<Nothing>> {
val code = CommonExceptionCode.INVALID_PARAMETER
return ResponseEntity.status(code.status)
.body(ApiResult.error(code.name, e.message ?: code.message))
}


@ExceptionHandler(Exception::class)
fun handleException(e: Exception): ResponseEntity<ApiResult<Nothing>> {
logger.error("예상치 못한 예외 발생", e)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.andone.memorip.domain.place.controller

import com.andone.memorip.common.response.ApiResult
import com.andone.memorip.domain.place.dto.UploadPlaceImageResponse
import com.andone.memorip.domain.place.service.PlaceImageService
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile

@RestController
@RequestMapping("/api/places/images")
class PlaceImageController(
private val placeImageService: PlaceImageService
) {

@PostMapping
fun upload(
@RequestPart file: MultipartFile
): ApiResult<UploadPlaceImageResponse> {
return ApiResult.success(
UploadPlaceImageResponse(
url = placeImageService.upload(file)
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.andone.memorip.domain.place.dto

data class UploadPlaceImageResponse(
val url: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.andone.memorip.domain.place.service

import com.andone.memorip.infra.storage.StorageService
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile

@Service
class PlaceImageService(
private val storageService: StorageService
) {
private val MAX_FILE_SIZE = 5 * 1024 * 1024

fun upload(file: MultipartFile): String {

require(file.size <= MAX_FILE_SIZE) {
"이미지 용량은 5MB를 초과할 수 없습니다."
}

return storageService.upload(
file = file,
dir = "place"
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.andone.memorip.infra.storage

import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import software.amazon.awssdk.services.s3.S3Client
import org.springframework.web.multipart.MultipartFile
import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import java.util.UUID

@Service
class StorageService(
private val s3Client: S3Client,
@Value("\${naver.s3.bucket}") private val bucket: String
) {
fun check() {
s3Client.headBucket { it.bucket(bucket) }
}

fun upload(
file: MultipartFile,
dir: String
): String {

require(value = !file.isEmpty) { "빈 파일입니다" }

val extension = file.originalFilename
?.substringAfterLast('.', "")
?.lowercase()
?: throw IllegalArgumentException("파일 확장자가 없습니다")

val key = "$dir/${UUID.randomUUID()}.$extension"

s3Client.putObject(
PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType(file.contentType)
.contentLength(file.size)
.build(),
RequestBody.fromInputStream(file.inputStream, file.size)
)

return s3Client.utilities()
.getUrl { it.bucket(bucket).key(key) }
.toExternalForm()
}
}
17 changes: 17 additions & 0 deletions server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ spring:
application:
name: memorip

servlet:
multipart:
max-file-size: 5MB
max-request-size: 5MB

# Database
datasource:
url: ${DB_URL:jdbc:postgresql://localhost:5432/memorip}
Expand All @@ -22,3 +27,15 @@ spring:
# Jackson (JSON null 필드 제외)
jackson:
default-property-inclusion: non_null



# Naver/Object Storage
naver:
s3:
access-key: ${NAVER_S3_ACCESS_KEY}
secret-key: ${NAVER_S3_SECRET_KEY}
bucket: ${NAVER_S3_BUCKET}
endpoint: https://kr.object.ncloudstorage.com
region: kr-standard