Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.

Commit c4449da

Browse files
committed
ASAP-456 비회원 이미지 업로드 로직 추가 및 테스트 보완
- `userId`를 nullable로 변경하여 비회원 이미지 업로드 지원. - 비회원 업로드 시 기본 ID로 "anonymous" 설정. - `ImageCommandServiceTest` 및 Controller 테스트에 관련 시나리오 추가. - API 문서 변경: 액세스 토큰 필드를 선택사항으로 수정하고 설명 추가.
1 parent 7f895d3 commit c4449da

9 files changed

Lines changed: 113 additions & 34 deletions

File tree

Application-Module/src/main/kotlin/com/asap/application/image/port/in/UploadImageUsecase.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ interface UploadImageUsecase {
99

1010
data class Command(
1111
val image: FileMetaData,
12-
val userId: String
12+
val userId: String? = null
1313
)
1414

1515
data class Response(
1616
val imageUrl: String
1717
)
18-
}
18+
}

Application-Module/src/main/kotlin/com/asap/application/image/service/ImageCommandService.kt

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,32 @@ import org.springframework.stereotype.Service
1010
@Service
1111
class ImageCommandService(
1212
private val imageManagementPort: ImageManagementPort,
13-
private val userManagementPort: UserManagementPort
13+
private val userManagementPort: UserManagementPort,
1414
) : UploadImageUsecase {
15+
companion object {
16+
private const val ANONYMOUS_OWNER_ID = "anonymous"
17+
}
18+
1519
override fun upload(command: UploadImageUsecase.Command): UploadImageUsecase.Response {
16-
val user = userManagementPort.getUserNotNull(DomainId(command.userId))
20+
val owner =
21+
when {
22+
command.userId != null -> {
23+
val user = userManagementPort.getUserNotNull(DomainId(command.userId))
24+
user.id.value
25+
}
1726

18-
val uploadedImage = imageManagementPort.save(
19-
ImageMetadata(
20-
owner = user.id.value,
21-
fileMetaData = command.image
27+
else -> ANONYMOUS_OWNER_ID
28+
}
29+
30+
val uploadedImage =
31+
imageManagementPort.save(
32+
ImageMetadata(
33+
owner = owner,
34+
fileMetaData = command.image,
35+
),
2236
)
23-
)
2437
return UploadImageUsecase.Response(
25-
imageUrl = uploadedImage.imageUrl
38+
imageUrl = uploadedImage.imageUrl,
2639
)
2740
}
28-
}
41+
}

Application-Module/src/test/kotlin/com/asap/application/image/service/ImageCommandServiceTest.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ package com.asap.application.image.service
22

33
import com.asap.application.image.port.`in`.UploadImageUsecase
44
import com.asap.application.image.port.out.ImageManagementPort
5+
import com.asap.application.image.vo.ImageMetadata
56
import com.asap.application.image.vo.UploadedImage
67
import com.asap.application.user.port.out.UserManagementPort
78
import com.asap.common.file.FileMetaData
89
import com.asap.domain.UserFixture
910
import io.kotest.core.spec.style.BehaviorSpec
1011
import io.kotest.matchers.nulls.shouldNotBeNull
12+
import io.kotest.matchers.shouldBe
1113
import io.mockk.every
1214
import io.mockk.mockk
15+
import io.mockk.slot
16+
import io.mockk.verify
1317
import java.io.InputStream
1418

1519
class ImageCommandServiceTest :
@@ -24,6 +28,38 @@ class ImageCommandServiceTest :
2428
mockUserManagementPort,
2529
)
2630

31+
given("익명 사용자의 이미지 업로드 요청이 들어올 때") {
32+
val command =
33+
UploadImageUsecase.Command(
34+
userId = null,
35+
image =
36+
FileMetaData(
37+
name = "name",
38+
contentType = "contentType",
39+
size = 1L,
40+
inputStream = InputStream.nullInputStream(),
41+
),
42+
)
43+
val imageMetadataSlot = slot<ImageMetadata>()
44+
every {
45+
mockImageManagementPort.save(capture(imageMetadataSlot))
46+
} returns
47+
UploadedImage(
48+
imageUrl = "anonymousImageUrl",
49+
)
50+
`when`("이미지 업로드 요청을 처리하면") {
51+
val response = imageCommandService.upload(command)
52+
then("이미지가 저장되어야 한다") {
53+
response.imageUrl shouldNotBeNull {
54+
this.isNotBlank()
55+
this.isNotEmpty()
56+
}
57+
verify { mockImageManagementPort.save(any()) }
58+
imageMetadataSlot.captured.owner shouldBe "anonymous"
59+
}
60+
}
61+
}
62+
2763
given("이미지 업로드 요청이 들어올 때") {
2864
val mockUser = UserFixture.createUser()
2965
val command =

Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/config/WebConfig.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
88

99
@Configuration
1010
class WebConfig(
11-
private val accessUserArgumentResolver: AccessUserArgumentResolver
11+
private val accessUserArgumentResolver: AccessUserArgumentResolver,
1212
) : WebMvcConfigurer {
13-
1413
override fun addCorsMappings(registry: CorsRegistry) {
15-
registry.addMapping("/**")
14+
registry
15+
.addMapping("/**")
1616
.allowedOrigins("*")
1717
.allowedMethods("GET", "POST", "PUT", "DELETE")
1818
.allowedHeaders("*")
@@ -21,4 +21,4 @@ class WebConfig(
2121
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
2222
resolvers.add(accessUserArgumentResolver)
2323
}
24-
}
24+
}

Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/security/annotation/AccessUserArgumentResolver.kt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,17 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver
1010
import org.springframework.web.method.support.ModelAndViewContainer
1111

1212
@Component
13-
class AccessUserArgumentResolver: HandlerMethodArgumentResolver {
14-
15-
override fun supportsParameter(parameter: MethodParameter): Boolean {
16-
return parameter.hasParameterAnnotation(AccessUser::class.java)
17-
}
13+
class AccessUserArgumentResolver : HandlerMethodArgumentResolver {
14+
override fun supportsParameter(parameter: MethodParameter): Boolean = parameter.hasParameterAnnotation(AccessUser::class.java)
1815

1916
override fun resolveArgument(
2017
parameter: MethodParameter,
2118
mavContainer: ModelAndViewContainer?,
2219
webRequest: NativeWebRequest,
23-
binderFactory: WebDataBinderFactory?
20+
binderFactory: WebDataBinderFactory?,
2421
): Any? {
25-
val userAuthentication = SecurityContextHolder.getContext().getAuthentication() as UserAuthentication
26-
val userId = userAuthentication.getDetails()
27-
return userId
22+
val authentication = SecurityContextHolder.getContext()?.getAuthentication()
23+
val userAuthentication = authentication as? UserAuthentication
24+
return userAuthentication?.getDetails()
2825
}
29-
}
26+
}

Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/image/api/ImageApi.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import org.springframework.web.multipart.MultipartFile
1717
interface ImageApi {
1818
@Operation(
1919
summary = "이미지 업로드",
20-
description = "이미지를 업로드합니다.",
20+
description = "이미지를 업로드합니다. 회원과 비회원 모두 이용 가능합니다.",
2121
)
2222
@PostMapping(consumes = ["multipart/form-data"])
2323
@ApiResponses(
@@ -28,8 +28,8 @@ interface ImageApi {
2828
headers = [
2929
Header(
3030
name = "Authorization",
31-
description = "액세스 토큰",
32-
required = true,
31+
description = "액세스 토큰 (선택사항)",
32+
required = false,
3333
),
3434
],
3535
),
@@ -41,6 +41,6 @@ interface ImageApi {
4141
)
4242
fun uploadImage(
4343
@RequestPart image: MultipartFile,
44-
@AccessUser userId: String,
44+
@AccessUser userId: String?,
4545
): UploadImageResponse
4646
}

Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/image/controller/ImageController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class ImageController(
1414
) : ImageApi {
1515
override fun uploadImage(
1616
image: MultipartFile,
17-
userId: String,
17+
userId: String?,
1818
): UploadImageResponse {
1919
val response =
2020
uploadImageUsecase.upload(

Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/image/controller/ImageControllerTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,39 @@ class ImageControllerTest : AcceptanceSupporter() {
5454
}
5555
}
5656
}
57+
58+
@Test
59+
fun uploadImageWithoutAuthentication() {
60+
// given
61+
val mockFile = MockMultipartFile("image", "test.jpg", "image/jpeg", "test".toByteArray())
62+
val mockFileMetaData = FileMetaData("test.jpg", 4, "image/jpeg", mockFile.inputStream)
63+
BDDMockito
64+
.given(fileConverter.convert(mockFile))
65+
.willReturn(mockFileMetaData)
66+
BDDMockito
67+
.given(
68+
uploadImageUsecase.upload(
69+
UploadImageUsecase.Command(
70+
image = mockFileMetaData,
71+
userId = null,
72+
),
73+
),
74+
).willReturn(UploadImageUsecase.Response("imageUrl"))
75+
// when
76+
val response =
77+
mockMvc.multipart("/api/v1/images") {
78+
file(mockFile)
79+
contentType = MediaType.MULTIPART_FORM_DATA
80+
// No Authorization header
81+
}
82+
// then
83+
response.andExpect {
84+
status { isOk() }
85+
jsonPath("$.imageUrl") {
86+
exists()
87+
isString()
88+
isNotEmpty()
89+
}
90+
}
91+
}
5792
}

Common-Module/src/main/kotlin/com/asap/common/security/SecurityContextHolder.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ class SecurityContextHolder {
44
companion object {
55
private val contextHolder = ThreadLocal<SecurityContext<*, *>>()
66

7-
fun getContext(): SecurityContext<*, *> {
8-
return contextHolder.get()
9-
}
7+
fun getContext(): SecurityContext<*, *>? = contextHolder.get()
108

119
fun setContext(context: SecurityContext<*, *>) {
1210
contextHolder.set(context)
@@ -16,4 +14,4 @@ class SecurityContextHolder {
1614
contextHolder.remove()
1715
}
1816
}
19-
}
17+
}

0 commit comments

Comments
 (0)