Skip to content

Commit 93c8051

Browse files
robinjoonclaude
andauthored
feat: Review 삭제 Mutation 추가 (#20) (#25)
본인 회고만 삭제 가능하도록 소유권 검증 포함한 deleteReview Mutation 구현 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bff9dce commit 93c8051

9 files changed

Lines changed: 111 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Review 삭제 Mutation 검증 체크리스트
2+
3+
## 필수 항목
4+
- [x] 아키텍처 원칙 준수 (docs/architecture.md 기준)
5+
- [x] 레이어 의존성 규칙 위반 없음
6+
- [x] 테스트 코드 작성 완료 (Domain, Application 필수)
7+
- [x] 모든 테스트 통과
8+
- [x] 기존 테스트 깨지지 않음
9+
10+
## 선택 항목 (해당 시)
11+
- [x] 소유권 검증 (본인 회고만 삭제 가능)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Review 삭제 Mutation 추가 계획
2+
3+
> Issue: #20
4+
5+
## 단계
6+
7+
- [x] 1단계: Domain — ReviewCommand.Delete 추가
8+
- [x] 2단계: Domain — ReviewRepository에 delete 메서드 추가
9+
- [x] 3단계: Application — ReviewService.delete 테스트 작성 (RED)
10+
- [x] 4단계: Application — ReviewService.delete 구현 (GREEN)
11+
- [x] 5단계: Infrastructure — ExposedReviewRepository.delete 구현
12+
- [x] 6단계: Presentation — GraphQL 스키마에 deleteReview Mutation 추가
13+
- [x] 7단계: Presentation — ReviewDataFetcher.deleteReview 구현
14+
- [x] 8단계: DGS Codegen 빌드 확인
15+
- [x] 9단계: 전체 테스트 통과 확인

src/main/kotlin/kr/io/team/loop/review/application/service/ReviewService.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package kr.io.team.loop.review.application.service
33
import kotlinx.datetime.LocalDate
44
import kotlinx.datetime.minus
55
import kr.io.team.loop.common.domain.MemberId
6+
import kr.io.team.loop.common.domain.exception.AccessDeniedException
67
import kr.io.team.loop.common.domain.exception.DuplicateEntityException
8+
import kr.io.team.loop.common.domain.exception.EntityNotFoundException
79
import kr.io.team.loop.review.application.dto.ReviewStatsDto
810
import kr.io.team.loop.review.domain.model.Review
911
import kr.io.team.loop.review.domain.model.ReviewCommand
@@ -17,6 +19,20 @@ import org.springframework.transaction.annotation.Transactional
1719
class ReviewService(
1820
private val reviewRepository: ReviewRepository,
1921
) {
22+
@Transactional
23+
fun delete(
24+
command: ReviewCommand.Delete,
25+
memberId: MemberId,
26+
) {
27+
val review =
28+
reviewRepository.findById(command.reviewId)
29+
?: throw EntityNotFoundException("Review not found: ${command.reviewId.value}")
30+
if (!review.isOwnedBy(memberId)) {
31+
throw AccessDeniedException("Review does not belong to member: ${memberId.value}")
32+
}
33+
reviewRepository.delete(command)
34+
}
35+
2036
@Transactional
2137
fun create(command: ReviewCommand.Create): Review =
2238
try {

src/main/kotlin/kr/io/team/loop/review/domain/model/ReviewCommand.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ sealed interface ReviewCommand {
99
val steps: List<ReviewStep>,
1010
val date: LocalDate,
1111
) : ReviewCommand
12+
13+
data class Delete(
14+
val reviewId: ReviewId,
15+
) : ReviewCommand
1216
}

src/main/kotlin/kr/io/team/loop/review/domain/repository/ReviewRepository.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import kr.io.team.loop.review.domain.model.ReviewQuery
88
interface ReviewRepository {
99
fun save(command: ReviewCommand.Create): Review
1010

11+
fun delete(command: ReviewCommand.Delete)
12+
1113
fun findAll(query: ReviewQuery): List<Review>
1214

1315
fun findById(id: kr.io.team.loop.review.domain.model.ReviewId): Review?

src/main/kotlin/kr/io/team/loop/review/infrastructure/persistence/ExposedReviewRepository.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,18 @@ import org.jetbrains.exposed.v1.core.count
1818
import org.jetbrains.exposed.v1.core.eq
1919
import org.jetbrains.exposed.v1.core.greaterEq
2020
import org.jetbrains.exposed.v1.core.lessEq
21+
import org.jetbrains.exposed.v1.jdbc.deleteWhere
2122
import org.jetbrains.exposed.v1.jdbc.insert
2223
import org.jetbrains.exposed.v1.jdbc.selectAll
2324
import org.springframework.stereotype.Repository
2425
import java.time.OffsetDateTime
2526

2627
@Repository
2728
class ExposedReviewRepository : ReviewRepository {
29+
override fun delete(command: ReviewCommand.Delete) {
30+
ReviewTable.deleteWhere { reviewId eq command.reviewId.value }
31+
}
32+
2833
override fun save(command: ReviewCommand.Create): Review {
2934
val now = OffsetDateTime.now()
3035
val periodKey = PeriodKey.daily(command.date)

src/main/kotlin/kr/io/team/loop/review/presentation/datafetcher/ReviewDataFetcher.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import kr.io.team.loop.common.domain.MemberId
1515
import kr.io.team.loop.review.application.service.ReviewService
1616
import kr.io.team.loop.review.domain.model.Review
1717
import kr.io.team.loop.review.domain.model.ReviewCommand
18+
import kr.io.team.loop.review.domain.model.ReviewId
1819
import kr.io.team.loop.review.domain.model.ReviewQuery
1920
import kr.io.team.loop.review.domain.model.ReviewStep
2021
import kotlin.time.Clock
@@ -82,6 +83,16 @@ class ReviewDataFetcher(
8283
return reviewService.create(command).toGraphql()
8384
}
8485

86+
@DgsMutation
87+
fun deleteReview(
88+
@InputArgument id: String,
89+
@Authorize memberId: Long,
90+
): Boolean {
91+
val command = ReviewCommand.Delete(reviewId = ReviewId(id.toLong()))
92+
reviewService.delete(command, MemberId(memberId))
93+
return true
94+
}
95+
8596
private fun Review.toGraphql(): ReviewGraphql =
8697
ReviewGraphql(
8798
id = id.value.toString(),

src/main/resources/schema/review.graphqls

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ extend type Mutation {
1515
"생성할 회고 정보"
1616
input: CreateReviewInput!
1717
): Review!
18+
19+
"회고를 삭제한다. (본인 회고만)"
20+
deleteReview(
21+
"삭제할 회고 ID"
22+
id: ID!
23+
): Boolean!
1824
}
1925

2026
"""회고"""

src/test/kotlin/kr/io/team/loop/review/application/service/ReviewServiceTest.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import io.kotest.core.spec.style.BehaviorSpec
55
import io.kotest.matchers.collections.shouldHaveSize
66
import io.kotest.matchers.shouldBe
77
import io.mockk.every
8+
import io.mockk.justRun
89
import io.mockk.mockk
10+
import io.mockk.verify
911
import kotlinx.datetime.LocalDate
1012
import kr.io.team.loop.common.domain.MemberId
13+
import kr.io.team.loop.common.domain.exception.AccessDeniedException
1114
import kr.io.team.loop.common.domain.exception.DuplicateEntityException
15+
import kr.io.team.loop.common.domain.exception.EntityNotFoundException
1216
import kr.io.team.loop.review.domain.model.PeriodKey
1317
import kr.io.team.loop.review.domain.model.Review
1418
import kr.io.team.loop.review.domain.model.ReviewCommand
@@ -208,4 +212,41 @@ class ReviewServiceTest :
208212
}
209213
}
210214
}
215+
216+
Given("회고 삭제 시") {
217+
val reviewId = ReviewId(1L)
218+
val command = ReviewCommand.Delete(reviewId = reviewId)
219+
220+
When("본인 회고이면") {
221+
every { reviewRepository.findById(reviewId) } returns savedReview
222+
justRun { reviewRepository.delete(command) }
223+
224+
reviewService.delete(command, memberId)
225+
226+
Then("삭제가 수행된다") {
227+
verify { reviewRepository.delete(command) }
228+
}
229+
}
230+
231+
When("회고가 존재하지 않으면") {
232+
every { reviewRepository.findById(reviewId) } returns null
233+
234+
Then("EntityNotFoundException이 발생한다") {
235+
shouldThrow<EntityNotFoundException> {
236+
reviewService.delete(command, memberId)
237+
}
238+
}
239+
}
240+
241+
When("다른 사용자의 회고이면") {
242+
val otherMemberId = MemberId(99L)
243+
every { reviewRepository.findById(reviewId) } returns savedReview
244+
245+
Then("AccessDeniedException이 발생한다") {
246+
shouldThrow<AccessDeniedException> {
247+
reviewService.delete(command, otherMemberId)
248+
}
249+
}
250+
}
251+
}
211252
})

0 commit comments

Comments
 (0)