diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftDelivery.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftDelivery.kt new file mode 100644 index 00000000..747e4a6b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftDelivery.kt @@ -0,0 +1,87 @@ +package kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence + +import kr.co.vividnext.sodalive.common.BaseEntity +import java.time.LocalDateTime +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table +import javax.persistence.UniqueConstraint + +@Entity +@Table( + name = "gift_delivery", + uniqueConstraints = [UniqueConstraint(name = "uk_gift_delivery_gift_id", columnNames = ["gift_id"])] +) +class GiftDelivery( + @Column(name = "gift_id", nullable = false) + val giftId: Long, + + @Column(name = "sender_name", nullable = false, length = 50) + val senderName: String, + + @Column(name = "sender_phone_number", nullable = false, length = 30) + val senderPhoneNumber: String, + + @Column(name = "sender_zip_code", nullable = false, length = 20) + val senderZipCode: String, + + @Column(name = "sender_address", nullable = false, length = 255) + val senderAddress: String, + + @Column(name = "sender_address_detail", length = 255) + val senderAddressDetail: String? = null, + + @Column(name = "recipient_name", length = 50) + var recipientName: String? = null, + + @Column(name = "recipient_phone_number", length = 30) + var recipientPhoneNumber: String? = null, + + @Column(name = "recipient_zip_code", length = 20) + var recipientZipCode: String? = null, + + @Column(name = "recipient_address", length = 255) + var recipientAddress: String? = null, + + @Column(name = "recipient_address_detail", length = 255) + var recipientAddressDetail: String? = null, + + @Column(name = "courier_company_name", length = 50) + var courierCompanyName: String? = null, + + @Column(name = "tracking_number", length = 100) + var trackingNumber: String? = null, + + @Column(name = "tracking_deadline_at", nullable = false) + val trackingDeadlineAt: LocalDateTime, + + @Column(name = "recipient_address_deadline_at") + var recipientAddressDeadlineAt: LocalDateTime? = null, + + @Column(name = "tracking_deadline_reminder_sent_at") + var trackingDeadlineReminderSentAt: LocalDateTime? = null, + + @Column(name = "recipient_address_deadline_reminder_sent_at") + var recipientAddressDeadlineReminderSentAt: LocalDateTime? = null, + + @Column(name = "tracking_registered_at") + var trackingRegisteredAt: LocalDateTime? = null, + + @Column(name = "arrived_at_mailbox_at") + var arrivedAtMailboxAt: LocalDateTime? = null, + + @Column(name = "inspection_completed_at") + var inspectionCompletedAt: LocalDateTime? = null, + + @Column(name = "delivered_at") + var deliveredAt: LocalDateTime? = null, + + @Column(name = "canceled_at") + var canceledAt: LocalDateTime? = null, + + @Column(name = "undeliverable_at") + var undeliverableAt: LocalDateTime? = null, + + @Column(name = "undeliverable_reason", length = 255) + var undeliverableReason: String? = null +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftDeliveryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftDeliveryRepository.kt new file mode 100644 index 00000000..63e2b543 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftDeliveryRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence + +import org.springframework.data.jpa.repository.JpaRepository + +interface GiftDeliveryRepository : JpaRepository { + fun findByGiftId(giftId: Long): GiftDelivery? +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftReview.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftReview.kt new file mode 100644 index 00000000..bde52bce --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftReview.kt @@ -0,0 +1,29 @@ +package kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table +import javax.persistence.UniqueConstraint + +@Entity +@Table( + name = "gift_review", + uniqueConstraints = [UniqueConstraint(name = "uk_gift_review_gift_id", columnNames = ["gift_id"])] +) +class GiftReview( + @Column(name = "gift_id", nullable = false) + val giftId: Long, + + @Column(name = "sender_member_id", nullable = false) + val senderMemberId: Long, + + @Column(name = "rating", nullable = false) + val rating: Int, + + @Column(name = "keywords", columnDefinition = "text") + var keywords: String? = null, + + @Column(name = "comment", length = 255) + var comment: String? = null +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftReviewRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftReviewRepository.kt new file mode 100644 index 00000000..2998a748 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftReviewRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence + +import org.springframework.data.jpa.repository.JpaRepository + +interface GiftReviewRepository : JpaRepository { + fun findByGiftId(giftId: Long): GiftReview? +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftEntityMappingTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftEntityMappingTest.kt new file mode 100644 index 00000000..9a758734 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftEntityMappingTest.kt @@ -0,0 +1,137 @@ +package kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence + +import kr.co.vividnext.sodalive.configs.QueryDslConfig +import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize +import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.context.annotation.Import +import org.springframework.dao.DataIntegrityViolationException +import java.time.LocalDateTime + +@DataJpaTest( + properties = [ + "spring.cache.type=none", + "spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;NON_KEYWORDS=VALUE" + ] +) +@Import(QueryDslConfig::class) +@DisplayName("선물 엔티티 매핑 테스트") +class GiftEntityMappingTest @Autowired constructor( + private val giftRepository: GiftRepository, + private val giftDeliveryRepository: GiftDeliveryRepository, + private val giftCategoryRepository: GiftCategoryRepository, + private val giftSizePriceRepository: GiftSizePriceRepository, + private val giftReviewRepository: GiftReviewRepository, + private val giftApplicationNoSequenceRepository: GiftApplicationNoSequenceRepository +) { + @Test + @DisplayName("선물 엔티티와 배송 정보를 저장하고 조회한다") + fun shouldSaveAndFindGiftWithDelivery() { + val category = giftCategoryRepository.save( + GiftCategory( + categoryCode = "DOLL", + name = "인형", + requiresDamageWaiver = true + ) + ) + giftSizePriceRepository.save( + GiftSizePrice( + sizeCode = GiftSize.SMALL, + basePriceCan = 100, + salePriceCan = 80 + ) + ) + val gift = giftRepository.save( + Gift( + applicationNo = "G20260929000001", + senderMemberId = 1L, + recipientMemberId = 2L, + status = GiftStatus.RECEIVED, + sizeCode = GiftSize.SMALL, + categoryId = category.id!!, + categoryNameSnapshot = category.name, + salePriceCan = 80, + canUsageId = 10L, + senderTermsAgreed = true, + senderPrivacyAgreed = true, + senderTermsAgreedAt = LocalDateTime.of(2026, 9, 29, 12, 0), + senderPrivacyAgreedAt = LocalDateTime.of(2026, 9, 29, 12, 0), + damageWaiverAgreed = true + ) + ) + + giftDeliveryRepository.save( + GiftDelivery( + giftId = gift.id!!, + senderName = "홍길동", + senderPhoneNumber = "01012345678", + senderZipCode = "06234", + senderAddress = "서울시 강남구", + senderAddressDetail = "101동 1001호", + trackingDeadlineAt = LocalDateTime.of(2026, 10, 2, 12, 0) + ) + ) + + val saved = giftRepository.findByApplicationNo("G20260929000001") + val delivery = giftDeliveryRepository.findByGiftId(gift.id!!) + + assertNotNull(saved) + assertEquals(GiftStatus.RECEIVED, saved!!.status) + assertEquals("홍길동", delivery!!.senderName) + assertNotNull(saved.createdAt) + assertNotNull(saved.updatedAt) + } + + @Test + @DisplayName("신청번호와 카테고리 코드와 리뷰 선물 ID는 중복 저장할 수 없다") + fun shouldRejectDuplicateUniqueValues() { + val category = giftCategoryRepository.save(GiftCategory(categoryCode = "DOLL", name = "인형")) + val gift = giftRepository.save(gift(category.id!!, "G20260929000001")) + giftRepository.saveAndFlush(gift(category.id!!, "G20260929000002")) + giftReviewRepository.saveAndFlush(GiftReview(giftId = gift.id!!, senderMemberId = 1L, rating = 5)) + + assertThrows(DataIntegrityViolationException::class.java) { + giftRepository.saveAndFlush(gift(category.id!!, "G20260929000001")) + } + assertThrows(DataIntegrityViolationException::class.java) { + giftCategoryRepository.saveAndFlush(GiftCategory(categoryCode = "DOLL", name = "중복")) + } + assertThrows(DataIntegrityViolationException::class.java) { + giftReviewRepository.saveAndFlush(GiftReview(giftId = gift.id!!, senderMemberId = 1L, rating = 4)) + } + } + + @Test + @DisplayName("날짜별 신청번호 채번 sequence를 저장한다") + fun shouldSaveApplicationNoSequence() { + val sequence = giftApplicationNoSequenceRepository.save( + GiftApplicationNoSequence(sequenceDate = "20260929", lastSequence = 1) + ) + + assertEquals("20260929", sequence.sequenceDate) + assertEquals(1, sequence.lastSequence) + } + + private fun gift(categoryId: Long, applicationNo: String) = Gift( + applicationNo = applicationNo, + senderMemberId = 1L, + recipientMemberId = 2L, + status = GiftStatus.RECEIVED, + sizeCode = GiftSize.SMALL, + categoryId = categoryId, + categoryNameSnapshot = "인형", + salePriceCan = 80, + canUsageId = 10L, + senderTermsAgreed = true, + senderPrivacyAgreed = true, + senderTermsAgreedAt = LocalDateTime.of(2026, 9, 29, 12, 0), + senderPrivacyAgreedAt = LocalDateTime.of(2026, 9, 29, 12, 0), + damageWaiverAgreed = true + ) +}