From 16b5dc6b25dc94c403e98b3afdb76cb656c3e756 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 14:20:59 +0900 Subject: [PATCH 01/26] =?UTF-8?q?test:=20account=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=95=88=EC=A0=84=EB=A7=9D=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usecase/AccountUseCaseImplTest.kt | 118 ++++++++++++++++++ .../usecase/ReceiptUseCaseImplTest.kt | 110 ++++++++++++---- .../account/domain/entity/AccountTest.kt | 28 +++++ .../account/domain/entity/ReceiptTest.kt | 28 +++++ .../account/fixture/AccountTestFixture.kt | 22 ++++ .../account/fixture/ReceiptTestFixture.kt | 25 ++++ 6 files changed, 310 insertions(+), 21 deletions(-) create mode 100644 src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt create mode 100644 src/test/kotlin/com/weeth/domain/account/domain/entity/AccountTest.kt create mode 100644 src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt create mode 100644 src/test/kotlin/com/weeth/domain/account/fixture/AccountTestFixture.kt create mode 100644 src/test/kotlin/com/weeth/domain/account/fixture/ReceiptTestFixture.kt diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt new file mode 100644 index 00000000..239e1605 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt @@ -0,0 +1,118 @@ +package com.weeth.domain.account.application.usecase + +import com.weeth.domain.account.application.dto.AccountDTO +import com.weeth.domain.account.application.dto.ReceiptDTO +import com.weeth.domain.account.application.exception.AccountExistsException +import com.weeth.domain.account.application.exception.AccountNotFoundException +import com.weeth.domain.account.application.mapper.AccountMapper +import com.weeth.domain.account.application.mapper.ReceiptMapper +import com.weeth.domain.account.domain.service.AccountGetService +import com.weeth.domain.account.domain.service.AccountSaveService +import com.weeth.domain.account.domain.service.ReceiptGetService +import com.weeth.domain.account.fixture.AccountTestFixture +import com.weeth.domain.account.fixture.ReceiptTestFixture +import com.weeth.domain.file.application.dto.response.FileResponse +import com.weeth.domain.file.application.mapper.FileMapper +import com.weeth.domain.file.domain.entity.FileOwnerType +import com.weeth.domain.file.domain.repository.FileReader +import com.weeth.domain.user.domain.service.CardinalGetService +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + +class AccountUseCaseImplTest : + DescribeSpec({ + val accountGetService = mockk() + val accountSaveService = mockk(relaxUnitFun = true) + val receiptGetService = mockk() + val fileReader = mockk() + val cardinalGetService = mockk() + val accountMapper = mockk() + val receiptMapper = mockk() + val fileMapper = mockk() + + val useCase = + AccountUseCaseImpl( + accountGetService, + accountSaveService, + receiptGetService, + fileReader, + cardinalGetService, + accountMapper, + receiptMapper, + fileMapper, + ) + + beforeTest { + clearMocks( + accountGetService, + accountSaveService, + receiptGetService, + fileReader, + cardinalGetService, + accountMapper, + receiptMapper, + fileMapper, + ) + } + + describe("find") { + context("존재하는 기수의 회비 조회 시") { + it("영수증과 파일 정보가 포함된 AccountResponse를 반환한다") { + val account = AccountTestFixture.createAccount(cardinal = 40) + val receipt = ReceiptTestFixture.createReceipt(id = 10L, amount = 5_000, account = account) + val fileResponse = mockk() + val receiptResponse = mockk() + val accountResponse = mockk() + + every { accountGetService.find(40) } returns account + every { receiptGetService.findAllByAccountId(account.id) } returns listOf(receipt) + every { fileReader.findAll(FileOwnerType.RECEIPT, receipt.id, null) } returns listOf(mockk()) + every { fileMapper.toFileResponse(any()) } returns fileResponse + every { receiptMapper.to(receipt, listOf(fileResponse)) } returns receiptResponse + every { accountMapper.to(account, listOf(receiptResponse)) } returns accountResponse + + val result = useCase.find(40) + + result shouldBe accountResponse + } + } + + context("존재하지 않는 기수 조회 시") { + it("AccountNotFoundException을 던진다") { + every { accountGetService.find(99) } throws AccountNotFoundException() + + shouldThrow { useCase.find(99) } + } + } + } + + describe("save") { + context("이미 존재하는 기수로 저장 시") { + it("AccountExistsException을 던진다") { + val dto = AccountDTO.Save("설명", 100_000, 40) + every { accountGetService.validate(40) } returns true + + shouldThrow { useCase.save(dto) } + } + } + + context("정상 저장 시") { + it("account가 저장된다") { + val dto = AccountDTO.Save("설명", 100_000, 40) + val account = AccountTestFixture.createAccount() + every { accountGetService.validate(40) } returns false + every { cardinalGetService.findByAdminSide(40) } returns mockk() + every { accountMapper.from(dto) } returns account + + useCase.save(dto) + + verify(exactly = 1) { accountSaveService.save(account) } + } + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt index b3d0853f..2d98af37 100644 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt @@ -2,13 +2,13 @@ package com.weeth.domain.account.application.usecase import com.weeth.domain.account.application.dto.ReceiptDTO import com.weeth.domain.account.application.mapper.ReceiptMapper -import com.weeth.domain.account.domain.entity.Account -import com.weeth.domain.account.domain.entity.Receipt import com.weeth.domain.account.domain.service.AccountGetService import com.weeth.domain.account.domain.service.ReceiptDeleteService import com.weeth.domain.account.domain.service.ReceiptGetService import com.weeth.domain.account.domain.service.ReceiptSaveService import com.weeth.domain.account.domain.service.ReceiptUpdateService +import com.weeth.domain.account.fixture.AccountTestFixture +import com.weeth.domain.account.fixture.ReceiptTestFixture import com.weeth.domain.file.application.dto.request.FileSaveRequest import com.weeth.domain.file.application.mapper.FileMapper import com.weeth.domain.file.domain.entity.File @@ -17,6 +17,7 @@ import com.weeth.domain.file.domain.repository.FileReader import com.weeth.domain.file.domain.repository.FileRepository import com.weeth.domain.user.domain.service.CardinalGetService import io.kotest.core.spec.style.DescribeSpec +import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -25,7 +26,7 @@ import java.time.LocalDate class ReceiptUseCaseImplTest : DescribeSpec({ val receiptGetService = mockk() - val receiptDeleteService = mockk() + val receiptDeleteService = mockk(relaxUnitFun = true) val receiptSaveService = mockk() val receiptUpdateService = mockk(relaxUnitFun = true) val accountGetService = mockk() @@ -49,36 +50,85 @@ class ReceiptUseCaseImplTest : fileMapper, ) + beforeTest { + clearMocks( + receiptGetService, + receiptDeleteService, + receiptSaveService, + receiptUpdateService, + accountGetService, + fileReader, + cardinalGetService, + receiptMapper, + fileMapper, + ) + } + + describe("save") { + context("파일이 있는 경우") { + it("영수증 저장 후 account.spend와 fileRepository.saveAll이 호출된다") { + val account = AccountTestFixture.createAccount(cardinal = 40) + val receipt = ReceiptTestFixture.createReceipt(id = 10L, amount = 5_000, account = account) + val files = listOf(mockk()) + val dto = + ReceiptDTO.Save( + "간식비", + "편의점", + 5_000, + LocalDate.of(2024, 9, 1), + 40, + listOf(FileSaveRequest("receipt.png", "TEMP/2024-09/receipt.png", 200L, "image/png")), + ) + + every { cardinalGetService.findByAdminSide(40) } returns mockk() + every { accountGetService.find(40) } returns account + every { receiptMapper.from(dto, account) } returns receipt + every { receiptSaveService.save(receipt) } returns receipt + every { fileMapper.toFileList(dto.files(), FileOwnerType.RECEIPT, receipt.id) } returns files + + useCase.save(dto) + + verify(exactly = 1) { receiptSaveService.save(receipt) } + verify(exactly = 1) { fileRepository.saveAll(files) } + } + } + + context("파일이 없는 경우") { + it("영수증 저장 후 fileRepository.saveAll은 빈 리스트로 호출된다") { + val account = AccountTestFixture.createAccount(cardinal = 40) + val receipt = ReceiptTestFixture.createReceipt(id = 11L, amount = 3_000, account = account) + val dto = ReceiptDTO.Save("교통비", "지하철", 3_000, LocalDate.of(2024, 9, 2), 40, emptyList()) + + every { cardinalGetService.findByAdminSide(40) } returns mockk() + every { accountGetService.find(40) } returns account + every { receiptMapper.from(dto, account) } returns receipt + every { receiptSaveService.save(receipt) } returns receipt + every { fileMapper.toFileList(emptyList(), FileOwnerType.RECEIPT, receipt.id) } returns emptyList() + + useCase.save(dto) + + verify(exactly = 1) { receiptSaveService.save(receipt) } + verify(exactly = 1) { fileRepository.saveAll(emptyList()) } + } + } + } + describe("update") { it("업데이트 파일이 있으면 기존 파일을 삭제 후 새 파일을 저장한다") { val receiptId = 10L - val account = - Account - .builder() - .id(1L) - .totalAmount(10000) - .currentAmount(10000) - .cardinal(40) - .receipts(mutableListOf()) - .build() - val receipt = - Receipt - .builder() - .id(receiptId) - .amount(1000) - .account(account) - .build() + val account = AccountTestFixture.createAccount(cardinal = 40) + val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = account) + account.spend(receipt) val dto = ReceiptDTO.Update( "desc", "source", - 2000, + 2_000, LocalDate.of(2026, 1, 1), 40, listOf(FileSaveRequest("new.png", "TEMP/2026-02/new.png", 100L, "image/png")), ) - val oldFiles = listOf(mockk()) val newFiles = listOf(mockk()) @@ -94,4 +144,22 @@ class ReceiptUseCaseImplTest : verify(exactly = 1) { receiptUpdateService.update(receipt, dto) } } } + + describe("delete") { + it("관련 파일 삭제 후 account.cancel이 호출되고 영수증이 삭제된다") { + val receiptId = 5L + val account = AccountTestFixture.createAccount(currentAmount = 100_000) + val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 10_000, account = account) + account.spend(receipt) // receipts 목록에 추가해야 cancel이 동작 + val files = listOf(mockk()) + + every { receiptGetService.find(receiptId) } returns receipt + every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns files + + useCase.delete(receiptId) + + verify(exactly = 1) { fileRepository.deleteAll(files) } + verify(exactly = 1) { receiptDeleteService.delete(receipt) } + } + } }) diff --git a/src/test/kotlin/com/weeth/domain/account/domain/entity/AccountTest.kt b/src/test/kotlin/com/weeth/domain/account/domain/entity/AccountTest.kt new file mode 100644 index 00000000..7e07286b --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/domain/entity/AccountTest.kt @@ -0,0 +1,28 @@ +package com.weeth.domain.account.domain.entity + +import com.weeth.domain.account.fixture.AccountTestFixture +import com.weeth.domain.account.fixture.ReceiptTestFixture +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class AccountTest : + StringSpec({ + "spend은 currentAmount를 영수증 금액만큼 감소시킨다" { + val account = AccountTestFixture.createAccount(currentAmount = 100_000) + val receipt = ReceiptTestFixture.createReceipt(amount = 10_000, account = account) + + account.spend(receipt) + + account.currentAmount shouldBe 90_000 + } + + "cancel은 currentAmount를 영수증 금액만큼 복원한다" { + val account = AccountTestFixture.createAccount(currentAmount = 100_000) + val receipt = ReceiptTestFixture.createReceipt(amount = 10_000, account = account) + account.spend(receipt) + + account.cancel(receipt) + + account.currentAmount shouldBe 100_000 + } + }) diff --git a/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt b/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt new file mode 100644 index 00000000..9537cb26 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt @@ -0,0 +1,28 @@ +package com.weeth.domain.account.domain.entity + +import com.weeth.domain.account.application.dto.ReceiptDTO +import com.weeth.domain.account.fixture.ReceiptTestFixture +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import java.time.LocalDate + +class ReceiptTest : + StringSpec({ + "update는 영수증 필드를 갱신한다" { + val receipt = + ReceiptTestFixture.createReceipt( + description = "기존 설명", + source = "기존 출처", + amount = 5_000, + date = LocalDate.of(2024, 1, 1), + ) + val dto = ReceiptDTO.Update("새로운 설명", "새 출처", 20_000, LocalDate.of(2025, 6, 1), 40, emptyList()) + + receipt.update(dto) + + receipt.description shouldBe "새로운 설명" + receipt.source shouldBe "새 출처" + receipt.amount shouldBe 20_000 + receipt.date shouldBe LocalDate.of(2025, 6, 1) + } + }) diff --git a/src/test/kotlin/com/weeth/domain/account/fixture/AccountTestFixture.kt b/src/test/kotlin/com/weeth/domain/account/fixture/AccountTestFixture.kt new file mode 100644 index 00000000..ed407387 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/fixture/AccountTestFixture.kt @@ -0,0 +1,22 @@ +package com.weeth.domain.account.fixture + +import com.weeth.domain.account.domain.entity.Account + +object AccountTestFixture { + fun createAccount( + id: Long = 1L, + description: String = "2024년 2학기 회비", + totalAmount: Int = 100_000, + currentAmount: Int = 100_000, + cardinal: Int = 40, + ): Account = + Account + .builder() + .id(id) + .description(description) + .totalAmount(totalAmount) + .currentAmount(currentAmount) + .cardinal(cardinal) + .receipts(mutableListOf()) + .build() +} diff --git a/src/test/kotlin/com/weeth/domain/account/fixture/ReceiptTestFixture.kt b/src/test/kotlin/com/weeth/domain/account/fixture/ReceiptTestFixture.kt new file mode 100644 index 00000000..6df91422 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/fixture/ReceiptTestFixture.kt @@ -0,0 +1,25 @@ +package com.weeth.domain.account.fixture + +import com.weeth.domain.account.domain.entity.Account +import com.weeth.domain.account.domain.entity.Receipt +import java.time.LocalDate + +object ReceiptTestFixture { + fun createReceipt( + id: Long = 1L, + description: String = "간식비", + source: String = "편의점", + amount: Int = 10_000, + date: LocalDate = LocalDate.of(2024, 9, 1), + account: Account = AccountTestFixture.createAccount(), + ): Receipt = + Receipt + .builder() + .id(id) + .description(description) + .source(source) + .amount(amount) + .date(date) + .account(account) + .build() +} From abaab1b7252e8e17a63c933216c66e2c14560e45 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 14:32:22 +0900 Subject: [PATCH 02/26] =?UTF-8?q?refactor:=20Account=20entity=20java=20->?= =?UTF-8?q?=20kotlin=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/weeth/domain/account/domain/entity/Account.kt} | 0 .../com/weeth/domain/account/domain/entity/Receipt.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/main/{java/com/weeth/domain/account/domain/entity/Account.java => kotlin/com/weeth/domain/account/domain/entity/Account.kt} (100%) rename src/main/{java/com/weeth/domain/account/domain/entity/Receipt.java => kotlin/com/weeth/domain/account/domain/entity/Receipt.kt} (100%) diff --git a/src/main/java/com/weeth/domain/account/domain/entity/Account.java b/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/domain/entity/Account.java rename to src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt diff --git a/src/main/java/com/weeth/domain/account/domain/entity/Receipt.java b/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/domain/entity/Receipt.java rename to src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt From 8143e35768e1b08486b6f407ff95f6b8c5486467 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 14:44:43 +0900 Subject: [PATCH 03/26] =?UTF-8?q?refactor:=20Account=20entity=20kotlin?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=AC=B8=EB=B2=95=20=EB=B3=80=EA=B2=BD,?= =?UTF-8?q?=20money=20vo=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/account/domain/entity/Account.kt | 90 ++++++++++++------- .../domain/account/domain/entity/Receipt.kt | 89 ++++++++++-------- .../weeth/domain/account/domain/vo/Money.kt | 20 +++++ 3 files changed, 131 insertions(+), 68 deletions(-) create mode 100644 src/main/kotlin/com/weeth/domain/account/domain/vo/Money.kt diff --git a/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt b/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt index 032749bb..c3f41341 100644 --- a/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt +++ b/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt @@ -1,46 +1,72 @@ -package com.weeth.domain.account.domain.entity; +package com.weeth.domain.account.domain.entity -import jakarta.persistence.*; -import com.weeth.global.common.entity.BaseEntity; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -import java.util.ArrayList; -import java.util.List; +import com.weeth.domain.account.domain.vo.Money +import com.weeth.global.common.entity.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id @Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@SuperBuilder -public class Account extends BaseEntity { - +class Account( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "account_id") - private Long id; - - private String description; + val id: Long = 0, + @Column(nullable = false) + val description: String, + @Column(nullable = false) + val totalAmount: Int, + @Column(nullable = false) + var currentAmount: Int, + @Column(nullable = false) + val cardinal: Int, +) : BaseEntity() { + fun spend(amount: Money) { + require(amount.value > 0) { "사용 금액은 0보다 커야 합니다: ${amount.value}" } + check(currentAmount >= amount.value) { "잔액이 부족합니다. 현재: $currentAmount, 요청: ${amount.value}" } + currentAmount -= amount.value + } - private Integer totalAmount; + fun cancelSpend(amount: Money) { + require(amount.value > 0) { "취소 금액은 0보다 커야 합니다: ${amount.value}" } + check(currentAmount + amount.value <= totalAmount) { "총액을 초과할 수 없습니다. 총액: $totalAmount" } + currentAmount += amount.value + } - private Integer currentAmount; + fun adjustSpend( + oldAmount: Money, + newAmount: Money, + ) { + cancelSpend(oldAmount) + spend(newAmount) + } - private Integer cardinal; + // Java interop overloads — removed when Java UseCases are migrated in Phase 5 + fun spend(amount: Int) = spend(Money.of(amount)) - @OneToMany(mappedBy = "account", cascade = CascadeType.REMOVE, orphanRemoval = true) - private List receipts = new ArrayList<>(); + fun cancelSpend(amount: Int) = cancelSpend(Money.of(amount)) - public void spend(Receipt receipt) { - this.receipts.add(receipt); - this.currentAmount -= receipt.getAmount(); - } + fun adjustSpend( + oldAmount: Int, + newAmount: Int, + ) = adjustSpend(Money.of(oldAmount), Money.of(newAmount)) - public void cancel(Receipt receipt) { - this.receipts.remove(receipt); - this.currentAmount += receipt.getAmount(); + companion object { + @JvmStatic + fun create( + description: String, + totalAmount: Int, + cardinal: Int, + ): Account { + require(totalAmount > 0) { "총액은 0보다 커야 합니다: $totalAmount" } + return Account( + description = description, + totalAmount = totalAmount, + currentAmount = totalAmount, + cardinal = cardinal, + ) + } } } diff --git a/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt b/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt index 83ea940e..376db547 100644 --- a/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt +++ b/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt @@ -1,45 +1,62 @@ -package com.weeth.domain.account.domain.entity; - -import jakarta.persistence.*; -import com.weeth.domain.account.application.dto.ReceiptDTO; -import com.weeth.global.common.entity.BaseEntity; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -import java.time.LocalDate; +package com.weeth.domain.account.domain.entity + +import com.weeth.global.common.entity.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import java.time.LocalDate @Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@SuperBuilder -public class Receipt extends BaseEntity { - +class Receipt( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "receipt_id") - private Long id; - - private String description; - - private String source; - - private Integer amount; - - private LocalDate date; - - @ManyToOne + val id: Long = 0, + @Column + var description: String?, + @Column + var source: String?, + @Column(nullable = false) + var amount: Int, + @Column(nullable = false) + var date: LocalDate, + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "account_id") - private Account account; - - public void update(ReceiptDTO.Update dto){ - this.description = dto.description(); - this.source = dto.source(); - this.amount = dto.amount(); - this.date = dto.date(); + val account: Account, +) : BaseEntity() { + fun update( + description: String?, + source: String?, + amount: Int, + date: LocalDate, + ) { + this.description = description + this.source = source + this.amount = amount + this.date = date } + companion object { + fun create( + description: String?, + source: String?, + amount: Int, + date: LocalDate, + account: Account, + ): Receipt { + require(amount > 0) { "금액은 0보다 커야 합니다: $amount" } + return Receipt( + description = description, + source = source, + amount = amount, + date = date, + account = account, + ) + } + } } diff --git a/src/main/kotlin/com/weeth/domain/account/domain/vo/Money.kt b/src/main/kotlin/com/weeth/domain/account/domain/vo/Money.kt new file mode 100644 index 00000000..2e856076 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/domain/vo/Money.kt @@ -0,0 +1,20 @@ +package com.weeth.domain.account.domain.vo + +@JvmInline +value class Money( + val value: Int, +) { + init { + require(value >= 0) { "금액은 0 이상이어야 합니다: $value" } + } + + operator fun plus(other: Money) = Money(value + other.value) + + operator fun minus(other: Money) = Money(value - other.value) + + companion object { + val ZERO = Money(0) + + fun of(value: Int) = Money(value) + } +} From 4644a95a74e1e356388fcb07f1905d4f5cd47f1f Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 14:47:00 +0900 Subject: [PATCH 04/26] =?UTF-8?q?refactor:=20entity=20=EC=BD=94=ED=8B=80?= =?UTF-8?q?=EB=A6=B0=20=EB=AC=B8=EB=B2=95=20=EB=B3=80=EA=B2=BD=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=B0=B8=EC=A1=B0=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mapper/AccountMapper.java | 2 - .../application/mapper/ReceiptMapper.java | 4 -- .../usecase/AccountUseCaseImpl.java | 2 +- .../usecase/ReceiptUseCaseImpl.java | 15 ++++--- .../domain/service/ReceiptUpdateService.java | 2 +- .../usecase/AccountUseCaseImplTest.kt | 6 +-- .../usecase/ReceiptUseCaseImplTest.kt | 27 ++++++------ .../account/domain/entity/AccountTest.kt | 44 +++++++++++++++---- .../account/domain/entity/ReceiptTest.kt | 4 +- .../account/fixture/AccountTestFixture.kt | 16 +++---- .../account/fixture/ReceiptTestFixture.kt | 17 ++++--- 11 files changed, 76 insertions(+), 63 deletions(-) diff --git a/src/main/java/com/weeth/domain/account/application/mapper/AccountMapper.java b/src/main/java/com/weeth/domain/account/application/mapper/AccountMapper.java index f428cc88..b6ae4f3a 100644 --- a/src/main/java/com/weeth/domain/account/application/mapper/AccountMapper.java +++ b/src/main/java/com/weeth/domain/account/application/mapper/AccountMapper.java @@ -15,6 +15,4 @@ public interface AccountMapper { @Mapping(target = "time", source = "account.modifiedAt") AccountDTO.Response to(Account account, List receipts); - @Mapping(target = "currentAmount", source = "totalAmount") - Account from(AccountDTO.Save dto); } diff --git a/src/main/java/com/weeth/domain/account/application/mapper/ReceiptMapper.java b/src/main/java/com/weeth/domain/account/application/mapper/ReceiptMapper.java index c2a8f8d7..730defbe 100644 --- a/src/main/java/com/weeth/domain/account/application/mapper/ReceiptMapper.java +++ b/src/main/java/com/weeth/domain/account/application/mapper/ReceiptMapper.java @@ -18,8 +18,4 @@ public interface ReceiptMapper { ReceiptDTO.Response to(Receipt receipt, List fileUrls); - @Mapping(target = "id", ignore = true) - @Mapping(target = "description", source = "dto.description") - @Mapping(target = "account", source = "account") - Receipt from(ReceiptDTO.Save dto, Account account); } diff --git a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java index d2d9ca00..4a333c4e 100644 --- a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java +++ b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java @@ -52,7 +52,7 @@ public void save(AccountDTO.Save dto) { validate(dto); cardinalGetService.findByAdminSide(dto.cardinal()); - accountSaveService.save(accountMapper.from(dto)); + accountSaveService.save(Account.Companion.create(dto.description(), dto.totalAmount(), dto.cardinal())); } private void validate(AccountDTO.Save dto) { diff --git a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java b/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java index 0a646340..72750f7c 100644 --- a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java +++ b/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java @@ -42,8 +42,10 @@ public void save(ReceiptDTO.Save dto) { cardinalGetService.findByAdminSide(dto.cardinal()); Account account = accountGetService.find(dto.cardinal()); - Receipt receipt = receiptSaveService.save(mapper.from(dto, account)); - account.spend(receipt); + Receipt receipt = receiptSaveService.save( + Receipt.Companion.create(dto.description(), dto.source(), dto.amount(), dto.date(), account) + ); + account.spend(dto.amount()); List files = fileMapper.toFileList(dto.files(), FileOwnerType.RECEIPT, receipt.getId()); fileRepository.saveAll(files); @@ -51,12 +53,12 @@ public void save(ReceiptDTO.Save dto) { @Override @Transactional - public void update(Long receiptId, ReceiptDTO.Update dto){ + public void update(Long receiptId, ReceiptDTO.Update dto) { Account account = accountGetService.find(dto.cardinal()); Receipt receipt = receiptGetService.find(receiptId); - account.cancel(receipt); + account.adjustSpend(receipt.getAmount(), dto.amount()); - if(!dto.files().isEmpty()){ // 업데이트하려는 파일이 있다면 파일을 전체 삭제한 뒤 저장 + if (!dto.files().isEmpty()) { // 업데이트하려는 파일이 있다면 파일을 전체 삭제한 뒤 저장 List fileList = getFiles(receiptId); fileRepository.deleteAll(fileList); @@ -64,7 +66,6 @@ public void update(Long receiptId, ReceiptDTO.Update dto){ fileRepository.saveAll(files); } receiptUpdateService.update(receipt, dto); - account.spend(receipt); } private List getFiles(Long receiptId) { @@ -77,7 +78,7 @@ public void delete(Long id) { Receipt receipt = receiptGetService.find(id); List fileList = fileReader.findAll(FileOwnerType.RECEIPT, id, null); - receipt.getAccount().cancel(receipt); + receipt.getAccount().cancelSpend(receipt.getAmount()); fileRepository.deleteAll(fileList); receiptDeleteService.delete(receipt); diff --git a/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java b/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java index 95462683..2d4a1f6a 100644 --- a/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java +++ b/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java @@ -9,6 +9,6 @@ @RequiredArgsConstructor public class ReceiptUpdateService { public void update(Receipt receipt, ReceiptDTO.Update dto) { - receipt.update(dto); + receipt.update(dto.description(), dto.source(), dto.amount(), dto.date()); } } \ No newline at end of file diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt index 239e1605..945acc73 100644 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt @@ -102,16 +102,14 @@ class AccountUseCaseImplTest : } context("정상 저장 시") { - it("account가 저장된다") { + it("Account.create로 생성된 account가 저장된다") { val dto = AccountDTO.Save("설명", 100_000, 40) - val account = AccountTestFixture.createAccount() every { accountGetService.validate(40) } returns false every { cardinalGetService.findByAdminSide(40) } returns mockk() - every { accountMapper.from(dto) } returns account useCase.save(dto) - verify(exactly = 1) { accountSaveService.save(account) } + verify(exactly = 1) { accountSaveService.save(any()) } } } } diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt index 2d98af37..b0517264 100644 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt @@ -7,6 +7,7 @@ import com.weeth.domain.account.domain.service.ReceiptDeleteService import com.weeth.domain.account.domain.service.ReceiptGetService import com.weeth.domain.account.domain.service.ReceiptSaveService import com.weeth.domain.account.domain.service.ReceiptUpdateService +import com.weeth.domain.account.domain.vo.Money import com.weeth.domain.account.fixture.AccountTestFixture import com.weeth.domain.account.fixture.ReceiptTestFixture import com.weeth.domain.file.application.dto.request.FileSaveRequest @@ -66,9 +67,9 @@ class ReceiptUseCaseImplTest : describe("save") { context("파일이 있는 경우") { - it("영수증 저장 후 account.spend와 fileRepository.saveAll이 호출된다") { + it("영수증 저장 후 fileRepository.saveAll이 호출된다") { val account = AccountTestFixture.createAccount(cardinal = 40) - val receipt = ReceiptTestFixture.createReceipt(id = 10L, amount = 5_000, account = account) + val savedReceipt = ReceiptTestFixture.createReceipt(id = 10L, amount = 5_000, account = account) val files = listOf(mockk()) val dto = ReceiptDTO.Save( @@ -82,13 +83,12 @@ class ReceiptUseCaseImplTest : every { cardinalGetService.findByAdminSide(40) } returns mockk() every { accountGetService.find(40) } returns account - every { receiptMapper.from(dto, account) } returns receipt - every { receiptSaveService.save(receipt) } returns receipt - every { fileMapper.toFileList(dto.files(), FileOwnerType.RECEIPT, receipt.id) } returns files + every { receiptSaveService.save(any()) } returns savedReceipt + every { fileMapper.toFileList(dto.files(), FileOwnerType.RECEIPT, savedReceipt.id) } returns files useCase.save(dto) - verify(exactly = 1) { receiptSaveService.save(receipt) } + verify(exactly = 1) { receiptSaveService.save(any()) } verify(exactly = 1) { fileRepository.saveAll(files) } } } @@ -96,18 +96,17 @@ class ReceiptUseCaseImplTest : context("파일이 없는 경우") { it("영수증 저장 후 fileRepository.saveAll은 빈 리스트로 호출된다") { val account = AccountTestFixture.createAccount(cardinal = 40) - val receipt = ReceiptTestFixture.createReceipt(id = 11L, amount = 3_000, account = account) + val savedReceipt = ReceiptTestFixture.createReceipt(id = 11L, amount = 3_000, account = account) val dto = ReceiptDTO.Save("교통비", "지하철", 3_000, LocalDate.of(2024, 9, 2), 40, emptyList()) every { cardinalGetService.findByAdminSide(40) } returns mockk() every { accountGetService.find(40) } returns account - every { receiptMapper.from(dto, account) } returns receipt - every { receiptSaveService.save(receipt) } returns receipt - every { fileMapper.toFileList(emptyList(), FileOwnerType.RECEIPT, receipt.id) } returns emptyList() + every { receiptSaveService.save(any()) } returns savedReceipt + every { fileMapper.toFileList(emptyList(), FileOwnerType.RECEIPT, savedReceipt.id) } returns emptyList() useCase.save(dto) - verify(exactly = 1) { receiptSaveService.save(receipt) } + verify(exactly = 1) { receiptSaveService.save(any()) } verify(exactly = 1) { fileRepository.saveAll(emptyList()) } } } @@ -118,7 +117,7 @@ class ReceiptUseCaseImplTest : val receiptId = 10L val account = AccountTestFixture.createAccount(cardinal = 40) val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = account) - account.spend(receipt) + account.spend(Money.of(receipt.amount)) // adjustSpend를 위한 사전 spend val dto = ReceiptDTO.Update( @@ -146,11 +145,11 @@ class ReceiptUseCaseImplTest : } describe("delete") { - it("관련 파일 삭제 후 account.cancel이 호출되고 영수증이 삭제된다") { + it("관련 파일 삭제 후 cancelSpend가 호출되고 영수증이 삭제된다") { val receiptId = 5L val account = AccountTestFixture.createAccount(currentAmount = 100_000) val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 10_000, account = account) - account.spend(receipt) // receipts 목록에 추가해야 cancel이 동작 + account.spend(Money.of(receipt.amount)) // cancelSpend를 위한 사전 spend val files = listOf(mockk()) every { receiptGetService.find(receiptId) } returns receipt diff --git a/src/test/kotlin/com/weeth/domain/account/domain/entity/AccountTest.kt b/src/test/kotlin/com/weeth/domain/account/domain/entity/AccountTest.kt index 7e07286b..0dc3cdd3 100644 --- a/src/test/kotlin/com/weeth/domain/account/domain/entity/AccountTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/domain/entity/AccountTest.kt @@ -1,28 +1,54 @@ package com.weeth.domain.account.domain.entity +import com.weeth.domain.account.domain.vo.Money import com.weeth.domain.account.fixture.AccountTestFixture -import com.weeth.domain.account.fixture.ReceiptTestFixture +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe class AccountTest : StringSpec({ - "spend은 currentAmount를 영수증 금액만큼 감소시킨다" { + "spend은 currentAmount를 Money 금액만큼 감소시킨다" { val account = AccountTestFixture.createAccount(currentAmount = 100_000) - val receipt = ReceiptTestFixture.createReceipt(amount = 10_000, account = account) - account.spend(receipt) + account.spend(Money.of(10_000)) account.currentAmount shouldBe 90_000 } - "cancel은 currentAmount를 영수증 금액만큼 복원한다" { - val account = AccountTestFixture.createAccount(currentAmount = 100_000) - val receipt = ReceiptTestFixture.createReceipt(amount = 10_000, account = account) - account.spend(receipt) + "cancelSpend은 currentAmount를 Money 금액만큼 복원한다" { + val account = AccountTestFixture.createAccount(currentAmount = 90_000) - account.cancel(receipt) + account.cancelSpend(Money.of(10_000)) account.currentAmount shouldBe 100_000 } + + "adjustSpend는 기존 금액을 취소하고 새 금액을 차감한다" { + val account = AccountTestFixture.createAccount(totalAmount = 100_000, currentAmount = 90_000) + + account.adjustSpend(Money.of(10_000), Money.of(20_000)) + + account.currentAmount shouldBe 80_000 + } + + "spend 시 잔액이 부족하면 IllegalStateException을 던진다" { + val account = AccountTestFixture.createAccount(currentAmount = 5_000) + + shouldThrow { account.spend(Money.of(10_000)) } + } + + "cancelSpend 시 총액을 초과하면 IllegalStateException을 던진다" { + val account = AccountTestFixture.createAccount(totalAmount = 100_000, currentAmount = 100_000) + + shouldThrow { account.cancelSpend(Money.of(1)) } + } + + "create는 currentAmount를 totalAmount와 동일하게 초기화한다" { + val account = Account.create("2학기 회비", 200_000, 41) + + account.currentAmount shouldBe 200_000 + account.totalAmount shouldBe 200_000 + account.cardinal shouldBe 41 + } }) diff --git a/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt b/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt index 9537cb26..dd98afa3 100644 --- a/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt @@ -1,6 +1,5 @@ package com.weeth.domain.account.domain.entity -import com.weeth.domain.account.application.dto.ReceiptDTO import com.weeth.domain.account.fixture.ReceiptTestFixture import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe @@ -16,9 +15,8 @@ class ReceiptTest : amount = 5_000, date = LocalDate.of(2024, 1, 1), ) - val dto = ReceiptDTO.Update("새로운 설명", "새 출처", 20_000, LocalDate.of(2025, 6, 1), 40, emptyList()) - receipt.update(dto) + receipt.update("새로운 설명", "새 출처", 20_000, LocalDate.of(2025, 6, 1)) receipt.description shouldBe "새로운 설명" receipt.source shouldBe "새 출처" diff --git a/src/test/kotlin/com/weeth/domain/account/fixture/AccountTestFixture.kt b/src/test/kotlin/com/weeth/domain/account/fixture/AccountTestFixture.kt index ed407387..1b515748 100644 --- a/src/test/kotlin/com/weeth/domain/account/fixture/AccountTestFixture.kt +++ b/src/test/kotlin/com/weeth/domain/account/fixture/AccountTestFixture.kt @@ -10,13 +10,11 @@ object AccountTestFixture { currentAmount: Int = 100_000, cardinal: Int = 40, ): Account = - Account - .builder() - .id(id) - .description(description) - .totalAmount(totalAmount) - .currentAmount(currentAmount) - .cardinal(cardinal) - .receipts(mutableListOf()) - .build() + Account( + id = id, + description = description, + totalAmount = totalAmount, + currentAmount = currentAmount, + cardinal = cardinal, + ) } diff --git a/src/test/kotlin/com/weeth/domain/account/fixture/ReceiptTestFixture.kt b/src/test/kotlin/com/weeth/domain/account/fixture/ReceiptTestFixture.kt index 6df91422..b02c7535 100644 --- a/src/test/kotlin/com/weeth/domain/account/fixture/ReceiptTestFixture.kt +++ b/src/test/kotlin/com/weeth/domain/account/fixture/ReceiptTestFixture.kt @@ -13,13 +13,12 @@ object ReceiptTestFixture { date: LocalDate = LocalDate.of(2024, 9, 1), account: Account = AccountTestFixture.createAccount(), ): Receipt = - Receipt - .builder() - .id(id) - .description(description) - .source(source) - .amount(amount) - .date(date) - .account(account) - .build() + Receipt( + id = id, + description = description, + source = source, + amount = amount, + date = date, + account = account, + ) } From 61a23190562bded3ddb73d097e298c0a63bf7c4e Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 14:51:36 +0900 Subject: [PATCH 05/26] =?UTF-8?q?refactor:=20Account=20dto,=20mapper=20jav?= =?UTF-8?q?a=20->=20kotlin=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/account/application/dto/request/AccountSaveRequest.kt} | 0 .../domain/account/application/dto/request/ReceiptSaveRequest.kt} | 0 .../com/weeth/domain/account/application/mapper/AccountMapper.kt} | 0 .../com/weeth/domain/account/application/mapper/ReceiptMapper.kt} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/main/{java/com/weeth/domain/account/application/dto/AccountDTO.java => kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt} (100%) rename src/main/{java/com/weeth/domain/account/application/dto/ReceiptDTO.java => kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt} (100%) rename src/main/{java/com/weeth/domain/account/application/mapper/AccountMapper.java => kotlin/com/weeth/domain/account/application/mapper/AccountMapper.kt} (100%) rename src/main/{java/com/weeth/domain/account/application/mapper/ReceiptMapper.java => kotlin/com/weeth/domain/account/application/mapper/ReceiptMapper.kt} (100%) diff --git a/src/main/java/com/weeth/domain/account/application/dto/AccountDTO.java b/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/application/dto/AccountDTO.java rename to src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt diff --git a/src/main/java/com/weeth/domain/account/application/dto/ReceiptDTO.java b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/application/dto/ReceiptDTO.java rename to src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt diff --git a/src/main/java/com/weeth/domain/account/application/mapper/AccountMapper.java b/src/main/kotlin/com/weeth/domain/account/application/mapper/AccountMapper.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/application/mapper/AccountMapper.java rename to src/main/kotlin/com/weeth/domain/account/application/mapper/AccountMapper.kt diff --git a/src/main/java/com/weeth/domain/account/application/mapper/ReceiptMapper.java b/src/main/kotlin/com/weeth/domain/account/application/mapper/ReceiptMapper.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/application/mapper/ReceiptMapper.java rename to src/main/kotlin/com/weeth/domain/account/application/mapper/ReceiptMapper.kt From 05975a21b2d0a22592b81b72ab937db115b04de7 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:04:01 +0900 Subject: [PATCH 06/26] =?UTF-8?q?refactor:=20Account=20dto=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20kotlin=20=EB=AC=B8=EB=B2=95=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/AccountSaveRequest.kt | 38 +++++------ .../dto/request/ReceiptSaveRequest.kt | 65 +++++++------------ .../dto/request/ReceiptUpdateRequest.kt | 27 ++++++++ .../dto/response/AccountResponse.kt | 21 ++++++ .../dto/response/ReceiptResponse.kt | 20 ++++++ 5 files changed, 108 insertions(+), 63 deletions(-) create mode 100644 src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt create mode 100644 src/main/kotlin/com/weeth/domain/account/application/dto/response/AccountResponse.kt create mode 100644 src/main/kotlin/com/weeth/domain/account/application/dto/response/ReceiptResponse.kt diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt index a9794584..46672a54 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt @@ -1,25 +1,17 @@ -package com.weeth.domain.account.application.dto; +package com.weeth.domain.account.application.dto.request -import jakarta.validation.constraints.NotNull; +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Positive -import java.time.LocalDateTime; -import java.util.List; - -public class AccountDTO { - - public record Response( - Long accountId, - String description, - Integer totalAmount, - Integer currentAmount, - LocalDateTime time, - Integer cardinal, - List receipts - ) {} - - public record Save( - String description, - @NotNull Integer totalAmount, - @NotNull Integer cardinal - ) {} -} +data class AccountSaveRequest( + @field:Schema(description = "회비 설명", example = "2024년 2학기 회비") + val description: String, + @field:Schema(description = "총 금액", example = "100000") + @field:NotNull + @field:Positive + val totalAmount: Int, + @field:Schema(description = "기수", example = "40") + @field:NotNull + val cardinal: Int, +) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt index 71ff3974..2f62ba93 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt @@ -1,42 +1,27 @@ -package com.weeth.domain.account.application.dto; +package com.weeth.domain.account.application.dto.request -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import com.weeth.domain.file.application.dto.request.FileSaveRequest; -import com.weeth.domain.file.application.dto.response.FileResponse; +import com.weeth.domain.file.application.dto.request.FileSaveRequest +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Positive +import java.time.LocalDate -import java.time.LocalDate; -import java.util.List; - -public class ReceiptDTO { - - public record Response( - Long id, - String description, - String source, - Integer amount, - LocalDate date, - List fileUrls - ) { - } - - public record Save( - String description, - String source, - @NotNull Integer amount, - @NotNull LocalDate date, - @NotNull Integer cardinal, - @Valid List<@NotNull FileSaveRequest> files - ) { - } - - public record Update( - String description, - String source, - @NotNull Integer amount, - @NotNull LocalDate date, - @NotNull Integer cardinal, - @Valid List<@NotNull FileSaveRequest> files - ) { - } -} +data class ReceiptSaveRequest( + @field:Schema(description = "영수증 설명", example = "간식비") + val description: String?, + @field:Schema(description = "출처", example = "편의점") + val source: String?, + @field:Schema(description = "사용 금액", example = "10000") + @field:NotNull + @field:Positive + val amount: Int, + @field:Schema(description = "사용 날짜", example = "2024-09-01") + @field:NotNull + val date: LocalDate, + @field:Schema(description = "기수", example = "40") + @field:NotNull + val cardinal: Int, + @field:Valid + val files: List<@NotNull FileSaveRequest>?, +) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt new file mode 100644 index 00000000..d6f52f3a --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt @@ -0,0 +1,27 @@ +package com.weeth.domain.account.application.dto.request + +import com.weeth.domain.file.application.dto.request.FileSaveRequest +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Positive +import java.time.LocalDate + +data class ReceiptUpdateRequest( + @field:Schema(description = "영수증 설명", example = "간식비") + val description: String?, + @field:Schema(description = "출처", example = "편의점") + val source: String?, + @field:Schema(description = "사용 금액", example = "10000") + @field:NotNull + @field:Positive + val amount: Int, + @field:Schema(description = "사용 날짜", example = "2024-09-01") + @field:NotNull + val date: LocalDate, + @field:Schema(description = "기수", example = "40") + @field:NotNull + val cardinal: Int, + @field:Valid + val files: List<@NotNull FileSaveRequest>?, +) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/response/AccountResponse.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/response/AccountResponse.kt new file mode 100644 index 00000000..3ac8b44d --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/response/AccountResponse.kt @@ -0,0 +1,21 @@ +package com.weeth.domain.account.application.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class AccountResponse( + @field:Schema(description = "회비 ID", example = "1") + val accountId: Long, + @field:Schema(description = "회비 설명", example = "2024년 2학기 회비") + val description: String, + @field:Schema(description = "총 금액", example = "100000") + val totalAmount: Int, + @field:Schema(description = "현재 금액", example = "90000") + val currentAmount: Int, + @field:Schema(description = "최종 수정 시각") + val time: LocalDateTime?, + @field:Schema(description = "기수", example = "40") + val cardinal: Int, + @field:Schema(description = "영수증 목록") + val receipts: List, +) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/response/ReceiptResponse.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/response/ReceiptResponse.kt new file mode 100644 index 00000000..df8d1d7b --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/response/ReceiptResponse.kt @@ -0,0 +1,20 @@ +package com.weeth.domain.account.application.dto.response + +import com.weeth.domain.file.application.dto.response.FileResponse +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +data class ReceiptResponse( + @field:Schema(description = "영수증 ID", example = "1") + val id: Long, + @field:Schema(description = "영수증 설명", example = "간식비") + val description: String?, + @field:Schema(description = "출처", example = "편의점") + val source: String?, + @field:Schema(description = "사용 금액", example = "10000") + val amount: Int, + @field:Schema(description = "사용 날짜", example = "2024-09-01") + val date: LocalDate, + @field:Schema(description = "첨부 파일 목록") + val fileUrls: List, +) From 650324388458a6b2aed71f0f434b0e4d68fbb4d3 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:04:48 +0900 Subject: [PATCH 07/26] =?UTF-8?q?refactor:=20Account=20mapper=20=EC=88=98?= =?UTF-8?q?=EB=8F=99=20mapper,=20=20kotlin=20=EB=AC=B8=EB=B2=95=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mapper/AccountMapper.kt | 35 ++++++++------- .../application/mapper/ReceiptMapper.kt | 43 +++++++++++-------- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/main/kotlin/com/weeth/domain/account/application/mapper/AccountMapper.kt b/src/main/kotlin/com/weeth/domain/account/application/mapper/AccountMapper.kt index b6ae4f3a..67fac93b 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/mapper/AccountMapper.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/mapper/AccountMapper.kt @@ -1,18 +1,23 @@ -package com.weeth.domain.account.application.mapper; +package com.weeth.domain.account.application.mapper -import com.weeth.domain.account.application.dto.AccountDTO; -import com.weeth.domain.account.application.dto.ReceiptDTO; -import com.weeth.domain.account.domain.entity.Account; -import org.mapstruct.*; - -import java.util.List; - -@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface AccountMapper { - - @Mapping(target = "accountId", source = "account.id") - @Mapping(target = "receipts", source = "receipts") - @Mapping(target = "time", source = "account.modifiedAt") - AccountDTO.Response to(Account account, List receipts); +import com.weeth.domain.account.application.dto.response.AccountResponse +import com.weeth.domain.account.application.dto.response.ReceiptResponse +import com.weeth.domain.account.domain.entity.Account +import org.springframework.stereotype.Component +@Component +class AccountMapper { + fun toResponse( + account: Account, + receipts: List, + ): AccountResponse = + AccountResponse( + accountId = account.id, + description = account.description, + totalAmount = account.totalAmount, + currentAmount = account.currentAmount, + time = account.modifiedAt, + cardinal = account.cardinal, + receipts = receipts, + ) } diff --git a/src/main/kotlin/com/weeth/domain/account/application/mapper/ReceiptMapper.kt b/src/main/kotlin/com/weeth/domain/account/application/mapper/ReceiptMapper.kt index 730defbe..9999da3a 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/mapper/ReceiptMapper.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/mapper/ReceiptMapper.kt @@ -1,21 +1,30 @@ -package com.weeth.domain.account.application.mapper; +package com.weeth.domain.account.application.mapper -import com.weeth.domain.account.application.dto.ReceiptDTO; -import com.weeth.domain.account.domain.entity.Account; -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.file.application.dto.response.FileResponse; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.MappingConstants; -import org.mapstruct.ReportingPolicy; +import com.weeth.domain.account.application.dto.response.ReceiptResponse +import com.weeth.domain.account.domain.entity.Receipt +import com.weeth.domain.file.application.dto.response.FileResponse +import org.springframework.stereotype.Component -import java.util.List; - -@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface ReceiptMapper { - - List to(List account); - - ReceiptDTO.Response to(Receipt receipt, List fileUrls); +@Component +class ReceiptMapper { + fun toResponse( + receipt: Receipt, + fileUrls: List, + ): ReceiptResponse = + ReceiptResponse( + id = receipt.id, + description = receipt.description, + source = receipt.source, + amount = receipt.amount, + date = receipt.date, + fileUrls = fileUrls, + ) + fun toResponses( + receipts: List, + filesByReceiptId: Map>, + ): List = + receipts.map { receipt -> + toResponse(receipt, filesByReceiptId[receipt.id] ?: emptyList()) + } } From 5760d2ba8ae5eab2e0cd30bb8aca7acf1f99fd83 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:05:29 +0900 Subject: [PATCH 08/26] =?UTF-8?q?refactor:=20dto,=20mapper=20=EC=BD=94?= =?UTF-8?q?=ED=8B=80=EB=A6=B0=20=EB=AC=B8=EB=B2=95=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=B0=B8=EC=A1=B0=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/usecase/AccountUseCase.java | 7 +++--- .../usecase/AccountUseCaseImpl.java | 23 +++++++++-------- .../application/usecase/ReceiptUseCase.java | 7 +++--- .../usecase/ReceiptUseCaseImpl.java | 25 ++++++++++--------- .../domain/service/ReceiptUpdateService.java | 8 +++--- .../presentation/AccountAdminController.java | 4 +-- .../presentation/AccountController.java | 4 +-- .../presentation/ReceiptAdminController.java | 7 +++--- .../usecase/AccountUseCaseImplTest.kt | 17 +++++++------ .../usecase/ReceiptUseCaseImplTest.kt | 17 +++++++------ 10 files changed, 63 insertions(+), 56 deletions(-) diff --git a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCase.java b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCase.java index a9eb972b..04058266 100644 --- a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCase.java +++ b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCase.java @@ -1,9 +1,10 @@ package com.weeth.domain.account.application.usecase; -import com.weeth.domain.account.application.dto.AccountDTO; +import com.weeth.domain.account.application.dto.request.AccountSaveRequest; +import com.weeth.domain.account.application.dto.response.AccountResponse; public interface AccountUseCase { - AccountDTO.Response find(Integer cardinal); + AccountResponse find(Integer cardinal); - void save(AccountDTO.Save dto); + void save(AccountSaveRequest dto); } diff --git a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java index 4a333c4e..8b5c4c48 100644 --- a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java +++ b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java @@ -1,7 +1,8 @@ package com.weeth.domain.account.application.usecase; -import com.weeth.domain.account.application.dto.AccountDTO; -import com.weeth.domain.account.application.dto.ReceiptDTO; +import com.weeth.domain.account.application.dto.request.AccountSaveRequest; +import com.weeth.domain.account.application.dto.response.AccountResponse; +import com.weeth.domain.account.application.dto.response.ReceiptResponse; import com.weeth.domain.account.application.exception.AccountExistsException; import com.weeth.domain.account.application.mapper.AccountMapper; import com.weeth.domain.account.application.mapper.ReceiptMapper; @@ -36,27 +37,27 @@ public class AccountUseCaseImpl implements AccountUseCase { private final FileMapper fileMapper; @Override - public AccountDTO.Response find(Integer cardinal) { + public AccountResponse find(Integer cardinal) { Account account = accountGetService.find(cardinal); List receipts = receiptGetService.findAllByAccountId(account.getId()); - List response = receipts.stream() - .map(receipt -> receiptMapper.to(receipt, getFiles(receipt.getId()))) + List response = receipts.stream() + .map(receipt -> receiptMapper.toResponse(receipt, getFiles(receipt.getId()))) .toList(); - return accountMapper.to(account, response); + return accountMapper.toResponse(account, response); } @Override @Transactional - public void save(AccountDTO.Save dto) { + public void save(AccountSaveRequest dto) { validate(dto); - cardinalGetService.findByAdminSide(dto.cardinal()); + cardinalGetService.findByAdminSide(dto.getCardinal()); - accountSaveService.save(Account.Companion.create(dto.description(), dto.totalAmount(), dto.cardinal())); + accountSaveService.save(Account.create(dto.getDescription(), dto.getTotalAmount(), dto.getCardinal())); } - private void validate(AccountDTO.Save dto) { - if (accountGetService.validate(dto.cardinal())) + private void validate(AccountSaveRequest dto) { + if (accountGetService.validate(dto.getCardinal())) throw new AccountExistsException(); } diff --git a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCase.java b/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCase.java index 855a24a2..e6252bcd 100644 --- a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCase.java +++ b/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCase.java @@ -1,11 +1,12 @@ package com.weeth.domain.account.application.usecase; -import com.weeth.domain.account.application.dto.ReceiptDTO; +import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest; +import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest; public interface ReceiptUseCase { - void save(ReceiptDTO.Save dto); + void save(ReceiptSaveRequest dto); - void update(Long receiptId, ReceiptDTO.Update dto); + void update(Long receiptId, ReceiptUpdateRequest dto); void delete(Long id); } diff --git a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java b/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java index 72750f7c..962a65b4 100644 --- a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java +++ b/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java @@ -1,7 +1,8 @@ package com.weeth.domain.account.application.usecase; import jakarta.transaction.Transactional; -import com.weeth.domain.account.application.dto.ReceiptDTO; +import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest; +import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest; import com.weeth.domain.account.application.mapper.ReceiptMapper; import com.weeth.domain.account.domain.entity.Account; import com.weeth.domain.account.domain.entity.Receipt; @@ -38,31 +39,31 @@ public class ReceiptUseCaseImpl implements ReceiptUseCase { @Override @Transactional - public void save(ReceiptDTO.Save dto) { - cardinalGetService.findByAdminSide(dto.cardinal()); + public void save(ReceiptSaveRequest dto) { + cardinalGetService.findByAdminSide(dto.getCardinal()); - Account account = accountGetService.find(dto.cardinal()); + Account account = accountGetService.find(dto.getCardinal()); Receipt receipt = receiptSaveService.save( - Receipt.Companion.create(dto.description(), dto.source(), dto.amount(), dto.date(), account) + Receipt.Companion.create(dto.getDescription(), dto.getSource(), dto.getAmount(), dto.getDate(), account) ); - account.spend(dto.amount()); + account.spend(dto.getAmount()); - List files = fileMapper.toFileList(dto.files(), FileOwnerType.RECEIPT, receipt.getId()); + List files = fileMapper.toFileList(dto.getFiles(), FileOwnerType.RECEIPT, receipt.getId()); fileRepository.saveAll(files); } @Override @Transactional - public void update(Long receiptId, ReceiptDTO.Update dto) { - Account account = accountGetService.find(dto.cardinal()); + public void update(Long receiptId, ReceiptUpdateRequest dto) { + Account account = accountGetService.find(dto.getCardinal()); Receipt receipt = receiptGetService.find(receiptId); - account.adjustSpend(receipt.getAmount(), dto.amount()); + account.adjustSpend(receipt.getAmount(), dto.getAmount()); - if (!dto.files().isEmpty()) { // 업데이트하려는 파일이 있다면 파일을 전체 삭제한 뒤 저장 + if (dto.getFiles() != null && !dto.getFiles().isEmpty()) { List fileList = getFiles(receiptId); fileRepository.deleteAll(fileList); - List files = fileMapper.toFileList(dto.files(), FileOwnerType.RECEIPT, receipt.getId()); + List files = fileMapper.toFileList(dto.getFiles(), FileOwnerType.RECEIPT, receipt.getId()); fileRepository.saveAll(files); } receiptUpdateService.update(receipt, dto); diff --git a/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java b/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java index 2d4a1f6a..82442c25 100644 --- a/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java +++ b/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java @@ -1,6 +1,6 @@ package com.weeth.domain.account.domain.service; -import com.weeth.domain.account.application.dto.ReceiptDTO; +import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest; import com.weeth.domain.account.domain.entity.Receipt; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -8,7 +8,7 @@ @Service @RequiredArgsConstructor public class ReceiptUpdateService { - public void update(Receipt receipt, ReceiptDTO.Update dto) { - receipt.update(dto.description(), dto.source(), dto.amount(), dto.date()); + public void update(Receipt receipt, ReceiptUpdateRequest dto) { + receipt.update(dto.getDescription(), dto.getSource(), dto.getAmount(), dto.getDate()); } -} \ No newline at end of file +} diff --git a/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java b/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java index bf1c565f..9b109651 100644 --- a/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java +++ b/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java @@ -1,6 +1,6 @@ package com.weeth.domain.account.presentation; -import com.weeth.domain.account.application.dto.AccountDTO; +import com.weeth.domain.account.application.dto.request.AccountSaveRequest; import com.weeth.domain.account.application.exception.AccountErrorCode; import com.weeth.domain.account.application.usecase.AccountUseCase; import com.weeth.global.common.exception.ApiErrorCodeExample; @@ -27,7 +27,7 @@ public class AccountAdminController { @PostMapping @Operation(summary="회비 총 금액 기입") - public CommonResponse save(@RequestBody @Valid AccountDTO.Save dto) { + public CommonResponse save(@RequestBody @Valid AccountSaveRequest dto) { accountUseCase.save(dto); return CommonResponse.success(ACCOUNT_SAVE_SUCCESS); } diff --git a/src/main/java/com/weeth/domain/account/presentation/AccountController.java b/src/main/java/com/weeth/domain/account/presentation/AccountController.java index 1cb72b9d..7151f7e4 100644 --- a/src/main/java/com/weeth/domain/account/presentation/AccountController.java +++ b/src/main/java/com/weeth/domain/account/presentation/AccountController.java @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import com.weeth.domain.account.application.dto.AccountDTO; +import com.weeth.domain.account.application.dto.response.AccountResponse; import com.weeth.domain.account.application.exception.AccountErrorCode; import com.weeth.domain.account.application.usecase.AccountUseCase; import com.weeth.global.common.exception.ApiErrorCodeExample; @@ -25,7 +25,7 @@ public class AccountController { @GetMapping("/{cardinal}") @Operation(summary="회비 내역 조회") - public CommonResponse find(@PathVariable Integer cardinal) { + public CommonResponse find(@PathVariable Integer cardinal) { return CommonResponse.success(ACCOUNT_FIND_SUCCESS,accountUseCase.find(cardinal)); } } diff --git a/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java b/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java index c9dfb434..de75f5c9 100644 --- a/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java +++ b/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java @@ -3,7 +3,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import com.weeth.domain.account.application.dto.ReceiptDTO; +import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest; +import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest; import com.weeth.domain.account.application.exception.AccountErrorCode; import com.weeth.domain.account.application.usecase.ReceiptUseCase; import com.weeth.global.common.exception.ApiErrorCodeExample; @@ -23,7 +24,7 @@ public class ReceiptAdminController { @PostMapping @Operation(summary="회비 사용 내역 기입") - public CommonResponse save(@RequestBody @Valid ReceiptDTO.Save dto) { + public CommonResponse save(@RequestBody @Valid ReceiptSaveRequest dto) { receiptUseCase.save(dto); return CommonResponse.success(RECEIPT_SAVE_SUCCESS); } @@ -37,7 +38,7 @@ public CommonResponse delete(@PathVariable Long receiptId) { @PatchMapping("/{receiptId}") @Operation(summary="회비 사용 내역 수정") - public CommonResponse update(@PathVariable Long receiptId, @RequestBody @Valid ReceiptDTO.Update dto) { + public CommonResponse update(@PathVariable Long receiptId, @RequestBody @Valid ReceiptUpdateRequest dto) { receiptUseCase.update(receiptId, dto); return CommonResponse.success(RECEIPT_UPDATE_SUCCESS); } diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt index 945acc73..fc435e32 100644 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt @@ -1,7 +1,8 @@ package com.weeth.domain.account.application.usecase -import com.weeth.domain.account.application.dto.AccountDTO -import com.weeth.domain.account.application.dto.ReceiptDTO +import com.weeth.domain.account.application.dto.request.AccountSaveRequest +import com.weeth.domain.account.application.dto.response.AccountResponse +import com.weeth.domain.account.application.dto.response.ReceiptResponse import com.weeth.domain.account.application.exception.AccountExistsException import com.weeth.domain.account.application.exception.AccountNotFoundException import com.weeth.domain.account.application.mapper.AccountMapper @@ -66,15 +67,15 @@ class AccountUseCaseImplTest : val account = AccountTestFixture.createAccount(cardinal = 40) val receipt = ReceiptTestFixture.createReceipt(id = 10L, amount = 5_000, account = account) val fileResponse = mockk() - val receiptResponse = mockk() - val accountResponse = mockk() + val receiptResponse = mockk() + val accountResponse = mockk() every { accountGetService.find(40) } returns account every { receiptGetService.findAllByAccountId(account.id) } returns listOf(receipt) every { fileReader.findAll(FileOwnerType.RECEIPT, receipt.id, null) } returns listOf(mockk()) every { fileMapper.toFileResponse(any()) } returns fileResponse - every { receiptMapper.to(receipt, listOf(fileResponse)) } returns receiptResponse - every { accountMapper.to(account, listOf(receiptResponse)) } returns accountResponse + every { receiptMapper.toResponse(receipt, listOf(fileResponse)) } returns receiptResponse + every { accountMapper.toResponse(account, listOf(receiptResponse)) } returns accountResponse val result = useCase.find(40) @@ -94,7 +95,7 @@ class AccountUseCaseImplTest : describe("save") { context("이미 존재하는 기수로 저장 시") { it("AccountExistsException을 던진다") { - val dto = AccountDTO.Save("설명", 100_000, 40) + val dto = AccountSaveRequest("설명", 100_000, 40) every { accountGetService.validate(40) } returns true shouldThrow { useCase.save(dto) } @@ -103,7 +104,7 @@ class AccountUseCaseImplTest : context("정상 저장 시") { it("Account.create로 생성된 account가 저장된다") { - val dto = AccountDTO.Save("설명", 100_000, 40) + val dto = AccountSaveRequest("설명", 100_000, 40) every { accountGetService.validate(40) } returns false every { cardinalGetService.findByAdminSide(40) } returns mockk() diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt index b0517264..4a121fa8 100644 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt @@ -1,6 +1,7 @@ package com.weeth.domain.account.application.usecase -import com.weeth.domain.account.application.dto.ReceiptDTO +import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest +import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest import com.weeth.domain.account.application.mapper.ReceiptMapper import com.weeth.domain.account.domain.service.AccountGetService import com.weeth.domain.account.domain.service.ReceiptDeleteService @@ -72,7 +73,7 @@ class ReceiptUseCaseImplTest : val savedReceipt = ReceiptTestFixture.createReceipt(id = 10L, amount = 5_000, account = account) val files = listOf(mockk()) val dto = - ReceiptDTO.Save( + ReceiptSaveRequest( "간식비", "편의점", 5_000, @@ -84,7 +85,7 @@ class ReceiptUseCaseImplTest : every { cardinalGetService.findByAdminSide(40) } returns mockk() every { accountGetService.find(40) } returns account every { receiptSaveService.save(any()) } returns savedReceipt - every { fileMapper.toFileList(dto.files(), FileOwnerType.RECEIPT, savedReceipt.id) } returns files + every { fileMapper.toFileList(dto.files, FileOwnerType.RECEIPT, savedReceipt.id) } returns files useCase.save(dto) @@ -97,7 +98,7 @@ class ReceiptUseCaseImplTest : it("영수증 저장 후 fileRepository.saveAll은 빈 리스트로 호출된다") { val account = AccountTestFixture.createAccount(cardinal = 40) val savedReceipt = ReceiptTestFixture.createReceipt(id = 11L, amount = 3_000, account = account) - val dto = ReceiptDTO.Save("교통비", "지하철", 3_000, LocalDate.of(2024, 9, 2), 40, emptyList()) + val dto = ReceiptSaveRequest("교통비", "지하철", 3_000, LocalDate.of(2024, 9, 2), 40, emptyList()) every { cardinalGetService.findByAdminSide(40) } returns mockk() every { accountGetService.find(40) } returns account @@ -120,7 +121,7 @@ class ReceiptUseCaseImplTest : account.spend(Money.of(receipt.amount)) // adjustSpend를 위한 사전 spend val dto = - ReceiptDTO.Update( + ReceiptUpdateRequest( "desc", "source", 2_000, @@ -131,16 +132,16 @@ class ReceiptUseCaseImplTest : val oldFiles = listOf(mockk()) val newFiles = listOf(mockk()) - every { accountGetService.find(dto.cardinal()) } returns account + every { accountGetService.find(dto.cardinal) } returns account every { receiptGetService.find(receiptId) } returns receipt every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns oldFiles - every { fileMapper.toFileList(dto.files(), FileOwnerType.RECEIPT, receiptId) } returns newFiles + every { fileMapper.toFileList(dto.files, FileOwnerType.RECEIPT, receiptId) } returns newFiles useCase.update(receiptId, dto) verify(exactly = 1) { fileRepository.deleteAll(oldFiles) } verify(exactly = 1) { fileRepository.saveAll(newFiles) } - verify(exactly = 1) { receiptUpdateService.update(receipt, dto) } + verify(exactly = 1) { receiptUpdateService.update(receipt, any()) } } } From e5186c6bd89ca53bb682eda035180b1a64d80c18 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:14:00 +0900 Subject: [PATCH 09/26] =?UTF-8?q?refactor:=20Account=20repository=20java?= =?UTF-8?q?=20->=20kotlin=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../weeth/domain/account/domain/repository/AccountRepository.kt} | 0 .../weeth/domain/account/domain/repository/ReceiptRepository.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/main/{java/com/weeth/domain/account/domain/repository/AccountRepository.java => kotlin/com/weeth/domain/account/domain/repository/AccountRepository.kt} (100%) rename src/main/{java/com/weeth/domain/account/domain/repository/ReceiptRepository.java => kotlin/com/weeth/domain/account/domain/repository/ReceiptRepository.kt} (100%) diff --git a/src/main/java/com/weeth/domain/account/domain/repository/AccountRepository.java b/src/main/kotlin/com/weeth/domain/account/domain/repository/AccountRepository.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/domain/repository/AccountRepository.java rename to src/main/kotlin/com/weeth/domain/account/domain/repository/AccountRepository.kt diff --git a/src/main/java/com/weeth/domain/account/domain/repository/ReceiptRepository.java b/src/main/kotlin/com/weeth/domain/account/domain/repository/ReceiptRepository.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/domain/repository/ReceiptRepository.java rename to src/main/kotlin/com/weeth/domain/account/domain/repository/ReceiptRepository.kt From 190330ab66cf63e0724e77e1a52e7bc638010a7d Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:15:27 +0900 Subject: [PATCH 10/26] =?UTF-8?q?refactor:=20Account=20repository=20kotlin?= =?UTF-8?q?=20=EB=AC=B8=EB=B2=95=EC=9C=BC=EB=A1=9C=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/AccountRepository.kt | 15 ++++++--------- .../domain/repository/ReceiptRepository.kt | 12 +++++------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/com/weeth/domain/account/domain/repository/AccountRepository.kt b/src/main/kotlin/com/weeth/domain/account/domain/repository/AccountRepository.kt index 0599083f..c7a9daeb 100644 --- a/src/main/kotlin/com/weeth/domain/account/domain/repository/AccountRepository.kt +++ b/src/main/kotlin/com/weeth/domain/account/domain/repository/AccountRepository.kt @@ -1,13 +1,10 @@ -package com.weeth.domain.account.domain.repository; +package com.weeth.domain.account.domain.repository -import com.weeth.domain.account.domain.entity.Account; -import org.springframework.data.jpa.repository.JpaRepository; +import com.weeth.domain.account.domain.entity.Account +import org.springframework.data.jpa.repository.JpaRepository -import java.util.Optional; +interface AccountRepository : JpaRepository { + fun findByCardinal(cardinal: Int): Account? -public interface AccountRepository extends JpaRepository { - - Optional findByCardinal(Integer cardinal); - - boolean existsByCardinal(Integer cardinal); + fun existsByCardinal(cardinal: Int): Boolean } diff --git a/src/main/kotlin/com/weeth/domain/account/domain/repository/ReceiptRepository.kt b/src/main/kotlin/com/weeth/domain/account/domain/repository/ReceiptRepository.kt index 588a79ff..4872fa45 100644 --- a/src/main/kotlin/com/weeth/domain/account/domain/repository/ReceiptRepository.kt +++ b/src/main/kotlin/com/weeth/domain/account/domain/repository/ReceiptRepository.kt @@ -1,10 +1,8 @@ -package com.weeth.domain.account.domain.repository; +package com.weeth.domain.account.domain.repository -import com.weeth.domain.account.domain.entity.Receipt; -import org.springframework.data.jpa.repository.JpaRepository; +import com.weeth.domain.account.domain.entity.Receipt +import org.springframework.data.jpa.repository.JpaRepository -import java.util.List; - -public interface ReceiptRepository extends JpaRepository { - List findAllByAccountIdOrderByCreatedAtDesc(Long accountId); +interface ReceiptRepository : JpaRepository { + fun findAllByAccountIdOrderByCreatedAtDesc(accountId: Long): List } From d8da0547ed6c666ac8882d942191a57a113539ac Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:17:42 +0900 Subject: [PATCH 11/26] =?UTF-8?q?refactor:=20AccountUseCaseImpl=20find()?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20N+1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usecase/AccountUseCaseImpl.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java index 8b5c4c48..98889c34 100644 --- a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java +++ b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java @@ -21,6 +21,8 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -40,10 +42,16 @@ public class AccountUseCaseImpl implements AccountUseCase { public AccountResponse find(Integer cardinal) { Account account = accountGetService.find(cardinal); List receipts = receiptGetService.findAllByAccountId(account.getId()); - List response = receipts.stream() - .map(receipt -> receiptMapper.toResponse(receipt, getFiles(receipt.getId()))) - .toList(); + List receiptIds = receipts.stream().map(Receipt::getId).toList(); + Map> filesByReceiptId = fileReader.findAll(FileOwnerType.RECEIPT, receiptIds, null) + .stream() + .collect(Collectors.groupingBy( + file -> file.getOwnerId(), + Collectors.mapping(fileMapper::toFileResponse, Collectors.toList()) + )); + + List response = receiptMapper.toResponses(receipts, filesByReceiptId); return accountMapper.toResponse(account, response); } @@ -61,9 +69,4 @@ private void validate(AccountSaveRequest dto) { throw new AccountExistsException(); } - private List getFiles(Long receiptId) { - return fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null).stream() - .map(fileMapper::toFileResponse) - .toList(); - } } From 1c06a32b170171e3b4f27f70bee8e17a91b94e61 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:18:35 +0900 Subject: [PATCH 12/26] =?UTF-8?q?refactor:=20Account=20repository=20?= =?UTF-8?q?=EC=BD=94=ED=8B=80=EB=A6=B0=20=EB=AC=B8=EB=B2=95=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/domain/service/AccountGetService.java | 5 +++-- .../application/usecase/AccountUseCaseImplTest.kt | 10 +++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/weeth/domain/account/domain/service/AccountGetService.java b/src/main/java/com/weeth/domain/account/domain/service/AccountGetService.java index bfc948f8..549f15c6 100644 --- a/src/main/java/com/weeth/domain/account/domain/service/AccountGetService.java +++ b/src/main/java/com/weeth/domain/account/domain/service/AccountGetService.java @@ -13,8 +13,9 @@ public class AccountGetService { private final AccountRepository accountRepository; public Account find(Integer cardinal) { - return accountRepository.findByCardinal(cardinal) - .orElseThrow(AccountNotFoundException::new); + Account account = accountRepository.findByCardinal(cardinal); + if (account == null) throw new AccountNotFoundException(); + return account; } public boolean validate(Integer cardinal) { diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt index fc435e32..22a88de0 100644 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt @@ -14,6 +14,7 @@ import com.weeth.domain.account.fixture.AccountTestFixture import com.weeth.domain.account.fixture.ReceiptTestFixture import com.weeth.domain.file.application.dto.response.FileResponse import com.weeth.domain.file.application.mapper.FileMapper +import com.weeth.domain.file.domain.entity.File import com.weeth.domain.file.domain.entity.FileOwnerType import com.weeth.domain.file.domain.repository.FileReader import com.weeth.domain.user.domain.service.CardinalGetService @@ -70,11 +71,14 @@ class AccountUseCaseImplTest : val receiptResponse = mockk() val accountResponse = mockk() + val mockFile = mockk() + every { mockFile.ownerId } returns receipt.id + every { accountGetService.find(40) } returns account every { receiptGetService.findAllByAccountId(account.id) } returns listOf(receipt) - every { fileReader.findAll(FileOwnerType.RECEIPT, receipt.id, null) } returns listOf(mockk()) - every { fileMapper.toFileResponse(any()) } returns fileResponse - every { receiptMapper.toResponse(receipt, listOf(fileResponse)) } returns receiptResponse + every { fileReader.findAll(FileOwnerType.RECEIPT, listOf(receipt.id), null) } returns listOf(mockFile) + every { fileMapper.toFileResponse(mockFile) } returns fileResponse + every { receiptMapper.toResponses(listOf(receipt), any()) } returns listOf(receiptResponse) every { accountMapper.toResponse(account, listOf(receiptResponse)) } returns accountResponse val result = useCase.find(40) From 64312a875efe55ebed34faefe25879da7a2239e2 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:30:36 +0900 Subject: [PATCH 13/26] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20service,=20usecase=20=EA=B4=80=EB=A0=A8=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/usecase/AccountUseCase.java | 10 -- .../usecase/AccountUseCaseImpl.java | 72 -------- .../application/usecase/ReceiptUseCase.java | 12 -- .../usecase/ReceiptUseCaseImpl.java | 87 --------- .../domain/service/AccountGetService.java | 24 --- .../domain/service/AccountSaveService.java | 17 -- .../domain/service/ReceiptDeleteService.java | 17 -- .../domain/service/ReceiptGetService.java | 25 --- .../domain/service/ReceiptSaveService.java | 17 -- .../domain/service/ReceiptUpdateService.java | 14 -- .../usecase/AccountUseCaseImplTest.kt | 121 ------------- .../usecase/ReceiptUseCaseImplTest.kt | 165 ------------------ 12 files changed, 581 deletions(-) delete mode 100644 src/main/java/com/weeth/domain/account/application/usecase/AccountUseCase.java delete mode 100644 src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java delete mode 100644 src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCase.java delete mode 100644 src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java delete mode 100644 src/main/java/com/weeth/domain/account/domain/service/AccountGetService.java delete mode 100644 src/main/java/com/weeth/domain/account/domain/service/AccountSaveService.java delete mode 100644 src/main/java/com/weeth/domain/account/domain/service/ReceiptDeleteService.java delete mode 100644 src/main/java/com/weeth/domain/account/domain/service/ReceiptGetService.java delete mode 100644 src/main/java/com/weeth/domain/account/domain/service/ReceiptSaveService.java delete mode 100644 src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java delete mode 100644 src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt delete mode 100644 src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt diff --git a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCase.java b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCase.java deleted file mode 100644 index 04058266..00000000 --- a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCase.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.weeth.domain.account.application.usecase; - -import com.weeth.domain.account.application.dto.request.AccountSaveRequest; -import com.weeth.domain.account.application.dto.response.AccountResponse; - -public interface AccountUseCase { - AccountResponse find(Integer cardinal); - - void save(AccountSaveRequest dto); -} diff --git a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java b/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java deleted file mode 100644 index 98889c34..00000000 --- a/src/main/java/com/weeth/domain/account/application/usecase/AccountUseCaseImpl.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.weeth.domain.account.application.usecase; - -import com.weeth.domain.account.application.dto.request.AccountSaveRequest; -import com.weeth.domain.account.application.dto.response.AccountResponse; -import com.weeth.domain.account.application.dto.response.ReceiptResponse; -import com.weeth.domain.account.application.exception.AccountExistsException; -import com.weeth.domain.account.application.mapper.AccountMapper; -import com.weeth.domain.account.application.mapper.ReceiptMapper; -import com.weeth.domain.account.domain.entity.Account; -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.account.domain.service.AccountGetService; -import com.weeth.domain.account.domain.service.AccountSaveService; -import com.weeth.domain.account.domain.service.ReceiptGetService; -import com.weeth.domain.file.application.dto.response.FileResponse; -import com.weeth.domain.file.application.mapper.FileMapper; -import com.weeth.domain.file.domain.entity.FileOwnerType; -import com.weeth.domain.file.domain.repository.FileReader; -import com.weeth.domain.user.domain.service.CardinalGetService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class AccountUseCaseImpl implements AccountUseCase { - - private final AccountGetService accountGetService; - private final AccountSaveService accountSaveService; - private final ReceiptGetService receiptGetService; - private final FileReader fileReader; - private final CardinalGetService cardinalGetService; - - private final AccountMapper accountMapper; - private final ReceiptMapper receiptMapper; - private final FileMapper fileMapper; - - @Override - public AccountResponse find(Integer cardinal) { - Account account = accountGetService.find(cardinal); - List receipts = receiptGetService.findAllByAccountId(account.getId()); - - List receiptIds = receipts.stream().map(Receipt::getId).toList(); - Map> filesByReceiptId = fileReader.findAll(FileOwnerType.RECEIPT, receiptIds, null) - .stream() - .collect(Collectors.groupingBy( - file -> file.getOwnerId(), - Collectors.mapping(fileMapper::toFileResponse, Collectors.toList()) - )); - - List response = receiptMapper.toResponses(receipts, filesByReceiptId); - return accountMapper.toResponse(account, response); - } - - @Override - @Transactional - public void save(AccountSaveRequest dto) { - validate(dto); - cardinalGetService.findByAdminSide(dto.getCardinal()); - - accountSaveService.save(Account.create(dto.getDescription(), dto.getTotalAmount(), dto.getCardinal())); - } - - private void validate(AccountSaveRequest dto) { - if (accountGetService.validate(dto.getCardinal())) - throw new AccountExistsException(); - } - -} diff --git a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCase.java b/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCase.java deleted file mode 100644 index e6252bcd..00000000 --- a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCase.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.weeth.domain.account.application.usecase; - -import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest; -import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest; - -public interface ReceiptUseCase { - void save(ReceiptSaveRequest dto); - - void update(Long receiptId, ReceiptUpdateRequest dto); - - void delete(Long id); -} diff --git a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java b/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java deleted file mode 100644 index 962a65b4..00000000 --- a/src/main/java/com/weeth/domain/account/application/usecase/ReceiptUseCaseImpl.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.weeth.domain.account.application.usecase; - -import jakarta.transaction.Transactional; -import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest; -import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest; -import com.weeth.domain.account.application.mapper.ReceiptMapper; -import com.weeth.domain.account.domain.entity.Account; -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.account.domain.service.*; -import com.weeth.domain.file.application.mapper.FileMapper; -import com.weeth.domain.file.domain.entity.File; -import com.weeth.domain.file.domain.entity.FileOwnerType; -import com.weeth.domain.file.domain.repository.FileReader; -import com.weeth.domain.file.domain.repository.FileRepository; -import com.weeth.domain.user.domain.service.CardinalGetService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class ReceiptUseCaseImpl implements ReceiptUseCase { - - private final ReceiptGetService receiptGetService; - private final ReceiptDeleteService receiptDeleteService; - private final ReceiptSaveService receiptSaveService; - private final ReceiptUpdateService receiptUpdateService; - private final AccountGetService accountGetService; - - private final FileReader fileReader; - private final FileRepository fileRepository; - - private final CardinalGetService cardinalGetService; - - private final ReceiptMapper mapper; - private final FileMapper fileMapper; - - - @Override - @Transactional - public void save(ReceiptSaveRequest dto) { - cardinalGetService.findByAdminSide(dto.getCardinal()); - - Account account = accountGetService.find(dto.getCardinal()); - Receipt receipt = receiptSaveService.save( - Receipt.Companion.create(dto.getDescription(), dto.getSource(), dto.getAmount(), dto.getDate(), account) - ); - account.spend(dto.getAmount()); - - List files = fileMapper.toFileList(dto.getFiles(), FileOwnerType.RECEIPT, receipt.getId()); - fileRepository.saveAll(files); - } - - @Override - @Transactional - public void update(Long receiptId, ReceiptUpdateRequest dto) { - Account account = accountGetService.find(dto.getCardinal()); - Receipt receipt = receiptGetService.find(receiptId); - account.adjustSpend(receipt.getAmount(), dto.getAmount()); - - if (dto.getFiles() != null && !dto.getFiles().isEmpty()) { - List fileList = getFiles(receiptId); - fileRepository.deleteAll(fileList); - - List files = fileMapper.toFileList(dto.getFiles(), FileOwnerType.RECEIPT, receipt.getId()); - fileRepository.saveAll(files); - } - receiptUpdateService.update(receipt, dto); - } - - private List getFiles(Long receiptId) { - return fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null); - } - - @Override - @Transactional - public void delete(Long id) { - Receipt receipt = receiptGetService.find(id); - List fileList = fileReader.findAll(FileOwnerType.RECEIPT, id, null); - - receipt.getAccount().cancelSpend(receipt.getAmount()); - - fileRepository.deleteAll(fileList); - receiptDeleteService.delete(receipt); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/AccountGetService.java b/src/main/java/com/weeth/domain/account/domain/service/AccountGetService.java deleted file mode 100644 index 549f15c6..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/AccountGetService.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.domain.entity.Account; -import com.weeth.domain.account.domain.repository.AccountRepository; -import com.weeth.domain.account.application.exception.AccountNotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class AccountGetService { - - private final AccountRepository accountRepository; - - public Account find(Integer cardinal) { - Account account = accountRepository.findByCardinal(cardinal); - if (account == null) throw new AccountNotFoundException(); - return account; - } - - public boolean validate(Integer cardinal) { - return accountRepository.existsByCardinal(cardinal); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/AccountSaveService.java b/src/main/java/com/weeth/domain/account/domain/service/AccountSaveService.java deleted file mode 100644 index d0bf2ccb..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/AccountSaveService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.domain.entity.Account; -import com.weeth.domain.account.domain.repository.AccountRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class AccountSaveService { - - private final AccountRepository accountRepository; - - public void save(Account account) { - accountRepository.save(account); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/ReceiptDeleteService.java b/src/main/java/com/weeth/domain/account/domain/service/ReceiptDeleteService.java deleted file mode 100644 index 7caca70e..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/ReceiptDeleteService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.account.domain.repository.ReceiptRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ReceiptDeleteService { - - private final ReceiptRepository receiptRepository; - - public void delete(Receipt receipt) { - receiptRepository.delete(receipt); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/ReceiptGetService.java b/src/main/java/com/weeth/domain/account/domain/service/ReceiptGetService.java deleted file mode 100644 index 61312284..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/ReceiptGetService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.account.domain.repository.ReceiptRepository; -import com.weeth.domain.account.application.exception.ReceiptNotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class ReceiptGetService { - - private final ReceiptRepository receiptRepository; - - public Receipt find(Long id) { - return receiptRepository.findById(id) - .orElseThrow(ReceiptNotFoundException::new); - } - - public List findAllByAccountId(Long accountId) { - return receiptRepository.findAllByAccountIdOrderByCreatedAtDesc(accountId); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/ReceiptSaveService.java b/src/main/java/com/weeth/domain/account/domain/service/ReceiptSaveService.java deleted file mode 100644 index 22a3933a..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/ReceiptSaveService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.domain.entity.Receipt; -import com.weeth.domain.account.domain.repository.ReceiptRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ReceiptSaveService { - - private final ReceiptRepository receiptRepository; - - public Receipt save(Receipt receipt) { - return receiptRepository.save(receipt); - } -} diff --git a/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java b/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java deleted file mode 100644 index 82442c25..00000000 --- a/src/main/java/com/weeth/domain/account/domain/service/ReceiptUpdateService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.weeth.domain.account.domain.service; - -import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest; -import com.weeth.domain.account.domain.entity.Receipt; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ReceiptUpdateService { - public void update(Receipt receipt, ReceiptUpdateRequest dto) { - receipt.update(dto.getDescription(), dto.getSource(), dto.getAmount(), dto.getDate()); - } -} diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt deleted file mode 100644 index 22a88de0..00000000 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/AccountUseCaseImplTest.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.weeth.domain.account.application.usecase - -import com.weeth.domain.account.application.dto.request.AccountSaveRequest -import com.weeth.domain.account.application.dto.response.AccountResponse -import com.weeth.domain.account.application.dto.response.ReceiptResponse -import com.weeth.domain.account.application.exception.AccountExistsException -import com.weeth.domain.account.application.exception.AccountNotFoundException -import com.weeth.domain.account.application.mapper.AccountMapper -import com.weeth.domain.account.application.mapper.ReceiptMapper -import com.weeth.domain.account.domain.service.AccountGetService -import com.weeth.domain.account.domain.service.AccountSaveService -import com.weeth.domain.account.domain.service.ReceiptGetService -import com.weeth.domain.account.fixture.AccountTestFixture -import com.weeth.domain.account.fixture.ReceiptTestFixture -import com.weeth.domain.file.application.dto.response.FileResponse -import com.weeth.domain.file.application.mapper.FileMapper -import com.weeth.domain.file.domain.entity.File -import com.weeth.domain.file.domain.entity.FileOwnerType -import com.weeth.domain.file.domain.repository.FileReader -import com.weeth.domain.user.domain.service.CardinalGetService -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe -import io.mockk.clearMocks -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify - -class AccountUseCaseImplTest : - DescribeSpec({ - val accountGetService = mockk() - val accountSaveService = mockk(relaxUnitFun = true) - val receiptGetService = mockk() - val fileReader = mockk() - val cardinalGetService = mockk() - val accountMapper = mockk() - val receiptMapper = mockk() - val fileMapper = mockk() - - val useCase = - AccountUseCaseImpl( - accountGetService, - accountSaveService, - receiptGetService, - fileReader, - cardinalGetService, - accountMapper, - receiptMapper, - fileMapper, - ) - - beforeTest { - clearMocks( - accountGetService, - accountSaveService, - receiptGetService, - fileReader, - cardinalGetService, - accountMapper, - receiptMapper, - fileMapper, - ) - } - - describe("find") { - context("존재하는 기수의 회비 조회 시") { - it("영수증과 파일 정보가 포함된 AccountResponse를 반환한다") { - val account = AccountTestFixture.createAccount(cardinal = 40) - val receipt = ReceiptTestFixture.createReceipt(id = 10L, amount = 5_000, account = account) - val fileResponse = mockk() - val receiptResponse = mockk() - val accountResponse = mockk() - - val mockFile = mockk() - every { mockFile.ownerId } returns receipt.id - - every { accountGetService.find(40) } returns account - every { receiptGetService.findAllByAccountId(account.id) } returns listOf(receipt) - every { fileReader.findAll(FileOwnerType.RECEIPT, listOf(receipt.id), null) } returns listOf(mockFile) - every { fileMapper.toFileResponse(mockFile) } returns fileResponse - every { receiptMapper.toResponses(listOf(receipt), any()) } returns listOf(receiptResponse) - every { accountMapper.toResponse(account, listOf(receiptResponse)) } returns accountResponse - - val result = useCase.find(40) - - result shouldBe accountResponse - } - } - - context("존재하지 않는 기수 조회 시") { - it("AccountNotFoundException을 던진다") { - every { accountGetService.find(99) } throws AccountNotFoundException() - - shouldThrow { useCase.find(99) } - } - } - } - - describe("save") { - context("이미 존재하는 기수로 저장 시") { - it("AccountExistsException을 던진다") { - val dto = AccountSaveRequest("설명", 100_000, 40) - every { accountGetService.validate(40) } returns true - - shouldThrow { useCase.save(dto) } - } - } - - context("정상 저장 시") { - it("Account.create로 생성된 account가 저장된다") { - val dto = AccountSaveRequest("설명", 100_000, 40) - every { accountGetService.validate(40) } returns false - every { cardinalGetService.findByAdminSide(40) } returns mockk() - - useCase.save(dto) - - verify(exactly = 1) { accountSaveService.save(any()) } - } - } - } - }) diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt deleted file mode 100644 index 4a121fa8..00000000 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/ReceiptUseCaseImplTest.kt +++ /dev/null @@ -1,165 +0,0 @@ -package com.weeth.domain.account.application.usecase - -import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest -import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest -import com.weeth.domain.account.application.mapper.ReceiptMapper -import com.weeth.domain.account.domain.service.AccountGetService -import com.weeth.domain.account.domain.service.ReceiptDeleteService -import com.weeth.domain.account.domain.service.ReceiptGetService -import com.weeth.domain.account.domain.service.ReceiptSaveService -import com.weeth.domain.account.domain.service.ReceiptUpdateService -import com.weeth.domain.account.domain.vo.Money -import com.weeth.domain.account.fixture.AccountTestFixture -import com.weeth.domain.account.fixture.ReceiptTestFixture -import com.weeth.domain.file.application.dto.request.FileSaveRequest -import com.weeth.domain.file.application.mapper.FileMapper -import com.weeth.domain.file.domain.entity.File -import com.weeth.domain.file.domain.entity.FileOwnerType -import com.weeth.domain.file.domain.repository.FileReader -import com.weeth.domain.file.domain.repository.FileRepository -import com.weeth.domain.user.domain.service.CardinalGetService -import io.kotest.core.spec.style.DescribeSpec -import io.mockk.clearMocks -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import java.time.LocalDate - -class ReceiptUseCaseImplTest : - DescribeSpec({ - val receiptGetService = mockk() - val receiptDeleteService = mockk(relaxUnitFun = true) - val receiptSaveService = mockk() - val receiptUpdateService = mockk(relaxUnitFun = true) - val accountGetService = mockk() - val fileReader = mockk() - val fileRepository = mockk(relaxed = true) - val cardinalGetService = mockk() - val receiptMapper = mockk() - val fileMapper = mockk() - - val useCase = - ReceiptUseCaseImpl( - receiptGetService, - receiptDeleteService, - receiptSaveService, - receiptUpdateService, - accountGetService, - fileReader, - fileRepository, - cardinalGetService, - receiptMapper, - fileMapper, - ) - - beforeTest { - clearMocks( - receiptGetService, - receiptDeleteService, - receiptSaveService, - receiptUpdateService, - accountGetService, - fileReader, - cardinalGetService, - receiptMapper, - fileMapper, - ) - } - - describe("save") { - context("파일이 있는 경우") { - it("영수증 저장 후 fileRepository.saveAll이 호출된다") { - val account = AccountTestFixture.createAccount(cardinal = 40) - val savedReceipt = ReceiptTestFixture.createReceipt(id = 10L, amount = 5_000, account = account) - val files = listOf(mockk()) - val dto = - ReceiptSaveRequest( - "간식비", - "편의점", - 5_000, - LocalDate.of(2024, 9, 1), - 40, - listOf(FileSaveRequest("receipt.png", "TEMP/2024-09/receipt.png", 200L, "image/png")), - ) - - every { cardinalGetService.findByAdminSide(40) } returns mockk() - every { accountGetService.find(40) } returns account - every { receiptSaveService.save(any()) } returns savedReceipt - every { fileMapper.toFileList(dto.files, FileOwnerType.RECEIPT, savedReceipt.id) } returns files - - useCase.save(dto) - - verify(exactly = 1) { receiptSaveService.save(any()) } - verify(exactly = 1) { fileRepository.saveAll(files) } - } - } - - context("파일이 없는 경우") { - it("영수증 저장 후 fileRepository.saveAll은 빈 리스트로 호출된다") { - val account = AccountTestFixture.createAccount(cardinal = 40) - val savedReceipt = ReceiptTestFixture.createReceipt(id = 11L, amount = 3_000, account = account) - val dto = ReceiptSaveRequest("교통비", "지하철", 3_000, LocalDate.of(2024, 9, 2), 40, emptyList()) - - every { cardinalGetService.findByAdminSide(40) } returns mockk() - every { accountGetService.find(40) } returns account - every { receiptSaveService.save(any()) } returns savedReceipt - every { fileMapper.toFileList(emptyList(), FileOwnerType.RECEIPT, savedReceipt.id) } returns emptyList() - - useCase.save(dto) - - verify(exactly = 1) { receiptSaveService.save(any()) } - verify(exactly = 1) { fileRepository.saveAll(emptyList()) } - } - } - } - - describe("update") { - it("업데이트 파일이 있으면 기존 파일을 삭제 후 새 파일을 저장한다") { - val receiptId = 10L - val account = AccountTestFixture.createAccount(cardinal = 40) - val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = account) - account.spend(Money.of(receipt.amount)) // adjustSpend를 위한 사전 spend - - val dto = - ReceiptUpdateRequest( - "desc", - "source", - 2_000, - LocalDate.of(2026, 1, 1), - 40, - listOf(FileSaveRequest("new.png", "TEMP/2026-02/new.png", 100L, "image/png")), - ) - val oldFiles = listOf(mockk()) - val newFiles = listOf(mockk()) - - every { accountGetService.find(dto.cardinal) } returns account - every { receiptGetService.find(receiptId) } returns receipt - every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns oldFiles - every { fileMapper.toFileList(dto.files, FileOwnerType.RECEIPT, receiptId) } returns newFiles - - useCase.update(receiptId, dto) - - verify(exactly = 1) { fileRepository.deleteAll(oldFiles) } - verify(exactly = 1) { fileRepository.saveAll(newFiles) } - verify(exactly = 1) { receiptUpdateService.update(receipt, any()) } - } - } - - describe("delete") { - it("관련 파일 삭제 후 cancelSpend가 호출되고 영수증이 삭제된다") { - val receiptId = 5L - val account = AccountTestFixture.createAccount(currentAmount = 100_000) - val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 10_000, account = account) - account.spend(Money.of(receipt.amount)) // cancelSpend를 위한 사전 spend - val files = listOf(mockk()) - - every { receiptGetService.find(receiptId) } returns receipt - every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns files - - useCase.delete(receiptId) - - verify(exactly = 1) { fileRepository.deleteAll(files) } - verify(exactly = 1) { receiptDeleteService.delete(receipt) } - } - } - }) From 34f3cad7347b04fcf03da66469831e25ae668929 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:31:26 +0900 Subject: [PATCH 14/26] =?UTF-8?q?refactor:=20=EC=95=84=ED=82=A4=ED=85=8D?= =?UTF-8?q?=EC=B2=98=20=EA=B5=AC=EC=A1=B0=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?usecase=20command/query=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usecase/command/ManageAccountUseCase.kt | 22 +++++++ .../usecase/command/ManageReceiptUseCase.kt | 59 +++++++++++++++++++ .../usecase/query/GetAccountQueryService.kt | 34 +++++++++++ 3 files changed, 115 insertions(+) create mode 100644 src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCase.kt create mode 100644 src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt create mode 100644 src/main/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryService.kt diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCase.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCase.kt new file mode 100644 index 00000000..70a54fad --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCase.kt @@ -0,0 +1,22 @@ +package com.weeth.domain.account.application.usecase.command + +import com.weeth.domain.account.application.dto.request.AccountSaveRequest +import com.weeth.domain.account.application.exception.AccountExistsException +import com.weeth.domain.account.domain.entity.Account +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.user.domain.service.CardinalGetService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class ManageAccountUseCase( + private val accountRepository: AccountRepository, + private val cardinalGetService: CardinalGetService, +) { + @Transactional + fun save(request: AccountSaveRequest) { + if (accountRepository.existsByCardinal(request.cardinal)) throw AccountExistsException() + cardinalGetService.findByAdminSide(request.cardinal) + accountRepository.save(Account.create(request.description, request.totalAmount, request.cardinal)) + } +} diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt new file mode 100644 index 00000000..bb07c71a --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt @@ -0,0 +1,59 @@ +package com.weeth.domain.account.application.usecase.command + +import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest +import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest +import com.weeth.domain.account.application.exception.AccountNotFoundException +import com.weeth.domain.account.application.exception.ReceiptNotFoundException +import com.weeth.domain.account.domain.entity.Receipt +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.account.domain.repository.ReceiptRepository +import com.weeth.domain.account.domain.vo.Money +import com.weeth.domain.file.application.mapper.FileMapper +import com.weeth.domain.file.domain.entity.FileOwnerType +import com.weeth.domain.file.domain.repository.FileReader +import com.weeth.domain.file.domain.repository.FileRepository +import com.weeth.domain.user.domain.service.CardinalGetService +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class ManageReceiptUseCase( + private val receiptRepository: ReceiptRepository, + private val accountRepository: AccountRepository, + private val fileReader: FileReader, + private val fileRepository: FileRepository, + private val cardinalGetService: CardinalGetService, + private val fileMapper: FileMapper, +) { + @Transactional + fun save(request: ReceiptSaveRequest) { + cardinalGetService.findByAdminSide(request.cardinal) + val account = accountRepository.findByCardinal(request.cardinal) ?: throw AccountNotFoundException() + val receipt = receiptRepository.save( + Receipt.create(request.description, request.source, request.amount, request.date, account), + ) + account.spend(Money.of(request.amount)) + fileRepository.saveAll(fileMapper.toFileList(request.files, FileOwnerType.RECEIPT, receipt.id)) + } + + @Transactional + fun update(receiptId: Long, request: ReceiptUpdateRequest) { + val account = accountRepository.findByCardinal(request.cardinal) ?: throw AccountNotFoundException() + val receipt = receiptRepository.findByIdOrNull(receiptId) ?: throw ReceiptNotFoundException() + account.adjustSpend(Money.of(receipt.amount), Money.of(request.amount)) + if (!request.files.isNullOrEmpty()) { + fileRepository.deleteAll(fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null)) + fileRepository.saveAll(fileMapper.toFileList(request.files, FileOwnerType.RECEIPT, receiptId)) + } + receipt.update(request.description, request.source, request.amount, request.date) + } + + @Transactional + fun delete(receiptId: Long) { + val receipt = receiptRepository.findByIdOrNull(receiptId) ?: throw ReceiptNotFoundException() + receipt.account.cancelSpend(Money.of(receipt.amount)) + fileRepository.deleteAll(fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null)) + receiptRepository.delete(receipt) + } +} diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryService.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryService.kt new file mode 100644 index 00000000..a8cabb06 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryService.kt @@ -0,0 +1,34 @@ +package com.weeth.domain.account.application.usecase.query + +import com.weeth.domain.account.application.dto.response.AccountResponse +import com.weeth.domain.account.application.exception.AccountNotFoundException +import com.weeth.domain.account.application.mapper.AccountMapper +import com.weeth.domain.account.application.mapper.ReceiptMapper +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.account.domain.repository.ReceiptRepository +import com.weeth.domain.file.application.mapper.FileMapper +import com.weeth.domain.file.domain.entity.FileOwnerType +import com.weeth.domain.file.domain.repository.FileReader +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class GetAccountQueryService( + private val accountRepository: AccountRepository, + private val receiptRepository: ReceiptRepository, + private val fileReader: FileReader, + private val accountMapper: AccountMapper, + private val receiptMapper: ReceiptMapper, + private val fileMapper: FileMapper, +) { + fun findByCardinal(cardinal: Int): AccountResponse { + val account = accountRepository.findByCardinal(cardinal) ?: throw AccountNotFoundException() + val receipts = receiptRepository.findAllByAccountIdOrderByCreatedAtDesc(account.id) + val receiptIds = receipts.map { it.id } + val filesByReceiptId = + fileReader.findAll(FileOwnerType.RECEIPT, receiptIds, null) + .groupBy({ it.ownerId }, { fileMapper.toFileResponse(it) }) + return accountMapper.toResponse(account, receiptMapper.toResponses(receipts, filesByReceiptId)) + } +} From 4e528505aca34174489f990c8d64b2a4d87ac96e Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:38:16 +0900 Subject: [PATCH 15/26] =?UTF-8?q?test:=20usecase=20command/query=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=8B=A4=EC=8B=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/ManageAccountUseCaseTest.kt | 47 ++++++ .../command/ManageReceiptUseCaseTest.kt | 154 ++++++++++++++++++ .../query/GetAccountQueryServiceTest.kt | 89 ++++++++++ 3 files changed, 290 insertions(+) create mode 100644 src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCaseTest.kt create mode 100644 src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt create mode 100644 src/test/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryServiceTest.kt diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCaseTest.kt new file mode 100644 index 00000000..ea07505f --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageAccountUseCaseTest.kt @@ -0,0 +1,47 @@ +package com.weeth.domain.account.application.usecase.command + +import com.weeth.domain.account.application.dto.request.AccountSaveRequest +import com.weeth.domain.account.application.exception.AccountExistsException +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.user.domain.service.CardinalGetService +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + +class ManageAccountUseCaseTest : + DescribeSpec({ + val accountRepository = mockk(relaxed = true) + val cardinalGetService = mockk(relaxUnitFun = true) + val useCase = ManageAccountUseCase(accountRepository, cardinalGetService) + + beforeTest { + clearMocks(accountRepository, cardinalGetService) + } + + describe("save") { + context("이미 존재하는 기수로 저장 시") { + it("AccountExistsException을 던진다") { + val dto = AccountSaveRequest("설명", 100_000, 40) + every { accountRepository.existsByCardinal(40) } returns true + + shouldThrow { useCase.save(dto) } + } + } + + context("정상 저장 시") { + it("account가 저장된다") { + val dto = AccountSaveRequest("설명", 100_000, 40) + every { accountRepository.existsByCardinal(40) } returns false + every { cardinalGetService.findByAdminSide(40) } returns mockk() + every { accountRepository.save(any()) } answers { firstArg() } + + useCase.save(dto) + + verify(exactly = 1) { accountRepository.save(any()) } + } + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt new file mode 100644 index 00000000..4d150dfa --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt @@ -0,0 +1,154 @@ +package com.weeth.domain.account.application.usecase.command + +import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest +import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest +import com.weeth.domain.account.application.exception.AccountNotFoundException +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.account.domain.repository.ReceiptRepository +import com.weeth.domain.account.domain.vo.Money +import com.weeth.domain.account.fixture.AccountTestFixture +import com.weeth.domain.account.fixture.ReceiptTestFixture +import com.weeth.domain.file.application.dto.request.FileSaveRequest +import com.weeth.domain.file.application.mapper.FileMapper +import com.weeth.domain.file.domain.entity.File +import com.weeth.domain.file.domain.entity.FileOwnerType +import com.weeth.domain.file.domain.repository.FileReader +import com.weeth.domain.file.domain.repository.FileRepository +import com.weeth.domain.user.domain.service.CardinalGetService +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import java.time.LocalDate +import java.util.Optional + +class ManageReceiptUseCaseTest : + DescribeSpec({ + val receiptRepository = mockk(relaxUnitFun = true) + val accountRepository = mockk() + val fileReader = mockk() + val fileRepository = mockk(relaxed = true) + val cardinalGetService = mockk(relaxUnitFun = true) + val fileMapper = mockk() + val useCase = + ManageReceiptUseCase( + receiptRepository, + accountRepository, + fileReader, + fileRepository, + cardinalGetService, + fileMapper, + ) + + beforeTest { + clearMocks(receiptRepository, accountRepository, fileReader, cardinalGetService, fileMapper) + } + + describe("save") { + context("파일이 있는 경우") { + it("영수증 저장 후 fileRepository.saveAll이 호출된다") { + val account = AccountTestFixture.createAccount(cardinal = 40) + val savedReceipt = ReceiptTestFixture.createReceipt(id = 10L, amount = 5_000, account = account) + val files = listOf(mockk()) + val dto = + ReceiptSaveRequest( + "간식비", + "편의점", + 5_000, + LocalDate.of(2024, 9, 1), + 40, + listOf(FileSaveRequest("receipt.png", "TEMP/2024-09/receipt.png", 200L, "image/png")), + ) + + every { cardinalGetService.findByAdminSide(40) } returns mockk() + every { accountRepository.findByCardinal(40) } returns account + every { receiptRepository.save(any()) } returns savedReceipt + every { fileMapper.toFileList(dto.files, FileOwnerType.RECEIPT, savedReceipt.id) } returns files + + useCase.save(dto) + + verify(exactly = 1) { receiptRepository.save(any()) } + verify(exactly = 1) { fileRepository.saveAll(files) } + } + } + + context("파일이 없는 경우") { + it("fileRepository.saveAll은 빈 리스트로 호출된다") { + val account = AccountTestFixture.createAccount(cardinal = 40) + val savedReceipt = ReceiptTestFixture.createReceipt(id = 11L, amount = 3_000, account = account) + val dto = ReceiptSaveRequest("교통비", "지하철", 3_000, LocalDate.of(2024, 9, 2), 40, emptyList()) + + every { cardinalGetService.findByAdminSide(40) } returns mockk() + every { accountRepository.findByCardinal(40) } returns account + every { receiptRepository.save(any()) } returns savedReceipt + every { fileMapper.toFileList(emptyList(), FileOwnerType.RECEIPT, savedReceipt.id) } returns emptyList() + + useCase.save(dto) + + verify(exactly = 1) { receiptRepository.save(any()) } + verify(exactly = 1) { fileRepository.saveAll(emptyList()) } + } + } + + context("존재하지 않는 기수로 저장 시") { + it("AccountNotFoundException을 던진다") { + val dto = ReceiptSaveRequest("간식비", "편의점", 5_000, LocalDate.of(2024, 9, 1), 99, null) + + every { cardinalGetService.findByAdminSide(99) } returns mockk() + every { accountRepository.findByCardinal(99) } returns null + + shouldThrow { useCase.save(dto) } + } + } + } + + describe("update") { + it("업데이트 파일이 있으면 기존 파일을 삭제 후 새 파일을 저장한다") { + val receiptId = 10L + val account = AccountTestFixture.createAccount(cardinal = 40) + val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = account) + account.spend(Money.of(receipt.amount)) + val dto = + ReceiptUpdateRequest( + "desc", + "source", + 2_000, + LocalDate.of(2026, 1, 1), + 40, + listOf(FileSaveRequest("new.png", "TEMP/2026-02/new.png", 100L, "image/png")), + ) + val oldFiles = listOf(mockk()) + val newFiles = listOf(mockk()) + + every { accountRepository.findByCardinal(dto.cardinal) } returns account + every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) + every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns oldFiles + every { fileMapper.toFileList(dto.files, FileOwnerType.RECEIPT, receiptId) } returns newFiles + + useCase.update(receiptId, dto) + + verify(exactly = 1) { fileRepository.deleteAll(oldFiles) } + verify(exactly = 1) { fileRepository.saveAll(newFiles) } + } + } + + describe("delete") { + it("관련 파일 삭제 후 cancelSpend가 호출되고 영수증이 삭제된다") { + val receiptId = 5L + val account = AccountTestFixture.createAccount(currentAmount = 100_000) + val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 10_000, account = account) + account.spend(Money.of(receipt.amount)) + val files = listOf(mockk()) + + every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) + every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns files + + useCase.delete(receiptId) + + verify(exactly = 1) { fileRepository.deleteAll(files) } + verify(exactly = 1) { receiptRepository.delete(receipt) } + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryServiceTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryServiceTest.kt new file mode 100644 index 00000000..d4a28466 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryServiceTest.kt @@ -0,0 +1,89 @@ +package com.weeth.domain.account.application.usecase.query + +import com.weeth.domain.account.application.exception.AccountNotFoundException +import com.weeth.domain.account.application.mapper.AccountMapper +import com.weeth.domain.account.application.mapper.ReceiptMapper +import com.weeth.domain.account.domain.repository.AccountRepository +import com.weeth.domain.account.domain.repository.ReceiptRepository +import com.weeth.domain.account.fixture.AccountTestFixture +import com.weeth.domain.account.fixture.ReceiptTestFixture +import com.weeth.domain.file.application.mapper.FileMapper +import com.weeth.domain.file.domain.entity.FileOwnerType +import com.weeth.domain.file.domain.repository.FileReader +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + +class GetAccountQueryServiceTest : + DescribeSpec({ + val accountRepository = mockk() + val receiptRepository = mockk() + val fileReader = mockk() + val accountMapper = mockk() + val receiptMapper = mockk() + val fileMapper = mockk() + val queryService = + GetAccountQueryService( + accountRepository, + receiptRepository, + fileReader, + accountMapper, + receiptMapper, + fileMapper, + ) + + beforeTest { + clearMocks(accountRepository, receiptRepository, fileReader, accountMapper, receiptMapper, fileMapper) + } + + describe("findByCardinal") { + context("존재하는 기수 조회 시") { + it("영수증이 있으면 fileReader.findAll을 receiptIds 배치로 1회 호출한다") { + val account = AccountTestFixture.createAccount(cardinal = 40) + val receipt1 = ReceiptTestFixture.createReceipt(id = 1L, account = account) + val receipt2 = ReceiptTestFixture.createReceipt(id = 2L, account = account) + val accountResponse = mockk() + + every { accountRepository.findByCardinal(40) } returns account + every { receiptRepository.findAllByAccountIdOrderByCreatedAtDesc(account.id) } returns + listOf(receipt1, receipt2) + every { fileReader.findAll(FileOwnerType.RECEIPT, listOf(1L, 2L), null) } returns emptyList() + every { fileMapper.toFileResponse(any()) } returns mockk() + every { receiptMapper.toResponses(any(), any()) } returns emptyList() + every { accountMapper.toResponse(account, emptyList()) } returns accountResponse + + val result = queryService.findByCardinal(40) + + result shouldBe accountResponse + verify(exactly = 1) { fileReader.findAll(FileOwnerType.RECEIPT, listOf(1L, 2L), null) } + } + + it("영수증이 없으면 fileReader.findAll을 빈 리스트로 호출한다") { + val account = AccountTestFixture.createAccount(cardinal = 40) + val accountResponse = mockk() + + every { accountRepository.findByCardinal(40) } returns account + every { receiptRepository.findAllByAccountIdOrderByCreatedAtDesc(account.id) } returns emptyList() + every { fileReader.findAll(FileOwnerType.RECEIPT, emptyList(), null) } returns emptyList() + every { receiptMapper.toResponses(emptyList(), emptyMap()) } returns emptyList() + every { accountMapper.toResponse(account, emptyList()) } returns accountResponse + + queryService.findByCardinal(40) + + verify(exactly = 1) { fileReader.findAll(FileOwnerType.RECEIPT, emptyList(), null) } + } + } + + context("존재하지 않는 기수 조회 시") { + it("AccountNotFoundException을 던진다") { + every { accountRepository.findByCardinal(99) } returns null + + shouldThrow { queryService.findByCardinal(99) } + } + } + } + }) From 34b7b5ec0aebd62a597ba2763481a576c4da36ee Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:40:22 +0900 Subject: [PATCH 16/26] =?UTF-8?q?refactor:=20Account=20usecase=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/presentation/AccountAdminController.java | 6 +++--- .../account/presentation/AccountController.java | 6 +++--- .../account/presentation/ReceiptAdminController.java | 10 +++++----- .../usecase/command/ManageReceiptUseCase.kt | 12 ++++++++---- .../usecase/query/GetAccountQueryService.kt | 3 ++- .../weeth/domain/account/domain/entity/Account.kt | 10 ---------- 6 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java b/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java index 9b109651..b3d24b25 100644 --- a/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java +++ b/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java @@ -2,7 +2,7 @@ import com.weeth.domain.account.application.dto.request.AccountSaveRequest; import com.weeth.domain.account.application.exception.AccountErrorCode; -import com.weeth.domain.account.application.usecase.AccountUseCase; +import com.weeth.domain.account.application.usecase.command.ManageAccountUseCase; import com.weeth.global.common.exception.ApiErrorCodeExample; import com.weeth.global.common.response.CommonResponse; import io.swagger.v3.oas.annotations.Operation; @@ -23,12 +23,12 @@ @ApiErrorCodeExample(AccountErrorCode.class) public class AccountAdminController { - private final AccountUseCase accountUseCase; + private final ManageAccountUseCase manageAccountUseCase; @PostMapping @Operation(summary="회비 총 금액 기입") public CommonResponse save(@RequestBody @Valid AccountSaveRequest dto) { - accountUseCase.save(dto); + manageAccountUseCase.save(dto); return CommonResponse.success(ACCOUNT_SAVE_SUCCESS); } } diff --git a/src/main/java/com/weeth/domain/account/presentation/AccountController.java b/src/main/java/com/weeth/domain/account/presentation/AccountController.java index 7151f7e4..2511a95e 100644 --- a/src/main/java/com/weeth/domain/account/presentation/AccountController.java +++ b/src/main/java/com/weeth/domain/account/presentation/AccountController.java @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import com.weeth.domain.account.application.dto.response.AccountResponse; import com.weeth.domain.account.application.exception.AccountErrorCode; -import com.weeth.domain.account.application.usecase.AccountUseCase; +import com.weeth.domain.account.application.usecase.query.GetAccountQueryService; import com.weeth.global.common.exception.ApiErrorCodeExample; import com.weeth.global.common.response.CommonResponse; import lombok.RequiredArgsConstructor; @@ -21,11 +21,11 @@ @ApiErrorCodeExample(AccountErrorCode.class) public class AccountController { - private final AccountUseCase accountUseCase; + private final GetAccountQueryService getAccountQueryService; @GetMapping("/{cardinal}") @Operation(summary="회비 내역 조회") public CommonResponse find(@PathVariable Integer cardinal) { - return CommonResponse.success(ACCOUNT_FIND_SUCCESS,accountUseCase.find(cardinal)); + return CommonResponse.success(ACCOUNT_FIND_SUCCESS, getAccountQueryService.findByCardinal(cardinal)); } } diff --git a/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java b/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java index de75f5c9..681a3b3d 100644 --- a/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java +++ b/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java @@ -6,7 +6,7 @@ import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest; import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest; import com.weeth.domain.account.application.exception.AccountErrorCode; -import com.weeth.domain.account.application.usecase.ReceiptUseCase; +import com.weeth.domain.account.application.usecase.command.ManageReceiptUseCase; import com.weeth.global.common.exception.ApiErrorCodeExample; import com.weeth.global.common.response.CommonResponse; import lombok.RequiredArgsConstructor; @@ -20,26 +20,26 @@ @ApiErrorCodeExample(AccountErrorCode.class) public class ReceiptAdminController { - private final ReceiptUseCase receiptUseCase; + private final ManageReceiptUseCase manageReceiptUseCase; @PostMapping @Operation(summary="회비 사용 내역 기입") public CommonResponse save(@RequestBody @Valid ReceiptSaveRequest dto) { - receiptUseCase.save(dto); + manageReceiptUseCase.save(dto); return CommonResponse.success(RECEIPT_SAVE_SUCCESS); } @DeleteMapping("/{receiptId}") @Operation(summary="회비 사용 내역 취소") public CommonResponse delete(@PathVariable Long receiptId) { - receiptUseCase.delete(receiptId); + manageReceiptUseCase.delete(receiptId); return CommonResponse.success(RECEIPT_DELETE_SUCCESS); } @PatchMapping("/{receiptId}") @Operation(summary="회비 사용 내역 수정") public CommonResponse update(@PathVariable Long receiptId, @RequestBody @Valid ReceiptUpdateRequest dto) { - receiptUseCase.update(receiptId, dto); + manageReceiptUseCase.update(receiptId, dto); return CommonResponse.success(RECEIPT_UPDATE_SUCCESS); } } diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt index bb07c71a..5ccd1003 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt @@ -30,15 +30,19 @@ class ManageReceiptUseCase( fun save(request: ReceiptSaveRequest) { cardinalGetService.findByAdminSide(request.cardinal) val account = accountRepository.findByCardinal(request.cardinal) ?: throw AccountNotFoundException() - val receipt = receiptRepository.save( - Receipt.create(request.description, request.source, request.amount, request.date, account), - ) + val receipt = + receiptRepository.save( + Receipt.create(request.description, request.source, request.amount, request.date, account), + ) account.spend(Money.of(request.amount)) fileRepository.saveAll(fileMapper.toFileList(request.files, FileOwnerType.RECEIPT, receipt.id)) } @Transactional - fun update(receiptId: Long, request: ReceiptUpdateRequest) { + fun update( + receiptId: Long, + request: ReceiptUpdateRequest, + ) { val account = accountRepository.findByCardinal(request.cardinal) ?: throw AccountNotFoundException() val receipt = receiptRepository.findByIdOrNull(receiptId) ?: throw ReceiptNotFoundException() account.adjustSpend(Money.of(receipt.amount), Money.of(request.amount)) diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryService.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryService.kt index a8cabb06..8d10b081 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryService.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/query/GetAccountQueryService.kt @@ -27,7 +27,8 @@ class GetAccountQueryService( val receipts = receiptRepository.findAllByAccountIdOrderByCreatedAtDesc(account.id) val receiptIds = receipts.map { it.id } val filesByReceiptId = - fileReader.findAll(FileOwnerType.RECEIPT, receiptIds, null) + fileReader + .findAll(FileOwnerType.RECEIPT, receiptIds, null) .groupBy({ it.ownerId }, { fileMapper.toFileResponse(it) }) return accountMapper.toResponse(account, receiptMapper.toResponses(receipts, filesByReceiptId)) } diff --git a/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt b/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt index c3f41341..dbad706e 100644 --- a/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt +++ b/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt @@ -43,16 +43,6 @@ class Account( spend(newAmount) } - // Java interop overloads — removed when Java UseCases are migrated in Phase 5 - fun spend(amount: Int) = spend(Money.of(amount)) - - fun cancelSpend(amount: Int) = cancelSpend(Money.of(amount)) - - fun adjustSpend( - oldAmount: Int, - newAmount: Int, - ) = adjustSpend(Money.of(oldAmount), Money.of(newAmount)) - companion object { @JvmStatic fun create( From 8264d7e18964e6749ec24965510a2bf2625b9478 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:44:21 +0900 Subject: [PATCH 17/26] =?UTF-8?q?refactor:=20Account=20exception,=20contro?= =?UTF-8?q?ller,=20responsecode=20java=20->=20kotlin=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/account/application/exception/AccountErrorCode.kt} | 0 .../account/application/exception/AccountExistsException.kt} | 0 .../account/application/exception/AccountNotFoundException.kt} | 0 .../account/application/exception/ReceiptNotFoundException.kt} | 0 .../weeth/domain/account/presentation/AccountAdminController.kt} | 0 .../com/weeth/domain/account/presentation/AccountController.kt} | 0 .../com/weeth/domain/account/presentation/AccountResponseCode.kt} | 0 .../weeth/domain/account/presentation/ReceiptAdminController.kt} | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename src/main/{java/com/weeth/domain/account/application/exception/AccountErrorCode.java => kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt} (100%) rename src/main/{java/com/weeth/domain/account/application/exception/AccountExistsException.java => kotlin/com/weeth/domain/account/application/exception/AccountExistsException.kt} (100%) rename src/main/{java/com/weeth/domain/account/application/exception/AccountNotFoundException.java => kotlin/com/weeth/domain/account/application/exception/AccountNotFoundException.kt} (100%) rename src/main/{java/com/weeth/domain/account/application/exception/ReceiptNotFoundException.java => kotlin/com/weeth/domain/account/application/exception/ReceiptNotFoundException.kt} (100%) rename src/main/{java/com/weeth/domain/account/presentation/AccountAdminController.java => kotlin/com/weeth/domain/account/presentation/AccountAdminController.kt} (100%) rename src/main/{java/com/weeth/domain/account/presentation/AccountController.java => kotlin/com/weeth/domain/account/presentation/AccountController.kt} (100%) rename src/main/{java/com/weeth/domain/account/presentation/AccountResponseCode.java => kotlin/com/weeth/domain/account/presentation/AccountResponseCode.kt} (100%) rename src/main/{java/com/weeth/domain/account/presentation/ReceiptAdminController.java => kotlin/com/weeth/domain/account/presentation/ReceiptAdminController.kt} (100%) diff --git a/src/main/java/com/weeth/domain/account/application/exception/AccountErrorCode.java b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/application/exception/AccountErrorCode.java rename to src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt diff --git a/src/main/java/com/weeth/domain/account/application/exception/AccountExistsException.java b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountExistsException.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/application/exception/AccountExistsException.java rename to src/main/kotlin/com/weeth/domain/account/application/exception/AccountExistsException.kt diff --git a/src/main/java/com/weeth/domain/account/application/exception/AccountNotFoundException.java b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountNotFoundException.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/application/exception/AccountNotFoundException.java rename to src/main/kotlin/com/weeth/domain/account/application/exception/AccountNotFoundException.kt diff --git a/src/main/java/com/weeth/domain/account/application/exception/ReceiptNotFoundException.java b/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptNotFoundException.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/application/exception/ReceiptNotFoundException.java rename to src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptNotFoundException.kt diff --git a/src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java b/src/main/kotlin/com/weeth/domain/account/presentation/AccountAdminController.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/presentation/AccountAdminController.java rename to src/main/kotlin/com/weeth/domain/account/presentation/AccountAdminController.kt diff --git a/src/main/java/com/weeth/domain/account/presentation/AccountController.java b/src/main/kotlin/com/weeth/domain/account/presentation/AccountController.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/presentation/AccountController.java rename to src/main/kotlin/com/weeth/domain/account/presentation/AccountController.kt diff --git a/src/main/java/com/weeth/domain/account/presentation/AccountResponseCode.java b/src/main/kotlin/com/weeth/domain/account/presentation/AccountResponseCode.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/presentation/AccountResponseCode.java rename to src/main/kotlin/com/weeth/domain/account/presentation/AccountResponseCode.kt diff --git a/src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java b/src/main/kotlin/com/weeth/domain/account/presentation/ReceiptAdminController.kt similarity index 100% rename from src/main/java/com/weeth/domain/account/presentation/ReceiptAdminController.java rename to src/main/kotlin/com/weeth/domain/account/presentation/ReceiptAdminController.kt From 76b75c80ddb3ee1292cf46a5fc82cd1371b21b7b Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:47:58 +0900 Subject: [PATCH 18/26] =?UTF-8?q?refactor:=20Account=20controller,=20respo?= =?UTF-8?q?nsecode=20kotlin=20=EB=AC=B8=EB=B2=95=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/AccountAdminController.kt | 50 ++++++------- .../account/presentation/AccountController.kt | 45 ++++++----- .../presentation/AccountResponseCode.kt | 31 +++----- .../presentation/ReceiptAdminController.kt | 74 +++++++++++-------- 4 files changed, 97 insertions(+), 103 deletions(-) diff --git a/src/main/kotlin/com/weeth/domain/account/presentation/AccountAdminController.kt b/src/main/kotlin/com/weeth/domain/account/presentation/AccountAdminController.kt index b3d24b25..92cc5a46 100644 --- a/src/main/kotlin/com/weeth/domain/account/presentation/AccountAdminController.kt +++ b/src/main/kotlin/com/weeth/domain/account/presentation/AccountAdminController.kt @@ -1,34 +1,32 @@ -package com.weeth.domain.account.presentation; +package com.weeth.domain.account.presentation -import com.weeth.domain.account.application.dto.request.AccountSaveRequest; -import com.weeth.domain.account.application.exception.AccountErrorCode; -import com.weeth.domain.account.application.usecase.command.ManageAccountUseCase; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static com.weeth.domain.account.presentation.AccountResponseCode.ACCOUNT_SAVE_SUCCESS; +import com.weeth.domain.account.application.dto.request.AccountSaveRequest +import com.weeth.domain.account.application.exception.AccountErrorCode +import com.weeth.domain.account.application.usecase.command.ManageAccountUseCase +import com.weeth.domain.account.presentation.AccountResponseCode.ACCOUNT_SAVE_SUCCESS +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController @Tag(name = "ACCOUNT ADMIN", description = "[ADMIN] 회비 어드민 API") @RestController -@RequiredArgsConstructor @RequestMapping("/api/v1/admin/account") -@ApiErrorCodeExample(AccountErrorCode.class) -public class AccountAdminController { - - private final ManageAccountUseCase manageAccountUseCase; - +@ApiErrorCodeExample(AccountErrorCode::class) +class AccountAdminController( + private val manageAccountUseCase: ManageAccountUseCase, +) { @PostMapping - @Operation(summary="회비 총 금액 기입") - public CommonResponse save(@RequestBody @Valid AccountSaveRequest dto) { - manageAccountUseCase.save(dto); - return CommonResponse.success(ACCOUNT_SAVE_SUCCESS); + @Operation(summary = "회비 총 금액 기입") + fun save( + @RequestBody @Valid dto: AccountSaveRequest, + ): CommonResponse { + manageAccountUseCase.save(dto) + return CommonResponse.success(ACCOUNT_SAVE_SUCCESS) } } diff --git a/src/main/kotlin/com/weeth/domain/account/presentation/AccountController.kt b/src/main/kotlin/com/weeth/domain/account/presentation/AccountController.kt index 2511a95e..96d9e4c6 100644 --- a/src/main/kotlin/com/weeth/domain/account/presentation/AccountController.kt +++ b/src/main/kotlin/com/weeth/domain/account/presentation/AccountController.kt @@ -1,31 +1,28 @@ -package com.weeth.domain.account.presentation; +package com.weeth.domain.account.presentation -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import com.weeth.domain.account.application.dto.response.AccountResponse; -import com.weeth.domain.account.application.exception.AccountErrorCode; -import com.weeth.domain.account.application.usecase.query.GetAccountQueryService; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import com.weeth.domain.account.application.dto.response.AccountResponse +import com.weeth.domain.account.application.exception.AccountErrorCode +import com.weeth.domain.account.application.usecase.query.GetAccountQueryService +import com.weeth.domain.account.presentation.AccountResponseCode.ACCOUNT_FIND_SUCCESS +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController -import static com.weeth.domain.account.presentation.AccountResponseCode.ACCOUNT_FIND_SUCCESS; @Tag(name = "ACCOUNT", description = "회비 API") @RestController -@RequiredArgsConstructor @RequestMapping("/api/v1/account") -@ApiErrorCodeExample(AccountErrorCode.class) -public class AccountController { - - private final GetAccountQueryService getAccountQueryService; - +@ApiErrorCodeExample(AccountErrorCode::class) +class AccountController( + private val getAccountQueryService: GetAccountQueryService, +) { @GetMapping("/{cardinal}") - @Operation(summary="회비 내역 조회") - public CommonResponse find(@PathVariable Integer cardinal) { - return CommonResponse.success(ACCOUNT_FIND_SUCCESS, getAccountQueryService.findByCardinal(cardinal)); - } + @Operation(summary = "회비 내역 조회") + fun find( + @PathVariable cardinal: Int, + ): CommonResponse = CommonResponse.success(ACCOUNT_FIND_SUCCESS, getAccountQueryService.findByCardinal(cardinal)) } diff --git a/src/main/kotlin/com/weeth/domain/account/presentation/AccountResponseCode.kt b/src/main/kotlin/com/weeth/domain/account/presentation/AccountResponseCode.kt index 4d1bf484..647dfc5e 100644 --- a/src/main/kotlin/com/weeth/domain/account/presentation/AccountResponseCode.kt +++ b/src/main/kotlin/com/weeth/domain/account/presentation/AccountResponseCode.kt @@ -1,29 +1,16 @@ -package com.weeth.domain.account.presentation; +package com.weeth.domain.account.presentation -import com.weeth.global.common.response.ResponseCodeInterface; -import lombok.Getter; -import org.springframework.http.HttpStatus; +import com.weeth.global.common.response.ResponseCodeInterface +import org.springframework.http.HttpStatus -@Getter -public enum AccountResponseCode implements ResponseCodeInterface { - // AccountAdminController 관련 +enum class AccountResponseCode( + override val code: Int, + override val status: HttpStatus, + override val message: String, +) : ResponseCodeInterface { ACCOUNT_SAVE_SUCCESS(1100, HttpStatus.OK, "회비가 성공적으로 저장되었습니다."), - - // AccountController 관련 ACCOUNT_FIND_SUCCESS(1101, HttpStatus.OK, "회비가 성공적으로 조회되었습니다."), - - // ReceiptAdminController 관련 RECEIPT_SAVE_SUCCESS(1102, HttpStatus.OK, "영수증이 성공적으로 저장되었습니다."), RECEIPT_DELETE_SUCCESS(1103, HttpStatus.OK, "영수증이 성공적으로 삭제되었습니다."), - RECEIPT_UPDATE_SUCCESS(1104, HttpStatus.OK, "영수증이 성공적으로 업데이트 되었습니다."); - - private final int code; - private final HttpStatus status; - private final String message; - - AccountResponseCode(int code, HttpStatus status, String message) { - this.code = code; - this.status = status; - this.message = message; - } + RECEIPT_UPDATE_SUCCESS(1104, HttpStatus.OK, "영수증이 성공적으로 업데이트 되었습니다."), } diff --git a/src/main/kotlin/com/weeth/domain/account/presentation/ReceiptAdminController.kt b/src/main/kotlin/com/weeth/domain/account/presentation/ReceiptAdminController.kt index 681a3b3d..3df7b96f 100644 --- a/src/main/kotlin/com/weeth/domain/account/presentation/ReceiptAdminController.kt +++ b/src/main/kotlin/com/weeth/domain/account/presentation/ReceiptAdminController.kt @@ -1,45 +1,57 @@ -package com.weeth.domain.account.presentation; +package com.weeth.domain.account.presentation -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest; -import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest; -import com.weeth.domain.account.application.exception.AccountErrorCode; -import com.weeth.domain.account.application.usecase.command.ManageReceiptUseCase; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; -import static com.weeth.domain.account.presentation.AccountResponseCode.*; +import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest +import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest +import com.weeth.domain.account.application.exception.AccountErrorCode +import com.weeth.domain.account.application.usecase.command.ManageReceiptUseCase +import com.weeth.domain.account.presentation.AccountResponseCode.RECEIPT_DELETE_SUCCESS +import com.weeth.domain.account.presentation.AccountResponseCode.RECEIPT_SAVE_SUCCESS +import com.weeth.domain.account.presentation.AccountResponseCode.RECEIPT_UPDATE_SUCCESS +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController @Tag(name = "RECEIPT ADMIN", description = "[ADMIN] 회비 어드민 API") @RestController -@RequiredArgsConstructor @RequestMapping("/api/v1/admin/receipts") -@ApiErrorCodeExample(AccountErrorCode.class) -public class ReceiptAdminController { - - private final ManageReceiptUseCase manageReceiptUseCase; - +@ApiErrorCodeExample(AccountErrorCode::class) +class ReceiptAdminController( + private val manageReceiptUseCase: ManageReceiptUseCase, +) { @PostMapping - @Operation(summary="회비 사용 내역 기입") - public CommonResponse save(@RequestBody @Valid ReceiptSaveRequest dto) { - manageReceiptUseCase.save(dto); - return CommonResponse.success(RECEIPT_SAVE_SUCCESS); + @Operation(summary = "회비 사용 내역 기입") + fun save( + @RequestBody @Valid dto: ReceiptSaveRequest, + ): CommonResponse { + manageReceiptUseCase.save(dto) + return CommonResponse.success(RECEIPT_SAVE_SUCCESS) } @DeleteMapping("/{receiptId}") - @Operation(summary="회비 사용 내역 취소") - public CommonResponse delete(@PathVariable Long receiptId) { - manageReceiptUseCase.delete(receiptId); - return CommonResponse.success(RECEIPT_DELETE_SUCCESS); + @Operation(summary = "회비 사용 내역 취소") + fun delete( + @PathVariable receiptId: Long, + ): CommonResponse { + manageReceiptUseCase.delete(receiptId) + return CommonResponse.success(RECEIPT_DELETE_SUCCESS) } @PatchMapping("/{receiptId}") - @Operation(summary="회비 사용 내역 수정") - public CommonResponse update(@PathVariable Long receiptId, @RequestBody @Valid ReceiptUpdateRequest dto) { - manageReceiptUseCase.update(receiptId, dto); - return CommonResponse.success(RECEIPT_UPDATE_SUCCESS); + @Operation(summary = "회비 사용 내역 수정") + fun update( + @PathVariable receiptId: Long, + @RequestBody @Valid dto: ReceiptUpdateRequest, + ): CommonResponse { + manageReceiptUseCase.update(receiptId, dto) + return CommonResponse.success(RECEIPT_UPDATE_SUCCESS) } } From b7a19900c2412c2cdf600de0c1cb09b7cdfb7134 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 15:48:26 +0900 Subject: [PATCH 19/26] =?UTF-8?q?refactor:=20Account=20exception=20kotlin?= =?UTF-8?q?=20=EB=AC=B8=EB=B2=95=EC=9C=BC=EB=A1=9C=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/exception/AccountErrorCode.kt | 30 ++++++++++--------- .../exception/AccountExistsException.kt | 11 ++----- .../exception/AccountNotFoundException.kt | 10 ++----- .../exception/ReceiptNotFoundException.kt | 10 ++----- 4 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt index d694c551..08bfe6bd 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt @@ -1,15 +1,14 @@ -package com.weeth.domain.account.application.exception; +package com.weeth.domain.account.application.exception -import com.weeth.global.common.exception.ErrorCodeInterface; -import com.weeth.global.common.exception.ExplainError; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum AccountErrorCode implements ErrorCodeInterface { +import com.weeth.global.common.exception.ErrorCodeInterface +import com.weeth.global.common.exception.ExplainError +import org.springframework.http.HttpStatus +enum class AccountErrorCode( + private val code: Int, + private val status: HttpStatus, + private val message: String, +) : ErrorCodeInterface { @ExplainError("요청한 회비 장부 ID가 존재하지 않을 때 발생합니다.") ACCOUNT_NOT_FOUND(2100, HttpStatus.NOT_FOUND, "존재하지 않는 장부입니다."), @@ -17,9 +16,12 @@ public enum AccountErrorCode implements ErrorCodeInterface { ACCOUNT_EXISTS(2101, HttpStatus.BAD_REQUEST, "이미 생성된 장부입니다."), @ExplainError("요청한 영수증 내역이 존재하지 않을 때 발생합니다.") - RECEIPT_NOT_FOUND(2102, HttpStatus.NOT_FOUND, "존재하지 않는 내역입니다."); + RECEIPT_NOT_FOUND(2102, HttpStatus.NOT_FOUND, "존재하지 않는 내역입니다."), + ; + + override fun getCode(): Int = code + + override fun getStatus(): HttpStatus = status - private final int code; - private final HttpStatus status; - private final String message; + override fun getMessage(): String = message } diff --git a/src/main/kotlin/com/weeth/domain/account/application/exception/AccountExistsException.kt b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountExistsException.kt index 9e6ed8b5..5886dead 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/exception/AccountExistsException.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountExistsException.kt @@ -1,10 +1,5 @@ -package com.weeth.domain.account.application.exception; +package com.weeth.domain.account.application.exception -import com.weeth.global.common.exception.BaseException; - -public class AccountExistsException extends BaseException { - public AccountExistsException() { - super(AccountErrorCode.ACCOUNT_EXISTS); - } -} +import com.weeth.global.common.exception.BaseException +class AccountExistsException : BaseException(AccountErrorCode.ACCOUNT_EXISTS) diff --git a/src/main/kotlin/com/weeth/domain/account/application/exception/AccountNotFoundException.kt b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountNotFoundException.kt index 2e480f40..c7dc5a24 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/exception/AccountNotFoundException.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountNotFoundException.kt @@ -1,9 +1,5 @@ -package com.weeth.domain.account.application.exception; +package com.weeth.domain.account.application.exception -import com.weeth.global.common.exception.BaseException; +import com.weeth.global.common.exception.BaseException -public class AccountNotFoundException extends BaseException { - public AccountNotFoundException() { - super(AccountErrorCode.ACCOUNT_NOT_FOUND); - } -} +class AccountNotFoundException : BaseException(AccountErrorCode.ACCOUNT_NOT_FOUND) diff --git a/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptNotFoundException.kt b/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptNotFoundException.kt index ac11d282..db6f1b51 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptNotFoundException.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptNotFoundException.kt @@ -1,9 +1,5 @@ -package com.weeth.domain.account.application.exception; +package com.weeth.domain.account.application.exception -import com.weeth.global.common.exception.BaseException; +import com.weeth.global.common.exception.BaseException -public class ReceiptNotFoundException extends BaseException { - public ReceiptNotFoundException() { - super(AccountErrorCode.RECEIPT_NOT_FOUND); - } -} +class ReceiptNotFoundException : BaseException(AccountErrorCode.RECEIPT_NOT_FOUND) From bd9ae94c109048792783bb32ed5ee48be2b1717d Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 16:01:10 +0900 Subject: [PATCH 20/26] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20@JvmStatic=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/weeth/domain/account/domain/entity/Account.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt b/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt index dbad706e..98f1b8e8 100644 --- a/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt +++ b/src/main/kotlin/com/weeth/domain/account/domain/entity/Account.kt @@ -44,7 +44,6 @@ class Account( } companion object { - @JvmStatic fun create( description: String, totalAmount: Int, From 9166e0ef369605cca7fd56cfaf1079995bbc33d9 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 16:15:13 +0900 Subject: [PATCH 21/26] =?UTF-8?q?fix:=20Receipt.update()=20=EA=B8=88?= =?UTF-8?q?=EC=95=A1=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/weeth/domain/account/domain/entity/Receipt.kt | 1 + .../weeth/domain/account/domain/entity/ReceiptTest.kt | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt b/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt index 376db547..b63786e1 100644 --- a/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt +++ b/src/main/kotlin/com/weeth/domain/account/domain/entity/Receipt.kt @@ -35,6 +35,7 @@ class Receipt( amount: Int, date: LocalDate, ) { + require(amount > 0) { "금액은 0보다 커야 합니다: $amount" } this.description = description this.source = source this.amount = amount diff --git a/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt b/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt index dd98afa3..ac753511 100644 --- a/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/domain/entity/ReceiptTest.kt @@ -1,6 +1,7 @@ package com.weeth.domain.account.domain.entity import com.weeth.domain.account.fixture.ReceiptTestFixture +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import java.time.LocalDate @@ -23,4 +24,12 @@ class ReceiptTest : receipt.amount shouldBe 20_000 receipt.date shouldBe LocalDate.of(2025, 6, 1) } + + "update 시 amount가 0 이하면 IllegalArgumentException을 던진다" { + val receipt = ReceiptTestFixture.createReceipt(amount = 5_000) + + shouldThrow { + receipt.update("설명", "출처", 0, LocalDate.of(2025, 6, 1)) + } + } }) From ec268ee6917c583df949a984531b23122ba36f8d Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 18:26:54 +0900 Subject: [PATCH 22/26] =?UTF-8?q?fix:=20updateReceipt=20=EA=B8=B0=EC=88=98?= =?UTF-8?q?=20=EC=A1=B4=EC=9E=AC=20=EA=B2=80=EC=A6=9D=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20description=20@NotBlank=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/application/dto/request/AccountSaveRequest.kt | 2 ++ .../account/application/usecase/command/ManageReceiptUseCase.kt | 1 + .../application/usecase/command/ManageReceiptUseCaseTest.kt | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt index 46672a54..c312cdf0 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt @@ -1,11 +1,13 @@ package com.weeth.domain.account.application.dto.request import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotNull import jakarta.validation.constraints.Positive data class AccountSaveRequest( @field:Schema(description = "회비 설명", example = "2024년 2학기 회비") + @field:NotBlank val description: String, @field:Schema(description = "총 금액", example = "100000") @field:NotNull diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt index 5ccd1003..d445a47a 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt @@ -43,6 +43,7 @@ class ManageReceiptUseCase( receiptId: Long, request: ReceiptUpdateRequest, ) { + cardinalGetService.findByAdminSide(request.cardinal) val account = accountRepository.findByCardinal(request.cardinal) ?: throw AccountNotFoundException() val receipt = receiptRepository.findByIdOrNull(receiptId) ?: throw ReceiptNotFoundException() account.adjustSpend(Money.of(receipt.amount), Money.of(request.amount)) diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt index 4d150dfa..031b945c 100644 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt @@ -122,7 +122,9 @@ class ManageReceiptUseCaseTest : val oldFiles = listOf(mockk()) val newFiles = listOf(mockk()) + every { cardinalGetService.findByAdminSide(dto.cardinal) } returns mockk() every { accountRepository.findByCardinal(dto.cardinal) } returns account + every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns oldFiles every { fileMapper.toFileList(dto.files, FileOwnerType.RECEIPT, receiptId) } returns newFiles From 8c9104ba20e0d5e3fded68929b901ea84a5ceb7a Mon Sep 17 00:00:00 2001 From: soo0711 Date: Thu, 19 Feb 2026 19:09:51 +0900 Subject: [PATCH 23/26] =?UTF-8?q?fix:=20=EC=98=81=EC=88=98=EC=A6=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=8B=9C=20=EB=B9=88=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usecase/command/ManageReceiptUseCase.kt | 2 +- .../command/ManageReceiptUseCaseTest.kt | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt index d445a47a..95f0df22 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt @@ -47,7 +47,7 @@ class ManageReceiptUseCase( val account = accountRepository.findByCardinal(request.cardinal) ?: throw AccountNotFoundException() val receipt = receiptRepository.findByIdOrNull(receiptId) ?: throw ReceiptNotFoundException() account.adjustSpend(Money.of(receipt.amount), Money.of(request.amount)) - if (!request.files.isNullOrEmpty()) { + if (request.files != null) { fileRepository.deleteAll(fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null)) fileRepository.saveAll(fileMapper.toFileList(request.files, FileOwnerType.RECEIPT, receiptId)) } diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt index 031b945c..cac249c9 100644 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt @@ -134,6 +134,34 @@ class ManageReceiptUseCaseTest : verify(exactly = 1) { fileRepository.deleteAll(oldFiles) } verify(exactly = 1) { fileRepository.saveAll(newFiles) } } + + it("빈 리스트로 업데이트 시 기존 파일을 모두 삭제한다") { + val receiptId = 11L + val account = AccountTestFixture.createAccount(cardinal = 40) + val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = account) + account.spend(Money.of(receipt.amount)) + val dto = + ReceiptUpdateRequest( + "desc", + "source", + 2_000, + LocalDate.of(2026, 1, 1), + 40, + emptyList(), + ) + val oldFiles = listOf(mockk()) + + every { cardinalGetService.findByAdminSide(dto.cardinal) } returns mockk() + every { accountRepository.findByCardinal(dto.cardinal) } returns account + every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) + every { fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null) } returns oldFiles + every { fileMapper.toFileList(emptyList(), FileOwnerType.RECEIPT, receiptId) } returns emptyList() + + useCase.update(receiptId, dto) + + verify(exactly = 1) { fileRepository.deleteAll(oldFiles) } + verify(exactly = 1) { fileRepository.saveAll(emptyList()) } + } } describe("delete") { From 584db68ac6f230ca413182c9b726fd4028690011 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Fri, 20 Feb 2026 13:45:37 +0900 Subject: [PATCH 24/26] =?UTF-8?q?refactor:=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EC=98=88=EC=8B=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/application/dto/request/AccountSaveRequest.kt | 2 +- .../account/application/dto/request/ReceiptSaveRequest.kt | 2 +- .../account/application/dto/request/ReceiptUpdateRequest.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt index c312cdf0..cc9be88c 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/request/AccountSaveRequest.kt @@ -13,7 +13,7 @@ data class AccountSaveRequest( @field:NotNull @field:Positive val totalAmount: Int, - @field:Schema(description = "기수", example = "40") + @field:Schema(description = "기수", example = "4") @field:NotNull val cardinal: Int, ) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt index 2f62ba93..c177a3fa 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptSaveRequest.kt @@ -19,7 +19,7 @@ data class ReceiptSaveRequest( @field:Schema(description = "사용 날짜", example = "2024-09-01") @field:NotNull val date: LocalDate, - @field:Schema(description = "기수", example = "40") + @field:Schema(description = "기수", example = "4") @field:NotNull val cardinal: Int, @field:Valid diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt index d6f52f3a..1489ff29 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt @@ -19,7 +19,7 @@ data class ReceiptUpdateRequest( @field:Schema(description = "사용 날짜", example = "2024-09-01") @field:NotNull val date: LocalDate, - @field:Schema(description = "기수", example = "40") + @field:Schema(description = "기수", example = "4") @field:NotNull val cardinal: Int, @field:Valid From 6ac9763f9de5e6e0030256d0cf418e26fa007133 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Fri, 20 Feb 2026 13:51:57 +0900 Subject: [PATCH 25/26] =?UTF-8?q?refactor:=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EC=98=88=EC=8B=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/application/dto/request/ReceiptUpdateRequest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt index 1489ff29..cb146aff 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/dto/request/ReceiptUpdateRequest.kt @@ -22,6 +22,10 @@ data class ReceiptUpdateRequest( @field:Schema(description = "기수", example = "4") @field:NotNull val cardinal: Int, + @field:Schema( + description = "첨부 파일 변경 규약: null=변경 안 함, []=전체 삭제, 배열 전달=해당 목록으로 교체", + nullable = true, + ) @field:Valid val files: List<@NotNull FileSaveRequest>?, ) From 538481edc8936e0ca522df5cc53f49f5a023f576 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Fri, 20 Feb 2026 14:07:14 +0900 Subject: [PATCH 26/26] =?UTF-8?q?fix:=20Receipt=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=8B=9C=20account=20=EB=B6=88=EC=9D=BC=EC=B9=98=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=B0=8F=20clearMocks=20=EB=88=84=EB=9D=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/exception/AccountErrorCode.kt | 3 +++ .../ReceiptAccountMismatchException.kt | 5 +++++ .../usecase/command/ManageReceiptUseCase.kt | 2 ++ .../usecase/command/ManageReceiptUseCaseTest.kt | 17 ++++++++++++++++- 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptAccountMismatchException.kt diff --git a/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt index 08bfe6bd..06ecb74d 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/exception/AccountErrorCode.kt @@ -17,6 +17,9 @@ enum class AccountErrorCode( @ExplainError("요청한 영수증 내역이 존재하지 않을 때 발생합니다.") RECEIPT_NOT_FOUND(2102, HttpStatus.NOT_FOUND, "존재하지 않는 내역입니다."), + + @ExplainError("영수증이 요청한 기수의 장부에 속하지 않을 때 발생합니다.") + RECEIPT_ACCOUNT_MISMATCH(2103, HttpStatus.BAD_REQUEST, "영수증이 해당 기수의 장부에 속하지 않습니다."), ; override fun getCode(): Int = code diff --git a/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptAccountMismatchException.kt b/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptAccountMismatchException.kt new file mode 100644 index 00000000..04a34880 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/account/application/exception/ReceiptAccountMismatchException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.account.application.exception + +import com.weeth.global.common.exception.BaseException + +class ReceiptAccountMismatchException : BaseException(AccountErrorCode.RECEIPT_ACCOUNT_MISMATCH) diff --git a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt index 95f0df22..ef0c939f 100644 --- a/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCase.kt @@ -3,6 +3,7 @@ package com.weeth.domain.account.application.usecase.command import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest import com.weeth.domain.account.application.exception.AccountNotFoundException +import com.weeth.domain.account.application.exception.ReceiptAccountMismatchException import com.weeth.domain.account.application.exception.ReceiptNotFoundException import com.weeth.domain.account.domain.entity.Receipt import com.weeth.domain.account.domain.repository.AccountRepository @@ -46,6 +47,7 @@ class ManageReceiptUseCase( cardinalGetService.findByAdminSide(request.cardinal) val account = accountRepository.findByCardinal(request.cardinal) ?: throw AccountNotFoundException() val receipt = receiptRepository.findByIdOrNull(receiptId) ?: throw ReceiptNotFoundException() + if (receipt.account.id != account.id) throw ReceiptAccountMismatchException() account.adjustSpend(Money.of(receipt.amount), Money.of(request.amount)) if (request.files != null) { fileRepository.deleteAll(fileReader.findAll(FileOwnerType.RECEIPT, receiptId, null)) diff --git a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt index cac249c9..2da320fb 100644 --- a/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt +++ b/src/test/kotlin/com/weeth/domain/account/application/usecase/command/ManageReceiptUseCaseTest.kt @@ -3,6 +3,7 @@ package com.weeth.domain.account.application.usecase.command import com.weeth.domain.account.application.dto.request.ReceiptSaveRequest import com.weeth.domain.account.application.dto.request.ReceiptUpdateRequest import com.weeth.domain.account.application.exception.AccountNotFoundException +import com.weeth.domain.account.application.exception.ReceiptAccountMismatchException import com.weeth.domain.account.domain.repository.AccountRepository import com.weeth.domain.account.domain.repository.ReceiptRepository import com.weeth.domain.account.domain.vo.Money @@ -43,7 +44,7 @@ class ManageReceiptUseCaseTest : ) beforeTest { - clearMocks(receiptRepository, accountRepository, fileReader, cardinalGetService, fileMapper) + clearMocks(receiptRepository, accountRepository, fileReader, fileRepository, cardinalGetService, fileMapper) } describe("save") { @@ -135,6 +136,20 @@ class ManageReceiptUseCaseTest : verify(exactly = 1) { fileRepository.saveAll(newFiles) } } + it("다른 기수의 장부에 속한 영수증을 수정하면 ReceiptAccountMismatchException을 던진다") { + val receiptId = 20L + val accountA = AccountTestFixture.createAccount(id = 1L, cardinal = 40) + val accountB = AccountTestFixture.createAccount(id = 2L, cardinal = 41) + val receipt = ReceiptTestFixture.createReceipt(id = receiptId, amount = 1_000, account = accountB) + val dto = ReceiptUpdateRequest("desc", "source", 2_000, LocalDate.of(2026, 1, 1), 40, null) + + every { cardinalGetService.findByAdminSide(dto.cardinal) } returns mockk() + every { accountRepository.findByCardinal(dto.cardinal) } returns accountA + every { receiptRepository.findById(receiptId) } returns Optional.of(receipt) + + shouldThrow { useCase.update(receiptId, dto) } + } + it("빈 리스트로 업데이트 시 기존 파일을 모두 삭제한다") { val receiptId = 11L val account = AccountTestFixture.createAccount(cardinal = 40)