Skip to content

Commit a1c0c54

Browse files
authored
[YS-310] feat: 실험 공고 수정 시, 입력 자동완성 지원을 위한 조회 API 구현 (#97)
* feat: add GetExperimentPostDetailForUpdateUseCase logic * test: add GetExperimentPostDetailForUpdateUseCase test code * refactor: rename mapper class for consistency
1 parent 3694925 commit a1c0c54

File tree

5 files changed

+322
-0
lines changed

5 files changed

+322
-0
lines changed

src/main/kotlin/com/dobby/backend/application/service/ExperimentPostService.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class ExperimentPostService(
2828
private val getExperimentPostApplyMethodUseCase: GetExperimentPostApplyMethodUseCase,
2929
private val generateExperimentPostPreSignedUrlUseCase: GenerateExperimentPostPreSignedUrlUseCase,
3030
private val updateExpiredExperimentPostUseCase: UpdateExpiredExperimentPostUseCase,
31+
private val getExperimentPostForUpdateUseCase: GetExperimentPostDetailForUpdateUseCase,
3132
private val deleteExperimentPostUseCase: DeleteExperimentPostUseCase,
3233
private val updateExperimentPostRecruitStatusUseCase: UpdateExperimentPostRecruitStatusUseCase,
3334
private val getExperimentPostTotalCountByCustomFilterUseCase: GetExperimentPostTotalCountByCustomFilterUseCase,
@@ -47,6 +48,11 @@ class ExperimentPostService(
4748
return updateExperimentPostUseCase.execute(input)
4849
}
4950

51+
@Transactional
52+
fun getExperimentPostDetailForUpdate(input: GetExperimentPostDetailForUpdateUseCase.Input): GetExperimentPostDetailForUpdateUseCase.Output {
53+
return getExperimentPostForUpdateUseCase.execute(input)
54+
}
55+
5056
@Transactional
5157
fun getExperimentPosts(input: GetExperimentPostsUseCase.Input): List<GetExperimentPostsUseCase.Output> {
5258
validateFilter(input)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.dobby.backend.application.usecase.experiment
2+
3+
import com.dobby.backend.application.usecase.UseCase
4+
import com.dobby.backend.domain.exception.ExperimentPostNotFoundException
5+
import com.dobby.backend.domain.gateway.experiment.ExperimentPostGateway
6+
import com.dobby.backend.domain.model.experiment.ExperimentPost
7+
import com.dobby.backend.domain.model.experiment.TargetGroup
8+
import com.dobby.backend.infrastructure.database.entity.enums.MatchType
9+
import com.dobby.backend.infrastructure.database.entity.enums.areaInfo.Area
10+
import com.dobby.backend.infrastructure.database.entity.enums.areaInfo.Region
11+
import com.dobby.backend.infrastructure.database.entity.enums.experiment.TimeSlot
12+
import com.dobby.backend.infrastructure.database.entity.enums.member.GenderType
13+
import java.time.LocalDate
14+
15+
class GetExperimentPostDetailForUpdateUseCase(
16+
private val experimentPostGateway: ExperimentPostGateway
17+
) : UseCase<GetExperimentPostDetailForUpdateUseCase.Input, GetExperimentPostDetailForUpdateUseCase.Output> {
18+
19+
data class Input(
20+
val experimentPostId: String,
21+
val memberId: String
22+
)
23+
24+
data class Output(
25+
val experimentPostDetail: ExperimentPostDetailForUpdate
26+
)
27+
28+
data class ExperimentPostDetailForUpdate(
29+
val experimentPostId: String,
30+
val title: String,
31+
val uploadDate: LocalDate,
32+
val uploaderName: String,
33+
val views: Int,
34+
val recruitStatus: Boolean,
35+
val summary: Summary,
36+
val targetGroup: TargetGroup,
37+
val address: Address,
38+
val content: String,
39+
val imageList: List<String>,
40+
val isAuthor: Boolean,
41+
val isUploaderActive: Boolean,
42+
val alarmAgree: Boolean
43+
) {
44+
data class Summary(
45+
val startDate: LocalDate?,
46+
val endDate: LocalDate?,
47+
val leadResearcher: String,
48+
val matchType: MatchType,
49+
val reward: String,
50+
val count: Int?,
51+
val timeRequired: TimeSlot?
52+
)
53+
54+
data class TargetGroup(
55+
val startAge: Int?,
56+
val endAge: Int?,
57+
val genderType: GenderType,
58+
val otherCondition: String?
59+
)
60+
61+
data class Address(
62+
val univName: String?,
63+
val region: Region?,
64+
val area: Area?,
65+
val detailedAddress: String?
66+
)
67+
}
68+
69+
override fun execute(input: Input): Output {
70+
val post = experimentPostGateway.findExperimentPostByMemberIdAndPostId(
71+
memberId = input.memberId,
72+
postId = input.experimentPostId
73+
) ?: throw ExperimentPostNotFoundException
74+
75+
return Output(
76+
experimentPostDetail = post.toExperimentPostDetailForUpdate(input.memberId)
77+
)
78+
}
79+
}
80+
81+
fun ExperimentPost.toExperimentPostDetailForUpdate(memberId: String): GetExperimentPostDetailForUpdateUseCase.ExperimentPostDetailForUpdate {
82+
return GetExperimentPostDetailForUpdateUseCase.ExperimentPostDetailForUpdate(
83+
experimentPostId = this.id,
84+
title = this.title,
85+
uploadDate = this.createdAt.toLocalDate(),
86+
uploaderName = this.member.name,
87+
views = this.views,
88+
recruitStatus = this.recruitStatus,
89+
summary = this.toSummaryForUpdate(),
90+
targetGroup = this.targetGroup.toTargetGroupForUpdate(),
91+
address = this.toAddressForUpdate(),
92+
content = this.content,
93+
imageList = this.images.map { it.imageUrl },
94+
isAuthor = this.member.id == memberId,
95+
isUploaderActive = this.member.deletedAt == null,
96+
alarmAgree = this.alarmAgree
97+
)
98+
}
99+
100+
fun ExperimentPost.toSummaryForUpdate(): GetExperimentPostDetailForUpdateUseCase.ExperimentPostDetailForUpdate.Summary {
101+
return GetExperimentPostDetailForUpdateUseCase.ExperimentPostDetailForUpdate.Summary(
102+
startDate = this.startDate,
103+
endDate = this.endDate,
104+
leadResearcher = this.leadResearcher,
105+
matchType = this.matchType,
106+
reward = this.reward,
107+
count = this.count,
108+
timeRequired = this.timeRequired
109+
)
110+
}
111+
112+
fun TargetGroup.toTargetGroupForUpdate(): GetExperimentPostDetailForUpdateUseCase.ExperimentPostDetailForUpdate.TargetGroup {
113+
return GetExperimentPostDetailForUpdateUseCase.ExperimentPostDetailForUpdate.TargetGroup(
114+
startAge = this.startAge,
115+
endAge = this.endAge,
116+
genderType = this.genderType,
117+
otherCondition = this.otherCondition
118+
)
119+
}
120+
121+
fun ExperimentPost.toAddressForUpdate(): GetExperimentPostDetailForUpdateUseCase.ExperimentPostDetailForUpdate.Address {
122+
return GetExperimentPostDetailForUpdateUseCase.ExperimentPostDetailForUpdate.Address(
123+
univName = this.univName,
124+
region = this.region,
125+
area = this.area,
126+
detailedAddress = this.detailedAddress
127+
)
128+
}

src/main/kotlin/com/dobby/backend/presentation/api/controller/ExperimentPostController.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,20 @@ class ExperimentPostController (
8888
return ExperimentPostMapper.toUpdateExperimentPostResponse(output)
8989
}
9090

91+
@PreAuthorize("hasRole('RESEARCHER')")
92+
@GetMapping("/{postId}/edit")
93+
@Operation(
94+
summary = "공고 수정 시 기존 내용 조회 API",
95+
description = "연구자가 공고를 수정할 때 기존 공고 내용을 불러옵니다."
96+
)
97+
fun getExperimentPostDetailForUpdate(
98+
@PathVariable postId: String
99+
): ExperimentPostDetailResponse {
100+
val input = ExperimentPostMapper.toGetExperimentPostDetailForUpdateUseCaseInput(postId)
101+
val output = experimentPostService.getExperimentPostDetailForUpdate(input)
102+
return ExperimentPostMapper.toGetExperimentPostDetailForUpdateResponse(output)
103+
}
104+
91105
@PreAuthorize("hasRole('RESEARCHER')")
92106
@PostMapping("/image-upload-request")
93107
@Operation(

src/main/kotlin/com/dobby/backend/presentation/api/mapper/ExperimentPostMapper.kt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,61 @@ object ExperimentPostMapper {
154154
)
155155
}
156156

157+
fun toGetExperimentPostDetailForUpdateUseCaseInput(experimentPostId: String): GetExperimentPostDetailForUpdateUseCase.Input {
158+
return GetExperimentPostDetailForUpdateUseCase.Input(
159+
experimentPostId = experimentPostId,
160+
memberId = getCurrentMemberId()
161+
)
162+
}
163+
164+
fun toGetExperimentPostDetailForUpdateResponse(response: GetExperimentPostDetailForUpdateUseCase.Output): ExperimentPostDetailResponse {
165+
return ExperimentPostDetailResponse(
166+
experimentPostId = response.experimentPostDetail.experimentPostId,
167+
title = response.experimentPostDetail.title,
168+
uploadDate = response.experimentPostDetail.uploadDate,
169+
uploaderName = response.experimentPostDetail.uploaderName,
170+
views = response.experimentPostDetail.views,
171+
recruitStatus = response.experimentPostDetail.recruitStatus,
172+
summary = response.experimentPostDetail.summary.toResponse(),
173+
targetGroup = response.experimentPostDetail.targetGroup.toResponse(),
174+
address = response.experimentPostDetail.address.toResponse(),
175+
content = response.experimentPostDetail.content,
176+
imageList = response.experimentPostDetail.imageList,
177+
isAuthor = response.experimentPostDetail.isAuthor,
178+
isUploaderActive = response.experimentPostDetail.isUploaderActive,
179+
alarmAgree = response.experimentPostDetail.alarmAgree
180+
)
181+
}
182+
183+
private fun GetExperimentPostDetailForUpdateUseCase.ExperimentPostDetailForUpdate.Summary.toResponse(): ExperimentPostDetailResponse.SummaryResponse {
184+
return ExperimentPostDetailResponse.SummaryResponse(
185+
startDate = this.startDate,
186+
endDate = this.endDate,
187+
leadResearcher = this.leadResearcher,
188+
matchType = this.matchType,
189+
reward = this.reward,
190+
count = this.count,
191+
timeRequired = this.timeRequired
192+
)
193+
}
194+
195+
private fun GetExperimentPostDetailForUpdateUseCase.ExperimentPostDetailForUpdate.TargetGroup.toResponse(): ExperimentPostDetailResponse.TargetGroupResponse {
196+
return ExperimentPostDetailResponse.TargetGroupResponse(
197+
startAge = this.startAge,
198+
endAge = this.endAge,
199+
genderType = this.genderType,
200+
otherCondition = this.otherCondition
201+
)
202+
}
203+
204+
private fun GetExperimentPostDetailForUpdateUseCase.ExperimentPostDetailForUpdate.Address.toResponse(): ExperimentPostDetailResponse.AddressResponse {
205+
return ExperimentPostDetailResponse.AddressResponse(
206+
univName = this.univName,
207+
region = this.region,
208+
area = this.area,
209+
detailedAddress = this.detailedAddress
210+
)
211+
}
157212

158213
fun toGetExperimentPostDetailUseCaseInput(experimentPostId: String): GetExperimentPostDetailUseCase.Input {
159214
return GetExperimentPostDetailUseCase.Input(
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.dobby.backend.application.usecase.experiment
2+
3+
import com.dobby.backend.domain.exception.ExperimentPostNotFoundException
4+
import com.dobby.backend.domain.gateway.experiment.ExperimentPostGateway
5+
import com.dobby.backend.domain.model.experiment.ApplyMethod
6+
import com.dobby.backend.domain.model.experiment.ExperimentPost
7+
import com.dobby.backend.domain.model.experiment.TargetGroup
8+
import com.dobby.backend.domain.model.member.Member
9+
import com.dobby.backend.infrastructure.database.entity.enums.MatchType
10+
import com.dobby.backend.infrastructure.database.entity.enums.areaInfo.Area
11+
import com.dobby.backend.infrastructure.database.entity.enums.areaInfo.Region
12+
import com.dobby.backend.infrastructure.database.entity.enums.experiment.TimeSlot
13+
import com.dobby.backend.infrastructure.database.entity.enums.member.*
14+
import io.kotest.core.spec.style.BehaviorSpec
15+
import io.kotest.matchers.shouldBe
16+
import io.mockk.every
17+
import io.mockk.mockk
18+
import java.time.LocalDate
19+
import java.time.LocalDateTime
20+
21+
class GetExperimentPostDetailForUpdateUseCaseTest : BehaviorSpec({
22+
val experimentPostGateway = mockk<ExperimentPostGateway>()
23+
val getExperimentPostDetailForUpdateUseCase = GetExperimentPostDetailForUpdateUseCase(experimentPostGateway)
24+
25+
val member = mockk<Member>()
26+
every { member.name } returns "임도비"
27+
every { member.id } returns "1"
28+
every { member.deletedAt } returns null
29+
30+
val targetGroup = mockk<TargetGroup>()
31+
every { targetGroup.id } returns "1"
32+
every { targetGroup.startAge } returns 18
33+
every { targetGroup.endAge } returns 30
34+
every { targetGroup.genderType } returns GenderType.ALL
35+
every { targetGroup.otherCondition } returns "특별한 조건 없음"
36+
37+
val applyMethod = mockk<ApplyMethod>()
38+
every { applyMethod.id } returns "1"
39+
every { applyMethod.phoneNum } returns "010-1234-5678"
40+
every { applyMethod.formUrl } returns "http://apply.com/form"
41+
every { applyMethod.content } returns "지원 방법에 대한 상세 설명"
42+
43+
val experimentPostId = "1"
44+
val experimentPost = ExperimentPost(
45+
id = experimentPostId,
46+
title = "야뿌들의 평균 식사량 체크 테스트",
47+
createdAt = LocalDateTime.now(),
48+
updatedAt = LocalDateTime.now(),
49+
member = member,
50+
views = 100,
51+
recruitStatus = false,
52+
startDate = LocalDate.now(),
53+
endDate = LocalDate.now(),
54+
leadResearcher = "Lead",
55+
matchType = MatchType.ALL,
56+
reward = "네이버페이 1000원",
57+
count = 10,
58+
timeRequired = TimeSlot.ABOUT_1H,
59+
targetGroup = targetGroup,
60+
applyMethod = applyMethod,
61+
region = Region.SEOUL,
62+
area = Area.GWANGJINGU,
63+
univName = "건국대학교",
64+
detailedAddress = "건국대학교 공학관",
65+
content = "야뿌들의 한끼 식사량을 체크하는 테스트입니다.",
66+
alarmAgree = false,
67+
images = mutableListOf()
68+
)
69+
70+
given("유효한 experimentPostId와 작성자 본인인 경우") {
71+
every { experimentPostGateway.findExperimentPostByMemberIdAndPostId("1", experimentPostId) } returns experimentPost
72+
73+
`when`("작성자가 수정용 상세 정보를 요청하면") {
74+
val input = GetExperimentPostDetailForUpdateUseCase.Input(
75+
experimentPostId = experimentPostId,
76+
memberId = member.id
77+
)
78+
val result = getExperimentPostDetailForUpdateUseCase.execute(input)
79+
80+
then("업데이트를 위한 상세 정보가 정상적으로 반환된다") {
81+
result.experimentPostDetail.title shouldBe experimentPost.title
82+
result.experimentPostDetail.isAuthor shouldBe true
83+
}
84+
}
85+
}
86+
87+
given("유효한 experimentPostId지만 작성자가 아닌 경우") {
88+
every { experimentPostGateway.findExperimentPostByMemberIdAndPostId("2", experimentPostId) } returns null
89+
90+
`when`("다른 사용자가 수정용 상세 정보를 요청하면") {
91+
val input = GetExperimentPostDetailForUpdateUseCase.Input(
92+
experimentPostId = experimentPostId,
93+
memberId = "2" // 작성자와 다른 아이디
94+
)
95+
96+
then("ExperimentPostNotFoundException이 발생한다") {
97+
val exception = runCatching { getExperimentPostDetailForUpdateUseCase.execute(input) }.exceptionOrNull()
98+
exception shouldBe ExperimentPostNotFoundException
99+
}
100+
}
101+
}
102+
103+
given("유효하지 않은 experimentPostId가 주어진 경우") {
104+
val invalidExperimentPostId = "999"
105+
every { experimentPostGateway.findExperimentPostByMemberIdAndPostId("1", invalidExperimentPostId) } returns null
106+
107+
`when`("수정용 상세 정보를 요청하면") {
108+
val input = GetExperimentPostDetailForUpdateUseCase.Input(
109+
experimentPostId = invalidExperimentPostId,
110+
memberId = member.id
111+
)
112+
113+
then("ExperimentPostNotFoundException이 발생한다") {
114+
val exception = runCatching { getExperimentPostDetailForUpdateUseCase.execute(input) }.exceptionOrNull()
115+
exception shouldBe ExperimentPostNotFoundException
116+
}
117+
}
118+
}
119+
})

0 commit comments

Comments
 (0)